kitgit

tirbofish/dropbear · diff

e00f91c · Thribhu K

wip: asset server remake and improved model importing wip: animation within gltf

Unverified

diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index 49c4aa3..62c6633 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -1,513 +1,222 @@
-use std::sync::{
-    Arc, LazyLock,
-    atomic::{AtomicU64, Ordering},
-};
-
-use dashmap::DashMap;
-
+use std::collections::HashMap;
+use std::hash::{DefaultHasher, Hash, Hasher};
+use std::marker::PhantomData;
+use std::sync::{Arc, LazyLock};
+use parking_lot::RwLock;
 use crate::{
-    graphics::{SharedGraphicsContext},
-    model::{Material, Mesh, Model, ModelId},
-    utils::ResourceReference,
     texture::Texture,
 };
+use crate::graphics::SharedGraphicsContext;
+use crate::model::Model;
 
-/// Opaque identifier returned from the [`AssetRegistry`] for stored assets.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-pub struct AssetHandle(u64);
-impl AssetHandle {
-    /// Creates a new [`AssetHandle`].
-    ///
-    /// This function does not guarantee if the raw value exists in the registry.
-    /// You will have to check yourself.
-    pub fn new(raw: impl Into<u64>) -> Self {
-        Self(raw.into())
-    }
-    /// Returns the raw/primitive [`u64`] value.
-    pub fn raw(&self) -> u64 {
-        self.0
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum AssetKind {
-    Model,
-    Material,
-    Mesh,
-}
+pub static ASSET_REGISTRY: LazyLock<Arc<RwLock<AssetRegistry>>> = LazyLock::new(|| Arc::new(RwLock::new(AssetRegistry::new())));
 
-#[derive(Debug, Eq, PartialEq, Hash)]
-pub enum PointerKind {
-    Const(&'static str),
-    Mut(&'static str),
+/// A handle with type [`T`] that provides an index to the [AssetRegistry] contents.
+#[derive(Hash, Eq, PartialEq, Debug)]
+pub struct Handle<T> {
+    pub id: u64,
+    _phantom: PhantomData<T>
 }
 
-/// Centralised cache for models and their dependent resources.
-///
-/// The registry assigns stable [`AssetHandle`] values that can be
-/// reused by systems without having to keep strong references to the
-/// underlying assets. Models are keyed by their [`ResourceReference`]
-/// while meshes and materials are keyed by `(ModelId, name)` pairs.
-pub struct AssetRegistry {
-    next_id: AtomicU64,
-
-    model_handles: DashMap<ResourceReference, AssetHandle>,
-    model_id_lookup: DashMap<ModelId, AssetHandle>,
-    model_references: DashMap<AssetHandle, ResourceReference>,
-    model_reference_lookup: DashMap<ResourceReference, AssetHandle>,
-    models: DashMap<AssetHandle, Arc<Model>>,
+impl<T> Copy for Handle<T> {}
 
-    material_lookup: DashMap<(ModelId, String), AssetHandle>,
-    material_owners: DashMap<AssetHandle, ModelId>,
-    material_references: DashMap<AssetHandle, ResourceReference>,
-    material_reference_lookup: DashMap<ResourceReference, AssetHandle>,
-    materials: DashMap<AssetHandle, Arc<Material>>,
+impl<T> Clone for Handle<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
 
-    mesh_lookup: DashMap<(ModelId, String), AssetHandle>,
-    mesh_owners: DashMap<AssetHandle, ModelId>,
-    mesh_references: DashMap<AssetHandle, ResourceReference>,
-    mesh_reference_lookup: DashMap<ResourceReference, AssetHandle>,
-    meshes: DashMap<AssetHandle, Arc<Mesh>>,
+impl<T> Handle<T> {
+    /// Creates a null handle, for when there is no way to uniquely identify a hash (such as a viewport texture).
+    ///
+    /// # Safety
+    /// You will want to watch out, as adding this onto the asset registry with a type
+    /// where there already is a null handle item, it will be overwritten and data
+    /// will not be saved. It is the reason why you will want to consider using the [Self::is_null]
+    /// function to verify if the storage of the type has gone through correctly.
+    pub const NULL: Self = Self { id: 0, _phantom: PhantomData };
 
-    /// Internal pointer database, typically used when querying in the database
-    pointers: DashMap<PointerKind, usize>,
+    /// Creates a new handle with the given ID.
+    pub fn new(id: u64) -> Self {
+        Self { id, _phantom: Default::default() }
+    }
 
-    /// Built-in textures created at runtime and reused across assets.
-    built_in_textures: DashMap<&'static str, Arc<Texture>>,
+    /// Returns true if the handle is null.
+    pub fn is_null(&self) -> bool {
+        self.id == 0
+    }
+}
 
-    /// 1×1 solid-colour textures cached by packed RGBA8.
-    solid_textures: DashMap<u32, Arc<Texture>>,
+pub struct AssetRegistry {
+    textures: HashMap<u64, Texture>,
+    texture_labels: HashMap<String, Handle<Texture>>,
 
-    /// Per-model import-scale overrides keyed by the model's resource reference.
-    ///
-    /// This is editor/user configuration and should outlive cached model data.
-    model_import_scales: DashMap<ResourceReference, f32>,
+    models: HashMap<u64, Model>,
+    model_labels: HashMap<String, Handle<Model>>,
 }
-
+/// Common
 impl AssetRegistry {
     pub fn new() -> Self {
         Self {
-            next_id: AtomicU64::new(1),
-            model_handles: DashMap::new(),
-            model_id_lookup: DashMap::new(),
-            model_references: DashMap::new(),
-            model_reference_lookup: DashMap::new(),
-            models: DashMap::new(),
-            material_lookup: DashMap::new(),
-            material_owners: DashMap::new(),
-            material_references: DashMap::new(),
-            material_reference_lookup: DashMap::new(),
-            materials: DashMap::new(),
-            mesh_lookup: DashMap::new(),
-            mesh_owners: DashMap::new(),
-            mesh_references: DashMap::new(),
-            mesh_reference_lookup: DashMap::new(),
-            meshes: DashMap::new(),
-            pointers: DashMap::new(),
-            built_in_textures: DashMap::new(),
-            solid_textures: DashMap::new(),
-            model_import_scales: DashMap::new(),
+            textures: Default::default(),
+            texture_labels: Default::default(),
+            models: Default::default(),
+            model_labels: Default::default(),
         }
     }
 
-    /// Returns the per-model import scale for the given resource reference.
-    ///
-    /// Defaults to `1.0` when no override exists.
-    pub fn model_import_scale(&self, reference: &ResourceReference) -> f32 {
-        self.model_import_scales
-            .get(reference)
-            .map(|v| *v.value())
-            .unwrap_or(1.0)
+    /// A convenient helper function for hashing a byte slice of data.
+    pub(crate) fn hash_bytes(data: &[u8]) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        data.hash(&mut hasher);
+        hasher.finish()
     }
 
-    /// Sets the per-model import scale override for the given resource reference.
-    pub fn set_model_import_scale(&self, reference: ResourceReference, scale: f32) {
-        let scale = if scale.is_finite() { scale } else { 1.0 };
-        self.model_import_scales.insert(reference, scale);
+    /// 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()
     }
 
-    /// Returns a cached 1×1 solid-colour texture, creating it on first use.
+    /// Checks if the asset registry contains a handle with the given hash.
     ///
-    /// The cache key is the packed RGBA8 bytes (little-endian).
-    pub fn solid_texture_rgba8(
-        &self,
-        graphics: Arc<SharedGraphicsContext>,
-        rgba: [u8; 4],
-    ) -> Arc<Texture> {
-        let key = u32::from_le_bytes(rgba);
-
-        if let Some(existing) = self.solid_textures.get(&key) {
-            return Arc::clone(existing.value());
-        }
-
-        let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]);
-
-        let texture = Texture::from_bytes_verbose_mipmapped(
-            graphics,
-            &rgba,
-            Some((1, 1)),
-            None,
-            None,
-            Some(label.as_str())
-        );
-        let texture = Arc::new(texture);
-
-        match self.solid_textures.entry(key) {
-            dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
-            dashmap::mapref::entry::Entry::Vacant(entry) => {
-                entry.insert(Arc::clone(&texture));
-                texture
-            }
-        }
+    /// It will check all different types, so it does not point out specifically where.
+    pub fn contains_hash(&self, hash: u64) -> bool {
+        self.textures.contains_key(&hash) || self.models.contains_key(&hash)
     }
 
-    /// Returns a cached 1×1 grey texture, creating it on first use.
+    /// Checks if the asset registry contains a handle with the given string label.
     ///
-    /// This is intended as a cheap fallback when a model/material has no diffuse texture.
-    pub fn grey_texture(&self, graphics: Arc<SharedGraphicsContext>) -> Arc<Texture> {
-        const KEY: &str = "builtin_grey_1x1";
-
-        if let Some(existing) = self.built_in_textures.get(KEY) {
-            return Arc::clone(existing.value());
-        }
-
-        // 50% grey, fully opaque.
-        let texture = self.solid_texture_rgba8(graphics, [128, 128, 128, 255]);
-
-        // Race-safe insert: if another thread beat us, reuse theirs.
-        match self.built_in_textures.entry(KEY) {
-            dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
-            dashmap::mapref::entry::Entry::Vacant(entry) => {
-                entry.insert(Arc::clone(&texture));
-                texture
-            }
-        }
+    /// It will check all different types, so it does not point out specifically where.
+    pub fn contains_label(&self, label: &str) -> bool {
+        self.texture_labels.contains_key(label) || self.model_labels.contains_key(label)
     }
+}
 
-    /// Clears all cached asset data (models/materials/meshes) from the registry.
+/// Texture stuff
+impl AssetRegistry {
+    /// Adds a texture and returns a handle.
     ///
-    /// This is intended for full scene reloads where the previous scene's assets
-    /// should be dropped and reloaded. Pointer entries are intentionally preserved.
-    pub fn clear_cached_assets(&self) {
-        self.model_handles.clear();
-        self.model_id_lookup.clear();
-        self.model_references.clear();
-        self.model_reference_lookup.clear();
-        self.models.clear();
-
-        self.material_lookup.clear();
-        self.material_owners.clear();
-        self.material_references.clear();
-        self.material_reference_lookup.clear();
-        self.materials.clear();
-
-        self.mesh_lookup.clear();
-        self.mesh_owners.clear();
-        self.mesh_references.clear();
-        self.mesh_reference_lookup.clear();
-        self.meshes.clear();
-    }
-
-    /// Adds a pointer to the asset registry.
-    pub fn add_pointer(&self, pointer_kind: PointerKind, pointer: usize) {
-        self.pointers.insert(pointer_kind, pointer);
-    }
-
-    /// Attempts to fetch a pointer from the [`AssetRegistry`] by its given [`PointerKind`]
-    pub fn get_pointer(&self, pointer_kind: PointerKind) -> Option<usize> {
-        self.pointers.get(&pointer_kind).map(|entry| *entry.value())
-    }
-
-    fn allocate_handle(&self) -> AssetHandle {
-        AssetHandle(self.next_id.fetch_add(1, Ordering::Relaxed))
-    }
-
-    /// Registers a model and caches its meshes and materials.
-    pub fn register_model(&self, reference: ResourceReference, model: Arc<Model>) -> AssetHandle {
-        let canonical = reference.clone();
-        self.model_import_scales.entry(canonical.clone()).or_insert(1.0);
-
-        let model_handle = if let Some(existing) = self.model_handles.get(&canonical) {
-            let handle = *existing;
-            self.models.insert(handle, Arc::clone(&model));
-            handle
-        } else {
-            let handle = self.allocate_handle();
-            self.models.insert(handle, Arc::clone(&model));
-            self.model_handles.insert(canonical.clone(), handle);
-            handle
-        };
-
-        self.model_id_lookup.insert(model.id, model_handle);
-
-        self.model_references
-            .insert(model_handle, canonical.clone());
-        self.model_reference_lookup.insert(canonical, model_handle);
-
-        self.cache_model_components(&model);
-
-        model_handle
-    }
-
-    /// Iterates through all models, allowing you to iterate through all items in the
-    /// model registry.
-    pub fn iter_model(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> {
-        self.iter_model_raw()
-    }
-
-    pub fn iter_model_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> {
-        self.models.iter()
-    }
-
-    pub fn iter_material(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> {
-        self.iter_material_raw()
-    }
-
-    pub fn iter_material_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> {
-        self.materials.iter()
-    }
-
-    /// Returns the cached model handle if it exists.
-    pub fn model_handle(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.model_handle_raw(reference)
-    }
-
-    pub fn model_handle_raw(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.model_handles.get(reference).map(|entry| *entry)
-    }
-
-    /// Fetches a model by handle.
-    pub fn get_model(&self, handle: AssetHandle) -> Option<Arc<Model>> {
-        self.get_model_raw(handle)
-    }
-
-    pub fn get_model_raw(&self, handle: AssetHandle) -> Option<Arc<Model>> {
-        self.models
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
+    /// This assumes a [Texture] has already been created by you. To create a new texture,
+    /// you can use [`Texture::from_bytes`].
+    pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> {
+        let handle = texture.hash.map(|v| Handle::new(v)).unwrap_or_else(|| Handle::NULL);
+        self.textures.entry(handle.id).or_insert(texture);
+        handle
     }
 
-    /// Fetches a material by handle.
-    pub fn get_material(&self, handle: AssetHandle) -> Option<Arc<Material>> {
-        self.get_material_raw(handle)
+    /// Adds a texture with a label. If the texture already exists (by hash),
+    /// returns the existing handle and updates the label to point at it.
+    pub fn add_texture_with_label(&mut self, label: impl Into<String>, texture: Texture) -> Handle<Texture> {
+        let handle = self.add_texture(texture);
+        self.texture_labels.insert(label.into(), handle.clone());
+        handle
     }
 
-    pub fn get_material_raw(&self, handle: AssetHandle) -> Option<Arc<Material>> {
-        self.materials
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
+    /// 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());
     }
 
-    /// Fetches a mesh by handle.
-    pub fn get_mesh(&self, handle: AssetHandle) -> Option<Arc<Mesh>> {
-        self.get_mesh_raw(handle)
+    /// 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_mesh_raw(&self, handle: AssetHandle) -> Option<Arc<Mesh>> {
-        self.meshes
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
+    pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> {
+        self.textures.get(&handle.id)
     }
 
-    /// Fetches a handle from a [`ResourceReference`] by checking through
-    /// each cache
-    pub fn get_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.material_handle_from_reference(reference)
-            .or_else(|| self.mesh_handle_from_reference(reference))
-            .or_else(|| self.model_handle_from_reference(reference))
+    pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> {
+        self.texture_labels
+            .get(label)
+            .and_then(|handle| self.textures.get(&handle.id))
     }
 
-    /// Retrieves (or lazily creates) the handle for a specific material on a model.
-    pub fn material_handle(&self, model_id: ModelId, name: &str) -> Option<AssetHandle> {
-        let key = (model_id, name.to_string());
-        self.material_lookup.get(&key).map(|entry| *entry)
+    pub fn get_texture_handle_from_label(&self, label: &str) -> Option<Handle<Texture>> {
+        self.texture_labels.get(label).cloned()
     }
 
-    /// Retrieves (or lazily creates) the handle for a specific mesh on a model.
-    pub fn mesh_handle(&self, model_id: ModelId, name: &str) -> Option<AssetHandle> {
-        let key = (model_id, name.to_string());
-        self.mesh_lookup.get(&key).map(|entry| *entry)
+    pub fn texture_handle_by_hash(&self, hash: u64) -> Option<Handle<Texture>> {
+        self.textures.contains_key(&hash).then(|| Handle::new(hash))
     }
 
-    /// Returns the kind of asset associated with a handle, if known.
-    pub fn kind(&self, handle: AssetHandle) -> Option<AssetKind> {
-        if self.models.contains_key(&handle) {
-            Some(AssetKind::Model)
-        } else if self.materials.contains_key(&handle) {
-            Some(AssetKind::Material)
-        } else if self.meshes.contains_key(&handle) {
-            Some(AssetKind::Mesh)
-        } else {
-            None
+    pub fn grey_texture(&mut self, graphics: Arc<SharedGraphicsContext>) -> Handle<Texture> {
+        let grey_handle = Handle::new(Self::hash_contents("Solid texture [128, 128, 128, 255]"));
+        
+        if self.contains_hash(grey_handle.id) {
+            return grey_handle;
         }
-    }
-
-    /// Returns `true` if the handle exists in any asset cache.
-    pub fn contains_handle(&self, handle: AssetHandle) -> bool {
-        self.models.contains_key(&handle)
-            || self.materials.contains_key(&handle)
-            || self.meshes.contains_key(&handle)
-    }
-
-    /// Returns `true` if the handle represents the expected asset kind.
-    pub fn is_handle_kind(&self, handle: AssetHandle, expected: AssetKind) -> bool {
-        matches!(self.kind(handle), Some(kind) if kind == expected)
-    }
-
-    /// Returns the `ResourceReference` recorded for a model handle, if any.
-    pub fn model_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.model_references
-            .get(&handle)
-            .map(|entry| entry.value().clone())
-    }
-
-    /// Attempts to resolve a model handle from a `ResourceReference`.
-    pub fn model_handle_from_reference(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.model_reference_lookup
-            .get(reference)
-            .map(|entry| *entry)
-    }
-
-    /// Attempts to resolve a model handle directly from a [`ModelId`].
-    pub fn model_handle_from_id(&self, model_id: ModelId) -> Option<AssetHandle> {
-        self.model_id_lookup.get(&model_id).map(|entry| *entry)
-    }
-
-    /// Returns `true` if the handle refers to a material asset.
-    pub fn is_material(&self, handle: AssetHandle) -> bool {
-        self.materials.contains_key(&handle)
-    }
 
-    /// Returns `true` if the handle refers to a mesh asset.
-    pub fn is_mesh(&self, handle: AssetHandle) -> bool {
-        self.meshes.contains_key(&handle)
+        self.solid_texture_rgba8(graphics, [128, 128, 128, 255])
     }
 
-    /// Returns `true` if the handle refers to a model asset.
-    pub fn is_model(&self, handle: AssetHandle) -> bool {
-        self.models.contains_key(&handle)
-    }
-
-    /// Returns the owning model ID for the given material handle.
-    pub fn material_owner(&self, handle: AssetHandle) -> Option<ModelId> {
-        self.material_owner_raw(handle)
-    }
+    pub fn solid_texture_rgba8(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        rgba: [u8; 4],
+    ) -> Handle<Texture> {
+        let handle = Handle::new(Self::hash_bytes(&rgba));
 
-    pub fn material_owner_raw(&self, handle: AssetHandle) -> Option<ModelId> {
-        self.material_owners.get(&handle).map(|entry| *entry)
-    }
+        if self.contains_hash(handle.id) {
+            return handle;
+        }
 
-    /// Returns the owning model ID for the given mesh handle.
-    pub fn mesh_owner(&self, handle: AssetHandle) -> Option<ModelId> {
-        self.mesh_owners.get(&handle).map(|entry| *entry)
-    }
+        let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]);
 
-    /// Returns the synthetic `ResourceReference` associated with a material handle.
-    pub fn material_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.material_reference_for_handle_raw(handle)
+        let texture = Texture::from_bytes_verbose_mipmapped(
+            graphics,
+            &rgba,
+            Some((1, 1)),
+            None,
+            None,
+            Some(label.as_str())
+        );
+        
+        self.add_texture_with_label(label, texture)
     }
+}
 
-    pub fn material_reference_for_handle_raw(
-        &self,
-        handle: AssetHandle,
-    ) -> Option<ResourceReference> {
-        self.material_references
-            .get(&handle)
-            .map(|entry| entry.value().clone())
+/// Model stuff
+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);
+        handle
     }
 
-    /// Returns the synthetic `ResourceReference` associated with a mesh handle.
-    pub fn mesh_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.mesh_reference_for_handle_raw(handle)
+    pub fn add_model_with_label(&mut self, label: impl Into<String>, model: Model) -> Handle<Model> {
+        let handle = self.add_model(model);
+        self.model_labels.insert(label.into(), handle.clone());
+        handle
     }
 
-    pub fn mesh_reference_for_handle_raw(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.mesh_references
-            .get(&handle)
-            .map(|entry| entry.value().clone())
+    pub fn label_model(&mut self, label: impl Into<String>, handle: Handle<Model>) {
+        self.model_labels.insert(label.into(), handle.clone());
     }
 
-    /// Attempts to resolve a material handle from its synthetic `ResourceReference`.
-    pub fn material_handle_from_reference(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.material_handle_from_reference_raw(reference)
+    pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Model> {
+        self.models.insert(handle.id, model)
     }
 
-    pub fn material_handle_from_reference_raw(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.material_reference_lookup
-            .get(reference)
-            .map(|entry| *entry)
+    pub fn get_model(&self, handle: Handle<Model>) -> Option<&Model> {
+        self.models.get(&handle.id)
     }
 
-    /// Attempts to resolve a mesh handle from its synthetic `ResourceReference`.
-    pub fn mesh_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.mesh_handle_from_reference_raw(reference)
+    pub fn get_model_by_label(&self, label: &str) -> Option<&Model> {
+        self.model_labels
+            .get(label)
+            .and_then(|handle| self.models.get(&handle.id))
     }
 
-    pub fn mesh_handle_from_reference_raw(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.mesh_reference_lookup
-            .get(reference)
-            .map(|entry| *entry)
+    pub fn get_model_handle_from_label(&self, label: &str) -> Option<Handle<Model>> {
+        self.model_labels.get(label).cloned()
     }
 
-    fn cache_model_components(&self, model: &Arc<Model>) {
-        let model_id = model.id;
-
-        for material in &model.materials {
-            let name = material.name.clone();
-            let key = (model_id, name.clone());
-            let handle = if let Some(existing) = self.material_lookup.get(&key) {
-                *existing
-            } else {
-                let handle = self.allocate_handle();
-                self.material_lookup.insert(key.clone(), handle);
-                handle
-            };
-
-            self.material_owners.insert(handle, model_id);
-
-            let reference = material_reference_from_model(model.as_ref(), &name)
-                .or_else(|| material_reference_fallback(model_id, &name));
-
-            if let Some(reference) = reference {
-                self.material_references.insert(handle, reference.clone());
-                self.material_reference_lookup.insert(reference, handle);
-            }
-
-            self.materials.insert(handle, Arc::new(material.clone()));
-        }
-
-        for mesh in &model.meshes {
-            let name = mesh.name.clone();
-            let key = (model_id, name.clone());
-            let handle = if let Some(existing) = self.mesh_lookup.get(&key) {
-                *existing
-            } else {
-                let handle = self.allocate_handle();
-                self.mesh_lookup.insert(key.clone(), handle);
-                handle
-            };
-
-            self.mesh_owners.insert(handle, model_id);
-
-            if let Some(reference) = mesh_reference(model_id, &name) {
-                self.mesh_references.insert(handle, reference.clone());
-                self.mesh_reference_lookup.insert(reference, handle);
-            }
-
-            self.meshes.insert(handle, Arc::new(mesh.clone()));
-        }
+    pub fn model_handle_by_hash(&self, hash: u64) -> Option<Handle<Model>> {
+        self.models.contains_key(&hash).then(|| Handle::new(hash))
     }
 }
 
@@ -515,74 +224,4 @@ impl Default for AssetRegistry {
     fn default() -> Self {
         Self::new()
     }
-}
-
-pub static ASSET_REGISTRY: LazyLock<AssetRegistry> = LazyLock::new(AssetRegistry::new);
-
-fn material_reference_from_model(model: &Model, name: &str) -> Option<ResourceReference> {
-    let base_uri = model.path.as_uri()?;
-    let material_component = sanitize_material_component(name);
-
-    if material_component.is_empty() {
-        return None;
-    }
-
-    let base = base_uri.trim_end_matches('/');
-    let combined = format!("{}/{}", base, material_component);
-
-    ResourceReference::from_euca_uri(combined).ok()
-}
-
-fn material_reference_fallback(model_id: ModelId, name: &str) -> Option<ResourceReference> {
-    resource_reference_for("materials", model_id, name)
-}
-
-fn mesh_reference(model_id: ModelId, name: &str) -> Option<ResourceReference> {
-    resource_reference_for("meshes", model_id, name)
-}
-
-fn resource_reference_for(
-    category: &str,
-    model_id: ModelId,
-    name: &str,
-) -> Option<ResourceReference> {
-    let sanitized = sanitize_component(name);
-    if sanitized.is_empty() {
-        return None;
-    }
-    let uri = format!("euca://{}/{}/{}", category, model_id.raw(), sanitized);
-    ResourceReference::from_euca_uri(uri).ok()
-}
-
-fn sanitize_material_component(input: &str) -> String {
-    let trimmed = input.trim();
-    if trimmed.is_empty() {
-        return String::new();
-    }
-
-    trimmed
-        .chars()
-        .map(|ch| match ch {
-            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch,
-            _ => '_',
-        })
-        .collect()
-}
-
-fn sanitize_component(input: &str) -> String {
-    let trimmed = input.trim();
-    if trimmed.is_empty() {
-        return String::new();
-    }
-
-    trimmed
-        .chars()
-        .map(|ch| {
-            let lower = ch.to_ascii_lowercase();
-            match lower {
-                'a'..='z' | '0'..='9' | '-' | '_' => lower,
-                _ => '_',
-            }
-        })
-        .collect()
-}
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 33c7115..99b534d 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -12,6 +12,7 @@ use glam::{DMat4, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
 use wgpu::{BindGroup};
+use crate::asset::ASSET_REGISTRY;
 
 pub const MAX_LIGHTS: usize = 10;
 
@@ -288,10 +289,11 @@ impl Light {
 
         log::trace!("Created new light uniform");
 
-        let cube_model = Model::load_from_memory(
+        let cube_model = Model::load_from_memory_raw(
             graphics.clone(),
             include_bytes!("../../../resources/models/cube.glb").to_vec(),
             label,
+            ASSET_REGISTRY.clone(),
             None
         )
         .await
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 93d69f7..e8f1c63 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -1,13 +1,12 @@
-use crate::asset::AssetRegistry;
+use crate::asset::{AssetRegistry, Handle};
 use crate::buffer::UniformBuffer;
 use crate::{
-    asset::{ASSET_REGISTRY, AssetHandle},
     graphics::{SharedGraphicsContext},
     utils::{ResourceReference},
     texture::{Texture, TextureWrapMode}
 };
 use image::GenericImageView;
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use rayon::prelude::*;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
@@ -16,164 +15,17 @@ use std::ops::Deref;
 use std::sync::{Arc, LazyLock};
 use std::time::Instant;
 use std::{mem, ops::Range, path::PathBuf};
-use glam::{Vec2, Vec3};
+use gltf::image::Format;
+use gltf::texture::MinFilter;
 use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup};
 
-pub static MODEL_CACHE: LazyLock<Mutex<HashMap<String, Arc<Model>>>> =
-    LazyLock::new(|| Mutex::new(HashMap::new()));
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct ModelId(pub u64);
-
-impl ModelId {
-    pub fn raw(&self) -> u64 {
-        self.0
-    }
-}
-
 #[derive(Clone)]
 pub struct Model {
     pub label: String,
+    pub(crate) hash: u64,
     pub path: ResourceReference,
     pub meshes: Vec<Mesh>,
     pub materials: Vec<Material>,
-    pub id: ModelId,
-}
-
-/// A shared, GPU-backed renderable shape.
-///
-/// This is intentionally a lightweight wrapper around an `Arc<Model>`.
-/// It exists so other systems can treat both model-loaded and procedurally
-/// generated geometry uniformly, while retaining clone-on-write semantics.
-#[derive(Clone)]
-pub struct SharedShape {
-    model: Arc<Model>,
-}
-
-impl SharedShape {
-    pub fn new(model: Arc<Model>) -> Self {
-        Self { model }
-    }
-
-    pub fn get(&self) -> Arc<Model> {
-        Arc::clone(&self.model)
-    }
-
-    pub fn make_mut(&mut self) -> &mut Model {
-        Arc::make_mut(&mut self.model)
-    }
-}
-
-impl Deref for SharedShape {
-    type Target = Model;
-
-    fn deref(&self) -> &Self::Target {
-        self.model.as_ref()
-    }
-}
-
-#[derive(Clone)]
-pub struct LoadedModel {
-    pub(crate) inner: Arc<SharedShape>,
-    handle: AssetHandle,
-}
-
-impl LoadedModel {
-    pub fn new(inner: Arc<Model>) -> Self {
-        Self::new_raw(&ASSET_REGISTRY, inner)
-    }
-
-    pub fn new_raw(registry: &AssetRegistry, inner: Arc<Model>) -> Self {
-        let reference = inner.path.clone();
-        let handle = registry.register_model(reference, Arc::clone(&inner));
-        let inner = Arc::new(SharedShape::new(inner));
-        Self { inner, handle }
-    }
-
-    pub fn from_registered(handle: AssetHandle, inner: Arc<Model>) -> Self {
-        let inner = Arc::new(SharedShape::new(inner));
-        Self { inner, handle }
-    }
-
-    pub fn from_asset_handle_raw(registry: &AssetRegistry, handle: AssetHandle) -> Option<Self> {
-        registry
-            .get_model(handle)
-            .map(|model| Self::from_registered(handle, model))
-    }
-
-    pub fn from_asset_handle(handle: AssetHandle) -> Option<Arc<LoadedModel>> {
-        Self::from_asset_handle_raw(&ASSET_REGISTRY, handle).map(|model| Arc::new(model))
-    }
-
-    /// Returns the unique identifier of the underlying model asset.
-    pub fn id(&self) -> ModelId {
-        self.inner.id
-    }
-
-    /// Returns the asset handle associated with the underlying model.
-    pub fn asset_handle(&self) -> AssetHandle {
-        self.handle
-    }
-
-    pub fn matches_resource(&self, reference: &ResourceReference) -> bool {
-        self.inner.matches_resource(reference)
-    }
-
-    /// Provides shared access to the underlying model.
-    pub fn get(&self) -> Arc<Model> {
-        self.inner.get()
-    }
-
-    /// Provides mutable access to the underlying model data, cloning if shared.
-    pub fn make_mut(&mut self) -> &mut Model {
-        let shape = Arc::make_mut(&mut self.inner);
-        shape.make_mut()
-    }
-
-    /// Re-registers the model with the global asset registry, ensuring cached
-    /// sub-assets stay in sync after mutations.
-    pub fn refresh_registry(&mut self) {
-        self.refresh_registry_raw(&ASSET_REGISTRY);
-    }
-
-    pub fn refresh_registry_raw(&mut self, registry: &AssetRegistry) {
-        let reference = self.inner.path.clone();
-        let updated_handle = registry.register_model(reference, self.get());
-        self.handle = updated_handle;
-    }
-
-    pub fn contains_material_handle(&self, handle: AssetHandle) -> bool {
-        self.contains_material_handle_raw(&ASSET_REGISTRY, handle)
-    }
-
-    pub fn contains_material_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        handle: AssetHandle,
-    ) -> bool {
-        self.inner.contains_material_handle_raw(registry, handle)
-    }
-
-    pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool {
-        self.contains_material_reference_raw(&ASSET_REGISTRY, reference)
-    }
-
-    pub fn contains_material_reference_raw(
-        &self,
-        registry: &AssetRegistry,
-        reference: &ResourceReference,
-    ) -> bool {
-        self.inner
-            .contains_material_reference_raw(registry, reference)
-    }
-}
-
-impl std::ops::Deref for LoadedModel {
-    type Target = Model;
-
-    fn deref(&self) -> &Self::Target {
-        self.inner.deref()
-    }
 }
 
 #[derive(Clone)]
@@ -183,11 +35,22 @@ pub struct Material {
     pub normal_texture: Texture,
     pub bind_group: wgpu::BindGroup,
     pub tint: [f32; 4],
+    pub emissive_factor: [f32; 3],
+    pub metallic_factor: f32,
+    pub roughness_factor: f32,
+    pub alpha_mode: gltf::material::AlphaMode,
+    pub alpha_cutoff: Option<f32>,
+    pub double_sided: bool,
+    pub occlusion_strength: f32,
+    pub normal_scale: f32,
     pub uv_tiling: [f32; 2],
     pub tint_buffer: UniformBuffer<MaterialUniform>,
     pub tint_bind_group: wgpu::BindGroup,
     pub texture_tag: Option<String>,
     pub wrap_mode: TextureWrapMode,
+    pub emissive_texture: Option<Texture>,
+    pub metallic_roughness_texture: Option<Texture>,
+    pub occlusion_texture: Option<Texture>,
 }
 
 impl Material {
@@ -196,27 +59,25 @@ impl Material {
         name: impl Into<String>,
         diffuse_texture: Texture,
         normal_texture: Texture,
-    ) -> Self {
-        Self::new_with_tint(graphics, name, diffuse_texture, normal_texture, [1.0, 1.0, 1.0, 1.0], None)
-    }
-
-    pub fn new_with_tint(
-        graphics: Arc<SharedGraphicsContext>,
-        name: impl Into<String>,
-        diffuse_texture: Texture,
-        normal_texture: Texture,
         tint: [f32; 4],
         texture_tag: Option<String>,
     ) -> Self {
-        let name: String = name.into();
+        let name = name.into();
 
         let uv_tiling = [1.0, 1.0];
         let uniform = MaterialUniform {
-            colour: tint,
+            base_colour: tint,
+            emissive: [0.0, 0.0, 0.0],
+            emissive_strength: 1.0,
+            metallic: 1.0,
+            roughness: 1.0,
+            normal_scale: 1.0,
+            occlusion_strength: 1.0,
+            alpha_cutoff: 0.5,
             uv_tiling,
-            _pad: [0.0, 0.0],
+            _pad: 0.0,
         };
-        
+
         let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
         tint_buffer.write(&graphics.queue, &uniform);
         let tint_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -236,11 +97,22 @@ impl Material {
             normal_texture,
             bind_group,
             tint,
+            emissive_factor: [0.0, 0.0, 0.0],
+            metallic_factor: 1.0,
+            roughness_factor: 1.0,
+            alpha_mode: gltf::material::AlphaMode::Opaque,
+            alpha_cutoff: None,
+            double_sided: false,
+            occlusion_strength: 1.0,
+            normal_scale: 1.0,
             uv_tiling,
             tint_buffer,
             tint_bind_group,
             texture_tag,
             wrap_mode: TextureWrapMode::Repeat,
+            emissive_texture: None,
+            metallic_roughness_texture: None,
+            occlusion_texture: None,
         }
     }
 
@@ -250,54 +122,46 @@ impl Material {
         normal: &Texture,
         name: &str,
     ) -> BindGroup {
-        graphics.
-                device
-                .create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some(format!("{} mipmapped compute bind group", name).as_str()),
-                    layout: &graphics.layouts.texture_bind_layout,
-                    entries: &[
-                        wgpu::BindGroupEntry {
-                            binding: 0,
-                            resource: wgpu::BindingResource::TextureView(
-                                &diffuse.view,
-                            ),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 1,
-                            resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 2,
-                            resource: wgpu::BindingResource::TextureView(&normal.view),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 3,
-                            resource: wgpu::BindingResource::Sampler(&normal.sampler),
-                        },
-                    ],
-                })
+        graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some(format!("{} texture bind group", name).as_str()),
+            layout: &graphics.layouts.texture_bind_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&diffuse.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: wgpu::BindingResource::TextureView(&normal.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 3,
+                    resource: wgpu::BindingResource::Sampler(&normal.sampler),
+                },
+            ],
+        })
     }
 
-    pub fn set_tint(&mut self, graphics: &SharedGraphicsContext, tint: [f32; 4]) {
-        self.tint = tint;
+    pub fn sync_uniform(&self, graphics: &SharedGraphicsContext) {
         let uniform = MaterialUniform {
-            colour: tint,
+            base_colour: self.tint,
+            emissive: self.emissive_factor,
+            emissive_strength: 1.0,
+            metallic: self.metallic_factor,
+            roughness: self.roughness_factor,
+            normal_scale: self.normal_scale,
+            occlusion_strength: self.occlusion_strength,
+            alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5),
             uv_tiling: self.uv_tiling,
-            _pad: [0.0, 0.0],
+            _pad: 0.0,
         };
 
         self.tint_buffer.write(&graphics.queue, &uniform);
     }
-
-    pub fn set_uv_tiling(&mut self, graphics: &SharedGraphicsContext, tiling: [f32; 2]) {
-        self.uv_tiling = tiling;
-        let uniform = MaterialUniform {
-            colour: self.tint,
-            uv_tiling: tiling,
-            _pad: [0.0, 0.0],
-        };
-        self.tint_buffer.write(&graphics.queue, &uniform);
-    }
 }
 
 #[derive(Clone)]
@@ -310,244 +174,512 @@ pub struct Mesh {
     pub vertices: Vec<ModelVertex>,
 }
 
+struct GLTFTextureInformation<'a> {
+    sampler: wgpu::SamplerDescriptor<'a>,
+    pixels: Vec<u8>,
+    mip_level_count: u32,
+    format: wgpu::TextureFormat,
+}
+
+struct GLTFMeshInformation {
+    name: String,
+    primitive_index: usize,
+    material_index: usize,
+    mode: gltf::mesh::Mode,
+    positions: Vec<[f32; 3]>,
+    indices: Vec<u32>,
+    normals: Vec<[f32; 3]>,
+    tangents: Vec<[f32; 4]>,
+    colors: Vec<[f32; 4]>,
+    joints: Vec<[u16; 4]>,
+    weights: Vec<[f32; 4]>,
+    tex_coords0: Vec<[f32; 2]>,
+    tex_coords1: Vec<[f32; 2]>,
+}
+
+struct GLTFMaterialInformation {
+    name: String,
+    diffuse_bytes: Option<Vec<u8>>,
+    normal_bytes: Option<Vec<u8>>,
+    emissive_bytes: Option<Vec<u8>>,
+    metallic_roughness_bytes: Option<Vec<u8>>,
+    occlusion_bytes: Option<Vec<u8>>,
+    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    tint: [f32; 4],
+    emissive_factor: [f32; 3],
+    metallic_factor: f32,
+    roughness_factor: f32,
+    alpha_mode: gltf::material::AlphaMode,
+    alpha_cutoff: Option<f32>,
+    double_sided: bool,
+    occlusion_strength: f32,
+    normal_scale: f32,
+}
+
+struct ProcessedMaterialTextures {
+    name: String,
+    diffuse: Option<(Vec<u8>, (u32, u32))>,
+    normal: Option<(Vec<u8>, (u32, u32))>,
+    emissive: Option<(Vec<u8>, (u32, u32))>,
+    metallic_roughness: Option<(Vec<u8>, (u32, u32))>,
+    occlusion: Option<(Vec<u8>, (u32, u32))>,
+    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    tint: [f32; 4],
+    emissive_factor: [f32; 3],
+    metallic_factor: f32,
+    roughness_factor: f32,
+    alpha_mode: gltf::material::AlphaMode,
+    alpha_cutoff: Option<f32>,
+    double_sided: bool,
+    occlusion_strength: f32,
+    normal_scale: f32,
+}
+
 impl Model {
-    /// Replaces the material textures (diffuse + normal) for the material identified by `material_name`.
-    ///
-    /// Note: `bind_group` must match the provided textures.
-    pub fn set_material_texture(
-        &mut self,
-        material_name: &str,
-        diffuse_texture: Texture,
-        normal_texture: Texture,
-        bind_group: wgpu::BindGroup,
-        texture_tag: Option<String>,
-    ) -> bool {
-        if let Some(material) = self
-            .materials
-            .iter_mut()
-            .find(|mat| mat.name == material_name)
-        {
-            material.diffuse_texture = diffuse_texture;
-            material.normal_texture = normal_texture;
-            material.bind_group = bind_group;
-            material.texture_tag = texture_tag;
-            true
-        } else {
-            false
+    fn sampler_descriptor_for_texture(texture: &gltf::Texture<'_>) -> wgpu::SamplerDescriptor<'static> {
+        let sampler = texture.sampler();
+
+        let mag_filter = match sampler.mag_filter() {
+            Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest,
+            _ => wgpu::FilterMode::Linear,
+        };
+
+        let (min_filter, mipmap_filter) = match sampler.min_filter() {
+            Some(MinFilter::Nearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
+            Some(MinFilter::Linear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
+            Some(MinFilter::NearestMipmapNearest) => {
+                (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest)
+            }
+            Some(MinFilter::LinearMipmapNearest) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
+            Some(MinFilter::NearestMipmapLinear) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear),
+            Some(MinFilter::LinearMipmapLinear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
+            None => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
+        };
+
+        fn map_wrap(wrap: gltf::texture::WrappingMode) -> wgpu::AddressMode {
+            match wrap {
+                gltf::texture::WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
+                gltf::texture::WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
+                gltf::texture::WrappingMode::Repeat => wgpu::AddressMode::Repeat,
+            }
         }
-    }
 
-    /// Removes any stored texture tag for the supplied material.
-    pub fn clear_material_texture_tag(&mut self, material_name: &str) -> bool {
-        if let Some(material) = self
-            .materials
-            .iter_mut()
-            .find(|mat| mat.name == material_name)
-        {
-            material.texture_tag = None;
-            true
-        } else {
-            false
+        wgpu::SamplerDescriptor {
+            label: None,
+            address_mode_u: map_wrap(sampler.wrap_s()),
+            address_mode_v: map_wrap(sampler.wrap_t()),
+            address_mode_w: wgpu::AddressMode::Repeat,
+            mag_filter,
+            min_filter,
+            mipmap_filter,
+            lod_min_clamp: 0.0,
+            lod_max_clamp: 32.0,
+            compare: None,
+            anisotropy_clamp: 1,
+            border_color: None,
         }
     }
 
-    /// Returns `true` if a material with `material_name` exists within this model.
-    pub fn contains_material(&self, material_name: &str) -> bool {
-        self.materials.iter().any(|mat| mat.name == material_name)
-    }
+    fn load_texture<'a>(tex: &'a gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation<'a> {
+        let sampler = tex.sampler();
 
-    /// Returns the registered asset handle for this model, if available.
-    pub fn asset_handle(&self) -> Option<AssetHandle> {
-        self.asset_handle_raw(&ASSET_REGISTRY)
-    }
+        let mag_filter = match sampler.mag_filter() {
+            Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest,
+            _ => wgpu::FilterMode::Linear,
+        };
 
-    pub fn asset_handle_raw(&self, registry: &AssetRegistry) -> Option<AssetHandle> {
-        registry.model_handle_from_reference(&self.path)
-    }
+        let (min_filter, mipmap_filter) = match sampler.min_filter() {
+            Some(MinFilter::Nearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
+            Some(MinFilter::Linear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
+            Some(MinFilter::NearestMipmapNearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
+            Some(MinFilter::LinearMipmapNearest) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
+            Some(MinFilter::NearestMipmapLinear) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear),
+            Some(MinFilter::LinearMipmapLinear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
+            None => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
+        };
 
-    /// Returns `true` if this model was loaded from the specified resource reference.
-    pub fn matches_resource(&self, reference: &ResourceReference) -> bool {
-        &self.path == reference
-    }
+        fn map_wrap(wrap: gltf::texture::WrappingMode) -> wgpu::AddressMode {
+            match wrap {
+                gltf::texture::WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
+                gltf::texture::WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
+                gltf::texture::WrappingMode::Repeat => wgpu::AddressMode::Repeat,
+            }
+        }
 
-    /// Returns `true` if this model owns the supplied material handle.
-    pub fn contains_material_handle(&self, material_handle: AssetHandle) -> bool {
-        self.contains_material_handle_raw(&ASSET_REGISTRY, material_handle)
-    }
+        let sampler = wgpu::SamplerDescriptor {
+            label: Some(tex.name().unwrap_or("Unnamed Texture Sampler")),
+            address_mode_u: map_wrap(sampler.wrap_s()),
+            address_mode_v: map_wrap(sampler.wrap_t()),
+            address_mode_w: wgpu::AddressMode::Repeat,
+            mag_filter,
+            min_filter,
+            mipmap_filter,
+            lod_min_clamp: 0.0,
+            lod_max_clamp: 32.0,
+            compare: None,
+            anisotropy_clamp: 1,
+            border_color: None,
+        };
 
-    pub fn contains_material_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        material_handle: AssetHandle,
-    ) -> bool {
-        registry.material_owner(material_handle) == Some(self.id)
-    }
+        let image_index = tex.source().index();
+        let image_data = &images[image_index];
 
-    /// Returns `true` if this model owns a material registered under the provided resource reference.
-    pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool {
-        self.contains_material_reference_raw(&ASSET_REGISTRY, reference)
-    }
+        let width = image_data.width;
+        let height = image_data.height;
 
-    pub fn contains_material_reference_raw(
-        &self,
-        registry: &AssetRegistry,
-        reference: &ResourceReference,
-    ) -> bool {
-        registry
-            .material_handle_from_reference(reference)
-            .map_or(false, |handle| {
-                self.contains_material_handle_raw(registry, handle)
-            })
-    }
+        let mip_level_count = (width.max(height) as f32).log2().floor() as u32 + 1;
+
+        let (pixels, format) = match image_data.format {
+            Format::R8 => (image_data.pixels.clone(), wgpu::TextureFormat::R8Unorm),
+            Format::R8G8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rg8Unorm),
+            Format::R8G8B8 => {
+                let mut rgba = Vec::with_capacity(image_data.pixels.len() / 3 * 4);
+                for chunk in image_data.pixels.chunks(3) {
+                    rgba.extend_from_slice(chunk);
+                    rgba.push(255);
+                }
+                (rgba, wgpu::TextureFormat::Rgba8Unorm)
+            },
+            Format::R8G8B8A8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rgba8Unorm),
+            _ => panic!("Unsupported format"),
+        };
 
-    /// Returns `true` if any material on this model is tagged with `texture_tag`.
-    pub fn contains_texture_tag(&self, texture_tag: &str) -> bool {
-        self.materials
-            .iter()
-            .any(|mat| mat.texture_tag.as_deref() == Some(texture_tag))
+        GLTFTextureInformation {
+            sampler,
+            mip_level_count,
+            pixels,
+            format,
+        }
     }
 
-    /// Returns `true` if the specified material currently carries `texture_tag`.
-    pub fn material_has_texture_tag(&self, material_name: &str, texture_tag: &str) -> bool {
-        self.materials
-            .iter()
-            .find(|mat| mat.name == material_name)
-            .and_then(|mat| mat.texture_tag.as_deref())
-            == Some(texture_tag)
+    fn load_materials(
+        gltf: &gltf::Document,
+        buffers: &Vec<gltf::buffer::Data>,
+    ) -> Vec<GLTFMaterialInformation> {
+        let read_texture_bytes = |texture: gltf::Texture<'_>| -> Option<Vec<u8>> {
+            let image = texture.source();
+            match image.source() {
+                gltf::image::Source::View { view, mime_type: _ } => {
+                    let buffer_data = &buffers[view.buffer().index()];
+                    let start = view.offset();
+                    let end = start + view.length();
+                    Some(buffer_data[start..end].to_vec())
+                }
+                gltf::image::Source::Uri { uri, mime_type: _ } => {
+                    log::warn!("External URI textures not supported: {}", uri);
+                    None
+                }
+            }
+        };
+
+        let mut material_data = Vec::new();
+
+        for material in gltf.materials() {
+            log::debug!("Processing material: {:?}", material.name());
+            let material_name = material.name().unwrap_or("Unnamed Material").to_string();
+
+            let tint = material.pbr_metallic_roughness().base_color_factor();
+            let tint = [tint[0], tint[1], tint[2], tint[3]];
+
+            let pbr = material.pbr_metallic_roughness();
+            let diffuse_texture = pbr.base_color_texture();
+            let metallic_roughness_texture = pbr.metallic_roughness_texture();
+            let normal_texture = material.normal_texture();
+            let occlusion_texture = material.occlusion_texture();
+            let emissive_texture = material.emissive_texture();
+
+            let diffuse_bytes = diffuse_texture
+                .as_ref()
+                .and_then(|info| read_texture_bytes(info.texture()));
+            let metallic_roughness_bytes = metallic_roughness_texture
+                .as_ref()
+                .and_then(|info| read_texture_bytes(info.texture()));
+
+            let normal_bytes = normal_texture
+                .as_ref()
+                .and_then(|info| read_texture_bytes(info.texture()));
+            let occlusion_bytes = occlusion_texture
+                .as_ref()
+                .and_then(|info| read_texture_bytes(info.texture()));
+            let emissive_bytes = emissive_texture
+                .as_ref()
+                .and_then(|info| read_texture_bytes(info.texture()));
+
+            let diffuse_sampler = diffuse_texture
+                .as_ref()
+                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
+            let metallic_roughness_sampler = metallic_roughness_texture
+                .as_ref()
+                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
+            let normal_sampler = normal_texture
+                .as_ref()
+                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
+            let occlusion_sampler = occlusion_texture
+                .as_ref()
+                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
+            let emissive_sampler = emissive_texture
+                .as_ref()
+                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
+
+            let emissive_factor = material.emissive_factor();
+            let emissive_factor = [
+                emissive_factor[0],
+                emissive_factor[1],
+                emissive_factor[2],
+            ];
+            let metallic_factor = pbr.metallic_factor();
+            let roughness_factor = pbr.roughness_factor();
+            let alpha_mode = material.alpha_mode();
+            let alpha_cutoff = material.alpha_cutoff();
+            let double_sided = material.double_sided();
+            let occlusion_strength = occlusion_texture
+                .as_ref()
+                .map(|info| info.strength())
+                .unwrap_or(1.0);
+            let normal_scale = normal_texture
+                .as_ref()
+                .map(|info| info.scale())
+                .unwrap_or(1.0);
+
+            material_data.push(GLTFMaterialInformation {
+                name: material_name,
+                diffuse_bytes,
+                normal_bytes,
+                emissive_bytes,
+                metallic_roughness_bytes,
+                occlusion_bytes,
+                diffuse_sampler,
+                normal_sampler,
+                emissive_sampler,
+                metallic_roughness_sampler,
+                occlusion_sampler,
+                tint,
+                emissive_factor,
+                metallic_factor,
+                roughness_factor,
+                alpha_mode,
+                alpha_cutoff,
+                double_sided,
+                occlusion_strength,
+                normal_scale,
+            });
+        }
+
+        if material_data.is_empty() {
+            material_data.push(GLTFMaterialInformation {
+                name: "Default".to_string(),
+                diffuse_bytes: None,
+                normal_bytes: None,
+                emissive_bytes: None,
+                metallic_roughness_bytes: None,
+                occlusion_bytes: None,
+                diffuse_sampler: None,
+                normal_sampler: None,
+                emissive_sampler: None,
+                metallic_roughness_sampler: None,
+                occlusion_sampler: None,
+                tint: [1.0, 1.0, 1.0, 1.0],
+                emissive_factor: [0.0, 0.0, 0.0],
+                metallic_factor: 1.0,
+                roughness_factor: 1.0,
+                alpha_mode: gltf::material::AlphaMode::Opaque,
+                alpha_cutoff: None,
+                double_sided: false,
+                occlusion_strength: 1.0,
+                normal_scale: 1.0,
+            });
+        }
+
+        material_data
     }
 
-    pub async fn load_from_memory<B>(
-        graphics: Arc<SharedGraphicsContext>,
-        buffer: B,
-        label: Option<&str>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel>
-    where
-        B: AsRef<[u8]>,
-    {
-        Self::load_from_memory_raw(
-            graphics,
-            buffer,
-            label,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-            import_scale
-        )
-        .await
+    fn load_meshes(
+        mesh: &gltf::Mesh,
+        buffers: &Vec<gltf::buffer::Data>,
+        mesh_collector: &mut Vec<GLTFMeshInformation>,
+    ) -> anyhow::Result<()> {
+        let mesh_name = mesh.name().unwrap_or("Unnamed Mesh").to_string();
+
+        for (primitive_index, primitive) in mesh.primitives().enumerate() {
+            let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
+
+            let positions: Vec<[f32; 3]> = reader
+                .read_positions()
+                .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))?
+                .collect();
+
+            let indices: Vec<u32> = reader
+                .read_indices()
+                .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))?
+                .into_u32()
+                .collect();
+
+            let normals: Vec<[f32; 3]> = reader
+                .read_normals()
+                .map(|iter| iter.collect())
+                .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
+
+            let tangents: Vec<[f32; 4]> = reader
+                .read_tangents()
+                .map(|iter| iter.collect())
+                .unwrap_or_else(|| vec![[0.0, 0.0, 0.0, 1.0]; positions.len()]);
+
+            let colors: Vec<[f32; 4]> = reader
+                .read_colors(0)
+                .map(|iter| iter.into_rgba_f32().collect())
+                .unwrap_or_else(|| vec![[1.0; 4]; positions.len()]);
+
+            let joints: Vec<[u16; 4]> = reader
+                .read_joints(0)
+                .map(|iter| iter.into_u16().collect())
+                .unwrap_or_else(|| vec![[0u16; 4]; positions.len()]);
+
+            let mut weights: Vec<[f32; 4]> = reader
+                .read_weights(0)
+                .map(|iter| iter.into_f32().collect())
+                .unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 0.0]; positions.len()]);
+
+            for weight in &mut weights {
+                let sum = weight[0] + weight[1] + weight[2] + weight[3];
+                if sum > 0.0 {
+                    weight[0] /= sum;
+                    weight[1] /= sum;
+                    weight[2] /= sum;
+                    weight[3] /= sum;
+                }
+            }
+
+            let tex_coords: Vec<[f32; 2]> = reader
+                .read_tex_coords(0)
+                .map(|iter| iter.into_f32().collect())
+                .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
+
+            let tex_coords1: Vec<[f32; 2]> = reader
+                .read_tex_coords(1)
+                .map(|iter| iter.into_f32().collect())
+                .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
+
+            let expected_len = positions.len();
+            let check_len = |label: &str, len: usize| -> anyhow::Result<()> {
+                if len == expected_len {
+                    Ok(())
+                } else {
+                    Err(anyhow::anyhow!(
+                        "Mesh attribute length mismatch for {}: expected {}, got {}",
+                        label,
+                        expected_len,
+                        len
+                    ))
+                }
+            };
+
+            check_len("normals", normals.len())?;
+            check_len("tangents", tangents.len())?;
+            check_len("colors", colors.len())?;
+            check_len("joints", joints.len())?;
+            check_len("weights", weights.len())?;
+            check_len("tex_coords0", tex_coords.len())?;
+            check_len("tex_coords1", tex_coords1.len())?;
+
+            mesh_collector.push(GLTFMeshInformation {
+                name: mesh_name.clone(),
+                primitive_index,
+                material_index: primitive.material().index().unwrap_or(0),
+                mode: primitive.mode(),
+                positions,
+                indices,
+                normals,
+                tangents,
+                colors,
+                joints,
+                weights,
+                tex_coords0: tex_coords,
+                tex_coords1,
+            });
+        }
+
+        Ok(())
     }
 
     pub async fn load_from_memory_raw<B>(
         graphics: Arc<SharedGraphicsContext>,
         buffer: B,
         label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
+        registry: Arc<RwLock<AssetRegistry>>,
         import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel>
+    ) -> anyhow::Result<Handle<Model>>
     where
         B: AsRef<[u8]>,
     {
-        let start = Instant::now();
-        let mut hasher = DefaultHasher::new();
-
-        let scale_key = import_scale.unwrap_or(1.0);
-        let cache_key = format!(
-            "{}::import_scale={:.8}",
-            label.unwrap_or("default"),
-            scale_key
-        );
+        let mut registry = registry.write();
 
-        if let Some(cached_model) = {
-            let cache_guard = cache.lock();
-            cache_guard.get(&cache_key).cloned()
-        } {
-            log::debug!("Model loaded from memory cache: {:?}", cache_key);
-            return Ok(LoadedModel::new_raw(registry, cached_model));
-        }
+        let model_label = label.unwrap_or("No named model");
+        let hash = if let Some(label) = label {
+            let mut hasher = DefaultHasher::default();
+            label.hash(&mut hasher);
+            hasher.finish()
+        } else {
+            let mut hasher = DefaultHasher::default();
+            buffer.as_ref().hash(&mut hasher);
+            hasher.finish()
+        };
 
-        log::trace!(
-            "========== Benchmarking speed of loading {:?} ==========",
-            label
-        );
-        log::debug!("Loading from memory");
-        let res_ref = ResourceReference::from_bytes(buffer.as_ref());
+        if let Some(model) = registry.model_handle_by_hash(hash) {
+            return Ok(model);
+        }
 
         let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?;
-        let mut meshes = Vec::new();
-
-        let mut texture_data: Vec<(String, Option<Vec<u8>>, Option<Vec<u8>>, [f32; 4])> = Vec::new();
-        for material in gltf.materials() {
-            log::debug!("Processing material: {:?}", material.name());
-            let material_name = material.name().unwrap_or("Unnamed Material").to_string();
-
-            let tint = material
-                .pbr_metallic_roughness()
-                .base_color_factor();
-
-            let tint = [tint[0], tint[1], tint[2], tint[3]];
-
-            let diffuse_bytes = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() {
-                let texture_info = pbr.texture();
-                let image = texture_info.source();
-                match image.source() {
-                    gltf::image::Source::View { view, mime_type: _ } => {
-                        let buffer_data = &buffers[view.buffer().index()];
-                        let start = view.offset();
-                        let end = start + view.length();
-                        Some(buffer_data[start..end].to_vec())
-                    }
-                    gltf::image::Source::Uri { uri, mime_type: _ } => {
-                        log::warn!("External URI textures not supported: {}", uri);
-                        None
-                    }
-                }
-            } else {
-                None
-            };
-
-            let normal_bytes = if let Some(info) = material.normal_texture() {
-                let image = info.texture().source();
-                match image.source() {
-                    gltf::image::Source::View { view, mime_type: _ } => {
-                        let buffer_data = &buffers[view.buffer().index()];
-                        let start = view.offset();
-                        let end = start + view.length();
-                        Some(buffer_data[start..end].to_vec())
-                    }
-                    gltf::image::Source::Uri { uri, mime_type: _ } => {
-                        log::warn!("External URI textures not supported: {}", uri);
-                        None
-                    }
-                }
-            } else {
-                None
-            };
 
-            texture_data.push((material_name, diffuse_bytes, normal_bytes, tint));
+        let mut meshes = Vec::new();
+        for mesh in gltf.meshes() {
+            Self::load_meshes(&mesh, &buffers, &mut meshes)?;
         }
 
-        if texture_data.is_empty() {
-            texture_data.push((
-                "Default".to_string(),
-                None,
-                None,
-                [1.0, 1.0, 1.0, 1.0],
-            ));
-        }
+        let material_data = Self::load_materials(&gltf, &buffers);
 
         let parallel_start = Instant::now();
-        let processed_textures: Vec<_> = texture_data
+        let processed_textures: Vec<ProcessedMaterialTextures> = material_data
             .into_par_iter()
-            .map(|(material_name, diffuse_bytes, normal_bytes, tint)| {
+            .map(|material_info| {
                 let material_start = Instant::now();
 
-                let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| {
+                let material_name = material_info.name;
+                let diffuse_bytes = material_info.diffuse_bytes;
+                let normal_bytes = material_info.normal_bytes;
+                let emissive_bytes = material_info.emissive_bytes;
+                let metallic_roughness_bytes = material_info.metallic_roughness_bytes;
+                let occlusion_bytes = material_info.occlusion_bytes;
+                let diffuse_sampler = material_info.diffuse_sampler;
+                let normal_sampler = material_info.normal_sampler;
+                let emissive_sampler = material_info.emissive_sampler;
+                let metallic_roughness_sampler = material_info.metallic_roughness_sampler;
+                let occlusion_sampler = material_info.occlusion_sampler;
+                let tint = material_info.tint;
+                let emissive_factor = material_info.emissive_factor;
+                let metallic_factor = material_info.metallic_factor;
+                let roughness_factor = material_info.roughness_factor;
+                let alpha_mode = material_info.alpha_mode;
+                let alpha_cutoff = material_info.alpha_cutoff;
+                let double_sided = material_info.double_sided;
+                let occlusion_strength = material_info.occlusion_strength;
+                let normal_scale = material_info.normal_scale;
+
+                let decode_rgba = |bytes: &Vec<u8>, label: &str| {
                     let load_start = Instant::now();
                     let image = match image::load_from_memory(bytes) {
                         Ok(image) => image,
                         Err(err) => {
                             log::warn!(
-                                "Failed to decode diffuse texture for material '{}': {} ({} bytes)",
+                                "Failed to decode {} texture for material '{}': {} ({} bytes)",
+                                label,
                                 material_name,
                                 err,
                                 bytes.len()
@@ -555,44 +687,35 @@ impl Model {
                             return None;
                         }
                     };
-                    log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed());
+                    log::trace!("Loading {} image to memory: {:?}", label, load_start.elapsed());
 
                     let rgba_start = Instant::now();
                     let rgba = image.to_rgba8();
                     log::trace!(
-                        "Converting diffuse image to rgba8 took {:?}",
+                        "Converting {} image to rgba8 took {:?}",
+                        label,
                         rgba_start.elapsed()
                     );
 
                     let dimensions = image.dimensions();
                     Some((rgba.into_raw(), dimensions))
-                });
+                };
 
+                let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| {
+                    decode_rgba(bytes, "diffuse")
+                });
                 let processed_normal = normal_bytes.as_ref().and_then(|bytes| {
-                    let load_start = Instant::now();
-                    let image = match image::load_from_memory(bytes) {
-                        Ok(image) => image,
-                        Err(err) => {
-                            log::warn!(
-                                "Failed to decode normal texture for material '{}': {} ({} bytes)",
-                                material_name,
-                                err,
-                                bytes.len()
-                            );
-                            return None;
-                        }
-                    };
-                    log::trace!("Loading normal image to memory: {:?}", load_start.elapsed());
-
-                    let rgba_start = Instant::now();
-                    let rgba = image.to_rgba8();
-                    log::trace!(
-                        "Converting normal image to rgba8 took {:?}",
-                        rgba_start.elapsed()
-                    );
-
-                    let dimensions = image.dimensions();
-                    Some((rgba.into_raw(), dimensions))
+                    decode_rgba(bytes, "normal")
+                });
+                let processed_emissive = emissive_bytes.as_ref().and_then(|bytes| {
+                    decode_rgba(bytes, "emissive")
+                });
+                let processed_metallic_roughness =
+                    metallic_roughness_bytes.as_ref().and_then(|bytes| {
+                        decode_rgba(bytes, "metallic_roughness")
+                    });
+                let processed_occlusion = occlusion_bytes.as_ref().and_then(|bytes| {
+                    decode_rgba(bytes, "occlusion")
                 });
 
                 log::trace!(
@@ -601,7 +724,28 @@ impl Model {
                     material_start.elapsed()
                 );
 
-                (material_name, processed_diffuse, processed_normal, tint)
+                ProcessedMaterialTextures {
+                    name: material_name,
+                    diffuse: processed_diffuse,
+                    normal: processed_normal,
+                    emissive: processed_emissive,
+                    metallic_roughness: processed_metallic_roughness,
+                    occlusion: processed_occlusion,
+                    diffuse_sampler,
+                    normal_sampler,
+                    emissive_sampler,
+                    metallic_roughness_sampler,
+                    occlusion_sampler,
+                    tint,
+                    emissive_factor,
+                    metallic_factor,
+                    roughness_factor,
+                    alpha_mode,
+                    alpha_cutoff,
+                    double_sided,
+                    occlusion_strength,
+                    normal_scale,
+                }
             })
             .collect();
 
@@ -616,20 +760,34 @@ impl Model {
         let flat_normal_texture =
             registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
 
-        for (material_name, processed_diffuse, processed_normal, tint) in processed_textures {
+        for processed in processed_textures {
             let start = Instant::now();
 
+            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_sampler = processed.diffuse_sampler;
+            let normal_sampler = processed.normal_sampler;
+            let emissive_sampler = processed.emissive_sampler;
+            let metallic_roughness_sampler = processed.metallic_roughness_sampler;
+            let occlusion_sampler = processed.occlusion_sampler;
+
             let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse {
                 Texture::from_bytes_verbose_mipmapped(
                     graphics.clone(),
                     &rgba_data,
                     Some(dimensions),
                     None,
-                    None,
+                    diffuse_sampler.clone(),
                     Some(material_name.as_str())
                 )
+            } else if let Some(grey) = registry.get_texture(grey_texture) {
+                (*grey).clone()
             } else {
-                (*grey_texture).clone()
+                anyhow::bail!("Unable to find processed diffuse or fetch fallback texture for model {:?}", label);
             };
 
             let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal {
@@ -638,269 +796,155 @@ impl Model {
                     &rgba_data,
                     Some(dimensions),
                     None,
-                    None,
+                    normal_sampler.clone(),
                     Some(material_name.as_str())
                 )
+            } else if let Some(tex) = registry.get_texture(flat_normal_texture) {
+                (*tex).clone()
             } else {
-                (*flat_normal_texture).clone()
+                anyhow::bail!("Unable to find processed normal or fetch fallback texture for model {:?}", label);
             };
+
+            let emissive_texture = processed_emissive.map(|(rgba_data, dimensions)| {
+                Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    emissive_sampler.clone(),
+                    Some(material_name.as_str())
+                )
+            });
+            let metallic_roughness_texture =
+                processed_metallic_roughness.map(|(rgba_data, dimensions)| {
+                    Texture::from_bytes_verbose_mipmapped(
+                        graphics.clone(),
+                        &rgba_data,
+                        Some(dimensions),
+                        None,
+                        metallic_roughness_sampler.clone(),
+                        Some(material_name.as_str())
+                    )
+                });
+            let occlusion_texture = processed_occlusion.map(|(rgba_data, dimensions)| {
+                Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    occlusion_sampler.clone(),
+                    Some(material_name.as_str())
+                )
+            });
             let texture_tag = Some(material_name.clone());
 
-            materials.push(Material::new_with_tint(
+            let mut material = Material::new(
                 graphics.clone(),
                 material_name,
                 diffuse_texture,
                 normal_texture,
-                tint,
+                processed.tint,
                 texture_tag,
-            ));
+            );
+
+            material.emissive_factor = processed.emissive_factor;
+            material.metallic_factor = processed.metallic_factor;
+            material.roughness_factor = processed.roughness_factor;
+            material.alpha_mode = processed.alpha_mode;
+            material.alpha_cutoff = processed.alpha_cutoff;
+            material.double_sided = processed.double_sided;
+            material.occlusion_strength = processed.occlusion_strength;
+            material.normal_scale = processed.normal_scale;
+            material.emissive_texture = emissive_texture;
+            material.metallic_roughness_texture = metallic_roughness_texture;
+            material.occlusion_texture = occlusion_texture;
+            material.sync_uniform(&graphics);
+
+            materials.push(material);
 
             log::trace!("Time to create GPU texture: {:?}", start.elapsed());
         }
 
-        for mesh in gltf.meshes() {
-            log::debug!("Processing mesh: {:?}", mesh.name());
-            for primitive in mesh.primitives() {
-                let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
-
-                let positions: Vec<[f32; 3]> = reader
-                    .read_positions()
-                    .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))?
-                    .collect();
-
-                let normals: Vec<[f32; 3]> = reader
-                    .read_normals()
-                    .map(|iter| iter.collect())
-                    .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
-
-                let tex_coords: Vec<[f32; 2]> = reader
-                    .read_tex_coords(0)
-                    .map(|iter| iter.into_f32().collect())
-                    .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
-
-                let mut vertices: Vec<ModelVertex> = positions
-                    .iter()
-                    .zip(normals.iter())
-                    .zip(tex_coords.iter())
-                    .map(|((pos, norm), tex)| ModelVertex {
-                        position: *pos,
-                        normal: *norm,
-                        tex_coords: *tex,
-                        tangent: [0.0; 3],
-                        bitangent: [0.0; 3],
-                    })
-                    .collect();
-
-                // Apply import scaling at load time so downstream systems (bounds, physics, etc.)
-                // see the baked geometry rather than a render-time transform hack.
-                let scale = import_scale.unwrap_or(1.0) as f32;
-                if (scale - 1.0).abs() > f32::EPSILON {
-                    for vertex in &mut vertices {
-                        vertex.position[0] *= scale;
-                        vertex.position[1] *= scale;
-                        vertex.position[2] *= scale;
-                    }
-                }
-
-                for v in &vertices {
-                    let _ = v.position.iter().map(|v| (*v as i32).hash(&mut hasher));
-                    let _ = v.normal.iter().map(|v| (*v as i32).hash(&mut hasher));
-                    let _ = v.tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher));
-                }
+        let mut gpu_meshes = Vec::new();
+        for mesh_info in meshes {
+            if mesh_info.mode != gltf::mesh::Mode::Triangles {
+                return Err(anyhow::anyhow!(
+                    "Unsupported primitive mode {:?} for mesh '{}' (primitive {})",
+                    mesh_info.mode,
+                    mesh_info.name,
+                    mesh_info.primitive_index
+                ));
+            }
 
-                let indices: Vec<u32> = reader
-                    .read_indices()
-                    .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))?
-                    .into_u32()
-                    .collect();
-                indices.hash(&mut hasher);
-
-                let mut triangles_included = vec![0; vertices.len()];
-                for c in indices.chunks(3) {
-                    let v0 = vertices[c[0] as usize];
-                    let v1 = vertices[c[1] as usize];
-                    let v2 = vertices[c[2] as usize];
-
-                    let pos0: Vec3 = v0.position.into();
-                    let pos1: Vec3 = v1.position.into();
-                    let pos2: Vec3 = v2.position.into();
-
-                    let uv0: Vec2 = v0.tex_coords.into();
-                    let uv1: Vec2 = v1.tex_coords.into();
-                    let uv2: Vec2 = v2.tex_coords.into();
-
-                    // Calculate the edges of the triangle
-                    let delta_pos1 = pos1 - pos0;
-                    let delta_pos2 = pos2 - pos0;
-
-                    // This will give us a direction to calculate the
-                    // tangent and bitangent
-                    let delta_uv1 = uv1 - uv0;
-                    let delta_uv2 = uv2 - uv0;
-
-                    // Solving the following system of equations will
-                    // give us the tangent and bitangent.
-                    //     delta_pos1 = delta_uv1.x * T + delta_u.y * B
-                    //     delta_pos2 = delta_uv2.x * T + delta_uv2.y * B
-                    // Luckily, the place I found this equation provided
-                    // the solution!
-                    let r = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x);
-                    let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r;
-                    // We flip the bitangent to enable right-handed normal
-                    // maps with wgpu texture coordinate system
-                    let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * -r;
-
-                    // We'll use the same tangent/bitangent for each vertex in the triangle
-                    vertices[c[0] as usize].tangent =
-                        (tangent + Vec3::from(vertices[c[0] as usize].tangent)).into();
-                    vertices[c[1] as usize].tangent =
-                        (tangent + Vec3::from(vertices[c[1] as usize].tangent)).into();
-                    vertices[c[2] as usize].tangent =
-                        (tangent + Vec3::from(vertices[c[2] as usize].tangent)).into();
-                    vertices[c[0] as usize].bitangent =
-                        (bitangent + Vec3::from(vertices[c[0] as usize].bitangent)).into();
-                    vertices[c[1] as usize].bitangent =
-                        (bitangent + Vec3::from(vertices[c[1] as usize].bitangent)).into();
-                    vertices[c[2] as usize].bitangent =
-                        (bitangent + Vec3::from(vertices[c[2] as usize].bitangent)).into();
-
-                    // Used to average the tangents/bitangents
-                    triangles_included[c[0] as usize] += 1;
-                    triangles_included[c[1] as usize] += 1;
-                    triangles_included[c[2] as usize] += 1;
-                }
+            let mut vertices = Vec::with_capacity(mesh_info.positions.len());
+            for index in 0..mesh_info.positions.len() {
+                vertices.push(ModelVertex {
+                    position: mesh_info.positions[index],
+                    normal: mesh_info.normals[index],
+                    tangent: mesh_info.tangents[index],
+                    tex_coords0: mesh_info.tex_coords0[index],
+                    tex_coords1: mesh_info.tex_coords1[index],
+                    colour0: mesh_info.colors[index],
+                    joints0: mesh_info.joints[index],
+                    weights0: mesh_info.weights[index],
+                });
+            }
 
-                for (i, n) in triangles_included.into_iter().enumerate() {
-                    let denom = 1.0 / n as f32;
-                    let v = &mut vertices[i];
-                    v.tangent = (Vec3::from(v.tangent) * denom).into();
-                    v.bitangent = (Vec3::from(v.bitangent) * denom).into();
+            let scale = import_scale.unwrap_or(1.0) as f32;
+            if (scale - 1.0).abs() > f32::EPSILON {
+                for vertex in &mut vertices {
+                    vertex.position[0] *= scale;
+                    vertex.position[1] *= scale;
+                    vertex.position[2] *= scale;
                 }
-
-                let vertex_buffer =
-                    graphics
-                        .device
-                        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                            label: Some(&format!("{:?} Vertex Buffer", label)),
-                            contents: bytemuck::cast_slice(&vertices),
-                            usage: wgpu::BufferUsages::VERTEX,
-                        });
-
-                let index_buffer =
-                    graphics
-                        .device
-                        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                            label: Some(&format!("{:?} Index Buffer", label)),
-                            contents: bytemuck::cast_slice(&indices),
-                            usage: wgpu::BufferUsages::INDEX,
-                        });
-
-                let material_index = primitive.material().index().unwrap_or(0);
-
-                meshes.push(Mesh {
-                    name: mesh.name().unwrap_or("Unnamed Mesh").to_string(),
-                    vertex_buffer,
-                    index_buffer,
-                    vertices,
-                    num_elements: indices.len() as u32,
-                    material: material_index,
-                });
             }
+
+            let vertex_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some(&format!("{} Vertex Buffer", model_label)),
+                        contents: bytemuck::cast_slice(&vertices),
+                        usage: wgpu::BufferUsages::VERTEX,
+                    });
+
+            let index_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some(&format!("{} Index Buffer", model_label)),
+                        contents: bytemuck::cast_slice(&mesh_info.indices),
+                        usage: wgpu::BufferUsages::INDEX,
+                    });
+
+            gpu_meshes.push(Mesh {
+                name: mesh_info.name,
+                vertex_buffer,
+                index_buffer,
+                vertices,
+                num_elements: mesh_info.indices.len() as u32,
+                material: mesh_info.material_index,
+            });
         }
 
         log::debug!("Successfully loaded model [{:?}]", label);
 
-        let model = Arc::new(Model {
-            meshes,
+        let model = Model {
+            label: model_label.to_string(),
+            hash,
+            path: ResourceReference::from_bytes(buffer.as_ref()),
+            meshes: gpu_meshes,
             materials,
-            label: label.unwrap_or("No named model").to_string(),
-            path: res_ref,
-            id: ModelId(hasher.finish()),
-        });
-
-        let loaded = LoadedModel::new_raw(registry, Arc::clone(&model));
-
-        {
-            let mut cache_guard = cache.lock();
-            cache_guard.insert(cache_key.clone(), model);
-        }
-        log::trace!("==================== DONE ====================");
-        log::debug!("Model cached from memory: {:?}", label);
-        log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label);
-        log::trace!("==============================================");
-        Ok(loaded)
-    }
-
-    pub async fn load(
-        graphics: Arc<SharedGraphicsContext>,
-        path: &PathBuf,
-        label: Option<&str>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel> {
-        Self::load_raw(
-            graphics,
-            path,
-            label,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-            import_scale
-        )
-        .await
-    }
-
-    pub async fn load_raw(
-        graphics: Arc<SharedGraphicsContext>,
-        path: &PathBuf,
-        label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel> {
-        let file_name = path.file_name();
-        log::debug!("Loading model [{:?}]", file_name);
-
-        let scale_key = import_scale.unwrap_or(1.0);
-        let path_str = format!("{}::import_scale={:.8}", path.to_string_lossy(), scale_key);
-
-        log::debug!("Checking if model exists in cache");
-        if let Some(cached_model) = {
-            let cache_guard = cache.lock();
-            cache_guard.get(&path_str).cloned()
-        } {
-            log::debug!("Model loaded from cache: {:?}", path_str);
-            return Ok(LoadedModel::new_raw(registry, cached_model));
-        }
-        log::debug!("Model does not exist in cache, loading memory...");
-
-        log::debug!("Path of model: {}", path.display());
-
-        let buffer = std::fs::read(path)?;
-        let loaded =
-            Self::load_from_memory_raw(graphics, buffer, label, registry, cache, import_scale)
-                .await?;
-
-        let mut model_clone: Model = (*loaded).clone();
-        if let Ok(reference) = ResourceReference::from_path(path) {
-            model_clone.path = reference;
-        }
-        if let Some(custom_label) = label {
-            model_clone.label = custom_label.to_string();
-        }
+        };
 
-        let updated = Arc::new(model_clone);
-        {
-            let mut cache_guard = cache.lock();
-            cache_guard.insert(path_str.clone(), Arc::clone(&updated));
-            if let Some(custom_label) = label {
-                let label_key = format!("{}::import_scale={:.8}", custom_label, scale_key);
-                cache_guard.insert(label_key, Arc::clone(&updated));
-            }
-        }
+        let handle = if let Some(label) = label {
+            registry.add_model_with_label(label, model)
+        } else {
+            registry.add_model(model)
+        };
 
-        log::debug!("Model cached and loaded: {:?}", file_name);
-        Ok(LoadedModel::new_raw(registry, updated))
+        Ok(handle)
     }
-
 }
 
 pub trait DrawModel<'a> {
@@ -1083,50 +1127,6 @@ where
     }
 }
 
-pub trait DrawShadow<'a> {
-    fn draw_shadow_mesh_instanced(
-        &mut self,
-        mesh: &'a Mesh,
-        instances: Range<u32>,
-        light_bind_group: &'a wgpu::BindGroup,
-    );
-
-    fn draw_shadow_model_instanced(
-        &mut self,
-        model: &'a Model,
-        instances: Range<u32>,
-        light_bind_group: &'a wgpu::BindGroup,
-    );
-}
-
-impl<'a, 'b> DrawShadow<'b> for wgpu::RenderPass<'a>
-where
-    'b: 'a,
-{
-    fn draw_shadow_mesh_instanced(
-        &mut self,
-        mesh: &'b Mesh,
-        instances: Range<u32>,
-        light_bind_group: &'b wgpu::BindGroup,
-    ) {
-        self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
-        self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
-        self.set_bind_group(0, light_bind_group, &[]);
-        self.draw_indexed(0..mesh.num_elements, 0, instances);
-    }
-
-    fn draw_shadow_model_instanced(
-        &mut self,
-        model: &'b Model,
-        instances: Range<u32>,
-        light_bind_group: &'b wgpu::BindGroup,
-    ) {
-        for mesh in &model.meshes {
-            self.draw_shadow_mesh_instanced(mesh, instances.clone(), light_bind_group);
-        }
-    }
-}
-
 pub trait Vertex {
     fn desc() -> VertexBufferLayout<'static>;
 }
@@ -1135,20 +1135,26 @@ pub trait Vertex {
 /// ```wgsl
 /// struct VertexInput {
 ///     @location(0) position: vec3<f32>,
-///     @location(1) tex_coords: vec2<f32>,
-///     @location(2) normal: vec3<f32>,
-///     @location(3) tangent: vec3<f32>,
-///     @location(4) bitangent: vec3<f32>,
+///     @location(1) normal: vec3<f32>,
+///     @location(2) tangent: vec4<f32>,
+///     @location(3) tex_coords0: vec2<f32>,
+///     @location(4) tex_coords1: vec2<f32>,
+///     @location(5) color0: vec4<f32>,
+///     @location(6) joints0: vec4<u32>,
+///     @location(7) weights0: vec4<f32>,
 /// };
 /// ```
 #[repr(C)]
-#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, serde::Serialize, serde::Deserialize)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct ModelVertex {
     pub position: [f32; 3],
-    pub tex_coords: [f32; 2],
     pub normal: [f32; 3],
-    pub tangent: [f32; 3],
-    pub bitangent: [f32; 3],
+    pub tangent: [f32; 4],      // xyz + handedness (w)
+    pub tex_coords0: [f32; 2],
+    pub tex_coords1: [f32; 2],  // optional, can be zeroed if missing
+    pub colour0: [f32; 4],       // optional, default to white
+    pub joints0: [u16; 4],
+    pub weights0: [f32; 4],
 }
 
 impl Vertex for ModelVertex {
@@ -1157,54 +1163,71 @@ impl Vertex for ModelVertex {
             array_stride: mem::size_of::<ModelVertex>() as BufferAddress,
             step_mode: wgpu::VertexStepMode::Vertex,
             attributes: &[
+                // position
                 VertexAttribute {
                     format: wgpu::VertexFormat::Float32x3,
                     offset: 0,
                     shader_location: 0,
                 },
+                // normal
                 wgpu::VertexAttribute {
                     offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
                     shader_location: 1,
-                    format: wgpu::VertexFormat::Float32x2,
+                    format: wgpu::VertexFormat::Float32x3,
                 },
+                // tangent
                 wgpu::VertexAttribute {
-                    offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
+                    offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
                     shader_location: 2,
-                    format: wgpu::VertexFormat::Float32x3,
+                    format: wgpu::VertexFormat::Float32x4,
                 },
+                // tex_coords0
                 wgpu::VertexAttribute {
-                    offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
+                    offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
                     shader_location: 3,
-                    format: wgpu::VertexFormat::Float32x3,
+                    format: wgpu::VertexFormat::Float32x2,
                 },
+                // tex_coords1
                 wgpu::VertexAttribute {
-                    offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress,
+                    offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
                     shader_location: 4,
-                    format: wgpu::VertexFormat::Float32x3,
+                    format: wgpu::VertexFormat::Float32x2,
+                },
+                // color0
+                wgpu::VertexAttribute {
+                    offset: mem::size_of::<[f32; 14]>() as wgpu::BufferAddress,
+                    shader_location: 5,
+                    format: wgpu::VertexFormat::Float32x4,
+                },
+                // joints0
+                wgpu::VertexAttribute {
+                    offset: mem::size_of::<[f32; 18]>() as wgpu::BufferAddress,
+                    shader_location: 6,
+                    format: wgpu::VertexFormat::Uint16x4,
+                },
+                // weights0
+                wgpu::VertexAttribute {
+                    offset: (mem::size_of::<[f32; 18]>() + mem::size_of::<[u16; 4]>())
+                        as wgpu::BufferAddress,
+                    shader_location: 7,
+                    format: wgpu::VertexFormat::Float32x4,
                 },
             ],
         }
     }
 }
 
