From 1da34285b0ca7cd5c7632245ae9cdc2ae1dd364b Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Mon, 20 Jan 2025 01:01:01 -0800 Subject: [PATCH] Unity fixes + decrunching (#744) * Fix typo * unity: Add some missing texture types * unity: add ScriptMapper and Shader ScriptMappers contain shader names, so we can actually sort materials by the shaders they use * Add Outer Wilds stub * Remove some cruft * fix use of shaderName * nicer message on metadata parse error * fix bug in concatBufs * use texture2ddecoder for decrunching * fix Outer Wilds shader error * fix wasm-bindgen version * rm log --- rust/Cargo.lock | 78 ++++---- rust/Cargo.toml | 3 +- rust/src/compression.rs | 24 +++ rust/src/unity/asset_file.rs | 5 +- rust/src/unity/types/binary.rs | 129 +++++++++++-- rust/src/unity/types/wasm.rs | 165 ++++++++++++++-- src/AShortHike/Scenes.ts | 3 +- src/Common/Unity/AssetManager.ts | 170 ++++++++++------ src/Common/Unity/GameObject.ts | 1 - src/OuterWilds/Scenes.ts | 320 +++++++++++++++++++++++++++++++ src/main.ts | 2 + 11 files changed, 778 insertions(+), 122 deletions(-) create mode 100644 src/OuterWilds/Scenes.ts diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0ad9ef8f7..62cdfc5ef 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -58,11 +58,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -104,9 +105,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -128,9 +129,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -252,9 +253,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -275,9 +276,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -328,9 +329,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashers" @@ -361,9 +362,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -406,15 +407,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.164" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lz4_flex" @@ -499,7 +500,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -522,6 +523,7 @@ dependencies = [ "num_enum", "polymorph", "rand", + "texture2ddecoder", "wasm-bindgen", "web-sys", ] @@ -613,7 +615,7 @@ version = "0.1.0" source = "git+https://github.com/wgreenberg/polymorph#2379ecce20537cb1b4524800a9c59a81c52eb53d" dependencies = [ "deku", - "env_logger 0.11.5", + "env_logger 0.11.6", "hashers", "log", "thiserror", @@ -649,18 +651,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -744,9 +746,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "safe_arch" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" dependencies = [ "bytemuck", ] @@ -783,9 +785,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -807,6 +809,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "texture2ddecoder" +version = "0.1.1" +source = "git+https://github.com/wgreenberg/texture2ddecoder#ec3e5b44bbd3caa68b5ca6a5eec4b8ae0edf23c9" +dependencies = [ + "paste", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -824,7 +834,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -902,7 +912,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "wasm-bindgen-shared", ] @@ -924,7 +934,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -947,9 +957,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.7.30" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e6db2670d2be78525979e9a5f9c69d296fd7d670549fe9ebf70f8708cb5019" +checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22" dependencies = [ "bytemuck", "safe_arch", @@ -1082,5 +1092,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0d3a9fdd7..49fbd8da0 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,9 +35,10 @@ lz4_flex = { version = "0.10.0", default-features = false, features = ["safe-dec lzma-rs = { version = "0.3.0", features = ["raw_decoder"] } naga = { git = "https://github.com/magcius/wgpu", branch = "issue-4349", features = ["glsl-in", "wgsl-out"] } num_enum = "0.5.7" -wasm-bindgen = "0.2.95" +wasm-bindgen = "=0.2.95" web-sys = { version = "0.3.48", features = ["console"] } nalgebra-glm = "0.19.0" rand = "0.8.5" getrandom = { version = "0.2.15", features = ["js"] } noclip-macros = { version = "*", path = "./noclip-macros" } +texture2ddecoder = { git = "https://github.com/wgreenberg/texture2ddecoder" } diff --git a/rust/src/compression.rs b/rust/src/compression.rs index d884b6fb6..448eb43cb 100644 --- a/rust/src/compression.rs +++ b/rust/src/compression.rs @@ -38,3 +38,27 @@ pub fn deflate_raw_decompress(src: &[u8]) -> Vec { inflate::inflate_bytes(src).unwrap() } +#[wasm_bindgen(js_name = "CrunchTexture")] +pub struct CrunchTexture { + handle: texture2ddecoder::CrunchHandle, +} + +#[wasm_bindgen(js_class = "CrunchTexture")] +impl CrunchTexture { + pub fn new(data: &[u8]) -> Result { + let handle = texture2ddecoder::CrunchHandle::new(data) + .map_err(|err| format!("{:?}", err))?; + Ok(Self { + handle, + }) + } + + pub fn get_num_levels(&self) -> u32 { + self.handle.get_num_levels() + } + + pub fn decode_level(&self, data: &[u8], level_index: u32) -> Result, String> { + self.handle.unpack_level(data, level_index) + .map_err(|err| err.into()) + } +} diff --git a/rust/src/unity/asset_file.rs b/rust/src/unity/asset_file.rs index ec09b9e33..400ea859d 100644 --- a/rust/src/unity/asset_file.rs +++ b/rust/src/unity/asset_file.rs @@ -40,7 +40,8 @@ impl AssetFile { pub fn append_metadata_chunk(&mut self, data: &[u8]) -> Result<(), String> { // data will be the file from bytes 0..data_offset, so skip to where the metadata starts let bitslice = BitSlice::from_slice(data); - let (rest, _) = SerializedFileHeader::read(&bitslice, ()).unwrap(); + let (rest, _) = SerializedFileHeader::read(&bitslice, ()) + .map_err(|err| format!("failed to parse metadata file header: {:?}", err))?; match SerializedFileMetadata::read(rest, self.header.version) { Ok((_, metadata)) => self.metadata = Some(metadata), Err(err) => return Err(format!("failed to parse metadata: {:?}", err)), @@ -77,7 +78,7 @@ impl AssetFile { ClassID::MonoBehavior }; result.push(AssetFileObject { - file_id:obj.file_id, + file_id: obj.file_id, byte_start, byte_size: obj.byte_size as usize, class_id, diff --git a/rust/src/unity/types/binary.rs b/rust/src/unity/types/binary.rs index 0471e4233..f88d80479 100644 --- a/rust/src/unity/types/binary.rs +++ b/rust/src/unity/types/binary.rs @@ -377,20 +377,120 @@ pub enum TextureWrapMode { MirrorOnce = 3, } +// copied from https://github.com/Unity-Technologies/UnityCsReference/blob/129a67089d125df5b95b659d3535deaf9968e86c/Editor/Mono/AssetPipeline/TextureImporterEnums.cs#L37 #[derive(DekuRead, Clone, Debug)] #[deku(type = "i32")] pub enum TextureFormat { - Alpha8 = 0x01, - RGB24 = 0x03, - RGBA32 = 0x04, - ARGB32 = 0x05, - BC1 = 0x0A, - BC2 = 0x0B, - BC3 = 0x0C, - BC6H = 0x18, - BC7 = 0x19, - DXT1Crunched = 0x1C, - DXT5Crunched = 0x1D, + // Alpha 8 bit texture format. + Alpha8 = 1, + // RGBA 16 bit texture format. + ARGB16 = 2, + // RGB 24 bit texture format. + RGB24 = 3, + // RGBA 32 bit texture format. + RGBA32 = 4, + // ARGB 32 bit texture format. + ARGB32 = 5, + // RGB 16 bit texture format. + RGB16 = 7, + // Red 16 bit texture format. + R16 = 9, + // DXT1 compressed texture format. + DXT1 = 10, + // DXT5 compressed texture format. + DXT5 = 12, + // RGBA 16 bit (4444) texture format. + RGBA16 = 13, + + // R 16 bit texture format. + RHalf = 15, + // RG 32 bit texture format. + RGHalf = 16, + // RGBA 64 bit texture format. + RGBAHalf = 17, + + // R 32 bit texture format. + RFloat = 18, + // RG 64 bit texture format. + RGFloat = 19, + // RGBA 128 bit texture format. + RGBAFloat = 20, + + // RGB 32 bit packed float format. + RGB9E5 = 22, + + // R BC4 compressed texture format. + BC4 = 26, + // RG BC5 compressed texture format. + BC5 = 27, + // HDR RGB BC6 compressed texture format. + BC6H = 24, + // RGBA BC7 compressed texture format. + BC7 = 25, + + // DXT1 crunched texture format. + DXT1Crunched = 28, + // DXT5 crunched texture format. + DXT5Crunched = 29, + // ETC (GLES2.0) 4 bits/pixel compressed RGB texture format. + EtcRGB4 = 34, + // EAC 4 bits/pixel compressed 16-bit R texture format + EacR = 41, + // EAC 4 bits/pixel compressed 16-bit signed R texture format + EacRSigned = 42, + // EAC 8 bits/pixel compressed 16-bit RG texture format + EacRG = 43, + // EAC 8 bits/pixel compressed 16-bit signed RG texture format + EacRGSigned = 44, + + // ETC2 (GLES3.0) 4 bits/pixel compressed RGB texture format. + Etc2RGB4 = 45, + // ETC2 (GLES3.0) 4 bits/pixel compressed RGB + 1-bit alpha texture format. + Etc2RGB4PunchthroughAlpha = 46, + // ETC2 (GLES3.0) 8 bits/pixel compressed RGBA texture format. + Etc2RGBA8 = 47, + + // ASTC uses 128bit block of varying sizes (we use only square blocks). It does not distinguish RGB/RGBA + Astc4x4 = 48, + Astc5x5 = 49, + Astc6x6 = 50, + Astc8x8 = 51, + Astc10x10 = 52, + Astc12x12 = 53, + + // RG 16 bit texture format. + RG16 = 62, + // Red 8 bit texture format. + R8 = 63, + // ETC1 crunched texture format. + EtcRGB4Crunched = 64, + // ETC2_RGBA8 crunched texture format. + Etc2RGBA8Crunched = 65, + + // ASTC (block size 4x4) compressed HDR RGB(A) texture format. + AstcHdr4x4 = 66, + // ASTC (block size 5x5) compressed HDR RGB(A) texture format. + AstcHdr5x5 = 67, + // ASTC (block size 4x6x6) compressed HDR RGB(A) texture format. + AstcHdr6x6 = 68, + // ASTC (block size 8x8) compressed HDR RGB(A) texture format. + AstcHdr8x8 = 69, + // ASTC (block size 10x10) compressed HDR RGB(A) texture format. + AstcHdr10x10 = 70, + // ASTC (block size 12x12) compressed HDR RGB(A) texture format. + AstcHdr12x12 = 71, + + RG32 = 72, + RGB48 = 73, + RGBA64 = 74, + R8Signed = 75, + RG16Signed = 76, + RGB24Signed = 77, + RGBA32Signed = 78, + R16Signed = 79, + RG32Signed = 80, + RGB48Signed = 81, + RGBA64Signed = 82, } #[derive(DekuRead, Clone, Debug)] @@ -406,3 +506,10 @@ pub struct MeshFilter { pub game_object: PPtr, pub mesh: PPtr, } + +#[derive(DekuRead, Clone, Debug)] +#[deku(ctx = "_version: UnityVersion")] +pub struct ScriptMapper { + pub shader_to_name_map: Map, CharArray>, + pub preload_shaders: bool, +} diff --git a/rust/src/unity/types/wasm.rs b/rust/src/unity/types/wasm.rs index 80186c7f0..45c1487b3 100644 --- a/rust/src/unity/types/wasm.rs +++ b/rust/src/unity/types/wasm.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; +use deku::DekuContainerRead; use noclip_macros::{FromStructPerField, FromEnumPerVariant, from}; use wasm_bindgen::prelude::*; use deku::{DekuRead, bitvec::BitSlice}; +use crate::unity::types::common::CharArray; use super::common::{ColorRGBA, Matrix4x4, PPtr, Quaternion, Vec2, Vec3, Vec4, AABB, UnityVersion}; use super::binary; @@ -23,7 +25,7 @@ macro_rules! define_create { } #[wasm_bindgen(js_name = "UnityPPtr")] -#[derive(Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)] pub struct WasmFriendlyPPtr { pub file_index: u32, pub path_id: i64, @@ -144,6 +146,27 @@ impl Material { } } +// Note: the actual Shader type is insanely complicated, so we don't want to +// actually implement a binary reader for it unless we're trying to actually +// recreate Unity's shader compilation pipeline. Instead, let's just read in the +// name to be identifiable in noclip. +#[wasm_bindgen(js_name = "UnityShader", getter_with_clone)] +#[derive(Debug, Clone)] +pub struct Shader { + pub name: String, +} + +#[wasm_bindgen(js_class = "UnityShader")] +impl Shader { + pub fn create(_version: UnityVersion, data: &[u8]) -> Result { + let (_, name) = CharArray::from_bytes((data, 0)) + .map_err(|err| format!("{:?}", err))?; + Ok(Shader { + name: name.into(), + }) + } +} + #[wasm_bindgen(js_name = "UnityTexEnv")] #[derive(FromStructPerField, Debug, Clone)] #[from(binary::TexEnv)] @@ -509,17 +532,116 @@ pub enum TextureWrapMode { #[derive(FromEnumPerVariant, Clone, Copy, Debug)] #[from(binary::TextureFormat)] pub enum TextureFormat { - Alpha8 = 0x01, - RGB24 = 0x03, - RGBA32 = 0x04, - ARGB32 = 0x05, - BC1 = 0x0A, - BC2 = 0x0B, - BC3 = 0x0C, - BC6H = 0x18, - BC7 = 0x19, - DXT1Crunched = 0x1C, - DXT5Crunched = 0x1D, + // Alpha 8 bit texture format. + Alpha8 = 1, + // RGBA 16 bit texture format. + ARGB16 = 2, + // RGB 24 bit texture format. + RGB24 = 3, + // RGBA 32 bit texture format. + RGBA32 = 4, + // ARGB 32 bit texture format. + ARGB32 = 5, + // RGB 16 bit texture format. + RGB16 = 7, + // Red 16 bit texture format. + R16 = 9, + // DXT1 compressed texture format. + DXT1 = 10, + // DXT5 compressed texture format. + DXT5 = 12, + // RGBA 16 bit (4444) texture format. + RGBA16 = 13, + + // R 16 bit texture format. + RHalf = 15, + // RG 32 bit texture format. + RGHalf = 16, + // RGBA 64 bit texture format. + RGBAHalf = 17, + + // R 32 bit texture format. + RFloat = 18, + // RG 64 bit texture format. + RGFloat = 19, + // RGBA 128 bit texture format. + RGBAFloat = 20, + + // RGB 32 bit packed float format. + RGB9E5 = 22, + + // R BC4 compressed texture format. + BC4 = 26, + // RG BC5 compressed texture format. + BC5 = 27, + // HDR RGB BC6 compressed texture format. + BC6H = 24, + // RGBA BC7 compressed texture format. + BC7 = 25, + + // DXT1 crunched texture format. + DXT1Crunched = 28, + // DXT5 crunched texture format. + DXT5Crunched = 29, + // ETC (GLES2.0) 4 bits/pixel compressed RGB texture format. + EtcRGB4 = 34, + // EAC 4 bits/pixel compressed 16-bit R texture format + EacR = 41, + // EAC 4 bits/pixel compressed 16-bit signed R texture format + EacRSigned = 42, + // EAC 8 bits/pixel compressed 16-bit RG texture format + EacRG = 43, + // EAC 8 bits/pixel compressed 16-bit signed RG texture format + EacRGSigned = 44, + + // ETC2 (GLES3.0) 4 bits/pixel compressed RGB texture format. + Etc2RGB4 = 45, + // ETC2 (GLES3.0) 4 bits/pixel compressed RGB + 1-bit alpha texture format. + Etc2RGB4PunchthroughAlpha = 46, + // ETC2 (GLES3.0) 8 bits/pixel compressed RGBA texture format. + Etc2RGBA8 = 47, + + // ASTC uses 128bit block of varying sizes (we use only square blocks). It does not distinguish RGB/RGBA + Astc4x4 = 48, + Astc5x5 = 49, + Astc6x6 = 50, + Astc8x8 = 51, + Astc10x10 = 52, + Astc12x12 = 53, + + // RG 16 bit texture format. + RG16 = 62, + // Red 8 bit texture format. + R8 = 63, + // ETC1 crunched texture format. + EtcRGB4Crunched = 64, + // ETC2_RGBA8 crunched texture format. + Etc2RGBA8Crunched = 65, + + // ASTC (block size 4x4) compressed HDR RGB(A) texture format. + AstcHdr4x4 = 66, + // ASTC (block size 5x5) compressed HDR RGB(A) texture format. + AstcHdr5x5 = 67, + // ASTC (block size 4x6x6) compressed HDR RGB(A) texture format. + AstcHdr6x6 = 68, + // ASTC (block size 8x8) compressed HDR RGB(A) texture format. + AstcHdr8x8 = 69, + // ASTC (block size 10x10) compressed HDR RGB(A) texture format. + AstcHdr10x10 = 70, + // ASTC (block size 12x12) compressed HDR RGB(A) texture format. + AstcHdr12x12 = 71, + + RG32 = 72, + RGB48 = 73, + RGBA64 = 74, + R8Signed = 75, + RG16Signed = 76, + RGB24Signed = 77, + RGBA32Signed = 78, + R16Signed = 79, + RG32Signed = 80, + RGB48Signed = 81, + RGBA64Signed = 82, } #[wasm_bindgen(js_name = "UnityTextureColorSpace")] @@ -576,6 +698,24 @@ pub struct StaticBatchInfo { pub submesh_count: u16, } +#[wasm_bindgen(js_name = "UnityScriptMapper")] +#[derive(Clone, Debug, FromStructPerField)] +#[from(binary::ScriptMapper)] +pub struct ScriptMapper { + shader_to_name_map: HashMap, +} + +#[wasm_bindgen(js_class = "UnityScriptMapper")] +impl ScriptMapper { + pub fn get_shader_pointers(&self) -> Vec { + self.shader_to_name_map.keys().cloned().collect() + } + + pub fn get_shader_names(&self) -> Vec { + self.shader_to_name_map.values().cloned().collect() + } +} + define_create!(GameObject, "UnityGameObject"); define_create!(Transform, "UnityTransform"); define_create!(Material, "UnityMaterial"); @@ -584,3 +724,4 @@ define_create!(VertexData, "UnityVertexData"); define_create!(Texture2D, "UnityTexture2D"); define_create!(MeshFilter, "UnityMeshFilter"); define_create!(MeshRenderer, "UnityMeshRenderer"); +define_create!(ScriptMapper, "UnityScriptMapper"); diff --git a/src/AShortHike/Scenes.ts b/src/AShortHike/Scenes.ts index c5baa0760..feba55db2 100644 --- a/src/AShortHike/Scenes.ts +++ b/src/AShortHike/Scenes.ts @@ -208,8 +208,7 @@ class TerrainMaterial extends UnityMaterialInstance { class AShortHikeMaterialFactory extends UnityMaterialFactory { public createMaterialInstance(runtime: UnityRuntime, materialData: UnityMaterialData): UnityMaterialInstance { - // TODO(jstpierre): Pull out serialized shader data - if (materialData.texturesByName.has('_Splat3')) + if (materialData.shader?.name?.startsWith('Custom Unlit/Unlit Terrain')) return new TerrainMaterial(runtime, materialData); else return new TempMaterial(runtime, materialData); diff --git a/src/Common/Unity/AssetManager.ts b/src/Common/Unity/AssetManager.ts index d2da7f542..28bfb02e2 100644 --- a/src/Common/Unity/AssetManager.ts +++ b/src/Common/Unity/AssetManager.ts @@ -1,6 +1,6 @@ import { vec2, vec3 } from 'gl-matrix'; -import { UnityAABB, UnityAssetFile, UnityAssetFileObject, UnityChannelInfo, UnityClassID, UnityGLTextureSettings, UnityMaterial, UnityMesh, UnityMeshCompression, UnityPPtr, UnityStreamingInfo, UnitySubMesh, UnityTexture2D, UnityTextureColorSpace, UnityTextureFormat, UnityVersion, UnityVertexFormat } from '../../../rust/pkg/noclip_support'; +import { UnityAABB, UnityAssetFile, UnityAssetFileObject, UnityChannelInfo, UnityClassID, UnityGLTextureSettings, UnityMaterial, UnityMesh, UnityMeshCompression, UnityPPtr, UnityShader, UnityStreamingInfo, UnitySubMesh, UnityTexture2D, UnityTextureColorSpace, UnityTextureFormat, UnityVersion, UnityVertexFormat, CrunchTexture } from '../../../rust/pkg/noclip_support'; import ArrayBufferSlice from '../../ArrayBufferSlice.js'; import { Color, TransparentBlack, colorNewFromRGBA } from '../../Color.js'; import { DataFetcher } from '../../DataFetcher.js'; @@ -16,9 +16,10 @@ import { rust } from '../../rustlib.js'; import { assert, assertExists, fallbackUndefined } from '../../util.js'; function concatBufs(a: Uint8Array, b: Uint8Array): Uint8Array { + const origByteLength = a.byteLength; const newBuffer = a.buffer.transfer(a.byteLength + b.byteLength); const result = new Uint8Array(newBuffer); - result.set(b, a.byteLength); + result.set(b, origByteLength); return result; } @@ -120,6 +121,7 @@ type ResType = T extends UnityAssetResourceType.Mesh ? UnityMeshData : T extends UnityAssetResourceType.Texture2D ? UnityTexture2DData : T extends UnityAssetResourceType.Material ? UnityMaterialData : + T extends UnityAssetResourceType.Shader ? UnityShaderData : never; type CreateFunc = (assetSystem: UnityAssetSystem, objData: AssetObjectData) => Promise; @@ -136,7 +138,7 @@ export class AssetFile { private promiseCache = new Map>(); public dataOffset: bigint = BigInt(0); - constructor(private path: string, public version: UnityVersion) { + constructor(public path: string, public version: UnityVersion) { } private ensureAssetFile(buffer: Uint8Array): void { @@ -148,7 +150,6 @@ export class AssetFile { private doneLoadingHeader(buffer: Uint8Array): void { this.ensureAssetFile(buffer); this.assetFile.append_metadata_chunk(buffer); - console.log(this.path, this.assetFile.get_version_string()); this.unityObjects = this.assetFile.get_objects(); for (let i = 0; i < this.unityObjects.length; i++) this.unityObjectByFileID.set(this.unityObjects[i].file_id, this.unityObjects[i]); @@ -214,7 +215,6 @@ export class AssetFile { if (this.waitForHeaderPromise !== null) await this.waitForHeaderPromise; - try { const obj = assertExists(this.unityObjectByFileID.get(pathID)); @@ -248,23 +248,18 @@ export class AssetFile { } private createMeshData = async (assetSystem: UnityAssetSystem, objData: AssetObjectData): Promise => { - try { - const mesh = rust.UnityMesh.create(assetSystem.version, objData.data); + const mesh = rust.UnityMesh.create(assetSystem.version, objData.data); - const streamingInfo: UnityStreamingInfo = mesh.streaming_info; - if (streamingInfo.path.length !== 0) { - const buf = await assetSystem.fetchStreamingInfo(streamingInfo); - mesh.set_vertex_data(buf.createTypedArray(Uint8Array)); - } + const streamingInfo: UnityStreamingInfo = mesh.streaming_info; + if (streamingInfo.path.length !== 0) { + const buf = await assetSystem.fetchStreamingInfo(streamingInfo); + mesh.set_vertex_data(buf.createTypedArray(Uint8Array)); + } - if (mesh.mesh_compression !== UnityMeshCompression.Off) { - return loadCompressedMesh(assetSystem.device, mesh); - } else { - return loadMesh(assetSystem.device, mesh); - } - } catch (e) { - console.error(objData); - throw e; + if (mesh.mesh_compression !== UnityMeshCompression.Off) { + return loadCompressedMesh(assetSystem.device, mesh); + } else { + return loadMesh(assetSystem.device, mesh); } }; @@ -283,6 +278,12 @@ export class AssetFile { return new UnityTexture2DData(assetSystem.renderCache, header, data); }; + private createShaderData = async (assetSystem: UnityAssetSystem, objData: AssetObjectData): Promise => { + const header = rust.UnityShader.create(assetSystem.version, objData.data); + const shaderData = new UnityShaderData(objData.location, header); + return shaderData; + }; + private createMaterialData = async (assetSystem: UnityAssetSystem, objData: AssetObjectData): Promise => { const header = rust.UnityMaterial.create(assetSystem.version, objData.data); const materialData = new UnityMaterialData(objData.location, header); @@ -294,14 +295,10 @@ export class AssetFile { if (this.promiseCache.has(pathID)) return this.promiseCache.get(pathID)! as Promise; - const promise = this.fetchObject(pathID).then((objData) => { - return createFunc(assetSystem, objData).then((v) => { - this.dataCache.set(pathID, v); - return v; - }).catch(e => { - console.error(`failed to fetch ${this.path}: ${pathID}, ${e}`); - throw e; - }); + const promise = this.fetchObject(pathID).then(async objData => { + const v = await createFunc(assetSystem, objData); + this.dataCache.set(pathID, v); + return v; }); this.promiseCache.set(pathID, promise); return promise; @@ -317,6 +314,8 @@ export class AssetFile { return this.fetchFromCache(assetSystem, pathID, this.createTexture2DData) as Promise>; else if (type === UnityAssetResourceType.Material) return this.fetchFromCache(assetSystem, pathID, this.createMaterialData) as Promise>; + else if (type === UnityAssetResourceType.Shader) + return this.fetchFromCache(assetSystem, pathID, this.createShaderData) as Promise>; else throw "whoops"; } @@ -332,14 +331,45 @@ export class AssetFile { } } +function pptrToKey(file: AssetFile, p: UnityPPtr): string { + return JSON.stringify([file.path, Number(p.path_id)]); +} + export class UnityAssetSystem { private assetFiles = new Map(); + private shaderPPtrToName = new Map(); public renderCache: GfxRenderCache; constructor(public device: GfxDevice, private dataFetcher: DataFetcher, private basePath: string, public version: UnityVersion) { this.renderCache = new GfxRenderCache(this.device); } + public async init() { + const globalGameManager = this.fetchAssetFile("globalgamemanagers", true); + await globalGameManager.waitForHeader(); + const scriptMapperFileObj = globalGameManager.unityObjects.find(obj => obj.class_id === UnityClassID.ScriptMapper); + if (scriptMapperFileObj === undefined) { + console.warn('no ScriptMapper found'); + return; + } + const scriptMapperPromise = globalGameManager.fetchObject(scriptMapperFileObj.file_id); + await this.fetchData(); + const scriptMapperData = await scriptMapperPromise; + const scriptMapper = rust.UnityScriptMapper.create(this.version, scriptMapperData.data); + const pptrs = scriptMapper.get_shader_pointers(); + const shaderNames = scriptMapper.get_shader_names(); + for (let i = 0; i < pptrs.length; i++) { + const assetFile = globalGameManager.getPPtrFile(this, pptrs[i]); + this.shaderPPtrToName.set(pptrToKey(assetFile, pptrs[i]), shaderNames[i]); + } + } + + public getShaderNameFromPPtr(location: AssetLocation, pptr: UnityPPtr): string | undefined { + const assetFile = location.file.getPPtrFile(this, pptr); + const key = pptrToKey(assetFile, pptr); + return this.shaderPPtrToName.get(key); + } + public async fetchBytes(filename: string, range: Range): Promise { return await this.dataFetcher.fetchData(`${this.basePath}/${filename}`, range); } @@ -540,40 +570,40 @@ function loadMesh(device: GfxDevice, mesh: UnityMesh): UnityMeshData { } function translateTextureFormat(fmt: UnityTextureFormat, colorSpace: UnityTextureColorSpace): GfxFormat { - if (fmt === rust.UnityTextureFormat.BC1 && colorSpace === rust.UnityTextureColorSpace.Linear) - return GfxFormat.BC1; - else if (fmt === rust.UnityTextureFormat.BC1 && colorSpace === rust.UnityTextureColorSpace.SRGB) - return GfxFormat.BC1_SRGB; - else if (fmt === rust.UnityTextureFormat.BC3 && colorSpace === rust.UnityTextureColorSpace.Linear) - return GfxFormat.BC3; - else if (fmt === rust.UnityTextureFormat.BC3 && colorSpace === rust.UnityTextureColorSpace.SRGB) - return GfxFormat.BC3_SRGB; + if (fmt === rust.UnityTextureFormat.Alpha8 && colorSpace === rust.UnityTextureColorSpace.Linear) + return GfxFormat.U8_R_NORM; + else if (fmt === rust.UnityTextureFormat.R8 && colorSpace === rust.UnityTextureColorSpace.Linear) + return GfxFormat.U8_R_NORM; + else if (fmt === rust.UnityTextureFormat.RHalf && colorSpace === rust.UnityTextureColorSpace.Linear) + return GfxFormat.U16_R_NORM; else if (fmt === rust.UnityTextureFormat.RGB24 && colorSpace === rust.UnityTextureColorSpace.Linear) return GfxFormat.U8_RGBA_NORM; else if (fmt === rust.UnityTextureFormat.RGB24 && colorSpace === rust.UnityTextureColorSpace.SRGB) return GfxFormat.U8_RGBA_SRGB; else if (fmt === rust.UnityTextureFormat.RGBA32 && colorSpace === rust.UnityTextureColorSpace.Linear) return GfxFormat.U8_RGBA_NORM; + else if (fmt === rust.UnityTextureFormat.RGBAHalf && colorSpace === rust.UnityTextureColorSpace.Linear) + return GfxFormat.U16_RGBA_NORM; else if (fmt === rust.UnityTextureFormat.RGBA32 && colorSpace === rust.UnityTextureColorSpace.SRGB) return GfxFormat.U8_RGBA_SRGB; else if (fmt === rust.UnityTextureFormat.ARGB32 && colorSpace === rust.UnityTextureColorSpace.Linear) return GfxFormat.U8_RGBA_NORM; else if (fmt === rust.UnityTextureFormat.ARGB32 && colorSpace === rust.UnityTextureColorSpace.SRGB) return GfxFormat.U8_RGBA_SRGB; - else if (fmt === rust.UnityTextureFormat.DXT1Crunched && colorSpace === rust.UnityTextureColorSpace.Linear) + else if ((fmt === rust.UnityTextureFormat.DXT1 || fmt === rust.UnityTextureFormat.DXT1Crunched) && colorSpace === rust.UnityTextureColorSpace.Linear) return GfxFormat.BC1; - else if (fmt === rust.UnityTextureFormat.DXT1Crunched && colorSpace === rust.UnityTextureColorSpace.SRGB) + else if ((fmt === rust.UnityTextureFormat.DXT1 || fmt === rust.UnityTextureFormat.DXT1Crunched) && colorSpace === rust.UnityTextureColorSpace.SRGB) return GfxFormat.BC1_SRGB; - else if (fmt === rust.UnityTextureFormat.DXT5Crunched && colorSpace === rust.UnityTextureColorSpace.Linear) + else if ((fmt === rust.UnityTextureFormat.DXT5 || fmt === rust.UnityTextureFormat.DXT5Crunched) && colorSpace === rust.UnityTextureColorSpace.Linear) return GfxFormat.BC3; - else if (fmt === rust.UnityTextureFormat.DXT5Crunched && colorSpace === rust.UnityTextureColorSpace.SRGB) + else if ((fmt === rust.UnityTextureFormat.DXT5 || fmt === rust.UnityTextureFormat.DXT5Crunched) && colorSpace === rust.UnityTextureColorSpace.SRGB) return GfxFormat.BC3_SRGB; else if (fmt === rust.UnityTextureFormat.BC7 && colorSpace === rust.UnityTextureColorSpace.Linear) return GfxFormat.BC7; else if (fmt === rust.UnityTextureFormat.BC7 && colorSpace === rust.UnityTextureColorSpace.SRGB) return GfxFormat.BC7_SRGB; else - throw "whoops"; + throw new Error(`unknown texture format ${fmt} and colorspace ${colorSpace} combo`); } function translateWrapMode(v: number): GfxWrapMode { @@ -607,16 +637,14 @@ function translateSampler(header: UnityGLTextureSettings): GfxSamplerDescriptor } function calcLevelSize(fmt: UnityTextureFormat, w: number, h: number): number { - if (fmt === rust.UnityTextureFormat.BC1 || fmt === rust.UnityTextureFormat.BC2 || fmt === rust.UnityTextureFormat.BC3 || fmt === rust.UnityTextureFormat.BC6H || fmt === rust.UnityTextureFormat.BC7|| fmt === rust.UnityTextureFormat.DXT1Crunched || fmt === rust.UnityTextureFormat.DXT5Crunched) { + if (fmt === rust.UnityTextureFormat.BC6H || fmt === rust.UnityTextureFormat.BC7|| fmt === rust.UnityTextureFormat.DXT1 || fmt === rust.UnityTextureFormat.DXT5 || fmt === rust.UnityTextureFormat.DXT1Crunched || fmt === rust.UnityTextureFormat.DXT5Crunched) { w = Math.max(w, 4); h = Math.max(h, 4); const depth = 1; const count = ((w * h) / 16) * depth; - if (fmt === rust.UnityTextureFormat.BC1 || fmt === rust.UnityTextureFormat.DXT1Crunched) + if (fmt === rust.UnityTextureFormat.DXT1 || fmt === rust.UnityTextureFormat.DXT1Crunched) return count * 8; - else if (fmt === rust.UnityTextureFormat.BC2) - return count * 16; - else if (fmt === rust.UnityTextureFormat.BC3 || fmt === rust.UnityTextureFormat.DXT5Crunched) + else if (fmt === rust.UnityTextureFormat.DXT5 || fmt === rust.UnityTextureFormat.DXT5Crunched) return count * 16; else if (fmt === rust.UnityTextureFormat.BC6H) return count * 16; @@ -630,6 +658,8 @@ function calcLevelSize(fmt: UnityTextureFormat, w: number, h: number): number { return w * h * 4; } else if (fmt === rust.UnityTextureFormat.RGBA32) { return w * h * 4; + } else if (fmt === rust.UnityTextureFormat.RGBAHalf) { + return w * h * 4; } else if (fmt === rust.UnityTextureFormat.ARGB32) { return w * h * 4; } else { @@ -682,19 +712,21 @@ export class UnityTexture2DData { this.gfxSampler = cache.createSampler(translateSampler(header.texture_settings)); - // TODO(jstpierre): Support crunched formats - if (header.texture_format === rust.UnityTextureFormat.DXT1Crunched) { - console.warn(`DXT1Crunched ${this.header.name}`); - return; - } - if (header.texture_format === rust.UnityTextureFormat.DXT5Crunched) { - console.warn(`DXT5Crunched ${this.header.name}`); - return; + if (header.texture_format === rust.UnityTextureFormat.DXT1Crunched || header.texture_format === rust.UnityTextureFormat.DXT5Crunched) { + const crunched = CrunchTexture.new(data); + const levels = []; + // FIXME: texture2ddecoder seems to be broken for higher mip levels + // let numLevels = crunched.get_num_levels(); + let numLevels = 1; + for (let i = 0; i < numLevels; i++) { + levels.push(crunched.decode_level(data, i)); + } + device.uploadTextureData(this.gfxTexture, 0, levels); + } else { + const oData = imageFormatConvertData(data, header.texture_format); + const levels = calcLevels(oData, header.texture_format, header.width, header.height, header.mip_count); + device.uploadTextureData(this.gfxTexture, 0, levels); } - - const oData = imageFormatConvertData(data, header.texture_format); - const levels = calcLevels(oData, header.texture_format, header.width, header.height, header.mip_count); - device.uploadTextureData(this.gfxTexture, 0, levels); } public fillTextureMapping(dst: TextureMapping): void { @@ -713,11 +745,22 @@ export class UnityTexture { } } +export class UnityShaderData { + public name: string; + + constructor(private location: AssetLocation, private header: UnityShader) { + this.name = header.name; + } + + public destroy(device: GfxDevice): void {} +} + export class UnityMaterialData { public name: string; public texturesByName: Map = new Map(); public colorsByName: Map = new Map(); public floatsByName: Map = new Map(); + public shader: UnityShaderData | null = null; constructor(private location: AssetLocation, private header: UnityMaterial) { this.name = this.header.name; @@ -776,6 +819,13 @@ export class UnityMaterialData { for (const name of this.header.get_float_keys()) { this.floatsByName.set(name, this.header.get_float_by_key(name)!); } + + const shaderPPtr = this.header.shader; + this.shader = await assetSystem.fetchResource(UnityAssetResourceType.Shader, this.location, shaderPPtr); + assert(this.shader !== null); + const shaderName = assetSystem.getShaderNameFromPPtr(this.location, shaderPPtr); + assert(shaderName !== undefined); + this.shader.name = shaderName; } public destroy(device: GfxDevice): void { @@ -785,7 +835,9 @@ export class UnityMaterialData { export async function createUnityAssetSystem(context: SceneContext, basePath: string, version: UnityVersion): Promise { const runtime = await context.dataShare.ensureObject(`UnityAssetSystem/${basePath}`, async () => { - return new UnityAssetSystem(context.device, context.dataFetcher, basePath, version); + const system = new UnityAssetSystem(context.device, context.dataFetcher, basePath, version); + await system.init(); + return system; }); return runtime; } diff --git a/src/Common/Unity/GameObject.ts b/src/Common/Unity/GameObject.ts index 1dcbe4dff..968481391 100644 --- a/src/Common/Unity/GameObject.ts +++ b/src/Common/Unity/GameObject.ts @@ -169,7 +169,6 @@ export class MeshRenderer extends UnityComponent { const materialPPtr = materials[i]; // Don't wait on materials, we can render them as they load in... this.fetchMaterial(level, i, materialPPtr); - materialPPtr.free(); } } diff --git a/src/OuterWilds/Scenes.ts b/src/OuterWilds/Scenes.ts new file mode 100644 index 000000000..600887c59 --- /dev/null +++ b/src/OuterWilds/Scenes.ts @@ -0,0 +1,320 @@ + +import * as Viewer from '../viewer.js'; +import { SceneContext } from '../SceneBase.js'; +import { fillMatrix4x4, fillVec4 } from '../gfx/helpers/UniformBufferHelpers.js'; +import { GfxDevice, GfxProgram } from '../gfx/platform/GfxPlatform.js'; +import { makeBackbufferDescSimple, standardFullClearRenderPassDescriptor } from '../gfx/helpers/RenderGraphHelpers.js'; +import { GfxrAttachmentSlot } from '../gfx/render/GfxRenderGraph.js'; +import { GfxRenderHelper } from '../gfx/render/GfxRenderHelper.js'; +import { UnityRuntime, MeshRenderer as UnityMeshRenderer, UnityMaterialFactory, UnityMaterialInstance, createUnityRuntime, UnityShaderProgramBase } from '../Common/Unity/GameObject.js'; +import { UnityMaterialData } from '../Common/Unity/AssetManager.js'; +import { GfxRenderInst, GfxRenderInstList } from '../gfx/render/GfxRenderInstManager.js'; +import { fallback, nArray } from '../util.js'; +import { TextureMapping } from '../TextureHolder.js'; +import { UnityVersion } from '../../rust/pkg/noclip_support.js'; + +class TempMaterialProgram extends UnityShaderProgramBase { + public static ub_MaterialParams = 2; + + public override both = ` +${UnityShaderProgramBase.Common} + +layout(std140) uniform ub_MaterialParams { + vec4 u_Color; + vec4 u_MainTexST; + vec4 u_Misc[1]; +}; + +#define u_AlphaCutoff (u_Misc[0].x) + +varying vec2 v_LightIntensity; +varying vec2 v_TexCoord0; + +#if defined VERT +void mainVS() { + mat4x3 t_WorldFromLocalMatrix = CalcWorldFromLocalMatrix(); + vec3 t_PositionWorld = t_WorldFromLocalMatrix * vec4(a_Position, 1.0); + vec3 t_LightDirection = normalize(vec3(.2, -1, .5)); + vec3 normal = MulNormalMatrix(t_WorldFromLocalMatrix, normalize(a_Normal)); + float t_LightIntensityF = dot(-normal, t_LightDirection); + float t_LightIntensityB = dot( normal, t_LightDirection); + + gl_Position = u_ProjectionView * vec4(t_PositionWorld, 1.0); + v_LightIntensity = vec2(t_LightIntensityF, t_LightIntensityB); + v_TexCoord0 = CalcScaleBias(a_TexCoord0, u_MainTexST); +} +#endif + +#if defined FRAG +uniform sampler2D u_Texture; + +void mainPS() { + vec4 t_Color = u_Color; + +#if defined USE_TEXTURE + t_Color *= texture(u_Texture, v_TexCoord0); + + if (t_Color.a < u_AlphaCutoff) + discard; +#endif + + float t_LightIntensity = gl_FrontFacing ? v_LightIntensity.x : v_LightIntensity.y; + float t_LightTint = 0.2 * t_LightIntensity; + vec4 t_FinalColor = t_Color + vec4(t_LightTint, t_LightTint, t_LightTint, 0.0); + t_FinalColor.rgb = pow(t_FinalColor.rgb, vec3(1.0 / 2.2)); + gl_FragColor = t_FinalColor; +} +#endif +`; +} + +class TempMaterial extends UnityMaterialInstance { + public textureMapping = nArray(1, () => new TextureMapping()); + public program = new TempMaterialProgram(); + public gfxProgram: GfxProgram; + public alphaCutoff: number = 0.0; + + constructor(runtime: UnityRuntime, private materialData: UnityMaterialData) { + super(); + + const hasMainTex = this.materialData.fillTextureMapping(this.textureMapping[0], '_MainTex'); + this.program.setDefineBool('USE_TEXTURE', hasMainTex); + + this.alphaCutoff = fallback(this.materialData.getFloat('_Cutoff'), 0.0); + + if (this.materialData.name.includes('Terrain')) + this.alphaCutoff = 0.0; + + this.gfxProgram = runtime.assetSystem.renderCache.createProgram(this.program); + } + + public prepareToRender(renderInst: GfxRenderInst): void { + renderInst.setSamplerBindingsFromTextureMappings(this.textureMapping); + + let offs = renderInst.allocateUniformBuffer(TempMaterialProgram.ub_MaterialParams, 12); + const d = renderInst.mapUniformBufferF32(TempMaterialProgram.ub_MaterialParams); + + offs += this.materialData.fillColor(d, offs, '_Color'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_MainTex'); + offs += fillVec4(d, offs, this.alphaCutoff); + + renderInst.setGfxProgram(this.gfxProgram); + } +} + +class TerrainMaterialProgram extends UnityShaderProgramBase { + public static ub_MaterialParams = 2; + + public override both = ` +${UnityShaderProgramBase.Common} + +layout(std140) uniform ub_MaterialParams { + vec4 u_Color; + vec4 u_TexST[6]; + vec4 u_Misc[1]; +}; + +varying vec2 v_LightIntensity; +varying vec2 v_TexCoord[6]; + +uniform sampler2D u_SideTex; +uniform sampler2D u_Control1; +uniform sampler2D u_Splat0; +uniform sampler2D u_Splat1; +uniform sampler2D u_Splat2; +uniform sampler2D u_Splat3; + +#ifdef VERT +void mainVS() { + Mat4x3 t_WorldFromLocalMatrix = CalcWorldFromLocalMatrix(); + vec3 t_PositionWorld = t_WorldFromLocalMatrix * vec4(a_Position, 1.0); + vec3 t_LightDirection = normalize(vec3(.2, -1, .5)); + vec3 normal = MulNormalMatrix(t_WorldFromLocalMatrix, normalize(a_Normal)); + float t_LightIntensityF = dot(-normal, t_LightDirection); + float t_LightIntensityB = dot( normal, t_LightDirection); + + gl_Position = u_ProjectionView * vec4(t_PositionWorld, 1.0); + v_LightIntensity = vec2(t_LightIntensityF, t_LightIntensityB); + + for (int i = 0; i < 6; i++) + v_TexCoord[i] = CalcScaleBias(a_TexCoord0, u_TexST[i]); +} +#endif + +#ifdef FRAG +void mainPS() { + vec4 t_BaseColor = texture(u_SideTex, v_TexCoord[0]); + vec4 t_Control = texture(u_Control1, v_TexCoord[1]); + + vec4 t_Color = vec4(0.0); + t_Color += texture(u_Splat0, v_TexCoord[2]) * t_Control.r; + t_Color += texture(u_Splat1, v_TexCoord[3]) * t_Control.g; + t_Color += texture(u_Splat2, v_TexCoord[4]) * t_Control.b; + t_Color += texture(u_Splat3, v_TexCoord[5]) * t_Control.a; + + float t_AllWeight = dot(t_Control, vec4(1.0)); + t_Color += (1.0 - t_AllWeight) * t_BaseColor; + + float t_LightIntensity = gl_FrontFacing ? v_LightIntensity.x : v_LightIntensity.y; + float t_LightTint = 0.2 * t_LightIntensity; + vec4 t_FinalColor = t_Color + vec4(t_LightTint, t_LightTint, t_LightTint, 0.0); + + t_FinalColor.rgb = pow(t_FinalColor.rgb, vec3(1.0 / 2.2)); + gl_FragColor = t_FinalColor; +} +#endif +`; +} + +class TerrainMaterial extends UnityMaterialInstance { + public textureMapping = nArray(6, () => new TextureMapping()); + public program = new TerrainMaterialProgram(); + public gfxProgram: GfxProgram; + public alphaCutoff: number = 0.0; + + constructor(runtime: UnityRuntime, private materialData: UnityMaterialData) { + super(); + + console.log(this.materialData.name, this.materialData.texturesByName.keys()); + this.materialData.fillTextureMapping(this.textureMapping[0], '_SideTex'); + this.materialData.fillTextureMapping(this.textureMapping[1], '_Control1'); + this.materialData.fillTextureMapping(this.textureMapping[2], '_Splat0'); + this.materialData.fillTextureMapping(this.textureMapping[3], '_Splat1'); + this.materialData.fillTextureMapping(this.textureMapping[4], '_Splat2'); + this.materialData.fillTextureMapping(this.textureMapping[5], '_Splat3'); + + this.alphaCutoff = fallback(this.materialData.getFloat('_Cutoff'), 0.0); + + this.gfxProgram = runtime.assetSystem.renderCache.createProgram(this.program); + } + + public prepareToRender(renderInst: GfxRenderInst): void { + renderInst.setSamplerBindingsFromTextureMappings(this.textureMapping); + + let offs = renderInst.allocateUniformBuffer(TempMaterialProgram.ub_MaterialParams, 64); + const d = renderInst.mapUniformBufferF32(TempMaterialProgram.ub_MaterialParams); + + offs += this.materialData.fillColor(d, offs, '_Color'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_SideTex'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_Control1'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_Splat0'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_Splat1'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_Splat2'); + offs += this.materialData.fillTexEnvScaleBias(d, offs, '_Splat3'); + offs += fillVec4(d, offs, 0.0); + + renderInst.setGfxProgram(this.gfxProgram); + } +} + +class OuterWildsMaterialFactory extends UnityMaterialFactory { + public registry: Map = new Map(); + + public createMaterialInstance(runtime: UnityRuntime, materialData: UnityMaterialData): UnityMaterialInstance { + // TODO(jstpierre): Pull out serialized shader data + // console.log(materialData.name, materialData.texturesByName.keys(), materialData.shader); + // const matType = materialData.name.split("_")[0]; + // const matNames = Array.from(materialData.texturesByName.keys()); + // const existing = this.registry.get(matType); + // if (existing) { + // if (JSON.stringify(matNames) !== JSON.stringify(existing)) { + // console.warn(`mat ${materialData.name} differs from ${existing}: ${matNames}`) + // } + // } else { + // console.log(`setting ${matType} => ${matNames}`); + // this.registry.set(matType, Array.from(matNames)); + // } + if (materialData.texturesByName.has('_Splat3')) + return new TerrainMaterial(runtime, materialData); + else + return new TempMaterial(runtime, materialData); + } +} + +const bindingLayouts = [ + { numUniformBuffers: 3, numSamplers: 6, }, +]; + +class UnityRenderer implements Viewer.SceneGfx { + private renderHelper: GfxRenderHelper; + private renderInstListMain = new GfxRenderInstList(); + + constructor(private runtime: UnityRuntime) { + this.renderHelper = new GfxRenderHelper(this.runtime.context.device, this.runtime.context); + } + + private prepareToRender(device: GfxDevice, viewerInput: Viewer.ViewerRenderInput): void { + this.runtime.update(); + + const template = this.renderHelper.pushTemplateRenderInst(); + template.setBindingLayouts(bindingLayouts); + + let offs = template.allocateUniformBuffer(0, 16); + const mapped = template.mapUniformBufferF32(0); + offs += fillMatrix4x4(mapped, offs, viewerInput.camera.clipFromWorldMatrix); + + this.renderHelper.renderInstManager.setCurrentList(this.renderInstListMain); + + const meshRenderers = this.runtime.getComponents(UnityMeshRenderer); + for (let i = 0; i < meshRenderers.length; i++) + meshRenderers[i].prepareToRender(this.renderHelper.renderInstManager, viewerInput); + + this.renderHelper.renderInstManager.popTemplate(); + this.renderHelper.prepareToRender(); + } + + public render(device: GfxDevice, viewerInput: Viewer.ViewerRenderInput) { + viewerInput.camera.setClipPlanes(1); + + const mainColorDesc = makeBackbufferDescSimple(GfxrAttachmentSlot.Color0, viewerInput, standardFullClearRenderPassDescriptor); + const mainDepthDesc = makeBackbufferDescSimple(GfxrAttachmentSlot.DepthStencil, viewerInput, standardFullClearRenderPassDescriptor); + + const builder = this.renderHelper.renderGraph.newGraphBuilder(); + + const mainColorTargetID = builder.createRenderTargetID(mainColorDesc, 'Main Color'); + const mainDepthTargetID = builder.createRenderTargetID(mainDepthDesc, 'Main Depth'); + builder.pushPass((pass) => { + pass.setDebugName('Main'); + pass.attachRenderTargetID(GfxrAttachmentSlot.Color0, mainColorTargetID); + pass.attachRenderTargetID(GfxrAttachmentSlot.DepthStencil, mainDepthTargetID); + pass.exec((passRenderer) => { + this.renderInstListMain.drawOnPassRenderer(this.renderHelper.renderCache, passRenderer); + }); + }); + builder.resolveRenderTargetToExternalTexture(mainColorTargetID, viewerInput.onscreenTexture); + + this.prepareToRender(device, viewerInput); + this.renderHelper.renderGraph.execute(builder); + this.renderInstListMain.reset(); + } + + public destroy(device: GfxDevice) { + this.runtime.destroy(device); + this.renderHelper.destroy(); + } +} + +class OuterWildsSceneDesc implements Viewer.SceneDesc { + constructor(public id: string, public name: string) { + } + + public async createScene(device: GfxDevice, context: SceneContext): Promise { + const runtime = await createUnityRuntime(context, `OuterWilds`, UnityVersion.V2019_4_39f1); + runtime.materialFactory = new OuterWildsMaterialFactory(); + await runtime.loadLevel(this.id); + + const renderer = new UnityRenderer(runtime); + return renderer; + } +} + +const id = 'OuterWilds'; +const name = 'Outer Wilds'; + +const sceneDescs = [ + new OuterWildsSceneDesc(`level0`, "Main Menu"), + new OuterWildsSceneDesc(`level1`, "Solar System"), + new OuterWildsSceneDesc(`level2`, "End"), +]; + +export const sceneGroup: Viewer.SceneGroup = { id, name, sceneDescs, hidden: true }; diff --git a/src/main.ts b/src/main.ts index 2b5a8d650..e90de511c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -93,6 +93,7 @@ import * as Scenes_Morrowind from './Morrowind/Scenes.js'; import * as Scenes_EstrangedActI from './SourceEngine/Scenes_EstrangedActI.js'; import * as Scenes_AShortHike from './AShortHike/Scenes.js'; import * as Scenes_NeonWhite from './NeonWhite/Scenes.js'; +import * as Scenes_OuterWilds from './OuterWilds/Scenes.js'; import { DroppedFileSceneDesc, traverseFileSystemDataTransfer } from './Scenes_FileDrops.js'; @@ -223,6 +224,7 @@ const sceneGroups: (string | SceneGroup)[] = [ Scenes_EstrangedActI.sceneGroup, Scenes_AShortHike.sceneGroup, Scenes_NeonWhite.sceneGroup, + Scenes_OuterWilds.sceneGroup, ]; function convertCanvasToPNG(canvas: HTMLCanvasElement): Promise {