kitgit

tirbofish/dropbear · commit

5b2e1a21ac1e117e61b07282462e9e933c8b5418

refactor: created new TextureBuilder struct to clean up the mess that was before. refactor: made AssetViewer use Arc<T> instead of references. fix: on colour change, it constantly recreates the texture.

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-03-07 15:06

view full diff

diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index 19a0a30..a7df5a3 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -11,6 +11,7 @@ readme.workspace = true
 [dependencies]
 dropbear_future-queue = { path = "../dropbear_future-queue" }
 dropbear-macro = { path = "../dropbear-macro" }
+dropbear-utils = { path = "../dropbear-utils" }
 slank = { path = "../slank", features = ["download-slang", "use-wgpu"] }
 
 anyhow.workspace = true
diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index ae82706..bc7970b 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -66,10 +66,10 @@ impl<T> Handle<T> {
 }
 
 pub struct AssetRegistry {
-    textures: HashMap<u64, Texture>,
+    textures: HashMap<u64, Arc<Texture>>,
     texture_labels: HashMap<String, Handle<Texture>>,
 
-    models: HashMap<u64, Model>,
+    models: HashMap<u64, Arc<Model>>,
     model_labels: HashMap<String, Handle<Model>>,
 }
 
@@ -113,13 +113,6 @@ impl AssetRegistry {
         hasher.finish()
     }
 
-    /// A convenient helper function for hashing a byte slice of data.
-    pub(crate) fn hash_contents<T: Hash>(data: T) -> u64 {
-        let mut hasher = DefaultHasher::new();
-        data.hash(&mut hasher);
-        hasher.finish()
-    }
-
     /// Checks if the asset registry contains a handle with the given hash.
     ///
     /// It will check all different types, so it does not point out specifically where.
@@ -146,7 +139,7 @@ impl AssetRegistry {
             .hash
             .map(|v| Handle::new(v))
             .unwrap_or_else(|| Handle::NULL);
-        self.textures.entry(handle.id).or_insert(texture);
+        self.textures.entry(handle.id).or_insert(Arc::new(texture));
         handle
     }
 
@@ -162,6 +155,14 @@ impl AssetRegistry {
         handle
     }
 
