kitgit

tirbofish/dropbear · diff

fa395da · Thribhu K

wip: morph targets refactor: changed my mess of bind groups into one clean and organised thing :)

Unverified

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index b812f40..e067bef 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -28,14 +28,18 @@ pub struct AnimationComponent {
 
     #[serde(skip)]
     pub bone_buffer: Option<wgpu::Buffer>,
-    #[serde(skip)]
-    pub bind_group: Option<wgpu::BindGroup>,
 
     #[serde(skip)]
     pub available_animations: Vec<String>,
 
     #[serde(skip)]
     pub last_animation_index: Option<usize>,
+
+    #[serde(skip)]
+    pub morph_weights: HashMap<usize, Vec<f32>>,
+
+    #[serde(skip)]
+    pub morph_buffer: Option<wgpu::Buffer>,
 }
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -73,9 +77,10 @@ impl Default for AnimationComponent {
             local_pose: HashMap::new(),
             skinning_matrices: Vec::new(),
             bone_buffer: None,
-            bind_group: None,
             available_animations: vec![],
             last_animation_index: None,
+            morph_weights: HashMap::new(),
+            morph_buffer: None,
         }
     }
 }
@@ -95,6 +100,7 @@ impl AnimationComponent {
 
         if self.active_animation_index != self.last_animation_index {
             self.local_pose.clear();
+            self.morph_weights.clear();
             self.last_animation_index = self.active_animation_index;
         }
 
@@ -157,11 +163,11 @@ impl AnimationComponent {
             }
 
             if count == 1 || settings.time <= channel.times[0] {
-                Self::apply_single_keyframe(channel, 0, &mut self.local_pose, model);
+                Self::apply_single_keyframe(channel, 0, &mut self.local_pose, &mut self.morph_weights, model);
                 continue;
             }
             if settings.time >= channel.times[count - 1] {
-                Self::apply_single_keyframe(channel, count - 1, &mut self.local_pose, model);
+                Self::apply_single_keyframe(channel, count - 1, &mut self.local_pose, &mut self.morph_weights, model);
                 continue;
             }
 
@@ -295,6 +301,41 @@ impl AnimationComponent {
                         }
                     };
                 }
+                ChannelValues::MorphWeights(values) => {
+                    let weights = self.morph_weights
+                        .entry(channel.target_node)
+                        .or_insert_with(|| vec![0.0; values[0].len()]);
+
+                    *weights = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx].clone(),
+                        AnimationInterpolation::Linear => {
+                            let a = &values[prev_idx];
+                            let b = &values[next_idx];
+                            a.iter().zip(b.iter())
+                                .map(|(a, b)| a + (b - a) * factor)
+                                .collect()
+                        }
+                        AnimationInterpolation::CubicSpline => {
+                            // stored as [in_tangent, value, out_tangent] per keyframe
+                            let p0 = &values[prev_idx * 3 + 1];
+                            let m0 = &values[prev_idx * 3 + 2];
+                            let m1 = &values[next_idx * 3 + 0];
+                            let p1 = &values[next_idx * 3 + 1];
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                            let h10 = t3 - 2.0 * t2 + t;
+                            let h01 = -2.0 * t3 + 3.0 * t2;
+                            let h11 = t3 - t2;
+                            p0.iter().enumerate()
+                                .map(|(i, p0i)| {
+                                    p0i * h00 + m0[i] * dt * h10 + p1[i] * h01 + m1[i] * dt * h11
+                                })
+                                .collect()
+                        }
+                    };
+                }
             }
         }
 
@@ -310,6 +351,7 @@ impl AnimationComponent {
         channel: &crate::model::AnimationChannel,
         index: usize,
         pose: &mut HashMap<usize, NodeTransform>,
+        morph_weights: &mut HashMap<usize, Vec<f32>>,
         model: &Model,
     ) {
         let transform = pose.entry(channel.target_node).or_insert_with(|| {
@@ -336,6 +378,16 @@ impl AnimationComponent {
                     transform.scale = *val;
                 }
             }
+            ChannelValues::MorphWeights(v) => {
+                let actual_index = match channel.interpolation {
+                    AnimationInterpolation::CubicSpline => index * 3 + 1,
+                    _ => index,
+                };
+
+                if let Some(frame) = v.get(actual_index) {
+                    morph_weights.insert(channel.target_node, frame.clone());
+                }
+            },
         }
     }
 
@@ -394,6 +446,36 @@ impl AnimationComponent {
             return;
         }
 
+        if !self.morph_weights.is_empty() {
+            let mut flat: Vec<f32> = Vec::new();
+            let mut sorted_nodes: Vec<usize> = self.morph_weights.keys().cloned().collect();
+            sorted_nodes.sort();
+            for node_idx in &sorted_nodes {
+                flat.extend_from_slice(&self.morph_weights[node_idx]);
+            }
+
+            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 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,
+                });
+
+                self.morph_buffer = Some(buffer);
+            }
+        }
+
         let data = bytemuck::cast_slice(&self.skinning_matrices);
 
         if let Some(buffer) = &self.bone_buffer {
@@ -407,19 +489,7 @@ impl AnimationComponent {
                     usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
                 });
 
-            let bind_group = graphics
-                .device
-                .create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("skinning bind group"),
-                    layout: &graphics.layouts.skinning_bind_group_layout,
-                    entries: &[wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: buffer.as_entire_binding(),
-                    }],
-                });
-
             self.bone_buffer = Some(buffer);
-            self.bind_group = Some(bind_group);
         }
     }
 }
