kitgit

tirbofish/dropbear · commit

65bc7fccb862c06a9e38cd495931fbe59b46f290

feature: added support for lighting as a storage resource. fix: dealt with some weird "../../../" corruption or bug. wip: redback-runtime runner (euca-runner) bin Unverified

Thribhu K <4tkbytes@pm.me> · 2026-01-18 11:38

view full diff

diff --git a/crates/dropbear-engine/src/buffer.rs b/crates/dropbear-engine/src/buffer.rs
index 6ad4874..27552d0 100644
--- a/crates/dropbear-engine/src/buffer.rs
+++ b/crates/dropbear-engine/src/buffer.rs
@@ -2,8 +2,6 @@
 
 use std::marker::PhantomData;
 
-use wgpu::BufferUsages;
-
 #[repr(C)]
 #[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct Vertex {
@@ -54,7 +52,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> {
         let buffer = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some(label),
             size,
-            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
+            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
             mapped_at_creation: false,
         });
 
@@ -78,6 +76,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> {
     }
 }
 
+/// A wrapper to a [wgpu::Buffer] that stores
 #[derive(Debug, Clone)]
 pub struct StorageBuffer<T> {
     buffer: wgpu::Buffer,
@@ -95,7 +94,7 @@ impl<T: bytemuck::Pod> StorageBuffer<T> {
         let buffer = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some(label),
             size,
-            usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
+            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
             mapped_at_creation: false,
         });
 
@@ -143,13 +142,6 @@ impl<T: bytemuck::Pod> ResizableBuffer<T> {
         }
     }
 
-    pub fn uniform(
-        device: &wgpu::Device,
-        label: &str,
-    ) -> Self {
-        Self::new(device, 1, BufferUsages::UNIFORM | BufferUsages::COPY_DST, label)
-    }
-
     pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) {
         if data.is_empty() {
             return;
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 8cb81c3..b93c290 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -1,10 +1,7 @@
-use crate::shader::Shader;
-use crate::texture::Texture;
 use crate::{BindGroupLayouts, texture};
 use crate::{
     State,
     egui_renderer::EguiRenderer,
-    model::{self, Vertex},
 };
 use dropbear_future_queue::FutureQueue;
 use egui::{Context, TextureId};
@@ -34,6 +31,7 @@ pub struct SharedGraphicsContext {
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
+    pub supports_storage: bool,
 }
 
 impl SharedGraphicsContext {
@@ -74,71 +72,9 @@ impl SharedGraphicsContext {
             texture_id: state.texture_id.clone(),
             surface: state.surface.clone(),
             surface_format: state.surface_format,
+            supports_storage: state.supports_storage,
         }
     }
-
-    pub fn create_render_pipline(
-        &self,
-        shader: &Shader,
-        bind_group_layouts: Vec<&BindGroupLayout>,
-        label: Option<&str>,
-    ) -> RenderPipeline {
-        let render_pipeline_layout =
-            self.device
-                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                    label: Some(label.unwrap_or("Render Pipeline Descriptor")),
-                    bind_group_layouts: bind_group_layouts.as_slice(),
-                    push_constant_ranges: &[],
-                });
-
-        let render_pipeline =
-            self.device
-                .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-                    label: Some(label.unwrap_or("Render Pipeline")),
-                    layout: Some(&render_pipeline_layout),
-                    vertex: wgpu::VertexState {
-                        module: &shader.module,
-                        entry_point: Some("vs_main"),
-                        buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()],
-                        compilation_options: wgpu::PipelineCompilationOptions::default(),
-                    },
-                    fragment: Some(wgpu::FragmentState {
-                        module: &shader.module,
-                        entry_point: Some("fs_main"),
-                        targets: &[Some(wgpu::ColorTargetState {
-                            format: Texture::TEXTURE_FORMAT,
-                            blend: Some(wgpu::BlendState::REPLACE),
-                            write_mask: wgpu::ColorWrites::ALL,
-                        })],
-                        compilation_options: wgpu::PipelineCompilationOptions::default(),
-                    }),
-                    primitive: wgpu::PrimitiveState {
-                        topology: wgpu::PrimitiveTopology::TriangleList,
-                        strip_index_format: None,
-                        front_face: wgpu::FrontFace::Ccw,
-                        cull_mode: Some(wgpu::Face::Back),
-                        polygon_mode: wgpu::PolygonMode::Fill,
-                        unclipped_depth: false,
-                        conservative: false,
-                    },
-                    depth_stencil: Some(wgpu::DepthStencilState {
-                        format: Texture::DEPTH_FORMAT,
-                        depth_write_enabled: true,
-                        depth_compare: CompareFunction::Greater,
-                        stencil: StencilState::default(),
-                        bias: DepthBiasState::default(),
-                    }),
-                    multisample: wgpu::MultisampleState {
-                        count: 1,
-                        mask: !0,
-                        alpha_to_coverage_enabled: false,
-                    },
-                    multiview: None,
-                    cache: None,
-                });
-        log::debug!("Created new render pipeline");
-        render_pipeline
-    }
 }
 
 #[derive(Default, Clone)]
@@ -176,6 +112,19 @@ impl Instance {
     }
 }
 
+/// Maps to
+/// ```wgsl
+/// struct InstanceInput {
+///     @location(5) model_matrix_0: vec4<f32>,
+///     @location(6) model_matrix_1: vec4<f32>,
+///     @location(7) model_matrix_2: vec4<f32>,
+///     @location(8) model_matrix_3: vec4<f32>,
+///
+///     @location(9) normal_matrix_0: vec3<f32>,
+///     @location(10) normal_matrix_1: vec3<f32>,
+///     @location(11) normal_matrix_2: vec3<f32>,
+/// };
+/// ```
 #[repr(C)]
 #[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct InstanceRaw {
@@ -189,38 +138,46 @@ impl InstanceRaw {
             array_stride: size_of::<InstanceRaw>() as BufferAddress,
             step_mode: wgpu::VertexStepMode::Instance,
             attributes: &[
-                // model
+                // model_matrix_0
                 wgpu::VertexAttribute {
                     offset: 0,
                     shader_location: 5,
                     format: wgpu::VertexFormat::Float32x4,
                 },
+                // model_matrix_1
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 4]>() as wgpu::BufferAddress,
                     shader_location: 6,
                     format: wgpu::VertexFormat::Float32x4,
                 },
+                // model_matrix_2
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 8]>() as wgpu::BufferAddress,
                     shader_location: 7,
                     format: wgpu::VertexFormat::Float32x4,
                 },
+                // model_matrix_3
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 12]>() as wgpu::BufferAddress,
                     shader_location: 8,
                     format: wgpu::VertexFormat::Float32x4,
                 },
-                // normal
+
+                // normal_matrix_0
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 16]>() as wgpu::BufferAddress,
                     shader_location: 9,
                     format: wgpu::VertexFormat::Float32x3,
                 },
+
+                // normal_matrix_1
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 19]>() as wgpu::BufferAddress,
                     shader_location: 10,
                     format: wgpu::VertexFormat::Float32x3,
                 },
+
+                // normal_matrix_2
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 22]>() as wgpu::BufferAddress,
                     shader_location: 11,
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index bc3694f..6e85de7 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -16,6 +16,7 @@ pub mod scene;
 pub mod shader;
 pub mod utils;
 pub mod texture;
+pub mod pipelines;
 
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
 pub const PHYSICS_STEP_RATE: u32 = 120;
@@ -39,7 +40,7 @@ use std::sync::OnceLock;
 use std::{fs, sync::Arc, time::{Duration, Instant}};
 use std::collections::HashMap;
 use std::rc::Rc;
-use wgpu::{BindGroupLayout, Device, ExperimentalFeatures, Instance, Queue, Surface, SurfaceConfiguration, SurfaceError, TextureFormat};
+use wgpu::{BindGroupLayout, BindGroupLayoutEntry, BindingType, BufferBindingType, Device, ExperimentalFeatures, Instance, Queue, ShaderStages, Surface, SurfaceConfiguration, SurfaceError, TextureFormat};
 use winit::event::{DeviceEvent, DeviceId};
 use winit::{
     application::ApplicationHandler,
@@ -51,7 +52,7 @@ use winit::{
 
 use crate::camera::Camera;
 use crate::graphics::{FrameGraphicsContext, SharedGraphicsContext};
-use crate::lighting::{Light, LightManager};
+use crate::lighting::{Light};
 use crate::texture::Texture;
 use crate::{egui_renderer::EguiRenderer};
 
@@ -63,6 +64,7 @@ use winit::window::{WindowAttributes, WindowId};
 use crate::scene::Scene;
 
 pub struct BindGroupLayouts {
+    pub shader_globals_bind_group_layout: BindGroupLayout,
     pub texture_bind_layout: BindGroupLayout,
     pub material_tint_bind_layout: BindGroupLayout,
     pub camera_bind_group_layout: BindGroupLayout,
@@ -76,6 +78,7 @@ pub struct State {
     // keep top for drop order
     pub window: Arc<Window>,
     pub instance: Arc<Instance>,
+    pub supports_storage: bool,
 
     pub surface: Arc<Surface<'static>>,
     pub surface_format: TextureFormat,
@@ -96,7 +99,7 @@ pub struct State {
 }
 
 impl State {
-    /// As defined in `shader.wgsl` as 
+    /// As defined in `shaders.wgsl` as
     /// ```
     /// @group(3) @binding(0)
     /// var<uniform> u_material: MaterialUniform;
@@ -146,6 +149,14 @@ impl State {
             })
             .await?;
 
+        let supports_storage_resources = adapter
+            .get_downlevel_capabilities()
+            .flags
+            .contains(wgpu::DownlevelFlags::VERTEX_STORAGE)
+            && device.limits().max_storage_buffers_per_shader_stage > 0;
+        
+        log::debug!("graphics device {} support storage resources", if !supports_storage_resources { "does not" } else { "does" });
+
         if WGPU_BACKEND.get().is_none() {
             let info = adapter.get_info();
             let os_info = os_info::get();
@@ -218,12 +229,6 @@ Hardware:
         let viewport_texture =
             Texture::viewport(&config, &device, Some("viewport texture"));
 
-        let texture_bind_group_layout =
-            device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT);
-
-        let material_tint_bind_group_layout =
-            device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT);
-
         let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new(
             &device,
             config.format,
@@ -238,11 +243,80 @@ Hardware:
             .renderer()
             .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear);
 
+        // shaders/shader.wgsl - @group(1)
         let camera_bind_group_layout = device.create_bind_group_layout(&Camera::CAMERA_BIND_GROUP_LAYOUT);
+        
+        // shader/light.wgsl - @group(1)
         let light_bind_group_layout = device.create_bind_group_layout(&Light::LIGHT_BIND_GROUP_LAYOUT);
-        let light_array_bind_group_layout = device.create_bind_group_layout(&LightManager::LIGHT_ARRAY_BIND_GROUP_LAYOUT);
+
+        // shaders/light.wgsl - @group(1)
         let light_cube_bind_group_layout = device
-            .create_bind_group_layout(&LightManager::LIGHT_CUBE_BIND_GROUP_LAYOUT);
+            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                entries: &[wgpu::BindGroupLayoutEntry {
+                    // @binding(0)
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT),
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                }],
+                label: Some("Per-Light Layout"),
+            });
+        
+        // shaders/shader.wgsl - @group(0)
+        let texture_bind_group_layout =
+            device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT);
+        
+        // shaders/shader.wgsl - @group(2)
+        let light_array_bind_group_layout = device.create_bind_group_layout(
+            &wgpu::BindGroupLayoutDescriptor {
+                entries: &[
+                    // @binding(0)
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Buffer {
+                            ty: if supports_storage_resources {
+                                // s_light_array
+                                wgpu::BufferBindingType::Storage { read_only: true }
+                            } else {
+                                // u_light_array
+                                wgpu::BufferBindingType::Uniform
+                            },
+                            has_dynamic_offset: false,
+                            min_binding_size: None,
+                        },
+                        count: None,
+                    },
+                ],
+                label: Some("Light Array Layout"),
+            }
+        );
+
+        // shaders/shader.wgsl - @group(3)
+        let material_tint_bind_group_layout =
+            device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT);
+
+        // shaders/shader.wgsl - @group(4)
+        let shader_globals_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("shader.wgsl globals bind group layout"),
+            entries: &[
+                BindGroupLayoutEntry {
+                    // @binding(0)
+                    binding: 0,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: BindingType::Buffer {
+                        ty: BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                }
+            ],
+        });
 
         let result = Self {
             surface: Arc::new(surface),
@@ -261,6 +335,7 @@ Hardware:
             physics_accumulator: Duration::ZERO,
             scene_manager: scene::Manager::new(),
             layouts: Arc::new(BindGroupLayouts {
+                shader_globals_bind_group_layout,
                 texture_bind_layout: texture_bind_group_layout,
                 material_tint_bind_layout: material_tint_bind_group_layout,
                 camera_bind_group_layout,
@@ -268,6 +343,7 @@ Hardware:
                 light_array_bind_group_layout,
                 light_cube_bind_group_layout,
             }),
+            supports_storage: supports_storage_resources,
         };
 
         Ok(result)
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index faa7d07..33c7115 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -1,20 +1,19 @@
 use crate::attenuation::{Attenuation, RANGE_50};
-use crate::buffer::{ResizableBuffer, StorageBuffer, UniformBuffer};
+use crate::buffer::{ResizableBuffer, UniformBuffer};
 use crate::graphics::SharedGraphicsContext;
