kitgit

tirbofish/dropbear · commit

f4471a782884cbe562b5f7af4e9e7489b19299f5

refactor: main shader, bind groups and textures. jarvis, run github actions

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-02-18 13:22

view full diff

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index 70da035..6701251 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -86,10 +86,12 @@ impl AnimationComponent {
         self.available_animations = model.animations.iter().map(|v| v.name.clone()).collect::<Vec<_>>();
 
         let Some(anim_idx) = self.active_animation_index else {
+            self.reset_to_bind_pose(model);
             return;
         };
 
         if anim_idx >= model.animations.len() {
+            self.reset_to_bind_pose(model);
             return;
         }
 
@@ -108,6 +110,7 @@ impl AnimationComponent {
             self.speed = settings.speed;
             self.looping = settings.looping;
             self.is_playing = settings.is_playing;
+            self.reset_to_bind_pose(model);
             return;
         }
         let animation = &model.animations[anim_idx];
@@ -129,6 +132,11 @@ impl AnimationComponent {
         self.looping = settings.looping;
         self.is_playing = settings.is_playing;
 
+        if !settings.is_playing {
+            self.reset_to_bind_pose(model);
+            return;
+        }
+
         for channel in &animation.channels {
             let count = channel.times.len();
             if count == 0 { continue; }
@@ -275,6 +283,11 @@ impl AnimationComponent {
         self.update_matrices(model);
     }
 
+    fn reset_to_bind_pose(&mut self, model: &Model) {
+        self.local_pose.clear();
+        self.update_matrices(model);
+    }
+
     fn apply_single_keyframe(
         channel: &crate::model::AnimationChannel,
         index: usize,
diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index 1cffb43..19d0135 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -26,8 +26,6 @@ impl<T> PartialEq for Handle<T> {
         }
         self.id == other.id
     }
-
-
 }
 
 impl<T> Copy for Handle<T> {}
@@ -98,6 +96,7 @@ impl AssetRegistry {
             animations: vec![],
             nodes: vec![],
         });
+        
         result
     }
 
@@ -187,13 +186,11 @@ impl AssetRegistry {
     }
 
     pub fn grey_texture(&mut self, graphics: Arc<SharedGraphicsContext>) -> Handle<Texture> {
-        let grey_handle = Handle::new(Self::hash_contents("Solid texture [128, 128, 128, 255]"));
-        
-        if self.contains_hash(grey_handle.id) {
-            return grey_handle;
-        }
-
-        self.solid_texture_rgba8(graphics, [128, 128, 128, 255])
+        self.solid_texture_rgba8_with_format(
+            graphics,
+            [128, 128, 128, 255],
+            Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+        )
     }
 
     pub fn solid_texture_rgba8(
@@ -201,21 +198,43 @@ impl AssetRegistry {
         graphics: Arc<SharedGraphicsContext>,
         rgba: [u8; 4],
     ) -> Handle<Texture> {
-        let handle = Handle::new(Self::hash_bytes(&rgba));
+        self.solid_texture_rgba8_with_format(
+            graphics,
+            rgba,
+            Texture::TEXTURE_FORMAT,
+        )
+    }
+
+    pub fn solid_texture_rgba8_with_format(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        rgba: [u8; 4],
+        format: wgpu::TextureFormat,
+    ) -> Handle<Texture> {
+        let format_tag = format!("{:?}", format);
+        let handle = Handle::new(Self::hash_contents((rgba, format_tag.as_str())));
 
         if self.contains_hash(handle.id) {
             return handle;
         }
 
-        let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]);
+        let label = format!(
+            "Solid texture [{}, {}, {}, {}] {}",
+            rgba[0],
+            rgba[1],
+            rgba[2],
+            rgba[3],
+            format_tag,
+        );
 
-        let texture = Texture::from_bytes_verbose_mipmapped(
+        let texture = Texture::from_bytes_verbose_mipmapped_with_format(
             graphics,
             &rgba,
             Some((1, 1)),
             None,
             None,
-            Some(label.as_str())
+            format,
+            Some(label.as_str()),
         );
         
         self.add_texture_with_label(label, texture)
diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index 2adaa22..f6a0d2a 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -194,6 +194,10 @@ impl Camera {
         yaw * pitch
     }
 
+    pub fn buffer(&self) -> &wgpu::Buffer {
+        self.buffer.buffer()
+    }
+
     pub fn forward(&self) -> DVec3 {
         (self.target - self.eye).normalize()
     }
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index af63ccc..0f2338c 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -75,11 +75,13 @@ use crate::pipelines::hdr::HdrPipeline;
 use crate::scene::Scene;
 
 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,
@@ -273,14 +275,54 @@ Hardware:
                 label: Some("Per-Light Layout"),
             });
         