diff --git a/crates/dropbear-engine/src/bind_groups.rs b/crates/dropbear-engine/src/bind_groups.rs
index 0304090..72407f2 100644
--- a/crates/dropbear-engine/src/bind_groups.rs
+++ b/crates/dropbear-engine/src/bind_groups.rs
@@ -1,125 +1,3 @@
-use crate::buffer::UniformBuffer;
-use crate::graphics::SharedGraphicsContext;
-use crate::pipelines::globals::Globals;
-use wgpu::{BindGroup, Buffer};
-
-/// Bind groups for @group(0)
-pub struct SceneGlobalsBindGroup {
-    pub bind_group: BindGroup,
-}
-
-impl SceneGlobalsBindGroup {
-    pub fn new(
-        graphics: &SharedGraphicsContext,
-        globals_buffer: &UniformBuffer<Globals>,
-        camera_buffer: &Buffer,
-    ) -> Self {
-        let bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                label: Some("scene globals+camera bind group"),
-                layout: &graphics.layouts.scene_globals_bind_group_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: globals_buffer.buffer().as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 1,
-                        resource: camera_buffer.as_entire_binding(),
-                    },
-                ],
-            });
-
-        Self { bind_group }
-    }
-
-    pub fn update(
-        &mut self,
-        graphics: &SharedGraphicsContext,
-        globals_buffer: &UniformBuffer<Globals>,
-        camera_buffer: &Buffer,
-    ) {
-        puffin::profile_function!();
-        self.bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                label: Some("scene globals+camera bind group"),
-                layout: &graphics.layouts.scene_globals_bind_group_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: globals_buffer.buffer().as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 1,
-                        resource: camera_buffer.as_entire_binding(),
-                    },
-                ],
-            });
-    }
-
-    pub fn as_ref(&self) -> &BindGroup {
-        &self.bind_group
-    }
-}
-
-/// Bind group for @group(2)
-pub struct LightSkinBindGroup {
-    pub bind_group: BindGroup,
-}
-
-impl LightSkinBindGroup {
-    pub fn new(
-        graphics: &SharedGraphicsContext,
-        light_buffer: &Buffer,
-        skinning_buffer: &Buffer,
-    ) -> Self {
-        let bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                label: Some("scene light+skin bind group"),
-                layout: &graphics.layouts.scene_light_skin_bind_group_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: light_buffer.as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 1,
-                        resource: skinning_buffer.as_entire_binding(),
-                    },
-                ],
-            });
-
-        Self { bind_group }
-    }
-
-    pub fn update(
-        &mut self,
-        graphics: &SharedGraphicsContext,
-        light_buffer: &Buffer,
-        skinning_buffer: &Buffer,
-    ) {
-        self.bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                label: Some("scene light+skin bind group"),
-                layout: &graphics.layouts.scene_light_skin_bind_group_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: light_buffer.as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 1,
-                        resource: skinning_buffer.as_entire_binding(),
-                    },
-                ],
-            });
-    }
-
-    pub fn as_ref(&self) -> &BindGroup {
-        &self.bind_group
-    }
+pub struct PerFrameBindGroup {
+    
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index 5b54f02..daf780c 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -4,10 +4,7 @@ use std::sync::Arc;
 
 use glam::{DMat4, DQuat, DVec3, Mat4};
 use serde::{Deserialize, Serialize};
-use wgpu::{
-    BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor,
-    BindGroupLayoutEntry, BindingType, BufferBindingType, ShaderStages,
-};
+use wgpu::Buffer;
 
 use crate::{buffer::UniformBuffer, graphics::SharedGraphicsContext};
 
@@ -74,7 +71,6 @@ pub struct Camera {
     pub uniform: CameraUniform,
 
     buffer: UniformBuffer<CameraUniform>,
-    pub bind_group: BindGroup,
 
     /// View matrix
     pub view_mat: DMat4,
@@ -94,21 +90,6 @@ pub struct CameraBuilder {
 }
 
 impl Camera {
-    pub const CAMERA_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> =
-        BindGroupLayoutDescriptor {
-            entries: &[BindGroupLayoutEntry {
-                binding: 0,
-                visibility: ShaderStages::VERTEX.union(ShaderStages::FRAGMENT),
-                ty: BindingType::Buffer {
-                    ty: BufferBindingType::Uniform,
-                    has_dynamic_offset: false,
-                    min_binding_size: None,
-                },
-                count: None,
-            }],
-            label: Some("camera_bind_group_layout"),
-        };
-
     /// Creates a new camera
     pub fn new(
         graphics: Arc<SharedGraphicsContext>,
@@ -136,15 +117,6 @@ impl Camera {
 
         let buffer = UniformBuffer::new(&graphics.device, "camera uniform");
 
-        let bind_group = graphics.device.create_bind_group(&BindGroupDescriptor {
-            layout: &graphics.layouts.camera_bind_group_layout,
-            entries: &[BindGroupEntry {
-                binding: 0,
-                resource: buffer.buffer().as_entire_binding(),
-            }],
-            label: Some("camera_bind_group"),
-        });
-
         let camera = Self {
             eye: builder.eye,
             target: builder.target,
@@ -154,7 +126,6 @@ impl Camera {
             zfar: builder.zfar,
             uniform,
             buffer,
-            bind_group,
             yaw,
             pitch,
             settings: builder.settings,
@@ -202,7 +173,7 @@ impl Camera {
         yaw * pitch
     }
 
-    pub fn buffer(&self) -> &wgpu::Buffer {
+    pub fn buffer(&self) -> &Buffer {
         self.buffer.buffer()
     }
 
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 65993aa..e472ae8 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -22,7 +22,6 @@ pub struct SharedGraphicsContext {
     pub surface_format: TextureFormat,
     pub surface_config: Arc<RwLock<SurfaceConfiguration>>,
     pub instance: Arc<wgpu::Instance>,
-    pub layouts: Arc<BindGroupLayouts>,
     pub window: Arc<Window>,
     pub viewport_texture: texture::Texture,
     pub depth_texture: texture::Texture,
@@ -32,24 +31,10 @@ pub struct SharedGraphicsContext {
     pub mipmapper: Arc<MipMapper>,
     pub hdr: Arc<RwLock<HdrPipeline>>,
     pub antialiasing: Arc<RwLock<AntiAliasingMode>>,
+    pub layouts: Arc<BindGroupLayouts>,
 }
 
 impl SharedGraphicsContext {
-    pub const MODEL_UNIFORM_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> =
-        wgpu::BindGroupLayoutDescriptor {
-            entries: &[wgpu::BindGroupLayoutEntry {
-                binding: 0,
-                visibility: wgpu::ShaderStages::VERTEX,
-                ty: wgpu::BindingType::Buffer {
-                    ty: wgpu::BufferBindingType::Uniform,
-                    has_dynamic_offset: false,
-                    min_binding_size: None,
-                },
-                count: None,
-            }],
-            label: Some("model_uniform_bind_group_layout"),
-        };
-
     pub fn get_egui_context(&self) -> Context {
         self.egui_renderer.lock().context().clone()
     }
@@ -62,7 +47,6 @@ impl SharedGraphicsContext {
             device: state.device.clone(),
             queue: state.queue.clone(),
             instance: state.instance.clone(),
-            layouts: state.layouts.clone(),
             window: state.window.clone(),
             viewport_texture: state.viewport_texture.clone(),
             depth_texture: state.depth_texture.clone(),
@@ -72,10 +56,9 @@ impl SharedGraphicsContext {
             surface_format: state.surface_format,
             mipmapper: state.mipmapper.clone(),
             hdr: state.hdr.clone(),
-            // yakui_renderer: state.yakui_renderer.clone(),
-            // yakui_texture: state.yakui_texture.clone(),
             surface_config: state.config.clone(),
             antialiasing: state.antialiasing.clone(),
+            layouts: state.layouts.clone(),
         }
     }
 }
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index f50f994..3fdb8e7 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -55,7 +55,7 @@ use std::{
     sync::Arc,
     time::{Duration, Instant},
 };
-use wgpu::{BindGroupLayout, BindGroupLayoutEntry, BindingType, BufferBindingType, Device, ExperimentalFeatures, Instance, Queue, ShaderStages, Surface, SurfaceConfiguration, SurfaceError, TextureFormat, TextureFormatFeatureFlags};
+use wgpu::{BindGroupLayoutEntry, BindingType, BufferBindingType, Device, ExperimentalFeatures, Instance, Queue, ShaderStages, Surface, SurfaceConfiguration, SurfaceError, TextureFormat, TextureFormatFeatureFlags};
 use winit::event::{DeviceEvent, DeviceId};
 use winit::{
     application::ApplicationHandler,
@@ -65,10 +65,8 @@ use winit::{
     window::Window,
 };
 
-use crate::camera::Camera;
 use crate::egui_renderer::EguiRenderer;
 use crate::graphics::{CommandEncoder, SharedGraphicsContext};
-use crate::lighting::Light;
 use crate::mipmap::MipMapper;
 use crate::texture::Texture;
 
@@ -82,16 +80,288 @@ use winit::window::{WindowAttributes, WindowId};
 use crate::multisampling::{AntiAliasingMode};
 
 pub struct BindGroupLayouts {
-    pub scene_globals_bind_group_layout: BindGroupLayout,
-    pub shader_globals_bind_group_layout: BindGroupLayout,
-    pub material_bind_layout: BindGroupLayout,
-    pub camera_bind_group_layout: BindGroupLayout,
-    pub light_bind_group_layout: BindGroupLayout,
-    pub light_array_bind_group_layout: BindGroupLayout,
-    pub scene_light_skin_bind_group_layout: BindGroupLayout,
-    pub light_cube_bind_group_layout: BindGroupLayout,
-    pub environment_bind_group_layout: BindGroupLayout,
-    pub skinning_bind_group_layout: BindGroupLayout,
+    pub camera_bind_group_layout: wgpu::BindGroupLayout,
+    pub per_frame_layout: wgpu::BindGroupLayout,
+    pub material_bind_layout: wgpu::BindGroupLayout,
+    pub animation_layout: wgpu::BindGroupLayout,
+    pub environment_layout: wgpu::BindGroupLayout,
+    pub light_cube_layout: wgpu::BindGroupLayout,
+}
+
+impl BindGroupLayouts {
+    pub fn init(device: &wgpu::Device) -> Self {
+        let camera_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("camera bind group layout"),
+                entries: &[BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                }],
+            });
+
+        let per_frame_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("per frame bind group layout"),
+            entries: &[
+                // u_globals
+                BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+                // u_camera
+                BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+                // s_light_array
+                BindGroupLayoutEntry {
+                    binding: 2,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Storage {
+                            read_only: true
+                        },
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+            ],
+        });
+
+        let material_bind_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("material_bind_layout"),
+                entries: &[
+                    // u_material
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Buffer {
+                            ty: wgpu::BufferBindingType::Uniform,
+                            has_dynamic_offset: false,
+                            min_binding_size: None,
+                        },
+                        count: None,
+                    },
+                    // t_diffuse
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            multisampled: false,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        },
+                        count: None,
+                    },
+                    // s_diffuse
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 2,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                    // t_normal
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 3,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            multisampled: false,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        },
+                        count: None,
+                    },
+                    // s_normal
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 4,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                    // t_emissive
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 5,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            multisampled: false,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        },
+                        count: None,
+                    },
+                    // s_emissive
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 6,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                    // t_metallic
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 7,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            multisampled: false,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        },
+                        count: None,
+                    },
+                    // s_metallic
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 8,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                    // t_occlusion
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 9,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            multisampled: false,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        },
+                        count: None,
+                    },
+                    // s_occlusion
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 10,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                ],
+            });
+
+        let animation_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("animation bind group layout"),
+            entries: &[
+                // s_skinning
+                BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Storage {
+                            read_only: true
+                        },
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+                // s_morph_deltas
+                BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Storage {
+                            read_only: true
+                        },
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+                // s_morph_weights
+                BindGroupLayoutEntry {
+                    binding: 2,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Storage {
+                            read_only: true
+                        },
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+                // u_morph_info
+                BindGroupLayoutEntry {
+                    binding: 3,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+            ],
+        });
+
+        let environment_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("environment bind group layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                            view_dimension: wgpu::TextureViewDimension::Cube,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
+                        count: None,
+                    },
+                ],
+            });
+
+        let light_cube_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("light cube bind group layout"),
+            entries: &[
+                // u_camera
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::VERTEX,
+                    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,
+                }
+            ],
+        });
+
+        Self {
+            camera_bind_group_layout,
+            per_frame_layout,
+            material_bind_layout,
+            animation_layout,
+            environment_layout,
+            light_cube_layout,
+        }
+    }
 }
 
 /// The backend information, such as the device, queue, config, surface, renderer, window and more.
@@ -106,7 +376,6 @@ pub struct State {
     pub queue: Arc<Queue>,
     pub config: Arc<RwLock<SurfaceConfiguration>>,
     pub is_surface_configured: bool,
-    pub layouts: Arc<BindGroupLayouts>,
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
     pub depth_texture: Texture,
     pub viewport_texture: Texture,
@@ -115,6 +384,7 @@ pub struct State {
     pub mipmapper: Arc<MipMapper>,
     pub hdr: Arc<RwLock<HdrPipeline>>,
     pub antialiasing: Arc<RwLock<AntiAliasingMode>>,
+    pub layouts: Arc<BindGroupLayouts>,
 
     physics_accumulator: Duration,
 
@@ -280,236 +550,6 @@ Hardware:
             .renderer()
             .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear);
 
-        // shaders/shader.wgsl - @group(1)
-        let camera_bind_group_layout =
-            device.create_bind_group_layout(&Camera::CAMERA_BIND_GROUP_LAYOUT);
-
-        // shader/light.wgsl - @group(1)
-        let light_bind_group_layout =
-            device.create_bind_group_layout(&Light::LIGHT_BIND_GROUP_LAYOUT);
-
-        // shaders/light.wgsl - @group(1)
-        let light_cube_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                entries: &[wgpu::BindGroupLayoutEntry {
-                    // @binding(0)
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT),
-                    ty: wgpu::BindingType::Buffer {
-                        ty: wgpu::BufferBindingType::Uniform,
-                        has_dynamic_offset: false,
-                        min_binding_size: None,
-                    },
-                    count: None,
-                }],
-                label: Some("Per-Light Layout"),
-            });
-
-        // shaders/shader.wgsl - @group(0)
-        let scene_globals_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("scene globals bind group layout"),
-                entries: &[
-                    // u_globals
-                    BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
-                        ty: BindingType::Buffer {
-                            ty: BufferBindingType::Uniform,
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                    // u_camera
-                    BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
-                        ty: BindingType::Buffer {
-                            ty: BufferBindingType::Uniform,
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                ],
-            });
-
-        // shaders/shader.wgsl - @group(1)
-        let material_bind_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("material_bind_layout"),
-                entries: &[
-                    // u_material
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Buffer {
-                            ty: wgpu::BufferBindingType::Uniform,
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                    // t_diffuse
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            multisampled: false,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                        },
-                        count: None,
-                    },
-                    // s_diffuse
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 2,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                        count: None,
-                    },
-                    // t_normal
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 3,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            multisampled: false,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                        },
-                        count: None,
-                    },
-                    // s_normal
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 4,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                        count: None,
-                    },
-                    // t_emissive
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 5,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            multisampled: false,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                        },
-                        count: None,
-                    },
-                    // s_emissive
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 6,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                        count: None,
-                    },
-                    // t_metallic
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 7,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            multisampled: false,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                        },
-                        count: None,
-                    },
-                    // s_metallic
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 8,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                        count: None,
-                    },
-                    // t_occlusion
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 9,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            multisampled: false,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                        },
-                        count: None,
-                    },
-                    // s_occlusion
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 10,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                        count: None,
-                    },
-                ],
-            });
-
-        // shaders/light.wgsl - @group(1)
-        let light_array_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                entries: &[
-                    // @binding(0)
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Buffer {
-                            ty: wgpu::BufferBindingType::Storage { read_only: true },
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                ],
-                label: Some("Light Array Layout"),
-            });
-
-        // shaders/shader.wgsl - @group(2)
-        let scene_light_skin_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("scene light+skinning bind group layout"),
-                entries: &[
-                    // s_light_array
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Buffer {
-                            ty: wgpu::BufferBindingType::Storage { read_only: true },
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                    // s_skinning
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: wgpu::ShaderStages::VERTEX,
-                        ty: wgpu::BindingType::Buffer {
-                            ty: wgpu::BufferBindingType::Storage { read_only: true },
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                ],
-            });
-
-        // shaders/shader.wgsl - legacy globals layout
-        let shader_globals_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("shader.wgsl globals bind group layout"),
-                entries: &[BindGroupLayoutEntry {
-                    // @binding(0)
-                    binding: 0,
-                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
-                    ty: BindingType::Buffer {
-                        ty: BufferBindingType::Uniform,
-                        has_dynamic_offset: false,
-                        min_binding_size: None,
-                    },
-                    count: None,
-                }],
-            });
-
         let device = Arc::new(device);
         let queue = Arc::new(queue);
 
