From cb65a52e95d0c7d132bd2a5fc268ab46e28afbc0 Mon Sep 17 00:00:00 2001 From: Akash Shakdwipeea Date: Sat, 4 Oct 2025 12:47:30 +0530 Subject: [PATCH] gltf models loader (#10) * use channels for comms this also refactors the app setup and messaging modules * remove double logging * load single model gltf files * add gltf loader * add sponza model * camera with rotors (#11) * orbit with rotors * add dolly behaviour to emulate zoom --- .gitattributes | 1 + AGENTS.md | 18 + Cargo.lock | 1221 +++++++++++++++++++++++++++++++- Cargo.toml | 10 +- src/camera.rs | 298 ++++++++ src/example.wgsl | 1 + src/gltf.rs | 224 ++++++ src/gltf.wgsl | 56 ++ src/lib.rs | 61 +- src/message.rs | 30 + src/platform/web/worker/mod.rs | 4 +- src/renderer/mod.rs | 664 +++++++++++------ src/renderer/scene.rs | 340 +++++++++ static/sponza.glb | 3 + static/vite.svg | 1 + 15 files changed, 2677 insertions(+), 255 deletions(-) create mode 100644 .gitattributes create mode 100644 AGENTS.md create mode 100644 src/camera.rs create mode 100644 src/gltf.rs create mode 100644 src/gltf.wgsl create mode 100644 src/renderer/scene.rs create mode 100644 static/sponza.glb create mode 100644 static/vite.svg diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6871389 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +static/sponza.glb filter=lfs diff=lfs merge=lfs -text diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..741fdc8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ +# Build, Lint, and Test Commands +- `npm run dev`: Start Vite dev server with hot reload for WASM bundle +- `npm run build`: Build optimized WASM and JS in `dist/` for development +- `npm run build-release`: Build optimized WASM and JS for production +- `cargo check`: Validate Rust sources quickly before full builds +- `cargo fmt`: Format Rust code with rustfmt +- No unit tests currently exist; add them as `*_tests.rs` modules + +# Code Style Guidelines +- **Rust 2021 idioms**: Use snake_case for modules, files, functions, and variables +- **Indentation**: 4 spaces (configured in rustfmt) +- **Imports**: Group std library, external crates, then local modules +- **Types**: Use descriptive struct fields and enum variants (e.g., `positions`, `normals`) +- **Error handling**: Use `thiserror` derive macro for custom error types +- **Naming**: Mirror GLTF semantics explicitly in struct fields +- **WGSL shaders**: Keep binding names aligned with Rust bind group layouts +- **JavaScript/TypeScript**: Format with prettier defaults +- **Comments**: Add documentation comments for public APIs using `///` diff --git a/Cargo.lock b/Cargo.lock index cbd035f..1c75ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -41,12 +50,33 @@ dependencies = [ "libloading", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base" version = "0.1.0" @@ -59,6 +89,9 @@ dependencies = [ "js-sys", "log", "raw-window-handle", + "reqwest", + "thiserror 2.0.15", + "ultraviolet", "wasm-bindgen", "wasm-bindgen-futures", "wasm-logger", @@ -72,6 +105,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -146,6 +185,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -199,6 +253,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -222,7 +286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -241,6 +305,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "document-features" version = "0.2.11" @@ -250,12 +325,37 @@ dependencies = [ "litrs", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -275,12 +375,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -288,7 +403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -302,12 +417,95 @@ dependencies = [ "syn", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gl_generator" version = "0.14.0" @@ -337,7 +535,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", "gltf-json", "image", @@ -430,6 +628,25 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.6.0" @@ -456,6 +673,231 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "image" version = "0.25.6" @@ -486,6 +928,33 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.15" @@ -553,6 +1022,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "litrs" version = "0.4.2" @@ -599,12 +1080,18 @@ dependencies = [ "bitflags 2.9.1", "block", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -615,6 +1102,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "naga" version = "26.0.0" @@ -637,10 +1135,27 @@ dependencies = [ "once_cell", "rustc-hash", "spirv", - "thiserror 2.0.12", + "thiserror 2.0.15", "unicode-ident", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -669,12 +1184,65 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "5.0.0" @@ -713,6 +1281,24 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" @@ -747,6 +1333,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "presser" version = "0.3.1" @@ -777,6 +1372,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "range-alloc" version = "0.1.4" @@ -804,12 +1405,118 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -822,12 +1529,53 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -860,12 +1608,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "slotmap" version = "1.0.7" @@ -881,6 +1653,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -890,12 +1672,24 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.104" @@ -907,6 +1701,60 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -927,11 +1775,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.15", ] [[package]] @@ -947,15 +1795,154 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ultraviolet" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea519dad475ee0446b8172793c3c327e4fc81dafdaf05aaac510e630000b6296" +dependencies = [ + "wide", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -968,18 +1955,71 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1124,7 +2164,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.15", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", @@ -1199,7 +2239,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.15", "wasm-bindgen", "web-sys", "wgpu-types", @@ -1217,17 +2257,27 @@ dependencies = [ "bytemuck", "js-sys", "log", - "thiserror 2.0.12", + "thiserror 2.0.15", "web-sys", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -1248,8 +2298,8 @@ checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ "windows-implement", "windows-interface", - "windows-result", - "windows-strings", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] @@ -1281,6 +2331,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -1290,13 +2351,40 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ "windows-targets 0.52.6", ] @@ -1438,12 +2526,111 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xml-rs" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 40be4e7..e52f093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,11 @@ web-sys = { version = "0.3.77", features = [ "Blob", "BlobPropertyBag", "Url", + "Request", + "RequestInit", + "RequestMode", + "Response", + "Headers" ]} js-sys = "0.3.77" bytemuck = { version = "1.23.1", features = [ @@ -45,10 +50,13 @@ bytemuck = { version = "1.23.1", features = [ cgmath = "0.18" raw-window-handle = "0.6.2" wgpu = "26.0.1" +reqwest = { version = "0.12.23", features = ["json"] } +thiserror = "2.0.15" +ultraviolet = "0.10.0" [dependencies.gltf] version = "1.4" features = ["extras", "names"] [package.metadata.wasm-pack.profile.release] -wasm-opt = false \ No newline at end of file +wasm-opt = false diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..740ef5a --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,298 @@ +use std::f32::consts::PI; + +use ultraviolet::{projection, Bivec3, Mat4, Rotor3, Vec3, Vec4}; +use wgpu::util::DeviceExt; + +use crate::{message::WheelMessage, renderer::scene::UniformResource}; + +const MIN_DISTANCE: f32 = 0.1; +const MAX_PITCH: f32 = PI / 2.0 - 0.01; +const ORBIT_SENSITIVITY: f32 = 0.005; +const ZOOM_SENSITIVITY: f32 = 0.002; + +#[repr(C)] +pub struct Camera { + // Hot data - cached computed matrix (64 bytes, 1 cache line) + pub view_proj: [[f32; 4]; 4], + + // Warm data - frequently accessed vectors (36 bytes) + position: Vec3, + target: Vec3, + up: Vec3, + + // Cold data - projection parameters (16 bytes) + fov: f32, + aspect_ratio: f32, + z_near: f32, + z_far: f32, + + // Rotor orientation + spherical coordinates for orbit camera behaviour + rotor: Rotor3, + distance: f32, + yaw: f32, + pitch: f32, + + // Dirty flag for lazy evaluation + dirty: bool, +} + +struct OrthonormalBasis { + right: Vec3, + up: Vec3, + forward: Vec3, +} + +impl OrthonormalBasis { + pub fn new(right: Vec3, up: Vec3, forward: Vec3) -> Self { + Self { right, up, forward } + } + + pub fn from_camera(camera: &Camera) -> Self { + let mut forward_offset = camera.target - camera.position; + if forward_offset.mag_sq() <= f32::EPSILON { + forward_offset = -Vec3::unit_z(); + } + + let forward = forward_offset.normalized(); + + let mut right = forward.cross(camera.up); + + // Check if right vector is near zero (forward and up are parallel) + if right.mag_sq() < 1e-10 { + // Try alternate axes to find a valid right vector + let alternate_axes = [Vec3::unit_y(), Vec3::unit_x()]; + for axis in alternate_axes.iter() { + right = forward.cross(*axis); + if right.mag_sq() >= 1e-10 { + break; + } + } + } + + right = right.normalized(); + let up = right.cross(forward).normalized(); + + Self::new(right, up, forward) + } +} + +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] +pub struct CameraUniform { + view_proj: [[f32; 4]; 4], +} + +impl Camera { + pub fn new(aspect_ratio: f32) -> Self { + let mut camera = Camera { + view_proj: [[0.0; 4]; 4], + position: Vec3::new(0.0, 1.5, 0.0), + target: Vec3::zero(), + up: Vec3::unit_y(), + fov: PI / 3.0, + aspect_ratio, + z_near: 0.1, + z_far: 100000.0, + rotor: Rotor3::identity(), + distance: 1.0, + yaw: 0.0, + pitch: 0.0, + dirty: true, + }; + + camera.compute_rotor(); + camera.compute_view_proj_mat(); + + camera + } + + pub fn compute_view_proj_mat(&mut self) { + let view = Mat4::look_at(self.position, self.target, self.up); + let proj = projection::rh_yup::perspective_wgpu_dx( + self.fov, + self.aspect_ratio, + self.z_near, + self.z_far, + ); + self.view_proj = (proj * view).into(); + self.dirty = false; + } + + pub fn look_at(&mut self, position: Vec3, target: Vec3) { + self.position = position; + self.target = target; + self.up = Vec3::unit_y(); + self.compute_rotor(); + self.dirty = true; + self.compute_view_proj_mat(); + } + + pub fn set_depth_range(&mut self, z_near: f32, z_far: f32) { + self.z_near = z_near; + self.z_far = z_far.max(z_near + f32::EPSILON); + self.dirty = true; + self.compute_view_proj_mat(); + } + + pub fn position(&self) -> Vec3 { + self.position + } + + pub fn orbit(&mut self, delta_x: f32, delta_y: f32) { + // Skip tiny movements to reduce unnecessary computations + if delta_x.abs() < 0.001 && delta_y.abs() < 0.001 { + return; + } + + let yaw_theta = delta_x * ORBIT_SENSITIVITY; + let yaw_rotor = + Rotor3::from_angle_plane(yaw_theta, Bivec3::from_normalized_axis(Vec3::unit_y())); + + let basis = OrthonormalBasis::from_camera(self); + + let desired_pitch = (self.pitch - delta_y * ORBIT_SENSITIVITY).clamp(-MAX_PITCH, MAX_PITCH); + let applied_pitch = desired_pitch - self.pitch; + + let pitch_rotor = + Rotor3::from_angle_plane(applied_pitch, Bivec3::from_normalized_axis(basis.right)); + + let orbit_rotor = (yaw_rotor * pitch_rotor).normalized(); + + self.rotor = (orbit_rotor * self.rotor).normalized(); + + let mut offset = self.position - self.target; + if offset.mag_sq() <= f32::EPSILON { + offset = Vec3::unit_z() * self.distance.max(MIN_DISTANCE); + } + + orbit_rotor.rotate_vec(&mut offset); + self.distance = offset.mag().max(MIN_DISTANCE); + self.position = offset + self.target; + + self.yaw += yaw_theta; + self.pitch = desired_pitch; + + self.dirty = true; + self.compute_view_proj_mat(); + } + + pub fn zoom(&mut self, msg: &WheelMessage) { + let mut delta = msg.delta_y as f32; + + // Match browser delta modes so the wheel delta is always roughly pixels. + match msg.delta_mode { + 1 => delta *= 16.0, + 2 => delta *= 800.0, + _ => {} + } + + // Scrolling up should zoom in. + delta = -delta; + + if delta.abs() <= f32::EPSILON { + return; + } + + // Get forward direction from camera position to target + let mut forward_vec = self.target - self.position; + if forward_vec.mag_sq() <= f32::EPSILON { + forward_vec = Vec3::unit_z(); + } + let forward_dir = forward_vec.normalized(); + let current_distance = forward_vec.mag(); + + // Scale dolly movement by distance to target for consistent perceived zoom speed + let dolly_distance = delta * ZOOM_SENSITIVITY * current_distance; + let dolly_translation = forward_dir * dolly_distance; + + self.position += dolly_translation; + self.target += dolly_translation; + + self.compute_rotor(); + self.dirty = true; + self.compute_view_proj_mat(); + } + + pub fn create_uniform_resource(&self, device: &wgpu::Device) -> UniformResource { + let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: "camera uniform buffer".into(), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + contents: bytemuck::cast_slice(&[self.view_proj]), + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Uniform bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Uniform bind group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 1, + resource: buffer.as_entire_binding(), + }], + }); + + UniformResource { + buffer, + bind_group, + bind_group_layout, + } + } + + fn compute_rotor(&mut self) { + let offset = self.position - self.target; + let distance = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt(); + self.distance = distance.max(MIN_DISTANCE); + + // to compute the initial rotor we will do two rotations + // these will orient the camera to the new coordinates + // + + // but first we need the orthonormal basis for the current camera + let basis = OrthonormalBasis::from_camera(self); + + // first rotation + // this is the swing to make position face the target + let camera_local_up = Vec3::unit_z(); + let swing_rotor = Rotor3::from_rotation_between(camera_local_up, -basis.forward); + + // now we need a twist rotor which aligns the camera up + let mut up_after_swing = self.up.clone(); + swing_rotor.rotate_vec(&mut up_after_swing); + + // to rotate a vector by a rotor we need + // - a bivector (represents the axis of rotation) + // - angle of rotation + let twist_axis = (-basis.forward).normalized(); + let twist_plane = Bivec3::from_normalized_axis(twist_axis); + + // Calculate twist angle between the up vectors: + // u1 × uc ⋅ (-f) + // θ = atan2( ————————————— , u1 ⋅ uc ) + // ‖u1 × uc‖ + // + // Where: + // u1 = up vector after swing rotation + // uc = camera's current up vector + // f = forward vector (twist axis) + let theta = up_after_swing + .cross(self.up) + .dot(twist_axis) + .atan2(up_after_swing.dot(self.up)); + + let twist_rotor = Rotor3::from_angle_plane(theta, twist_plane); + + self.rotor = (swing_rotor * twist_rotor).normalized(); + } +} diff --git a/src/example.wgsl b/src/example.wgsl index 3b81f2b..a1aa0a2 100644 --- a/src/example.wgsl +++ b/src/example.wgsl @@ -6,6 +6,7 @@ struct UniformData { } @group(0) @binding(0) var uni: UniformData; +@group(1) @binding(0) var view_proj: mat4x4; struct VertexInput { @location(0) pos: vec3, diff --git a/src/gltf.rs b/src/gltf.rs new file mode 100644 index 0000000..1d9535d --- /dev/null +++ b/src/gltf.rs @@ -0,0 +1,224 @@ +use gltf::Gltf; +use ultraviolet::{Mat4, Vec3, Vec4}; +use wgpu::TextureFormat; + +use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder}; + +#[derive(Clone, Copy, Debug)] +pub struct ModelBounds { + pub min: [f32; 3], + pub max: [f32; 3], +} + +impl ModelBounds { + fn new(min: [f32; 3], max: [f32; 3]) -> Self { + Self { min, max } + } + + fn include_point(&mut self, point: [f32; 3]) { + for i in 0..3 { + self.min[i] = self.min[i].min(point[i]); + self.max[i] = self.max[i].max(point[i]); + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ImportError { + #[error("failed to fetch the model")] + Http(#[from] reqwest::Error), + + #[error("failed to decode bytes")] + GltfParse(#[from] gltf::Error), + + #[error("failed to load model")] + LoadError, +} + +fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> { + use gltf::mesh::util::ReadTexCoords; + + match tex_coords { + ReadTexCoords::F32(iter) => iter.collect(), + ReadTexCoords::U16(iter) => iter + .map(|[u, v]| [u as f32 / u16::MAX as f32, v as f32 / u16::MAX as f32]) + .collect(), + ReadTexCoords::U8(iter) => iter + .map(|[u, v]| [u as f32 / u8::MAX as f32, v as f32 / u8::MAX as f32]) + .collect(), + } +} + +fn convert_indices(indices: gltf::mesh::util::ReadIndices<'_>) -> Vec { + use gltf::mesh::util::ReadIndices; + + match indices { + ReadIndices::U8(iter) => iter.map(|i| i as u32).collect(), + ReadIndices::U16(iter) => iter.map(|i| i as u32).collect(), + ReadIndices::U32(iter) => iter.collect(), + } +} + +fn mat4_from_gltf(matrix: [[f32; 4]; 4]) -> Mat4 { + Mat4::new( + Vec4::new(matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3]), + Vec4::new(matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3]), + Vec4::new(matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3]), + Vec4::new(matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3]), + ) +} + +fn visit_node<'a>( + node: gltf::Node<'a>, + parent_transform: Mat4, + device: &wgpu::Device, + resources: &mut crate::renderer::GpuResources, + meshes: &mut Vec, + data_blob: &[u8], + pipeline_index: usize, + model_bounds: &mut Option, +) { + let local_transform = mat4_from_gltf(node.transform().matrix()); + let world_transform = parent_transform * local_transform; + let normal_matrix = world_transform.inversed().transposed(); + + if let Some(mesh) = node.mesh() { + for primitive in mesh.primitives() { + let reader = primitive.reader(|buffer| match buffer.source() { + gltf::buffer::Source::Bin => Some(&data_blob[..]), + _ => None, + }); + + let mut positions: Vec<[f32; 3]> = match reader.read_positions() { + Some(iter) => iter.collect(), + None => Vec::new(), + }; + + if positions.is_empty() { + continue; + } + + let vertex_count = positions.len(); + + let default_normal_vec = normal_matrix.transform_vec3(Vec3::unit_y()).normalized(); + let default_normal = [ + default_normal_vec.x, + default_normal_vec.y, + default_normal_vec.z, + ]; + + let mut normals: Vec<[f32; 3]> = reader + .read_normals() + .map(|iter| { + iter.map(|normal| { + let vec = Vec3::new(normal[0], normal[1], normal[2]); + let transformed = normal_matrix.transform_vec3(vec).normalized(); + [transformed.x, transformed.y, transformed.z] + }) + .collect() + }) + .unwrap_or_else(|| vec![default_normal; vertex_count]); + + if normals.len() != vertex_count { + normals.resize(vertex_count, default_normal); + } + + let mut uvs: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(convert_tex_coords) + .unwrap_or_else(|| vec![[0.0, 0.0]; vertex_count]); + + if uvs.len() != vertex_count { + uvs.resize(vertex_count, [0.0, 0.0]); + } + + for position in &mut positions { + let vec = Vec3::new(position[0], position[1], position[2]); + let transformed = world_transform.transform_point3(vec); + *position = [transformed.x, transformed.y, transformed.z]; + } + + for position in &positions { + if let Some(bounds) = model_bounds.as_mut() { + bounds.include_point(*position); + } else { + *model_bounds = Some(ModelBounds::new(*position, *position)); + } + } + + let indices: Vec = reader + .read_indices() + .map(convert_indices) + .unwrap_or_else(|| (0..vertex_count as u32).collect()); + + if indices.is_empty() { + continue; + } + + let mesh = MeshBuilder::new() + .with_vertices(device, resources, &positions, &normals, &uvs) + .with_indices(device, resources, &indices) + .with_pipeline(pipeline_index) + .build(); + + meshes.push(mesh); + } + } + + for child in node.children() { + visit_node( + child, + world_transform, + device, + resources, + meshes, + data_blob, + pipeline_index, + model_bounds, + ); + } +} + +pub async fn load_gltf_model( + device: &wgpu::Device, + resources: &mut crate::renderer::GpuResources, + meshes: &mut Vec, + surface_format: TextureFormat, +) -> Result, ImportError> { + let glb_data = reqwest::get("http://localhost:8080/sponza.glb") + .await? + .bytes() + .await?; + + let model = Gltf::from_slice(&glb_data)?; + let data_blob = model.blob.as_ref().ok_or(ImportError::LoadError)?; + + let vertex_layout = mesh_vertex_layout(); + + let pipeline_index = resources.get_or_create_pipeline( + device, + "gltf_standard", + &vertex_layout, + include_str!("./gltf.wgsl"), + surface_format, + ); + + let mut model_bounds: Option = None; + + for scene in model.scenes() { + for node in scene.nodes() { + visit_node( + node, + Mat4::identity(), + device, + resources, + meshes, + data_blob, + pipeline_index, + &mut model_bounds, + ); + } + } + + Ok(model_bounds) +} diff --git a/src/gltf.wgsl b/src/gltf.wgsl new file mode 100644 index 0000000..11ba06d --- /dev/null +++ b/src/gltf.wgsl @@ -0,0 +1,56 @@ +struct UniformData { + mouse_move: vec2, + mouse_click: vec2, + resolution: vec2, + time: f32, + _padding0: f32, + camera_position: vec4, +} + +@group(0) @binding(0) var uni: UniformData; +@group(1) @binding(1) var view_proj: mat4x4; + +struct VertexInput { + @location(0) pos: vec3, + @location(1) normal: vec3, +// @location(2) uv: vec2 +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_pos: vec3, + @location(1) normal: vec3 +} + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.clip_position = view_proj * vec4(in.pos, 1.0); + out.world_pos = in.pos; + out.normal = normalize(in.normal); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let light_direction = normalize(vec3(0.35, 1.0, 0.45)); + let light_color = vec3(1.0, 0.95, 0.85); + let base_color = vec3(1.0, 0.0, 0.0); + + let normal = normalize(in.normal); + let view_dir = normalize(uni.camera_position.xyz - in.world_pos); + + let diffuse_strength = max(dot(normal, light_direction), 0.0); + let ambient = 0.15; + + var specular = 0.0; + if (diffuse_strength > 0.0) { + let halfway_dir = normalize(light_direction + view_dir); + specular = pow(max(dot(normal, halfway_dir), 0.0), 32.0); + } + + let lighting = min(base_color * (ambient + diffuse_strength) + light_color * specular, vec3(1.0)); + let x = select(0.0, 0.3, distance(in.clip_position.xy, uni.mouse_move) < 25.0); + let y = select(0.0, 0.3, distance(in.clip_position.xy, uni.mouse_click) < 25.0); + return vec4(lighting + x - y, 1.0); +} diff --git a/src/lib.rs b/src/lib.rs index f40e592..f93db44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,17 @@ use std::sync::mpsc::{self, Sender}; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; +#[cfg(target_arch = "wasm32")] +use web_sys::AddEventListenerOptions; + use crate::{message::WindowEvent, platform::web, platform::web::worker::MainWorker}; +mod camera; +mod gltf; mod message; mod platform; mod renderer; + #[cfg(target_arch = "wasm32")] pub struct App { _worker: platform::web::worker::MainWorker, @@ -16,6 +22,8 @@ pub struct App { // Store closures to keep them alive resize_listener: Option>, mousemove_listener: Option>, + mousedown_listener: Option>, + wheel_listener: Option>, } impl App { @@ -36,10 +44,12 @@ impl App { worker_chan: sender, resize_listener: None, mousemove_listener: None, + mousedown_listener: None, + wheel_listener: None, }; app.setup_event_listeners(); - return Ok(app); + Ok(app) } #[cfg(target_arch = "wasm32")] @@ -70,6 +80,9 @@ impl App { let mousemove_listener: Closure = Closure::new(move |event: web_sys::MouseEvent| { use crate::message::MouseMessage; + if event.buttons() & 0x04 != 0 { + event.prevent_default(); + } let mouse_event_data = MouseMessage::from_evt(event.clone()); let mut event_data = WindowEvent::PointerMove(mouse_event_data.clone()); @@ -91,8 +104,51 @@ impl App { .add_event_listener_with_callback("click", mousemove_listener.as_ref().unchecked_ref()) .unwrap(); + let mousedown_listener: Closure = + Closure::new(move |event: web_sys::MouseEvent| { + if event.button() == 1 { + event.prevent_default(); + } + }); + + let _ = window + .add_event_listener_with_callback( + "mousedown", + mousedown_listener.as_ref().unchecked_ref(), + ) + .unwrap(); + + let wheel_worker_chan = self.worker_chan.clone(); + let wheel_listener: Closure = + Closure::new(move |event: web_sys::WheelEvent| { + use crate::message::WheelMessage; + + event.prevent_default(); + let wheel_event_data = WheelMessage::from_evt(event); + + wheel_worker_chan + .send(WindowEvent::PointerWheel(wheel_event_data)) + .unwrap(); + }); + + let wheel_options = { + let options = AddEventListenerOptions::new(); + options.set_passive(false); + options + }; + + let _ = window + .add_event_listener_with_callback_and_add_event_listener_options( + "wheel", + wheel_listener.as_ref().unchecked_ref(), + &wheel_options, + ) + .unwrap(); + self.resize_listener = Some(resize_listener); self.mousemove_listener = Some(mousemove_listener); + self.mousedown_listener = Some(mousedown_listener); + self.wheel_listener = Some(wheel_listener); } } @@ -100,7 +156,6 @@ impl App { #[wasm_bindgen] pub fn main() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - console_log::init_with_level(log::Level::Info).unwrap(); wasm_logger::init(wasm_logger::Config::default()); wasm_bindgen_futures::spawn_local(async { @@ -116,5 +171,3 @@ pub fn worker_entrypoint(ptr: u32) { let work = unsafe { Box::from_raw(ptr as *mut Box) }; (*work)(); } - - diff --git a/src/message.rs b/src/message.rs index b1a5cea..abaf41e 100644 --- a/src/message.rs +++ b/src/message.rs @@ -5,6 +5,7 @@ pub enum WindowEvent { Resize(ResizeMessage), PointerMove(MouseMessage), PointerClick(MouseMessage), + PointerWheel(WheelMessage), } // Display for WindowEvent @@ -14,6 +15,7 @@ impl fmt::Display for WindowEvent { WindowEvent::Resize(msg) => write!(f, "Resize: {:?}", msg), WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg), WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg), + WindowEvent::PointerWheel(msg) => write!(f, "PointerWheel: {:?}", msg), } } } @@ -29,6 +31,7 @@ pub struct ResizeMessage { pub struct MouseMessage { pub scale_factor: f64, pub button: f64, + pub buttons: u16, pub client_x: f64, pub client_y: f64, pub movement_x: f64, @@ -43,6 +46,7 @@ impl MouseMessage { Self { scale_factor: window.device_pixel_ratio(), button: event.button() as f64, + buttons: event.buttons(), client_x: event.client_x() as f64, client_y: event.client_y() as f64, movement_x: event.movement_x() as f64, @@ -52,3 +56,29 @@ impl MouseMessage { } } } + +#[derive(Debug, Clone)] +pub struct WheelMessage { + pub scale_factor: f64, + pub delta_x: f64, + pub delta_y: f64, + pub delta_z: f64, + pub delta_mode: u32, + pub client_x: f64, + pub client_y: f64, +} + +impl WheelMessage { + pub fn from_evt(event: web_sys::WheelEvent) -> Self { + let window = web_sys::window().unwrap(); + Self { + scale_factor: window.device_pixel_ratio(), + delta_x: event.delta_x(), + delta_y: event.delta_y(), + delta_z: event.delta_z(), + delta_mode: event.delta_mode(), + client_x: event.client_x() as f64, + client_y: event.client_y() as f64, + } + } +} diff --git a/src/platform/web/worker/mod.rs b/src/platform/web/worker/mod.rs index 4f690e8..0299aeb 100644 --- a/src/platform/web/worker/mod.rs +++ b/src/platform/web/worker/mod.rs @@ -1,10 +1,10 @@ use crate::message::WindowEvent; +use log::info; use std::sync::mpsc::Receiver; use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; use wasm_bindgen::{prelude::*, JsValue}; use wasm_bindgen_futures::JsFuture; use web_sys::MessageEvent; -use log::info; /// Binds JS. #[wasm_bindgen(module = "/src/platform/web/worker/workerGen.js")] @@ -47,8 +47,6 @@ impl Debug for MainWorker { } } - - impl MainWorker { /// Spawns main worker from the window context. pub fn spawn( diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index efe78d1..c514abd 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -1,14 +1,240 @@ -use std::{cell::RefCell, rc::Rc, sync::mpsc::Receiver}; +use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc, sync::mpsc::Receiver}; use log::info; use wasm_bindgen::{prelude::Closure, JsCast}; +use wasm_bindgen_futures::spawn_local; use web_sys::DedicatedWorkerGlobalScope; -use wgpu::util::DeviceExt; -use crate::message::{MouseMessage, ResizeMessage, WindowEvent}; +use crate::{ + gltf::{load_gltf_model, ImportError, ModelBounds}, + message::{MouseMessage, ResizeMessage, WindowEvent}, + renderer::scene::Scene, +}; + +pub mod scene; + +const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + +pub struct GpuResources { + // Core resources + buffers: Vec, + pipelines: Vec, + textures: Vec, + + // Layout management + pipeline_layouts: Vec, + bind_group_layouts: Vec, + + // Simple name-based pipeline lookup + pipeline_registry: HashMap, + + // Shader modules cache + shader_modules: HashMap, +} + +impl GpuResources { + pub fn new() -> Self { + Self { + buffers: Vec::new(), + pipelines: Vec::new(), + textures: Vec::new(), + pipeline_layouts: Vec::new(), + bind_group_layouts: Vec::new(), + pipeline_registry: HashMap::new(), + shader_modules: HashMap::new(), + } + } + + pub fn add_position_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex { + let index = self.buffers.len() as u32; + self.buffers.push(buffer); + BufferIndex { + index, + _buffer_type: PhantomData, + } + } + + pub fn add_normal_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex { + let index = self.buffers.len() as u32; + self.buffers.push(buffer); + BufferIndex { + index, + _buffer_type: PhantomData, + } + } + + pub fn add_uv_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex { + let index = self.buffers.len() as u32; + self.buffers.push(buffer); + BufferIndex { + index, + _buffer_type: PhantomData, + } + } + + pub fn add_index_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex { + let index = self.buffers.len() as u32; + self.buffers.push(buffer); + BufferIndex { + index, + _buffer_type: PhantomData, + } + } + + #[inline(always)] + pub fn get_buffer(&self, id: &BufferIndex) -> &wgpu::Buffer { + &self.buffers[id.index as usize] + } + + pub fn create_pipeline( + &mut self, + device: &wgpu::Device, + name: &str, + vertex_layout: &[wgpu::VertexBufferLayout], + shader_source: &str, + surface_format: wgpu::TextureFormat, + ) -> Result { + if self.pipeline_registry.contains_key(name) { + return Err(format!("Pipeline '{}' already exists", name)); + } + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(name), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let layout = self.get_or_create_pipeline_layout(device, name); + + // Determine entry points based on pipeline name + let (vertex_entry, fragment_entry) = match name { + "triangle_colored" => ("v_main", "f_main"), + _ => ("vs_main", "fs_main"), + }; + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(name), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some(vertex_entry), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: vertex_layout, + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::LessEqual, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some(fragment_entry), + compilation_options: wgpu::PipelineCompilationOptions::default(), + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + cache: None, + }); + + let index = self.pipelines.len(); + self.pipelines.push(pipeline); + self.pipeline_registry.insert(name.to_string(), index); + + Ok(index) + } + + pub fn get_pipeline(&self, name: &str) -> Option { + self.pipeline_registry.get(name).copied() + } + + pub fn get_or_create_pipeline( + &mut self, + device: &wgpu::Device, + name: &str, + vertex_layout: &[wgpu::VertexBufferLayout], + shader_source: &str, + surface_format: wgpu::TextureFormat, + ) -> usize { + if let Some(index) = self.get_pipeline(name) { + return index; + } + + self.create_pipeline(device, name, vertex_layout, shader_source, surface_format) + .expect(&format!("Failed to create pipeline '{}'", name)) + } + + pub fn get_pipeline_by_index(&self, index: usize) -> &wgpu::RenderPipeline { + &self.pipelines[index] + } + + pub fn set_bind_group_layouts(&mut self, layouts: &[wgpu::BindGroupLayout; 2]) { + self.bind_group_layouts = layouts.to_vec(); + } + + fn get_or_create_pipeline_layout( + &mut self, + device: &wgpu::Device, + label: &str, + ) -> wgpu::PipelineLayout { + if self.pipeline_layouts.is_empty() { + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some(label), + bind_group_layouts: &self.bind_group_layouts.iter().collect::>(), + push_constant_ranges: &[], + }); + self.pipeline_layouts.push(layout); + } + self.pipeline_layouts[0].clone() + } +} + +impl Default for GpuResources { + fn default() -> Self { + Self::new() + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BufferIndex { + pub index: u32, + _buffer_type: PhantomData, +} + +impl BufferIndex { + pub fn new(index: u32) -> Self { + Self { + index, + _buffer_type: PhantomData, + } + } +} + +// Kinds of buffers supported +pub struct Position; +pub struct Normal; +pub struct UV; +pub struct Index; -/// Drawing relative data. -/// Note that this belongs to main worker. pub struct Renderer { canvas: web_sys::OffscreenCanvas, events_chan: Receiver, @@ -16,29 +242,55 @@ pub struct Renderer { device: wgpu::Device, queue: wgpu::Queue, surface_config: wgpu::SurfaceConfiguration, - vertex_buffer: wgpu::Buffer, - index_buffer: wgpu::Buffer, - index_num: u32, - uniform_data: UniformData, - uniform_buffer: wgpu::Buffer, - uniform_bind_group: wgpu::BindGroup, - render_pipeline: wgpu::RenderPipeline, + scene: Scene, + resources: GpuResources, + depth_texture: wgpu::Texture, + depth_view: wgpu::TextureView, } impl Renderer { + fn create_depth_texture( + device: &wgpu::Device, + config: &wgpu::SurfaceConfiguration, + ) -> (wgpu::Texture, wgpu::TextureView) { + let size = wgpu::Extent3d { + width: config.width.max(1), + height: config.height.max(1), + depth_or_array_layers: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("depth texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: DEPTH_FORMAT, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + (texture, view) + } + + fn recreate_depth_texture(&mut self) { + let (texture, view) = Self::create_depth_texture(&self.device, &self.surface_config); + self.depth_texture = texture; + self.depth_view = view; + } + pub async fn new(canvas: web_sys::OffscreenCanvas, events_chan: Receiver) -> Self { let id = wgpu::InstanceDescriptor { backends: wgpu::Backends::BROWSER_WEBGPU, ..Default::default() }; - // wgpu instance let instance = wgpu::Instance::new(&id); - // wgpu surface let surface = instance .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone())) .unwrap(); - // wgpu adapter let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { compatible_surface: Some(&surface), @@ -52,7 +304,6 @@ impl Renderer { info!("Adapter features: {:?}", adapter.features()); info!("Adapter limits: {:?}", adapter.limits()); - // wgpu device and queue let descriptor = wgpu::DeviceDescriptor { required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), @@ -62,8 +313,7 @@ impl Renderer { }; let (device, queue) = adapter.request_device(&descriptor).await.unwrap(); - info!("after"); - // wgpu surface configuration + let surface_caps = surface.get_capabilities(&adapter); let surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, @@ -80,40 +330,19 @@ impl Renderer { surface_config.width, surface_config.height ); surface.configure(&device, &surface_config); - // wgpu vertex buffer - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Vertex buffer"), - contents: bytemuck::cast_slice(VERTICES), - usage: wgpu::BufferUsages::VERTEX, - }); - // wgpu index buffer - let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Index buffer"), - contents: bytemuck::cast_slice(INDICES), - usage: wgpu::BufferUsages::INDEX, - }); - // wgpu uniform buffer - let uniform_data = UniformData { - resolution: [canvas.width() as f32, canvas.height() as f32], - mouse_move: [std::f32::MIN, std::f32::MIN], - mouse_click: [std::f32::MIN, std::f32::MIN], - ..Default::default() - }; - let (uniform_buffer, uniform_layout, uniform_bind_group) = - Renderer::create_uniform_buffer(&device, bytemuck::cast_slice(&[uniform_data][..])); - // wgpu shader module - let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader module"), - source: wgpu::ShaderSource::Wgsl(include_str!("../example.wgsl").into()), - }); - // wgpu render pipeline - let render_pipeline = Renderer::create_render_pipeline( + + let (depth_texture, depth_view) = Self::create_depth_texture(&device, &surface_config); + + let mut resources = GpuResources::new(); + + let mut scene = Scene::new( &device, - &[&uniform_layout], - &shader_module, - &surface_config, + ultraviolet::Vec2::new(canvas.width() as f32, canvas.height() as f32), ); + resources.set_bind_group_layouts(&scene.bind_group_layout); + scene.create_default_triangle(&device, &mut resources, surface_config.format); + Self { canvas, events_chan, @@ -121,108 +350,15 @@ impl Renderer { device, queue, surface_config, - vertex_buffer, - index_buffer, - index_num: INDICES.len() as u32, - uniform_data, - uniform_buffer, - uniform_bind_group, - render_pipeline, + scene, + resources, + depth_texture, + depth_view, } } - fn create_uniform_buffer( - device: &wgpu::Device, - contents: &[u8], - ) -> (wgpu::Buffer, wgpu::BindGroupLayout, wgpu::BindGroup) { - let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Uniform buffer"), - contents, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Uniform bind group layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Uniform bind group"), - layout: &bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - (buffer, bind_group_layout, bind_group) - } - - fn create_render_pipeline( - device: &wgpu::Device, - bind_group_layouts: &[&wgpu::BindGroupLayout], - shader_module: &wgpu::ShaderModule, - surface_config: &wgpu::SurfaceConfiguration, - ) -> wgpu::RenderPipeline { - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render pipeline layout"), - bind_group_layouts, - push_constant_ranges: &[], - }); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - cache: None, - label: Some("Render pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - compilation_options: wgpu::PipelineCompilationOptions::default(), - module: shader_module, - entry_point: Some("v_main"), - buffers: &[Vertex::layout()], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - fragment: Some(wgpu::FragmentState { - compilation_options: wgpu::PipelineCompilationOptions::default(), - module: shader_module, - entry_point: Some("f_main"), - targets: &[Some(wgpu::ColorTargetState { - format: surface_config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }) - } - fn render(&mut self, time: f32) { - // Write uniform data to its buffer - self.uniform_data.time = time * 0.001; - self.queue.write_buffer( - &self.uniform_buffer, - 0, - bytemuck::cast_slice(&[self.uniform_data][..]), - ); + self.scene.update(&self.queue, time); let surface_texture = self.surface.get_current_texture().unwrap(); let texture_view = surface_texture.texture.create_view(&Default::default()); @@ -249,37 +385,98 @@ impl Renderer { store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: None, + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), occlusion_query_set: None, timestamp_writes: None, }); - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); - render_pass.draw_indexed(0..self.index_num, 0, 0..1); + + for (i, bind_group) in self.scene.bind_groups.iter().enumerate() { + render_pass.set_bind_group(i as u32, bind_group, &[]); + } + + for mesh in &self.scene.meshes { + render_pass.set_pipeline(self.resources.get_pipeline_by_index(mesh.pipeline_index)); + + render_pass.set_vertex_buffer( + 0, + self.resources + .get_buffer(&mesh.position_buffer_index) + .slice(..), + ); + render_pass.set_vertex_buffer( + 1, + self.resources + .get_buffer(&mesh.normal_buffer_index) + .slice(..), + ); + render_pass.set_vertex_buffer( + 2, + self.resources.get_buffer(&mesh.uv_buffer_index).slice(..), + ); + + render_pass.set_index_buffer( + self.resources + .get_buffer(&mesh.index_buffer_index) + .slice(..), + mesh.index_format, + ); + + render_pass.draw_indexed(0..mesh.index_count, 0, 0..mesh.instance_count); + } } self.queue.submit(std::iter::once(encoder.finish())); surface_texture.present(); } - pub fn handle_event(&mut self, event: WindowEvent) { + pub async fn handle_event(renderer: Rc>, event: WindowEvent) { match event { - WindowEvent::PointerMove(msg) => self.mouse_move(msg), - WindowEvent::Resize(msg) => self.resize(msg), - WindowEvent::PointerClick(msg) => self.mouse_click(msg), + WindowEvent::PointerMove(msg) => { + renderer.borrow_mut().mouse_move(msg); + } + WindowEvent::Resize(msg) => { + renderer.borrow_mut().resize(msg); + } + WindowEvent::PointerClick(msg) => { + { + let mut r = renderer.borrow_mut(); + let x = (msg.offset_x * msg.scale_factor) as f32; + let y = (msg.offset_y * msg.scale_factor) as f32; + r.scene.frame_metadata.mouse_click = [x, y]; + log::info!("clicked"); + } + if let Err(e) = Self::load_assets_async(renderer.clone()).await { + log::error!("failed to load gltf: {e}"); + } + } + WindowEvent::PointerWheel(msg) => { + let mut r = renderer.borrow_mut(); + r.scene.cam.zoom(&msg); + } } } pub fn run_render_loop(renderer: Rc>) { let render_frame: Closure = Closure::new(move |time: f32| { { - let mut r = renderer.borrow_mut(); + let event = { renderer.borrow_mut().events_chan.try_recv() }; - if let Ok(event) = r.events_chan.try_recv() { - r.handle_event(event); + if let Ok(event) = event { + let renderer_clone = renderer.clone(); + spawn_local(async move { + Self::handle_event(renderer_clone, event).await; + }); } + } + { + let mut r = renderer.borrow_mut(); r.render(time); } @@ -302,9 +499,9 @@ impl Renderer { self.surface_config.width = new_width; self.surface_config.height = new_height; self.surface.configure(&self.device, &self.surface_config); + self.recreate_depth_texture(); - // Update uniform data - self.uniform_data.resolution = [new_width as f32, new_height as f32]; + self.scene.frame_metadata.resolution = [new_width as f32, new_height as f32]; info!( "Resized: ({}, {}), scale: {}", @@ -314,79 +511,86 @@ impl Renderer { } pub fn mouse_move(&mut self, msg: MouseMessage) { - // Update uniform data let x = (msg.offset_x * msg.scale_factor) as f32; let y = (msg.offset_y * msg.scale_factor) as f32; - self.uniform_data.mouse_move = [x, y]; - } + self.scene.frame_metadata.mouse_move = [x, y]; - pub fn mouse_click(&mut self, msg: MouseMessage) { - info!("clicked"); - // Update uniform data - let x = (msg.offset_x * msg.scale_factor) as f32; - let y = (msg.offset_y * msg.scale_factor) as f32; - self.uniform_data.mouse_click = [x, y]; - } -} - -/// Simple vertex format. -#[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - pos: [f32; 3], - color: [f32; 3], -} - -impl Vertex { - fn layout() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - // pos - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - // color - offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x3, - }, - ], + if (msg.buttons & 0x04) != 0 { + let delta_x = (msg.movement_x * msg.scale_factor) as f32; + let delta_y = (msg.movement_y * msg.scale_factor) as f32; + self.scene.cam.orbit(delta_x, delta_y); } } + + // currently this replaces everything, will need more sophisticated mechanisms later + pub async fn load_assets_async(renderer: Rc>) -> Result<(), ImportError> { + let (device, surface_format, bind_group_layout) = { + let r = renderer.borrow(); + ( + r.device.clone(), + r.surface_config.format, + r.scene.bind_group_layout.clone(), + ) + }; + + let mut meshes = Vec::new(); + + let mut original_resources = { + let mut r = renderer.borrow_mut(); + r.scene.meshes.clear(); + std::mem::take(&mut r.resources) + }; + + original_resources.set_bind_group_layouts(&bind_group_layout); + + let bounds = load_gltf_model( + &device, + &mut original_resources, + &mut meshes, + surface_format, + ) + .await?; + + { + let mut r = renderer.borrow_mut(); + r.resources = original_resources; + r.scene.meshes = meshes; + + if let Some(ModelBounds { min, max }) = bounds { + let center = ultraviolet::Vec3::new( + (min[0] + max[0]) * 0.5, + (min[1] + max[1]) * 0.5, + (min[2] + max[2]) * 0.5, + ); + + let extent = + ultraviolet::Vec3::new(max[0] - min[0], max[1] - min[1], max[2] - min[2]); + let radius = + 0.5 * (extent.x * extent.x + extent.y * extent.y + extent.z * extent.z).sqrt(); + let radius = radius.max(1.0); + + // set the camera position after load, so we are not disoriented + let eye_offset = ultraviolet::Vec3::new(0.0, radius * 0.05, radius * 0.25); + + // Keep the near plane proportional to the model size to avoid + // extreme depth ranges when loading very large assets + let near_plane = (radius * 0.001).max(0.1); + + // The far plane must be far enough to cover the entire model. + // Using a fixed upper clamp caused large models to be clipped + // completely; relying on the model radius instead. + let far_plane = (radius * 4.0).max(near_plane + 1.0); + r.scene.cam.set_depth_range(near_plane, far_plane); + r.scene.cam.look_at(center + eye_offset, center); + } + } + + Ok(()) + } } -/// Vertex example. -const VERTICES: &[Vertex] = &[ - Vertex { - pos: [0.0, 0.5, 0.0], // Top-left - color: [1.0, 0.0, 1.0], // Magenta - }, - Vertex { - pos: [-0.5, -0.5, 0.0], // Bottom-left - color: [0.0, 0.0, 1.0], // Blue - }, - Vertex { - pos: [0.5, -0.5, 0.0], // Top-right - color: [1.0, 1.0, 0.0], // Yellow - }, -]; - -const INDICES: &[u32] = &[0, 1, 2]; // CCW, quad - -/// Simple uniform data. -#[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)] -struct UniformData { - mouse_move: [f32; 2], - mouse_click: [f32; 2], - resolution: [f32; 2], - time: f32, - _padding: f32, +impl From> for u32 { + fn from(value: BufferIndex) -> Self { + value.index + } } - - diff --git a/src/renderer/scene.rs b/src/renderer/scene.rs new file mode 100644 index 0000000..ad3d3ef --- /dev/null +++ b/src/renderer/scene.rs @@ -0,0 +1,340 @@ +use wgpu::util::DeviceExt; + +use crate::{ + camera::Camera, + renderer::{BufferIndex, GpuResources, Index, Normal, Position, UV}, +}; + +pub struct UniformResource { + pub buffer: wgpu::Buffer, + pub bind_group: wgpu::BindGroup, + pub bind_group_layout: wgpu::BindGroupLayout, +} + +/// Simple uniform data. +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)] +pub struct FrameMetadata { + pub mouse_move: [f32; 2], + pub mouse_click: [f32; 2], + pub resolution: [f32; 2], + time: f32, + _padding0: f32, + pub camera_position: [f32; 4], +} + +impl FrameMetadata { + pub fn new(dimension: ultraviolet::Vec2) -> Self { + FrameMetadata { + resolution: dimension.into(), + mouse_move: [std::f32::MIN, std::f32::MIN], + mouse_click: [std::f32::MIN, std::f32::MIN], + _padding0: 0.0, + camera_position: [0.0, 0.0, 0.0, 1.0], + ..Default::default() + } + } + + pub fn set_camera_position(&mut self, position: ultraviolet::Vec3) { + self.camera_position = [position.x, position.y, position.z, 1.0]; + } + + pub fn create_uniform_resource(self, device: &wgpu::Device) -> UniformResource { + let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("frame metadata uniform buffer"), + contents: bytemuck::cast_slice(&[self][..]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Uniform bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Uniform bind group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + UniformResource { + buffer, + bind_group_layout, + bind_group, + } + } +} + +pub struct Mesh { + pub pipeline_index: usize, + pub position_buffer_index: BufferIndex, + pub normal_buffer_index: BufferIndex, + pub uv_buffer_index: BufferIndex, + pub index_buffer_index: BufferIndex, + pub index_format: wgpu::IndexFormat, + pub index_count: u32, + pub instance_count: u32, +} + +type VertexBufferSet = (BufferIndex, BufferIndex, BufferIndex); +type IndexBufferInfo = (BufferIndex, u32, wgpu::IndexFormat); + +pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 3] { + [ + wgpu::VertexBufferLayout { + array_stride: 12, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }], + }, + wgpu::VertexBufferLayout { + array_stride: 12, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 1, + format: wgpu::VertexFormat::Float32x3, + }], + }, + wgpu::VertexBufferLayout { + array_stride: 8, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 2, + format: wgpu::VertexFormat::Float32x2, + }], + }, + ] +} + +pub struct MeshBuilder { + indices: I, + vertices: V, + pipeline: P, + instance_count: u32, +} + +impl MeshBuilder<(), (), ()> { + pub fn new() -> Self { + Self { + indices: (), + vertices: (), + pipeline: (), + instance_count: 1, + } + } +} + +impl

MeshBuilder<(), (), P> { + pub fn with_vertices( + self, + device: &wgpu::Device, + resources: &mut GpuResources, + positions: &[[f32; 3]], + normals: &[[f32; 3]], + uvs: &[[f32; 2]], + ) -> MeshBuilder<(), VertexBufferSet, P> { + let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Mesh Positions"), + contents: bytemuck::cast_slice(positions), + usage: wgpu::BufferUsages::VERTEX, + }); + let normal_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Mesh Normals"), + contents: bytemuck::cast_slice(normals), + usage: wgpu::BufferUsages::VERTEX, + }); + let uv_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Mesh UVs"), + contents: bytemuck::cast_slice(uvs), + usage: wgpu::BufferUsages::VERTEX, + }); + + let position_buffer_index = resources.add_position_buffer(position_buffer); + let normal_buffer_index = resources.add_normal_buffer(normal_buffer); + let uv_buffer_index = resources.add_uv_buffer(uv_buffer); + + MeshBuilder { + vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index), + indices: self.indices, + pipeline: self.pipeline, + instance_count: self.instance_count, + } + } +} + +impl MeshBuilder<(), V, P> { + pub fn with_indices( + self, + device: &wgpu::Device, + resources: &mut GpuResources, + indices: &[u32], + ) -> MeshBuilder { + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Mesh Indices"), + contents: bytemuck::cast_slice(indices), + usage: wgpu::BufferUsages::INDEX, + }); + + let index_buffer_index = resources.add_index_buffer(index_buffer); + + MeshBuilder { + indices: ( + index_buffer_index, + indices.len() as u32, + wgpu::IndexFormat::Uint32, + ), + vertices: self.vertices, + pipeline: self.pipeline, + instance_count: self.instance_count, + } + } +} + +impl MeshBuilder { + pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder { + MeshBuilder { + pipeline: pipeline_index, + indices: self.indices, + vertices: self.vertices, + instance_count: self.instance_count, + } + } +} + +impl MeshBuilder { + pub fn build(self) -> Mesh { + Mesh { + pipeline_index: self.pipeline, + position_buffer_index: (self.vertices).0, + normal_buffer_index: (self.vertices).1, + uv_buffer_index: (self.vertices).2, + index_buffer_index: (self.indices).0, + index_count: (self.indices).1, + index_format: (self.indices).2, + instance_count: self.instance_count, + } + } +} + +/// Simple vertex format. +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vertex { + pos: [f32; 3], + color: [f32; 3], +} + +/// Triangle vertex data. +const VERTICES: &[Vertex] = &[ + Vertex { + pos: [0.0, 0.5, 0.0], + color: [1.0, 0.0, 1.0], // Magenta + }, + Vertex { + pos: [-0.5, -0.5, 0.0], + color: [0.0, 0.0, 1.0], // Blue + }, + Vertex { + pos: [0.5, -0.5, 0.0], + color: [1.0, 1.0, 0.0], // Yellow + }, +]; +const INDICES: &[u32] = &[0, 1, 2]; + +pub struct Scene { + pub uniform_buffers: [wgpu::Buffer; 2], + pub bind_groups: [wgpu::BindGroup; 2], + pub bind_group_layout: [wgpu::BindGroupLayout; 2], + pub frame_metadata: FrameMetadata, + pub cam: Camera, + pub meshes: Vec, +} + +impl Scene { + pub fn new(device: &wgpu::Device, dimension: ultraviolet::Vec2) -> Self { + let cam = Camera::new(dimension.x / dimension.y); + let mut frame_metadata = FrameMetadata::new(dimension); + frame_metadata.set_camera_position(cam.position()); + + let uniform_resource = frame_metadata.create_uniform_resource(device); + let camera_resource = cam.create_uniform_resource(device); + + Scene { + uniform_buffers: [uniform_resource.buffer, camera_resource.buffer], + bind_groups: [uniform_resource.bind_group, camera_resource.bind_group], + bind_group_layout: [ + uniform_resource.bind_group_layout, + camera_resource.bind_group_layout, + ], + frame_metadata, + cam, + meshes: Vec::new(), + } + } + + pub fn create_default_triangle( + &mut self, + device: &wgpu::Device, + resources: &mut GpuResources, + surface_format: wgpu::TextureFormat, + ) { + let positions: Vec<[f32; 3]> = VERTICES.iter().map(|v| v.pos).collect(); + // Colors ride through the "normal" slot because the render path always binds + // three vertex buffers (position, normal, uv) for every mesh. + // todo clean that shit up + let colors: Vec<[f32; 3]> = VERTICES.iter().map(|v| v.color).collect(); + let uvs: &[[f32; 2]] = &[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]]; + + let vertex_layout = mesh_vertex_layout(); + + let pipeline_index = resources.get_or_create_pipeline( + device, + "triangle_colored", + &vertex_layout, + include_str!("../example.wgsl"), + surface_format, + ); + + let mesh = MeshBuilder::new() + .with_vertices(device, resources, &positions, &colors, uvs) + .with_indices(device, resources, INDICES) + .with_pipeline(pipeline_index) + .build(); + + self.meshes.push(mesh); + } + + pub fn update(&mut self, queue: &wgpu::Queue, time: f32) { + self.frame_metadata.time = time * 0.001; + self.frame_metadata.set_camera_position(self.cam.position()); + + queue.write_buffer( + &self.uniform_buffers[0], + 0, + bytemuck::cast_slice(&[self.frame_metadata][..]), + ); + + queue.write_buffer( + &self.uniform_buffers[1], + 0, + bytemuck::cast_slice(&[self.cam.view_proj]), + ); + } +} diff --git a/static/sponza.glb b/static/sponza.glb new file mode 100644 index 0000000..0a78e90 --- /dev/null +++ b/static/sponza.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a6f816f0748adede08ad5b6e0545a4589faaef14b53bbc124e625c65ac0d8e9 +size 275552224 diff --git a/static/vite.svg b/static/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/static/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file