kitgit

tirbofish/dropbear · diff

aa5d3a3 · Thribhu K

fix: bug with padding of MaterialUniform

Unverified

diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index bc7970b..a0de69c 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -1,6 +1,6 @@
 use crate::graphics::SharedGraphicsContext;
 use crate::model::Model;
-use crate::texture::Texture;
+use crate::texture::{Texture, TextureBuilder};
 use crate::utils::ResourceReference;
 use parking_lot::RwLock;
 use std::collections::HashMap;
@@ -219,15 +219,12 @@ impl AssetRegistry {
             return handle;
         }
 
-        let texture = Texture::from_bytes_verbose_mipmapped_with_format(
-            graphics,
-            &rgba,
-            Some((1, 1)),
-            None,
-            None,
-            format,
-            Some(label.as_str()),
-        );
+        let texture = TextureBuilder::new(&graphics.device)
+            .with_raw_pixels(graphics.clone(), &rgba)
+            .size(1, 1)
+            .format(format)
+            .label(label.as_str())
+            .build();
 
         self.add_texture_with_label(label, texture)
     }
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 0133352..8534ba4 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -1,8 +1,9 @@
 use crate::asset::{AssetRegistry, Handle};
 use crate::buffer::UniformBuffer;
+use crate::texture::TextureBuilder;
 use crate::{
     graphics::SharedGraphicsContext,
-    texture::{Texture, TextureBuilder, TextureWrapMode},
+    texture::{Texture, TextureWrapMode},
     utils::ResourceReference,
 };
 use gltf::image::{Format, Source};
