kitgit

tirbofish/dropbear · commit

69c2f901cb7d5013f36c142f5c35a66e716a42e2

feature: implement reflections, improved model textures. for some reason, collider wireframe doesnt render and i deadass dont know why.

Unverified

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

view full diff

diff --git a/crates/dropbear-engine/src/bind_groups.rs b/crates/dropbear-engine/src/bind_groups.rs
new file mode 100644
index 0000000..762605a
--- /dev/null
+++ b/crates/dropbear-engine/src/bind_groups.rs
@@ -0,0 +1,124 @@
+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,
+    ) {
+        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
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 7b9d16e..7a62455 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -1,6 +1,7 @@
 pub mod animation;
 pub mod asset;
 pub mod attenuation;
+pub mod bind_groups;
 pub mod buffer;
 pub mod camera;
 pub mod colour;
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 85e804e..25acec1 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -5,7 +5,7 @@ use crate::{
     texture::{Texture, TextureWrapMode},
     utils::ResourceReference,
 };
-use gltf::image::Format;
+use gltf::image::{Format, Source};
 use gltf::texture::MinFilter;
 use parking_lot::RwLock;
 use puffin::profile_scope;
@@ -329,6 +329,7 @@ pub enum AnimationInterpolation {
 struct GLTFTextureInformation {
     sampler: wgpu::SamplerDescriptor<'static>,
     pixels: Vec<u8>,
+    mime_type: Option<String>,
     width: u32,
     height: u32,
     #[allow(dead_code)]
@@ -342,6 +343,11 @@ impl GLTFTextureInformation {
         puffin::profile_function!();
         let sampler = tex.sampler();
 
+        let mime = match tex.source().source() {
+            Source::View { mime_type, .. } => Some(mime_type.to_string()),
+            Source::Uri { mime_type, .. } => mime_type.map(|value| value.to_string()),
+        };
+
         let mag_filter = match sampler.mag_filter() {
             Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest,
             _ => wgpu::FilterMode::Linear,
@@ -408,7 +414,26 @@ impl GLTFTextureInformation {
                 (rgba, wgpu::TextureFormat::Rgba8Unorm)
             }
             Format::R8G8B8A8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rgba8Unorm),
-            _ => panic!("Unsupported format"),
+            Format::R16 => (image_data.pixels.clone(), wgpu::TextureFormat::R16Unorm),
+            Format::R16G16 => (image_data.pixels.clone(), wgpu::TextureFormat::Rg16Unorm),
+            Format::R16G16B16 => {
+                let mut rgba = Vec::with_capacity(image_data.pixels.len() / 6 * 8);
+                for chunk in image_data.pixels.chunks(6) {
+                    rgba.extend_from_slice(chunk);
+                    rgba.extend_from_slice(&[255u8, 255u8]);
+                }
+                (rgba, wgpu::TextureFormat::Rgba16Unorm)
+            }
+            Format::R16G16B16A16 => (image_data.pixels.clone(), wgpu::TextureFormat::Rgba16Unorm),
+            Format::R32G32B32FLOAT => {
+                let mut rgba = Vec::with_capacity(image_data.pixels.len() / 12 * 16);
+                for chunk in image_data.pixels.chunks(12) {
+                    rgba.extend_from_slice(chunk);
+                    rgba.extend_from_slice(&1.0f32.to_ne_bytes());
+                }
+                (rgba, wgpu::TextureFormat::Rgba32Float)
+            }
+            Format::R32G32B32A32FLOAT => (image_data.pixels.clone(), wgpu::TextureFormat::Rgba32Float),
         };
 
         GLTFTextureInformation {
@@ -418,6 +443,7 @@ impl GLTFTextureInformation {
             format,
             width,
             height,
+            mime_type: mime,
         }
     }
 }
@@ -458,16 +484,11 @@ struct GLTFMaterialInformation {
 
 struct ProcessedMaterialTextures {
     name: String,
-    diffuse: Option<(Vec<u8>, (u32, u32))>,
-    normal: Option<(Vec<u8>, (u32, u32))>,
-    emissive: Option<(Vec<u8>, (u32, u32))>,
-    metallic_roughness: Option<(Vec<u8>, (u32, u32))>,
-    occlusion: Option<(Vec<u8>, (u32, u32))>,
-    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    diffuse: Option<ProcessedTexture>,
+    normal: Option<ProcessedTexture>,
+    emissive: Option<ProcessedTexture>,
+    metallic_roughness: Option<ProcessedTexture>,
+    occlusion: Option<ProcessedTexture>,
     tint: [f32; 4],
     emissive_factor: [f32; 3],
     metallic_factor: f32,
@@ -479,6 +500,14 @@ struct ProcessedMaterialTextures {
     normal_scale: f32,
 }
 
+struct ProcessedTexture {
+    pixels: Vec<u8>,
+    dimensions: (u32, u32),
+    format: wgpu::TextureFormat,
+    sampler: wgpu::SamplerDescriptor<'static>,
+    mime_type: Option<String>,
+}
+
 impl Model {
     fn load_materials(
         gltf: &gltf::Document,
@@ -954,28 +983,23 @@ impl Model {
                 puffin::profile_scope!("processing material textures");
                 let material_name = material_info.name;
 
-                let extract = |info: Option<GLTFTextureInformation>| -> (
-                    Option<(Vec<u8>, (u32, u32))>,
-                    Option<wgpu::SamplerDescriptor<'static>>,
-                ) {
-                    if let Some(info) = info {
-                        (
-                            Some((info.pixels, (info.width, info.height))),
-                            Some(info.sampler),
-                        )
-                    } else {
-                        (None, None)
-                    }
-                };
-
-                let (processed_diffuse, diffuse_sampler) = extract(material_info.diffuse_texture);
-                let (processed_normal, normal_sampler) = extract(material_info.normal_texture);
-                let (processed_emissive, emissive_sampler) =
-                    extract(material_info.emissive_texture);
-                let (processed_metallic_roughness, metallic_roughness_sampler) =
+                let extract =
+                    |info: Option<GLTFTextureInformation>| -> Option<ProcessedTexture> {
+                        info.map(|info| ProcessedTexture {
+                            pixels: info.pixels,
+                            dimensions: (info.width, info.height),
+                            format: info.format,
+                            sampler: info.sampler,
+                            mime_type: info.mime_type,
+                        })
+                    };
+
+                let processed_diffuse = extract(material_info.diffuse_texture);
+                let processed_normal = extract(material_info.normal_texture);
+                let processed_emissive = extract(material_info.emissive_texture);
+                let processed_metallic_roughness =
                     extract(material_info.metallic_roughness_texture);
-                let (processed_occlusion, occlusion_sampler) =
-                    extract(material_info.occlusion_texture);
+                let processed_occlusion = extract(material_info.occlusion_texture);
 
                 let tint = material_info.tint;
                 let emissive_factor = material_info.emissive_factor;
@@ -994,11 +1018,6 @@ impl Model {
                     emissive: processed_emissive,
                     metallic_roughness: processed_metallic_roughness,
                     occlusion: processed_occlusion,
-                    diffuse_sampler,
-                    normal_sampler,
-                    emissive_sampler,
-                    metallic_roughness_sampler,
-                    occlusion_sampler,
                     tint,
                     emissive_factor,
                     metallic_factor,
@@ -1039,21 +1058,16 @@ impl Model {
             let processed_emissive = processed.emissive;
             let processed_metallic_roughness = processed.metallic_roughness;
             let processed_occlusion = processed.occlusion;
-            let diffuse_sampler = processed.diffuse_sampler;
-            let normal_sampler = processed.normal_sampler;
-            let emissive_sampler = processed.emissive_sampler;
-            let metallic_roughness_sampler = processed.metallic_roughness_sampler;
-            let occlusion_sampler = processed.occlusion_sampler;
-
-            let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse {
-                Texture::from_bytes_verbose_mipmapped_with_format(
+            let diffuse_texture = if let Some(diffuse) = processed_diffuse {
+                let format = diffuse.format.add_srgb_suffix();
+                Texture::from_raw_pixels_mipmapped_with_format(
                     graphics.clone(),
-                    &rgba_data,
-                    Some(dimensions),
-                    None,
-                    diffuse_sampler.clone(),
-                    Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+                    &diffuse.pixels,
+                    diffuse.dimensions,
+                    format,
+                    Some(diffuse.sampler),
                     Some(material_name.as_str()),
+                    diffuse.mime_type.as_deref(),
                 )
             } else if let Some(white) = registry.get_texture(white_srgb_texture) {
                 (*white).clone()
@@ -1065,15 +1079,15 @@ impl Model {
             };
 
             let has_normal_texture = processed_normal.is_some();
-            let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal {
-                Texture::from_bytes_verbose_mipmapped_with_format(
+            let normal_texture = if let Some(normal) = processed_normal {
+                Texture::from_raw_pixels_mipmapped_with_format(
                     graphics.clone(),
-                    &rgba_data,
-                    Some(dimensions),
-                    None,
-                    normal_sampler.clone(),
-                    Texture::TEXTURE_FORMAT_BASE,
+                    &normal.pixels,
+                    normal.dimensions,
+                    normal.format,
+                    Some(normal.sampler),
                     Some(material_name.as_str()),
+                    normal.mime_type.as_deref(),
                 )
             } else if let Some(tex) = registry.get_texture(flat_normal_texture) {
                 (*tex).clone()
@@ -1084,38 +1098,38 @@ impl Model {
                 );
             };
 
-            let emissive_texture = processed_emissive.map(|(rgba_data, dimensions)| {
-                Texture::from_bytes_verbose_mipmapped_with_format(
+            let emissive_texture = processed_emissive.map(|emissive| {
+                let format = emissive.format.add_srgb_suffix();
+                Texture::from_raw_pixels_mipmapped_with_format(
                     graphics.clone(),
-                    &rgba_data,
-                    Some(dimensions),
-                    None,
-                    emissive_sampler.clone(),
-                    Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+                    &emissive.pixels,
+                    emissive.dimensions,
+                    format,
+                    Some(emissive.sampler),
                     Some(material_name.as_str()),
+                    emissive.mime_type.as_deref(),
                 )
             });
-            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 occlusion_texture = processed_occlusion.map(|(rgba_data, dimensions)| {
-                Texture::from_bytes_verbose_mipmapped_with_format(
+            let metallic_roughness_texture = processed_metallic_roughness.map(|metallic| {
+                Texture::from_raw_pixels_mipmapped_with_format(
+                    graphics.clone(),
+                    &metallic.pixels,
+                    metallic.dimensions,
+                    metallic.format,
+                    Some(metallic.sampler),
+                    Some(material_name.as_str()),
+                    metallic.mime_type.as_deref(),
+                )
+            });
+            let occlusion_texture = processed_occlusion.map(|occlusion| {
+                Texture::from_raw_pixels_mipmapped_with_format(
                     graphics.clone(),
-                    &rgba_data,
-                    Some(dimensions),
-                    None,
-                    occlusion_sampler.clone(),
-                    Texture::TEXTURE_FORMAT_BASE,
+                    &occlusion.pixels,
+                    occlusion.dimensions,
+                    occlusion.format,
+                    Some(occlusion.sampler),
                     Some(material_name.as_str()),
+                    occlusion.mime_type.as_deref(),
                 )
             });
 
@@ -1287,6 +1301,7 @@ pub trait DrawModel<'a> {
         material: &'a Material,
         globals_camera_bind_group: &'a wgpu::BindGroup,
         light_skin_bind_group: &'a wgpu::BindGroup,
+        environment_bind_group: &'a wgpu::BindGroup,
     );
     fn draw_mesh_instanced(
         &mut self,
@@ -1295,6 +1310,7 @@ pub trait DrawModel<'a> {
         instances: Range<u32>,
         globals_camera_bind_group: &'a wgpu::BindGroup,
         light_skin_bind_group: &'a wgpu::BindGroup,
+        environment_bind_group: &'a wgpu::BindGroup,
     );
 
     #[allow(unused)]
@@ -1303,6 +1319,7 @@ pub trait DrawModel<'a> {
         model: &'a Model,
         globals_camera_bind_group: &'a wgpu::BindGroup,
         light_skin_bind_group: &'a wgpu::BindGroup,
+        environment_bind_group: &'a wgpu::BindGroup,
     );
     fn draw_model_instanced(
         &mut self,
@@ -1310,6 +1327,7 @@ pub trait DrawModel<'a> {
         instances: Range<u32>,
         globals_camera_bind_group: &'a wgpu::BindGroup,
         light_skin_bind_group: &'a wgpu::BindGroup,
+        environment_bind_group: &'a wgpu::BindGroup,
     );
 }
 
@@ -1323,6 +1341,7 @@ where
         material: &'b Material,
         globals_camera_bind_group: &'b wgpu::BindGroup,
         light_skin_bind_group: &'a wgpu::BindGroup,
+        environment_bind_group: &'a wgpu::BindGroup,
     ) {
         self.draw_mesh_instanced(
             mesh,
@@ -1330,6 +1349,7 @@ where
             0..1,
             globals_camera_bind_group,
             light_skin_bind_group,
+            environment_bind_group,
         );
     }
 
@@ -1340,12 +1360,14 @@ where
         instances: Range<u32>,
         globals_camera_bind_group: &'b wgpu::BindGroup,
         light_skin_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(3, environment_bind_group, &[]);
 
         self.draw_indexed(0..mesh.num_elements, 0, instances);
     }
@@ -1355,12 +1377,14 @@ where
         model: &'b Model,
         globals_camera_bind_group: &'b wgpu::BindGroup,
         light_skin_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,
+            environment_bind_group
         );
     }
 
@@ -1370,6 +1394,7 @@ where
         instances: Range<u32>,
         globals_camera_bind_group: &'b wgpu::BindGroup,
         light_skin_bind_group: &'a wgpu::BindGroup,
+        environment_bind_group: &'a wgpu::BindGroup,
     ) {
         for mesh in &model.meshes {
             let material = &model.materials[mesh.material];
@@ -1379,6 +1404,7 @@ where
                 instances.clone(),
                 globals_camera_bind_group,
                 light_skin_bind_group,
+                environment_bind_group,
             );
         }
     }
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
index 3e808dd..d552663 100644
--- a/crates/dropbear-engine/src/pipelines/globals.rs
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -3,10 +3,6 @@ use std::sync::Arc;
 use crate::buffer::UniformBuffer;
 use crate::graphics::SharedGraphicsContext;
 
-/// Mirrors `Globals` in `pipelines/shaders/shader.wgsl` (`@group(4) @binding(0)`).
-///
-/// Note: the `_padding` ensures the uniform is 16 bytes, which matches WGSL
-/// uniform layout expectations.
 #[repr(C)]
 #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct Globals {
@@ -25,7 +21,6 @@ impl Default for Globals {
     }
 }
 
-/// Owns the globals uniform buffer and its bind group.
 #[derive(Debug, Clone)]
 pub struct GlobalsUniform {
     pub data: Globals,
diff --git a/crates/dropbear-engine/src/pipelines/hdr.rs b/crates/dropbear-engine/src/pipelines/hdr.rs
index 831d257..085951b 100644
--- a/crates/dropbear-engine/src/pipelines/hdr.rs
+++ b/crates/dropbear-engine/src/pipelines/hdr.rs
@@ -165,4 +165,4 @@ impl HdrPipeline {
         pass.set_bind_group(0, &self.bind_group, &[]);
         pass.draw(0..3, 0..1);
     }
-}
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
index fdb6d43..6d61dd1 100644
--- a/crates/dropbear-engine/src/pipelines/shader.rs
+++ b/crates/dropbear-engine/src/pipelines/shader.rs
@@ -26,6 +26,7 @@ impl DropbearShaderPipeline for MainRenderPipeline {
             &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)
         ];
 
         let pipeline_layout =
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index 0da63ad..e9e2176 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -73,6 +73,11 @@ var<storage, read> s_light_array: array<Light>;
 @group(2) @binding(1)
 var<storage, read> s_skinning: array<mat4x4<f32>>;
 
+@group(3) @binding(0)
+var env_map: texture_cube<f32>;
+@group(3) @binding(1)
+var env_sampler: sampler;
+
 struct InstanceInput {
     @location(8) model_matrix_0: vec4<f32>,
     @location(9) model_matrix_1: vec4<f32>,
@@ -103,6 +108,7 @@ struct VertexOutput {
     @location(2) world_position: vec3<f32>,
     @location(3) world_tangent: vec3<f32>,
     @location(4) world_bitangent: vec3<f32>,
+    @location(5) world_view_position: vec3<f32>,
 };
 
 @vertex
@@ -156,6 +162,7 @@ fn vs_main(
     out.world_position = world_position.xyz;
     out.world_tangent = world_tangent;
     out.world_bitangent = world_bitangent;
+    out.world_view_position = u_camera.view_pos.xyz;
     return out;
 }
 
@@ -311,6 +318,11 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
 
     final_color *= occlusion;
 
+    let world_reflect = reflect(-view_dir, world_normal);
+    let reflection = textureSample(env_map, env_sampler, world_reflect).rgb;
+    let shininess = (1.0 - roughness) * metallic;
+    final_color += reflection * shininess;
+
     final_color += emissive;
 
     return vec4<f32>(final_color, base_colour.a);
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index c00172e..4ab0ec8 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -70,6 +70,7 @@ pub struct Texture {
     pub view: wgpu::TextureView,
     pub hash: Option<u64>,
     pub reference: Option<ResourceReference>,
+    pub mime_type: Option<String>,
 }
 
 impl Texture {
@@ -145,6 +146,7 @@ impl Texture {
             size,
             hash: None,
             reference: None,
+            mime_type: None,
         }
     }
 
@@ -195,6 +197,7 @@ impl Texture {
             label: label.to_potential_string(),
             hash: None,
             reference: None,
+            mime_type: None,
         }
     }
 
@@ -236,6 +239,7 @@ impl Texture {
             view,
             hash: None,
             reference: None,
+            mime_type: None,
         }
     }
 
@@ -509,9 +513,183 @@ impl Texture {
             view,
             hash: Some(hash),
             reference: Some(ResourceReference::from_bytes(bytes)),
+            mime_type: None,
         }
     }
 
+    fn bytes_per_pixel(format: wgpu::TextureFormat) -> Option<u32> {
+        format.block_copy_size(None)
+    }
+
+    pub fn from_raw_pixels_mipmapped_with_format(
+        graphics: Arc<SharedGraphicsContext>,
+        pixels: &[u8],
+        dimensions: (u32, u32),
+        format: wgpu::TextureFormat,
+        sampler: Option<wgpu::SamplerDescriptor>,
+        label: Option<&str>,
+        mime_type: Option<&str>,
+    ) -> Self {
+        puffin::profile_function!(label.unwrap_or(""));
+        if let Some(l) = label {
+            log::debug!("Loading texture: {l}");
+        }
+
+        let hash = AssetRegistry::hash_bytes(pixels);
+
+        let bytes_per_pixel = match Self::bytes_per_pixel(format) {
+            Some(value) => value,
+            None => {
+                log::error!(
+                    "Texture [{:?}] has unsupported format {:?}; falling back to 1x1 magenta.",
+                    label,
+                    format
+                );
+                return Self::from_bytes_verbose_mipmapped_with_format(
+                    graphics,
+                    &[255, 0, 255, 255],
+                    Some((1, 1)),
+                    None,
+                    sampler,
+                    Texture::TEXTURE_FORMAT,
+                    label,
+                );
+            }
+        };
+
+        let expected_len = (dimensions.0 as usize)
+            .saturating_mul(dimensions.1 as usize)
+            .saturating_mul(bytes_per_pixel as usize);
+        if pixels.len() != expected_len {
+            log::error!(
+                "Texture [{:?}] byte length {} does not match expected {} for {:?} ({}x{}). Falling back to 1x1 magenta.",
+                label,
+                pixels.len(),
+                expected_len,
+                format,
+                dimensions.0,
+                dimensions.1
+            );
+            return Self::from_bytes_verbose_mipmapped_with_format(
+                graphics,
+                &[255, 0, 255, 255],
+                Some((1, 1)),
+                None,
+                sampler,
+                Texture::TEXTURE_FORMAT,
+                label,
+            );
+        }
+
+        let size = wgpu::Extent3d {
+            width: dimensions.0,
+            height: dimensions.1,
+            depth_or_array_layers: 1,
+        };
+
+        let mip_level_count = size.width.min(size.height).ilog2() + 1;
+        log::debug!("Mip level count [{:?}]: {}", label, mip_level_count);
+
+        let texture = graphics.device.create_texture(&wgpu::TextureDescriptor {
+            label: Some(format!("{:?} raw texture", label).as_str()),
+            size,
+            mip_level_count,
+            sample_count: 1,
+            dimension: wgpu::TextureDimension::D2,
+            format,
+            usage: wgpu::TextureUsages::TEXTURE_BINDING
+                | wgpu::TextureUsages::RENDER_ATTACHMENT
+                | wgpu::TextureUsages::COPY_DST
+                | wgpu::TextureUsages::COPY_SRC,
+            view_formats: &[],
+        });
+
+        let unpadded_bytes_per_row = bytes_per_pixel * size.width;
+        let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT
+            - 1)
+            & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1);
+
+        if padded_bytes_per_row == unpadded_bytes_per_row {
+            puffin::profile_scope!("write to texture");
+            graphics.queue.write_texture(
+                wgpu::TexelCopyTextureInfo {
+                    texture: &texture,
+                    mip_level: 0,
+                    origin: wgpu::Origin3d::ZERO,
+                    aspect: wgpu::TextureAspect::All,
+                },
+                pixels,
+                wgpu::TexelCopyBufferLayout {
+                    offset: 0,
+                    bytes_per_row: Some(unpadded_bytes_per_row),
+                    rows_per_image: Some(size.height),
+                },
+                size,
+            );
+        } else {
+            puffin::profile_scope!("write to texture");
+            let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize];
+            let src_stride = unpadded_bytes_per_row as usize;
+            let dst_stride = padded_bytes_per_row as usize;
+            for row in 0..size.height as usize {
+                let src_start = row * src_stride;
+                let dst_start = row * dst_stride;
+                padded[dst_start..dst_start + src_stride]
+                    .copy_from_slice(&pixels[src_start..src_start + src_stride]);
+            }
+
+            graphics.queue.write_texture(
+                wgpu::TexelCopyTextureInfo {
+                    texture: &texture,
+                    mip_level: 0,
+                    origin: wgpu::Origin3d::ZERO,
+                    aspect: wgpu::TextureAspect::All,
+                },
+                &padded,
+                wgpu::TexelCopyBufferLayout {
+                    offset: 0,
+                    bytes_per_row: Some(padded_bytes_per_row),
+                    rows_per_image: Some(size.height),
+                },
+                size,
+            );
+        }
+
+        let sampler_desc = sampler.unwrap_or(wgpu::SamplerDescriptor {
+            address_mode_u: wgpu::AddressMode::ClampToEdge,
+            address_mode_v: wgpu::AddressMode::ClampToEdge,
+            address_mode_w: wgpu::AddressMode::ClampToEdge,
+            mag_filter: wgpu::FilterMode::Linear,
+            min_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Linear,
+            ..Default::default()
+        });
+
+        let sampler = graphics.device.create_sampler(&sampler_desc);
+        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+        let texture = Self {
+            label: label.to_potential_string(),
+            texture,
+            sampler,
+            size,
+            view,
+            hash: Some(hash),
+            reference: Some(ResourceReference::from_bytes(pixels)),
+            mime_type: mime_type.map(|value| value.to_string()),
+        };
+
+        if let Err(err) =
+            graphics
+                .mipmapper
+                .compute_mipmaps(&graphics.device, &graphics.queue, &texture)
+        {
+            log_once::warn_once!("Failed to generate mipmaps: {}", err);
+        }
+
+        texture
+    }
+
     pub fn sampler_from_wrap(wrap: TextureWrapMode) -> wgpu::SamplerDescriptor<'static> {
         wgpu::SamplerDescriptor {
             address_mode_u: wrap.into(),
diff --git a/crates/dropbear-macro/src/lib.rs b/crates/dropbear-macro/src/lib.rs
index 15df2ab..74b193a 100644
--- a/crates/dropbear-macro/src/lib.rs
+++ b/crates/dropbear-macro/src/lib.rs
@@ -876,6 +876,7 @@ fn build_jni_return(
             match #inner_name(#(#call_args),*) {
                 crate::scripting::result::DropbearNativeResult::Ok(()) => (),
                 crate::scripting::result::DropbearNativeResult::Err(e) => {
+                    eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                     let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                 }
             }
@@ -922,6 +923,7 @@ fn build_jni_return(
                         None => std::ptr::null_mut(),
                     },
                     crate::scripting::result::DropbearNativeResult::Err(e) => {
+                        eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                         let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                         std::ptr::null_mut()
                     }
@@ -944,6 +946,7 @@ fn build_jni_return(
                         None => std::ptr::null_mut(),
                     },
                     crate::scripting::result::DropbearNativeResult::Err(e) => {
+                        eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                         let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                         std::ptr::null_mut()
                     }
@@ -965,6 +968,7 @@ fn build_jni_return(
                     None => std::ptr::null_mut(),
                 },
                 crate::scripting::result::DropbearNativeResult::Err(e) => {
+                    eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                     let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                     std::ptr::null_mut()
                 }
@@ -984,6 +988,7 @@ fn build_jni_return(
                     }
                 },
                 crate::scripting::result::DropbearNativeResult::Err(e) => {
+                    eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                     let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                     crate::ffi_error_return!()
                 }
@@ -999,6 +1004,7 @@ fn build_jni_return(
             match #inner_name(#(#call_args),*) {
                 crate::scripting::result::DropbearNativeResult::Ok(val) => #cast,
                 crate::scripting::result::DropbearNativeResult::Err(e) => {
+                    eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                     let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                     crate::ffi_error_return!()
                 }
@@ -1017,6 +1023,7 @@ fn build_jni_return(
                 }
             },
             crate::scripting::result::DropbearNativeResult::Err(e) => {
+                eprintln!("JNI call failed in {}: {:?}", stringify!(#inner_name), e);
                 let _ = env.throw_new("java/lang/RuntimeException", format!("JNI call failed: {:?}", e));
                 std::ptr::null_mut()
             }
diff --git a/crates/eucalyptus-core/src/camera.rs b/crates/eucalyptus-core/src/camera.rs
index 05a8e12..8217e4b 100644
--- a/crates/eucalyptus-core/src/camera.rs
+++ b/crates/eucalyptus-core/src/camera.rs
@@ -12,7 +12,6 @@ use egui::{CollapsingHeader, Ui};
 use glam::DVec3;
 use hecs::{Entity, World};
 use serde::{Deserialize, Serialize};
-use std::any::Any;
 use std::sync::Arc;
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -77,7 +76,9 @@ impl Component for Camera {
 
 impl InspectableComponent for Camera {
     fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
-        CollapsingHeader::new("Camera3D").show(ui, |ui| {
+        CollapsingHeader::new("Camera3D")
+            .default_open(true)
+            .show(ui, |ui| {
             let mut changed = false;
 
             ui.horizontal(|ui| {
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 380bff4..b061d3c 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -54,9 +54,9 @@ pub struct ComponentRegistry {
 /// Describes a handy little future for [`Component::init`], which deals with initialising a component from its serialized form.
 ///
 /// Typically thrown in as a return parameter as `-> ComponentInitFuture<'a, Self>`
-pub type ComponentInitFuture<'a, T: Component> = std::pin::Pin<
+pub type ComponentInitFuture<'a, T> = std::pin::Pin<
     Box<
-        dyn std::future::Future<Output = anyhow::Result<T::RequiredComponentTypes>>
+        dyn std::future::Future<Output = anyhow::Result<<T as Component>::RequiredComponentTypes>>
             + Send
             + Sync
             + 'a,
@@ -401,6 +401,8 @@ impl ComponentRegistry {
             } else {
                 ui.label(format!("{} (no inspector)", desc.type_name));
             }
+
+            ui.separator();
         }
     }
 
@@ -468,10 +470,10 @@ pub trait Component: Sync + Send {
 
     /// Converts [`Self::SerializedForm`] into a [`Component`] instance that can be added to
     /// `hecs::EntityBuilder` during scene initialisation.
-    fn init<'a>(
-        ser: &'a Self::SerializedForm,
+    fn init(
+        ser: &'_ Self::SerializedForm,
         graphics: Arc<SharedGraphicsContext>,
-    ) -> ComponentInitFuture<'a, Self>;
+    ) -> ComponentInitFuture<'_, Self>;
 
     /// Called every frame to update the component's state.
     fn update_component(
@@ -510,10 +512,10 @@ impl Component for MeshRenderer {
         }
     }
 
-    fn init<'a>(
-        ser: &'a Self::SerializedForm,
+    fn init(
+        ser: &'_ Self::SerializedForm,
         graphics: Arc<SharedGraphicsContext>,
-    ) -> ComponentInitFuture<'a, Self> {
+    ) -> ComponentInitFuture<'_, Self> {
         Box::pin(async move {
             let import_scale = ser.import_scale.unwrap_or(1.0);
 
@@ -747,7 +749,7 @@ impl InspectableComponent for MeshRenderer {
             let proc_obj = ProcedurallyGeneratedObject::cuboid(size_vec);
             if force_new {
                 let mut model =
-                    proc_obj.construct(graphics.clone(), None, None, None, ASSET_REGISTRY.clone());
+                    { proc_obj.construct(graphics.clone(), None, None, None, ASSET_REGISTRY.clone()) };
                 let mut hasher = DefaultHasher::new();
                 model.hash.hash(&mut hasher);
                 if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
@@ -785,8 +787,10 @@ impl InspectableComponent for MeshRenderer {
                     model.hash = current_model.id;
                     model.label = existing_label;
 
-                    let mut asset = ASSET_REGISTRY.write();
-                    asset.update_model(current_model, model);
+                    {
+                        let mut asset = ASSET_REGISTRY.write();
+                        asset.update_model(current_model, model);
+                    }
                 } else {
                     let handle =
                         proc_obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone());
@@ -797,26 +801,32 @@ impl InspectableComponent for MeshRenderer {
             renderer.reset_texture_override();
         };
 
-        CollapsingHeader::new("Mesh Renderer").show(ui, |ui| {
-            let registry = ASSET_REGISTRY.read();
-            let current_model = registry.get_model(self.model());
-            let model_list = registry.list_models();
-
-            let model_reference = current_model
-                .as_ref()
-                .map(|model| model.path.clone())
-                .unwrap_or_default();
-            let model_label = current_model
-                .as_ref()
-                .map(|model| model.label.clone())
-                .unwrap_or_else(|| "None".to_string());
+        CollapsingHeader::new("Mesh Renderer")
+            .default_open(true)
+            .show(ui, |ui| {
+            let (model_reference, model_title, model_list) = {
+                let registry = ASSET_REGISTRY.read();
+                let current_model = registry.get_model(self.model());
+                let model_list = registry.list_models();
+
+                let model_reference = current_model
+                    .as_ref()
+                    .map(|model| model.path.clone())
+                    .unwrap_or_default();
+                let model_label = current_model
+                    .as_ref()
+                    .map(|model| model.label.clone())
+                    .unwrap_or_else(|| "None".to_string());
+
+                let model_title = match &model_reference.ref_type {
+                    ResourceReferenceType::None => "None".to_string(),
+                    ResourceReferenceType::ProcObj(obj) => match obj.ty {
+                        ProcObjType::Cuboid => "Cuboid".to_string(),
+                    },
+                    _ => model_label,
+                };
 
-            let model_title = match &model_reference.ref_type {
-                ResourceReferenceType::None => "None".to_string(),
-                ResourceReferenceType::ProcObj(obj) => match obj.ty {
-                    ProcObjType::Cuboid => "Cuboid".to_string(),
-                },
-                _ => model_label,
+                (model_reference, model_title, model_list)
             };
 
             ui.vertical(|ui| {
diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index b947238..50f2ee5 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -255,7 +255,7 @@ impl ProjectConfig {
             {
                 match SceneConfig::read_from(&path) {
                     Ok(scene) => {
-                        log::debug!("Loaded scene config: {}", scene.scene_name);
+                        log::debug!("Loaded scene config from file, added to SCENES entry: {}", scene.scene_name);
                         scene_configs.push(scene);
                     }
                     Err(e) => {
diff --git a/crates/eucalyptus-core/src/logging.rs b/crates/eucalyptus-core/src/logging.rs
index e5802ff..1cdbbad 100644
--- a/crates/eucalyptus-core/src/logging.rs
+++ b/crates/eucalyptus-core/src/logging.rs
@@ -44,7 +44,7 @@ impl Display for LogLevel {
 pub static GLOBAL_TOASTS: Lazy<Mutex<Toasts>> = Lazy::new(|| {
     Mutex::new(
         Toasts::new()
-            .anchor(egui::Align2::RIGHT_BOTTOM, (-10.0, -10.0))
+            .anchor(egui::Align2::RIGHT_BOTTOM, (-10.0, -28.0))
             .direction(egui::Direction::BottomUp),
     )
 });
diff --git a/crates/eucalyptus-core/src/physics/kcc.rs b/crates/eucalyptus-core/src/physics/kcc.rs
index 7c9bdf2..fe21794 100644
--- a/crates/eucalyptus-core/src/physics/kcc.rs
+++ b/crates/eucalyptus-core/src/physics/kcc.rs
@@ -316,7 +316,7 @@ fn move_character(
         };
 
         if body_type != RigidBodyType::KinematicPositionBased {
-            return Err(DropbearNativeError::InvalidArgument);
+            return Ok(()); // soft error, just tell the user
         }
 
         let collider_handles = physics_state
@@ -337,7 +337,7 @@ fn move_character(
             *collider.position()
         };
 
-        let filter = QueryFilter::default().exclude_rigid_body(*rigid_body_handle);
+        let filter = QuprintlneryFilter::default().exclude_rigid_body(*rigid_body_handle);
         let query_pipeline = physics_state.broad_phase.as_query_pipeline(
             physics_state.narrow_phase.query_dispatcher(),
             &physics_state.bodies,
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 8a1229f..eb203e0 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -4,7 +4,7 @@ pub mod loading;
 pub mod scripting;
 
 use crate::camera::CameraComponent;
-use crate::component::{Component, ComponentRegistry, SerializedComponent};
+use crate::component::{ComponentRegistry, SerializedComponent};
 use crate::hierarchy::{Children, Parent, SceneHierarchy};
 use crate::physics::PhysicsState;
 use crate::physics::collider::ColliderGroup;
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index e6f25c5..56daa97 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -21,7 +21,6 @@ use hecs::{Entity, World};
 use once_cell::sync::Lazy;
 use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
-use std::any::Any;
 use std::borrow::Borrow;
 use std::collections::HashMap;
 use std::fmt;
diff --git a/crates/eucalyptus-core/src/transform.rs b/crates/eucalyptus-core/src/transform.rs
index deb86a7..fa37c50 100644
--- a/crates/eucalyptus-core/src/transform.rs
+++ b/crates/eucalyptus-core/src/transform.rs
@@ -54,12 +54,14 @@ impl Component for EntityTransform {
 
 impl InspectableComponent for EntityTransform {
     fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
-        CollapsingHeader::new("Entity Transform").show(ui, |ui| {
-            CollapsingHeader::new("Local").show(ui, |ui| {
+        CollapsingHeader::new("Entity Transform")
+            .default_open(true)
+            .show(ui, |ui| {
+            CollapsingHeader::new("Local").default_open(true).show(ui, |ui| {
                 self.local_mut().inspect(ui);
             });
             ui.add_space(4.0);
-            CollapsingHeader::new("World").show(ui, |ui| {
+            CollapsingHeader::new("World").default_open(true).show(ui, |ui| {
                 self.world_mut().inspect(ui);
             });
         });
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index a7f315e..b806ed4 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -55,7 +55,7 @@ impl ComponentNodeSelection {
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-struct ComponentNodeKey {
+pub(crate) struct ComponentNodeKey {
     entity_bits: u64,
     component_type_id: u64,
 }
@@ -92,19 +92,9 @@ pub struct StaticallyKept {
     context_menu_tab: Option<EditorTab>,
     pub(crate) is_focused: bool,
     pub(crate) old_pos: Transform,
-    pub(crate) scale_locked: bool,
 
-    pub(crate) old_label_entity: Option<hecs::Entity>,
-    pub(crate) label_original: Option<String>,
-    pub(crate) label_last_edit: Option<Instant>,
-
-    pub(crate) transform_old_entity: Option<hecs::Entity>,
-    pub(crate) transform_original_transform: Option<Transform>,
     pub(crate) entity_transform_original: Option<EntityTransform>,
 
-    pub(crate) transform_in_progress: bool,
-    pub(crate) transform_rotation_cache: HashMap<Entity, glam::DVec3>,
-
     pub(crate) dragged_asset: Option<DraggedAsset>,
     pub(crate) asset_node_assets: HashMap<u64, DraggedAsset>,
     pub(crate) asset_node_info: HashMap<u64, AssetNodeInfo>,
@@ -307,6 +297,7 @@ pub(crate) struct FsEntry {
 
 #[derive(Clone, Debug)]
 pub(crate) struct AssetRenameState {
+    #[allow(dead_code)] // cbb to refactor just to remove this
     pub(crate) node_id: u64,
     pub(crate) original_path: PathBuf,
     pub(crate) buffer: String,
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index ec85e8c..26853a0 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -94,6 +94,7 @@ pub struct Editor {
     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 active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -284,6 +285,7 @@ impl Editor {
             default_skinning_buffer: None,
             default_skinning_bind_group: None,
             light_skin_bind_group: None,
+            scene_globals_bind_group: None,
         })
     }
 
@@ -949,7 +951,19 @@ impl Editor {
                             self.signal = Signal::StopPlaying;
                         }
                     } else if ui.button("Play").clicked() {
-                        self.signal = Signal::Play;
+                        // run prechecks to ensure a starting camera exists and stuff
+                        let mut found_starting = false;
+                        for (_, comp) in self.world.query::<(&Camera, &CameraComponent)>().iter() {
+                            if comp.starting_camera {
+                                found_starting = true;
+                            }
+                        }
+
+                        if !found_starting {
+                            fatal!("Unable to start play mode: No initial camera set for this scene");
+                        } else {
+                            self.signal = Signal::Play;
+                        }
                     }
                     ui.menu_button("Export", |ui| {
                         // todo: create a window for better build menu
@@ -1148,7 +1162,18 @@ impl Editor {
                         ui.add_enabled_ui(!can_play, |ui| {
                             if ui.button("▶").clicked() {
                                 log::debug!("Menu Button Play button pressed");
-                                self.signal = Signal::Play;
+                                let mut found_starting = false;
+                                for (_, comp) in self.world.query::<(&Camera, &CameraComponent)>().iter() {
+                                    if comp.starting_camera {
+                                        found_starting = true;
+                                    }
+                                }
+
+                                if !found_starting {
+                                    fatal!("Unable to start play mode: No initial camera set for this scene");
+                                } else {
+                                    self.signal = Signal::Play;
+                                }
                             }
                         });
                     });
diff --git a/crates/eucalyptus-editor/src/editor/resource.rs b/crates/eucalyptus-editor/src/editor/resource.rs
index 9e958b2..c6435d1 100644
--- a/crates/eucalyptus-editor/src/editor/resource.rs
+++ b/crates/eucalyptus-editor/src/editor/resource.rs
@@ -1,6 +1,7 @@
+use hecs::Entity;
 use crate::editor::{EditorTabViewer, TABS_GLOBAL};
 use dropbear_engine::camera::Camera;
-use eucalyptus_core::camera::CameraComponent;
+use eucalyptus_core::camera::{CameraComponent, CameraType};
 
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) {
@@ -15,12 +16,8 @@ impl<'a> EditorTabViewer<'a> {
                 ui.label(format!("Entity ID: {}", inspect_entity.id()));
                 ui.separator();
 
-                if self
-                    .world
-                    .query_one::<(&Camera, &CameraComponent)>(inspect_entity)
-                    .get()
-                    .is_ok()
-                {
+                let mut local_unset_comp = false;
+                if let Ok((_, comp)) = self.world.query_one::<(&Camera, &CameraComponent)>(inspect_entity).get() {
                     let is_active = self
                         .active_camera
                         .lock()
@@ -38,10 +35,38 @@ impl<'a> EditorTabViewer<'a> {
                             let mut active_camera = self.active_camera.lock();
                             *active_camera = Some(inspect_entity);
                         }
+
+                        let mut is_starting = comp.starting_camera;
+                        let is_starting_label = if comp.camera_type == CameraType::Debug {
+                            is_starting = false;
+                            "Cannot set a Debug camera as starting"
+                        } else if is_starting {
+                            "Already set as starting"
+                        } else {
+                            "Set as starting"
+                        };
+
+                        if ui
+                            .add_enabled(!is_starting, egui::Button::new(is_starting_label))
+                            .clicked()
+                        {
+                            local_unset_comp = true;
+                        }
                     });
                     ui.separator();
                 }
 
+                if local_unset_comp {
+                    for (e, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() {
+                        if e == inspect_entity {
+                            comp.starting_camera = true;
+                            continue;
+                        }
+                        log::debug!("Unset starting camera for entity {:?}", e);
+                        comp.starting_camera = false;
+                    }
+                }
+
                 self.component_registry.inspect_components(
                     self.world,
                     inspect_entity,
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 524eddb..e170cab 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -502,29 +502,21 @@ impl Scene for Editor {
             .expect("Default skinning buffer not initialized");
 
         // 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 self.scene_globals_bind_group.is_none() {
+            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 globals = self
-                    .shader_globals
-                    .as_ref()
-                    .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
@@ -575,35 +567,14 @@ impl Scene for Editor {
                 render_pass.draw_model_instanced(
                     model,
                     0..instance_count,
-                    &globals_camera_bind_group,
+                    scene_globals_bind_group.as_ref(),
                     &light_skin_bind_group,
+                    environment_bind_group,
                 );
             }
         }
 
         if let Some(lcp) = &self.light_cube_pipeline {
-            let globals = self
-                .shader_globals
-                .as_ref()
-                .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_buffer) in animated_instances {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
@@ -667,12 +638,43 @@ impl Scene for Editor {
                 render_pass.draw_model_instanced(
                     model,
                     0..1,
-                    &globals_camera_bind_group,
+                    scene_globals_bind_group.as_ref(),
                     &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above
+                    environment_bind_group,
                 );
             }
         }
 
+        if let Some(sky) = &self.sky_pipeline {
+            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("sky render pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                    depth_slice: None,
+                })],
+                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,
+                }),
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            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.draw(0..3, 0..1);
+        }
+
         // collider pipeline
         {
             let show_hitboxes = self
@@ -687,14 +689,9 @@ impl Scene for Editor {
                 })
                 .unwrap_or(false);
 
-            log_once::debug_once!(
-                "show_hitboxes = {}, current_scene_name = {:?}",
-                show_hitboxes,
-                self.current_scene_name
-            );
-
             if show_hitboxes {
                 if let Some(collider_pipeline) = &self.collider_wireframe_pipeline {
+                    log_once::debug_once!("Found collider wireframe pipeline");
                     let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("collider wireframe render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -760,11 +757,6 @@ impl Scene for Editor {
                                 });
                         }
                     }
-                    log_once::debug_once!(
-                        "Collider wireframe: {} entities with colliders, {} total colliders",
-                        entity_count,
-                        collider_count
-                    );
 
                     if !instances_by_shape.is_empty() {
                         let total_instances: usize =
@@ -814,40 +806,12 @@ impl Scene for Editor {
                             render_pass.draw_indexed(0..geometry.index_count, 0, 0..count as u32);
                         }
                     }
+                } else {
+                    log_once::warn_once!("No collider pipeline found");
                 }
             }
         }
 
-        if let Some(sky) = &self.sky_pipeline {
-            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("sky render pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: hdr.view(),
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    },
-                    depth_slice: None,
-                })],
-                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,
-                }),
-                timestamp_writes: None,
-                occlusion_query_set: None,
-            });
-
-            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.draw(0..3, 0..1);
-        }
-
         hdr.process(&mut encoder, &graphics.viewport_texture.view);
 
         if let Err(e) = encoder.submit() {
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index c6cfac0..9b2f72d 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -1,39 +1,22 @@
 use crate::editor::{AssetClipboard, Editor, EditorState, Signal};
 use crate::spawn::{PendingSpawn, push_pending_spawn};
 use dropbear_engine::graphics::SharedGraphicsContext;
-use dropbear_engine::utils::{EUCA_SCHEME, relative_path_from_euca};
 use egui::Align2;
 use eucalyptus_core::camera::{CameraComponent, CameraType};
 use eucalyptus_core::scripting::{BuildStatus, build_jvm};
-use eucalyptus_core::states::{EditorTab, PROJECT};
+use eucalyptus_core::states::{PROJECT};
 use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console};
 use std::fs;
 use std::path::PathBuf;
 use std::sync::Arc;
 use winit::keyboard::KeyCode;
 
-fn resolve_editor_path(uri: &str) -> PathBuf {
-    if uri.starts_with(EUCA_SCHEME) {
-        let relative =
-            relative_path_from_euca(uri).unwrap_or_else(|_| uri.trim_start_matches(EUCA_SCHEME));
-        let project_path = PROJECT.read().project_path.clone();
-        project_path.join("resources").join(relative)
-    } else {
-        PathBuf::from(uri)
-    }
-}
-
 pub trait SignalController {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>;
 }
 
 impl SignalController for Editor {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> {
-        fn is_legacy_internal_cube_uri(uri: &str) -> bool {
-            let uri = uri.replace('\\', "/");
-            uri.ends_with("internal/dropbear/models/cube")
-        }
-
         let local_signal: Option<Signal> = None;
         let show = true;
 
@@ -346,8 +329,8 @@ impl SignalController for Editor {
                                     self.signal = Signal::None;
                                     self.show_build_window = false;
                                     self.editor_state = EditorState::Editing;
-                                    self.dock_state
-                                        .push_to_focused_leaf(EditorTab::ErrorConsole);
+                                    // self.dock_state
+                                    //     .push_to_focused_leaf(EditorTab::ErrorConsole); // getting too problematic
                                 }
                             }
                         }
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index fdc79df..a3a174f 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -137,6 +137,7 @@ pub struct PlayMode {
     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>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -228,6 +229,7 @@ impl PlayMode {
             default_skinning_buffer: None,
             default_skinning_bind_group: None,
             light_skin_bind_group: None,
+            scene_globals_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 e2bf1af..a4a0635 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -36,11 +36,6 @@ use winit::event_loop::ActiveEventLoop;
 
 impl Scene for PlayMode {
     fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        // let mut yak = yakui_winit::YakuiWinit::new(&graphics.window);
-        // yak.set_automatic_viewport(false);
-        // yak.set_automatic_scale_factor(false);
-        // self.yakui_winit = Some(yak);
-
         if self.current_scene.is_none() {
             let initial_scene = if let Some(s) = &self.initial_scene {
                 s.clone()
@@ -60,7 +55,7 @@ impl Scene for PlayMode {
         }
     }
 
-    fn physics_update(&mut self, dt: f32, _graphics: Arc<SharedGraphicsContext>) {
+    fn physics_update(&muprintlnt self, dt: f32, _graphics: Arc<SharedGraphicsContext>) {
         if self.scripts_ready {
             let _ = self
                 .script_manager
@@ -542,93 +537,69 @@ impl Scene for PlayMode {
                         }));
                     });
 
-                    // overlay
-                    // UI_CONTEXT.with(|yakui_cell| {
-                    //     let yak = yakui_cell.borrow();
-                    //     let mut yakui = yak.yakui_state.lock();
-                    //
-                    //     let tex_size = graphics.viewport_texture.size;
-                    //     let viewport_size = yakui::geometry::Vec2::new(
-                    //         tex_size.width as f32,
-                    //         tex_size.height as f32,
+                    // if let Some(kino) = &mut self.kino {
+                    //     // #[allow(dead_code)]
+                    //     let no_texture = kino.add_texture_from_bytes(
+                    //         &graphics.device,
+                    //         &graphics.queue,
+                    //         "no texture",
+                    //         include_bytes!("../../../resources/textures/no-texture.png"),
+                    //         256,
+                    //         256,
                     //     );
-                    //     yakui.set_surface_size(viewport_size);
-                    //     yakui.set_unscaled_viewport(yakui::geometry::Rect::from_pos_size(
-                    //         yakui::geometry::Vec2::ZERO,
-                    //         viewport_size,
-                    //     ));
-                    //     yakui.set_scale_factor(graphics.window.scale_factor() as f32);
-                    //
-                    //     yakui.start();
-                    //
-                    //     // eucalyptus_core::ui::poll();
-                    //
-                    //     yakui.finish();
-                    // });
 
-                    if let Some(kino) = &mut self.kino {
-                        // #[allow(dead_code)]
-                        let no_texture = kino.add_texture_from_bytes(
-                            &graphics.device,
-                            &graphics.queue,
-                            "no texture",
-                            include_bytes!("../../../resources/textures/no-texture.png"),
-                            256,
-                            256,
-                        );
-
-                        let parent = kino_ui::rect_container(
-                            kino,
-                            Rectangle::new("parent")
-                                .fill(Fill::new([1.0, 1.0, 1.0, 1.0]))
-                                .size(vec2(400.0, 400.0)),
-                            |kino| {
-                                kino.add_widget(
-                                    Rectangle::new("rect")
-                                        .texture(no_texture)
-                                        .size(vec2(128.0, 100.0))
-                                        .border(Border::new([1.0, 0.0, 0.0, 1.0], 3.0))
-                                        .fill(Fill::new([1.0, 1.0, 1.0, 1.0]))
-                                        .texture(no_texture)
-                                        .build(),
-                                );
-                            },
-                        );
+                    //     let parent = kino_ui::rect_container(
+                    //         kino,
+                    //         Rectangle::new("parent")
+                    //             .fill(Fill::new([1.0, 1.0, 1.0, 1.0]))
+                    //             .size(vec2(400.0, 400.0)),
+                    //         |kino| {
+                    //             kino.add_widget(
+                    //                 Rectangle::new("rect")
+                    //                     .texture(no_texture)
+                    //                     .size(vec2(128.0, 100.0))
+                    //                     .border(Border::new([1.0, 0.0, 0.0, 1.0], 3.0))
+                    //                     .fill(Fill::new([1.0, 1.0, 1.0, 1.0]))
+                    //                     .texture(no_texture)
+                    //                     .build(),
+                    //             );
+                    //         },
+                    //     );
 
-                        kino_ui::label(kino, "Hello World!", |l| {
-                            l.position = vec2(
-                                graphics.viewport_texture.size.width as f32 / 2.0,
-                                graphics.viewport_texture.size.height as f32 / 2.0,
-                            );
-                            l.metrics.font_size = 30.0;
-                        });
+                    //     kino_ui::label(kino, "Hello World!", |l| {
+                    //         l.position = vec2(
+                    //             graphics.viewport_texture.size.width as f32 / 2.0,
+                    //             graphics.viewport_texture.size.height as f32 / 2.0,
+                    //         );
+                    //         l.metrics.font_size = 30.0;
+                    //     });
 
-                        kino.poll();
+                    //     kino.poll();
 
-                        if kino.response(parent).clicked {
-                            println!("Parent clicked!");
-                        };
+                    //     if kino.response(parent).clicked {
+                    //         println!("Parent clicked!");
+                    //     };
 
-                        // if kino.response(parent).hovering {
-                        //     println!("Parent hovering");
-                        // };
+                    //     // if kino.response(parent).hovering {
+                    //     //     println!("Parent hovering");
+                    //     // };
 
-                        if kino.response("rect").clicked {
-                            println!("child clicked!");
-                        };
+                    //     if kino.response("rect").clicked {
+                    //         println!("child clicked!");
+                    //     };
 
-                        // if kino.response("rect").hovering {
-                        //     println!("child hovering");
-                        // };
+                    //     // if kino.response("rect").hovering {
+                    //     //     println!("child hovering");
+                    //     // };
 
-                        if kino.response("Hello World!").clicked {
-                            println!("text clicked")
-                        }
+                    //     if kino.response("Hello World!").clicked {
+                    //         println!("text clicked")
+                    //     }
 
-                        if kino.response("Hello World!").hovering {
-                            println!("text hovering");
-                        };
-                    }
+                    //     if kino.response("Hello World!").hovering {
+                    //         println!("text hovering");
+                    //     };
+                    // }
                 } else {
                     log::warn!("No such camera exists in the world");
                 }
@@ -841,29 +812,21 @@ impl Scene for PlayMode {
             .expect("Default skinning buffer not initialized");
 
         // 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 self.scene_globals_bind_group.is_none() {
+            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 globals = self
-                    .shader_globals
-                    .as_ref()
-                    .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
@@ -914,35 +877,14 @@ impl Scene for PlayMode {
                 render_pass.draw_model_instanced(
                     model,
                     0..instance_count,
-                    &globals_camera_bind_group,
+                    scene_globals_bind_group.as_ref(),
                     &light_skin_bind_group,
+                    environment_bind_group,
                 );
             }
         }
 
         if let Some(lcp) = &self.light_cube_pipeline {
-            let globals = self
-                .shader_globals
-                .as_ref()
-                .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_buffer) in animated_instances {
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
@@ -1006,8 +948,9 @@ impl Scene for PlayMode {
                 render_pass.draw_model_instanced(
                     model,
                     0..1,
-                    &globals_camera_bind_group,
+                    scene_globals_bind_group.as_ref(),
                     &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above
+                    environment_bind_group,
                 );
             }
         }
@@ -1026,6 +969,66 @@ impl Scene for PlayMode {
                 })
                 .unwrap_or(false);
 
+            // UI_CONTEXT.with(|v| {
+            //     let commands = graphics.yakui_renderer.lock().paint(
+            //         &mut v.borrow().yakui_state.lock(),
+            //         &graphics.device,
+            //         &graphics.queue,
+            //         SurfaceInfo {
+            //             format: Texture::TEXTURE_FORMAT,
+            //             sample_count: 1,
+            //             color_attachment: &graphics.viewport_texture.view,
+            //             resolve_target: None,
+            //         }
+            //     );
+            //
+            //     graphics.queue.submit([commands]);
+            // });
+        }
+
+        if let Some(sky) = &self.sky_pipeline {
+            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("sky render pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                    depth_slice: None,
+                })],
+                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,
+                }),
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            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.draw(0..3, 0..1);
+        }
+
+        {
+            let show_hitboxes = self
+                .current_scene
+                .as_ref()
+                .and_then(|scene_name| {
+                    let scenes = SCENES.read();
+                    scenes
+                        .iter()
+                        .find(|scene| &scene.scene_name == scene_name)
+                        .map(|scene| scene.settings.show_hitboxes)
+                })
+                .unwrap_or(false);
+
             if show_hitboxes {
                 if let Some(collider_pipeline) = &self.collider_wireframe_pipeline {
                     let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -1140,52 +1143,6 @@ impl Scene for PlayMode {
                     }
                 }
             }
-
-            // UI_CONTEXT.with(|v| {
-            //     let commands = graphics.yakui_renderer.lock().paint(
-            //         &mut v.borrow().yakui_state.lock(),
-            //         &graphics.device,
-            //         &graphics.queue,
-            //         SurfaceInfo {
-            //             format: Texture::TEXTURE_FORMAT,
-            //             sample_count: 1,
-            //             color_attachment: &graphics.viewport_texture.view,
-            //             resolve_target: None,
-            //         }
-            //     );
-            //
-            //     graphics.queue.submit([commands]);
-            // });
-        }
-
-        if let Some(sky) = &self.sky_pipeline {
-            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("sky render pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: hdr.view(),
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    },
-                    depth_slice: None,
-                })],
-                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,
-                }),
-                timestamp_writes: None,
-                occlusion_query_set: None,
-            });
-
-            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.draw(0..3, 0..1);
         }
 
         hdr.process(&mut encoder, &graphics.viewport_texture.view);