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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("hello world!");
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use ultraviolet::{projection, Bivec3, Mat4, Rotor3, Vec3};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::{message::WheelMessage, renderer::scene::UniformResource};
|
||||
|
||||
const MIN_DISTANCE: f32 = 0.1;
|
||||
const MAX_PITCH: f32 = PI / 2.0 - 0.01;
|
||||
const ORBIT_SENSITIVITY: f32 = 0.005;
|
||||
const ZOOM_SENSITIVITY: f32 = 0.002;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Camera {
|
||||
// Hot data - cached computed matrix (64 bytes, 1 cache line)
|
||||
pub view_proj: [[f32; 4]; 4],
|
||||
|
||||
// Warm data - frequently accessed vectors (36 bytes)
|
||||
position: Vec3,
|
||||
target: Vec3,
|
||||
up: Vec3,
|
||||
|
||||
// Cold data - projection parameters (16 bytes)
|
||||
fov: f32,
|
||||
aspect_ratio: f32,
|
||||
z_near: f32,
|
||||
z_far: f32,
|
||||
|
||||
// Rotor orientation for orbit camera behaviour
|
||||
rotor: Rotor3,
|
||||
distance: f32,
|
||||
|
||||
// Dirty flag for lazy evaluation
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
struct OrthonormalBasis {
|
||||
right: Vec3,
|
||||
up: Vec3,
|
||||
forward: Vec3,
|
||||
}
|
||||
|
||||
impl OrthonormalBasis {
|
||||
pub fn new(right: Vec3, up: Vec3, forward: Vec3) -> Self {
|
||||
Self { right, up, forward }
|
||||
}
|
||||
|
||||
pub fn from_camera(camera: &Camera) -> Self {
|
||||
let mut forward_offset = camera.target - camera.position;
|
||||
if forward_offset.mag_sq() <= f32::EPSILON {
|
||||
forward_offset = -Vec3::unit_z();
|
||||
}
|
||||
|
||||
let forward = forward_offset.normalized();
|
||||
|
||||
let mut right = forward.cross(camera.up);
|
||||
|
||||
// Check if right vector is near zero (forward and up are parallel)
|
||||
if right.mag_sq() < 1e-10 {
|
||||
// Try alternate axes to find a valid right vector
|
||||
let alternate_axes = [Vec3::unit_y(), Vec3::unit_x()];
|
||||
for axis in alternate_axes.iter() {
|
||||
right = forward.cross(*axis);
|
||||
if right.mag_sq() >= 1e-10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
right = right.normalized();
|
||||
let up = right.cross(forward).normalized();
|
||||
|
||||
Self::new(right, up, forward)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
|
||||
pub struct CameraUniform {
|
||||
view_proj: [[f32; 4]; 4],
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn new(aspect_ratio: f32) -> Self {
|
||||
let mut camera = Camera {
|
||||
view_proj: [[0.0; 4]; 4],
|
||||
position: Vec3::new(0.0, 0.5, 3.0),
|
||||
target: Vec3::new(0.0, 0.0, 0.0),
|
||||
up: Vec3::unit_y(),
|
||||
fov: PI / 3.0,
|
||||
aspect_ratio,
|
||||
z_near: 0.1,
|
||||
z_far: 100000.0,
|
||||
rotor: Rotor3::identity(),
|
||||
distance: 1.0,
|
||||
dirty: true,
|
||||
};
|
||||
|
||||
camera.compute_rotor();
|
||||
camera.compute_view_proj_mat();
|
||||
|
||||
camera
|
||||
}
|
||||
|
||||
pub fn compute_view_proj_mat(&mut self) {
|
||||
let view = Mat4::look_at(self.position, self.target, self.up);
|
||||
let proj = projection::rh_yup::perspective_wgpu_dx(
|
||||
self.fov,
|
||||
self.aspect_ratio,
|
||||
self.z_near,
|
||||
self.z_far,
|
||||
);
|
||||
self.view_proj = (proj * view).into();
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
pub fn look_at(&mut self, position: Vec3, target: Vec3) {
|
||||
self.position = position;
|
||||
self.target = target;
|
||||
self.up = Vec3::unit_y();
|
||||
self.compute_rotor();
|
||||
self.dirty = true;
|
||||
self.compute_view_proj_mat();
|
||||
}
|
||||
|
||||
pub fn set_depth_range(&mut self, z_near: f32, z_far: f32) {
|
||||
self.z_near = z_near;
|
||||
self.z_far = z_far.max(z_near + f32::EPSILON);
|
||||
self.dirty = true;
|
||||
self.compute_view_proj_mat();
|
||||
}
|
||||
|
||||
pub fn position(&self) -> Vec3 {
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn 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 {
|
||||
return;
|
||||
}
|
||||
|
||||
let yaw_theta = delta_x * ORBIT_SENSITIVITY;
|
||||
let yaw_rotor =
|
||||
Rotor3::from_angle_plane(yaw_theta, Bivec3::from_normalized_axis(Vec3::unit_y()));
|
||||
|
||||
let basis = OrthonormalBasis::from_camera(self);
|
||||
|
||||
let pitch_angle = (delta_y * ORBIT_SENSITIVITY).clamp(-MAX_PITCH, MAX_PITCH);
|
||||
|
||||
let pitch_rotor =
|
||||
Rotor3::from_angle_plane(pitch_angle, Bivec3::from_normalized_axis(basis.right));
|
||||
|
||||
let orbit_rotor = (yaw_rotor * pitch_rotor).normalized();
|
||||
|
||||
self.rotor = (orbit_rotor * self.rotor).normalized();
|
||||
|
||||
let mut offset = self.position - self.target;
|
||||
if offset.mag_sq() <= f32::EPSILON {
|
||||
offset = Vec3::unit_z() * self.distance.max(MIN_DISTANCE);
|
||||
}
|
||||
|
||||
orbit_rotor.rotate_vec(&mut offset);
|
||||
self.distance = offset.mag().max(MIN_DISTANCE);
|
||||
self.position = offset + self.target;
|
||||
|
||||
self.dirty = true;
|
||||
self.compute_view_proj_mat();
|
||||
}
|
||||
|
||||
pub fn zoom(&mut self, msg: &WheelMessage) {
|
||||
let mut delta = msg.delta_y as f32;
|
||||
|
||||
// Match browser delta modes so the wheel delta is always roughly pixels.
|
||||
match msg.delta_mode {
|
||||
1 => delta *= 16.0,
|
||||
2 => delta *= 800.0,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Scrolling up should zoom in.
|
||||
delta = -delta;
|
||||
|
||||
if delta.abs() <= f32::EPSILON {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get forward direction from camera position to target
|
||||
let mut forward_vec = self.target - self.position;
|
||||
if forward_vec.mag_sq() <= f32::EPSILON {
|
||||
forward_vec = Vec3::unit_z();
|
||||
}
|
||||
let forward_dir = forward_vec.normalized();
|
||||
let current_distance = forward_vec.mag();
|
||||
|
||||
// Scale dolly movement by distance to target for consistent perceived zoom speed
|
||||
let dolly_distance = delta * ZOOM_SENSITIVITY * current_distance;
|
||||
let dolly_translation = forward_dir * dolly_distance;
|
||||
|
||||
self.position += dolly_translation;
|
||||
self.target += dolly_translation;
|
||||
|
||||
self.compute_rotor();
|
||||
self.dirty = true;
|
||||
self.compute_view_proj_mat();
|
||||
}
|
||||
|
||||
pub fn create_uniform_resource(&self, device: &wgpu::Device) -> UniformResource {
|
||||
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: "camera uniform buffer".into(),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
contents: bytemuck::cast_slice(&[self.view_proj]),
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Camera bind group layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Camera bind group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
UniformResource {
|
||||
buffer,
|
||||
bind_group,
|
||||
bind_group_layout,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_rotor(&mut self) {
|
||||
let offset = self.position - self.target;
|
||||
let distance = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt();
|
||||
self.distance = distance.max(MIN_DISTANCE);
|
||||
|
||||
// to compute the initial rotor we will do two rotations
|
||||
// these will orient the camera to the new coordinates
|
||||
//
|
||||
|
||||
// but first we need the orthonormal basis for the current camera
|
||||
let basis = OrthonormalBasis::from_camera(self);
|
||||
|
||||
// first rotation
|
||||
// this is the swing to make position face the target
|
||||
let camera_local_up = Vec3::unit_z();
|
||||
let swing_rotor = Rotor3::from_rotation_between(camera_local_up, -basis.forward);
|
||||
|
||||
// now we need a twist rotor which aligns the camera up
|
||||
let mut up_after_swing = self.up.clone();
|
||||
swing_rotor.rotate_vec(&mut up_after_swing);
|
||||
|
||||
// to rotate a vector by a rotor we need
|
||||
// - a bivector (represents the axis of rotation)
|
||||
// - angle of rotation
|
||||
let twist_axis = (-basis.forward).normalized();
|
||||
let twist_plane = Bivec3::from_normalized_axis(twist_axis);
|
||||
|
||||
// Calculate twist angle between the up vectors:
|
||||
// u1 × uc ⋅ (-f)
|
||||
// θ = atan2( ————————————— , u1 ⋅ uc )
|
||||
// ‖u1 × uc‖
|
||||
//
|
||||
// Where:
|
||||
// u1 = up vector after swing rotation
|
||||
// uc = camera's current up vector
|
||||
// f = forward vector (twist axis)
|
||||
let theta = up_after_swing
|
||||
.cross(self.up)
|
||||
.dot(twist_axis)
|
||||
.atan2(up_after_swing.dot(self.up));
|
||||
|
||||
let twist_rotor = Rotor3::from_angle_plane(theta, twist_plane);
|
||||
|
||||
self.rotor = (swing_rotor * twist_rotor).normalized();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
struct UniformData {
|
||||
mouse_move: vec2<f32>,
|
||||
mouse_click: vec2<f32>,
|
||||
resolution: vec2<f32>,
|
||||
time: 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) color: vec3<f32>
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(1) color: vec3<f32>
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn v_main(in: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.pos = vec4<f32>(in.pos, 1.0);
|
||||
let fluc = sin(modf(uni.time).fract * 3.141592) * 0.3 + 0.7;
|
||||
out.color = in.color * fluc;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn f_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let x = select(0.0, 0.3, distance(in.pos.xy, uni.mouse_move) < 25.0);
|
||||
let y = select(0.0, 0.3, distance(in.pos.xy, uni.mouse_click) < 25.0);
|
||||
return vec4f(in.color + x - y, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
use gltf::Gltf;
|
||||
use ultraviolet::{Mat4, Vec3};
|
||||
use wgpu::TextureFormat;
|
||||
|
||||
use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ModelBounds {
|
||||
pub min: [f32; 3],
|
||||
pub max: [f32; 3],
|
||||
}
|
||||
|
||||
impl ModelBounds {
|
||||
fn new(min: [f32; 3], max: [f32; 3]) -> Self {
|
||||
Self { min, max }
|
||||
}
|
||||
|
||||
fn include_point(&mut self, point: [f32; 3]) {
|
||||
for i in 0..3 {
|
||||
self.min[i] = self.min[i].min(point[i]);
|
||||
self.max[i] = self.max[i].max(point[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ImportError {
|
||||
#[error("failed to fetch the model")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("failed to decode bytes")]
|
||||
GltfParse(#[from] gltf::Error),
|
||||
|
||||
#[error("failed to load model")]
|
||||
LoadError,
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> {
|
||||
use gltf::mesh::util::ReadTexCoords;
|
||||
|
||||
match tex_coords {
|
||||
ReadTexCoords::F32(iter) => iter.collect(),
|
||||
ReadTexCoords::U16(iter) => iter
|
||||
.map(|[u, v]| [u as f32 / u16::MAX as f32, v as f32 / u16::MAX as f32])
|
||||
.collect(),
|
||||
ReadTexCoords::U8(iter) => iter
|
||||
.map(|[u, v]| [u as f32 / u8::MAX as f32, v as f32 / u8::MAX as f32])
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_indices(indices: gltf::mesh::util::ReadIndices<'_>) -> Vec<u32> {
|
||||
use gltf::mesh::util::ReadIndices;
|
||||
|
||||
match indices {
|
||||
ReadIndices::U8(iter) => iter.map(|i| i as u32).collect(),
|
||||
ReadIndices::U16(iter) => iter.map(|i| i as u32).collect(),
|
||||
ReadIndices::U32(iter) => iter.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_node<'a>(
|
||||
node: gltf::Node<'a>,
|
||||
parent_transform: Mat4,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut crate::renderer::GpuResources,
|
||||
meshes: &mut Vec<crate::renderer::scene::Mesh>,
|
||||
data_blob: &[u8],
|
||||
pipeline_index: usize,
|
||||
model_bounds: &mut Option<ModelBounds>,
|
||||
) {
|
||||
let local_transform = Mat4::from(node.transform().matrix());
|
||||
let world_transform = parent_transform * local_transform;
|
||||
let normal_matrix = world_transform.inversed().transposed();
|
||||
|
||||
if let Some(mesh) = node.mesh() {
|
||||
for primitive in mesh.primitives() {
|
||||
let reader = primitive.reader(|buffer| match buffer.source() {
|
||||
gltf::buffer::Source::Bin => Some(&data_blob[..]),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let positions: Vec<[f32; 3]> = match reader.read_positions() {
|
||||
Some(iter) => iter.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
if positions.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let vertex_count = positions.len();
|
||||
|
||||
let default_normal_vec = normal_matrix.transform_vec3(Vec3::unit_y()).normalized();
|
||||
let default_normal = [
|
||||
default_normal_vec.x,
|
||||
default_normal_vec.y,
|
||||
default_normal_vec.z,
|
||||
];
|
||||
|
||||
let mut normals: Vec<[f32; 3]> = reader
|
||||
.read_normals()
|
||||
.map(|iter| {
|
||||
iter.map(|normal| {
|
||||
let vec = Vec3::new(normal[0], normal[1], normal[2]);
|
||||
let transformed = normal_matrix.transform_vec3(vec).normalized();
|
||||
[transformed.x, transformed.y, transformed.z]
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![default_normal; vertex_count]);
|
||||
|
||||
if normals.len() != vertex_count {
|
||||
normals.resize(vertex_count, default_normal);
|
||||
}
|
||||
|
||||
let mut uvs: Vec<[f32; 2]> = reader
|
||||
.read_tex_coords(0)
|
||||
.map(convert_tex_coords)
|
||||
.unwrap_or_else(|| vec![[0.0, 0.0]; vertex_count]);
|
||||
|
||||
if uvs.len() != vertex_count {
|
||||
uvs.resize(vertex_count, [0.0, 0.0]);
|
||||
}
|
||||
|
||||
for position in &positions {
|
||||
let vec = Vec3::new(position[0], position[1], position[2]);
|
||||
let transformed = world_transform.transform_point3(vec);
|
||||
let world_point = [transformed.x, transformed.y, transformed.z];
|
||||
if let Some(bounds) = model_bounds.as_mut() {
|
||||
bounds.include_point(world_point);
|
||||
} else {
|
||||
*model_bounds = Some(ModelBounds::new(world_point, world_point));
|
||||
}
|
||||
}
|
||||
|
||||
let indices: Vec<u32> = reader
|
||||
.read_indices()
|
||||
.map(convert_indices)
|
||||
.unwrap_or_else(|| (0..vertex_count as u32).collect());
|
||||
|
||||
if indices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
for child in node.children() {
|
||||
visit_node(
|
||||
child,
|
||||
world_transform,
|
||||
device,
|
||||
resources,
|
||||
meshes,
|
||||
data_blob,
|
||||
pipeline_index,
|
||||
model_bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_gltf_model(
|
||||
device: &wgpu::Device,
|
||||
resources: &mut crate::renderer::GpuResources,
|
||||
meshes: &mut Vec<crate::renderer::scene::Mesh>,
|
||||
surface_format: TextureFormat,
|
||||
) -> Result<Option<ModelBounds>, ImportError> {
|
||||
let glb_data = reqwest::get("http://localhost:8080/themanor.glb")
|
||||
.await?
|
||||
.bytes()
|
||||
.await?;
|
||||
|
||||
let model = Gltf::from_slice(&glb_data)?;
|
||||
let data_blob = model.blob.as_ref().ok_or(ImportError::LoadError)?;
|
||||
|
||||
let vertex_layout = mesh_vertex_layout();
|
||||
|
||||
let pipeline_index = resources.get_or_create_pipeline(
|
||||
device,
|
||||
"gltf_standard",
|
||||
&vertex_layout,
|
||||
include_str!("./gltf.wgsl"),
|
||||
surface_format,
|
||||
);
|
||||
|
||||
let mut model_bounds: Option<ModelBounds> = None;
|
||||
|
||||
for scene in model.scenes() {
|
||||
for node in scene.nodes() {
|
||||
visit_node(
|
||||
node,
|
||||
Mat4::identity(),
|
||||
device,
|
||||
resources,
|
||||
meshes,
|
||||
data_blob,
|
||||
pipeline_index,
|
||||
&mut model_bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(model_bounds)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use core::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WindowEvent {
|
||||
Resize(ResizeMessage),
|
||||
PointerMove(MouseMessage),
|
||||
PointerClick(MouseMessage),
|
||||
PointerWheel(WheelMessage),
|
||||
Keyboard(KeyboardMessage),
|
||||
}
|
||||
|
||||
// Display for WindowEvent
|
||||
impl fmt::Display for WindowEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
WindowEvent::Resize(msg) => write!(f, "Resize: {:?}", msg),
|
||||
WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg),
|
||||
WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg),
|
||||
WindowEvent::PointerWheel(msg) => write!(f, "PointerWheel: {:?}", msg),
|
||||
WindowEvent::Keyboard(msg) => write!(f, "Keyboard: {:?}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResizeMessage {
|
||||
pub scale_factor: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MouseMessage {
|
||||
pub scale_factor: f64,
|
||||
pub button: f64,
|
||||
pub buttons: u16,
|
||||
pub client_x: f64,
|
||||
pub client_y: f64,
|
||||
pub movement_x: f64,
|
||||
pub movement_y: f64,
|
||||
pub offset_x: f64,
|
||||
pub offset_y: f64,
|
||||
}
|
||||
|
||||
impl MouseMessage {
|
||||
pub fn from_evt(event: web_sys::MouseEvent) -> Self {
|
||||
let window = web_sys::window().unwrap();
|
||||
Self {
|
||||
scale_factor: window.device_pixel_ratio(),
|
||||
button: event.button() as f64,
|
||||
buttons: event.buttons(),
|
||||
client_x: event.client_x() as f64,
|
||||
client_y: event.client_y() as f64,
|
||||
movement_x: event.movement_x() as f64,
|
||||
movement_y: event.movement_y() as f64,
|
||||
offset_x: event.offset_x() as f64,
|
||||
offset_y: event.offset_y() as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WheelMessage {
|
||||
pub scale_factor: f64,
|
||||
pub delta_x: f64,
|
||||
pub delta_y: f64,
|
||||
pub delta_z: f64,
|
||||
pub delta_mode: u32,
|
||||
pub client_x: f64,
|
||||
pub client_y: f64,
|
||||
}
|
||||
|
||||
impl WheelMessage {
|
||||
pub fn from_evt(event: web_sys::WheelEvent) -> Self {
|
||||
let window = web_sys::window().unwrap();
|
||||
Self {
|
||||
scale_factor: window.device_pixel_ratio(),
|
||||
delta_x: event.delta_x(),
|
||||
delta_y: event.delta_y(),
|
||||
delta_z: event.delta_z(),
|
||||
delta_mode: event.delta_mode(),
|
||||
client_x: event.client_x() as f64,
|
||||
client_y: event.client_y() as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,5 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod web;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native;
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
pub mod worker;
|
||||
|
||||
pub fn get_canvas_element(selectors: &str) -> web_sys::HtmlCanvasElement {
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let element = document.query_selector(selectors).unwrap().unwrap();
|
||||
let canvas = element.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
|
||||
let scale_factor = window.device_pixel_ratio();
|
||||
let width = (canvas.client_width() as f64 * scale_factor) as u32;
|
||||
let height = (canvas.client_height() as f64 * scale_factor) as u32;
|
||||
canvas.set_width(width);
|
||||
canvas.set_height(height);
|
||||
canvas
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::message::WindowEvent;
|
||||
use log::info;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
|
||||
use wasm_bindgen::{prelude::*, JsValue};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::MessageEvent;
|
||||
|
||||
/// Binds JS.
|
||||
#[wasm_bindgen(module = "/src/platform/web/worker/workerGen.js")]
|
||||
extern "C" {
|
||||
/// Spawn new worker in JS side in order to make bundler know about dependency.
|
||||
#[wasm_bindgen(js_name = "createWorker")]
|
||||
fn create_worker(kind: &str, name: &str) -> web_sys::Worker;
|
||||
}
|
||||
|
||||
/// Binds JS.
|
||||
/// This makes wasm-bindgen bring `mainWorker.js` to the `pkg` directory.
|
||||
/// So that bundler can bundle it together.
|
||||
#[wasm_bindgen(module = "/src/platform/web/worker/mainWorker.js")]
|
||||
extern "C" {
|
||||
/// Nothing to do.
|
||||
#[wasm_bindgen]
|
||||
fn attachMain();
|
||||
}
|
||||
|
||||
pub struct MainWorker {
|
||||
handle: web_sys::Worker,
|
||||
name: String,
|
||||
_callback: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
impl Drop for MainWorker {
|
||||
/// Terminates web worker *immediately*.
|
||||
fn drop(&mut self) {
|
||||
self.handle.terminate();
|
||||
info!("Worker({}) was terminated", &self.name);
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for MainWorker {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MainWorker")
|
||||
.field("handle", &self.handle)
|
||||
.field("name", &self.name)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MainWorker {
|
||||
/// Spawns main worker from the window context.
|
||||
pub fn spawn(
|
||||
name: &str,
|
||||
id: usize,
|
||||
f: impl FnOnce() + Send + 'static,
|
||||
) -> Result<Self, JsValue> {
|
||||
// Creates a new worker.
|
||||
let handle = create_worker("main", name);
|
||||
|
||||
// Double-boxing because `dyn FnOnce` is unsized and so `Box<dyn FnOnce()>` has
|
||||
// an undefined layout (although I think in practice its a pointer and a length?).
|
||||
let ptr = Box::into_raw(Box::new(Box::new(f) as Box<dyn FnOnce()>));
|
||||
|
||||
// Sets default callback.
|
||||
let callback = Closure::new(|_ev| {
|
||||
info!("got a message..canvas?");
|
||||
});
|
||||
handle.set_onmessage(Some(callback.as_ref().unchecked_ref()));
|
||||
|
||||
let msg: js_sys::Array = [
|
||||
&wasm_bindgen::module(),
|
||||
&id.into(),
|
||||
&wasm_bindgen::memory(),
|
||||
&JsValue::from(ptr as u32),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
info!("posting message");
|
||||
handle.post_message(&msg)?;
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
name: name.to_owned(),
|
||||
_callback: callback,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transfer_ownership(&self, canvas: &web_sys::HtmlCanvasElement) {
|
||||
let offscreen_canvas = canvas.transfer_control_to_offscreen().unwrap();
|
||||
let transfer_list = js_sys::Array::new();
|
||||
transfer_list.push(&offscreen_canvas);
|
||||
|
||||
info!("posting canvas (is_undefined: {})", canvas.is_undefined());
|
||||
self.handle
|
||||
.post_message_with_transfer(&offscreen_canvas, &transfer_list)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
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::<T>::new(canvas, events_chan).await));
|
||||
Renderer::run_render_loop(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MainWorker {
|
||||
type Target = web_sys::Worker;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.handle
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_canvas_transfer() -> web_sys::OffscreenCanvas {
|
||||
let global = js_sys::global().unchecked_into::<web_sys::DedicatedWorkerGlobalScope>();
|
||||
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _reject| {
|
||||
let handler = Closure::once(move |event: MessageEvent| {
|
||||
let data = event.data();
|
||||
|
||||
info!("data received: {:?}", data);
|
||||
|
||||
// Check if the received data is an OffscreenCanvas directly
|
||||
if data.is_instance_of::<web_sys::OffscreenCanvas>() {
|
||||
resolve
|
||||
.call1(&JsValue::NULL, &data)
|
||||
.expect("resolve failed");
|
||||
}
|
||||
});
|
||||
|
||||
global.set_onmessage(Some(handler.as_ref().unchecked_ref()));
|
||||
handler.forget();
|
||||
});
|
||||
|
||||
let canvas: web_sys::OffscreenCanvas = JsFuture::from(promise)
|
||||
.await
|
||||
.expect("promise rejected")
|
||||
.unchecked_into();
|
||||
|
||||
info!("received canvas: {:?}", canvas);
|
||||
canvas
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export function createWorker(kind, name) {
|
||||
switch (kind) {
|
||||
case 'main':
|
||||
const main = new Worker(new URL('./mainWorker.js', import.meta.url), {
|
||||
type: 'module',
|
||||
/* @vite-ignore */ name, // vite doesn't allow non static value here.
|
||||
});
|
||||
return main;
|
||||
default:
|
||||
console.log("unsurpported type of worker: ", kind);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
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, JsFuture};
|
||||
use web_sys::{DedicatedWorkerGlobalScope, File, MessageEvent};
|
||||
|
||||
use crate::{
|
||||
gltf::{load_gltf_model, ImportError, ModelBounds},
|
||||
message::{MouseMessage, ResizeMessage, WindowEvent},
|
||||
renderer::scene::Scene,
|
||||
};
|
||||
|
||||
pub mod scene;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use scene::Mesh;
|
||||
|
||||
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
|
||||
pub struct GpuResources {
|
||||
// Core resources
|
||||
buffers: Vec<wgpu::Buffer>,
|
||||
pipelines: Vec<wgpu::RenderPipeline>,
|
||||
textures: Vec<wgpu::Texture>,
|
||||
|
||||
// Layout management
|
||||
pipeline_layouts: Vec<wgpu::PipelineLayout>,
|
||||
bind_group_layouts: Vec<wgpu::BindGroupLayout>,
|
||||
|
||||
// Simple name-based pipeline lookup
|
||||
pipeline_registry: HashMap<String, usize>,
|
||||
|
||||
// Shader modules cache
|
||||
shader_modules: HashMap<String, wgpu::ShaderModule>,
|
||||
}
|
||||
|
||||
impl GpuResources {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buffers: Vec::new(),
|
||||
pipelines: Vec::new(),
|
||||
textures: Vec::new(),
|
||||
pipeline_layouts: Vec::new(),
|
||||
bind_group_layouts: Vec::new(),
|
||||
pipeline_registry: HashMap::new(),
|
||||
shader_modules: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_position_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex<Position> {
|
||||
let index = self.buffers.len() as u32;
|
||||
self.buffers.push(buffer);
|
||||
BufferIndex {
|
||||
index,
|
||||
_buffer_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_normal_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex<Normal> {
|
||||
let index = self.buffers.len() as u32;
|
||||
self.buffers.push(buffer);
|
||||
BufferIndex {
|
||||
index,
|
||||
_buffer_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_uv_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex<UV> {
|
||||
let index = self.buffers.len() as u32;
|
||||
self.buffers.push(buffer);
|
||||
BufferIndex {
|
||||
index,
|
||||
_buffer_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_index_buffer(&mut self, buffer: wgpu::Buffer) -> BufferIndex<Index> {
|
||||
let index = self.buffers.len() as u32;
|
||||
self.buffers.push(buffer);
|
||||
BufferIndex {
|
||||
index,
|
||||
_buffer_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
pub fn create_pipeline(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
name: &str,
|
||||
vertex_layout: &[wgpu::VertexBufferLayout],
|
||||
shader_source: &str,
|
||||
surface_format: wgpu::TextureFormat,
|
||||
) -> Result<usize, String> {
|
||||
if self.pipeline_registry.contains_key(name) {
|
||||
return Err(format!("Pipeline '{}' already exists", name));
|
||||
}
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(name),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let layout = self.get_or_create_pipeline_layout(device, name);
|
||||
|
||||
// Determine entry points based on pipeline name
|
||||
let (vertex_entry, fragment_entry) = match name {
|
||||
"triangle_colored" => ("v_main", "f_main"),
|
||||
_ => ("vs_main", "fs_main"),
|
||||
};
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(name),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some(vertex_entry),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
buffers: vertex_layout,
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
unclipped_depth: false,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: DEPTH_FORMAT,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some(fragment_entry),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: surface_format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let index = self.pipelines.len();
|
||||
self.pipelines.push(pipeline);
|
||||
self.pipeline_registry.insert(name.to_string(), index);
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
pub fn get_pipeline(&self, name: &str) -> Option<usize> {
|
||||
self.pipeline_registry.get(name).copied()
|
||||
}
|
||||
|
||||
pub fn get_or_create_pipeline(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
name: &str,
|
||||
vertex_layout: &[wgpu::VertexBufferLayout],
|
||||
shader_source: &str,
|
||||
surface_format: wgpu::TextureFormat,
|
||||
) -> usize {
|
||||
if let Some(index) = self.get_pipeline(name) {
|
||||
return index;
|
||||
}
|
||||
|
||||
self.create_pipeline(device, name, vertex_layout, shader_source, surface_format)
|
||||
.expect(&format!("Failed to create pipeline '{}'", name))
|
||||
}
|
||||
|
||||
pub fn get_pipeline_by_index(&self, index: usize) -> &wgpu::RenderPipeline {
|
||||
&self.pipelines[index]
|
||||
}
|
||||
|
||||
pub fn set_bind_group_layouts(&mut self, layouts: &[wgpu::BindGroupLayout; 2]) {
|
||||
self.bind_group_layouts = layouts.to_vec();
|
||||
}
|
||||
|
||||
fn get_or_create_pipeline_layout(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
label: &str,
|
||||
) -> wgpu::PipelineLayout {
|
||||
if self.pipeline_layouts.is_empty() {
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some(label),
|
||||
bind_group_layouts: &self.bind_group_layouts.iter().collect::<Vec<_>>(),
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
self.pipeline_layouts.push(layout);
|
||||
}
|
||||
self.pipeline_layouts[0].clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GpuResources {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BufferIndex<T> {
|
||||
pub index: u32,
|
||||
_buffer_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> BufferIndex<T> {
|
||||
pub fn new(index: u32) -> Self {
|
||||
Self {
|
||||
index,
|
||||
_buffer_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kinds of buffers supported
|
||||
pub struct Position;
|
||||
pub struct Normal;
|
||||
pub struct UV;
|
||||
pub struct Index;
|
||||
pub struct ModelMatrix;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
) -> (wgpu::Texture, wgpu::TextureView) {
|
||||
let size = wgpu::Extent3d {
|
||||
width: config.width.max(1),
|
||||
height: config.height.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("depth texture"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: DEPTH_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
(texture, view)
|
||||
}
|
||||
|
||||
fn recreate_depth_texture(&mut self) {
|
||||
let (texture, view) =
|
||||
Self::create_depth_texture(&self.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 {
|
||||
let id = wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::BROWSER_WEBGPU,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let instance = wgpu::Instance::new(&id);
|
||||
let surface = instance
|
||||
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))
|
||||
.unwrap();
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("Adapter info: {:?}", adapter.get_info());
|
||||
info!("Adapter features: {:?}", adapter.features());
|
||||
info!("Adapter limits: {:?}", adapter.limits());
|
||||
|
||||
let descriptor = wgpu::DeviceDescriptor {
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
label: None,
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
trace: wgpu::Trace::default(),
|
||||
};
|
||||
|
||||
let (device, queue) = adapter.request_device(&descriptor).await.unwrap();
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_caps.formats[0],
|
||||
width: canvas.clone().width(),
|
||||
height: canvas.clone().height(),
|
||||
present_mode: surface_caps.present_modes[0],
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
info!(
|
||||
"suface size: {} x {}",
|
||||
surface_config.width, surface_config.height
|
||||
);
|
||||
surface.configure(&device, &surface_config);
|
||||
|
||||
let (depth_texture, depth_view) = Self::create_depth_texture(&device, &surface_config);
|
||||
|
||||
let mut resources = GpuResources::new();
|
||||
let context = RendererContext {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
surface_config,
|
||||
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.context, &mut self.resources);
|
||||
|
||||
let surface_texture = self.context.surface.get_current_texture().unwrap();
|
||||
let texture_view = surface_texture.texture.create_view(&Default::default());
|
||||
let mut encoder =
|
||||
self.context
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render command encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Render pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
depth_slice: None,
|
||||
view: &texture_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.context.depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
});
|
||||
|
||||
for (i, bind_group) in self.scene.bind_groups().iter().enumerate() {
|
||||
render_pass.set_bind_group(i as u32, bind_group, &[]);
|
||||
}
|
||||
|
||||
for mesh in self.scene.meshes() {
|
||||
render_pass.set_pipeline(self.resources.get_pipeline_by_index(mesh.pipeline_index));
|
||||
|
||||
render_pass.set_vertex_buffer(
|
||||
0,
|
||||
self.resources
|
||||
.get_buffer(&mesh.position_buffer_index)
|
||||
.slice(..),
|
||||
);
|
||||
render_pass.set_vertex_buffer(
|
||||
1,
|
||||
self.resources
|
||||
.get_buffer(&mesh.normal_buffer_index)
|
||||
.slice(..),
|
||||
);
|
||||
render_pass.set_vertex_buffer(
|
||||
2,
|
||||
self.resources.get_buffer(&mesh.uv_buffer_index).slice(..),
|
||||
);
|
||||
render_pass.set_vertex_buffer(
|
||||
3,
|
||||
self.resources
|
||||
.get_buffer(&mesh.model_buffer_index)
|
||||
.slice(..),
|
||||
);
|
||||
|
||||
render_pass.set_index_buffer(
|
||||
self.resources
|
||||
.get_buffer(&mesh.index_buffer_index)
|
||||
.slice(..),
|
||||
mesh.index_format,
|
||||
);
|
||||
|
||||
render_pass.draw_indexed(0..mesh.index_count, 0, 0..mesh.instance_count);
|
||||
}
|
||||
}
|
||||
self.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) => {
|
||||
renderer.borrow_mut().mouse_move(msg);
|
||||
}
|
||||
WindowEvent::Resize(msg) => {
|
||||
renderer.borrow_mut().resize(msg);
|
||||
}
|
||||
WindowEvent::PointerClick(msg) => {
|
||||
{
|
||||
let mut r = renderer.borrow_mut();
|
||||
let x = (msg.offset_x * msg.scale_factor) as f32;
|
||||
let y = (msg.offset_y * msg.scale_factor) as f32;
|
||||
r.scene.handle_mouse_click(x, y);
|
||||
log::info!("clicked");
|
||||
}
|
||||
|
||||
// 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.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<T>>>) {
|
||||
let render_frame: Closure<dyn FnMut(f32)> = Closure::new(move |time: f32| {
|
||||
{
|
||||
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 {
|
||||
Self::handle_event(renderer_clone, event).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if let Ok(mut r) = renderer.try_borrow_mut() {
|
||||
r.render(time);
|
||||
}
|
||||
}
|
||||
|
||||
Self::run_render_loop(renderer.clone());
|
||||
});
|
||||
|
||||
let global = js_sys::global().unchecked_into::<DedicatedWorkerGlobalScope>();
|
||||
|
||||
global
|
||||
.request_animation_frame(render_frame.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
|
||||
render_frame.forget();
|
||||
}
|
||||
|
||||
fn resize(&mut self, msg: ResizeMessage) {
|
||||
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.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.resize(
|
||||
new_width as f64,
|
||||
new_height as f64,
|
||||
msg.scale_factor,
|
||||
&self.context.queue,
|
||||
);
|
||||
|
||||
info!(
|
||||
"Resized: ({}, {}), scale: {}",
|
||||
new_width, new_height, msg.scale_factor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mouse_move(&mut self, msg: MouseMessage) {
|
||||
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.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<T>>>) -> Result<(), ImportError> {
|
||||
let (device, surface_format) = {
|
||||
let r = renderer.borrow();
|
||||
(
|
||||
r.context.device.clone(),
|
||||
r.context.surface_config.format,
|
||||
)
|
||||
};
|
||||
|
||||
let mut meshes = Vec::new();
|
||||
|
||||
let mut original_resources = {
|
||||
let mut r = renderer.borrow_mut();
|
||||
r.scene.clear();
|
||||
std::mem::take(&mut r.resources)
|
||||
};
|
||||
|
||||
let bounds = load_gltf_model(
|
||||
&device,
|
||||
&mut original_resources,
|
||||
&mut meshes,
|
||||
surface_format,
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut r = renderer.borrow_mut();
|
||||
r.resources = original_resources;
|
||||
|
||||
for mesh in meshes {
|
||||
r.scene.add_mesh(mesh);
|
||||
}
|
||||
|
||||
if let Some(ModelBounds { min, max }) = bounds {
|
||||
let center = ultraviolet::Vec3::new(
|
||||
(min[0] + max[0]) * 0.5,
|
||||
(min[1] + max[1]) * 0.5,
|
||||
(min[2] + max[2]) * 0.5,
|
||||
);
|
||||
|
||||
let extent =
|
||||
ultraviolet::Vec3::new(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
|
||||
let radius =
|
||||
0.5 * (extent.x * extent.x + extent.y * extent.y + extent.z * extent.z).sqrt();
|
||||
let radius = radius.max(1.0);
|
||||
|
||||
// set the camera position after load, so we are not disoriented
|
||||
let eye_offset = ultraviolet::Vec3::new(0.0, radius * 0.05, radius * 0.25);
|
||||
|
||||
// Keep the near plane proportional to the model size to avoid
|
||||
// extreme depth ranges when loading very large assets
|
||||
let near_plane = (radius * 0.001).max(0.1);
|
||||
|
||||
// The far plane must be far enough to cover the entire model.
|
||||
// Using a fixed upper clamp caused large models to be clipped
|
||||
// completely; relying on the model radius instead.
|
||||
let far_plane = (radius * 4.0).max(near_plane + 1.0);
|
||||
r.scene.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: scene::Scene> From<BufferIndex<T>> for u32 {
|
||||
fn from(value: BufferIndex<T>) -> Self {
|
||||
value.index
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
use ultraviolet::Mat4;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
renderer::{self, BufferIndex, GpuResources, Index, ModelMatrix, Normal, Position, UV},
|
||||
};
|
||||
|
||||
pub struct UniformResource {
|
||||
pub buffer: wgpu::Buffer,
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
pub bind_group_layout: wgpu::BindGroupLayout,
|
||||
}
|
||||
|
||||
/// Simple uniform data.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)]
|
||||
pub struct FrameMetadata {
|
||||
pub mouse_move: [f32; 2],
|
||||
pub mouse_click: [f32; 2],
|
||||
pub resolution: [f32; 2],
|
||||
time: f32,
|
||||
_padding0: f32,
|
||||
pub camera_position: [f32; 4],
|
||||
}
|
||||
|
||||
impl FrameMetadata {
|
||||
pub fn new(dimension: ultraviolet::Vec2) -> Self {
|
||||
FrameMetadata {
|
||||
resolution: dimension.into(),
|
||||
mouse_move: [std::f32::MIN, std::f32::MIN],
|
||||
mouse_click: [std::f32::MIN, std::f32::MIN],
|
||||
_padding0: 0.0,
|
||||
camera_position: [0.0, 0.0, 0.0, 1.0],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_camera_position(&mut self, position: ultraviolet::Vec3) {
|
||||
self.camera_position = [position.x, position.y, position.z, 1.0];
|
||||
}
|
||||
|
||||
pub fn 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"),
|
||||
contents: bytemuck::cast_slice(&[self][..]),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Uniform bind group layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Uniform bind group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
UniformResource {
|
||||
buffer,
|
||||
bind_group_layout,
|
||||
bind_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Mesh {
|
||||
pub pipeline_index: usize,
|
||||
pub position_buffer_index: BufferIndex<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,
|
||||
pub instance_count: u32,
|
||||
}
|
||||
|
||||
type VertexBufferSet = (BufferIndex<Position>, BufferIndex<Normal>, BufferIndex<UV>);
|
||||
type IndexBufferInfo = (BufferIndex<Index>, u32, wgpu::IndexFormat);
|
||||
|
||||
pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 4] {
|
||||
[
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
}],
|
||||
},
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
}],
|
||||
},
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: 8,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
}],
|
||||
},
|
||||
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, M> {
|
||||
indices: I,
|
||||
vertices: V,
|
||||
pipeline: P,
|
||||
model_matrix: M,
|
||||
instance_count: u32,
|
||||
}
|
||||
|
||||
impl Default for MeshBuilder<(), (), (), ()> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indices: (),
|
||||
vertices: (),
|
||||
pipeline: (),
|
||||
model_matrix: (),
|
||||
instance_count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, M> MeshBuilder<(), (), P, M> {
|
||||
pub fn with_vertices(
|
||||
self,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut GpuResources,
|
||||
positions: &[[f32; 3]],
|
||||
normals: &[[f32; 3]],
|
||||
uvs: &[[f32; 2]],
|
||||
) -> MeshBuilder<(), VertexBufferSet, P, M> {
|
||||
let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Positions"),
|
||||
contents: bytemuck::cast_slice(positions),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let normal_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Normals"),
|
||||
contents: bytemuck::cast_slice(normals),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let uv_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh UVs"),
|
||||
contents: bytemuck::cast_slice(uvs),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let position_buffer_index = resources.add_position_buffer(position_buffer);
|
||||
let normal_buffer_index = resources.add_normal_buffer(normal_buffer);
|
||||
let uv_buffer_index = resources.add_uv_buffer(uv_buffer);
|
||||
|
||||
MeshBuilder {
|
||||
vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index),
|
||||
indices: self.indices,
|
||||
pipeline: self.pipeline,
|
||||
model_matrix: self.model_matrix,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, M> {
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Indices"),
|
||||
contents: bytemuck::cast_slice(indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
let index_buffer_index = resources.add_index_buffer(index_buffer);
|
||||
|
||||
MeshBuilder {
|
||||
indices: (
|
||||
index_buffer_index,
|
||||
indices.len() as u32,
|
||||
wgpu::IndexFormat::Uint32,
|
||||
),
|
||||
vertices: self.vertices,
|
||||
pipeline: self.pipeline,
|
||||
model_matrix: self.model_matrix,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<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,
|
||||
instance_count: self.instance_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn camera_mut(&mut self) -> Option<&mut Camera> {
|
||||
None
|
||||
}
|
||||
|
||||
fn uniform_buffers(&self) -> Option<&[wgpu::Buffer]> {
|
||||
None
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
renderer_context: &renderer::RendererContext,
|
||||
_resources: &mut GpuResources,
|
||||
) {
|
||||
let camera_position = if let Some(cam) = self.camera_mut() {
|
||||
cam.position()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
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 view_proj_copy = if let Some(cam) = self.camera_mut() {
|
||||
cam.view_proj
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(buffers) = self.uniform_buffers() {
|
||||
if buffers.len() >= 2 {
|
||||
renderer_context.queue.write_buffer(
|
||||
&buffers[0],
|
||||
0,
|
||||
bytemuck::cast_slice(&[fm_copy]),
|
||||
);
|
||||
renderer_context.queue.write_buffer(
|
||||
&buffers[1],
|
||||
0,
|
||||
bytemuck::cast_slice(&[view_proj_copy]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user