@@ -520,56 +560,7 @@ Hardware:
             antialiasing,
         )));
 
-        // let yakui_renderer = Arc::new(Mutex::new(yakui_wgpu::YakuiWgpu::new(
-        //     &device,
-        //     &queue
-        // )));
-
-        // let yakui_texture = yakui_renderer.lock().add_texture(
-        //     viewport_texture.view.clone(),
-        //     wgpu::FilterMode::default(),
-        //     wgpu::FilterMode::default(),
-        //     wgpu::FilterMode::default(),
-        //     wgpu::AddressMode::default(),
-        // );
-
-        let environment_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("environment bind group layout"),
-                entries: &[
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
-                            view_dimension: wgpu::TextureViewDimension::Cube,
-                            multisampled: false,
-                        },
-                        count: None,
-                    },
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
-                        count: None,
-                    },
-                ],
-            });
-
-        let skinning_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("skinning bind group layout"),
-                entries: &[wgpu::BindGroupLayoutEntry {
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::VERTEX,
-                    ty: wgpu::BindingType::Buffer {
-                        ty: wgpu::BufferBindingType::Storage { read_only: true },
-                        has_dynamic_offset: false,
-                        min_binding_size: None,
-                    },
-                    count: None,
-                }],
-            });
+        let layouts = BindGroupLayouts::init(&device);
 
         let result = Self {
             surface: Arc::new(surface),
@@ -589,21 +580,8 @@ Hardware:
             physics_accumulator: Duration::ZERO,
             scene_manager: scene::Manager::new(),
             antialiasing: Arc::new(RwLock::new(antialiasing)),
-            layouts: Arc::new(BindGroupLayouts {
-                scene_globals_bind_group_layout,
-                shader_globals_bind_group_layout,
-                material_bind_layout,
-                camera_bind_group_layout,
-                light_bind_group_layout,
-                light_array_bind_group_layout,
-                scene_light_skin_bind_group_layout,
-                light_cube_bind_group_layout,
-                environment_bind_group_layout,
-                skinning_bind_group_layout,
-            }),
-            // yakui_renderer,
-            // yakui_texture,
             hdr,
+            layouts: Arc::new(layouts),
         };
 
         Ok(result)
@@ -851,7 +829,6 @@ Hardware:
         drop(self.egui_renderer);
 
         drop(self.depth_texture);
