kitgit

tirbofish/dropbear · commit

67dbcbe58f0a5f52c4f59b40a36f5103691c15dc

feature: skybox fix: lighting todo: allow for customisable skyboxes (user defined).

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-02-07 14:40

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 4aa4d1e..aed3628 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -83,9 +83,9 @@ combine = "4.6"
 glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" }
 
 [workspace.dependencies.image]
-version = "0.25"
+version = "0.24"
 default-features = false
-features = ["png"]
+features = ["png", "jpeg", "hdr"]
 
 [profile.dev]
 opt-level = 1
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index bce2147..b972591 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -44,16 +44,12 @@ dashmap.workspace = true
 typetag.workspace = true
 postcard.workspace = true
 pollster.workspace = true
+image.workspace = true
 #yakui-wgpu.workspace = true
 #yakui.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
 
-[dependencies.image]
-version = "0.25"
-default-features = false
-features = ["png", "jpeg"]
-
 [build-dependencies]
 slank = { path = "../slank", features = ["download-slang"] }
\ No newline at end of file
diff --git a/crates/dropbear-engine/build.rs b/crates/dropbear-engine/build.rs
index 7f01aa4..d495733 100644
--- a/crates/dropbear-engine/build.rs
+++ b/crates/dropbear-engine/build.rs
@@ -5,15 +5,12 @@ fn main() {
     // let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube")));
 
     SlangShaderBuilder::new("light_cube")
-        .add_source_path("src/pipelines/shaders/light.slang").unwrap()
+        .add_source_path("src/shaders/light.slang").unwrap()
         .compile_to_out_dir(SlangTarget::SpirV).unwrap();
 
     SlangShaderBuilder::new("blit_shader")
-        .add_source_path("src/pipelines/shaders/blit.slang").unwrap()
+        .add_source_path("src/shaders/blit.slang").unwrap()
         .compile_to_out_dir(SlangTarget::SpirV).unwrap();
 
-    // unable to do mipmap.slang because it required texture_storage_2d, one write and one read. 
-    // just wasn't possible with slang :(
-
-    
+    println!("cargo:rerun-if-changed=src/shaders");
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index efb966a..089c09d 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -236,8 +236,9 @@ impl Camera {
     }
 
     pub fn update_view_proj(&mut self) {
-        let mvp = self.build_vp();
-        self.uniform.view_proj = mvp.as_mat4().to_cols_array_2d();
+        let mut uniform = self.uniform;
+        uniform.update(self);
+        self.uniform = uniform;
     }
 
     pub fn move_forwards(&mut self, dt: f32) {
@@ -298,20 +299,40 @@ impl Camera {
 #[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct CameraUniform {
     pub view_position: [f32; 4],
+    pub view: [[f32; 4]; 4],
     pub view_proj: [[f32; 4]; 4],
+    pub inv_proj: [[f32; 4]; 4],
+    pub inv_view: [[f32; 4]; 4],
 }
 
 impl CameraUniform {
     pub fn new() -> Self {
         Self {
             view_position: [0.0; 4],
+            view: Mat4::IDENTITY.to_cols_array_2d(),
             view_proj: Mat4::IDENTITY.to_cols_array_2d(),
+            inv_proj: Mat4::IDENTITY.to_cols_array_2d(),
+            inv_view: Mat4::IDENTITY.to_cols_array_2d(),
         }
     }
 
     pub fn update(&mut self, camera: &mut Camera) {
         self.view_position = camera.eye.as_vec3().extend(1.0).to_array();
-        self.view_proj = (DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * camera.build_vp())
+        
+        let vp = camera.build_vp();
+        let view = camera.view_mat;
+        let proj = camera.proj_mat;
+        
+        let wgpu_matrix = DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX);
+        self.view = view.as_mat4().to_cols_array_2d();
+        self.view_proj = vp.as_mat4().to_cols_array_2d();
+        
+        self.inv_proj = (wgpu_matrix * proj)
+            .inverse()
+            .as_mat4()
+            .to_cols_array_2d();
+        self.inv_view = view
+            .inverse()
             .as_mat4()
             .to_cols_array_2d();
     }
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 618ac77..ed8bb82 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -18,6 +18,7 @@ pub mod utils;
 pub mod texture;
 pub mod pipelines;
 pub mod mipmap;
+pub mod sky;
 
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
 pub const PHYSICS_STEP_RATE: u32 = 120;
@@ -73,6 +74,7 @@ pub struct BindGroupLayouts {
     pub light_bind_group_layout: BindGroupLayout,
     pub light_array_bind_group_layout: BindGroupLayout,
     pub light_cube_bind_group_layout: BindGroupLayout,
+    pub environment_bind_group_layout: BindGroupLayout,
 }
 
 /// The backend information, such as the device, queue, config, surface, renderer, window and more.
@@ -146,15 +148,18 @@ impl State {
 
         let limits = wgpu::Limits {
             max_bind_groups: 8,
-            ..Default::default()
+            ..wgpu::Limits::defaults()
         };
 
+        let supported_features = adapter.features();
+        let features = supported_features & wgpu::Features::all_webgpu_mask();
+
         let (device, queue) = adapter
             .request_device(&wgpu::DeviceDescriptor {
                 label: Some(format!("{} graphics device", title).as_str()),
-                required_features: wgpu::Features::empty(),
+                required_features: features,
                 required_limits: limits,
-                experimental_features: unsafe { ExperimentalFeatures::enabled() },
+                experimental_features: ExperimentalFeatures::default(),
                 memory_hints: Default::default(),
                 trace: wgpu::Trace::Off,
             })
@@ -354,6 +359,29 @@ Hardware:
         //     wgpu::AddressMode::default(),
         // );
 
+        let environment_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("environment_layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                            view_dimension: wgpu::TextureViewDimension::Cube,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
+                        count: None,
+                    },
+                ],
+            });
+
         let result = Self {
             surface: Arc::new(surface),
             surface_format,
@@ -379,6 +407,7 @@ Hardware:
                 light_bind_group_layout,
                 light_array_bind_group_layout,
                 light_cube_bind_group_layout,
+                environment_bind_group_layout,
             }),
             supports_storage: supports_storage_resources,
             // yakui_renderer,
