kitgit

tirbofish/dropbear · commit

6f0d1f12193452304c0678bebd578175657b13c5

Merge pull request #89 from tirbofish/patch patch + feature: asset server and fixing ui

Unverified

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

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 18b21b4..2a94bc3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,7 +31,7 @@ env_logger = "0.11"
 futures = "0.3"
 gilrs = "0.11"
 git2 = { version = "0.20", features = ["vendored-openssl"] }
-glam = { version = "0.30", features = ["serde", "mint"] }
+glam = { version = "0.30", features = ["serde", "mint", "bytemuck"] }
 hecs = { version = "0.11", features = ["serde"] }
 log = "0.4"
 log-once = "0.4"
@@ -84,6 +84,7 @@ glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" }
 puffin = "0.19"
 bitflags = "2.10"
 features = "0.10"
+puffin_http = "0.16"
 
 [workspace.dependencies.image]
 version = "0.24"
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index 094c9cf..c59f954 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -47,6 +47,7 @@ pollster.workspace = true
 image.workspace = true
 puffin.workspace = true
 bitflags.workspace = true
+puffin_http.workspace = true
 
 #yakui-wgpu.workspace = true
 #yakui.workspace = true
diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
new file mode 100644
index 0000000..135b58a
--- /dev/null
+++ b/crates/dropbear-engine/src/animation.rs
@@ -0,0 +1,323 @@
+use dropbear_traits::SerializableComponent;
+use std::collections::HashMap;
+use std::sync::Arc;
+use glam::Mat4;
+use wgpu::util::DeviceExt;
+use dropbear_macro::SerializableComponent;
+use crate::graphics::SharedGraphicsContext;
+use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)]
+pub struct AnimationComponent {
+    #[serde(default)]
+    pub active_animation_index: Option<usize>,
+    #[serde(default)]
+    pub time: f32,
+    #[serde(default)]
+    pub speed: f32,
+    #[serde(default)]
+    pub looping: bool,
+    #[serde(default)]
+    pub is_playing: bool,
+
+    #[serde(skip)]
+    pub local_pose: HashMap<usize, NodeTransform>,
+    #[serde(skip)]
+    pub skinning_matrices: Vec<Mat4>,
+
+    #[serde(skip)]
+    pub bone_buffer: Option<wgpu::Buffer>,
+    #[serde(skip)]
+    pub bind_group: Option<wgpu::BindGroup>,
+}
+
+impl Default for AnimationComponent {
+    fn default() -> Self {
+        Self {
+            active_animation_index: None,
+            time: 0.0,
+            speed: 1.0,
+            looping: true,
+            is_playing: true,
+            local_pose: HashMap::new(),
+            skinning_matrices: Vec::new(),
+            bone_buffer: None,
+            bind_group: None,
+        }
+    }
+}
+
+impl AnimationComponent {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn update(&mut self, dt: f32, model: &Model) {
+        puffin::profile_function!(&model.label);
+        if !self.is_playing || self.active_animation_index.is_none() {
+            return;
+        }
+
+        let anim_idx = self.active_animation_index.unwrap();
+        let animation = &model.animations[anim_idx];
+
+        self.time += dt * self.speed;
+        if self.looping {
+            if animation.duration > 0.0 {
+                self.time %= animation.duration;
+            }
+        } else {
+            self.time = self.time.clamp(0.0, animation.duration);
+            if self.time >= animation.duration {
+                self.is_playing = false;
+            }
+        }
+
+        for channel in &animation.channels {
+            let count = channel.times.len();
+            if count == 0 { continue; }
+
+            // Handle out of bounds / single keyframe
+            if count == 1 || self.time <= channel.times[0] {
+                Self::apply_single_keyframe(channel, 0, &mut self.local_pose, model);
+                continue;
+            }
+            if self.time >= channel.times[count - 1] {
+                Self::apply_single_keyframe(channel, count - 1, &mut self.local_pose, model);
+                continue;
+            }
+            
+            let next_idx = channel.times.partition_point(|&t| t <= self.time);
+            let prev_idx = next_idx.saturating_sub(1);
+
+            let start_time = channel.times[prev_idx];
+            let end_time = channel.times[next_idx];
+            let duration = end_time - start_time;
+
+            let factor = if duration > 0.0 {
+                (self.time - start_time) / duration
+            } else {
+                0.0
+            };
+
+            let transform = self.local_pose
+                .entry(channel.target_node)
+                .or_insert_with(|| {
+                    model.nodes.get(channel.target_node)
+                        .map(|n| n.transform.clone())
+                        .unwrap_or_else(NodeTransform::identity)
+                });
+
+            let dt = end_time - start_time;
+            
+            match &channel.values {
+                ChannelValues::Translations(values) => {
+                    transform.translation = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx],
+                        AnimationInterpolation::Linear => {
+                             let start = values[prev_idx];
+                             let end = values[next_idx];
+                             start.lerp(end, factor)
+                        },
+                        AnimationInterpolation::CubicSpline => {
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            
+                            let idx0 = prev_idx * 3;
+                            let idx1 = next_idx * 3;
+                            
+                            if idx1 + 1 >= values.len() {
+                                values[idx0 + 1]
+                            } else {
+                                let p0 = values[idx0 + 1];
+                                let m0 = values[idx0 + 2] * dt;
+                                let m1 = values[idx1 + 0] * dt;
+                                let p1 = values[idx1 + 1];
+                                
+                                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                                let h10 = t3 - 2.0 * t2 + t;
+                                let h01 = -2.0 * t3 + 3.0 * t2;
+                                let h11 = t3 - t2;
+                                
+                                p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
+                            }
+                        }
+                    };
+                }
+                ChannelValues::Rotations(values) => {
+                    transform.rotation = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx],
+                        AnimationInterpolation::Linear => {
+                            let start = values[prev_idx];
+                            let end = values[next_idx];
+                            start.slerp(end, factor).normalize()
+                        },
+                        AnimationInterpolation::CubicSpline => {
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            
+                            let idx0 = prev_idx * 3;
+                            let idx1 = next_idx * 3;
+                            
+                             if idx1 + 1 >= values.len() {
+                                values[idx0 + 1]
+                            } else {
+                                let p0 = values[idx0 + 1];
+                                let m0 = values[idx0 + 2] * dt;
+                                let m1 = values[idx1 + 0] * dt;
+                                let p1 = values[idx1 + 1];
+                                
+                                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                                let h10 = t3 - 2.0 * t2 + t;
+                                let h01 = -2.0 * t3 + 3.0 * t2;
+                                let h11 = t3 - t2;
+                                
+                                let res = p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11;
+                                res.normalize()
+                            }
+                        }
+                    };
+                }
+                ChannelValues::Scales(values) => {
+                    transform.scale = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx],
+                        AnimationInterpolation::Linear => {
+                            let start = values[prev_idx];
+                            let end = values[next_idx];
+                            start.lerp(end, factor)
+                        },
+                        AnimationInterpolation::CubicSpline => {
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            
+                            let idx0 = prev_idx * 3;
+                            let idx1 = next_idx * 3;
+                            
+                            if idx1 + 1 >= values.len() {
+                                values[idx0 + 1]
+                            } else {
+                                let p0 = values[idx0 + 1];
+                                let m0 = values[idx0 + 2] * dt;
+                                let m1 = values[idx1 + 0] * dt;
+                                let p1 = values[idx1 + 1];
+                                
+                                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                                let h10 = t3 - 2.0 * t2 + t;
+                                let h01 = -2.0 * t3 + 3.0 * t2;
+                                let h11 = t3 - t2;
+                                
+                                p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
+                            }
+                        }
+                    };
+                }
+            }
+        }
+
+        self.update_matrices(model);
+    }
+
+    fn apply_single_keyframe(
+        channel: &crate::model::AnimationChannel,
+        index: usize,
+        pose: &mut HashMap<usize, NodeTransform>,
+        model: &Model
+    ) {
+        let transform = pose.entry(channel.target_node).or_insert_with(|| {
+            model.nodes.get(channel.target_node)
+                .map(|n| n.transform.clone())
+                .unwrap_or_else(NodeTransform::identity)
+        });
+
+        match &channel.values {
+            ChannelValues::Translations(v) => {
+                if let Some(val) = v.get(index) { transform.translation = *val; }
+            }
+            ChannelValues::Rotations(v) => {
+                if let Some(val) = v.get(index) { transform.rotation = *val; }
+            }
+            ChannelValues::Scales(v) => {
+                if let Some(val) = v.get(index) { transform.scale = *val; }
+            }
+        }
+    }
+
+    fn update_matrices(&mut self, model: &Model) {
+        if let Some(skin) = model.skins.first() {
+            if self.skinning_matrices.len() != skin.joints.len() {
+                self.skinning_matrices.resize(skin.joints.len(), Mat4::IDENTITY);
+            }
+
+            let mut global_transforms = HashMap::new();
+            
+            for &joint_idx in &skin.joints {
+                self.resolve_global_transform(joint_idx, model, &mut global_transforms);
+            }
+
+            for (i, &joint_node_idx) in skin.joints.iter().enumerate() {
+                if let Some(global_transform) = global_transforms.get(&joint_node_idx) {
+                    let inverse_bind = skin.inverse_bind_matrices[i];
+                    self.skinning_matrices[i] = *global_transform * inverse_bind;
+                }
+            }
+        }
+    }
+
+    /// Recursively calculates and caches the global world matrix for a node.
+    fn resolve_global_transform(
+        &self,
+        node_idx: usize,
+        model: &Model,
+        cache: &mut HashMap<usize, Mat4>,
+    ) -> Mat4 {
+        if let Some(&matrix) = cache.get(&node_idx) {
+            return matrix;
+        }
+
+        let node = &model.nodes[node_idx];
+        let local_matrix = self.local_pose.get(&node_idx)
+            .map(|transform| transform.to_matrix())
+            .unwrap_or_else(|| node.transform.to_matrix());
+
+        let global_matrix = if let Some(parent_idx) = node.parent {
+            let parent_global = self.resolve_global_transform(parent_idx, model, cache);
+            parent_global * local_matrix
+        } else {
+            local_matrix
+        };
+
+        cache.insert(node_idx, global_matrix);
+        global_matrix
+    }
+
+    pub fn prepare_gpu_resources(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        if self.skinning_matrices.is_empty() { return; }
+
+        let data = bytemuck::cast_slice(&self.skinning_matrices);
+
+        if let Some(buffer) = &self.bone_buffer {
+            graphics.queue.write_buffer(buffer, 0, data);
+        } else {
+            let buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("skinning buffer"),
+                contents: data,
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            });
+
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("skinning bind group"),
+                layout: &graphics.layouts.skinning_bind_group_layout,
+                entries: &[wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: buffer.as_entire_binding(),
+                }],
+            });
+
+            self.bone_buffer = Some(buffer);
+            self.bind_group = Some(bind_group);
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index 49c4aa3..411e870 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -1,513 +1,229 @@
-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()
+    /// 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
     }
 
-    pub fn iter_model_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> {
-        self.models.iter()
+    /// 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 iter_material(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> {
-        self.iter_material_raw()
+    /// 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());
     }
 