@@ -160,89 +161,81 @@ pub enum ChannelValues {
 }
 
 impl Material {
-    pub fn new(
+    fn build_bind_group(
         registry: &mut AssetRegistry,
-        graphics: Arc<SharedGraphicsContext>,
-        name: impl Into<String>,
+        graphics: &Arc<SharedGraphicsContext>,
+        tint_buffer: &UniformBuffer<MaterialUniform>,
         diffuse_texture: Handle<Texture>,
         normal_texture: Option<Handle<Texture>>,
         emissive_texture: Option<Handle<Texture>>,
         metallic_roughness_texture: Option<Handle<Texture>>,
         occlusion_texture: Option<Handle<Texture>>,
-        tint: [f32; 4],
-        texture_tag: Option<String>,
-    ) -> Self {
-        puffin::profile_function!();
-        let name = name.into();
-
-        let uv_tiling = [1.0, 1.0];
-        let uniform = MaterialUniform {
-            base_colour: tint,
-            emissive: [0.0, 0.0, 0.0],
-            emissive_strength: 1.0,
-            metallic: 1.0,
-            roughness: 1.0,
-            normal_scale: 1.0,
-            occlusion_strength: 1.0,
-            alpha_cutoff: 0.5,
-            uv_tiling,
-            has_normal_texture: normal_texture.is_some() as u32,
-            has_emissive_texture: emissive_texture.is_some() as u32,
-            has_metallic_texture: metallic_roughness_texture.is_some() as u32,
-            has_occlusion_texture: occlusion_texture.is_some() as u32,
-            pad: 0,
-        };
+    ) -> wgpu::BindGroup {
+        let d = registry
+            .get_texture(diffuse_texture)
+            .expect("diffuse texture handle missing from registry");
 
-        let d = registry.get_texture(diffuse_texture).unwrap();
-        
         let n = if let Some(normal) = normal_texture {
-            registry.get_texture(normal).unwrap()
+            registry
+                .get_texture(normal)
+                .expect("normal texture handle missing from registry")
         } else {
             let flat_normal_texture = registry.solid_texture_rgba8(
                 graphics.clone(),
                 [128, 128, 255, 255],
                 Some(Texture::TEXTURE_FORMAT_BASE),
             );
-            registry.get_texture(flat_normal_texture).unwrap()
+            registry
+                .get_texture(flat_normal_texture)
+                .expect("flat normal fallback texture missing from registry")
         };
 
-        let e = if let Some(e) = emissive_texture {
-            registry.get_texture(e).unwrap()
+        let e = if let Some(emissive) = emissive_texture {
+            registry
+                .get_texture(emissive)
+                .expect("emissive texture handle missing from registry")
         } else {
-            let e = registry.solid_texture_rgba8(
+            let emissive = registry.solid_texture_rgba8(
                 graphics.clone(),
                 [0, 0, 0, 255],
                 Some(Texture::TEXTURE_FORMAT_BASE),
             );
-            registry.get_texture(e).unwrap()
+            registry
+                .get_texture(emissive)
+                .expect("emissive fallback texture missing from registry")
         };
 
-        let mr = if let Some(m) = metallic_roughness_texture {
-            registry.get_texture(m).unwrap()
+        let mr = if let Some(metallic_roughness) = metallic_roughness_texture {
+            registry
+                .get_texture(metallic_roughness)
+                .expect("metallic roughness texture handle missing from registry")
         } else {
-            let metallic_roughness_texture = registry.solid_texture_rgba8(
+            let metallic_roughness = registry.solid_texture_rgba8(
                 graphics.clone(),
                 [0, 128, 0, 255],
                 Some(Texture::TEXTURE_FORMAT_BASE),
             );
-            registry.get_texture(metallic_roughness_texture).unwrap()
+            registry
+                .get_texture(metallic_roughness)
+                .expect("metallic roughness fallback texture missing from registry")
         };
 
-        let o = if let Some(o) = occlusion_texture {
-            registry.get_texture(o).unwrap()
+        let o = if let Some(occlusion) = occlusion_texture {
+            registry
+                .get_texture(occlusion)
+                .expect("occlusion texture handle missing from registry")
         } else {
-            let o = registry.solid_texture_rgba8(
+            let occlusion = registry.solid_texture_rgba8(
                 graphics.clone(),
                 [255, 255, 255, 255],
                 Some(Texture::TEXTURE_FORMAT_BASE),
             );
-            registry.get_texture(o).unwrap()
+            registry
+                .get_texture(occlusion)
+                .expect("occlusion fallback texture missing from registry")
         };
 
-        let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
-        tint_buffer.write(&graphics.queue, &uniform);
-
-        let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+        graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
             label: Some("material bind group"),
             layout: &graphics.layouts.material_bind_layout,
             entries: &[
@@ -276,15 +269,11 @@ impl Material {
                 },
                 wgpu::BindGroupEntry {
                     binding: 7,
-                    resource: wgpu::BindingResource::TextureView(
-                        &mr.view,
-                    ),
+                    resource: wgpu::BindingResource::TextureView(&mr.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 8,
-                    resource: wgpu::BindingResource::Sampler(
-                        &mr.sampler,
-                    ),
+                    resource: wgpu::BindingResource::Sampler(&mr.sampler),
                 },
                 wgpu::BindGroupEntry {
                     binding: 9,
@@ -295,7 +284,55 @@ impl Material {
                     resource: wgpu::BindingResource::Sampler(&o.sampler),
                 },
             ],
-        });
+        })
+    }
+
+    pub fn new(
+        registry: &mut AssetRegistry,
+        graphics: Arc<SharedGraphicsContext>,
+        name: impl Into<String>,
+        diffuse_texture: Handle<Texture>,
+        normal_texture: Option<Handle<Texture>>,
+        emissive_texture: Option<Handle<Texture>>,
+        metallic_roughness_texture: Option<Handle<Texture>>,
+        occlusion_texture: Option<Handle<Texture>>,
+        tint: [f32; 4],
+        texture_tag: Option<String>,
+    ) -> Self {
+        puffin::profile_function!();
+        let name = name.into();
+
+        let uv_tiling = [1.0, 1.0];
+        let uniform = MaterialUniform {
+            base_colour: tint,
+            emissive: [0.0, 0.0, 0.0],
+            emissive_strength: 1.0,
+            metallic: 1.0,
+            roughness: 1.0,
+            normal_scale: 1.0,
+            occlusion_strength: 1.0,
+            alpha_cutoff: 0.5,
+            uv_tiling,
+            has_normal_texture: normal_texture.is_some() as u32,
+            has_emissive_texture: emissive_texture.is_some() as u32,
+            has_metallic_texture: metallic_roughness_texture.is_some() as u32,
+            has_occlusion_texture: occlusion_texture.is_some() as u32,
+            pad: 0,
+            _pad: 0,
+        };
+
+        let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
+        tint_buffer.write(&graphics.queue, &uniform);
+        let bind_group = Self::build_bind_group(
+            registry,
+            &graphics,
+            &tint_buffer,
+            diffuse_texture,
+            normal_texture,
+            emissive_texture,
+            metallic_roughness_texture,
+            occlusion_texture,
+        );
 
         Self {
             name,
@@ -337,10 +374,28 @@ impl Material {
             has_occlusion_texture: self.occlusion_texture.is_some() as u32,
             pad: 0,
             has_normal_texture: self.normal_texture.is_some() as u32,
+            _pad: 0,
         };
 
         self.tint_buffer.write(&graphics.queue, &uniform);
     }
+
+    pub fn rebuild_bind_group(
+        &mut self,
+        registry: &mut AssetRegistry,
+        graphics: &Arc<SharedGraphicsContext>,
+    ) {
+        self.bind_group = Self::build_bind_group(
+            registry,
+            graphics,
+            &self.tint_buffer,
+            self.diffuse_texture,
+            self.normal_texture,
+            self.emissive_texture,
+            self.metallic_roughness_texture,
+            self.occlusion_texture,
+        );
+    }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
@@ -1139,20 +1194,16 @@ impl Model {
             let material_name = processed.name;
 
             let build_texture = |tex: ProcessedTexture, format: wgpu::TextureFormat| -> Texture {
-                let wrap = match tex.sampler.address_mode_u {
-                    wgpu::AddressMode::Repeat | wgpu::AddressMode::MirrorRepeat => TextureWrapMode::Repeat,
-                    _ => TextureWrapMode::Clamp,
-                };
                 let mut builder = TextureBuilder::new(&graphics.device)
                     .with_raw_pixels(graphics.clone(), tex.pixels.as_slice())
                     .size(tex.dimensions.0, tex.dimensions.1)
                     .format(format)
-                    .wrap_mode(wrap)
-                    .mag_filter(tex.sampler.mag_filter)
+                    .sampler(tex.sampler)
                     .label(material_name.as_str());
-                if let Some(ref mime) = tex.mime_type {
-                    builder = builder.mime_type(mime.as_str());
+                if let Some(mime) = tex.mime_type {
+                    builder = builder.mime_type(&mime);
                 }
+
                 builder.build()
             };
 
@@ -1665,6 +1716,7 @@ pub struct MaterialUniform {
     pub normal_scale: f32,
     pub occlusion_strength: f32,
     pub alpha_cutoff: f32,
+    pub _pad: u32,
     pub uv_tiling: [f32; 2],
     pub has_normal_texture: u32,
     pub has_emissive_texture: u32,
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index 7b26fd2..b53390c 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -1,5 +1,3 @@
-// Main shaders for standard objects.
-
 struct Globals {
     num_lights: u32,
     ambient_strength: f32,
@@ -187,7 +185,7 @@ fn vs_main(
         u_morph_info.uses_morph != 0u,
     );
     let world_position = model_matrix * skin_matrix * vec4<f32>(morphed_pos, 1.0);
-    
+
     let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz;
     let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz;
 
@@ -278,9 +276,10 @@ fn apply_normal_map(
     world_normal_in: vec3<f32>,
     world_tangent_in: vec3<f32>,
     world_bitangent_in: vec3<f32>,
-    normal_sample_rgb: vec3<f32>,
+    normal_sample_raw: vec3<f32>,
+    normal_scale: f32,
 ) -> vec3<f32> {
-    let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0));
+    let unpacked = normalize((normal_sample_raw * 2.0 - 1.0) * vec3<f32>(normal_scale, normal_scale, 1.0));
 
     let n = normalize(world_normal_in);
     var t = normalize(world_tangent_in);
@@ -291,7 +290,11 @@ fn apply_normal_map(
     let b = cross(n, t) * handedness;
 
     let tbn = mat3x3<f32>(t, b, n);
-    return normalize(tbn * normal_ts);
+    return normalize(tbn * unpacked);
+}
+
+fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> {
+    return f0 + (vec3<f32>(1.0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
 }
 
 @fragment
@@ -307,13 +310,13 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
 
     var world_normal: vec3<f32>;
     if (u_material.has_normal_texture != 0u) {
-        let object_normal = textureSample(t_normal, s_normal, uv).xyz;
-        let scaled_normal = normalize((object_normal * 2.0 - 1.0) * vec3<f32>(u_material.normal_scale, u_material.normal_scale, 1.0));
+        let normal_sample_raw = textureSample(t_normal, s_normal, uv).xyz;
         world_normal = apply_normal_map(
             in.world_normal,
             in.world_tangent,
             in.world_bitangent,
-            scaled_normal,
+            normal_sample_raw,
+            u_material.normal_scale,
         );
     } else {
         world_normal = normalize(in.world_normal);
@@ -340,6 +343,7 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
     }
 
     let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
+
     let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.rgb * occlusion;
 
     var final_color = ambient;
@@ -356,14 +360,16 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
         }
     }
 
-    final_color *= occlusion;
-
     let world_reflect = reflect(-view_dir, world_normal);
     let reflection = textureSample(env_map, env_sampler, world_reflect).rgb;
-    let shininess = (1.0 - roughness) * metallic;
-    final_color += reflection * shininess;
+    let n_dot_v = max(dot(world_normal, view_dir), 0.0);
+    let f0 = mix(vec3<f32>(0.04), base_colour.rgb, metallic);
+    let fresnel = fresnel_schlick(n_dot_v, f0);
+    let reflection_weight = fresnel * (1.0 - roughness) * mix(vec3<f32>(1.0), base_colour.rgb, metallic);
+    final_color += reflection * reflection_weight;
 
     final_color += emissive;
 
+    // return vec4<f32>(uv.x, uv.y, 0.0, 1.0);
     return vec4<f32>(final_color, base_colour.a);
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 707485e..2e754a1 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -2,11 +2,11 @@ use std::sync::Arc;
 
 use crate::asset::AssetRegistry;
 use crate::graphics::SharedGraphicsContext;
-use crate::utils::{ResourceReference, ToPotentialString};
+use crate::utils::{ResourceReference};
 use image::{DynamicImage, GenericImageView, RgbaImage};
 use rkyv::Archive;
 use serde::{Deserialize, Serialize};
-use wgpu::{TextureAspect, TextureFormat, TextureUsages, TextureViewDescriptor, TextureViewDimension};
+use wgpu::{SamplerDescriptor, TextureAspect, TextureFormat, TextureUsages, TextureViewDescriptor, TextureViewDimension};
 use crate::multisampling::{AntiAliasingMode};
 
 /// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture.
@@ -162,9 +162,9 @@ impl<'a> TextureBuilder<'a> {
             mip_level_count: 1,
             auto_mip: false,
             mag_filter: wgpu::FilterMode::Linear,
-            min_filter: wgpu::FilterMode::Nearest,
-            mipmap_filter: wgpu::FilterMode::Nearest,
-            wrap_mode: TextureWrapMode::Clamp,
+            min_filter: wgpu::FilterMode::Linear,
+            mipmap_filter: wgpu::FilterMode::Linear,
+            wrap_mode: TextureWrapMode::Repeat,
             compare: None,
             lod_min_clamp: 0.0,
             lod_max_clamp: 32.0,
@@ -247,6 +247,16 @@ impl<'a> TextureBuilder<'a> {
         self
     }
 
+    pub fn sampler(mut self, sampler: SamplerDescriptor) -> Self {
+        self.wrap_mode = sampler.address_mode_u.into();
+        self.mag_filter = sampler.mag_filter;
+        self.min_filter = sampler.min_filter;
+        self.compare = sampler.compare;
+        self.lod_max_clamp = sampler.lod_max_clamp;
+        self.lod_min_clamp = sampler.lod_min_clamp;
+        self
+    }
+
     pub fn with_bytes(mut self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self {
         self.graphics = Some(graphics);
         let hash = AssetRegistry::hash_bytes(bytes);
@@ -615,366 +625,6 @@ impl Texture {
     pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
     pub const TEXTURE_FORMAT_BASE: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
     pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
-
-    /// Loads the texture from bytes and generates mipmaps on the GPU using the specified format.
-    pub(crate) fn from_bytes_verbose_mipmapped_with_format(
-        graphics: Arc<SharedGraphicsContext>,
-        bytes: &[u8],
-        dimensions: Option<(u32, u32)>,
-        view_descriptor: Option<wgpu::TextureViewDescriptor>,
-        sampler: Option<wgpu::SamplerDescriptor>,
-        format: wgpu::TextureFormat,
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or(""));
-        let texture = Self::from_bytes_verbose_with_format(
-            graphics.clone(),
-            bytes,
-            dimensions,
-            Some(format),
-            view_descriptor,
-            sampler,
-            label,
-        );
-
-        if let Err(err) =
-            graphics
-                .mipmapper
-                .compute_mipmaps(&graphics.device, &graphics.queue, &texture)
-        {
-            log_once::warn_once!("Failed to generate mipmaps: {}", err);
-        }
-
-        texture
-    }
-
-    fn from_bytes_verbose_with_format(
-        graphics: Arc<SharedGraphicsContext>,
-        bytes: &[u8],
-        dimensions: Option<(u32, u32)>,
-        format_override: Option<wgpu::TextureFormat>,
-        view_descriptor: Option<wgpu::TextureViewDescriptor>,
-        sampler: Option<wgpu::SamplerDescriptor>,
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or(""));
-        if let Some(l) = label {
-            log::debug!("Loading texture: {l}");
-        }
-
-        let hash = AssetRegistry::hash_bytes(bytes);
-
-        let (diffuse_rgba, dimensions) = {
-            puffin::profile_scope!("load from memory image");
-            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.",
-                                label,
-                                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.",
-                            label,
-                            err
-                        );
-                        (vec![255, 0, 255, 255], (1, 1))
-                    }
-                }
-            }
-        };
-
-        let size = wgpu::Extent3d {
-            width: dimensions.0,
-            height: dimensions.1,
-            depth_or_array_layers: 1,
-        };
-
-        let mip_level_count = size.width.min(size.height).ilog2() + 1;
-        log::debug!("Mip level count [{:?}]: {}", label, mip_level_count);
-
-        let texture_format = format_override.unwrap_or(Texture::TEXTURE_FORMAT);
-        let texture = graphics.device.create_texture(&wgpu::TextureDescriptor {
-            label: Some(format!("{:?} diffuse blit texture", label).as_str()),
-            size,
-            mip_level_count,
-            sample_count: 1,
-            dimension: wgpu::TextureDimension::D2,
-            format: texture_format,
-            usage: wgpu::TextureUsages::TEXTURE_BINDING
-                | wgpu::TextureUsages::RENDER_ATTACHMENT
-                | wgpu::TextureUsages::COPY_DST
-                | wgpu::TextureUsages::COPY_SRC,
-            view_formats: &[],
-        });
-
-        let unpadded_bytes_per_row = 4 * size.width;
-        let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT
-            - 1)
-            & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1);
-        debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize);
-
-        if padded_bytes_per_row == unpadded_bytes_per_row {
-            puffin::profile_scope!("write to texture");
-            graphics.queue.write_texture(
-                wgpu::TexelCopyTextureInfo {
-                    texture: &texture,
-                    mip_level: 0,
-                    origin: wgpu::Origin3d::ZERO,
-                    aspect: wgpu::TextureAspect::All,
-                },
-                &diffuse_rgba,
-                wgpu::TexelCopyBufferLayout {
-                    offset: 0,
-                    bytes_per_row: Some(unpadded_bytes_per_row),
-                    rows_per_image: Some(size.height),
-                },
-                size,
-            );
-        } else {
-            puffin::profile_scope!("write to texture");
-            let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize];
-            let src_stride = unpadded_bytes_per_row as usize;
-            let dst_stride = padded_bytes_per_row as usize;
-            for row in 0..size.height as usize {
-                let src_start = row * src_stride;
-                let dst_start = row * dst_stride;
-                padded[dst_start..dst_start + src_stride]
-                    .copy_from_slice(&diffuse_rgba[src_start..src_start + src_stride]);
-            }
-
-            graphics.queue.write_texture(
-                wgpu::TexelCopyTextureInfo {
-                    texture: &texture,
-                    mip_level: 0,
-                    origin: wgpu::Origin3d::ZERO,
-                    aspect: wgpu::TextureAspect::All,
-                },
-                &padded,
-                wgpu::TexelCopyBufferLayout {
-                    offset: 0,
-                    bytes_per_row: Some(padded_bytes_per_row),
-                    rows_per_image: Some(size.height),
-                },
-                size,
-            );
-        }
-
-        let sampler_desc = if let Some(sampler) = sampler {
-            sampler
-        } else {
-            wgpu::SamplerDescriptor {
-                address_mode_u: wgpu::AddressMode::ClampToEdge,
-                address_mode_v: wgpu::AddressMode::ClampToEdge,
-                address_mode_w: wgpu::AddressMode::ClampToEdge,
-                mag_filter: wgpu::FilterMode::Linear,
-                min_filter: wgpu::FilterMode::Nearest,
-                mipmap_filter: wgpu::FilterMode::Linear,
-                ..Default::default()
-            }
-        };
-
-        let sampler = graphics.device.create_sampler(&sampler_desc);
-
-        let view = texture.create_view(&view_descriptor.unwrap_or_default());
-
-        Self {
-            label: label.to_potential_string(),
-            texture,
-            sampler,
-            size,
-            view,
-            hash: Some(hash),
-            reference: Some(ResourceReference::from_bytes(bytes)),
-        }
-    }
-
-    pub fn from_raw_pixels_mipmapped_with_format(
-        graphics: Arc<SharedGraphicsContext>,
-        pixels: &[u8],
-        dimensions: (u32, u32),
-        format: wgpu::TextureFormat,
-        sampler: Option<wgpu::SamplerDescriptor>,
-        label: Option<&str>,
-        _mime_type: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or(""));
-        if let Some(l) = label {
-            log::debug!("Loading texture: {l}");
-        }
-
-        let hash = AssetRegistry::hash_bytes(pixels);
-
-        let bytes_per_pixel = match format.block_copy_size(None) {
-            Some(value) => value,
-            None => {
-                log::error!(
-                    "Texture [{:?}] has unsupported format {:?}; falling back to 1x1 magenta.",
-                    label,
-                    format
-                );
-                return Self::from_bytes_verbose_mipmapped_with_format(
-                    graphics,
-                    &[255, 0, 255, 255],
-                    Some((1, 1)),
-                    None,
-                    sampler,
-                    Texture::TEXTURE_FORMAT,
-                    label,
-                );
-            }
-        };
-
-        let expected_len = (dimensions.0 as usize)
-            .saturating_mul(dimensions.1 as usize)
-            .saturating_mul(bytes_per_pixel as usize);
-        if pixels.len() != expected_len {
-            log::error!(
-                "Texture [{:?}] byte length {} does not match expected {} for {:?} ({}x{}). Falling back to 1x1 magenta.",
-                label,
-                pixels.len(),
-                expected_len,
-                format,
-                dimensions.0,
-                dimensions.1
-            );
-            return Self::from_bytes_verbose_mipmapped_with_format(
-                graphics,
-                &[255, 0, 255, 255],
-                Some((1, 1)),
-                None,
-                sampler,
-                Texture::TEXTURE_FORMAT,
-                label,
-            );
-        }
-
-        let size = wgpu::Extent3d {
-            width: dimensions.0,
-            height: dimensions.1,
-            depth_or_array_layers: 1,
-        };
-
-        let mip_level_count = size.width.min(size.height).ilog2() + 1;
-        log::debug!("Mip level count [{:?}]: {}", label, mip_level_count);
-
-        let texture = graphics.device.create_texture(&wgpu::TextureDescriptor {
-            label: Some(format!("{:?} raw texture", label).as_str()),
-            size,
-            mip_level_count,
-            sample_count: 1,
-            dimension: wgpu::TextureDimension::D2,
-            format,
-            usage: wgpu::TextureUsages::TEXTURE_BINDING
-                | wgpu::TextureUsages::RENDER_ATTACHMENT
-                | wgpu::TextureUsages::COPY_DST
-                | wgpu::TextureUsages::COPY_SRC,
-            view_formats: &[],
-        });
-
-        let unpadded_bytes_per_row = bytes_per_pixel * size.width;
-        let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT
-            - 1)
-            & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1);
-
-        if padded_bytes_per_row == unpadded_bytes_per_row {
-            puffin::profile_scope!("write to texture");
-            graphics.queue.write_texture(
-                wgpu::TexelCopyTextureInfo {
-                    texture: &texture,
-                    mip_level: 0,
-                    origin: wgpu::Origin3d::ZERO,
-                    aspect: wgpu::TextureAspect::All,
-                },
-                pixels,
-                wgpu::TexelCopyBufferLayout {
-                    offset: 0,
-                    bytes_per_row: Some(unpadded_bytes_per_row),
-                    rows_per_image: Some(size.height),
-                },
-                size,
-            );
-        } else {
-            puffin::profile_scope!("write to texture");
-            let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize];
-            let src_stride = unpadded_bytes_per_row as usize;
-            let dst_stride = padded_bytes_per_row as usize;
-            for row in 0..size.height as usize {
-                let src_start = row * src_stride;
-                let dst_start = row * dst_stride;
-                padded[dst_start..dst_start + src_stride]
-                    .copy_from_slice(&pixels[src_start..src_start + src_stride]);
-            }
-
-            graphics.queue.write_texture(
-                wgpu::TexelCopyTextureInfo {
-                    texture: &texture,
-                    mip_level: 0,
-                    origin: wgpu::Origin3d::ZERO,
-                    aspect: wgpu::TextureAspect::All,
-                },
-                &padded,
-                wgpu::TexelCopyBufferLayout {
-                    offset: 0,
-                    bytes_per_row: Some(padded_bytes_per_row),
-                    rows_per_image: Some(size.height),
-                },
-                size,
-            );
-        }
-
-        let sampler_desc = sampler.unwrap_or(wgpu::SamplerDescriptor {
-            address_mode_u: wgpu::AddressMode::ClampToEdge,
-            address_mode_v: wgpu::AddressMode::ClampToEdge,
-            address_mode_w: wgpu::AddressMode::ClampToEdge,
-            mag_filter: wgpu::FilterMode::Linear,
-            min_filter: wgpu::FilterMode::Nearest,
-            mipmap_filter: wgpu::FilterMode::Linear,
-            ..Default::default()
-        });
-
-        let sampler = graphics.device.create_sampler(&sampler_desc);
-        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
-
-        let texture = Self {
-            label: label.to_potential_string(),
-            texture,
-            sampler,
-            size,
-            view,
-            hash: Some(hash),
-            reference: Some(ResourceReference::from_bytes(pixels)),
-        };
-
-        if let Err(err) =
-            graphics
-                .mipmapper
-                .compute_mipmaps(&graphics.device, &graphics.queue, &texture)
-        {
-            log_once::warn_once!("Failed to generate mipmaps: {}", err);
-        }
-
-        texture
-    }
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
@@ -998,6 +648,17 @@ impl Into<wgpu::AddressMode> for TextureWrapMode {
     }
 }
 