@@ -874,7 +903,7 @@ impl App {
     /// window config.
     fn new(app_data: AppInfo, future_queue: Option<Arc<FutureQueue>>) -> Self {
         let instance = Arc::new(Instance::new(&wgpu::InstanceDescriptor {
-            backends: wgpu::Backends::all(),
+            backends: wgpu::Backends::PRIMARY,
             ..Default::default()
         }));
 
@@ -914,7 +943,13 @@ impl App {
 
         let window_id = window.id();
 
-        let mut win_state = pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone()))?;
+        let mut win_state = match pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone())) {
+            Ok(v) => v,
+            Err(e) => {
+                // force a panic because pollster doesnt panic on error
+                panic!("Failed to create window: {}", e);
+            }
+        };
 
         let size = win_state.window.inner_size();
         win_state.resize(size.width, size.height);
diff --git a/crates/dropbear-engine/src/mipmap.rs b/crates/dropbear-engine/src/mipmap.rs
index 8f78a2f..2f7b994 100644
--- a/crates/dropbear-engine/src/mipmap.rs
+++ b/crates/dropbear-engine/src/mipmap.rs
@@ -93,7 +93,7 @@ impl MipMapper {
 
         let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
             label: Some("mipmap compute shader"),
-            source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()),
+            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/mipmap.wgsl").into()),
         });
 
         let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
diff --git a/crates/dropbear-engine/src/pipelines/hdr.rs b/crates/dropbear-engine/src/pipelines/hdr.rs
index 9ba354e..1753c14 100644
--- a/crates/dropbear-engine/src/pipelines/hdr.rs
+++ b/crates/dropbear-engine/src/pipelines/hdr.rs
@@ -35,8 +35,6 @@ impl HdrPipeline {
             Some("Hdr::texture"),
         );
 
-        
-
         let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
             label: Some("Hdr::layout"),
             entries: &[
@@ -75,7 +73,7 @@ impl HdrPipeline {
         });
 
         // We'll cover the shader next
-        let shader = wgpu::include_wgsl!("shaders/hdr.wgsl");
+        let shader = wgpu::include_wgsl!("../shaders/hdr.wgsl");
         let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
             label: None,
             bind_group_layouts: &[&layout],
diff --git a/crates/dropbear-engine/src/pipelines/mod.rs b/crates/dropbear-engine/src/pipelines/mod.rs
index 13c0f4f..5885750 100644
--- a/crates/dropbear-engine/src/pipelines/mod.rs
+++ b/crates/dropbear-engine/src/pipelines/mod.rs
@@ -23,7 +23,7 @@ pub trait DropbearShaderPipeline {
     fn pipeline(&self) -> &wgpu::RenderPipeline;
 }
 
