gltf models loader (#10)

* use channels for comms

this also refactors the app setup and messaging modules

* remove double logging

* load single model gltf files

* add gltf loader

* add sponza model

* camera with rotors (#11)

* orbit with rotors

* add dolly behaviour to emulate zoom
This commit is contained in:
Akash Shakdwipeea
2025-10-04 12:47:30 +05:30
committed by GitHub
parent 3b81926ae1
commit cb65a52e95
15 changed files with 2677 additions and 255 deletions
+1
View File
@@ -0,0 +1 @@
static/sponza.glb filter=lfs diff=lfs merge=lfs -text
+18
View File
@@ -0,0 +1,18 @@
# Build, Lint, and Test Commands
- `npm run dev`: Start Vite dev server with hot reload for WASM bundle
- `npm run build`: Build optimized WASM and JS in `dist/` for development
- `npm run build-release`: Build optimized WASM and JS for production
- `cargo check`: Validate Rust sources quickly before full builds
- `cargo fmt`: Format Rust code with rustfmt
- No unit tests currently exist; add them as `*_tests.rs` modules
# Code Style Guidelines
- **Rust 2021 idioms**: Use snake_case for modules, files, functions, and variables
- **Indentation**: 4 spaces (configured in rustfmt)
- **Imports**: Group std library, external crates, then local modules
- **Types**: Use descriptive struct fields and enum variants (e.g., `positions`, `normals`)
- **Error handling**: Use `thiserror` derive macro for custom error types
- **Naming**: Mirror GLTF semantics explicitly in struct fields
- **WGSL shaders**: Keep binding names aligned with Rust bind group layouts
- **JavaScript/TypeScript**: Format with prettier defaults
- **Comments**: Add documentation comments for public APIs using `///`
Generated
+1204 -17
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -37,6 +37,11 @@ web-sys = { version = "0.3.77", features = [
"Blob",
"BlobPropertyBag",
"Url",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Headers"
]}
js-sys = "0.3.77"
bytemuck = { version = "1.23.1", features = [
@@ -45,6 +50,9 @@ bytemuck = { version = "1.23.1", features = [
cgmath = "0.18"
raw-window-handle = "0.6.2"
wgpu = "26.0.1"
reqwest = { version = "0.12.23", features = ["json"] }
thiserror = "2.0.15"
ultraviolet = "0.10.0"
[dependencies.gltf]
version = "1.4"
+298
View File
@@ -0,0 +1,298 @@
use std::f32::consts::PI;
use ultraviolet::{projection, Bivec3, Mat4, Rotor3, Vec3, Vec4};
use wgpu::util::DeviceExt;
use crate::{message::WheelMessage, renderer::scene::UniformResource};
const MIN_DISTANCE: f32 = 0.1;
const MAX_PITCH: f32 = PI / 2.0 - 0.01;
const ORBIT_SENSITIVITY: f32 = 0.005;
const ZOOM_SENSITIVITY: f32 = 0.002;
#[repr(C)]
pub struct Camera {
// Hot data - cached computed matrix (64 bytes, 1 cache line)
pub view_proj: [[f32; 4]; 4],
// Warm data - frequently accessed vectors (36 bytes)
position: Vec3,
target: Vec3,
up: Vec3,
// Cold data - projection parameters (16 bytes)
fov: f32,
aspect_ratio: f32,
z_near: f32,
z_far: f32,
// Rotor orientation + spherical coordinates for orbit camera behaviour
rotor: Rotor3,
distance: f32,
yaw: f32,
pitch: f32,
// Dirty flag for lazy evaluation
dirty: bool,
}
struct OrthonormalBasis {
right: Vec3,
up: Vec3,
forward: Vec3,
}
impl OrthonormalBasis {
pub fn new(right: Vec3, up: Vec3, forward: Vec3) -> Self {
Self { right, up, forward }
}
pub fn from_camera(camera: &Camera) -> Self {
let mut forward_offset = camera.target - camera.position;
if forward_offset.mag_sq() <= f32::EPSILON {
forward_offset = -Vec3::unit_z();
}
let forward = forward_offset.normalized();
let mut right = forward.cross(camera.up);
// Check if right vector is near zero (forward and up are parallel)
if right.mag_sq() < 1e-10 {
// Try alternate axes to find a valid right vector
let alternate_axes = [Vec3::unit_y(), Vec3::unit_x()];
for axis in alternate_axes.iter() {
right = forward.cross(*axis);
if right.mag_sq() >= 1e-10 {
break;
}
}
}
right = right.normalized();
let up = right.cross(forward).normalized();
Self::new(right, up, forward)
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct CameraUniform {
view_proj: [[f32; 4]; 4],
}
impl Camera {
pub fn new(aspect_ratio: f32) -> Self {
let mut camera = Camera {
view_proj: [[0.0; 4]; 4],
position: Vec3::new(0.0, 1.5, 0.0),
target: Vec3::zero(),
up: Vec3::unit_y(),
fov: PI / 3.0,
aspect_ratio,
z_near: 0.1,
z_far: 100000.0,
rotor: Rotor3::identity(),
distance: 1.0,
yaw: 0.0,
pitch: 0.0,
dirty: true,
};
camera.compute_rotor();
camera.compute_view_proj_mat();
camera
}
pub fn compute_view_proj_mat(&mut self) {
let view = Mat4::look_at(self.position, self.target, self.up);
let proj = projection::rh_yup::perspective_wgpu_dx(
self.fov,
self.aspect_ratio,
self.z_near,
self.z_far,
);
self.view_proj = (proj * view).into();
self.dirty = false;
}
pub fn look_at(&mut self, position: Vec3, target: Vec3) {
self.position = position;
self.target = target;
self.up = Vec3::unit_y();
self.compute_rotor();
self.dirty = true;
self.compute_view_proj_mat();
}
pub fn set_depth_range(&mut self, z_near: f32, z_far: f32) {
self.z_near = z_near;
self.z_far = z_far.max(z_near + f32::EPSILON);
self.dirty = true;
self.compute_view_proj_mat();
}
pub fn position(&self) -> Vec3 {
self.position
}
pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
// Skip tiny movements to reduce unnecessary computations
if delta_x.abs() < 0.001 && delta_y.abs() < 0.001 {
return;
}
let yaw_theta = delta_x * ORBIT_SENSITIVITY;
let yaw_rotor =
Rotor3::from_angle_plane(yaw_theta, Bivec3::from_normalized_axis(Vec3::unit_y()));
let basis = OrthonormalBasis::from_camera(self);
let desired_pitch = (self.pitch - delta_y * ORBIT_SENSITIVITY).clamp(-MAX_PITCH, MAX_PITCH);
let applied_pitch = desired_pitch - self.pitch;
let pitch_rotor =
Rotor3::from_angle_plane(applied_pitch, Bivec3::from_normalized_axis(basis.right));
let orbit_rotor = (yaw_rotor * pitch_rotor).normalized();
self.rotor = (orbit_rotor * self.rotor).normalized();
let mut offset = self.position - self.target;
if offset.mag_sq() <= f32::EPSILON {
offset = Vec3::unit_z() * self.distance.max(MIN_DISTANCE);
}
orbit_rotor.rotate_vec(&mut offset);
self.distance = offset.mag().max(MIN_DISTANCE);
self.position = offset + self.target;
self.yaw += yaw_theta;
self.pitch = desired_pitch;
self.dirty = true;
self.compute_view_proj_mat();
}
pub fn zoom(&mut self, msg: &WheelMessage) {
let mut delta = msg.delta_y as f32;
// Match browser delta modes so the wheel delta is always roughly pixels.
match msg.delta_mode {
1 => delta *= 16.0,
2 => delta *= 800.0,
_ => {}
}
// Scrolling up should zoom in.
delta = -delta;
if delta.abs() <= f32::EPSILON {
return;
}
// Get forward direction from camera position to target
let mut forward_vec = self.target - self.position;
if forward_vec.mag_sq() <= f32::EPSILON {
forward_vec = Vec3::unit_z();
}
let forward_dir = forward_vec.normalized();
let current_distance = forward_vec.mag();
// Scale dolly movement by distance to target for consistent perceived zoom speed
let dolly_distance = delta * ZOOM_SENSITIVITY * current_distance;
let dolly_translation = forward_dir * dolly_distance;
self.position += dolly_translation;
self.target += dolly_translation;
self.compute_rotor();
self.dirty = true;
self.compute_view_proj_mat();
}
pub fn create_uniform_resource(&self, device: &wgpu::Device) -> UniformResource {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: "camera uniform buffer".into(),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
contents: bytemuck::cast_slice(&[self.view_proj]),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform bind group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 1,
resource: buffer.as_entire_binding(),
}],
});
UniformResource {
buffer,
bind_group,
bind_group_layout,
}
}
fn compute_rotor(&mut self) {
let offset = self.position - self.target;
let distance = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt();
self.distance = distance.max(MIN_DISTANCE);
// to compute the initial rotor we will do two rotations
// these will orient the camera to the new coordinates
//
// but first we need the orthonormal basis for the current camera
let basis = OrthonormalBasis::from_camera(self);
// first rotation
// this is the swing to make position face the target
let camera_local_up = Vec3::unit_z();
let swing_rotor = Rotor3::from_rotation_between(camera_local_up, -basis.forward);
// now we need a twist rotor which aligns the camera up
let mut up_after_swing = self.up.clone();
swing_rotor.rotate_vec(&mut up_after_swing);
// to rotate a vector by a rotor we need
// - a bivector (represents the axis of rotation)
// - angle of rotation
let twist_axis = (-basis.forward).normalized();
let twist_plane = Bivec3::from_normalized_axis(twist_axis);
// Calculate twist angle between the up vectors:
// u1 × uc ⋅ (-f)
// θ = atan2( ————————————— , u1 ⋅ uc )
// ‖u1 × uc‖
//
// Where:
// u1 = up vector after swing rotation
// uc = camera's current up vector
// f = forward vector (twist axis)
let theta = up_after_swing
.cross(self.up)
.dot(twist_axis)
.atan2(up_after_swing.dot(self.up));
let twist_rotor = Rotor3::from_angle_plane(theta, twist_plane);
self.rotor = (swing_rotor * twist_rotor).normalized();
}
}
+1
View File
@@ -6,6 +6,7 @@ struct UniformData {
}
@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>,
+224
View File
@@ -0,0 +1,224 @@
use gltf::Gltf;
use ultraviolet::{Mat4, Vec3, Vec4};
use wgpu::TextureFormat;
use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder};
#[derive(Clone, Copy, Debug)]
pub struct ModelBounds {
pub min: [f32; 3],
pub max: [f32; 3],
}
impl ModelBounds {
fn new(min: [f32; 3], max: [f32; 3]) -> Self {
Self { min, max }
}
fn include_point(&mut self, point: [f32; 3]) {
for i in 0..3 {
self.min[i] = self.min[i].min(point[i]);
self.max[i] = self.max[i].max(point[i]);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
#[error("failed to fetch the model")]
Http(#[from] reqwest::Error),
#[error("failed to decode bytes")]
GltfParse(#[from] gltf::Error),
#[error("failed to load model")]
LoadError,
}
fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> {
use gltf::mesh::util::ReadTexCoords;
match tex_coords {
ReadTexCoords::F32(iter) => iter.collect(),
ReadTexCoords::U16(iter) => iter
.map(|[u, v]| [u as f32 / u16::MAX as f32, v as f32 / u16::MAX as f32])
.collect(),
ReadTexCoords::U8(iter) => iter
.map(|[u, v]| [u as f32 / u8::MAX as f32, v as f32 / u8::MAX as f32])
.collect(),
}
}
fn convert_indices(indices: gltf::mesh::util::ReadIndices<'_>) -> Vec<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 mat4_from_gltf(matrix: [[f32; 4]; 4]) -> Mat4 {
Mat4::new(
Vec4::new(matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3]),
Vec4::new(matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3]),
Vec4::new(matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3]),
Vec4::new(matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3]),
)
}
fn visit_node<'a>(
node: gltf::Node<'a>,
parent_transform: Mat4,
device: &wgpu::Device,
resources: &mut crate::renderer::GpuResources,
meshes: &mut Vec<crate::renderer::scene::Mesh>,
data_blob: &[u8],
pipeline_index: usize,
model_bounds: &mut Option<ModelBounds>,
) {
let local_transform = mat4_from_gltf(node.transform().matrix());
let world_transform = parent_transform * local_transform;
let normal_matrix = world_transform.inversed().transposed();
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let reader = primitive.reader(|buffer| match buffer.source() {
gltf::buffer::Source::Bin => Some(&data_blob[..]),
_ => None,
});
let mut positions: Vec<[f32; 3]> = match reader.read_positions() {
Some(iter) => iter.collect(),
None => Vec::new(),
};
if positions.is_empty() {
continue;
}
let vertex_count = positions.len();
let default_normal_vec = normal_matrix.transform_vec3(Vec3::unit_y()).normalized();
let default_normal = [
default_normal_vec.x,
default_normal_vec.y,
default_normal_vec.z,
];
let mut normals: Vec<[f32; 3]> = reader
.read_normals()
.map(|iter| {
iter.map(|normal| {
let vec = Vec3::new(normal[0], normal[1], normal[2]);
let transformed = normal_matrix.transform_vec3(vec).normalized();
[transformed.x, transformed.y, transformed.z]
})
.collect()
})
.unwrap_or_else(|| vec![default_normal; vertex_count]);
if normals.len() != vertex_count {
normals.resize(vertex_count, default_normal);
}
let mut uvs: Vec<[f32; 2]> = reader
.read_tex_coords(0)
.map(convert_tex_coords)
.unwrap_or_else(|| vec![[0.0, 0.0]; vertex_count]);
if uvs.len() != vertex_count {
uvs.resize(vertex_count, [0.0, 0.0]);
}
for position in &mut positions {
let vec = Vec3::new(position[0], position[1], position[2]);
let transformed = world_transform.transform_point3(vec);
*position = [transformed.x, transformed.y, transformed.z];
}
for position in &positions {
if let Some(bounds) = model_bounds.as_mut() {
bounds.include_point(*position);
} else {
*model_bounds = Some(ModelBounds::new(*position, *position));
}
}
let indices: Vec<u32> = reader
.read_indices()
.map(convert_indices)
.unwrap_or_else(|| (0..vertex_count as u32).collect());
if indices.is_empty() {
continue;
}
let mesh = MeshBuilder::new()
.with_vertices(device, resources, &positions, &normals, &uvs)
.with_indices(device, resources, &indices)
.with_pipeline(pipeline_index)
.build();
meshes.push(mesh);
}
}
for child in node.children() {
visit_node(
child,
world_transform,
device,
resources,
meshes,
data_blob,
pipeline_index,
model_bounds,
);
}
}
pub async fn load_gltf_model(
device: &wgpu::Device,
resources: &mut crate::renderer::GpuResources,
meshes: &mut Vec<crate::renderer::scene::Mesh>,
surface_format: TextureFormat,
) -> Result<Option<ModelBounds>, ImportError> {
let glb_data = reqwest::get("http://localhost:8080/sponza.glb")
.await?
.bytes()
.await?;
let model = Gltf::from_slice(&glb_data)?;
let data_blob = model.blob.as_ref().ok_or(ImportError::LoadError)?;
let vertex_layout = mesh_vertex_layout();
let pipeline_index = resources.get_or_create_pipeline(
device,
"gltf_standard",
&vertex_layout,
include_str!("./gltf.wgsl"),
surface_format,
);
let mut model_bounds: Option<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)
}
+56
View File
@@ -0,0 +1,56 @@
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(1) var<uniform> view_proj: mat4x4<f32>;
struct VertexInput {
@location(0) pos: vec3<f32>,
@location(1) normal: vec3<f32>,
// @location(2) uv: vec2<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;
out.clip_position = view_proj * vec4<f32>(in.pos, 1.0);
out.world_pos = in.pos;
out.normal = normalize(in.normal);
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45));
let light_color = vec3<f32>(1.0, 0.95, 0.85);
let base_color = vec3<f32>(1.0, 0.0, 0.0);
let 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);
}
+57 -4
View File
@@ -3,11 +3,17 @@ use std::sync::mpsc::{self, Sender};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use web_sys::AddEventListenerOptions;
use crate::{message::WindowEvent, platform::web, platform::web::worker::MainWorker};
mod camera;
mod gltf;
mod message;
mod platform;
mod renderer;
#[cfg(target_arch = "wasm32")]
pub struct App {
_worker: platform::web::worker::MainWorker,
@@ -16,6 +22,8 @@ pub struct App {
// Store closures to keep them alive
resize_listener: Option<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 {
@@ -36,10 +44,12 @@ impl App {
worker_chan: sender,
resize_listener: None,
mousemove_listener: None,
mousedown_listener: None,
wheel_listener: None,
};
app.setup_event_listeners();
return Ok(app);
Ok(app)
}
#[cfg(target_arch = "wasm32")]
@@ -70,6 +80,9 @@ impl App {
let mousemove_listener: Closure<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());
@@ -91,8 +104,51 @@ impl App {
.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);
}
}
@@ -100,7 +156,6 @@ impl App {
#[wasm_bindgen]
pub fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Info).unwrap();
wasm_logger::init(wasm_logger::Config::default());
wasm_bindgen_futures::spawn_local(async {
@@ -116,5 +171,3 @@ pub fn worker_entrypoint(ptr: u32) {
let work = unsafe { Box::from_raw(ptr as *mut Box<dyn FnOnce()>) };
(*work)();
}
+30
View File
@@ -5,6 +5,7 @@ pub enum WindowEvent {
Resize(ResizeMessage),
PointerMove(MouseMessage),
PointerClick(MouseMessage),
PointerWheel(WheelMessage),
}
// Display for WindowEvent
@@ -14,6 +15,7 @@ impl fmt::Display for WindowEvent {
WindowEvent::Resize(msg) => write!(f, "Resize: {:?}", msg),
WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg),
WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg),
WindowEvent::PointerWheel(msg) => write!(f, "PointerWheel: {:?}", msg),
}
}
}
@@ -29,6 +31,7 @@ pub struct ResizeMessage {
pub struct MouseMessage {
pub scale_factor: f64,
pub button: f64,
pub buttons: u16,
pub client_x: f64,
pub client_y: f64,
pub movement_x: f64,
@@ -43,6 +46,7 @@ impl MouseMessage {
Self {
scale_factor: window.device_pixel_ratio(),
button: event.button() as f64,
buttons: event.buttons(),
client_x: event.client_x() as f64,
client_y: event.client_y() as f64,
movement_x: event.movement_x() as f64,
@@ -52,3 +56,29 @@ impl MouseMessage {
}
}
}
#[derive(Debug, Clone)]
pub struct WheelMessage {
pub scale_factor: f64,
pub delta_x: f64,
pub delta_y: f64,
pub delta_z: f64,
pub delta_mode: u32,
pub client_x: f64,
pub client_y: f64,
}
impl WheelMessage {
pub fn from_evt(event: web_sys::WheelEvent) -> Self {
let window = web_sys::window().unwrap();
Self {
scale_factor: window.device_pixel_ratio(),
delta_x: event.delta_x(),
delta_y: event.delta_y(),
delta_z: event.delta_z(),
delta_mode: event.delta_mode(),
client_x: event.client_x() as f64,
client_y: event.client_y() as f64,
}
}
}
+1 -3
View File
@@ -1,10 +1,10 @@
use crate::message::WindowEvent;
use log::info;
use std::sync::mpsc::Receiver;
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
use wasm_bindgen::{prelude::*, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::MessageEvent;
use log::info;
/// Binds JS.
#[wasm_bindgen(module = "/src/platform/web/worker/workerGen.js")]
@@ -47,8 +47,6 @@ impl Debug for MainWorker {
}
}
impl MainWorker {
/// Spawns main worker from the window context.
pub fn spawn(
+434 -230
View File
@@ -1,14 +1,240 @@
use std::{cell::RefCell, rc::Rc, sync::mpsc::Receiver};
use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc, sync::mpsc::Receiver};
use log::info;
use wasm_bindgen::{prelude::Closure, JsCast};
use wasm_bindgen_futures::spawn_local;
use web_sys::DedicatedWorkerGlobalScope;
use wgpu::util::DeviceExt;
use crate::message::{MouseMessage, ResizeMessage, WindowEvent};
use crate::{
gltf::{load_gltf_model, ImportError, ModelBounds},
message::{MouseMessage, ResizeMessage, WindowEvent},
renderer::scene::Scene,
};
pub mod scene;
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
pub struct GpuResources {
// Core resources
buffers: Vec<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,
}
}
#[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;
/// Drawing relative data.
/// Note that this belongs to main worker.
pub struct Renderer {
canvas: web_sys::OffscreenCanvas,
events_chan: Receiver<WindowEvent>,
@@ -16,29 +242,55 @@ pub struct Renderer {
device: wgpu::Device,
queue: wgpu::Queue,
surface_config: wgpu::SurfaceConfiguration,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_num: u32,
uniform_data: UniformData,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
render_pipeline: wgpu::RenderPipeline,
scene: Scene,
resources: GpuResources,
depth_texture: wgpu::Texture,
depth_view: wgpu::TextureView,
}
impl Renderer {
fn create_depth_texture(
device: &wgpu::Device,
config: &wgpu::SurfaceConfiguration,
) -> (wgpu::Texture, wgpu::TextureView) {
let size = wgpu::Extent3d {
width: config.width.max(1),
height: config.height.max(1),
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("depth texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
fn recreate_depth_texture(&mut self) {
let (texture, view) = Self::create_depth_texture(&self.device, &self.surface_config);
self.depth_texture = texture;
self.depth_view = view;
}
pub async fn new(canvas: web_sys::OffscreenCanvas, events_chan: Receiver<WindowEvent>) -> Self {
let id = wgpu::InstanceDescriptor {
backends: wgpu::Backends::BROWSER_WEBGPU,
..Default::default()
};
// wgpu instance
let instance = wgpu::Instance::new(&id);
// wgpu surface
let surface = instance
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))
.unwrap();
// wgpu adapter
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
@@ -52,7 +304,6 @@ impl Renderer {
info!("Adapter features: {:?}", adapter.features());
info!("Adapter limits: {:?}", adapter.limits());
// wgpu device and queue
let descriptor = wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
@@ -62,8 +313,7 @@ impl Renderer {
};
let (device, queue) = adapter.request_device(&descriptor).await.unwrap();
info!("after");
// wgpu surface configuration
let surface_caps = surface.get_capabilities(&adapter);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
@@ -80,40 +330,19 @@ impl Renderer {
surface_config.width, surface_config.height
);
surface.configure(&device, &surface_config);
// wgpu vertex buffer
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
// wgpu index buffer
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index buffer"),
contents: bytemuck::cast_slice(INDICES),
usage: wgpu::BufferUsages::INDEX,
});
// wgpu uniform buffer
let uniform_data = UniformData {
resolution: [canvas.width() as f32, canvas.height() as f32],
mouse_move: [std::f32::MIN, std::f32::MIN],
mouse_click: [std::f32::MIN, std::f32::MIN],
..Default::default()
};
let (uniform_buffer, uniform_layout, uniform_bind_group) =
Renderer::create_uniform_buffer(&device, bytemuck::cast_slice(&[uniform_data][..]));
// wgpu shader module
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader module"),
source: wgpu::ShaderSource::Wgsl(include_str!("../example.wgsl").into()),
});
// wgpu render pipeline
let render_pipeline = Renderer::create_render_pipeline(
let (depth_texture, depth_view) = Self::create_depth_texture(&device, &surface_config);
let mut resources = GpuResources::new();
let mut scene = Scene::new(
&device,
&[&uniform_layout],
&shader_module,
&surface_config,
ultraviolet::Vec2::new(canvas.width() as f32, canvas.height() as f32),
);
resources.set_bind_group_layouts(&scene.bind_group_layout);
scene.create_default_triangle(&device, &mut resources, surface_config.format);
Self {
canvas,
events_chan,
@@ -121,108 +350,15 @@ impl Renderer {
device,
queue,
surface_config,
vertex_buffer,
index_buffer,
index_num: INDICES.len() as u32,
uniform_data,
uniform_buffer,
uniform_bind_group,
render_pipeline,
scene,
resources,
depth_texture,
depth_view,
}
}
fn create_uniform_buffer(
device: &wgpu::Device,
contents: &[u8],
) -> (wgpu::Buffer, wgpu::BindGroupLayout, wgpu::BindGroup) {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Uniform buffer"),
contents,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform bind group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
(buffer, bind_group_layout, bind_group)
}
fn create_render_pipeline(
device: &wgpu::Device,
bind_group_layouts: &[&wgpu::BindGroupLayout],
shader_module: &wgpu::ShaderModule,
surface_config: &wgpu::SurfaceConfiguration,
) -> wgpu::RenderPipeline {
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render pipeline layout"),
bind_group_layouts,
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
cache: None,
label: Some("Render pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
compilation_options: wgpu::PipelineCompilationOptions::default(),
module: shader_module,
entry_point: Some("v_main"),
buffers: &[Vertex::layout()],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
compilation_options: wgpu::PipelineCompilationOptions::default(),
module: shader_module,
entry_point: Some("f_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
})
}
fn render(&mut self, time: f32) {
// Write uniform data to its buffer
self.uniform_data.time = time * 0.001;
self.queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&[self.uniform_data][..]),
);
self.scene.update(&self.queue, time);
let surface_texture = self.surface.get_current_texture().unwrap();
let texture_view = surface_texture.texture.create_view(&Default::default());
@@ -249,37 +385,98 @@ impl Renderer {
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.draw_indexed(0..self.index_num, 0, 0..1);
for (i, bind_group) in self.scene.bind_groups.iter().enumerate() {
render_pass.set_bind_group(i as u32, bind_group, &[]);
}
for mesh in &self.scene.meshes {
render_pass.set_pipeline(self.resources.get_pipeline_by_index(mesh.pipeline_index));
render_pass.set_vertex_buffer(
0,
self.resources
.get_buffer(&mesh.position_buffer_index)
.slice(..),
);
render_pass.set_vertex_buffer(
1,
self.resources
.get_buffer(&mesh.normal_buffer_index)
.slice(..),
);
render_pass.set_vertex_buffer(
2,
self.resources.get_buffer(&mesh.uv_buffer_index).slice(..),
);
render_pass.set_index_buffer(
self.resources
.get_buffer(&mesh.index_buffer_index)
.slice(..),
mesh.index_format,
);
render_pass.draw_indexed(0..mesh.index_count, 0, 0..mesh.instance_count);
}
}
self.queue.submit(std::iter::once(encoder.finish()));
surface_texture.present();
}
pub fn handle_event(&mut self, event: WindowEvent) {
pub async fn handle_event(renderer: Rc<RefCell<Self>>, event: WindowEvent) {
match event {
WindowEvent::PointerMove(msg) => self.mouse_move(msg),
WindowEvent::Resize(msg) => self.resize(msg),
WindowEvent::PointerClick(msg) => self.mouse_click(msg),
WindowEvent::PointerMove(msg) => {
renderer.borrow_mut().mouse_move(msg);
}
WindowEvent::Resize(msg) => {
renderer.borrow_mut().resize(msg);
}
WindowEvent::PointerClick(msg) => {
{
let mut r = renderer.borrow_mut();
let x = (msg.offset_x * msg.scale_factor) as f32;
let y = (msg.offset_y * msg.scale_factor) as f32;
r.scene.frame_metadata.mouse_click = [x, y];
log::info!("clicked");
}
if let Err(e) = Self::load_assets_async(renderer.clone()).await {
log::error!("failed to load gltf: {e}");
}
}
WindowEvent::PointerWheel(msg) => {
let mut r = renderer.borrow_mut();
r.scene.cam.zoom(&msg);
}
}
}
pub fn run_render_loop(renderer: Rc<RefCell<Renderer>>) {
let render_frame: Closure<dyn FnMut(f32)> = Closure::new(move |time: f32| {
{
let mut r = renderer.borrow_mut();
let event = { renderer.borrow_mut().events_chan.try_recv() };
if let Ok(event) = r.events_chan.try_recv() {
r.handle_event(event);
if let Ok(event) = event {
let renderer_clone = renderer.clone();
spawn_local(async move {
Self::handle_event(renderer_clone, event).await;
});
}
}
{
let mut r = renderer.borrow_mut();
r.render(time);
}
@@ -302,9 +499,9 @@ impl Renderer {
self.surface_config.width = new_width;
self.surface_config.height = new_height;
self.surface.configure(&self.device, &self.surface_config);
self.recreate_depth_texture();
// Update uniform data
self.uniform_data.resolution = [new_width as f32, new_height as f32];
self.scene.frame_metadata.resolution = [new_width as f32, new_height as f32];
info!(
"Resized: ({}, {}), scale: {}",
@@ -314,79 +511,86 @@ impl Renderer {
}
pub fn mouse_move(&mut self, msg: MouseMessage) {
// Update uniform data
let x = (msg.offset_x * msg.scale_factor) as f32;
let y = (msg.offset_y * msg.scale_factor) as f32;
self.uniform_data.mouse_move = [x, y];
}
self.scene.frame_metadata.mouse_move = [x, y];
pub fn mouse_click(&mut self, msg: MouseMessage) {
info!("clicked");
// Update uniform data
let x = (msg.offset_x * msg.scale_factor) as f32;
let y = (msg.offset_y * msg.scale_factor) as f32;
self.uniform_data.mouse_click = [x, y];
}
}
/// Simple vertex format.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
pos: [f32; 3],
color: [f32; 3],
}
impl Vertex {
fn layout() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
// pos
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
// color
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
],
if (msg.buttons & 0x04) != 0 {
let delta_x = (msg.movement_x * msg.scale_factor) as f32;
let delta_y = (msg.movement_y * msg.scale_factor) as f32;
self.scene.cam.orbit(delta_x, delta_y);
}
}
// currently this replaces everything, will need more sophisticated mechanisms later
pub async fn load_assets_async(renderer: Rc<RefCell<Renderer>>) -> Result<(), ImportError> {
let (device, surface_format, bind_group_layout) = {
let r = renderer.borrow();
(
r.device.clone(),
r.surface_config.format,
r.scene.bind_group_layout.clone(),
)
};
let mut meshes = Vec::new();
let mut original_resources = {
let mut r = renderer.borrow_mut();
r.scene.meshes.clear();
std::mem::take(&mut r.resources)
};
original_resources.set_bind_group_layouts(&bind_group_layout);
let bounds = load_gltf_model(
&device,
&mut original_resources,
&mut meshes,
surface_format,
)
.await?;
{
let mut r = renderer.borrow_mut();
r.resources = original_resources;
r.scene.meshes = meshes;
if let Some(ModelBounds { min, max }) = bounds {
let center = ultraviolet::Vec3::new(
(min[0] + max[0]) * 0.5,
(min[1] + max[1]) * 0.5,
(min[2] + max[2]) * 0.5,
);
let extent =
ultraviolet::Vec3::new(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
let radius =
0.5 * (extent.x * extent.x + extent.y * extent.y + extent.z * extent.z).sqrt();
let radius = radius.max(1.0);
// set the camera position after load, so we are not disoriented
let eye_offset = ultraviolet::Vec3::new(0.0, radius * 0.05, radius * 0.25);
// Keep the near plane proportional to the model size to avoid
// extreme depth ranges when loading very large assets
let near_plane = (radius * 0.001).max(0.1);
// The far plane must be far enough to cover the entire model.
// Using a fixed upper clamp caused large models to be clipped
// completely; relying on the model radius instead.
let far_plane = (radius * 4.0).max(near_plane + 1.0);
r.scene.cam.set_depth_range(near_plane, far_plane);
r.scene.cam.look_at(center + eye_offset, center);
}
}
Ok(())
}
}
/// Vertex example.
const VERTICES: &[Vertex] = &[
Vertex {
pos: [0.0, 0.5, 0.0], // Top-left
color: [1.0, 0.0, 1.0], // Magenta
},
Vertex {
pos: [-0.5, -0.5, 0.0], // Bottom-left
color: [0.0, 0.0, 1.0], // Blue
},
Vertex {
pos: [0.5, -0.5, 0.0], // Top-right
color: [1.0, 1.0, 0.0], // Yellow
},
];
const INDICES: &[u32] = &[0, 1, 2]; // CCW, quad
/// Simple uniform data.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)]
struct UniformData {
mouse_move: [f32; 2],
mouse_click: [f32; 2],
resolution: [f32; 2],
time: f32,
_padding: f32,
impl<T> From<BufferIndex<T>> for u32 {
fn from(value: BufferIndex<T>) -> Self {
value.index
}
}
+340
View File
@@ -0,0 +1,340 @@
use wgpu::util::DeviceExt;
use crate::{
camera::Camera,
renderer::{BufferIndex, GpuResources, Index, Normal, Position, UV},
};
pub struct UniformResource {
pub buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: wgpu::BindGroupLayout,
}
/// Simple uniform data.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)]
pub struct FrameMetadata {
pub mouse_move: [f32; 2],
pub mouse_click: [f32; 2],
pub resolution: [f32; 2],
time: f32,
_padding0: f32,
pub camera_position: [f32; 4],
}
impl FrameMetadata {
pub fn new(dimension: ultraviolet::Vec2) -> Self {
FrameMetadata {
resolution: dimension.into(),
mouse_move: [std::f32::MIN, std::f32::MIN],
mouse_click: [std::f32::MIN, std::f32::MIN],
_padding0: 0.0,
camera_position: [0.0, 0.0, 0.0, 1.0],
..Default::default()
}
}
pub fn set_camera_position(&mut self, position: ultraviolet::Vec3) {
self.camera_position = [position.x, position.y, position.z, 1.0];
}
pub fn create_uniform_resource(self, device: &wgpu::Device) -> UniformResource {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("frame metadata uniform buffer"),
contents: bytemuck::cast_slice(&[self][..]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform bind group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
UniformResource {
buffer,
bind_group_layout,
bind_group,
}
}
}
pub struct Mesh {
pub pipeline_index: usize,
pub position_buffer_index: BufferIndex<Position>,
pub normal_buffer_index: BufferIndex<Normal>,
pub uv_buffer_index: BufferIndex<UV>,
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>; 3] {
[
wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x2,
}],
},
]
}
pub struct MeshBuilder<I, V, P> {
indices: I,
vertices: V,
pipeline: P,
instance_count: u32,
}
impl MeshBuilder<(), (), ()> {
pub fn new() -> Self {
Self {
indices: (),
vertices: (),
pipeline: (),
instance_count: 1,
}
}
}
impl<P> MeshBuilder<(), (), P> {
pub fn with_vertices(
self,
device: &wgpu::Device,
resources: &mut GpuResources,
positions: &[[f32; 3]],
normals: &[[f32; 3]],
uvs: &[[f32; 2]],
) -> MeshBuilder<(), VertexBufferSet, P> {
let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Positions"),
contents: bytemuck::cast_slice(positions),
usage: wgpu::BufferUsages::VERTEX,
});
let normal_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Normals"),
contents: bytemuck::cast_slice(normals),
usage: wgpu::BufferUsages::VERTEX,
});
let uv_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh UVs"),
contents: bytemuck::cast_slice(uvs),
usage: wgpu::BufferUsages::VERTEX,
});
let position_buffer_index = resources.add_position_buffer(position_buffer);
let normal_buffer_index = resources.add_normal_buffer(normal_buffer);
let uv_buffer_index = resources.add_uv_buffer(uv_buffer);
MeshBuilder {
vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index),
indices: self.indices,
pipeline: self.pipeline,
instance_count: self.instance_count,
}
}
}
impl<V, P> MeshBuilder<(), V, P> {
pub fn with_indices(
self,
device: &wgpu::Device,
resources: &mut GpuResources,
indices: &[u32],
) -> MeshBuilder<IndexBufferInfo, V, P> {
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Indices"),
contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX,
});
let index_buffer_index = resources.add_index_buffer(index_buffer);
MeshBuilder {
indices: (
index_buffer_index,
indices.len() as u32,
wgpu::IndexFormat::Uint32,
),
vertices: self.vertices,
pipeline: self.pipeline,
instance_count: self.instance_count,
}
}
}
impl<I, V> MeshBuilder<I, V, ()> {
pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder<I, V, usize> {
MeshBuilder {
pipeline: pipeline_index,
indices: self.indices,
vertices: self.vertices,
instance_count: self.instance_count,
}
}
}
impl MeshBuilder<IndexBufferInfo, VertexBufferSet, usize> {
pub fn build(self) -> Mesh {
Mesh {
pipeline_index: self.pipeline,
position_buffer_index: (self.vertices).0,
normal_buffer_index: (self.vertices).1,
uv_buffer_index: (self.vertices).2,
index_buffer_index: (self.indices).0,
index_count: (self.indices).1,
index_format: (self.indices).2,
instance_count: self.instance_count,
}
}
}
/// Simple vertex format.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pos: [f32; 3],
color: [f32; 3],
}
/// Triangle vertex data.
const VERTICES: &[Vertex] = &[
Vertex {
pos: [0.0, 0.5, 0.0],
color: [1.0, 0.0, 1.0], // Magenta
},
Vertex {
pos: [-0.5, -0.5, 0.0],
color: [0.0, 0.0, 1.0], // Blue
},
Vertex {
pos: [0.5, -0.5, 0.0],
color: [1.0, 1.0, 0.0], // Yellow
},
];
const INDICES: &[u32] = &[0, 1, 2];
pub struct Scene {
pub uniform_buffers: [wgpu::Buffer; 2],
pub bind_groups: [wgpu::BindGroup; 2],
pub bind_group_layout: [wgpu::BindGroupLayout; 2],
pub frame_metadata: FrameMetadata,
pub cam: Camera,
pub meshes: Vec<Mesh>,
}
impl Scene {
pub fn new(device: &wgpu::Device, dimension: ultraviolet::Vec2) -> Self {
let cam = Camera::new(dimension.x / dimension.y);
let mut frame_metadata = FrameMetadata::new(dimension);
frame_metadata.set_camera_position(cam.position());
let uniform_resource = frame_metadata.create_uniform_resource(device);
let camera_resource = cam.create_uniform_resource(device);
Scene {
uniform_buffers: [uniform_resource.buffer, camera_resource.buffer],
bind_groups: [uniform_resource.bind_group, camera_resource.bind_group],
bind_group_layout: [
uniform_resource.bind_group_layout,
camera_resource.bind_group_layout,
],
frame_metadata,
cam,
meshes: Vec::new(),
}
}
pub fn create_default_triangle(
&mut self,
device: &wgpu::Device,
resources: &mut GpuResources,
surface_format: wgpu::TextureFormat,
) {
let positions: Vec<[f32; 3]> = VERTICES.iter().map(|v| v.pos).collect();
// Colors ride through the "normal" slot because the render path always binds
// three vertex buffers (position, normal, uv) for every mesh.
// todo clean that shit up
let colors: Vec<[f32; 3]> = VERTICES.iter().map(|v| v.color).collect();
let uvs: &[[f32; 2]] = &[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]];
let vertex_layout = mesh_vertex_layout();
let pipeline_index = resources.get_or_create_pipeline(
device,
"triangle_colored",
&vertex_layout,
include_str!("../example.wgsl"),
surface_format,
);
let mesh = MeshBuilder::new()
.with_vertices(device, resources, &positions, &colors, uvs)
.with_indices(device, resources, INDICES)
.with_pipeline(pipeline_index)
.build();
self.meshes.push(mesh);
}
pub fn update(&mut self, queue: &wgpu::Queue, time: f32) {
self.frame_metadata.time = time * 0.001;
self.frame_metadata.set_camera_position(self.cam.position());
queue.write_buffer(
&self.uniform_buffers[0],
0,
bytemuck::cast_slice(&[self.frame_metadata][..]),
);
queue.write_buffer(
&self.uniform_buffers[1],
0,
bytemuck::cast_slice(&[self.cam.view_proj]),
);
}
}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB