kitgit

tirbofish/dropbear · diff

6efc74a · Thribhu K

fix: for some reason, the normals were all mixed up. fixed that up fix: made mouse delta be clamped to the FPS fix: got shaders back to working. fix: proc objects use default textures instead of all white (incl emissive and diffuse). jarvis, run github actions

Unverified

diff --git a/.cargo/config.toml b/.cargo/config.toml
index d2312c6..119d027 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,3 +1,6 @@
 [target.x86_64-unknown-linux-gnu]
 linker = "clang"
 rustflags = ["-C", "link-arg=-fuse-ld=mold", "-C", "relocation-model=pic"]
+
+# note: if you get a compile error such as "Recompile with RPIC", just run `cargo build` 
+# instead of `cargo build -p eucalyptus-core -p eucalyptus-editor`. 
diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index e067bef..5380ef8 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -1,9 +1,17 @@
+use crate::buffer::{ResizableBuffer, UniformBuffer};
 use crate::graphics::SharedGraphicsContext;
 use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
 use glam::Mat4;
 use std::collections::HashMap;
 use std::sync::Arc;
-use wgpu::util::DeviceExt;
+
+#[repr(C)]
+#[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct MorphTargetInfo {
+    num_vertices: u32,
+    num_targets: u32,
+    _padding: [u32; 2],
+}
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct AnimationComponent {
@@ -27,7 +35,13 @@ pub struct AnimationComponent {
     pub skinning_matrices: Vec<Mat4>,
 
     #[serde(skip)]
-    pub bone_buffer: Option<wgpu::Buffer>,
+    pub skinning_buffer: Option<ResizableBuffer<Mat4>>,
+    #[serde(skip)]
+    pub morph_deltas_buffer: Option<ResizableBuffer<f32>>,
+    #[serde(skip)]
+    pub morph_weights_buffer: Option<ResizableBuffer<f32>>,
+    #[serde(skip)]
+    pub morph_info_buffer: Option<UniformBuffer<MorphTargetInfo>>,
 
     #[serde(skip)]
     pub available_animations: Vec<String>,
@@ -37,9 +51,6 @@ pub struct AnimationComponent {
 
     #[serde(skip)]
     pub morph_weights: HashMap<usize, Vec<f32>>,
-
-    #[serde(skip)]
-    pub morph_buffer: Option<wgpu::Buffer>,
 }
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -76,11 +87,13 @@ impl Default for AnimationComponent {
             animation_settings: HashMap::new(),
             local_pose: HashMap::new(),
             skinning_matrices: Vec::new(),
-            bone_buffer: None,
             available_animations: vec![],
             last_animation_index: None,
             morph_weights: HashMap::new(),
-            morph_buffer: None,
+            skinning_buffer: None,
+            morph_deltas_buffer: None,
+            morph_weights_buffer: None,
+            morph_info_buffer: None,
         }
     }
 }
@@ -442,54 +455,76 @@ impl AnimationComponent {
     }
 
     pub fn prepare_gpu_resources(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        if self.skinning_matrices.is_empty() {
+        let has_skinning = !self.skinning_matrices.is_empty();
+        let has_morph_weights = !self.morph_weights.is_empty();
+
+        if !has_skinning && !has_morph_weights {
             return;
         }
 
-        if !self.morph_weights.is_empty() {
+        if has_skinning {
+            let buffer = self.skinning_buffer.get_or_insert_with(|| {
+                ResizableBuffer::new(
+                    &graphics.device,
+                    self.skinning_matrices.len(),
+                    wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                    "skinning buffer",
+                )
+            });
+
+            buffer.write(&graphics.device, &graphics.queue, &self.skinning_matrices);
+        }
+
+        if has_skinning || has_morph_weights {
             let mut flat: Vec<f32> = Vec::new();
             let mut sorted_nodes: Vec<usize> = self.morph_weights.keys().cloned().collect();
             sorted_nodes.sort();
+            let mut num_targets: usize = 0;
+
             for node_idx in &sorted_nodes {
-                flat.extend_from_slice(&self.morph_weights[node_idx]);
+                let weights = &self.morph_weights[node_idx];
+                num_targets = num_targets.max(weights.len());
+                flat.extend_from_slice(weights);
             }
 
-            let data = bytemuck::cast_slice(&flat);
-
-            if let Some(buffer) = &self.morph_buffer {
-                if buffer.size() as usize == data.len() {
-                    graphics.queue.write_buffer(buffer, 0, data);
-                } else {
-                    // size changed (different animation, different target count) — recreate
-                    self.morph_buffer = None;
-                }
+            if flat.is_empty() {
+                flat.push(0.0);
             }
 
-            if self.morph_buffer.is_none() {
-                let buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                    label: Some("morph weights buffer"),
-                    contents: data,
-                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
-                });
+            let weights_buffer = self.morph_weights_buffer.get_or_insert_with(|| {
+                ResizableBuffer::new(
+                    &graphics.device,
+                    flat.len().max(1),
+                    wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                    "morph weights buffer",
+                )
+            });
+
+            weights_buffer.write(&graphics.device, &graphics.queue, &flat);
+
+            let info = MorphTargetInfo {
+                num_vertices: 0,
+                num_targets: num_targets as u32,
+                _padding: [0; 2],
+            };
 
-                self.morph_buffer = Some(buffer);
-            }
-        }
+            let info_buffer = self.morph_info_buffer.get_or_insert_with(|| {
+                UniformBuffer::new(&graphics.device, "morph info buffer")
+            });
 
-        let data = bytemuck::cast_slice(&self.skinning_matrices);
+            info_buffer.write(&graphics.queue, &info);
 
-        if let Some(buffer) = &self.bone_buffer {
-            graphics.queue.write_buffer(buffer, 0, data);
-        } else {
-            let buffer = graphics
-                .device
-                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                    label: Some("skinning buffer"),
-                    contents: data,
-                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
-                });
+            let morph_deltas = [0.0f32];
+            let deltas_buffer = self.morph_deltas_buffer.get_or_insert_with(|| {
+                ResizableBuffer::new(
+                    &graphics.device,
+                    morph_deltas.len(),
+                    wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                    "morph deltas buffer",
+                )
+            });
 
-            self.bone_buffer = Some(buffer);
+            deltas_buffer.write(&graphics.device, &graphics.queue, &morph_deltas);
         }
     }
 }
diff --git a/crates/dropbear-engine/src/buffer.rs b/crates/dropbear-engine/src/buffer.rs
index 8c4d124..8640d4b 100644
--- a/crates/dropbear-engine/src/buffer.rs
+++ b/crates/dropbear-engine/src/buffer.rs
@@ -2,34 +2,6 @@
 
 use std::marker::PhantomData;
 
-#[repr(C)]
-#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct Vertex {
-    pub position: [f32; 3],
-    pub tex_coords: [f32; 2],
-}
-
-impl Vertex {
-    pub fn desc() -> wgpu::VertexBufferLayout<'static> {
-        wgpu::VertexBufferLayout {
-            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
-            step_mode: wgpu::VertexStepMode::Vertex,
-            attributes: &[
-                wgpu::VertexAttribute {
-                    offset: 0,
-                    shader_location: 0,
-                    format: wgpu::VertexFormat::Float32x3,
-                },
-                wgpu::VertexAttribute {
-                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
-                    shader_location: 1,
-                    format: wgpu::VertexFormat::Float32x2,
-                },
-            ],
-        }
-    }
-}
-
 #[derive(Debug, Clone)]
 pub struct ResizableBuffer<T> {
     buffer: wgpu::Buffer,
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index e472ae8..8cf784c 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -1,3 +1,4 @@
+use crate::model::Vertex;
 use crate::{BindGroupLayouts, texture};
 use crate::{State, egui_renderer::EguiRenderer};
 use dropbear_future_queue::FutureQueue;
@@ -118,8 +119,8 @@ pub struct InstanceRaw {
     normal: [[f32; 3]; 3],
 }
 
-impl InstanceRaw {
-    pub fn desc() -> VertexBufferLayout<'static> {
+impl Vertex for InstanceRaw {
+    fn desc() -> VertexBufferLayout<'static> {
         VertexBufferLayout {
             array_stride: size_of::<InstanceRaw>() as BufferAddress,
             step_mode: wgpu::VertexStepMode::Instance,
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 3fdb8e7..415f737 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -334,22 +334,19 @@ impl BindGroupLayouts {
             });
 
         let light_cube_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-            label: Some("light cube bind group layout"),
+            label: Some("light cube light bind group layout"),
             entries: &[
-                // u_camera
+                // u_light
                 wgpu::BindGroupLayoutEntry {
                     binding: 0,
                     visibility: wgpu::ShaderStages::VERTEX,
-                    ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
                     count: None,
                 },
-                // u_light
-                wgpu::BindGroupLayoutEntry {
-                    binding: 1,
-                    visibility: wgpu::ShaderStages::VERTEX,
-                    ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
-                    count: None,
-                }
             ],
         });
 
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 6f0acc0..6e84d62 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -271,6 +271,7 @@ pub struct Light {
     pub cube_model: Handle<Model>,
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
+    pub bind_group: wgpu::BindGroup,
     pub instance_buffer: ResizableBuffer<InstanceInput>,
     pub component: LightComponent,
 }
@@ -308,6 +309,14 @@ impl Light {
         let label_str = label.unwrap_or("Light").to_string();
 
         let buffer = UniformBuffer::new(&graphics.device, &label_str);
+        let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some(&format!("{} light bind group", label_str)),
+            layout: &graphics.layouts.light_cube_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: buffer.buffer().as_entire_binding(),
+            }],
+        });
 
         let transform = light.to_transform();
         let instance: InstanceInput = DMat4::from_scale_rotation_translation(
@@ -333,6 +342,7 @@ impl Light {
             cube_model,
             label: label_str,
             buffer,
+            bind_group,
             instance_buffer,
             component: light.clone(),
         }
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 265786b..61a7ac1 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -1043,11 +1043,21 @@ impl Model {
             [255, 255, 255, 255],
             Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
         );
+        let black_srgb_texture = registry.solid_texture_rgba8_with_format(
+            graphics.clone(),
+            [0, 0, 0, 255],
+            Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+        );
         let white_linear_texture = registry.solid_texture_rgba8_with_format(
             graphics.clone(),
             [255, 255, 255, 255],
             Texture::TEXTURE_FORMAT_BASE,
         );
+        let green_linear_texture = registry.solid_texture_rgba8_with_format(
+            graphics.clone(),
+            [0, 255, 0, 255],
+            Texture::TEXTURE_FORMAT_BASE,
+        );
         let flat_normal_texture = registry.solid_texture_rgba8_with_format(
             graphics.clone(),
             [128, 128, 255, 255],
@@ -1140,7 +1150,7 @@ impl Model {
 
             let emissive_texture_bound = emissive_texture
                 .clone()
-                .or_else(|| registry.get_texture(white_srgb_texture).cloned())
+                .or_else(|| registry.get_texture(black_srgb_texture).cloned())
                 .ok_or_else(|| {
                     anyhow::anyhow!(
                         "Unable to resolve emissive fallback texture for model {:?}",
@@ -1149,7 +1159,7 @@ impl Model {
                 })?;
             let metallic_roughness_texture_bound = metallic_roughness_texture
                 .clone()
-                .or_else(|| registry.get_texture(white_linear_texture).cloned())
+                .or_else(|| registry.get_texture(green_linear_texture).cloned())
                 .ok_or_else(|| {
                     anyhow::anyhow!(
                         "Unable to resolve metallic fallback texture for model {:?}",
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 90c9037..4133516 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -17,8 +17,6 @@ pub struct LightCubePipeline {
     pipeline_layout: wgpu::PipelineLayout,
     pipeline: wgpu::RenderPipeline,
     storage_buffer: Option<StorageBuffer<LightArrayUniform>>,
-    /// Bind group, defined in `shaders/shader.wgsl` as @group(2)
-    light_bind_group: wgpu::BindGroup,
 }
 
 impl DropbearShaderPipeline for LightCubePipeline {
@@ -34,6 +32,7 @@ impl DropbearShaderPipeline for LightCubePipeline {
                 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                     label: Some("light cube pipeline layout"),
                     bind_group_layouts: &[
+                        &graphics.layouts.camera_bind_group_layout,
                         &graphics.layouts.light_cube_layout,
                     ],
                     push_constant_ranges: &[],
@@ -98,25 +97,11 @@ impl DropbearShaderPipeline for LightCubePipeline {
         let storage_buffer =
             StorageBuffer::new(&graphics.device, "light cube pipeline storage buffer");
 
-        let light_buffer: &wgpu::Buffer = storage_buffer.buffer();
-
-        let light_bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                layout: &graphics.layouts.light_cube_layout,
-                entries: &[wgpu::BindGroupEntry {
-                    binding: 0,
-                    resource: light_buffer.as_entire_binding(),
-                }],
-                label: Some("light array bind group"),
-            });
-
         Self {
             shader,
             pipeline_layout,
             pipeline,
             storage_buffer: Some(storage_buffer),
-            light_bind_group,
         }
     }
 
@@ -134,10 +119,6 @@ impl DropbearShaderPipeline for LightCubePipeline {
 }
 
 impl LightCubePipeline {
-    pub fn bind_group(&self) -> &wgpu::BindGroup {
-        &self.light_bind_group
-    }
-
     pub fn light_buffer(&self) -> &wgpu::Buffer {
         self.storage_buffer
             .as_ref()
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index 75b583c..9382ce1 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -82,43 +82,63 @@ impl ProcedurallyGeneratedObject {
         };
 
         let material = material.unwrap_or_else(|| {
-            let flat_normal_handle = _rguard.solid_texture_rgba8_with_format(
+            let white_srgb_texture = _rguard.solid_texture_rgba8_with_format(
                 graphics.clone(),
-                [128, 128, 255, 255],
-                Texture::TEXTURE_FORMAT_BASE,
+                [255, 255, 255, 255],
+                Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
             );
-            let white_srgb_handle = _rguard.solid_texture_rgba8_with_format(
+            let black_srgb_texture = _rguard.solid_texture_rgba8_with_format(
                 graphics.clone(),
-                [255, 255, 255, 255],
+                [0, 0, 0, 255],
                 Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
             );
-            let white_linear_handle = _rguard.solid_texture_rgba8_with_format(
+            let white_linear_texture = _rguard.solid_texture_rgba8_with_format(
                 graphics.clone(),
                 [255, 255, 255, 255],
                 Texture::TEXTURE_FORMAT_BASE,
             );
-            let flat_normal = _rguard
-                .get_texture(flat_normal_handle)
-                .expect("Flat normal texture handle missing")
-                .clone();
+            let green_linear_texture = _rguard.solid_texture_rgba8_with_format(
+                graphics.clone(),
+                [0, 255, 0, 255],
+                Texture::TEXTURE_FORMAT_BASE,
+            );
+            let flat_normal_texture = _rguard.solid_texture_rgba8_with_format(
+                graphics.clone(),
+                [128, 128, 255, 255],
+                Texture::TEXTURE_FORMAT_BASE,
+            );
+
             let white_srgb = _rguard
-                .get_texture(white_srgb_handle)
-                .expect("White SRGB texture handle missing")
-                .clone();
+                .get_texture(white_srgb_texture)
+                .cloned()
+                .expect("Missing procedural white srgb texture");
+            let black_srgb = _rguard
+                .get_texture(black_srgb_texture)
+                .cloned()
+                .expect("Missing procedural black srgb texture");
             let white_linear = _rguard
-                .get_texture(white_linear_handle)
-                .expect("White linear texture handle missing")
-                .clone();
+                .get_texture(white_linear_texture)
+                .cloned()
+                .expect("Missing procedural white linear texture");
+            let green_linear = _rguard
+                .get_texture(green_linear_texture)
+                .cloned()
+                .expect("Missing procedural green linear texture");
+            let flat_normal = _rguard
+                .get_texture(flat_normal_texture)
+                .cloned()
+                .expect("Missing procedural flat normal texture");
+
             Material::new(
                 graphics.clone(),
                 "procedural_material",
-                white_srgb.clone(),
+                white_srgb,
                 flat_normal,
                 None,
                 None,
                 None,
-                white_srgb,
-                white_linear.clone(),
+                black_srgb,
+                green_linear,
                 white_linear,
                 false,
                 [1.0, 1.0, 1.0, 1.0],
diff --git a/crates/dropbear-engine/src/shaders/light.slang b/crates/dropbear-engine/src/shaders/light.slang
index 1732d40..4aa7915 100644
--- a/crates/dropbear-engine/src/shaders/light.slang
+++ b/crates/dropbear-engine/src/shaders/light.slang
@@ -3,7 +3,7 @@
 [[vk::binding(0, 0)]]
 ConstantBuffer<CameraUniform> u_camera;
 
-[[vk::binding(1, 0)]]
+[[vk::binding(0, 1)]]
 ConstantBuffer<Light> u_light;
 
 struct VertexInput {
diff --git a/crates/eucalyptus-editor/src/editor/input.rs b/crates/eucalyptus-editor/src/editor/input.rs
index 5550da3..ec0d30a 100644
--- a/crates/eucalyptus-editor/src/editor/input.rs
+++ b/crates/eucalyptus-editor/src/editor/input.rs
@@ -326,8 +326,14 @@ impl Mouse for Editor {
                     .query_one::<(&mut Camera, &CameraComponent)>(active_camera)
                     .get()
             {
+                let frame_scale = if self.dt > 0.0 {
+                    (self.dt as f64 / (1.0 / 60.0)).clamp(0.1, 4.0)
+                } else {
+                    1.0
+                };
+
                 if let Some((dx, dy)) = delta {
-                    camera.track_mouse_delta(dx, dy);
+                    camera.track_mouse_delta(dx * frame_scale, dy * frame_scale);
                     self.input_state.mouse_delta = Some((dx, dy));
                 } else {
                     log_once::warn_once!("Unable to track mouse delta, attempting fallback");
@@ -335,7 +341,7 @@ impl Mouse for Editor {
                     if let Some(old_mouse_pos) = self.input_state.last_mouse_pos {
                         let dx = position.x - old_mouse_pos.0;
                         let dy = position.y - old_mouse_pos.1;
-                        camera.track_mouse_delta(dx, dy);
+                        camera.track_mouse_delta(dx * frame_scale, dy * frame_scale);
                         self.input_state.mouse_delta = Some((dx, dy));
                         log_once::debug_once!("Fallback mouse tracking used");
                     } else {
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 386d20b..1fb63a7 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -77,6 +77,7 @@ use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
 
 pub struct Editor {
+    pub dt: f32,
     pub scene_command: SceneCommand,
     pub world: Box<World>,
     pub physics_state: PhysicsState,
@@ -314,6 +315,7 @@ impl Editor {
             default_morph_weights_buffer: None,
             default_morph_info_buffer: None,
             default_animation_bind_group: None,
+            dt: 60.0,
         })
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 930c3f5..1db4caf 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -17,7 +17,6 @@ use eucalyptus_core::physics::collider::ColliderShapeKey;
 use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry};
 use eucalyptus_core::properties::CustomProperties;
 use eucalyptus_core::states::{Label, WorldLoadingStatus};
-use glam::{Mat4};
 use log;
 use parking_lot::Mutex;
 use std::collections::HashMap;
@@ -100,6 +99,8 @@ impl Scene for Editor {
         dt: f32,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
     ) {
+        self.dt = dt;
+
         if let Some(rx) = &self.play_mode_exit_rx {
             if rx.try_recv().is_ok() {
                 log::info!("Play mode process has exited, returning to editing mode");
@@ -347,12 +348,6 @@ impl Scene for Editor {
         log_once::debug_once!("Camera ready");
         log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
-        let Some(pipeline) = &self.main_render_pipeline else {
-            log_once::warn_once!("Render pipeline not ready");
-            return;
-        };
-        log_once::debug_once!("Pipeline ready");
-
         let Some(camera_bind_group) = self.camera_bind_group.as_ref() else {
             log_once::warn_once!("Camera bind group not ready");
             return;
@@ -414,14 +409,20 @@ impl Scene for Editor {
         }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(u64, InstanceRaw, Vec<Mat4>, Vec<f32>, u32, u32)> = Vec::new();
+        let mut animated_instances: Vec<(
+            u64,
+            InstanceRaw,
+            wgpu::Buffer,
+            wgpu::Buffer,
+            wgpu::Buffer,
+            wgpu::Buffer,
+        )> = Vec::new();
 
         {
-            puffin::profile_scope!("Locating all renderers and animation components");
-            let registry = ASSET_REGISTRY.read();
+            puffin::profile_scope!("finding all renderers and animation components");
             let mut query = self
                 .world
-                .query::<(&MeshRenderer, Option<&AnimationComponent>)>();
+                .query::<(&MeshRenderer, Option<&mut AnimationComponent>)>();
 
             for (renderer, animation) in query.iter() {
                 let handle = renderer.model();
@@ -431,50 +432,54 @@ impl Scene for Editor {
 
                 let instance = renderer.instance.to_raw();
                 if let Some(animation) = animation {
-                    if !animation.skinning_matrices.is_empty() {
-                        let morph_weights = if animation.morph_weights.is_empty() {
-                            Vec::new()
-                        } else {
-                            let mut sorted_nodes: Vec<usize> =
-                                animation.morph_weights.keys().cloned().collect();
-                            sorted_nodes.sort();
-                            let mut flat = Vec::new();
-                            for node_idx in &sorted_nodes {
-                                if let Some(weights) = animation.morph_weights.get(node_idx) {
-                                    flat.extend_from_slice(weights);
-                                }
-                            }
-                            flat
-                        };
-
-                        let num_targets = animation
-                            .morph_weights
-                            .values()
-                            .next()
-                            .map(|weights| weights.len() as u32)
-                            .unwrap_or(0);
-                        let num_vertices = registry
-                            .get_model(handle)
-                            .map(|model| {
-                                model
-                                    .meshes
-                                    .iter()
-                                    .map(|mesh| mesh.vertices.len())
-                                    .sum::<usize>() as u32
-                            })
-                            .unwrap_or(0);
-
-                        animated_instances.push((
-                            handle.id,
-                            instance,
-                            animation.skinning_matrices.clone(),
-                            morph_weights,
-                            num_vertices,
-                            num_targets,
-                        ));
-                    } else {
+                    if animation.skinning_matrices.is_empty() {
                         static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
                     }
+
+                    animation.prepare_gpu_resources(graphics.clone());
+
+                    let Some(skinning_buffer) = animation
+                        .skinning_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+                    let Some(morph_deltas_buffer) = animation
+                        .morph_deltas_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+                    let Some(morph_weights_buffer) = animation
+                        .morph_weights_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+                    let Some(morph_info_buffer) = animation
+                        .morph_info_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+
+                    animated_instances.push((
+                        handle.id,
+                        instance,
+                        skinning_buffer,
+                        morph_deltas_buffer,
+                        morph_weights_buffer,
+                        morph_info_buffer,
+                    ));
                 } else {
                     static_batches.entry(handle.id).or_default().push(instance);
                 }
@@ -543,54 +548,38 @@ impl Scene for Editor {
                         continue;
                     };
 
-                    render_pass.draw_light_model(model, camera_bind_group, light_pipeline.bind_group());
+                    render_pass.draw_light_model(model, camera_bind_group, &light.bind_group);
                 }
             }
         }
 
-        let default_skinning_buffer = self
-            .default_skinning_buffer
-            .as_ref()
-            .expect("Default skinning buffer not initialized");
-        let default_animation_bind_group = self
-            .default_animation_bind_group
-            .as_ref()
-            .expect("Default animation bind group not initialized");
-        let default_morph_weights_buffer = self
-            .default_morph_weights_buffer
-            .as_ref()
-            .expect("Default morph weights buffer not initialized");
-        let default_morph_info_buffer = self
-            .default_morph_info_buffer
-            .as_ref()
-            .expect("Default morph info buffer not initialized");
-
-        graphics.queue.write_buffer(
-            default_skinning_buffer,
-            0,
-            bytemuck::cast_slice(&[Mat4::IDENTITY]),
-        );
-        graphics.queue.write_buffer(
-            default_morph_info_buffer,
-            0,
-            bytemuck::cast_slice(&[0u32; 4]),
-        );
-
         // model rendering
         let sky = self
             .sky_pipeline
             .as_ref()
-            .expect("Sky pipeline must be initialized before rendering models");
+            .expect("Sky pipeline must be initialised before rendering models");
         let environment_bind_group = &sky.environment_bind_group;
 
-        let per_frame_bind_group = pipeline
-            .per_frame
-            .as_ref()
-            .expect("Per-frame bind group not initialized");
-        
+        let Some(pipeline) = self.main_render_pipeline.as_mut() else {
+            log_once::warn_once!("Render pipeline not ready");
+            return;
+        };
+        log_once::debug_once!("Pipeline ready");
+
+        // static models
         if let Some(_) = &self.light_cube_pipeline {
             puffin::profile_scope!("model render pass");
             for (model, handle, instance_count) in prepared_models {
+                let default_animation_bind_group = self
+                    .default_animation_bind_group
+                    .as_ref()
+                    .expect("Default animation bind group not initialised");
+                let per_frame_bind_group = pipeline
+                    .per_frame
+                    .as_ref()
+                    .expect("Per-frame bind group not initialised")
+                    .clone();
+
                 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("model render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -623,27 +612,51 @@ impl Scene for Editor {
                 render_pass.draw_model_instanced(
                     model,
                     0..instance_count,
-                    per_frame_bind_group,
+                    &per_frame_bind_group,
                     default_animation_bind_group,
                     environment_bind_group,
                 );
             }
         }
 
-        if let Some(_lcp) = &self.light_cube_pipeline {
+        // animated models
+        if let Some(_) = &self.light_cube_pipeline {
             puffin::profile_scope!("animated model render pass");
-            for (handle, instance, skinning_matrices, morph_weights, num_vertices, num_targets) in animated_instances {
+            for (
+                handle,
+                instance,
+                skinning_buffer,
+                morph_deltas_buffer,
+                morph_weights_buffer,
+                morph_info_buffer,
+            ) in animated_instances
+            {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
                 };
 
+                let animation_bind_group = pipeline
+                    .animation_bind_group(
+                        graphics.clone(),
+                        &skinning_buffer,
+                        &morph_deltas_buffer,
+                        &morph_weights_buffer,
+                        &morph_info_buffer,
+                    )
+                    .clone();
+                let per_frame_bind_group = pipeline
+                    .per_frame
+                    .as_ref()
+                    .expect("Per-frame bind group not initialised")
+                    .clone();
+
                 let instance_buffer = self.animated_instance_buffer.get_or_insert_with(|| {
                     ResizableBuffer::new(
                         &graphics.device,
                         1,
                         wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                        "Runtime Animated Instance Buffer",
+                        "animated instance buffer",
                     )
                 });
                 instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
@@ -674,41 +687,11 @@ impl Scene for Editor {
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
 
-                let max_skinning_matrices = 256usize;
-                let matrices = if skinning_matrices.len() > max_skinning_matrices {
-                    &skinning_matrices[..max_skinning_matrices]
-                } else {
-                    &skinning_matrices[..]
-                };
-                graphics.queue.write_buffer(
-                    default_skinning_buffer,
-                    0,
-                    bytemuck::cast_slice(matrices),
-                );
-
-                let weights = if morph_weights.len() > MAX_MORPH_WEIGHTS {
-                    &morph_weights[..MAX_MORPH_WEIGHTS]
-                } else {
-                    &morph_weights[..]
-                };
-                if !weights.is_empty() {
-                    graphics.queue.write_buffer(
-                        default_morph_weights_buffer,
-                        0,
-                        bytemuck::cast_slice(weights),
-                    );
-                }
-                graphics.queue.write_buffer(
-                    default_morph_info_buffer,
-                    0,
-                    bytemuck::cast_slice(&[num_vertices, num_targets, 0, 0]),
-                );
-
                 render_pass.draw_model_instanced(
                     model,
                     0..1,
-                    per_frame_bind_group,
-                    default_animation_bind_group,
+                    &per_frame_bind_group,
+                    &animation_bind_group,
                     environment_bind_group,
                 );
             }
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 18b4533..b2d62c0 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -1,7 +1,6 @@
 use std::sync::Arc;
 
 use crate::PlayMode;
-use crate::MAX_MORPH_WEIGHTS;
 use dropbear_engine::animation::AnimationComponent;
 use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
 use dropbear_engine::buffer::ResizableBuffer;
@@ -26,7 +25,7 @@ use eucalyptus_core::rapier3d::prelude::QueryFilter;
 use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResult};
 use eucalyptus_core::states::SCENES;
 use eucalyptus_core::states::{Label, PROJECT};
-use glam::{DVec3, Mat4, Quat, Vec2};
+use glam::{DVec3, Quat, Vec2};
 use hecs::Entity;
 // use kino_ui::widgets::rect::Rectangle;
 // use kino_ui::widgets::{Border, Fill};
@@ -650,12 +649,6 @@ impl Scene for PlayMode {
         log_once::debug_once!("Camera ready");
         log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
-        let Some(pipeline) = &self.main_pipeline else {
-            log_once::warn_once!("Render pipeline not ready");
-            return;
-        };
-        log_once::debug_once!("Pipeline ready");
-
         let Some(camera_bind_group) = self.camera_bind_group.as_ref() else {
             log_once::warn_once!("Camera bind group not ready");
             return;
@@ -714,13 +707,19 @@ impl Scene for PlayMode {
         }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(u64, InstanceRaw, Vec<Mat4>, Vec<f32>, u32, u32)> = Vec::new();
+        let mut animated_instances: Vec<(
+            u64,
+            InstanceRaw,
+            wgpu::Buffer,
+            wgpu::Buffer,
+            wgpu::Buffer,
+            wgpu::Buffer,
+        )> = Vec::new();
 
         {
-            let registry = ASSET_REGISTRY.read();
             let mut query = self
                 .world
-                .query::<(&MeshRenderer, Option<&AnimationComponent>)>();
+                .query::<(&MeshRenderer, Option<&mut AnimationComponent>)>();
 
             for (renderer, animation) in query.iter() {
                 let handle = renderer.model();
@@ -730,50 +729,54 @@ impl Scene for PlayMode {
 
                 let instance = renderer.instance.to_raw();
                 if let Some(animation) = animation {
-                    if !animation.skinning_matrices.is_empty() {
-                        let morph_weights = if animation.morph_weights.is_empty() {
-                            Vec::new()
-                        } else {
-                            let mut sorted_nodes: Vec<usize> =
-                                animation.morph_weights.keys().cloned().collect();
-                            sorted_nodes.sort();
-                            let mut flat = Vec::new();
-                            for node_idx in &sorted_nodes {
-                                if let Some(weights) = animation.morph_weights.get(node_idx) {
-                                    flat.extend_from_slice(weights);
-                                }
-                            }
-                            flat
-                        };
-
-                        let num_targets = animation
-                            .morph_weights
-                            .values()
-                            .next()
-                            .map(|weights| weights.len() as u32)
-                            .unwrap_or(0);
-                        let num_vertices = registry
-                            .get_model(handle)
-                            .map(|model| {
-                                model
-                                    .meshes
-                                    .iter()
-                                    .map(|mesh| mesh.vertices.len())
-                                    .sum::<usize>() as u32
-                            })
-                            .unwrap_or(0);
-
-                        animated_instances.push((
-                            handle.id,
-                            instance,
-                            animation.skinning_matrices.clone(),
-                            morph_weights,
-                            num_vertices,
-                            num_targets,
-                        ));
-                    } else {
+                    if animation.skinning_matrices.is_empty() {
                         static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
                     }
+
+                    animation.prepare_gpu_resources(graphics.clone());
+
+                    let Some(skinning_buffer) = animation
+                        .skinning_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+                    let Some(morph_deltas_buffer) = animation
+                        .morph_deltas_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+                    let Some(morph_weights_buffer) = animation
+                        .morph_weights_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+                    let Some(morph_info_buffer) = animation
+                        .morph_info_buffer
+                        .as_ref()
+                        .map(|buffer| buffer.buffer().clone())
+                    else {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                        continue;
+                    };
+
+                    animated_instances.push((
+                        handle.id,
+                        instance,
+                        skinning_buffer,
+                        morph_deltas_buffer,
+                        morph_weights_buffer,
+                        morph_info_buffer,
+                    ));
                 } else {
                     static_batches.entry(handle.id).or_default().push(instance);
                 }
@@ -841,52 +844,35 @@ impl Scene for PlayMode {
                         continue;
                     };
 
-                    render_pass.draw_light_model(model, camera_bind_group, light_pipeline.bind_group());
+                    render_pass.draw_light_model(model, camera_bind_group, &light.bind_group);
                 }
             }
         }
 
-        let default_skinning_buffer = self
-            .default_skinning_buffer
-            .as_ref()
-            .expect("Default skinning buffer not initialised");
         let default_animation_bind_group = self
             .default_animation_bind_group
             .as_ref()
-            .expect("Default animation bind group not initialized");
-        let default_morph_weights_buffer = self
-            .default_morph_weights_buffer
-            .as_ref()
-            .expect("Default morph weights buffer not initialised");
-        let default_morph_info_buffer = self
-            .default_morph_info_buffer
-            .as_ref()
-            .expect("Default morph info buffer not initialised");
-
-        graphics.queue.write_buffer(
-            default_skinning_buffer,
-            0,
-            bytemuck::cast_slice(&[Mat4::IDENTITY]),
-        );
-        graphics.queue.write_buffer(
-            default_morph_info_buffer,
-            0,
-            bytemuck::cast_slice(&[0u32; 4]),
-        );
+            .expect("Default animation bind group not initialised");
 
         // model rendering
         let sky = self
             .sky_pipeline
             .as_ref()
-            .expect("Sky pipeline must be initialized before rendering models");
+            .expect("Sky pipeline must be initialised before rendering models");
         let environment_bind_group = &sky.environment_bind_group;
 
-        let per_frame_bind_group = pipeline
-            .per_frame
-            .as_ref()
-            .expect("Per-frame bind group not initialized");
+        let Some(pipeline) = self.main_pipeline.as_mut() else {
+            log_once::warn_once!("Render pipeline not ready");
+            return;
+        };
+        log_once::debug_once!("Pipeline ready");
         
         for (model, handle, instance_count) in prepared_models {
+            let per_frame_bind_group = pipeline
+                .per_frame
+                .as_ref()
+                .expect("Per-frame bind group not initialised")
+                .clone();
             let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                 label: Some("model render pass"),
                 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -919,19 +905,42 @@ impl Scene for PlayMode {
             render_pass.draw_model_instanced(
                 model,
                 0..instance_count,
-                per_frame_bind_group,
+                &per_frame_bind_group,
                 default_animation_bind_group,
                 environment_bind_group,
             );
         }
 
         if let Some(_lcp) = &self.light_cube_pipeline {
-            for (handle, instance, skinning_matrices, morph_weights, num_vertices, num_targets) in animated_instances {
+            for (
+                handle,
+                instance,
+                skinning_buffer,
+                morph_deltas_buffer,
+                morph_weights_buffer,
+                morph_info_buffer,
+            ) in animated_instances
+            {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
                 };
 
+                let animation_bind_group = pipeline
+                    .animation_bind_group(
+                        graphics.clone(),
+                        &skinning_buffer,
+                        &morph_deltas_buffer,
+                        &morph_weights_buffer,
+                        &morph_info_buffer,
+                    )
+                    .clone();
+                let per_frame_bind_group = pipeline
+                    .per_frame
+                    .as_ref()
+                    .expect("Per-frame bind group not initialised")
+                    .clone();
+
                 let instance_buffer = self.animated_instance_buffer.get_or_insert_with(|| {
                     ResizableBuffer::new(
                         &graphics.device,
@@ -968,41 +977,11 @@ impl Scene for PlayMode {
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
 
-                let max_skinning_matrices = 256usize;
-                let matrices = if skinning_matrices.len() > max_skinning_matrices {
-                    &skinning_matrices[..max_skinning_matrices]
-                } else {
-                    &skinning_matrices[..]
-                };
-                graphics.queue.write_buffer(
-                    default_skinning_buffer,
-                    0,
-                    bytemuck::cast_slice(matrices),
-                );
-
-                let weights = if morph_weights.len() > MAX_MORPH_WEIGHTS {
-                    &morph_weights[..MAX_MORPH_WEIGHTS]
-                } else {
-                    &morph_weights[..]
-                };
-                if !weights.is_empty() {
-                    graphics.queue.write_buffer(
-                        default_morph_weights_buffer,
-                        0,
-                        bytemuck::cast_slice(weights),
-                    );
-                }
-                graphics.queue.write_buffer(
-                    default_morph_info_buffer,
-                    0,
-                    bytemuck::cast_slice(&[num_vertices, num_targets, 0, 0]),
-                );
-
                 render_pass.draw_model_instanced(
                     model,
                     0..1,
-                    per_frame_bind_group,
-                    default_animation_bind_group,
+                    &per_frame_bind_group,
+                    &animation_bind_group,
                     environment_bind_group,
                 );
             }