-        drop(self.layouts);
 
         drop(self.surface);
 
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 731d584..6f0acc0 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -8,7 +8,6 @@ use crate::{entity::Transform, model::Model};
 use glam::{DMat4, DQuat, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
-use wgpu::BindGroup;
 
 pub const MAX_LIGHTS: usize = 10;
 
@@ -272,28 +271,11 @@ pub struct Light {
     pub cube_model: Handle<Model>,
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
-    pub bind_group: BindGroup,
     pub instance_buffer: ResizableBuffer<InstanceInput>,
     pub component: LightComponent,
 }
 
 impl Light {
-    pub const LIGHT_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> =
-        wgpu::BindGroupLayoutDescriptor {
-            // @binding(0)
-            entries: &[wgpu::BindGroupLayoutEntry {
-                binding: 0,
-                visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT),
-                ty: wgpu::BindingType::Buffer {
-                    ty: wgpu::BufferBindingType::Uniform,
-                    has_dynamic_offset: false,
-                    min_binding_size: None,
-                },
-                count: None,
-            }],
-            label: Some("light bind group layout descriptor"),
-        };
-
     pub async fn new(
         graphics: Arc<SharedGraphicsContext>,
         light: LightComponent,
@@ -327,17 +309,6 @@ impl Light {
 
         let buffer = UniformBuffer::new(&graphics.device, &label_str);
 
-        let bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                layout: &graphics.layouts.light_bind_group_layout,
-                entries: &[wgpu::BindGroupEntry {
-                    binding: 0,
-                    resource: buffer.buffer().as_entire_binding(),
-                }],
-                label,
-            });
-
         let transform = light.to_transform();
         let instance: InstanceInput = DMat4::from_scale_rotation_translation(
             transform.scale,
@@ -362,7 +333,6 @@ 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 25acec1..265786b 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -14,9 +14,9 @@ use serde::{Deserialize, Serialize};
 use std::hash::{DefaultHasher, Hash, Hasher};
 use std::sync::Arc;
 use std::{mem, ops::Range};
-use wgpu::{BindGroup, BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt};
+use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt};
 
-// do not derive clone otherwise it wil take too much memory
+// note to self: do not derive clone otherwise it wil take too much memory
 // #[derive(Clone)]
 pub struct Model {
     pub hash: u64, // also the id related to the handle
@@ -47,7 +47,6 @@ pub struct Material {
     pub emissive_texture: Option<Texture>,
     pub metallic_roughness_texture: Option<Texture>,
     pub occlusion_texture: Option<Texture>,
-    pub bind_group: wgpu::BindGroup,
     pub tint: [f32; 4],
     pub emissive_factor: [f32; 3],
     pub metallic_factor: f32,
@@ -59,6 +58,7 @@ pub struct Material {
     pub normal_scale: f32,
     pub uv_tiling: [f32; 2],
     pub tint_buffer: UniformBuffer<MaterialUniform>,
+    pub bind_group: wgpu::BindGroup,
     pub texture_tag: Option<String>,
     pub wrap_mode: TextureWrapMode,
     pub has_normal_texture: bool,
@@ -151,6 +151,7 @@ pub enum ChannelValues {
     Translations(Vec<glam::Vec3>),
     Rotations(Vec<glam::Quat>),
     Scales(Vec<glam::Vec3>),
+    MorphWeights(Vec<Vec<f32>>),
 }
 
 impl Material {
@@ -193,22 +194,65 @@ impl Material {
         let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
         tint_buffer.write(&graphics.queue, &uniform);
 
-        let bind_group = Self::create_bind_group(
-            &graphics,
-            &diffuse_texture,
-            &normal_texture,
-            &emissive_texture_bound,
-            &metallic_roughness_texture_bound,
-            &occlusion_texture_bound,
-            &tint_buffer,
-            &name,
-        );
+        let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("material bind group"),
+            layout: &graphics.layouts.material_bind_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: tint_buffer.buffer().as_entire_binding(),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 3,
+                    resource: wgpu::BindingResource::TextureView(&normal_texture.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 4,
+                    resource: wgpu::BindingResource::Sampler(&normal_texture.sampler),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 5,
+                    resource: wgpu::BindingResource::TextureView(&emissive_texture_bound.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 6,
+                    resource: wgpu::BindingResource::Sampler(&emissive_texture_bound.sampler),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 7,
+                    resource: wgpu::BindingResource::TextureView(
+                        &metallic_roughness_texture_bound.view,
+                    ),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 8,
+                    resource: wgpu::BindingResource::Sampler(
+                        &metallic_roughness_texture_bound.sampler,
+                    ),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 9,
+                    resource: wgpu::BindingResource::TextureView(&occlusion_texture_bound.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 10,
+                    resource: wgpu::BindingResource::Sampler(&occlusion_texture_bound.sampler),
+                },
+            ],
+        });
 
         Self {
             name,
             diffuse_texture,
             normal_texture,
-            bind_group,
             tint,
             emissive_factor: [0.0, 0.0, 0.0],
             metallic_factor: 1.0,
@@ -220,6 +264,7 @@ impl Material {
             normal_scale: 1.0,
             uv_tiling,
             tint_buffer,
+            bind_group,
             texture_tag,
             wrap_mode: TextureWrapMode::Repeat,
             emissive_texture,
@@ -229,71 +274,6 @@ impl Material {
         }
     }
 
-    pub fn create_bind_group(
-        graphics: &SharedGraphicsContext,
-        diffuse: &Texture,
-        normal: &Texture,
-        emissive: &Texture,
-        metallic_roughness: &Texture,
-        occlusion: &Texture,
-        uniform_buffer: &UniformBuffer<MaterialUniform>,
-        name: &str,
-    ) -> BindGroup {
-        puffin::profile_function!();
-        graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                label: Some(format!("{} texture bind group", name).as_str()),
-                layout: &graphics.layouts.material_bind_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: uniform_buffer.buffer().as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 1,
-                        resource: wgpu::BindingResource::TextureView(&diffuse.view),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 2,
-                        resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 3,
-                        resource: wgpu::BindingResource::TextureView(&normal.view),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 4,
-                        resource: wgpu::BindingResource::Sampler(&normal.sampler),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 5,
-                        resource: wgpu::BindingResource::TextureView(&emissive.view),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 6,
-                        resource: wgpu::BindingResource::Sampler(&emissive.sampler),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 7,
-                        resource: wgpu::BindingResource::TextureView(&metallic_roughness.view),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 8,
-                        resource: wgpu::BindingResource::Sampler(&metallic_roughness.sampler),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 9,
-                        resource: wgpu::BindingResource::TextureView(&occlusion.view),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 10,
-                        resource: wgpu::BindingResource::Sampler(&occlusion.sampler),
-                    },
-                ],
-            })
-    }
-
     pub fn sync_uniform(&self, graphics: &SharedGraphicsContext) {
         let uniform = MaterialUniform {
             base_colour: self.tint,
@@ -833,6 +813,18 @@ impl Model {
                     max_time = max_time.max(last_time);
                 }
 
+                let num_morph_targets = channel
+                    .target()
+                    .node()
+                    .mesh()
+                    .map(|mesh| {
+                        mesh.primitives()
+                            .next()
+                            .map(|prim| prim.morph_targets().count())
+                            .unwrap_or(0)
+                    })
+                    .unwrap_or(0);
+
                 let values = match target.property() {
                     gltf::animation::Property::Translation => {
                         puffin::profile_scope!("reading translation values");
@@ -894,17 +886,30 @@ impl Model {
                     }
                     gltf::animation::Property::MorphTargetWeights => {
                         puffin::profile_scope!("reading morph target weights");
-                        // Skip morph targets for now
-                        continue;
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::MorphTargetWeights(iter) => {
+                                    let flat: Vec<f32> = iter.into_f32().collect();
+
+                                    let weights: Vec<Vec<f32>> = flat
+                                        .chunks(num_morph_targets)
+                                        .map(|chunk| chunk.to_vec())
+                                        .collect();
+
+                                    ChannelValues::MorphWeights(weights)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
                     }
                 };
 
                 let interpolation = match channel.sampler().interpolation() {
                     gltf::animation::Interpolation::Linear => AnimationInterpolation::Linear,
                     gltf::animation::Interpolation::Step => AnimationInterpolation::Step,
-                    gltf::animation::Interpolation::CubicSpline => {
-                        AnimationInterpolation::CubicSpline
-                    }
+                    gltf::animation::Interpolation::CubicSpline => AnimationInterpolation::CubicSpline, // note with morph target weights
                 };
 
                 channels.push(AnimationChannel {
@@ -1300,7 +1305,7 @@ pub trait DrawModel<'a> {
         mesh: &'a Mesh,
         material: &'a Material,
         globals_camera_bind_group: &'a wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     );
     fn draw_mesh_instanced(
@@ -1309,7 +1314,7 @@ pub trait DrawModel<'a> {
         material: &'a Material,
         instances: Range<u32>,
         globals_camera_bind_group: &'a wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     );
 
@@ -1318,7 +1323,7 @@ pub trait DrawModel<'a> {
         &mut self,
         model: &'a Model,
         globals_camera_bind_group: &'a wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     );
     fn draw_model_instanced(
@@ -1326,7 +1331,7 @@ pub trait DrawModel<'a> {
         model: &'a Model,
         instances: Range<u32>,
         globals_camera_bind_group: &'a wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     );
 }
@@ -1340,7 +1345,7 @@ where
         mesh: &'b Mesh,
         material: &'b Material,
         globals_camera_bind_group: &'b wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     ) {
         self.draw_mesh_instanced(
@@ -1348,7 +1353,7 @@ where
             material,
             0..1,
             globals_camera_bind_group,
-            light_skin_bind_group,
+            animation_bind_group,
             environment_bind_group,
         );
     }
@@ -1359,14 +1364,14 @@ where
         material: &'b Material,
         instances: Range<u32>,
         globals_camera_bind_group: &'b wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     ) {
         self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
         self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
         self.set_bind_group(0, globals_camera_bind_group, &[]);
         self.set_bind_group(1, &material.bind_group, &[]);
-        self.set_bind_group(2, light_skin_bind_group, &[]);
+        self.set_bind_group(2, animation_bind_group, &[]);
         self.set_bind_group(3, environment_bind_group, &[]);
 
         self.draw_indexed(0..mesh.num_elements, 0, instances);
@@ -1376,14 +1381,14 @@ where
         &mut self,
         model: &'b Model,
         globals_camera_bind_group: &'b wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     ) {
         self.draw_model_instanced(
             model,
             0..1,
             globals_camera_bind_group,
-            light_skin_bind_group,
+            animation_bind_group,
             environment_bind_group
         );
     }
@@ -1393,7 +1398,7 @@ where
         model: &'b Model,
         instances: Range<u32>,
         globals_camera_bind_group: &'b wgpu::BindGroup,
-        light_skin_bind_group: &'a wgpu::BindGroup,
+        animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     ) {
         for mesh in &model.meshes {
@@ -1403,7 +1408,7 @@ where
                 material,
                 instances.clone(),
                 globals_camera_bind_group,
-                light_skin_bind_group,
+                animation_bind_group,
                 environment_bind_group,
             );
         }
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
index d552663..4120217 100644
--- a/crates/dropbear-engine/src/pipelines/globals.rs
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -25,7 +25,6 @@ impl Default for Globals {
 pub struct GlobalsUniform {
     pub data: Globals,
     pub buffer: UniformBuffer<Globals>,
-    pub bind_group: wgpu::BindGroup,
 }
 
 impl GlobalsUniform {
@@ -34,24 +33,12 @@ impl GlobalsUniform {
 
         let buffer = UniformBuffer::new(&graphics.device, label);
 
-        let bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                layout: &graphics.layouts.shader_globals_bind_group_layout,
-                entries: &[wgpu::BindGroupEntry {
-                    binding: 0,
-                    resource: buffer.buffer().as_entire_binding(),
-                }],
-                label: Some(label),
-            });
-
         let data = Globals::default();
         buffer.write(&graphics.queue, &data);
 
         Self {
             data,
             buffer,
-            bind_group,
         }
     }
 
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 7f346d7..90c9037 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -34,8 +34,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_bind_group_layout,
+                        &graphics.layouts.light_cube_layout,
                     ],
                     push_constant_ranges: &[],
                 });
@@ -104,7 +103,7 @@ impl DropbearShaderPipeline for LightCubePipeline {
         let light_bind_group = graphics
             .device
             .create_bind_group(&wgpu::BindGroupDescriptor {
-                layout: &graphics.layouts.light_array_bind_group_layout,
+                layout: &graphics.layouts.light_cube_layout,
                 entries: &[wgpu::BindGroupEntry {
                     binding: 0,
                     resource: light_buffer.as_entire_binding(),
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
index 0e8b053..037e973 100644
--- a/crates/dropbear-engine/src/pipelines/shader.rs
+++ b/crates/dropbear-engine/src/pipelines/shader.rs
@@ -1,7 +1,7 @@
 use crate::graphics::{InstanceRaw, SharedGraphicsContext};
 use crate::model;
 use crate::model::Vertex;
-use crate::pipelines::DropbearShaderPipeline;
+use crate::pipelines::{DropbearShaderPipeline};
 use crate::shader::Shader;
 use crate::texture::Texture;
 use std::sync::Arc;
@@ -12,6 +12,11 @@ pub struct MainRenderPipeline {
     shader: Shader,
     pipeline_layout: wgpu::PipelineLayout,
     pipeline: wgpu::RenderPipeline,
+
+    pub per_frame: Option<wgpu::BindGroup>,
+    pub per_material: Option<wgpu::BindGroup>,
+    pub animation: Option<wgpu::BindGroup>,
+    pub environment: Option<wgpu::BindGroup>,
 }
 
 impl DropbearShaderPipeline for MainRenderPipeline {
@@ -23,10 +28,10 @@ impl DropbearShaderPipeline for MainRenderPipeline {
         );
 
         let bind_group_layouts = vec![
-            &graphics.layouts.scene_globals_bind_group_layout, // @group(0)
-            &graphics.layouts.material_bind_layout,            // @group(1)
-            &graphics.layouts.scene_light_skin_bind_group_layout, // @group(2)
-            &graphics.layouts.environment_bind_group_layout,    // @group(3)
+            &graphics.layouts.per_frame_layout,
+            &graphics.layouts.material_bind_layout, 
+            &graphics.layouts.animation_layout,
+            &graphics.layouts.environment_layout,
         ];
 
         let pipeline_layout =
@@ -92,13 +97,13 @@ impl DropbearShaderPipeline for MainRenderPipeline {
             shader,
             pipeline_layout,
             pipeline,
+            per_frame: None,
+            per_material: None,
+            animation: None,
+            environment: None,
         }
     }
 
-    fn shader(&self) -> &Shader {
-        &self.shader
-    }
-
     fn pipeline_layout(&self) -> &wgpu::PipelineLayout {
         &self.pipeline_layout
     }
@@ -106,4 +111,177 @@ impl DropbearShaderPipeline for MainRenderPipeline {
     fn pipeline(&self) -> &wgpu::RenderPipeline {
         &self.pipeline
     }
+    
+    fn shader(&self) -> &Shader {
+        &self.shader
+    }
+}
+
+impl MainRenderPipeline {
+    pub fn per_frame_bind_group(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        globals_buffer: &wgpu::Buffer,
+        camera_buffer: &wgpu::Buffer,
+        light_array_buffer: &wgpu::Buffer,
+    ) -> &wgpu::BindGroup {
+        if self.per_frame.is_none() {
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("per frame bind group"),
+                layout: &graphics.layouts.per_frame_layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: globals_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: camera_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 2,
+                        resource: light_array_buffer.as_entire_binding(),
+                    },
+                ],
+            });
+
+            self.per_frame = Some(bind_group);
+        }
+
+        self.per_frame.as_ref().unwrap() // safe as its guaranteed to always have some content
+    }
+
+    pub fn per_material_bind_group(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        material_uniform_buffer: &wgpu::Buffer,
+        diffuse_texture: &Texture,
+        normal_texture: &Texture,
+        emissive_texture: &Texture,
+        metallic_texture: &Texture,
+        occlusion_texture: &Texture,
+    ) -> &wgpu::BindGroup {
+        if self.per_material.is_none() {
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("per material bind group"),
+                layout: &graphics.layouts.material_bind_layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: material_uniform_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 2,
+                        resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 3,
+                        resource: wgpu::BindingResource::TextureView(&normal_texture.view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 4,
+                        resource: wgpu::BindingResource::Sampler(&normal_texture.sampler),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 5,
+                        resource: wgpu::BindingResource::TextureView(&emissive_texture.view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 6,
+                        resource: wgpu::BindingResource::Sampler(&emissive_texture.sampler),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 7,
+                        resource: wgpu::BindingResource::TextureView(&metallic_texture.view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 8,
+                        resource: wgpu::BindingResource::Sampler(&metallic_texture.sampler),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 9,
+                        resource: wgpu::BindingResource::TextureView(&occlusion_texture.view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 10,
+                        resource: wgpu::BindingResource::Sampler(&occlusion_texture.sampler),
+                    },
+                ],
+            });
+
+            self.per_material = Some(bind_group);
+        }
+
+        self.per_material.as_ref().unwrap()
+    }
+
+    pub fn animation_bind_group(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        skinning_buffer: &wgpu::Buffer,
+        morph_deltas_buffer: &wgpu::Buffer,
+        morph_weights_buffer: &wgpu::Buffer,
+        morph_info_buffer: &wgpu::Buffer,
+    ) -> &wgpu::BindGroup {
+        if self.animation.is_none() {
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("animation bind group"),
+                layout: &graphics.layouts.animation_layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: skinning_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: morph_deltas_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 2,
+                        resource: morph_weights_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 3,
+                        resource: morph_info_buffer.as_entire_binding(),
+                    },
+                ],
+            });
+
+            self.animation = Some(bind_group);
+        }
+
+        self.animation.as_ref().unwrap()
+    }
+
+    pub fn environment_bind_group(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        environment_view: &wgpu::TextureView,
+        environment_sampler: &wgpu::Sampler,
+    ) -> &wgpu::BindGroup {
+        if self.environment.is_none() {
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("environment bind group"),
+                layout: &graphics.layouts.environment_layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: wgpu::BindingResource::TextureView(environment_view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: wgpu::BindingResource::Sampler(environment_sampler),
+                    },
+                ],
+            });
+
+            self.environment = Some(bind_group);
+        }
+
+        self.environment.as_ref().unwrap()
+    }
 }