-    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()))
-    }
-
-    /// Fetches a material by handle.
-    pub fn get_material(&self, handle: AssetHandle) -> Option<Arc<Material>> {
-        self.get_material_raw(handle)
-    }
-
-    pub fn get_material_raw(&self, handle: AssetHandle) -> Option<Arc<Material>> {
-        self.materials
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
+    /// Removes a label from the texture registry, but keeps it in the registry.
+    ///
+    /// When the label is removed, the [Handle] is still valid.
+    pub fn remove_label_texture(&mut self, label: &str) {
+        self.texture_labels.remove(label);
     }
 
-    /// 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 +231,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/buffer.rs b/crates/dropbear-engine/src/buffer.rs
index 27552d0..7dd16d7 100644
--- a/crates/dropbear-engine/src/buffer.rs
+++ b/crates/dropbear-engine/src/buffer.rs
@@ -64,6 +64,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> {
     }
 
     pub fn write(&self, queue: &wgpu::Queue, value: &T) {
+        puffin::profile_function!(&self.label);
         queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
     }
 
@@ -106,6 +107,7 @@ impl<T: bytemuck::Pod> StorageBuffer<T> {
     }
 
     pub fn write(&self, queue: &wgpu::Queue, value: &T) {
+        puffin::profile_function!(self.label());
         queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
     }
 
@@ -143,6 +145,7 @@ impl<T: bytemuck::Pod> ResizableBuffer<T> {
     }
 
     pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) {
+        puffin::profile_function!(&self.label);
         if data.is_empty() {
             return;
         }
diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index 089c09d..2adaa22 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -231,11 +231,13 @@ impl Camera {
     }
 
     pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        puffin::profile_function!();
         self.update_view_proj();
         self.buffer.write(&graphics.queue, &self.uniform);
     }
 
     pub fn update_view_proj(&mut self) {
+        puffin::profile_function!();
         let mut uniform = self.uniform;
         uniform.update(self);
         self.uniform = uniform;
diff --git a/crates/dropbear-engine/src/egui_renderer.rs b/crates/dropbear-engine/src/egui_renderer.rs
index dcd2530..1ecf70a 100644
--- a/crates/dropbear-engine/src/egui_renderer.rs
+++ b/crates/dropbear-engine/src/egui_renderer.rs
@@ -27,6 +27,7 @@ impl EguiRenderer {
         msaa_samples: u32,
         window: &Window,
     ) -> EguiRenderer {
+        puffin::profile_function!();
         let egui_context = Context::default();
 
         let egui_state = egui_winit::State::new(
@@ -75,6 +76,7 @@ impl EguiRenderer {
         window_surface_view: &TextureView,
         screen_descriptor: ScreenDescriptor,
     ) {
+        puffin::profile_function!();
         if !self.frame_started {
             panic!("begin_frame must be called before end_frame_and_draw can be called!");
         }
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index 3e49d4e..72f7486 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -9,14 +9,15 @@ use std::{
 };
 
 use crate::{
-    asset::{ASSET_REGISTRY, AssetHandle, AssetKind, AssetRegistry},
+    asset::{ASSET_REGISTRY, AssetRegistry},
     graphics::{Instance, SharedGraphicsContext},
-    model::{LoadedModel, MODEL_CACHE, Model, ModelId},
+    model::{Model},
     utils::ResourceReference,
     texture::Texture,
 };
 use anyhow::anyhow;
 use dropbear_macro::SerializableComponent;
+use crate::asset::Handle;
 
 /// A type of transform that is attached to all entities. It contains the local and world transforms.
 #[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone, SerializableComponent)]
@@ -166,87 +167,48 @@ impl Transform {
 /// It includes the instances as well as a handle. The reason for a handle is so the model being rendered can be swapped
 /// to something else without deleting the entire renderer. Also saves memory by rendering anything that has been loaded.
 pub struct MeshRenderer {
-    handle: LoadedModel,
-    pub instance: Instance,
-    pub previous_matrix: DMat4,
-    pub is_selected: bool,
-    pub material_overrides: Vec<MaterialOverride>,
-    original_material_snapshots: HashMap<String, MaterialSnapshot>,
-    texture_identifier_cache: HashMap<String, String>,
     import_scale: f32,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct MaterialOverride {
-    pub target_material: String,
-    pub source_model: ResourceReference,
-    pub source_material: String,
-}
 
-#[derive(Clone)]
-struct MaterialSnapshot {
-    diffuse: Texture,
-    normal: Texture,
-    bind_group: wgpu::BindGroup,
-    texture_tag: Option<String>,
+    handle: Handle<Model>,
+    instance: Instance,
+    previous_matrix: DMat4,
+    texture_override: Option<Handle<Texture>>,
 }
 
 impl MeshRenderer {
+    pub fn from_handle(model: Handle<Model>) -> Self {
+        Self {
+            handle: model,
+            instance: Instance::default(),
+            previous_matrix: DMat4::IDENTITY,
+            import_scale: 1.0,
+            texture_override: None,
+        }
+    }
+    
     pub async fn from_path(
         graphics: Arc<SharedGraphicsContext>,
         path: impl AsRef<Path>,
         label: Option<&str>,
     ) -> anyhow::Result<Self> {
         let path = path.as_ref().to_path_buf();
-        let handle = Model::load(graphics, &path, label, None).await?;
-        Ok(Self::from_handle(handle))
-    }
-
-    /// Creates a new [`MeshRenderer`] instance from a [`LoadedModel`] handle with an explicit per-renderer import scale.
-    pub fn from_handle_with_import_scale(handle: LoadedModel, import_scale: f32) -> Self {
-        Self {
+        let handle = Model::load_from_memory_raw(
+            graphics.clone(),
+            std::fs::read(path)?,
+            label,
+            ASSET_REGISTRY.clone(),
+        ).await?;
+        Ok(Self {
             handle,
-            instance: Instance::new(DVec3::ZERO, DQuat::IDENTITY, DVec3::ONE),
+            instance: Instance::default(),
+            import_scale: 1.0,
             previous_matrix: DMat4::IDENTITY,
-            is_selected: false,
-            material_overrides: Vec::new(),
-            original_material_snapshots: HashMap::new(),
-            texture_identifier_cache: HashMap::new(),
-            import_scale,
-        }
-    }
-
-    /// Creates a new [`MeshRenderer`] instance from a [`LoadedModel`] handle
-    pub fn from_handle(handle: LoadedModel) -> Self {
-        Self::from_handle_with_import_scale(handle, 1.0)
-    }
-
-    pub fn model(&self) -> Arc<Model> {
-        self.handle.get()
-    }
-
-    pub fn model_id(&self) -> ModelId {
-        self.handle.id()
-    }
-
-    pub fn asset_handle(&self) -> AssetHandle {
-        self.handle.asset_handle()
-    }
-
-    pub fn handle(&self) -> &LoadedModel {
-        &self.handle
-    }
-
-    pub fn handle_mut(&mut self) -> &mut LoadedModel {
-        &mut self.handle
-    }
-
-    pub fn make_model_mut(&mut self) -> &mut Model {
-        self.handle.make_mut()
+            texture_override: None,
+        })
     }
 
     pub fn update(&mut self, transform: &Transform) {
-        // Import scaling is per-renderer and should not mutate shared model buffers.
+        puffin::profile_function!();
         let scale = transform.scale * glam::DVec3::splat(self.import_scale as f64);
         let current_matrix = DMat4::from_scale_rotation_translation(
             scale,
@@ -259,376 +221,56 @@ impl MeshRenderer {
         }
     }
 
-    /// Swaps the currently loaded model for that renderer by the provided [`LoadedModel`]
-    pub fn set_handle(&mut self, handle: LoadedModel) {
-        self.set_handle_raw(handle);
-    }
-
-    pub fn set_handle_raw(&mut self, handle: LoadedModel) {
-        self.handle = handle;
-        self.material_overrides.clear();
-        self.original_material_snapshots.clear();
-        self.texture_identifier_cache.clear();
-    }
-
-    /// Swaps the currently loaded model for that renderer by the provided [`AssetHandle`]
-    ///
-    /// Returns an error if the assethandle provided is not in the model registry.
-    pub fn set_asset_handle(&mut self, handle: AssetHandle) -> anyhow::Result<()> {
-        if !ASSET_REGISTRY.contains_handle(handle) {
-            return Err(anyhow!(
-                "Asset handle {} is not registered with the asset registry",
-                handle.raw()
-            ));
-        }
-
-        if !ASSET_REGISTRY.is_handle_kind(handle, AssetKind::Model) {
-            return Err(anyhow!(
-                "Asset handle {} does not refer to a model asset",
-                handle.raw()
-            ));
-        }
-
-        let model = ASSET_REGISTRY
-            .get_model(handle)
-            .ok_or_else(|| anyhow!("Model handle {} not found", handle.raw()))?;
-
-        self.set_handle_raw(LoadedModel::from_registered(handle, model));
-        Ok(())
-    }
-
-    /// Swaps the loaded model to a different one by using an AssetHandle.
-    ///
-    /// The main difference between [`MeshRenderer::set_asset_handle`] and
-    /// [`MeshRenderer::set_asset_handle_raw`] is that it does not use any static variables
-    /// (like [`ASSET_REGISTRY`]), instead allowing for the registry to be manually provided.
-    pub fn set_asset_handle_raw(
-        &mut self,
-        registry: &AssetRegistry,
-        handle: AssetHandle,
-    ) -> anyhow::Result<()> {
-        if !registry.contains_handle(handle) {
-            return Err(anyhow!(
-                "Asset handle {} is not registered with the asset registry",
-                handle.raw()
-            ));
-        }
-
-        if !registry.is_handle_kind(handle, AssetKind::Model) {
-            return Err(anyhow!(
-                "Asset handle {} does not refer to a model asset",
-                handle.raw()
-            ));
-        }
-
-        let model = registry
-            .get_model(handle)
-            .ok_or_else(|| anyhow!("Model handle {} not found", handle.raw()))?;
-
-        self.set_handle_raw(LoadedModel::from_registered(handle, model));
-        Ok(())
-    }
-
-    pub fn uses_model_handle(&self, handle: AssetHandle) -> bool {
-        self.asset_handle() == handle
-    }
-
-    pub fn uses_model_reference(&self, reference: &ResourceReference) -> bool {
-        self.handle().matches_resource(reference)
-    }
-
-    pub fn contains_material_handle(&self, handle: AssetHandle) -> bool {
-        self.handle().contains_material_handle(handle)
-    }
-
-    pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool {
-        self.handle().contains_material_reference(reference)
-    }
-
-    pub fn material_handle(&self, material_name: &str) -> Option<AssetHandle> {
-        self.material_handle_raw(&ASSET_REGISTRY, material_name)
-    }
-
-    pub fn collect_all_material_handles_raw(
-        &self,
-        registry: &AssetRegistry,
-    ) -> Vec<AssetHandle> {
-        let model = self.model();
-        let model_id = self.model_id();
-
-        model
-            .materials
-            .iter()
-            .filter_map(|material| {
-                registry.material_handle(model_id, &material.name)
-            })
-            .collect()
-    }
-
-    pub fn collect_all_material_handles(&self) -> Vec<AssetHandle> {
-        self.collect_all_material_handles_raw(&ASSET_REGISTRY)
-    }
-
-    pub fn material_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        material_name: &str,
-    ) -> Option<AssetHandle> {
-        registry.material_handle(self.model_id(), material_name)
-    }
-
-    pub fn mesh_handle(&self, mesh_name: &str) -> Option<AssetHandle> {
-        self.mesh_handle_raw(&ASSET_REGISTRY, mesh_name)
-    }
-
-    pub fn mesh_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        mesh_name: &str,
-    ) -> Option<AssetHandle> {
-        registry.mesh_handle(self.model_id(), mesh_name)
-    }
-
-    pub fn apply_material_override(
-        &mut self,
-        target_material: &str,
-        source_model: ResourceReference,
-        source_material: &str,
-    ) -> anyhow::Result<()> {
-        self.apply_material_override_raw(
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-            target_material,
-            source_model,
-            source_material,
-        )
-    }
-
-    pub fn apply_material_override_raw(
-        &mut self,
-        registry: &AssetRegistry,
-        model_cache: &Mutex<HashMap<String, Arc<Model>>>,
-        target_material: &str,
-        source_model: ResourceReference,
-        source_material: &str,
-    ) -> anyhow::Result<()> {
-        let snapshot_entry = {
-            let current_model = self.model();
-            let original = current_model
-                .materials
-                .iter()
-                .find(|mat| mat.name == target_material)
-                .ok_or_else(|| {
-                    anyhow!(
-                        "Target material '{}' does not exist on model '{}'",
-                        target_material,
-                        current_model.label
-                    )
-                })?;
-
-            MaterialSnapshot {
-                diffuse: original.diffuse_texture.clone(),
-                normal: original.normal_texture.clone(),
-                bind_group: original.bind_group.clone(),
-                texture_tag: original.texture_tag.clone(),
-            }
-        };
-
-        self.original_material_snapshots
-            .entry(target_material.to_string())
-            .or_insert(snapshot_entry);
-
-        let source_reference = registry
-            .model_handle_from_reference(&source_model)
-            .ok_or_else(|| {
-                anyhow!(
-                    "Source model {:?} is not registered in the asset registry",
-                    source_model
-                )
-            })?;
-
-        let source_model_arc = registry.get_model(source_reference).ok_or_else(|| {
-            anyhow!(
-                "Unable to fetch model handle {:?} from registry",
-                source_reference
-            )
-        })?;
-
-        let material = source_model_arc
-            .materials
-            .iter()
-            .find(|mat| mat.name == source_material)
-            .ok_or_else(|| {
-                anyhow!(
-                    "Material '{}' does not exist on source model {:?}",
-                    source_material,
-                    source_model
-                )
-            })?;
-
-        {
-            let model = self.make_model_mut();
-            if !model.set_material_texture(
-                target_material,
-                material.diffuse_texture.clone(),
-                material.normal_texture.clone(),
-                material.bind_group.clone(),
-                material.texture_tag.clone(),
-            ) {
-                anyhow::bail!(
-                    "Target material '{}' does not exist on model '{}'",
-                    target_material,
-                    model.label
-                );
-            }
-        }
-
-        let original_reference = self.model().path.clone();
-        let is_default = original_reference == source_model && target_material == source_material;
-
-        self.material_overrides
-            .retain(|entry| entry.target_material != target_material);
-
-        if !is_default {
-            self.material_overrides.push(MaterialOverride {
-                target_material: target_material.to_string(),
-                source_model,
-                source_material: source_material.to_string(),
-            });
-        } else {
-            self.original_material_snapshots.remove(target_material);
-            self.clear_material_override(target_material);
-        }
-
-        // ensure downstream caches observe the newly applied material state
-        self.handle.refresh_registry_raw(registry);
-
-        self.refresh_model_cache_with(model_cache);
-
-        Ok(())
-    }
-
-    pub fn material_overrides(&self) -> &[MaterialOverride] {
-        &self.material_overrides
-    }
-
-    pub fn clear_texture_identifier_cache(&mut self) {
-        self.texture_identifier_cache.clear();
-    }
-
-    pub fn register_texture_identifier(&mut self, identifier: String, material_name: String) {
-        match self.texture_identifier_cache.entry(identifier) {
-            Entry::Occupied(_) => {}
-            Entry::Vacant(slot) => {
-                slot.insert(material_name);
-            }
-        }
-    }
-
-    pub fn resolve_texture_identifier(&self, identifier: &str) -> Option<&str> {
-        self.texture_identifier_cache
-            .get(identifier)
-            .map(|value| value.as_str())
-    }
-
-    pub fn sync_asset_registry(&mut self) {
-        self.handle.refresh_registry_raw(&ASSET_REGISTRY);
-        self.refresh_model_cache_with(LazyLock::force(&MODEL_CACHE));
-    }
-
-    pub fn clear_material_override(&mut self, target_material: &str) {
-        self.material_overrides
-            .retain(|entry| entry.target_material != target_material);
-    }
-
-    pub fn restore_original_material(&mut self, target_material: &str) -> anyhow::Result<()> {
-        self.restore_original_material_raw(
-            target_material,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-        )
-    }
-
-    pub fn restore_original_material_raw(
-        &mut self,
-        target_material: &str,
-        registry: &AssetRegistry,
-        model_cache: &Mutex<HashMap<String, Arc<Model>>>,
-    ) -> anyhow::Result<()> {
-        let snapshot = self
-            .original_material_snapshots
-            .get(target_material)
-            .cloned();
-
-        self.clear_material_override(target_material);
-
-        if let Some(snapshot) = snapshot {
-            let model = self.make_model_mut();
-            if !model.set_material_texture(
-                target_material,
-                snapshot.diffuse.clone(),
-                snapshot.normal.clone(),
-                snapshot.bind_group.clone(),
-                snapshot.texture_tag.clone(),
-            ) {
-                anyhow::bail!(
-                    "Target material '{}' does not exist on model '{}'",
-                    target_material,
-                    model.label
-                );
-            }
-
-            if snapshot.texture_tag.is_none() {
-                let _ = model.clear_material_texture_tag(target_material);
-            }
-
-            self.original_material_snapshots.remove(target_material);
-        }
-
-        self.handle.refresh_registry_raw(registry);
-        self.refresh_model_cache_with(model_cache);
-
-        Ok(())
-    }
-
-    fn refresh_model_cache_with(&self, cache: &Mutex<HashMap<String, Arc<Model>>>) {
-        let mut guard = cache.lock();
-        self.refresh_model_cache_raw(&mut guard);
-    }
-
-    fn refresh_model_cache_raw(&self, cache: &mut HashMap<String, Arc<Model>>) {
-        let current = self.handle.get();
-        let keys: Vec<String> = cache
-            .iter()
-            .filter_map(|(key, model)| (model.id == current.id).then(|| key.clone()))
-            .collect();
-
-        for key in keys {
-            cache.insert(key, Arc::clone(&current));
-        }
-    }
-
-    pub fn import_scale(&self) -> f32 {
-        self.import_scale
-    }
-
     pub fn set_import_scale(&mut self, scale: f32) {
         self.import_scale = scale;
     }
 
-    // Backwards-compat helper names (kept for now).
-    pub fn effective_import_scale(&self) -> f32 {
-        self.import_scale
-    }
-
-    pub fn custom_import_scale(&self) -> Option<f32> {
-        Some(self.import_scale)
+    pub fn set_model(&mut self, model: Handle<Model>) {
+        self.handle = model;
+    }
+
+    pub fn model(&self) -> Handle<Model> {
+        self.handle
+    }
+
+    pub fn set_texture_override(&mut self, texture: Handle<Texture>) {
+        self.texture_override = Some(texture);
+    }
+
+    pub fn is_texture_attached(&self, texture: Handle<Texture>) -> bool {
+        let registry = ASSET_REGISTRY.read();
+        
+        if let Some(model) = registry.get_model(self.handle) {
+            for material in &model.materials {
+                if material.diffuse_texture.hash == Some(texture.id) {
+                    return true;
+                }
+                if material.normal_texture.hash == Some(texture.id) {
+                    return true;
+                }
+                if let Some(emissive) = &material.emissive_texture {
+                    if emissive.hash == Some(texture.id) {
+                        return true;
+                    }
+                }
+                if let Some(mr) = &material.metallic_roughness_texture {
+                    if mr.hash == Some(texture.id) {
+                        return true;
+                    }
+                }
+                if let Some(occ) = &material.occlusion_texture {
+                    if occ.hash == Some(texture.id) {
+                        return true;
+                    }
+                }
+            }
+        }
+        
+        false
     }
 
-    pub fn set_custom_import_scale(&mut self, scale: Option<f32>) {
-        if let Some(scale) = scale {
-            self.import_scale = scale;
-        }
+    pub fn reset_texture_override(&mut self) {
+        self.texture_override = None;
     }
 }
 
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 6f1c631..bbc0234 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -31,7 +31,6 @@ pub struct SharedGraphicsContext {
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
-    pub supports_storage: bool,
     pub mipmapper: Arc<MipMapper>,
     pub hdr: Arc<RwLock<HdrPipeline>>,
     // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>,
@@ -76,7 +75,6 @@ impl SharedGraphicsContext {
             texture_id: state.texture_id.clone(),
             surface: state.surface.clone(),
             surface_format: state.surface_format,
-            supports_storage: state.supports_storage,
             mipmapper: state.mipmapper.clone(),
             hdr: state.hdr.clone(),
             // yakui_renderer: state.yakui_renderer.clone(),
@@ -150,46 +148,46 @@ impl InstanceRaw {
                 // model_matrix_0
                 wgpu::VertexAttribute {
                     offset: 0,
-                    shader_location: 5,
+                    shader_location: 8,
                     format: wgpu::VertexFormat::Float32x4,
                 },
                 // model_matrix_1
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 4]>() as wgpu::BufferAddress,
-                    shader_location: 6,
+                    shader_location: 9,
                     format: wgpu::VertexFormat::Float32x4,
                 },
                 // model_matrix_2
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 8]>() as wgpu::BufferAddress,
-                    shader_location: 7,
+                    shader_location: 10,
                     format: wgpu::VertexFormat::Float32x4,
                 },
                 // model_matrix_3
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 12]>() as wgpu::BufferAddress,
-                    shader_location: 8,
+                    shader_location: 11,
                     format: wgpu::VertexFormat::Float32x4,
                 },
 
                 // normal_matrix_0
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 16]>() as wgpu::BufferAddress,
-                    shader_location: 9,
+                    shader_location: 12,
                     format: wgpu::VertexFormat::Float32x3,
                 },
 
                 // normal_matrix_1
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 19]>() as wgpu::BufferAddress,
-                    shader_location: 10,
+                    shader_location: 13,
                     format: wgpu::VertexFormat::Float32x3,
                 },
 
                 // normal_matrix_2
                 wgpu::VertexAttribute {
                     offset: size_of::<[f32; 22]>() as wgpu::BufferAddress,
-                    shader_location: 11,
+                    shader_location: 14,
                     format: wgpu::VertexFormat::Float32x3,
                 },
             ],