-use crate::shader::Shader;
-use crate::texture::Texture;
+use crate::pipelines::light_cube::InstanceInput;
 use crate::{
-    entity::{EntityTransform, Transform},
-    model::{self, Model, Vertex},
+    entity::Transform,
+    model::Model,
 };
 use dropbear_macro::SerializableComponent;
 use dropbear_traits::SerializableComponent;
-use glam::{DMat4, DQuat, DVec3};
+use glam::{DMat4, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
-use wgpu::{BindGroup, Buffer, BufferAddress, CompareFunction, DepthBiasState, RenderPipeline, StencilState, VertexBufferLayout};
+use wgpu::{BindGroup};
 
-pub const MAX_LIGHTS: usize = 8;
+pub const MAX_LIGHTS: usize = 10;
 
 #[repr(C)]
 #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
@@ -245,12 +244,13 @@ pub struct Light {
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
     pub bind_group: BindGroup,
-    pub instance_buffer: ResizableBuffer<InstanceRaw>,
+    pub instance_buffer: ResizableBuffer<InstanceInput>,
 }
 
 impl Light {
     pub const LIGHT_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = 
         wgpu::BindGroupLayoutDescriptor {
+            // @binding(0)
             entries: &[wgpu::BindGroupLayoutEntry {
                 binding: 0,
                 visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT),
@@ -313,11 +313,7 @@ impl Light {
                 label,
             });
 
-        let instance = Instance::new(
-            transform.position,
-            transform.rotation,
-            DVec3::new(0.25, 0.25, 0.25),
-        );
+        let instance: InstanceInput = DMat4::from_scale_rotation_translation(transform.scale, transform.rotation, transform.position).into();
 
         let mut instance_buffer = ResizableBuffer::new(
             &graphics.device,
@@ -326,7 +322,7 @@ impl Light {
             "Light Instance Buffer"
         );
 
-        instance_buffer.write(&graphics.device, &graphics.queue, &[instance.to_raw()]);
+        instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
 
         log::debug!("Created new light [{}]", label_str);
 
@@ -370,292 +366,4 @@ impl Light {
     pub fn label(&self) -> &str {
         &self.label
     }
-}
-
-#[derive(Clone)]
-pub struct LightManager {
-    pub pipeline: Option<RenderPipeline>,
-    light_array_buffer: Option<StorageBuffer<LightArrayUniform>>,
-    light_array_bind_group: Option<BindGroup>,
-}
-
-impl Default for LightManager {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl LightManager {
-    pub const LIGHT_ARRAY_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = 
-        wgpu::BindGroupLayoutDescriptor {
-            entries: &[
-                // light data
-                wgpu::BindGroupLayoutEntry {
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT),
-                    ty: wgpu::BindingType::Buffer {
-                        ty: wgpu::BufferBindingType::Storage { read_only: true },
-                        has_dynamic_offset: false,
-                        min_binding_size: None,
-                    },
-                    count: None,
-                },
-            ],
-            label: Some("Light Array Layout"),
-        };
-    pub const LIGHT_CUBE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = 
-        wgpu::BindGroupLayoutDescriptor {
-            entries: &[wgpu::BindGroupLayoutEntry {
-                binding: 0,
-                visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT),
-                ty: wgpu::BindingType::Buffer {
-                    ty: wgpu::BufferBindingType::Uniform,
-                    has_dynamic_offset: false,
-                    min_binding_size: None,
-                },
-                count: None,
-            }],
-            label: Some("Per-Light Layout"),
-        };
-    
-    pub fn new() -> Self {
-        log::info!("Initialised lighting");
-        Self {
-            pipeline: None,
-            light_array_buffer: None,
-            light_array_bind_group: None,
-        }
-    }
-
-    pub fn create_light_array_resources(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        let buffer = StorageBuffer::new(&graphics.device, "Light Array");
-
-        let bind_group = graphics
-            .device
-            .create_bind_group(&wgpu::BindGroupDescriptor {
-                layout: &graphics.layouts.light_array_bind_group_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: buffer.buffer().as_entire_binding(),
-                    },
-                ],
-                label: Some("Light Array Bind Group"),
-            });
-
-        self.light_array_buffer = Some(buffer);
-        self.light_array_bind_group = Some(bind_group);
-
-        log::debug!("Created light array resources");
-    }
-
-    pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) {
-        let mut light_array = LightArrayUniform::default();
-        let mut light_index = 0;
-
-        for (light_component, s_trans, e_trans, light) in world
-            .query::<(&LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>()
-            .iter()
-        {
-            let instance = if let Some(transform) = e_trans {
-                let sync_transform = transform.sync();
-                Instance::from_matrix(sync_transform.matrix())
-            } else if let Some(transform) = s_trans {
-                Instance::from_matrix(transform.matrix())
-            } else {
-                panic!("Unable to locate either a \"Transform\" or an \"EntityTransform\" component for the light {}", light.label);
-            };
-
-            light.instance_buffer.write(&graphics.device, &graphics.queue, &[instance.to_raw()]);
-
-            if light_component.enabled && light_index < MAX_LIGHTS {
-                let uniform = *light.uniform();
-
-                light.buffer.write(&graphics.queue, &uniform);
-
-                light_array.lights[light_index] = uniform;
-                light_index += 1;
-            }
-        }
-
-        light_array.light_count = light_index as u32;
-        if let Some(buf) = &self.light_array_buffer {
-            buf.write(&graphics.queue, &light_array);
-        }
-
-        log_once::debug_once!("LightUniform size = {}", size_of::<LightUniform>());
-    }
-
-    pub fn bind_group(&self) -> &BindGroup {
-        self.light_array_bind_group.as_ref().unwrap()
-    }
-
-    pub fn create_render_pipeline(
-        &mut self,
-        graphics: Arc<SharedGraphicsContext>,
-        shader_contents: &str,
-        label: Option<&str>,
-    ) {
-        use crate::shader::Shader;
-
-        let shader = Shader::new(graphics.clone(), shader_contents, label);
-
-        // the light cube rendering
-        let pipeline = Self::create_render_pipeline_for_lighting(
-            graphics,
-            &shader,
-            label,
-        );
-
-        self.pipeline = Some(pipeline);
-        log::debug!("Created ECS light render pipeline");
-    }
-
-    // light cube rendering
-    fn create_render_pipeline_for_lighting(
-        graphics: Arc<SharedGraphicsContext>,
-        shader: &Shader,
-        label: Option<&str>,
-    ) -> RenderPipeline {
-        let render_pipeline_layout =
-            graphics
-                .device
-                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                    label: Some(label.unwrap_or("Light Render Pipeline Descriptor")),
-                    bind_group_layouts: &[&graphics.layouts.camera_bind_group_layout, &graphics.layouts.light_cube_bind_group_layout],
-                    push_constant_ranges: &[],
-                });
-
-        graphics
-            .device
-            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-                label: Some("Render Pipeline"),
-                layout: Some(&render_pipeline_layout),
-                vertex: wgpu::VertexState {
-                    module: &shader.module,
-                    entry_point: Some("vs_main"),
-                    buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()],
-                    compilation_options: Default::default(),
-                },
-                fragment: Some(wgpu::FragmentState {
-                    module: &shader.module,
-                    entry_point: Some("fs_main"),
-                    targets: &[Some(wgpu::ColorTargetState {
-                        format: Texture::TEXTURE_FORMAT,
-                        blend: Some(wgpu::BlendState {
-                            alpha: wgpu::BlendComponent::REPLACE,
-                            color: wgpu::BlendComponent::REPLACE,
-                        }),
-                        write_mask: wgpu::ColorWrites::ALL,
-                    })],
-                    compilation_options: Default::default(),
-                }),
-                primitive: wgpu::PrimitiveState {
-                    topology: wgpu::PrimitiveTopology::TriangleList,
-                    strip_index_format: None,
-                    front_face: wgpu::FrontFace::Ccw,
-                    cull_mode: Some(wgpu::Face::Back),
-                    // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
-                    polygon_mode: wgpu::PolygonMode::Fill,
-                    // Requires Features::DEPTH_CLIP_CONTROL
-                    unclipped_depth: false,
-                    // Requires Features::CONSERVATIVE_RASTERIZATION
-                    conservative: false,
-                },
-                depth_stencil: Some(wgpu::DepthStencilState {
-                    format: crate::Texture::DEPTH_FORMAT,
-                    depth_write_enabled: true,
-                    depth_compare: CompareFunction::Greater,
-                    stencil: StencilState::default(),
-                    bias: DepthBiasState::default(),
-                }),
-                multisample: wgpu::MultisampleState {
-                    count: 1,
-                    mask: !0,
-                    alpha_to_coverage_enabled: false,
-                },
-                multiview: None,
-                cache: None,
-            })
-    }
-}
-
-#[derive(Default)]
-pub struct Instance {
-    pub position: DVec3,
-    pub rotation: DQuat,
-    pub scale: DVec3,
-
-    buffer: Option<Buffer>,
-}
-
-impl Instance {
-    pub fn new(position: DVec3, rotation: DQuat, scale: DVec3) -> Self {
-        Self {
-            position,
-            rotation,
-            scale,
-            buffer: None,
-        }
-    }
-
-    pub fn to_raw(&self) -> InstanceRaw {
-        let model_matrix =
-            DMat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
-        InstanceRaw {
-            model: model_matrix.as_mat4().to_cols_array_2d(),
-        }
-    }
-
-    pub fn buffer(&self) -> &Buffer {
-        self.buffer.as_ref().unwrap()
-    }
-
-    pub fn from_matrix(mat: DMat4) -> Self {
-        let (scale, rotation, position) = mat.to_scale_rotation_translation();
-        Instance {
-            position,
-            rotation,
-            scale,
-            buffer: None,
-        }
-    }
-}
-
-#[repr(C)]
-#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct InstanceRaw {
-    model: [[f32; 4]; 4],
-}
-
-impl InstanceRaw {
-    fn desc() -> VertexBufferLayout<'static> {
-        VertexBufferLayout {
-            array_stride: size_of::<InstanceRaw>() as BufferAddress,
-            step_mode: wgpu::VertexStepMode::Instance,
-            attributes: &[
-                // model
-                wgpu::VertexAttribute {
-                    offset: 0,
-                    shader_location: 5,
-                    format: wgpu::VertexFormat::Float32x4,
-                },
-                wgpu::VertexAttribute {
-                    offset: size_of::<[f32; 4]>() as wgpu::BufferAddress,
-                    shader_location: 6,
-                    format: wgpu::VertexFormat::Float32x4,
-                },
-                wgpu::VertexAttribute {
-                    offset: size_of::<[f32; 8]>() as wgpu::BufferAddress,
-                    shader_location: 7,
-                    format: wgpu::VertexFormat::Float32x4,
-                },
-                wgpu::VertexAttribute {
-                    offset: size_of::<[f32; 12]>() as wgpu::BufferAddress,
-                    shader_location: 8,
-                    format: wgpu::VertexFormat::Float32x4,
-                },
-            ],
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 6966208..56d03f5 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -537,9 +537,20 @@ impl Model {
             .map(|(material_name, diffuse_bytes, normal_bytes, tint)| {
                 let material_start = Instant::now();
 
-                let processed_diffuse = diffuse_bytes.as_ref().map(|bytes| {
+                let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| {
                     let load_start = Instant::now();
-                    let image = image::load_from_memory(bytes).unwrap();
+                    let image = match image::load_from_memory(bytes) {
+                        Ok(image) => image,
+                        Err(err) => {
+                            log::warn!(
+                                "Failed to decode diffuse texture for material '{}': {} ({} bytes)",
+                                material_name,
+                                err,
+                                bytes.len()
+                            );
+                            return None;
+                        }
+                    };
                     log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed());
 
                     let rgba_start = Instant::now();
@@ -550,12 +561,23 @@ impl Model {
                     );
 
                     let dimensions = image.dimensions();
-                    (rgba.into_raw(), dimensions)
+                    Some((rgba.into_raw(), dimensions))
                 });
 
-                let processed_normal = normal_bytes.as_ref().map(|bytes| {
+                let processed_normal = normal_bytes.as_ref().and_then(|bytes| {
                     let load_start = Instant::now();
-                    let image = image::load_from_memory(bytes).unwrap();
+                    let image = match image::load_from_memory(bytes) {
+                        Ok(image) => image,
+                        Err(err) => {
+                            log::warn!(
+                                "Failed to decode normal texture for material '{}': {} ({} bytes)",
+                                material_name,
+                                err,
+                                bytes.len()
+                            );
+                            return None;
+                        }
+                    };
                     log::trace!("Loading normal image to memory: {:?}", load_start.elapsed());
 
                     let rgba_start = Instant::now();
@@ -566,7 +588,7 @@ impl Model {
                     );
 
                     let dimensions = image.dimensions();
-                    (rgba.into_raw(), dimensions)
+                    Some((rgba.into_raw(), dimensions))
                 });
 
                 log::trace!(
@@ -1091,6 +1113,16 @@ pub trait Vertex {
     fn desc() -> VertexBufferLayout<'static>;
 }
 
+/// Maps to
+/// ```wgsl
+/// struct VertexInput {
+///     @location(0) position: vec3<f32>,
+///     @location(1) tex_coords: vec2<f32>,
+///     @location(2) normal: vec3<f32>,
+///     @location(3) tangent: vec3<f32>,
+///     @location(4) bitangent: vec3<f32>,
+/// };
+/// ```
 #[repr(C)]
 #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, serde::Serialize, serde::Deserialize)]
 pub struct ModelVertex {
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
new file mode 100644
index 0000000..6d63b61
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -0,0 +1,72 @@
+use std::sync::Arc;
+
+use crate::buffer::UniformBuffer;
+use crate::graphics::SharedGraphicsContext;
+
+/// Mirrors `Globals` in `pipelines/shaders/shader.wgsl` (`@group(4) @binding(0)`).
+///
+/// Note: the `_padding` ensures the uniform is 16 bytes, which matches WGSL
+/// uniform layout expectations.
+#[repr(C)]
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct Globals {
+    pub num_lights: u32,
+    pub ambient_strength: f32,
+    pub _padding: [u32; 2],
+}
+
+impl Default for Globals {
+    fn default() -> Self {
+        Self {
+            num_lights: 0,
+            ambient_strength: 0.1,
+            _padding: [0; 2],
+        }
+    }
+}
+
+/// Owns the globals uniform buffer and its bind group.
+#[derive(Debug, Clone)]
+pub struct GlobalsUniform {
+    pub data: Globals,
+    pub buffer: UniformBuffer<Globals>,
+    pub bind_group: wgpu::BindGroup,
+}
+
+impl GlobalsUniform {
+    pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self {
+        let label = label.unwrap_or("shader globals");
+
+        let buffer = UniformBuffer::new(&graphics.device, label);
+
+        let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            layout: &graphics.layouts.shader_globals_bind_group_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: buffer.buffer().as_entire_binding(),
+            }],
+            label: Some(label),
+        });
+
+        let data = Globals::default();
+        buffer.write(&graphics.queue, &data);
+
+        Self {
+            data,
+            buffer,
+            bind_group,
+        }
+    }
+
+    pub fn write(&mut self, queue: &wgpu::Queue) {
+        self.buffer.write(queue, &self.data);
+    }
+
+    pub fn set_num_lights(&mut self, num_lights: u32) {
+        self.data.num_lights = num_lights;
+    }
+
+    pub fn set_ambient_strength(&mut self, ambient_strength: f32) {
+        self.data.ambient_strength = ambient_strength;
+    }
+}
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
new file mode 100644
index 0000000..8caccc2
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -0,0 +1,292 @@
+use std::sync::Arc;
+use std::mem::size_of;
+use glam::DMat4;
+use wgpu::{BufferAddress, VertexAttribute, VertexFormat};
+use crate::buffer::{StorageBuffer, UniformBuffer};
+use crate::entity::{EntityTransform, Transform};
+use crate::graphics::SharedGraphicsContext;
+use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS};
+use crate::model::Vertex;
+use crate::pipelines::DropbearShaderPipeline;
+use crate::shader::Shader;
+use crate::texture::Texture;
+
+pub struct LightCubePipeline {
+    shader: Shader,
+    pipeline_layout: wgpu::PipelineLayout,
+    pipeline: wgpu::RenderPipeline,
+    storage_buffer: Option<StorageBuffer<LightArrayUniform>>,
+    uniform_buffer: Option<UniformBuffer<LightArrayUniform>>,
+    /// Bind group, defined in `shaders/shader.wgsl` as @group(2). 
+    /// 
+    /// This can either be a storage buffer or a uniform buffer depending on if the backend
+    /// supports storage resources. 
+    light_bind_group: wgpu::BindGroup,
+}
+
+impl DropbearShaderPipeline for LightCubePipeline {
+    fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
+        let shader = Shader::new(
+            graphics.clone(),
+            include_str!("shaders/light.wgsl"),
+            Some("light cube shader"),
+        );
+
+        let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("light cube pipeline layout"),
+            bind_group_layouts: &[
+                &graphics.layouts.camera_bind_group_layout,
+                &graphics.layouts.light_cube_bind_group_layout,
+            ],
+            push_constant_ranges: &[],
+        });
+
+        let pipeline = graphics.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("light cube pipeline"),
+            layout: Some(&pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                compilation_options: Default::default(),
+                buffers: &[
+                    // model
+                    VertexInput::desc(),
+                    // instance
+                    InstanceInput::desc(),
+                ],
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &shader.module,
+                entry_point: Some("fs_main"),
+                targets: &[Some(wgpu::ColorTargetState {
+                    format: Texture::TEXTURE_FORMAT,
+                    blend: Some(wgpu::BlendState {
+                        alpha: wgpu::BlendComponent::REPLACE,
+                        color: wgpu::BlendComponent::REPLACE,
+                    }),
+                    write_mask: wgpu::ColorWrites::ALL,
+                })],
+                compilation_options: Default::default(),
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+                front_face: wgpu::FrontFace::Ccw,
+                cull_mode: Some(wgpu::Face::Back),
+                polygon_mode: wgpu::PolygonMode::Fill,
+                unclipped_depth: false,
+                conservative: false,
+            },
+            depth_stencil: Some(wgpu::DepthStencilState {
+                format: crate::Texture::DEPTH_FORMAT,
+                depth_write_enabled: true,
+                depth_compare: wgpu::CompareFunction::Greater,
+                stencil: wgpu::StencilState::default(),
+                bias: wgpu::DepthBiasState::default(),
+            }),
+            multisample: wgpu::MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            multiview: None,
+            cache: None,
+        });
+
+        let mut storage_buffer = None;
+        let mut uniform_buffer = None;
+
+        if graphics.supports_storage {
+            storage_buffer = Some(StorageBuffer::new(
+                &graphics.device,
+                "light cube pipeline storage buffer",
+            ));
+        } else {
+            uniform_buffer = Some(UniformBuffer::new(
+                &graphics.device,
+                "light cube pipeline uniform buffer",
+            ));
+        }
+
+        let light_buffer: &wgpu::Buffer = if let Some(buf) = &storage_buffer {
+            buf.buffer()
+        } else if let Some(buf) = &uniform_buffer {
+            buf.buffer()
+        } else {
+            panic!("Either a storage buffer or a uniform buffer should have been created");
+        };
+
+        let light_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            layout: &graphics.layouts.light_array_bind_group_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: light_buffer.as_entire_binding(),
+            }],
+            label: Some("light array bind group"),
+        });
+
+        Self {
+            shader,
+            pipeline_layout,
+            pipeline,
+            storage_buffer,
+            uniform_buffer,
+            light_bind_group,
+        }
+    }
+
+    fn shader(&self) -> &Shader {
+        &self.shader
+    }
+
+    fn pipeline_layout(&self) -> &wgpu::PipelineLayout {
+        &self.pipeline_layout
+    }
+
+    fn pipeline(&self) -> &wgpu::RenderPipeline {
+        &self.pipeline
+    }
+}
+
+impl LightCubePipeline {
+    pub fn bind_group(&self) -> &wgpu::BindGroup {
+        &self.light_bind_group
+    }
+
+    pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) {
+        debug_assert!(
+            self.storage_buffer.is_some() ^ self.uniform_buffer.is_some(),
+            "Expected exactly one of storage_buffer/uniform_buffer to exist"
+        );
+
+        let mut light_array = LightArrayUniform::default();
+
+        let mut light_index: usize = 0;
+
+        for (light_component, s_trans, e_trans, light) in world
+            .query::<(&LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>()
+            .iter()
+        {
+            let instance: InstanceInput = if let Some(transform) = e_trans {
+                let sync_transform = transform.sync();
+                sync_transform.matrix().into()
+            } else if let Some(transform) = s_trans {
+                transform.matrix().into()
+            } else {
+                panic!("Unable to locate either a \"Transform\" or an \"EntityTransform\" component for the light {}", light.label);
+            };
+
+            light.instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
+
+            if light_component.enabled && light_index < MAX_LIGHTS {
+                let uniform = *light.uniform();
+
+                light.buffer.write(&graphics.queue, &uniform);
+
+                light_array.lights[light_index] = uniform;
+                light_index += 1;
+            }
+        }
+
+        light_array.light_count = light_index as u32;
+
+        if let Some(buf) = &self.storage_buffer {
+            buf.write(&graphics.queue, &light_array);
+        } else if let Some(buf) = &self.uniform_buffer {
+            buf.write(&graphics.queue, &light_array);
+        } else {
+            panic!("Either a storage buffer or a uniform buffer should have been created");
+        }
+    }
+
+    pub fn buffer(&self) -> &wgpu::Buffer {
+        if let Some(s) = &self.storage_buffer {
+            s.buffer()
+        } else if let Some(u) = &self.uniform_buffer {
+            u.buffer()
+        } else {
+            panic!("Either a storage buffer or a uniform buffer should have been created");
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct VertexInput {
+    position: [f32; 3],
+}
+
+impl Vertex for VertexInput {
+    fn desc() -> wgpu::VertexBufferLayout<'static> {
+        wgpu::VertexBufferLayout {
+            array_stride: size_of::<VertexInput>() as BufferAddress,
+            step_mode: wgpu::VertexStepMode::Vertex,
+            attributes: &[
+                // position
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 0,
+                    shader_location: 0,
+                }
+            ],
+        }
+    }
+}
+
+/// As mapped in `shaders/light.wgsl` as
+/// ```wgsl
+/// struct InstanceInput {
+///     @location(5) model_matrix_0: vec4<f32>,
+///     @location(6) model_matrix_1: vec4<f32>,
+///     @location(7) model_matrix_2: vec4<f32>,
+///     @location(8) model_matrix_3: vec4<f32>,
+/// }
+/// ```
+#[repr(C)]
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct InstanceInput {
+    pub model_matrix: [[f32; 4]; 4],
+}
+
+impl Vertex for InstanceInput {
+    fn desc() -> wgpu::VertexBufferLayout<'static> {
+        wgpu::VertexBufferLayout {
+            array_stride: size_of::<InstanceInput>() as BufferAddress,
+            step_mode: wgpu::VertexStepMode::Instance,
+            attributes: &[
+                // model_matrix_0
+                wgpu::VertexAttribute {
+                    offset: 0,
+                    shader_location: 5,
+                    format: wgpu::VertexFormat::Float32x4,
+                },
+                // model_matrix_1
+                wgpu::VertexAttribute {
+                    offset: size_of::<[f32; 4]>() as wgpu::BufferAddress,
+                    shader_location: 6,
+                    format: wgpu::VertexFormat::Float32x4,
+                },
+                // model_matrix_2
+                wgpu::VertexAttribute {
+                    offset: size_of::<[f32; 8]>() as wgpu::BufferAddress,
+                    shader_location: 7,
+                    format: wgpu::VertexFormat::Float32x4,
+                },
+                // model_matrix_3
+                wgpu::VertexAttribute {
+                    offset: size_of::<[f32; 12]>() as wgpu::BufferAddress,
+                    shader_location: 8,
+                    format: wgpu::VertexFormat::Float32x4,
+                },
+            ],
+        }
+    }
+}
+
+impl Into<InstanceInput> for DMat4 {
+    fn into(self) -> InstanceInput {
+        InstanceInput {
+            model_matrix: self.as_mat4().to_cols_array_2d(),
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/mod.rs b/crates/dropbear-engine/src/pipelines/mod.rs
new file mode 100644
index 0000000..f151c40
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/mod.rs
@@ -0,0 +1,23 @@
+use std::sync::Arc;
+use crate::graphics::SharedGraphicsContext;
+use crate::shader::Shader;
+
+pub mod shader;
+pub mod light_cube;
+pub mod globals;
+
+pub use globals::{Globals, GlobalsUniform};
+
+/// A helper in defining a pipelines required information, as well as getters. 
+/// 
+/// This contains the bare minimum for any pipeline. 
+pub trait DropbearShaderPipeline {
+    /// Creates a new instance of a pipeline. 
+    fn new(graphics: Arc<SharedGraphicsContext>) -> Self;
+    /// Fetches the shader property
+    fn shader(&self) -> &Shader;
+    /// Fetches the pipeline layout
+    fn pipeline_layout(&self) -> &wgpu::PipelineLayout;
+    /// Fetches the pipeline
+    fn pipeline(&self) -> &wgpu::RenderPipeline;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
new file mode 100644
index 0000000..eaa8a1d
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shader.rs
@@ -0,0 +1,111 @@
+use std::sync::Arc;
+use wgpu::{CompareFunction, DepthBiasState, StencilState};
+use crate::graphics::{InstanceRaw, SharedGraphicsContext};
+use crate::model;
+use crate::model::Vertex;
+use crate::pipelines::DropbearShaderPipeline;
+use crate::shader::Shader;
+use crate::texture::Texture;
+
+/// As defined in `shaders/shader.wgsl`
+pub struct MainRenderPipeline {
+    shader: Shader,
+    pipeline_layout: wgpu::PipelineLayout,
+    pipeline: wgpu::RenderPipeline,
+}
+
+impl DropbearShaderPipeline for MainRenderPipeline {
+    fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
+        let shader = Shader::new(
+            graphics.clone(),
+            include_str!("shaders/shader.wgsl"),
+            Some("viewport shaders"),
+        );
+
+        let bind_group_layouts = vec![
+            &graphics.layouts.texture_bind_layout, // @group(0)
+            &graphics.layouts.camera_bind_group_layout, // @group(1)
+            &graphics.layouts.light_array_bind_group_layout, // @group(2)
+            &graphics.layouts.material_tint_bind_layout, // @group(3)
+            &graphics.layouts.shader_globals_bind_group_layout, // @group(4)
+        ];
+
+        let pipeline_layout =
+            graphics.device
+                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                    label: Some("main render pipeline layout"),
+                    bind_group_layouts: bind_group_layouts.as_slice(),
+                    push_constant_ranges: &[],
+                });
+
+        let pipeline =
+            graphics.device
+                .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+                    label: Some("main render pipeline"),
+                    layout: Some(&pipeline_layout),
+                    vertex: wgpu::VertexState {
+                        module: &shader.module,
+                        entry_point: Some("vs_main"),
+                        buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()],
+                        compilation_options: wgpu::PipelineCompilationOptions::default(),
+                    },
+                    fragment: Some(wgpu::FragmentState {
+                        module: &shader.module,
+                        entry_point: if graphics.supports_storage {
+                            Some("s_fs_main")
+                        } else {
+                            Some("u_fs_main")
+                        },
+                        targets: &[Some(wgpu::ColorTargetState {
+                            format: Texture::TEXTURE_FORMAT,
+                            blend: Some(wgpu::BlendState::REPLACE),
+                            write_mask: wgpu::ColorWrites::ALL,
+                        })],
+                        compilation_options: wgpu::PipelineCompilationOptions::default(),
+                    }),
+                    primitive: wgpu::PrimitiveState {
+                        topology: wgpu::PrimitiveTopology::TriangleList,
+                        strip_index_format: None,
+                        front_face: wgpu::FrontFace::Ccw,
+                        cull_mode: Some(wgpu::Face::Back),
+                        polygon_mode: wgpu::PolygonMode::Fill,
+                        unclipped_depth: false,
+                        conservative: false,
+                    },
+                    depth_stencil: Some(wgpu::DepthStencilState {
+                        format: Texture::DEPTH_FORMAT,
+                        depth_write_enabled: true,
+                        depth_compare: CompareFunction::Greater,
+                        stencil: StencilState::default(),
+                        bias: DepthBiasState::default(),
+                    }),
+                    multisample: wgpu::MultisampleState {
+                        count: 1,
+                        mask: !0,
+                        alpha_to_coverage_enabled: false,
+                    },
+                    multiview: None,
+                    cache: None,
+                });
+
+        log::debug!("Created main render pipeline");
+
+        Self {
+            shader,
+            pipeline_layout,
+            pipeline,
+        }
+    }
+
+    fn shader(&self) -> &Shader {
+        &self.shader
+    }
+
+    fn pipeline_layout(&self) -> &wgpu::PipelineLayout {
+        &self.pipeline_layout
+    }
+
+    fn pipeline(&self) -> &wgpu::RenderPipeline {
+        &self.pipeline
+    }
+}
diff --git a/crates/dropbear-engine/src/pipelines/shaders/light.wgsl b/crates/dropbear-engine/src/pipelines/shaders/light.wgsl
new file mode 100644
index 0000000..d31ebc8
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/light.wgsl
@@ -0,0 +1,62 @@
+// Shader for rendering the white cube for lighting (or diff depending on colour)
+
+struct Light {
+    position: vec4<f32>,
+    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
+    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
+    constant: f32,
+    lin: f32,
+    quadratic: f32,
+    cutoff: f32,
+}
+
+struct CameraUniform {
+    view_pos: vec4<f32>,
+    view_proj: mat4x4<f32>,
+};
+
+@group(0) @binding(0)
+var<uniform> camera: CameraUniform;
+
+@group(1) @binding(0)
+var<uniform> light: Light;
+
+struct InstanceInput {
+    @location(5) model_matrix_0: vec4<f32>,
+    @location(6) model_matrix_1: vec4<f32>,
+    @location(7) model_matrix_2: vec4<f32>,
+    @location(8) model_matrix_3: vec4<f32>,
+}
+
+struct VertexInput {
+    @location(0) position: vec3<f32>,
+};
+
+struct VertexOutput {
+    @builtin(position) clip_position: vec4<f32>,
+    @location(0) color: vec3<f32>,
+};
+
+@vertex
+fn vs_main(
+    model: VertexInput,
+    instance: InstanceInput,
+) -> VertexOutput {
+    let model_matrix = mat4x4<f32>(
+        instance.model_matrix_0,
+        instance.model_matrix_1,
+        instance.model_matrix_2,
+        instance.model_matrix_3,
+    );
+    var out: VertexOutput;
+    out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0);
+    out.color = light.color.xyz;
+    return out;
+}
+
+// Fragment shaders
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    return vec4<f32>(in.color, 1.0);
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl b/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl
new file mode 100644
index 0000000..dcf0012
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl
@@ -0,0 +1,290 @@
+// Main shaders for standard objects.
+
+struct Globals {
+    num_lights: u32,
+    ambient_strength: f32,
+}
+
+struct CameraUniform {
+    view_pos: vec4<f32>,
+    view_proj: 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, 1, 2)
+    constant: f32,
+    lin: f32,
+    quadratic: f32,
+    cutoff: f32,
+}
+
+struct MaterialUniform {
+    // for stuff like tinting
+    colour: vec4<f32>,
+
+    // scales incoming UVs before sampling
+    uv_tiling: vec2<f32>,
+}
+
+const c_max_lights: u32 = 10u;
+
+@group(0) @binding(0)
+var t_diffuse: texture_2d<f32>;
+@group(0) @binding(1)
+var s_diffuse: sampler;
+@group(0) @binding(2)
+var t_normal: texture_2d<f32>;
+@group(0) @binding(3)
+var s_normal: sampler;
+
+@group(1) @binding(0)
+var<uniform> u_camera: CameraUniform;
+
+@group(2) @binding(0)
+var<storage, read> s_light_array: array<Light>;
+@group(2) @binding(0)
+var<uniform> u_light_array: array<Light, 10>; // when storage is not available
+
+@group(3) @binding(0)
+var<uniform> u_material: MaterialUniform;
+
+@group(4) @binding(0)
+var<uniform> u_globals: Globals;
+
+struct InstanceInput {
+    @location(5) model_matrix_0: vec4<f32>,
+    @location(6) model_matrix_1: vec4<f32>,
+    @location(7) model_matrix_2: vec4<f32>,
+    @location(8) model_matrix_3: vec4<f32>,
+
+    @location(9) normal_matrix_0: vec3<f32>,
+    @location(10) normal_matrix_1: vec3<f32>,
+    @location(11) normal_matrix_2: vec3<f32>,
+};
+
+struct VertexInput {
+    @location(0) position: vec3<f32>,
+    @location(1) tex_coords: vec2<f32>,
+    @location(2) normal: vec3<f32>,
+    @location(3) tangent: vec3<f32>,
+    @location(4) bitangent: vec3<f32>,
+};
+
+struct VertexOutput {
+    @builtin(position) clip_position: vec4<f32>,
+    @location(0) tex_coords: vec2<f32>,
+    @location(1) world_normal: vec3<f32>,
+    @location(2) world_position: vec3<f32>,
+    @location(3) world_tangent: vec3<f32>,
+    @location(4) world_bitangent: vec3<f32>,
+};
+
+@vertex
+fn vs_main(
+    model: VertexInput,
+    instance: InstanceInput,
+) -> VertexOutput {
+    let model_matrix = mat4x4<f32>(
+        instance.model_matrix_0,
+        instance.model_matrix_1,
+        instance.model_matrix_2,
+        instance.model_matrix_3,
+    );
+    let normal_matrix = mat3x3<f32>(
+        instance.normal_matrix_0,
+        instance.normal_matrix_1,
+        instance.normal_matrix_2,
+    );
+
+    let world_normal = normalize(normal_matrix * model.normal);
+    let world_tangent = normalize(normal_matrix * model.tangent);
+    let world_bitangent = normalize(normal_matrix * model.bitangent);
+    let world_position = model_matrix * vec4<f32>(model.position, 1.0);
+
+    var out: VertexOutput;
+    out.clip_position = u_camera.view_proj * world_position;
+    out.tex_coords = model.tex_coords;
+    out.world_normal = world_normal;
+    out.world_position = world_position.xyz;
+    out.world_tangent = world_tangent;
+    out.world_bitangent = world_bitangent;
+    return out;
+}
+
+fn directional_light(
+    light: Light,
+    world_normal: vec3<f32>,
+    view_dir: vec3<f32>,
+    tex_color: vec3<f32>,
+    world_pos: vec3<f32>
+) -> vec3<f32> {
+    let light_dir = normalize(-light.direction.xyz);
+
+    let ambient = light.color.xyz * u_globals.ambient_strength * tex_color;
+
+    let diff = max(dot(world_normal, light_dir), 0.0);
+    let diffuse = light.color.xyz * diff * tex_color;
+
+    let reflect_dir = reflect(-light_dir, world_normal);
+    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+    let specular = light.color.xyz * spec * tex_color;
+
+    return ambient + diffuse + specular;
+}
+
+fn point_light(
+    light: Light,
+    world_pos: vec3<f32>,
+    world_normal: vec3<f32>,
+    view_dir: vec3<f32>,
+    tex_color: vec3<f32>
+) -> vec3<f32> {
+    let light_dir = normalize(light.position.xyz - world_pos);
+
+    let distance = length(light.position.xyz - world_pos);
+    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
+
+    let ambient = light.color.xyz * u_globals.ambient_strength * tex_color;
+
+    let diff = max(dot(world_normal, light_dir), 0.0);
+    let diffuse = light.color.xyz * diff * tex_color;
+
+    let reflect_dir = reflect(-light_dir, world_normal);
+    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+    let specular = light.color.xyz * spec * tex_color;
+
+    return (ambient + diffuse + specular) * attenuation;
+}
+
+fn spot_light(
+    light: Light,
+    world_pos: vec3<f32>,
+    world_normal: vec3<f32>,
+    view_dir: vec3<f32>,
+    tex_color: vec3<f32>
+) -> vec3<f32> {
+    let light_dir = normalize(light.position.xyz - world_pos);
+    let theta = dot(light_dir, normalize(-light.direction.xyz));
+    let outer_cutoff = light.direction.w;
+
+    let epsilon = light.cutoff - outer_cutoff;
+    let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
+
+    let distance = length(light.position.xyz - world_pos);
+    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
+
+    let ambient = light.color.xyz * u_globals.ambient_strength * tex_color;
+
+    let diff = max(dot(world_normal, light_dir), 0.0);
+    let diffuse = light.color.xyz * diff * tex_color * intensity;
+
+    let reflect_dir = reflect(-light_dir, world_normal);
+    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+    let specular = light.color.xyz * spec * tex_color * intensity;
+
+    return (ambient + diffuse + specular) * attenuation;
+}
+
+fn apply_normal_map(
+    world_normal_in: vec3<f32>,
+    world_tangent_in: vec3<f32>,
+    world_bitangent_in: vec3<f32>,
+    normal_sample_rgb: vec3<f32>,
+) -> vec3<f32> {
+    let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0));
+
+    let n = normalize(world_normal_in);
+    var t = normalize(world_tangent_in);
+    t = normalize(t - n * dot(n, t));
+
+    let b_in = normalize(world_bitangent_in);
+    let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0);
+    let b = cross(n, t) * handedness;
+
+    let tbn = mat3x3<f32>(t, b, n);
+    return normalize(tbn * normal_ts);
+}
+
+// when storage is supported
+@fragment
+fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    let uv = in.tex_coords * u_material.uv_tiling;
+    var tex_color = textureSample(t_diffuse, s_diffuse, uv);
+    var object_normal = textureSample(t_normal, s_normal, uv);
+
+    let base_colour = tex_color * u_material.colour;
+
+    if (base_colour.a < 0.1) {
+        discard;
+    }
+
+    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
+
+    let world_normal = apply_normal_map(
+        in.world_normal,
+        in.world_tangent,
+        in.world_bitangent,
+        object_normal.xyz,
+    );
+
+    var final_color = vec3<f32>(0.0);
+
+    for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) {
+        let light = s_light_array[i];
+
+        let light_type = i32(light.color.w + 0.1);
+
+        if (light_type == 0) {
+            final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position);
+        } else if (light_type == 1) {
+            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
+        } else if (light_type == 2) {
+            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
+        }
+    }
+
+    return vec4<f32>(final_color, base_colour.a);
+}
+
+// when storage is NOT supported
+@fragment
+fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    let uv = in.tex_coords * u_material.uv_tiling;
+    var tex_color = textureSample(t_diffuse, s_diffuse, uv);
+    var object_normal = textureSample(t_normal, s_normal, uv);
+
+    let base_colour = tex_color * u_material.colour;
+
+    if (base_colour.a < 0.1) {
+        discard;
+    }
+
+    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
+
+    let world_normal = apply_normal_map(
+        in.world_normal,
+        in.world_tangent,
+        in.world_bitangent,
+        object_normal.xyz,
+    );
+
+    var final_color = vec3<f32>(0.0);
+
+    for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) {
+        let light = u_light_array[i];
+
+        let light_type = i32(light.color.w + 0.1);
+
+        if (light_type == 0) {
+            final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position);
+        } else if (light_type == 1) {
+            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
+        } else if (light_type == 2) {
+            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
+        }
+    }
+
+    return vec4<f32>(final_color, base_colour.a);
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shader.rs b/crates/dropbear-engine/src/shader.rs
index 17636a1..772572f 100644
--- a/crates/dropbear-engine/src/shader.rs
+++ b/crates/dropbear-engine/src/shader.rs
@@ -1,14 +1,26 @@
-//! Deals with shaders, including WESL shaders
+//! Deals with shaders, primarily around the [Shader] struct. 
 
 use std::ops::Deref;
 use crate::graphics::SharedGraphicsContext;
 use std::sync::Arc;
 use wgpu::ShaderModule;
 