diff --git a/crates/dropbear-engine/src/shaders/light.slang b/crates/dropbear-engine/src/shaders/light.slang
index 9a2a330..1732d40 100644
--- a/crates/dropbear-engine/src/shaders/light.slang
+++ b/crates/dropbear-engine/src/shaders/light.slang
@@ -1,10 +1,10 @@
 #include "dropbear.slang"
 
 [[vk::binding(0, 0)]]
-ConstantBuffer<CameraUniform> camera;
+ConstantBuffer<CameraUniform> u_camera;
 
-[[vk::binding(0, 1)]]
-ConstantBuffer<Light> light;
+[[vk::binding(1, 0)]]
+ConstantBuffer<Light> u_light;
 
 struct VertexInput {
     [[vk::location(0)]]
@@ -40,8 +40,8 @@ VertexOutput vs_main(VertexInput input) {
     );
 
     VertexOutput output;
-    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
-    output.color = light.color.xyz;
+    output.clip_position = mul(u_camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
+    output.color = u_light.color.xyz;
     return output;
 }
 
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index e9e2176..9c3a9d4 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -40,11 +40,20 @@ struct MaterialUniform {
     has_occlusion_texture: u32, // 0,1 bool
 }
 
+struct MorphTargetInfo {
+    num_vertices: u32,
+    num_targets: u32,
+}
+
+// per-frame
 @group(0) @binding(0)
 var<uniform> u_globals: Globals;
 @group(0) @binding(1)
 var<uniform> u_camera: CameraUniform;
+@group(0) @binding(2)
+var<storage, read> s_light_array: array<Light>;
 
+// per-material
 @group(1) @binding(0)
 var<uniform> u_material: MaterialUniform;
 @group(1) @binding(1)
@@ -68,11 +77,17 @@ var t_occlusion: texture_2d<f32>;
 @group(1) @binding(10)
 var s_occlusion: sampler;
 
+// animation
 @group(2) @binding(0)
-var<storage, read> s_light_array: array<Light>;
-@group(2) @binding(1)
 var<storage, read> s_skinning: array<mat4x4<f32>>;
+@group(2) @binding(1)
+var<storage, read> s_morph_deltas: array<f32>;
+@group(2) @binding(2)
+var<storage, read> s_morph_weights: array<f32>;
+@group(2) @binding(3)
+var<uniform> u_morph_info: MorphTargetInfo;
 
+// environment
 @group(3) @binding(0)
 var env_map: texture_cube<f32>;
 @group(3) @binding(1)
diff --git a/crates/dropbear-engine/src/sky.rs b/crates/dropbear-engine/src/sky.rs
index e6df4c3..4d34df3 100644
--- a/crates/dropbear-engine/src/sky.rs
+++ b/crates/dropbear-engine/src/sky.rs
@@ -1,5 +1,5 @@
 use crate::graphics::SharedGraphicsContext;
-use crate::pipelines::create_render_pipeline_ex;
+use crate::pipelines::{create_render_pipeline_ex};
 use crate::texture::Texture;
 use image::codecs::hdr::HdrDecoder;
 use std::io::Cursor;
@@ -264,29 +264,78 @@ impl HdrLoader {
 pub struct SkyPipeline {
     pub texture: CubeTexture,
     pub pipeline: wgpu::RenderPipeline,
-    pub bind_group: wgpu::BindGroup,
+    pub camera_layout: wgpu::BindGroupLayout,
+    pub environment_layout: wgpu::BindGroupLayout,
+    pub camera_bind_group: wgpu::BindGroup,
+    pub environment_bind_group: wgpu::BindGroup,
 }
 
 impl SkyPipeline {
-    pub fn new(graphics: Arc<SharedGraphicsContext>, sky_texture: CubeTexture) -> Self {
+    pub fn new(
+        graphics: Arc<SharedGraphicsContext>,
+        sky_texture: CubeTexture,
+        camera_buffer: &wgpu::Buffer,
+    ) -> Self {
         puffin::profile_function!();
-        let environment_bind_group =
-            graphics
-                .device
-                .create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("environment_bind_group"),
-                    layout: &graphics.layouts.environment_bind_group_layout,
-                    entries: &[
-                        wgpu::BindGroupEntry {
-                            binding: 0,
-                            resource: wgpu::BindingResource::TextureView(&sky_texture.view()),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 1,
-                            resource: wgpu::BindingResource::Sampler(sky_texture.sampler()),
-                        },
-                    ],
-                });
+        let camera_layout = graphics.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("sky camera bind group layout"),
+            entries: &[wgpu::BindGroupLayoutEntry {
+                binding: 0,
+                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
+                ty: wgpu::BindingType::Buffer {
+                    ty: wgpu::BufferBindingType::Uniform,
+                    has_dynamic_offset: false,
+                    min_binding_size: None,
+                },
+                count: None,
+            }],
+        });
+
+        let environment_layout = graphics.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("sky environment bind group layout"),
+            entries: &[
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                        view_dimension: wgpu::TextureViewDimension::Cube,
+                        multisampled: false,
+                    },
+                    count: None,
+                },
+                wgpu::BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
+                    count: None,
+                },
+            ],
+        });
+
+        let camera_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("sky camera bind group"),
+            layout: &camera_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: camera_buffer.as_entire_binding(),
+            }],
+        });
+
+        let environment_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("sky environment bind group"),
+            layout: &environment_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(sky_texture.view()),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(sky_texture.sampler()),
+                },
+            ],
+        });
 
         let sky_pipeline = {
             let layout = graphics
@@ -294,8 +343,8 @@ impl SkyPipeline {
                 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                     label: Some("Sky Pipeline Layout"),
                     bind_group_layouts: &[
-                        &graphics.layouts.camera_bind_group_layout,
-                        &graphics.layouts.environment_bind_group_layout,
+                        &camera_layout,
+                        &environment_layout,
                     ],
                     push_constant_ranges: &[],
                 });
@@ -318,7 +367,10 @@ impl SkyPipeline {
         Self {
             texture: sky_texture,
             pipeline: sky_pipeline,
-            bind_group: environment_bind_group,
+            camera_layout,
+            environment_layout,
+            camera_bind_group,
+            environment_bind_group,
         }
     }
 }
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 62b475a..68ed8ca 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -7,60 +7,6 @@ use image::GenericImageView;
 use serde::{Deserialize, Serialize};
 use crate::multisampling::{AntiAliasingMode};
 
-/// As defined in `shaders.wgsl` as
-/// ```
-/// @group(0) @binding(0)
-/// var t_diffuse: texture_2d<f32>;
-/// @group(0) @binding(1)
-/// var s_diffuse: sampler;
-/// @group(0) @binding(2)
-/// var t_normal: texture_2d<f32>;
-/// @group(0) @binding(3)
-/// var s_normal: sampler;
-/// ```
-pub const TEXTURE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> =
-    wgpu::BindGroupLayoutDescriptor {
-        entries: &[
-            // t_diffuse
-            wgpu::BindGroupLayoutEntry {
-                binding: 0,
-                visibility: wgpu::ShaderStages::FRAGMENT,
-                ty: wgpu::BindingType::Texture {
-                    multisampled: false,
-                    view_dimension: wgpu::TextureViewDimension::D2,
-                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                },
-                count: None,
-            },
-            // s_diffuse
-            wgpu::BindGroupLayoutEntry {
-                binding: 1,
-                visibility: wgpu::ShaderStages::FRAGMENT,
-                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                count: None,
-            },
-            // t_normal
-            wgpu::BindGroupLayoutEntry {
-                binding: 2,
-                visibility: wgpu::ShaderStages::FRAGMENT,
-                ty: wgpu::BindingType::Texture {
-                    multisampled: false,
-                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                    view_dimension: wgpu::TextureViewDimension::D2,
-                },
-                count: None,
-            },
-            // s_normal
-            wgpu::BindGroupLayoutEntry {
-                binding: 3,
-                visibility: wgpu::ShaderStages::FRAGMENT,
-                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                count: None,
-            },
-        ],
-        label: Some("texture bind group layout"),
-    };
-
 #[derive(Clone)]
 /// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture.
 pub struct Texture {
diff --git a/crates/eucalyptus-core/src/asset/model.rs b/crates/eucalyptus-core/src/asset/model.rs
index ec3e4ea..63e56ac 100644
--- a/crates/eucalyptus-core/src/asset/model.rs
+++ b/crates/eucalyptus-core/src/asset/model.rs
@@ -407,6 +407,7 @@ pub enum NChannelValues {
     Translations { values: Vec<NVector3> },
     Rotations { values: Vec<NQuaternion> },
     Scales { values: Vec<NVector3> },
+    MorphWeights { values: Vec<Vec<f64>> },
 }
 
 impl ToJObject for NChannelValues {
@@ -442,6 +443,16 @@ impl ToJObject for NChannelValues {
                     .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
                 Ok(obj)
             }
+            NChannelValues::MorphWeights { values } => {
+                let class = env
+                    .find_class("com/dropbear/asset/model/ChannelValues$MorphWeights")
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+                let list = values.to_jobject(env)?;
+                let obj = env
+                    .new_object(&class, "(Ljava/util/List;)V", &[JValue::Object(&list)])
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+                Ok(obj)
+            }
         }
     }
 }
@@ -570,6 +581,12 @@ fn map_channel_values(values: &ChannelValues) -> NChannelValues {
         ChannelValues::Scales(list) => NChannelValues::Scales {
             values: list.iter().map(|v| NVector3::from(*v)).collect(),
         },
+        ChannelValues::MorphWeights(items) => NChannelValues::MorphWeights {
+            values: items
+                .iter()
+                .map(|weights| weights.iter().map(|v| *v as f64).collect())
+                .collect(),
+        },
     }
 }
 
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index e158ee2..386d20b 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -13,6 +13,8 @@ pub mod viewport;
 
 pub(crate) use crate::editor::dock::*;
 