@@ -231,6 +229,7 @@ impl CommandEncoder {
     ///
     /// Panics if an unwinding error is caught, or just returns the error as normal.
     pub fn submit(self) -> anyhow::Result<()> {
+        puffin::profile_function!();
         let command_buffer = self.inner.finish();
 
         match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
diff --git a/crates/dropbear-engine/src/input.rs b/crates/dropbear-engine/src/input.rs
index 819b935..3d918a2 100644
--- a/crates/dropbear-engine/src/input.rs
+++ b/crates/dropbear-engine/src/input.rs
@@ -196,6 +196,7 @@ impl Manager {
     }
 
     pub fn update(&mut self, gilrs: &mut Gilrs) {
+        puffin::profile_function!();
         self.just_pressed_keys.clear();
         self.just_released_keys.clear();
         self.just_pressed_mouse_buttons.clear();
@@ -259,6 +260,7 @@ impl Manager {
     }
 
     pub fn handle_controller_event(&mut self, event: gilrs::Event) {
+        puffin::profile_function!();
         for (name, handler) in self.controller_handlers.iter_mut() {
             if self.active_handlers.contains(name) {
                 match event.event {
@@ -299,6 +301,7 @@ impl Manager {
     }
 
     pub fn poll_controllers(&mut self, gilrs: &mut Gilrs) {
+        puffin::profile_function!();
         while let Some(event) = gilrs.next_event() {
             self.handle_controller_event(event);
         }
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index a2de0a3..5841f21 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -20,6 +20,7 @@ pub mod pipelines;
 pub mod mipmap;
 pub mod sky;
 pub mod features;
+pub mod animation;
 
 features! {
     pub mod build {
@@ -28,6 +29,12 @@ features! {
     }
 }
 
+features! {
+    pub mod graphics_features {
+        const SupportsStorage = 0b00000001
+    }
+}
+
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
 pub const PHYSICS_STEP_RATE: u32 = 120;
 const MAX_PHYSICS_STEPS_PER_FRAME: usize = 4;
@@ -76,13 +83,13 @@ use crate::scene::Scene;
 
 pub struct BindGroupLayouts {
     pub shader_globals_bind_group_layout: BindGroupLayout,
-    pub texture_bind_layout: BindGroupLayout,
-    pub material_tint_bind_layout: BindGroupLayout,
+    pub material_bind_layout: BindGroupLayout,
     pub camera_bind_group_layout: BindGroupLayout,
     pub light_bind_group_layout: BindGroupLayout,
     pub light_array_bind_group_layout: BindGroupLayout,
     pub light_cube_bind_group_layout: BindGroupLayout,
     pub environment_bind_group_layout: BindGroupLayout,
+    pub skinning_bind_group_layout: BindGroupLayout,
 }
 
 /// The backend information, such as the device, queue, config, surface, renderer, window and more.
@@ -90,7 +97,6 @@ pub struct State {
     // keep top for drop order
     pub window: Arc<Window>,
     pub instance: Arc<Instance>,
-    pub supports_storage: bool,
 
     pub surface: Arc<Surface<'static>>,
     pub surface_format: TextureFormat,
@@ -115,25 +121,6 @@ pub struct State {
 }
 
 impl State {
-    /// As defined in `shaders.wgsl` as
-    /// ```
-    /// @group(3) @binding(0)
-    /// var<uniform> u_material: MaterialUniform;
-    /// ```
-    const MATERIAL_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = wgpu::BindGroupLayoutDescriptor {
-        entries: &[wgpu::BindGroupLayoutEntry {
-            binding: 0,
-            visibility: wgpu::ShaderStages::FRAGMENT,
-            ty: wgpu::BindingType::Buffer {
-                ty: wgpu::BufferBindingType::Uniform,
-                has_dynamic_offset: false,
-                min_binding_size: None,
-            },
-            count: None,
-        }],
-        label: Some("material bind group layout"),
-    };
-
     /// Asynchronously initialised the state and sets up the backend and surface for wgpu to render to.
     pub async fn new(window: Arc<Window>, instance: Arc<Instance>, future_queue: Arc<FutureQueue>) -> anyhow::Result<Self> {
         let title = window.title();
@@ -178,8 +165,12 @@ impl State {
             .flags
             .contains(wgpu::DownlevelFlags::VERTEX_STORAGE)
             && device.limits().max_storage_buffers_per_shader_stage > 0;
+
+        if supports_storage_resources {
+            graphics_features::enable(graphics_features::SupportsStorage);
+        }
         
-        log::debug!("graphics device {} support storage resources", if !supports_storage_resources { "does not" } else { "does" });
+        log::debug!("graphics device {} support storage resources", if !supports_storage_resources { "DOES NOT" } else { "DOES" });
 
         if WGPU_BACKEND.get().is_none() {
             let info = adapter.get_info();
@@ -293,11 +284,61 @@ Hardware:
                 label: Some("Per-Light Layout"),
             });
         
-        // shaders/shader.wgsl - @group(0)
-        let texture_bind_group_layout =
-            device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT);
-        
         // shaders/shader.wgsl - @group(2)
+        let material_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("material_bind_layout"),
+            entries: &[
+                // t_diffuse
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        multisampled: false,
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                    },
+                    count: None,
+                },
+                // s_diffuse
+                wgpu::BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                    count: None,
+                },
+                // t_normal
+                wgpu::BindGroupLayoutEntry {
+                    binding: 2,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        multisampled: false,
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                    },
+                    count: None,
+                },
+                // s_normal
+                wgpu::BindGroupLayoutEntry {
+                    binding: 3,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                    count: None,
+                },
+                // u_material
+                wgpu::BindGroupLayoutEntry {
+                    binding: 4,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
+            ],
+        });
+        
+        // shaders/shader.wgsl - @group(1)
         let light_array_bind_group_layout = device.create_bind_group_layout(
             &wgpu::BindGroupLayoutDescriptor {
                 entries: &[
@@ -306,13 +347,7 @@ Hardware:
                         binding: 0,
                         visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
                         ty: wgpu::BindingType::Buffer {
-                            ty: if supports_storage_resources {
-                                // s_light_array
-                                wgpu::BufferBindingType::Storage { read_only: true }
-                            } else {
-                                // u_light_array
-                                wgpu::BufferBindingType::Uniform
-                            },
+                            ty: wgpu::BufferBindingType::Storage { read_only: true },
                             has_dynamic_offset: false,
                             min_binding_size: None,
                         },
@@ -324,10 +359,6 @@ Hardware:
         );
 
         // shaders/shader.wgsl - @group(3)
-        let material_tint_bind_group_layout =
-            device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT);
-
-        // shaders/shader.wgsl - @group(4)
         let shader_globals_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
             label: Some("shader.wgsl globals bind group layout"),
             entries: &[
@@ -369,7 +400,7 @@ Hardware:
 
         let environment_bind_group_layout =
             device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("environment_layout"),
+                label: Some("environment bind group layout"),
                 entries: &[
                     wgpu::BindGroupLayoutEntry {
                         binding: 0,
@@ -390,6 +421,20 @@ Hardware:
                 ],
             });
 
+        let skinning_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("skinning bind group layout"),
+            entries: &[wgpu::BindGroupLayoutEntry {
+                binding: 0,
+                visibility: wgpu::ShaderStages::VERTEX,
+                ty: wgpu::BindingType::Buffer {
+                    ty: wgpu::BufferBindingType::Storage { read_only: true },
+                    has_dynamic_offset: false,
+                    min_binding_size: None,
+                },
+                count: None,
+            }],
+        });
+
         let result = Self {
             surface: Arc::new(surface),
             surface_format,
@@ -409,15 +454,14 @@ Hardware:
             scene_manager: scene::Manager::new(),
             layouts: Arc::new(BindGroupLayouts {
                 shader_globals_bind_group_layout,
-                texture_bind_layout: texture_bind_group_layout,
-                material_tint_bind_layout: material_tint_bind_group_layout,
+                material_bind_layout,
                 camera_bind_group_layout,
                 light_bind_group_layout,
                 light_array_bind_group_layout,
                 light_cube_bind_group_layout,
                 environment_bind_group_layout,
+                skinning_bind_group_layout,
             }),
-            supports_storage: supports_storage_resources,
             // yakui_renderer,
             // yakui_texture,
             hdr,
@@ -462,6 +506,7 @@ Hardware:
         event_loop: &ActiveEventLoop,
         graphics: Arc<SharedGraphicsContext>,
     ) -> anyhow::Result<Vec<scene::SceneCommand>> {
+        puffin::profile_function!();
         if !self.is_surface_configured {
             return Ok(Vec::new());
         }
@@ -511,6 +556,7 @@ Hardware:
             });
 
         { // ensures clearing of the encoder is done correctly. 
+            puffin::profile_scope!("surface clear");
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("surface clear render encoder"));
 
             {
@@ -916,6 +962,12 @@ impl App {
     fn new(app_data: AppInfo, future_queue: Option<Arc<FutureQueue>>) -> Self {
         if build::is_enabled(build::Debug) {
             puffin::set_scopes_on(true);
+
+            if let Err(e) = puffin_http::Server::new("127.0.0.1:8585") {
+                log::error!("Unable to start puffin http server: {}", e);
+            } else {
+                log::info!("Started puffin http server at \"127.0.0.1:8585\"");
+            };
         }
 
         let instance = Arc::new(Instance::new(&wgpu::InstanceDescriptor {
@@ -953,6 +1005,8 @@ impl App {
 
     /// Creates a new window and adds it to its internal window manager (its really just a hashmap).
     pub fn create_window(&mut self, event_loop: &ActiveEventLoop, attribs: WindowAttributes) -> anyhow::Result<WindowId> {
+        puffin::profile_function!("load wgpu state");
+
         let window = Arc::new(
             event_loop.create_window(attribs)?
         );
@@ -1092,7 +1146,7 @@ impl ApplicationHandler for App {
                 puffin::GlobalProfiler::lock().new_frame();
 
                 let frame_start = Instant::now();
-
+                
                 let active_handlers = state.scene_manager.get_active_input_handlers();
                 self.input_manager.set_active_handlers(active_handlers);
 
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 33c7115..ce76cd5 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -12,6 +12,9 @@ use glam::{DMat4, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
 use wgpu::{BindGroup};
+use crate::asset::{Handle, ASSET_REGISTRY};
+use crate::model::Material;
+use crate::procedural::{ProcObj, ProcedurallyGeneratedObject};
 
 pub const MAX_LIGHTS: usize = 10;
 
@@ -240,7 +243,7 @@ impl LightComponent {
 #[derive(Clone)]
 pub struct Light {
     pub uniform: LightUniform,
-    pub cube_model: Arc<Model>,
+    pub cube_model: Handle<Model>,
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
     pub bind_group: BindGroup,
@@ -270,6 +273,7 @@ impl Light {
         transform: Transform,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!();
         let forward = DVec3::new(0.0, 0.0, -1.0);
         let direction = transform.rotation * forward;
 
@@ -288,15 +292,13 @@ impl Light {
 
         log::trace!("Created new light uniform");
 
-        let cube_model = Model::load_from_memory(
-            graphics.clone(),
-            include_bytes!("../../../resources/models/cube.glb").to_vec(),
-            label,
-            None
-        )
-        .await
-        .expect("failed to load light cube model")
-        .get();
+        let cube_model = ProcedurallyGeneratedObject::cuboid(DVec3::ONE)
+            .build_model(
+                graphics.clone(),
+                None,
+                None,
+                ASSET_REGISTRY.clone()
+            );
 
         let label_str = label.unwrap_or("Light").to_string();
 
@@ -337,6 +339,7 @@ impl Light {
     }
 
     pub fn update(&mut self, graphics: &SharedGraphicsContext, light: &mut LightComponent, transform: &Transform) {
+        puffin::profile_function!();
         self.uniform.position = dvec3_to_uniform_array(transform.position);
 
         let forward = DVec3::new(0.0, 0.0, -1.0);
@@ -359,8 +362,8 @@ impl Light {
         &self.uniform
     }
 
-    pub fn model(&self) -> &Model {
-        &self.cube_model
+    pub fn model(&self) -> Handle<Model> {
+        self.cube_model
     }
 
     pub fn label(&self) -> &str {
diff --git a/crates/dropbear-engine/src/mipmap.rs b/crates/dropbear-engine/src/mipmap.rs
index 2f7b994..9ca57e2 100644
--- a/crates/dropbear-engine/src/mipmap.rs
+++ b/crates/dropbear-engine/src/mipmap.rs
@@ -12,6 +12,7 @@ pub struct MipMapper {
 
 impl MipMapper {
     pub fn new(device: &wgpu::Device) -> Self {
+        puffin::profile_function!();
         let blit_shader = device.create_shader_module(slank::CompiledSlangShader::from_bytes(
             "mipmap blit_shader", 
             include_slang!("blit_shader")
@@ -126,6 +127,7 @@ impl MipMapper {
         queue: &wgpu::Queue,
         texture: &Texture,
     ) -> anyhow::Result<()> {
+        puffin::profile_function!();
         let texture = &texture.texture;
 
         match texture.format() {
@@ -273,6 +275,7 @@ impl MipMapper {
         queue: &wgpu::Queue,
         texture: &Texture,
     ) -> anyhow::Result<()> {
+        puffin::profile_function!();
         let texture = &texture.texture;
 
         match texture.format() {
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 93d69f7..58837b3 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 image::GenericImageView;
+use parking_lot::{Mutex, RwLock};
 use rayon::prelude::*;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
@@ -16,178 +15,126 @@ 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 puffin::profile_scope;
 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(crate) hash: u64, // also the id related to the handle
     pub label: String,
     pub path: ResourceReference,
     pub meshes: Vec<Mesh>,
     pub materials: Vec<Material>,
-    pub id: ModelId,
+    pub skins: Vec<Skin>,
+    pub animations: Vec<Animation>,
+    pub nodes: Vec<Node>,
 }
 
-/// 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>,
+pub struct Mesh {
+    pub name: String,
+    pub vertex_buffer: wgpu::Buffer,
+    pub index_buffer: wgpu::Buffer,
+    pub num_elements: u32,
+    pub material: usize,
+    pub vertices: Vec<ModelVertex>,
 }
 
-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)
-    }
+#[derive(Clone)]
+pub struct Material {
+    pub name: String,
+    pub diffuse_texture: Texture,
+    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 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 Deref for SharedShape {
-    type Target = Model;
-
-    fn deref(&self) -> &Self::Target {
-        self.model.as_ref()
-    }
+/// Represents a node in the scene graph (can be a joint/bone or a mesh)
+#[derive(Clone, Debug)]
+pub struct Node {
+    pub name: String,
+    pub parent: Option<usize>,
+    pub children: Vec<usize>,
+    pub transform: NodeTransform,
 }
 
-#[derive(Clone)]
-pub struct LoadedModel {
-    pub(crate) inner: Arc<SharedShape>,
-    handle: AssetHandle,
+/// Local transform of a node relative to its parent
+#[derive(Clone, Debug)]
+pub struct NodeTransform {
+    pub translation: glam::Vec3,
+    pub rotation: glam::Quat,
+    pub scale: glam::Vec3,
 }
 
-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
+impl NodeTransform {
+    pub fn to_matrix(&self) -> glam::Mat4 {
+        glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
     }
 
-    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 identity() -> Self {
+        Self {
+            translation: glam::Vec3::ZERO,
+            rotation: glam::Quat::IDENTITY,
+            scale: glam::Vec3::ONE,
+        }
     }
+}
 
-    pub fn contains_material_reference_raw(
-        &self,
-        registry: &AssetRegistry,
-        reference: &ResourceReference,
-    ) -> bool {
-        self.inner
-            .contains_material_reference_raw(registry, reference)
-    }
+/// A skin defines how a mesh is bound to a skeleton
+#[derive(Clone)]
+pub struct Skin {
+    pub name: String,
+    /// Indices of joints (nodes) in the Model's nodes array
+    pub joints: Vec<usize>,
+    /// Inverse bind matrices - one per joint
+    pub inverse_bind_matrices: Vec<glam::Mat4>,
+    /// Optional root joint index
+    pub skeleton_root: Option<usize>,
 }
 
-impl std::ops::Deref for LoadedModel {
-    type Target = Model;
+/// An animation that can be played on a skeleton
+#[derive(Clone)]
+pub struct Animation {
+    pub name: String,
+    pub channels: Vec<AnimationChannel>,
+    pub duration: f32,
+}
 
-    fn deref(&self) -> &Self::Target {
-        self.inner.deref()
-    }
+/// Describes how an animation affects a specific node
+#[derive(Clone)]
+pub struct AnimationChannel {
+    /// Target node index in the Model's nodes array
+    pub target_node: usize,
+    /// Keyframe times
+    pub times: Vec<f32>,
+    /// Animation data
+    pub values: ChannelValues,
+    /// Interpolation method
+    pub interpolation: AnimationInterpolation,
 }
 
 #[derive(Clone)]
-pub struct Material {
-    pub name: String,
-    pub diffuse_texture: Texture,
-    pub normal_texture: Texture,
-    pub bind_group: wgpu::BindGroup,
-    pub tint: [f32; 4],
-    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 enum ChannelValues {
+    Translations(Vec<glam::Vec3>),
+    Rotations(Vec<glam::Quat>),
+    Scales(Vec<glam::Vec3>),
 }
 
 impl Material {
@@ -196,39 +143,30 @@ 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();
+        puffin::profile_function!();
+        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 {
-            layout: &graphics.layouts.material_tint_bind_layout,
-            entries: &[wgpu::BindGroupEntry {
-                binding: 0,
-                resource: tint_buffer.buffer().as_entire_binding(),
-            }],
-            label: Some("material tint bind group"),
-        });
-
-        let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &name);
+        
+        let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &tint_buffer, &name);
 
         Self {
             name,
@@ -236,11 +174,21 @@ 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,
         }
     }
 
@@ -248,376 +196,748 @@ impl Material {
         graphics: &SharedGraphicsContext,
         diffuse: &Texture,
         normal: &Texture,
+        uniform_buffer: &UniformBuffer<MaterialUniform>,
         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),
-                        },
-                    ],
-                })
+        puffin::profile_function!();
+        graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some(format!("{} texture bind group", name).as_str()),
+            layout: &graphics.layouts.material_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),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 4,
+                    resource: uniform_buffer.buffer().as_entire_binding(),
+                },
+            ],
+        })
     }
 
-    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(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AnimationInterpolation {
+    /// The animated values are linearly interpolated between keyframes
+    Linear,
+    /// The animated values remain constant between keyframes
+    Step,
+    /// The animated values are interpolated using a cubic spline
+    CubicSpline,
 }
 
-#[derive(Clone)]
-pub struct Mesh {
-    pub name: String,
-    pub vertex_buffer: wgpu::Buffer,
-    pub index_buffer: wgpu::Buffer,
-    pub num_elements: u32,
-    pub material: usize,
-    pub vertices: Vec<ModelVertex>,
+struct GLTFTextureInformation {
+    sampler: wgpu::SamplerDescriptor<'static>,
+    pixels: Vec<u8>,
+    width: u32,
+    height: u32,
+    #[allow(dead_code)]
+    mip_level_count: u32,
+    #[allow(dead_code)]
+    format: wgpu::TextureFormat,
 }
 
-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
-        }
-    }
+impl GLTFTextureInformation {
+    fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation {
+        let sampler = tex.sampler();
 
-    /// 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
+        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,
+            }
         }
-    }
 
-    /// 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)
-    }
+        let sampler = 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 the registered asset handle for this model, if available.
-    pub fn asset_handle(&self) -> Option<AssetHandle> {
-        self.asset_handle_raw(&ASSET_REGISTRY)
-    }
+        let image_index = tex.source().index();
+        let image_data = &images[image_index];
 
-    pub fn asset_handle_raw(&self, registry: &AssetRegistry) -> Option<AssetHandle> {
-        registry.model_handle_from_reference(&self.path)
-    }
+        let width = image_data.width;
+        let height = image_data.height;
 
-    /// Returns `true` if this model was loaded from the specified resource reference.
-    pub fn matches_resource(&self, reference: &ResourceReference) -> bool {
-        &self.path == reference
-    }
+        let mip_level_count = (width.max(height) as f32).log2().floor() as u32 + 1;
 
-    /// 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 (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"),
+        };
 
-    pub fn contains_material_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        material_handle: AssetHandle,
-    ) -> bool {
-        registry.material_owner(material_handle) == Some(self.id)
+        GLTFTextureInformation {
+            sampler,
+            mip_level_count,
+            pixels,
+            format,
+            width,
+            height,
+        }
     }
+}
 
-    /// 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)
-    }
+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]>,
+}
 
-    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)
-            })
-    }
+struct GLTFMaterialInformation {
+    name: String,
+    diffuse_texture: Option<GLTFTextureInformation>,
+    normal_texture: Option<GLTFTextureInformation>,
+    emissive_texture: Option<GLTFTextureInformation>,
+    metallic_roughness_texture: Option<GLTFTextureInformation>,
+    occlusion_texture: Option<GLTFTextureInformation>,
+    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,
+}
 
-    /// 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))
-    }
+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,
+}
 
-    /// 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)
-    }
+impl Model {
+    fn load_materials(
+        gltf: &gltf::Document,
+        _buffers: &Vec<gltf::buffer::Data>,
+        images: &Vec<gltf::image::Data>,
+    ) -> Vec<GLTFMaterialInformation> {
+        puffin::profile_function!();
+        let process_texture = |texture: gltf::Texture<'_>| -> Option<GLTFTextureInformation> {
+            puffin::profile_scope!("reading texture bytes", texture.name().unwrap_or("Unnamed Texture"));
+            Some(GLTFTextureInformation::fetch(&texture, images))
+        };
 
-    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
-    }
+        let mut material_data = Vec::new();
 
-    pub async fn load_from_memory_raw<B>(
-        graphics: Arc<SharedGraphicsContext>,
-        buffer: B,
-        label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel>
-    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
-        );
+        for material in gltf.materials() {
+            let material_name = material.name().unwrap_or("Unnamed Material").to_string();
+            puffin::profile_scope!("loading material", &material_name);
 
-        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 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_texture_info = diffuse_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+            let metallic_roughness_texture_info = metallic_roughness_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+
+            let normal_texture_info = normal_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+            let occlusion_texture_info = occlusion_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+            let emissive_texture_info = emissive_texture
+                .as_ref()
+                .and_then(|info| process_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_texture: diffuse_texture_info,
+                normal_texture: normal_texture_info,
+                emissive_texture: emissive_texture_info,
+                metallic_roughness_texture: metallic_roughness_texture_info,
+                occlusion_texture: occlusion_texture_info,
+                tint,
+                emissive_factor,
+                metallic_factor,
+                roughness_factor,
+                alpha_mode,
+                alpha_cutoff,
+                double_sided,
+                occlusion_strength,
+                normal_scale,
+            });
         }
 
-        log::trace!(
-            "========== Benchmarking speed of loading {:?} ==========",
-            label
-        );
-        log::debug!("Loading from memory");
-        let res_ref = ResourceReference::from_bytes(buffer.as_ref());
+        if material_data.is_empty() {
+            material_data.push(GLTFMaterialInformation {
+                name: "Default".to_string(),
+                diffuse_texture: None,
+                normal_texture: None,
+                emissive_texture: None,
+                metallic_roughness_texture: None,
+                occlusion_texture: 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,
+            });
+        }
 
-        let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?;
-        let mut meshes = Vec::new();
+        material_data
+    }
 
-        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();
+    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();
+        puffin::profile_function!(&mesh_name);
+
+        for (primitive_index, primitive) in mesh.primitives().enumerate() {
+            puffin::profile_scope!("reading primitive", &format!("{}[{}]", &mesh_name, primitive_index));
+            
+            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
+                    ))
+                }
+            };
 
