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
+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();
}
}