-fn create_render_pipeline(
+pub fn create_render_pipeline(
     label: Option<&str>,
     device: &wgpu::Device,
     layout: &wgpu::PipelineLayout,
@@ -33,6 +33,32 @@ fn create_render_pipeline(
     topology: wgpu::PrimitiveTopology,
     shader: wgpu::ShaderModuleDescriptor,
 ) -> wgpu::RenderPipeline {
+    create_render_pipeline_ex(
+        label,
+        device,
+        layout,
+        color_format,
+        depth_format,
+        vertex_layouts,
+        topology,
+        shader,
+        true, // depth_write_enabled
+        wgpu::CompareFunction::LessEqual,
+    )
+}
+
+pub fn create_render_pipeline_ex(
+    label: Option<&str>,
+    device: &wgpu::Device,
+    layout: &wgpu::PipelineLayout,
+    color_format: wgpu::TextureFormat,
+    depth_format: Option<wgpu::TextureFormat>,
+    vertex_layouts: &[wgpu::VertexBufferLayout],
+    topology: wgpu::PrimitiveTopology,
+    shader: wgpu::ShaderModuleDescriptor,
+    depth_write_enabled: bool,
+    depth_compare: wgpu::CompareFunction,
+) -> wgpu::RenderPipeline {
     let shader = device.create_shader_module(shader);
 
     device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
@@ -68,8 +94,8 @@ fn create_render_pipeline(
         },
         depth_stencil: depth_format.map(|format| wgpu::DepthStencilState {
             format,
-            depth_write_enabled: true,
-            depth_compare: wgpu::CompareFunction::LessEqual, // UDPATED!
+            depth_write_enabled,
+            depth_compare,
             stencil: wgpu::StencilState::default(),
             bias: wgpu::DepthBiasState::default(),
         }),
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
index e0b7be9..31b7be8 100644
--- a/crates/dropbear-engine/src/pipelines/shader.rs
+++ b/crates/dropbear-engine/src/pipelines/shader.rs
@@ -18,7 +18,7 @@ impl DropbearShaderPipeline for MainRenderPipeline {
     fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
         let shader = Shader::new(
             graphics.clone(),
-            include_str!("shaders/shader.wgsl"),
+            include_str!("../shaders/shader.wgsl"),
             Some("viewport shaders"),
         );
 
diff --git a/crates/dropbear-engine/src/pipelines/shaders/blit.slang b/crates/dropbear-engine/src/pipelines/shaders/blit.slang
deleted file mode 100644
index 4b14a95..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/blit.slang
+++ /dev/null
@@ -1,35 +0,0 @@
-#include "dropbear.slang"
-
-struct VertexOutput {
-    float4 clip_position : SV_Position;
-
-    [[vk_location(0)]]
-    float2 uv;
-};
-
-[[vk_binding(0, 0)]]
-Texture2D tex;
-
-[[vk_binding(1, 0)]]
-SamplerState tex_sampler;
-
-[shader("vertex")]
-VertexOutput vs_main(
-    uint in_vertex_index: SV_VulkanVertexID
-) {
-    VertexOutput output;
-
-    float x = float((in_vertex_index << 1u) & 2u);
-    float y = float(in_vertex_index & 2u);
-
-    output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
-    output.uv = float2(x, 1.0 - y);
-
-    return output;
-}
-
-[shader("fragment")]
-float4 fs_main(VertexOutput input) : SV_Target0 {
-    float4 s = tex.Sample(tex_sampler, input.uv);
-    return s;
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/dropbear.slang b/crates/dropbear-engine/src/pipelines/shaders/dropbear.slang
deleted file mode 100644
index ff6f1d2..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/dropbear.slang
+++ /dev/null
@@ -1,45 +0,0 @@
-struct Globals {
-    uint num_lights;
-    float ambient_strength;
-};
-
-/// A standard light descriptor struct. 
-struct Light {
-    float4 position;
-    float4 direction; // x, y, z, outer_cutoff_angle
-    float4 color; // r, g, b, light_type (0, 1, 2)
-
-    float constant;
-    float lin;
-    float quadratic;
-
-    float cutoff;
-};
-
-struct CameraUniform {
-    float4 view_pos;
-    float4x4 view_proj;
-};
-
-struct MaterialUniform {
-    // for stuff like tinting
-    float4 colour;
-
-    // scales incoming UVs before sampling
-    float2 uv_tiling;
-};
-
-/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. 
-float4x4 vs_input_matrix(
-    float4 mat1, 
-    float4 mat2, 
-    float4 mat3, 
-    float4 mat4
-) {
-    return transpose(float4x4(
-        mat1,
-        mat2,
-        mat3,
-        mat4
-    ));
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/hdr.wgsl b/crates/dropbear-engine/src/pipelines/shaders/hdr.wgsl
deleted file mode 100644
index 2dc5562..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/hdr.wgsl
+++ /dev/null
@@ -1,55 +0,0 @@
-// Maps HDR values to linear values
-// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces
-fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> {
-    let m1 = mat3x3(
-        0.59719, 0.07600, 0.02840,
-        0.35458, 0.90834, 0.13383,
-        0.04823, 0.01566, 0.83777,
-    );
-    let m2 = mat3x3(
-        1.60475, -0.10208, -0.00327,
-        -0.53108,  1.10813, -0.07276,
-        -0.07367, -0.00605,  1.07602,
-    );
-    let v = m1 * hdr;
-    let a = v * (v + 0.0245786) - 0.000090537;
-    let b = v * (0.983729 * v + 0.4329510) + 0.238081;
-    return clamp(m2 * (a / b), vec3(0.0), vec3(1.0));
-}
-
-struct VertexOutput {
-    @location(0) uv: vec2<f32>,
-    @builtin(position) clip_position: vec4<f32>,
-};
-
-@vertex
-fn vs_main(
-    @builtin(vertex_index) vi: u32,
-) -> VertexOutput {
-    var out: VertexOutput;
-    // Generate a triangle that covers the whole screen
-    out.uv = vec2<f32>(
-        f32((vi << 1u) & 2u),
-        f32(vi & 2u),
-    );
-    out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0);
-    // We need to invert the y coordinate so the image
-    // is not upside down
-    out.uv.y = 1.0 - out.uv.y;
-    return out;
-}
-
-@group(0)
-@binding(0)
-var hdr_image: texture_2d<f32>;
-
-@group(0)
-@binding(1)
-var hdr_sampler: sampler;
-
-@fragment
-fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> {
-    let hdr = textureSample(hdr_image, hdr_sampler, vs.uv);
-    let sdr = aces_tone_map(hdr.rgb);
-    return vec4(sdr, hdr.a);
-}
diff --git a/crates/dropbear-engine/src/pipelines/shaders/light.slang b/crates/dropbear-engine/src/pipelines/shaders/light.slang
deleted file mode 100644
index 9a2a330..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/light.slang
+++ /dev/null
@@ -1,51 +0,0 @@
-#include "dropbear.slang"
-
-[[vk::binding(0, 0)]]
-ConstantBuffer<CameraUniform> camera;
-
-[[vk::binding(0, 1)]]
-ConstantBuffer<Light> light;
-
-struct VertexInput {
-    [[vk::location(0)]]
-    float3 position : POSITION;
-    
-    [[vk::location(5)]]
-    float4 model_matrix_0 : TEXCOORD5;
-    
-    [[vk::location(6)]]
-    float4 model_matrix_1 : TEXCOORD6;
-    
-    [[vk::location(7)]]
-    float4 model_matrix_2 : TEXCOORD7;
-    
-    [[vk::location(8)]]
-    float4 model_matrix_3 : TEXCOORD8;
-};
-
-struct VertexOutput {
-    float4 clip_position : SV_Position;
-    
-    [[vk::location(0)]]
-    float3 color : COLOR0;
-};
-
-[shader("vertex")]
-VertexOutput vs_main(VertexInput input) {
-    float4x4 model_matrix = vs_input_matrix(
-        input.model_matrix_0,
-        input.model_matrix_1,
-        input.model_matrix_2,
-        input.model_matrix_3,
-    );
-
-    VertexOutput output;
-    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
-    output.color = light.color.xyz;
-    return output;
-}
-
-[shader("fragment")]
-float4 fs_main(VertexOutput input) : SV_Target0 {
-    return float4(input.color, 1.0);
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl b/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl
deleted file mode 100644
index cfc908b..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl
+++ /dev/null
@@ -1,31 +0,0 @@
-@group(0)
-@binding(0)
-var src: texture_storage_2d<rgba8unorm, read>;
-@group(0)
-@binding(1)
-var dst: texture_storage_2d<rgba8unorm, write>;
-
-@compute
-@workgroup_size(16, 16, 1)
-fn compute_mipmap(
-    @builtin(global_invocation_id) gid: vec3<u32>,
-) {
-    let dstPos = gid.xy;
-    let srcPos = gid.xy * 2;
-
-    let dim = textureDimensions(src);
-
-    if (dstPos.x >= dim.x || dstPos.y >= dim.y) {
-        return;
-    }
-
-    let t00 = textureLoad(src, srcPos);
-    let t01 = textureLoad(src, srcPos + vec2(0, 1));
-    let t10 = textureLoad(src, srcPos + vec2(1, 0));
-    let t11 = textureLoad(src, srcPos + vec2(1, 1));
-
-    // A simple linear average of 4 adjacent pixels
-    let t = (t00 + t01 + t10 + t11) * 0.25;
-
-    textureStore(dst, dstPos, t);
-}
diff --git a/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl b/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl
deleted file mode 100644
index dcf0012..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl
+++ /dev/null
@@ -1,290 +0,0 @@
-// 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/shaders/blit.slang b/crates/dropbear-engine/src/shaders/blit.slang
new file mode 100644
index 0000000..4b14a95
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/blit.slang
@@ -0,0 +1,35 @@
+#include "dropbear.slang"
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+
+    [[vk_location(0)]]
+    float2 uv;
+};
+
+[[vk_binding(0, 0)]]
+Texture2D tex;
+
+[[vk_binding(1, 0)]]
+SamplerState tex_sampler;
+
+[shader("vertex")]
+VertexOutput vs_main(
+    uint in_vertex_index: SV_VulkanVertexID
+) {
+    VertexOutput output;
+
+    float x = float((in_vertex_index << 1u) & 2u);
+    float y = float(in_vertex_index & 2u);
+
+    output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
+    output.uv = float2(x, 1.0 - y);
+
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    float4 s = tex.Sample(tex_sampler, input.uv);
+    return s;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/dropbear.slang b/crates/dropbear-engine/src/shaders/dropbear.slang
new file mode 100644
index 0000000..5445568
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/dropbear.slang
@@ -0,0 +1,48 @@
+struct Globals {
+    uint num_lights;
+    float ambient_strength;
+};
+
+/// A standard light descriptor struct. 
+struct Light {
+    float4 position;
+    float4 direction; // x, y, z, outer_cutoff_angle
+    float4 color; // r, g, b, light_type (0, 1, 2)
+
+    float constant;
+    float lin;
+    float quadratic;
+
+    float cutoff;
+};
+
+struct CameraUniform {
+    float4 view_pos;
+    float4x4 view;
+    float4x4 view_proj;
+    float4x4 inv_proj;
+    float4x4 inv_view;
+};
+
+struct MaterialUniform {
+    // for stuff like tinting
+    float4 colour;
+
+    // scales incoming UVs before sampling
+    float2 uv_tiling;
+};
+
+/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. 
+float4x4 vs_input_matrix(
+    float4 mat1, 
+    float4 mat2, 
+    float4 mat3, 
+    float4 mat4
+) {
+    return transpose(float4x4(
+        mat1,
+        mat2,
+        mat3,
+        mat4
+    ));
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/equirectangular.wgsl b/crates/dropbear-engine/src/shaders/equirectangular.wgsl
new file mode 100644
index 0000000..b771de5
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/equirectangular.wgsl
@@ -0,0 +1,86 @@
+const PI: f32 = 3.1415926535897932384626433832795;
+
+struct Face {
+    forward: vec3<f32>,
+    up: vec3<f32>,
+    right: vec3<f32>,
+}
+
+@group(0)
+@binding(0)
+var src: texture_2d<f32>;
+
+@group(0)
+@binding(1)
+var dst: texture_storage_2d_array<rgba32float, write>;
+
+@compute
+@workgroup_size(16, 16, 1)
+fn compute_equirect_to_cubemap(
+    @builtin(global_invocation_id)
+    gid: vec3<u32>,
+) {
+    // If texture size is not divisible by 32, we
+    // need to make sure we don't try to write to
+    // pixels that don't exist.
+    if gid.x >= u32(textureDimensions(dst).x) {
+        return;
+    }
+
+    var FACES: array<Face, 6> = array(
+        // FACES +X
+        Face(
+            vec3(1.0, 0.0, 0.0),  // forward
+            vec3(0.0, 1.0, 0.0),  // up
+            vec3(0.0, 0.0, -1.0), // right
+        ),
+        // FACES -X
+        Face (
+            vec3(-1.0, 0.0, 0.0),
+            vec3(0.0, 1.0, 0.0),
+            vec3(0.0, 0.0, 1.0),
+        ),
+        // FACES +Y
+        Face (
+            vec3(0.0, -1.0, 0.0),
+            vec3(0.0, 0.0, 1.0),
+            vec3(1.0, 0.0, 0.0),
+        ),
+        // FACES -Y
+        Face (
+            vec3(0.0, 1.0, 0.0),
+            vec3(0.0, 0.0, -1.0),
+            vec3(1.0, 0.0, 0.0),
+        ),
+        // FACES +Z
+        Face (
+            vec3(0.0, 0.0, 1.0),
+            vec3(0.0, 1.0, 0.0),
+            vec3(1.0, 0.0, 0.0),
+        ),
+        // FACES -Z
+        Face (
+            vec3(0.0, 0.0, -1.0),
+            vec3(0.0, 1.0, 0.0),
+            vec3(-1.0, 0.0, 0.0),
+        ),
+    );
+
+    // Get texture coords relative to cubemap face
+    let dst_dimensions = vec2<f32>(textureDimensions(dst));
+    let cube_uv = vec2<f32>(gid.xy) / dst_dimensions * 2.0 - 1.0;
+
+    // Get spherical coordinate from cube_uv
+    let face = FACES[gid.z];
+    let spherical = normalize(face.forward + face.right * cube_uv.x + face.up * cube_uv.y);
+
+    // Get coordinate on the equirectangular texture
+    let inv_atan = vec2(0.1591, 0.3183);
+    let eq_uv = vec2(atan2(spherical.z, spherical.x), asin(spherical.y)) * inv_atan + 0.5;
+    let eq_pixel = vec2<i32>(eq_uv * vec2<f32>(textureDimensions(src)));
+
+    // We use textureLoad() as textureSample() is not allowed in compute shaders
+    var sample = textureLoad(src, eq_pixel, 0);
+
+    textureStore(dst, gid.xy, gid.z, sample);
+}
diff --git a/crates/dropbear-engine/src/shaders/hdr.wgsl b/crates/dropbear-engine/src/shaders/hdr.wgsl
new file mode 100644
index 0000000..2dc5562
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/hdr.wgsl
@@ -0,0 +1,55 @@
+// Maps HDR values to linear values
+// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces
+fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> {
+    let m1 = mat3x3(
+        0.59719, 0.07600, 0.02840,
+        0.35458, 0.90834, 0.13383,
+        0.04823, 0.01566, 0.83777,
+    );
+    let m2 = mat3x3(
+        1.60475, -0.10208, -0.00327,
+        -0.53108,  1.10813, -0.07276,
+        -0.07367, -0.00605,  1.07602,
+    );
+    let v = m1 * hdr;
+    let a = v * (v + 0.0245786) - 0.000090537;
+    let b = v * (0.983729 * v + 0.4329510) + 0.238081;
+    return clamp(m2 * (a / b), vec3(0.0), vec3(1.0));
+}
+
+struct VertexOutput {
+    @location(0) uv: vec2<f32>,
+    @builtin(position) clip_position: vec4<f32>,
+};
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) vi: u32,
+) -> VertexOutput {
+    var out: VertexOutput;
+    // Generate a triangle that covers the whole screen
+    out.uv = vec2<f32>(
+        f32((vi << 1u) & 2u),
+        f32(vi & 2u),
+    );
+    out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0);
+    // We need to invert the y coordinate so the image
+    // is not upside down
+    out.uv.y = 1.0 - out.uv.y;
+    return out;
+}
+
+@group(0)
+@binding(0)
+var hdr_image: texture_2d<f32>;
+
+@group(0)
+@binding(1)
+var hdr_sampler: sampler;
+
+@fragment
+fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> {
+    let hdr = textureSample(hdr_image, hdr_sampler, vs.uv);
+    let sdr = aces_tone_map(hdr.rgb);
+    return vec4(sdr, hdr.a);
+}
diff --git a/crates/dropbear-engine/src/shaders/light.slang b/crates/dropbear-engine/src/shaders/light.slang
new file mode 100644
index 0000000..9a2a330
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/light.slang
@@ -0,0 +1,51 @@
+#include "dropbear.slang"
+
+[[vk::binding(0, 0)]]
+ConstantBuffer<CameraUniform> camera;
+
+[[vk::binding(0, 1)]]
+ConstantBuffer<Light> light;
+
+struct VertexInput {
+    [[vk::location(0)]]
+    float3 position : POSITION;
+    
+    [[vk::location(5)]]
+    float4 model_matrix_0 : TEXCOORD5;
+    
+    [[vk::location(6)]]
+    float4 model_matrix_1 : TEXCOORD6;
+    
+    [[vk::location(7)]]
+    float4 model_matrix_2 : TEXCOORD7;
+    
+    [[vk::location(8)]]
+    float4 model_matrix_3 : TEXCOORD8;
+};
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+    
+    [[vk::location(0)]]
+    float3 color : COLOR0;
+};
+
+[shader("vertex")]
+VertexOutput vs_main(VertexInput input) {
+    float4x4 model_matrix = vs_input_matrix(
+        input.model_matrix_0,
+        input.model_matrix_1,
+        input.model_matrix_2,
+        input.model_matrix_3,
+    );
+
+    VertexOutput output;
+    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
+    output.color = light.color.xyz;
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    return float4(input.color, 1.0);
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/mipmap.wgsl b/crates/dropbear-engine/src/shaders/mipmap.wgsl
new file mode 100644
index 0000000..cfc908b
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/mipmap.wgsl
@@ -0,0 +1,31 @@
+@group(0)
+@binding(0)
+var src: texture_storage_2d<rgba8unorm, read>;
+@group(0)
+@binding(1)
+var dst: texture_storage_2d<rgba8unorm, write>;
+
+@compute
+@workgroup_size(16, 16, 1)
+fn compute_mipmap(
+    @builtin(global_invocation_id) gid: vec3<u32>,
+) {
+    let dstPos = gid.xy;
+    let srcPos = gid.xy * 2;
+
+    let dim = textureDimensions(src);
+
+    if (dstPos.x >= dim.x || dstPos.y >= dim.y) {
+        return;
+    }
+
+    let t00 = textureLoad(src, srcPos);
+    let t01 = textureLoad(src, srcPos + vec2(0, 1));
+    let t10 = textureLoad(src, srcPos + vec2(1, 0));
+    let t11 = textureLoad(src, srcPos + vec2(1, 1));
+
+    // A simple linear average of 4 adjacent pixels
+    let t = (t00 + t01 + t10 + t11) * 0.25;
+
+    textureStore(dst, dstPos, t);
+}
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
new file mode 100644
index 0000000..a1f912a
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -0,0 +1,288 @@
+// Main shaders for standard objects.
+
+struct Globals {
+    num_lights: u32,
+    ambient_strength: f32,
+}
+
+struct CameraUniform {
+    view_pos: vec4<f32>,
+    view: mat4x4<f32>,
+    view_proj: mat4x4<f32>,
+    inv_proj: mat4x4<f32>,
+    inv_view: mat4x4<f32>,
+}
+struct Light {
+    position: vec4<f32>,
+    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
+    color: vec4<f32>, // r, g, b, light_type (0, 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 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 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 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 (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 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 (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,
+    );
+
+    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz;
+    var final_color = ambient;
+
+    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,
+    );
+
+    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz;
+    var final_color = ambient;
+
+    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);
+}
diff --git a/crates/dropbear-engine/src/shaders/sky.wgsl b/crates/dropbear-engine/src/shaders/sky.wgsl
new file mode 100644
index 0000000..4469190
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/sky.wgsl
@@ -0,0 +1,46 @@
+struct Camera {
+    view_pos: vec4<f32>,
+    view: mat4x4<f32>,
+    view_proj: mat4x4<f32>,
+    inv_proj: mat4x4<f32>,
+    inv_view: mat4x4<f32>,
+}
+@group(0) @binding(0)
+var<uniform> camera: Camera;
+
+@group(1)
+@binding(0)
+var env_map: texture_cube<f32>;
+@group(1)
+@binding(1)
+var env_sampler: sampler;
+
+struct VertexOutput {
+    @builtin(position) frag_position: vec4<f32>,
+    @location(0) clip_position: vec4<f32>,
+}
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) id: u32,
+) -> VertexOutput {
+    let uv = vec2<f32>(vec2<u32>(
+        id & 1u,
+        (id >> 1u) & 1u,
+    ));
+    var out: VertexOutput;
+    let ndc = uv * 4.0 - 1.0;
+    out.frag_position = vec4(ndc, 0.0, 1.0);
+    out.clip_position = vec4(ndc, 1.0, 1.0);
+    return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    let view_pos_homogeneous = camera.inv_proj * in.clip_position;
+    let view_ray_direction = view_pos_homogeneous.xyz / view_pos_homogeneous.w;
+    var ray_direction = normalize((camera.inv_view * vec4(view_ray_direction, 0.0)).xyz);
+
+    let smpl = textureSample(env_map, env_sampler, ray_direction);
+    return smpl;
+}
diff --git a/crates/dropbear-engine/src/sky.rs b/crates/dropbear-engine/src/sky.rs
new file mode 100644
index 0000000..8d50886
--- /dev/null
+++ b/crates/dropbear-engine/src/sky.rs
@@ -0,0 +1,298 @@
+use std::io::Cursor;
+use std::sync::Arc;
+use crate::texture::Texture;
+use image::codecs::hdr::HdrDecoder;
+use crate::graphics::SharedGraphicsContext;
+use crate::pipelines::{create_render_pipeline_ex};
+
+pub struct CubeTexture {
+    texture: wgpu::Texture,
+    sampler: wgpu::Sampler,
+    view: wgpu::TextureView,
+}
+
+impl CubeTexture {
+    pub fn create_2d(
+        device: &wgpu::Device,
+        width: u32,
+        height: u32,
+        format: wgpu::TextureFormat,
+        mip_level_count: u32,
+        usage: wgpu::TextureUsages,
+        mag_filter: wgpu::FilterMode,
+        label: Option<&str>,
+    ) -> Self {
+        let texture = device.create_texture(&wgpu::TextureDescriptor {
+            label,
+            size: wgpu::Extent3d {
+                width,
+                height,
+                // A cube has 6 sides, so we need 6 layers
+                depth_or_array_layers: 6,
+            },
+            mip_level_count,
+            sample_count: 1,
+            dimension: wgpu::TextureDimension::D2,
+            format,
+            usage,
+            view_formats: &[],
+        });
+
+        let view = texture.create_view(&wgpu::TextureViewDescriptor {
+            label,
+            dimension: Some(wgpu::TextureViewDimension::Cube),
+            array_layer_count: Some(6),
+            ..Default::default()
+        });
+
+        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+            label,
+            address_mode_u: wgpu::AddressMode::ClampToEdge,
+            address_mode_v: wgpu::AddressMode::ClampToEdge,
+            address_mode_w: wgpu::AddressMode::ClampToEdge,
+            mag_filter,
+            min_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Nearest,
+            ..Default::default()
+        });
+
+        Self {
+            texture,
+            sampler,
+            view,
+        }
+    }
+
+    pub fn texture(&self) -> &wgpu::Texture { &self.texture }
+
+    pub fn view(&self) -> &wgpu::TextureView { &self.view }
+
+    pub fn sampler(&self) -> &wgpu::Sampler { &self.sampler }
+
+}
+
+pub struct HdrLoader {
+    texture_format: wgpu::TextureFormat,
+    equirect_layout: wgpu::BindGroupLayout,
+    equirect_to_cubemap: wgpu::ComputePipeline,
+}
+
+impl HdrLoader {
+    pub fn new(device: &wgpu::Device) -> Self {
+        let module = device.create_shader_module(wgpu::include_wgsl!("shaders/equirectangular.wgsl"));
+        let texture_format = wgpu::TextureFormat::Rgba32Float;
+        let equirect_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("HdrLoader::equirect_layout"),
+            entries: &[
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::COMPUTE,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        multisampled: false,
+                    },
+                    count: None,
+                },
+                wgpu::BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: wgpu::ShaderStages::COMPUTE,
+                    ty: wgpu::BindingType::StorageTexture {
+                        access: wgpu::StorageTextureAccess::WriteOnly,
+                        format: texture_format,
+                        view_dimension: wgpu::TextureViewDimension::D2Array,
+                    },
+                    count: None,
+                },
+            ],
+        });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: None,
+            bind_group_layouts: &[&equirect_layout],
+            push_constant_ranges: &[],
+        });
+
+        let equirect_to_cubemap =
+            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
+                label: Some("equirect_to_cubemap"),
+                layout: Some(&pipeline_layout),
+                module: &module,
+                entry_point: Some("compute_equirect_to_cubemap"),
+                compilation_options: Default::default(),
+                cache: None,
+            });
+
+        Self {
+            equirect_to_cubemap,
+            texture_format,
+            equirect_layout,
+        }
+    }
+
+    pub fn from_equirectangular_bytes(
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        data: &[u8],
+        dst_size: u32,
+        label: Option<&str>,
+    ) -> anyhow::Result<CubeTexture> {
+        let loader = Self::new(device);
+
+        let hdr_decoder = HdrDecoder::new(Cursor::new(data))?;
+        let meta = hdr_decoder.metadata();
+
+        #[cfg(not(target_arch="wasm32"))]
+        let pixels = {
+            let mut pixels = vec![[0.0, 0.0, 0.0, 0.0]; meta.width as usize * meta.height as usize];
+            hdr_decoder.read_image_transform(
+                |pix| {
+                    let rgb = pix.to_hdr();
+                    [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32]
+                },
+                &mut pixels[..],
+            )?;
+            pixels
+        };
+        #[cfg(target_arch="wasm32")]
+        let pixels = hdr_decoder.read_image_native()?
+            .into_iter()
+            .map(|pix| {
+                let rgb = pix.to_hdr();
+                [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32]
+            })
+            .collect::<Vec<_>>();
+
+        let src = Texture::create_2d_texture(
+            device,
+            meta.width,
+            meta.height,
+            loader.texture_format,
+            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
+            wgpu::FilterMode::Linear,
+            None,
+        );
+
+        queue.write_texture(
+            wgpu::TexelCopyTextureInfo {
+                texture: &src.texture,
+                mip_level: 0,
+                origin: wgpu::Origin3d::ZERO,
+                aspect: wgpu::TextureAspect::All,
+            },
+            &bytemuck::cast_slice(&pixels),
+            wgpu::TexelCopyBufferLayout {
+                offset: 0,
+                bytes_per_row: Some(src.size.width * std::mem::size_of::<[f32; 4]>() as u32),
+                rows_per_image: Some(src.size.height),
+            },
+            src.size,
+        );
+
+        let dst = CubeTexture::create_2d(
+            device,
+            dst_size,
+            dst_size,
+            loader.texture_format,
+            1,
+            // We are going to write to `dst` texture so we
+            // need to use a `STORAGE_BINDING`.
+            wgpu::TextureUsages::STORAGE_BINDING
+                | wgpu::TextureUsages::TEXTURE_BINDING,
+            wgpu::FilterMode::Nearest,
+            label,
+        );
+
+        let dst_view = dst.texture().create_view(&wgpu::TextureViewDescriptor {
+            label,
+            // Normally, you'd use `TextureViewDimension::Cube`
+            // for a cube texture, but we can't use that
+            // view dimension with a `STORAGE_BINDING`.
+            // We need to access the cube texture layers
+            // directly.
+            dimension: Some(wgpu::TextureViewDimension::D2Array),
+            ..Default::default()
+        });
+
+        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label,
+            layout: &loader.equirect_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&src.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::TextureView(&dst_view),
+                },
+            ],
+        });
+
+        let mut encoder = device.create_command_encoder(&Default::default());
+        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label, timestamp_writes: None });
+
+        let num_workgroups = (dst_size + 15) / 16;
+        pass.set_pipeline(&loader.equirect_to_cubemap);
+        pass.set_bind_group(0, &bind_group, &[]);
+        pass.dispatch_workgroups(num_workgroups, num_workgroups, 6);
+
+        drop(pass);
+
+        queue.submit([encoder.finish()]);
+
+        Ok(dst)
+    }
+}
+
+pub struct SkyPipeline {
+    pub texture: CubeTexture,
+    pub pipeline: wgpu::RenderPipeline,
+    pub bind_group: wgpu::BindGroup,
+}
+
+impl SkyPipeline {
+    pub fn new(graphics: Arc<SharedGraphicsContext>, sky_texture: CubeTexture) -> Self {
+        let environment_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("environment_bind_group"),
+            layout: &graphics.layouts.environment_bind_group_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&sky_texture.view()),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(sky_texture.sampler()),
+                },
+            ],
+        });
+
+        let sky_pipeline = {
+            let layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Sky Pipeline Layout"),
+                bind_group_layouts: &[&graphics.layouts.camera_bind_group_layout, &graphics.layouts.environment_bind_group_layout],
+                push_constant_ranges: &[],
+            });
+            let shader = wgpu::include_wgsl!("shaders/sky.wgsl");
+            create_render_pipeline_ex(
+                Some("sky render pipeline"),
+                &graphics.device,
+                &layout,
+                graphics.hdr.read().format(),
+                Some(Texture::DEPTH_FORMAT),
+                &[],
+                wgpu::PrimitiveTopology::TriangleList,
+                shader,
+                false,
+                wgpu::CompareFunction::GreaterEqual,
+            )
+        };
+        
+        Self {
+            texture: sky_texture,
+            pipeline: sky_pipeline,
+            bind_group: environment_bind_group,
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index ffe8fd9..5ad6d8d 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -17,7 +17,7 @@ use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::EntityTransform;
 use dropbear_engine::graphics::InstanceRaw;
 use dropbear_engine::pipelines::light_cube::LightCubePipeline;
-use dropbear_engine::texture::TextureWrapMode;
+use dropbear_engine::texture::{Texture, TextureWrapMode};
 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};
