kitgit

tirbofish/dropbear · commit

42723a07486b7f05d3840e71f9cb0353261081f4

fix: egui kept on dragging/highlighting all of my characters (which was really frustrating). perf: improving performance and looks wip: outline shader for selection (as if selection worked in the first place LMAOOOO) refactor: removed old ColliderWireframePipeline code (since we changed the backend to use DebugDraw). perf: changed compilation back to mold instead of lld to improve build times. im so tired hopefully the tabuteau will be happy :/

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-04-01 16:19

view full diff

diff --git a/.cargo/config.toml b/.cargo/config.toml
index e65d04d..83c73e4 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,7 +1,8 @@
 [target.x86_64-unknown-linux-gnu]
 linker = "clang"
 # rustflags = ["-C", "link-arg=-fuse-ld=mold", "-C", "relocation-model=pic"]
-rustflags = ["-C", "link-arg=-fuse-ld=lld"]
+ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
+#rustflags = ["-C", "link-arg=-fuse-ld=lld"]
 
 # note: if you get a compile error such as "Recompile with RPIC", just run `cargo build` 
 # instead of `cargo build -p eucalyptus-core -p eucalyptus-editor`. 
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index 95b17dc..8c74ddc 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -52,6 +52,7 @@ float-derive.workspace = true
 rkyv.workspace = true
 bevy_mikktspace.workspace = true
 wesl.workspace = true
+egui_extras.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
diff --git a/crates/dropbear-engine/src/buffer.rs b/crates/dropbear-engine/src/buffer.rs
index edfbb51..cc3f304 100644
--- a/crates/dropbear-engine/src/buffer.rs
+++ b/crates/dropbear-engine/src/buffer.rs
@@ -2,6 +2,9 @@
 
 use std::marker::PhantomData;
 use bytemuck::NoUninit;
