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:
@@ -0,0 +1 @@
|
|||||||
|
static/sponza.glb filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -37,6 +37,11 @@ web-sys = { version = "0.3.77", features = [
|
|||||||
"Blob",
|
"Blob",
|
||||||
"BlobPropertyBag",
|
"BlobPropertyBag",
|
||||||
"Url",
|
"Url",
|
||||||
|
"Request",
|
||||||
|
"RequestInit",
|
||||||
|
"RequestMode",
|
||||||
|
"Response",
|
||||||
|
"Headers"
|
||||||
]}
|
]}
|
||||||
js-sys = "0.3.77"
|
js-sys = "0.3.77"
|
||||||
bytemuck = { version = "1.23.1", features = [
|
bytemuck = { version = "1.23.1", features = [
|
||||||
@@ -45,10 +50,13 @@ bytemuck = { version = "1.23.1", features = [
|
|||||||
cgmath = "0.18"
|
cgmath = "0.18"
|
||||||
raw-window-handle = "0.6.2"
|
raw-window-handle = "0.6.2"
|
||||||
wgpu = "26.0.1"
|
wgpu = "26.0.1"
|
||||||
|
reqwest = { version = "0.12.23", features = ["json"] }
|
||||||
|
thiserror = "2.0.15"
|
||||||
|
ultraviolet = "0.10.0"
|
||||||
|
|
||||||
[dependencies.gltf]
|
[dependencies.gltf]
|
||||||
version = "1.4"
|
version = "1.4"
|
||||||
features = ["extras", "names"]
|
features = ["extras", "names"]
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
wasm-opt = false
|
wasm-opt = false
|
||||||
|
|||||||
+298
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ struct UniformData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> uni: UniformData;
|
@group(0) @binding(0) var<uniform> uni: UniformData;
|
||||||
|
@group(1) @binding(0) var<uniform> view_proj: mat4x4<f32>;
|
||||||
|
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@location(0) pos: vec3<f32>,
|
@location(0) pos: vec3<f32>,
|
||||||
|
|||||||
+224
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -3,11 +3,17 @@ use std::sync::mpsc::{self, Sender};
|
|||||||
use wasm_bindgen::closure::Closure;
|
use wasm_bindgen::closure::Closure;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use web_sys::AddEventListenerOptions;
|
||||||
|
|
||||||
use crate::{message::WindowEvent, platform::web, platform::web::worker::MainWorker};
|
use crate::{message::WindowEvent, platform::web, platform::web::worker::MainWorker};
|
||||||
|
|
||||||
|
mod camera;
|
||||||
|
mod gltf;
|
||||||
mod message;
|
mod message;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod renderer;
|
mod renderer;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
_worker: platform::web::worker::MainWorker,
|
_worker: platform::web::worker::MainWorker,
|
||||||
@@ -16,6 +22,8 @@ pub struct App {
|
|||||||
// Store closures to keep them alive
|
// Store closures to keep them alive
|
||||||
resize_listener: Option<Closure<dyn FnMut()>>,
|
resize_listener: Option<Closure<dyn FnMut()>>,
|
||||||
mousemove_listener: Option<Closure<dyn FnMut(web_sys::MouseEvent)>>,
|
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 {
|
impl App {
|
||||||
@@ -36,10 +44,12 @@ impl App {
|
|||||||
worker_chan: sender,
|
worker_chan: sender,
|
||||||
resize_listener: None,
|
resize_listener: None,
|
||||||
mousemove_listener: None,
|
mousemove_listener: None,
|
||||||
|
mousedown_listener: None,
|
||||||
|
wheel_listener: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
app.setup_event_listeners();
|
app.setup_event_listeners();
|
||||||
return Ok(app);
|
Ok(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -70,6 +80,9 @@ impl App {
|
|||||||
let mousemove_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
|
let mousemove_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
|
||||||
Closure::new(move |event: web_sys::MouseEvent| {
|
Closure::new(move |event: web_sys::MouseEvent| {
|
||||||
use crate::message::MouseMessage;
|
use crate::message::MouseMessage;
|
||||||
|
if event.buttons() & 0x04 != 0 {
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
let mouse_event_data = MouseMessage::from_evt(event.clone());
|
let mouse_event_data = MouseMessage::from_evt(event.clone());
|
||||||
|
|
||||||
let mut event_data = WindowEvent::PointerMove(mouse_event_data.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())
|
.add_event_listener_with_callback("click", mousemove_listener.as_ref().unchecked_ref())
|
||||||
.unwrap();
|
.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.resize_listener = Some(resize_listener);
|
||||||
self.mousemove_listener = Some(mousemove_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]
|
#[wasm_bindgen]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
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_logger::init(wasm_logger::Config::default());
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local(async {
|
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()>) };
|
let work = unsafe { Box::from_raw(ptr as *mut Box<dyn FnOnce()>) };
|
||||||
(*work)();
|
(*work)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub enum WindowEvent {
|
|||||||
Resize(ResizeMessage),
|
Resize(ResizeMessage),
|
||||||
PointerMove(MouseMessage),
|
PointerMove(MouseMessage),
|
||||||
PointerClick(MouseMessage),
|
PointerClick(MouseMessage),
|
||||||
|
PointerWheel(WheelMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display for WindowEvent
|
// Display for WindowEvent
|
||||||
@@ -14,6 +15,7 @@ impl fmt::Display for WindowEvent {
|
|||||||
WindowEvent::Resize(msg) => write!(f, "Resize: {:?}", msg),
|
WindowEvent::Resize(msg) => write!(f, "Resize: {:?}", msg),
|
||||||
WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg),
|
WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg),
|
||||||
WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg),
|
WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg),
|
||||||
|
WindowEvent::PointerWheel(msg) => write!(f, "PointerWheel: {:?}", msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,7 @@ pub struct ResizeMessage {
|
|||||||
pub struct MouseMessage {
|
pub struct MouseMessage {
|
||||||
pub scale_factor: f64,
|
pub scale_factor: f64,
|
||||||
pub button: f64,
|
pub button: f64,
|
||||||
|
pub buttons: u16,
|
||||||
pub client_x: f64,
|
pub client_x: f64,
|
||||||
pub client_y: f64,
|
pub client_y: f64,
|
||||||
pub movement_x: f64,
|
pub movement_x: f64,
|
||||||
@@ -43,6 +46,7 @@ impl MouseMessage {
|
|||||||
Self {
|
Self {
|
||||||
scale_factor: window.device_pixel_ratio(),
|
scale_factor: window.device_pixel_ratio(),
|
||||||
button: event.button() as f64,
|
button: event.button() as f64,
|
||||||
|
buttons: event.buttons(),
|
||||||
client_x: event.client_x() as f64,
|
client_x: event.client_x() as f64,
|
||||||
client_y: event.client_y() as f64,
|
client_y: event.client_y() as f64,
|
||||||
movement_x: event.movement_x() 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,10 +1,10 @@
|
|||||||
use crate::message::WindowEvent;
|
use crate::message::WindowEvent;
|
||||||
|
use log::info;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
|
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
|
||||||
use wasm_bindgen::{prelude::*, JsValue};
|
use wasm_bindgen::{prelude::*, JsValue};
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::MessageEvent;
|
use web_sys::MessageEvent;
|
||||||
use log::info;
|
|
||||||
|
|
||||||
/// Binds JS.
|
/// Binds JS.
|
||||||
#[wasm_bindgen(module = "/src/platform/web/worker/workerGen.js")]
|
#[wasm_bindgen(module = "/src/platform/web/worker/workerGen.js")]
|
||||||
@@ -47,8 +47,6 @@ impl Debug for MainWorker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl MainWorker {
|
impl MainWorker {
|
||||||
/// Spawns main worker from the window context.
|
/// Spawns main worker from the window context.
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
|
|||||||
+434
-230
@@ -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 log::info;
|
||||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use web_sys::DedicatedWorkerGlobalScope;
|
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 {
|
pub struct Renderer {
|
||||||
canvas: web_sys::OffscreenCanvas,
|
canvas: web_sys::OffscreenCanvas,
|
||||||
events_chan: Receiver<WindowEvent>,
|
events_chan: Receiver<WindowEvent>,
|
||||||
@@ -16,29 +242,55 @@ pub struct Renderer {
|
|||||||
device: wgpu::Device,
|
device: wgpu::Device,
|
||||||
queue: wgpu::Queue,
|
queue: wgpu::Queue,
|
||||||
surface_config: wgpu::SurfaceConfiguration,
|
surface_config: wgpu::SurfaceConfiguration,
|
||||||
vertex_buffer: wgpu::Buffer,
|
scene: Scene,
|
||||||
index_buffer: wgpu::Buffer,
|
resources: GpuResources,
|
||||||
index_num: u32,
|
depth_texture: wgpu::Texture,
|
||||||
uniform_data: UniformData,
|
depth_view: wgpu::TextureView,
|
||||||
uniform_buffer: wgpu::Buffer,
|
|
||||||
uniform_bind_group: wgpu::BindGroup,
|
|
||||||
render_pipeline: wgpu::RenderPipeline,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
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 {
|
pub async fn new(canvas: web_sys::OffscreenCanvas, events_chan: Receiver<WindowEvent>) -> Self {
|
||||||
let id = wgpu::InstanceDescriptor {
|
let id = wgpu::InstanceDescriptor {
|
||||||
backends: wgpu::Backends::BROWSER_WEBGPU,
|
backends: wgpu::Backends::BROWSER_WEBGPU,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// wgpu instance
|
|
||||||
let instance = wgpu::Instance::new(&id);
|
let instance = wgpu::Instance::new(&id);
|
||||||
// wgpu surface
|
|
||||||
let surface = instance
|
let surface = instance
|
||||||
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))
|
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// wgpu adapter
|
|
||||||
let adapter = instance
|
let adapter = instance
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
compatible_surface: Some(&surface),
|
compatible_surface: Some(&surface),
|
||||||
@@ -52,7 +304,6 @@ impl Renderer {
|
|||||||
info!("Adapter features: {:?}", adapter.features());
|
info!("Adapter features: {:?}", adapter.features());
|
||||||
info!("Adapter limits: {:?}", adapter.limits());
|
info!("Adapter limits: {:?}", adapter.limits());
|
||||||
|
|
||||||
// wgpu device and queue
|
|
||||||
let descriptor = wgpu::DeviceDescriptor {
|
let descriptor = wgpu::DeviceDescriptor {
|
||||||
required_features: wgpu::Features::empty(),
|
required_features: wgpu::Features::empty(),
|
||||||
required_limits: wgpu::Limits::default(),
|
required_limits: wgpu::Limits::default(),
|
||||||
@@ -62,8 +313,7 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (device, queue) = adapter.request_device(&descriptor).await.unwrap();
|
let (device, queue) = adapter.request_device(&descriptor).await.unwrap();
|
||||||
info!("after");
|
|
||||||
// wgpu surface configuration
|
|
||||||
let surface_caps = surface.get_capabilities(&adapter);
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
let surface_config = wgpu::SurfaceConfiguration {
|
let surface_config = wgpu::SurfaceConfiguration {
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
@@ -80,40 +330,19 @@ impl Renderer {
|
|||||||
surface_config.width, surface_config.height
|
surface_config.width, surface_config.height
|
||||||
);
|
);
|
||||||
surface.configure(&device, &surface_config);
|
surface.configure(&device, &surface_config);
|
||||||
// wgpu vertex buffer
|
|
||||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let (depth_texture, depth_view) = Self::create_depth_texture(&device, &surface_config);
|
||||||
label: Some("Vertex buffer"),
|
|
||||||
contents: bytemuck::cast_slice(VERTICES),
|
let mut resources = GpuResources::new();
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
|
||||||
});
|
let mut scene = Scene::new(
|
||||||
// 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(
|
|
||||||
&device,
|
&device,
|
||||||
&[&uniform_layout],
|
ultraviolet::Vec2::new(canvas.width() as f32, canvas.height() as f32),
|
||||||
&shader_module,
|
|
||||||
&surface_config,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
resources.set_bind_group_layouts(&scene.bind_group_layout);
|
||||||
|
scene.create_default_triangle(&device, &mut resources, surface_config.format);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
canvas,
|
canvas,
|
||||||
events_chan,
|
events_chan,
|
||||||
@@ -121,108 +350,15 @@ impl Renderer {
|
|||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
surface_config,
|
surface_config,
|
||||||
vertex_buffer,
|
scene,
|
||||||
index_buffer,
|
resources,
|
||||||
index_num: INDICES.len() as u32,
|
depth_texture,
|
||||||
uniform_data,
|
depth_view,
|
||||||
uniform_buffer,
|
|
||||||
uniform_bind_group,
|
|
||||||
render_pipeline,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
fn render(&mut self, time: f32) {
|
||||||
// Write uniform data to its buffer
|
self.scene.update(&self.queue, time);
|
||||||
self.uniform_data.time = time * 0.001;
|
|
||||||
self.queue.write_buffer(
|
|
||||||
&self.uniform_buffer,
|
|
||||||
0,
|
|
||||||
bytemuck::cast_slice(&[self.uniform_data][..]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let surface_texture = self.surface.get_current_texture().unwrap();
|
let surface_texture = self.surface.get_current_texture().unwrap();
|
||||||
let texture_view = surface_texture.texture.create_view(&Default::default());
|
let texture_view = surface_texture.texture.create_view(&Default::default());
|
||||||
@@ -249,37 +385,98 @@ impl Renderer {
|
|||||||
store: wgpu::StoreOp::Store,
|
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,
|
occlusion_query_set: None,
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
});
|
});
|
||||||
render_pass.set_pipeline(&self.render_pipeline);
|
|
||||||
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
for (i, bind_group) in self.scene.bind_groups.iter().enumerate() {
|
||||||
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
render_pass.set_bind_group(i as u32, bind_group, &[]);
|
||||||
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
}
|
||||||
render_pass.draw_indexed(0..self.index_num, 0, 0..1);
|
|
||||||
|
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()));
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
surface_texture.present();
|
surface_texture.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_event(&mut self, event: WindowEvent) {
|
pub async fn handle_event(renderer: Rc<RefCell<Self>>, event: WindowEvent) {
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::PointerMove(msg) => self.mouse_move(msg),
|
WindowEvent::PointerMove(msg) => {
|
||||||
WindowEvent::Resize(msg) => self.resize(msg),
|
renderer.borrow_mut().mouse_move(msg);
|
||||||
WindowEvent::PointerClick(msg) => self.mouse_click(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>>) {
|
pub fn run_render_loop(renderer: Rc<RefCell<Renderer>>) {
|
||||||
let render_frame: Closure<dyn FnMut(f32)> = Closure::new(move |time: f32| {
|
let render_frame: Closure<dyn FnMut(f32)> = Closure::new(move |time: f32| {
|
||||||
{
|
{
|
||||||
let mut r = renderer.borrow_mut();
|
let event = { renderer.borrow_mut().events_chan.try_recv() };
|
||||||
|
|
||||||
if let Ok(event) = r.events_chan.try_recv() {
|
if let Ok(event) = event {
|
||||||
r.handle_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);
|
r.render(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,9 +499,9 @@ impl Renderer {
|
|||||||
self.surface_config.width = new_width;
|
self.surface_config.width = new_width;
|
||||||
self.surface_config.height = new_height;
|
self.surface_config.height = new_height;
|
||||||
self.surface.configure(&self.device, &self.surface_config);
|
self.surface.configure(&self.device, &self.surface_config);
|
||||||
|
self.recreate_depth_texture();
|
||||||
|
|
||||||
// Update uniform data
|
self.scene.frame_metadata.resolution = [new_width as f32, new_height as f32];
|
||||||
self.uniform_data.resolution = [new_width as f32, new_height as f32];
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Resized: ({}, {}), scale: {}",
|
"Resized: ({}, {}), scale: {}",
|
||||||
@@ -314,79 +511,86 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_move(&mut self, msg: MouseMessage) {
|
pub fn mouse_move(&mut self, msg: MouseMessage) {
|
||||||
// Update uniform data
|
|
||||||
let x = (msg.offset_x * msg.scale_factor) as f32;
|
let x = (msg.offset_x * msg.scale_factor) as f32;
|
||||||
let y = (msg.offset_y * msg.scale_factor) as f32;
|
let y = (msg.offset_y * msg.scale_factor) as f32;
|
||||||
self.uniform_data.mouse_move = [x, y];
|
self.scene.frame_metadata.mouse_move = [x, y];
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mouse_click(&mut self, msg: MouseMessage) {
|
if (msg.buttons & 0x04) != 0 {
|
||||||
info!("clicked");
|
let delta_x = (msg.movement_x * msg.scale_factor) as f32;
|
||||||
// Update uniform data
|
let delta_y = (msg.movement_y * msg.scale_factor) as f32;
|
||||||
let x = (msg.offset_x * msg.scale_factor) as f32;
|
self.scene.cam.orbit(delta_x, delta_y);
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
impl<T> From<BufferIndex<T>> for u32 {
|
||||||
const VERTICES: &[Vertex] = &[
|
fn from(value: BufferIndex<T>) -> Self {
|
||||||
Vertex {
|
value.index
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
Binary file not shown.
@@ -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 |
Reference in New Issue
Block a user