-/// A nice little struct that stored basic information about a WGPU shader.
+/// A nice little struct that stored basic information about a WGPU shaders.
 pub struct Shader {
+    /// The label of the shader. 
+    /// 
+    /// If it is not set in [Shader::new], the default is "shader". 
     pub label: String,
+
+    /// The compiled content of the WGSL shader.
+    /// 
+    /// When [Shader] is dereferenced (such as that in `&shader`), it will automatically reference 
+    /// this module.  
     pub module: ShaderModule,
+
+    /// The content of the shader as a readable string content, in the case you need to look
+    /// at the original source. 
+    pub content: String,
 }
 
 impl Deref for Shader {
@@ -33,7 +45,7 @@ impl Shader {
                 source: wgpu::ShaderSource::Wgsl(shader_file_contents.into()),
             });
 
-        log::debug!("Created new shader under the label: {:?}", label);
+        log::debug!("Created new shaders under the label: {:?}", label);
 
         Self {
             label: match label {
@@ -41,6 +53,7 @@ impl Shader {
                 None => "shader".into(),
             },
             module,
+            content: shader_file_contents.to_string(),
         }
     }
 }
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index fdb663e..0bb37b7 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -1,12 +1,12 @@
 use std::{fs, path::PathBuf, sync::Arc};
 