+const MAX_MORPH_WEIGHTS: usize = 4096;
+
 use crate::about::AboutWindow;
 use crate::build::build;
 use crate::debug;
@@ -57,6 +59,7 @@ use eucalyptus_core::{
     utils::ViewportMode,
     warn,
 };
+use glam::Mat4;
 use hecs::{Entity, World};
 use log::{debug, error};
 use parking_lot::{Mutex, RwLock};
@@ -67,6 +70,7 @@ use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, time::Instant};
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
 use wgpu::{Color, Extent3d};
+use wgpu::util::DeviceExt;
 use winit::dpi::PhysicalSize;
 use dropbear_engine::multisampling::AntiAliasingMode;
 use winit::window::{CursorGrabMode, WindowAttributes};
@@ -92,10 +96,12 @@ pub struct Editor {
     pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     pub mipmapper: Option<MipMapper>,
     pub sky_pipeline: Option<SkyPipeline>,
+    pub(crate) camera_bind_group: Option<wgpu::BindGroup>,
     pub(crate) default_skinning_buffer: Option<wgpu::Buffer>,
-    pub(crate) default_skinning_bind_group: Option<wgpu::BindGroup>,
-    pub(crate) light_skin_bind_group: Option<wgpu::BindGroup>,
-    pub(crate) scene_globals_bind_group: Option<dropbear_engine::bind_groups::SceneGlobalsBindGroup>,
+    pub(crate) default_morph_deltas_buffer: Option<wgpu::Buffer>,
+    pub(crate) default_morph_weights_buffer: Option<wgpu::Buffer>,
+    pub(crate) default_morph_info_buffer: Option<wgpu::Buffer>,
+    pub(crate) default_animation_bind_group: Option<wgpu::BindGroup>,
 
     pub active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -302,10 +308,12 @@ impl Editor {
             collider_instance_buffer: None,
             mipmapper: None,
             sky_pipeline: None,
+            camera_bind_group: None,
             default_skinning_buffer: None,
-            default_skinning_bind_group: None,
-            light_skin_bind_group: None,
-            scene_globals_bind_group: None,
+            default_morph_deltas_buffer: None,
+            default_morph_weights_buffer: None,
+            default_morph_info_buffer: None,
+            default_animation_bind_group: None,
         })
     }
 
@@ -1402,25 +1410,146 @@ impl Editor {
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
         self.mipmapper = None;
 
+        self.camera_bind_group = None;
+        self.default_skinning_buffer = None;
+        self.default_morph_deltas_buffer = None;
+        self.default_morph_weights_buffer = None;
+        self.default_morph_info_buffer = None;
+        self.default_animation_bind_group = None;
+
         self.texture_id = Some((*graphics.texture_id).clone());
         self.window = Some(graphics.window.clone());
         self.is_world_loaded.mark_rendering_loaded();
 
-        let sky_texture = HdrLoader::from_equirectangular_bytes(
-            &graphics.device,
-            &graphics.queue,
-            skybox_texture.map_or(DEFAULT_SKY_TEXTURE, |v| v.as_slice()),
-            1080,
-            Some("sky texture"),
-        );
+        let mut pending_sky_pipeline = None;
+
+        let active_camera = self.active_camera.lock().clone();
+        if let Some(camera_entity) = active_camera {
+            if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() {
+                self.camera_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                    label: Some("editor camera bind group"),
+                    layout: &graphics.layouts.camera_bind_group_layout,
+                    entries: &[wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: camera.buffer().as_entire_binding(),
+                    }],
+                }));
+
+                let max_skinning_matrices = 256usize;
+                let identity = vec![Mat4::IDENTITY; max_skinning_matrices];
+                let skinning_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                    label: Some("editor default skinning buffer"),
+                    contents: bytemuck::cast_slice(&identity),
+                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                });
 
-        match sky_texture {
-            Ok(sky_texture) => {
-                self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture));
-            }
-            Err(e) => {
-                error!("Failed to load sky texture: {}", e);
+                let morph_deltas_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                    label: Some("editor default morph deltas buffer"),
+                    contents: bytemuck::cast_slice(&[0.0f32]),
+                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                });
+
+                let morph_weights = vec![0.0f32; MAX_MORPH_WEIGHTS];
+                let morph_weights_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                    label: Some("editor default morph weights buffer"),
+                    contents: bytemuck::cast_slice(&morph_weights),
+                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                });
+
+                let morph_info_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                    label: Some("editor default morph info buffer"),
+                    contents: bytemuck::cast_slice(&[0u32; 4]),
+                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+                });
+
+                self.default_skinning_buffer = Some(skinning_buffer);
+                self.default_morph_deltas_buffer = Some(morph_deltas_buffer);
+                self.default_morph_weights_buffer = Some(morph_weights_buffer);
+                self.default_morph_info_buffer = Some(morph_info_buffer);
+
+                let skinning_buffer = self
+                    .default_skinning_buffer
+                    .as_ref()
+                    .expect("Default skinning buffer missing");
+                let morph_deltas_buffer = self
+                    .default_morph_deltas_buffer
+                    .as_ref()
+                    .expect("Default morph deltas buffer missing");
+                let morph_weights_buffer = self
+                    .default_morph_weights_buffer
+                    .as_ref()
+                    .expect("Default morph weights buffer missing");
+                let morph_info_buffer = self
+                    .default_morph_info_buffer
+                    .as_ref()
+                    .expect("Default morph info buffer missing");
+
+                self.default_animation_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                    label: Some("editor default animation bind group"),
+                    layout: &graphics.layouts.animation_layout,
+                    entries: &[
+                        wgpu::BindGroupEntry {
+                            binding: 0,
+                            resource: skinning_buffer.as_entire_binding(),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 1,
+                            resource: morph_deltas_buffer.as_entire_binding(),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 2,
+                            resource: morph_weights_buffer.as_entire_binding(),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 3,
+                            resource: morph_info_buffer.as_entire_binding(),
+                        },
+                    ],
+                }));
+
+                if let Some(main_pipeline) = self.main_render_pipeline.as_mut() {
+                    if let (Some(globals), Some(light_pipeline)) = (
+                        self.shader_globals.as_ref(),
+                        self.light_cube_pipeline.as_ref(),
+                    ) {
+                        let _ = main_pipeline.per_frame_bind_group(
+                            graphics.clone(),
+                            globals.buffer.buffer(),
+                            camera.buffer(),
+                            light_pipeline.light_buffer(),
+                        );
+                    }
+                }
+
+                let sky_texture_result = HdrLoader::from_equirectangular_bytes(
+                    &graphics.device,
+                    &graphics.queue,
+                    skybox_texture.map_or(DEFAULT_SKY_TEXTURE, |v| v.as_slice()),
+                    1080,
+                    Some("sky texture"),
+                );
+
+                match sky_texture_result {
+                    Ok(sky_texture) => {
+                        pending_sky_pipeline = Some(SkyPipeline::new(
+                            graphics.clone(),
+                            sky_texture,
+                            camera.buffer(),
+                        ));
+                    }
+                    Err(e) => {
+                        error!("Failed to load sky texture: {}", e);
+                    }
+                }
+            } else {
+                error!("Unable to create editor bind groups without an active camera component");
             }
+        } else {
+            error!("Unable to create editor bind groups without an active camera");
+        }
+
+        if let Some(sky_pipeline) = pending_sky_pipeline {
+            self.sky_pipeline = Some(sky_pipeline);
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 2e7288f..930c3f5 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -26,7 +26,6 @@ use std::{
     fs,
     path::{Path, PathBuf},
 };
-use wgpu::util::DeviceExt;
 use winit::{event::WindowEvent, event_loop::ActiveEventLoop, keyboard::KeyCode};
 use winit::event::{MouseScrollDelta, TouchPhase};
 
@@ -354,6 +353,11 @@ impl Scene for Editor {
         };
         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;
+        };
+
         {
             puffin::profile_scope!("Clearing viewport");
             let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -410,10 +414,11 @@ impl Scene for Editor {
         }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::Buffer)> = Vec::new();
+        let mut animated_instances: Vec<(u64, InstanceRaw, Vec<Mat4>, Vec<f32>, u32, u32)> = Vec::new();
 
         {
             puffin::profile_scope!("Locating all renderers and animation components");
+            let registry = ASSET_REGISTRY.read();
             let mut query = self
                 .world
                 .query::<(&MeshRenderer, Option<&AnimationComponent>)>();
@@ -425,8 +430,51 @@ impl Scene for Editor {
                 }
 
                 let instance = renderer.instance.to_raw();
-                if let Some(buffer) = animation.and_then(|anim| anim.bone_buffer.clone()) {
-                    animated_instances.push((handle.id, instance, buffer));
+                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 {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                    }
                 } else {
                     static_batches.entry(handle.id).or_default().push(instance);
                 }
@@ -495,78 +543,54 @@ impl Scene for Editor {
                         continue;
                     };
 
-                    render_pass.draw_light_model(model, &camera.bind_group, &light.bind_group);
+                    render_pass.draw_light_model(model, camera_bind_group, light_pipeline.bind_group());
                 }
             }
         }
 