+impl Into<TextureWrapMode> for wgpu::AddressMode {
+    fn into(self) -> TextureWrapMode {
+        match self {
+            wgpu::AddressMode::ClampToEdge => TextureWrapMode::Clamp,
+            wgpu::AddressMode::Repeat => TextureWrapMode::Repeat,
+            wgpu::AddressMode::MirrorRepeat => TextureWrapMode::Repeat,
+            wgpu::AddressMode::ClampToBorder => TextureWrapMode::Clamp,
+        }
+    }
+}
+
 pub struct DropbearEngineLogo;
 
 impl DropbearEngineLogo {
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index c06bd98..89b5f28 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -678,6 +678,12 @@ impl Component for MeshRenderer {
                     if let Some(tex) = get_tex_handle(&m.metallic_roughness_texture).await {
                         mat.metallic_roughness_texture = Some(tex?);
                     }
+
+                    {
+                        let mut registry = ASSET_REGISTRY.write();
+                        mat.rebuild_bind_group(&mut registry, &graphics);
+                    }
+                    mat.sync_uniform(&graphics);
                 }
             }
 
@@ -1398,6 +1404,10 @@ impl InspectableComponent for MeshRenderer {
                                                     material.uv_tiling = default.uv_tiling;
                                                     material.texture_tag = default.texture_tag.clone();
                                                     material.wrap_mode = default.wrap_mode;
+                                                    {
+                                                        let mut registry = ASSET_REGISTRY.write();
+                                                        material.rebuild_bind_group(&mut registry, &graphics);
+                                                    }
                                                     material.sync_uniform(&graphics);
                                                 }
                                             }
