kitgit

tirbofish/dropbear · diff

d915a72 · Thribhu K

wip: animation wip: animation scripting feature: implemented puffin profiler into dropbear_engine refactor: completely remade the asset server, with support for typesafe handles instead of using arbitrary values (fixes #87) refactor: model importing to be able to export in a custom format during production builds are still not available yet because the other crates are still fucked.

Unverified

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 62c6633..411e870 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -119,6 +119,13 @@ impl AssetRegistry {
         self.texture_labels.insert(label.into(), handle.clone());
     }
 
+    /// 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);
+    }
+
     /// 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> {
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 99b534d..ce76cd5 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -12,7 +12,9 @@ use glam::{DMat4, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
 use wgpu::{BindGroup};
-use crate::asset::ASSET_REGISTRY;
+use crate::asset::{Handle, ASSET_REGISTRY};
+use crate::model::Material;
+use crate::procedural::{ProcObj, ProcedurallyGeneratedObject};
 
 pub const MAX_LIGHTS: usize = 10;
 
@@ -241,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,
@@ -271,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;
 
@@ -289,16 +292,13 @@ impl Light {
 
         log::trace!("Created new light uniform");
 
-        let cube_model = Model::load_from_memory_raw(
-            graphics.clone(),
-            include_bytes!("../../../resources/models/cube.glb").to_vec(),
-            label,
-            ASSET_REGISTRY.clone(),
-            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();
 
@@ -339,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);
@@ -361,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 e8f1c63..58837b3 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -5,7 +5,7 @@ use crate::{
     utils::{ResourceReference},
     texture::{Texture, TextureWrapMode}
 };
-use image::GenericImageView;
+// use image::GenericImageView;
 use parking_lot::{Mutex, RwLock};
 use rayon::prelude::*;
 use serde::{Deserialize, Serialize};
@@ -17,15 +17,29 @@ use std::time::Instant;
 use std::{mem, ops::Range, path::PathBuf};
 use gltf::image::Format;
 use gltf::texture::MinFilter;
+use puffin::profile_scope;
 use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup};
 
 #[derive(Clone)]
 pub struct Model {
+    pub(crate) hash: u64, // also the id related to the handle
     pub label: String,
-    pub(crate) hash: u64,
     pub path: ResourceReference,
     pub meshes: Vec<Mesh>,
     pub materials: Vec<Material>,
+    pub skins: Vec<Skin>,
+    pub animations: Vec<Animation>,
+    pub nodes: Vec<Node>,
+}
+
+#[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>,
 }
 
 #[derive(Clone)]