-        if self.default_skinning_bind_group.is_none() {
-            let identity = [Mat4::IDENTITY.to_cols_array_2d()];
-            let buffer = graphics
-                .device
-                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                    label: Some("default skinning buffer"),
-                    contents: bytemuck::cast_slice(&identity),
-                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
-                });
-
-            let bind_group = graphics
-                .device
-                .create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("default skinning bind group"),
-                    layout: &graphics.layouts.skinning_bind_group_layout,
-                    entries: &[wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: buffer.as_entire_binding(),
-                    }],
-                });
-
-            self.default_skinning_buffer = Some(buffer);
-            self.default_skinning_bind_group = Some(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");
-        let environment_bind_group = &sky.bind_group;
+        let sky = self
+            .sky_pipeline
+            .as_ref()
+            .expect("Sky pipeline must be initialized before rendering models");
+        let environment_bind_group = &sky.environment_bind_group;
 
-        let globals = self.shader_globals.as_ref().expect("Shader globals not initialised");
-        if let Some(scene_globals) = &mut self.scene_globals_bind_group {
-            scene_globals.update(&graphics, &globals.buffer, camera.buffer());
-        } else {
-            self.scene_globals_bind_group = Some(dropbear_engine::bind_groups::SceneGlobalsBindGroup::new(
-                &graphics,
-                &globals.buffer,
-                camera.buffer(),
-            ));
-        }
-        let scene_globals_bind_group = self.scene_globals_bind_group.as_ref().unwrap();
+        let per_frame_bind_group = pipeline
+            .per_frame
+            .as_ref()
+            .expect("Per-frame bind group not initialized");
         
-        if let Some(lcp) = &self.light_cube_pipeline {
+        if let Some(_) = &self.light_cube_pipeline {
             puffin::profile_scope!("model render pass");
             for (model, handle, instance_count) in prepared_models {
-                let light_skin_bind_group =
-                    graphics
-                        .device
-                        .create_bind_group(&wgpu::BindGroupDescriptor {
-                            label: Some("scene light+skin bind group"),
-                            layout: &graphics.layouts.scene_light_skin_bind_group_layout,
-                            entries: &[
-                                wgpu::BindGroupEntry {
-                                    binding: 0,
-                                    resource: lcp.light_buffer().as_entire_binding(),
-                                },
-                                wgpu::BindGroupEntry {
-                                    binding: 1,
-                                    resource: default_skinning_buffer.as_entire_binding(),
-                                },
-                            ],
-                        });
-
                 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("model render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -599,16 +623,16 @@ impl Scene for Editor {
                 render_pass.draw_model_instanced(
                     model,
                     0..instance_count,
-                    scene_globals_bind_group.as_ref(),
-                    &light_skin_bind_group,
+                    per_frame_bind_group,
+                    default_animation_bind_group,
                     environment_bind_group,
                 );
             }
         }
 
-        if let Some(lcp) = &self.light_cube_pipeline {
+        if let Some(_lcp) = &self.light_cube_pipeline {
             puffin::profile_scope!("animated model render pass");
-            for (handle, instance, skin_buffer) in animated_instances {
+            for (handle, instance, skinning_matrices, morph_weights, num_vertices, num_targets) in animated_instances {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
@@ -649,30 +673,42 @@ impl Scene for Editor {
 
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
-                if self.light_skin_bind_group.is_none() {
-                    self.light_skin_bind_group = Some(graphics.device.create_bind_group(
-                        &wgpu::BindGroupDescriptor {
-                            label: Some("scene light+skin bind group"),
-                            layout: &graphics.layouts.scene_light_skin_bind_group_layout,
-                            entries: &[
-                                wgpu::BindGroupEntry {
-                                    binding: 0,
-                                    resource: lcp.light_buffer().as_entire_binding(),
-                                },
-                                wgpu::BindGroupEntry {
-                                    binding: 1,
-                                    resource: skin_buffer.as_entire_binding(),
-                                },
-                            ],
-                        },
-                    ));
+
+                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,
-                    scene_globals_bind_group.as_ref(),
-                    &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above
+                    per_frame_bind_group,
+                    default_animation_bind_group,
                     environment_bind_group,
                 );
             }
@@ -704,8 +740,8 @@ impl Scene for Editor {
             });
 
             render_pass.set_pipeline(&sky.pipeline);
-            render_pass.set_bind_group(0, &camera.bind_group, &[]);
-            render_pass.set_bind_group(1, &sky.bind_group, &[]);
+            render_pass.set_bind_group(0, &sky.camera_bind_group, &[]);
+            render_pass.set_bind_group(1, &sky.environment_bind_group, &[]);
             render_pass.draw(0..3, 0..1);
         }
 
@@ -751,7 +787,7 @@ impl Scene for Editor {
                     });
 
                     render_pass.set_pipeline(&collider_pipeline.pipeline);
-                    render_pass.set_bind_group(0, &camera.bind_group, &[]);
+                    render_pass.set_bind_group(0, camera_bind_group, &[]);
 
                     let mut instances_by_shape: HashMap<
                         ColliderShapeKey,
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index fe8e017..d4eb8b9 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -9,13 +9,7 @@ use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_
 use std::fs;
 use std::path::PathBuf;
 use std::sync::Arc;
-use log::error;
 use winit::keyboard::KeyCode;
-use dropbear_engine::pipelines::GlobalsUniform;
-use dropbear_engine::pipelines::light_cube::LightCubePipeline;
-use dropbear_engine::pipelines::shader::MainRenderPipeline;
-use dropbear_engine::sky::{HdrLoader, SkyPipeline, DEFAULT_SKY_TEXTURE};
-use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline;
 
 pub trait SignalController {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>;
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 2edcf3c..5947599 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -2,6 +2,7 @@
 
 use crossbeam_channel::{Receiver, unbounded};
 use dropbear_engine::buffer::ResizableBuffer;
+use dropbear_engine::camera::Camera;
 use dropbear_engine::future::{FutureHandle, FutureQueue};
 use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext};
 use dropbear_engine::pipelines::DropbearShaderPipeline;
@@ -27,6 +28,7 @@ use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult};
 use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
 use eucalyptus_core::states::{SCENES, Script, WorldLoadingStatus};
 use futures::executor;
+use glam::Mat4;
 use hecs::{Entity, World};
 use kino_ui::KinoState;
 use kino_ui::rendering::KinoWGPURenderer;
@@ -36,12 +38,15 @@ use std::collections::HashMap;
 use std::path::PathBuf;
 use std::sync::Arc;
 use wgpu::SurfaceConfiguration;
+use wgpu::util::DeviceExt;
 use winit::window::Fullscreen;
 
 mod command;
 mod input;
 mod scene;
 
+const MAX_MORPH_WEIGHTS: usize = 4096;
+
 #[cfg(feature = "debug")]
 fn find_jvm_library_path() -> PathBuf {
     let proj = eucalyptus_core::states::PROJECT.read();
@@ -134,10 +139,12 @@ pub struct PlayMode {
     animated_instance_buffer: Option<ResizableBuffer<InstanceRaw>>,
     collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     sky_pipeline: Option<SkyPipeline>,
+    camera_bind_group: Option<wgpu::BindGroup>,
     default_skinning_buffer: Option<wgpu::Buffer>,
-    default_skinning_bind_group: Option<wgpu::BindGroup>,
-    light_skin_bind_group: Option<wgpu::BindGroup>,
-    scene_globals_bind_group: Option<dropbear_engine::bind_groups::SceneGlobalsBindGroup>,
+    default_morph_deltas_buffer: Option<wgpu::Buffer>,
+    default_morph_weights_buffer: Option<wgpu::Buffer>,
+    default_morph_info_buffer: Option<wgpu::Buffer>,
+    default_animation_bind_group: Option<wgpu::BindGroup>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -226,10 +233,12 @@ impl PlayMode {
             },
             kino: None,
             sky_pipeline: None,
+            camera_bind_group: None,
             default_skinning_buffer: None,
-            default_skinning_bind_group: None,
-            light_skin_bind_group: None,
-            scene_globals_bind_group: None,
+            default_morph_deltas_buffer: None,
+            default_morph_weights_buffer: None,
+            default_morph_info_buffer: None,
+            default_animation_bind_group: None,
         };
 
         log::debug!("Created new play mode instance");
@@ -244,6 +253,12 @@ impl PlayMode {
         self.collider_wireframe_pipeline = None;
         self.kino = None;
         self.sky_pipeline = None;
+        self.camera_bind_group = None;
+        self.default_skinning_buffer = None;
+        self.default_morph_deltas_buffer = None;
+        self.default_morph_weights_buffer = None;
+        self.default_morph_info_buffer = None;
+        self.default_animation_bind_group = None;
 
         self.load_wgpu_nerdy_stuff(graphics, sky_texture);
     }
@@ -257,6 +272,85 @@ impl PlayMode {
         ));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
 
+        let mut camera_bind_group = None;
+        let mut pending_sky_pipeline = None;
+
+        if self.default_skinning_buffer.is_none() {
+            let max_skinning_matrices = 256usize;
+            let identity = vec![Mat4::IDENTITY; max_skinning_matrices];
+            let skinning_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("runtime default skinning buffer"),
+                contents: bytemuck::cast_slice(&identity),
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            });
+
+            let morph_deltas_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("runtime default morph deltas buffer"),
+                contents: bytemuck::cast_slice(&[0.0f32]),
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            });
+
+            let morph_weights = vec![0.0f32; MAX_MORPH_WEIGHTS];
+            let morph_weights_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("runtime default morph weights buffer"),
+                contents: bytemuck::cast_slice(&morph_weights),
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            });
+
+            let morph_info_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("runtime default morph info buffer"),
+                contents: bytemuck::cast_slice(&[0u32; 4]),
+                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+            });
+
+            self.default_skinning_buffer = Some(skinning_buffer);
+            self.default_morph_deltas_buffer = Some(morph_deltas_buffer);
+            self.default_morph_weights_buffer = Some(morph_weights_buffer);
+            self.default_morph_info_buffer = Some(morph_info_buffer);
+        }
+
+        if self.default_animation_bind_group.is_none() {
+            let skinning_buffer = self
+                .default_skinning_buffer
+                .as_ref()
+                .expect("Default skinning buffer missing");
+            let morph_deltas_buffer = self
+                .default_morph_deltas_buffer
+                .as_ref()
+                .expect("Default morph deltas buffer missing");
+            let morph_weights_buffer = self
+                .default_morph_weights_buffer
+                .as_ref()
+                .expect("Default morph weights buffer missing");
+            let morph_info_buffer = self
+                .default_morph_info_buffer
+                .as_ref()
+                .expect("Default morph info buffer missing");
+
+            self.default_animation_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("runtime default animation bind group"),
+                layout: &graphics.layouts.animation_layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: skinning_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: morph_deltas_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 2,
+                        resource: morph_weights_buffer.as_entire_binding(),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 3,
+                        resource: morph_info_buffer.as_entire_binding(),
+                    },
+                ],
+            }));
+        }
+
         self.kino = Some(KinoState::new(
             KinoWGPURenderer::new(
                 &graphics.device,
@@ -270,7 +364,7 @@ impl PlayMode {
             KinoWinitWindowing::new(graphics.window.clone(), None),
         ));
 
-        let sky_texture = HdrLoader::from_equirectangular_bytes(
+        let sky_texture_result = HdrLoader::from_equirectangular_bytes(
             &graphics.device,
             &graphics.queue,
             sky_texture.map_or(DEFAULT_SKY_TEXTURE, |v| v.as_slice()),
@@ -278,13 +372,52 @@ impl PlayMode {
             Some("sky texture"),
         );
 
-        match sky_texture {
-            Ok(sky_texture) => {
-                self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture));
-            }
-            Err(e) => {
-                error!("Failed to load sky texture: {}", e);
+        if let Some(camera_entity) = self.active_camera {
+            if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() {
+                camera_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                    label: Some("runtime camera bind group"),
+                    layout: &graphics.layouts.camera_bind_group_layout,
+                    entries: &[wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: camera.buffer().as_entire_binding(),
+                    }],
+                }));
+
+                match sky_texture_result {
+                    Ok(sky_texture) => {
+                        pending_sky_pipeline = Some(SkyPipeline::new(
+                            graphics.clone(),
+                            sky_texture,
+                            camera.buffer(),
+                        ));
+                    }
+                    Err(e) => {
+                        error!("Failed to load sky texture: {}", e);
+                    }
+                }
+
+                if let (Some(main_pipeline), Some(globals), Some(light_pipeline)) = (
+                    self.main_pipeline.as_mut(),
+                    self.shader_globals.as_ref(),
+                    self.light_cube_pipeline.as_ref(),
+                ) {
+                    let _ = main_pipeline.per_frame_bind_group(
+                        graphics.clone(),
+                        globals.buffer.buffer(),
+                        camera.buffer(),
+                        light_pipeline.light_buffer(),
+                    );
+                }
+            } else {
+                error!("Unable to create bind groups without an active camera component");
             }
+        } else {
+            error!("Unable to create bind groups without an active camera");
+        }
+
+        self.camera_bind_group = camera_bind_group;
+        if let Some(sky_pipeline) = pending_sky_pipeline {
+            self.sky_pipeline = Some(sky_pipeline);
         }
     }
 
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 0bf1b36..18b4533 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -1,6 +1,7 @@
 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;