-            let tint = material
-                .pbr_metallic_roughness()
-                .base_color_factor();
+            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,
+            });
+        }
 
-            let tint = [tint[0], tint[1], tint[2], tint[3]];
+        Ok(())
+    }
 
-            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
+    fn load_nodes(gltf: &gltf::Document) -> Vec<Node> {
+        puffin::profile_function!("loading nodes");
+        let mut nodes = Vec::new();
+        
+        for node in gltf.nodes() {
+            profile_scope!("reading node", node.name().unwrap_or("Unnamed Node"));
+            let (translation, rotation, scale) = node.transform().decomposed();
+            
+            let transform = NodeTransform {
+                translation: glam::Vec3::from(translation),
+                rotation: glam::Quat::from_array(rotation),
+                scale: glam::Vec3::from(scale),
+            };
+            
+            nodes.push(Node {
+                name: node.name().unwrap_or("Unnamed Node").to_string(),
+                parent: None,
+                children: node.children().map(|n| n.index()).collect(),
+                transform,
+            });
+        }
+        
+        for (node_index, node) in gltf.nodes().enumerate() {
+            profile_scope!("second pass enumerating children", node.name().unwrap_or("Unnamed Node"));
+            for child in node.children() {
+                if let Some(child_node) = nodes.get_mut(child.index()) {
+                    child_node.parent = Some(node_index);
+                }
+            }
+        }
+        
+        nodes
+    }
+
+    fn load_skins(gltf: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec<Skin> {
+        puffin::profile_function!("loading skins");
+        let mut skins = Vec::new();
+        
+        for skin in gltf.skins() {
+            puffin::profile_scope!("reading skin", skin.name().unwrap_or("Unnamed Skin"));
+            let joints: Vec<usize> = skin.joints().map(|j| j.index()).collect();
+            
+            let inverse_bind_matrices = if let Some(accessor) = skin.inverse_bind_matrices() {
+                let view = accessor.view().expect("Accessor must have a buffer view");
+                let buffer_data = &buffers[view.buffer().index()];
+                let start = view.offset() + accessor.offset();
+                let stride = view.stride().unwrap_or(accessor.size());
+                
+                let mut matrices = Vec::with_capacity(accessor.count());
+                for i in 0..accessor.count() {
+                    let offset = start + i * stride;
+                    let matrix_bytes = &buffer_data[offset..offset + 64];
+                    
+                    let mut floats = [0f32; 16];
+                    for (j, chunk) in matrix_bytes.chunks_exact(4).enumerate() {
+                        floats[j] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
                     }
+                    
+                    matrices.push(glam::Mat4::from_cols_array(&floats));
                 }
+                matrices
             } else {
-                None
+                vec![glam::Mat4::IDENTITY; joints.len()]
             };
+            
+            skins.push(Skin {
+                name: skin.name().unwrap_or("Unnamed Skin").to_string(),
+                joints,
+                inverse_bind_matrices,
+                skeleton_root: skin.skeleton().map(|n| n.index()),
+            });
+        }
+        
+        skins
+    }
 
-            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())
+    fn load_animations(gltf: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec<Animation> {
+        puffin::profile_function!("loading animations");
+        let mut animations = Vec::new();
+        
+        for animation in gltf.animations() {
+            puffin::profile_scope!("reading animation", animation.name().unwrap_or("Unnamed Animation"));
+            let mut channels = Vec::new();
+            let mut max_time = 0.0f32;
+            
+            for channel in animation.channels() {
+                let target = channel.target();
+                let reader = channel.reader(|buffer| Some(&buffers[buffer.index()]));
+                let interpolation_mode = channel.sampler().interpolation();
+                
+                let times: Vec<f32> = if let Some(inputs) = reader.read_inputs() {
+                    inputs.collect()
+                } else {
+                    continue;
+                };
+                
+                if let Some(&last_time) = times.last() {
+                    max_time = max_time.max(last_time);
+                }
+                
+                let values = match target.property() {
+                    gltf::animation::Property::Translation => {
+                        puffin::profile_scope!("reading translation values");
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::Translations(iter) => {
+                                    let translations: Vec<glam::Vec3> = iter
+                                        .map(|t| glam::Vec3::from(t))
+                                        .collect();
+                                    ChannelValues::Translations(translations)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
                     }
-                    gltf::image::Source::Uri { uri, mime_type: _ } => {
-                        log::warn!("External URI textures not supported: {}", uri);
-                        None
+                    gltf::animation::Property::Rotation => {
+                        puffin::profile_scope!("reading rotation values");
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::Rotations(iter) => {
+                                    let rotations: Vec<glam::Quat> = if interpolation_mode == gltf::animation::Interpolation::CubicSpline {
+                                        iter.into_f32()
+                                            .enumerate()
+                                            .map(|(i, r)| {
+                                                let q = glam::Quat::from_array(r);
+                                                if i % 3 == 1 {
+                                                    q.normalize()
+                                                } else {
+                                                    q
+                                                }
+                                            })
+                                            .collect()
+                                    } else {
+                                        iter.into_f32()
+                                            .map(|r| glam::Quat::from_array(r).normalize())
+                                            .collect()
+                                    };
+                                    ChannelValues::Rotations(rotations)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
                     }
-                }
+                    gltf::animation::Property::Scale => {
+                        puffin::profile_scope!("reading scale values");
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::Scales(iter) => {
+                                    let scales: Vec<glam::Vec3> = iter
+                                        .map(|s| glam::Vec3::from(s))
+                                        .collect();
+                                    ChannelValues::Scales(scales)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
+                    }
+                    gltf::animation::Property::MorphTargetWeights => {
+                        puffin::profile_scope!("reading morph target weights");
+                        // Skip morph targets for now
+                        continue;
+                    }
+                };
+                
+                let interpolation = match channel.sampler().interpolation() {
+                    gltf::animation::Interpolation::Linear => AnimationInterpolation::Linear,
+                    gltf::animation::Interpolation::Step => AnimationInterpolation::Step,
+                    gltf::animation::Interpolation::CubicSpline => AnimationInterpolation::CubicSpline,
+                };
+                
+                channels.push(AnimationChannel {
+                    target_node: target.node().index(),
+                    times,
+                    values,
+                    interpolation,
+                });
+            }
+            
+            animations.push(Animation {
+                name: animation.name().unwrap_or("Unnamed Animation").to_string(),
+                channels,
+                duration: max_time,
+            });
+        }
+        
+        animations
+    }
+
+    pub async fn load_from_memory_raw<B>(
+        graphics: Arc<SharedGraphicsContext>,
+        buffer: B,
+        label: Option<&str>,
+        registry: Arc<RwLock<AssetRegistry>>,
+    ) -> anyhow::Result<Handle<Model>>
+    where
+        B: AsRef<[u8]>,
+    {
+        puffin::profile_function!(label.unwrap_or("unlabelled model"));
+        let mut registry = registry.write();
+
+        let model_label = label.unwrap_or("No named model");
+        let hash = {
+            puffin::profile_scope!("hashing model");
+            let mut hasher = DefaultHasher::default();
+            if let Some(label) = label {
+                label.hash(&mut hasher);
             } else {
-                None
+                buffer.as_ref().hash(&mut hasher);
             };
+            hasher.finish()
+        };
 
-            texture_data.push((material_name, diffuse_bytes, normal_bytes, tint));
+        if let Some(model) = registry.model_handle_by_hash(hash) {
+            return Ok(model);
         }
 
-        if texture_data.is_empty() {
-            texture_data.push((
-                "Default".to_string(),
-                None,
-                None,
-                [1.0, 1.0, 1.0, 1.0],
-            ));
-        }
+        let (gltf, buffers, images) = gltf::import_slice(buffer.as_ref())?;
 
-        let parallel_start = Instant::now();
-        let processed_textures: Vec<_> = texture_data
-            .into_par_iter()
-            .map(|(material_name, diffuse_bytes, normal_bytes, tint)| {
-                let material_start = Instant::now();
-
-                let processed_diffuse = diffuse_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 diffuse texture for material '{}': {} ({} bytes)",
-                                material_name,
-                                err,
-                                bytes.len()
-                            );
-                            return None;
-                        }
-                    };
-                    log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed());
-
-                    let rgba_start = Instant::now();
-                    let rgba = image.to_rgba8();
-                    log::trace!(
-                        "Converting diffuse image to rgba8 took {:?}",
-                        rgba_start.elapsed()
-                    );
-
-                    let dimensions = image.dimensions();
-                    Some((rgba.into_raw(), dimensions))
-                });
+        let mut meshes = Vec::new();
+        for mesh in gltf.meshes() {
+            Self::load_meshes(&mesh, &buffers, &mut meshes)?;
+        }
 
-                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))
-                });
+        let nodes = Self::load_nodes(&gltf);
+        
+        let skins = Self::load_skins(&gltf, &buffers);
+        
+        let animations = Self::load_animations(&gltf, &buffers);
+        
+        log::debug!(
+            "Loaded {} nodes, {} skins, {} animations for model [{:?}]",
+            nodes.len(),
+            skins.len(),
+            animations.len(),
+            label
+        );
 
-                log::trace!(
-                    "Parallel processing of material '{}' took: {:?}",
-                    material_name,
-                    material_start.elapsed()
-                );
+        let material_data = Self::load_materials(&gltf, &buffers, &images);
 
-                (material_name, processed_diffuse, processed_normal, tint)
+        let processed_textures: Vec<ProcessedMaterialTextures> = material_data
+            .into_par_iter()
+            .map(|material_info| {
+                puffin::profile_scope!("processing material textures");
+                let material_name = material_info.name;
+
+                let extract = |info: Option<GLTFTextureInformation>| -> (Option<(Vec<u8>, (u32, u32))>, Option<wgpu::SamplerDescriptor<'static>>) {
+                    if let Some(info) = info {
+                         (Some((info.pixels, (info.width, info.height))), Some(info.sampler))
+                    } else {
+                         (None, None)
+                    }
+                };
+
+                let (processed_diffuse, diffuse_sampler) = extract(material_info.diffuse_texture);
+                let (processed_normal, normal_sampler) = extract(material_info.normal_texture);
+                let (processed_emissive, emissive_sampler) = extract(material_info.emissive_texture);
+                let (processed_metallic_roughness, metallic_roughness_sampler) = extract(material_info.metallic_roughness_texture);
+                let (processed_occlusion, occlusion_sampler) = extract(material_info.occlusion_texture);
+
+                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;
+
+                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();
 
-        log::trace!(
-            "Total parallel image processing took: {:?}",
-            parallel_start.elapsed()
-        );
-
         let mut materials = Vec::new();
 
         let grey_texture = registry.grey_texture(graphics.clone());
         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 {
-            let start = Instant::now();
+        for processed in processed_textures {
+            puffin::profile_scope!("creating material");
+
+            let material_name = processed.name;
+            let processed_diffuse = processed.diffuse;
+            let processed_normal = processed.normal;
+            let processed_emissive = processed.emissive;
+            let processed_metallic_roughness = processed.metallic_roughness;
+            let processed_occlusion = processed.occlusion;
+            let diffuse_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(
@@ -625,11 +945,13 @@ impl Model {
                     &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 +960,147 @@ 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,
-            ));
+            );
 
-            log::trace!("Time to create GPU texture: {:?}", start.elapsed());
+            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);
         }
 
-        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 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;
-                }
-
-                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 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 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 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],
                 });
             }
+
+            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();
-        }
+            skins,
+            animations,
+            nodes,
+        };
 
-        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> {
@@ -911,6 +1111,7 @@ pub trait DrawModel<'a> {
         material: &'a Material,
         camera_bind_group: &'a wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     );
     fn draw_mesh_instanced(
         &mut self,
@@ -919,6 +1120,7 @@ pub trait DrawModel<'a> {
         instances: Range<u32>,
         camera_bind_group: &'a wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     );
 
     #[allow(unused)]
@@ -947,8 +1149,9 @@ where
         material: &'b Material,
         camera_bind_group: &'b wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     ) {
-        self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group);
+        self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group, skin_bind_group);
     }
 
     fn draw_mesh_instanced(
@@ -958,13 +1161,18 @@ where
         instances: Range<u32>,
         camera_bind_group: &'b wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a 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, &material.bind_group, &[]);
         self.set_bind_group(1, camera_bind_group, &[]);
         self.set_bind_group(2, light_bind_group, &[]);
-        self.set_bind_group(3, &material.tint_bind_group, &[]);
+        
+        if let Some(skin_bg) = skin_bind_group {
+            self.set_bind_group(4, skin_bg, &[]);
+        }
+
         self.draw_indexed(0..mesh.num_elements, 0, instances);
     }
 
@@ -992,6 +1200,7 @@ where
                 instances.clone(),
                 camera_bind_group,
                 light_bind_group,
+                None, // Provide an AnimationComponent if available in a future update
             );
         }
     }