-use image::{EncodableLayout, GenericImageView};
+use image::GenericImageView;
 use serde::{Deserialize, Serialize};
 
 use crate::graphics::SharedGraphicsContext;
 
 
-/// As defined in `shader.wgsl` as 
+/// As defined in `shaders.wgsl` as 
 /// ```
 /// @group(0) @binding(0)
 /// var t_diffuse: texture_2d<f32>;
@@ -193,10 +193,39 @@ impl Texture {
         view_descriptor: Option<wgpu::TextureViewDescriptor>,
         sampler: Option<wgpu::SamplerDescriptor>,
     ) -> Self {
-        let diffuse_image = image::load_from_memory(bytes).unwrap();
-        let diffuse_rgba = diffuse_image.to_rgba8();
-
-        let dimensions = dimensions.unwrap_or_else(|| diffuse_image.dimensions());
+        let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) {
+            Ok(image) => {
+                let rgba = image.to_rgba8().into_raw();
+                let dims = dimensions.unwrap_or_else(|| image.dimensions());
+                (rgba, dims)
+            }
+            Err(err) => {
+                if let Some(dims) = dimensions {
+                    let expected_len = (dims.0 as usize)
+                        .saturating_mul(dims.1 as usize)
+                        .saturating_mul(4);
+                    if bytes.len() == expected_len {
+                        (bytes.to_vec(), dims)
+                    } else {
+                        log::error!(
+                            "Texture decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
+                            err,
+                            expected_len,
+                            dims.0,
+                            dims.1,
+                            bytes.len()
+                        );
+                        (vec![255, 0, 255, 255], (1, 1))
+                    }
+                } else {
+                    log::error!(
+                        "Texture decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.",
+                        err
+                    );
+                    (vec![255, 0, 255, 255], (1, 1))
+                }
+            }
+        };
         let size = wgpu::Extent3d {
             width: dimensions.0,
             height: dimensions.1,
@@ -219,7 +248,6 @@ impl Texture {
         let texture = device.create_texture(&desc);
 
         let unpadded_bytes_per_row = 4 * size.width;
-        debug_assert!(unpadded_bytes_per_row.is_power_of_two());
         let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1) & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1);
         debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize);
 