@@ -51,7 +51,7 @@ use std::{
     time::Instant,
 };
 use std::rc::Rc;
-use log::debug;
+use log::{debug, error};
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
 use wgpu::{Color, Extent3d};
@@ -59,9 +59,10 @@ use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
 use winit::dpi::PhysicalSize;
 use dropbear_engine::mipmap::MipMapper;
-use dropbear_engine::pipelines::DropbearShaderPipeline;
+use dropbear_engine::pipelines::{create_render_pipeline, DropbearShaderPipeline};
 use dropbear_engine::pipelines::shader::MainRenderPipeline;
 use dropbear_engine::pipelines::GlobalsUniform;
+use dropbear_engine::sky::{HdrLoader, SkyPipeline};
 use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline;
@@ -88,6 +89,7 @@ pub struct Editor {
     pub shader_globals: Option<GlobalsUniform>,
     pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     pub mipmapper: Option<MipMapper>,
+    pub sky_pipeline: Option<SkyPipeline>,
 
     pub active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -271,6 +273,7 @@ impl Editor {
             collider_wireframe_geometry_cache: HashMap::new(),
             collider_instance_buffer: None,
             mipmapper: None,
+            sky_pipeline: None,
         })
     }
 
@@ -1356,6 +1359,24 @@ impl Editor {
         self.texture_id = Some((*graphics.texture_id).clone());
         self.window = Some(graphics.window.clone());
         self.is_world_loaded.mark_rendering_loaded();
+
+        let sky_bytes = include_bytes!("../../../../resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr");
+        let sky_texture = HdrLoader::from_equirectangular_bytes(
+            &graphics.device,
+            &graphics.queue,
+            sky_bytes,
+            1080,
+            Some("sky texture")
+        );
+
+        match sky_texture {
+            Ok(sky_texture) => {
+                self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture));
+            }
+            Err(e) => {
+                error!("Failed to load sky texture: {}", e);
+            }
+        }
     }
 
     /// Initialises another eucalyptus-editor play mode app as a separate process and monitors it in a separate thread.
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index f6dbb89..010711b 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -389,35 +389,34 @@ impl Scene for Editor {
         };
         log_once::debug_once!("Pipeline ready");
 