@@ -45,7 +59,6 @@ pub struct Material {
     pub normal_scale: f32,
     pub uv_tiling: [f32; 2],
     pub tint_buffer: UniformBuffer<MaterialUniform>,
-    pub tint_bind_group: wgpu::BindGroup,
     pub texture_tag: Option<String>,
     pub wrap_mode: TextureWrapMode,
     pub emissive_texture: Option<Texture>,
@@ -53,6 +66,77 @@ pub struct Material {
     pub occlusion_texture: Option<Texture>,
 }
 
+/// 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,
+}
+
+/// 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 NodeTransform {
+    pub fn to_matrix(&self) -> glam::Mat4 {
+        glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
+    }
+
+    pub fn identity() -> Self {
+        Self {
+            translation: glam::Vec3::ZERO,
+            rotation: glam::Quat::IDENTITY,
+            scale: glam::Vec3::ONE,
+        }
+    }
+}
+
+/// 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>,
+}
+
+/// An animation that can be played on a skeleton
+#[derive(Clone)]
+pub struct Animation {
+    pub name: String,
+    pub channels: Vec<AnimationChannel>,
+    pub duration: f32,
+}
+
+/// 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 enum ChannelValues {
+    Translations(Vec<glam::Vec3>),
+    Rotations(Vec<glam::Quat>),
+    Scales(Vec<glam::Vec3>),
+}
+
 impl Material {
     pub fn new(
         graphics: Arc<SharedGraphicsContext>,
@@ -62,6 +146,7 @@ impl Material {
         tint: [f32; 4],
         texture_tag: Option<String>,
     ) -> Self {
+        puffin::profile_function!();
         let name = name.into();
 
         let uv_tiling = [1.0, 1.0];
@@ -80,16 +165,8 @@ impl Material {
 
         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,
@@ -107,7 +184,6 @@ impl Material {
             normal_scale: 1.0,
             uv_tiling,
             tint_buffer,
-            tint_bind_group,
             texture_tag,
             wrap_mode: TextureWrapMode::Repeat,
             emissive_texture: None,
@@ -120,11 +196,13 @@ impl Material {
         graphics: &SharedGraphicsContext,
         diffuse: &Texture,
         normal: &Texture,
+        uniform_buffer: &UniformBuffer<MaterialUniform>,
         name: &str,
     ) -> BindGroup {
+        puffin::profile_function!();
         graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
             label: Some(format!("{} texture bind group", name).as_str()),
-            layout: &graphics.layouts.texture_bind_layout,
+            layout: &graphics.layouts.material_bind_layout,
             entries: &[
                 wgpu::BindGroupEntry {
                     binding: 0,
@@ -142,6 +220,10 @@ impl Material {
                     binding: 3,
                     resource: wgpu::BindingResource::Sampler(&normal.sampler),
                 },
+                wgpu::BindGroupEntry {
+                    binding: 4,
+                    resource: uniform_buffer.buffer().as_entire_binding(),
+                },
             ],
         })
     }
@@ -164,131 +246,29 @@ impl Material {
     }
 }
 
-#[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>,
+#[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,
 }
 
-struct GLTFTextureInformation<'a> {
-    sampler: wgpu::SamplerDescriptor<'a>,
+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,
 }
 
-struct GLTFMeshInformation {
-    name: String,
-    primitive_index: usize,
-    material_index: usize,
-    mode: gltf::mesh::Mode,
-    positions: Vec<[f32; 3]>,
-    indices: Vec<u32>,
-    normals: Vec<[f32; 3]>,
-    tangents: Vec<[f32; 4]>,
-    colors: Vec<[f32; 4]>,
-    joints: Vec<[u16; 4]>,
-    weights: Vec<[f32; 4]>,
-    tex_coords0: Vec<[f32; 2]>,
-    tex_coords1: Vec<[f32; 2]>,
-}
-
-struct GLTFMaterialInformation {
-    name: String,
-    diffuse_bytes: Option<Vec<u8>>,
-    normal_bytes: Option<Vec<u8>>,
-    emissive_bytes: Option<Vec<u8>>,
-    metallic_roughness_bytes: Option<Vec<u8>>,
-    occlusion_bytes: Option<Vec<u8>>,
-    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    tint: [f32; 4],
-    emissive_factor: [f32; 3],
-    metallic_factor: f32,
-    roughness_factor: f32,
-    alpha_mode: gltf::material::AlphaMode,
-    alpha_cutoff: Option<f32>,
-    double_sided: bool,
-    occlusion_strength: f32,
-    normal_scale: f32,
-}
-
-struct ProcessedMaterialTextures {
-    name: String,
-    diffuse: Option<(Vec<u8>, (u32, u32))>,
-    normal: Option<(Vec<u8>, (u32, u32))>,
-    emissive: Option<(Vec<u8>, (u32, u32))>,
-    metallic_roughness: Option<(Vec<u8>, (u32, u32))>,
-    occlusion: Option<(Vec<u8>, (u32, u32))>,
-    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
-    tint: [f32; 4],
-    emissive_factor: [f32; 3],
-    metallic_factor: f32,
-    roughness_factor: f32,
-    alpha_mode: gltf::material::AlphaMode,
-    alpha_cutoff: Option<f32>,
-    double_sided: bool,
-    occlusion_strength: f32,
-    normal_scale: f32,
-}
-
-impl Model {
-    fn sampler_descriptor_for_texture(texture: &gltf::Texture<'_>) -> wgpu::SamplerDescriptor<'static> {
-        let sampler = texture.sampler();
-
-        let mag_filter = match sampler.mag_filter() {
-            Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest,
-            _ => wgpu::FilterMode::Linear,
-        };
-
-        let (min_filter, mipmap_filter) = match sampler.min_filter() {
-            Some(MinFilter::Nearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
-            Some(MinFilter::Linear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
-            Some(MinFilter::NearestMipmapNearest) => {
-                (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest)
-            }
-            Some(MinFilter::LinearMipmapNearest) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
-            Some(MinFilter::NearestMipmapLinear) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear),
-            Some(MinFilter::LinearMipmapLinear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
-            None => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
-        };
-
-        fn map_wrap(wrap: gltf::texture::WrappingMode) -> wgpu::AddressMode {
-            match wrap {
-                gltf::texture::WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
-                gltf::texture::WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
-                gltf::texture::WrappingMode::Repeat => wgpu::AddressMode::Repeat,
-            }
-        }
-
-        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,
-        }
-    }
-
-    fn load_texture<'a>(tex: &'a gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation<'a> {
+impl GLTFTextureInformation {
+    fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation {
         let sampler = tex.sampler();
 
         let mag_filter = match sampler.mag_filter() {
@@ -315,7 +295,7 @@ impl Model {
         }
 
         let sampler = wgpu::SamplerDescriptor {
-            label: Some(tex.name().unwrap_or("Unnamed Texture Sampler")),
+            label: None,
             address_mode_u: map_wrap(sampler.wrap_s()),
             address_mode_v: map_wrap(sampler.wrap_t()),
             address_mode_w: wgpu::AddressMode::Repeat,
@@ -357,34 +337,86 @@ impl Model {
             mip_level_count,
             pixels,
             format,
+            width,
+            height,
         }
     }
+}
+
+struct GLTFMeshInformation {
+    name: String,
+    primitive_index: usize,
+    material_index: usize,
+    mode: gltf::mesh::Mode,
+    positions: Vec<[f32; 3]>,
+    indices: Vec<u32>,
+    normals: Vec<[f32; 3]>,
+    tangents: Vec<[f32; 4]>,
+    colors: Vec<[f32; 4]>,
+    joints: Vec<[u16; 4]>,
+    weights: Vec<[f32; 4]>,
+    tex_coords0: Vec<[f32; 2]>,
+    tex_coords1: Vec<[f32; 2]>,
+}
+
+struct GLTFMaterialInformation {
+    name: String,
+    diffuse_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,
+}
+
+struct ProcessedMaterialTextures {
+    name: String,
+    diffuse: Option<(Vec<u8>, (u32, u32))>,
+    normal: Option<(Vec<u8>, (u32, u32))>,
+    emissive: Option<(Vec<u8>, (u32, u32))>,
+    metallic_roughness: Option<(Vec<u8>, (u32, u32))>,
+    occlusion: Option<(Vec<u8>, (u32, u32))>,
+    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    tint: [f32; 4],
+    emissive_factor: [f32; 3],
+    metallic_factor: f32,
+    roughness_factor: f32,
+    alpha_mode: gltf::material::AlphaMode,
+    alpha_cutoff: Option<f32>,
+    double_sided: bool,
+    occlusion_strength: f32,
+    normal_scale: f32,
+}
 
+impl Model {
     fn load_materials(
         gltf: &gltf::Document,
-        buffers: &Vec<gltf::buffer::Data>,
+        _buffers: &Vec<gltf::buffer::Data>,
+        images: &Vec<gltf::image::Data>,
     ) -> Vec<GLTFMaterialInformation> {
-        let read_texture_bytes = |texture: gltf::Texture<'_>| -> Option<Vec<u8>> {
-            let image = texture.source();
-            match image.source() {
-                gltf::image::Source::View { view, mime_type: _ } => {
-                    let buffer_data = &buffers[view.buffer().index()];
-                    let start = view.offset();
-                    let end = start + view.length();
-                    Some(buffer_data[start..end].to_vec())
-                }
-                gltf::image::Source::Uri { uri, mime_type: _ } => {
-                    log::warn!("External URI textures not supported: {}", uri);
-                    None
-                }
-            }
+        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))
         };
 
         let mut material_data = Vec::new();
 
         for material in gltf.materials() {
-            log::debug!("Processing material: {:?}", material.name());
             let material_name = material.name().unwrap_or("Unnamed Material").to_string();
+            puffin::profile_scope!("loading material", &material_name);
 
             let tint = material.pbr_metallic_roughness().base_color_factor();
             let tint = [tint[0], tint[1], tint[2], tint[3]];
@@ -396,38 +428,22 @@ impl Model {
             let occlusion_texture = material.occlusion_texture();
             let emissive_texture = material.emissive_texture();
 
-            let diffuse_bytes = diffuse_texture
-                .as_ref()
-                .and_then(|info| read_texture_bytes(info.texture()));
-            let metallic_roughness_bytes = metallic_roughness_texture
-                .as_ref()
-                .and_then(|info| read_texture_bytes(info.texture()));
-
-            let normal_bytes = normal_texture
-                .as_ref()
-                .and_then(|info| read_texture_bytes(info.texture()));
-            let occlusion_bytes = occlusion_texture
+            let diffuse_texture_info = diffuse_texture
                 .as_ref()
-                .and_then(|info| read_texture_bytes(info.texture()));
-            let emissive_bytes = emissive_texture
+                .and_then(|info| process_texture(info.texture()));
+            let metallic_roughness_texture_info = metallic_roughness_texture
                 .as_ref()
-                .and_then(|info| read_texture_bytes(info.texture()));
+                .and_then(|info| process_texture(info.texture()));
 
-            let diffuse_sampler = diffuse_texture
-                .as_ref()
-                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
-            let metallic_roughness_sampler = metallic_roughness_texture
-                .as_ref()
-                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
-            let normal_sampler = normal_texture
+            let normal_texture_info = normal_texture
                 .as_ref()
-                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
-            let occlusion_sampler = occlusion_texture
+                .and_then(|info| process_texture(info.texture()));
+            let occlusion_texture_info = occlusion_texture
                 .as_ref()
-                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
-            let emissive_sampler = emissive_texture
+                .and_then(|info| process_texture(info.texture()));
+            let emissive_texture_info = emissive_texture
                 .as_ref()
-                .map(|info| Self::sampler_descriptor_for_texture(&info.texture()));
+                .and_then(|info| process_texture(info.texture()));
 
             let emissive_factor = material.emissive_factor();
             let emissive_factor = [
@@ -451,16 +467,11 @@ impl Model {
 
             material_data.push(GLTFMaterialInformation {
                 name: material_name,
-                diffuse_bytes,
-                normal_bytes,
-                emissive_bytes,
-                metallic_roughness_bytes,
-                occlusion_bytes,
-                diffuse_sampler,
-                normal_sampler,
-                emissive_sampler,
-                metallic_roughness_sampler,
-                occlusion_sampler,
+                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,
@@ -476,16 +487,11 @@ impl Model {
         if material_data.is_empty() {
             material_data.push(GLTFMaterialInformation {
                 name: "Default".to_string(),
-                diffuse_bytes: None,
-                normal_bytes: None,
-                emissive_bytes: None,
-                metallic_roughness_bytes: None,
-                occlusion_bytes: None,
-                diffuse_sampler: None,
-                normal_sampler: None,
-                emissive_sampler: None,
-                metallic_roughness_sampler: None,
-                occlusion_sampler: None,
+                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,
@@ -507,8 +513,11 @@ impl Model {
         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
@@ -609,26 +618,221 @@ impl Model {
         Ok(())
     }
 
+    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 {
+                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
+    }
+
+    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::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>>,
-        import_scale: Option<f64>,
     ) -> 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 = if let Some(label) = label {
+        let hash = {
+            puffin::profile_scope!("hashing model");
             let mut hasher = DefaultHasher::default();
-            label.hash(&mut hasher);
-            hasher.finish()
-        } else {
-            let mut hasher = DefaultHasher::default();
-            buffer.as_ref().hash(&mut hasher);
+            if let Some(label) = label {
+                label.hash(&mut hasher);
+            } else {
+                buffer.as_ref().hash(&mut hasher);
+            };
             hasher.finish()
         };
 
@@ -636,32 +840,49 @@ impl Model {
             return Ok(model);
         }
 
-        let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?;
+        let (gltf, buffers, images) = gltf::import_slice(buffer.as_ref())?;
 
         let mut meshes = Vec::new();
         for mesh in gltf.meshes() {
             Self::load_meshes(&mesh, &buffers, &mut meshes)?;
         }
 
-        let material_data = Self::load_materials(&gltf, &buffers);
+        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
+        );
+
+        let material_data = Self::load_materials(&gltf, &buffers, &images);
 
-        let parallel_start = Instant::now();
         let processed_textures: Vec<ProcessedMaterialTextures> = material_data
             .into_par_iter()
             .map(|material_info| {
-                let material_start = Instant::now();
-
+                puffin::profile_scope!("processing material textures");
                 let material_name = material_info.name;
-                let diffuse_bytes = material_info.diffuse_bytes;
-                let normal_bytes = material_info.normal_bytes;
-                let emissive_bytes = material_info.emissive_bytes;
-                let metallic_roughness_bytes = material_info.metallic_roughness_bytes;
-                let occlusion_bytes = material_info.occlusion_bytes;
-                let diffuse_sampler = material_info.diffuse_sampler;
-                let normal_sampler = material_info.normal_sampler;
-                let emissive_sampler = material_info.emissive_sampler;
-                let metallic_roughness_sampler = material_info.metallic_roughness_sampler;
-                let occlusion_sampler = material_info.occlusion_sampler;
+
+                let 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;
@@ -672,58 +893,6 @@ impl Model {
                 let occlusion_strength = material_info.occlusion_strength;
                 let normal_scale = material_info.normal_scale;
 
-                let decode_rgba = |bytes: &Vec<u8>, label: &str| {
-                    let load_start = Instant::now();
-                    let image = match image::load_from_memory(bytes) {
-                        Ok(image) => image,
-                        Err(err) => {
-                            log::warn!(
-                                "Failed to decode {} texture for material '{}': {} ({} bytes)",
-                                label,
-                                material_name,
-                                err,
-                                bytes.len()
-                            );
-                            return None;
-                        }
-                    };
-                    log::trace!("Loading {} image to memory: {:?}", label, load_start.elapsed());
-
-                    let rgba_start = Instant::now();
-                    let rgba = image.to_rgba8();
-                    log::trace!(
-                        "Converting {} image to rgba8 took {:?}",
-                        label,
-                        rgba_start.elapsed()
-                    );
-
-                    let dimensions = image.dimensions();
-                    Some((rgba.into_raw(), dimensions))
-                };
-
-                let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| {
-                    decode_rgba(bytes, "diffuse")
-                });
-                let processed_normal = normal_bytes.as_ref().and_then(|bytes| {
-                    decode_rgba(bytes, "normal")
-                });
-                let processed_emissive = emissive_bytes.as_ref().and_then(|bytes| {
-                    decode_rgba(bytes, "emissive")
-                });
-                let processed_metallic_roughness =
-                    metallic_roughness_bytes.as_ref().and_then(|bytes| {
-                        decode_rgba(bytes, "metallic_roughness")
-                    });
-                let processed_occlusion = occlusion_bytes.as_ref().and_then(|bytes| {
-                    decode_rgba(bytes, "occlusion")
-                });
-
-                log::trace!(
-                    "Parallel processing of material '{}' took: {:?}",
-                    material_name,
-                    material_start.elapsed()
-                );
-
                 ProcessedMaterialTextures {
                     name: material_name,
                     diffuse: processed_diffuse,
@@ -749,11 +918,6 @@ impl Model {
             })
             .collect();
 
-        log::trace!(
-            "Total parallel image processing took: {:?}",
-            parallel_start.elapsed()
-        );
-
         let mut materials = Vec::new();
 
         let grey_texture = registry.grey_texture(graphics.clone());
@@ -761,7 +925,7 @@ impl Model {
             registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
 
         for processed in processed_textures {
-            let start = Instant::now();
+            puffin::profile_scope!("creating material");
 
             let material_name = processed.name;
             let processed_diffuse = processed.diffuse;
@@ -861,8 +1025,6 @@ impl Model {
             material.sync_uniform(&graphics);
 
             materials.push(material);
-
-            log::trace!("Time to create GPU texture: {:?}", start.elapsed());
         }
 
         let mut gpu_meshes = Vec::new();
@@ -890,15 +1052,6 @@ impl Model {
                 });
             }
 
-            let scale = import_scale.unwrap_or(1.0) as f32;
-            if (scale - 1.0).abs() > f32::EPSILON {
-                for vertex in &mut vertices {
-                    vertex.position[0] *= scale;
-                    vertex.position[1] *= scale;
-                    vertex.position[2] *= scale;
-                }
-            }
-
             let vertex_buffer =
                 graphics
                     .device
@@ -935,6 +1088,9 @@ impl Model {
             path: ResourceReference::from_bytes(buffer.as_ref()),
             meshes: gpu_meshes,
             materials,
+            skins,
+            animations,
+            nodes,
         };
 
         let handle = if let Some(label) = label {
@@ -955,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,
@@ -963,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)]
@@ -991,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(
@@ -1002,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);
     }
 
@@ -1036,6 +1200,7 @@ where
                 instances.clone(),
                 camera_bind_group,
                 light_bind_group,
+                None, // Provide an AnimationComponent if available in a future update
             );
         }
     }
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 6a8a7a7..d19b34e 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -93,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",
@@ -230,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 753003d..6dc4de9 100644
--- a/crates/dropbear-engine/src/procedural/cube.rs
+++ b/crates/dropbear-engine/src/procedural/cube.rs
@@ -5,6 +5,7 @@ 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;
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index ce7350a..9c6f6f2 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -8,6 +8,7 @@ use crate::utils::ResourceReference;
 use crate::model::ModelVertex;
 use std::hash::{DefaultHasher, Hasher};
 use std::sync::Arc;
+use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
 use wgpu::util::DeviceExt;
 
@@ -43,8 +44,9 @@ impl ProcedurallyGeneratedObject {
         graphics: Arc<SharedGraphicsContext>,
         material: Option<Material>,
         label: Option<&str>,
-        registry: &mut AssetRegistry,
+        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));
@@ -54,7 +56,9 @@ impl ProcedurallyGeneratedObject {
             .map(|s| s.to_string())
             .unwrap_or_else(|| format!("procedural_{hash:016x}"));
 
-        if let Some(handle) = registry.model_handle_by_hash(hash) {
+        let mut _rguard = registry.write();
+
+        if let Some(handle) = _rguard.model_handle_by_hash(hash) {
             return handle;
         }
 
@@ -87,14 +91,14 @@ impl ProcedurallyGeneratedObject {
         };
 
         let material = material.unwrap_or_else(|| {
-            let grey_handle = registry.grey_texture(graphics.clone());
+            let grey_handle = _rguard.grey_texture(graphics.clone());
             let flat_normal_handle =
-                registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
-            let grey = registry
+                _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 = registry
+            let flat_normal = _rguard
                 .get_texture(flat_normal_handle)
                 .expect("Flat normal texture handle missing")
                 .clone();
@@ -114,8 +118,11 @@ impl ProcedurallyGeneratedObject {
             path: ResourceReference::from_bytes(hash.to_le_bytes()),
             meshes: vec![mesh],
             materials: vec![material],
+            skins: Vec::new(),
+            animations: Vec::new(),
+            nodes: Vec::new(),
         };
 
-        registry.add_model_with_label(label, 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/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index dd2a210..233aff0 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -44,44 +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(2) normal: vec3<f32>,
-    @location(3) tangent: vec3<f32>,
-    @location(1) tex_coords0: vec2<f32>,
-    @location(1) tex_coords1: vec2<f32>,
-    @location(1) colour0: vec4<f32>,
-    @location(1) joints(0): vec4<u32>,
-    @location(1) joints(0): vec4<u32>,
-
-
-    @location(4) bitangent: vec3<f32>,
+    @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 {
@@ -110,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;
@@ -213,29 +233,24 @@ 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;
     }
 
-    let normal = normalize(world_normal);
-    let tangent = normalize(world_tangent.xyz);
-    let bitangent = cross(n, t) * world_tangent.w;
-
     let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
 
     let world_normal = apply_normal_map(
         in.world_normal,
         in.world_tangent,
-        bitangent,
+        in.world_bitangent,
         object_normal.xyz,
     );
 
@@ -259,48 +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 normal = normalize(world_normal);
-    let tangent = normalize(world_tangent.xyz);
-    let bitangent = cross(n, t) * world_tangent.w;
-
-    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
-
-    let world_normal = apply_normal_map(
-        in.world_normal,
-        in.world_tangent,
-        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 36bfe50..4b1dd30 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -85,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,
@@ -110,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,
@@ -148,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),
@@ -198,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),
@@ -235,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))
     }
@@ -243,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)
     }
 
@@ -257,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,
@@ -289,23 +296,26 @@ impl Texture {
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!(label.unwrap_or(""));
         let hash = AssetRegistry::hash_bytes(bytes);
         
-        let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) {
-            Ok(image) => {
-                let rgba = image.to_rgba8().into_raw();
-                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!(
+        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,
@@ -314,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))
+                    }
                 }
             }
         };
@@ -355,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,
@@ -371,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;
@@ -472,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/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
new file mode 100644
index 0000000..4119d09
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
@@ -0,0 +1,55 @@
+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
+    }
+}
+
+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..79b5b50
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
@@ -0,0 +1,4 @@
+package com.dropbear.asset
+
+interface AssetType {
+}
\ 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..a4c69da
--- /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(val id: Long): AssetType {
+    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..4ffe7ca
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
@@ -0,0 +1,22 @@
+package com.dropbear.asset
+
+class Texture(val id: Long): AssetType {
+    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/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/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/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/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? {