-#[repr(C, align(16))]
+#[repr(C)]
 #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct MaterialUniform {
-    /// RGBA tint multiplier applied to the sampled base colour.
-    pub colour: [f32; 4],
-
-    /// Scales incoming UVs before sampling (repeat counts when using Repeat wrap mode).
+    pub base_colour: [f32; 4],
+    pub emissive: [f32; 3],
+    pub emissive_strength: f32,
+    pub metallic: f32,
+    pub roughness: f32,
+    pub normal_scale: f32,
+    pub occlusion_strength: f32,
+    pub alpha_cutoff: f32,
     pub uv_tiling: [f32; 2],
-
-    pub _pad: [f32; 2],
-}
-
-impl MaterialUniform {
-    pub fn new(colour: [f32; 4]) -> Self {
-        Self {
-            colour,
-            uv_tiling: [1.0, 1.0],
-            _pad: [0.0, 0.0],
-        }
-    }
+    pub _pad: f32,
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 737bdfc..6a8a7a7 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -10,7 +10,6 @@ use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS};
 use crate::model::Vertex;
 use crate::pipelines::DropbearShaderPipeline;
 use crate::shader::Shader;
-use crate::texture::Texture;
 
 pub struct LightCubePipeline {
     shader: Shader,
diff --git a/crates/dropbear-engine/src/procedural/cube.rs b/crates/dropbear-engine/src/procedural/cube.rs
index c31acfd..753003d 100644
--- a/crates/dropbear-engine/src/procedural/cube.rs
+++ b/crates/dropbear-engine/src/procedural/cube.rs
@@ -11,42 +11,55 @@ impl ProcedurallyGeneratedObject {
         let uv_y = 1.0_f32;
         let uv_z = 1.0_f32;
 
+        let make_vertex = |position: [f32; 3], normal: [f32; 3], tangent: [f32; 3], uv: [f32; 2]| {
+            ModelVertex {
+                position,
+                normal,
+                tangent: [tangent[0], tangent[1], tangent[2], 1.0],
+                tex_coords0: uv,
+                tex_coords1: [0.0, 0.0],
+                colour0: [1.0, 1.0, 1.0, 1.0],
+                joints0: [0, 0, 0, 0],
+                weights0: [1.0, 0.0, 0.0, 0.0],
+            }
+        };
+
         let vertices = vec![
             // Front Face (Normal: 0, 0, 1)
-            ModelVertex { position: [-half.x, -half.y,  half.z], tex_coords: [0.0,  uv_y], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x, -half.y,  half.z], tex_coords: [uv_x, uv_y], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y,  half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y,  half.z], tex_coords: [0.0,  0.0],  normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([-half.x, -half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0,  uv_y]),
+            make_vertex([ half.x, -half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [uv_x, uv_y]),
+            make_vertex([ half.x,  half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([-half.x,  half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Back Face (Normal: 0, 0, -1)
-            ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [0.0,  uv_y], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [uv_x, uv_y], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y, -half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y, -half.z], tex_coords: [0.0,  0.0],  normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([ half.x, -half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0,  uv_y]),
+            make_vertex([-half.x, -half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [uv_x, uv_y]),
+            make_vertex([-half.x,  half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([ half.x,  half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Top Face (Normal: 0, 1, 0)
-            ModelVertex { position: [-half.x,  half.y,  half.z], tex_coords: [0.0,  uv_z], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
-            ModelVertex { position: [ half.x,  half.y,  half.z], tex_coords: [uv_x, uv_z], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
-            ModelVertex { position: [ half.x,  half.y, -half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
-            ModelVertex { position: [-half.x,  half.y, -half.z], tex_coords: [0.0,  0.0],  normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
+            make_vertex([-half.x,  half.y,  half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  uv_z]),
+            make_vertex([ half.x,  half.y,  half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, uv_z]),
+            make_vertex([ half.x,  half.y, -half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([-half.x,  half.y, -half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Bottom Face (Normal: 0, -1, 0)
-            ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [0.0,  uv_z], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
-            ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [uv_x, uv_z], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
-            ModelVertex { position: [ half.x, -half.y,  half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
-            ModelVertex { position: [-half.x, -half.y,  half.z], tex_coords: [0.0,  0.0],  normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
+            make_vertex([-half.x, -half.y, -half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  uv_z]),
+            make_vertex([ half.x, -half.y, -half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, uv_z]),
+            make_vertex([ half.x, -half.y,  half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([-half.x, -half.y,  half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Right Face (Normal: 1, 0, 0)
-            ModelVertex { position: [ half.x, -half.y,  half.z], tex_coords: [0.0,  uv_y], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [uv_z, uv_y], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y, -half.z], tex_coords: [uv_z, 0.0],  normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y,  half.z], tex_coords: [0.0,  0.0],  normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([ half.x, -half.y,  half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0,  uv_y]),
+            make_vertex([ half.x, -half.y, -half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [uv_z, uv_y]),
+            make_vertex([ half.x,  half.y, -half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [uv_z, 0.0]),
+            make_vertex([ half.x,  half.y,  half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0,  0.0]),
 
             // Left Face (Normal: -1, 0, 0)
-            ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [0.0,  uv_y], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x, -half.y,  half.z], tex_coords: [uv_z, uv_y], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y,  half.z], tex_coords: [uv_z, 0.0],  normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y, -half.z], tex_coords: [0.0,  0.0],  normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([-half.x, -half.y, -half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0,  uv_y]),
+            make_vertex([-half.x, -half.y,  half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [uv_z, uv_y]),
+            make_vertex([-half.x,  half.y,  half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [uv_z, 0.0]),
+            make_vertex([-half.x,  half.y, -half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0,  0.0]),
         ];
 
         let indices: Vec<u32> = vec![
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index f588ef1..ce7350a 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -1,15 +1,13 @@
 //! Starter objects like planes and primitive objects are that made during runtime with
 //! vertices rather than from a model.
 
-use crate::asset::{AssetRegistry, ASSET_REGISTRY};
+use crate::asset::{AssetRegistry, Handle};
 use crate::graphics::SharedGraphicsContext;
-use crate::model::{LoadedModel, Material, Mesh, Model, ModelId, MODEL_CACHE};
+use crate::model::{Material, Mesh, Model};
 use crate::utils::ResourceReference;
 use crate::model::ModelVertex;
-use parking_lot::Mutex;
-use std::collections::HashMap;
 use std::hash::{DefaultHasher, Hasher};
-use std::sync::{Arc, LazyLock};
+use std::sync::Arc;
 use serde::{Deserialize, Serialize};
 use wgpu::util::DeviceExt;
 
@@ -45,24 +43,8 @@ impl ProcedurallyGeneratedObject {
         graphics: Arc<SharedGraphicsContext>,
         material: Option<Material>,
         label: Option<&str>,
-    ) -> LoadedModel {
-        self.build_model_raw(
-            graphics,
-            material,
-            label,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-        )
-    }
-
-    pub fn build_model_raw(
-        self,
-        graphics: Arc<SharedGraphicsContext>,
-        material: Option<Material>,
-        label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
-    ) -> LoadedModel {
+        registry: &mut AssetRegistry,
+    ) -> Handle<Model> {
         let mut hasher = DefaultHasher::new();
         hasher.write(bytemuck::cast_slice(&self.vertices));
         hasher.write(bytemuck::cast_slice(&self.indices));
@@ -72,11 +54,8 @@ impl ProcedurallyGeneratedObject {
             .map(|s| s.to_string())
             .unwrap_or_else(|| format!("procedural_{hash:016x}"));
 
-        if let Some(cached_model) = {
-            let cache_guard = cache.lock();
-            cache_guard.get(&label).cloned()
-        } {
-            return LoadedModel::new_raw(registry, cached_model);
+        if let Some(handle) = registry.model_handle_by_hash(hash) {
+            return handle;
         }
 
         let vertices = self.vertices;
@@ -108,31 +87,35 @@ impl ProcedurallyGeneratedObject {
         };
 
         let material = material.unwrap_or_else(|| {
-            let grey = registry.grey_texture(graphics.clone());
-            let flat_normal = registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
-            Material::new_with_tint(
+            let grey_handle = registry.grey_texture(graphics.clone());
+            let flat_normal_handle =
+                registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
+            let grey = registry
+                .get_texture(grey_handle)
+                .expect("Grey texture handle missing")
+                .clone();
+            let flat_normal = registry
+                .get_texture(flat_normal_handle)
+                .expect("Flat normal texture handle missing")
+                .clone();
+            Material::new(
                 graphics.clone(),
                 "procedural_material",
-                (*grey).clone(),
-                (*flat_normal).clone(),
+                grey,
+                flat_normal,
                 [1.0, 1.0, 1.0, 1.0],
                 Some("procedural_material".to_string()),
             )
         });
 
-        let model = Arc::new(Model {
+        let model = Model {
             label: label.clone(),
+            hash,
             path: ResourceReference::from_bytes(hash.to_le_bytes()),
             meshes: vec![mesh],
             materials: vec![material],
-            id: ModelId(hash),
-        });
-
-        {
-            let mut cache_guard = cache.lock();
-            cache_guard.insert(label, Arc::clone(&model));
-        }
+        };
 
-        LoadedModel::new_raw(registry, model)
+        registry.add_model_with_label(label, model)
     }
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/dropbear.slang b/crates/dropbear-engine/src/shaders/dropbear.slang
index 5445568..203cd8c 100644
--- a/crates/dropbear-engine/src/shaders/dropbear.slang
+++ b/crates/dropbear-engine/src/shaders/dropbear.slang
@@ -25,10 +25,14 @@ struct CameraUniform {
 };
 
 struct MaterialUniform {
-    // for stuff like tinting
-    float4 colour;
-
-    // scales incoming UVs before sampling
+    float4 base_colour;
+    float3 emissive;
+    float emissive_strength;
+    float metallic;
+    float roughness;
+    float normal_scale;
+    float occlusion_strength;
+    float alpha_cutoff;
     float2 uv_tiling;
 };
 
diff --git a/crates/dropbear-engine/src/shaders/equirectangular.wgsl b/crates/dropbear-engine/src/shaders/equirectangular.wgsl
index b771de5..440155f 100644
--- a/crates/dropbear-engine/src/shaders/equirectangular.wgsl
+++ b/crates/dropbear-engine/src/shaders/equirectangular.wgsl
@@ -80,7 +80,7 @@ fn compute_equirect_to_cubemap(
     let eq_pixel = vec2<i32>(eq_uv * vec2<f32>(textureDimensions(src)));
 
     // We use textureLoad() as textureSample() is not allowed in compute shaders
-    var sample = textureLoad(src, eq_pixel, 0);
+    var smpl = textureLoad(src, eq_pixel, 0);
 
-    textureStore(dst, gid.xy, gid.z, sample);
+    textureStore(dst, gid.xy, gid.z, smpl);
 }
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index a1f912a..dd2a210 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -23,10 +23,14 @@ struct Light {
 }
 
 struct MaterialUniform {
-    // for stuff like tinting
-    colour: vec4<f32>,
-
-    // scales incoming UVs before sampling
+    base_colour: vec4<f32>,
+    emissive: vec3<f32>,
+    emissive_strength: f32,
+    metallic: f32,
+    roughness: f32,
+    normal_scale: f32,
+    occlusion_strength: f32,
+    alpha_cutoff: f32,
     uv_tiling: vec2<f32>,
 }
 
@@ -68,9 +72,15 @@ struct InstanceInput {
 
 struct VertexInput {
     @location(0) position: vec3<f32>,
-    @location(1) tex_coords: vec2<f32>,
     @location(2) normal: vec3<f32>,
     @location(3) tangent: vec3<f32>,
+    @location(1) tex_coords0: vec2<f32>,
+    @location(1) tex_coords1: vec2<f32>,
+    @location(1) colour0: vec4<f32>,
+    @location(1) joints(0): vec4<u32>,
+    @location(1) joints(0): vec4<u32>,
+
+
     @location(4) bitangent: vec3<f32>,
 };
 
@@ -216,12 +226,16 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
         discard;
     }
 
+    let normal = normalize(world_normal);
+    let tangent = normalize(world_tangent.xyz);
+    let bitangent = cross(n, t) * world_tangent.w;
+
     let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
 
     let world_normal = apply_normal_map(
         in.world_normal,
         in.world_tangent,
-        in.world_bitangent,
+        bitangent,
         object_normal.xyz,
     );
 
@@ -258,12 +272,16 @@ fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
         discard;
     }
 
+    let normal = normalize(world_normal);
+    let tangent = normalize(world_tangent.xyz);
+    let bitangent = cross(n, t) * world_tangent.w;
+
     let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
 
     let world_normal = apply_normal_map(
         in.world_normal,
         in.world_tangent,
-        in.world_bitangent,
+        bitangent,
         object_normal.xyz,
     );
 
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 44c6ab5..36bfe50 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -2,7 +2,7 @@ use std::{fs, path::PathBuf, sync::Arc};
 
 use image::GenericImageView;
 use serde::{Deserialize, Serialize};
-
+use crate::asset::AssetRegistry;
 use crate::graphics::SharedGraphicsContext;
 use crate::utils::ToPotentialString;
 
@@ -68,6 +68,7 @@ pub struct Texture {
     pub sampler: wgpu::Sampler,
     pub size: wgpu::Extent3d,
     pub view: wgpu::TextureView,
+    pub(crate) hash: Option<u64>,
 }
 
 impl Texture {
@@ -137,6 +138,7 @@ impl Texture {
             view,
             sampler,
             size,
+            hash: None,
         }
     }
 
@@ -184,6 +186,7 @@ impl Texture {
             size,
             view,
             label: label.to_potential_string(),
+            hash: None,
         }
     }
 
@@ -222,6 +225,7 @@ impl Texture {
             sampler,
             size,
             view,
+            hash: None,
         }
     }
 
@@ -285,6 +289,8 @@ impl Texture {
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
     ) -> Self {
+        let hash = AssetRegistry::hash_bytes(bytes);
+        
         let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) {
             Ok(image) => {
                 let rgba = image.to_rgba8().into_raw();
@@ -416,6 +422,7 @@ impl Texture {
             sampler,
             size,
             view,
+            hash: Some(hash),
         }
     }
 
diff --git a/resources/models/cube.glb b/resources/models/cube.glb
deleted file mode 100644
index 88c5ec8..0000000
Binary files a/resources/models/cube.glb and /dev/null differ
diff --git a/resources/models/error.glb b/resources/models/error.glb
deleted file mode 100644
index 496e346..0000000
Binary files a/resources/models/error.glb and /dev/null differ