@@ -239,34 +267,34 @@ impl Texture {
                 },
                 size,
             );
-        }
+        } else {
+            let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize];
+            let src_stride = unpadded_bytes_per_row as usize;
+            let dst_stride = padded_bytes_per_row as usize;
+            for row in 0..size.height as usize {
+                let src_start = row * src_stride;
+                let dst_start = row * dst_stride;
+                padded[dst_start..dst_start + src_stride]
+                    .copy_from_slice(&diffuse_rgba[src_start..src_start + src_stride]);
+            }
 
-        let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize];
-        let src_stride = unpadded_bytes_per_row as usize;
-        let dst_stride = padded_bytes_per_row as usize;
-        for row in 0..size.height as usize {
-            let src_start = row * src_stride;
-            let dst_start = row * dst_stride;
-            padded[dst_start..dst_start + src_stride]
-                .copy_from_slice(&diffuse_rgba.as_bytes()[src_start..src_start + src_stride]);
+            queue.write_texture(
+                wgpu::TexelCopyTextureInfo {
+                    texture: &texture,
+                    mip_level: 0,
+                    origin: wgpu::Origin3d::ZERO,
+                    aspect: wgpu::TextureAspect::All,
+                },
+                &padded,
+                wgpu::TexelCopyBufferLayout {
+                    offset: 0,
+                    bytes_per_row: Some(padded_bytes_per_row),
+                    rows_per_image: Some(size.height),
+                },
+                size,
+            );
         }
 