-        // shaders/shader.wgsl - @group(2)
+        // 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: &[
-                // t_diffuse
+                // 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,
@@ -290,14 +332,14 @@ Hardware:
                 },
                 // s_diffuse
                 wgpu::BindGroupLayoutEntry {
-                    binding: 1,
+                    binding: 2,
                     visibility: wgpu::ShaderStages::FRAGMENT,
                     ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                     count: None,
                 },
                 // t_normal
                 wgpu::BindGroupLayoutEntry {
-                    binding: 2,
+                    binding: 3,
                     visibility: wgpu::ShaderStages::FRAGMENT,
                     ty: wgpu::BindingType::Texture {
                         multisampled: false,
@@ -308,26 +350,69 @@ Hardware:
                 },
                 // s_normal
                 wgpu::BindGroupLayoutEntry {
-                    binding: 3,
+                    binding: 4,
                     visibility: wgpu::ShaderStages::FRAGMENT,
                     ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                     count: None,
                 },
-                // u_material
+                // t_emissive
                 wgpu::BindGroupLayoutEntry {
-                    binding: 4,
+                    binding: 5,
                     visibility: wgpu::ShaderStages::FRAGMENT,
-                    ty: wgpu::BindingType::Buffer {
-                        ty: wgpu::BufferBindingType::Uniform,
-                        has_dynamic_offset: false,
-                        min_binding_size: None,
+                    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/shader.wgsl - @group(1)
+
+        // shaders/light.wgsl - @group(1)
         let light_array_bind_group_layout = device.create_bind_group_layout(
             &wgpu::BindGroupLayoutDescriptor {
                 entries: &[
@@ -347,7 +432,36 @@ Hardware:
             }
         );
 
-        // shaders/shader.wgsl - @group(3)
+        // 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: &[
@@ -442,11 +556,13 @@ Hardware:
             physics_accumulator: Duration::ZERO,
             scene_manager: scene::Manager::new(),
             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,
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 0b9bd26..d14ac57 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -61,6 +61,7 @@ pub struct Material {
     pub tint_buffer: UniformBuffer<MaterialUniform>,
     pub texture_tag: Option<String>,
     pub wrap_mode: TextureWrapMode,
+    pub has_normal_texture: bool,
 }
 
 #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)]
@@ -158,6 +159,13 @@ impl Material {
         name: impl Into<String>,
         diffuse_texture: Texture,
         normal_texture: Texture,
+        emissive_texture: Option<Texture>,
+        metallic_roughness_texture: Option<Texture>,
+        occlusion_texture: Option<Texture>,
+        emissive_texture_bound: Texture,
+        metallic_roughness_texture_bound: Texture,
+        occlusion_texture_bound: Texture,
+        has_normal_texture: bool,
         tint: [f32; 4],
         texture_tag: Option<String>,
     ) -> Self {
@@ -175,13 +183,26 @@ impl Material {
             occlusion_strength: 1.0,
             alpha_cutoff: 0.5,
             uv_tiling,
-            _pad: 0.0,
+            has_normal_texture: has_normal_texture as u32,
+            has_emissive_texture: emissive_texture.is_some() as u32,
+            has_metallic_texture: metallic_roughness_texture.is_some() as u32,
+            has_occlusion_texture: occlusion_texture.is_some() as u32,
+            pad: 0,
         };
 
         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, &tint_buffer, &name);
+        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,
+        );
 
         Self {
             name,
@@ -201,9 +222,10 @@ impl Material {
             tint_buffer,
             texture_tag,
             wrap_mode: TextureWrapMode::Repeat,
-            emissive_texture: None,
-            metallic_roughness_texture: None,
-            occlusion_texture: None,
+            emissive_texture,
+            metallic_roughness_texture,
+            occlusion_texture,
+            has_normal_texture,
         }
     }
 
@@ -211,6 +233,9 @@ impl Material {
         graphics: &SharedGraphicsContext,
         diffuse: &Texture,
         normal: &Texture,
+        emissive: &Texture,
+        metallic_roughness: &Texture,
+        occlusion: &Texture,
         uniform_buffer: &UniformBuffer<MaterialUniform>,
         name: &str,
     ) -> BindGroup {
@@ -221,23 +246,47 @@ impl Material {
             entries: &[
                 wgpu::BindGroupEntry {
                     binding: 0,
-                    resource: wgpu::BindingResource::TextureView(&diffuse.view),
+                    resource: uniform_buffer.buffer().as_entire_binding(),
                 },
                 wgpu::BindGroupEntry {
                     binding: 1,
-                    resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
+                    resource: wgpu::BindingResource::TextureView(&diffuse.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 2,
-                    resource: wgpu::BindingResource::TextureView(&normal.view),
+                    resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
                 },
                 wgpu::BindGroupEntry {
                     binding: 3,
-                    resource: wgpu::BindingResource::Sampler(&normal.sampler),
+                    resource: wgpu::BindingResource::TextureView(&normal.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 4,
-                    resource: uniform_buffer.buffer().as_entire_binding(),
+                    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),
                 },
             ],
         })
@@ -254,7 +303,11 @@ impl Material {
             occlusion_strength: self.occlusion_strength,
             alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5),
             uv_tiling: self.uv_tiling,
-            _pad: 0.0,
+            has_normal_texture: self.has_normal_texture as u32,
+            has_emissive_texture: self.emissive_texture.is_some() as u32,
+            has_metallic_texture: self.metallic_roughness_texture.is_some() as u32,
+            has_occlusion_texture: self.occlusion_texture.is_some() as u32,
+            pad: 0,
         };
 
         self.tint_buffer.write(&graphics.queue, &uniform);
@@ -932,9 +985,21 @@ impl Model {
 
         let mut materials = Vec::new();
 
-        let grey_texture = registry.grey_texture(graphics.clone());
-        let flat_normal_texture =
-            registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
+        let white_srgb_texture = registry.solid_texture_rgba8_with_format(
+            graphics.clone(),
+            [255, 255, 255, 255],
+            Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+        );
+        let white_linear_texture = registry.solid_texture_rgba8_with_format(
+            graphics.clone(),
+            [255, 255, 255, 255],
+            Texture::TEXTURE_FORMAT_BASE,
+        );
+        let flat_normal_texture = registry.solid_texture_rgba8_with_format(
+            graphics.clone(),
+            [128, 128, 255, 255],
+            Texture::TEXTURE_FORMAT_BASE,
+        );
 
         for processed in processed_textures {
             puffin::profile_scope!("creating material");
@@ -952,28 +1017,31 @@ impl Model {
             let occlusion_sampler = processed.occlusion_sampler;
 
             let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse {
-                Texture::from_bytes_verbose_mipmapped(
+                Texture::from_bytes_verbose_mipmapped_with_format(
                     graphics.clone(),
                     &rgba_data,
                     Some(dimensions),
                     None,
                     diffuse_sampler.clone(),
-                    Some(material_name.as_str())
+                    Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+                    Some(material_name.as_str()),
                 )
-            } else if let Some(grey) = registry.get_texture(grey_texture) {
-                (*grey).clone()
+            } else if let Some(white) = registry.get_texture(white_srgb_texture) {
+                (*white).clone()
             } else {
                 anyhow::bail!("Unable to find processed diffuse or fetch fallback texture for model {:?}", label);
             };
 
+            let has_normal_texture = processed_normal.is_some();
             let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal {
-                Texture::from_bytes_verbose_mipmapped(
+                Texture::from_bytes_verbose_mipmapped_with_format(
                     graphics.clone(),
                     &rgba_data,
                     Some(dimensions),
                     None,
                     normal_sampler.clone(),
-                    Some(material_name.as_str())
+                    Texture::TEXTURE_FORMAT_BASE,
+                    Some(material_name.as_str()),
                 )
             } else if let Some(tex) = registry.get_texture(flat_normal_texture) {
                 (*tex).clone()
@@ -982,36 +1050,51 @@ impl Model {
             };
 
             let emissive_texture = processed_emissive.map(|(rgba_data, dimensions)| {
-                Texture::from_bytes_verbose_mipmapped(
+                Texture::from_bytes_verbose_mipmapped_with_format(
                     graphics.clone(),
                     &rgba_data,
                     Some(dimensions),
                     None,
                     emissive_sampler.clone(),
-                    Some(material_name.as_str())
+                    Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+                    Some(material_name.as_str()),
+                )
+            });
+            let metallic_roughness_texture = processed_metallic_roughness.map(|(rgba_data, dimensions)| {
+                Texture::from_bytes_verbose_mipmapped_with_format(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    metallic_roughness_sampler.clone(),
+                    Texture::TEXTURE_FORMAT_BASE,
+                    Some(material_name.as_str()),
                 )
             });
-            let metallic_roughness_texture =
-                processed_metallic_roughness.map(|(rgba_data, dimensions)| {
-                    Texture::from_bytes_verbose_mipmapped(
-                        graphics.clone(),
-                        &rgba_data,
-                        Some(dimensions),
-                        None,
-                        metallic_roughness_sampler.clone(),
-                        Some(material_name.as_str())
-                    )
-                });
             let occlusion_texture = processed_occlusion.map(|(rgba_data, dimensions)| {
-                Texture::from_bytes_verbose_mipmapped(
+                Texture::from_bytes_verbose_mipmapped_with_format(
                     graphics.clone(),
                     &rgba_data,
                     Some(dimensions),
                     None,
                     occlusion_sampler.clone(),
-                    Some(material_name.as_str())
+                    Texture::TEXTURE_FORMAT_BASE,
+                    Some(material_name.as_str()),
                 )
             });
+
+            let emissive_texture_bound = emissive_texture
+                .clone()
+                .or_else(|| registry.get_texture(white_srgb_texture).cloned())
+                .ok_or_else(|| anyhow::anyhow!("Unable to resolve emissive fallback texture for model {:?}", label))?;
+            let metallic_roughness_texture_bound = metallic_roughness_texture
+                .clone()
+                .or_else(|| registry.get_texture(white_linear_texture).cloned())
+                .ok_or_else(|| anyhow::anyhow!("Unable to resolve metallic fallback texture for model {:?}", label))?;
+            let occlusion_texture_bound = occlusion_texture
+                .clone()
+                .or_else(|| registry.get_texture(white_linear_texture).cloned())
+                .ok_or_else(|| anyhow::anyhow!("Unable to resolve occlusion fallback texture for model {:?}", label))?;
             let texture_tag = Some(material_name.clone());
 
             let mut material = Material::new(
@@ -1019,6 +1102,13 @@ impl Model {
                 material_name,
                 diffuse_texture,
                 normal_texture,
+                emissive_texture.clone(),
+                metallic_roughness_texture.clone(),
+                occlusion_texture.clone(),
+                emissive_texture_bound,
+                metallic_roughness_texture_bound,
+                occlusion_texture_bound,
+                has_normal_texture,
                 processed.tint,
                 texture_tag,
             );
@@ -1144,35 +1234,31 @@ pub trait DrawModel<'a> {
         &mut self,
         mesh: &'a Mesh,
         material: &'a Material,
-        camera_bind_group: &'a wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'a wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     );
     fn draw_mesh_instanced(
         &mut self,
         mesh: &'a Mesh,
         material: &'a Material,
         instances: Range<u32>,
-        camera_bind_group: &'a wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'a wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     );
 
     #[allow(unused)]
     fn draw_model(
         &mut self,
         model: &'a Model,
-        camera_bind_group: &'a wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'a wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     );
     fn draw_model_instanced(
         &mut self,
         model: &'a Model,
         instances: Range<u32>,
-        camera_bind_group: &'a wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'a wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     );
 }
 
@@ -1184,11 +1270,10 @@ where
         &mut self,
         mesh: &'b Mesh,
         material: &'b Material,
-        camera_bind_group: &'b wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'b wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     ) {
-        self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group, skin_bind_group);
+        self.draw_mesh_instanced(mesh, material, 0..1, globals_camera_bind_group, light_skin_bind_group);
     }
 
     fn draw_mesh_instanced(
@@ -1196,19 +1281,14 @@ where
         mesh: &'b Mesh,
         material: &'b Material,
         instances: Range<u32>,
-        camera_bind_group: &'b wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'b wgpu::BindGroup,
+        light_skin_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, &material.bind_group, &[]);
-        self.set_bind_group(1, camera_bind_group, &[]);
-        self.set_bind_group(2, light_bind_group, &[]);
-        
-        if let Some(skin_bg) = skin_bind_group {
-            self.set_bind_group(4, skin_bg, &[]);
-        }
+        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.draw_indexed(0..mesh.num_elements, 0, instances);
     }
@@ -1216,20 +1296,18 @@ where
     fn draw_model(
         &mut self,
         model: &'b Model,
-        camera_bind_group: &'b wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'b wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     ) {
-        self.draw_model_instanced(model, 0..1, camera_bind_group, light_bind_group, skin_bind_group);
+        self.draw_model_instanced(model, 0..1, globals_camera_bind_group, light_skin_bind_group);
     }
 
     fn draw_model_instanced(
         &mut self,
         model: &'b Model,
         instances: Range<u32>,
-        camera_bind_group: &'b wgpu::BindGroup,
-        light_bind_group: &'a wgpu::BindGroup,
-        skin_bind_group: Option<&'a wgpu::BindGroup>,
+        globals_camera_bind_group: &'b wgpu::BindGroup,
+        light_skin_bind_group: &'a wgpu::BindGroup,
     ) {
         for mesh in &model.meshes {
             let material = &model.materials[mesh.material];
@@ -1237,9 +1315,8 @@ where
                 mesh,
                 material,
                 instances.clone(),
-                camera_bind_group,
-                light_bind_group,
-                skin_bind_group,
+                globals_camera_bind_group,
+                light_skin_bind_group,
             );
         }
     }
@@ -1462,5 +1539,9 @@ pub struct MaterialUniform {
     pub occlusion_strength: f32,
     pub alpha_cutoff: f32,
     pub uv_tiling: [f32; 2],
-    pub _pad: f32,
+    pub has_normal_texture: u32,
+    pub has_emissive_texture: u32,
+    pub has_metallic_texture: u32,
+    pub has_occlusion_texture: u32,
+    pub pad: u32,
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index c65ea00..52fbc67 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -130,6 +130,13 @@ impl LightCubePipeline {
         &self.light_bind_group
     }
 
+    pub fn light_buffer(&self) -> &wgpu::Buffer {
+        self.storage_buffer
+            .as_ref()
+            .expect("Light cube storage buffer missing")
+            .buffer()
+    }
+
     pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) {
         let mut light_array = LightArrayUniform::default();
 
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
index b397c16..fd58b3c 100644
--- a/crates/dropbear-engine/src/pipelines/shader.rs
+++ b/crates/dropbear-engine/src/pipelines/shader.rs
@@ -23,11 +23,9 @@ impl DropbearShaderPipeline for MainRenderPipeline {
         );
 
         let bind_group_layouts = vec![
-            &graphics.layouts.material_bind_layout, // @group(0)
-            &graphics.layouts.camera_bind_group_layout, // @group(1)
-            &graphics.layouts.light_array_bind_group_layout, // @group(2)
-            &graphics.layouts.shader_globals_bind_group_layout, // @group(3)
-            &graphics.layouts.skinning_bind_group_layout, // @group(4)
+            &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)
         ];
 
         let pipeline_layout =
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index 66c748a..0bd075e 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -4,6 +4,7 @@
 use crate::asset::{AssetRegistry, Handle};
 use crate::graphics::SharedGraphicsContext;
 use crate::model::{Material, Mesh, Model};
+use crate::texture::Texture;
 use crate::utils::ResourceReference;
 use crate::model::ModelVertex;
 use std::hash::{DefaultHasher, Hasher};
@@ -85,22 +86,45 @@ impl ProcedurallyGeneratedObject {
         };
 
         let material = material.unwrap_or_else(|| {
-            let grey_handle = _rguard.grey_texture(graphics.clone());
-            let flat_normal_handle =
-                _rguard.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
-            let grey = _rguard
-                .get_texture(grey_handle)
-                .expect("Grey texture handle missing")
-                .clone();
+            let flat_normal_handle = _rguard.solid_texture_rgba8_with_format(
+                graphics.clone(),
+                [128, 128, 255, 255],
+                Texture::TEXTURE_FORMAT_BASE,
+            );
+            let white_srgb_handle = _rguard.solid_texture_rgba8_with_format(
+                graphics.clone(),
+                [255, 255, 255, 255],
+                Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+            );
+            let white_linear_handle = _rguard.solid_texture_rgba8_with_format(
+                graphics.clone(),
+                [255, 255, 255, 255],
+                Texture::TEXTURE_FORMAT_BASE,
+            );
             let flat_normal = _rguard
                 .get_texture(flat_normal_handle)
                 .expect("Flat normal texture handle missing")
                 .clone();
+            let white_srgb = _rguard
+                .get_texture(white_srgb_handle)
+                .expect("White SRGB texture handle missing")
+                .clone();
+            let white_linear = _rguard
+                .get_texture(white_linear_handle)
+                .expect("White linear texture handle missing")
+                .clone();
             Material::new(
                 graphics.clone(),
                 "procedural_material",
-                grey,
+                white_srgb.clone(),
                 flat_normal,
+                None,
+                None,
+                None,
+                white_srgb,
+                white_linear.clone(),
+                white_linear,
+                false,
                 [1.0, 1.0, 1.0, 1.0],
                 Some("procedural_material".to_string()),
             )
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index 90b5601..0da63ad 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -12,6 +12,7 @@ struct CameraUniform {
     inv_proj: mat4x4<f32>,
     inv_view: mat4x4<f32>,
 }
+
 struct Light {
     position: vec4<f32>,
     direction: vec4<f32>, // x, y, z, outer_cutoff_angle
@@ -32,29 +33,44 @@ struct MaterialUniform {
     occlusion_strength: f32,
     alpha_cutoff: f32,
     uv_tiling: vec2<f32>,
+
+    has_normal_texture: u32, // 0,1 bool
+    has_emissive_texture: u32, // 0,1 bool
+    has_metallic_texture: u32, // 0,1 bool
+    has_occlusion_texture: u32, // 0,1 bool
 }
 
 @group(0) @binding(0)
-var t_diffuse: texture_2d<f32>;
+var<uniform> u_globals: Globals;
 @group(0) @binding(1)
+var<uniform> u_camera: CameraUniform;
+
+@group(1) @binding(0)
+var<uniform> u_material: MaterialUniform;
+@group(1) @binding(1)
+var t_diffuse: texture_2d<f32>;
+@group(1) @binding(2)
 var s_diffuse: sampler;
-@group(0) @binding(2)
+@group(1) @binding(3)
 var t_normal: texture_2d<f32>;
-@group(0) @binding(3)
+@group(1) @binding(4)
 var s_normal: sampler;
-@group(0) @binding(4)
-var<uniform> u_material: MaterialUniform;
-
-@group(1) @binding(0)
-var<uniform> u_camera: CameraUniform;
+@group(1) @binding(5)
+var t_emissive: texture_2d<f32>;
+@group(1) @binding(6)
+var s_emissive: sampler;
+@group(1) @binding(7)
+var t_metallic: texture_2d<f32>;
+@group(1) @binding(8)
+var s_metallic: sampler;
+@group(1) @binding(9)
+var t_occlusion: texture_2d<f32>;
+@group(1) @binding(10)
+var s_occlusion: sampler;
 
 @group(2) @binding(0)
 var<storage, read> s_light_array: array<Light>;
-
-@group(3) @binding(0)
-var<uniform> u_globals: Globals;
-
-@group(4) @binding(0)
+@group(2) @binding(1)
 var<storage, read> s_skinning: array<mat4x4<f32>>;
 
 struct InstanceInput {
@@ -234,41 +250,68 @@ fn apply_normal_map(
 @fragment
 fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
     let uv = in.tex_coords * u_material.uv_tiling;
-    var tex_color = textureSample(t_diffuse, s_diffuse, uv);
-    var object_normal = textureSample(t_normal, s_normal, uv);
 
+    let tex_color = textureSample(t_diffuse, s_diffuse, uv);
     let base_colour = tex_color * u_material.base_colour;
 
     if (base_colour.a < u_material.alpha_cutoff) {
         discard;
     }
 
-    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
+    var world_normal: vec3<f32>;
+    if (u_material.has_normal_texture != 0u) {
+        let object_normal = textureSample(t_normal, s_normal, uv).xyz;
+        let scaled_normal = normalize((object_normal * 2.0 - 1.0) * vec3<f32>(u_material.normal_scale, u_material.normal_scale, 1.0));
+        world_normal = apply_normal_map(
+            in.world_normal,
+            in.world_tangent,
+            in.world_bitangent,
+            scaled_normal,
+        );
+    } else {
+        world_normal = normalize(in.world_normal);
+    }
 
-    let world_normal = apply_normal_map(
-        in.world_normal,
-        in.world_tangent,
-        in.world_bitangent,
-        object_normal.xyz,
-    );
+    var metallic = u_material.metallic;
+    var roughness = u_material.roughness;
+    if (u_material.has_metallic_texture != 0u) {
+        let mr = textureSample(t_metallic, s_metallic, uv);
+        metallic  *= mr.b;
+        roughness *= mr.g;
+    }
+
+    var occlusion = 1.0;
+    if (u_material.has_occlusion_texture != 0u) {
+        let occ = textureSample(t_occlusion, s_occlusion, uv).r;
+        occlusion = 1.0 + u_material.occlusion_strength * (occ - 1.0);
+    }
+
+    var emissive = u_material.emissive * u_material.emissive_strength;
+    if (u_material.has_emissive_texture != 0u) {
+        let emissive_tex = textureSample(t_emissive, s_emissive, uv).rgb;
+        emissive *= emissive_tex;
+    }
+
+    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
+    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.rgb * occlusion;
 
-    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz;
     var final_color = ambient;
 
-    for(var i = 0u; i < u_globals.num_lights; i += 1u) {
+    for (var i = 0u; i < u_globals.num_lights; i += 1u) {
         let light = s_light_array[i];
-
         let light_type = i32(light.color.w + 0.1);
-
         if (light_type == 0) {
-            final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position);
+            final_color += directional_light(light, world_normal, view_dir, base_colour.rgb, in.world_position);
         } else if (light_type == 1) {
-            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
+            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.rgb);
         } else if (light_type == 2) {
-            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
+            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.rgb);
         }
     }
 
-    return vec4<f32>(final_color, base_colour.a);
-}
+    final_color *= occlusion;
 
+    final_color += emissive;
+
+    return vec4<f32>(final_color, base_colour.a);
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 36d8f8a..166dac5 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -75,6 +75,7 @@ pub struct Texture {
 impl Texture {
     /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier
     pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
+    pub const TEXTURE_FORMAT_BASE: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
     pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
 
     pub fn create_2d_texture(
@@ -92,6 +93,7 @@ impl Texture {
             height,
             depth_or_array_layers: 1,
         };
+
         Self::create_texture(
             device,
             label,
@@ -270,7 +272,7 @@ impl Texture {
         label: Option<&str>,
     ) -> Self {
         puffin::profile_function!(label.unwrap_or(""));
-        let texture = Self::from_bytes_verbose(
+        let texture = Self::from_bytes_verbose_with_format(
             graphics.clone(),
             bytes,
             dimensions,
@@ -290,6 +292,37 @@ impl Texture {
         texture
     }
 
+    /// Loads the texture from bytes and generates mipmaps on the GPU using the specified format.
+    pub fn from_bytes_verbose_mipmapped_with_format(
+        graphics: Arc<SharedGraphicsContext>,
+        bytes: &[u8],
+        dimensions: Option<(u32, u32)>,
+        view_descriptor: Option<wgpu::TextureViewDescriptor>,
+        sampler: Option<wgpu::SamplerDescriptor>,
+        format: wgpu::TextureFormat,
+        label: Option<&str>,
+    ) -> Self {
+        puffin::profile_function!(label.unwrap_or(""));
+        let texture = Self::from_bytes_verbose_with_format(
+            graphics.clone(),
+            bytes,
+            dimensions,
+            Some(format),
+            view_descriptor,
+            sampler,
+            label,
+        );
+
+        if let Err(err) = graphics
+            .mipmapper
+            .compute_mipmaps(&graphics.device, &graphics.queue, &texture)
+        {
+            log_once::warn_once!("Failed to generate mipmaps: {}", err);
+        }
+
+        texture
+    }
+
     /// Loads the texture from bytes, with options for more arguments. 
     /// 
     /// Requires more arguments. For a simpler usage, you should use [Self::from_bytes]
@@ -302,6 +335,26 @@ impl Texture {
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
     ) -> Self {
+        Self::from_bytes_verbose_with_format(
+            graphics,
+            bytes,
+            dimensions,
+            None,
+            view_descriptor,
+            sampler,
+            label,
+        )
+    }
+
+    fn from_bytes_verbose_with_format(
+        graphics: Arc<SharedGraphicsContext>,
+        bytes: &[u8],
+        dimensions: Option<(u32, u32)>,
+        format_override: Option<wgpu::TextureFormat>,
+        view_descriptor: Option<wgpu::TextureViewDescriptor>,
+        sampler: Option<wgpu::SamplerDescriptor>,
+        label: Option<&str>,
+    ) -> Self {
         puffin::profile_function!(label.unwrap_or(""));
         if let Some(l) = label {
             log::debug!("Loading texture: {l}");
@@ -357,13 +410,14 @@ impl Texture {
         let mip_level_count = size.width.min(size.height).ilog2() + 1;
         log::debug!("Mip level count [{:?}]: {}", label, mip_level_count);
 
+        let texture_format = format_override.unwrap_or(Texture::TEXTURE_FORMAT);
         let texture = graphics.device.create_texture(&wgpu::TextureDescriptor {
             label: Some(format!("{:?} diffuse blit texture", label).as_str()),
             size,
             mip_level_count,
             sample_count: 1,
             dimension: wgpu::TextureDimension::D2,
-            format: Texture::TEXTURE_FORMAT,
+            format: texture_format,
             usage: wgpu::TextureUsages::TEXTURE_BINDING
                 | wgpu::TextureUsages::RENDER_ATTACHMENT
                 | wgpu::TextureUsages::COPY_DST
diff --git a/crates/eucalyptus-core/src/lighting.rs b/crates/eucalyptus-core/src/lighting.rs
index cf0ed8d..0b75a2c 100644
--- a/crates/eucalyptus-core/src/lighting.rs
+++ b/crates/eucalyptus-core/src/lighting.rs
@@ -78,7 +78,7 @@ impl InspectableComponent for Light {
             ui.label("Uniform");
 
             ui.label("Light Type");
-            ComboBox::from_id_salt("Light Type").show_ui(ui, |ui| {
+            ComboBox::from_id_salt("Light Type").selected_text(format!("{}", self.component.light_type)).show_ui(ui, |ui| {
                 ui.selectable_value(&mut self.component.light_type, LightType::Directional, "Directional");
                 ui.selectable_value(&mut self.component.light_type, LightType::Point, "Point");
                 ui.selectable_value(&mut self.component.light_type, LightType::Spot, "Spot");
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index d903a0c..38ddde8 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -1216,14 +1216,20 @@ impl<'a> EditorTabViewer<'a> {
         queue.push(async move {
             let path = reference.resolve()?;
             let buffer = fs::read(&path)?;
-            let handle = Model::load_from_memory_raw(
+            let handle = match Model::load_from_memory_raw(
                 graphics.clone(),
                 buffer,
                 Some(reference.clone()),
                 None,
                 ASSET_REGISTRY.clone(),
             )
-            .await?;
+            .await {
+                Ok(v) => v,
+                Err(e) => {
+                    warn!("Unable to load model {}: {}", reference, e);
+                    return Err(e);
+                },
+            };
 
             let mut registry = ASSET_REGISTRY.write();
             if let Some(model) = registry.get_model_mut(handle) {
@@ -1250,7 +1256,13 @@ impl<'a> EditorTabViewer<'a> {
         let queue = graphics.future_queue.clone();
         queue.push(async move {
             let path = reference.resolve()?;
-            let texture = Texture::from_file(graphics.clone(), &path, Some(&label)).await?;
+            let texture = match Texture::from_file(graphics.clone(), &path, Some(&label)).await{
+                Ok(v) => v,
+                Err(e) => {
+                    warn!("Unable to load texture {}: {}", reference, e);
+                    return Err(e);
+                },
+            };
             let mut registry = ASSET_REGISTRY.write();
             registry.add_texture_with_label(label.clone(), texture);
             Ok::<(), anyhow::Error>(())
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 7c5e60c..f0c91c7 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -98,6 +98,7 @@ pub struct Editor {
     pub sky_pipeline: Option<SkyPipeline>,
     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 active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -288,6 +289,7 @@ impl Editor {
             sky_pipeline: None,
             default_skinning_buffer: None,
             default_skinning_bind_group: None,
+            light_skin_bind_group: None,
         })
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 1f47f1e..06e808c 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -369,7 +369,7 @@ impl Scene for Editor {
             }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::BindGroup)> = Vec::new();
+        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::Buffer)> = Vec::new();
 
         {
             let mut query = self
@@ -383,8 +383,8 @@ impl Scene for Editor {
                 }
 
                 let instance = renderer.instance.to_raw();
-                if let Some(bind_group) = animation.and_then(|anim| anim.bind_group.clone()) {
-                    animated_instances.push((handle.id, instance, bind_group));
+                if let Some(buffer) = animation.and_then(|anim| anim.bone_buffer.clone()) {
+                    animated_instances.push((handle.id, instance, buffer));
                 } else {
                     static_batches.entry(handle.id).or_default().push(instance);
                 }
@@ -485,19 +485,50 @@ impl Scene for Editor {
             self.default_skinning_bind_group = Some(bind_group);
         }
 
-        let default_skinning_bind_group = self
-            .default_skinning_bind_group
+        let default_skinning_buffer = self
+            .default_skinning_buffer
             .as_ref()
-            .expect("Default skinning bind group not initialized");
+            .expect("Default skinning buffer not initialized");
 
         // model rendering
         if let Some(lcp) = &self.light_cube_pipeline {
             for (model, handle, instance_count) in prepared_models {
-                let globals_bind_group = &self
+                let globals = self
                     .shader_globals
                     .as_ref()
-                    .expect("Shader globals not initialised")
-                    .bind_group;
+                    .expect("Shader globals not initialised");
+                let globals_camera_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(),
+                            },
+                        ],
+                    },
+                );
+                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 {
@@ -531,26 +562,38 @@ impl Scene for Editor {
                 } else {
                     continue;
                 }
-                render_pass.set_bind_group(3, globals_bind_group, &[]);
-
                 render_pass.draw_model_instanced(
                     model,
                     0..instance_count,
-                    &camera.bind_group,
-                    lcp.bind_group(),
-                    Some(default_skinning_bind_group),
+                    &globals_camera_bind_group,
+                    &light_skin_bind_group,
                 );
             }
         }
 
         if let Some(lcp) = &self.light_cube_pipeline {
-            let globals_bind_group = &self
+            let globals = self
                 .shader_globals
                 .as_ref()
-                .expect("Shader globals not initialised")
-                .bind_group;
+                .expect("Shader globals not initialised");
+            let globals_camera_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(),
+                        },
+                    ],
+                },
+            );
 
-            for (handle, instance, skin_bind_group) in animated_instances {
+            for (handle, instance, skin_buffer) in animated_instances {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
@@ -594,14 +637,30 @@ impl Scene for Editor {
 
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
-                render_pass.set_bind_group(3, globals_bind_group, &[]);
+                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(),
+                                },
+                            ],
+                        },
+                    ));
+                }
 
                 render_pass.draw_model_instanced(
                     model,
                     0..1,
-                    &camera.bind_group,
-                    lcp.bind_group(),
-                    Some(&skin_bind_group),
+                    &globals_camera_bind_group,
+                    &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above
                 );
             }
         }
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index ad46d52..82d8638 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -128,6 +128,7 @@ pub struct PlayMode {
     sky_pipeline: Option<SkyPipeline>,
     default_skinning_buffer: Option<wgpu::Buffer>,
     default_skinning_bind_group: Option<wgpu::BindGroup>,
+    light_skin_bind_group: Option<wgpu::BindGroup>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -217,6 +218,7 @@ impl PlayMode {
             sky_pipeline: None,
             default_skinning_buffer: None,
             default_skinning_bind_group: None,
+            light_skin_bind_group: None,
         };
 
         log::debug!("Created new play mode instance");
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 9ae847b..0a6c903 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -641,7 +641,7 @@ impl Scene for PlayMode {
             }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::BindGroup)> = Vec::new();
+        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::Buffer)> = Vec::new();
 
         {
             let mut query = self
@@ -655,8 +655,8 @@ impl Scene for PlayMode {
                 }
 
                 let instance = renderer.instance.to_raw();
-                if let Some(bind_group) = animation.and_then(|anim| anim.bind_group.clone()) {
-                    animated_instances.push((handle.id, instance, bind_group));
+                if let Some(buffer) = animation.and_then(|anim| anim.bone_buffer.clone()) {
+                    animated_instances.push((handle.id, instance, buffer));
                 } else {
                     static_batches.entry(handle.id).or_default().push(instance);
                 }
@@ -758,19 +758,50 @@ impl Scene for PlayMode {
             self.default_skinning_bind_group = Some(bind_group);
         }
 
-        let default_skinning_bind_group = self
-            .default_skinning_bind_group
+        let default_skinning_buffer = self
+            .default_skinning_buffer
             .as_ref()
-            .expect("Default skinning bind group not initialized");
+            .expect("Default skinning buffer not initialized");
 
         // model rendering
         if let Some(lcp) = &self.light_cube_pipeline {
             for (model, handle, instance_count) in prepared_models {
-                let globals_bind_group = &self
+                let globals = self
                     .shader_globals
                     .as_ref()
-                    .expect("Shader globals not initialised")
-                    .bind_group;
+                    .expect("Shader globals not initialised");
+                let globals_camera_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(),
+                            },
+                        ],
+                    },
+                );
+                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 {
@@ -804,26 +835,38 @@ impl Scene for PlayMode {
                 } else {
                     continue;
                 }
-                render_pass.set_bind_group(3, globals_bind_group, &[]);
-
                 render_pass.draw_model_instanced(
                     model,
                     0..instance_count,
-                    &camera.bind_group,
-                    lcp.bind_group(),
-                    Some(default_skinning_bind_group),
+                    &globals_camera_bind_group,
+                    &light_skin_bind_group,
                 );
             }
         }
 
         if let Some(lcp) = &self.light_cube_pipeline {
-            let globals_bind_group = &self
+            let globals = self
                 .shader_globals
                 .as_ref()
-                .expect("Shader globals not initialised")
-                .bind_group;
+                .expect("Shader globals not initialised");
+            let globals_camera_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(),
+                        },
+                    ],
+                },
+            );
 
-            for (handle, instance, skin_bind_group) in animated_instances {
+            for (handle, instance, skin_buffer) in animated_instances {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
@@ -867,14 +910,30 @@ impl Scene for PlayMode {
 
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
-                render_pass.set_bind_group(3, globals_bind_group, &[]);
+                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(),
+                                },
+                            ],
+                        },
+                    ));
+                }
 
                 render_pass.draw_model_instanced(
                     model,
                     0..1,
-                    &camera.bind_group,
-                    lcp.bind_group(),
-                    Some(&skin_bind_group),
+                    &globals_camera_bind_group,
+                    &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above
                 );
             }
         }