-        { // ensures clearing of the encoder is done correctly. 
-            let mut encoder = CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder"));
-
-            {
-                let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("viewport clear pass"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: hdr.view(),
-                        depth_slice: None,
-                        resolve_target: None,
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(wgpu::Color {
-                                r: 100.0 / 255.0,
-                                g: 149.0 / 255.0,
-                                b: 237.0 / 255.0,
-                                a: 1.0,
-                            }),
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: None,
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
-            }
-
-            if let Err(e) = encoder.submit() {
-                log_once::error_once!("{}", e);
-            }
+        {
+            let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("viewport clear pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    depth_slice: None,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(wgpu::Color {
+                            r: 100.0 / 255.0,
+                            g: 149.0 / 255.0,
+                            b: 237.0 / 255.0,
+                            a: 1.0,
+                        }),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &graphics.depth_texture.view,
+                    depth_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(0.0),
+                        store: wgpu::StoreOp::Store,
+                    }),
+                    stencil_ops: None,
+                }),
+                occlusion_query_set: None,
+                timestamp_writes: None,
+            });
         }
 
         let lights = {
@@ -509,14 +508,14 @@ impl Scene for Editor {
                         depth_slice: None,
                         resolve_target: None,
                         ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(clear_color),
+                            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::Clear(0.0),
+                            load: wgpu::LoadOp::Load,
                             store: wgpu::StoreOp::Store,
                         }),
                         stencil_ops: None,
