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:
Akash Shakdwipeea
2025-10-22 18:08:02 +05:30
committed by GitHub
co-authored by Amp
parent cb65a52e95
commit b295c0bf0b
33 changed files with 5516 additions and 995 deletions
+13 -1
View File
@@ -1,3 +1,15 @@
[build] [build]
target = "wasm32-unknown-unknown" 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']
+1
View File
@@ -16,3 +16,4 @@
- **WGSL shaders**: Keep binding names aligned with Rust bind group layouts - **WGSL shaders**: Keep binding names aligned with Rust bind group layouts
- **JavaScript/TypeScript**: Format with prettier defaults - **JavaScript/TypeScript**: Format with prettier defaults
- **Comments**: Add documentation comments for public APIs using `///` - **Comments**: Add documentation comments for public APIs using `///`
- **Logging**: Use `log::info!`, `log::error!`, etc. from the log crate, not `println!`
Generated
+107 -38
View File
@@ -77,28 +77,6 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@@ -438,6 +416,21 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -445,6 +438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@@ -453,6 +447,34 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 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]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@@ -471,10 +493,16 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]
@@ -969,9 +997,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -1000,6 +1028,23 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.174" version = "0.2.174"
@@ -1405,6 +1450,29 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" 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]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.23" version = "0.12.23"
@@ -2022,21 +2090,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"rustversion", "rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
"wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.100" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@@ -2048,9 +2117,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.50" version = "0.4.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -2061,9 +2130,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.100" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2071,9 +2140,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.100" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2084,9 +2153,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.100" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2104,9 +2173,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.77" version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
+14 -29
View File
@@ -1,22 +1,8 @@
[package] [workspace]
name = "base" members = ["renderer", "level-editor"]
description = "wgpu + webpack base" resolver = "2"
version = "0.1.0"
edition = "2021"
[lib] [workspace.dependencies]
crate-type = ["cdylib"]
[features]
default = []
atomics = []
bulk-memory = []
[profile.release]
opt-level = "z"
lto = true
[dependencies]
wasm-bindgen = { version = "0.2.100", features = ["enable-interning"] } wasm-bindgen = { version = "0.2.100", features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.50" wasm-bindgen-futures = "0.4.50"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
@@ -28,8 +14,13 @@ web-sys = { version = "0.3.77", features = [
"Document", "Document",
"Element", "Element",
"HtmlCanvasElement", "HtmlCanvasElement",
"HtmlInputElement",
"File",
"FileList",
"OffscreenCanvas", "OffscreenCanvas",
"MouseEvent", "MouseEvent",
"WheelEvent",
"KeyboardEvent",
"Worker", "Worker",
"DedicatedWorkerGlobalScope", "DedicatedWorkerGlobalScope",
"Event", "Event",
@@ -41,22 +32,16 @@ web-sys = { version = "0.3.77", features = [
"RequestInit", "RequestInit",
"RequestMode", "RequestMode",
"Response", "Response",
"Headers" "Headers",
"AddEventListenerOptions"
]} ]}
js-sys = "0.3.77" js-sys = "0.3.77"
bytemuck = { version = "1.23.1", features = [ bytemuck = { version = "1.23.1", features = ["derive"] }
"derive"
]}
cgmath = "0.18" cgmath = "0.18"
raw-window-handle = "0.6.2" raw-window-handle = "0.6.2"
wgpu = "26.0.1" wgpu = "26.0.1"
reqwest = { version = "0.12.23", features = ["json"] } reqwest = { version = "0.12.23", features = ["json"] }
thiserror = "2.0.15" thiserror = "2.0.15"
ultraviolet = "0.10.0" ultraviolet = "0.10.0"
futures = "0.3"
[dependencies.gltf] gltf = { version = "1.4", features = ["extras", "names", "KHR_lights_punctual"] }
version = "1.4"
features = ["extras", "names"]
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
+38
View File
@@ -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"]
+46
View File
@@ -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
+221
View File
@@ -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!();
+68
View File
@@ -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);
}
+3903
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -4,19 +4,23 @@
"name": "basic", "name": "basic",
"version": "0.1.0", "version": "0.1.0",
"scripts": { "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-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' wasm-pack build --release --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 -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-dev": "vite build --mode development",
"bundle-release": "vite build", "bundle-release": "vite build",
"build": "run-s clean wasm-dev bundle-dev", "build": "run-s clean wasm-dev bundle-dev",
"build-release": "run-s clean wasm-release bundle-release", "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", "start": "vite preview",
"clean": "rimraf --glob dist **/pkg", "clean": "rimraf --glob dist **/pkg",
"clean-all": "rimraf --glob dist **/pkg target node_modules" "clean-all": "rimraf --glob dist **/pkg target node_modules"
}, },
"devDependencies": { "devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.7.0", "@wasm-tool/wasm-pack-plugin": "^1.7.0",
"vite": "^5.1.6", "vite": "^7.1.10",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.3.0",
"rollup-plugin-copy": "^3.5.0", "rollup-plugin-copy": "^3.5.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
+52
View File
@@ -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
+222
View File
@@ -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)
}
}
+16 -18
View File
@@ -1,6 +1,6 @@
use std::f32::consts::PI; 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 wgpu::util::DeviceExt;
use crate::{message::WheelMessage, renderer::scene::UniformResource}; use crate::{message::WheelMessage, renderer::scene::UniformResource};
@@ -26,11 +26,9 @@ pub struct Camera {
z_near: f32, z_near: f32,
z_far: f32, z_far: f32,
// Rotor orientation + spherical coordinates for orbit camera behaviour // Rotor orientation for orbit camera behaviour
rotor: Rotor3, rotor: Rotor3,
distance: f32, distance: f32,
yaw: f32,
pitch: f32,
// Dirty flag for lazy evaluation // Dirty flag for lazy evaluation
dirty: bool, dirty: bool,
@@ -86,8 +84,8 @@ impl Camera {
pub fn new(aspect_ratio: f32) -> Self { pub fn new(aspect_ratio: f32) -> Self {
let mut camera = Camera { let mut camera = Camera {
view_proj: [[0.0; 4]; 4], view_proj: [[0.0; 4]; 4],
position: Vec3::new(0.0, 1.5, 0.0), position: Vec3::new(0.0, 0.5, 3.0),
target: Vec3::zero(), target: Vec3::new(0.0, 0.0, 0.0),
up: Vec3::unit_y(), up: Vec3::unit_y(),
fov: PI / 3.0, fov: PI / 3.0,
aspect_ratio, aspect_ratio,
@@ -95,8 +93,6 @@ impl Camera {
z_far: 100000.0, z_far: 100000.0,
rotor: Rotor3::identity(), rotor: Rotor3::identity(),
distance: 1.0, distance: 1.0,
yaw: 0.0,
pitch: 0.0,
dirty: true, dirty: true,
}; };
@@ -138,6 +134,12 @@ impl Camera {
self.position 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) { pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
// Skip tiny movements to reduce unnecessary computations // Skip tiny movements to reduce unnecessary computations
if delta_x.abs() < 0.001 && delta_y.abs() < 0.001 { if delta_x.abs() < 0.001 && delta_y.abs() < 0.001 {
@@ -150,11 +152,10 @@ impl Camera {
let basis = OrthonormalBasis::from_camera(self); let basis = OrthonormalBasis::from_camera(self);
let desired_pitch = (self.pitch - delta_y * ORBIT_SENSITIVITY).clamp(-MAX_PITCH, MAX_PITCH); let pitch_angle = (delta_y * ORBIT_SENSITIVITY).clamp(-MAX_PITCH, MAX_PITCH);
let applied_pitch = desired_pitch - self.pitch;
let pitch_rotor = 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(); let orbit_rotor = (yaw_rotor * pitch_rotor).normalized();
@@ -169,9 +170,6 @@ impl Camera {
self.distance = offset.mag().max(MIN_DISTANCE); self.distance = offset.mag().max(MIN_DISTANCE);
self.position = offset + self.target; self.position = offset + self.target;
self.yaw += yaw_theta;
self.pitch = desired_pitch;
self.dirty = true; self.dirty = true;
self.compute_view_proj_mat(); self.compute_view_proj_mat();
} }
@@ -221,9 +219,9 @@ impl Camera {
}); });
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 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 { entries: &[wgpu::BindGroupLayoutEntry {
binding: 1, binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BufferBindingType::Uniform,
@@ -235,10 +233,10 @@ impl Camera {
}); });
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform bind group"), label: Some("Camera bind group"),
layout: &bind_group_layout, layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry { entries: &[wgpu::BindGroupEntry {
binding: 1, binding: 0,
resource: buffer.as_entire_binding(), resource: buffer.as_entire_binding(),
}], }],
}); });
+13 -21
View File
@@ -1,5 +1,5 @@
use gltf::Gltf; use gltf::Gltf;
use ultraviolet::{Mat4, Vec3, Vec4}; use ultraviolet::{Mat4, Vec3};
use wgpu::TextureFormat; use wgpu::TextureFormat;
use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder}; use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder};
@@ -33,6 +33,9 @@ pub enum ImportError {
#[error("failed to load model")] #[error("failed to load model")]
LoadError, LoadError,
#[error("{0}")]
Other(String),
} }
fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> { 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>( fn visit_node<'a>(
node: gltf::Node<'a>, node: gltf::Node<'a>,
parent_transform: Mat4, parent_transform: Mat4,
@@ -78,7 +72,7 @@ fn visit_node<'a>(
pipeline_index: usize, pipeline_index: usize,
model_bounds: &mut Option<ModelBounds>, 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 world_transform = parent_transform * local_transform;
let normal_matrix = world_transform.inversed().transposed(); let normal_matrix = world_transform.inversed().transposed();
@@ -89,7 +83,7 @@ fn visit_node<'a>(
_ => None, _ => None,
}); });
let mut positions: Vec<[f32; 3]> = match reader.read_positions() { let positions: Vec<[f32; 3]> = match reader.read_positions() {
Some(iter) => iter.collect(), Some(iter) => iter.collect(),
None => Vec::new(), None => Vec::new(),
}; };
@@ -132,17 +126,14 @@ fn visit_node<'a>(
uvs.resize(vertex_count, [0.0, 0.0]); 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 vec = Vec3::new(position[0], position[1], position[2]);
let transformed = world_transform.transform_point3(vec); let transformed = world_transform.transform_point3(vec);
*position = [transformed.x, transformed.y, transformed.z]; let world_point = [transformed.x, transformed.y, transformed.z];
}
for position in &positions {
if let Some(bounds) = model_bounds.as_mut() { if let Some(bounds) = model_bounds.as_mut() {
bounds.include_point(*position); bounds.include_point(world_point);
} else { } 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; continue;
} }
let mesh = MeshBuilder::new() let mesh = MeshBuilder::default()
.with_vertices(device, resources, &positions, &normals, &uvs) .with_vertices(device, resources, &positions, &normals, &uvs)
.with_indices(device, resources, &indices) .with_indices(device, resources, &indices)
.with_pipeline(pipeline_index) .with_pipeline(pipeline_index)
.with_model_matrix(device, resources, world_transform)
.build(); .build();
meshes.push(mesh); meshes.push(mesh);
@@ -185,7 +177,7 @@ pub async fn load_gltf_model(
meshes: &mut Vec<crate::renderer::scene::Mesh>, meshes: &mut Vec<crate::renderer::scene::Mesh>,
surface_format: TextureFormat, surface_format: TextureFormat,
) -> Result<Option<ModelBounds>, ImportError> { ) -> 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? .await?
.bytes() .bytes()
.await?; .await?;
+18 -6
View File
@@ -8,12 +8,16 @@ struct UniformData {
} }
@group(0) @binding(0) var<uniform> uni: 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 { struct VertexInput {
@location(0) pos: vec3<f32>, @location(0) pos: vec3<f32>,
@location(1) normal: 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 { struct VertexOutput {
@@ -22,11 +26,19 @@ struct VertexOutput {
@location(1) normal: vec3<f32> @location(1) normal: vec3<f32>
} }
@vertex @vertex
fn vs_main(in: VertexInput) -> VertexOutput { fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.clip_position = view_proj * vec4<f32>(in.pos, 1.0); let model = mat4x4<f32>(
out.world_pos = in.pos; 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); out.normal = normalize(in.normal);
return out; return out;
} }
@@ -35,7 +47,7 @@ fn vs_main(in: VertexInput) -> VertexOutput {
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45)); let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45));
let light_color = vec3<f32>(1.0, 0.95, 0.85); 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 normal = normalize(in.normal);
let view_dir = normalize(uni.camera_position.xyz - in.world_pos); 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; let ambient = 0.15;
var specular = 0.0; var specular = 0.0;
if (diffuse_strength > 0.0) { if diffuse_strength > 0.0 {
let halfway_dir = normalize(light_direction + view_dir); let halfway_dir = normalize(light_direction + view_dir);
specular = pow(max(dot(normal, halfway_dir), 0.0), 32.0); specular = pow(max(dot(normal, halfway_dir), 0.0), 32.0);
} }
+30
View File
@@ -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), PointerMove(MouseMessage),
PointerClick(MouseMessage), PointerClick(MouseMessage),
PointerWheel(WheelMessage), PointerWheel(WheelMessage),
Keyboard(KeyboardMessage),
} }
// Display for WindowEvent // Display for WindowEvent
@@ -16,6 +17,7 @@ impl fmt::Display for WindowEvent {
WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg), WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg),
WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg), WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg),
WindowEvent::PointerWheel(msg) => write!(f, "PointerWheel: {:?}", 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(); .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; use crate::renderer::Renderer;
let canvas = wait_for_canvas_transfer().await; 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); Renderer::run_render_loop(renderer);
} }
} }
@@ -1,9 +1,11 @@
use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc, sync::mpsc::Receiver}; use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc, sync::mpsc::Receiver};
use futures::channel::oneshot;
use log::info; use log::info;
use ultraviolet::Vec4;
use wasm_bindgen::{prelude::Closure, JsCast}; use wasm_bindgen::{prelude::Closure, JsCast};
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::DedicatedWorkerGlobalScope; use web_sys::{DedicatedWorkerGlobalScope, File, MessageEvent};
use crate::{ use crate::{
gltf::{load_gltf_model, ImportError, ModelBounds}, gltf::{load_gltf_model, ImportError, ModelBounds},
@@ -13,6 +15,9 @@ use crate::{
pub mod scene; pub mod scene;
// Re-export commonly used types
pub use scene::Mesh;
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
pub struct GpuResources { 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)] #[inline(always)]
pub fn get_buffer<T>(&self, id: &BufferIndex<T>) -> &wgpu::Buffer { pub fn get_buffer<T>(&self, id: &BufferIndex<T>) -> &wgpu::Buffer {
&self.buffers[id.index as usize] &self.buffers[id.index as usize]
@@ -234,21 +248,26 @@ pub struct Position;
pub struct Normal; pub struct Normal;
pub struct UV; pub struct UV;
pub struct Index; pub struct Index;
pub struct ModelMatrix;
pub struct Renderer { pub struct RendererContext {
canvas: web_sys::OffscreenCanvas, pub device: wgpu::Device,
events_chan: Receiver<WindowEvent>, pub queue: wgpu::Queue,
surface: wgpu::Surface<'static>, pub surface_config: wgpu::SurfaceConfiguration,
device: wgpu::Device, pub surface: wgpu::Surface<'static>,
queue: wgpu::Queue, pub depth_texture: wgpu::Texture,
surface_config: wgpu::SurfaceConfiguration, pub depth_view: wgpu::TextureView,
scene: Scene,
resources: GpuResources,
depth_texture: wgpu::Texture,
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( fn create_depth_texture(
device: &wgpu::Device, device: &wgpu::Device,
config: &wgpu::SurfaceConfiguration, config: &wgpu::SurfaceConfiguration,
@@ -266,7 +285,7 @@ impl Renderer {
sample_count: 1, sample_count: 1,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT, format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[], view_formats: &[],
}); });
@@ -276,9 +295,10 @@ impl Renderer {
} }
fn recreate_depth_texture(&mut self) { fn recreate_depth_texture(&mut self) {
let (texture, view) = Self::create_depth_texture(&self.device, &self.surface_config); let (texture, view) =
self.depth_texture = texture; Self::create_depth_texture(&self.context.device, &self.context.surface_config);
self.depth_view = view; self.context.depth_texture = texture;
self.context.depth_view = view;
} }
pub async fn new(canvas: web_sys::OffscreenCanvas, events_chan: Receiver<WindowEvent>) -> Self { 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 (depth_texture, depth_view) = Self::create_depth_texture(&device, &surface_config);
let mut resources = GpuResources::new(); let mut resources = GpuResources::new();
let context = RendererContext {
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,
surface, surface,
device, device,
queue, queue,
surface_config, surface_config,
scene,
resources,
depth_texture, depth_texture,
depth_view, depth_view,
};
let scene = T::setup(&context, &mut resources);
Self {
canvas,
events_chan,
context,
scene,
resources,
} }
} }
fn render(&mut self, time: f32) { 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 texture_view = surface_texture.texture.create_view(&Default::default());
let mut encoder = self let mut encoder =
self.context
.device .device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { .create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render command encoder"), label: Some("Render command encoder"),
@@ -386,7 +404,7 @@ impl Renderer {
}, },
})], })],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view, view: &self.context.depth_view,
depth_ops: Some(wgpu::Operations { depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0), load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store, store: wgpu::StoreOp::Store,
@@ -397,11 +415,11 @@ impl Renderer {
timestamp_writes: None, 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, &[]); 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_pipeline(self.resources.get_pipeline_by_index(mesh.pipeline_index));
render_pass.set_vertex_buffer( render_pass.set_vertex_buffer(
@@ -420,6 +438,12 @@ impl Renderer {
2, 2,
self.resources.get_buffer(&mesh.uv_buffer_index).slice(..), 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( render_pass.set_index_buffer(
self.resources self.resources
@@ -431,10 +455,102 @@ impl Renderer {
render_pass.draw_indexed(0..mesh.index_count, 0, 0..mesh.instance_count); 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(); 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) { pub async fn handle_event(renderer: Rc<RefCell<Self>>, event: WindowEvent) {
match event { match event {
WindowEvent::PointerMove(msg) => { WindowEvent::PointerMove(msg) => {
@@ -448,25 +564,50 @@ impl Renderer {
let mut r = renderer.borrow_mut(); let mut r = renderer.borrow_mut();
let x = (msg.offset_x * msg.scale_factor) as f32; let x = (msg.offset_x * msg.scale_factor) as f32;
let y = (msg.offset_y * 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"); 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) => { WindowEvent::PointerWheel(msg) => {
let mut r = renderer.borrow_mut(); 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 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 { if let Ok(event) = event {
let renderer_clone = renderer.clone(); let renderer_clone = renderer.clone();
spawn_local(async move { 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); r.render(time);
} }
}
Self::run_render_loop(renderer.clone()); Self::run_render_loop(renderer.clone());
}); });
@@ -496,12 +639,19 @@ impl Renderer {
let new_width = (msg.width * msg.scale_factor) as u32; let new_width = (msg.width * msg.scale_factor) as u32;
let new_height = (msg.height * 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() { if new_width != self.canvas.width() || new_height != self.canvas.height() {
self.surface_config.width = new_width; self.context.surface_config.width = new_width;
self.surface_config.height = new_height; self.context.surface_config.height = new_height;
self.surface.configure(&self.device, &self.surface_config); self.context
.surface
.configure(&self.context.device, &self.context.surface_config);
self.recreate_depth_texture(); 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!( info!(
"Resized: ({}, {}), scale: {}", "Resized: ({}, {}), scale: {}",
@@ -511,25 +661,20 @@ impl Renderer {
} }
pub fn mouse_move(&mut self, msg: MouseMessage) { 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 { if (msg.buttons & 0x04) != 0 {
let delta_x = (msg.movement_x * msg.scale_factor) as f32; let delta_x = (msg.movement_x * msg.scale_factor) as f32;
let delta_y = (msg.movement_y * 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 // currently this replaces everything, will need more sophisticated mechanisms later
pub async fn load_assets_async(renderer: Rc<RefCell<Renderer>>) -> Result<(), ImportError> { pub async fn load_assets_async(renderer: Rc<RefCell<Renderer<T>>>) -> Result<(), ImportError> {
let (device, surface_format, bind_group_layout) = { let (device, surface_format) = {
let r = renderer.borrow(); let r = renderer.borrow();
( (
r.device.clone(), r.context.device.clone(),
r.surface_config.format, r.context.surface_config.format,
r.scene.bind_group_layout.clone(),
) )
}; };
@@ -537,12 +682,10 @@ impl Renderer {
let mut original_resources = { let mut original_resources = {
let mut r = renderer.borrow_mut(); let mut r = renderer.borrow_mut();
r.scene.meshes.clear(); r.scene.clear();
std::mem::take(&mut r.resources) std::mem::take(&mut r.resources)
}; };
original_resources.set_bind_group_layouts(&bind_group_layout);
let bounds = load_gltf_model( let bounds = load_gltf_model(
&device, &device,
&mut original_resources, &mut original_resources,
@@ -554,7 +697,10 @@ impl Renderer {
{ {
let mut r = renderer.borrow_mut(); let mut r = renderer.borrow_mut();
r.resources = original_resources; r.resources = original_resources;
r.scene.meshes = meshes;
for mesh in meshes {
r.scene.add_mesh(mesh);
}
if let Some(ModelBounds { min, max }) = bounds { if let Some(ModelBounds { min, max }) = bounds {
let center = ultraviolet::Vec3::new( let center = ultraviolet::Vec3::new(
@@ -580,16 +726,22 @@ impl Renderer {
// Using a fixed upper clamp caused large models to be clipped // Using a fixed upper clamp caused large models to be clipped
// completely; relying on the model radius instead. // completely; relying on the model radius instead.
let far_plane = (radius * 4.0).max(near_plane + 1.0); let far_plane = (radius * 4.0).max(near_plane + 1.0);
r.scene.cam.set_depth_range(near_plane, far_plane); r.scene.set_camera_depth_range(near_plane, far_plane);
r.scene.cam.look_at(center + eye_offset, center); r.scene.set_camera_look_at(center + eye_offset, center);
} }
} }
Ok(()) 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 { fn from(value: BufferIndex<T>) -> Self {
value.index value.index
} }
@@ -1,8 +1,9 @@
use ultraviolet::Mat4;
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use crate::{ use crate::{
camera::Camera, camera::Camera,
renderer::{BufferIndex, GpuResources, Index, Normal, Position, UV}, renderer::{self, BufferIndex, GpuResources, Index, ModelMatrix, Normal, Position, UV},
}; };
pub struct UniformResource { pub struct UniformResource {
@@ -39,6 +40,10 @@ impl FrameMetadata {
self.camera_position = [position.x, position.y, position.z, 1.0]; 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 { pub fn create_uniform_resource(self, device: &wgpu::Device) -> UniformResource {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("frame metadata uniform buffer"), label: Some("frame metadata uniform buffer"),
@@ -82,6 +87,7 @@ pub struct Mesh {
pub position_buffer_index: BufferIndex<Position>, pub position_buffer_index: BufferIndex<Position>,
pub normal_buffer_index: BufferIndex<Normal>, pub normal_buffer_index: BufferIndex<Normal>,
pub uv_buffer_index: BufferIndex<UV>, pub uv_buffer_index: BufferIndex<UV>,
pub model_buffer_index: BufferIndex<ModelMatrix>,
pub index_buffer_index: BufferIndex<Index>, pub index_buffer_index: BufferIndex<Index>,
pub index_format: wgpu::IndexFormat, pub index_format: wgpu::IndexFormat,
pub index_count: u32, pub index_count: u32,
@@ -91,7 +97,7 @@ pub struct Mesh {
type VertexBufferSet = (BufferIndex<Position>, BufferIndex<Normal>, BufferIndex<UV>); type VertexBufferSet = (BufferIndex<Position>, BufferIndex<Normal>, BufferIndex<UV>);
type IndexBufferInfo = (BufferIndex<Index>, u32, wgpu::IndexFormat); 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 { wgpu::VertexBufferLayout {
array_stride: 12, array_stride: 12,
@@ -120,28 +126,56 @@ pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 3] {
format: wgpu::VertexFormat::Float32x2, 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, indices: I,
vertices: V, vertices: V,
pipeline: P, pipeline: P,
model_matrix: M,
instance_count: u32, instance_count: u32,
} }
impl MeshBuilder<(), (), ()> { impl Default for MeshBuilder<(), (), (), ()> {
pub fn new() -> Self { fn default() -> Self {
Self { Self {
indices: (), indices: (),
vertices: (), vertices: (),
pipeline: (), pipeline: (),
model_matrix: (),
instance_count: 1, instance_count: 1,
} }
} }
} }
impl<P> MeshBuilder<(), (), P> { impl<P, M> MeshBuilder<(), (), P, M> {
pub fn with_vertices( pub fn with_vertices(
self, self,
device: &wgpu::Device, device: &wgpu::Device,
@@ -149,7 +183,7 @@ impl<P> MeshBuilder<(), (), P> {
positions: &[[f32; 3]], positions: &[[f32; 3]],
normals: &[[f32; 3]], normals: &[[f32; 3]],
uvs: &[[f32; 2]], uvs: &[[f32; 2]],
) -> MeshBuilder<(), VertexBufferSet, P> { ) -> MeshBuilder<(), VertexBufferSet, P, M> {
let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Positions"), label: Some("Mesh Positions"),
contents: bytemuck::cast_slice(positions), contents: bytemuck::cast_slice(positions),
@@ -174,18 +208,19 @@ impl<P> MeshBuilder<(), (), P> {
vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index), vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index),
indices: self.indices, indices: self.indices,
pipeline: self.pipeline, pipeline: self.pipeline,
model_matrix: self.model_matrix,
instance_count: self.instance_count, instance_count: self.instance_count,
} }
} }
} }
impl<V, P> MeshBuilder<(), V, P> { impl<V, P, M> MeshBuilder<(), V, P, M> {
pub fn with_indices( pub fn with_indices(
self, self,
device: &wgpu::Device, device: &wgpu::Device,
resources: &mut GpuResources, resources: &mut GpuResources,
indices: &[u32], indices: &[u32],
) -> MeshBuilder<IndexBufferInfo, V, P> { ) -> MeshBuilder<IndexBufferInfo, V, P, M> {
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Indices"), label: Some("Mesh Indices"),
contents: bytemuck::cast_slice(indices), contents: bytemuck::cast_slice(indices),
@@ -202,29 +237,57 @@ impl<V, P> MeshBuilder<(), V, P> {
), ),
vertices: self.vertices, vertices: self.vertices,
pipeline: self.pipeline, pipeline: self.pipeline,
model_matrix: self.model_matrix,
instance_count: self.instance_count, instance_count: self.instance_count,
} }
} }
} }
impl<I, V> MeshBuilder<I, V, ()> { impl<I, V, M> MeshBuilder<I, V, (), M> {
pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder<I, V, usize> { pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder<I, V, usize, M> {
MeshBuilder { MeshBuilder {
pipeline: pipeline_index, pipeline: pipeline_index,
indices: self.indices, indices: self.indices,
vertices: self.vertices, vertices: self.vertices,
model_matrix: self.model_matrix,
instance_count: self.instance_count, 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 { pub fn build(self) -> Mesh {
Mesh { Mesh {
pipeline_index: self.pipeline, pipeline_index: self.pipeline,
position_buffer_index: (self.vertices).0, position_buffer_index: (self.vertices).0,
normal_buffer_index: (self.vertices).1, normal_buffer_index: (self.vertices).1,
uv_buffer_index: (self.vertices).2, uv_buffer_index: (self.vertices).2,
model_buffer_index: self.model_matrix,
index_buffer_index: (self.indices).0, index_buffer_index: (self.indices).0,
index_count: (self.indices).1, index_count: (self.indices).1,
index_format: (self.indices).2, index_format: (self.indices).2,
@@ -233,108 +296,93 @@ impl MeshBuilder<IndexBufferInfo, VertexBufferSet, usize> {
} }
} }
/// Simple vertex format. pub trait Scene: Sized {
#[repr(C)] fn setup(renderer_context: &renderer::RendererContext, resources: &mut GpuResources) -> Self;
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] fn bind_groups(&self) -> &[wgpu::BindGroup];
pub struct Vertex { fn meshes(&self) -> &[Mesh];
pos: [f32; 3], fn handle_mouse_click(&mut self, x: f32, y: f32);
color: [f32; 3], 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. fn camera_mut(&mut self) -> Option<&mut Camera> {
const VERTICES: &[Vertex] = &[ None
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>,
} }
impl Scene { fn uniform_buffers(&self) -> Option<&[wgpu::Buffer]> {
pub fn new(device: &wgpu::Device, dimension: ultraviolet::Vec2) -> Self { None
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); fn resize(&mut self, width: f64, height: f64, _scale_factor: f64, queue: &wgpu::Queue) {
let camera_resource = cam.create_uniform_resource(device); 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 { let view_proj_copy = if let Some(cam) = self.camera_mut() {
uniform_buffers: [uniform_resource.buffer, camera_resource.buffer], cam.update_aspect_ratio(width as f32 / height as f32);
bind_groups: [uniform_resource.bind_group, camera_resource.bind_group], cam.view_proj
bind_group_layout: [ } else {
uniform_resource.bind_group_layout, return;
camera_resource.bind_group_layout, };
],
frame_metadata, if let Some(buffers) = self.uniform_buffers() {
cam, if buffers.len() >= 2 {
meshes: Vec::new(), 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, &mut self,
device: &wgpu::Device, renderer_context: &renderer::RendererContext,
resources: &mut GpuResources, _resources: &mut GpuResources,
surface_format: wgpu::TextureFormat,
) { ) {
let positions: Vec<[f32; 3]> = VERTICES.iter().map(|v| v.pos).collect(); let camera_position = if let Some(cam) = self.camera_mut() {
// Colors ride through the "normal" slot because the render path always binds cam.position()
// three vertex buffers (position, normal, uv) for every mesh. } else {
// todo clean that shit up return;
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 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( let view_proj_copy = if let Some(cam) = self.camera_mut() {
device, cam.view_proj
"triangle_colored", } else {
&vertex_layout, return;
include_str!("../example.wgsl"), };
surface_format,
);
let mesh = MeshBuilder::new() if let Some(buffers) = self.uniform_buffers() {
.with_vertices(device, resources, &positions, &colors, uvs) if buffers.len() >= 2 {
.with_indices(device, resources, INDICES) renderer_context.queue.write_buffer(
.with_pipeline(pipeline_index) &buffers[0],
.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, 0,
bytemuck::cast_slice(&[self.frame_metadata][..]), bytemuck::cast_slice(&[fm_copy]),
); );
renderer_context.queue.write_buffer(
queue.write_buffer( &buffers[1],
&self.uniform_buffers[1],
0, 0,
bytemuck::cast_slice(&[self.cam.view_proj]), bytemuck::cast_slice(&[view_proj_copy]),
); );
} }
} }
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "nightly" channel = "nightly-2025-10-20"
-173
View File
@@ -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)();
}
-17
View File
@@ -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
View File
@@ -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 () => { const start = async () => {
await wbg_init(); await wbg_init();
Binary file not shown.
+295 -500
View File
File diff suppressed because it is too large Load Diff