@@ -1730,6 +1740,11 @@ impl InspectableComponent for MeshRenderer {
                                         ) {
                                             if let Some(tex) = new_diffuse {
                                                 material.diffuse_texture = tex;
+                                                {
+                                                    let mut registry = ASSET_REGISTRY.write();
+                                                    material.rebuild_bind_group(&mut registry, &graphics);
+                                                }
+                                                material.sync_uniform(&graphics);
                                             }
                                         }
 
@@ -1743,6 +1758,11 @@ impl InspectableComponent for MeshRenderer {
                                         ) {
                                             if let Some(tex) = new_normal {
                                                 material.normal_texture = Some(tex);
+                                                {
+                                                    let mut registry = ASSET_REGISTRY.write();
+                                                    material.rebuild_bind_group(&mut registry, &graphics);
+                                                }
+                                                material.sync_uniform(&graphics);
                                             }
                                         }
 
@@ -1755,6 +1775,11 @@ impl InspectableComponent for MeshRenderer {
                                             default_emissive,
                                         ) {
                                             material.emissive_texture = new_emissive;
+                                            {
+                                                let mut registry = ASSET_REGISTRY.write();
+                                                material.rebuild_bind_group(&mut registry, &graphics);
+                                            }
+                                            material.sync_uniform(&graphics);
                                         }
 
                                         if let Some(new_mr) = texture_combo(
@@ -1766,6 +1791,11 @@ impl InspectableComponent for MeshRenderer {
                                             default_mr,
                                         ) {
                                             material.metallic_roughness_texture = new_mr;
+                                            {
+                                                let mut registry = ASSET_REGISTRY.write();
+                                                material.rebuild_bind_group(&mut registry, &graphics);
+                                            }
+                                            material.sync_uniform(&graphics);
                                         }
 
                                         if let Some(new_occ) = texture_combo(
@@ -1777,6 +1807,11 @@ impl InspectableComponent for MeshRenderer {
                                             default_occ,
                                         ) {
                                             material.occlusion_texture = new_occ;
+                                            {
+                                                let mut registry = ASSET_REGISTRY.write();
+                                                material.rebuild_bind_group(&mut registry, &graphics);
+                                            }
+                                            material.sync_uniform(&graphics);
                                         }
                                     });
                             }
diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index cfa0996..5860cae 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -195,6 +195,8 @@ impl ProjectConfig {
                     e
                 );
 
+                log::error!("{}", msg);
+
                 let should_recover = rfd::MessageDialog::new()
                     .set_title("Scene loading error")
                     .set_description(&msg)
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index adcd122..5938e24 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -480,7 +480,7 @@ impl Scene for Editor {
             globals.write(&graphics.queue);
         }
 
-        let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
+        let mut static_batches: HashMap<u64, Vec<(Entity, InstanceRaw)>> = HashMap::new();
         let mut animated_instances: Vec<
             (Entity, u64, InstanceRaw, wgpu::Buffer, wgpu::Buffer, wgpu::Buffer, u32),
         > = Vec::new();
@@ -503,7 +503,10 @@ impl Scene for Editor {
                     let has_skinning = !animation.skinning_matrices.is_empty();
                     let has_morph_weights = !animation.morph_weights.is_empty();
                     if !has_skinning && !has_morph_weights {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                        static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     }
 
@@ -518,12 +521,18 @@ impl Scene for Editor {
                     } else if !has_skinning {
                         let Some(default_skinning_buffer) = self.default_skinning_buffer.as_ref()
                         else {
-                            static_batches.entry(handle.id).or_default().push(instance);
+                            static_batches
+                                .entry(handle.id)
+                                .or_default()
+                                .push((entity, instance));
                             continue;
                         };
                         default_skinning_buffer.clone()
                     } else {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                        static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     };
                     let Some(morph_weights_buffer) = animation
@@ -531,7 +540,10 @@ impl Scene for Editor {
                         .as_ref()
                         .map(|buffer| buffer.buffer().clone())
                     else {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                        static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     };
                     let Some(morph_info_buffer) = animation
@@ -539,7 +551,10 @@ impl Scene for Editor {
                         .as_ref()
                         .map(|buffer| buffer.buffer().clone())
                     else {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                        static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     };
 
@@ -553,20 +568,29 @@ impl Scene for Editor {
                         animation.morph_weight_count,
                     ));
                 } else {
-                    static_batches.entry(handle.id).or_default().push(instance);
+                    static_batches
+                        .entry(handle.id)
+                        .or_default()
+                        .push((entity, instance));
                 }
             }
         }
 
         let registry = ASSET_REGISTRY.read();
         let mut prepared_models = Vec::new();