@@ -539,8 +538,6 @@ impl Scene for Editor {
             }
         }
 
-                
-
         if let Some(lcp) = &self.light_cube_pipeline {
             for (model, instance_buffer, instance_count) in prepared_models {
                 let globals_bind_group = &self
@@ -717,8 +714,41 @@ impl Scene for Editor {
                             );
                         }
                     }
-                }
+}
             }
+
+            if let Some(sky) = &self.sky_pipeline {
+                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                    label: Some("sky render pass"),
+                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                        view: hdr.view(),
+                        resolve_target: None,
+                        ops: wgpu::Operations {
+                            load: wgpu::LoadOp::Load,
+                            store: wgpu::StoreOp::Store,
+                        },
+                        depth_slice: None,
+                    })],
+                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                        view: &graphics.depth_texture.view,
+                        depth_ops: Some(wgpu::Operations {
+                            load: wgpu::LoadOp::Load,
+                            store: wgpu::StoreOp::Store,
+                        }),
+                        stencil_ops: None,
+                    }),
+                    timestamp_writes: None,
+                    occlusion_query_set: None,
+                });
+
+                render_pass.set_pipeline(&sky.pipeline);
+                render_pass.set_bind_group(0, &camera.bind_group, &[]);
+                render_pass.set_bind_group(1, &sky.bind_group, &[]);
+                render_pass.draw(0..3, 0..1);
+            }
+
+            hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
             if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index fbda95f..14983d5 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -667,6 +667,8 @@ impl Scene for PlayMode {
                 });
             }
 