-        queue.write_texture(
-            wgpu::TexelCopyTextureInfo {
-                texture: &texture,
-                mip_level: 0,
-                origin: wgpu::Origin3d::ZERO,
-                aspect: wgpu::TextureAspect::All,
-            },
-            &padded,
-            wgpu::TexelCopyBufferLayout {
-                offset: 0,
-                bytes_per_row: Some(padded_bytes_per_row),
-                rows_per_image: Some(size.height),
-            },
-            size,
-        );
-
         let sampler_desc = if let Some(sampler) = sampler {
             sampler
         } else {
diff --git a/crates/euca-runner/Cargo.toml b/crates/euca-runner/Cargo.toml
new file mode 100644
index 0000000..5cbba10
--- /dev/null
+++ b/crates/euca-runner/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "euca-runner"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+readme = "README.md"
+
+[dependencies]
+eucalyptus-core = { path = "../eucalyptus-core" }
+redback-runtime = { path = "../redback-runtime", default-features = false }
+dropbear-engine = { path = "../dropbear-engine" }
+
+tokio.workspace = true
+anyhow.workspace = true
+postcard.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+app_dirs2.workspace = true
+chrono.workspace = true
+env_logger.workspace = true
+colored.workspace = true
+ron.workspace = true
+hecs.workspace = true
+bytemuck.workspace = true
+log-once.workspace = true
+winit.workspace = true
+serde.workspace = true
+crossbeam-channel.workspace = true
+gilrs.workspace = true
+futures.workspace = true
diff --git a/crates/euca-runner/README.md b/crates/euca-runner/README.md
new file mode 100644
index 0000000..695914d
--- /dev/null
+++ b/crates/euca-runner/README.md
@@ -0,0 +1,5 @@
+# euca-runner
+
+Runs any app made with the eucalyptus editor. 
+
+Bascially redback-runtime without the debug feature. 
\ No newline at end of file
diff --git a/crates/euca-runner/src/main.rs b/crates/euca-runner/src/main.rs
new file mode 100644
index 0000000..b3bc66c
--- /dev/null
+++ b/crates/euca-runner/src/main.rs
@@ -0,0 +1,121 @@
+use app_dirs2::AppInfo;
+use dropbear_engine::future::FutureQueue;
+use eucalyptus_core::runtime::RuntimeProjectConfig;
+use parking_lot::RwLock;
+use redback_runtime::PlayMode;
+use std::env::current_exe;
+use std::fs;
+use std::rc::Rc;
+use std::sync::Arc;
+use ron::ser::PrettyConfig;
+use winit::window::WindowAttributes;
+use dropbear_engine::{DropbearAppBuilder, DropbearWindowBuilder};
+use serde::{Deserialize, Serialize};
+
+#[tokio::main]
+async fn main() {
+    env_logger::init();
+
+    dropbear_engine::panic::set_hook();
+    log::debug!("Set panic hook");
+
+    let window_config_file = current_exe().unwrap()
+        .parent()
+        .ok_or(anyhow::anyhow!(
+            "Unable to get parent of current executable"
+        )).unwrap()
+        .join("config.eucfg");
+    log::debug!("Fetched window config file path: {}", window_config_file.display());
+
+    log::debug!("Reading from window config file");
+    let value = fs::read(&window_config_file);
+
+    let config = match value {
+        Ok(val) => {
+            log::debug!("Config file exists, reading contents");
+            let config = ron::de::from_bytes::<ConfigFile>(val.as_slice()).unwrap();
+            log::debug!("File converted to ConfigFile");
+            config
+        }
+        Err(e) => {
+            log::warn!("Unable to read config: {}", e);
+            log::warn!("Creating new config file to overwrite old one");
+            let cfg = ConfigFile {
+                jvm_args: None,
+                max_fps: 60,
+                target_resolution: WindowModes::Windowed(1920, 1080),
+            };
+            let vec = ron::ser::to_string_pretty(&cfg, PrettyConfig::default()).unwrap();
+            if let Err(e) = fs::write(&window_config_file, vec) {
+                log::warn!("Unable to write, still running game: {}", e);
+            }
+            cfg
+        }
+    };
+
+    let path = current_exe().unwrap()
+        .parent()
+        .ok_or(anyhow::anyhow!(
+                "Unable to locate parent folder for current executable"
+            )).unwrap()
+        .join("data.eupak");
+    log::debug!("scene config (potential) file path: {}", path.display());
+
+    let scene_config = fs::read(&path).unwrap();
+    log::debug!("Located scene config file: [{}] ({} bytes)", path.display(), scene_config.len());
+
+    let scene_config: RuntimeProjectConfig = postcard::from_bytes(&scene_config).unwrap();
+
+    log::debug!("Converted scene config file to RuntimeProjectConfig");
+
+    let runtime_scene = Rc::new(RwLock::new(PlayMode::new(None).unwrap()));
+    let future_queue = Arc::new(FutureQueue::new());
+
+    let authors = scene_config.authors.developer.clone();
+    let project_name = scene_config.project_name.clone();
+
+    let name = Box::leak(project_name.into_boxed_str());
+    let author = Box::leak(authors.into_boxed_str());
+
+    log::debug!("Loading {} by {}", name, author);
+
+    let attributes = WindowAttributes::default();
+
+    match config.target_resolution {
+        WindowModes::Windowed(_, _) => {}
+        WindowModes::Maximised => {}
+        WindowModes::Fullscreen => {}
+    }
+
+    let window = DropbearWindowBuilder::new()
+        .with_attributes(attributes)
+        .add_scene_with_input(runtime_scene, "runtime_scene")
+        .set_initial_scene("runtime_scene")
+        .build();
+
+    log::debug!("Running dropbear app");
+
+    DropbearAppBuilder::new()
+        .add_window(window)
+        .max_fps(config.max_fps)
+        .app_data(AppInfo {
+            name,
+            author,
+        })
+        .with_future_queue(future_queue)
+        .run().await.unwrap();
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct ConfigFile {
+    pub jvm_args: Option<String>,
+    pub max_fps: u32,
+    pub target_resolution: WindowModes
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub enum WindowModes {
+    Windowed(u32, u32),
+    Maximised,
+    Fullscreen,
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index e743cae..631d5c1 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -101,9 +101,10 @@ impl ProjectConfig {
     /// # Parameters
     /// * path - The root config **file** for the project
     pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> {
-        let ron_str = fs::read_to_string(path.as_ref())?;
+        let path = path.as_ref().to_path_buf();
+        let ron_str = fs::read_to_string(&path)?;
         let mut config: ProjectConfig = ron::de::from_str(ron_str.as_str())?;
-        config.project_path = path.as_ref().parent().unwrap().to_path_buf();
+        config.project_path = path.parent().unwrap().to_path_buf();
         log::info!("Loaded project!");
         log::debug!("Loaded config info");
         log::debug!("Updating with new content");
@@ -129,7 +130,7 @@ impl ProjectConfig {
                     if io_err.kind() == std::io::ErrorKind::NotFound {
                         log::warn!("resources.eucc not found, creating default.");
                         let default = ResourceConfig {
-                            path: project_root.join("/resources"),
+                            path: project_root.join("resources"),
                             nodes: vec![],
                         };
                         log::debug!("Writing to {}", default.path.display());
@@ -156,7 +157,7 @@ impl ProjectConfig {
                     if io_err.kind() == std::io::ErrorKind::NotFound {
                         log::warn!("source.eucc not found, creating default.");
                         let default = SourceConfig {
-                            path: project_root.join("../../../src"),
+                            path: project_root.join("src"),
                             nodes: vec![],
                         };
                         default.write_to(&project_root)?;
@@ -343,14 +344,14 @@ impl ResourceConfig {
     /// # Parameters
     /// - path: The root **folder** of the project
     pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
-        let resource_dir = path.as_ref().join("../../../resources");
+        let resource_dir = path.as_ref().join("resources");
         let updated_config = ResourceConfig {
             path: resource_dir.clone(),
             nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["thumbnails"].as_slice()),
         };
         let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default())
             .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?;
-        let config_path = path.as_ref().join("../../../resources").join("resources.eucc");
+        let config_path = path.as_ref().join("resources").join("resources.eucc");
         fs::create_dir_all(config_path.parent().unwrap())?;
         fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
         Ok(())
@@ -370,7 +371,7 @@ impl ResourceConfig {
     /// # Parameters
     /// - path: The location to the **resources.eucc** file
     pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> {
-        let config_path = path.as_ref().join("../../../resources").join("resources.eucc");
+        let config_path = path.as_ref().join("resources").join("resources.eucc");
         let ron_str = fs::read_to_string(&config_path)?;
         let config: ResourceConfig = ron::de::from_str(&ron_str)
             .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?;
@@ -396,7 +397,7 @@ impl SourceConfig {
     /// # Parameters
     /// - path: The root **folder** of the project
     pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
-        let resource_dir = path.as_ref().join("../../../src");
+        let resource_dir = path.as_ref().join("src");
         let updated_config = SourceConfig {
             path: resource_dir.clone(),
             nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["scripts"].as_slice()),
@@ -404,7 +405,7 @@ impl SourceConfig {
 
         let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default())
             .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?;
-        let config_path = path.as_ref().join("../../../src").join("source.eucc");
+        let config_path = path.as_ref().join("src").join("source.eucc");
         fs::create_dir_all(config_path.parent().unwrap())?;
         fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
         Ok(())
@@ -413,7 +414,7 @@ impl SourceConfig {
     /// # Parameters
     /// - path: The location to the **source.eucc** file
     pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> {
-        let config_path = path.as_ref().join("../../../src").join("source.eucc");
+        let config_path = path.as_ref().join("src").join("source.eucc");
         let ron_str = fs::read_to_string(&config_path)?;
         let config: SourceConfig = ron::de::from_str(&ron_str)
             .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?;
@@ -459,7 +460,7 @@ fn collect_nodes(
                     ResourceType::Model
                 } else if parent_folder.contains("texture") {
                     ResourceType::Texture
-                } else if parent_folder.contains("shader") {
+                } else if parent_folder.contains("shaders") {
                     ResourceType::Shader
                 } else if entry_path
                     .extension()
diff --git a/crates/eucalyptus-core/src/physics/collider/shader.rs b/crates/eucalyptus-core/src/physics/collider/shader.rs
index 50d6368..e1067df 100644
--- a/crates/eucalyptus-core/src/physics/collider/shader.rs
+++ b/crates/eucalyptus-core/src/physics/collider/shader.rs
@@ -3,6 +3,7 @@
 use std::mem::size_of;
 use std::sync::Arc;
 
+use dropbear_engine::pipelines::DropbearShaderPipeline;
 use dropbear_engine::{entity::Transform, texture::Texture};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::shader::Shader;
@@ -12,17 +13,17 @@ use glam::Mat4;
 use crate::physics::collider::{ColliderShape, WireframeGeometry};
 
 pub struct ColliderWireframePipeline {
+    pub shader: Shader,
+    pub pipeline_layout: PipelineLayout,
     pub pipeline: RenderPipeline,
 }
 
-impl ColliderWireframePipeline {
-    pub fn new(
-        graphics: Arc<SharedGraphicsContext>,
-    ) -> Self {
+impl DropbearShaderPipeline for ColliderWireframePipeline {
+    fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
         let shader = Shader::new(
             graphics.clone(),
-            include_str!("../../../../../resources/shaders/collider.wgsl"),
-            Some("collider wireframe shader"),
+            include_str!("shaders/collider.wgsl"),
+            Some("collider wireframe shaders"),
         );
 
         let pipeline_layout = graphics.device.create_pipeline_layout(&PipelineLayoutDescriptor {
@@ -90,7 +91,23 @@ impl ColliderWireframePipeline {
             cache: None,
         });
 
-        Self { pipeline }
+        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
     }
 }
 
diff --git a/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl b/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
new file mode 100644
index 0000000..b899ac9
--- /dev/null
+++ b/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
@@ -0,0 +1,45 @@
+struct CameraUniform {
+    view_pos: vec4<f32>,
+    view_proj: 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/scripting.rs b/crates/eucalyptus-core/src/scripting.rs
index d4a8ef7..4926101 100644
--- a/crates/eucalyptus-core/src/scripting.rs
+++ b/crates/eucalyptus-core/src/scripting.rs
@@ -675,14 +675,14 @@ impl Drop for ScriptManager {
 fn get_gradle_command(project_root: impl AsRef<Path>) -> String {
     let project_root = project_root.as_ref().to_owned();
     if cfg!(target_os = "windows") {
-        let gradlew = project_root.join("../../../gradlew.bat");
+        let gradlew = project_root.join("gradlew.bat");
         if gradlew.exists() {
             gradlew.to_string_lossy().to_string()
         } else {
             "gradle.bat".to_string()
         }
     } else {
-        let gradlew = project_root.join("../../../gradlew");
+        let gradlew = project_root.join("gradlew");
         if gradlew.exists() {
             "./gradlew".to_string()
         } else {
@@ -707,14 +707,14 @@ pub async fn build_jvm(
     let _ = status_sender.send(BuildStatus::Started);
 
     let _ = status_sender.send(BuildStatus::Building(format!("Building manifest at {}", project_root.join("build/magna-carta/jvmMain/RunnableRegistry.kt").display())));
-    if let Err(e) = magna_carta::parse(project_root.join("../../../src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) {
+    if let Err(e) = magna_carta::parse(project_root.join("src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) {
         let _ = status_sender.send(BuildStatus::Failed(format!("Failed to build manifest: {}", e)));
         return Err(e);
     }
     let _ = status_sender.send(BuildStatus::Building(String::from("Successfully built manifest!")));
 
     if !(project_root.join("build.gradle").exists()
-        || project_root.join("../../../build.gradle.kts").exists())
+        || project_root.join("build.gradle.kts").exists())
     {
         let err = format!("No Gradle build script found in: {:?}", project_root);
         let _ = status_sender.send(BuildStatus::Failed(err.clone()));
@@ -766,7 +766,7 @@ pub async fn build_jvm(
         return Err(anyhow::anyhow!(err_msg));
     }
 
-    let libs_dir = project_root.join("../../../build").join("libs");
+    let libs_dir = project_root.join("build").join("libs");
     if !libs_dir.exists() {
         let err = "Build succeeded but 'build/libs' directory is missing".to_string();
         let _ = status_sender.send(BuildStatus::Failed(err.clone()));
@@ -870,7 +870,7 @@ pub async fn build_native(
     }
 
     let _ = status_sender.send(BuildStatus::Building(format!("Building manifest at {}", project_root.join("build/magna-carta/jvmMain/RunnableRegistry.kt").display())));
-    if let Err(e) = magna_carta::parse(project_root.join("../../../src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) {
+    if let Err(e) = magna_carta::parse(project_root.join("src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) {
         let _ = status_sender.send(BuildStatus::Failed(format!("Failed to build manifest: {}", e)));
         return Err(e);
     }
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 7dbd854..a0e0ad7 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -16,7 +16,6 @@ use once_cell::sync::Lazy;
 use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
 use std::borrow::Borrow;
-use std::collections::HashMap;
 use std::fmt;
 use std::fmt::{Display, Formatter};
 use std::ops::{Deref, DerefMut};
@@ -138,7 +137,7 @@ impl Display for ResourceType {
             ResourceType::Unknown => "unknown",
             ResourceType::Model => "model",
             ResourceType::Texture => "texture",
-            ResourceType::Shader => "shader",
+            ResourceType::Shader => "shaders",
             ResourceType::Thumbnail => "thumbnail",
             ResourceType::Script => "script",
             ResourceType::Config => "eucalyptus project config",
@@ -280,14 +279,6 @@ pub enum WorldLoadingStatus {
     Completed,
 }
 
-#[derive(Clone, Serialize, Deserialize)]
-pub struct RuntimeData {
-    pub project_config: ProjectConfig,
-    pub source_config: SourceConfig,
-    pub scene_data: Vec<SceneConfig>,
-    pub scripts: HashMap<String, String>,
-}
-
 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, SerializableComponent)]
 pub struct Label(String);
 
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index a471eaa..8a5e054 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -274,7 +274,7 @@ impl ResolveReference for ResourceReference {
                     };
 
                     if !project_path.as_os_str().is_empty() {
-                        let root = project_path.join("../../../resources");
+                        let root = project_path.join("resources");
                         return resolve_resource_from_root(relative, &root);
                     }
                 }
@@ -294,7 +294,7 @@ fn runtime_resources_dir() -> anyhow::Result<PathBuf> {
     let dir = current_exe
         .parent()
         .ok_or_else(|| anyhow::anyhow!("Unable to locate parent folder of runtime executable"))?;
-    let resources_dir = dir.join("../../../resources");
+    let resources_dir = dir.join("resources");
     if !resources_dir.exists() {
         anyhow::bail!(
             "Runtime resources directory is missing at '{}'. Ensure the packaged build includes a 'resources' folder next to the executable (current exe: {}).",
diff --git a/crates/eucalyptus-editor/src/build.rs b/crates/eucalyptus-editor/src/build.rs
index 4520cf8..e86eb37 100644
--- a/crates/eucalyptus-editor/src/build.rs
+++ b/crates/eucalyptus-editor/src/build.rs
@@ -82,8 +82,8 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> {
     log::debug!("Exported scene config to {:?}", eupak_path);
 
     // copy resources
-    let resources_src = project_root.join("../../../resources");
-    let resources_dst = build_dir.join("../../../resources");
+    let resources_src = project_root.join("resources");
+    let resources_dst = build_dir.join("resources");
     if resources_src.exists() {
         copy_dir_recursive(&resources_src, &resources_dst)?;
         log::debug!("Copied resources to {:?}", resources_dst);
@@ -212,8 +212,8 @@ pub async fn package(
     }
     tokio_fs::copy(&data_src, package_dir.join("data.eupak")).await?;
 
-    let resources_src = build_dir.join("../../../resources");
-    let resources_dst = package_dir.join("../../../resources");
+    let resources_src = build_dir.join("resources");
+    let resources_dst = package_dir.join("resources");
     if resources_src.exists() && !resources_dst.exists() {
         let src = resources_src.clone();
         let dst = resources_dst.clone();
@@ -231,7 +231,7 @@ pub async fn package(
     let magna_output_dir = project_root.join("build/magna-carta/nativeLibMain");
     tokio_fs::create_dir_all(&magna_output_dir).await?;
 
-    magna_carta::parse(project_root.join("../../../src"), Target::Native, &magna_output_dir)?;
+    magna_carta::parse(project_root.join("src"), Target::Native, &magna_output_dir)?;
 
     // let magna_status = Command::new("magna-carta")
     //     .arg("--input")
@@ -600,7 +600,7 @@ async fn run_gradle_build(project_root: &Path) -> anyhow::Result<()> {
 
     #[cfg(not(windows))]
     {
-        let script = project_root.join("../../../gradlew");
+        let script = project_root.join("gradlew");
         if !script.exists() {
             bail!("Gradle wrapper not found at {}", script.display());
         }
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index 6590b8d..04f0bb3 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -1149,7 +1149,7 @@ impl<'a> EditorTabViewer<'a> {
     ) {
         let label = "euca://resources";
         builder.node(Self::dir_node_labeled(label, "resources"));
-        let resources_root = project_root.join("../../../../resources");
+        let resources_root = project_root.join("resources");
         if resources_root.exists() {
             Self::walk_resource_directory(cfg, builder, &resources_root, &resources_root);
         } else {
@@ -1219,7 +1219,7 @@ impl<'a> EditorTabViewer<'a> {
     ) {
         let label = "euca://scripts";
         builder.node(Self::dir_node_labeled(label, "scripts"));
-        let scripts_root = project_root.join("../../../../src");
+        let scripts_root = project_root.join("src");
         if !scripts_root.exists() {
             Self::walk_resource_directory(cfg, builder, &scripts_root, &scripts_root);
             builder.close_dir();
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index b4c171d..ed8cb05 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -15,9 +15,9 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
 use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::EntityTransform;
 use dropbear_engine::graphics::InstanceRaw;
-use dropbear_engine::shader::Shader;
+use dropbear_engine::pipelines::light_cube::LightCubePipeline;
 use dropbear_engine::texture::TextureWrapMode;
-use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, lighting::LightManager, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData};
+use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData};
 use egui::{self, Context};
 use egui_dock::{DockArea, DockState, NodeIndex, Style};
 use eucalyptus_core::{register_components, APP_INFO};
@@ -53,10 +53,13 @@ use std::rc::Rc;
 use log::debug;
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
-use wgpu::{Color, Extent3d, RenderPipeline};
+use wgpu::{Color, Extent3d};
 use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
 use winit::dpi::PhysicalSize;
+use dropbear_engine::pipelines::DropbearShaderPipeline;
+use dropbear_engine::pipelines::shader::MainRenderPipeline;
+use dropbear_engine::pipelines::GlobalsUniform;
 use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline;
@@ -71,14 +74,17 @@ pub struct Editor {
     pub dock_state: DockState<EditorTab>,
     pub texture_id: Option<egui::TextureId>,
     pub size: Extent3d,
-    pub render_pipeline: Option<RenderPipeline>,
     pub instance_buffer_cache: HashMap<ModelId, ResizableBuffer<InstanceRaw>>,
-    pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     pub collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>,
     pub collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>,
-    pub light_manager: LightManager,
     pub color: Color,
 
+    // rendering
+    pub light_cube_pipeline: Option<LightCubePipeline>,
+    pub main_render_pipeline: Option<MainRenderPipeline>,
+    pub shader_globals: Option<GlobalsUniform>,
+    pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
+
     pub active_camera: Arc<Mutex<Option<Entity>>>,
 
     pub is_viewport_focused: bool,
@@ -186,7 +192,8 @@ impl Editor {
             dock_state,
             texture_id: None,
             size: Extent3d::default(),
-            render_pipeline: None,
+            main_render_pipeline: None,
+            shader_globals: None,
             color: Color::default(),
             is_viewport_focused: false,
             // is_cursor_locked: false,
@@ -208,7 +215,7 @@ impl Editor {
             gizmo_orientation: GizmoOrientation::Global,
             play_mode_backup: None,
             input_state: Box::new(InputState::new()),
-            light_manager: LightManager::new(),
+            light_cube_pipeline: None,
             active_camera: Arc::new(Mutex::new(None)),
             progress_tx: None,
             is_world_loaded: IsWorldLoadedYet::new(),
@@ -616,9 +623,10 @@ impl Editor {
         self.previously_selected_entity = None;
         self.active_camera.lock().take();
 
-        self.render_pipeline = None;
+        self.main_render_pipeline = None;
+        self.shader_globals = None;
         self.texture_id = None;
-        self.light_manager = LightManager::new();
+        self.light_cube_pipeline = None;
 
         {
             let mut cache = MODEL_CACHE.lock();
@@ -1042,13 +1050,22 @@ impl Editor {
 
         let editor_ptr = self as *mut Editor;
 
+        let Some(view) = self.texture_id else {
+            egui::CentralPanel::default().show(ctx, |ui| {
+                ui.centered_and_justified(|ui| {
+                    ui.label("Viewport is still initialising...");
+                });
+            });
+            return;
+        };
+
         egui::CentralPanel::default().show(ctx, |ui| {
             DockArea::new(&mut self.dock_state)
                 .style(Style::from_egui(ui.style().as_ref()))
                 .show_inside(
                     ui,
                     &mut EditorTabViewer {
-                        view: self.texture_id.unwrap(),
+                        view,
                         gizmo: &mut self.gizmo,
                         tex_size: self.size,
                         world: &mut self.world,
@@ -1302,38 +1319,12 @@ impl Editor {
     ///
     /// **Note**: To be ran AFTER [`Editor::load_project_config`]
     pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
-        let shader = Shader::new(
-            graphics.clone(),
-            include_str!("../../../../resources/shaders/shader.wgsl"),
-            Some("viewport shader"),
-        );
-
-        self.light_manager
-            .create_light_array_resources(graphics.clone());
-
-        let pipeline = graphics.create_render_pipline(
-            &shader,
-            vec![
-                &graphics.layouts.texture_bind_layout,
-                &graphics.layouts.camera_bind_group_layout,
-                &graphics.layouts.light_array_bind_group_layout,
-                &graphics.layouts.material_tint_bind_layout,
-            ],
-            None,
-        );
-        self.render_pipeline = Some(pipeline);
-
-        self.light_manager.create_render_pipeline(
-            graphics.clone(),
-            include_str!("../../../../resources/shaders/light.wgsl"),
-            Some("Light Pipeline"),
-        );
-
-        let collider_pipeline = ColliderWireframePipeline::new(graphics.clone());
-        self.collider_wireframe_pipeline = Some(collider_pipeline);
+        self.main_render_pipeline = Some(MainRenderPipeline::new(graphics.clone()));
+        self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone()));
+        self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("editor shader globals")));
+        self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
 
         self.texture_id = Some((*graphics.texture_id).clone());
-
         self.window = Some(graphics.window.clone());
         self.is_world_loaded.mark_rendering_loaded();
     }
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index ffdd8e9..9c6ac22 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -11,7 +11,7 @@ use dropbear_engine::graphics::{FrameGraphicsContext, InstanceRaw};
 use dropbear_engine::model::MODEL_CACHE;
 use dropbear_engine::{
     entity::{EntityTransform, MeshRenderer, Transform},
-    lighting::{Light, LightComponent},
+    lighting::{Light, LightComponent, MAX_LIGHTS},
     model::{DrawLight, DrawModel},
     scene::{Scene, SceneCommand},
 };
@@ -333,13 +333,12 @@ impl Scene for Editor {
             }
         }
 
-        {
-            let sim_world = self.world.as_ref();
-
-            self.light_manager
-                .update(graphics.clone(), sim_world);
+        if let Some(l) = &mut self.light_cube_pipeline {
+            l.update(graphics.clone(), &self.world);
+        }
 
-            self.nerd_stats.write().record_stats(dt, sim_world.len() as u32);
+        {
+            self.nerd_stats.write().record_stats(dt, self.world.len() as u32);
         }
 
         self.input_state.window = self.window.clone();
@@ -348,6 +347,10 @@ impl Scene for Editor {
     }
 
     fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) {
+        self.size = graphics.viewport_texture.size;
+        self.texture_id = Some(*graphics.texture_id.clone());
+        self.window = Some(graphics.window.clone());
+
         self.show_ui(&graphics.get_egui_context());
         let clear_color = Color {
             r: 100.0 / 255.0,
@@ -356,9 +359,6 @@ impl Scene for Editor {
             a: 1.0,
         };
         self.color = clear_color;
-        self.size = graphics.viewport_texture.size;
-        self.texture_id = Some(*graphics.texture_id.clone());
-        self.window = Some(graphics.window.clone());
         eucalyptus_core::logging::render(&graphics.get_egui_context());
 
         let cam = {
@@ -379,7 +379,7 @@ impl Scene for Editor {
         log_once::debug_once!("Camera ready");
         log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
-        let Some(pipeline) = &self.render_pipeline else {
+        let Some(pipeline) = &self.main_render_pipeline else {
             log_once::warn_once!("Render pipeline not ready");
             return;
         };
@@ -394,6 +394,16 @@ impl Scene for Editor {
             lights
         };
 
+            if let Some(globals) = &mut self.shader_globals {
+                let enabled_count = lights
+                    .iter()
+                    .filter(|(_, comp)| comp.enabled)
+                    .take(MAX_LIGHTS)
+                    .count() as u32;
+                globals.set_num_lights(enabled_count);
+                globals.write(&graphics.queue);
+            }
+
         let renderers = {
             let mut renderers = Vec::new();
             let mut query = self.world.query::<&MeshRenderer>();
@@ -455,8 +465,6 @@ impl Scene for Editor {
             }
         }
 
-        self.light_manager.update(graphics.clone(), &self.world);
-
         {
             let mut render_pass = frame_ctx.encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -481,8 +489,8 @@ impl Scene for Editor {
                     occlusion_query_set: None,
                     timestamp_writes: None,
                 });
-            if let Some(light_pipeline) = &self.light_manager.pipeline {
-                render_pass.set_pipeline(light_pipeline);
+            if let Some(light_pipeline) = &self.light_cube_pipeline {
+                render_pass.set_pipeline(light_pipeline.pipeline());
                 for (light, component) in &lights {
                     render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
                     if component.visible {
@@ -495,39 +503,47 @@ impl Scene for Editor {
                 }
             }
         }
-
-        for (model, instance_buffer, instance_count) in prepared_models {
-            let mut render_pass = frame_ctx.encoder
-                .begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("model render pass"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &frame_ctx.view,
-                        depth_slice: None,
-                        resolve_target: None,
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                        view: &graphics.depth_texture.view,
-                        depth_ops: Some(wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
+        if let Some(lcp) = &self.light_cube_pipeline {
+            for (model, instance_buffer, instance_count) in prepared_models {
+                let globals_bind_group = &self
+                    .shader_globals
+                    .as_ref()
+                    .expect("Shader globals not initialised")
+                    .bind_group;
+
+                let mut render_pass = frame_ctx.encoder
+                    .begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("model render pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: &frame_ctx.view,
+                            depth_slice: None,
+                            resolve_target: None,
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
                         }),
-                        stencil_ops: None,
-                    }),
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
-            render_pass.set_pipeline(pipeline);
-            render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
-            render_pass.draw_model_instanced(
-                &model,
-                0..instance_count,
-                &camera.bind_group,
-                self.light_manager.bind_group(),
-            );
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
+                render_pass.set_pipeline(pipeline.pipeline());
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                render_pass.set_bind_group(4, globals_bind_group, &[]);
+                render_pass.draw_model_instanced(
+                    &model,
+                    0..instance_count,
+                    &camera.bind_group,
+                    lcp.bind_group(),
+                );
+            }
         }
 
         {
diff --git a/crates/eucalyptus-editor/src/menu.rs b/crates/eucalyptus-editor/src/menu.rs
index e886b5a..caf6fec 100644
--- a/crates/eucalyptus-editor/src/menu.rs
+++ b/crates/eucalyptus-editor/src/menu.rs
@@ -6,6 +6,7 @@ use eucalyptus_core::config::ProjectConfig;
 use eucalyptus_core::states::PROJECT;
 use git2::Repository;
 use log::{self, debug};
+use log_once::debug_once;
 use rfd::FileDialog;
 use std::sync::Arc;
 use std::{fs, path::PathBuf};
@@ -143,7 +144,7 @@ impl MainMenu {
 
                             fs::write(&dest_script_path, content)?;
 
-                            let build_gradle_path = project_root.join("../../../build.gradle.kts");
+                            let build_gradle_path = project_root.join("build.gradle.kts");
                             let gradle_content = fs::read_to_string(&build_gradle_path)?;
 
                             let updated_gradle_content = gradle_content
@@ -510,26 +511,26 @@ impl Mouse for MainMenu {
 
 impl Controller for MainMenu {
     fn button_down(&mut self, button: gilrs::Button, id: gilrs::GamepadId) {
-        debug!("Controller button {:?} pressed! [{}]", button, id);
+        debug_once!("Controller button {:?} pressed! [{}]", button, id);
     }
 
     fn button_up(&mut self, button: gilrs::Button, id: gilrs::GamepadId) {
-        debug!("Controller button {:?} released! [{}]", button, id);
+        debug_once!("Controller button {:?} released! [{}]", button, id);
     }
 
     fn left_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) {
-        debug!("Left stick changed: x = {} | y = {} | id = {}", x, y, id);
+        debug_once!("Left stick changed: x = {} | y = {} | id = {}", x, y, id);
     }
 
     fn right_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) {
-        debug!("Right stick changed: x = {} | y = {} | id = {}", x, y, id);
+        debug_once!("Right stick changed: x = {} | y = {} | id = {}", x, y, id);
     }
 
     fn on_connect(&mut self, id: gilrs::GamepadId) {
-        debug!("Controller connected [{}]", id);
+        debug_once!("Controller connected [{}]", id);
     }
 
     fn on_disconnect(&mut self, id: gilrs::GamepadId) {
-        debug!("Controller disconnected [{}]", id);
+        debug_once!("Controller disconnected [{}]", id);
     }
 }
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 8556ba7..1302e58 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -26,7 +26,7 @@ fn resolve_editor_path(uri: &str) -> PathBuf {
         let relative = relative_path_from_euca(uri)
             .unwrap_or_else(|_| uri.trim_start_matches(EUCA_SCHEME));
         let project_path = PROJECT.read().project_path.clone();
-        project_path.join("../../../resources").join(relative)
+        project_path.join("resources").join(relative)
     } else {
         PathBuf::from(uri)
     }
@@ -198,7 +198,7 @@ impl SignalController for Editor {
                             let cfg = PROJECT.read();
                             cfg.project_path.clone()
                         };
-                        let libs_dir = project_root.join("../../../build").join("libs");
+                        let libs_dir = project_root.join("build").join("libs");
                         if !libs_dir.exists() {
                             let err =
                                 "Build succeeded but 'build/libs' directory is missing".to_string();
diff --git a/crates/eucalyptus-editor/src/utils.rs b/crates/eucalyptus-editor/src/utils.rs
index d93493c..974229c 100644
--- a/crates/eucalyptus-editor/src/utils.rs
+++ b/crates/eucalyptus-editor/src/utils.rs
@@ -113,7 +113,7 @@ pub fn start_project_creation(
             ("git", 0.1, "Creating a git folder..."),
             ("src", 0.2, "Creating src folder..."),
             ("resources/models", 0.4, "Creating models folder..."),
-            ("resources/shaders", 0.6, "Creating shader folder..."),
+            ("resources/shaders", 0.6, "Creating shaders folder..."),
             ("resources/textures", 0.8, "Creating textures folder..."),
             ("src2", 0.9, "Creating project config file..."),
         ];
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index d904a9f..816d008 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -3,17 +3,18 @@
 use std::sync::Arc;
 use crossbeam_channel::{unbounded, Receiver};
 use dropbear_engine::buffer::ResizableBuffer;
+use dropbear_engine::pipelines::DropbearShaderPipeline;
+use dropbear_engine::pipelines::GlobalsUniform;
+use dropbear_engine::pipelines::light_cube::LightCubePipeline;
+use dropbear_engine::pipelines::shader::MainRenderPipeline;
 use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline;
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use futures::executor;
 use hecs::{Entity, World};
-use wgpu::{RenderPipeline};
 use dropbear_engine::future::FutureHandle;
 use dropbear_engine::graphics::SharedGraphicsContext;
-use dropbear_engine::lighting::LightManager;
 use dropbear_engine::scene::SceneCommand;
-use dropbear_engine::shader::Shader;
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
 use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT};
@@ -42,7 +43,7 @@ fn find_jvm_library_path() -> PathBuf {
     } else {
         proj.project_path.clone()
     }
-    .join("../../../../build/libs");
+    .join("build/libs");
 
     let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
 
@@ -80,8 +81,10 @@ pub struct PlayMode {
     active_camera: Option<Entity>,
     
     // rendering
-    render_pipeline: Option<RenderPipeline>,
-    light_manager: LightManager,
+    light_cube_pipeline: Option<LightCubePipeline>,
+    main_pipeline: Option<MainRenderPipeline>,
+    shader_globals: Option<GlobalsUniform>,
+    collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -103,7 +106,6 @@ pub struct PlayMode {
     collision_force_event_receiver: Option<std::sync::mpsc::Receiver<ContactForceEvent>>,
     event_collector: ChannelEventCollector,
 
-    collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>,
     collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>,
 }
@@ -135,8 +137,9 @@ impl PlayMode {
             pending_world: None,
             pending_camera: None,
             active_camera: None,
-            render_pipeline: None,
-            light_manager: Default::default(),
+            main_pipeline: None,
+            light_cube_pipeline: None,
+            shader_globals: None,
             scripts_ready: false,
             has_initial_resize_done: false,
             physics_pipeline: Default::default(),
@@ -157,35 +160,10 @@ impl PlayMode {
     }
 
     pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        let shader = Shader::new(
-            graphics.clone(),
-            include_str!("../../../resources/shaders/shader.wgsl"),
-            Some("viewport shader"),
-        );
-
-        self.light_manager
-            .create_light_array_resources(graphics.clone());
-
-        let pipeline = graphics.create_render_pipline(
-            &shader,
-            vec![
-                &graphics.layouts.texture_bind_layout,
-                &graphics.layouts.camera_bind_group_layout,
-                &graphics.layouts.light_array_bind_group_layout,
-                &graphics.layouts.material_tint_bind_layout,
-            ],
-            None,
-        );
-        self.render_pipeline = Some(pipeline);
-
-        self.light_manager.create_render_pipeline(
-            graphics.clone(),
-            include_str!("../../../resources/shaders/light.wgsl"),
-            Some("Light Pipeline"),
-        );
-
-        let collider_pipeline = ColliderWireframePipeline::new(graphics.clone());
-        self.collider_wireframe_pipeline = Some(collider_pipeline);
+        self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone()));
+        self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone()));
+        self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals")));
+        self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
     }
 
     fn reload_scripts_for_current_world(&mut self) {
@@ -305,7 +283,8 @@ impl PlayMode {
         self.physics_state = Box::new(PhysicsState::new());
         self.physics_receiver = None;
         self.active_camera = None;
-        self.render_pipeline = None;
+        self.main_pipeline = None;
+        self.light_cube_pipeline = None;
         self.current_scene = None;
         self.world_loading_progress = None;
         self.world_receiver = None;
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 6cdb995..4299975 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -1,6 +1,7 @@
 use std::sync::Arc;
 
 use std::collections::HashMap;
+use dropbear_engine::pipelines::DropbearShaderPipeline;
 use eucalyptus_core::egui::{CentralPanel, MenuBar, TopBottomPanel};
 use eucalyptus_core::physics::collider::ColliderGroup;
 use eucalyptus_core::physics::collider::ColliderShapeKey;
@@ -15,6 +16,7 @@ use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
 use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext, FrameGraphicsContext};
 use dropbear_engine::lighting::{Light, LightComponent};
+use dropbear_engine::lighting::MAX_LIGHTS;
 use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE};
 use dropbear_engine::scene::{Scene, SceneCommand};
 use eucalyptus_core::camera::CameraComponent;
@@ -362,9 +364,11 @@ impl Scene for PlayMode {
             }
         }
 
-        self.light_manager
-            .update(graphics.clone(), &self.world);
-
+        if let Some(l) = &mut self.light_cube_pipeline {
+            l.update(graphics.clone(), &self.world);
+        }
+        
+        #[cfg(feature = "debug")]
         TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| {
             MenuBar::new().ui(ui, |ui| {
                 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
@@ -466,7 +470,7 @@ impl Scene for PlayMode {
         log_once::debug_once!("Camera ready");
         log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
-        let Some(pipeline) = &self.render_pipeline else {
+        let Some(pipeline) = &self.main_pipeline else {
             log_once::warn_once!("Render pipeline not ready");
             return;
         };
@@ -488,6 +492,16 @@ impl Scene for PlayMode {
             lights
         };
 
+        if let Some(globals) = &mut self.shader_globals {
+            let enabled_count = lights
+                .iter()
+                .filter(|(_, comp)| comp.enabled)
+                .take(MAX_LIGHTS)
+                .count() as u32;
+            globals.set_num_lights(enabled_count);
+            globals.write(&graphics.queue);
+        }
+
         let renderers = {
             let mut renderers = Vec::new();
             let mut query = self.world.query::<&MeshRenderer>();
@@ -549,8 +563,6 @@ impl Scene for PlayMode {
             }
         }
 
-        self.light_manager.update(graphics.clone(), &self.world);
-
         {
             let mut render_pass = frame_ctx.encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -575,8 +587,8 @@ impl Scene for PlayMode {
                     occlusion_query_set: None,
                     timestamp_writes: None,
                 });
-            if let Some(light_pipeline) = &self.light_manager.pipeline {
-                render_pass.set_pipeline(light_pipeline);
+            if let Some(light_pipeline) = &self.light_cube_pipeline {
+                render_pass.set_pipeline(light_pipeline.pipeline());
                 for (light, component) in &lights {
                     render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
                     if component.visible {
@@ -591,6 +603,18 @@ impl Scene for PlayMode {
         }
 
         for (model, instance_buffer, instance_count) in prepared_models {
+            let light_bind_group = self
+                .light_cube_pipeline
+                .as_ref()
+                .expect("Light cube pipeline not initialised")
+                .bind_group();
+
+            let globals_bind_group = &self
+                .shader_globals
+                .as_ref()
+                .expect("Shader globals not initialised")
+                .bind_group;
+
             let mut render_pass = frame_ctx.encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("model render pass"),
@@ -614,13 +638,14 @@ impl Scene for PlayMode {
                     occlusion_query_set: None,
                     timestamp_writes: None,
                 });
-            render_pass.set_pipeline(pipeline);
+            render_pass.set_pipeline(pipeline.pipeline());
             render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+            render_pass.set_bind_group(4, globals_bind_group, &[]);
             render_pass.draw_model_instanced(
                 &model,
                 0..instance_count,
                 &camera.bind_group,
-                self.light_manager.bind_group(),
+                light_bind_group,
             );
         }
 
diff --git a/resources/shaders/collider.wgsl b/resources/shaders/collider.wgsl
deleted file mode 100644
index b899ac9..0000000
--- a/resources/shaders/collider.wgsl
+++ /dev/null
@@ -1,45 +0,0 @@
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view_proj: 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/resources/shaders/light.wgsl b/resources/shaders/light.wgsl
deleted file mode 100644
index b976066..0000000
--- a/resources/shaders/light.wgsl
+++ /dev/null
@@ -1,62 +0,0 @@
-// Shader for rendering the white cube for lighting (or diff depending on colour)
-
-struct Light {
-    position: vec4<f32>,
-    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
-    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
-    constant: f32,
-    lin: f32,
-    quadratic: f32,
-    cutoff: f32,
-}
-
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view_proj: mat4x4<f32>,
-};
-
-@group(0) @binding(0)
-var<uniform> camera: CameraUniform;
-
-@group(1) @binding(0)
-var<uniform> light: Light;
-
-struct InstanceInput {
-    @location(5) model_matrix_0: vec4<f32>,
-    @location(6) model_matrix_1: vec4<f32>,
-    @location(7) model_matrix_2: vec4<f32>,
-    @location(8) model_matrix_3: vec4<f32>,
-}
-
-struct VertexInput {
-    @location(0) position: vec3<f32>,
-};
-
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) color: vec3<f32>,
-};
-
-@vertex
-fn vs_main(
-    model: VertexInput,
-    instance: InstanceInput,
-) -> VertexOutput {
-    let model_matrix = mat4x4<f32>(
-        instance.model_matrix_0,
-        instance.model_matrix_1,
-        instance.model_matrix_2,
-        instance.model_matrix_3,
-    );
-    var out: VertexOutput;
-    out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0);
-    out.color = light.color.xyz;
-    return out;
-}
-
-// Fragment shader
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    return vec4<f32>(in.color, 1.0);
-}
\ No newline at end of file
diff --git a/resources/shaders/shader.wgsl b/resources/shaders/shader.wgsl
deleted file mode 100644
index 2ec6c26..0000000
--- a/resources/shaders/shader.wgsl
+++ /dev/null
@@ -1,245 +0,0 @@
-// Main shader for standard objects.
-
-const MAX_LIGHTS: u32 = 8;
-
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view_proj: 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, 1, 2)
-    constant: f32,
-    lin: f32,
-    quadratic: f32,
-    cutoff: f32,
-}
-
-struct LightArray {
-    _lights: array<Light, MAX_LIGHTS>,
-    light_count: u32,
-    ambient_strength: f32,
-}
-
-struct MaterialUniform {
-    // for stuff like tinting
-    colour: vec4<f32>,
-
-    // scales incoming UVs before sampling
-    uv_tiling: vec2<f32>,
-    _pad: vec2<f32>,
-}
-
-@group(0) @binding(0)
-var t_diffuse: texture_2d<f32>;
-@group(0) @binding(1)
-var s_diffuse: sampler;
-@group(0) @binding(2)
-var t_normal: texture_2d<f32>;
-@group(0) @binding(3)
-var s_normal: sampler;
-
-@group(3) @binding(0)
-var<uniform> u_material: MaterialUniform;
-
-@group(1) @binding(0)
-var<uniform> camera: CameraUniform;
-
-@group(2) @binding(0)
-var<storage, read> light_array: LightArray;
-
-struct InstanceInput {
-    @location(5) model_matrix_0: vec4<f32>,
-    @location(6) model_matrix_1: vec4<f32>,
-    @location(7) model_matrix_2: vec4<f32>,
-    @location(8) model_matrix_3: vec4<f32>,
-
-    @location(9) normal_matrix_0: vec3<f32>,
-    @location(10) normal_matrix_1: vec3<f32>,
-    @location(11) normal_matrix_2: vec3<f32>,
-};
-
-struct VertexInput {
-    @location(0) position: vec3<f32>,
-    @location(1) tex_coords: vec2<f32>,
-    @location(2) normal: vec3<f32>,
-    @location(3) tangent: vec3<f32>,
-    @location(4) bitangent: vec3<f32>,
-};
-
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) tex_coords: vec2<f32>,
-    @location(1) world_normal: vec3<f32>,
-    @location(2) world_position: vec3<f32>,
-    @location(3) world_tangent: vec3<f32>,
-    @location(4) world_bitangent: vec3<f32>,
-};
-
-@vertex
-fn vs_main(
-    model: VertexInput,
-    instance: InstanceInput,
-) -> VertexOutput {
-    let model_matrix = mat4x4<f32>(
-        instance.model_matrix_0,
-        instance.model_matrix_1,
-        instance.model_matrix_2,
-        instance.model_matrix_3,
-    );
-    let normal_matrix = mat3x3<f32>(
-        instance.normal_matrix_0,
-        instance.normal_matrix_1,
-        instance.normal_matrix_2,
-    );
-
-    let world_normal = normalize(normal_matrix * model.normal);
-    let world_tangent = normalize(normal_matrix * model.tangent);
-    let world_bitangent = normalize(normal_matrix * model.bitangent);
-    let world_position = model_matrix * vec4<f32>(model.position, 1.0);
-
-    var out: VertexOutput;
-    out.clip_position = camera.view_proj * world_position;
-    out.tex_coords = model.tex_coords;
-    out.world_normal = world_normal;
-    out.world_position = world_position.xyz;
-    out.world_tangent = world_tangent;
-    out.world_bitangent = world_bitangent;
-    return out;
-}
-
-fn directional_light(
-    light: Light,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>,
-    world_pos: vec3<f32>
-) -> vec3<f32> {
-    let light_dir = normalize(-light.direction.xyz);
-
-    let ambient = light.color.xyz * light_array.ambient_strength * tex_color;
-
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color;
-
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color;
-
-    return ambient + diffuse + specular;
-}
-
-fn point_light(
-    light: Light,
-    world_pos: vec3<f32>,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>
-) -> vec3<f32> {
-    let light_dir = normalize(light.position.xyz - world_pos);
-
-    let distance = length(light.position.xyz - world_pos);
-    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
-
-    let ambient = light.color.xyz * light_array.ambient_strength * tex_color;
-
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color;
-
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color;
-
-    return (ambient + diffuse + specular) * attenuation;
-}
-
-fn spot_light(
-    light: Light,
-    world_pos: vec3<f32>,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>
-) -> vec3<f32> {
-    let light_dir = normalize(light.position.xyz - world_pos);
-    let theta = dot(light_dir, normalize(-light.direction.xyz));
-    let outer_cutoff = light.direction.w;
-
-    let epsilon = light.cutoff - outer_cutoff;
-    let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
-
-    let distance = length(light.position.xyz - world_pos);
-    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
-
-    let ambient = light.color.xyz * light_array.ambient_strength * tex_color;
-
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color * intensity;
-
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color * intensity;
-
-    return (ambient + diffuse + specular) * attenuation;
-}
-
-fn apply_normal_map(
-    world_normal_in: vec3<f32>,
-    world_tangent_in: vec3<f32>,
-    world_bitangent_in: vec3<f32>,
-    normal_sample_rgb: vec3<f32>,
-) -> vec3<f32> {
-    let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0));
-
-    let n = normalize(world_normal_in);
-    var t = normalize(world_tangent_in);
-    t = normalize(t - n * dot(n, t));
-
-    let b_in = normalize(world_bitangent_in);
-    let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0);
-    let b = cross(n, t) * handedness;
-
-    let tbn = mat3x3<f32>(t, b, n);
-    return normalize(tbn * normal_ts);
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let uv = in.tex_coords * u_material.uv_tiling;
-    var tex_color = textureSample(t_diffuse, s_diffuse, uv);
-    var object_normal = textureSample(t_normal, s_normal, uv);
-
-    let base_colour = tex_color * u_material.colour;
-
-    if (base_colour.a < 0.1) {
-        discard;
-    }
-
-    let view_dir = normalize(camera.view_pos.xyz - in.world_position);
-
-    let world_normal = apply_normal_map(
-        in.world_normal,
-        in.world_tangent,
-        in.world_bitangent,
-        object_normal.xyz,
-    );
-
-    var final_color = vec3<f32>(0.0);
-
-    for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) {
-        let light = light_array._lights[i];
-
-        let light_type = i32(light.color.w + 0.1);
-
-        if (light_type == 0) {
-            final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position);
-        } else if (light_type == 1) {
-            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
-        } else if (light_type == 2) {
-            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
-        }
-    }
-
-    return vec4<f32>(final_color, base_colour.a);
-}
\ No newline at end of file