-        for (handle, instances) in static_batches {
+        for (handle, batched_instances) in static_batches {
             puffin::profile_scope!("preparing models");
             let Some(model) = registry.get_model(Handle::new(handle)) else {
                 log_once::error_once!("Missing model handle {} in registry", handle);
                 continue;
             };
 
+            let entity = batched_instances.first().map(|(entity, _)| *entity);
+            let instances: Vec<InstanceRaw> = batched_instances
+                .into_iter()
+                .map(|(_, instance)| instance)
+                .collect();
+
             let instance_buffer = self.instance_buffer_cache.entry(handle).or_insert_with(|| {
                 ResizableBuffer::new(
                     &graphics.device,
@@ -577,7 +601,7 @@ impl Scene for Editor {
             });
             instance_buffer.write(&graphics.device, &graphics.queue, &instances);
 
-            prepared_models.push((model, handle, instances.len() as u32));
+            prepared_models.push((model, handle, instances.len() as u32, entity));
         }
 
         if let Some(light_pipeline) = &self.light_cube_pipeline {
@@ -640,7 +664,13 @@ impl Scene for Editor {
         // static models
         if let Some(_) = &self.light_cube_pipeline {
             puffin::profile_scope!("model render pass");
-            for (model, handle, instance_count) in prepared_models {
+            for (model, handle, instance_count, entity) in prepared_models {
+                let Some(entity) = entity else {
+                    continue;
+                };
+                let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else {
+                    continue;
+                };
                 let default_skinning_buffer = self
                     .default_skinning_buffer
                     .as_ref()
@@ -740,6 +770,12 @@ impl Scene for Editor {
                         .write_buffer(default_morph_info_buffer, 0, bytemuck::bytes_of(&info));
 
                     let material = &model.materials[mesh.material];
+                    let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) {
+                        mat
+                    } else {
+                        log_once::warn_once!("Unable to locate MeshRenderer's material_snapshot for that specific material");
+                        material
+                    };
                     render_pass.draw_mesh_instanced(
                         mesh,
                         material,
@@ -765,6 +801,9 @@ impl Scene for Editor {
                 morph_weight_count,
             ) in animated_instances
             {
+                let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else {
+                    continue;
+                };
                 puffin::profile_scope!("rendering animated model", format!("{:?}", entity));
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
@@ -848,6 +887,12 @@ impl Scene for Editor {
                         .write_buffer(&morph_info_buffer, 0, bytemuck::bytes_of(&info));
 
                     let material = &model.materials[mesh.material];
+                    let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) {
+                        mat
+                    } else {
+                        log_once::warn_once!("Unable to locate MeshRenderer's material_snapshot for that specific material");
+                        material
+                    };
                     render_pass.draw_mesh_instanced(
                         mesh,
                         material,