@@ -1083,50 +1292,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 +1300,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 +1328,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..d19b34e 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,
@@ -94,7 +93,7 @@ impl DropbearShaderPipeline for LightCubePipeline {
         let mut storage_buffer = None;
         let mut uniform_buffer = None;
 
-        if graphics.supports_storage {
+        if crate::graphics_features::is_enabled(crate::graphics_features::SupportsStorage) {
             storage_buffer = Some(StorageBuffer::new(
                 &graphics.device,
                 "light cube pipeline storage buffer",
@@ -231,7 +230,7 @@ impl Vertex for VertexInput {
     }
 }
 
-/// As mapped in `shaders/light.wgsl` as
+/// As mapped in `shaders/light.slang` as
 /// ```wgsl
 /// struct InstanceInput {
 ///     @location(5) model_matrix_0: vec4<f32>,
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
index 31b7be8..fb2135e 100644
--- a/crates/dropbear-engine/src/pipelines/shader.rs
+++ b/crates/dropbear-engine/src/pipelines/shader.rs
@@ -1,5 +1,6 @@
 use std::sync::Arc;
 use wgpu::{CompareFunction, DepthBiasState, StencilState};
+use crate::buffer::{StorageBuffer, UniformBuffer};
 use crate::graphics::{InstanceRaw, SharedGraphicsContext};
 use crate::model;
 use crate::model::Vertex;
@@ -23,11 +24,11 @@ impl DropbearShaderPipeline for MainRenderPipeline {
         );
 
         let bind_group_layouts = vec![
-            &graphics.layouts.texture_bind_layout, // @group(0)
+            &graphics.layouts.material_bind_layout, // @group(0)
             &graphics.layouts.camera_bind_group_layout, // @group(1)
             &graphics.layouts.light_array_bind_group_layout, // @group(2)
-            &graphics.layouts.material_tint_bind_layout, // @group(3)
-            &graphics.layouts.shader_globals_bind_group_layout, // @group(4)
+            &graphics.layouts.shader_globals_bind_group_layout, // @group(3)
+            &graphics.layouts.skinning_bind_group_layout, // @group(4)
         ];
 
         let pipeline_layout =
@@ -52,7 +53,7 @@ impl DropbearShaderPipeline for MainRenderPipeline {
                     },
                     fragment: Some(wgpu::FragmentState {
                         module: &shader.module,
-                        entry_point: if graphics.supports_storage {
+                        entry_point: if crate::graphics_features::is_enabled(crate::graphics_features::SupportsStorage) {
                             Some("s_fs_main")
                         } else {
                             Some("u_fs_main")
diff --git a/crates/dropbear-engine/src/procedural/cube.rs b/crates/dropbear-engine/src/procedural/cube.rs
index c31acfd..6dc4de9 100644
--- a/crates/dropbear-engine/src/procedural/cube.rs
+++ b/crates/dropbear-engine/src/procedural/cube.rs
@@ -5,48 +5,62 @@ impl ProcedurallyGeneratedObject {
     ///
     /// `size` is the full extents (width, height, depth).
     pub fn cuboid(size: glam::DVec3) -> Self {
+        puffin::profile_function!();
         let half = (size / 2.0).as_vec3();
 
         let uv_x = 1.0_f32;
         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..9c6f6f2 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -1,15 +1,14 @@
 //! 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 parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
 use wgpu::util::DeviceExt;
 
@@ -45,24 +44,9 @@ 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: Arc<RwLock<AssetRegistry>>,
+    ) -> Handle<Model> {
+        puffin::profile_function!();
         let mut hasher = DefaultHasher::new();
         hasher.write(bytemuck::cast_slice(&self.vertices));
         hasher.write(bytemuck::cast_slice(&self.indices));
@@ -72,11 +56,10 @@ 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);
+        let mut _rguard = registry.write();
+
+        if let Some(handle) = _rguard.model_handle_by_hash(hash) {
+            return handle;
         }
 
         let vertices = self.vertices;
@@ -108,31 +91,38 @@ 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 = _rguard.grey_texture(graphics.clone());
+            let flat_normal_handle =
+                _rguard.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
+            let grey = _rguard
+                .get_texture(grey_handle)
+                .expect("Grey texture handle missing")
+                .clone();
+            let flat_normal = _rguard
+                .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));
-        }
+            skins: Vec::new(),
+            animations: Vec::new(),
+            nodes: Vec::new(),
+        };
 
-        LoadedModel::new_raw(registry, model)
+        _rguard.add_model_with_label(label, model)
     }
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/scene.rs b/crates/dropbear-engine/src/scene.rs
index 4dc785c..b53e10b 100644
--- a/crates/dropbear-engine/src/scene.rs
+++ b/crates/dropbear-engine/src/scene.rs
@@ -93,17 +93,20 @@ impl Manager {
         graphics: Arc<SharedGraphicsContext>,
         event_loop: &ActiveEventLoop,
     ) -> Vec<SceneCommand> {
+        puffin::profile_function!();
         // transition scene
         if let Some(next_scene_name) = self.next_scene.take() {
             if let Some(current_scene_name) = &self.current_scene
                 && let Some(scene) = self.scenes.get_mut(current_scene_name)
             {
                 {
+                    puffin::profile_scope!("exit old scene", current_scene_name);
                     scene.write().exit(event_loop);
                 }
             }
             if let Some(scene) = self.scenes.get_mut(&next_scene_name) {
                 {
+                    puffin::profile_scope!("load new scene", &next_scene_name);
                     scene.write().load(graphics.clone());
                 }
             }
@@ -115,6 +118,7 @@ impl Manager {
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
             {
+                puffin::profile_scope!("update new scene", &scene_name);
                 scene.write().update(dt, graphics.clone());
             }
             let command = scene.write().run_command();
@@ -124,6 +128,7 @@ impl Manager {
                         if current == &target {
                             // reload the scene
                             if let Some(scene) = self.scenes.get_mut(current) {
+                                puffin::profile_scope!("reload the scene", &current);
                                 scene.write().exit(event_loop);
                                 scene.write().load(graphics.clone());
 
@@ -155,25 +160,31 @@ impl Manager {
         dt: f32,
         graphics: Arc<SharedGraphicsContext>,
     ) {
+        puffin::profile_function!();
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
+            puffin::profile_scope!("physics-update the scene", &scene_name);
             scene.write().physics_update(dt, graphics.clone())
         }
     }
 
     pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        puffin::profile_function!();
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
+            puffin::profile_scope!("render the scene", &scene_name);
             scene.write().render(graphics.clone())
         }
     }
 
     pub fn handle_event(&mut self, event: &WindowEvent) {
+        puffin::profile_function!();
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
+            puffin::profile_scope!("handle winit window event in the scene", &scene_name);
             scene.write().handle_event(event);
         }
     }
diff --git a/crates/dropbear-engine/src/shader.rs b/crates/dropbear-engine/src/shader.rs
index e955d17..5907024 100644
--- a/crates/dropbear-engine/src/shader.rs
+++ b/crates/dropbear-engine/src/shader.rs
@@ -39,6 +39,7 @@ impl Shader {
         shader_file_contents: &str,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!();
         let module = graphics
             .device
             .create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -61,6 +62,7 @@ impl Shader {
     }
 
     pub fn from_slang(graphics: Arc<SharedGraphicsContext>, shader: &CompiledSlangShader) -> Self {
+        puffin::profile_function!();
         let module = graphics
             .device
             .create_shader_module(shader.create_wgpu_shader());
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..233aff0 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>,
 }
 
@@ -40,38 +44,42 @@ var s_diffuse: sampler;
 var t_normal: texture_2d<f32>;
 @group(0) @binding(3)
 var s_normal: sampler;
+@group(0) @binding(4)
+var<uniform> u_material: MaterialUniform;
 
 @group(1) @binding(0)
 var<uniform> u_camera: CameraUniform;
 
 @group(2) @binding(0)
 var<storage, read> s_light_array: array<Light>;
-@group(2) @binding(0)
-var<uniform> u_light_array: array<Light, 10>; // when storage is not available
 
 @group(3) @binding(0)
-var<uniform> u_material: MaterialUniform;
+var<uniform> u_globals: Globals;
 
 @group(4) @binding(0)
-var<uniform> u_globals: Globals;
+var<storage, read> s_skinning: array<mat4x4<f32>>;
 
 struct InstanceInput {
-    @location(5) model_matrix_0: vec4<f32>,
-    @location(6) model_matrix_1: vec4<f32>,
-    @location(7) model_matrix_2: vec4<f32>,
-    @location(8) model_matrix_3: vec4<f32>,
-
-    @location(9) normal_matrix_0: vec3<f32>,
-    @location(10) normal_matrix_1: vec3<f32>,
-    @location(11) normal_matrix_2: vec3<f32>,
+    @location(8) model_matrix_0: vec4<f32>,
+    @location(9) model_matrix_1: vec4<f32>,
+    @location(10) model_matrix_2: vec4<f32>,
+    @location(11) model_matrix_3: vec4<f32>,
+
+    @location(12) normal_matrix_0: vec3<f32>,
+    @location(13) normal_matrix_1: vec3<f32>,
+    @location(14) normal_matrix_2: vec3<f32>,
 };
 
 struct VertexInput {
     @location(0) position: vec3<f32>,
-    @location(1) tex_coords: vec2<f32>,
-    @location(2) normal: vec3<f32>,
-    @location(3) tangent: vec3<f32>,
-    @location(4) bitangent: vec3<f32>,
+    @location(1) normal: vec3<f32>,
+    @location(2) tangent: vec4<f32>,
+    @location(3) tex_coords0: vec2<f32>,
+    @location(4) tex_coords1: vec2<f32>,
+    @location(5) colour0: vec4<f32>,
+
+    @location(6) joints: vec4<u32>,
+    @location(7) weights: vec4<f32>,
 };
 
 struct VertexOutput {
@@ -100,14 +108,36 @@ fn vs_main(
         instance.normal_matrix_2,
     );
 
-    let world_normal = normalize(normal_matrix * model.normal);
-    let world_tangent = normalize(normal_matrix * model.tangent);
-    let world_bitangent = normalize(normal_matrix * model.bitangent);
-    let world_position = model_matrix * vec4<f32>(model.position, 1.0);
+    var skin_matrix = mat4x4<f32>(
+        vec4<f32>(1.0, 0.0, 0.0, 0.0),
+        vec4<f32>(0.0, 1.0, 0.0, 0.0),
+        vec4<f32>(0.0, 0.0, 1.0, 0.0),
+        vec4<f32>(0.0, 0.0, 0.0, 1.0)
+    );
+
+    if (dot(model.weights, vec4<f32>(1.0)) > 0.0) {
+        let j = model.joints;
+        let w = model.weights;
+
+        skin_matrix =
+            (s_skinning[j.x] * w.x) +
+            (s_skinning[j.y] * w.y) +
+            (s_skinning[j.z] * w.z) +
+            (s_skinning[j.w] * w.w);
+    }
+
+    let world_position = model_matrix * skin_matrix * vec4<f32>(model.position, 1.0);
+    
+    let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz;
+    let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz;
+
+    let world_normal = normalize(normal_matrix * skin_normal);
+    let world_tangent = normalize(normal_matrix * skin_tangent);
+    let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w);
 
     var out: VertexOutput;
     out.clip_position = u_camera.view_proj * world_position;
-    out.tex_coords = model.tex_coords;
+    out.tex_coords = model.tex_coords0;
     out.world_normal = world_normal;
     out.world_position = world_position.xyz;
     out.world_tangent = world_tangent;
@@ -203,16 +233,15 @@ fn apply_normal_map(
     return normalize(tbn * normal_ts);
 }
 
-// when storage is supported
 @fragment
 fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
     let uv = in.tex_coords * u_material.uv_tiling;
     var tex_color = textureSample(t_diffuse, s_diffuse, uv);
     var object_normal = textureSample(t_normal, s_normal, uv);
 
-    let base_colour = tex_color * u_material.colour;
+    let base_colour = tex_color * u_material.base_colour;
 
-    if (base_colour.a < 0.1) {
+    if (base_colour.a < u_material.alpha_cutoff) {
         discard;
     }
 
@@ -245,44 +274,3 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
     return vec4<f32>(final_color, base_colour.a);
 }
 
-// when storage is NOT supported
-@fragment
-fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let uv = in.tex_coords * u_material.uv_tiling;
-    var tex_color = textureSample(t_diffuse, s_diffuse, uv);
-    var object_normal = textureSample(t_normal, s_normal, uv);
-
-    let base_colour = tex_color * u_material.colour;
-
-    if (base_colour.a < 0.1) {
-        discard;
-    }
-
-    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
-
-    let world_normal = apply_normal_map(
-        in.world_normal,
-        in.world_tangent,
-        in.world_bitangent,
-        object_normal.xyz,
-    );
-
-    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz;
-    var final_color = ambient;
-
-    for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) {
-        let light = u_light_array[i];
-
-        let light_type = i32(light.color.w + 0.1);
-
-        if (light_type == 0) {
-            final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position);
-        } else if (light_type == 1) {
-            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
-        } else if (light_type == 2) {
-            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz);
-        }
-    }
-
-    return vec4<f32>(final_color, base_colour.a);
-}
diff --git a/crates/dropbear-engine/src/sky.rs b/crates/dropbear-engine/src/sky.rs
index d0c7456..5451340 100644
--- a/crates/dropbear-engine/src/sky.rs
+++ b/crates/dropbear-engine/src/sky.rs
@@ -24,6 +24,7 @@ impl CubeTexture {
         mag_filter: wgpu::FilterMode,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!();
         let texture = device.create_texture(&wgpu::TextureDescriptor {
             label,
             size: wgpu::Extent3d {
@@ -81,6 +82,7 @@ pub struct HdrLoader {
 
 impl HdrLoader {
     pub fn new(device: &wgpu::Device) -> Self {
+        puffin::profile_function!();
         let module = device.create_shader_module(wgpu::include_wgsl!("shaders/equirectangular.wgsl"));
         let texture_format = wgpu::TextureFormat::Rgba32Float;
         let equirect_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@@ -139,6 +141,7 @@ impl HdrLoader {
         dst_size: u32,
         label: Option<&str>,
     ) -> anyhow::Result<CubeTexture> {
+        puffin::profile_function!();
         let loader = Self::new(device);
 
         let hdr_decoder = HdrDecoder::new(Cursor::new(data))?;
@@ -255,6 +258,7 @@ pub struct SkyPipeline {
 
 impl SkyPipeline {
     pub fn new(graphics: Arc<SharedGraphicsContext>, sky_texture: CubeTexture) -> Self {
+        puffin::profile_function!();
         let environment_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
             label: Some("environment_bind_group"),
             layout: &graphics.layouts.environment_bind_group_layout,
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 44c6ab5..4b1dd30 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 {
@@ -84,6 +85,7 @@ impl Texture {
         mag_filter: wgpu::FilterMode,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!(label.unwrap_or("create 2d texture"));
         let size = wgpu::Extent3d {
             width,
             height,
@@ -109,6 +111,7 @@ impl Texture {
         dimension: wgpu::TextureDimension,
         mag_filter: wgpu::FilterMode,
     ) -> Self {
+        puffin::profile_function!(label.unwrap_or("create texture"));
         let texture = device.create_texture(&wgpu::TextureDescriptor {
             label,
             size,
@@ -137,6 +140,7 @@ impl Texture {
             view,
             sampler,
             size,
+            hash: None,
         }
     }
 
@@ -146,6 +150,7 @@ impl Texture {
         device: &wgpu::Device,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!(label.unwrap_or("depth texture"));
         let size = wgpu::Extent3d {
             width: config.width.max(1),
             height: config.height.max(1),
@@ -184,6 +189,7 @@ impl Texture {
             size,
             view,
             label: label.to_potential_string(),
+            hash: None,
         }
     }
 
@@ -195,6 +201,7 @@ impl Texture {
         device: &wgpu::Device,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!(label.unwrap_or("viewport texture"));
         let size = wgpu::Extent3d {
             width: config.width.max(1),
             height: config.height.max(1),
@@ -222,6 +229,7 @@ impl Texture {
             sampler,
             size,
             view,
+            hash: None,
         }
     }
 
@@ -231,6 +239,7 @@ impl Texture {
         path: &PathBuf,
         label: Option<&str>,
     ) -> anyhow::Result<Self> {
+        puffin::profile_function!(label.unwrap_or(""));
         let data = fs::read(path)?;
         Ok(Self::from_bytes(graphics.clone(), &data, label))
     }
@@ -239,6 +248,7 @@ impl Texture {
     /// 
     /// If you want more customisability in the texture being generated, you can use [Self::from_bytes_verbose]
     pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8], label: Option<&str>) -> Self {
+        puffin::profile_function!(label.unwrap_or(""));
         Self::from_bytes_verbose_mipmapped(graphics, bytes, None, None, None, label)
     }
 
@@ -253,6 +263,7 @@ impl Texture {
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!(label.unwrap_or(""));
         let texture = Self::from_bytes_verbose(
             graphics.clone(),
             bytes,
@@ -285,21 +296,26 @@ impl Texture {
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
     ) -> Self {
-        let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) {
-            Ok(image) => {
-                let rgba = image.to_rgba8().into_raw();
-                let dims = dimensions.unwrap_or_else(|| image.dimensions());
-                (rgba, dims)
-            }
-            Err(err) => {
-                if let Some(dims) = dimensions {
-                    let expected_len = (dims.0 as usize)
-                        .saturating_mul(dims.1 as usize)
-                        .saturating_mul(4);
-                    if bytes.len() == expected_len {
-                        (bytes.to_vec(), dims)
-                    } else {
-                        log::error!(
+        puffin::profile_function!(label.unwrap_or(""));
+        let hash = AssetRegistry::hash_bytes(bytes);
+        
+        let (diffuse_rgba, dimensions) = {
+            puffin::profile_scope!("load from memory image");
+            match image::load_from_memory(bytes) {
+                Ok(image) => {
+                    let rgba = image.to_rgba8().into_raw();
+                    let dims = dimensions.unwrap_or_else(|| image.dimensions());
+                    (rgba, dims)
+                }
+                Err(err) => {
+                    if let Some(dims) = dimensions {
+                        let expected_len = (dims.0 as usize)
+                            .saturating_mul(dims.1 as usize)
+                            .saturating_mul(4);
+                        if bytes.len() == expected_len {
+                            (bytes.to_vec(), dims)
+                        } else {
+                            log::error!(
                             "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
                             label,
                             err,
@@ -308,15 +324,16 @@ impl Texture {
                             dims.1,
                             bytes.len()
                         );
-                        (vec![255, 0, 255, 255], (1, 1))
-                    }
-                } else {
-                    log::error!(
+                            (vec![255, 0, 255, 255], (1, 1))
+                        }
+                    } else {
+                        log::error!(
                         "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.",
                         label,
                         err
                     );
-                    (vec![255, 0, 255, 255], (1, 1))
+                        (vec![255, 0, 255, 255], (1, 1))
+                    }
                 }
             }
         };
@@ -349,6 +366,7 @@ impl Texture {
         debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize);
 
         if padded_bytes_per_row == unpadded_bytes_per_row {
+            puffin::profile_scope!("write to texture");
             graphics.queue.write_texture(
                 wgpu::TexelCopyTextureInfo {
                     texture: &texture,
@@ -365,6 +383,7 @@ impl Texture {
                 size,
             );
         } else {
+            puffin::profile_scope!("write to texture");
             let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize];
             let src_stride = unpadded_bytes_per_row as usize;
             let dst_stride = padded_bytes_per_row as usize;
@@ -416,6 +435,7 @@ impl Texture {
             sampler,
             size,
             view,
+            hash: Some(hash),
         }
     }
 
@@ -465,6 +485,7 @@ impl DropbearEngineLogo {
     /// 
     /// Returns (the bytes, width, height) in resp order. 
     pub fn generate() -> anyhow::Result<(Vec<u8>, u32, u32)> {
+        puffin::profile_function!("generate dropbear engine logo");
         let image = image::load_from_memory(Self::DROPBEAR_ENGINE_LOGO)?.into_rgba8();
         let (width, height) = image.dimensions();
         let rgba = image.into_raw();
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index fe07164..8a5acb9 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -152,12 +152,14 @@ impl ResourceReference {
 
     /// Creates a new `ResourceReference` from bytes
     pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
+        puffin::profile_function!();
         Self {
             ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()),
         }
     }
 
     pub fn from_reference(ref_type: ResourceReferenceType) -> Self {
+        puffin::profile_function!(format!("{:?}", ref_type));
         match ref_type {
             ResourceReferenceType::File(reference) => {
                 let canonical = canonicalize_euca_uri(&reference)
@@ -172,6 +174,7 @@ impl ResourceReference {
 
     /// Creates a [`ResourceReference`] directly from an euca URI (e.g. `euca://models/cube.glb`).
     pub fn from_euca_uri(uri: impl AsRef<str>) -> anyhow::Result<Self> {
+        puffin::profile_function!(uri.as_ref());
         let canonical = canonicalize_euca_uri(uri.as_ref())?;
         Ok(Self {
             ref_type: ResourceReferenceType::File(canonical),
@@ -212,6 +215,7 @@ impl ResourceReference {
     ///
     /// Returns `None` if the path doesn't contain "resources" or if the path after resources is empty.
     pub fn from_path(full_path: impl AsRef<Path>) -> anyhow::Result<Self> {
+        puffin::profile_function!(full_path.as_ref().display().to_string());
         let path = full_path.as_ref();
 
         let components: Vec<_> = path.components().collect();
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index 50a9e1f..e0af790 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -29,6 +29,7 @@ pub use dropbear_traits as traits;
 
 pub use egui;
 pub use rapier3d;
+use dropbear_engine::animation::AnimationComponent;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer};
 use dropbear_traits::registry::ComponentRegistry;
@@ -69,6 +70,7 @@ pub fn register_components(
     component_registry.register_with_default::<ColliderGroup>();
     component_registry.register_with_default::<KCC>();
     component_registry.register_with_default::<UIComponent>();
+    component_registry.register_with_default::<AnimationComponent>();
 
     component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>(
         |_, _, renderer| {
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index c52f982..11cbe5a 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -157,10 +157,13 @@ impl SceneConfig {
 
                     let model = std::sync::Arc::new(Model {
                         label: "None".to_string(),
+                        hash: *id,
                         path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }),
                         meshes: Vec::new(),
                         materials: Vec::new(),
-                        id: ModelId(*id),
+                        skins: Vec::new(),
+                        animations: Vec::new(),
+                        nodes: Vec::new(),
                     });
                     let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model);
                     MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 010711b..8910a14 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -571,7 +571,7 @@ impl Scene for Editor {
                     });
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
-                render_pass.set_bind_group(4, globals_bind_group, &[]);
+                render_pass.set_bind_group(3, globals_bind_group, &[]);
                 render_pass.draw_model_instanced(
                     &model,
                     0..instance_count,
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 3007a74..763150a 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -552,10 +552,13 @@ impl SignalController for Editor {
 
                     let model = std::sync::Arc::new(Model {
                         label: "None".to_string(),
+                        hash: unassigned_id,
                         path: reference,
                         meshes: Vec::new(),
                         materials: Vec::new(),
-                        id: ModelId(unassigned_id),
+                        skins: Vec::new(),
+                        animations: Vec::new(),
+                        nodes: Vec::new(),
                     });
 
                     let loaded_model = LoadedModel::new_raw(
@@ -625,10 +628,13 @@ impl SignalController for Editor {
 
                 let model = std::sync::Arc::new(Model {
                     label: "None".to_string(),
+                    hash: unassigned_id,
                     path: reference,
                     meshes: Vec::new(),
                     materials: Vec::new(),
-                    id: ModelId(unassigned_id),
+                    skins: Vec::new(),
+                    animations: Vec::new(),
+                    nodes: Vec::new(),
                 });
                 let loaded_model = LoadedModel::new_raw(&dropbear_engine::asset::ASSET_REGISTRY, model);
 
diff --git a/crates/eucalyptus-editor/src/spawn.rs b/crates/eucalyptus-editor/src/spawn.rs
index 008568b..91d3e2a 100644
--- a/crates/eucalyptus-editor/src/spawn.rs
+++ b/crates/eucalyptus-editor/src/spawn.rs
@@ -280,10 +280,13 @@ async fn load_renderer_from_serialized(
         ResourceReferenceType::Unassigned { id } => {
             let model = std::sync::Arc::new(Model {
                 label: "None".to_string(),
+                hash: *id,
                 path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }),
                 meshes: Vec::new(),
                 materials: Vec::new(),
-                id: ModelId(*id),
+                skins: Vec::new(),
+                animations: Vec::new(),
+                nodes: Vec::new(),
             });
 
             let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model);
diff --git a/crates/kino-ui/src/camera.rs b/crates/kino-ui/src/camera.rs
index f1b9753..4b1c675 100644
--- a/crates/kino-ui/src/camera.rs
+++ b/crates/kino-ui/src/camera.rs
@@ -1,5 +1,5 @@
-use bytemuck::{Pod, Zeroable};
-use glam::{Mat4, Vec2};
+    use bytemuck::{Pod, Zeroable};
+use glam::{Mat4, Vec2, Vec3};
 use crate::math::Rect;
 
 pub struct Camera2D {
@@ -19,15 +19,24 @@ impl Default for Camera2D {
 impl Camera2D {
     /// Returns the orthographic view-projection matrix for the current camera state
     pub fn view_proj(&self, screen_size: Vec2) -> Mat4 {
-        let width = screen_size.x / self.zoom;
-        let height = screen_size.y / self.zoom;
+        let (width, height) = (screen_size.x, screen_size.y);
 
-        let left = self.position.x;
-        let right = self.position.x + width;
-        let top = self.position.y;
-        let bottom = self.position.y + height;
+        let view = Mat4::look_at_rh(
+            self.position.extend(1.0),
+            self.position.extend(0.0),
+            Vec3::Y,
+        );
 
-        Mat4::orthographic_lh(left, right, bottom, top, -1.0, 1.0)
+        let proj = Mat4::orthographic_rh(
+            0.0,
+            width,
+            height,
+            0.0,
+            -1.0,
+            1.0,
+        );
+
+        proj * view
     }
 
     /// Set the camera's position (top-left corner of view)
diff --git a/crates/kino-ui/src/widgets/layout.rs b/crates/kino-ui/src/widgets/layout.rs
new file mode 100644
index 0000000..5f17de2
--- /dev/null
+++ b/crates/kino-ui/src/widgets/layout.rs
@@ -0,0 +1,218 @@
+// In widgets/layout.rs
+
+use std::any::Any;
+use glam::{vec2, Vec2};
+use crate::{KinoState, UiNode, UiInstructionType, ContaineredWidgetType, WidgetId};
+use crate::widgets::{Anchor, ContaineredWidget};
+
+fn calculate_node_size(node: &UiNode) -> Vec2 {
+    match &node.instruction {
+        UiInstructionType::Widget(widget) => widget.size(),
+        UiInstructionType::Containered(ContaineredWidgetType::Start { widget, .. }) => {
+            if let Some(row) = widget.as_any().downcast_ref::<Row>() {
+                calculate_row_size(&node.children, row.spacing)
+            } else if let Some(column) = widget.as_any().downcast_ref::<Column>() {
+                calculate_column_size(&node.children, column.spacing)
+            } else {
+                Vec2::ZERO
+            }
+        }
+        _ => Vec2::ZERO,
+    }
+}
+
+fn calculate_row_size(children: &[UiNode], spacing: f32) -> Vec2 {
+    if children.is_empty() {
+        return Vec2::ZERO;
+    }
+
+    let mut total_width = 0.0;
+    let mut max_height: f32 = 0.0;
+
+    for (i, child) in children.iter().enumerate() {
+        let child_size = calculate_node_size(child);
+        total_width += child_size.x;
+        max_height = max_height.max(child_size.y);
+
+        if i < children.len() - 1 {
+            total_width += spacing;
+        }
+    }
+
+    vec2(total_width, max_height)
+}
+
+fn calculate_column_size(children: &[UiNode], spacing: f32) -> Vec2 {
+    if children.is_empty() {
+        return Vec2::ZERO;
+    }
+
+    let mut total_height = 0.0;
+    let mut max_width: f32 = 0.0;
+
+    for (i, child) in children.iter().enumerate() {
+        let child_size = calculate_node_size(child);
+        total_height += child_size.y;
+        max_width = max_width.max(child_size.x);
+
+        if i < children.len() - 1 {
+            total_height += spacing;
+        }
+    }
+
+    vec2(max_width, total_height)
+}
+
+pub struct Row {
+    pub id: WidgetId,
+    pub anchor: Anchor,
+    pub position: Vec2,
+    pub spacing: f32,
+}
+
+impl Row {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            spacing: 8.0,
+        }
+    }
+
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+}
+
+impl ContaineredWidget for Row {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let total_size = calculate_row_size(&children, self.spacing);
+
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => vec2(-total_size.x / 2.0, 0.0),
+        };
+
+        let start_pos = self.position + offset;
+        let mut x_offset = 0.0;
+
+        for child in children {
+            let child_size = calculate_node_size(&child);
+
+            state.push_container(crate::math::Rect::new(
+                start_pos + vec2(x_offset, 0.0),
+                vec2(child_size.x, total_size.y)
+            ));
+
+            state.render_tree(vec![child]);
+            state.pop_container();
+
+            x_offset += child_size.x + self.spacing;
+        }
+    }
+
+    fn size(&self, children: &[UiNode]) -> Vec2 {
+        calculate_row_size(children, self.spacing)
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+pub struct Column {
+    pub id: WidgetId,
+    pub anchor: Anchor,
+    pub position: Vec2,
+    pub spacing: f32,
+}
+
+impl Column {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            spacing: 8.0,
+        }
+    }
+
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+}
+
+impl ContaineredWidget for Column {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let total_size = calculate_column_size(&children, self.spacing);
+
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => vec2(0.0, -total_size.y / 2.0),
+        };
+
+        let start_pos = self.position + offset;
+        let mut y_offset = 0.0;
+
+        for child in children {
+            let child_size = calculate_node_size(&child);
+
+            state.push_container(crate::math::Rect::new(
+                start_pos + vec2(0.0, y_offset),
+                vec2(total_size.x, child_size.y)
+            ));
+
+            state.render_tree(vec![child]);
+            state.pop_container();
+
+            y_offset += child_size.y + self.spacing;
+        }
+    }
+
+    fn size(&self, children: &[UiNode]) -> Vec2 {
+        calculate_column_size(children, self.spacing)
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/mod.rs b/crates/kino-ui/src/widgets/mod.rs
index 20c1dfe..0f23c9a 100644
--- a/crates/kino-ui/src/widgets/mod.rs
+++ b/crates/kino-ui/src/widgets/mod.rs
@@ -1,8 +1,10 @@
 pub mod rect;
 pub mod shorthand;
 pub mod text;
+pub mod layout;
 
 use std::any::Any;
+use glam::Vec2;
 use crate::{KinoState, UiNode, WidgetId};
 
 /// Determines how the object is anchored.
@@ -21,12 +23,14 @@ pub trait NativeWidget: Send + Sync {
     /// The state is provided for you to manipulate, such as adding a new response based on the
     /// [`WidgetId`]. 
     fn render(self: Box<Self>, state: &mut KinoState);
+    fn size(&self) -> Vec2;
     fn id(&self) -> WidgetId;
     fn as_any(&self) -> &dyn Any;
 }
 
 pub trait ContaineredWidget: Send + Sync {
     fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState);
+    fn size(&self, children: &[UiNode]) -> Vec2;
     fn id(&self) -> WidgetId;
     fn as_any(&self) -> &dyn Any;
 }
diff --git a/crates/kino-ui/src/widgets/rect.rs b/crates/kino-ui/src/widgets/rect.rs
index b547784..e1917d7 100644
--- a/crates/kino-ui/src/widgets/rect.rs
+++ b/crates/kino-ui/src/widgets/rect.rs
@@ -65,7 +65,7 @@ impl Rectangle {
             id: id.into(),
             anchor: Anchor::TopLeft,
             position: Vec2::ZERO,
-            size: vec2(64.0, 64.0),
+            size: vec2(64.0, 128.0),
             rotation: 0.0,
             uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
             fill: Fill::default(),
@@ -220,6 +220,10 @@ impl NativeWidget for Rectangle {
         self.render_body(state, &rect, rot);
     }
 
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+
     fn id(&self) -> WidgetId {
         self.id
     }
@@ -238,6 +242,10 @@ impl ContaineredWidget for Rectangle {
         state.pop_container();
     }
 
+    fn size(&self, _children: &[UiNode]) -> Vec2 {
+        self.size
+    }
+
     fn id(&self) -> WidgetId {
         self.id
     }
diff --git a/crates/kino-ui/src/widgets/shorthand.rs b/crates/kino-ui/src/widgets/shorthand.rs
index e84b96c..75f5262 100644
--- a/crates/kino-ui/src/widgets/shorthand.rs
+++ b/crates/kino-ui/src/widgets/shorthand.rs
@@ -1,4 +1,5 @@
 use crate::{KinoState, WidgetId};
+use crate::widgets::layout::{Column, Row};
 use crate::widgets::rect::Rectangle;
 use crate::widgets::text::Text;
 
@@ -42,4 +43,38 @@ where
     let mut text = Text::new(text);
     configure(&mut text);
     kino.add_widget(Box::new(text))
+}
+
+/// Shorthand for [`Row`], used for displaying items that are displayed horizontally.
+pub fn row<C>(
+    kino: &mut KinoState,
+    row: Row,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = row.id;
+
+    kino.add_container(Box::new(row));
+    contents(kino);
+    kino.end_container(id);
+    id
+}
+
+/// Shorthand for [`Column`], used for displaying items that are displayed vertically.
+pub fn column<C>(
+    kino: &mut KinoState,
+    column: Column,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = column.id;
+
+    kino.add_container(Box::new(column));
+    contents(kino);
+    kino.end_container(id);
+    id
 }
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/text.rs b/crates/kino-ui/src/widgets/text.rs
index 857c6ed..06a7dcb 100644
--- a/crates/kino-ui/src/widgets/text.rs
+++ b/crates/kino-ui/src/widgets/text.rs
@@ -126,6 +126,10 @@ impl NativeWidget for Text {
         });
     }
 
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+
     fn id(&self) -> WidgetId {
         self.id
     }
diff --git a/crates/kino-ui/src/windowing.rs b/crates/kino-ui/src/windowing.rs
index 3f70541..2731bdd 100644
--- a/crates/kino-ui/src/windowing.rs
+++ b/crates/kino-ui/src/windowing.rs
@@ -2,6 +2,7 @@ use std::sync::Arc;
 use glam::Vec2;
 use winit::event::{ElementState, MouseButton, WindowEvent};
 use winit::window::Window;
+use crate::KinoState;
 
 pub struct KinoWinitWindowing {
     // mouse
@@ -9,29 +10,46 @@ pub struct KinoWinitWindowing {
     pub mouse_button: MouseButton,
     pub mouse_press_state: ElementState,
 
-    // keyboard
-
     // windowing
-    _window: Arc<Window>,
+    window: Arc<Window>,
     /// The top-left most pixel
     pub viewport_offset: Vec2,
     /// Scale from screen-space to viewport texture space
     pub viewport_scale: Vec2,
+
+    pub scale_factor: f32,
+    pub auto_scale: bool,
 }
 
 impl KinoWinitWindowing {
     /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset.
-    pub fn new(window: Arc<Window>) -> Self {
+    pub fn new(window: Arc<Window>, scale_factor: Option<f32>) -> Self {
+        let auto_scale = scale_factor.is_none();
+        let scale_factor = scale_factor.unwrap_or(window.scale_factor() as f32);
         Self {
             mouse_position: Default::default(),
             mouse_button: MouseButton::Left,
             mouse_press_state: ElementState::Released,
-            _window: window,
+            window,
             viewport_offset: Default::default(),
             viewport_scale: Vec2::ONE,
+            scale_factor,
+            auto_scale,
         }
     }
 
+    /// Get the physical size of the window in pixels
+    pub fn physical_size(&self) -> (u32, u32) {
+        let size = self.window.inner_size();
+        (size.width, size.height)
+    }
+
+    /// Get the logical size of the window (physical size / scale_factor)
+    pub fn logical_size(&self) -> Vec2 {
+        let (w, h) = self.physical_size();
+        Vec2::new(w as f32, h as f32) / self.scale_factor
+    }
+
     pub(crate) fn handle_event(&mut self, event: &WindowEvent) {
         match event {
             WindowEvent::CursorMoved { position, .. } => {
@@ -43,11 +61,50 @@ impl KinoWinitWindowing {
                 self.mouse_button = *button;
                 self.mouse_press_state = *state;
             }
+            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
+                if self.auto_scale { return; }
+                self.scale_factor = *scale_factor as f32;
+            }
             WindowEvent::Resized(_) => {}
             WindowEvent::KeyboardInput { .. } => {}
             WindowEvent::MouseWheel { .. } => {}
-            WindowEvent::ScaleFactorChanged { .. } => {}
             _ => {}
         }
     }
+}
+
+impl KinoState {
+    /// Get the current DPI scale factor
+    pub fn scale_factor(&self) -> f32 {
+        self.windowing.scale_factor
+    }
+
+    /// Scale a logical size to physical pixels
+    pub fn to_physical(&self, logical: f32) -> f32 {
+        logical * self.windowing.scale_factor
+    }
+
+    /// Scale a logical Vec2 to physical pixels
+    pub fn to_physical_vec(&self, logical: Vec2) -> Vec2 {
+        logical * self.windowing.scale_factor
+    }
+
+    /// Scale physical pixels to logical size
+    pub fn to_logical(&self, physical: f32) -> f32 {
+        physical / self.windowing.scale_factor
+    }
+
+    /// Scale physical Vec2 to logical size
+    pub fn to_logical_vec(&self, physical: Vec2) -> Vec2 {
+        physical / self.windowing.scale_factor
+    }
+
+    pub fn set_scale_factor(&mut self, factor: Option<f32>) {
+        if let Some(factor) = factor {
+            self.windowing.scale_factor = factor;
+            self.windowing.auto_scale = false;
+        } else {
+            self.windowing.auto_scale = true;
+        }
+    }
 }
\ No newline at end of file
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
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index 9ed9e3e..ced3eed 100644
--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -1,9 +1,9 @@
 package com.dropbear
 
-import com.dropbear.asset.AssetHandle
+import com.dropbear.asset.AssetType
+import com.dropbear.asset.Handle
 import com.dropbear.ffi.NativeEngine
 import com.dropbear.input.InputState
-import com.dropbear.logging.Logger
 import com.dropbear.scene.SceneManager
 import com.dropbear.ui.UIInstruction
 import com.dropbear.ui.UIInstructionSet
@@ -32,17 +32,6 @@ class DropbearEngine(val native: NativeEngine) {
         fun getLastErrMsg(): String? {
             return lastErrorMessage
         }
-
-        /**
-         * Globally sets whether exceptions should be thrown when an error occurs.
-         *
-         * This can be run in your update loop without consequences.
-         */
-        @Deprecated("Currently not supported anymore, automatically throws exception on error. " +
-                "Better to catch the exception instead", level = DeprecationLevel.HIDDEN)
-        fun callExceptionOnError(toggle: Boolean) {
-            exceptionOnError = toggle
-        }
     }
 
     /**
@@ -61,19 +50,10 @@ class DropbearEngine(val native: NativeEngine) {
      * ## Warning
      * The eucalyptus asset URI (or `euca://`) is case-sensitive.
      */
-    fun getAsset(eucaURI: String): AssetHandle? {
+    fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? {
         val id = com.dropbear.getAsset(eucaURI)
-        return if (id != null) AssetHandle(id) else null
-    }
-
-    /**
-     * Globally sets whether exceptions should be thrown when an error occurs.
-     *
-     * This can be run in your update loop without consequences.
-     */
-    @Deprecated("Currently not supported anymore, automatically throws exception on error. " +
-            "Better to catch the exception instead", level = DeprecationLevel.HIDDEN)
-    fun callExceptionOnError(toggle: Boolean) {
+        if (id == null || id <= 0L) return null
+        return Handle(id)
     }
 
     /**
diff --git a/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
new file mode 100644
index 0000000..a0f7dcf
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
@@ -0,0 +1,57 @@
+package com.dropbear.animation
+
+import com.dropbear.EntityId
+import com.dropbear.ecs.Component
+
+class AnimationComponent(parentEntity: EntityId) : Component(parentEntity, "AnimationComponent") {
+    var activeAnimationIndex: Int?
+        get() = getActiveAnimationIndex()
+        set(value) = setActiveAnimationIndex(value)
+
+    var time: Float
+        get() = getTime()
+        set(value) = setTime(value)
+
+    var speed: Float
+        get() = getSpeed()
+        set(value) = setSpeed(value)
+
+    var looping: Boolean
+        get() = getLooping()
+        set(value) = setLooping(value)
+
+    var isPlaying: Boolean
+        get() = getIsPlaying()
+        set(value) = setIsPlaying(value)
+
+    fun pause() {
+        isPlaying = false
+    }
+
+    fun play() {
+        isPlaying = true
+    }
+
+    fun stop() {
+        isPlaying = false
+        time = 0f
+        activeAnimationIndex = null
+    }
+
+    fun reset() {
+        time = 0f
+    }
+
+    fun setAnimation(index: Int, speed: Float = 1f) = setActiveAnimationIndex(index).let { setSpeed(speed) }
+}
+
+expect fun AnimationComponent.getActiveAnimationIndex(): Int?
+expect fun AnimationComponent.setActiveAnimationIndex(index: Int?)
+expect fun AnimationComponent.getTime(): Float
+expect fun AnimationComponent.setTime(value: Float)
+expect fun AnimationComponent.getSpeed(): Float
+expect fun AnimationComponent.setSpeed(value: Float)
+expect fun AnimationComponent.getLooping(): Boolean
+expect fun AnimationComponent.setLooping(value: Boolean)
+expect fun AnimationComponent.getIsPlaying(): Boolean
+expect fun AnimationComponent.setIsPlaying(value: Boolean)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetHandle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetHandle.kt
deleted file mode 100644
index 3a9459e..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/asset/AssetHandle.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.dropbear.asset
-
-import com.dropbear.DropbearEngine
-
-/**
- * A handle that describes the type of asset in the ASSET_REGISTRY
- */
-class AssetHandle(private val id: Long): Handle(id) {
-    /**
-     * Converts an [AssetHandle] to a [ModelHandle].
-     *
-     * It can return null if the asset is not a model.
-     */
-    fun asModelHandle(): ModelHandle? {
-        val result = isModelHandle(id)
-        return if (result) {
-            ModelHandle(id)
-        } else {
-            null
-        }
-    }
-
-    override fun asAssetHandle(): AssetHandle {
-        return this
-    }
-
-    /**
-     * Converts an [AssetHandle] to a [TextureHandle].
-     *
-     * It can return null if the asset is not a texture.
-     */
-    fun asTextureHandle(): TextureHandle? {
-        return if (isTextureHandle(id)) TextureHandle(id) else null
-    }
-
-    override fun toString(): String {
-        return "AssetHandle(id=$id)"
-    }
-}
-
-
-internal expect fun isTextureHandle(id: Long): Boolean
-internal expect fun isModelHandle(id: Long): Boolean
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
new file mode 100644
index 0000000..ac80dcd
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
@@ -0,0 +1,3 @@
+package com.dropbear.asset
+
+open class AssetType internal constructor(open val id: Long)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt
index 57f134c..d0183db 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt
@@ -1,46 +1,28 @@
 package com.dropbear.asset
 
+import com.dropbear.utils.ID
+
 /**
  * Describes a handle of an asset, or anything really.
  *
- * Aims to allow people to group up different handle types ([AssetHandle], [ModelHandle] etc...)
+ * Aims to allow people to group up different handle types
  * into a list or a vector.
  *
  * All handles must be positive, non-zero values. If the id does not follow that rule, it is considered invalid.
  */
-abstract class Handle(private val id: Long) {
+class Handle<T: AssetType>(private val id: Long): ID(id) {
     init {
-        require(id > 0) { "Handle id must be a positive, non-zero value. Got: $id" }
+        require(id > 0) { "Handle id must be a positive value. Got: $id" }
     }
 
-    /**
-     * Returns the raw id of the handle
-     */
-    fun raw(): Long = id
-
-    /**
-     * Returns the handle as an [AssetHandle].
-     *
-     * This will not return null as all handles are a type of [AssetHandle].
-     */
-    abstract fun asAssetHandle(): AssetHandle
-
-    override fun toString(): String {
-        return "Handle(id=$id)"
+    companion object {
+        fun <T: AssetType> invalid() = Handle<T>(0)
     }
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-
-        val otherAsset = when (other) {
-            is Handle      -> other.asAssetHandle()
-            is AssetHandle -> other
-            else           -> return false
-        }
-
-        val thisAsset = asAssetHandle()
-        return thisAsset.raw() == otherAsset.raw()
+    fun raw(): Long {
+        return id
     }
+}
 
-    override fun hashCode(): Int = asAssetHandle().raw().hashCode()
-}
\ No newline at end of file
+typealias TextureHandle = Handle<Texture>
+typealias ModelHandle = Handle<Model>
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
new file mode 100644
index 0000000..fb70859
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
@@ -0,0 +1,34 @@
+package com.dropbear.asset
+
+import com.dropbear.asset.model.Mesh
+import com.dropbear.asset.model.Material
+import com.dropbear.asset.model.Skin
+import com.dropbear.asset.model.Animation
+import com.dropbear.asset.model.Node
+
+class Model(override val id: Long): AssetType(id) {
+    val label: String
+        get() = getLabel()
+
+    val meshes: List<Mesh>
+        get() = getMeshes()
+
+    val materials: List<Material>
+        get() = getMaterials()
+
+    val skins: List<Skin>
+        get() = getSkins()
+
+    val animations: List<Animation>
+        get() = getAnimations()
+
+    val nodes: List<Node>
+        get() = getNodes()
+}
+
+expect fun Model.getLabel(): String
+expect fun Model.getMeshes(): List<Mesh>
+expect fun Model.getMaterials(): List<Material>
+expect fun Model.getSkins(): List<Skin>
+expect fun Model.getAnimations(): List<Animation>
+expect fun Model.getNodes(): List<Node>
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/ModelHandle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/ModelHandle.kt
deleted file mode 100644
index a31f87e..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/asset/ModelHandle.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.dropbear.asset
-
-/**
- * Another type of asset handle, this wraps the id of the asset
- * into something that only models can access. 
- */
-class ModelHandle(private val id: Long): Handle(id) {
-    override fun asAssetHandle(): AssetHandle = AssetHandle(id)
-
-    override fun toString(): String {
-        return "ModelHandle(id=$id)"
-    }
-}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
new file mode 100644
index 0000000..a6b7f70
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
@@ -0,0 +1,22 @@
+package com.dropbear.asset
+
+class Texture(override val id: Long): AssetType(id) {
+    var label: String?
+        get() = getLabel()
+        set(value) = setLabel(value)
+
+    val width: Int
+        get() = getWidth()
+
+    val height: Int
+        get() = getHeight()
+
+    val depth: Int
+        get() = getDepth()
+}
+
+expect fun Texture.getLabel(): String?
+expect fun Texture.setLabel(value: String?)
+expect fun Texture.getWidth(): Int
+expect fun Texture.getHeight(): Int
+expect fun Texture.getDepth(): Int
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/TextureHandle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/TextureHandle.kt
deleted file mode 100644
index a93257a..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/asset/TextureHandle.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.dropbear.asset
-
-/**
- * Another type of asset handle, this wraps the id into 
- * another form that only texture related functions can use. 
- */
-class TextureHandle(private val id: Long): Handle(id) {
-    /**
-     * The name of the texture/material.
-     */
-    val name: String?
-        get() = getTextureName(id)
-
-    override fun asAssetHandle(): AssetHandle = AssetHandle(id)
-
-    override fun toString(): String {
-        return "TextureHandle(id=$id)"
-    }
-}
-
-internal expect fun TextureHandle.getTextureName(id: Long): String?
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
new file mode 100644
index 0000000..fe9649a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
@@ -0,0 +1,51 @@
+package com.dropbear.asset.model
+
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Quaternionf
+
+data class Animation(
+    val name: String,
+    val channels: List<AnimationChannel>,
+    val duration: Float
+)
+
+data class AnimationChannel(
+    val targetNode: Int,
+    val times: DoubleArray,
+    val values: ChannelValues,
+    val interpolation: AnimationInterpolation
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as AnimationChannel
+
+        if (targetNode != other.targetNode) return false
+        if (!times.contentEquals(other.times)) return false
+        if (values != other.values) return false
+        if (interpolation != other.interpolation) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = targetNode
+        result = 31 * result + times.contentHashCode()
+        result = 31 * result + values.hashCode()
+        result = 31 * result + interpolation.hashCode()
+        return result
+    }
+}
+
+enum class AnimationInterpolation {
+    LINEAR,
+    STEP,
+    CUBIC_SPLINE
+}
+
+sealed class ChannelValues {
+    data class Translations(val values: List<Vector3f>) : ChannelValues()
+    data class Rotations(val values: List<Quaternionf>) : ChannelValues()
+    data class Scales(val values: List<Vector3f>) : ChannelValues()
+}
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Material.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Material.kt
new file mode 100644
index 0000000..9e1c9ce
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Material.kt
@@ -0,0 +1,24 @@
+package com.dropbear.asset.model
+
+import com.dropbear.asset.Texture
+import com.dropbear.math.Vector2f
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Vector4f
+
+data class Material(
+    val name: String,
+    val diffuseTexture: Texture,
+    val normalTexture: Texture,
+    val tint: Vector4f,
+    val emissiveFactor: Vector3f,
+    val metallicFactor: Float,
+    val roughnessFactor: Float,
+    val alphaCutoff: Float?,
+    val doubleSided: Boolean,
+    val occlusionStrength: Float,
+    val normalScale: Float,
+    val uvTiling: Vector2f,
+    val emissiveTexture: Texture?,
+    val metallicRoughnessTexture: Texture?,
+    val occlusionTexture: Texture?
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
new file mode 100644
index 0000000..e93890e
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
@@ -0,0 +1,8 @@
+package com.dropbear.asset.model
+
+data class Mesh(
+    val name: String,
+    val numElements: Int,
+    val materialIndex: Int,
+    val vertices: List<ModelVertex>
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/ModelVertex.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/ModelVertex.kt
new file mode 100644
index 0000000..82f946c
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/ModelVertex.kt
@@ -0,0 +1,16 @@
+package com.dropbear.asset.model
+
+import com.dropbear.math.Vector2f
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Vector4f
+
+data class ModelVertex(
+    val position: Vector3f,
+    val normal: Vector3f,
+    val tangent: Vector4f,
+    val texCoords0: Vector2f,
+    val texCoords1: Vector2f,
+    val colour0: Vector4f,
+    val joints0: IntArray,
+    val weights0: Vector4f
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Node.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Node.kt
new file mode 100644
index 0000000..c9b3a6a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Node.kt
@@ -0,0 +1,17 @@
+package com.dropbear.asset.model
+
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Quaternionf
+
+data class NodeTransform(
+    val translation: Vector3f,
+    val rotation: Quaternionf,
+    val scale: Vector3f
+)
+
+data class Node(
+    val name: String,
+    val parent: Int?,
+    val children: List<Int>,
+    val transform: NodeTransform
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Skin.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Skin.kt
new file mode 100644
index 0000000..5f31487
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Skin.kt
@@ -0,0 +1,8 @@
+package com.dropbear.asset.model
+
+data class Skin(
+    val name: String,
+    val joints: List<Int>,
+    val inverseBindMatrices: List<DoubleArray>,
+    val skeletonRoot: Int?
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/math/Angle.kt b/scripting/commonMain/kotlin/com/dropbear/math/Angle.kt
new file mode 100644
index 0000000..350fd3a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/math/Angle.kt
@@ -0,0 +1,79 @@
+package com.dropbear.math
+
+import kotlin.math.*
+
+/**
+ * Represents an angle with support for both degrees and radians.
+ * Provides normalization and common trigonometric operations.
+ */
+class Angle private constructor(private val radians: Double) {
+
+    companion object {
+        /** Creates an Angle from degrees */
+        fun fromDegrees(degrees: Double): Angle = Angle(degreesToRadians(degrees))
+
+        /** Creates an Angle from radians */
+        fun fromRadians(radians: Double): Angle = Angle(radians)
+
+        /** Zero angle */
+        val ZERO = Angle(0.0)
+
+        /** Right angle (90 degrees) */
+        val RIGHT = fromDegrees(90.0)
+
+        /** Straight angle (180 degrees) */
+        val STRAIGHT = fromDegrees(180.0)
+
+        /** Full rotation (360 degrees) */
+        val FULL = fromDegrees(360.0)
+    }
+
+    /** Get the angle value in degrees */
+    val degrees: Double
+        get() = radiansToDegrees(radians)
+
+    /** Get the angle value in radians */
+    fun toRadians(): Double = radians
+
+    /** Get the angle value in degrees */
+    fun toDegrees(): Double = degrees
+
+    /** Normalize the angle to range [0, 360) degrees or [0, 2π) radians */
+    fun normalized(): Angle {
+        val normalized = radians % (2 * PI)
+        return Angle(if (normalized < 0) normalized + 2 * PI else normalized)
+    }
+
+    /** Normalize the angle to range [-180, 180) degrees or [-π, π) radians */
+    fun normalizedSigned(): Angle {
+        val norm = normalized().radians
+        return if (norm > PI) Angle(norm - 2 * PI) else Angle(norm)
+    }
+
+    operator fun plus(other: Angle): Angle = Angle(radians + other.radians)
+    operator fun minus(other: Angle): Angle = Angle(radians - other.radians)
+    operator fun times(scalar: Double): Angle = Angle(radians * scalar)
+    operator fun div(scalar: Double): Angle = Angle(radians / scalar)
+    operator fun unaryMinus(): Angle = Angle(-radians)
+
+    operator fun compareTo(other: Angle): Int = radians.compareTo(other.radians)
+
+    fun sin(): Double = sin(radians)
+    fun cos(): Double = cos(radians)
+    fun tan(): Double = tan(radians)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Angle) return false
+        return radians == other.radians
+    }
+
+    override fun hashCode(): Int = radians.hashCode()
+
+    override fun toString(): String = "${degrees}°"
+}
+
+fun Double.degrees(): Angle = Angle.fromDegrees(this)
+fun Double.radians(): Angle = Angle.fromRadians(this)
+fun Int.degrees(): Angle = Angle.fromDegrees(this.toDouble())
+fun Int.radians(): Angle = Angle.fromRadians(this.toDouble())
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
index 2e74526..e98999e 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
@@ -16,4 +16,10 @@ inline fun buildUI(block: UIBuilder.() -> Unit): UIInstructionSet {
 
 fun UIBuilder.add(instruction: UIInstruction) {
     instructions.add(instruction)
-}
\ No newline at end of file
+}
+
+fun UIBuilder.add(instructionSet: UIInstructionSet) {
+    instructions.addAll(instructionSet)
+}
+
+fun UIBuilder.add(block: UIBuilder.() -> Widget) = add(block().toInstruction())
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt
new file mode 100644
index 0000000..5e9d853
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt
@@ -0,0 +1,14 @@
+package com.dropbear.ui.styling
+
+enum class Anchor {
+    /**
+     * A center anchor is when the position is based on the center of the object (such as the
+     * center of a circle)
+     */
+    Center,
+
+    /**
+     * A top left anchor is when the position is based on the top left corner of the object.
+     */
+    TopLeft,
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt
new file mode 100644
index 0000000..c564aad
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt
@@ -0,0 +1,5 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.utils.Colour
+
+data class Fill(var colour: Colour)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt
deleted file mode 100644
index c6756a1..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.dropbear.ui.widgets
-
-import com.dropbear.ui.UIBuilder
-import com.dropbear.ui.UIInstruction
-import com.dropbear.ui.Widget
-import com.dropbear.ui.WidgetId
-import com.dropbear.ui.styling.Alignment
-
-class Align(
-    override var id: WidgetId,
-    var align: Alignment,
-    var widgets: MutableList<Widget> = mutableListOf(),
-): Widget() {
-    companion object {
-        fun center(id: WidgetId) = Align(align = Alignment.CENTER, id = id)
-    }
-
-    sealed class AlignmentInstruction: UIInstruction {
-        data class StartAlignmentBlock(val id: WidgetId, val align: Align) : AlignmentInstruction()
-        data class EndAlignmentBlock(val id: WidgetId) : AlignmentInstruction()
-    }
-
-    override fun toInstruction(): List<UIInstruction> {
-        val instructions = mutableListOf<UIInstruction>()
-        instructions.add(AlignmentInstruction.StartAlignmentBlock(id, this))
-        widgets.forEach { widget ->
-            widget.toInstruction().forEach { instruction ->
-                instructions.add(instruction)
-            }
-        }
-        instructions.add(AlignmentInstruction.EndAlignmentBlock(id))
-        return instructions.toList()
-    }
-}
-
-fun UIBuilder.center(id: WidgetId, block: UIBuilder.() -> Unit = {}) {
-    val align = Align.center(id)
-    instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align))
-    block(this)
-    instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id))
-}
-
-fun UIBuilder.align(alignment: Alignment, id: WidgetId, block: UIBuilder.() -> Unit = {}) {
-    val align = Align(align = alignment, id = id)
-    instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align))
-    block(this)
-    instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id))
-}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt
deleted file mode 100644
index d3bd5ec..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.dropbear.ui.widgets
-
-import com.dropbear.ui.UIBuilder
-import com.dropbear.ui.UIInstruction
-import com.dropbear.ui.Widget
-import com.dropbear.ui.WidgetId
-import com.dropbear.ui.styling.Alignment
-import com.dropbear.ui.styling.Border
-import com.dropbear.ui.styling.BorderRadius
-import com.dropbear.ui.styling.DynamicButtonStyle
-import com.dropbear.ui.styling.Padding
-import com.dropbear.ui.styling.TextStyle
-import com.dropbear.ui.styling.fonts.TextAlignment
-import com.dropbear.utils.Colour
-import com.dropbear.utils.ID
-
-class Button(
-    var text: String,
-    var alignment: Alignment,
-    var padding: Padding,
-    var borderRadius: BorderRadius,
-    var style: DynamicButtonStyle,
-    var hoverStyle: DynamicButtonStyle,
-    var downStyle: DynamicButtonStyle,
-    id: WidgetId = WidgetId(text.hashCode().toLong()),
-): Widget() {
-    override var id: WidgetId
-
-    init {
-        this.id = id
-    }
-
-    val clicked: Boolean
-        get() = getClicked()
-
-    val hovering: Boolean
-        get() = getHovering()
-
-    companion object {
-        fun styled(text: String, id: String = text) : Button {
-            val style = DynamicButtonStyle(
-                fill = Colour.BACKGROUND_3,
-                text = TextStyle(
-                    colour = Colour.WHITE.adjust(0.8),
-                    align = TextAlignment.Center
-                ),
-                border = Border(
-                    colour = Colour.BACKGROUND_1,
-                    width = 1.0
-                )
-            )
-
-            val hoverStyle = DynamicButtonStyle(
-                fill = Colour.BACKGROUND_3.adjust(1.2),
-                border = Border(Colour.WHITE.adjust(0.75), 1.0),
-            )
-
-            val downStyle = DynamicButtonStyle(
-                fill = Colour.BACKGROUND_3.adjust(0.8),
-                border = Border(Colour.WHITE, 1.0)
-            )
-
-            val result = Button(
-                text = text,
-                alignment = Alignment.CENTER,
-                padding = Padding.balanced(20.0, 10.0),
-                borderRadius = BorderRadius.uniform(6.0),
-                style = style,
-                hoverStyle = hoverStyle,
-                downStyle = downStyle,
-            )
-
-            result.id = WidgetId(id.hashCode().toLong())
-
-            return result
-        }
-
-        fun unstyled(text: String, id: String = text) : Button {
-            val result = Button(
-                text = text,
-                alignment = Alignment.CENTER,
-                padding = Padding.zero(),
-                borderRadius = BorderRadius(),
-                style = DynamicButtonStyle(),
-                hoverStyle = DynamicButtonStyle(),
-                downStyle = DynamicButtonStyle(),
-            )
-
-            result.id = WidgetId(id.hashCode().toLong())
-
-            return result
-        }
-    }
-
-    sealed class ButtonInstruction: UIInstruction {
-        data class Button(val id: WidgetId, val button: com.dropbear.ui.widgets.Button) : ButtonInstruction()
-    }
-
-    override fun toInstruction(): List<UIInstruction> {
-        return listOf(ButtonInstruction.Button(this.id, this))
-    }
-}
-
-expect fun Button.getClicked(): Boolean
-expect fun Button.getHovering(): Boolean
-
-// fits that of yakui_widgets::shorthand::button
-fun UIBuilder.button(text: String, block: Button.() -> Unit = {}): Button {
-    val btn = Button.styled(text).apply(block)
-    btn.toInstruction().forEach {
-        instructions.add(it)
-    }
-    return btn
-}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Checkbox.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Checkbox.kt
deleted file mode 100644
index 0c682fb..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Checkbox.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.dropbear.ui.widgets
-
-import com.dropbear.ui.UIBuilder
-import com.dropbear.ui.UIInstruction
-import com.dropbear.ui.Widget
-import com.dropbear.ui.WidgetId
-
-class Checkbox(
-    override val id: WidgetId,
-    initiallyChecked: Boolean,
-) : Widget() {
-    private var cachedChecked: Boolean = initiallyChecked
-
-    /**
-     * To set a new checkbox value, make it so the class is different next frame.
-     */
-    var checked: Boolean
-        get() {
-            if (hasCheckedState()) {
-                cachedChecked = getChecked()
-            }
-            return cachedChecked
-        }
-        private set(value) {
-            cachedChecked = value
-        }
-
-    init {
-        checked = initiallyChecked
-    }
-
-    sealed class CheckboxInstruction: UIInstruction {
-        data class Checkbox(val id: WidgetId, val checked: Boolean) : CheckboxInstruction()
-    }
-
-    override fun toInstruction(): List<UIInstruction> {
-        return listOf(CheckboxInstruction.Checkbox(id, checked))
-    }
-}
-
-expect fun Checkbox.getChecked(): Boolean
-expect fun Checkbox.hasCheckedState(): Boolean
-
-fun UIBuilder.checkbox(checked: Boolean, id: WidgetId, block: Checkbox.() -> Unit = {}): Checkbox {
-    val cb = Checkbox(id, checked).apply(block)
-    cb.toInstruction().forEach { instruction -> instructions.add(instruction) }
-    return cb
-}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/ColouredBox.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/ColouredBox.kt
deleted file mode 100644
index 1e4377a..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/ColouredBox.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.dropbear.ui.widgets
-
-import com.dropbear.math.Vector2d
-import com.dropbear.ui.UIBuilder
-import com.dropbear.ui.UIInstruction
-import com.dropbear.ui.Widget
-import com.dropbear.ui.WidgetId
-import com.dropbear.ui.buildUI
-import com.dropbear.utils.Colour
-
-class ColouredBox(
-    override val id: WidgetId,
-    var colour: Colour = Colour.WHITE,
-    var minSize: Vector2d = Vector2d.zero(),
-) : Widget() {
-    var widgets: MutableList<UIInstruction> = mutableListOf()
-
-    companion object {
-        fun sized(id: WidgetId, colour: Colour, size: Vector2d): ColouredBox {
-            return ColouredBox(id, colour, size)
-        }
-
-        fun container(id: WidgetId, colour: Colour): ColouredBox {
-            return ColouredBox(id, colour, Vector2d.zero())
-        }
-    }
-
-    sealed class ColouredBoxInstruction: UIInstruction {
-        data class StartColouredBoxInstruction(val id: WidgetId, val box: ColouredBox): ColouredBoxInstruction()
-        data class EndColouredBoxInstruction(val id: WidgetId): ColouredBoxInstruction()
-    }
-
-    override fun toInstruction(): List<UIInstruction> {
-        val list = mutableListOf<UIInstruction>()
-        list.add(ColouredBoxInstruction.StartColouredBoxInstruction(this.id, this))
-        list.addAll(widgets)
-        list.add(ColouredBoxInstruction.EndColouredBoxInstruction(this.id))
-        return list.toList()
-    }
-
-    fun addChild(widget: Widget) {
-        widgets.addAll(widget.toInstruction())
-    }
-}
-
-fun UIBuilder.colouredBox(id: WidgetId, colour: Colour, minSize: Vector2d, block: ColouredBox.() -> Unit = {}): ColouredBox {
-    val box = ColouredBox(id, colour, minSize).apply(block)
-    instructions.addAll(box.toInstruction())
-    return box
-}
-
-fun UIBuilder.colouredBoxContainer(id: WidgetId, colour: Colour, children: UIBuilder.() -> Unit): ColouredBox {
-    val box = ColouredBox(id = id, colour = colour)
-    val newUI = UIBuilder()
-    children(newUI) // execute
-    box.widgets.addAll(newUI.build())
-    instructions.addAll(box.toInstruction())
-    return box
-}
-
-fun dummy() {
-    val instructions = buildUI {
-        colouredBox(WidgetId("something"), Colour.TRANSPARENT, Vector2d.zero())
-
-        colouredBoxContainer(WidgetId("something"), Colour.TRANSPARENT) {
-
-        }
-    }
-}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt
new file mode 100644
index 0000000..8a0bab7
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt
@@ -0,0 +1,70 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.styling.Anchor
+
+class Column(
+	override var id: WidgetId,
+	var anchor: Anchor = Anchor.TopLeft,
+	var position: Vector2d = Vector2d.zero(),
+	var spacing: Double = 8.0,
+): Widget() {
+	sealed class ColumnInstruction: UIInstruction {
+		data class StartColumnBlock(val id: WidgetId, val column: com.dropbear.ui.widgets.Column) : ColumnInstruction()
+		data class EndColumnBlock(val id: WidgetId) : ColumnInstruction()
+	}
+
+	override fun toInstruction(): List<UIInstruction> {
+		return listOf(ColumnInstruction.StartColumnBlock(id, this), ColumnInstruction.EndColumnBlock(id))
+	}
+
+	fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+		return listOf(ColumnInstruction.StartColumnBlock(id, this)) +
+			children +
+			ColumnInstruction.EndColumnBlock(id)
+	}
+
+	fun startInstruction(): UIInstruction = ColumnInstruction.StartColumnBlock(id, this)
+
+	fun endInstruction(): UIInstruction = ColumnInstruction.EndColumnBlock(id)
+
+	fun at(position: Vector2d): Column {
+		this.position = position
+		return this
+	}
+
+	fun spacing(spacing: Double): Column {
+		this.spacing = spacing
+		return this
+	}
+
+	fun withAnchor(anchor: Anchor): Column {
+		this.anchor = anchor
+		return this
+	}
+}
+
+fun UIBuilder.column(id: WidgetId = WidgetId(generateId().toLong()), block: Column.() -> Unit = {}): Column {
+	val column = Column(id = id).apply(block)
+	column.toInstruction().forEach { instructions.add(it) }
+	return column
+}
+
+fun UIBuilder.column(id: WidgetId, content: UIBuilder.(Column) -> Unit) {
+	column(id, columnBlock = {}, content = content)
+}
+
+fun UIBuilder.column(
+	id: WidgetId,
+	columnBlock: Column.() -> Unit = {},
+	content: UIBuilder.(Column) -> Unit
+) {
+	val column = Column(id = id).apply(columnBlock)
+	instructions.add(column.startInstruction())
+	this.content(column)
+	instructions.add(column.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt
new file mode 100644
index 0000000..fd1efd4
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt
@@ -0,0 +1,118 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.asset.Handle
+import com.dropbear.asset.Texture
+import com.dropbear.math.Angle
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.styling.Anchor
+import com.dropbear.ui.styling.Border
+import com.dropbear.ui.styling.Fill
+import com.dropbear.utils.Colour
+
+class Rectangle(
+    override var id: WidgetId,
+    var anchor: Anchor = Anchor.TopLeft,
+    var position: Vector2d = Vector2d.zero(),
+    var size: Vector2d = Vector2d(64.0, 128.0),
+    var texture: Handle<Texture>? = null,
+    var rotation: Angle = Angle.ZERO,
+    var uv: List<Vector2d> = listOf(Vector2d.zero(), Vector2d(1.0, 0.0), Vector2d.one(), Vector2d(0.0, 1.0)),
+    var fill: Fill = Fill(Colour.WHITE),
+    var border: Border? = null
+): Widget() {
+    init {
+        require(uv.size == 4) { "Rectangle uv must have 4 coordinates." }
+    }
+
+    sealed class RectangleInstruction: UIInstruction {
+        data class Rectangle(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction()
+        data class StartRectangleBlock(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction()
+        data class EndRectangleBlock(val id: WidgetId) : RectangleInstruction()
+    }
+
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(RectangleInstruction.Rectangle(id, this))
+    }
+
+    fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+        return listOf(RectangleInstruction.StartRectangleBlock(id, this)) +
+            children +
+            RectangleInstruction.EndRectangleBlock(id)
+    }
+
+    fun startInstruction(): UIInstruction = RectangleInstruction.StartRectangleBlock(id, this)
+
+    fun endInstruction(): UIInstruction = RectangleInstruction.EndRectangleBlock(id)
+
+    fun with(position: Vector2d, size: Vector2d): Rectangle {
+        this.position = position
+        this.size = size
+        return this
+    }
+
+    fun withAnchor(anchor: Anchor): Rectangle {
+        this.anchor = anchor
+        return this
+    }
+
+    fun at(position: Vector2d): Rectangle {
+        this.position = position
+        return this
+    }
+
+    fun size(size: Vector2d): Rectangle {
+        this.size = size
+        return this
+    }
+
+    fun fill(fill: Fill): Rectangle {
+        this.fill = fill
+        return this
+    }
+
+    fun border(border: Border?): Rectangle {
+        this.border = border
+        return this
+    }
+
+    fun rotate(angle: Angle): Rectangle {
+        this.rotation = angle
+        return this
+    }
+
+    fun texture(texture: Handle<Texture>?): Rectangle {
+        this.texture = texture
+        return this
+    }
+
+    fun uv(coords: List<Vector2d>): Rectangle {
+        require(coords.size == 4) { "Rectangle uv must have 4 coordinates." }
+        this.uv = coords
+        return this
+    }
+}
+
+fun UIBuilder.rectangle(id: WidgetId = WidgetId(generateId().toLong()), block: Rectangle.() -> Unit = {}): Rectangle {
+    val rect = Rectangle(id = id).apply(block)
+    rect.toInstruction().forEach { instructions.add(it) }
+    return rect
+}
+
+fun UIBuilder.container(id: WidgetId, block: UIBuilder.(Rectangle) -> Unit) {
+    container(id, rectBlock = {}, content = block)
+}
+
+fun UIBuilder.container(
+    id: WidgetId,
+    rectBlock: Rectangle.() -> Unit = {},
+    content: UIBuilder.(Rectangle) -> Unit
+) {
+    val rect = Rectangle(id = id).apply(rectBlock)
+    instructions.add(rect.startInstruction())
+    this.content(rect)
+    instructions.add(rect.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt
new file mode 100644
index 0000000..955ee08
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt
@@ -0,0 +1,70 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.styling.Anchor
+
+class Row(
+	override var id: WidgetId,
+	var anchor: Anchor = Anchor.TopLeft,
+	var position: Vector2d = Vector2d.zero(),
+	var spacing: Double = 8.0,
+): Widget() {
+	sealed class RowInstruction: UIInstruction {
+		data class StartRowBlock(val id: WidgetId, val row: com.dropbear.ui.widgets.Row) : RowInstruction()
+		data class EndRowBlock(val id: WidgetId) : RowInstruction()
+	}
+
+	override fun toInstruction(): List<UIInstruction> {
+		return listOf(RowInstruction.StartRowBlock(id, this), RowInstruction.EndRowBlock(id))
+	}
+
+	fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+		return listOf(RowInstruction.StartRowBlock(id, this)) +
+			children +
+			RowInstruction.EndRowBlock(id)
+	}
+
+	fun startInstruction(): UIInstruction = RowInstruction.StartRowBlock(id, this)
+
+	fun endInstruction(): UIInstruction = RowInstruction.EndRowBlock(id)
+
+	fun at(position: Vector2d): Row {
+		this.position = position
+		return this
+	}
+
+	fun spacing(spacing: Double): Row {
+		this.spacing = spacing
+		return this
+	}
+
+	fun withAnchor(anchor: Anchor): Row {
+		this.anchor = anchor
+		return this
+	}
+}
+
+fun UIBuilder.row(id: WidgetId = WidgetId(generateId().toLong()), block: Row.() -> Unit = {}): Row {
+	val row = Row(id = id).apply(block)
+	row.toInstruction().forEach { instructions.add(it) }
+	return row
+}
+
+fun UIBuilder.row(id: WidgetId, content: UIBuilder.(Row) -> Unit) {
+	row(id, rowBlock = {}, content = content)
+}
+
+fun UIBuilder.row(
+	id: WidgetId,
+	rowBlock: Row.() -> Unit = {},
+	content: UIBuilder.(Row) -> Unit
+) {
+	val row = Row(id = id).apply(rowBlock)
+	instructions.add(row.startInstruction())
+	this.content(row)
+	instructions.add(row.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/jvmMain/java/com/dropbear/asset/AssetHandleNative.java b/scripting/jvmMain/java/com/dropbear/asset/AssetHandleNative.java
deleted file mode 100644
index adc49b0..0000000
--- a/scripting/jvmMain/java/com/dropbear/asset/AssetHandleNative.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.dropbear.asset;
-
-import com.dropbear.EucalyptusCoreLoader;
-
-public class AssetHandleNative {
-    static {
-        new EucalyptusCoreLoader().ensureLoaded();
-    }
-
-    public static native boolean isModelHandle(long assetRegistryHandle, long handle);
-    public static native boolean isTextureHandle(long assetRegistryHandle, long handle);
-}
diff --git a/scripting/jvmMain/java/com/dropbear/asset/TextureHandleNative.java b/scripting/jvmMain/java/com/dropbear/asset/TextureHandleNative.java
deleted file mode 100644
index 7fc5a1c..0000000
--- a/scripting/jvmMain/java/com/dropbear/asset/TextureHandleNative.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.dropbear.asset;
-
-import com.dropbear.EucalyptusCoreLoader;
-
-public class TextureHandleNative {
-    static {
-        new EucalyptusCoreLoader().ensureLoaded();
-    }
-
-    public static native String getTextureName(long assetRegistryHandle, long handle);
-}
diff --git a/scripting/jvmMain/kotlin/com/dropbear/asset/AssetHandle.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/asset/AssetHandle.jvm.kt
deleted file mode 100644
index 7fba170..0000000
--- a/scripting/jvmMain/kotlin/com/dropbear/asset/AssetHandle.jvm.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.dropbear.asset
-
-import com.dropbear.DropbearEngine
-
-internal actual fun isModelHandle(id: Long): Boolean {
-    return AssetHandleNative.isModelHandle(DropbearEngine.native.assetHandle, id)
-}
-
-internal actual fun isTextureHandle(id: Long): Boolean {
-    return AssetHandleNative.isTextureHandle(DropbearEngine.native.assetHandle, id)
-}
\ No newline at end of file
diff --git a/scripting/jvmMain/kotlin/com/dropbear/asset/TextureHandle.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/asset/TextureHandle.jvm.kt
deleted file mode 100644
index 487eaaa..0000000
--- a/scripting/jvmMain/kotlin/com/dropbear/asset/TextureHandle.jvm.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.dropbear.asset
-
-import com.dropbear.DropbearEngine
-
-internal actual fun TextureHandle.getTextureName(id: Long): String? {
-    return TextureHandleNative.getTextureName(DropbearEngine.native.assetHandle, id)
-}
\ No newline at end of file
diff --git a/scripting/jvmMain/kotlin/com/dropbear/components/MeshRenderer.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/components/MeshRenderer.jvm.kt
index 42559e6..3355da6 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/components/MeshRenderer.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/components/MeshRenderer.jvm.kt
@@ -2,7 +2,6 @@ package com.dropbear.components
 
 import com.dropbear.DropbearEngine
 import com.dropbear.EntityId
-import com.dropbear.asset.ModelHandle
 import com.dropbear.asset.TextureHandle
 
 internal actual fun MeshRenderer.getModel(id: EntityId): ModelHandle? {
diff --git a/scripting/nativeMain/kotlin/com/dropbear/asset/AssetHandle.native.kt b/scripting/nativeMain/kotlin/com/dropbear/asset/AssetHandle.native.kt
deleted file mode 100644
index a68613e..0000000
--- a/scripting/nativeMain/kotlin/com/dropbear/asset/AssetHandle.native.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.dropbear.asset
-
-internal actual fun isModelHandle(id: Long): Boolean {
-    TODO("Not yet implemented")
-}
-
-internal actual fun isTextureHandle(id: Long): Boolean {
-    TODO("Not yet implemented")
-}
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/asset/TextureHandle.native.kt b/scripting/nativeMain/kotlin/com/dropbear/asset/TextureHandle.native.kt
deleted file mode 100644
index 5bb87cb..0000000
--- a/scripting/nativeMain/kotlin/com/dropbear/asset/TextureHandle.native.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.dropbear.asset
-
-internal actual fun TextureHandle.getTextureName(id: Long): String? {
-    TODO("Not yet implemented")
-}
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/components/MeshRenderer.native.kt b/scripting/nativeMain/kotlin/com/dropbear/components/MeshRenderer.native.kt
index ebce5ff..da6f42a 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/components/MeshRenderer.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/components/MeshRenderer.native.kt
@@ -1,7 +1,6 @@
 package com.dropbear.components
 
 import com.dropbear.EntityId
-import com.dropbear.asset.ModelHandle
 import com.dropbear.asset.TextureHandle
 
 internal actual fun MeshRenderer.getModel(id: EntityId): ModelHandle? {