+            hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
             if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
@@ -972,6 +974,8 @@ impl Scene for PlayMode {
                 }
             }
 
+            hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
             if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
@@ -997,6 +1001,8 @@ impl Scene for PlayMode {
         if let Some(kino) = &mut self.kino {
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder"));
             kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view());
+
+            hdr.process(&mut encoder, &graphics.viewport_texture.view);
             if let Err(e) = encoder.submit() {
                 log_once::error_once!("Unable to submit kino: {}", e);
             }
diff --git a/docs/renderdoc_wayland.cap b/docs/renderdoc_wayland.cap
new file mode 100644
index 0000000..0a432e3
--- /dev/null
+++ b/docs/renderdoc_wayland.cap
@@ -0,0 +1,40 @@
+{
+    "rdocCaptureSettings": 1,
+    "settings": {
+        "autoStart": false,
+        "commandLine": "",
+        "environment": [
+            {
+                "separator": "Platform style",
+                "type": "Set",
+                "value": "x11",
+                "variable": "WINIT_UNIX_BACKEND"
+            },
+            {
+                "separator": "Platform style",
+                "type": "Set",
+                "value": "",
+                "variable": "WAYLAND_DISPLAY"
+            }
+        ],
+        "executable": "/home/tirbofish/dropbear/target/debug/eucalyptus-editor",
+        "inject": false,
+        "numQueuedFrames": 0,
+        "options": {
+            "allowFullscreen": true,
+            "allowVSync": true,
+            "apiValidation": false,
+            "captureAllCmdLists": false,
+            "captureCallstacks": false,
+            "captureCallstacksOnlyDraws": false,
+            "debugOutputMute": true,
+            "delayForDebugger": 0,
+            "hookIntoChildren": false,
+            "refAllResources": false,
+            "softMemoryLimit": 0,
+            "verifyBufferAccess": false
+        },
+        "queuedFrameCap": 0,
+        "workingDir": ""
+    }
+}
diff --git a/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr b/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr
new file mode 100644
index 0000000..25d9934
Binary files /dev/null and b/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr differ