+use dropbear_utils::Dirty;
+
+pub trait BufferType {}
 
 #[derive(Debug, Clone)]
 pub struct ResizableBuffer<T> {
@@ -12,6 +15,8 @@ pub struct ResizableBuffer<T> {
     _marker: PhantomData<T>,
 }
 
+impl<T> BufferType for ResizableBuffer<T> {}
+
 impl<T: NoUninit> ResizableBuffer<T> {
     pub fn new(
         device: &wgpu::Device,
@@ -79,13 +84,15 @@ impl<T: NoUninit> ResizableBuffer<T> {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct UniformBuffer<T> {
     buffer: wgpu::Buffer,
     label: String,
     _marker: PhantomData<T>,
 }
 
+impl<T> BufferType for UniformBuffer<T> {}
+
 impl<T: NoUninit> UniformBuffer<T> {
     pub fn new(device: &wgpu::Device, label: &str) -> Self {
         let size = (std::mem::size_of::<T>() as wgpu::BufferAddress).max(16);
@@ -125,6 +132,8 @@ pub struct StorageBuffer<T> {
     _marker: PhantomData<T>,
 }
 
+impl<T> BufferType for StorageBuffer<T> {}
+
 impl<T: NoUninit> StorageBuffer<T> {
     pub fn new_read_only(device: &wgpu::Device, label: &str) -> Self {
         Self::new(device, label, true)
@@ -200,3 +209,32 @@ impl<T: NoUninit> StorageBuffer<T> {
         queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(values));
     }
 }
+
+#[derive(Clone, PartialEq)]
+pub struct MutableDataBuffer<T> {
+    data: Dirty<T>,
+    pub buffer: UniformBuffer<T>,
+}
+
+impl<T: NoUninit> MutableDataBuffer<T> {
+    pub fn new(data: Dirty<T>, buffer: UniformBuffer<T>) -> Self {
+        Self {
+            data,
+            buffer,
+        }
+    }
+    
+    pub fn write(&mut self, queue: &wgpu::Queue) {
+        if let Some(value) = self.data.get_if_dirty() {
+            self.buffer.write(queue, value);
+        }
+    }
+    
+    pub fn get_data(&self) -> &T {
+        self.data.get()
+    }
+    
+    pub fn set_data(&mut self, value: T) {
+        self.data.set(value);
+    }
+}
diff --git a/crates/dropbear-engine/src/egui_renderer.rs b/crates/dropbear-engine/src/egui_renderer.rs
index c460b93..679c649 100644
--- a/crates/dropbear-engine/src/egui_renderer.rs
+++ b/crates/dropbear-engine/src/egui_renderer.rs
@@ -1,4 +1,4 @@
-use egui::Context;
+use egui::{Context, FullOutput};
 use egui_wgpu::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView};
 use egui_wgpu::{Renderer, RendererOptions, ScreenDescriptor, wgpu};
 use egui_winit::State;
@@ -67,6 +67,58 @@ impl EguiRenderer {
         self.frame_started = true;
     }
 
+    pub fn take_input(&mut self, window: &Window) -> (egui::RawInput, egui::Context) {
+        egui_extras::install_image_loaders(self.state.egui_ctx());
+        let raw_input = self.state.take_egui_input(window);
+        let ctx = self.state.egui_ctx().clone();
+        (raw_input, ctx)
+    }
+
+    pub fn process_output(
+        &mut self,
+        full_output: egui::FullOutput,
+        device: &Device,
+        queue: &Queue,
+        window: &Window,
+        window_surface_view: &TextureView,
+        screen_descriptor: ScreenDescriptor,
+    ) -> CommandEncoder {
+        let mut encoder = device
+            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+                label: Some("egui render encoder"),
+            });
+
+        self.handle_full_output(
+            full_output,
+            device,
+            queue,
+            &mut encoder,
+            window,
+            window_surface_view,
+            screen_descriptor,
+        );
+
+        encoder
+    }
+
+    pub fn run_ui(
+        &mut self,
+        run_ui: impl FnMut(&mut egui::Ui),
+
+        device: &Device,
+        queue: &Queue,
+        window: &Window,
+        window_surface_view: &TextureView,
+        screen_descriptor: ScreenDescriptor,
+    ) -> CommandEncoder {
+        let (raw_input, ctx) = self.take_input(window);
+        self.frame_started = true;
+        let full_output = ctx.run_ui(raw_input, run_ui);
+        self.frame_started = false;
+
+        self.process_output(full_output, device, queue, window, window_surface_view, screen_descriptor)
+    }
+
     pub fn end_frame_and_draw(
         &mut self,
         device: &Device,
@@ -77,14 +129,36 @@ impl EguiRenderer {
         screen_descriptor: ScreenDescriptor,
     ) {
         puffin::profile_function!();
+
         if !self.frame_started {
             panic!("begin_frame must be called before end_frame_and_draw can be called!");
         }
 
-        self.ppp(screen_descriptor.pixels_per_point);
-
         let full_output = self.state.egui_ctx().end_pass();
 
+        self.handle_full_output(
+            full_output,
+            device,
+            queue,
+            encoder,
+            window,
+            window_surface_view,
+            screen_descriptor,
+        );
+    }
+
+    fn handle_full_output(
+        &mut self,
+        full_output: FullOutput,
+        device: &Device,
+        queue: &Queue,
+        encoder: &mut CommandEncoder,
+        window: &Window,
+        window_surface_view: &TextureView,
+        screen_descriptor: ScreenDescriptor,
+    ) {
+        self.ppp(screen_descriptor.pixels_per_point);
+
         self.state
             .handle_platform_output(window, full_output.platform_output);
 
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 15a3403..e001ab9 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -38,12 +38,6 @@ pub struct SharedGraphicsContext {
 }
 
 impl SharedGraphicsContext {
-    pub fn get_egui_context(&self) -> Context {
-        self.egui_renderer.lock().context().clone()
-    }
-}
-
-impl SharedGraphicsContext {
     pub(crate) fn from_state(state: &State) -> Self {
         SharedGraphicsContext {
             future_queue: state.future_queue.clone(),
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index f80f276..3366f62 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -779,50 +779,57 @@ Hardware:
             }
         }
 
-        self.egui_renderer.lock().begin_frame(&self.window);
+        let mut window_commands = Vec::new();
 
-        let mut scene_manager = std::mem::replace(&mut self.scene_manager, scene::Manager::new());
 
-        let physics_dt = Duration::from_secs_f32(1.0 / PHYSICS_STEP_RATE as f32);
-        let frame_dt = Duration::from_secs_f32(previous_dt).min(Duration::from_millis(250));
-        let mut physics_accumulator = self.physics_accumulator + frame_dt;
+        let (raw_input, egui_ctx) = self.egui_renderer.lock().take_input(&self.window);
 
-        let window_commands = {
-            let mut steps = 0usize;
-            while physics_accumulator >= physics_dt && steps < MAX_PHYSICS_STEPS_PER_FRAME {
-                scene_manager.physics_update(physics_dt.as_secs_f32(), graphics.clone());
-                physics_accumulator -= physics_dt;
-                steps += 1;
-            }
+        let full_output = egui_ctx.run_ui(raw_input, |ui| {
+            let mut scene_manager = std::mem::replace(&mut self.scene_manager, scene::Manager::new());
 
-            if steps == MAX_PHYSICS_STEPS_PER_FRAME && physics_accumulator >= physics_dt {
-                physics_accumulator = physics_accumulator.min(physics_dt);
-            }
+            let physics_dt = Duration::from_secs_f32(1.0 / PHYSICS_STEP_RATE as f32);
+            let frame_dt = Duration::from_secs_f32(previous_dt).min(Duration::from_millis(250));
+            let mut physics_accumulator = self.physics_accumulator + frame_dt;
 
-            let commands = scene_manager.update(previous_dt, graphics.clone(), event_loop);
-            scene_manager.render(graphics.clone());
-            commands
-        };
+            let commands = {
+                let mut steps = 0usize;
+                while physics_accumulator >= physics_dt && steps < MAX_PHYSICS_STEPS_PER_FRAME {
+                    scene_manager.physics_update(physics_dt.as_secs_f32(), graphics.clone(), ui);
+                    physics_accumulator -= physics_dt;
+                    steps += 1;
+                }
 
-        self.physics_accumulator = physics_accumulator;
+                if steps == MAX_PHYSICS_STEPS_PER_FRAME && physics_accumulator >= physics_dt {
+                    physics_accumulator = physics_accumulator.min(physics_dt);
+                }
 
-        self.scene_manager = scene_manager;
+                let commands = scene_manager.update(previous_dt, graphics.clone(), event_loop, ui);
+                scene_manager.render(graphics.clone(), ui);
+                commands
+            };
 
-        let mut encoder = self
-            .device
-            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
-                label: Some("egui render encoder"),
-            });
+            window_commands.extend(commands);
 
-        {
-            let hdr = self.hdr.read();
-            hdr.process(&mut encoder, &self.viewport_texture.view);
-        }
+            self.physics_accumulator = physics_accumulator;
+
+            self.scene_manager = scene_manager;
+
+            let mut encoder = self
+                .device
+                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+                    label: Some("egui render encoder"),
+                });
+
+            {
+                let hdr = self.hdr.read();
+                hdr.process(&mut encoder, &self.viewport_texture.view);
+            }
+        });
 
-        self.egui_renderer.lock().end_frame_and_draw(
+        let encoder = self.egui_renderer.lock().process_output(
+            full_output,
             &self.device,
             &self.queue,
-            &mut encoder,
             &self.window,
             &view,
             screen_descriptor,
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 59bf635..3cfca43 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -1,5 +1,5 @@
 use crate::asset::{AssetRegistry, Handle};
-use crate::buffer::UniformBuffer;
+use crate::buffer::{MutableDataBuffer, UniformBuffer};
 use crate::texture::TextureBuilder;
 use crate::{
     graphics::SharedGraphicsContext,
@@ -16,10 +16,12 @@ use serde::{Deserialize, Serialize};
 use std::hash::{DefaultHasher, Hash, Hasher};
 use std::sync::Arc;
 use std::{mem, ops::Range};
+use std::cmp::PartialEq;
 use wgpu::{
     BufferAddress, FilterMode, MipmapFilterMode, VertexAttribute, VertexBufferLayout,
     util::DeviceExt,
 };
+use dropbear_utils::Dirty;
 
 // note to self: do not derive clone otherwise it wil take too much memory
 // #[derive(Clone)]
@@ -49,28 +51,39 @@ pub struct Mesh {
     pub morph_default_weights: Vec<f32>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, PartialEq)]
 pub struct Material {
     pub name: String,
+    pub texture_tag: Option<String>,
+
     pub diffuse_texture: Handle<Texture>,
-    pub normal_texture: Option<Handle<Texture>>,
-    pub emissive_texture: Option<Handle<Texture>>,
+    pub base_colour: [f32; 4],
+
     pub metallic_roughness_texture: Option<Handle<Texture>>,
-    pub occlusion_texture: Option<Handle<Texture>>,
-    pub tint: [f32; 4],
-    pub emissive_factor: [f32; 3],
     pub metallic_factor: f32,
     pub roughness_factor: f32,
-    pub alpha_mode: AlphaMode,
-    pub alpha_cutoff: Option<f32>,
-    pub double_sided: bool,
-    pub occlusion_strength: f32,
+
+    pub normal_texture: Option<Handle<Texture>>,
     pub normal_scale: f32,
+
+    pub emissive_texture: Option<Handle<Texture>>,
+    pub emissive_factor: [f32; 3],
+    pub emissive_strength: f32,
+
+    pub occlusion_texture: Option<Handle<Texture>>,
+    pub occlusion_strength: f32,
+
     pub uv_tiling: [f32; 2],
-    pub tint_buffer: UniformBuffer<MaterialUniform>,
-    pub bind_group: wgpu::BindGroup,
-    pub texture_tag: Option<String>,
+    pub uv_offset: [f32; 2],
     pub wrap_mode: TextureWrapMode,
+
+    pub alpha_mode: AlphaMode,
+    pub alpha_cutoff: Option<f32>,
+
+    pub uniform: UniformBuffer<MaterialUniform>,
+    pub bind_group: wgpu::BindGroup,
+
+    // pub double_sided: bool, // not sure as to why its here
 }
 
 #[derive(
@@ -86,11 +99,12 @@ pub struct Material {
     rkyv::Deserialize,
     Default,
 )]
+#[repr(u32)]
 pub enum AlphaMode {
     #[default]
     Opaque = 1,
-    Mask,
-    Blend,
+    Mask   = 2,
+    Blend  = 3,
 }
 
 impl Into<AlphaMode> for gltf::material::AlphaMode {
@@ -332,22 +346,32 @@ impl Material {
         let name = name.into();
 
         let uv_tiling = [1.0, 1.0];
+
         let uniform = MaterialUniform {
             base_colour: tint,
-            emissive: [0.0, 0.0, 0.0],
-            emissive_strength: 1.0,
-            metallic: 1.0,
-            roughness: 1.0,
+
+            metallic_factor: 1.0,
+            roughness_factor: 1.0,
+
             normal_scale: 1.0,
+
+            _padding: 0.0,
+            emissive_factor: [0.0, 0.0, 0.0],
+            emissive_strength: 1.0,
+
             occlusion_strength: 1.0,
-            alpha_cutoff: 0.5,
+
+            _padding2: 0.0,
             uv_tiling,
+            uv_offset: [0.0; 2],
+
+            alpha_cutoff: 0.5,
+            alpha_mode: AlphaMode::default() as u32,
+
             has_normal_texture: normal_texture.is_some() 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,
-            _pad: 0,
         };
 
         let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
@@ -367,17 +391,18 @@ impl Material {
             name,
             diffuse_texture,
             normal_texture,
-            tint,
+            base_colour: tint,
             emissive_factor: [0.0, 0.0, 0.0],
+            emissive_strength: 1.0,
             metallic_factor: 1.0,
             roughness_factor: 1.0,
             alpha_mode: AlphaMode::Opaque,
             alpha_cutoff: None,
-            double_sided: false,
             occlusion_strength: 1.0,
             normal_scale: 1.0,
             uv_tiling,
-            tint_buffer,
+            uv_offset: [0.0; 2],
+            uniform: tint_buffer,
             bind_group,
             texture_tag,
             wrap_mode: TextureWrapMode::Repeat,
@@ -387,26 +412,28 @@ impl Material {
         }
     }
 
-    pub fn sync_uniform(&self, graphics: &SharedGraphicsContext) {
+    pub fn sync_uniform(&self, graphics: &SharedGraphicsContext) {        
         let uniform = MaterialUniform {
-            base_colour: self.tint,
-            emissive: self.emissive_factor,
-            emissive_strength: 1.0,
-            metallic: self.metallic_factor,
-            roughness: self.roughness_factor,
+            base_colour: self.base_colour,
+            emissive_factor: self.emissive_factor,
+            emissive_strength: self.emissive_strength,
+            metallic_factor: self.metallic_factor,
+            roughness_factor: self.roughness_factor,
             normal_scale: self.normal_scale,
             occlusion_strength: self.occlusion_strength,
             alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5),
+            alpha_mode: self.alpha_mode as u32,
             uv_tiling: self.uv_tiling,
+            uv_offset: self.uv_offset,
+            has_normal_texture: self.normal_texture.is_some() 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,
-            has_normal_texture: self.normal_texture.is_some() as u32,
-            _pad: 0,
+            _padding: 0.0,
+            _padding2: 0.0,
         };
 
-        self.tint_buffer.write(&graphics.queue, &uniform);
+        self.uniform.write(&graphics.queue, &uniform);
     }
 
     pub fn rebuild_bind_group(
@@ -417,7 +444,7 @@ impl Material {
         self.bind_group = Self::build_bind_group(
             registry,
             graphics,
-            &self.tint_buffer,
+            &self.uniform,
             self.diffuse_texture,
             self.normal_texture,
             self.emissive_texture,
@@ -607,7 +634,6 @@ struct GLTFMaterialInformation {
     roughness_factor: f32,
     alpha_mode: gltf::material::AlphaMode,
     alpha_cutoff: Option<f32>,
-    double_sided: bool,
     occlusion_strength: f32,
     normal_scale: f32,
 }
@@ -625,7 +651,6 @@ struct ProcessedMaterialTextures {
     roughness_factor: f32,
     alpha_mode: gltf::material::AlphaMode,
     alpha_cutoff: Option<f32>,
-    double_sided: bool,
     occlusion_strength: f32,
     normal_scale: f32,
 }
@@ -736,7 +761,6 @@ impl Model {
             let roughness_factor = pbr.roughness_factor();
             let alpha_mode = material.alpha_mode();
             let alpha_cutoff = material.alpha_cutoff();
-            let double_sided = material.double_sided();
             let occlusion_strength = occlusion_texture
                 .as_ref()
                 .map(|info| info.strength())
@@ -759,7 +783,6 @@ impl Model {
                 roughness_factor,
                 alpha_mode,
                 alpha_cutoff,
-                double_sided,
                 occlusion_strength,
                 normal_scale,
             });
@@ -779,7 +802,6 @@ impl Model {
                 roughness_factor: 1.0,
                 alpha_mode: gltf::material::AlphaMode::Opaque,
                 alpha_cutoff: None,
-                double_sided: false,
                 occlusion_strength: 1.0,
                 normal_scale: 1.0,
             });
@@ -1267,7 +1289,6 @@ impl Model {
                 let roughness_factor = material_info.roughness_factor;
                 let alpha_mode = material_info.alpha_mode;
                 let alpha_cutoff = material_info.alpha_cutoff;
-                let double_sided = material_info.double_sided;
                 let occlusion_strength = material_info.occlusion_strength;
                 let normal_scale = material_info.normal_scale;
 
@@ -1284,7 +1305,6 @@ impl Model {
                     roughness_factor,
                     alpha_mode,
                     alpha_cutoff,
-                    double_sided,
                     occlusion_strength,
                     normal_scale,
                 }
@@ -1363,7 +1383,6 @@ impl Model {
             material.roughness_factor = processed.roughness_factor;
             material.alpha_mode = processed.alpha_mode.into();
             material.alpha_cutoff = processed.alpha_cutoff;
-            material.double_sided = processed.double_sided;
             material.occlusion_strength = processed.occlusion_strength;
             material.normal_scale = processed.normal_scale;
             material.sync_uniform(&graphics);
@@ -1824,21 +1843,32 @@ impl Hash for ModelVertex {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct MaterialUniform {
-    pub base_colour: [f32; 4], // colour
-    pub emissive: [f32; 3],
-    pub emissive_strength: f32,
-    pub metallic: f32,
-    pub roughness: f32,
+    pub base_colour: [f32; 4],
+
+    pub metallic_factor: f32,
+    pub roughness_factor: f32,
+
     pub normal_scale: f32,
+
+    pub _padding: f32, // 4 byte padding to align emissive_factor to offset 32
+
+    pub emissive_factor: [f32; 3],
+    pub emissive_strength: f32,
+
     pub occlusion_strength: f32,
-    pub alpha_cutoff: f32,
-    pub _pad: u32,
+
+    pub _padding2: f32, // 4 byte padding to align uv_tiling to offset 56
+
     pub uv_tiling: [f32; 2],
+    pub uv_offset: [f32; 2],
+
+    pub alpha_cutoff: f32,
+    pub alpha_mode: u32,
+
     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/builder.rs b/crates/dropbear-engine/src/pipelines/builder.rs
new file mode 100644
index 0000000..652b55e
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/builder.rs
@@ -0,0 +1,261 @@
+use std::num::NonZeroU32;
+
+use anyhow::*;
+use crate::model::Vertex;
+
+pub struct RenderPipelineBuilder<'a> {
+    layout: Option<&'a wgpu::PipelineLayout>,
+    vertex_shader: Option<wgpu::ShaderModuleDescriptor<'a>>,
+    fragment_shader: Option<wgpu::ShaderModuleDescriptor<'a>>,
+    front_face: wgpu::FrontFace,
+    cull_mode: Option<wgpu::Face>,
+    depth_bias: i32,
+    depth_bias_slope_scale: f32,
+    depth_bias_clamp: f32,
+    primitive_topology: wgpu::PrimitiveTopology,
+    color_states: Vec<Option<wgpu::ColorTargetState>>,
+    depth_stencil: Option<wgpu::DepthStencilState>,
+    index_format: wgpu::IndexFormat,
+    vertex_buffers: Vec<wgpu::VertexBufferLayout<'a>>,
+    sample_count: u32,
+    sample_mask: u64,
+    alpha_to_coverage_enabled: bool,
+    multiview_mask: Option<NonZeroU32>,
+    maybe_fragment_entry_point: Option<&'a str>,
+    maybe_vertex_entry_point: Option<&'a str>,
+}
+
+impl<'a> RenderPipelineBuilder<'a> {
+    pub fn new() -> Self {
+        Self {
+            layout: None,
+            vertex_shader: None,
+            fragment_shader: None,
+            front_face: wgpu::FrontFace::Ccw,
+            cull_mode: None,
+            depth_bias: 0,
+            depth_bias_slope_scale: 0.0,
+            depth_bias_clamp: 0.0,
+            primitive_topology: wgpu::PrimitiveTopology::TriangleList,
+            color_states: Vec::new(),
+            depth_stencil: None,
+            index_format: wgpu::IndexFormat::Uint32,
+            vertex_buffers: Vec::new(),
+            sample_count: 1,
+            sample_mask: !0,
+            alpha_to_coverage_enabled: false,
+            multiview_mask: None,
+            maybe_vertex_entry_point: None,
+            maybe_fragment_entry_point: None,
+        }
+    }
+
+    pub fn layout(&mut self, layout: &'a wgpu::PipelineLayout) -> &mut Self {
+        self.layout = Some(layout);
+        self
+    }
+
+    pub fn vertex_shader(&mut self, src: wgpu::ShaderModuleDescriptor<'a>) -> &mut Self {
+        self.vertex_shader = Some(src);
+        self
+    }
+
+    pub fn vertex_entry_point(&mut self, entry_point: &'a str) -> &mut Self {
+        self.maybe_vertex_entry_point = Some(entry_point);
+        self
+    }
+
+    pub fn fragment_shader(&mut self, src: wgpu::ShaderModuleDescriptor<'a>) -> &mut Self {
+        self.fragment_shader = Some(src);
+        self
+    }
+
+    pub fn fragment_entry_point(&mut self, entry_point: &'a str) -> &mut Self {
+        self.maybe_fragment_entry_point = Some(entry_point);
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn front_face(&mut self, ff: wgpu::FrontFace) -> &mut Self {
+        self.front_face = ff;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn cull_mode(&mut self, cm: Option<wgpu::Face>) -> &mut Self {
+        self.cull_mode = cm;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn depth_bias(&mut self, db: i32) -> &mut Self {
+        self.depth_bias = db;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn depth_bias_slope_scale(&mut self, dbss: f32) -> &mut Self {
+        self.depth_bias_slope_scale = dbss;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn depth_bias_clamp(&mut self, dbc: f32) -> &mut Self {
+        self.depth_bias_clamp = dbc;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn primitive_topology(&mut self, pt: wgpu::PrimitiveTopology) -> &mut Self {
+        self.primitive_topology = pt;
+        self
+    }
+
+    pub fn color_state(&mut self, cs: wgpu::ColorTargetState) -> &mut Self {
+        self.color_states.push(Some(cs));
+        self
+    }
+
+    /// Helper method for [RenderPipelineBuilder::color_state]
+    pub fn color_solid(&mut self, format: wgpu::TextureFormat) -> &mut Self {
+        self.color_state(wgpu::ColorTargetState {
+            format,
+            blend: None,
+            write_mask: wgpu::ColorWrites::ALL,
+        })
+    }
+
+    pub fn depth_stencil(&mut self, dss: wgpu::DepthStencilState) -> &mut Self {
+        self.depth_stencil = Some(dss);
+        self
+    }
+
+    /// Helper method for [RenderPipelineBuilder::depth_stencil]
+    pub fn depth_no_stencil(
+        &mut self,
+        format: wgpu::TextureFormat,
+        depth_write_enabled: bool,
+        depth_compare: wgpu::CompareFunction,
+    ) -> &mut Self {
+        self.depth_stencil(wgpu::DepthStencilState {
+            format,
+            depth_write_enabled: Some(depth_write_enabled),
+            depth_compare: Some(depth_compare),
+            stencil: Default::default(),
+            bias: wgpu::DepthBiasState::default(),
+        })
+    }
+
+    /// Helper method for [RenderPipelineBuilder::depth_no_stencil]
+    pub fn depth_format(&mut self, format: wgpu::TextureFormat) -> &mut Self {
+        self.depth_no_stencil(format, true, wgpu::CompareFunction::Less)
+    }
+
+    #[allow(dead_code)]
+    pub fn index_format(&mut self, ifmt: wgpu::IndexFormat) -> &mut Self {
+        self.index_format = ifmt;
+        self
+    }
+
+    pub fn vertex_buffer<V: Vertex>(&mut self) -> &mut Self {
+        self.vertex_buffers.push(V::desc());
+        self
+    }
+
+    pub fn vertex_buffer_desc(&mut self, vb: wgpu::VertexBufferLayout<'a>) -> &mut Self {
+        self.vertex_buffers.push(vb);
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn sample_count(&mut self, sc: u32) -> &mut Self {
+        self.sample_count = sc;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn sample_mask(&mut self, sm: u64) -> &mut Self {
+        self.sample_mask = sm;
+        self
+    }
+
+    #[allow(dead_code)]
+    pub fn alpha_to_coverage_enabled(&mut self, atce: bool) -> &mut Self {
+        self.alpha_to_coverage_enabled = atce;
+        self
+    }
+
+    pub fn multiview(&mut self, value: Option<NonZeroU32>) -> &mut Self {
+        self.multiview_mask = value;
+        self
+    }
+
+    pub fn build(&mut self, device: &wgpu::Device) -> Result<wgpu::RenderPipeline> {
+        let layout = self.layout.clone();
+
+        // Render pipelines always have a vertex shader, but due
+        // to the way the builder pattern works, we can't
+        // guarantee that the user will specify one, so we'll
+        // just return an error if they forgot.
+        //
+        // We could supply a default one, but a "default" vertex
+        // could take on many forms. An error is much more
+        // explicit.
+        if self.vertex_shader.is_none() {
+            bail!("No vertex shader supplied!")
+        }
+        let vs = create_shader_module(
+            device,
+            self.vertex_shader
+                .take()
+                .context("Please include a vertex shader")?,
+        );
+
+        let frag_module = self
+            .fragment_shader
+            .clone()
+            .map(|src| create_shader_module(device, src));
+        let frag_state = frag_module.as_ref().map(|module| wgpu::FragmentState {
+            module,
+            entry_point: self.maybe_fragment_entry_point,
+            compilation_options: Default::default(),
+            targets: &self.color_states,
+        });
+
+        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("Render Pipeline"),
+            layout,
+            vertex: wgpu::VertexState {
+                module: &vs,
+                entry_point: self.maybe_vertex_entry_point,
+                buffers: &self.vertex_buffers,
+                compilation_options: Default::default(),
+            },
+            fragment: frag_state,
+            primitive: wgpu::PrimitiveState {
+                topology: self.primitive_topology,
+                front_face: self.front_face,
+                cull_mode: self.cull_mode,
+                strip_index_format: None,
+                polygon_mode: wgpu::PolygonMode::Fill,
+                ..Default::default()
+            },
+            depth_stencil: self.depth_stencil.clone(),
+            multisample: wgpu::MultisampleState {
+                count: self.sample_count,
+                mask: self.sample_mask,
+                alpha_to_coverage_enabled: self.alpha_to_coverage_enabled,
+            },
+            multiview_mask: self.multiview_mask,
+            cache: None,
+        });
+        Ok(pipeline)
+    }
+}
+
+fn create_shader_module(
+    device: &wgpu::Device,
+    spirv: wgpu::ShaderModuleDescriptor,
+) -> wgpu::ShaderModule {
+    device.create_shader_module(spirv)
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/mod.rs b/crates/dropbear-engine/src/pipelines/mod.rs
index a58249f..ec90a9a 100644
--- a/crates/dropbear-engine/src/pipelines/mod.rs
+++ b/crates/dropbear-engine/src/pipelines/mod.rs
@@ -7,6 +7,7 @@ pub mod hdr;
 pub mod light_cube;
 pub mod shader;
 pub mod animation;
+pub mod builder;
 
 pub use globals::{Globals, GlobalsUniform};
 
diff --git a/crates/dropbear-engine/src/scene.rs b/crates/dropbear-engine/src/scene.rs
index 943d613..be1e590 100644
--- a/crates/dropbear-engine/src/scene.rs
+++ b/crates/dropbear-engine/src/scene.rs
@@ -10,12 +10,13 @@ use crate::multisampling::AntiAliasingMode;
 use crate::{WindowData, graphics::SharedGraphicsContext, input};
 use parking_lot::RwLock;
 use std::{collections::HashMap, rc::Rc, sync::Arc};
+use egui::Ui;
 
 pub trait Scene {
-    fn load(&mut self, graphics: Arc<SharedGraphicsContext>);
-    fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>);
-    fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>);
-    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>);
+    fn load(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
+    fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
+    fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
+    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
     fn exit(&mut self, event_loop: &ActiveEventLoop);
     fn handle_event(&mut self, _event: &WindowEvent) {}
     /// By far a mess of a trait however it works.
@@ -95,6 +96,7 @@ impl Manager {
         dt: f32,
         graphics: Arc<SharedGraphicsContext>,
         event_loop: &ActiveEventLoop,
+        ui: &mut Ui,
     ) -> Vec<SceneCommand> {
         puffin::profile_function!();
         // transition scene
@@ -110,7 +112,7 @@ impl Manager {
             if let Some(scene) = self.scenes.get_mut(&next_scene_name) {
                 {
                     puffin::profile_scope!("load new scene", &next_scene_name);
-                    scene.write().load(graphics.clone());
+                    scene.write().load(graphics.clone(), ui);
                 }
             }
             self.current_scene = Some(next_scene_name);
@@ -122,7 +124,7 @@ impl Manager {
         {
             {
                 puffin::profile_scope!("update new scene", &scene_name);
-                scene.write().update(dt, graphics.clone());
+                scene.write().update(dt, graphics.clone(), ui);
             }
             let command = scene.write().run_command();
             match command {
@@ -133,7 +135,7 @@ impl Manager {
                             if let Some(scene) = self.scenes.get_mut(current) {
                                 puffin::profile_scope!("reload the scene", &current);
                                 scene.write().exit(event_loop);
-                                scene.write().load(graphics.clone());
+                                scene.write().load(graphics.clone(), ui);
 
                                 log::debug!("Reloaded scene: {}", current);
                             }
@@ -162,23 +164,23 @@ impl Manager {
         Vec::new()
     }
 
-    pub fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>) {
+    pub fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui) {
         puffin::profile_function!();
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
             puffin::profile_scope!("physics-update the scene", &scene_name);
-            scene.write().physics_update(dt, graphics.clone())
+            scene.write().physics_update(dt, graphics.clone(), ui)
         }
     }
 
-    pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
+    pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui) {
         puffin::profile_function!();
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
             puffin::profile_scope!("render the scene", &scene_name);
-            scene.write().render(graphics.clone())
+            scene.write().render(graphics.clone(), ui)
         }
     }
 
diff --git a/crates/dropbear-engine/src/shader.rs b/crates/dropbear-engine/src/shader.rs
index cf3b660..0c3f6ac 100644
--- a/crates/dropbear-engine/src/shader.rs
+++ b/crates/dropbear-engine/src/shader.rs
@@ -54,8 +54,6 @@ impl Shader {
 
         log::debug!("Created new shaders under the label: {:?}", label);
 
-        CompiledSlangShader::from_bytes("light cube", slank::include_slang!("light_cube"));
-
         Self {
             label: match label {
                 Some(label) => label.into(),
diff --git a/crates/dropbear-engine/src/shaders/common/light.wesl b/crates/dropbear-engine/src/shaders/common/light.wesl
index e69de29..129eb23 100644
--- a/crates/dropbear-engine/src/shaders/common/light.wesl
+++ b/crates/dropbear-engine/src/shaders/common/light.wesl
@@ -0,0 +1,109 @@
+import super::super::shader::u_globals;
+
+@group(0) @binding(2) var<storage, read> s_light_array: array<Light>;
+
+struct Light {
+    position:  vec4<f32>,
+    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
+    color:     vec4<f32>, // r, g, b, light_type (0=directional, 1=point, 2=spot)
+    constant:  f32,
+    lin:       f32,
+    quadratic: f32,
+    cutoff:    f32,   // inner cutoff (cos of angle)
+}
+
+const LIGHT_DIRECTIONAL: u32 = 0u;
+const LIGHT_POINT: u32       = 1u;
+const LIGHT_SPOT: u32        = 2u;
+
+fn diffuse(
+    light_dir:    vec3<f32>,
+    normal:       vec3<f32>,
+    light_color:  vec3<f32>,
+    object_color: vec3<f32>,
+) -> vec3<f32> {
+    let diff = max(dot(normal, light_dir), 0.0);
+    return diff * light_color * object_color;
+}
+
+// Blinn-Phong specular
+fn specular(
+    light_dir:   vec3<f32>,
+    normal:      vec3<f32>,
+    view_dir:    vec3<f32>,
+    light_color: vec3<f32>,
+    shininess:   f32,
+) -> vec3<f32> {
+    let halfway  = normalize(light_dir + view_dir);
+    let spec     = pow(max(dot(normal, halfway), 0.0), shininess);
+    return light_color * spec;
+}
+
+fn attenuation(light: Light, world_pos: vec3<f32>) -> f32 {
+    let d = length(light.position.xyz - world_pos);
+    return 1.0 / (light.constant + light.lin * d + light.quadratic * d * d);
+}
+
+fn calculate_light(
+    light:        Light,
+    world_pos:    vec3<f32>,
+    normal:       vec3<f32>,   // normalised world-space surface normal
+    view_dir:     vec3<f32>,   // normalised direction from fragment to camera
+    object_color: vec3<f32>,
+    shininess:    f32,
+    spec_scale:   f32,
+) -> vec3<f32> {
+    let light_type  = u32(light.color.w);
+    let light_color = light.color.xyz;
+
+    var light_dir: vec3<f32>;
+    var atten:     f32 = 1.0;
+    var intensity: f32 = 1.0;
+
+    if light_type == LIGHT_DIRECTIONAL {
+        light_dir = normalize(-light.direction.xyz);
+    } else {
+        // point and spot included
+        light_dir = normalize(light.position.xyz - world_pos);
+        atten     = attenuation(light, world_pos);
+
+        if light_type == LIGHT_SPOT {
+            // smooth edge
+            let theta        = dot(light_dir, normalize(-light.direction.xyz));
+            let inner_cutoff = light.cutoff;
+            let outer_cutoff = light.direction.w;
+            let epsilon      = inner_cutoff - outer_cutoff;
+            intensity        = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
+        }
+    }
+
+    let d = diffuse(light_dir, normal, light_color, object_color) * intensity;
+    let s = specular(light_dir, normal, view_dir, light_color, shininess) * intensity * spec_scale;
+
+    return (d + s) * atten;
+}
+
+
+fn calculate_lighting(
+    world_pos:    vec3<f32>,
+    normal:       vec3<f32>,
+    view_dir:     vec3<f32>,
+    object_color: vec3<f32>,
+    shininess:    f32,
+    spec_scale:   f32,
+) -> vec3<f32> {
+    var result = u_globals.ambient_strength * object_color;
+    let num_lights = u_globals.num_lights;
+    for (var i = 0u; i < num_lights; i++) {
+        result += calculate_light(
+            s_light_array[i],
+            world_pos,
+            normal,
+            view_dir,
+            object_color,
+            shininess,
+            spec_scale,
+        );
+    }
+    return result;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/common/material.wesl b/crates/dropbear-engine/src/shaders/common/material.wesl
index f4b61f2..bcff314 100644
--- a/crates/dropbear-engine/src/shaders/common/material.wesl
+++ b/crates/dropbear-engine/src/shaders/common/material.wesl
@@ -1,20 +1,32 @@
 struct MaterialUniform {
-    base_colour:       vec4<f32>,
-    emissive:          vec3<f32>,
+    base_colour: vec4<f32>,
+
+    metallic: f32,
+    roughness: f32,
+
+    normal_scale: f32,
+
+    emissive_factor: vec3<f32>,
     emissive_strength: f32,
-    metallic:          f32,
-    roughness:         f32,
-    normal_scale:      f32,
+    
     occlusion_strength: f32,
-    alpha_cutoff:      f32,
-    uv_tiling:         vec2<f32>,
 
-    has_normal_texture:   u32,
+    uv_tiling: vec2<f32>,
+    uv_offset: vec2<f32>,
+
+    alpha_cutoff: f32,
+    alpha_mode: u32,
+
+    has_normal_texture: u32,
     has_emissive_texture: u32,
     has_metallic_texture: u32,
     has_occlusion_texture: u32,
 }
 
+const ALPHAMODE_OPAQUE: u32 = 1;
+const ALPHAMODE_MASK: u32 = 2;
+const ALPHAMODE_BLEND: u32 = 3;
+
 @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;
diff --git a/crates/dropbear-engine/src/shaders/mask.wesl b/crates/dropbear-engine/src/shaders/mask.wesl
new file mode 100644
index 0000000..28290a7
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/mask.wesl
@@ -0,0 +1,39 @@
+struct VertexOutput {
+    @builtin(position) clip_position: vec4<f32>,
+    @location(0) uv: vec2<f32>,
+}
+
+@group(0) @binding(0)
+var s_mask: sampler;
+@group(0) @binding(1)
+var t_mask: texture_2d<f32>;
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) in_vertex_index: u32,
+) -> VertexOutput {
+    var out: VertexOutput;
+
+    // draw fullscreen triangle
+    let x = f32((in_vertex_index << 1u) & 2u);
+    let y = f32(in_vertex_index & 2u);
+    out.clip_position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
+    out.uv = vec2<f32>(x, 1.0-y);
+    return out;
+}
+
+@fragment
+fn fs_mask(in: VertexOutput) {
+    let sample = textureSample(t_mask, s_mask, in.uv);
+    // We invert this check so that the mask will render objects in
+    // the center
+    if (sample.a > 0.1) {
+        discard;
+    }
+}
+
+@fragment
+fn fs_colour(in: VertexOutput) -> @location(0) vec4<f32> {
+    let sample = textureSample(t_mask, s_mask, in.uv);
+    return sample;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/shader.wesl b/crates/dropbear-engine/src/shaders/shader.wesl
index 867b04e..4fb0a49 100644
--- a/crates/dropbear-engine/src/shaders/shader.wesl
+++ b/crates/dropbear-engine/src/shaders/shader.wesl
@@ -1,6 +1,7 @@
 import super::common::animation;
 import super::common::material;
 import super::common::environment;
+import super::common::light;
 
 struct Globals {
     num_lights:       u32,
@@ -15,20 +16,10 @@ struct CameraUniform {
     inv_view:  mat4x4<f32>,
 }
 
-struct Light {
-    position:  vec4<f32>,
-    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
-    color:     vec4<f32>, // r, g, b, light_type (0=directional, 1=point, 2=spot)
-    constant:  f32,
-    lin:       f32,
-    quadratic: f32,
-    cutoff:    f32,
-}
 
 // per-frame
 @group(0) @binding(0) var<uniform>       u_globals:     Globals;
 @group(0) @binding(1) var<uniform>       u_camera:      CameraUniform;
-@group(0) @binding(2) var<storage, read> s_light_array: array<Light>;
 
 struct InstanceInput {
     @location(8)  model_matrix_0: vec4<f32>,
@@ -64,7 +55,6 @@ struct VertexOutput {
 };
 
 
-
 @vertex
 fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput {
     let model_matrix = mat4x4<f32>(
@@ -106,7 +96,53 @@ fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput {
 
 @fragment
 fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let uv = in.tex_coords * material::u_material.uv_tiling;
-
-    return textureSample(material::t_diffuse, material::s_diffuse, uv);
+    let uv = in.tex_coords * material::u_material.uv_tiling + material::u_material.uv_offset;
+
+    let albedo = textureSample(material::t_diffuse, material::s_diffuse, uv)
+        * material::u_material.base_colour;
+
+    if material::u_material.alpha_mode == material::ALPHAMODE_MASK && albedo.a < material::u_material.alpha_cutoff {
+        discard;
+    }
+
+    // normal
+    var N = normalize(in.world_normal);
+    if material::u_material.has_normal_texture != 0u {
+        let tbn = mat3x3<f32>(
+            normalize(in.world_tangent),
+            normalize(in.world_bitangent),
+            N,
+        );
+        let n_sample = textureSample(material::t_normal, material::s_normal, uv).xyz;
+        let n_ts     = (n_sample * 2.0 - 1.0) * vec3<f32>(material::u_material.normal_scale, material::u_material.normal_scale, 1.0);
+        N = normalize(tbn * n_ts);
+    }
+
+    // occlusion
+    var occlusion = 1.0;
+    if material::u_material.has_occlusion_texture != 0u {
+        let occ_sample = textureSample(material::t_occlusion, material::s_occlusion, uv).r;
+        occlusion = 1.0 + material::u_material.occlusion_strength * (occ_sample - 1.0);
+    }
+
+    let roughness = material::u_material.roughness;
+    let shininess = max(2.0 / max(roughness * roughness, 0.001) - 2.0, 2.0);
+    let spec_scale = (1.0 - roughness) * (1.0 - roughness);
+
+    let view_dir = normalize(in.world_view_position - in.world_position);
+    let lit = light::calculate_lighting(
+        in.world_position,
+        N,
+        view_dir,
+        albedo.rgb,
+        shininess,
+        spec_scale,
+    ) * occlusion;
+
+    var emissive = material::u_material.emissive_factor * material::u_material.emissive_strength;
+    if material::u_material.has_emissive_texture != 0u {
+        emissive *= textureSample(material::t_emissive, material::s_emissive, uv).rgb;
+    }
+
+    return vec4<f32>(lit + emissive, albedo.a);
 }
\ No newline at end of file
diff --git a/crates/dropbear-utils/src/either.rs b/crates/dropbear-utils/src/either.rs
index 886f359..78096a6 100644
--- a/crates/dropbear-utils/src/either.rs
+++ b/crates/dropbear-utils/src/either.rs
@@ -1,4 +1,4 @@
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
 pub struct Dirty<T> {
     value: T,
     dirty: bool,
@@ -52,8 +52,8 @@ impl<T> Dirty<T> {
         self.dirty = true;
     }
 
-    /// Takes the value and clears the dirty flag if dirty, returning None if clean.
-    pub fn take_if_dirty(&mut self) -> Option<&T> {
+    /// Fetches the value and clears the dirty flag if dirty, returning None if clean.
+    pub fn get_if_dirty(&mut self) -> Option<&T> {
         if self.dirty {
             self.dirty = false;
             Some(&self.value)
diff --git a/crates/eucalyptus-core/src/asset/model.rs b/crates/eucalyptus-core/src/asset/model.rs
index b38684e..02d3c7f 100644
--- a/crates/eucalyptus-core/src/asset/model.rs
+++ b/crates/eucalyptus-core/src/asset/model.rs
@@ -115,7 +115,6 @@ pub struct NMaterial {
     pub metallic_factor: f32,
     pub roughness_factor: f32,
     pub alpha_cutoff: Option<f32>,
-    pub double_sided: bool,
     pub occlusion_strength: f32,
     pub normal_scale: f32,
     pub uv_tiling: NVector2,
@@ -161,7 +160,6 @@ impl ToJObject for NMaterial {
             JValue::Double(self.metallic_factor as f64),
             JValue::Double(self.roughness_factor as f64),
             JValue::Object(&alpha_cutoff),
-            JValue::Bool(self.double_sided),
             JValue::Double(self.occlusion_strength as f64),
             JValue::Double(self.normal_scale as f64),
             JValue::Object(&uv_tiling),
@@ -181,7 +179,6 @@ impl ToJObject for NMaterial {
                     com.dropbear.math.Vector3d,
                     double, double,
                     java.lang.Double,
-                    boolean,
                     double, double,
                     com.dropbear.math.Vector2d,
                     com.dropbear.asset.Texture,
@@ -542,12 +539,11 @@ fn map_material(
         name: material.name.clone(),
         diffuse_texture: material.diffuse_texture.id,
         normal_texture: material.normal_texture.and_then(|v| Some(v.id)),
-        tint: NVector4::from(material.tint),
+        tint: NVector4::from(material.base_colour),
         emissive_factor: NVector3::from(material.emissive_factor),
         metallic_factor: material.metallic_factor,
         roughness_factor: material.roughness_factor,
         alpha_cutoff: material.alpha_cutoff,
-        double_sided: material.double_sided,
         occlusion_strength: material.occlusion_strength,
         normal_scale: material.normal_scale,
         uv_tiling: NVector2::from(material.uv_tiling),
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 02d6cdb..28b0205 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -900,13 +900,12 @@ impl Component for MeshRenderer {
 
             for (label, m) in &ser.texture_override {
                 if let Some(mat) = renderer.material_snapshot.get_mut(&label.clone()) {
-                    mat.tint = m.tint;
+                    mat.base_colour = m.tint;
                     mat.emissive_factor = m.emissive_factor;
                     mat.metallic_factor = m.metallic_factor;
                     mat.roughness_factor = m.roughness_factor;
                     mat.alpha_mode = m.alpha_mode;
                     mat.alpha_cutoff = m.alpha_cutoff;
-                    mat.double_sided = m.double_sided;
                     mat.occlusion_strength = m.occlusion_strength;
                     mat.normal_scale = m.normal_scale;
                     mat.uv_tiling = m.uv_tiling;
@@ -1124,13 +1123,12 @@ impl Component for MeshRenderer {
                 SerializedMaterialCustomisation {
                     label: label.clone(),
                     diffuse_texture,
-                    tint: mat.tint,
+                    tint: mat.base_colour,
                     emissive_factor: mat.emissive_factor,
                     metallic_factor: mat.metallic_factor,
                     roughness_factor: mat.roughness_factor,
                     alpha_mode: mat.alpha_mode,
                     alpha_cutoff: mat.alpha_cutoff,
-                    double_sided: mat.double_sided,
                     occlusion_strength: mat.occlusion_strength,
                     normal_scale: mat.normal_scale,
                     uv_tiling: mat.uv_tiling,
@@ -1506,18 +1504,20 @@ impl InspectableComponent for MeshRenderer {
                                     apply_cuboid(self, size, false);
                                     for (name, saved_mat) in saved_materials {
                                         if let Some(mat) = self.material_snapshot.get_mut(&name) {
-                                            mat.tint = saved_mat.tint;
+                                            mat.base_colour = saved_mat.base_colour;
                                             mat.emissive_factor = saved_mat.emissive_factor;
+                                            mat.emissive_strength = saved_mat.emissive_strength;
                                             mat.metallic_factor = saved_mat.metallic_factor;
                                             mat.roughness_factor = saved_mat.roughness_factor;
                                             mat.alpha_mode = saved_mat.alpha_mode;
                                             mat.alpha_cutoff = saved_mat.alpha_cutoff;
-                                            mat.double_sided = saved_mat.double_sided;
                                             mat.occlusion_strength = saved_mat.occlusion_strength;
                                             mat.normal_scale = saved_mat.normal_scale;
                                             mat.uv_tiling = saved_mat.uv_tiling;
+                                            mat.uv_offset = saved_mat.uv_offset;
                                             mat.wrap_mode = saved_mat.wrap_mode;
                                             mat.texture_tag = saved_mat.texture_tag.clone();
+                                            mat.sync_uniform(&graphics);
                                         }
                                     }
                                 }
@@ -1630,16 +1630,17 @@ impl InspectableComponent for MeshRenderer {
                                                     material.metallic_roughness_texture =
                                                         default.metallic_roughness_texture;
                                                     material.occlusion_texture = default.occlusion_texture;
-                                                    material.tint = default.tint;
+                                                    material.base_colour = default.base_colour;
                                                     material.emissive_factor = default.emissive_factor;
+                                                    material.emissive_strength = default.emissive_strength;
                                                     material.metallic_factor = default.metallic_factor;
                                                     material.roughness_factor = default.roughness_factor;
                                                     material.alpha_mode = default.alpha_mode;
                                                     material.alpha_cutoff = default.alpha_cutoff;
-                                                    material.double_sided = default.double_sided;
                                                     material.occlusion_strength = default.occlusion_strength;
                                                     material.normal_scale = default.normal_scale;
                                                     material.uv_tiling = default.uv_tiling;
+                                                    material.uv_offset = default.uv_offset;
                                                     material.texture_tag = default.texture_tag.clone();
                                                     material.wrap_mode = default.wrap_mode;
                                                     {
@@ -1651,207 +1652,13 @@ impl InspectableComponent for MeshRenderer {
                                             }
                                         });
 
-                                        ui.add_space(4.0);
-                                        ui.label(
-                                            RichText::new("Colors")
-                                                .strong()
-                                                .color(ui.visuals().text_color()),
-                                        );
-                                        Grid::new(format!("material_colors_{}", material_name))
-                                            .num_columns(2)
-                                            .spacing([12.0, 6.0])
-                                            .striped(true)
-                                            .show(ui, |ui| {
-                                                ui.label("Tint");
-                                                let mut tint_rgb =
-                                                    [material.tint[0], material.tint[1], material.tint[2]];
-                                                if egui::color_picker::color_edit_button_rgb(
-                                                    ui,
-                                                    &mut tint_rgb,
-                                                )
-                                                .changed()
-                                                {
-                                                    material.tint[0] = tint_rgb[0];
-                                                    material.tint[1] = tint_rgb[1];
-                                                    material.tint[2] = tint_rgb[2];
-                                                }
-                                                ui.end_row();
-
-                                                ui.label("Emissive");
-                                                let mut emissive_rgb = [
-                                                    material.emissive_factor[0],
-                                                    material.emissive_factor[1],
-                                                    material.emissive_factor[2],
-                                                ];
-                                                if egui::color_picker::color_edit_button_rgb(
-                                                    ui,
-                                                    &mut emissive_rgb,
-                                                )
-                                                .changed()
-                                                {
-                                                    material.emissive_factor[0] = emissive_rgb[0];
-                                                    material.emissive_factor[1] = emissive_rgb[1];
-                                                    material.emissive_factor[2] = emissive_rgb[2];
-                                                }
-                                                ui.end_row();
-                                            });
-
-                                        ui.add_space(6.0);
-                                        ui.label(RichText::new("Surface").strong());
-                                        Grid::new(format!("material_surface_{}", material_name))
-                                            .num_columns(2)
-                                            .spacing([12.0, 6.0])
-                                            .striped(true)
-                                            .show(ui, |ui| {
-                                                ui.label("Metallic");
-                                                ui.add(
-                                                    DragValue::new(&mut material.metallic_factor)
-                                                        .speed(0.01)
-                                                        .range(0.0..=1.0),
-                                                );
-                                                ui.end_row();
-
-                                                ui.label("Roughness");
-                                                ui.add(
-                                                    DragValue::new(&mut material.roughness_factor)
-                                                        .speed(0.01)
-                                                        .range(0.0..=1.0),
-                                                );
-                                                ui.end_row();
-
-                                                ui.label("Occlusion Strength");
-                                                ui.add(
-                                                    DragValue::new(&mut material.occlusion_strength)
-                                                        .speed(0.01)
-                                                        .range(0.0..=1.0),
-                                                );
-                                                ui.end_row();
-
-                                                ui.label("Normal Scale");
-                                                ui.add(
-                                                    DragValue::new(&mut material.normal_scale)
-                                                        .speed(0.01)
-                                                        .range(0.0..=10.0),
-                                                );
-                                                ui.end_row();
-                                            });
+                                        let mut uniform_dirty = false;
 
-                                        ui.add_space(6.0);
-                                        ui.label(RichText::new("Alpha").strong());
-                                        Grid::new(format!("material_alpha_{}", material_name))
-                                            .num_columns(2)
-                                            .spacing([12.0, 6.0])
-                                            .striped(true)
-                                            .show(ui, |ui| {
-                                                ui.label("Alpha Mode");
-                                                egui::ComboBox::from_id_salt(format!(
-                                                    "alpha_mode_{}",
-                                                    material_name
-                                                ))
-                                                .selected_text(match material.alpha_mode {
-                                                    dropbear_engine::model::AlphaMode::Opaque => {
-                                                        "Opaque"
-                                                    }
-                                                    dropbear_engine::model::AlphaMode::Mask => {
-                                                        "Mask"
-                                                    }
-                                                    dropbear_engine::model::AlphaMode::Blend => {
-                                                        "Blend"
-                                                    }
-                                                })
-                                                .show_ui(ui, |ui| {
-                                                    ui.selectable_value(
-                                                        &mut material.alpha_mode,
-                                                        dropbear_engine::model::AlphaMode::Opaque,
-                                                        "Opaque",
-                                                    );
-                                                    ui.selectable_value(
-                                                        &mut material.alpha_mode,
-                                                        dropbear_engine::model::AlphaMode::Mask,
-                                                        "Mask",
-                                                    );
-                                                    ui.selectable_value(
-                                                        &mut material.alpha_mode,
-                                                        dropbear_engine::model::AlphaMode::Blend,
-                                                        "Blend",
-                                                    );
-                                                });
-                                                ui.end_row();
-
-                                                ui.label("Alpha Cutoff");
-                                                let mut cutoff = material.alpha_cutoff.unwrap_or(0.5);
-                                                if ui
-                                                    .add(
-                                                        DragValue::new(&mut cutoff)
-                                                            .speed(0.01)
-                                                            .range(0.0..=1.0),
-                                                    )
-                                                    .changed()
-                                                {
-                                                    material.alpha_cutoff = Some(cutoff);
-                                                }
-                                                ui.end_row();
-
-                                                ui.label("Double Sided");
-                                                ui.checkbox(&mut material.double_sided, "");
-                                                ui.end_row();
-                                            });
-
-                                        ui.add_space(6.0);
-                                        ui.label(RichText::new("UV & Wrap").strong());
-                                        Grid::new(format!("material_uv_{}", material_name))
-                                            .num_columns(2)
-                                            .spacing([12.0, 6.0])
-                                            .striped(true)
-                                            .show(ui, |ui| {
-                                                ui.label("Wrap");
-                                                egui::ComboBox::from_id_salt(format!(
-                                                    "wrap_mode_{}",
-                                                    material_name
-                                                ))
-                                                .selected_text(match material.wrap_mode {
-                                                    dropbear_engine::texture::TextureWrapMode::Repeat => {
-                                                        "Repeat"
-                                                    }
-                                                    dropbear_engine::texture::TextureWrapMode::Clamp => {
-                                                        "Clamp"
-                                                    }
-                                                })
-                                                .show_ui(ui, |ui| {
-                                                    ui.selectable_value(
-                                                        &mut material.wrap_mode,
-                                                        dropbear_engine::texture::TextureWrapMode::Repeat,
-                                                        "Repeat",
-                                                    );
-                                                    ui.selectable_value(
-                                                        &mut material.wrap_mode,
-                                                        dropbear_engine::texture::TextureWrapMode::Clamp,
-                                                        "Clamp",
-                                                    );
-                                                });
-                                                ui.end_row();
-
-                                                ui.label("UV Tiling");
-                                                ui.horizontal(|ui| {
-                                                    ui.add(
-                                                        DragValue::new(&mut material.uv_tiling[0])
-                                                            .speed(0.05)
-                                                            .range(0.01..=10_000.0),
-                                                    );
-                                                    ui.label("x");
-                                                    ui.add(
-                                                        DragValue::new(&mut material.uv_tiling[1])
-                                                            .speed(0.05)
-                                                            .range(0.01..=10_000.0),
-                                                    );
-                                                });
-                                                ui.end_row();
-                                            });
-
-                                        ui.add_space(8.0);
-                                        ui.label(RichText::new("Textures").strong());
+                                        // helpers
                                         let texture_matches =
-                                            |current: Option<Handle<Texture>>, candidate: Option<Handle<Texture>>| {
+                                            |current: Option<Handle<Texture>>,
+                                                candidate: Option<Handle<Texture>>|
+                                            -> bool {
                                                 match (current, candidate) {
                                                     (None, None) => true,
                                                     (Some(cur), Some(def)) => cur == def,
@@ -1877,22 +1684,28 @@ impl InspectableComponent for MeshRenderer {
                                             }
                                             format!("Texture {:016x}", handle.id)
                                         };
-                                        let original_label = |original: Option<Handle<Texture>>| -> String {
-                                            original
-                                                .map(handle_label)
-                                                .unwrap_or_else(|| "Original (None)".to_string())
-                                        };
+                                        let original_label =
+                                            |original: Option<Handle<Texture>>| -> String {
+                                                original
+                                                    .map(handle_label)
+                                                    .unwrap_or_else(|| "Original (None)".to_string())
+                                            };
                                         let texture_combo =
                                             |ui: &mut egui::Ui,
-                                             slot_label: &str,
-                                             slot_id: &str,
-                                             current_texture: Option<Handle<Texture>>,
-                                             original_texture: Option<Handle<Texture>>,
-                                             default_texture: Option<Handle<Texture>>|
-                                             -> Option<Option<Handle<Texture>>> {
+                                                slot_id: &str,
+                                                current_texture: Option<Handle<Texture>>,
+                                                original_texture: Option<Handle<Texture>>,
+                                                default_texture: Option<Handle<Texture>>|
+                                            -> Option<Option<Handle<Texture>>> {
                                                 let mut updated = None;
-                                                let is_original = texture_matches(current_texture, original_texture);
-                                                let is_default = texture_matches(current_texture, default_texture);
+                                                let is_original = texture_matches(
+                                                    current_texture,
+                                                    original_texture,
+                                                );
+                                                let is_default = texture_matches(
+                                                    current_texture,
+                                                    default_texture,
+                                                );
                                                 let selected_text = if is_original {
                                                     original_label(original_texture)
                                                 } else if is_default {
@@ -1902,62 +1715,57 @@ impl InspectableComponent for MeshRenderer {
                                                 } else {
                                                     "None".to_string()
                                                 };
-
-                                                ui.horizontal(|ui| {
-                                                    ui.label(slot_label);
-                                                    ComboBox::from_id_salt(format!(
-                                                        "texture_slot_{}_{}",
-                                                        material_name, slot_id
-                                                    ))
-                                                    .selected_text(selected_text)
-                                                    .show_ui(ui, |ui| {
-                                                        if ui
-                                                            .selectable_label(
-                                                                false,
-                                                                original_label(original_texture),
-                                                            )
-                                                            .clicked()
-                                                        {
-                                                            updated = Some(original_texture);
-                                                        }
-
-                                                        ui.separator();
-
+                                                ComboBox::from_id_salt(format!(
+                                                    "texture_slot_{}_{}",
+                                                    material_name, slot_id
+                                                ))
+                                                .selected_text(selected_text)
+                                                .show_ui(ui, |ui| {
+                                                    if ui
+                                                        .selectable_label(
+                                                            false,
+                                                            original_label(original_texture),
+                                                        )
+                                                        .clicked()
+                                                    {
+                                                        updated = Some(original_texture);
+                                                    }
+                                                    ui.separator();
+                                                    if ui.selectable_label(false, "Default").clicked()
+                                                    {
+                                                        updated = Some(default_texture);
+                                                    }
+                                                    ui.separator();
+                                                    for (handle, label) in &texture_options {
                                                         if ui
-                                                            .selectable_label(false, "Default")
+                                                            .selectable_label(false, label)
                                                             .clicked()
+                                                            && ASSET_REGISTRY
+                                                                .read()
+                                                                .get_texture(*handle)
+                                                                .is_some()
                                                         {
-                                                            updated = Some(default_texture);
-                                                        }
-
-                                                        ui.separator();
-
-                                                        for (handle, label) in &texture_options {
-                                                            if ui
-                                                                .selectable_label(false, label)
-                                                                .clicked()
-                                                            {
-                                                                if let Some(texture) = ASSET_REGISTRY
-                                                                    .read()
-                                                                    .get_texture(*handle)
-                                                                {
-                                                                    let _ = texture;
-                                                                    updated = Some(Some(*handle));
-                                                                }
-                                                            }
+                                                            updated = Some(Some(*handle));
                                                         }
-                                                    });
+                                                    }
                                                 });
-
                                                 updated
                                             };
 
+                                        let (
+                                            default_diffuse,
+                                            default_normal,
+                                            default_emissive,
+                                            default_mr,
+                                            default_occ,
+                                        ) = default_textures.clone();
                                         let original_diffuse =
                                             default_material.as_ref().map(|m| m.diffuse_texture);
                                         let original_normal =
                                             default_material.as_ref().and_then(|m| m.normal_texture);
-                                        let original_emissive =
-                                            default_material.as_ref().and_then(|m| m.emissive_texture);
+                                        let original_emissive = default_material
+                                            .as_ref()
+                                            .and_then(|m| m.emissive_texture);
                                         let original_mr = default_material
                                             .as_ref()
                                             .and_then(|m| m.metallic_roughness_texture);
@@ -1965,90 +1773,480 @@ impl InspectableComponent for MeshRenderer {
                                             .as_ref()
                                             .and_then(|m| m.occlusion_texture);
 
-                                        let (default_diffuse, default_normal, default_emissive, default_mr, default_occ) =
-                                            default_textures.clone();
+                                        CollapsingHeader::new("Diffuse")
+                                            .id_salt(format!(
+                                                "diffuse_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!(
+                                                    "diffuse_grid_{}",
+                                                    material_name
+                                                ))
+                                                .num_columns(2)
+                                                .spacing([12.0, 6.0])
+                                                .striped(true)
+                                                .show(ui, |ui| {
+                                                    ui.label("Texture");
+                                                    if let Some(new_tex) = texture_combo(
+                                                        ui,
+                                                        "diffuse",
+                                                        Some(material.diffuse_texture),
+                                                        original_diffuse,
+                                                        default_diffuse,
+                                                    ) {
+                                                        if let Some(tex) = new_tex {
+                                                            material.diffuse_texture = tex;
+                                                        }
+                                                        let mut registry = ASSET_REGISTRY.write();
+                                                        material.rebuild_bind_group(
+                                                            &mut registry,
+                                                            &graphics,
+                                                        );
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+
+                                                    ui.label("Tint");
+                                                    let mut tint_rgb = [
+                                                        material.base_colour[0],
+                                                        material.base_colour[1],
+                                                        material.base_colour[2],
+                                                    ];
+                                                    if egui::color_picker::color_edit_button_rgb(
+                                                        ui,
+                                                        &mut tint_rgb,
+                                                    )
+                                                    .changed()
+                                                    {
+                                                        uniform_dirty = true;
+                                                    }
+                                                    material.base_colour[0] = tint_rgb[0];
+                                                    material.base_colour[1] = tint_rgb[1];
+                                                    material.base_colour[2] = tint_rgb[2];
+                                                    ui.end_row();
+
+                                                    ui.label("Opacity");
+                                                    if ui
+                                                        .add(
+                                                            DragValue::new(
+                                                                &mut material.base_colour[3],
+                                                            )
+                                                            .speed(0.01)
+                                                            .range(0.0..=1.0),
+                                                        )
+                                                        .changed()
+                                                    {
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+                                                });
+                                            });
 
-                                        if let Some(new_diffuse) = texture_combo(
-                                            ui,
-                                            "Diffuse",
-                                            "diffuse",
-                                            Some(material.diffuse_texture),
-                                            original_diffuse,
-                                            default_diffuse,
-                                        ) {
-                                            if let Some(tex) = new_diffuse {
-                                                material.diffuse_texture = tex;
-                                                {
-                                                    let mut registry = ASSET_REGISTRY.write();
-                                                    material.rebuild_bind_group(&mut registry, &graphics);
-                                                }
-                                                material.sync_uniform(&graphics);
-                                            }
-                                        }
+                                        CollapsingHeader::new("Normal")
+                                            .id_salt(format!(
+                                                "normal_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!(
+                                                    "normal_grid_{}",
+                                                    material_name
+                                                ))
+                                                .num_columns(2)
+                                                .spacing([12.0, 6.0])
+                                                .striped(true)
+                                                .show(ui, |ui| {
+                                                    ui.label("Texture");
+                                                    if let Some(new_tex) = texture_combo(
+                                                        ui,
+                                                        "normal",
+                                                        material.normal_texture,
+                                                        original_normal,
+                                                        default_normal,
+                                                    ) {
+                                                        material.normal_texture = new_tex;
+                                                        let mut registry = ASSET_REGISTRY.write();
+                                                        material.rebuild_bind_group(
+                                                            &mut registry,
+                                                            &graphics,
+                                                        );
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
 
-                                        if let Some(new_normal) = texture_combo(
-                                            ui,
-                                            "Normal",
-                                            "normal",
-                                            material.normal_texture,
-                                            original_normal,
-                                            default_normal,
-                                        ) {
-                                            if let Some(tex) = new_normal {
-                                                material.normal_texture = Some(tex);
-                                                {
-                                                    let mut registry = ASSET_REGISTRY.write();
-                                                    material.rebuild_bind_group(&mut registry, &graphics);
-                                                }
-                                                material.sync_uniform(&graphics);
-                                            }
-                                        }
+                                                    ui.label("Scale");
+                                                    if ui
+                                                        .add(
+                                                            DragValue::new(
+                                                                &mut material.normal_scale,
+                                                            )
+                                                            .speed(0.01)
+                                                            .range(0.0..=10.0),
+                                                        )
+                                                        .changed()
+                                                    {
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+                                                });
+                                            });
 
-                                        if let Some(new_emissive) = texture_combo(
-                                            ui,
-                                            "Emissive",
-                                            "emissive",
-                                            material.emissive_texture,
-                                            original_emissive,
-                                            default_emissive,
-                                        ) {
-                                            material.emissive_texture = new_emissive;
-                                            {
-                                                let mut registry = ASSET_REGISTRY.write();
-                                                material.rebuild_bind_group(&mut registry, &graphics);
-                                            }
-                                            material.sync_uniform(&graphics);
-                                        }
+                                        CollapsingHeader::new("Metallic-Roughness")
+                                            .id_salt(format!(
+                                                "mr_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!("mr_grid_{}", material_name))
+                                                    .num_columns(2)
+                                                    .spacing([12.0, 6.0])
+                                                    .striped(true)
+                                                    .show(ui, |ui| {
+                                                        ui.label("Texture");
+                                                        if let Some(new_tex) = texture_combo(
+                                                            ui,
+                                                            "metal_rough",
+                                                            material.metallic_roughness_texture,
+                                                            original_mr,
+                                                            default_mr,
+                                                        ) {
+                                                            material.metallic_roughness_texture =
+                                                                new_tex;
+                                                            let mut registry =
+                                                                ASSET_REGISTRY.write();
+                                                            material.rebuild_bind_group(
+                                                                &mut registry,
+                                                                &graphics,
+                                                            );
+                                                            uniform_dirty = true;
+                                                        }
+                                                        ui.end_row();
 
-                                        if let Some(new_mr) = texture_combo(
-                                            ui,
-                                            "Metal/Rough",
-                                            "metal_rough",
-                                            material.metallic_roughness_texture,
-                                            original_mr,
-                                            default_mr,
-                                        ) {
-                                            material.metallic_roughness_texture = new_mr;
-                                            {
-                                                let mut registry = ASSET_REGISTRY.write();
-                                                material.rebuild_bind_group(&mut registry, &graphics);
-                                            }
-                                            material.sync_uniform(&graphics);
-                                        }
+                                                        ui.label("Metallic");
+                                                        if ui
+                                                            .add(
+                                                                DragValue::new(
+                                                                    &mut material.metallic_factor,
+                                                                )
+                                                                .speed(0.01)
+                                                                .range(0.0..=1.0),
+                                                            )
+                                                            .changed()
+                                                        {
+                                                            uniform_dirty = true;
+                                                        }
+                                                        ui.end_row();
 
-                                        if let Some(new_occ) = texture_combo(
-                                            ui,
-                                            "Occlusion",
-                                            "occlusion",
-                                            material.occlusion_texture,
-                                            original_occ,
-                                            default_occ,
-                                        ) {
-                                            material.occlusion_texture = new_occ;
-                                            {
-                                                let mut registry = ASSET_REGISTRY.write();
-                                                material.rebuild_bind_group(&mut registry, &graphics);
-                                            }
+                                                        ui.label("Roughness");
+                                                        if ui
+                                                            .add(
+                                                                DragValue::new(
+                                                                    &mut material.roughness_factor,
+                                                                )
+                                                                .speed(0.01)
+                                                                .range(0.0..=1.0),
+                                                            )
+                                                            .changed()
+                                                        {
+                                                            uniform_dirty = true;
+                                                        }
+                                                        ui.end_row();
+                                                    });
+                                            });
+
+                                        CollapsingHeader::new("Emissive")
+                                            .id_salt(format!(
+                                                "emissive_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!(
+                                                    "emissive_grid_{}",
+                                                    material_name
+                                                ))
+                                                .num_columns(2)
+                                                .spacing([12.0, 6.0])
+                                                .striped(true)
+                                                .show(ui, |ui| {
+                                                    ui.label("Texture");
+                                                    if let Some(new_tex) = texture_combo(
+                                                        ui,
+                                                        "emissive",
+                                                        material.emissive_texture,
+                                                        original_emissive,
+                                                        default_emissive,
+                                                    ) {
+                                                        material.emissive_texture = new_tex;
+                                                        let mut registry = ASSET_REGISTRY.write();
+                                                        material.rebuild_bind_group(
+                                                            &mut registry,
+                                                            &graphics,
+                                                        );
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+
+                                                    ui.label("Colour");
+                                                    if egui::color_picker::color_edit_button_rgb(
+                                                        ui,
+                                                        &mut material.emissive_factor,
+                                                    )
+                                                    .changed()
+                                                    {
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+
+                                                    ui.label("Strength");
+                                                    if ui
+                                                        .add(
+                                                            DragValue::new(
+                                                                &mut material.emissive_strength,
+                                                            )
+                                                            .speed(0.01)
+                                                            .range(0.0..=100.0),
+                                                        )
+                                                        .changed()
+                                                    {
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+                                                });
+                                            });
+
+                                        CollapsingHeader::new("Occlusion")
+                                            .id_salt(format!(
+                                                "occlusion_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!(
+                                                    "occlusion_grid_{}",
+                                                    material_name
+                                                ))
+                                                .num_columns(2)
+                                                .spacing([12.0, 6.0])
+                                                .striped(true)
+                                                .show(ui, |ui| {
+                                                    ui.label("Texture");
+                                                    if let Some(new_tex) = texture_combo(
+                                                        ui,
+                                                        "occlusion",
+                                                        material.occlusion_texture,
+                                                        original_occ,
+                                                        default_occ,
+                                                    ) {
+                                                        material.occlusion_texture = new_tex;
+                                                        let mut registry = ASSET_REGISTRY.write();
+                                                        material.rebuild_bind_group(
+                                                            &mut registry,
+                                                            &graphics,
+                                                        );
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+
+                                                    ui.label("Strength");
+                                                    if ui
+                                                        .add(
+                                                            DragValue::new(
+                                                                &mut material.occlusion_strength,
+                                                            )
+                                                            .speed(0.01)
+                                                            .range(0.0..=1.0),
+                                                        )
+                                                        .changed()
+                                                    {
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+                                                });
+                                            });
+
+                                        CollapsingHeader::new("UV")
+                                            .id_salt(format!(
+                                                "uv_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!("uv_grid_{}", material_name))
+                                                    .num_columns(2)
+                                                    .spacing([12.0, 6.0])
+                                                    .striped(true)
+                                                    .show(ui, |ui| {
+                                                        ui.label("Tiling");
+                                                        ui.horizontal(|ui| {
+                                                            if ui
+                                                                .add(
+                                                                    DragValue::new(
+                                                                        &mut material.uv_tiling[0],
+                                                                    )
+                                                                    .speed(0.05)
+                                                                    .range(0.01..=10_000.0),
+                                                                )
+                                                                .changed()
+                                                                | ui.add(
+                                                                    DragValue::new(
+                                                                        &mut material.uv_tiling[1],
+                                                                    )
+                                                                    .speed(0.05)
+                                                                    .range(0.01..=10_000.0),
+                                                                )
+                                                                .changed()
+                                                            {
+                                                                uniform_dirty = true;
+                                                            }
+                                                        });
+                                                        ui.end_row();
+
+                                                        ui.label("Offset");
+                                                        ui.horizontal(|ui| {
+                                                            if ui
+                                                                .add(
+                                                                    DragValue::new(
+                                                                        &mut material.uv_offset[0],
+                                                                    )
+                                                                    .speed(0.005),
+                                                                )
+                                                                .changed()
+                                                                | ui.add(
+                                                                    DragValue::new(
+                                                                        &mut material.uv_offset[1],
+                                                                    )
+                                                                    .speed(0.005),
+                                                                )
+                                                                .changed()
+                                                            {
+                                                                uniform_dirty = true;
+                                                            }
+                                                        });
+                                                        ui.end_row();
+
+                                                        ui.label("Wrap");
+                                                        egui::ComboBox::from_id_salt(format!(
+                                                            "wrap_mode_{}",
+                                                            material_name
+                                                        ))
+                                                        .selected_text(
+                                                            match material.wrap_mode {
+                                                                dropbear_engine::texture::TextureWrapMode::Repeat => "Repeat",
+                                                                dropbear_engine::texture::TextureWrapMode::Clamp => "Clamp",
+                                                            },
+                                                        )
+                                                        .show_ui(ui, |ui| {
+                                                            if ui
+                                                                .selectable_value(
+                                                                    &mut material.wrap_mode,
+                                                                    dropbear_engine::texture::TextureWrapMode::Repeat,
+                                                                    "Repeat",
+                                                                )
+                                                                .changed()
+                                                                | ui.selectable_value(
+                                                                    &mut material.wrap_mode,
+                                                                    dropbear_engine::texture::TextureWrapMode::Clamp,
+                                                                    "Clamp",
+                                                                )
+                                                                .changed()
+                                                            {
+                                                                let mut registry = ASSET_REGISTRY.write();
+                                                                material.rebuild_bind_group(&mut registry, &graphics);
+                                                                uniform_dirty = true;
+                                                            }
+                                                        });
+                                                        ui.end_row();
+                                                    });
+                                            });
+
+                                        CollapsingHeader::new("Alpha")
+                                            .id_salt(format!(
+                                                "alpha_{}_{}",
+                                                material_id,
+                                                entity.to_bits()
+                                            ))
+                                            .default_open(true)
+                                            .show(ui, |ui| {
+                                                Grid::new(format!(
+                                                    "alpha_grid_{}",
+                                                    material_name
+                                                ))
+                                                .num_columns(2)
+                                                .spacing([12.0, 6.0])
+                                                .striped(true)
+                                                .show(ui, |ui| {
+                                                    ui.label("Mode");
+                                                    egui::ComboBox::from_id_salt(format!(
+                                                        "alpha_mode_{}",
+                                                        material_name
+                                                    ))
+                                                    .selected_text(match material.alpha_mode {
+                                                        dropbear_engine::model::AlphaMode::Opaque => {
+                                                            "Opaque"
+                                                        }
+                                                        dropbear_engine::model::AlphaMode::Mask => {
+                                                            "Mask"
+                                                        }
+                                                        dropbear_engine::model::AlphaMode::Blend => {
+                                                            "Blend"
+                                                        }
+                                                    })
+                                                    .show_ui(ui, |ui| {
+                                                        if ui
+                                                            .selectable_value(
+                                                                &mut material.alpha_mode,
+                                                                dropbear_engine::model::AlphaMode::Opaque,
+                                                                "Opaque",
+                                                            )
+                                                            .changed()
+                                                            | ui.selectable_value(
+                                                                &mut material.alpha_mode,
+                                                                dropbear_engine::model::AlphaMode::Mask,
+                                                                "Mask",
+                                                            )
+                                                            .changed()
+                                                            | ui.selectable_value(
+                                                                &mut material.alpha_mode,
+                                                                dropbear_engine::model::AlphaMode::Blend,
+                                                                "Blend",
+                                                            )
+                                                            .changed()
+                                                        {
+                                                            uniform_dirty = true;
+                                                        }
+                                                    });
+                                                    ui.end_row();
+
+                                                    ui.label("Cutoff");
+                                                    let mut cutoff =
+                                                        material.alpha_cutoff.unwrap_or(0.5);
+                                                    if ui
+                                                        .add(
+                                                            DragValue::new(&mut cutoff)
+                                                                .speed(0.01)
+                                                                .range(0.0..=1.0),
+                                                        )
+                                                        .changed()
+                                                    {
+                                                        material.alpha_cutoff = Some(cutoff);
+                                                        uniform_dirty = true;
+                                                    }
+                                                    ui.end_row();
+                                                });
+                                            });
+
+                                        if uniform_dirty {
                                             material.sync_uniform(&graphics);
                                         }
                                     });
diff --git a/crates/eucalyptus-core/src/mesh.rs b/crates/eucalyptus-core/src/mesh.rs
index 453ad62..62c1d48 100644
--- a/crates/eucalyptus-core/src/mesh.rs
+++ b/crates/eucalyptus-core/src/mesh.rs
@@ -235,7 +235,7 @@ fn set_material_tint(
         .get_mut(&material_name)
         .ok_or(DropbearNativeError::InvalidArgument)?;
 
-    material.tint = [r, g, b, a];
+    material.base_colour = [r, g, b, a];
     material.sync_uniform(graphics);
     Ok(())
 }
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index f119852..91984d8 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -14,7 +14,6 @@
 //!     - `-colonly` (invisible collision mesh)
 
 pub mod collider_group;
-pub mod shader;
 
 use crate::component::{
     Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent,
diff --git a/crates/eucalyptus-core/src/physics/collider/shader.rs b/crates/eucalyptus-core/src/physics/collider/shader.rs
deleted file mode 100644
index 265051c..0000000
--- a/crates/eucalyptus-core/src/physics/collider/shader.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-//! Shader code relating to displaying physics collider rendering.
-
-use std::mem::size_of;
-use std::sync::Arc;
-
-use crate::physics::collider::{ColliderShape, WireframeGeometry};
-use dropbear_engine::graphics::SharedGraphicsContext;
-use dropbear_engine::pipelines::DropbearShaderPipeline;
-use dropbear_engine::shader::Shader;
-use dropbear_engine::wgpu::{
-    BlendState, BufferAddress, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
-    DepthStencilState, FragmentState, FrontFace, MultisampleState, PipelineLayout,
-    PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipeline,
-    RenderPipelineDescriptor, StencilState, VertexAttribute, VertexBufferLayout, VertexFormat,
-    VertexState, VertexStepMode,
-};
-use dropbear_engine::{entity::Transform, texture::Texture};
-use glam::Mat4;
-
-pub struct ColliderWireframePipeline {
-    pub shader: Shader,
-    pub pipeline_layout: PipelineLayout,
-    pub pipeline: RenderPipeline,
-}
-
-impl DropbearShaderPipeline for ColliderWireframePipeline {
-    fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
-        let shader = Shader::new(
-            graphics.clone(),
-            include_str!("shaders/collider.wgsl"),
-            Some("collider wireframe shaders"),
-        );
-
-        let pipeline_layout = graphics
-            .device
-            .create_pipeline_layout(&PipelineLayoutDescriptor {
-                label: Some("collider wireframe pipeline layout descriptor"),
-                bind_group_layouts: &[
-                    Some(&graphics.layouts.camera_bind_group_layout), // @group(0)
-                ],
-                immediate_size: 0,
-            });
-
-        let hdr_format = graphics.hdr.read().format();
-        let sample_count: u32 = (*graphics.antialiasing.read()).into();
-        let pipeline = graphics
-            .device
-            .create_render_pipeline(&RenderPipelineDescriptor {
-                label: Some("Collider Wireframe Pipeline"),
-                layout: Some(&pipeline_layout),
-                vertex: VertexState {
-                    module: &shader.module,
-                    entry_point: Some("vs_main"),
-                    buffers: &[
-                        VertexBufferLayout {
-                            array_stride: size_of::<[f32; 3]>() as BufferAddress,
-                            step_mode: VertexStepMode::Vertex,
-                            attributes: &[VertexAttribute {
-                                offset: 0,
-                                shader_location: 0,
-                                format: VertexFormat::Float32x3,
-                            }],
-                        },
-                        ColliderInstanceRaw::desc(),
-                    ],
-                    compilation_options: Default::default(),
-                },
-                fragment: Some(FragmentState {
-                    module: &shader.module,
-                    entry_point: Some("fs_main"),
-                    targets: &[Some(ColorTargetState {
-                        format: hdr_format,
-                        blend: Some(BlendState::ALPHA_BLENDING),
-                        write_mask: ColorWrites::ALL,
-                    })],
-                    compilation_options: Default::default(),
-                }),
-                primitive: PrimitiveState {
-                    topology: PrimitiveTopology::LineList,
-                    strip_index_format: None,
-                    front_face: FrontFace::Ccw,
-                    cull_mode: None,
-                    unclipped_depth: false,
-                    polygon_mode: PolygonMode::Fill,
-                    conservative: false,
-                },
-                depth_stencil: Some(DepthStencilState {
-                    format: Texture::DEPTH_FORMAT,
-                    depth_write_enabled: Some(false),
-                    depth_compare: Some(CompareFunction::Always),
-                    stencil: StencilState::default(),
-                    bias: DepthBiasState::default(),
-                }),
-                multisample: MultisampleState {
-                    count: sample_count,
-                    mask: !0,
-                    alpha_to_coverage_enabled: false,
-                },
-                cache: None,
-                multiview_mask: None,
-            });
-
-        Self {
-            shader,
-            pipeline_layout,
-            pipeline,
-        }
-    }
-
-    fn shader(&self) -> &Shader {
-        &self.shader
-    }
-
-    fn pipeline_layout(&self) -> &PipelineLayout {
-        &self.pipeline_layout
-    }
-
-    fn pipeline(&self) -> &RenderPipeline {
-        &self.pipeline
-    }
-}
-
-#[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
-#[repr(C)]
-pub struct ColliderUniform {
-    model_matrix: [[f32; 4]; 4],
-    color: [f32; 4],
-}
-
-impl ColliderUniform {
-    pub fn new(transform: &Transform, color: [f32; 4]) -> Self {
-        Self {
-            model_matrix: transform.matrix().as_mat4().to_cols_array_2d(),
-            color,
-        }
-    }
-
-    pub fn from_matrix(matrix: glam::Mat4, color: [f32; 4]) -> Self {
-        Self {
-            model_matrix: matrix.to_cols_array_2d(),
-            color,
-        }
-    }
-}
-
-#[repr(C)]
-#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct ColliderInstanceRaw {
-    pub model: [[f32; 4]; 4],
-    pub color: [f32; 4],
-}
-
-impl ColliderInstanceRaw {
-    pub fn from_matrix(matrix: Mat4, color: [f32; 4]) -> Self {
-        Self {
-            model: matrix.to_cols_array_2d(),
-            color,
-        }
-    }
-
-    pub fn desc() -> VertexBufferLayout<'static> {
-        const ATTRIBS: [VertexAttribute; 5] = [
-            VertexAttribute {
-                offset: 0,
-                shader_location: 1,
-                format: VertexFormat::Float32x4,
-            },
-            VertexAttribute {
-                offset: size_of::<[f32; 4]>() as BufferAddress,
-                shader_location: 2,
-                format: VertexFormat::Float32x4,
-            },
-            VertexAttribute {
-                offset: size_of::<[f32; 8]>() as BufferAddress,
-                shader_location: 3,
-                format: VertexFormat::Float32x4,
-            },
-            VertexAttribute {
-                offset: size_of::<[f32; 12]>() as BufferAddress,
-                shader_location: 4,
-                format: VertexFormat::Float32x4,
-            },
-            VertexAttribute {
-                offset: size_of::<[f32; 16]>() as BufferAddress,
-                shader_location: 5,
-                format: VertexFormat::Float32x4,
-            },
-        ];
-
-        VertexBufferLayout {
-            array_stride: size_of::<ColliderInstanceRaw>() as BufferAddress,
-            step_mode: VertexStepMode::Instance,
-            attributes: &ATTRIBS,
-        }
-    }
-}
-
-pub fn create_wireframe_geometry(
-    graphics: Arc<SharedGraphicsContext>,
-    shape: &ColliderShape,
-) -> WireframeGeometry {
-    match shape {
-        ColliderShape::Box { half_extents } => {
-            WireframeGeometry::box_wireframe(graphics, half_extents.to_float_array())
-        }
-        ColliderShape::Sphere { radius } => {
-            WireframeGeometry::sphere_wireframe(graphics, *radius, 16, 16)
-        }
-        ColliderShape::Capsule {
-            half_height,
-            radius,
-        } => WireframeGeometry::capsule_wireframe(graphics, *half_height, *radius, 16),
-        ColliderShape::Cylinder {
-            half_height,
-            radius,
-        } => WireframeGeometry::cylinder_wireframe(graphics, *half_height, *radius, 16),
-        ColliderShape::Cone {
-            half_height,
-            radius,
-        } => WireframeGeometry::cone_wireframe(graphics, *half_height, *radius, 16),
-    }
-}
diff --git a/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl b/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
deleted file mode 100644
index c60a251..0000000
--- a/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
+++ /dev/null
@@ -1,48 +0,0 @@
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view: mat4x4<f32>,
-    view_proj: mat4x4<f32>,
-    inv_proj: mat4x4<f32>,
-    inv_view: mat4x4<f32>,
-};
-
-@group(0) @binding(0)
-var <uniform> camera: CameraUniform;
-
-struct VertexInput {
-    @location(0) position: vec3<f32>,
-    @location(1) model_0: vec4<f32>,
-    @location(2) model_1: vec4<f32>,
-    @location(3) model_2: vec4<f32>,
-    @location(4) model_3: vec4<f32>,
-    @location(5) color: vec4<f32>,
-}
-
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) color: vec4<f32>,
-}
-
-@vertex
-fn vs_main(input: VertexInput) -> VertexOutput {
-    var output: VertexOutput;
-
-    let model = mat4x4<f32>(
-        input.model_0,
-        input.model_1,
-        input.model_2,
-        input.model_3,
-    );
-
-    let world_position = model * vec4<f32>(input.position, 1.0);
-    output.clip_position = camera.view_proj * world_position;
-
-    output.color = input.color;
-
-    return output;
-}
-
-@fragment
-fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
-    return input.color;
-}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/rendering.rs b/crates/eucalyptus-core/src/rendering.rs
index 640e022..71cfdc0 100644
--- a/crates/eucalyptus-core/src/rendering.rs
+++ b/crates/eucalyptus-core/src/rendering.rs
@@ -206,7 +206,7 @@ impl RendererCommon {
                     &graphics.device,
                     instances.len().max(1),
                     wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                    "Runtime Instance Buffer",
+                    &format!("resizable buffer<handle={}>", handle_id),
                 ));
             instance_buffer.write(&graphics.device, &graphics.queue, &instances);
 
diff --git a/crates/eucalyptus-core/src/ser/model.rs b/crates/eucalyptus-core/src/ser/model.rs
index 94e0a00..4560f7c 100644
--- a/crates/eucalyptus-core/src/ser/model.rs
+++ b/crates/eucalyptus-core/src/ser/model.rs
@@ -218,7 +218,6 @@ pub struct EucalyptusMaterial {
     pub roughness_factor: f32,
     pub alpha_mode: AlphaMode,
     pub alpha_cutoff: Option<f32>,
-    pub double_sided: bool,
     pub occlusion_strength: f32,
     pub normal_scale: f32,
     pub uv_tiling: [f32; 2],
@@ -253,13 +252,12 @@ impl From<Material> for EucalyptusMaterial {
             emissive_texture: get_texture(value.emissive_texture),
             metallic_roughness_texture: get_texture(value.metallic_roughness_texture),
             occlusion_texture: get_texture(value.occlusion_texture),
-            tint: value.tint,
+            tint: value.base_colour,
             emissive_factor: value.emissive_factor,
             metallic_factor: value.metallic_factor,
             roughness_factor: value.roughness_factor,
             alpha_mode: value.alpha_mode,
             alpha_cutoff: value.alpha_cutoff,
-            double_sided: value.double_sided,
             occlusion_strength: value.occlusion_strength,
             normal_scale: value.normal_scale,
             uv_tiling: value.uv_tiling,
@@ -379,7 +377,6 @@ impl EucalyptusMaterial {
         material.roughness_factor = self.roughness_factor;
         material.alpha_mode = self.alpha_mode;
         material.alpha_cutoff = self.alpha_cutoff;
-        material.double_sided = self.double_sided;
         material.occlusion_strength = self.occlusion_strength;
         material.normal_scale = self.normal_scale;
         material.uv_tiling = self.uv_tiling;
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 5f9e477..cdfee7d 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -483,7 +483,6 @@ pub struct SerializedMaterialCustomisation {
     pub roughness_factor: f32,
     pub alpha_mode: AlphaMode,
     pub alpha_cutoff: Option<f32>,
-    pub double_sided: bool,
     pub occlusion_strength: f32,
     pub normal_scale: f32,
     pub uv_tiling: [f32; 2],
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index ae66938..912bb1e 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -404,7 +404,6 @@ impl AsFile for ResourceReference {
                         roughness_factor: 1.0,
                         alpha_mode: dropbear_engine::model::AlphaMode::Opaque,
                         alpha_cutoff: None,
-                        double_sided: false,
                         occlusion_strength: 1.0,
                         normal_scale: 1.0,
                         uv_tiling: [1.0, 1.0],
diff --git a/crates/eucalyptus-editor/Cargo.toml b/crates/eucalyptus-editor/Cargo.toml
index 8fa25b2..c2c070f 100644
--- a/crates/eucalyptus-editor/Cargo.toml
+++ b/crates/eucalyptus-editor/Cargo.toml
@@ -59,6 +59,7 @@ downcast-rs.workspace = true
 puffin.workspace = true
 image.workspace = true
 rkyv.workspace = true
+wesl.workspace = true
 
 [target.'cfg(unix)'.dependencies]
 daemonize = "0.5.0"
diff --git a/crates/eucalyptus-editor/src/about.rs b/crates/eucalyptus-editor/src/about.rs
index 4b562d6..10e387a 100644
--- a/crates/eucalyptus-editor/src/about.rs
+++ b/crates/eucalyptus-editor/src/about.rs
@@ -2,7 +2,7 @@
 
 use dropbear_engine::input::{Controller, Keyboard, Mouse};
 use dropbear_engine::scene::{Scene, SceneCommand};
-use egui::CentralPanel;
+use egui::{CentralPanel, Ui};
 use eucalyptus_core::input::InputState;
 use gilrs::{Button, GamepadId};
 use winit::dpi::PhysicalPosition;
@@ -28,7 +28,7 @@ impl AboutWindow {
 }
 
 impl Scene for AboutWindow {
-    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
+    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _ui: &mut Ui,) {
         self.window = Some(graphics.window.id());
     }
 
@@ -36,6 +36,7 @@ impl Scene for AboutWindow {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
@@ -43,10 +44,9 @@ impl Scene for AboutWindow {
         &mut self,
         _dt: f32,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
-        let mut ui = egui::Ui::new(graphics.get_egui_context(), egui::Id::new("about window ui"), egui::UiBuilder::default());
-        
-        CentralPanel::default().show_inside(&mut ui, |ui| {
+        CentralPanel::default().show_inside(ui, |ui| {
             ui.vertical_centered(|ui| {
                 ui.add_space(8.0);
                 ui.heading("eucalyptus editor");
@@ -79,6 +79,7 @@ impl Scene for AboutWindow {
     fn render<'a>(
         &mut self,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
diff --git a/crates/eucalyptus-editor/src/debug/window.rs b/crates/eucalyptus-editor/src/debug/window.rs
index 67fb478..7bb2e96 100644
--- a/crates/eucalyptus-editor/src/debug/window.rs
+++ b/crates/eucalyptus-editor/src/debug/window.rs
@@ -2,7 +2,7 @@
 //!
 //! Can also be technically used as a template for other windowed scenes (as its essentially the basics)
 
-use egui::{CentralPanel, UiBuilder};
+use egui::{CentralPanel, Ui};
 use gilrs::{Button, GamepadId};
 use winit::dpi::PhysicalPosition;
 use winit::event::MouseButton;
@@ -31,7 +31,7 @@ impl DebugWindow {
 }
 
 impl Scene for DebugWindow {
-    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
+    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _ui: &mut Ui,) {
         self.window = Some(graphics.window.id());
     }
 
@@ -39,6 +39,7 @@ impl Scene for DebugWindow {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
@@ -46,13 +47,9 @@ impl Scene for DebugWindow {
         &mut self,
         _dt: f32,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
-        let mut ui = egui::Ui::new(
-            graphics.get_egui_context(),
-            egui::Id::from("egui ui for debug window"),
-            UiBuilder::default(),
-        );
-        CentralPanel::default().show_inside(&mut ui, |ui| {
+        CentralPanel::default().show_inside(ui, |ui| {
             ui.label("Hello Debug Window!");
         });
 
@@ -62,6 +59,7 @@ impl Scene for DebugWindow {
     fn render<'a>(
         &mut self,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 86f17af..55ee0ae 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -1464,7 +1464,7 @@ impl Editor {
         {
             let mut project_path = self.project_path.lock();
             crate::utils::show_new_project_window(
-                &graphics.get_egui_context(),
+                ui.ctx(),
                 &mut self.show_new_project,
                 &mut self.project_name,
                 &mut project_path,
@@ -1480,13 +1480,13 @@ impl Editor {
             self.pending_scene_switch = false;
         }
 
-        self.show_nerd_stats_window(&graphics.get_egui_context());
+        self.show_nerd_stats_window(ui.ctx());
 
         let mut open_flag = self.open_new_scene_window;
         let mut close_requested = false;
         if open_flag {
             egui::Window::new("New Scene").open(&mut open_flag).show(
-                &graphics.get_egui_context(),
+                ui.ctx(),
                 |ui| {
                     ui.vertical(|ui| {
                         ui.label("Name: ");
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index aba803e..43d9d66 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -31,7 +31,7 @@ use winit::{event::WindowEvent, event_loop::ActiveEventLoop, keyboard::KeyCode};
 use eucalyptus_core::rendering::RendererCommon;
 
 impl Scene for Editor {
-    fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
+    fn load(&mut self, graphics: Arc<SharedGraphicsContext>, _ui: &mut Ui) {
         {
             let src_path = {
                 let project = PROJECT.read();
@@ -143,6 +143,7 @@ impl Scene for Editor {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
@@ -150,6 +151,7 @@ impl Scene for Editor {
         &mut self,
         dt: f32,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
         self.dt = dt;
 
@@ -168,7 +170,7 @@ impl Scene for Editor {
         }
 
         if let Some(mut receiver) = self.world_receiver.take() {
-            self.show_project_loading_window(&graphics.get_egui_context());
+            self.show_project_loading_window(ui.ctx());
             if let Ok(loaded_world) = receiver.try_recv() {
                 self.world = Box::new(loaded_world);
                 self.is_world_loaded.mark_project_loaded();
@@ -341,7 +343,7 @@ impl Scene for Editor {
             }
         }
 
-        let _ = self.run_signal(graphics.clone());
+        let _ = self.run_signal(graphics.clone(), ui.ctx());
 
         if let Some(e) = self.previously_selected_entity
             && let Ok(entity) = self.world.query_one::<&mut MeshRenderer>(e).get()
@@ -381,7 +383,7 @@ impl Scene for Editor {
                 .record_stats(dt, self.world.len() as u32);
         }
 
-        let open_ui_editor = graphics.get_egui_context().data_mut(|d| {
+        let open_ui_editor = ui.ctx().data_mut(|d: &mut egui::util::IdTypeMap| {
             d.get_temp::<Option<Entity>>(egui::Id::new("open_ui_editor"))
                 .flatten()
                 .inspect(|_| d.remove::<Option<Entity>>(egui::Id::new("open_ui_editor")))
@@ -448,8 +450,8 @@ impl Scene for Editor {
         self.input_state.mouse_delta = None;
     }
 
-    fn render(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        self.editor_specific_render(&graphics);
+    fn render(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui) {
+        self.editor_specific_render(&graphics, ui);
 
         let hdr = graphics.hdr.read();
         let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder"));
@@ -716,18 +718,12 @@ impl Editor {
         }
     }
 
-    fn editor_specific_render(&mut self, graphics: &Arc<SharedGraphicsContext>) {
+    fn editor_specific_render(&mut self, graphics: &Arc<SharedGraphicsContext>, ui: &mut Ui) {
         self.size = graphics.viewport_texture.size;
         self.texture_id = Some(*graphics.texture_id.clone());
         self.window = Some(graphics.window.clone());
 
-        let mut ui = Ui::new(
-            graphics.get_egui_context(),
-            egui::Id::new("ui"),
-            UiBuilder::default(),
-        );
-
-        self.show_ui(&mut ui, graphics.clone());
-        eucalyptus_core::logging::render(&mut ui);
+        self.show_ui(ui, graphics.clone());
+        eucalyptus_core::logging::render(ui);
     }
 }
diff --git a/crates/eucalyptus-editor/src/editor/settings/editor.rs b/crates/eucalyptus-editor/src/editor/settings/editor.rs
index 5d6599b..526ca45 100644
--- a/crates/eucalyptus-editor/src/editor/settings/editor.rs
+++ b/crates/eucalyptus-editor/src/editor/settings/editor.rs
@@ -5,7 +5,7 @@ use app_dirs2::AppDataType;
 use dropbear_engine::input::{Controller, Keyboard, Mouse};
 use dropbear_engine::multisampling::AntiAliasingMode;
 use dropbear_engine::scene::{Scene, SceneCommand};
-use egui::{CentralPanel, ComboBox, Id, Panel, Slider, SliderClamping};
+use egui::{CentralPanel, ComboBox, Id, Panel, Slider, SliderClamping, Ui};
 use egui_dock::DockState;
 use egui_ltreeview::{Action, NodeBuilder};
 use eucalyptus_core::input::InputState;
@@ -138,7 +138,7 @@ impl EditorSettingsWindow {
 }
 
 impl Scene for EditorSettingsWindow {
-    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
+    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _ui: &mut Ui,) {
         self.window = Some(graphics.window.id());
     }
 
@@ -146,6 +146,7 @@ impl Scene for EditorSettingsWindow {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
@@ -153,10 +154,9 @@ impl Scene for EditorSettingsWindow {
         &mut self,
         _dt: f32,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
-        let mut ui = egui::Ui::new(graphics.get_egui_context(), egui::Id::new("editor settings window ui"), egui::UiBuilder::default());
-
-        CentralPanel::default().show_inside(&mut ui, |ui| {
+        CentralPanel::default().show_inside(ui, |ui| {
             let mut editor = EDITOR_SETTINGS.write();
 
             Panel::left("editor_settings_tree_panel")
@@ -265,6 +265,7 @@ impl Scene for EditorSettingsWindow {
     fn render<'a>(
         &mut self,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/settings/project.rs b/crates/eucalyptus-editor/src/editor/settings/project.rs
index 33dbda3..fd766eb 100644
--- a/crates/eucalyptus-editor/src/editor/settings/project.rs
+++ b/crates/eucalyptus-editor/src/editor/settings/project.rs
@@ -2,7 +2,7 @@
 
 use dropbear_engine::input::{Controller, Keyboard, Mouse};
 use dropbear_engine::scene::{Scene, SceneCommand};
-use egui::{CentralPanel, Color32, Id, RichText, Slider, SliderClamping};
+use egui::{CentralPanel, Color32, Id, RichText, Slider, SliderClamping, Ui};
 use egui_ltreeview::{Action, NodeBuilder};
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::states::PROJECT;
@@ -44,7 +44,7 @@ impl ProjectSettingsWindow {
 }
 
 impl Scene for ProjectSettingsWindow {
-    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
+    fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _ui: &mut Ui,) {
         self.window = Some(graphics.window.id());
     }
 
@@ -52,6 +52,7 @@ impl Scene for ProjectSettingsWindow {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
@@ -59,9 +60,9 @@ impl Scene for ProjectSettingsWindow {
         &mut self,
         _dt: f32,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
-        let mut ui = egui::Ui::new(graphics.get_egui_context(), egui::Id::new("project settings window ui"), egui::UiBuilder::default());
-        CentralPanel::default().show_inside(&mut ui, |ui| {
+        CentralPanel::default().show_inside(ui, |ui| {
             let mut project = PROJECT.write();
 
             egui::Panel::left("project_settings_tree_panel")
@@ -201,6 +202,7 @@ impl Scene for ProjectSettingsWindow {
     fn render<'a>(
         &mut self,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
diff --git a/crates/eucalyptus-editor/src/lib.rs b/crates/eucalyptus-editor/src/lib.rs
index 5f0d5c3..ccf1075 100644
--- a/crates/eucalyptus-editor/src/lib.rs
+++ b/crates/eucalyptus-editor/src/lib.rs
@@ -10,6 +10,7 @@ pub mod signal;
 pub mod spawn;
 pub mod stats;
 pub mod utils;
+// pub mod outline;
 
 use editor::docks::asset_viewer::AssetViewerDock;
 use editor::docks::build_console::BuildConsoleDock;
diff --git a/crates/eucalyptus-editor/src/menu.rs b/crates/eucalyptus-editor/src/menu.rs
index e36301c..16eb418 100644
--- a/crates/eucalyptus-editor/src/menu.rs
+++ b/crates/eucalyptus-editor/src/menu.rs
@@ -6,7 +6,7 @@ use dropbear_engine::{
     input::{Controller, Keyboard, Mouse},
     scene::{Scene, SceneCommand},
 };
-use egui::{self, FontId, Frame, RichText, UiBuilder};
+use egui::{self, FontId, Frame, RichText, Ui, UiBuilder};
 use egui_toast::{ToastOptions, Toasts};
 use eucalyptus_core::config::ProjectConfig;
 use eucalyptus_core::states::PROJECT;
@@ -243,6 +243,7 @@ impl Scene for MainMenu {
     fn load(
         &mut self,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui
     ) {
         log::info!("Loaded main menu scene");
     }
@@ -251,6 +252,7 @@ impl Scene for MainMenu {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
@@ -258,12 +260,14 @@ impl Scene for MainMenu {
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
 
     fn render<'a>(
         &mut self,
         graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
         #[allow(clippy::collapsible_if)]
         if let Some(handle) = self.project_creation_handle.as_ref() {
@@ -303,23 +307,17 @@ impl Scene for MainMenu {
             }
         }
 
-        let mut ui = egui::Ui::new(
-            graphics.get_egui_context(),
-            egui::Id::new("main menu ui"),
-            UiBuilder::default(),
-        );
-
         let screen_size: (f32, f32) = (
             graphics.window.inner_size().width as f32 - 100.0,
             graphics.window.inner_size().height as f32 - 100.0,
         );
-        let egui_ctx = graphics.get_egui_context();
+        let egui_ctx = ui.ctx().clone();
         let mut local_open_project = false;
         let mut local_select_project = false;
 
         egui::CentralPanel::default()
             .frame(Frame::new())
-            .show_inside(&mut ui, |ui| {
+            .show_inside(ui, |ui| {
                 ui.vertical_centered(|ui| {
                     ui.add_space(64.0);
                     ui.label(RichText::new("Eucalyptus").font(FontId::proportional(32.0)));
@@ -512,7 +510,7 @@ impl Scene for MainMenu {
                 });
         }
 
-        self.toast.show(&mut ui);
+        self.toast.show(ui);
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {
diff --git a/crates/eucalyptus-editor/src/outline.rs b/crates/eucalyptus-editor/src/outline.rs
new file mode 100644
index 0000000..703dde0
--- /dev/null
+++ b/crates/eucalyptus-editor/src/outline.rs
@@ -0,0 +1,146 @@
+//! Outline shader
+//!
+//! General idea:
+//! - 1. get that one entity as a sort of map
+//! - 2. run an edge detection, and apply an orange outline for the edge (perhaps a specific width)
+
+use std::sync::Arc;
+use dropbear_engine::graphics::{CommandEncoder, SharedGraphicsContext};
+use dropbear_engine::pipelines::builder::RenderPipelineBuilder;
+use dropbear_engine::pipelines::DropbearShaderPipeline;
+use dropbear_engine::texture::{Texture, TextureBuilder};
+
+pub struct OutlineShader {
+    depth_stencil: Texture,
+    mask_bind_group: wgpu::BindGroup,
+    pub mask_pipeline: wgpu::RenderPipeline,
+}
+
+impl OutlineShader {
+    pub fn new(graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self> {
+        let size = graphics.window.inner_size();
+
+        let depth_stencil_format = wgpu::TextureFormat::Depth24PlusStencil8;
+        let depth_stencil = TextureBuilder::new(&graphics.device)
+            .label("depth stencil")
+            .size(size.width, size.height)
+            .format(depth_stencil_format)
+            .usage(wgpu::TextureUsages::RENDER_ATTACHMENT)
+            .build();
+
+        let source = wesl::Wesl::new("src/shaders")
+            .add_package(&dropbear_engine::shader::code::PACKAGE)
+            .compile(&"dropbear_shaders::mask".parse()?)
+            .inspect_err(|e| {
+                panic!("{e}");
+            })?
+            .to_string();
+
+        let mask_shader = wgpu::ShaderModuleDescriptor {
+            label: Some("mask shader"),
+            source: wgpu::ShaderSource::Wgsl(source.into()),
+        };
+
+        let mask_bind_group_layout = graphics.device
+            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("mask bind group layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                ],
+            });
+
+        let mask_bind_group = graphics.device
+            .create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("mask_bind_group"),
+                layout: &mask_bind_group_layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: wgpu::BindingResource::Sampler(&mask_texture.sampler),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: wgpu::BindingResource::TextureView(&mask_texture.view),
+                    },
+                ],
+            });
+
+        let mask_pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("mask pipeline layout"),
+            bind_group_layouts: &[
+                Some(&mask_bind_group_layout)
+            ],
+            immediate_size: 0,
+        });
+
+        let mask_pipeline = RenderPipelineBuilder::new()
+            .vertex_shader(mask_shader.clone())
+            .fragment_shader(mask_shader.clone())
+            .fragment_entry_point("fs_mask")
+            .cull_mode(Some(wgpu::Face::Back))
+            .depth_stencil(wgpu::DepthStencilState {
+                format: depth_stencil_format,
+                depth_write_enabled: Some(false),
+                depth_compare: Some(wgpu::CompareFunction::Always),
+                stencil: wgpu::StencilState {
+                    write_mask: 0xFF,
+                    read_mask: 0xFF,
+                    front: wgpu::StencilFaceState {
+                        compare: wgpu::CompareFunction::Always,
+                        pass_op: wgpu::StencilOperation::Replace,
+                        ..Default::default()
+                    },
+                    back: wgpu::StencilFaceState::IGNORE,
+                },
+                bias: wgpu::DepthBiasState::default(),
+            })
+            .layout(&mask_pipeline_layout)
+            .build(&graphics.device)?;
+
+        Self {
+            depth_stencil,
+            mask_bind_group: (),
+            mask_pipeline,
+        }
+    }
+
+    pub fn draw(&self, graphics: Arc<SharedGraphicsContext>, encoder: &mut CommandEncoder) {
+        {
+            let mut draw_mask_stencil = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("draw mask stencil"),
+                color_attachments: &[],
+                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &self.depth_stencil.view, // draw onto depth_stencil
+                    depth_ops: None,
+                    stencil_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(0),
+                        store: wgpu::StoreOp::Store,
+                    }),
+                }),
+                timestamp_writes: None,
+                multiview_mask: None,
+                occlusion_query_set: None,
+            });
+
+            draw_mask_stencil.set_stencil_reference(0xFF);
+            draw_mask_stencil.set_pipeline(&self.mask_pipeline);
+            draw_mask_stencil.set_bind_group(0, &self.mask_bind_group, &[]);
+            draw_mask_stencil.draw(0..3, 0..1);
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 72a2690..45a2e84 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -17,11 +17,11 @@ use std::sync::Arc;
 use winit::keyboard::KeyCode;
 
 pub trait SignalController {
-    fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>;
+    fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>, ctx: &egui::Context) -> anyhow::Result<()>;
 }
 
 impl SignalController for Editor {
-    fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> {
+    fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>, ctx: &egui::Context) -> anyhow::Result<()> {
         let mut requeue = vec![];
         while let Some(signal) = self.signal.pop_front() {
             let local_signal: Option<Signal> = None;
@@ -384,7 +384,7 @@ impl SignalController for Editor {
                                 .fixed_size([500.0, 400.0])
                                 .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
                                 .open(&mut window_open)
-                                .show(&graphics.get_egui_context(), |ui| {
+                                .show(ctx, |ui| {
                                     ui.vertical_centered(|ui| {
                                         ui.heading("Gradle Build Progress");
                                         ui.add_space(10.0);
@@ -491,7 +491,7 @@ impl SignalController for Editor {
                                 .fixed_size([700.0, 500.0])
                                 .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
                                 .open(&mut window_open)
-                                .show(&graphics.get_egui_context(), |ui| {
+                                .show(ctx, |ui| {
                                     ui.vertical(|ui| {
                                         ui.heading("Build Failed");
                                         ui.add_space(5.0);
diff --git a/crates/eucalyptus-editor/src/stats.rs b/crates/eucalyptus-editor/src/stats.rs
index 4f0dcea..45b752f 100644
--- a/crates/eucalyptus-editor/src/stats.rs
+++ b/crates/eucalyptus-editor/src/stats.rs
@@ -313,14 +313,10 @@ impl NerdStats {
     }
 
     /// Shows the egui window as a CentralPanel, typically used for another window.
-    pub fn show_window(&mut self, ctx: &Context) {
-        let mut nerd_stats_ui = egui::Ui::new(ctx.clone(), egui::Id::new("nerd-stats ui"), egui::UiBuilder::default());
-
-        egui::CentralPanel::default().show_inside(&mut nerd_stats_ui, |ui| {
+    pub fn show_window(&mut self, ui: &mut Ui) {
+        egui::CentralPanel::default().show_inside(ui, |ui| {
             self.content(ui);
         });
-
-        ctx.request_repaint();
     }
 }
 
@@ -328,26 +324,31 @@ impl Scene for NerdStats {
     fn load(
         &mut self,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
     fn physics_update(
         &mut self,
         _dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
     }
     fn update(
         &mut self,
         dt: f32,
         _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _ui: &mut Ui,
     ) {
         self.record_stats(dt, self.entity_count);
     }
+    
     fn render<'a>(
         &mut self,
-        graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>,
+        ui: &mut Ui,
     ) {
-        self.show_window(&graphics.get_egui_context());
+        self.show_window(ui);
     }
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
 }
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 61ec844..0b72924 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -24,11 +24,12 @@ use hecs::Entity;
 use kino_ui::WidgetTree;
 use kino_ui::rendering::KinoRenderTargetId;
 use std::collections::HashMap;
+use egui::Ui;
 use winit::event::WindowEvent;
 use winit::event_loop::ActiveEventLoop;
 
 impl Scene for PlayMode {
-    fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
+    fn load(&mut self, graphics: Arc<SharedGraphicsContext>, _ui: &mut Ui,) {
         if self.current_scene.is_none() {
             let initial_scene = if let Some(s) = &self.initial_scene {
                 s.clone()
@@ -48,7 +49,7 @@ impl Scene for PlayMode {
         }
     }
 
-    fn physics_update(&mut self, dt: f32, _graphics: Arc<SharedGraphicsContext>) {
+    fn physics_update(&mut self, dt: f32, _graphics: Arc<SharedGraphicsContext>, _ui: &mut Ui,) {
         if self.scripts_ready {
             let _ = self
                 .script_manager
@@ -273,7 +274,7 @@ impl Scene for PlayMode {
         }
     }
 
-    fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>) {
+    fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui,) {
         graphics.future_queue.poll();
         self.poll(graphics.clone());
 
@@ -401,10 +402,8 @@ impl Scene for PlayMode {
             graphics.clone(),
         );
 
-        let mut ui = egui::Ui::new(graphics.get_egui_context(), egui::Id::new("redback-runtime ui"), egui::UiBuilder::default());
-
         #[cfg(feature = "debug")]
-        egui::Panel::top("menu_bar").show_inside(&mut ui, |ui| {
+        egui::Panel::top("menu_bar").show_inside(ui, |ui| {
             egui::MenuBar::new().ui(ui, |ui| {
                 use crate::WindowMode;
                 ui.menu_button("Window", |ui| {
@@ -473,11 +472,10 @@ impl Scene for PlayMode {
             });
         });
 
-        CentralPanel::default().show_inside(&mut ui, |ui| {
+        CentralPanel::default().show_inside(ui, |ui| {
             if let Some(p) = &self.scene_progress {
                 if !p.is_everything_loaded() && p.is_first_scene {
                     ui.centered_and_justified(|ui| {
-                        egui_extras::install_image_loaders(&graphics.get_egui_context());
                         ui.add(
                             egui::Image::new(egui::include_image!(
                                 "../../../resources/eucalyptus-editor.png"
@@ -599,7 +597,7 @@ impl Scene for PlayMode {
         self.input_state.mouse_delta = None;
     }
 
-    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
+    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, _ui: &mut Ui,) {
         let hdr = graphics.hdr.read();
         let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder"));
 
diff --git a/include/dropbear.h b/include/dropbear.h
index 403d67a..8cb0d38 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -317,7 +317,6 @@ typedef struct NMaterial {
     float metallic_factor;
     float roughness_factor;
     const float* alpha_cutoff;
-    bool double_sided;
     float occlusion_strength;
     float normal_scale;
     NVector2 uv_tiling;