@@ -30,7 +31,6 @@ use hecs::Entity;
 // use kino_ui::widgets::rect::Rectangle;
 // use kino_ui::widgets::{Border, Fill};
 use std::collections::HashMap;
-use wgpu::util::DeviceExt;
 use winit::event::WindowEvent;
 use winit::event_loop::ActiveEventLoop;
 
@@ -656,6 +656,11 @@ impl Scene for PlayMode {
         };
         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;
+        };
+
         {
             let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                 label: Some("viewport clear pass"),
@@ -709,9 +714,10 @@ impl Scene for PlayMode {
         }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::Buffer)> = Vec::new();
+        let mut animated_instances: Vec<(u64, InstanceRaw, Vec<Mat4>, Vec<f32>, u32, u32)> = Vec::new();
 
         {
+            let registry = ASSET_REGISTRY.read();
             let mut query = self
                 .world
                 .query::<(&MeshRenderer, Option<&AnimationComponent>)>();
@@ -723,8 +729,51 @@ impl Scene for PlayMode {
                 }
 
                 let instance = renderer.instance.to_raw();
-                if let Some(buffer) = animation.and_then(|anim| anim.bone_buffer.clone()) {
-                    animated_instances.push((handle.id, instance, buffer));
+                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 {
+                        static_batches.entry(handle.id).or_default().push(instance);
+                    }
                 } else {
                     static_batches.entry(handle.id).or_default().push(instance);
                 }
@@ -792,118 +841,92 @@ impl Scene for PlayMode {
                         continue;
                     };
 
-                    render_pass.draw_light_model(model, &camera.bind_group, &light.bind_group);
+                    render_pass.draw_light_model(model, camera_bind_group, light_pipeline.bind_group());
                 }
             }
         }
 
-        if self.default_skinning_bind_group.is_none() {
-            let identity = [Mat4::IDENTITY.to_cols_array_2d()];
-            let buffer = graphics
-                .device
-                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                    label: Some("default skinning buffer"),
-                    contents: bytemuck::cast_slice(&identity),
-                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
-                });
-
-            let bind_group = graphics
-                .device
-                .create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("default skinning bind group"),
-                    layout: &graphics.layouts.skinning_bind_group_layout,
-                    entries: &[wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: buffer.as_entire_binding(),
-                    }],
-                });
-
-            self.default_skinning_buffer = Some(buffer);
-            self.default_skinning_bind_group = Some(bind_group);
-        }
-
         let default_skinning_buffer = self
             .default_skinning_buffer
             .as_ref()
-            .expect("Default skinning buffer not initialized");
+            .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]),
+        );
 
         // model rendering
-        let sky = self.sky_pipeline.as_ref().expect("Sky pipeline must be initialized before rendering models");
-        let environment_bind_group = &sky.bind_group;
-        
-        let globals = self.shader_globals.as_ref().expect("Shader globals not initialised");
-        if let Some(scene_globals) = &mut self.scene_globals_bind_group {
-            scene_globals.update(&graphics, &globals.buffer, camera.buffer());
-        } else {
-            self.scene_globals_bind_group = Some(dropbear_engine::bind_groups::SceneGlobalsBindGroup::new(
-                &graphics,
-                &globals.buffer,
-                camera.buffer(),
-            ));
-        }
-        let scene_globals_bind_group = self.scene_globals_bind_group.as_ref().unwrap();
-        
-        if let Some(lcp) = &self.light_cube_pipeline {
-            for (model, handle, instance_count) in prepared_models {
-                let light_skin_bind_group =
-                    graphics
-                        .device
-                        .create_bind_group(&wgpu::BindGroupDescriptor {
-                            label: Some("scene light+skin bind group"),
-                            layout: &graphics.layouts.scene_light_skin_bind_group_layout,
-                            entries: &[
-                                wgpu::BindGroupEntry {
-                                    binding: 0,
-                                    resource: lcp.light_buffer().as_entire_binding(),
-                                },
-                                wgpu::BindGroupEntry {
-                                    binding: 1,
-                                    resource: default_skinning_buffer.as_entire_binding(),
-                                },
-                            ],
-                        });
+        let sky = self
+            .sky_pipeline
+            .as_ref()
+            .expect("Sky pipeline must be initialized before rendering models");
+        let environment_bind_group = &sky.environment_bind_group;
 
-                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("model render pass"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: hdr.render_view(),
-                        depth_slice: None,
-                        resolve_target: hdr.resolve_target(),
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                        view: &graphics.depth_texture.view,
-                        depth_ops: Some(wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
-                        }),
-                        stencil_ops: None,
+        let per_frame_bind_group = pipeline
+            .per_frame
+            .as_ref()
+            .expect("Per-frame bind group not initialized");
+        
+        for (model, handle, instance_count) in prepared_models {
+            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("model render pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.render_view(),
+                    depth_slice: None,
+                    resolve_target: hdr.resolve_target(),
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &graphics.depth_texture.view,
+                    depth_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
                     }),
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
-                render_pass.set_pipeline(pipeline.pipeline());
-                if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
-                    render_pass
-                        .set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
-                } else {
-                    continue;
-                }
-                render_pass.draw_model_instanced(
-                    model,
-                    0..instance_count,
-                    scene_globals_bind_group.as_ref(),
-                    &light_skin_bind_group,
-                    environment_bind_group,
-                );
+                    stencil_ops: None,
+                }),
+                occlusion_query_set: None,
+                timestamp_writes: None,
+            });
+            render_pass.set_pipeline(pipeline.pipeline());
+            if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
+                render_pass
+                    .set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
+            } else {
+                continue;
             }
+            render_pass.draw_model_instanced(
+                model,
+                0..instance_count,
+                per_frame_bind_group,
+                default_animation_bind_group,
+                environment_bind_group,
+            );
         }
 
-        if let Some(lcp) = &self.light_cube_pipeline {
-            for (handle, instance, skin_buffer) in animated_instances {
+        if let Some(_lcp) = &self.light_cube_pipeline {
+            for (handle, instance, skinning_matrices, morph_weights, num_vertices, num_targets) in animated_instances {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
@@ -944,30 +967,42 @@ impl Scene for PlayMode {
 
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
-                if self.light_skin_bind_group.is_none() {
-                    self.light_skin_bind_group = Some(graphics.device.create_bind_group(
-                        &wgpu::BindGroupDescriptor {
-                            label: Some("scene light+skin bind group"),
-                            layout: &graphics.layouts.scene_light_skin_bind_group_layout,
-                            entries: &[
-                                wgpu::BindGroupEntry {
-                                    binding: 0,
-                                    resource: lcp.light_buffer().as_entire_binding(),
-                                },
-                                wgpu::BindGroupEntry {
-                                    binding: 1,
-                                    resource: skin_buffer.as_entire_binding(),
-                                },
-                            ],
-                        },
-                    ));
+
+                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,
-                    scene_globals_bind_group.as_ref(),
-                    &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above
+                    per_frame_bind_group,
+                    default_animation_bind_group,
                     environment_bind_group,
                 );
             }
@@ -998,8 +1033,8 @@ impl Scene for PlayMode {
             });
 
             render_pass.set_pipeline(&sky.pipeline);
-            render_pass.set_bind_group(0, &camera.bind_group, &[]);
-            render_pass.set_bind_group(1, &sky.bind_group, &[]);
+            render_pass.set_bind_group(0, &sky.camera_bind_group, &[]);
+            render_pass.set_bind_group(1, &sky.environment_bind_group, &[]);
             render_pass.draw(0..3, 0..1);
         }
 
@@ -1042,7 +1077,7 @@ impl Scene for PlayMode {
                     });
 
                     render_pass.set_pipeline(&collider_pipeline.pipeline);
-                    render_pass.set_bind_group(0, &camera.bind_group, &[]);
+                    render_pass.set_bind_group(0, camera_bind_group, &[]);
 
                     let mut instances_by_shape: HashMap<
                         ColliderShapeKey,
diff --git a/include/dropbear.h b/include/dropbear.h
index b6d4173..ebafb09 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -115,10 +115,17 @@ typedef struct NQuaternionArray {
     size_t capacity;
 } NQuaternionArray;
 
+typedef struct f64ArrayArray {
+    double* values;
+    size_t length;
+    size_t capacity;
+} f64ArrayArray;
+
 typedef enum NChannelValuesTag {
     NChannelValuesTag_Translations = 0,
     NChannelValuesTag_Rotations = 1,
     NChannelValuesTag_Scales = 2,
+    NChannelValuesTag_MorphWeights = 3,
 } NChannelValuesTag;
 
 typedef struct NChannelValuesTranslations {
@@ -133,10 +140,15 @@ typedef struct NChannelValuesScales {
     NVector3Array values;
 } NChannelValuesScales;
 
+typedef struct NChannelValuesMorphWeights {
+    f64ArrayArray values;
+} NChannelValuesMorphWeights;
+
 typedef union NChannelValuesData {
     NChannelValuesTranslations Translations;
     NChannelValuesRotations Rotations;
     NChannelValuesScales Scales;
+    NChannelValuesMorphWeights MorphWeights;
 } NChannelValuesData;
 
 typedef struct NChannelValuesFfi {
@@ -387,12 +399,6 @@ typedef struct NShapeCastHit {
     NShapeCastStatus status;
 } NShapeCastHit;
 
-typedef struct f64ArrayArray {
-    double* values;
-    size_t length;
-    size_t capacity;
-} f64ArrayArray;
-
 typedef struct NSkin {
     const char* name;
     i32Array joints;