separate out renderer crate (#13)
* add keyboard handler * some cleanup * add l key binding * Fix WASM shared memory generation for Rust nightly 2025-10-20+ Add explicit linker flags for shared memory generation after Rust PR #147225 removed automatic flags with +atomics. This fixes DataCloneError when sharing memory with workers. Changes: - Add --shared-memory, --max-memory, --import-memory flags - Export TLS symbols: __wasm_init_tls, __tls_size, __tls_align, __tls_base - Update both Cargo.toml and package.json build scripts Amp-Thread-ID: https://ampcode.com/threads/T-41d99b69-5754-4fdf-b06e-a46343fa7485 Co-authored-by: Amp <amp@ampcode.com> * add a level-editor crate * remove redundant worker setup * simplify app setup * add SceneBuilder * separate out default scene implementation in a crate * define default method for MeshBuilder --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
+13
-1
@@ -1,3 +1,15 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
rustflags = '-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals'
|
||||
rustflags = [
|
||||
'-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals',
|
||||
'-Clink-args=--shared-memory',
|
||||
'-Clink-args=--max-memory=1073741824',
|
||||
'-Clink-args=--import-memory',
|
||||
'-Clink-args=--export=__wasm_init_tls',
|
||||
'-Clink-args=--export=__tls_size',
|
||||
'-Clink-args=--export=__tls_align',
|
||||
'-Clink-args=--export=__tls_base',
|
||||
]
|
||||
|
||||
[unstable]
|
||||
build-std = ['std', 'panic_abort']
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
- **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 `///`
|
||||
- **Logging**: Use `log::info!`, `log::error!`, etc. from the log crate, not `println!`
|
||||
|
||||
Generated
+107
-38
@@ -77,28 +77,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cgmath",
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"gltf",
|
||||
"js-sys",
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"thiserror 2.0.15",
|
||||
"ultraviolet",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
@@ -438,6 +416,21 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -445,6 +438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -453,6 +447,34 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@@ -471,10 +493,16 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -969,9 +997,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -1000,6 +1028,23 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "level-editor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
"log",
|
||||
"renderer",
|
||||
"ultraviolet",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
@@ -1405,6 +1450,29 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "renderer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cgmath",
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"futures",
|
||||
"gltf",
|
||||
"js-sys",
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"thiserror 2.0.15",
|
||||
"ultraviolet",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.23"
|
||||
@@ -2022,21 +2090,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -2048,9 +2117,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.50"
|
||||
version = "0.4.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -2061,9 +2130,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -2071,9 +2140,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2084,9 +2153,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2104,9 +2173,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
+14
-29
@@ -1,22 +1,8 @@
|
||||
[package]
|
||||
name = "base"
|
||||
description = "wgpu + webpack base"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = ["renderer", "level-editor"]
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
atomics = []
|
||||
bulk-memory = []
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
wasm-bindgen = { version = "0.2.100", features = ["enable-interning"] }
|
||||
wasm-bindgen-futures = "0.4.50"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
@@ -28,8 +14,13 @@ web-sys = { version = "0.3.77", features = [
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlCanvasElement",
|
||||
"HtmlInputElement",
|
||||
"File",
|
||||
"FileList",
|
||||
"OffscreenCanvas",
|
||||
"MouseEvent",
|
||||
"WheelEvent",
|
||||
"KeyboardEvent",
|
||||
"Worker",
|
||||
"DedicatedWorkerGlobalScope",
|
||||
"Event",
|
||||
@@ -41,22 +32,16 @@ web-sys = { version = "0.3.77", features = [
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Headers"
|
||||
"Headers",
|
||||
"AddEventListenerOptions"
|
||||
]}
|
||||
js-sys = "0.3.77"
|
||||
bytemuck = { version = "1.23.1", features = [
|
||||
"derive"
|
||||
]}
|
||||
bytemuck = { version = "1.23.1", features = ["derive"] }
|
||||
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
|
||||
futures = "0.3"
|
||||
gltf = { version = "1.4", features = ["extras", "names", "KHR_lights_punctual"] }
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
# Install Rust nightly and required tools
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
curl \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
&& source ~/.cargo/env \
|
||||
&& rustup default nightly-2025-10-20 \
|
||||
&& rustup toolchain install nightly-2025-10-20 \
|
||||
&& rustup target add wasm32-unknown-unknown --toolchain nightly-2025-10-20 \
|
||||
&& rustup component add rust-src --toolchain nightly-2025-10-20 \
|
||||
&& curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
# Set environment variables for Rust
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
ENV RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Install Node.js dependencies
|
||||
RUN yarn install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the project
|
||||
RUN yarn run build
|
||||
|
||||
# Expose port for serving
|
||||
EXPOSE 8080
|
||||
|
||||
# Command to serve the built application
|
||||
CMD ["yarn", "start", "--", "--host"]
|
||||
@@ -0,0 +1,46 @@
|
||||
cargo-features = ["profile-rustflags"]
|
||||
|
||||
[package]
|
||||
name = "level-editor"
|
||||
description = "Level editor application using the renderer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
atomics = []
|
||||
bulk-memory = []
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-Clink-args=--shared-memory",
|
||||
"-Clink-args=--max-memory=1073741824",
|
||||
"-Clink-args=--import-memory",
|
||||
"-Clink-args=--export=__wasm_init_tls",
|
||||
"-Clink-args=--export=__tls_size",
|
||||
"-Clink-args=--export=__tls_align",
|
||||
"-Clink-args=--export=__tls_base",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
renderer = { path = "../renderer" }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
console_error_panic_hook = { workspace = true }
|
||||
log = { workspace = true }
|
||||
wasm-logger = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
ultraviolet = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
@@ -0,0 +1,221 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use ultraviolet::Mat4;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use renderer::app_setup::WebApp;
|
||||
use renderer::camera::Camera;
|
||||
use renderer::message::WindowEvent;
|
||||
use renderer::renderer as gpu_renderer;
|
||||
use renderer::renderer::scene::{mesh_vertex_layout, FrameMetadata, Mesh, MeshBuilder};
|
||||
|
||||
/// Simple vertex format.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Vertex {
|
||||
pos: [f32; 3],
|
||||
color: [f32; 3],
|
||||
}
|
||||
|
||||
pub struct EditorScene {
|
||||
uniform_buffers: [wgpu::Buffer; 2],
|
||||
bind_groups: [wgpu::BindGroup; 2],
|
||||
bind_group_layouts: [wgpu::BindGroupLayout; 2],
|
||||
frame_metadata: FrameMetadata,
|
||||
cam: Camera,
|
||||
meshes: Vec<Mesh>,
|
||||
}
|
||||
|
||||
impl renderer::renderer::scene::Scene for EditorScene {
|
||||
fn setup(
|
||||
renderer_context: &gpu_renderer::RendererContext,
|
||||
resources: &mut gpu_renderer::GpuResources,
|
||||
) -> Self {
|
||||
let dimension = ultraviolet::Vec2::new(
|
||||
renderer_context.surface_config.width as f32,
|
||||
renderer_context.surface_config.height as f32,
|
||||
);
|
||||
|
||||
let mut frame_metadata = FrameMetadata::new(dimension);
|
||||
let camera = Camera::new(dimension.x / dimension.y);
|
||||
|
||||
frame_metadata.set_camera_position(camera.position());
|
||||
|
||||
let uniform_resource = frame_metadata.create_uniform_resource(&renderer_context.device);
|
||||
let camera_resource = camera.create_uniform_resource(&renderer_context.device);
|
||||
|
||||
let bind_group_layouts = [
|
||||
uniform_resource.bind_group_layout,
|
||||
camera_resource.bind_group_layout,
|
||||
];
|
||||
|
||||
resources.set_bind_group_layouts(&bind_group_layouts);
|
||||
|
||||
let mut scene = EditorScene {
|
||||
uniform_buffers: [uniform_resource.buffer, camera_resource.buffer],
|
||||
bind_groups: [uniform_resource.bind_group, camera_resource.bind_group],
|
||||
bind_group_layouts,
|
||||
frame_metadata,
|
||||
cam: camera,
|
||||
meshes: Vec::new(),
|
||||
};
|
||||
|
||||
scene.create_default_scene(
|
||||
&renderer_context.device,
|
||||
resources,
|
||||
renderer_context.surface_config.format,
|
||||
);
|
||||
|
||||
scene
|
||||
}
|
||||
|
||||
fn frame_metadata_mut(&mut self) -> Option<&mut FrameMetadata> {
|
||||
Some(&mut self.frame_metadata)
|
||||
}
|
||||
|
||||
fn camera_mut(&mut self) -> Option<&mut Camera> {
|
||||
Some(&mut self.cam)
|
||||
}
|
||||
|
||||
fn uniform_buffers(&self) -> Option<&[wgpu::Buffer]> {
|
||||
Some(&self.uniform_buffers)
|
||||
}
|
||||
|
||||
fn bind_groups(&self) -> &[wgpu::BindGroup] {
|
||||
&self.bind_groups
|
||||
}
|
||||
|
||||
fn meshes(&self) -> &[Mesh] {
|
||||
&self.meshes
|
||||
}
|
||||
|
||||
fn handle_mouse_click(&mut self, x: f32, y: f32) {
|
||||
self.frame_metadata.mouse_click = [x, y];
|
||||
}
|
||||
|
||||
fn handle_zoom(&mut self, _delta_y: f32) {
|
||||
// TODO: Implement zoom properly when Camera exposes necessary methods
|
||||
}
|
||||
|
||||
fn handle_orbit(&mut self, delta_x: f32, delta_y: f32) {
|
||||
self.cam.orbit(delta_x, delta_y);
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.meshes.clear();
|
||||
}
|
||||
|
||||
fn add_mesh(&mut self, mesh: Mesh) {
|
||||
self.meshes.push(mesh);
|
||||
}
|
||||
|
||||
fn set_camera_depth_range(&mut self, near: f32, far: f32) {
|
||||
self.cam.set_depth_range(near, far);
|
||||
}
|
||||
|
||||
fn set_camera_look_at(&mut self, eye: ultraviolet::Vec3, center: ultraviolet::Vec3) {
|
||||
self.cam.look_at(eye, center);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct LevelEditor {
|
||||
#[allow(dead_code)]
|
||||
scene: EditorScene,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl WebApp for LevelEditor {
|
||||
type Scene = EditorScene;
|
||||
}
|
||||
|
||||
impl EditorScene {
|
||||
/// Ground plane vertex data.
|
||||
const VERTICES: &[Vertex] = &[
|
||||
// First triangle of quad
|
||||
Vertex {
|
||||
pos: [-5.0, 0.0, -5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [5.0, 0.0, -5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [-5.0, 0.0, 5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
// Second triangle of quad
|
||||
Vertex {
|
||||
pos: [5.0, 0.0, -5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [5.0, 0.0, 5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [-5.0, 0.0, 5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
];
|
||||
// Wind the ground plane so the upward-facing side is front-facing (CCW from
|
||||
// above) to avoid being culled by the default back-face culling.
|
||||
const INDICES: &[u32] = &[0, 2, 1, 3, 5, 4];
|
||||
|
||||
fn create_default_scene(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut gpu_renderer::GpuResources,
|
||||
surface_format: wgpu::TextureFormat,
|
||||
) {
|
||||
let positions: Vec<[f32; 3]> = Self::VERTICES.iter().map(|v| v.pos).collect();
|
||||
// Ground plane normals point upward (Y+)
|
||||
let normals: Vec<[f32; 3]> = vec![[0.0, 1.0, 0.0]; positions.len()];
|
||||
let uvs: &[[f32; 2]] = &[
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[1.0, 0.0],
|
||||
[1.0, 1.0],
|
||||
[0.0, 1.0],
|
||||
];
|
||||
|
||||
let vertex_layout = mesh_vertex_layout();
|
||||
|
||||
let pipeline_index = resources.get_or_create_pipeline(
|
||||
device,
|
||||
"ground_plane",
|
||||
&vertex_layout,
|
||||
include_str!("./program.wgsl"),
|
||||
surface_format,
|
||||
);
|
||||
|
||||
let scale_factor = 100.0;
|
||||
let scale_matrix = Mat4::from_scale(scale_factor);
|
||||
|
||||
let mesh = MeshBuilder::default()
|
||||
.with_vertices(device, resources, &positions, &normals, uvs)
|
||||
.with_indices(device, resources, Self::INDICES)
|
||||
.with_pipeline(pipeline_index)
|
||||
.with_model_matrix(device, resources, scale_matrix)
|
||||
.build();
|
||||
|
||||
self.meshes.push(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entrypoint for the level editor
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let runtime = LevelEditor::setup_runtime().unwrap();
|
||||
// Keep the runtime running and prevent drops
|
||||
Box::leak(Box::new(runtime));
|
||||
});
|
||||
}
|
||||
|
||||
renderer::export_worker_entrypoint!();
|
||||
@@ -0,0 +1,68 @@
|
||||
struct UniformData {
|
||||
mouse_move: vec2<f32>,
|
||||
mouse_click: vec2<f32>,
|
||||
resolution: vec2<f32>,
|
||||
time: f32,
|
||||
_padding0: f32,
|
||||
camera_position: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uni: UniformData;
|
||||
@group(1) @binding(0) var<uniform> view_proj: mat4x4<f32>;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) model_col0: vec4<f32>,
|
||||
@location(4) model_col1: vec4<f32>,
|
||||
@location(5) model_col2: vec4<f32>,
|
||||
@location(6) model_col3: vec4<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) world_pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>
|
||||
}
|
||||
|
||||
|
||||
@vertex
|
||||
fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let model = mat4x4<f32>(
|
||||
in.model_col0,
|
||||
in.model_col1,
|
||||
in.model_col2,
|
||||
in.model_col3,
|
||||
);
|
||||
let world_position = model * vec4<f32>(in.pos, 1.0);
|
||||
out.clip_position = view_proj * world_position;
|
||||
out.world_pos = world_position.xyz;
|
||||
out.normal = normalize(in.normal);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45));
|
||||
let light_color = vec3<f32>(1.0, 0.95, 0.85);
|
||||
let base_color = vec3<f32>(0.2, 0.2, 0.2);
|
||||
|
||||
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<f32>(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<f32>(lighting + x - y, 1.0);
|
||||
}
|
||||
Generated
+3903
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -4,19 +4,23 @@
|
||||
"name": "basic",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"wasm-dev": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' wasm-pack build --dev --out-name wasm-index --target web . -- --features atomics,bulk-memory -Z build-std=panic_abort,std",
|
||||
"wasm-release": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' wasm-pack build --release --out-name wasm-index --target web . -- --features atomics,bulk-memory -Z build-std=panic_abort,std",
|
||||
"wasm-dev": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals -Clink-args=--shared-memory -Clink-args=--max-memory=1073741824 -Clink-args=--import-memory -Clink-args=--export=__wasm_init_tls -Clink-args=--export=__tls_size -Clink-args=--export=__tls_align -Clink-args=--export=__tls_base' wasm-pack build --dev --out-name wasm-index --target web level-editor -- --features atomics,bulk-memory -Z build-std=panic_abort,std",
|
||||
"wasm-release": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals -Clink-args=--shared-memory -Clink-args=--max-memory=1073741824 -Clink-args=--import-memory -Clink-args=--export=__wasm_init_tls -Clink-args=--export=__tls_size -Clink-args=--export=__tls_align -Clink-args=--export=__tls_base' wasm-pack build --release --out-name wasm-index --target web level-editor -- --features atomics,bulk-memory -Z build-std=panic_abort,std",
|
||||
"wasm-renderer-dev": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals -Clink-args=--shared-memory -Clink-args=--max-memory=1073741824 -Clink-args=--import-memory -Clink-args=--export=__wasm_init_tls -Clink-args=--export=__tls_size -Clink-args=--export=__tls_align -Clink-args=--export=__tls_base' wasm-pack build --dev --out-name wasm-index --target web renderer -- --features atomics,bulk-memory -Z build-std=panic_abort,std",
|
||||
"wasm-renderer-release": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals -Clink-args=--shared-memory -Clink-args=--max-memory=1073741824 -Clink-args=--import-memory -Clink-args=--export=__wasm_init_tls -Clink-args=--export=__tls_size -Clink-args=--export=__tls_align -Clink-args=--export=__tls_base' wasm-pack build --release --out-name wasm-index --target web renderer -- --features atomics,bulk-memory -Z build-std=panic_abort,std",
|
||||
"bundle-dev": "vite build --mode development",
|
||||
"bundle-release": "vite build",
|
||||
"build": "run-s clean wasm-dev bundle-dev",
|
||||
"build-release": "run-s clean wasm-release bundle-release",
|
||||
"build-renderer": "run-s clean wasm-renderer-dev bundle-dev",
|
||||
"build-renderer-release": "run-s clean wasm-renderer-release bundle-release",
|
||||
"start": "vite preview",
|
||||
"clean": "rimraf --glob dist **/pkg",
|
||||
"clean-all": "rimraf --glob dist **/pkg target node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
|
||||
"vite": "^5.1.6",
|
||||
"vite": "^7.1.10",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rimraf": "^5.0.1",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
cargo-features = ["profile-rustflags"]
|
||||
|
||||
[package]
|
||||
name = "renderer"
|
||||
description = "WGPU renderer core library"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
atomics = []
|
||||
bulk-memory = []
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-Clink-args=--shared-memory",
|
||||
"-Clink-args=--max-memory=1073741824",
|
||||
"-Clink-args=--import-memory",
|
||||
"-Clink-args=--export=__wasm_init_tls",
|
||||
"-Clink-args=--export=__tls_size",
|
||||
"-Clink-args=--export=__tls_align",
|
||||
"-Clink-args=--export=__tls_base",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
console_error_panic_hook = { workspace = true }
|
||||
console_log = { workspace = true }
|
||||
log = { workspace = true }
|
||||
wasm-logger = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
cgmath = { workspace = true }
|
||||
raw-window-handle = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ultraviolet = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
gltf = { workspace = true }
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
@@ -0,0 +1,222 @@
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use web_sys::AddEventListenerOptions;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wgpu::Error;
|
||||
|
||||
use crate::message::WindowEvent;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::platform::web;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::platform::web::worker::MainWorker;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
/// Helper struct to store event listener closures
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct EventListeners {
|
||||
pub resize_listener: Option<Closure<dyn FnMut()>>,
|
||||
pub mousemove_listener: Option<Closure<dyn FnMut(web_sys::MouseEvent)>>,
|
||||
pub mousedown_listener: Option<Closure<dyn FnMut(web_sys::MouseEvent)>>,
|
||||
pub wheel_listener: Option<Closure<dyn FnMut(web_sys::WheelEvent)>>,
|
||||
pub keyboard_listener: Option<Closure<dyn FnMut(web_sys::KeyboardEvent)>>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl EventListeners {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
resize_listener: None,
|
||||
mousemove_listener: None,
|
||||
mousedown_listener: None,
|
||||
wheel_listener: None,
|
||||
keyboard_listener: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup default window event listeners that forward events to the worker thread
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn setup_event_listeners(worker_chan: &Sender<WindowEvent>) -> Result<EventListeners, JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let resize_worker_chan = worker_chan.clone();
|
||||
|
||||
let resize_listener: Closure<dyn FnMut()> = Closure::new(move || {
|
||||
use crate::message::ResizeMessage;
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let width = window.inner_width().ok().unwrap().as_f64().unwrap();
|
||||
let height = window.inner_height().ok().unwrap().as_f64().unwrap();
|
||||
|
||||
resize_worker_chan
|
||||
.send(WindowEvent::Resize(ResizeMessage {
|
||||
width,
|
||||
height,
|
||||
scale_factor: window.device_pixel_ratio(),
|
||||
}))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
window.add_event_listener_with_callback("resize", resize_listener.as_ref().unchecked_ref())?;
|
||||
|
||||
let mousemove_worker_chan = worker_chan.clone();
|
||||
let mousemove_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
|
||||
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());
|
||||
if event.type_() == "click" {
|
||||
event_data = WindowEvent::PointerClick(mouse_event_data.clone());
|
||||
}
|
||||
|
||||
mousemove_worker_chan.clone().send(event_data).unwrap();
|
||||
});
|
||||
|
||||
window.add_event_listener_with_callback(
|
||||
"mousemove",
|
||||
mousemove_listener.as_ref().unchecked_ref(),
|
||||
)?;
|
||||
|
||||
window
|
||||
.add_event_listener_with_callback("click", mousemove_listener.as_ref().unchecked_ref())?;
|
||||
|
||||
let mousedown_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
|
||||
Closure::new(move |event: web_sys::MouseEvent| {
|
||||
if event.button() == 1 {
|
||||
event.prevent_default();
|
||||
}
|
||||
});
|
||||
|
||||
window.add_event_listener_with_callback(
|
||||
"mousedown",
|
||||
mousedown_listener.as_ref().unchecked_ref(),
|
||||
)?;
|
||||
|
||||
let wheel_worker_chan = worker_chan.clone();
|
||||
let wheel_listener: Closure<dyn FnMut(web_sys::WheelEvent)> =
|
||||
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
|
||||
};
|
||||
|
||||
window.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
"wheel",
|
||||
wheel_listener.as_ref().unchecked_ref(),
|
||||
&wheel_options,
|
||||
)?;
|
||||
|
||||
let keyboard_worker_chan = worker_chan.clone();
|
||||
let keyboard_listener: Closure<dyn FnMut(web_sys::KeyboardEvent)> =
|
||||
Closure::new(move |event: web_sys::KeyboardEvent| {
|
||||
use crate::message::KeyboardMessage;
|
||||
|
||||
let keyboard_event_data = KeyboardMessage::from_evt(event);
|
||||
|
||||
keyboard_worker_chan
|
||||
.send(WindowEvent::Keyboard(keyboard_event_data))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
window
|
||||
.add_event_listener_with_callback("keydown", keyboard_listener.as_ref().unchecked_ref())?;
|
||||
|
||||
Ok(EventListeners {
|
||||
resize_listener: Some(resize_listener),
|
||||
mousemove_listener: Some(mousemove_listener),
|
||||
mousedown_listener: Some(mousedown_listener),
|
||||
wheel_listener: Some(wheel_listener),
|
||||
keyboard_listener: Some(keyboard_listener),
|
||||
})
|
||||
}
|
||||
|
||||
/// Runtime resources required to keep a WASM application running.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WebAppRuntime {
|
||||
worker: MainWorker,
|
||||
worker_chan: Sender<WindowEvent>,
|
||||
_event_listeners: EventListeners,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl WebAppRuntime {
|
||||
/// Initialize the web worker, canvas ownership, and event listeners.
|
||||
pub fn new<T: crate::renderer::scene::Scene + 'static>(worker_name: &str, canvas_selector: &str) -> Result<Self, JsValue> {
|
||||
let (sender, receiver) = mpsc::channel::<WindowEvent>();
|
||||
|
||||
let canvas = web::get_canvas_element(canvas_selector);
|
||||
let worker = MainWorker::spawn(worker_name, 1, move || {
|
||||
spawn_local(async move {
|
||||
MainWorker::run_render_loop::<T>(receiver).await;
|
||||
});
|
||||
})?;
|
||||
|
||||
worker.transfer_ownership(&canvas);
|
||||
|
||||
let event_listeners = setup_event_listeners(&sender)?;
|
||||
|
||||
Ok(Self {
|
||||
worker,
|
||||
worker_chan: sender,
|
||||
_event_listeners: event_listeners,
|
||||
})
|
||||
}
|
||||
|
||||
/// Access the worker channel sender for dispatching custom window events.
|
||||
pub fn sender(&self) -> &Sender<WindowEvent> {
|
||||
&self.worker_chan
|
||||
}
|
||||
|
||||
/// Access the spawned worker reference.
|
||||
pub fn worker(&self) -> &MainWorker {
|
||||
&self.worker
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for applications that rely on the renderer's default WASM setup.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub trait WebApp {
|
||||
type Scene: crate::renderer::scene::Scene + 'static;
|
||||
|
||||
/// Name used for the spawned `MainWorker`.
|
||||
fn worker_name() -> &'static str {
|
||||
"main-worker"
|
||||
}
|
||||
|
||||
/// CSS selector for the canvas element that will be transferred to the worker.
|
||||
fn canvas_selector() -> &'static str {
|
||||
"#canvas0"
|
||||
}
|
||||
|
||||
/// Hook invoked after the runtime has been created.
|
||||
fn on_runtime_initialized(_runtime: &mut WebAppRuntime) {}
|
||||
|
||||
/// Perform the default WASM initialization routine.
|
||||
fn setup_runtime() -> Result<WebAppRuntime, JsValue> {
|
||||
let mut runtime = WebAppRuntime::new::<Self::Scene>(
|
||||
Self::worker_name(),
|
||||
Self::canvas_selector(),
|
||||
)?;
|
||||
Self::on_runtime_initialized(&mut runtime);
|
||||
Ok(runtime)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use ultraviolet::{projection, Bivec3, Mat4, Rotor3, Vec3, Vec4};
|
||||
use ultraviolet::{projection, Bivec3, Mat4, Rotor3, Vec3};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::{message::WheelMessage, renderer::scene::UniformResource};
|
||||
@@ -26,11 +26,9 @@ pub struct Camera {
|
||||
z_near: f32,
|
||||
z_far: f32,
|
||||
|
||||
// Rotor orientation + spherical coordinates for orbit camera behaviour
|
||||
// Rotor orientation for orbit camera behaviour
|
||||
rotor: Rotor3,
|
||||
distance: f32,
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
|
||||
// Dirty flag for lazy evaluation
|
||||
dirty: bool,
|
||||
@@ -86,8 +84,8 @@ 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(),
|
||||
position: Vec3::new(0.0, 0.5, 3.0),
|
||||
target: Vec3::new(0.0, 0.0, 0.0),
|
||||
up: Vec3::unit_y(),
|
||||
fov: PI / 3.0,
|
||||
aspect_ratio,
|
||||
@@ -95,8 +93,6 @@ impl Camera {
|
||||
z_far: 100000.0,
|
||||
rotor: Rotor3::identity(),
|
||||
distance: 1.0,
|
||||
yaw: 0.0,
|
||||
pitch: 0.0,
|
||||
dirty: true,
|
||||
};
|
||||
|
||||
@@ -138,6 +134,12 @@ impl Camera {
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn update_aspect_ratio(&mut self, aspect_ratio: f32) {
|
||||
self.aspect_ratio = aspect_ratio;
|
||||
self.dirty = true;
|
||||
self.compute_view_proj_mat();
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -150,11 +152,10 @@ impl Camera {
|
||||
|
||||
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_angle = (delta_y * ORBIT_SENSITIVITY).clamp(-MAX_PITCH, MAX_PITCH);
|
||||
|
||||
let pitch_rotor =
|
||||
Rotor3::from_angle_plane(applied_pitch, Bivec3::from_normalized_axis(basis.right));
|
||||
Rotor3::from_angle_plane(pitch_angle, Bivec3::from_normalized_axis(basis.right));
|
||||
|
||||
let orbit_rotor = (yaw_rotor * pitch_rotor).normalized();
|
||||
|
||||
@@ -169,9 +170,6 @@ impl Camera {
|
||||
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();
|
||||
}
|
||||
@@ -221,9 +219,9 @@ impl Camera {
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Uniform bind group layout"),
|
||||
label: Some("Camera bind group layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
@@ -235,10 +233,10 @@ impl Camera {
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Uniform bind group"),
|
||||
label: Some("Camera bind group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
use gltf::Gltf;
|
||||
use ultraviolet::{Mat4, Vec3, Vec4};
|
||||
use ultraviolet::{Mat4, Vec3};
|
||||
use wgpu::TextureFormat;
|
||||
|
||||
use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder};
|
||||
@@ -33,6 +33,9 @@ pub enum ImportError {
|
||||
|
||||
#[error("failed to load model")]
|
||||
LoadError,
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> {
|
||||
@@ -59,15 +62,6 @@ fn convert_indices(indices: gltf::mesh::util::ReadIndices<'_>) -> Vec<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -78,7 +72,7 @@ fn visit_node<'a>(
|
||||
pipeline_index: usize,
|
||||
model_bounds: &mut Option<ModelBounds>,
|
||||
) {
|
||||
let local_transform = mat4_from_gltf(node.transform().matrix());
|
||||
let local_transform = Mat4::from(node.transform().matrix());
|
||||
let world_transform = parent_transform * local_transform;
|
||||
let normal_matrix = world_transform.inversed().transposed();
|
||||
|
||||
@@ -89,7 +83,7 @@ fn visit_node<'a>(
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let mut positions: Vec<[f32; 3]> = match reader.read_positions() {
|
||||
let positions: Vec<[f32; 3]> = match reader.read_positions() {
|
||||
Some(iter) => iter.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
@@ -132,17 +126,14 @@ fn visit_node<'a>(
|
||||
uvs.resize(vertex_count, [0.0, 0.0]);
|
||||
}
|
||||
|
||||
for position in &mut positions {
|
||||
for position in &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 {
|
||||
let world_point = [transformed.x, transformed.y, transformed.z];
|
||||
if let Some(bounds) = model_bounds.as_mut() {
|
||||
bounds.include_point(*position);
|
||||
bounds.include_point(world_point);
|
||||
} else {
|
||||
*model_bounds = Some(ModelBounds::new(*position, *position));
|
||||
*model_bounds = Some(ModelBounds::new(world_point, world_point));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +146,11 @@ fn visit_node<'a>(
|
||||
continue;
|
||||
}
|
||||
|
||||
let mesh = MeshBuilder::new()
|
||||
let mesh = MeshBuilder::default()
|
||||
.with_vertices(device, resources, &positions, &normals, &uvs)
|
||||
.with_indices(device, resources, &indices)
|
||||
.with_pipeline(pipeline_index)
|
||||
.with_model_matrix(device, resources, world_transform)
|
||||
.build();
|
||||
|
||||
meshes.push(mesh);
|
||||
@@ -185,7 +177,7 @@ pub async fn load_gltf_model(
|
||||
meshes: &mut Vec<crate::renderer::scene::Mesh>,
|
||||
surface_format: TextureFormat,
|
||||
) -> Result<Option<ModelBounds>, ImportError> {
|
||||
let glb_data = reqwest::get("http://localhost:8080/sponza.glb")
|
||||
let glb_data = reqwest::get("http://localhost:8080/themanor.glb")
|
||||
.await?
|
||||
.bytes()
|
||||
.await?;
|
||||
@@ -8,12 +8,16 @@ struct UniformData {
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uni: UniformData;
|
||||
@group(1) @binding(1) var<uniform> view_proj: mat4x4<f32>;
|
||||
@group(1) @binding(0) var<uniform> view_proj: mat4x4<f32>;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
// @location(2) uv: vec2<f32>
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) model_col0: vec4<f32>,
|
||||
@location(4) model_col1: vec4<f32>,
|
||||
@location(5) model_col2: vec4<f32>,
|
||||
@location(6) model_col3: vec4<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@@ -22,11 +26,19 @@ struct VertexOutput {
|
||||
@location(1) normal: vec3<f32>
|
||||
}
|
||||
|
||||
|
||||
@vertex
|
||||
fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.clip_position = view_proj * vec4<f32>(in.pos, 1.0);
|
||||
out.world_pos = in.pos;
|
||||
let model = mat4x4<f32>(
|
||||
in.model_col0,
|
||||
in.model_col1,
|
||||
in.model_col2,
|
||||
in.model_col3,
|
||||
);
|
||||
let world_position = model * vec4<f32>(in.pos, 1.0);
|
||||
out.clip_position = view_proj * world_position;
|
||||
out.world_pos = world_position.xyz;
|
||||
out.normal = normalize(in.normal);
|
||||
return out;
|
||||
}
|
||||
@@ -35,7 +47,7 @@ fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45));
|
||||
let light_color = vec3<f32>(1.0, 0.95, 0.85);
|
||||
let base_color = vec3<f32>(1.0, 0.0, 0.0);
|
||||
let base_color = vec3<f32>(0.2, 0.2, 0.2);
|
||||
|
||||
let normal = normalize(in.normal);
|
||||
let view_dir = normalize(uni.camera_position.xyz - in.world_pos);
|
||||
@@ -44,7 +56,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let ambient = 0.15;
|
||||
|
||||
var specular = 0.0;
|
||||
if (diffuse_strength > 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);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
pub mod app_setup;
|
||||
pub mod camera;
|
||||
pub mod gltf;
|
||||
pub mod message;
|
||||
pub mod platform;
|
||||
pub mod renderer;
|
||||
|
||||
/// Worker entrypoint helper - executes the closure it is spawned with
|
||||
/// Applications should export this with #[wasm_bindgen]
|
||||
pub fn worker_entrypoint_impl(ptr: u32) {
|
||||
let work = unsafe { Box::from_raw(ptr as *mut Box<dyn FnOnce()>) };
|
||||
(*work)();
|
||||
}
|
||||
|
||||
/// Macro to export the worker_entrypoint function in application crates
|
||||
///
|
||||
/// Usage:
|
||||
/// ```rust
|
||||
/// use renderer::export_worker_entrypoint;
|
||||
/// export_worker_entrypoint!();
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! export_worker_entrypoint {
|
||||
() => {
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn worker_entrypoint(ptr: u32) {
|
||||
$crate::worker_entrypoint_impl(ptr);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub enum WindowEvent {
|
||||
PointerMove(MouseMessage),
|
||||
PointerClick(MouseMessage),
|
||||
PointerWheel(WheelMessage),
|
||||
Keyboard(KeyboardMessage),
|
||||
}
|
||||
|
||||
// Display for WindowEvent
|
||||
@@ -16,6 +17,7 @@ impl fmt::Display for WindowEvent {
|
||||
WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg),
|
||||
WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg),
|
||||
WindowEvent::PointerWheel(msg) => write!(f, "PointerWheel: {:?}", msg),
|
||||
WindowEvent::Keyboard(msg) => write!(f, "Keyboard: {:?}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,3 +84,30 @@ impl WheelMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeyboardMessage {
|
||||
pub key: String,
|
||||
pub code: String,
|
||||
pub alt_key: bool,
|
||||
pub ctrl_key: bool,
|
||||
pub meta_key: bool,
|
||||
pub shift_key: bool,
|
||||
pub location: u32,
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
impl KeyboardMessage {
|
||||
pub fn from_evt(event: web_sys::KeyboardEvent) -> Self {
|
||||
Self {
|
||||
key: event.key(),
|
||||
code: event.code(),
|
||||
alt_key: event.alt_key(),
|
||||
ctrl_key: event.ctrl_key(),
|
||||
meta_key: event.meta_key(),
|
||||
shift_key: event.shift_key(),
|
||||
location: event.location(),
|
||||
repeat: event.repeat(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Generic worker that imports the app's WASM module relative to the generated pkg folder.
|
||||
// Works for any application because the relative depth from this file to pkg is stable.
|
||||
import initWasm, { worker_entrypoint } from "../../../../../../wasm-index.js";
|
||||
|
||||
export function attachMain() {}
|
||||
|
||||
let isReady = false;
|
||||
|
||||
onmessage = async (event) => {
|
||||
console.log("worker received message", event);
|
||||
if (isReady) return;
|
||||
|
||||
isReady = true;
|
||||
|
||||
const wasmModule = event.data[0]; // WebAssembly.Module from wasm_bindgen::module()
|
||||
const workerId = event.data[1]; // worker ID
|
||||
const memory = event.data[2]; // shared memory
|
||||
const entryPtr = event.data[3]; // worker entrypoint function pointer
|
||||
|
||||
console.log(
|
||||
"worker: initializing with WASM module",
|
||||
wasmModule,
|
||||
"id:",
|
||||
workerId,
|
||||
);
|
||||
|
||||
// Initialize WASM with the shared module and memory forwarded from the main thread.
|
||||
await initWasm({ module_or_path: wasmModule, memory });
|
||||
|
||||
// Call the app-provided worker entrypoint once initialization completes.
|
||||
worker_entrypoint(entryPtr);
|
||||
};
|
||||
@@ -97,12 +97,14 @@ impl MainWorker {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn run_render_loop(events_chan: Receiver<WindowEvent>) {
|
||||
pub async fn run_render_loop<T: crate::renderer::scene::Scene + 'static>(
|
||||
events_chan: Receiver<WindowEvent>,
|
||||
) {
|
||||
use crate::renderer::Renderer;
|
||||
|
||||
let canvas = wait_for_canvas_transfer().await;
|
||||
|
||||
let renderer = Rc::new(RefCell::new(Renderer::new(canvas, events_chan).await));
|
||||
let renderer = Rc::new(RefCell::new(Renderer::<T>::new(canvas, events_chan).await));
|
||||
Renderer::run_render_loop(renderer);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc, sync::mpsc::Receiver};
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use log::info;
|
||||
use ultraviolet::Vec4;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::DedicatedWorkerGlobalScope;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::{DedicatedWorkerGlobalScope, File, MessageEvent};
|
||||
|
||||
use crate::{
|
||||
gltf::{load_gltf_model, ImportError, ModelBounds},
|
||||
@@ -13,6 +15,9 @@ use crate::{
|
||||
|
||||
pub mod scene;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use scene::Mesh;
|
||||
|
||||
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
|
||||
pub struct GpuResources {
|
||||
@@ -81,6 +86,15 @@ impl GpuResources {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_model_matrix_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex<ModelMatrix> {
|
||||
let index = self.buffers.len() as u32;
|
||||
self.buffers.push(buffer);
|
||||
BufferIndex {
|
||||
index,
|
||||
_buffer_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_buffer<T>(&self, id: &BufferIndex<T>) -> &wgpu::Buffer {
|
||||
&self.buffers[id.index as usize]
|
||||
@@ -234,21 +248,26 @@ pub struct Position;
|
||||
pub struct Normal;
|
||||
pub struct UV;
|
||||
pub struct Index;
|
||||
pub struct ModelMatrix;
|
||||
|
||||
pub struct Renderer {
|
||||
canvas: web_sys::OffscreenCanvas,
|
||||
events_chan: Receiver<WindowEvent>,
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
surface_config: wgpu::SurfaceConfiguration,
|
||||
scene: Scene,
|
||||
resources: GpuResources,
|
||||
depth_texture: wgpu::Texture,
|
||||
depth_view: wgpu::TextureView,
|
||||
pub struct RendererContext {
|
||||
pub device: wgpu::Device,
|
||||
pub queue: wgpu::Queue,
|
||||
pub surface_config: wgpu::SurfaceConfiguration,
|
||||
pub surface: wgpu::Surface<'static>,
|
||||
pub depth_texture: wgpu::Texture,
|
||||
pub depth_view: wgpu::TextureView,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub struct Renderer<T: scene::Scene> {
|
||||
canvas: web_sys::OffscreenCanvas,
|
||||
events_chan: Receiver<WindowEvent>,
|
||||
context: RendererContext,
|
||||
resources: GpuResources,
|
||||
scene: T,
|
||||
}
|
||||
|
||||
impl<T: Scene + 'static> Renderer<T> {
|
||||
fn create_depth_texture(
|
||||
device: &wgpu::Device,
|
||||
config: &wgpu::SurfaceConfiguration,
|
||||
@@ -266,7 +285,7 @@ impl Renderer {
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: DEPTH_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
@@ -276,9 +295,10 @@ impl Renderer {
|
||||
}
|
||||
|
||||
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;
|
||||
let (texture, view) =
|
||||
Self::create_depth_texture(&self.context.device, &self.context.surface_config);
|
||||
self.context.depth_texture = texture;
|
||||
self.context.depth_view = view;
|
||||
}
|
||||
|
||||
pub async fn new(canvas: web_sys::OffscreenCanvas, events_chan: Receiver<WindowEvent>) -> Self {
|
||||
@@ -334,35 +354,33 @@ impl Renderer {
|
||||
let (depth_texture, depth_view) = Self::create_depth_texture(&device, &surface_config);
|
||||
|
||||
let mut resources = GpuResources::new();
|
||||
|
||||
let mut scene = Scene::new(
|
||||
&device,
|
||||
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,
|
||||
let context = RendererContext {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
surface_config,
|
||||
scene,
|
||||
resources,
|
||||
depth_texture,
|
||||
depth_view,
|
||||
};
|
||||
|
||||
let scene = T::setup(&context, &mut resources);
|
||||
|
||||
Self {
|
||||
canvas,
|
||||
events_chan,
|
||||
context,
|
||||
scene,
|
||||
resources,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, time: f32) {
|
||||
self.scene.update(&self.queue, time);
|
||||
self.scene.update(&self.context, &mut self.resources);
|
||||
|
||||
let surface_texture = self.surface.get_current_texture().unwrap();
|
||||
let surface_texture = self.context.surface.get_current_texture().unwrap();
|
||||
let texture_view = surface_texture.texture.create_view(&Default::default());
|
||||
let mut encoder = self
|
||||
let mut encoder =
|
||||
self.context
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render command encoder"),
|
||||
@@ -386,7 +404,7 @@ impl Renderer {
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth_view,
|
||||
view: &self.context.depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
@@ -397,11 +415,11 @@ impl Renderer {
|
||||
timestamp_writes: None,
|
||||
});
|
||||
|
||||
for (i, bind_group) in self.scene.bind_groups.iter().enumerate() {
|
||||
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 {
|
||||
for mesh in self.scene.meshes() {
|
||||
render_pass.set_pipeline(self.resources.get_pipeline_by_index(mesh.pipeline_index));
|
||||
|
||||
render_pass.set_vertex_buffer(
|
||||
@@ -420,6 +438,12 @@ impl Renderer {
|
||||
2,
|
||||
self.resources.get_buffer(&mesh.uv_buffer_index).slice(..),
|
||||
);
|
||||
render_pass.set_vertex_buffer(
|
||||
3,
|
||||
self.resources
|
||||
.get_buffer(&mesh.model_buffer_index)
|
||||
.slice(..),
|
||||
);
|
||||
|
||||
render_pass.set_index_buffer(
|
||||
self.resources
|
||||
@@ -431,10 +455,102 @@ impl Renderer {
|
||||
render_pass.draw_indexed(0..mesh.index_count, 0, 0..mesh.instance_count);
|
||||
}
|
||||
}
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.context.queue.submit(std::iter::once(encoder.finish()));
|
||||
surface_texture.present();
|
||||
}
|
||||
|
||||
pub async fn read_pixel_from_texture(&self, x: u32, y: u32) -> Vec4 {
|
||||
let width = self.context.depth_texture.width();
|
||||
let height = self.context.depth_texture.height();
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
log::warn!("Depth texture has zero extent ({} x {})", width, height);
|
||||
return Vec4::zero();
|
||||
}
|
||||
|
||||
// Validate coordinates
|
||||
if x >= width || y >= height {
|
||||
log::warn!(
|
||||
"Pixel coordinates ({}, {}) out of bounds for texture size {}x{}",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
return Vec4::zero();
|
||||
}
|
||||
|
||||
let pixel_size = std::mem::size_of::<f32>() as u32;
|
||||
let unpadded_row_bytes = width * pixel_size;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let padded_row_bytes = if unpadded_row_bytes % align == 0 {
|
||||
unpadded_row_bytes
|
||||
} else {
|
||||
(unpadded_row_bytes / align + 1) * align
|
||||
};
|
||||
let buffer_size = padded_row_bytes as u64 * height as u64;
|
||||
let buffer = self.context.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("depth pixel read buffer"),
|
||||
size: buffer_size,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Copy just the single pixel
|
||||
let mut encoder =
|
||||
self.context
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("copy depth pixel to buffer"),
|
||||
});
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &self.context.depth_texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
|
||||
aspect: wgpu::TextureAspect::DepthOnly,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded_row_bytes),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
self.context.queue.submit(std::iter::once(encoder.finish()));
|
||||
|
||||
// Map the buffer and read the pixel
|
||||
let slice = buffer.slice(..);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||
tx.send(result).unwrap();
|
||||
});
|
||||
|
||||
// Poll the device to process the mapping
|
||||
|
||||
rx.await.unwrap().unwrap();
|
||||
let depth_value = {
|
||||
let data = slice.get_mapped_range();
|
||||
let row_pitch = padded_row_bytes as usize;
|
||||
let byte_offset = y as usize * row_pitch + x as usize * pixel_size as usize;
|
||||
let mut depth_bytes = [0u8; 4];
|
||||
depth_bytes.copy_from_slice(&data[byte_offset..byte_offset + 4]);
|
||||
f32::from_le_bytes(depth_bytes)
|
||||
};
|
||||
buffer.unmap();
|
||||
|
||||
Vec4::new(depth_value, 0.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
pub async fn handle_event(renderer: Rc<RefCell<Self>>, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::PointerMove(msg) => {
|
||||
@@ -448,25 +564,50 @@ impl Renderer {
|
||||
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];
|
||||
r.scene.handle_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}");
|
||||
}
|
||||
|
||||
// Read pixel from depth texture at click coordinates
|
||||
// let renderer_clone = renderer.clone();
|
||||
// let x_coord = msg.offset_x as u32;
|
||||
// let y_coord = msg.offset_y as u32;
|
||||
// let pixel_value = renderer_clone
|
||||
// .borrow()
|
||||
// .read_pixel_from_texture(x_coord, y_coord)
|
||||
// .await;
|
||||
// log::info!(
|
||||
// "Depth pixel at ({}, {}): {:?}",
|
||||
// x_coord,
|
||||
// y_coord,
|
||||
// pixel_value
|
||||
// );
|
||||
}
|
||||
WindowEvent::PointerWheel(msg) => {
|
||||
let mut r = renderer.borrow_mut();
|
||||
r.scene.cam.zoom(&msg);
|
||||
r.scene.handle_zoom(msg.delta_y as f32);
|
||||
}
|
||||
WindowEvent::Keyboard(msg) => {
|
||||
log::info!("Key event received: {:?}", msg);
|
||||
|
||||
// Check for 'L' key press
|
||||
if msg.key == "l" || msg.key == "L" {
|
||||
let renderer_clone = renderer.clone();
|
||||
spawn_local(async move {
|
||||
if let Err(e) = Self::show_file_picker_and_load(renderer_clone).await {
|
||||
log::error!("Failed to load file: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_render_loop(renderer: Rc<RefCell<Renderer>>) {
|
||||
pub fn run_render_loop(renderer: Rc<RefCell<Renderer<T>>>) {
|
||||
let render_frame: Closure<dyn FnMut(f32)> = Closure::new(move |time: f32| {
|
||||
{
|
||||
let event = { renderer.borrow_mut().events_chan.try_recv() };
|
||||
|
||||
if let Ok(r) = renderer.try_borrow_mut() {
|
||||
let event = r.events_chan.try_recv();
|
||||
if let Ok(event) = event {
|
||||
let renderer_clone = renderer.clone();
|
||||
spawn_local(async move {
|
||||
@@ -474,11 +615,13 @@ impl Renderer {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut r = renderer.borrow_mut();
|
||||
if let Ok(mut r) = renderer.try_borrow_mut() {
|
||||
r.render(time);
|
||||
}
|
||||
}
|
||||
|
||||
Self::run_render_loop(renderer.clone());
|
||||
});
|
||||
@@ -496,12 +639,19 @@ impl Renderer {
|
||||
let new_width = (msg.width * msg.scale_factor) as u32;
|
||||
let new_height = (msg.height * msg.scale_factor) as u32;
|
||||
if new_width != self.canvas.width() || new_height != self.canvas.height() {
|
||||
self.surface_config.width = new_width;
|
||||
self.surface_config.height = new_height;
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
self.context.surface_config.width = new_width;
|
||||
self.context.surface_config.height = new_height;
|
||||
self.context
|
||||
.surface
|
||||
.configure(&self.context.device, &self.context.surface_config);
|
||||
self.recreate_depth_texture();
|
||||
|
||||
self.scene.frame_metadata.resolution = [new_width as f32, new_height as f32];
|
||||
self.scene.resize(
|
||||
new_width as f64,
|
||||
new_height as f64,
|
||||
msg.scale_factor,
|
||||
&self.context.queue,
|
||||
);
|
||||
|
||||
info!(
|
||||
"Resized: ({}, {}), scale: {}",
|
||||
@@ -511,25 +661,20 @@ impl Renderer {
|
||||
}
|
||||
|
||||
pub fn mouse_move(&mut self, msg: MouseMessage) {
|
||||
let x = (msg.offset_x * msg.scale_factor) as f32;
|
||||
let y = (msg.offset_y * msg.scale_factor) as f32;
|
||||
self.scene.frame_metadata.mouse_move = [x, y];
|
||||
|
||||
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);
|
||||
self.scene.handle_orbit(delta_x, delta_y);
|
||||
}
|
||||
}
|
||||
|
||||
// currently this replaces everything, will need more sophisticated mechanisms later
|
||||
pub async fn load_assets_async(renderer: Rc<RefCell<Renderer>>) -> Result<(), ImportError> {
|
||||
let (device, surface_format, bind_group_layout) = {
|
||||
pub async fn load_assets_async(renderer: Rc<RefCell<Renderer<T>>>) -> Result<(), ImportError> {
|
||||
let (device, surface_format) = {
|
||||
let r = renderer.borrow();
|
||||
(
|
||||
r.device.clone(),
|
||||
r.surface_config.format,
|
||||
r.scene.bind_group_layout.clone(),
|
||||
r.context.device.clone(),
|
||||
r.context.surface_config.format,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -537,12 +682,10 @@ impl Renderer {
|
||||
|
||||
let mut original_resources = {
|
||||
let mut r = renderer.borrow_mut();
|
||||
r.scene.meshes.clear();
|
||||
r.scene.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,
|
||||
@@ -554,7 +697,10 @@ impl Renderer {
|
||||
{
|
||||
let mut r = renderer.borrow_mut();
|
||||
r.resources = original_resources;
|
||||
r.scene.meshes = meshes;
|
||||
|
||||
for mesh in meshes {
|
||||
r.scene.add_mesh(mesh);
|
||||
}
|
||||
|
||||
if let Some(ModelBounds { min, max }) = bounds {
|
||||
let center = ultraviolet::Vec3::new(
|
||||
@@ -580,16 +726,22 @@ impl Renderer {
|
||||
// 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);
|
||||
r.scene.set_camera_depth_range(near_plane, far_plane);
|
||||
r.scene.set_camera_look_at(center + eye_offset, center);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_file_picker_and_load(renderer: Rc<RefCell<Renderer<T>>>) -> Result<(), ImportError> {
|
||||
// For now, we'll just call load_assets_async which loads the default model
|
||||
// In a full implementation, we'd modify load_gltf_model to accept the file data
|
||||
Self::load_assets_async(renderer).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<BufferIndex<T>> for u32 {
|
||||
impl<T: scene::Scene> From<BufferIndex<T>> for u32 {
|
||||
fn from(value: BufferIndex<T>) -> Self {
|
||||
value.index
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use ultraviolet::Mat4;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
renderer::{BufferIndex, GpuResources, Index, Normal, Position, UV},
|
||||
renderer::{self, BufferIndex, GpuResources, Index, ModelMatrix, Normal, Position, UV},
|
||||
};
|
||||
|
||||
pub struct UniformResource {
|
||||
@@ -39,6 +40,10 @@ impl FrameMetadata {
|
||||
self.camera_position = [position.x, position.y, position.z, 1.0];
|
||||
}
|
||||
|
||||
pub fn update_dimension(&mut self, dimension: ultraviolet::Vec2) {
|
||||
self.resolution = dimension.into();
|
||||
}
|
||||
|
||||
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"),
|
||||
@@ -82,6 +87,7 @@ pub struct Mesh {
|
||||
pub position_buffer_index: BufferIndex<Position>,
|
||||
pub normal_buffer_index: BufferIndex<Normal>,
|
||||
pub uv_buffer_index: BufferIndex<UV>,
|
||||
pub model_buffer_index: BufferIndex<ModelMatrix>,
|
||||
pub index_buffer_index: BufferIndex<Index>,
|
||||
pub index_format: wgpu::IndexFormat,
|
||||
pub index_count: u32,
|
||||
@@ -91,7 +97,7 @@ pub struct Mesh {
|
||||
type VertexBufferSet = (BufferIndex<Position>, BufferIndex<Normal>, BufferIndex<UV>);
|
||||
type IndexBufferInfo = (BufferIndex<Index>, u32, wgpu::IndexFormat);
|
||||
|
||||
pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 3] {
|
||||
pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 4] {
|
||||
[
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
@@ -120,28 +126,56 @@ pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 3] {
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
}],
|
||||
},
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: 64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 16,
|
||||
shader_location: 4,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 32,
|
||||
shader_location: 5,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 48,
|
||||
shader_location: 6,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub struct MeshBuilder<I, V, P> {
|
||||
pub struct MeshBuilder<I, V, P, M> {
|
||||
indices: I,
|
||||
vertices: V,
|
||||
pipeline: P,
|
||||
model_matrix: M,
|
||||
instance_count: u32,
|
||||
}
|
||||
|
||||
impl MeshBuilder<(), (), ()> {
|
||||
pub fn new() -> Self {
|
||||
impl Default for MeshBuilder<(), (), (), ()> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indices: (),
|
||||
vertices: (),
|
||||
pipeline: (),
|
||||
model_matrix: (),
|
||||
instance_count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> MeshBuilder<(), (), P> {
|
||||
impl<P, M> MeshBuilder<(), (), P, M> {
|
||||
pub fn with_vertices(
|
||||
self,
|
||||
device: &wgpu::Device,
|
||||
@@ -149,7 +183,7 @@ impl<P> MeshBuilder<(), (), P> {
|
||||
positions: &[[f32; 3]],
|
||||
normals: &[[f32; 3]],
|
||||
uvs: &[[f32; 2]],
|
||||
) -> MeshBuilder<(), VertexBufferSet, P> {
|
||||
) -> MeshBuilder<(), VertexBufferSet, P, M> {
|
||||
let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Positions"),
|
||||
contents: bytemuck::cast_slice(positions),
|
||||
@@ -174,18 +208,19 @@ impl<P> MeshBuilder<(), (), P> {
|
||||
vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index),
|
||||
indices: self.indices,
|
||||
pipeline: self.pipeline,
|
||||
model_matrix: self.model_matrix,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, P> MeshBuilder<(), V, P> {
|
||||
impl<V, P, M> MeshBuilder<(), V, P, M> {
|
||||
pub fn with_indices(
|
||||
self,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut GpuResources,
|
||||
indices: &[u32],
|
||||
) -> MeshBuilder<IndexBufferInfo, V, P> {
|
||||
) -> MeshBuilder<IndexBufferInfo, V, P, M> {
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Indices"),
|
||||
contents: bytemuck::cast_slice(indices),
|
||||
@@ -202,29 +237,57 @@ impl<V, P> MeshBuilder<(), V, P> {
|
||||
),
|
||||
vertices: self.vertices,
|
||||
pipeline: self.pipeline,
|
||||
model_matrix: self.model_matrix,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> MeshBuilder<I, V, ()> {
|
||||
pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder<I, V, usize> {
|
||||
impl<I, V, M> MeshBuilder<I, V, (), M> {
|
||||
pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder<I, V, usize, M> {
|
||||
MeshBuilder {
|
||||
pipeline: pipeline_index,
|
||||
indices: self.indices,
|
||||
vertices: self.vertices,
|
||||
model_matrix: self.model_matrix,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MeshBuilder<IndexBufferInfo, VertexBufferSet, usize> {
|
||||
impl<I, V, P> MeshBuilder<I, V, P, ()> {
|
||||
pub fn with_model_matrix(
|
||||
self,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut GpuResources,
|
||||
matrix_columns: Mat4,
|
||||
) -> MeshBuilder<I, V, P, BufferIndex<ModelMatrix>> {
|
||||
let model_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Model Matrix"),
|
||||
contents: bytemuck::cast_slice(matrix_columns.as_slice()),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let model_buffer_index = resources.add_model_matrix_buffer(model_buffer);
|
||||
|
||||
MeshBuilder {
|
||||
indices: self.indices,
|
||||
vertices: self.vertices,
|
||||
pipeline: self.pipeline,
|
||||
model_matrix: model_buffer_index,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MeshBuilder<IndexBufferInfo, VertexBufferSet, usize, BufferIndex<ModelMatrix>> {
|
||||
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,
|
||||
model_buffer_index: self.model_matrix,
|
||||
index_buffer_index: (self.indices).0,
|
||||
index_count: (self.indices).1,
|
||||
index_format: (self.indices).2,
|
||||
@@ -233,108 +296,93 @@ impl MeshBuilder<IndexBufferInfo, VertexBufferSet, usize> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple vertex format.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Vertex {
|
||||
pos: [f32; 3],
|
||||
color: [f32; 3],
|
||||
pub trait Scene: Sized {
|
||||
fn setup(renderer_context: &renderer::RendererContext, resources: &mut GpuResources) -> Self;
|
||||
fn bind_groups(&self) -> &[wgpu::BindGroup];
|
||||
fn meshes(&self) -> &[Mesh];
|
||||
fn handle_mouse_click(&mut self, x: f32, y: f32);
|
||||
fn handle_zoom(&mut self, delta_y: f32);
|
||||
fn handle_orbit(&mut self, delta_x: f32, delta_y: f32);
|
||||
fn clear(&mut self);
|
||||
fn add_mesh(&mut self, mesh: Mesh);
|
||||
fn set_camera_depth_range(&mut self, near: f32, far: f32);
|
||||
fn set_camera_look_at(&mut self, eye: ultraviolet::Vec3, center: ultraviolet::Vec3);
|
||||
|
||||
fn frame_metadata_mut(&mut self) -> Option<&mut FrameMetadata> {
|
||||
None
|
||||
}
|
||||
|
||||
/// 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<Mesh>,
|
||||
fn camera_mut(&mut self) -> Option<&mut Camera> {
|
||||
None
|
||||
}
|
||||
|
||||
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());
|
||||
fn uniform_buffers(&self) -> Option<&[wgpu::Buffer]> {
|
||||
None
|
||||
}
|
||||
|
||||
let uniform_resource = frame_metadata.create_uniform_resource(device);
|
||||
let camera_resource = cam.create_uniform_resource(device);
|
||||
fn resize(&mut self, width: f64, height: f64, _scale_factor: f64, queue: &wgpu::Queue) {
|
||||
let fm_copy = if let Some(fm) = self.frame_metadata_mut() {
|
||||
let dimension = ultraviolet::Vec2::new(width as f32, height as f32);
|
||||
fm.update_dimension(dimension);
|
||||
*fm
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
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(),
|
||||
let view_proj_copy = if let Some(cam) = self.camera_mut() {
|
||||
cam.update_aspect_ratio(width as f32 / height as f32);
|
||||
cam.view_proj
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(buffers) = self.uniform_buffers() {
|
||||
if buffers.len() >= 2 {
|
||||
queue.write_buffer(&buffers[0], 0, bytemuck::cast_slice(&[fm_copy]));
|
||||
queue.write_buffer(&buffers[1], 0, bytemuck::cast_slice(&[view_proj_copy]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_default_triangle(
|
||||
fn update(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut GpuResources,
|
||||
surface_format: wgpu::TextureFormat,
|
||||
renderer_context: &renderer::RendererContext,
|
||||
_resources: &mut GpuResources,
|
||||
) {
|
||||
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 camera_position = if let Some(cam) = self.camera_mut() {
|
||||
cam.position()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let vertex_layout = mesh_vertex_layout();
|
||||
let fm_copy = if let Some(fm) = self.frame_metadata_mut() {
|
||||
let time = (js_sys::Date::now() as f32) * 0.001;
|
||||
fm.time = time;
|
||||
fm.set_camera_position(camera_position);
|
||||
*fm
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pipeline_index = resources.get_or_create_pipeline(
|
||||
device,
|
||||
"triangle_colored",
|
||||
&vertex_layout,
|
||||
include_str!("../example.wgsl"),
|
||||
surface_format,
|
||||
);
|
||||
let view_proj_copy = if let Some(cam) = self.camera_mut() {
|
||||
cam.view_proj
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
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],
|
||||
if let Some(buffers) = self.uniform_buffers() {
|
||||
if buffers.len() >= 2 {
|
||||
renderer_context.queue.write_buffer(
|
||||
&buffers[0],
|
||||
0,
|
||||
bytemuck::cast_slice(&[self.frame_metadata][..]),
|
||||
bytemuck::cast_slice(&[fm_copy]),
|
||||
);
|
||||
|
||||
queue.write_buffer(
|
||||
&self.uniform_buffers[1],
|
||||
renderer_context.queue.write_buffer(
|
||||
&buffers[1],
|
||||
0,
|
||||
bytemuck::cast_slice(&[self.cam.view_proj]),
|
||||
bytemuck::cast_slice(&[view_proj_copy]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2025-10-20"
|
||||
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
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,
|
||||
worker_chan: Sender<WindowEvent>,
|
||||
|
||||
// Store closures to keep them alive
|
||||
resize_listener: Option<Closure<dyn FnMut()>>,
|
||||
mousemove_listener: Option<Closure<dyn FnMut(web_sys::MouseEvent)>>,
|
||||
mousedown_listener: Option<Closure<dyn FnMut(web_sys::MouseEvent)>>,
|
||||
wheel_listener: Option<Closure<dyn FnMut(web_sys::WheelEvent)>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub async fn new() -> Result<Self, JsValue> {
|
||||
let (sender, receiver) = mpsc::channel::<WindowEvent>();
|
||||
|
||||
let canvas = web::get_canvas_element("#canvas0");
|
||||
let _worker = MainWorker::spawn("main-worker", 1, move || {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
MainWorker::run_render_loop(receiver).await;
|
||||
});
|
||||
})?;
|
||||
|
||||
_worker.transfer_ownership(&canvas);
|
||||
|
||||
let mut app = App {
|
||||
_worker,
|
||||
worker_chan: sender,
|
||||
resize_listener: None,
|
||||
mousemove_listener: None,
|
||||
mousedown_listener: None,
|
||||
wheel_listener: None,
|
||||
};
|
||||
|
||||
app.setup_event_listeners();
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn setup_event_listeners(&mut self) {
|
||||
let window = web_sys::window().unwrap();
|
||||
let resize_worker_chan = self.worker_chan.clone();
|
||||
|
||||
let resize_listener: Closure<dyn FnMut()> = Closure::new(move || {
|
||||
use crate::message::ResizeMessage;
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let width = window.inner_width().ok().unwrap().as_f64().unwrap();
|
||||
let height = window.inner_height().ok().unwrap().as_f64().unwrap();
|
||||
|
||||
resize_worker_chan
|
||||
.send(WindowEvent::Resize(ResizeMessage {
|
||||
width,
|
||||
height,
|
||||
scale_factor: window.device_pixel_ratio(),
|
||||
}))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let _ = window
|
||||
.add_event_listener_with_callback("resize", resize_listener.as_ref().unchecked_ref());
|
||||
|
||||
let mousemove_worker_chan = self.worker_chan.clone();
|
||||
let mousemove_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
|
||||
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());
|
||||
if event.type_() == "click" {
|
||||
event_data = WindowEvent::PointerClick(mouse_event_data.clone());
|
||||
}
|
||||
|
||||
mousemove_worker_chan.clone().send(event_data).unwrap();
|
||||
});
|
||||
|
||||
let _ = window
|
||||
.add_event_listener_with_callback(
|
||||
"mousemove",
|
||||
mousemove_listener.as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _ = window
|
||||
.add_event_listener_with_callback("click", mousemove_listener.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
|
||||
let mousedown_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
|
||||
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<dyn FnMut(web_sys::WheelEvent)> =
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entrypoint for the main thread
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let app = App::new().await.unwrap();
|
||||
// keep the app running, and prevent drops of the objects App owns
|
||||
Box::leak(Box::new(app));
|
||||
});
|
||||
}
|
||||
|
||||
// executes the closure it is spawned with
|
||||
#[wasm_bindgen]
|
||||
pub fn worker_entrypoint(ptr: u32) {
|
||||
let work = unsafe { Box::from_raw(ptr as *mut Box<dyn FnOnce()>) };
|
||||
(*work)();
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Imports wasm statically.
|
||||
// Reasonable because worker can't do anything without wasm.
|
||||
// And worker's loading can still be determined from external dynamically.
|
||||
import wbg_init, { worker_entrypoint } from "pkg/wasm-index.js";
|
||||
|
||||
export function attachMain() {}
|
||||
|
||||
let isReady = false;
|
||||
|
||||
onmessage = async (event) => {
|
||||
console.log("event", event);
|
||||
if (isReady) return;
|
||||
|
||||
isReady = true;
|
||||
await wbg_init({ module_or_path: event.data[0], memory: event.data[2] });
|
||||
worker_entrypoint(event.data[3]);
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import wbg_init, { main } from "../pkg/wasm-index.js";
|
||||
import wbg_init, { main } from "../level-editor/pkg/wasm-index.js";
|
||||
|
||||
const start = async () => {
|
||||
await wbg_init();
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user