+    pub fn update_texture(
+        &mut self,
+        handle: Handle<Texture>,
+        new_texture: Texture,
+    ) -> Option<Arc<Texture>> {
+        self.textures.insert(handle.id, Arc::new(new_texture))
+    }
+
     /// Maps a label to an existing texture handle.
     pub fn label_texture(&mut self, label: impl Into<String>, handle: Handle<Texture>) {
         self.texture_labels.insert(label.into(), handle.clone());
@@ -174,20 +175,15 @@ impl AssetRegistry {
         self.texture_labels.remove(label);
     }
 
-    /// Updates the asset server by inserting the texture provided at the location of the handle,
-    /// and removing the old texture (by returning it back to you).
-    pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> {
-        self.textures.insert(handle.id, texture)
+    pub fn get_texture(&self, handle: Handle<Texture>) -> Option<Arc<Texture>> {
+        self.textures.get(&handle.id).cloned()
     }
 
-    pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> {
-        self.textures.get(&handle.id)
-    }
-
-    pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> {
+    pub fn get_texture_by_label(&self, label: &str) -> Option<Arc<Texture>> {
         self.texture_labels
             .get(label)
             .and_then(|handle| self.textures.get(&handle.id))
+            .cloned()
     }
 
     pub fn get_texture_handle_from_label(&self, label: &str) -> Option<Handle<Texture>> {
@@ -199,10 +195,10 @@ impl AssetRegistry {
     }
 
     pub fn grey_texture(&mut self, graphics: Arc<SharedGraphicsContext>) -> Handle<Texture> {
-        self.solid_texture_rgba8_with_format(
+        self.solid_texture_rgba8(
             graphics,
             [128, 128, 128, 255],
-            Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+            Some(Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix()),
         )
     }
 
@@ -210,27 +206,18 @@ impl AssetRegistry {
         &mut self,
         graphics: Arc<SharedGraphicsContext>,
         rgba: [u8; 4],
+        format: Option<wgpu::TextureFormat>,
     ) -> Handle<Texture> {
-        self.solid_texture_rgba8_with_format(graphics, rgba, Texture::TEXTURE_FORMAT)
-    }
+        let format = format.unwrap_or(Texture::TEXTURE_FORMAT);
 
-    pub fn solid_texture_rgba8_with_format(
-        &mut self,
-        graphics: Arc<SharedGraphicsContext>,
-        rgba: [u8; 4],
-        format: wgpu::TextureFormat,
-    ) -> Handle<Texture> {
         let format_tag = format!("{:?}", format);
-        let handle = Handle::new(Self::hash_contents((rgba, format_tag.as_str())));
-
-        if self.contains_hash(handle.id) {
-            return handle;
-        }
-
         let label = format!(
             "Solid texture [{}, {}, {}, {}] {}",
             rgba[0], rgba[1], rgba[2], rgba[3], format_tag,
         );
+        if let Some(handle) = self.get_texture_handle_from_label(label.as_str()) {
+            return handle;
+        }
 
         let texture = Texture::from_bytes_verbose_mipmapped_with_format(
             graphics,
@@ -260,7 +247,7 @@ impl AssetRegistry {
 impl AssetRegistry {
     pub fn add_model(&mut self, model: Model) -> Handle<Model> {
         let handle = Handle::new(model.hash);
-        self.models.entry(handle.id).or_insert(model);
+        self.models.entry(handle.id).or_insert(Arc::new(model));
         handle
     }
 
@@ -278,7 +265,7 @@ impl AssetRegistry {
         self.model_labels.insert(label.into(), handle.clone());
     }
 
-    pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Model> {
+    pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Arc<Model>> {
         if let Some(existing) = self.models.get(&handle.id) {
             if existing.label.eq_ignore_ascii_case("light cube") {
                 log::warn!("Attempted to update protected model '{}'", existing.label);
@@ -286,21 +273,18 @@ impl AssetRegistry {
             }
         }
 
-        self.models.insert(handle.id, model)
-    }
-
-    pub fn get_model(&self, handle: Handle<Model>) -> Option<&Model> {
-        self.models.get(&handle.id)
+        self.models.insert(handle.id, Arc::new(model))
     }
 
-    pub fn get_model_mut(&mut self, handle: Handle<Model>) -> Option<&mut Model> {
-        self.models.get_mut(&handle.id)
+    pub fn get_model(&self, handle: Handle<Model>) -> Option<Arc<Model>> {
+        self.models.get(&handle.id).cloned()
     }
 
-    pub fn get_model_by_label(&self, label: &str) -> Option<&Model> {
+    pub fn get_model_by_label(&self, label: &str) -> Option<Arc<Model>> {
         self.model_labels
             .get(label)
             .and_then(|handle| self.models.get(&handle.id))
+            .cloned()
     }
 
     pub fn get_model_handle_from_label(&self, label: &str) -> Option<Handle<Model>> {
diff --git a/crates/dropbear-engine/src/billboarding.rs b/crates/dropbear-engine/src/billboarding.rs
index ec79992..95c4fd3 100644
--- a/crates/dropbear-engine/src/billboarding.rs
+++ b/crates/dropbear-engine/src/billboarding.rs
@@ -117,7 +117,7 @@ impl BillboardPipeline {
 
         {
             let mut registry = ASSET_REGISTRY.write();
-            let handle = registry.solid_texture_rgba8(graphics.clone(), [0, 0, 0, 0]); // make transparent for now
+            let handle = registry.solid_texture_rgba8(graphics.clone(), [0, 0, 0, 0], None); // make transparent for now
             let _ = registry.get_texture(handle);
         }
 
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index 01cd9d2..2d8afa0 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -392,26 +392,20 @@ impl MeshRenderer {
 
         if let Some(model) = registry.get_model(self.handle) {
             for material in &model.materials {
-                if material.diffuse_texture.hash == Some(texture.id) {
+                if material.diffuse_texture == texture {
                     return true;
                 }
-                if material.normal_texture.hash == Some(texture.id) {
+                if material.normal_texture == Some(texture) {
                     return true;
                 }
-                if let Some(emissive) = &material.emissive_texture {
-                    if emissive.hash == Some(texture.id) {
-                        return true;
-                    }
+                if material.emissive_texture == Some(texture) {
+                    return true;
                 }
-                if let Some(mr) = &material.metallic_roughness_texture {
-                    if mr.hash == Some(texture.id) {
-                        return true;
-                    }
+                if material.metallic_roughness_texture == Some(texture) {
+                    return true;
                 }
-                if let Some(occ) = &material.occlusion_texture {
-                    if occ.hash == Some(texture.id) {
-                        return true;
-                    }
+                if material.occlusion_texture == Some(texture) {
+                    return true;
                 }
             }
         }
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 8cf784c..17b0048 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -24,8 +24,8 @@ pub struct SharedGraphicsContext {
     pub surface_config: Arc<RwLock<SurfaceConfiguration>>,
     pub instance: Arc<wgpu::Instance>,
     pub window: Arc<Window>,
-    pub viewport_texture: texture::Texture,
-    pub depth_texture: texture::Texture,
+    pub viewport_texture: Arc<texture::Texture>,
+    pub depth_texture: Arc<texture::Texture>,
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 34c1483..51d101c 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -69,7 +69,7 @@ use winit::{
 use crate::egui_renderer::EguiRenderer;
 use crate::graphics::{CommandEncoder, SharedGraphicsContext};
 use crate::mipmap::MipMapper;
-use crate::texture::Texture;
+use crate::texture::{Texture, TextureBuilder};
 
 use crate::pipelines::hdr::HdrPipeline;
 use crate::scene::Scene;
@@ -375,8 +375,8 @@ pub struct State {
     pub config: Arc<RwLock<SurfaceConfiguration>>,
     pub is_surface_configured: bool,
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
-    pub depth_texture: Texture,
-    pub viewport_texture: Texture,
+    pub depth_texture: Arc<Texture>,
+    pub viewport_texture: Arc<Texture>,
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
     pub mipmapper: Arc<MipMapper>,
@@ -529,8 +529,15 @@ Hardware:
             surface.configure(&device, &config);
         }
 
-        let depth_texture = Texture::depth_texture(&config, &device, antialiasing, Some("depth texture"));
-        let viewport_texture = Texture::viewport(&config, &device, Some("viewport texture"));
+        let depth_texture = Arc::new(TextureBuilder::new(&device)
+            .depth(&config, antialiasing)
+            .label("depth texture")
+            .build());
+
+        let viewport_texture = Arc::new(TextureBuilder::new(&device)
+            .viewport(&config)
+            .label("viewport texture")
+            .build());
 
         let mipmapper = Arc::new(MipMapper::new(&device));
 
@@ -598,10 +605,18 @@ Hardware:
             self.hdr.write().resize(&self.device, width, height, Some(*self.antialiasing.read()));
         }
 
-        self.depth_texture =
-            Texture::depth_texture(&self.config.read(), &self.device, *self.antialiasing.read(), Some("depth texture"));
-        self.viewport_texture =
-            Texture::viewport(&self.config.read(), &self.device, Some("viewport texture"));
+        let depth_texture = TextureBuilder::new(&self.device)
+            .depth(&self.config.read(), *self.antialiasing.read())
+            .label("depth texture")
+            .build();
+
+        let viewport_texture = TextureBuilder::new(&self.device)
+            .viewport(&self.config.read())
+            .label("viewport texture")
+            .build();
+        
+        self.depth_texture = Arc::new(depth_texture);
+        self.viewport_texture = Arc::new(viewport_texture);
         self.egui_renderer
             .lock()
             .renderer()
@@ -621,8 +636,11 @@ Hardware:
         *self.antialiasing.write() = antialiasing;
 
         let config = self.config.read().clone();
-        self.depth_texture =
-            Texture::depth_texture(&config, &self.device, antialiasing, Some("depth texture"));
+        let depth_texture = TextureBuilder::new(&self.device)
+            .depth(&config, antialiasing)
+            .label("depth texture")
+            .build();
+        self.depth_texture = Arc::new(depth_texture);
         self.hdr
             .write()
             .resize(&self.device, config.width, config.height, Some(antialiasing));
@@ -645,8 +663,18 @@ Hardware:
         config.width = width;
         config.height = height;
 
-        self.depth_texture = Texture::depth_texture(&config, &self.device, *self.antialiasing.read(), Some("depth texture"));
-        self.viewport_texture = Texture::viewport(&config, &self.device, Some("viewport texture"));
+        let depth_texture = TextureBuilder::new(&self.device)
+            .depth(&config, *self.antialiasing.read())
+            .label("depth texture")
+            .build();
+
+        let viewport_texture = TextureBuilder::new(&self.device)
+            .viewport(&config)
+            .label("viewport texture")
+            .build();
+        
+        self.depth_texture = Arc::new(depth_texture);
+        self.viewport_texture = Arc::new(viewport_texture);
         self.hdr.write().resize(&self.device, width, height, Some(*self.antialiasing.read()));
         self.egui_renderer
             .lock()
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 410eab5..dfd3e4c 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -2,7 +2,7 @@ use crate::asset::{AssetRegistry, Handle};
 use crate::buffer::UniformBuffer;
 use crate::{
     graphics::SharedGraphicsContext,
-    texture::{Texture, TextureWrapMode},
+    texture::{Texture, TextureBuilder, TextureWrapMode},
     utils::ResourceReference,
 };
 use gltf::image::{Format, Source};
@@ -47,11 +47,11 @@ pub struct Mesh {
 #[derive(Clone)]
 pub struct Material {
     pub name: String,
-    pub diffuse_texture: Texture,
-    pub normal_texture: Texture,
-    pub emissive_texture: Option<Texture>,
-    pub metallic_roughness_texture: Option<Texture>,
-    pub occlusion_texture: Option<Texture>,
+    pub diffuse_texture: Handle<Texture>,
+    pub normal_texture: Option<Handle<Texture>>,
+    pub emissive_texture: Option<Handle<Texture>>,
+    pub metallic_roughness_texture: Option<Handle<Texture>>,
+    pub occlusion_texture: Option<Handle<Texture>>,
     pub tint: [f32; 4],
     pub emissive_factor: [f32; 3],
     pub metallic_factor: f32,
@@ -66,7 +66,6 @@ pub struct Material {
     pub bind_group: wgpu::BindGroup,
     pub texture_tag: Option<String>,
     pub wrap_mode: TextureWrapMode,
-    pub has_normal_texture: bool,
 }
 
 #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)]
@@ -161,17 +160,14 @@ pub enum ChannelValues {
 
 impl Material {
     pub fn new(
+        registry: &mut AssetRegistry,
         graphics: Arc<SharedGraphicsContext>,
         name: impl Into<String>,
-        diffuse_texture: Texture,
-        normal_texture: Texture,
-        emissive_texture: Option<Texture>,
-        metallic_roughness_texture: Option<Texture>,
-        occlusion_texture: Option<Texture>,
-        emissive_texture_bound: Texture,
-        metallic_roughness_texture_bound: Texture,
-        occlusion_texture_bound: Texture,
-        has_normal_texture: bool,
+        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 {
@@ -189,13 +185,59 @@ impl Material {
             occlusion_strength: 1.0,
             alpha_cutoff: 0.5,
             uv_tiling,
-            has_normal_texture: has_normal_texture as u32,
+            has_normal_texture: normal_texture.is_some() as u32,
             has_emissive_texture: emissive_texture.is_some() as u32,
             has_metallic_texture: metallic_roughness_texture.is_some() as u32,
             has_occlusion_texture: occlusion_texture.is_some() as u32,
             pad: 0,
         };
 
+        let d = registry.get_texture(diffuse_texture).unwrap();
+        
+        let n = if let Some(normal) = normal_texture {
+            registry.get_texture(normal).unwrap()
+        } 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()
+        };
+
+        let e = if let Some(e) = emissive_texture {
+            registry.get_texture(e).unwrap()
+        } else {
+            let e = registry.solid_texture_rgba8(
+                graphics.clone(),
+                [0, 0, 0, 255],
+                Some(Texture::TEXTURE_FORMAT_BASE),
+            );
+            registry.get_texture(e).unwrap()
+        };
+
+        let mr = if let Some(m) = metallic_roughness_texture {
+            registry.get_texture(m).unwrap()
+        } else {
+            let metallic_roughness_texture = registry.solid_texture_rgba8(
+                graphics.clone(),
+                [0, 128, 0, 255],
+                Some(Texture::TEXTURE_FORMAT_BASE),
+            );
+            registry.get_texture(metallic_roughness_texture).unwrap()
+        };
+
+        let o = if let Some(o) = occlusion_texture {
+            registry.get_texture(o).unwrap()
+        } else {
+            let o = registry.solid_texture_rgba8(
+                graphics.clone(),
+                [255, 255, 255, 255],
+                Some(Texture::TEXTURE_FORMAT_BASE),
+            );
+            registry.get_texture(o).unwrap()
+        };
+
         let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
         tint_buffer.write(&graphics.queue, &uniform);
 
@@ -209,47 +251,47 @@ impl Material {
                 },
                 wgpu::BindGroupEntry {
                     binding: 1,
-                    resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
+                    resource: wgpu::BindingResource::TextureView(&d.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 2,
-                    resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
+                    resource: wgpu::BindingResource::Sampler(&d.sampler),
                 },
                 wgpu::BindGroupEntry {
                     binding: 3,
-                    resource: wgpu::BindingResource::TextureView(&normal_texture.view),
+                    resource: wgpu::BindingResource::TextureView(&n.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 4,
-                    resource: wgpu::BindingResource::Sampler(&normal_texture.sampler),
+                    resource: wgpu::BindingResource::Sampler(&n.sampler),
                 },
                 wgpu::BindGroupEntry {
                     binding: 5,
-                    resource: wgpu::BindingResource::TextureView(&emissive_texture_bound.view),
+                    resource: wgpu::BindingResource::TextureView(&e.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 6,
-                    resource: wgpu::BindingResource::Sampler(&emissive_texture_bound.sampler),
+                    resource: wgpu::BindingResource::Sampler(&e.sampler),
                 },
                 wgpu::BindGroupEntry {
                     binding: 7,
                     resource: wgpu::BindingResource::TextureView(
-                        &metallic_roughness_texture_bound.view,
+                        &mr.view,
                     ),
                 },
                 wgpu::BindGroupEntry {
                     binding: 8,
                     resource: wgpu::BindingResource::Sampler(
-                        &metallic_roughness_texture_bound.sampler,
+                        &mr.sampler,
                     ),
                 },
                 wgpu::BindGroupEntry {
                     binding: 9,
-                    resource: wgpu::BindingResource::TextureView(&occlusion_texture_bound.view),
+                    resource: wgpu::BindingResource::TextureView(&o.view),
                 },
                 wgpu::BindGroupEntry {
                     binding: 10,
-                    resource: wgpu::BindingResource::Sampler(&occlusion_texture_bound.sampler),
+                    resource: wgpu::BindingResource::Sampler(&o.sampler),
                 },
             ],
         });
@@ -275,7 +317,6 @@ impl Material {
             emissive_texture,
             metallic_roughness_texture,
             occlusion_texture,
-            has_normal_texture,
         }
     }
 
@@ -290,11 +331,11 @@ impl Material {
             occlusion_strength: self.occlusion_strength,
             alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5),
             uv_tiling: self.uv_tiling,
-            has_normal_texture: self.has_normal_texture as u32,
             has_emissive_texture: self.emissive_texture.is_some() as u32,
             has_metallic_texture: self.metallic_roughness_texture.is_some() as u32,
             has_occlusion_texture: self.occlusion_texture.is_some() as u32,
             pad: 0,
+            has_normal_texture: self.normal_texture.is_some() as u32,
         };
 
         self.tint_buffer.write(&graphics.queue, &uniform);
@@ -1077,157 +1118,71 @@ impl Model {
 
         let mut materials = Vec::new();
 
-        let white_srgb_texture = registry.solid_texture_rgba8_with_format(
-            graphics.clone(),
-            [255, 255, 255, 255],
-            Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
-        );
-        let black_srgb_texture = registry.solid_texture_rgba8_with_format(
-            graphics.clone(),
-            [0, 0, 0, 255],
-            Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
-        );
-        let white_linear_texture = registry.solid_texture_rgba8_with_format(
-            graphics.clone(),
-            [255, 255, 255, 255],
-            Texture::TEXTURE_FORMAT_BASE,
-        );
-        let green_linear_texture = registry.solid_texture_rgba8_with_format(
-            graphics.clone(),
-            [0, 255, 0, 255],
-            Texture::TEXTURE_FORMAT_BASE,
-        );
-        let flat_normal_texture = registry.solid_texture_rgba8_with_format(
-            graphics.clone(),
-            [128, 128, 255, 255],
-            Texture::TEXTURE_FORMAT_BASE,
-        );
-
         for processed in processed_textures {
             puffin::profile_scope!("creating material");
 
             let material_name = processed.name;
-            let processed_diffuse = processed.diffuse;
-            let processed_normal = processed.normal;
-            let processed_emissive = processed.emissive;
-            let processed_metallic_roughness = processed.metallic_roughness;
-            let processed_occlusion = processed.occlusion;
-            let diffuse_texture = if let Some(diffuse) = processed_diffuse {
-                let format = diffuse.format.add_srgb_suffix();
-                Texture::from_raw_pixels_mipmapped_with_format(
-                    graphics.clone(),
-                    &diffuse.pixels,
-                    diffuse.dimensions,
-                    format,
-                    Some(diffuse.sampler),
-                    Some(material_name.as_str()),
-                    diffuse.mime_type.as_deref(),
-                )
-            } else if let Some(white) = registry.get_texture(white_srgb_texture) {
-                (*white).clone()
-            } else {
-                anyhow::bail!(
-                    "Unable to find processed diffuse or fetch fallback texture for model {:?}",
-                    label
-                );
+
+            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)
+                    .from_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)
+                    .label(material_name.as_str());
+                if let Some(ref mime) = tex.mime_type {
+                    builder = builder.mime_type(mime.as_str());
+                }
+                builder.build()
             };
 
-            let has_normal_texture = processed_normal.is_some();
-            let normal_texture = if let Some(normal) = processed_normal {
-                Texture::from_raw_pixels_mipmapped_with_format(
+            let diffuse_handle = if let Some(diffuse) = processed.diffuse {
+                let format = diffuse.format.add_srgb_suffix();
+                registry.add_texture(build_texture(diffuse, format))
+            } else {
+                registry.solid_texture_rgba8(
                     graphics.clone(),
-                    &normal.pixels,
-                    normal.dimensions,
-                    normal.format,
-                    Some(normal.sampler),
-                    Some(material_name.as_str()),
-                    normal.mime_type.as_deref(),
+                    [255, 255, 255, 255],
+                    Some(Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix()),
                 )
-            } else if let Some(tex) = registry.get_texture(flat_normal_texture) {
-                (*tex).clone()
-            } else {
-                anyhow::bail!(
-                    "Unable to find processed normal or fetch fallback texture for model {:?}",
-                    label
-                );
             };
 
-            let emissive_texture = processed_emissive.map(|emissive| {
-                let format = emissive.format.add_srgb_suffix();
-                Texture::from_raw_pixels_mipmapped_with_format(
-                    graphics.clone(),
-                    &emissive.pixels,
-                    emissive.dimensions,
-                    format,
-                    Some(emissive.sampler),
-                    Some(material_name.as_str()),
-                    emissive.mime_type.as_deref(),
-                )
+            let normal_handle = processed.normal.map(|n| {
+                let format = n.format;
+                registry.add_texture(build_texture(n, format))
             });
-            let metallic_roughness_texture = processed_metallic_roughness.map(|metallic| {
-                Texture::from_raw_pixels_mipmapped_with_format(
-                    graphics.clone(),
-                    &metallic.pixels,
-                    metallic.dimensions,
-                    metallic.format,
-                    Some(metallic.sampler),
-                    Some(material_name.as_str()),
-                    metallic.mime_type.as_deref(),
-                )
+
+            let emissive_handle = processed.emissive.map(|e| {
+                let format = e.format.add_srgb_suffix();
+                registry.add_texture(build_texture(e, format))
             });
-            let occlusion_texture = processed_occlusion.map(|occlusion| {
-                Texture::from_raw_pixels_mipmapped_with_format(
-                    graphics.clone(),
-                    &occlusion.pixels,
-                    occlusion.dimensions,
-                    occlusion.format,
-                    Some(occlusion.sampler),
-                    Some(material_name.as_str()),
-                    occlusion.mime_type.as_deref(),
-                )
+
+            let metallic_roughness_handle = processed.metallic_roughness.map(|mr| {
+                let format = mr.format;
+                registry.add_texture(build_texture(mr, format))
+            });
+
+            let occlusion_handle = processed.occlusion.map(|occ| {
+                let format = occ.format;
+                registry.add_texture(build_texture(occ, format))
             });
 
-            let emissive_texture_bound = emissive_texture
-                .clone()
-                .or_else(|| registry.get_texture(black_srgb_texture).cloned())
-                .ok_or_else(|| {
-                    anyhow::anyhow!(
-                        "Unable to resolve emissive fallback texture for model {:?}",
-                        label
-                    )
-                })?;
-            let metallic_roughness_texture_bound = metallic_roughness_texture
-                .clone()
-                .or_else(|| registry.get_texture(green_linear_texture).cloned())
-                .ok_or_else(|| {
-                    anyhow::anyhow!(
-                        "Unable to resolve metallic fallback texture for model {:?}",
-                        label
-                    )
-                })?;
-            let occlusion_texture_bound = occlusion_texture
-                .clone()
-                .or_else(|| registry.get_texture(white_linear_texture).cloned())
-                .ok_or_else(|| {
-                    anyhow::anyhow!(
-                        "Unable to resolve occlusion fallback texture for model {:?}",
-                        label
-                    )
-                })?;
             let texture_tag = Some(material_name.clone());
 
             let mut material = Material::new(
+                &mut *registry,
                 graphics.clone(),
                 material_name,
-                diffuse_texture,
-                normal_texture,
-                emissive_texture.clone(),
-                metallic_roughness_texture.clone(),
-                occlusion_texture.clone(),
-                emissive_texture_bound,
-                metallic_roughness_texture_bound,
-                occlusion_texture_bound,
-                has_normal_texture,
+                diffuse_handle,
+                normal_handle,
+                emissive_handle,
+                metallic_roughness_handle,
+                occlusion_handle,
                 processed.tint,
                 texture_tag,
             );
@@ -1240,9 +1195,6 @@ impl Model {
             material.double_sided = processed.double_sided;
             material.occlusion_strength = processed.occlusion_strength;
             material.normal_scale = processed.normal_scale;
-            material.emissive_texture = emissive_texture;
-            material.metallic_roughness_texture = metallic_roughness_texture;
-            material.occlusion_texture = occlusion_texture;
             material.sync_uniform(&graphics);
 
             materials.push(material);
@@ -1311,24 +1263,7 @@ impl Model {
             });
         }
 
-        if let Some(resref) = optional_resref.clone() {
-            for material in &mut materials {
-                material.diffuse_texture.reference = Some(resref.clone());
-                material.normal_texture.reference = Some(resref.clone());
 
-                if let Some(texture) = material.emissive_texture.as_mut() {
-                    texture.reference = Some(resref.clone());
-                }
-
-                if let Some(texture) = material.metallic_roughness_texture.as_mut() {
-                    texture.reference = Some(resref.clone());
-                }
-
-                if let Some(texture) = material.occlusion_texture.as_mut() {
-                    texture.reference = Some(resref.clone());
-                }
-            }
-        }
 
         log::debug!("Successfully loaded model [{:?}]", label);
 
@@ -1707,7 +1642,7 @@ impl Hash for ModelVertex {
 #[repr(C)]
 #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct MaterialUniform {
-    pub base_colour: [f32; 4],
+    pub base_colour: [f32; 4], // colour
     pub emissive: [f32; 3],
     pub emissive_strength: f32,
     pub metallic: f32,
diff --git a/crates/dropbear-engine/src/pipelines/hdr.rs b/crates/dropbear-engine/src/pipelines/hdr.rs
index 364aff6..4257e36 100644
--- a/crates/dropbear-engine/src/pipelines/hdr.rs
+++ b/crates/dropbear-engine/src/pipelines/hdr.rs
@@ -1,5 +1,5 @@
 use crate::pipelines::create_render_pipeline;
-use crate::texture::Texture;
+use crate::texture::{Texture, TextureBuilder};
 use wgpu::Operations;
 
 pub struct HdrPipeline {
@@ -28,29 +28,25 @@ impl HdrPipeline {
         // features to be enabled for rendering.
         let format = wgpu::TextureFormat::Rgba16Float;
 
-        let texture = Texture::create_2d_texture(
-            device,
-            width,
-            height,
-            format,
-            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
-            wgpu::FilterMode::Nearest,
-            None,
-            Some("Hdr::texture"),
-        );
+        let texture = TextureBuilder::new(device)
+            .size(width, height)
+            .format(format)
+            .usage(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT)
+            .mag_filter(wgpu::FilterMode::Nearest)
+            .label("Hdr::texture")
+            .build();
 
         let msaa_texture = match antialiasing {
             crate::multisampling::AntiAliasingMode::None => None,
-            _ => Some(Texture::create_2d_texture(
-                device,
-                width,
-                height,
-                format,
-                wgpu::TextureUsages::RENDER_ATTACHMENT,
-                wgpu::FilterMode::Nearest,
-                Some(antialiasing),
-                Some("Hdr::msaa_texture"),
-            )),
+            _ => Some(TextureBuilder::new(device)
+                .size(width, height)
+                .format(format)
+                .usage(wgpu::TextureUsages::RENDER_ATTACHMENT)
+                .mag_filter(wgpu::FilterMode::Nearest)
+                .label("Hdr::texture")
+                .antialiasing(antialiasing)
+                .build()
+            ),
         };
 
         let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@@ -129,28 +125,25 @@ impl HdrPipeline {
     pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32, antialiasing: Option<crate::multisampling::AntiAliasingMode>) {
         self.antialiasing = antialiasing.unwrap_or(self.antialiasing);
 
-        self.texture = Texture::create_2d_texture(
-            device,
-            width,
-            height,
-            self.format,
-            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
-            wgpu::FilterMode::Nearest,
-            None,
-            Some("Hdr::texture"),
-        );
+        self.texture = TextureBuilder::new(device)
+            .size(width, height)
+            .format(self.format)
+            .usage(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT)
+            .mag_filter(wgpu::FilterMode::Nearest)
+            .label("Hdr::texture")
+            .build();
+
         self.msaa_texture = match self.antialiasing {
             crate::multisampling::AntiAliasingMode::None => None,
-            _ => Some(Texture::create_2d_texture(
-                device,
-                width,
-                height,
-                self.format,
-                wgpu::TextureUsages::RENDER_ATTACHMENT,
-                wgpu::FilterMode::Nearest,
-                Some(self.antialiasing),
-                Some("Hdr::msaa_texture"),
-            )),
+            _ => Some(TextureBuilder::new(device)
+                .size(width, height)
+                .format(self.format)
+                .usage(wgpu::TextureUsages::RENDER_ATTACHMENT)
+                .mag_filter(wgpu::FilterMode::Nearest)
+                .label("Hdr::texture")
+                .antialiasing(self.antialiasing)
+                .build()
+            ),
         };
         self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
             label: Some("Hdr::bind_group"),
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index 83a1b53..da57d66 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -5,7 +5,6 @@ use crate::asset::{AssetRegistry, Handle};
 use crate::graphics::SharedGraphicsContext;
 use crate::model::ModelVertex;
 use crate::model::{Material, Mesh, Model};
-use crate::texture::Texture;
 use crate::utils::ResourceReference;
 use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
@@ -38,8 +37,6 @@ impl ProcedurallyGeneratedObject {
         hash: Option<u64>,
         registry: Arc<RwLock<AssetRegistry>>,
     ) -> Model {
-        let mut _rguard = registry.write();
-
         let hash = if let Some(hash) = hash {
             hash
         } else {
@@ -86,65 +83,22 @@ impl ProcedurallyGeneratedObject {
         };
 
         let material = material.unwrap_or_else(|| {
-            let white_srgb_texture = _rguard.solid_texture_rgba8_with_format(
-                graphics.clone(),
-                [255, 255, 255, 255],
-                Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
-            );
-            let black_srgb_texture = _rguard.solid_texture_rgba8_with_format(
-                graphics.clone(),
-                [0, 0, 0, 255],
-                Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
-            );
-            let white_linear_texture = _rguard.solid_texture_rgba8_with_format(
+            let mut _rguard = registry.write();
+            let white_srgb_texture = _rguard.solid_texture_rgba8(
                 graphics.clone(),
                 [255, 255, 255, 255],
-                Texture::TEXTURE_FORMAT_BASE,
-            );
-            let green_linear_texture = _rguard.solid_texture_rgba8_with_format(
-                graphics.clone(),
-                [0, 255, 0, 255],
-                Texture::TEXTURE_FORMAT_BASE,
-            );
-            let flat_normal_texture = _rguard.solid_texture_rgba8_with_format(
-                graphics.clone(),
-                [128, 128, 255, 255],
-                Texture::TEXTURE_FORMAT_BASE,
+                None,
             );
 
-            let white_srgb = _rguard
-                .get_texture(white_srgb_texture)
-                .cloned()
-                .expect("Missing procedural white srgb texture");
-            let black_srgb = _rguard
-                .get_texture(black_srgb_texture)
-                .cloned()
-                .expect("Missing procedural black srgb texture");
-            let white_linear = _rguard
-                .get_texture(white_linear_texture)
-                .cloned()
-                .expect("Missing procedural white linear texture");
-            let green_linear = _rguard
-                .get_texture(green_linear_texture)
-                .cloned()
-                .expect("Missing procedural green linear texture");
-            let flat_normal = _rguard
-                .get_texture(flat_normal_texture)
-                .cloned()
-                .expect("Missing procedural flat normal texture");
-
             Material::new(
+                &mut _rguard,
                 graphics.clone(),
                 "procedural_material",
-                white_srgb,
-                flat_normal,
+                white_srgb_texture,
+                None,
                 None,
                 None,
                 None,
-                black_srgb,
-                green_linear,
-                white_linear,
-                false,
                 [1.0, 1.0, 1.0, 1.0],
                 Some("procedural_material".to_string()),
             )
diff --git a/crates/dropbear-engine/src/sky.rs b/crates/dropbear-engine/src/sky.rs
index 4d34df3..97c8d87 100644
--- a/crates/dropbear-engine/src/sky.rs
+++ b/crates/dropbear-engine/src/sky.rs
@@ -1,6 +1,6 @@
 use crate::graphics::SharedGraphicsContext;
 use crate::pipelines::{create_render_pipeline_ex};
-use crate::texture::Texture;
+use crate::texture::{Texture, TextureBuilder};
 use image::codecs::hdr::HdrDecoder;
 use std::io::Cursor;
 use std::sync::Arc;
@@ -176,16 +176,12 @@ impl HdrLoader {
             })
             .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,
-            None,
-        );
+        let src = TextureBuilder::new(&device)
+            .size(meta.width, meta.height)
+            .format(loader.texture_format)
+            .usage(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST)
+            .mag_filter(wgpu::FilterMode::Linear)
+            .build();
 
         queue.write_texture(
             wgpu::TexelCopyTextureInfo {
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 68ed8ca..a1ba27d 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -1,4 +1,4 @@
-use std::{fs, path::PathBuf, sync::Arc};
+use std::sync::Arc;
 
 use crate::asset::AssetRegistry;
 use crate::graphics::SharedGraphicsContext;
@@ -7,7 +7,6 @@ use image::GenericImageView;
 use serde::{Deserialize, Serialize};
 use crate::multisampling::{AntiAliasingMode};
 
-#[derive(Clone)]
 /// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture.
 pub struct Texture {
     pub label: Option<String>,
@@ -17,243 +16,496 @@ pub struct Texture {
     pub view: wgpu::TextureView,
     pub hash: Option<u64>,
     pub reference: Option<ResourceReference>,
-    pub mime_type: Option<String>,
 }
 
-impl Texture {
-    /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier
-    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;
+pub struct TextureBuilder<'a> {
+    device: &'a wgpu::Device,
+    graphics: Option<Arc<SharedGraphicsContext>>,
 
-    pub fn create_2d_texture(
-        device: &wgpu::Device,
-        width: u32,
-        height: u32,
-        format: wgpu::TextureFormat,
-        usage: wgpu::TextureUsages,
-        mag_filter: wgpu::FilterMode,
-        antialiasing: Option<AntiAliasingMode>,
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or("create 2d texture"));
-        let size = wgpu::Extent3d {
-            width,
-            height,
-            depth_or_array_layers: 1,
-        };
+    width: u32,
+    height: u32,
+    depth_or_array_layers: u32,
 
-        Self::create_texture(
-            device,
-            label,
-            size,
-            format,
-            usage,
-            wgpu::TextureDimension::D2,
-            mag_filter,
-            antialiasing,
-        )
-    }
+    format: wgpu::TextureFormat,
+    usage: wgpu::TextureUsages,
+    dimension: wgpu::TextureDimension,
+    sample_count: u32,
+    mip_level_count: u32,
+    auto_mip: bool,
 
-    pub fn create_texture(
-        device: &wgpu::Device,
-        label: Option<&str>,
-        size: wgpu::Extent3d,
-        format: wgpu::TextureFormat,
-        usage: wgpu::TextureUsages,
-        dimension: wgpu::TextureDimension,
-        mag_filter: wgpu::FilterMode,
-        antialiasing: Option<AntiAliasingMode>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or("create texture"));
-        let texture = device.create_texture(&wgpu::TextureDescriptor {
-            label,
-            size,
-            mip_level_count: 1,
-            sample_count: antialiasing.unwrap_or_default().into(),
-            dimension,
-            format,
-            usage,
-            view_formats: &[],
-        });
+    mag_filter: wgpu::FilterMode,
+    min_filter: wgpu::FilterMode,
+    mipmap_filter: wgpu::FilterMode,
+    wrap_mode: TextureWrapMode,
+    compare: Option<wgpu::CompareFunction>,
+    lod_min_clamp: f32,
+    lod_max_clamp: f32,
 
-        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
-        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
-            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()
-        });
+    view_descriptor: Option<wgpu::TextureViewDescriptor<'a>>,
 
-        Self {
-            label: label.and_then(|v| Some(v.to_string())),
-            texture,
-            view,
-            sampler,
-            size,
-            hash: None,
-            reference: None,
-            mime_type: None,
-        }
-    }
+    label: Option<&'a str>,
+    mime_type: Option<String>,
 
-    /// Creates a new depth texture. This is an internal function.
-    pub fn depth_texture(
-        config: &wgpu::SurfaceConfiguration,
-        device: &wgpu::Device,
-        antialiasing: AntiAliasingMode,
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or("depth texture"));
-        let size = wgpu::Extent3d {
-            width: config.width.max(1),
-            height: config.height.max(1),
-            depth_or_array_layers: 1,
-        };
+    source: TextureSource<'a>,
+}
 
-        let desc = wgpu::TextureDescriptor {
-            label,
-            size,
-            mip_level_count: 1, // leave me alone
-            sample_count: antialiasing.into(),
-            dimension: wgpu::TextureDimension::D2,
-            format: Self::DEPTH_FORMAT,
-            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
-            view_formats: &[],
-        };
-        let texture = device.create_texture(&desc);
+enum TextureSource<'a> {
+    Empty,
+    Bytes(&'a [u8]),
+    RawPixels(&'a [u8]),
+    #[allow(dead_code)]
+    SurfaceConfig(&'a wgpu::SurfaceConfiguration),
+    #[allow(dead_code)]
+    Depth(&'a wgpu::SurfaceConfiguration),
+}
 
-        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
-            address_mode_u: wgpu::AddressMode::ClampToEdge,
-            address_mode_v: wgpu::AddressMode::ClampToEdge,
-            address_mode_w: wgpu::AddressMode::ClampToEdge,
+impl<'a> TextureBuilder<'a> {
+    pub fn new(device: &'a wgpu::Device) -> Self {
+        Self {
+            device,
+            graphics: None,
+            width: 1,
+            height: 1,
+            depth_or_array_layers: 1,
+            format: Texture::TEXTURE_FORMAT,
+            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
+            dimension: wgpu::TextureDimension::D2,
+            sample_count: 1,
+            mip_level_count: 1,
+            auto_mip: false,
             mag_filter: wgpu::FilterMode::Linear,
-            min_filter: wgpu::FilterMode::Linear,
+            min_filter: wgpu::FilterMode::Nearest,
             mipmap_filter: wgpu::FilterMode::Nearest,
-            compare: Some(wgpu::CompareFunction::LessEqual),
+            wrap_mode: TextureWrapMode::Clamp,
+            compare: None,
             lod_min_clamp: 0.0,
-            lod_max_clamp: 100.0,
-            ..Default::default()
-        });
-        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
-
-        Self {
-            texture,
-            sampler,
-            size,
-            view,
-            label: label.to_potential_string(),
-            hash: None,
-            reference: None,
+            lod_max_clamp: 32.0,
+            view_descriptor: None,
+            label: None,
             mime_type: None,
+            source: TextureSource::Empty,
         }
     }
 
-    /// Creates a viewport texture.
-    ///  
-    /// This is an internal function.
-    pub fn viewport(
-        config: &wgpu::SurfaceConfiguration,
-        device: &wgpu::Device,
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or("viewport texture"));
-        let size = wgpu::Extent3d {
-            width: config.width.max(1),
-            height: config.height.max(1),
-            depth_or_array_layers: 1,
-        };
+    pub fn size(mut self, width: u32, height: u32) -> Self {
+        self.width = width;
+        self.height = height;
+        self
+    }
 
-        let desc = wgpu::TextureDescriptor {
-            label,
-            size,
-            mip_level_count: 1, // leave me alone
-            sample_count: 1,
-            dimension: wgpu::TextureDimension::D2,
-            format: config.format.add_srgb_suffix(),
-            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
-            view_formats: &[],
-        };
+    pub fn size_from_config(mut self, config: &'a wgpu::SurfaceConfiguration) -> Self {
+        self.width = config.width.max(1);
+        self.height = config.height.max(1);
+        self
+    }
 
-        let texture = device.create_texture(&desc);
-        let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
-        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+    pub fn format(mut self, format: wgpu::TextureFormat) -> Self {
+        self.format = format;
+        self
+    }
 
-        Self {
-            label: label.to_potential_string(),
-            texture,
-            sampler,
-            size,
-            view,
-            hash: None,
-            reference: None,
-            mime_type: None,
+    pub fn usage(mut self, usage: wgpu::TextureUsages) -> Self {
+        self.usage = usage;
+        self
+    }
+
+    /// preset: depth_texture()
+    pub fn depth(mut self, config: &'a wgpu::SurfaceConfiguration, antialiasing: AntiAliasingMode) -> Self {
+        self.source = TextureSource::Depth(config);
+        self.width = config.width.max(1);
+        self.height = config.height.max(1);
+        self.format = Texture::DEPTH_FORMAT;
+        self.usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
+        self.sample_count = antialiasing.into();
+        self.mag_filter = wgpu::FilterMode::Linear;
+        self.min_filter = wgpu::FilterMode::Linear;
+        self.mipmap_filter = wgpu::FilterMode::Nearest;
+        self.compare = Some(wgpu::CompareFunction::LessEqual);
+        self.lod_min_clamp = 0.0;
+        self.lod_max_clamp = 100.0;
+        self
+    }
+
+    /// preset: viewport()
+    pub fn viewport(mut self, config: &'a wgpu::SurfaceConfiguration) -> Self {
+        self.source = TextureSource::SurfaceConfig(config);
+        self.width = config.width.max(1);
+        self.height = config.height.max(1);
+        self.format = config.format.add_srgb_suffix();
+        self.usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
+        self.mag_filter = wgpu::FilterMode::Linear;
+        self.min_filter = wgpu::FilterMode::Linear;
+        self.mipmap_filter = wgpu::FilterMode::Nearest;
+        self
+    }
+
+    pub fn render_target(mut self) -> Self {
+        self.usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
+        self
+    }
+
+    pub fn mag_filter(mut self, filter: wgpu::FilterMode) -> Self {
+        self.mag_filter = filter;
+        self
+    }
+
+    pub fn wrap_mode(mut self, wrap: TextureWrapMode) -> Self {
+        self.wrap_mode = wrap;
+        self
+    }
+
+    pub fn antialiasing(mut self, aa: AntiAliasingMode) -> Self {
+        self.sample_count = aa.into();
+        self
+    }
+
+    pub fn from_bytes(mut self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self {
+        self.graphics = Some(graphics);
+        self.source = TextureSource::Bytes(bytes);
+        self.auto_mip = true;
+        self.mipmap_filter = wgpu::FilterMode::Linear;
+        self.usage = wgpu::TextureUsages::TEXTURE_BINDING
+            | wgpu::TextureUsages::RENDER_ATTACHMENT
+            | wgpu::TextureUsages::COPY_DST
+            | wgpu::TextureUsages::COPY_SRC;
+        self
+    }
+
+    pub fn from_raw_pixels(mut self, graphics: Arc<SharedGraphicsContext>, pixels: &'a [u8]) -> Self {
+        self.graphics = Some(graphics);
+        self.source = TextureSource::RawPixels(pixels);
+        self.auto_mip = true;
+        self.mipmap_filter = wgpu::FilterMode::Linear;
+        self.usage = wgpu::TextureUsages::TEXTURE_BINDING
+            | wgpu::TextureUsages::RENDER_ATTACHMENT
+            | wgpu::TextureUsages::COPY_DST
+            | wgpu::TextureUsages::COPY_SRC;
+        self
+    }
+
+    pub fn with_auto_mip(mut self) -> Self {
+        self.auto_mip = true;
+        self
+    }
+
+    pub fn label(mut self, label: &'a str) -> Self {
+        self.label = Some(label);
+        self
+    }
+
+    pub fn mime_type(mut self, mime: &str) -> Self {
+        self.mime_type = Some(mime.to_string());
+        self
+    }
+
+    pub fn view_descriptor(mut self, desc: wgpu::TextureViewDescriptor<'a>) -> Self {
+        self.view_descriptor = Some(desc);
+        self
+    }
+
+    pub fn build(self) -> Texture {
+        puffin::profile_function!(self.label.unwrap_or("TextureBuilder::build"));
+
+        match &self.source {
+            TextureSource::Bytes(bytes) => {
+                let graphics = self
+                    .graphics
+                    .as_ref()
+                    .expect("from_bytes() requires graphics context");
+                let hash = AssetRegistry::hash_bytes(bytes);
+                let requested_dimensions = Some((self.width, self.height)).filter(|&d| d != (1, 1));
+
+                let (rgba, dimensions) = match image::load_from_memory(bytes) {
+                    Ok(image) => {
+                        let rgba = image.to_rgba8().into_raw();
+                        let dims = requested_dimensions.unwrap_or_else(|| image.dimensions());
+                        (rgba, dims)
+                    }
+                    Err(err) => {
+                        if let Some(dims) = requested_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.",
+                                    self.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.",
+                                self.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 = self.compute_mip_level_count(size);
+                let texture = self.create_texture(&graphics.device, size, self.format, mip_level_count);
+                Self::upload_level0(&graphics.queue, &texture, size, &rgba, 4);
+                self.finish_uploaded_texture(
+                    &graphics,
+                    texture,
+                    size,
+                    hash,
+                    ResourceReference::from_bytes(bytes),
+                )
+            }
+            TextureSource::RawPixels(pixels) => {
+                let graphics = self
+                    .graphics
+                    .as_ref()
+                    .expect("from_raw_pixels() requires graphics context");
+                let hash = AssetRegistry::hash_bytes(pixels);
+
+                let (format, pixels, dimensions) = match self.format.block_copy_size(None) {
+                    Some(bytes_per_pixel) => {
+                        let dimensions = (self.width, self.height);
+                        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 {
+                            (self.format, pixels.to_vec(), dimensions)
+                        } else {
+                            log::error!(
+                                "Texture [{:?}] byte length {} does not match expected {} for {:?} ({}x{}). Falling back to 1x1 magenta.",
+                                self.label,
+                                pixels.len(),
+                                expected_len,
+                                self.format,
+                                dimensions.0,
+                                dimensions.1
+                            );
+                            (Texture::TEXTURE_FORMAT, vec![255, 0, 255, 255], (1, 1))
+                        }
+                    }
+                    None => {
+                        log::error!(
+                            "Texture [{:?}] has unsupported format {:?}; falling back to 1x1 magenta.",
+                            self.label,
+                            self.format
+                        );
+                        (Texture::TEXTURE_FORMAT, vec![255, 0, 255, 255], (1, 1))
+                    }
+                };
+
+                let size = wgpu::Extent3d {
+                    width: dimensions.0,
+                    height: dimensions.1,
+                    depth_or_array_layers: 1,
+                };
+
+                let bytes_per_pixel = format
+                    .block_copy_size(None)
+                    .expect("fallback format must have a valid block size");
+                let mip_level_count = self.compute_mip_level_count(size);
+                let texture = self.create_texture(&graphics.device, size, format, mip_level_count);
+                Self::upload_level0(&graphics.queue, &texture, size, &pixels, bytes_per_pixel);
+                self.finish_uploaded_texture(
+                    &graphics,
+                    texture,
+                    size,
+                    hash,
+                    ResourceReference::from_bytes(pixels.as_slice()),
+                )
+            }
+            _ => {
+                let size = wgpu::Extent3d {
+                    width: self.width,
+                    height: self.height,
+                    depth_or_array_layers: self.depth_or_array_layers,
+                };
+
+                let texture = self.device.create_texture(&wgpu::TextureDescriptor {
+                    label: self.label,
+                    size,
+                    mip_level_count: self.mip_level_count,
+                    sample_count: self.sample_count,
+                    dimension: self.dimension,
+                    format: self.format,
+                    usage: self.usage,
+                    view_formats: &[],
+                });
+
+                let view = texture.create_view(
+                    &self.view_descriptor.clone().unwrap_or_default()
+                );
+                let sampler = self.device.create_sampler(&self.build_sampler_desc());
+
+                Texture {
+                    label: self.label.map(|s| s.to_string()),
+                    texture,
+                    view,
+                    sampler,
+                    size,
+                    hash: None,
+                    reference: None,
+                }
+            }
         }
     }
 
-    /// Loads the texture from a file.
-    pub async fn from_file(
-        graphics: Arc<SharedGraphicsContext>,
-        path: &PathBuf,
-        label: Option<&str>,
-    ) -> anyhow::Result<Self> {
-        puffin::profile_function!(label.unwrap_or(""));
-        let data = fs::read(path)?;
-        let mut result = Self::from_bytes(graphics.clone(), &data, label);
-        result.reference = Some(ResourceReference::from_path(path)?);
-        Ok(result)
+    fn compute_mip_level_count(&self, size: wgpu::Extent3d) -> u32 {
+        if self.auto_mip {
+            size.width.min(size.height).ilog2() + 1
+        } else {
+            self.mip_level_count
+        }
     }
 
-    /// Loads the texture from bytes.
-    ///
-    /// If you want more customisability in the texture being generated, you can use [Self::from_bytes_verbose]
-    pub fn from_bytes(
-        graphics: Arc<SharedGraphicsContext>,
-        bytes: &[u8],
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or(""));
-        Self::from_bytes_verbose_mipmapped(graphics, bytes, None, None, None, label)
+    fn create_texture(
+        &self,
+        device: &wgpu::Device,
+        size: wgpu::Extent3d,
+        format: wgpu::TextureFormat,
+        mip_level_count: u32,
+    ) -> wgpu::Texture {
+        device.create_texture(&wgpu::TextureDescriptor {
+            label: self.label,
+            size,
+            mip_level_count,
+            sample_count: self.sample_count,
+            dimension: self.dimension,
+            format,
+            usage: self.usage,
+            view_formats: &[],
+        })
     }
 
-    /// Loads the texture from bytes and generates mipmaps on the GPU.
-    ///
-    /// This is the recommended constructor for any sampled texture used for rendering.
-    pub fn from_bytes_verbose_mipmapped(
-        graphics: Arc<SharedGraphicsContext>,
-        bytes: &[u8],
-        dimensions: Option<(u32, u32)>,
-        view_descriptor: Option<wgpu::TextureViewDescriptor>,
-        sampler: Option<wgpu::SamplerDescriptor>,
-        label: Option<&str>,
-    ) -> Self {
-        puffin::profile_function!(label.unwrap_or(""));
-        let texture = Self::from_bytes_verbose_with_format(
-            graphics.clone(),
-            bytes,
-            dimensions,
-            None,
-            view_descriptor,
+    fn upload_level0(
+        queue: &wgpu::Queue,
+        texture: &wgpu::Texture,
+        size: wgpu::Extent3d,
+        pixels: &[u8],
+        bytes_per_pixel: u32,
+    ) {
+        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 {
+            queue.write_texture(
+                wgpu::TexelCopyTextureInfo {
+                    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 {
+            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]);
+            }
+
+            queue.write_texture(
+                wgpu::TexelCopyTextureInfo {
+                    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,
+            );
+        }
+    }
+
+    fn finish_uploaded_texture(
+        &self,
+        graphics: &SharedGraphicsContext,
+        texture: wgpu::Texture,
+        size: wgpu::Extent3d,
+        hash: u64,
+        reference: ResourceReference,
+    ) -> Texture {
+        let sampler_desc = self.build_sampler_desc();
+        let view_descriptor = self.view_descriptor.clone().unwrap_or_default();
+        let view = texture.create_view(&view_descriptor);
+        let sampler = graphics.device.create_sampler(&sampler_desc);
+
+        let built = Texture {
+            label: self.label.map(|s| s.to_string()),
+            texture,
             sampler,
-            label,
-        );
+            size,
+            view,
+            hash: Some(hash),
+            reference: Some(reference),
+        };
 
-        if let Err(err) =
-            graphics
+        if self.auto_mip {
+            if let Err(err) = graphics
                 .mipmapper
-                .compute_mipmaps(&graphics.device, &graphics.queue, &texture)
-        {
-            log_once::warn_once!("Failed to generate mipmaps: {}", err);
+                .compute_mipmaps(&graphics.device, &graphics.queue, &built)
+            {
+                log_once::warn_once!("Failed to generate mipmaps: {}", err);
+            }
         }
 
-        texture
+        built
+    }
+
+    fn build_sampler_desc(&self) -> wgpu::SamplerDescriptor<'static> {
+        let addr: wgpu::AddressMode = self.wrap_mode.into();
+        wgpu::SamplerDescriptor {
+            address_mode_u: addr,
+            address_mode_v: addr,
+            address_mode_w: addr,
+            mag_filter: self.mag_filter,
+            min_filter: self.min_filter,
+            mipmap_filter: self.mipmap_filter,
+            compare: self.compare,
+            lod_min_clamp: self.lod_min_clamp,
+            lod_max_clamp: self.lod_max_clamp,
+            ..Default::default()
+        }
     }
+}
+
+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 fn from_bytes_verbose_mipmapped_with_format(
+    pub(crate) fn from_bytes_verbose_mipmapped_with_format(
         graphics: Arc<SharedGraphicsContext>,
         bytes: &[u8],
         dimensions: Option<(u32, u32)>,
@@ -284,29 +536,6 @@ impl Texture {
         texture
     }
 
-    /// Loads the texture from bytes, with options for more arguments.
-    ///
-    /// Requires more arguments. For a simpler usage, you should use [Self::from_bytes]
-    pub fn from_bytes_verbose(
-        graphics: Arc<SharedGraphicsContext>,
-        bytes: &[u8],
-        dimensions: Option<(u32, u32)>,
-        _texture_descriptor: Option<wgpu::TextureDescriptor>,
-        view_descriptor: Option<wgpu::TextureViewDescriptor>,
-        sampler: Option<wgpu::SamplerDescriptor>,
-        label: Option<&str>,
-    ) -> Self {
-        Self::from_bytes_verbose_with_format(
-            graphics,
-            bytes,
-            dimensions,
-            None,
-            view_descriptor,
-            sampler,
-            label,
-        )
-    }
-
     fn from_bytes_verbose_with_format(
         graphics: Arc<SharedGraphicsContext>,
         bytes: &[u8],
@@ -464,14 +693,9 @@ impl Texture {
             view,
             hash: Some(hash),
             reference: Some(ResourceReference::from_bytes(bytes)),
-            mime_type: None,
         }
     }
 
-    fn bytes_per_pixel(format: wgpu::TextureFormat) -> Option<u32> {
-        format.block_copy_size(None)
-    }
-
     pub fn from_raw_pixels_mipmapped_with_format(
         graphics: Arc<SharedGraphicsContext>,
         pixels: &[u8],
@@ -479,7 +703,7 @@ impl Texture {
         format: wgpu::TextureFormat,
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
-        mime_type: Option<&str>,
+        _mime_type: Option<&str>,
     ) -> Self {
         puffin::profile_function!(label.unwrap_or(""));
         if let Some(l) = label {
@@ -488,7 +712,7 @@ impl Texture {
 
         let hash = AssetRegistry::hash_bytes(pixels);
 
-        let bytes_per_pixel = match Self::bytes_per_pixel(format) {
+        let bytes_per_pixel = match format.block_copy_size(None) {
             Some(value) => value,
             None => {
                 log::error!(
@@ -627,7 +851,6 @@ impl Texture {
             view,
             hash: Some(hash),
             reference: Some(ResourceReference::from_bytes(pixels)),
-            mime_type: mime_type.map(|value| value.to_string()),
         };
 
         if let Err(err) =
@@ -640,18 +863,6 @@ impl Texture {
 
         texture
     }
-
-    pub fn sampler_from_wrap(wrap: TextureWrapMode) -> wgpu::SamplerDescriptor<'static> {
-        wgpu::SamplerDescriptor {
-            address_mode_u: wrap.into(),
-            address_mode_v: wrap.into(),
-            address_mode_w: wrap.into(),
-            mag_filter: wgpu::FilterMode::Linear,
-            min_filter: wgpu::FilterMode::Nearest,
-            mipmap_filter: wgpu::FilterMode::Linear,
-            ..Default::default()
-        }
-    }
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
diff --git a/crates/eucalyptus-core/src/animation.rs b/crates/eucalyptus-core/src/animation.rs
index 24fa293..d9b0212 100644
--- a/crates/eucalyptus-core/src/animation.rs
+++ b/crates/eucalyptus-core/src/animation.rs
@@ -55,7 +55,7 @@ impl Component for AnimationComponent {
             return;
         };
 
-        self.update(dt, model);
+        self.update(dt, &model);
 
         if let Ok(mut entity_transform) = world.get::<&mut EntityTransform>(entity) {
             if model.skins.is_empty() {
diff --git a/crates/eucalyptus-core/src/asset/model.rs b/crates/eucalyptus-core/src/asset/model.rs
index 63e56ac..151da5b 100644
--- a/crates/eucalyptus-core/src/asset/model.rs
+++ b/crates/eucalyptus-core/src/asset/model.rs
@@ -8,7 +8,6 @@ use dropbear_engine::model::{
     Animation, AnimationChannel, AnimationInterpolation, ChannelValues, Material, Mesh,
     ModelVertex, Node, NodeTransform, Skin,
 };
-use dropbear_engine::texture::Texture;
 use jni::JNIEnv;
 use jni::objects::{JObject, JValue};
 
@@ -103,7 +102,10 @@ impl ToJObject for NMesh {
 pub struct NMaterial {
     pub name: String,
     pub diffuse_texture: u64,
-    pub normal_texture: u64,
+    pub normal_texture: Option<u64>,
+    pub emissive_texture: Option<u64>,
+    pub metallic_roughness_texture: Option<u64>,
+    pub occlusion_texture: Option<u64>,
     pub tint: NVector4,
     pub emissive_factor: NVector3,
     pub metallic_factor: f32,
@@ -113,9 +115,6 @@ pub struct NMaterial {
     pub occlusion_strength: f32,
     pub normal_scale: f32,
     pub uv_tiling: NVector2,
-    pub emissive_texture: Option<u64>,
-    pub metallic_roughness_texture: Option<u64>,
-    pub occlusion_texture: Option<u64>,
 }
 
 impl ToJObject for NMaterial {
@@ -128,11 +127,10 @@ impl ToJObject for NMaterial {
             .new_string(&self.name)
             .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
         let diffuse_texture = new_texture(env, self.diffuse_texture)?;
-        let normal_texture = new_texture(env, self.normal_texture)?;
-        let tint = self.tint.to_jobject(env)?;
-        let emissive_factor = self.emissive_factor.to_jobject(env)?;
-        let uv_tiling = self.uv_tiling.to_jobject(env)?;
-        let alpha_cutoff = self.alpha_cutoff.to_jobject(env)?;
+        let normal_texture = match self.normal_texture {
+            Some(id) => new_texture(env, id)?,
+            None => JObject::null(),
+        };
         let emissive_texture = match self.emissive_texture {
             Some(id) => new_texture(env, id)?,
             None => JObject::null(),
@@ -145,6 +143,10 @@ impl ToJObject for NMaterial {
             Some(id) => new_texture(env, id)?,
             None => JObject::null(),
         };
+        let tint = self.tint.to_jobject(env)?;
+        let emissive_factor = self.emissive_factor.to_jobject(env)?;
+        let uv_tiling = self.uv_tiling.to_jobject(env)?;
+        let alpha_cutoff = self.alpha_cutoff.to_jobject(env)?;
 
         let args = [
             JValue::Object(&name),
@@ -466,13 +468,6 @@ fn new_texture<'a>(env: &mut JNIEnv<'a>, texture_id: u64) -> DropbearNativeResul
         .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
 }
 
-fn texture_handle_id(registry: &dropbear_engine::asset::AssetRegistry, texture: &Texture) -> u64 {
-    texture
-        .hash
-        .and_then(|hash| registry.texture_handle_by_hash(hash).map(|h| h.id))
-        .unwrap_or(0)
-}
-
 fn map_vertex(vertex: &ModelVertex) -> NModelVertex {
     NModelVertex {
         position: NVector3::from(vertex.position),
@@ -496,13 +491,13 @@ fn map_mesh(mesh: &Mesh) -> NMesh {
 }
 
 fn map_material(
-    registry: &dropbear_engine::asset::AssetRegistry,
+    _registry: &dropbear_engine::asset::AssetRegistry,
     material: &Material,
 ) -> NMaterial {
     NMaterial {
         name: material.name.clone(),
-        diffuse_texture: texture_handle_id(registry, &material.diffuse_texture),
-        normal_texture: texture_handle_id(registry, &material.normal_texture),
+        diffuse_texture: material.diffuse_texture.id,
+        normal_texture: material.normal_texture.and_then(|v| Some(v.id)),
         tint: NVector4::from(material.tint),
         emissive_factor: NVector3::from(material.emissive_factor),
         metallic_factor: material.metallic_factor,
@@ -512,21 +507,13 @@ fn map_material(
         occlusion_strength: material.occlusion_strength,
         normal_scale: material.normal_scale,
         uv_tiling: NVector2::from(material.uv_tiling),
-        emissive_texture: material
-            .emissive_texture
-            .as_ref()
-            .map(|tex| texture_handle_id(registry, tex))
-            .filter(|id| *id != 0),
+        emissive_texture: material.emissive_texture.and_then(|v| Some(v.id)),
         metallic_roughness_texture: material
             .metallic_roughness_texture
-            .as_ref()
-            .map(|tex| texture_handle_id(registry, tex))
-            .filter(|id| *id != 0),
+            .and_then(|v| Some(v.id)),
         occlusion_texture: material
             .occlusion_texture
-            .as_ref()
-            .map(|tex| texture_handle_id(registry, tex))
-            .filter(|id| *id != 0),
+            .and_then(|v| Some(v.id)),
     }
 }
 
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 0459e73..45aeaa2 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -4,11 +4,11 @@ use crate::states::{SerializedMaterialCustomisation, SerializedMeshRenderer};
 use crate::utils::ResolveReference;
 use downcast_rs::{Downcast, impl_downcast};
 use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
-use dropbear_engine::entity::{EntityTransform, MeshRenderer};
+use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::model::Model;
 use dropbear_engine::procedural::{ProcObjType, ProcedurallyGeneratedObject};
-use dropbear_engine::texture::Texture;
+use dropbear_engine::texture::{Texture, TextureBuilder};
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
 use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, UiBuilder};
 use hecs::{Entity, World};
@@ -579,7 +579,8 @@ impl Component for MeshRenderer {
                     mat.texture_tag = m.texture_tag.clone();
                     mat.wrap_mode = m.wrap_mode;
 
-                    let get_tex = async |resource: &Option<ResourceReference>| -> Option<anyhow::Result<Texture>> {
+                    let get_tex_handle =
+                        async |resource: &Option<ResourceReference>| -> Option<anyhow::Result<Handle<Texture>>> {
                         if let Some(dif) = resource {
                             match &dif.ref_type {
                                 ResourceReferenceType::None => {
@@ -587,12 +588,21 @@ impl Component for MeshRenderer {
                                 }
                                 ResourceReferenceType::File(_) => {
                                     let path = dif.resolve().ok()?;
-                                    let tex = Texture::from_file(graphics.clone(), &path, Some(&label)).await;
-                                    Some(tex)
+                                    let bytes = std::fs::read(&path).ok()?;
+                                    let texture = TextureBuilder::new(&graphics.device)
+                                        .from_bytes(graphics.clone(), bytes.as_slice())
+                                        .label(label.as_str())
+                                        .build();
+                                    let mut registry = ASSET_REGISTRY.write();
+                                    Some(Ok(registry.add_texture_with_label(label.clone(), texture)))
                                 }
                                 ResourceReferenceType::Bytes(bytes) => {
-                                    let tex = Texture::from_bytes(graphics.clone(), bytes, Some(&label));
-                                    Some(Ok(tex))
+                                    let texture = TextureBuilder::new(&graphics.device)
+                                        .from_bytes(graphics.clone(), bytes)
+                                        .label(label.as_str())
+                                        .build();
+                                    let mut registry = ASSET_REGISTRY.write();
+                                    Some(Ok(registry.add_texture_with_label(label.clone(), texture)))
                                 }
                                 ResourceReferenceType::ProcObj(_) => {
                                     Some(Err(anyhow::anyhow!("Using a ProcObj as a texture is not valid, for texture with label {}", label)))
@@ -603,23 +613,23 @@ impl Component for MeshRenderer {
                         }
                     };
 
-                    if let Some(tex) = get_tex(&m.diffuse_texture).await {
+                    if let Some(tex) = get_tex_handle(&m.diffuse_texture).await {
                         mat.diffuse_texture = tex?;
                     }
 
-                    if let Some(tex) = get_tex(&m.emissive_texture).await {
+                    if let Some(tex) = get_tex_handle(&m.emissive_texture).await {
                         mat.emissive_texture = Some(tex?);
                     }
 
-                    if let Some(tex) = get_tex(&m.normal_texture).await {
-                        mat.normal_texture = tex?;
+                    if let Some(tex) = get_tex_handle(&m.normal_texture).await {
+                        mat.normal_texture = Some(tex?);
                     }
 
-                    if let Some(tex) = get_tex(&m.occlusion_texture).await {
+                    if let Some(tex) = get_tex_handle(&m.occlusion_texture).await {
                         mat.occlusion_texture = Some(tex?);
                     }
 
-                    if let Some(tex) = get_tex(&m.metallic_roughness_texture).await {
+                    if let Some(tex) = get_tex_handle(&m.metallic_roughness_texture).await {
                         mat.metallic_roughness_texture = Some(tex?);
                     }
                 }
@@ -635,17 +645,23 @@ impl Component for MeshRenderer {
         _physics: &mut PhysicsState,
         entity: Entity,
         _dt: f32,
-        _graphics: Arc<SharedGraphicsContext>,
+        graphics: Arc<SharedGraphicsContext>,
     ) {
         if let Ok(transform) = world.query_one::<&EntityTransform>(entity).get() {
             self.update(&transform.propagate(&world, entity));
+        } else {
+            self.update(&Transform::new());
+        }
+
+        for (_, v) in self.material_snapshot.iter() {
+            v.sync_uniform(&graphics);
         }
     }
 
     fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> {
         let asset = ASSET_REGISTRY.read();
         let model = asset.get_model(self.model());
-        let (label, handle) = if let Some(model) = model {
+        let (label, handle) = if let Some(model) = model.as_ref() {
             (model.label.clone(), model.path.clone())
         } else {
             if !self.model().is_null() {
@@ -659,20 +675,57 @@ impl Component for MeshRenderer {
 
         let mut texture_override: HashMap<String, SerializedMaterialCustomisation> = HashMap::new();
         for (label, mat) in &self.material_snapshot {
-            let diffuse_texture = mat.diffuse_texture.reference.clone();
-            let normal_texture = mat.normal_texture.reference.clone();
-            let emissive_texture = mat
-                .emissive_texture
-                .as_ref()
-                .and_then(|t| t.reference.clone());
-            let occlusion_texture = mat
-                .occlusion_texture
+            let default_material = model
                 .as_ref()
-                .and_then(|t| t.reference.clone());
-            let metallic_roughness_texture = mat
-                .metallic_roughness_texture
-                .as_ref()
-                .and_then(|t| t.reference.clone());
+                .and_then(|m| m.materials.iter().find(|default| default.name == *label));
+
+            let diffuse_texture = if default_material
+                .map(|default| default.diffuse_texture)
+                == Some(mat.diffuse_texture)
+            {
+                None
+            } else {
+                asset
+                    .get_texture(mat.diffuse_texture)
+                    .and_then(|t| t.reference.clone())
+            };
+
+            let normal_texture = if default_material.map(|default| default.normal_texture)
+                == Some(mat.normal_texture)
+            {
+                None
+            } else {
+                mat.normal_texture
+                    .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
+            };
+
+            let emissive_texture = if default_material.map(|default| default.emissive_texture)
+                == Some(mat.emissive_texture)
+            {
+                None
+            } else {
+                mat.emissive_texture
+                    .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
+            };
+
+            let occlusion_texture = if default_material.map(|default| default.occlusion_texture)
+                == Some(mat.occlusion_texture)
+            {
+                None
+            } else {
+                mat.occlusion_texture
+                    .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
+            };
+
+            let metallic_roughness_texture = if default_material
+                .map(|default| default.metallic_roughness_texture)
+                == Some(mat.metallic_roughness_texture)
+            {
+                None
+            } else {
+                mat.metallic_roughness_texture
+                    .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
+            };
 
             texture_override.insert(
                 label.to_string(),
@@ -1124,38 +1177,38 @@ impl InspectableComponent for MeshRenderer {
 
                             let default_textures = {
                                 let mut registry = ASSET_REGISTRY.write();
-                                let white_srgb = registry.solid_texture_rgba8_with_format(
+                                let white_srgb = registry.solid_texture_rgba8(
                                     graphics.clone(),
                                     [255, 255, 255, 255],
-                                    Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+                                    Some(Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix()),
                                 );
-                                let black_srgb = registry.solid_texture_rgba8_with_format(
+                                let black_srgb = registry.solid_texture_rgba8(
                                     graphics.clone(),
                                     [0, 0, 0, 255],
-                                    Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(),
+                                    Some(Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix()),
                                 );
-                                let white_linear = registry.solid_texture_rgba8_with_format(
+                                let white_linear = registry.solid_texture_rgba8(
                                     graphics.clone(),
                                     [255, 255, 255, 255],
-                                    Texture::TEXTURE_FORMAT_BASE,
+                                    Some(Texture::TEXTURE_FORMAT_BASE),
                                 );
-                                let green_linear = registry.solid_texture_rgba8_with_format(
+                                let green_linear = registry.solid_texture_rgba8(
                                     graphics.clone(),
                                     [0, 255, 0, 255],
-                                    Texture::TEXTURE_FORMAT_BASE,
+                                    Some(Texture::TEXTURE_FORMAT_BASE),
                                 );
-                                let flat_normal = registry.solid_texture_rgba8_with_format(
+                                let flat_normal = registry.solid_texture_rgba8(
                                     graphics.clone(),
                                     [128, 128, 255, 255],
-                                    Texture::TEXTURE_FORMAT_BASE,
+                                    Some(Texture::TEXTURE_FORMAT_BASE),
                                 );
 
                                 (
-                                    registry.get_texture(white_srgb).cloned(),
-                                    registry.get_texture(flat_normal).cloned(),
-                                    registry.get_texture(black_srgb).cloned(),
-                                    registry.get_texture(green_linear).cloned(),
-                                    registry.get_texture(white_linear).cloned(),
+                                    Some(white_srgb),
+                                    Some(flat_normal),
+                                    Some(black_srgb),
+                                    Some(green_linear),
+                                    Some(white_linear),
                                 )
                             };
 
@@ -1179,6 +1232,32 @@ impl InspectableComponent for MeshRenderer {
                                     .id_salt(format!("{} {}", material_id, entity.to_bits()))
                                     .default_open(true)
                                     .show(ui, |ui| {
+                                        ui.horizontal(|ui| {
+                                            if ui.button("Reset Material").clicked() {
+                                                if let Some(default) = default_material.as_ref() {
+                                                    material.diffuse_texture = default.diffuse_texture;
+                                                    material.normal_texture = default.normal_texture;
+                                                    material.emissive_texture = default.emissive_texture;
+                                                    material.metallic_roughness_texture =
+                                                        default.metallic_roughness_texture;
+                                                    material.occlusion_texture = default.occlusion_texture;
+                                                    material.tint = default.tint;
+                                                    material.emissive_factor = default.emissive_factor;
+                                                    material.metallic_factor = default.metallic_factor;
+                                                    material.roughness_factor = default.roughness_factor;
+                                                    material.alpha_mode = default.alpha_mode;
+                                                    material.alpha_cutoff = default.alpha_cutoff;
+                                                    material.double_sided = default.double_sided;
+                                                    material.occlusion_strength = default.occlusion_strength;
+                                                    material.normal_scale = default.normal_scale;
+                                                    material.uv_tiling = default.uv_tiling;
+                                                    material.texture_tag = default.texture_tag.clone();
+                                                    material.wrap_mode = default.wrap_mode;
+                                                    material.sync_uniform(&graphics);
+                                                }
+                                            }
+                                        });
+
                                         ui.add_space(4.0);
                                         ui.label(
                                             RichText::new("Colors")
@@ -1400,66 +1479,54 @@ impl InspectableComponent for MeshRenderer {
                                         ui.add_space(8.0);
                                         ui.label(RichText::new("Textures").strong());
                                         let texture_matches =
-                                            |current: Option<&Texture>, candidate: Option<&Texture>| {
+                                            |current: Option<Handle<Texture>>, candidate: Option<Handle<Texture>>| {
                                                 match (current, candidate) {
                                                     (None, None) => true,
-                                                    (Some(cur), Some(def)) => match (cur.hash, def.hash) {
-                                                        (Some(a), Some(b)) if a == b => true,
-                                                        _ => match (&cur.reference, &def.reference) {
-                                                            (Some(a), Some(b)) if a == b => true,
-                                                            _ => match (&cur.label, &def.label) {
-                                                                (Some(a), Some(b)) => a == b,
-                                                                _ => false,
-                                                            },
-                                                        },
-                                                    },
+                                                    (Some(cur), Some(def)) => cur == def,
                                                     _ => false,
                                                 }
                                             };
-                                        let original_label = |original: &Option<Texture>| -> String {
-                                            if let Some(texture) = original {
+                                        let handle_label = |handle: Handle<Texture>| -> String {
+                                            let registry = ASSET_REGISTRY.read();
+                                            if let Some(texture) = registry.get_texture(handle) {
                                                 if let Some(label) = &texture.label {
-                                                    label.clone()
-                                                } else if let Some(reference) = &texture.reference {
-                                                    reference
-                                                        .as_uri()
-                                                        .map(|uri| {
-                                                            uri.rsplit('/')
-                                                                .next()
-                                                                .filter(|v| !v.is_empty())
-                                                                .unwrap_or(uri)
-                                                                .to_string()
-                                                        })
-                                                        .unwrap_or_else(|| "Original".to_string())
-                                                } else {
-                                                    "Original".to_string()
+                                                    return label.clone();
+                                                }
+                                                if let Some(reference) = &texture.reference {
+                                                    if let Some(uri) = reference.as_uri() {
+                                                        return uri
+                                                            .rsplit('/')
+                                                            .next()
+                                                            .filter(|v| !v.is_empty())
+                                                            .unwrap_or(uri)
+                                                            .to_string();
+                                                    }
                                                 }
-                                            } else {
-                                                "Original (None)".to_string()
                                             }
+                                            format!("Texture {:016x}", handle.id)
+                                        };
+                                        let original_label = |original: Option<Handle<Texture>>| -> String {
+                                            original
+                                                .map(handle_label)
+                                                .unwrap_or_else(|| "Original (None)".to_string())
                                         };
                                         let texture_combo =
                                             |ui: &mut egui::Ui,
                                              slot_label: &str,
                                              slot_id: &str,
-                                             current_texture: Option<&Texture>,
-                                             original_texture: Option<Texture>,
-                                             default_texture: Option<Texture>|
-                                             -> Option<Option<Texture>> {
+                                             current_texture: Option<Handle<Texture>>,
+                                             original_texture: Option<Handle<Texture>>,
+                                             default_texture: Option<Handle<Texture>>|
+                                             -> Option<Option<Handle<Texture>>> {
                                                 let mut updated = None;
-                                                let is_original =
-                                                    texture_matches(current_texture, original_texture.as_ref());
-                                                let is_default =
-                                                    texture_matches(current_texture, default_texture.as_ref());
+                                                let is_original = texture_matches(current_texture, original_texture);
+                                                let is_default = texture_matches(current_texture, default_texture);
                                                 let selected_text = if is_original {
-                                                    original_label(&original_texture)
+                                                    original_label(original_texture)
                                                 } else if is_default {
                                                     "Default".to_string()
-                                                } else if let Some(texture) = current_texture {
-                                                    texture
-                                                        .label
-                                                        .clone()
-                                                        .unwrap_or_else(|| "Texture".to_string())
+                                                } else if let Some(handle) = current_texture {
+                                                    handle_label(handle)
                                                 } else {
                                                     "None".to_string()
                                                 };
@@ -1475,11 +1542,11 @@ impl InspectableComponent for MeshRenderer {
                                                         if ui
                                                             .selectable_label(
                                                                 false,
-                                                                original_label(&original_texture),
+                                                                original_label(original_texture),
                                                             )
                                                             .clicked()
                                                         {
-                                                            updated = Some(original_texture.clone());
+                                                            updated = Some(original_texture);
                                                         }
 
                                                         ui.separator();
@@ -1488,7 +1555,7 @@ impl InspectableComponent for MeshRenderer {
                                                             .selectable_label(false, "Default")
                                                             .clicked()
                                                         {
-                                                            updated = Some(default_texture.clone());
+                                                            updated = Some(default_texture);
                                                         }
 
                                                         ui.separator();
@@ -1501,9 +1568,9 @@ impl InspectableComponent for MeshRenderer {
                                                                 if let Some(texture) = ASSET_REGISTRY
                                                                     .read()
                                                                     .get_texture(*handle)
-                                                                    .cloned()
                                                                 {
-                                                                    updated = Some(Some(texture));
+                                                                    let _ = texture;
+                                                                    updated = Some(Some(*handle));
                                                                 }
                                                             }
                                                         }
@@ -1514,17 +1581,17 @@ impl InspectableComponent for MeshRenderer {
                                             };
 
                                         let original_diffuse =
-                                            default_material.as_ref().map(|m| m.diffuse_texture.clone());
+                                            default_material.as_ref().map(|m| m.diffuse_texture);
                                         let original_normal =
-                                            default_material.as_ref().map(|m| m.normal_texture.clone());
+                                            default_material.as_ref().and_then(|m| m.normal_texture);
                                         let original_emissive =
-                                            default_material.as_ref().and_then(|m| m.emissive_texture.clone());
+                                            default_material.as_ref().and_then(|m| m.emissive_texture);
                                         let original_mr = default_material
                                             .as_ref()
-                                            .and_then(|m| m.metallic_roughness_texture.clone());
+                                            .and_then(|m| m.metallic_roughness_texture);
                                         let original_occ = default_material
                                             .as_ref()
-                                            .and_then(|m| m.occlusion_texture.clone());
+                                            .and_then(|m| m.occlusion_texture);
 
                                         let (default_diffuse, default_normal, default_emissive, default_mr, default_occ) =
                                             default_textures.clone();
@@ -1533,7 +1600,7 @@ impl InspectableComponent for MeshRenderer {
                                             ui,
                                             "Diffuse",
                                             "diffuse",
-                                            Some(&material.diffuse_texture),
+                                            Some(material.diffuse_texture),
                                             original_diffuse,
                                             default_diffuse,
                                         ) {
@@ -1546,12 +1613,12 @@ impl InspectableComponent for MeshRenderer {
                                             ui,
                                             "Normal",
                                             "normal",
-                                            Some(&material.normal_texture),
+                                            material.normal_texture,
                                             original_normal,
                                             default_normal,
                                         ) {
                                             if let Some(tex) = new_normal {
-                                                material.normal_texture = tex;
+                                                material.normal_texture = Some(tex);
                                             }
                                         }
 
@@ -1559,7 +1626,7 @@ impl InspectableComponent for MeshRenderer {
                                             ui,
                                             "Emissive",
                                             "emissive",
-                                            material.emissive_texture.as_ref(),
+                                            material.emissive_texture,
                                             original_emissive,
                                             default_emissive,
                                         ) {
@@ -1570,7 +1637,7 @@ impl InspectableComponent for MeshRenderer {
                                             ui,
                                             "Metal/Rough",
                                             "metal_rough",
-                                            material.metallic_roughness_texture.as_ref(),
+                                            material.metallic_roughness_texture,
                                             original_mr,
                                             default_mr,
                                         ) {
@@ -1581,7 +1648,7 @@ impl InspectableComponent for MeshRenderer {
                                             ui,
                                             "Occlusion",
                                             "occlusion",
-                                            material.occlusion_texture.as_ref(),
+                                            material.occlusion_texture,
                                             original_occ,
                                             default_occ,
                                         ) {
diff --git a/crates/eucalyptus-core/src/mesh.rs b/crates/eucalyptus-core/src/mesh.rs
index d0b686b..781ab79 100644
--- a/crates/eucalyptus-core/src/mesh.rs
+++ b/crates/eucalyptus-core/src/mesh.rs
@@ -1,4 +1,6 @@
 pub mod shared {
+    use std::sync::Arc;
+
     use dropbear_engine::asset::AssetRegistry;
     use dropbear_engine::entity::MeshRenderer;
     use dropbear_engine::model::Model;
@@ -27,10 +29,10 @@ pub mod shared {
             .map(|mat| mat.name.clone())
     }
 
-    pub fn model_for_renderer<'a>(
-        registry: &'a AssetRegistry,
+    pub fn model_for_renderer(
+        registry: &AssetRegistry,
         renderer: &MeshRenderer,
-    ) -> Option<&'a Model> {
+    ) -> Option<Arc<Model>> {
         registry.get_model(renderer.model())
     }
 }
@@ -110,17 +112,15 @@ fn get_all_texture_ids(
         shared::model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
 
     let mut ids = HashSet::new();
-    let mut push_handle = |texture: &Texture| {
-        if let Some(hash) = texture.hash {
-            if let Some(handle) = reader.texture_handle_by_hash(hash) {
-                ids.insert(handle.id);
-            }
-        }
+    let mut push_handle = |handle: &Handle<Texture>| {
+        ids.insert(handle.id);
     };
 
     for material in &model.materials {
         push_handle(&material.diffuse_texture);
-        push_handle(&material.normal_texture);
+        if let Some(tex) = &material.normal_texture {
+            push_handle(tex);
+        }
         if let Some(tex) = &material.emissive_texture {
             push_handle(tex);
         }
@@ -154,7 +154,7 @@ fn get_texture(
         .map_err(|_| DropbearNativeError::NoSuchComponent)?;
     let model =
         shared::model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
-    let idx = match shared::resolve_target_material_index(model, &material_name) {
+    let idx = match shared::resolve_target_material_index(&model, &material_name) {
         Some(value) => value,
         None => return Ok(None),
     };
@@ -163,11 +163,9 @@ fn get_texture(
         .get(idx)
         .ok_or(DropbearNativeError::InvalidArgument)?;
 
-    Ok(material
+    Ok(Some(material
         .diffuse_texture
-        .hash
-        .and_then(|hash| reader.texture_handle_by_hash(hash))
-        .map(|handle| handle.id))
+        .id))
 }
 
 #[dropbear_macro::export(
@@ -190,20 +188,20 @@ fn set_texture_override(
         .map_err(|_| DropbearNativeError::NoSuchComponent)?;
     let model =
         shared::model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
-    let _ = shared::resolve_target_material_name(model, &material_name)
+    let _ = shared::resolve_target_material_name(&model, &material_name)
         .ok_or(DropbearNativeError::InvalidArgument)?;
 
     let handle = Handle::<Texture>::new(texture_handle);
-    let Some(diff_tex) = reader.get_texture(handle) else {
+    if reader.get_texture(handle).is_none() {
         return Err(DropbearNativeError::InvalidHandle);
-    };
+    }
 
     let mut renderer = world
         .get::<&mut MeshRenderer>(entity)
         .map_err(|_| DropbearNativeError::NoSuchComponent)?;
 
     if let Some(v) = renderer.material_snapshot.get_mut(&material_name) {
-        v.diffuse_texture = diff_tex.clone();
+        v.diffuse_texture = handle;
     } else {
         return Err(DropbearNativeError::InvalidArgument);
     }
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index c7128b0..ce8fbb7 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -1,6 +1,6 @@
 use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::model::Model;
-use dropbear_engine::texture::Texture;
+use dropbear_engine::texture::TextureBuilder;
 use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference};
 use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder};
 use eucalyptus_core::states::PROJECT;
@@ -1212,7 +1212,7 @@ impl<'a> EditorTabViewer<'a> {
                 graphics.clone(),
                 buffer,
                 Some(reference.clone()),
-                None,
+                Some(label.as_str()),
                 ASSET_REGISTRY.clone(),
             )
             .await
@@ -1225,10 +1225,6 @@ impl<'a> EditorTabViewer<'a> {
             };
 
             let mut registry = ASSET_REGISTRY.write();
-            if let Some(model) = registry.get_model_mut(handle) {
-                model.path = reference.clone();
-                model.label = label.clone();
-            }
             registry.label_model(label.clone(), handle);
 
             Ok::<(), anyhow::Error>(())
@@ -1249,13 +1245,12 @@ impl<'a> EditorTabViewer<'a> {
         let queue = graphics.future_queue.clone();
         queue.push(async move {
             let path = reference.resolve()?;
-            let texture = match Texture::from_file(graphics.clone(), &path, Some(&label)).await {
-                Ok(v) => v,
-                Err(e) => {
-                    eucalyptus_core::warn!("Unable to load texture {}: {}", reference, e);
-                    return Err(e);
-                }
-            };
+            let bytes = fs::read(&path)?;
+            let mut texture = TextureBuilder::new(&graphics.device)
+                .from_bytes(graphics.clone(), bytes.as_slice())
+                .label(label.as_str())
+                .build();
+            texture.reference = Some(reference.clone());
             let mut registry = ASSET_REGISTRY.write();
             registry.add_texture_with_label(label.clone(), texture);
             Ok::<(), anyhow::Error>(())
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 339ff4a..8a0c280 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -579,48 +579,46 @@ impl Scene for Editor {
             prepared_models.push((model, handle, instances.len() as u32));
         }
 
-        {
-            puffin::profile_scope!("light cube render pass");
-            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("light cube render pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: hdr.render_view(),
-                    depth_slice: None,
-                    resolve_target: hdr.resolve_target(),
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                    view: &graphics.depth_texture.view,
-                    depth_ops: Some(wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    }),
-                    stencil_ops: None,
-                }),
-                occlusion_query_set: None,
-                timestamp_writes: None,
-            });
-            if let Some(light_pipeline) = &self.light_cube_pipeline {
-                render_pass.set_pipeline(light_pipeline.pipeline());
-                for light in &lights {
-                    render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
-                    if !light.component.visible {
-                        continue;
-                    }
-
-                    let Some(model) = registry.get_model(light.cube_model) else {
-                        log_once::error_once!(
-                            "Missing light cube model handle {} in registry",
-                            light.cube_model.id
-                        );
-                        continue;
-                    };
+        if let Some(light_pipeline) = &self.light_cube_pipeline {
+            if let Some(l) = lights.first()
+                && let Some(model) = registry.get_model(l.cube_model)
+            {
+                {
+                    puffin::profile_scope!("light cube pass");
+                    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("light cube render pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: hdr.render_view(),
+                            depth_slice: None,
+                            resolve_target: hdr.resolve_target(),
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
+                        }),
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
 
-                    render_pass.draw_light_model(model, camera_bind_group, &light.bind_group);
+                    render_pass.set_pipeline(light_pipeline.pipeline());
+                    for light in &lights {
+                        render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
+                        if !light.component.visible {
+                            continue;
+                        }
+                        render_pass.draw_light_model(&model, camera_bind_group, &light.bind_group);
+                    }
                 }
+            } else {
+                log_once::error_once!("Missing light cube model handle in registry",);
             }
         }
 
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index f5f22de..96de6d9 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -1,6 +1,7 @@
 //! Allows you to a launch play mode as another window.
 
 use crossbeam_channel::{Receiver, unbounded};
+use dropbear_engine::billboarding::BillboardPipeline;
 use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::future::{FutureHandle, FutureQueue};
@@ -40,7 +41,6 @@ use std::sync::Arc;
 use wgpu::SurfaceConfiguration;
 use wgpu::util::DeviceExt;
 use winit::window::Fullscreen;
-use dropbear_engine::billboarding::BillboardPipeline;
 
 mod command;
 mod input;
@@ -248,7 +248,11 @@ impl PlayMode {
         Ok(result)
     }
 
-    pub fn reload_wgpu(&mut self, graphics: Arc<SharedGraphicsContext>, sky_texture: Option<&Vec<u8>>) {
+    pub fn reload_wgpu(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        sky_texture: Option<&Vec<u8>>,
+    ) {
         self.light_cube_pipeline = None;
         self.main_pipeline = None;
         self.shader_globals = None;
@@ -265,7 +269,11 @@ impl PlayMode {
         self.load_wgpu_nerdy_stuff(graphics, sky_texture);
     }
 
-    pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, sky_texture: Option<&Vec<u8>>) {
+    pub fn load_wgpu_nerdy_stuff<'a>(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        sky_texture: Option<&Vec<u8>>,
+    ) {
         self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone()));
         self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone()));
         self.shader_globals = Some(GlobalsUniform::new(
@@ -280,31 +288,43 @@ impl PlayMode {
         if self.default_skinning_buffer.is_none() {
             let max_skinning_matrices = 256usize;
             let identity = vec![Mat4::IDENTITY; max_skinning_matrices];
-            let skinning_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some("runtime default skinning buffer"),
-                contents: bytemuck::cast_slice(&identity),
-                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
-            });
-
-            let morph_deltas_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some("runtime default morph deltas buffer"),
-                contents: bytemuck::cast_slice(&[0.0f32]),
-                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
-            });
+            let skinning_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some("runtime default skinning buffer"),
+                        contents: bytemuck::cast_slice(&identity),
+                        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                    });
+
+            let morph_deltas_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some("runtime default morph deltas buffer"),
+                        contents: bytemuck::cast_slice(&[0.0f32]),
+                        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                    });
 
             let morph_weights = vec![0.0f32; MAX_MORPH_WEIGHTS];
-            let morph_weights_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some("runtime default morph weights buffer"),
-                contents: bytemuck::cast_slice(&morph_weights),
-                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
-            });
+            let morph_weights_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some("runtime default morph weights buffer"),
+                        contents: bytemuck::cast_slice(&morph_weights),
+                        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+                    });
 
             let morph_info = dropbear_engine::animation::MorphTargetInfo::default();
-            let morph_info_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some("runtime default morph info buffer"),
-                contents: bytemuck::bytes_of(&morph_info),
-                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
-            });
+            let morph_info_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some("runtime default morph info buffer"),
+                        contents: bytemuck::bytes_of(&morph_info),
+                        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+                    });
 
             self.default_skinning_buffer = Some(skinning_buffer);
             self.default_morph_deltas_buffer = Some(morph_deltas_buffer);
@@ -330,28 +350,30 @@ impl PlayMode {
                 .as_ref()
                 .expect("Default morph info buffer missing");
 
-            self.default_animation_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
-                label: Some("runtime default animation bind group"),
-                layout: &graphics.layouts.animation_layout,
-                entries: &[
-                    wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: skinning_buffer.as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 1,
-                        resource: morph_deltas_buffer.as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 2,
-                        resource: morph_weights_buffer.as_entire_binding(),
-                    },
-                    wgpu::BindGroupEntry {
-                        binding: 3,
-                        resource: morph_info_buffer.as_entire_binding(),
-                    },
-                ],
-            }));
+            self.default_animation_bind_group = Some(graphics.device.create_bind_group(
+                &wgpu::BindGroupDescriptor {
+                    label: Some("runtime default animation bind group"),
+                    layout: &graphics.layouts.animation_layout,
+                    entries: &[
+                        wgpu::BindGroupEntry {
+                            binding: 0,
+                            resource: skinning_buffer.as_entire_binding(),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 1,
+                            resource: morph_deltas_buffer.as_entire_binding(),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 2,
+                            resource: morph_weights_buffer.as_entire_binding(),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 3,
+                            resource: morph_info_buffer.as_entire_binding(),
+                        },
+                    ],
+                },
+            ));
         }
 
         self.kino = Some(KinoState::new(
@@ -379,14 +401,16 @@ impl PlayMode {
 
         if let Some(camera_entity) = self.active_camera {
             if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() {
-                camera_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("runtime camera bind group"),
-                    layout: &graphics.layouts.camera_bind_group_layout,
-                    entries: &[wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: camera.buffer().as_entire_binding(),
-                    }],
-                }));
+                camera_bind_group = Some(graphics.device.create_bind_group(
+                    &wgpu::BindGroupDescriptor {
+                        label: Some("runtime camera bind group"),
+                        layout: &graphics.layouts.camera_bind_group_layout,
+                        entries: &[wgpu::BindGroupEntry {
+                            binding: 0,
+                            resource: camera.buffer().as_entire_binding(),
+                        }],
+                    },
+                ));
 
                 match sky_texture_result {
                     Ok(sky_texture) => {
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 77873ae..a296c81 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -29,11 +29,11 @@ use eucalyptus_core::states::{Label, PROJECT};
 use eucalyptus_core::ui::HUDComponent;
 use glam::{DVec3, Mat3, Mat4, Quat, Vec2, Vec3};
 use hecs::Entity;
+use kino_ui::WidgetTree;
+use kino_ui::rendering::KinoRenderTargetId;
 use std::collections::HashMap;
 use winit::event::WindowEvent;
 use winit::event_loop::ActiveEventLoop;
-use kino_ui::rendering::KinoRenderTargetId;
-use kino_ui::{WidgetTree};
 
 impl Scene for PlayMode {
     fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
@@ -672,9 +672,15 @@ impl Scene for PlayMode {
         }
 
         let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<
-            (Entity, u64, InstanceRaw, wgpu::Buffer, wgpu::Buffer, wgpu::Buffer, u32),
-        > = Vec::new();
+        let mut animated_instances: Vec<(
+            Entity,
+            u64,
+            InstanceRaw,
+            wgpu::Buffer,
+            wgpu::Buffer,
+            wgpu::Buffer,
+            u32,
+        )> = Vec::new();
 
         {
             let mut query = self
@@ -769,50 +775,47 @@ impl Scene for PlayMode {
         }
 
         let registry = ASSET_REGISTRY.read();
-        {
-            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("light cube render pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: hdr.render_view(),
-                    depth_slice: None,
-                    resolve_target: hdr.resolve_target(),
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                    view: &graphics.depth_texture.view,
-                    depth_ops: Some(wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    }),
-                    stencil_ops: None,
-                }),
-                occlusion_query_set: None,
-                timestamp_writes: None,
-            });
-            if let Some(light_pipeline) = &self.light_cube_pipeline {
-                render_pass.set_pipeline(light_pipeline.pipeline());
-                for light in &lights {
-                    render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
-                    if !light.component.visible {
-                        continue;
-                    }
-
-                    let Some(model) = registry.get_model(light.cube_model) else {
-                        log_once::error_once!(
-                            "Missing light cube model handle {} in registry",
-                            light.cube_model.id
-                        );
-                        continue;
-                    };
+        if let Some(light_pipeline) = &self.light_cube_pipeline {
+            if let Some(l) = lights.first()
+                && let Some(model) = registry.get_model(l.cube_model)
+            {
+                {
+                    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("light cube render pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: hdr.render_view(),
+                            depth_slice: None,
+                            resolve_target: hdr.resolve_target(),
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
+                        }),
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
 
-                    render_pass.draw_light_model(model, camera_bind_group, &light.bind_group);
+                    render_pass.set_pipeline(light_pipeline.pipeline());
+                    for light in &lights {
+                        render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
+                        if !light.component.visible {
+                            continue;
+                        }
+                        render_pass.draw_light_model(&model, camera_bind_group, &light.bind_group);
+                    }
                 }
+            } else {
+                log_once::error_once!("Missing light cube model handle in registry",);
             }
         }
-
         let default_skinning_buffer = self
             .default_skinning_buffer
             .as_ref()
@@ -838,7 +841,7 @@ impl Scene for PlayMode {
             return;
         };
         log_once::debug_once!("Pipeline ready");
-        
+
         for (model, handle, instance_count) in prepared_models {
             let morph_deltas_buffer = model
                 .morph_deltas_buffer
@@ -886,8 +889,7 @@ impl Scene for PlayMode {
             });
             render_pass.set_pipeline(pipeline.pipeline());
             if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
-                render_pass
-                    .set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
             } else {
                 continue;
             }
@@ -921,9 +923,11 @@ impl Scene for PlayMode {
                     _padding: Default::default(),
                 };
 
-                graphics
-                    .queue
-                    .write_buffer(default_morph_info_buffer, 0, bytemuck::bytes_of(&info));
+                graphics.queue.write_buffer(
+                    default_morph_info_buffer,
+                    0,
+                    bytemuck::bytes_of(&info),
+                );
 
                 let material = &model.materials[mesh.material];
                 render_pass.draw_mesh_instanced(
@@ -975,17 +979,17 @@ impl Scene for PlayMode {
                     .expect("Per-frame bind group not initialised")
                     .clone();
 
-                let instance_buffer = self
-                    .animated_instance_buffers
-                    .entry(entity)
-                    .or_insert_with(|| {
-                        ResizableBuffer::new(
-                            &graphics.device,
-                            1,
-                            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                            "Runtime Animated Instance Buffer",
-                        )
-                    });
+                let instance_buffer =
+                    self.animated_instance_buffers
+                        .entry(entity)
+                        .or_insert_with(|| {
+                            ResizableBuffer::new(
+                                &graphics.device,
+                                1,
+                                wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                                "Runtime Animated Instance Buffer",
+                            )
+                        });
                 instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
 
                 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -1202,12 +1206,9 @@ impl Scene for PlayMode {
         {
             // 1. render billboards and prepare views
             if let Some(kino) = &mut self.kino {
-                let mut kino_encoder = CommandEncoder::new(graphics.clone(), Some("kino billboard encoder"));
-                kino.render_billboard_targets(
-                    &graphics.device,
-                    &graphics.queue,
-                    &mut kino_encoder,
-                );
+                let mut kino_encoder =
+                    CommandEncoder::new(graphics.clone(), Some("kino billboard encoder"));
+                kino.render_billboard_targets(&graphics.device, &graphics.queue, &mut kino_encoder);
 
                 if let Err(e) = kino_encoder.submit() {
                     log_once::error_once!("Unable to submit billboard kino pass: {}", e);
@@ -1234,8 +1235,7 @@ impl Scene for PlayMode {
                 let mut query = self
                     .world
                     .query::<(Entity, &BillboardComponent, &EntityTransform)>();
-                for (entity, billboard, entity_transform) in query.iter()
-                {
+                for (entity, billboard, entity_transform) in query.iter() {
                     if !billboard.enabled {
                         continue;
                     }
@@ -1274,7 +1274,8 @@ impl Scene for PlayMode {
                         }
                     };
 
-                    let transform = Mat4::from_scale_rotation_translation(scale, rotation, position);
+                    let transform =
+                        Mat4::from_scale_rotation_translation(scale, rotation, position);
                     billboards.push((transform, texture_view));
                 }
 
diff --git a/include/dropbear.h b/include/dropbear.h
index ebafb09..375fd6b 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -308,7 +308,10 @@ typedef struct NVector2 {
 typedef struct NMaterial {
     const char* name;
     uint64_t diffuse_texture;
-    uint64_t normal_texture;
+    const uint64_t* normal_texture;
+    const uint64_t* emissive_texture;
+    const uint64_t* metallic_roughness_texture;
+    const uint64_t* occlusion_texture;
     NVector4 tint;
     NVector3 emissive_factor;
     float metallic_factor;
@@ -318,9 +321,6 @@ typedef struct NMaterial {
     float occlusion_strength;
     float normal_scale;
     NVector2 uv_tiling;
-    const uint64_t* emissive_texture;
-    const uint64_t* metallic_roughness_texture;
-    const uint64_t* occlusion_texture;
 } NMaterial;
 
 typedef struct NMaterialArray {