kitgit

tirbofish/dropbear · commit

2d2c8e2ac9d89e63dfb0de91d07b4d269fc35c86

Merge pull request #88 from tirbofish/ui feature: ui + more

Unverified

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

view full diff

diff --git a/.cargo/config.toml b/.cargo/config.toml
index 5c0c649..d2312c6 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,3 +1,3 @@
 [target.x86_64-unknown-linux-gnu]
 linker = "clang"
-rustflags = ["-C", "link-arg=-fuse-ld=mold"]
\ No newline at end of file
+rustflags = ["-C", "link-arg=-fuse-ld=mold", "-C", "relocation-model=pic"]
diff --git a/.github/workflows/create_executable.yaml b/.github/workflows/create_executable.yaml
index 2849e23..30cef50 100644
--- a/.github/workflows/create_executable.yaml
+++ b/.github/workflows/create_executable.yaml
@@ -71,7 +71,8 @@ jobs:
         with:
           targets: ${{ matrix.target }}
       - name: build eucalyptus packages
-        # run: cargo build --release --package eucalyptus-core --package eucalyptus-editor --package magna-carta --package redback-runtime --target ${{ matrix.target }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         run: cargo build --release --package eucalyptus-core --package eucalyptus-editor --package magna-carta --target ${{ matrix.target }}
       - name: prepare the artifact
         run: |
diff --git a/Cargo.toml b/Cargo.toml
index 6652f99..2a94bc3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,10 +4,11 @@ package.edition = "2024"
 package.license = "MIT OR Apache-2.0"
 package.repository = "https://github.com/tirbofish/dropbear"
 package.readme = "README.md"
+package.authors = ["Thribhu K <4tkbytes@pm.me>"]
 
 resolver = "3"
 members = [
-    "crates/*"
+    "crates/*",
 ]
 
 [workspace.dependencies]
@@ -30,11 +31,11 @@ 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"
-model_to_image = "0.1"
+#model_to_image = "0.1"
 once_cell = "1.21"
 parking_lot = {version = "0.12", features = ["deadlock_detection"] }
 rfd = "0.17"
@@ -71,13 +72,24 @@ dyn-hash = "1.0"
 semver = { version = "1.0", features = ["serde"] }
 rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] }
 cbindgen = { version = "0.29.2" }
-postcard = { version = "1.1"}
+postcard = { version = "1.1", features = ["use-std"]}
 pollster = "0.4"
+#yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] }
+#yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
+#yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
+thiserror = "2.0"
+tempfile = "3.24"
+combine = "4.6"
+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.25"
+version = "0.24"
 default-features = false
-features = ["png"]
+features = ["png", "jpeg", "hdr"]
 
 [profile.dev]
 opt-level = 1
diff --git a/README.md b/README.md
index 581fb3d..44b8560 100644
--- a/README.md
+++ b/README.md
@@ -5,26 +5,26 @@
 
 dropbear is a game engine used to create games, made in Rust and scripted with the Kotlin Language.
 
-It's name is a double entendre, with it being the nickname of koalas but also fits in nicely with the theme of rust utilising memory management with "drops".
+Its name is a double entendre, with it being the nickname of koalas but also fits in nicely with the theme of rust utilising memory management with "drops".
 
 If you might have not realised, all the crates/projects names are after Australian items.
 
 ## Projects
 
-- [dropbear-engine](https://github.com/tirbofish/dropbear/tree/main/dropbear-engine) is the rendering engine that uses wgpu and the main name of the project.
-- [dropbear-shader](https://github.com/tirbofish/dropbear/tree/main/dropbear-shader) contains WESL shaders for users to import
-- [eucalyptus-editor](https://github.com/tirbofish/dropbear/tree/main/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
-- [eucalyptus-core](https://github.com/tirbofish/dropbear/tree/main/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other.
-- [redback-runtime](https://github.com/tirbofish/dropbear/tree/main/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
+- [dropbear-engine](https://github.com/tirbofish/dropbear/tree/main/crates/dropbear-engine) is the rendering engine that uses wgpu and the main name of the project.
+- [eucalyptus-editor](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
+- [eucalyptus-core](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other.
+- [redback-runtime](https://github.com/tirbofish/dropbear/tree/main/crates/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
+- [magna-carta](https://github.com/tirbofish/dropbear/tree/main/crates/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching.
+- [kino-ui](https://github.com/tirbofish/dropbear/tree/main/crates/kino-ui) is the main runtime-side UI system to render widgets and elements. 
 
 [//]: # (- [eucalyptus-sdk]&#40;https://github.com/tirbofish/dropbear/tree/main/eucalyptus-sdk&#41; is used to develop plugins to be used with the `eucalyptus-editor`)
 
 ### Related Projects
 
-- [magna-carta](https://github.com/tirbofish/dropbear/tree/main/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching. 
-- [magna-carta-plugin](https://github.com/tirbofish/dropbear/tree/main/magna-carta-plugin) is a Gradle plugin for generating metadata during compile time with the help of the magna-carta cli tool. 
 - [dropbear_future-queue](https://github.com/tirbofish/dropbear/tree/main/dropbear_future-queue) is a handy library for dealing with async in a sync context
-- [model_to_image](https://github.com/tirbofish/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` and a custom made rasteriser. _(very crude but usable)_
+- [model_to_image](https://github.com/tirbofish/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` and a custom made rasterizer. _(very crude but usable)_
+- [slank](https://github.com/tirbofish/dropbear/tree/main/crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time.
 
 ## Build
 
@@ -103,7 +103,7 @@ a specific package, ensure you run `cargo build -p {package} -p eucalyptus-core`
 
 If you get any FFI errors (likely a getter), you compiled the library wrong. 
 
-</details>
+</details> 
 
 ### Prebuilt
 
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index b80cba2..c59f954 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -12,6 +12,7 @@ readme.workspace = true
 dropbear_future-queue = { path = "../dropbear_future-queue" }
 dropbear-traits = { path = "../dropbear-traits" }
 dropbear-macro = { path = "../dropbear-macro" }
+slank = { path = "../slank", features = ["download-slang", "use-wgpu"] }
 
 anyhow.workspace = true
 app_dirs2.workspace = true
@@ -43,11 +44,16 @@ dashmap.workspace = true
 typetag.workspace = true
 postcard.workspace = true
 pollster.workspace = true
+image.workspace = true
+puffin.workspace = true
+bitflags.workspace = true
+puffin_http.workspace = true
+
+#yakui-wgpu.workspace = true
+#yakui.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
 
-[dependencies.image]
-version = "0.25"
-default-features = false
-features = ["png", "jpeg"]
+[build-dependencies]
+slank = { path = "../slank", features = ["download-slang"] }
\ No newline at end of file
diff --git a/crates/dropbear-engine/build.rs b/crates/dropbear-engine/build.rs
new file mode 100644
index 0000000..d495733
--- /dev/null
+++ b/crates/dropbear-engine/build.rs
@@ -0,0 +1,16 @@
+use slank::{SlangShaderBuilder, SlangTarget};
+
+fn main() {
+    // to copy paste:         
+    // let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube")));
+
+    SlangShaderBuilder::new("light_cube")
+        .add_source_path("src/shaders/light.slang").unwrap()
+        .compile_to_out_dir(SlangTarget::SpirV).unwrap();
+
+    SlangShaderBuilder::new("blit_shader")
+        .add_source_path("src/shaders/blit.slang").unwrap()
+        .compile_to_out_dir(SlangTarget::SpirV).unwrap();
+
+    println!("cargo:rerun-if-changed=src/shaders");
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
new file mode 100644
index 0000000..135b58a
--- /dev/null
+++ b/crates/dropbear-engine/src/animation.rs
@@ -0,0 +1,323 @@
+use dropbear_traits::SerializableComponent;
+use std::collections::HashMap;
+use std::sync::Arc;
+use glam::Mat4;
+use wgpu::util::DeviceExt;
+use dropbear_macro::SerializableComponent;
+use crate::graphics::SharedGraphicsContext;
+use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)]
+pub struct AnimationComponent {
+    #[serde(default)]
+    pub active_animation_index: Option<usize>,
+    #[serde(default)]
+    pub time: f32,
+    #[serde(default)]
+    pub speed: f32,
+    #[serde(default)]
+    pub looping: bool,
+    #[serde(default)]
+    pub is_playing: bool,
+
+    #[serde(skip)]
+    pub local_pose: HashMap<usize, NodeTransform>,
+    #[serde(skip)]
+    pub skinning_matrices: Vec<Mat4>,
+
+    #[serde(skip)]
+    pub bone_buffer: Option<wgpu::Buffer>,
+    #[serde(skip)]
+    pub bind_group: Option<wgpu::BindGroup>,
+}
+
+impl Default for AnimationComponent {
+    fn default() -> Self {
+        Self {
+            active_animation_index: None,
+            time: 0.0,
+            speed: 1.0,
+            looping: true,
+            is_playing: true,
+            local_pose: HashMap::new(),
+            skinning_matrices: Vec::new(),
+            bone_buffer: None,
+            bind_group: None,
+        }
+    }
+}
+
+impl AnimationComponent {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn update(&mut self, dt: f32, model: &Model) {
+        puffin::profile_function!(&model.label);
+        if !self.is_playing || self.active_animation_index.is_none() {
+            return;
+        }
+
+        let anim_idx = self.active_animation_index.unwrap();
+        let animation = &model.animations[anim_idx];
+
+        self.time += dt * self.speed;
+        if self.looping {
+            if animation.duration > 0.0 {
+                self.time %= animation.duration;
+            }
+        } else {
+            self.time = self.time.clamp(0.0, animation.duration);
+            if self.time >= animation.duration {
+                self.is_playing = false;
+            }
+        }
+
+        for channel in &animation.channels {
+            let count = channel.times.len();
+            if count == 0 { continue; }
+
+            // Handle out of bounds / single keyframe
+            if count == 1 || self.time <= channel.times[0] {
+                Self::apply_single_keyframe(channel, 0, &mut self.local_pose, model);
+                continue;
+            }
+            if self.time >= channel.times[count - 1] {
+                Self::apply_single_keyframe(channel, count - 1, &mut self.local_pose, model);
+                continue;
+            }
+            
+            let next_idx = channel.times.partition_point(|&t| t <= self.time);
+            let prev_idx = next_idx.saturating_sub(1);
+
+            let start_time = channel.times[prev_idx];
+            let end_time = channel.times[next_idx];
+            let duration = end_time - start_time;
+
+            let factor = if duration > 0.0 {
+                (self.time - start_time) / duration
+            } else {
+                0.0
+            };
+
+            let transform = self.local_pose
+                .entry(channel.target_node)
+                .or_insert_with(|| {
+                    model.nodes.get(channel.target_node)
+                        .map(|n| n.transform.clone())
+                        .unwrap_or_else(NodeTransform::identity)
+                });
+
+            let dt = end_time - start_time;
+            
+            match &channel.values {
+                ChannelValues::Translations(values) => {
+                    transform.translation = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx],
+                        AnimationInterpolation::Linear => {
+                             let start = values[prev_idx];
+                             let end = values[next_idx];
+                             start.lerp(end, factor)
+                        },
+                        AnimationInterpolation::CubicSpline => {
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            
+                            let idx0 = prev_idx * 3;
+                            let idx1 = next_idx * 3;
+                            
+                            if idx1 + 1 >= values.len() {
+                                values[idx0 + 1]
+                            } else {
+                                let p0 = values[idx0 + 1];
+                                let m0 = values[idx0 + 2] * dt;
+                                let m1 = values[idx1 + 0] * dt;
+                                let p1 = values[idx1 + 1];
+                                
+                                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                                let h10 = t3 - 2.0 * t2 + t;
+                                let h01 = -2.0 * t3 + 3.0 * t2;
+                                let h11 = t3 - t2;
+                                
+                                p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
+                            }
+                        }
+                    };
+                }
+                ChannelValues::Rotations(values) => {
+                    transform.rotation = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx],
+                        AnimationInterpolation::Linear => {
+                            let start = values[prev_idx];
+                            let end = values[next_idx];
+                            start.slerp(end, factor).normalize()
+                        },
+                        AnimationInterpolation::CubicSpline => {
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            
+                            let idx0 = prev_idx * 3;
+                            let idx1 = next_idx * 3;
+                            
+                             if idx1 + 1 >= values.len() {
+                                values[idx0 + 1]
+                            } else {
+                                let p0 = values[idx0 + 1];
+                                let m0 = values[idx0 + 2] * dt;
+                                let m1 = values[idx1 + 0] * dt;
+                                let p1 = values[idx1 + 1];
+                                
+                                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                                let h10 = t3 - 2.0 * t2 + t;
+                                let h01 = -2.0 * t3 + 3.0 * t2;
+                                let h11 = t3 - t2;
+                                
+                                let res = p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11;
+                                res.normalize()
+                            }
+                        }
+                    };
+                }
+                ChannelValues::Scales(values) => {
+                    transform.scale = match channel.interpolation {
+                        AnimationInterpolation::Step => values[prev_idx],
+                        AnimationInterpolation::Linear => {
+                            let start = values[prev_idx];
+                            let end = values[next_idx];
+                            start.lerp(end, factor)
+                        },
+                        AnimationInterpolation::CubicSpline => {
+                            let t = factor;
+                            let t2 = t * t;
+                            let t3 = t2 * t;
+                            
+                            let idx0 = prev_idx * 3;
+                            let idx1 = next_idx * 3;
+                            
+                            if idx1 + 1 >= values.len() {
+                                values[idx0 + 1]
+                            } else {
+                                let p0 = values[idx0 + 1];
+                                let m0 = values[idx0 + 2] * dt;
+                                let m1 = values[idx1 + 0] * dt;
+                                let p1 = values[idx1 + 1];
+                                
+                                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
+                                let h10 = t3 - 2.0 * t2 + t;
+                                let h01 = -2.0 * t3 + 3.0 * t2;
+                                let h11 = t3 - t2;
+                                
+                                p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
+                            }
+                        }
+                    };
+                }
+            }
+        }
+
+        self.update_matrices(model);
+    }
+
+    fn apply_single_keyframe(
+        channel: &crate::model::AnimationChannel,
+        index: usize,
+        pose: &mut HashMap<usize, NodeTransform>,
+        model: &Model
+    ) {
+        let transform = pose.entry(channel.target_node).or_insert_with(|| {
+            model.nodes.get(channel.target_node)
+                .map(|n| n.transform.clone())
+                .unwrap_or_else(NodeTransform::identity)
+        });
+
+        match &channel.values {
+            ChannelValues::Translations(v) => {
+                if let Some(val) = v.get(index) { transform.translation = *val; }
+            }
+            ChannelValues::Rotations(v) => {
+                if let Some(val) = v.get(index) { transform.rotation = *val; }
+            }
+            ChannelValues::Scales(v) => {
+                if let Some(val) = v.get(index) { transform.scale = *val; }
+            }
+        }
+    }
+
+    fn update_matrices(&mut self, model: &Model) {
+        if let Some(skin) = model.skins.first() {
+            if self.skinning_matrices.len() != skin.joints.len() {
+                self.skinning_matrices.resize(skin.joints.len(), Mat4::IDENTITY);
+            }
+
+            let mut global_transforms = HashMap::new();
+            
+            for &joint_idx in &skin.joints {
+                self.resolve_global_transform(joint_idx, model, &mut global_transforms);
+            }
+
+            for (i, &joint_node_idx) in skin.joints.iter().enumerate() {
+                if let Some(global_transform) = global_transforms.get(&joint_node_idx) {
+                    let inverse_bind = skin.inverse_bind_matrices[i];
+                    self.skinning_matrices[i] = *global_transform * inverse_bind;
+                }
+            }
+        }
+    }
+
+    /// Recursively calculates and caches the global world matrix for a node.
+    fn resolve_global_transform(
+        &self,
+        node_idx: usize,
+        model: &Model,
+        cache: &mut HashMap<usize, Mat4>,
+    ) -> Mat4 {
+        if let Some(&matrix) = cache.get(&node_idx) {
+            return matrix;
+        }
+
+        let node = &model.nodes[node_idx];
+        let local_matrix = self.local_pose.get(&node_idx)
+            .map(|transform| transform.to_matrix())
+            .unwrap_or_else(|| node.transform.to_matrix());
+
+        let global_matrix = if let Some(parent_idx) = node.parent {
+            let parent_global = self.resolve_global_transform(parent_idx, model, cache);
+            parent_global * local_matrix
+        } else {
+            local_matrix
+        };
+
+        cache.insert(node_idx, global_matrix);
+        global_matrix
+    }
+
+    pub fn prepare_gpu_resources(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        if self.skinning_matrices.is_empty() { return; }
+
+        let data = bytemuck::cast_slice(&self.skinning_matrices);
+
+        if let Some(buffer) = &self.bone_buffer {
+            graphics.queue.write_buffer(buffer, 0, data);
+        } else {
+            let buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("skinning buffer"),
+                contents: data,
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            });
+
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("skinning bind group"),
+                layout: &graphics.layouts.skinning_bind_group_layout,
+                entries: &[wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: buffer.as_entire_binding(),
+                }],
+            });
+
+            self.bone_buffer = Some(buffer);
+            self.bind_group = Some(bind_group);
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index 49c4aa3..411e870 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -1,513 +1,229 @@
-use std::sync::{
-    Arc, LazyLock,
-    atomic::{AtomicU64, Ordering},
-};
-
-use dashmap::DashMap;
-
+use std::collections::HashMap;
+use std::hash::{DefaultHasher, Hash, Hasher};
+use std::marker::PhantomData;
+use std::sync::{Arc, LazyLock};
+use parking_lot::RwLock;
 use crate::{
-    graphics::{SharedGraphicsContext},
-    model::{Material, Mesh, Model, ModelId},
-    utils::ResourceReference,
     texture::Texture,
 };
+use crate::graphics::SharedGraphicsContext;
+use crate::model::Model;
 
-/// Opaque identifier returned from the [`AssetRegistry`] for stored assets.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-pub struct AssetHandle(u64);
-impl AssetHandle {
-    /// Creates a new [`AssetHandle`].
-    ///
-    /// This function does not guarantee if the raw value exists in the registry.
-    /// You will have to check yourself.
-    pub fn new(raw: impl Into<u64>) -> Self {
-        Self(raw.into())
-    }
-    /// Returns the raw/primitive [`u64`] value.
-    pub fn raw(&self) -> u64 {
-        self.0
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum AssetKind {
-    Model,
-    Material,
-    Mesh,
-}
+pub static ASSET_REGISTRY: LazyLock<Arc<RwLock<AssetRegistry>>> = LazyLock::new(|| Arc::new(RwLock::new(AssetRegistry::new())));
 
-#[derive(Debug, Eq, PartialEq, Hash)]
-pub enum PointerKind {
-    Const(&'static str),
-    Mut(&'static str),
+/// A handle with type [`T`] that provides an index to the [AssetRegistry] contents.
+#[derive(Hash, Eq, PartialEq, Debug)]
+pub struct Handle<T> {
+    pub id: u64,
+    _phantom: PhantomData<T>
 }
 
-/// Centralised cache for models and their dependent resources.
-///
-/// The registry assigns stable [`AssetHandle`] values that can be
-/// reused by systems without having to keep strong references to the
-/// underlying assets. Models are keyed by their [`ResourceReference`]
-/// while meshes and materials are keyed by `(ModelId, name)` pairs.
-pub struct AssetRegistry {
-    next_id: AtomicU64,
-
-    model_handles: DashMap<ResourceReference, AssetHandle>,
-    model_id_lookup: DashMap<ModelId, AssetHandle>,
-    model_references: DashMap<AssetHandle, ResourceReference>,
-    model_reference_lookup: DashMap<ResourceReference, AssetHandle>,
-    models: DashMap<AssetHandle, Arc<Model>>,
+impl<T> Copy for Handle<T> {}
 
-    material_lookup: DashMap<(ModelId, String), AssetHandle>,
-    material_owners: DashMap<AssetHandle, ModelId>,
-    material_references: DashMap<AssetHandle, ResourceReference>,
-    material_reference_lookup: DashMap<ResourceReference, AssetHandle>,
-    materials: DashMap<AssetHandle, Arc<Material>>,
+impl<T> Clone for Handle<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
 
-    mesh_lookup: DashMap<(ModelId, String), AssetHandle>,
-    mesh_owners: DashMap<AssetHandle, ModelId>,
-    mesh_references: DashMap<AssetHandle, ResourceReference>,
-    mesh_reference_lookup: DashMap<ResourceReference, AssetHandle>,
-    meshes: DashMap<AssetHandle, Arc<Mesh>>,
+impl<T> Handle<T> {
+    /// Creates a null handle, for when there is no way to uniquely identify a hash (such as a viewport texture).
+    ///
+    /// # Safety
+    /// You will want to watch out, as adding this onto the asset registry with a type
+    /// where there already is a null handle item, it will be overwritten and data
+    /// will not be saved. It is the reason why you will want to consider using the [Self::is_null]
+    /// function to verify if the storage of the type has gone through correctly.
+    pub const NULL: Self = Self { id: 0, _phantom: PhantomData };
 
-    /// Internal pointer database, typically used when querying in the database
-    pointers: DashMap<PointerKind, usize>,
+    /// Creates a new handle with the given ID.
+    pub fn new(id: u64) -> Self {
+        Self { id, _phantom: Default::default() }
+    }
 
-    /// Built-in textures created at runtime and reused across assets.
-    built_in_textures: DashMap<&'static str, Arc<Texture>>,
+    /// Returns true if the handle is null.
+    pub fn is_null(&self) -> bool {
+        self.id == 0
+    }
+}
 
-    /// 1×1 solid-colour textures cached by packed RGBA8.
-    solid_textures: DashMap<u32, Arc<Texture>>,
+pub struct AssetRegistry {
+    textures: HashMap<u64, Texture>,
+    texture_labels: HashMap<String, Handle<Texture>>,
 
-    /// Per-model import-scale overrides keyed by the model's resource reference.
-    ///
-    /// This is editor/user configuration and should outlive cached model data.
-    model_import_scales: DashMap<ResourceReference, f32>,
+    models: HashMap<u64, Model>,
+    model_labels: HashMap<String, Handle<Model>>,
 }
-
+/// Common
 impl AssetRegistry {
     pub fn new() -> Self {
         Self {
-            next_id: AtomicU64::new(1),
-            model_handles: DashMap::new(),
-            model_id_lookup: DashMap::new(),
-            model_references: DashMap::new(),
-            model_reference_lookup: DashMap::new(),
-            models: DashMap::new(),
-            material_lookup: DashMap::new(),
-            material_owners: DashMap::new(),
-            material_references: DashMap::new(),
-            material_reference_lookup: DashMap::new(),
-            materials: DashMap::new(),
-            mesh_lookup: DashMap::new(),
-            mesh_owners: DashMap::new(),
-            mesh_references: DashMap::new(),
-            mesh_reference_lookup: DashMap::new(),
-            meshes: DashMap::new(),
-            pointers: DashMap::new(),
-            built_in_textures: DashMap::new(),
-            solid_textures: DashMap::new(),
-            model_import_scales: DashMap::new(),
+            textures: Default::default(),
+            texture_labels: Default::default(),
+            models: Default::default(),
+            model_labels: Default::default(),
         }
     }
 
-    /// Returns the per-model import scale for the given resource reference.
-    ///
-    /// Defaults to `1.0` when no override exists.
-    pub fn model_import_scale(&self, reference: &ResourceReference) -> f32 {
-        self.model_import_scales
-            .get(reference)
-            .map(|v| *v.value())
-            .unwrap_or(1.0)
+    /// A convenient helper function for hashing a byte slice of data.
+    pub(crate) fn hash_bytes(data: &[u8]) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        data.hash(&mut hasher);
+        hasher.finish()
     }
 
-    /// Sets the per-model import scale override for the given resource reference.
-    pub fn set_model_import_scale(&self, reference: ResourceReference, scale: f32) {
-        let scale = if scale.is_finite() { scale } else { 1.0 };
-        self.model_import_scales.insert(reference, scale);
+    /// A convenient helper function for hashing a byte slice of data.
+    pub(crate) fn hash_contents<T: Hash>(data: T) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        data.hash(&mut hasher);
+        hasher.finish()
     }
 
-    /// Returns a cached 1×1 solid-colour texture, creating it on first use.
+    /// Checks if the asset registry contains a handle with the given hash.
     ///
-    /// The cache key is the packed RGBA8 bytes (little-endian).
-    pub fn solid_texture_rgba8(
-        &self,
-        graphics: Arc<SharedGraphicsContext>,
-        rgba: [u8; 4],
-    ) -> Arc<Texture> {
-        let key = u32::from_le_bytes(rgba);
-
-        if let Some(existing) = self.solid_textures.get(&key) {
-            return Arc::clone(existing.value());
-        }
-
-        let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]);
-
-        let texture = Texture::from_bytes_verbose_mipmapped(
-            graphics,
-            &rgba,
-            Some((1, 1)),
-            None,
-            None,
-            Some(label.as_str())
-        );
-        let texture = Arc::new(texture);
-
-        match self.solid_textures.entry(key) {
-            dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
-            dashmap::mapref::entry::Entry::Vacant(entry) => {
-                entry.insert(Arc::clone(&texture));
-                texture
-            }
-        }
+    /// It will check all different types, so it does not point out specifically where.
+    pub fn contains_hash(&self, hash: u64) -> bool {
+        self.textures.contains_key(&hash) || self.models.contains_key(&hash)
     }
 
-    /// Returns a cached 1×1 grey texture, creating it on first use.
+    /// Checks if the asset registry contains a handle with the given string label.
     ///
-    /// This is intended as a cheap fallback when a model/material has no diffuse texture.
-    pub fn grey_texture(&self, graphics: Arc<SharedGraphicsContext>) -> Arc<Texture> {
-        const KEY: &str = "builtin_grey_1x1";
-
-        if let Some(existing) = self.built_in_textures.get(KEY) {
-            return Arc::clone(existing.value());
-        }
-
-        // 50% grey, fully opaque.
-        let texture = self.solid_texture_rgba8(graphics, [128, 128, 128, 255]);
-
-        // Race-safe insert: if another thread beat us, reuse theirs.
-        match self.built_in_textures.entry(KEY) {
-            dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
-            dashmap::mapref::entry::Entry::Vacant(entry) => {
-                entry.insert(Arc::clone(&texture));
-                texture
-            }
-        }
+    /// It will check all different types, so it does not point out specifically where.
+    pub fn contains_label(&self, label: &str) -> bool {
+        self.texture_labels.contains_key(label) || self.model_labels.contains_key(label)
     }
+}
 
-    /// Clears all cached asset data (models/materials/meshes) from the registry.
+/// Texture stuff
+impl AssetRegistry {
+    /// Adds a texture and returns a handle.
     ///
-    /// This is intended for full scene reloads where the previous scene's assets
-    /// should be dropped and reloaded. Pointer entries are intentionally preserved.
-    pub fn clear_cached_assets(&self) {
-        self.model_handles.clear();
-        self.model_id_lookup.clear();
-        self.model_references.clear();
-        self.model_reference_lookup.clear();
-        self.models.clear();
-
-        self.material_lookup.clear();
-        self.material_owners.clear();
-        self.material_references.clear();
-        self.material_reference_lookup.clear();
-        self.materials.clear();
-
-        self.mesh_lookup.clear();
-        self.mesh_owners.clear();
-        self.mesh_references.clear();
-        self.mesh_reference_lookup.clear();
-        self.meshes.clear();
-    }
-
-    /// Adds a pointer to the asset registry.
-    pub fn add_pointer(&self, pointer_kind: PointerKind, pointer: usize) {
-        self.pointers.insert(pointer_kind, pointer);
-    }
-
-    /// Attempts to fetch a pointer from the [`AssetRegistry`] by its given [`PointerKind`]
-    pub fn get_pointer(&self, pointer_kind: PointerKind) -> Option<usize> {
-        self.pointers.get(&pointer_kind).map(|entry| *entry.value())
-    }
-
-    fn allocate_handle(&self) -> AssetHandle {
-        AssetHandle(self.next_id.fetch_add(1, Ordering::Relaxed))
-    }
-
-    /// Registers a model and caches its meshes and materials.
-    pub fn register_model(&self, reference: ResourceReference, model: Arc<Model>) -> AssetHandle {
-        let canonical = reference.clone();
-        self.model_import_scales.entry(canonical.clone()).or_insert(1.0);
-
-        let model_handle = if let Some(existing) = self.model_handles.get(&canonical) {
-            let handle = *existing;
-            self.models.insert(handle, Arc::clone(&model));
-            handle
-        } else {
-            let handle = self.allocate_handle();
-            self.models.insert(handle, Arc::clone(&model));
-            self.model_handles.insert(canonical.clone(), handle);
-            handle
-        };
-
-        self.model_id_lookup.insert(model.id, model_handle);
-
-        self.model_references
-            .insert(model_handle, canonical.clone());
-        self.model_reference_lookup.insert(canonical, model_handle);
-
-        self.cache_model_components(&model);
-
-        model_handle
-    }
-
-    /// Iterates through all models, allowing you to iterate through all items in the
-    /// model registry.
-    pub fn iter_model(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> {
-        self.iter_model_raw()
+    /// This assumes a [Texture] has already been created by you. To create a new texture,
+    /// you can use [`Texture::from_bytes`].
+    pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> {
+        let handle = texture.hash.map(|v| Handle::new(v)).unwrap_or_else(|| Handle::NULL);
+        self.textures.entry(handle.id).or_insert(texture);
+        handle
     }
 
-    pub fn iter_model_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> {
-        self.models.iter()
+    /// Adds a texture with a label. If the texture already exists (by hash),
+    /// returns the existing handle and updates the label to point at it.
+    pub fn add_texture_with_label(&mut self, label: impl Into<String>, texture: Texture) -> Handle<Texture> {
+        let handle = self.add_texture(texture);
+        self.texture_labels.insert(label.into(), handle.clone());
+        handle
     }
 
-    pub fn iter_material(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> {
-        self.iter_material_raw()
+    /// Maps a label to an existing texture handle.
+    pub fn label_texture(&mut self, label: impl Into<String>, handle: Handle<Texture>) {
+        self.texture_labels.insert(label.into(), handle.clone());
     }
 
-    pub fn iter_material_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> {
-        self.materials.iter()
-    }
-
-    /// Returns the cached model handle if it exists.
-    pub fn model_handle(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.model_handle_raw(reference)
-    }
-
-    pub fn model_handle_raw(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.model_handles.get(reference).map(|entry| *entry)
-    }
-
-    /// Fetches a model by handle.
-    pub fn get_model(&self, handle: AssetHandle) -> Option<Arc<Model>> {
-        self.get_model_raw(handle)
-    }
-
-    pub fn get_model_raw(&self, handle: AssetHandle) -> Option<Arc<Model>> {
-        self.models
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
-    }
-
-    /// Fetches a material by handle.
-    pub fn get_material(&self, handle: AssetHandle) -> Option<Arc<Material>> {
-        self.get_material_raw(handle)
-    }
-
-    pub fn get_material_raw(&self, handle: AssetHandle) -> Option<Arc<Material>> {
-        self.materials
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
+    /// Removes a label from the texture registry, but keeps it in the registry.
+    ///
+    /// When the label is removed, the [Handle] is still valid.
+    pub fn remove_label_texture(&mut self, label: &str) {
+        self.texture_labels.remove(label);
     }
 
-    /// Fetches a mesh by handle.
-    pub fn get_mesh(&self, handle: AssetHandle) -> Option<Arc<Mesh>> {
-        self.get_mesh_raw(handle)
+    /// Updates the asset server by inserting the texture provided at the location of the handle,
+    /// and removing the old texture (by returning it back to you).
+    pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> {
+        self.textures.insert(handle.id, texture)
     }
 
-    pub fn get_mesh_raw(&self, handle: AssetHandle) -> Option<Arc<Mesh>> {
-        self.meshes
-            .get(&handle)
-            .map(|entry| Arc::clone(entry.value()))
+    pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> {
+        self.textures.get(&handle.id)
     }
 
-    /// Fetches a handle from a [`ResourceReference`] by checking through
-    /// each cache
-    pub fn get_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.material_handle_from_reference(reference)
-            .or_else(|| self.mesh_handle_from_reference(reference))
-            .or_else(|| self.model_handle_from_reference(reference))
+    pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> {
+        self.texture_labels
+            .get(label)
+            .and_then(|handle| self.textures.get(&handle.id))
     }
 
-    /// Retrieves (or lazily creates) the handle for a specific material on a model.
-    pub fn material_handle(&self, model_id: ModelId, name: &str) -> Option<AssetHandle> {
-        let key = (model_id, name.to_string());
-        self.material_lookup.get(&key).map(|entry| *entry)
+    pub fn get_texture_handle_from_label(&self, label: &str) -> Option<Handle<Texture>> {
+        self.texture_labels.get(label).cloned()
     }
 
-    /// Retrieves (or lazily creates) the handle for a specific mesh on a model.
-    pub fn mesh_handle(&self, model_id: ModelId, name: &str) -> Option<AssetHandle> {
-        let key = (model_id, name.to_string());
-        self.mesh_lookup.get(&key).map(|entry| *entry)
+    pub fn texture_handle_by_hash(&self, hash: u64) -> Option<Handle<Texture>> {
+        self.textures.contains_key(&hash).then(|| Handle::new(hash))
     }
 
-    /// Returns the kind of asset associated with a handle, if known.
-    pub fn kind(&self, handle: AssetHandle) -> Option<AssetKind> {
-        if self.models.contains_key(&handle) {
-            Some(AssetKind::Model)
-        } else if self.materials.contains_key(&handle) {
-            Some(AssetKind::Material)
-        } else if self.meshes.contains_key(&handle) {
-            Some(AssetKind::Mesh)
-        } else {
-            None
+    pub fn grey_texture(&mut self, graphics: Arc<SharedGraphicsContext>) -> Handle<Texture> {
+        let grey_handle = Handle::new(Self::hash_contents("Solid texture [128, 128, 128, 255]"));
+        
+        if self.contains_hash(grey_handle.id) {
+            return grey_handle;
         }
-    }
-
-    /// Returns `true` if the handle exists in any asset cache.
-    pub fn contains_handle(&self, handle: AssetHandle) -> bool {
-        self.models.contains_key(&handle)
-            || self.materials.contains_key(&handle)
-            || self.meshes.contains_key(&handle)
-    }
-
-    /// Returns `true` if the handle represents the expected asset kind.
-    pub fn is_handle_kind(&self, handle: AssetHandle, expected: AssetKind) -> bool {
-        matches!(self.kind(handle), Some(kind) if kind == expected)
-    }
-
-    /// Returns the `ResourceReference` recorded for a model handle, if any.
-    pub fn model_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.model_references
-            .get(&handle)
-            .map(|entry| entry.value().clone())
-    }
-
-    /// Attempts to resolve a model handle from a `ResourceReference`.
-    pub fn model_handle_from_reference(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.model_reference_lookup
-            .get(reference)
-            .map(|entry| *entry)
-    }
-
-    /// Attempts to resolve a model handle directly from a [`ModelId`].
-    pub fn model_handle_from_id(&self, model_id: ModelId) -> Option<AssetHandle> {
-        self.model_id_lookup.get(&model_id).map(|entry| *entry)
-    }
-
-    /// Returns `true` if the handle refers to a material asset.
-    pub fn is_material(&self, handle: AssetHandle) -> bool {
-        self.materials.contains_key(&handle)
-    }
 
-    /// Returns `true` if the handle refers to a mesh asset.
-    pub fn is_mesh(&self, handle: AssetHandle) -> bool {
-        self.meshes.contains_key(&handle)
+        self.solid_texture_rgba8(graphics, [128, 128, 128, 255])
     }
 
-    /// Returns `true` if the handle refers to a model asset.
-    pub fn is_model(&self, handle: AssetHandle) -> bool {
-        self.models.contains_key(&handle)
-    }
-
-    /// Returns the owning model ID for the given material handle.
-    pub fn material_owner(&self, handle: AssetHandle) -> Option<ModelId> {
-        self.material_owner_raw(handle)
-    }
+    pub fn solid_texture_rgba8(
+        &mut self,
+        graphics: Arc<SharedGraphicsContext>,
+        rgba: [u8; 4],
+    ) -> Handle<Texture> {
+        let handle = Handle::new(Self::hash_bytes(&rgba));
 
-    pub fn material_owner_raw(&self, handle: AssetHandle) -> Option<ModelId> {
-        self.material_owners.get(&handle).map(|entry| *entry)
-    }
+        if self.contains_hash(handle.id) {
+            return handle;
+        }
 
-    /// Returns the owning model ID for the given mesh handle.
-    pub fn mesh_owner(&self, handle: AssetHandle) -> Option<ModelId> {
-        self.mesh_owners.get(&handle).map(|entry| *entry)
-    }
+        let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]);
 
-    /// Returns the synthetic `ResourceReference` associated with a material handle.
-    pub fn material_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.material_reference_for_handle_raw(handle)
+        let texture = Texture::from_bytes_verbose_mipmapped(
+            graphics,
+            &rgba,
+            Some((1, 1)),
+            None,
+            None,
+            Some(label.as_str())
+        );
+        
+        self.add_texture_with_label(label, texture)
     }
+}
 
-    pub fn material_reference_for_handle_raw(
-        &self,
-        handle: AssetHandle,
-    ) -> Option<ResourceReference> {
-        self.material_references
-            .get(&handle)
-            .map(|entry| entry.value().clone())
+/// Model stuff
+impl AssetRegistry {
+    pub fn add_model(&mut self, model: Model) -> Handle<Model> {
+        let handle = Handle::new(model.hash);
+        self.models.entry(handle.id).or_insert(model);
+        handle
     }
 
-    /// Returns the synthetic `ResourceReference` associated with a mesh handle.
-    pub fn mesh_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.mesh_reference_for_handle_raw(handle)
+    pub fn add_model_with_label(&mut self, label: impl Into<String>, model: Model) -> Handle<Model> {
+        let handle = self.add_model(model);
+        self.model_labels.insert(label.into(), handle.clone());
+        handle
     }
 
-    pub fn mesh_reference_for_handle_raw(&self, handle: AssetHandle) -> Option<ResourceReference> {
-        self.mesh_references
-            .get(&handle)
-            .map(|entry| entry.value().clone())
+    pub fn label_model(&mut self, label: impl Into<String>, handle: Handle<Model>) {
+        self.model_labels.insert(label.into(), handle.clone());
     }
 
-    /// Attempts to resolve a material handle from its synthetic `ResourceReference`.
-    pub fn material_handle_from_reference(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.material_handle_from_reference_raw(reference)
+    pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Model> {
+        self.models.insert(handle.id, model)
     }
 
-    pub fn material_handle_from_reference_raw(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.material_reference_lookup
-            .get(reference)
-            .map(|entry| *entry)
+    pub fn get_model(&self, handle: Handle<Model>) -> Option<&Model> {
+        self.models.get(&handle.id)
     }
 
-    /// Attempts to resolve a mesh handle from its synthetic `ResourceReference`.
-    pub fn mesh_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> {
-        self.mesh_handle_from_reference_raw(reference)
+    pub fn get_model_by_label(&self, label: &str) -> Option<&Model> {
+        self.model_labels
+            .get(label)
+            .and_then(|handle| self.models.get(&handle.id))
     }
 
-    pub fn mesh_handle_from_reference_raw(
-        &self,
-        reference: &ResourceReference,
-    ) -> Option<AssetHandle> {
-        self.mesh_reference_lookup
-            .get(reference)
-            .map(|entry| *entry)
+    pub fn get_model_handle_from_label(&self, label: &str) -> Option<Handle<Model>> {
+        self.model_labels.get(label).cloned()
     }
 
-    fn cache_model_components(&self, model: &Arc<Model>) {
-        let model_id = model.id;
-
-        for material in &model.materials {
-            let name = material.name.clone();
-            let key = (model_id, name.clone());
-            let handle = if let Some(existing) = self.material_lookup.get(&key) {
-                *existing
-            } else {
-                let handle = self.allocate_handle();
-                self.material_lookup.insert(key.clone(), handle);
-                handle
-            };
-
-            self.material_owners.insert(handle, model_id);
-
-            let reference = material_reference_from_model(model.as_ref(), &name)
-                .or_else(|| material_reference_fallback(model_id, &name));
-
-            if let Some(reference) = reference {
-                self.material_references.insert(handle, reference.clone());
-                self.material_reference_lookup.insert(reference, handle);
-            }
-
-            self.materials.insert(handle, Arc::new(material.clone()));
-        }
-
-        for mesh in &model.meshes {
-            let name = mesh.name.clone();
-            let key = (model_id, name.clone());
-            let handle = if let Some(existing) = self.mesh_lookup.get(&key) {
-                *existing
-            } else {
-                let handle = self.allocate_handle();
-                self.mesh_lookup.insert(key.clone(), handle);
-                handle
-            };
-
-            self.mesh_owners.insert(handle, model_id);
-
-            if let Some(reference) = mesh_reference(model_id, &name) {
-                self.mesh_references.insert(handle, reference.clone());
-                self.mesh_reference_lookup.insert(reference, handle);
-            }
-
-            self.meshes.insert(handle, Arc::new(mesh.clone()));
-        }
+    pub fn model_handle_by_hash(&self, hash: u64) -> Option<Handle<Model>> {
+        self.models.contains_key(&hash).then(|| Handle::new(hash))
     }
 }
 
@@ -515,74 +231,4 @@ impl Default for AssetRegistry {
     fn default() -> Self {
         Self::new()
     }
-}
-
-pub static ASSET_REGISTRY: LazyLock<AssetRegistry> = LazyLock::new(AssetRegistry::new);
-
-fn material_reference_from_model(model: &Model, name: &str) -> Option<ResourceReference> {
-    let base_uri = model.path.as_uri()?;
-    let material_component = sanitize_material_component(name);
-
-    if material_component.is_empty() {
-        return None;
-    }
-
-    let base = base_uri.trim_end_matches('/');
-    let combined = format!("{}/{}", base, material_component);
-
-    ResourceReference::from_euca_uri(combined).ok()
-}
-
-fn material_reference_fallback(model_id: ModelId, name: &str) -> Option<ResourceReference> {
-    resource_reference_for("materials", model_id, name)
-}
-
-fn mesh_reference(model_id: ModelId, name: &str) -> Option<ResourceReference> {
-    resource_reference_for("meshes", model_id, name)
-}
-
-fn resource_reference_for(
-    category: &str,
-    model_id: ModelId,
-    name: &str,
-) -> Option<ResourceReference> {
-    let sanitized = sanitize_component(name);
-    if sanitized.is_empty() {
-        return None;
-    }
-    let uri = format!("euca://{}/{}/{}", category, model_id.raw(), sanitized);
-    ResourceReference::from_euca_uri(uri).ok()
-}
-
-fn sanitize_material_component(input: &str) -> String {
-    let trimmed = input.trim();
-    if trimmed.is_empty() {
-        return String::new();
-    }
-
-    trimmed
-        .chars()
-        .map(|ch| match ch {
-            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch,
-            _ => '_',
-        })
-        .collect()
-}
-
-fn sanitize_component(input: &str) -> String {
-    let trimmed = input.trim();
-    if trimmed.is_empty() {
-        return String::new();
-    }
-
-    trimmed
-        .chars()
-        .map(|ch| {
-            let lower = ch.to_ascii_lowercase();
-            match lower {
-                'a'..='z' | '0'..='9' | '-' | '_' => lower,
-                _ => '_',
-            }
-        })
-        .collect()
-}
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/buffer.rs b/crates/dropbear-engine/src/buffer.rs
index 27552d0..7dd16d7 100644
--- a/crates/dropbear-engine/src/buffer.rs
+++ b/crates/dropbear-engine/src/buffer.rs
@@ -64,6 +64,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> {
     }
 
     pub fn write(&self, queue: &wgpu::Queue, value: &T) {
+        puffin::profile_function!(&self.label);
         queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
     }
 
@@ -106,6 +107,7 @@ impl<T: bytemuck::Pod> StorageBuffer<T> {
     }
 
     pub fn write(&self, queue: &wgpu::Queue, value: &T) {
+        puffin::profile_function!(self.label());
         queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
     }
 
@@ -143,6 +145,7 @@ impl<T: bytemuck::Pod> ResizableBuffer<T> {
     }
 
     pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) {
+        puffin::profile_function!(&self.label);
         if data.is_empty() {
             return;
         }
diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index 325733d..2adaa22 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -94,7 +94,7 @@ pub struct CameraBuilder {
 }
 
 impl Camera {
-    pub const CAMERA_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = 
+    pub const CAMERA_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> =
         BindGroupLayoutDescriptor {
             entries: &[BindGroupLayoutEntry {
                 binding: 0,
@@ -231,52 +231,55 @@ 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) {
-        let mvp = self.build_vp();
-        self.uniform.view_proj = mvp.as_mat4().to_cols_array_2d();
+        puffin::profile_function!();
+        let mut uniform = self.uniform;
+        uniform.update(self);
+        self.uniform = uniform;
     }
 
-    pub fn move_forwards(&mut self) {
+    pub fn move_forwards(&mut self, dt: f32) {
         let forward = (self.target - self.eye).normalize();
-        self.eye += forward * self.settings.speed;
-        self.target += forward * self.settings.speed;
+        self.eye += forward * self.settings.speed * dt as f64;
+        self.target += forward * self.settings.speed * dt as f64;
     }
 
-    pub fn move_back(&mut self) {
+    pub fn move_back(&mut self, dt: f32) {
         let forward = (self.target - self.eye).normalize();
-        self.eye -= forward * self.settings.speed;
-        self.target -= forward * self.settings.speed;
+        self.eye -= forward * self.settings.speed * dt as f64;
+        self.target -= forward * self.settings.speed * dt as f64;
     }
 
-    pub fn move_right(&mut self) {
+    pub fn move_right(&mut self, dt: f32) {
         let forward = (self.target - self.eye).normalize();
         // LH: right = up.cross(forward)
         let right = self.up.cross(forward).normalize();
-        self.eye += right * self.settings.speed;
-        self.target += right * self.settings.speed;
+        self.eye += right * self.settings.speed * dt as f64;
+        self.target += right * self.settings.speed * dt as f64;
     }
 
-    pub fn move_left(&mut self) {
+    pub fn move_left(&mut self, dt: f32) {
         let forward = (self.target - self.eye).normalize();
         let right = self.up.cross(forward).normalize();
-        self.eye -= right * self.settings.speed;
-        self.target -= right * self.settings.speed;
+        self.eye -= right * self.settings.speed * dt as f64;
+        self.target -= right * self.settings.speed * dt as f64;
     }
 
-    pub fn move_up(&mut self) {
+    pub fn move_up(&mut self, dt: f32) {
         let up = self.up.normalize();
-        self.eye += up * self.settings.speed;
-        self.target += up * self.settings.speed;
+        self.eye += up * self.settings.speed * dt as f64;
+        self.target += up * self.settings.speed * dt as f64;
     }
 
-    pub fn move_down(&mut self) {
+    pub fn move_down(&mut self, dt: f32) {
         let up = self.up.normalize();
-        self.eye -= up * self.settings.speed;
-        self.target -= up * self.settings.speed;
+        self.eye -= up * self.settings.speed * dt as f64;
+        self.target -= up * self.settings.speed * dt as f64;
     }
 
     pub fn track_mouse_delta(&mut self, dx: f64, dy: f64) {
@@ -298,20 +301,40 @@ impl Camera {
 #[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct CameraUniform {
     pub view_position: [f32; 4],
+    pub view: [[f32; 4]; 4],
     pub view_proj: [[f32; 4]; 4],
+    pub inv_proj: [[f32; 4]; 4],
+    pub inv_view: [[f32; 4]; 4],
 }
 
 impl CameraUniform {
     pub fn new() -> Self {
         Self {
             view_position: [0.0; 4],
+            view: Mat4::IDENTITY.to_cols_array_2d(),
             view_proj: Mat4::IDENTITY.to_cols_array_2d(),
+            inv_proj: Mat4::IDENTITY.to_cols_array_2d(),
+            inv_view: Mat4::IDENTITY.to_cols_array_2d(),
         }
     }
 
     pub fn update(&mut self, camera: &mut Camera) {
         self.view_position = camera.eye.as_vec3().extend(1.0).to_array();
-        self.view_proj = (DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * camera.build_vp())
+        
+        let vp = camera.build_vp();
+        let view = camera.view_mat;
+        let proj = camera.proj_mat;
+        
+        let wgpu_matrix = DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX);
+        self.view = view.as_mat4().to_cols_array_2d();
+        self.view_proj = vp.as_mat4().to_cols_array_2d();
+        
+        self.inv_proj = (wgpu_matrix * proj)
+            .inverse()
+            .as_mat4()
+            .to_cols_array_2d();
+        self.inv_view = view
+            .inverse()
             .as_mat4()
             .to_cols_array_2d();
     }
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/features.rs b/crates/dropbear-engine/src/features.rs
new file mode 100644
index 0000000..b905c5e
--- /dev/null
+++ b/crates/dropbear-engine/src/features.rs
@@ -0,0 +1,301 @@
+// Copyright ⓒ 2017 Fletcher Nichol and/or applicable contributors.
+//
+// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE or
+// <http://www.apache.org/licenses/LICENSE-2.0>) or the MIT license (see<LICENSE-MIT or
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be copied, modified, or
+// distributed except according to those terms.
+
+//! `features` is a small library that implements runtime [feature toggles][fowler_toggles] for
+//! your library or program allowing behavior to be changed on boot or dynamically at runtime using
+//! the same compiled binary artifact. This is different from cargo's [feature][cargo_feature]
+//! support which uses conditional compilation.
+//!
+//! At its core, it is a macro (`features!`) that takes a collection of feature flag names which it
+//! uses to generate a module containing a function to enable a feature toggle (`::enable()`), a
+//! function to disable a feature toggle (`::disable()`) and a function to check if a feature
+//! toggle is enabled (`::is_enabled()`).
+//!
+//! ## Example
+//!
+//! Basic example:
+//!
+//! ```
+//! #[macro_use]
+//! extern crate bitflags;
+//! #[macro_use]
+//! extern crate features;
+//!
+//! features! {
+//!     pub mod feature {
+//!         const Alpha = 0b00000001,
+//!         const Beta = 0b00000010
+//!     }
+//! }
+//!
+//! fn main() {
+//!     assert_eq!(false, feature::is_enabled(feature::Alpha));
+//!     assert_eq!(false, feature::is_enabled(feature::Beta));
+//!
+//!     feature::enable(feature::Beta);
+//!     assert_eq!(false, feature::is_enabled(feature::Alpha));
+//!     assert_eq!(true, feature::is_enabled(feature::Beta));
+//! }
+//! ```
+//!
+//! Multiple feature sets:
+//!
+//! ```
+//! #[macro_use]
+//! extern crate bitflags;
+//! #[macro_use]
+//! extern crate features;
+//!
+//! features! {
+//!     pub mod ux {
+//!         const JsonOutput = 0b10000000,
+//!         const VerboseOutput = 0b01000000
+//!     }
+//! }
+//!
+//! features! {
+//!     pub mod srv {
+//!         const Http2Downloading = 0b10000000,
+//!         const BitTorrentDownloading = 0b01000000
+//!     }
+//! }
+//!
+//! fn main() {
+//!     // Parse CLI args, environment, read config file etc...
+//!     srv::enable(srv::BitTorrentDownloading);
+//!     ux::enable(ux::JsonOutput);
+//!
+//!     if srv::is_enabled(srv::Http2Downloading) {
+//!         println!("Downloading via http2...");
+//!     } else if srv::is_enabled(srv::BitTorrentDownloading) {
+//!         println!("Downloading via bit torrent...");
+//!     } else {
+//!         println!("Downloading the old fashioned way...");
+//!     }
+//!
+//!     if ux::is_enabled(ux::VerboseOutput) {
+//!         println!("COOL");
+//!     }
+//! }
+//! ```
+//!
+//! ## Feature Toggle References
+//!
+//! Here are some articles and projects which insipred the implementation of `features`:
+//!
+//! * [Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) (Martin Fowler's
+//! blog)
+//! * [Using Feature Flags to Ship Changes with
+//! Confidence](https://blog.travis-ci.com/2014-03-04-use-feature-flags-to-ship-changes-with-confidence/)
+//! (TravisCI's blog)
+//! * [Feature Toggles are one of the worst kinds of Technical
+//! Debt](http://swreflections.blogspot.ca/2014/08/feature-toggles-are-one-of-worst-kinds.html)
+//! (excellent cautionary tale and warning)
+//! * [Feature toggle](https://en.wikipedia.org/wiki/Feature_toggle) (Wikipedia article)
+//! * [Ruby feature gem](https://github.com/mgsnova/feature) (nice prior art)
+//!
+//! [fowler_toggles]: https://martinfowler.com/articles/feature-toggles.html
+//! [cargo_feature]: http://doc.crates.io/manifest.html#the-features-section
+
+/// The `features!` macro generates a module to contain all feature toggling logic.
+///
+/// # Examples
+///
+/// Basic example:
+///
+/// ```
+/// #[macro_use]
+/// extern crate bitflags;
+/// #[macro_use]
+/// extern crate features;
+///
+/// features! {
+///     pub mod feature {
+///         const Alpha = 0b00000001,
+///         const Beta = 0b00000010
+///     }
+/// }
+///
+/// fn main() {
+///     assert_eq!(false, feature::is_enabled(feature::Alpha));
+///     assert_eq!(false, feature::is_enabled(feature::Beta));
+///
+///     feature::enable(feature::Beta);
+///     assert_eq!(false, feature::is_enabled(feature::Alpha));
+///     assert_eq!(true, feature::is_enabled(feature::Beta));
+/// }
+/// ```
+///
+/// Multiple feature sets:
+///
+/// ```
+/// #[macro_use]
+/// extern crate bitflags;
+/// #[macro_use]
+/// extern crate features;
+///
+/// features! {
+///     pub mod ux {
+///         const JsonOutput = 0b10000000,
+///         const VerboseOutput = 0b01000000
+///     }
+/// }
+///
+/// features! {
+///     pub mod srv {
+///         const Http2Downloading = 0b10000000,
+///         const BitTorrentDownloading = 0b01000000
+///     }
+/// }
+///
+/// fn main() {
+///     // Parse CLI args, environment, read config file etc...
+///     srv::enable(srv::BitTorrentDownloading);
+///     ux::enable(ux::JsonOutput);
+///
+///     if srv::is_enabled(srv::Http2Downloading) {
+///         println!("Downloading via http2...");
+///     } else if srv::is_enabled(srv::BitTorrentDownloading) {
+///         println!("Downloading via bit torrent...");
+///     } else {
+///         println!("Downloading the old fashioned way...");
+///     }
+///
+///     if ux::is_enabled(ux::VerboseOutput) {
+///         println!("COOL");
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! features {
+    (mod $mod_name:ident {
+        $($(#[$flag_attr:meta])* const $flag:ident = $value:expr),+
+    }) => {
+        #[allow(non_upper_case_globals)]
+        mod $mod_name {
+            use $crate::features;
+            features! {
+                @_impl mod $mod_name {
+                    $($(#[$flag_attr])* const $flag = $value),+
+                }
+            }
+        }
+    };
+    (pub mod $mod_name:ident {
+        $($(#[$flag_attr:meta])* const $flag:ident = $value:expr),+
+    }) => {
+        #[allow(non_upper_case_globals)]
+        pub mod $mod_name {
+            use $crate::features;
+            features! {
+                @_impl mod $mod_name {
+                    $($(#[$flag_attr])* const $flag = $value),+
+                }
+            }
+        }
+    };
+    (@_impl mod $mod_name:ident {
+        $($(#[$flag_attr:meta])* const $flag:ident = $value:expr),+
+    }) => {
+        use std::sync::atomic;
+        use bitflags::bitflags;
+
+        bitflags! {
+            pub struct Flags: usize {
+                $(
+                    $(#[$flag_attr])* const $flag = $value;
+                )+
+            }
+        }
+        $(
+            pub const $flag: Flags = Flags::$flag;
+        )+
+
+        static FEATURES: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
+
+        #[allow(dead_code)]
+        pub fn enable(flag: Flags) {
+            let mut flags = Flags::from_bits_truncate(FEATURES.load(atomic::Ordering::Relaxed));
+            flags.insert(flag);
+            FEATURES.store(flags.bits(), atomic::Ordering::Relaxed);
+        }
+
+        #[allow(dead_code)]
+        pub fn disable(flag: Flags) {
+            let mut flags = Flags::from_bits_truncate(FEATURES.load(atomic::Ordering::Relaxed));
+            flags.remove(flag);
+            FEATURES.store(flags.bits(), atomic::Ordering::Relaxed);
+        }
+
+        #[allow(dead_code)]
+        pub fn is_enabled(flag: Flags) -> bool {
+            flags().contains(flag)
+        }
+
+        #[allow(dead_code)]
+        pub fn flags() -> Flags {
+            Flags::from_bits_truncate(FEATURES.load(atomic::Ordering::Relaxed))
+        }
+    };
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn enabling() {
+        features! {
+            pub mod f {
+                const Alpha = 0b00000001,
+                const Beta = 0b00000010
+            }
+        }
+
+        assert_eq!(false, f::is_enabled(f::Alpha));
+        assert_eq!(false, f::is_enabled(f::Beta));
+
+        f::enable(f::Alpha);
+        assert_eq!(true, f::is_enabled(f::Alpha));
+        assert_eq!(false, f::is_enabled(f::Beta));
+
+        // Enable again
+        f::enable(f::Alpha);
+        assert_eq!(true, f::is_enabled(f::Alpha));
+        assert_eq!(false, f::is_enabled(f::Beta));
+
+        f::enable(f::Beta);
+        assert_eq!(true, f::is_enabled(f::Alpha));
+        assert_eq!(true, f::is_enabled(f::Beta));
+    }
+
+    #[test]
+    fn disabling() {
+        features! {
+            pub mod f {
+                const Cool = 0b01000000,
+                const Beans = 0b10000000
+            }
+        }
+
+        f::enable(f::Cool);
+        f::enable(f::Beans);
+        assert_eq!(true, f::is_enabled(f::Cool));
+        assert_eq!(true, f::is_enabled(f::Beans));
+
+        f::disable(f::Cool);
+        assert_eq!(false, f::is_enabled(f::Cool));
+        assert_eq!(true, f::is_enabled(f::Beans));
+
+        // Disable again
+        f::disable(f::Cool);
+        assert_eq!(false, f::is_enabled(f::Cool));
+        assert_eq!(true, f::is_enabled(f::Beans));
+
+        f::disable(f::Beans);
+        assert_eq!(false, f::is_enabled(f::Cool));
+        assert_eq!(false, f::is_enabled(f::Beans));
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 6a085db..bbc0234 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -7,12 +7,13 @@ use crate::{
 use dropbear_future_queue::FutureQueue;
 use egui::{Context, TextureId};
 use glam::{DMat4, DQuat, DVec3, Mat3};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use std::sync::Arc;
 use wgpu::*;
 use winit::window::Window;
 
 use crate::mipmap::MipMapper;
+use crate::pipelines::hdr::HdrPipeline;
 
 pub const NO_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/no-texture.png");
 
@@ -21,6 +22,7 @@ pub struct SharedGraphicsContext {
     pub queue: Arc<Queue>,
     pub surface: Arc<Surface<'static>>,
     pub surface_format: TextureFormat,
+    pub surface_config: Arc<RwLock<SurfaceConfiguration>>,
     pub instance: Arc<wgpu::Instance>,
     pub layouts: Arc<BindGroupLayouts>,
     pub window: Arc<Window>,
@@ -29,8 +31,10 @@ 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>>,
+    // pub yakui_texture: yakui::TextureId,
 }
 
 impl SharedGraphicsContext {
@@ -71,8 +75,11 @@ 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(),
+            // yakui_texture: state.yakui_texture.clone(),
+            surface_config: state.config.clone(),
         }
     }
 }
@@ -141,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,
                 },
             ],
@@ -191,6 +198,7 @@ impl InstanceRaw {
 
 /// A wrapper to the [wgpu::CommandEncoder]
 pub struct CommandEncoder {
+    queue: Arc<Queue>,
     inner: wgpu::CommandEncoder,
 }
 
@@ -212,18 +220,20 @@ impl CommandEncoder {
     /// Creates a new instance of a command encoder. 
     pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self {
         Self {
+            queue: graphics.queue.clone(),
             inner: graphics.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }),
         }
     }
-    
-    /// Submits the command encoder for execution. 
-    /// 
-    /// Panics if an unwinding error is caught, or just returns the error as normal. 
-    pub fn submit(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> {
+
+    /// Submits the command encoder for execution.
+    ///
+    /// 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(|| {
-            graphics.queue.submit(std::iter::once(command_buffer));
+            self.queue.submit(std::iter::once(command_buffer));
         })) {
             Ok(_) => {Ok(())}
             Err(_) => {
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 2d3af5b..5841f21 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -18,6 +18,22 @@ pub mod utils;
 pub mod texture;
 pub mod pipelines;
 pub mod mipmap;
+pub mod sky;
+pub mod features;
+pub mod animation;
+
+features! {
+    pub mod build {
+        const Debug = 0b00000001,
+        const Release = 0b00000000
+    }
+}
+
+features! {
+    pub mod graphics_features {
+        const SupportsStorage = 0b00000001
+    }
+}
 
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
 pub const PHYSICS_STEP_RATE: u32 = 120;
@@ -62,16 +78,18 @@ pub use gilrs;
 pub use wgpu;
 pub use winit;
 use winit::window::{WindowAttributes, WindowId};
+use crate::pipelines::hdr::HdrPipeline;
 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.
@@ -79,13 +97,12 @@ 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,
     pub device: Arc<Device>,
     pub queue: Arc<Queue>,
-    pub config: SurfaceConfiguration,
+    pub config: Arc<RwLock<SurfaceConfiguration>>,
     pub is_surface_configured: bool,
     pub layouts: Arc<BindGroupLayouts>,
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
@@ -94,32 +111,16 @@ pub struct State {
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
     pub mipmapper: Arc<MipMapper>,
+    pub hdr: Arc<RwLock<HdrPipeline>>,
 
     physics_accumulator: Duration,
 
     pub scene_manager: scene::Manager,
+    // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>,
+    // pub yakui_texture: yakui::TextureId,
 }
 
 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();
@@ -142,15 +143,18 @@ impl State {
 
         let limits = wgpu::Limits {
             max_bind_groups: 8,
-            ..Default::default()
+            ..wgpu::Limits::defaults()
         };
 
+        let supported_features = adapter.features();
+        let features = supported_features & wgpu::Features::all_webgpu_mask();
+
         let (device, queue) = adapter
             .request_device(&wgpu::DeviceDescriptor {
                 label: Some(format!("{} graphics device", title).as_str()),
-                required_features: wgpu::Features::empty(),
+                required_features: features,
                 required_limits: limits,
-                experimental_features: unsafe { ExperimentalFeatures::enabled() },
+                experimental_features: ExperimentalFeatures::default(),
                 memory_hints: Default::default(),
                 trace: wgpu::Trace::Off,
             })
@@ -161,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();
@@ -210,21 +218,22 @@ Hardware:
 
         let surface_caps = surface.get_capabilities(&adapter);
 
-        // let surface_format = surface_caps
-        //     .formats
-        //     .iter()
-        //     .find(|f| f.is_srgb())
-        //     .copied()
-        //     .unwrap_or(TextureFormat::Rgba16Float);
+        let surface_format = surface_caps
+            .formats
+            .iter()
+            .find(|f| f.is_srgb())
+            .copied()
+            .unwrap_or(TextureFormat::Rgba16Float);
         
         let config = SurfaceConfiguration {
             usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
-            format: Texture::TEXTURE_FORMAT,
+            format: surface_format,
             width: initial_width,
             height: initial_height,
             present_mode: surface_caps.present_modes[0],
-            alpha_mode: surface_caps.alpha_modes[0],
-            view_formats: vec![],
+            // alpha_mode: surface_caps.alpha_modes[0],
+            alpha_mode: wgpu::CompositeAlphaMode::Auto,
+            view_formats: vec![surface_format.add_srgb_suffix()],
             desired_maximum_frame_latency: 2,
         };
 
@@ -240,7 +249,7 @@ Hardware:
 
         let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new(
             &device,
-            config.format,
+            surface_format,
             None,
             1,
             &window,
@@ -275,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: &[
@@ -288,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,
                         },
@@ -306,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: &[
@@ -327,12 +376,71 @@ Hardware:
             ],
         });
 
+        let device = Arc::new(device);
+        let queue = Arc::new(queue);
+
+        let hdr = Arc::new(RwLock::new(HdrPipeline::new(
+            &device,
+            &config,
+            config.format.add_srgb_suffix(),
+        )));
+
+        // let yakui_renderer = Arc::new(Mutex::new(yakui_wgpu::YakuiWgpu::new(
+        //     &device,
+        //     &queue
+        // )));
+
+        // let yakui_texture = yakui_renderer.lock().add_texture(
+        //     viewport_texture.view.clone(),
+        //     wgpu::FilterMode::default(),
+        //     wgpu::FilterMode::default(),
+        //     wgpu::FilterMode::default(),
+        //     wgpu::AddressMode::default(),
+        // );
+
+        let environment_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("environment bind group layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                            view_dimension: wgpu::TextureViewDimension::Cube,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
+                        count: None,
+                    },
+                ],
+            });
+
+        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: Texture::TEXTURE_FORMAT,
-            device: Arc::new(device),
-            queue: Arc::new(queue),
-            config,
+            surface_format,
+            device,
+            queue,
+            config: Arc::new(RwLock::new(config)),
             is_surface_configured,
             depth_texture,
             window,
@@ -346,14 +454,17 @@ 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,
         };
 
         Ok(result)
@@ -362,16 +473,20 @@ Hardware:
     /// A helper function that changes the surface config when resized (+ depth texture).
     pub fn resize(&mut self, width: u32, height: u32) {
         if width > 0 && height > 0 {
-            self.config.width = width;
-            self.config.height = height;
-            self.surface.configure(&self.device, &self.config);
+            {
+                let mut config = self.config.write();
+                config.width = width;
+                config.height = height;
+            }
+            self.surface.configure(&self.device, &self.config.read());
             self.is_surface_configured = true;
+            self.hdr.write().resize(&self.device, width, height);
         }
 
         self.depth_texture =
-            Texture::depth_texture(&self.config, &self.device, Some("depth texture"));
+            Texture::depth_texture(&self.config.read(), &self.device, Some("depth texture"));
         self.viewport_texture =
-            Texture::viewport(&self.config, &self.device, Some("viewport texture"));
+            Texture::viewport(&self.config.read(), &self.device, Some("viewport texture"));
         self.egui_renderer
             .lock()
             .renderer()
@@ -391,22 +506,25 @@ Hardware:
         event_loop: &ActiveEventLoop,
         graphics: Arc<SharedGraphicsContext>,
     ) -> anyhow::Result<Vec<scene::SceneCommand>> {
+        puffin::profile_function!();
         if !self.is_surface_configured {
             return Ok(Vec::new());
         }
-
+        
+        let config = self.config.read().clone();
+        
         let output = match self.surface.get_current_texture() {
             Ok(val) => val,
             Err(e) => {
                 return match e {
                     SurfaceError::Lost => {
                         log_once::warn_once!("Surface lost, reconfiguring...");
-                        self.surface.configure(&self.device, &self.config);
+                        self.surface.configure(&self.device, &config);
                         Ok(Vec::new())
                     }
                     SurfaceError::Outdated => {
                         log_once::warn_once!("Surface outdated, reconfiguring...");
-                        self.surface.configure(&self.device, &self.config);
+                        self.surface.configure(&self.device, &config);
                         Ok(Vec::new())
                     }
                     SurfaceError::Timeout => {
@@ -425,22 +543,29 @@ Hardware:
         };
 
         let screen_descriptor = ScreenDescriptor {
-            size_in_pixels: [self.config.width, self.config.height],
+            size_in_pixels: [config.width, config.height],
             pixels_per_point: self.window.scale_factor() as f32,
         };
 
         let view = output
             .texture
-            .create_view(&wgpu::TextureViewDescriptor::default());
+            .create_view(&wgpu::TextureViewDescriptor {
+                label: Some("surface texture view descriptor"),
+                format: Some(self.config.read().format.add_srgb_suffix()),
+                ..Default::default()
+            });
 
         { // 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"));
 
             {
+                let hdr = self.hdr.read();
                 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("surface clear pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &view,
+                        view: hdr.view(),
+                        // view: &view,
                         depth_slice: None,
                         resolve_target: None,
                         ops: wgpu::Operations {
@@ -454,7 +579,7 @@ Hardware:
                 });
             }
 
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
         }
@@ -494,6 +619,11 @@ Hardware:
                 label: Some("egui render encoder"),
             });
 
+        {
+            let hdr = self.hdr.read();
+            hdr.process(&mut encoder, &self.viewport_texture.view);
+        }
+        
         self.egui_renderer.lock().end_frame_and_draw(
             &self.device,
             &self.queue,
@@ -830,8 +960,18 @@ impl App {
     /// Creates a new instance of the application. It only sets the default for the struct + the
     /// window config.
     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 {
-            backends: wgpu::Backends::all(),
+            backends: wgpu::Backends::PRIMARY,
             ..Default::default()
         }));
 
@@ -865,13 +1005,21 @@ 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)?
         );
 
         let window_id = window.id();
 
-        let mut win_state = pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone()))?;
+        let mut win_state = match pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone())) {
+            Ok(v) => v,
+            Err(e) => {
+                // force a panic because pollster doesnt panic on error
+                panic!("Failed to create window: {}", e);
+            }
+        };
 
         let size = win_state.window.inner_size();
         win_state.resize(size.width, size.height);
@@ -984,6 +1132,8 @@ impl ApplicationHandler for App {
             .lock()
             .handle_input(&state.window, &event);
 
+        state.scene_manager.handle_event(&event);
+
         match event {
             WindowEvent::Resized(size) => {
                 state.resize(size.width, size.height);
@@ -993,8 +1143,10 @@ impl ApplicationHandler for App {
             WindowEvent::RedrawRequested => {
                 self.future_queue.poll();
 
-                let frame_start = Instant::now();
+                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 74e7d4d..ce76cd5 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -12,6 +12,9 @@ use glam::{DMat4, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
 use wgpu::{BindGroup};
+use crate::asset::{Handle, ASSET_REGISTRY};
+use crate::model::Material;
+use crate::procedural::{ProcObj, ProcedurallyGeneratedObject};
 
 pub const MAX_LIGHTS: usize = 10;
 
@@ -240,7 +243,7 @@ impl LightComponent {
 #[derive(Clone)]
 pub struct Light {
     pub uniform: LightUniform,
-    pub cube_model: Arc<Model>,
+    pub cube_model: Handle<Model>,
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
     pub bind_group: BindGroup,
@@ -270,11 +273,9 @@ impl Light {
         transform: Transform,
         label: Option<&str>,
     ) -> Self {
+        puffin::profile_function!();
         let forward = DVec3::new(0.0, 0.0, -1.0);
-        let mut direction = transform.rotation * forward;
-        if matches!(light.light_type, LightType::Directional) {
-            direction = -direction;
-        }
+        let direction = transform.rotation * forward;
 
         let uniform = LightUniform {
             position: dvec3_to_uniform_array(transform.position),
@@ -291,15 +292,13 @@ impl Light {
 
         log::trace!("Created new light uniform");
 
-        let cube_model = Model::load_from_memory(
-            graphics.clone(),
-            include_bytes!("../../../resources/models/cube.glb").to_vec(),
-            label,
-            None
-        )
-        .await
-        .expect("failed to load light cube model")
-        .get();
+        let cube_model = ProcedurallyGeneratedObject::cuboid(DVec3::ONE)
+            .build_model(
+                graphics.clone(),
+                None,
+                None,
+                ASSET_REGISTRY.clone()
+            );
 
         let label_str = label.unwrap_or("Light").to_string();
 
@@ -340,13 +339,11 @@ 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);
-        let mut direction = transform.rotation * forward;
-        if matches!(light.light_type, LightType::Directional) {
-            direction = -direction;
-        }
+        let direction = transform.rotation * forward;
         self.uniform.direction =
             dvec3_direction_to_uniform_array(direction, light.outer_cutoff_angle);
 
@@ -365,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 d397eca..9ca57e2 100644
--- a/crates/dropbear-engine/src/mipmap.rs
+++ b/crates/dropbear-engine/src/mipmap.rs
@@ -1,4 +1,6 @@
-use crate::texture::Texture;
+use slank::{include_slang, utils::WgpuUtils};
+
+use crate::{texture::Texture};
 
 pub struct MipMapper {
     blit_mipmap: wgpu::RenderPipeline,
@@ -10,10 +12,11 @@ pub struct MipMapper {
 
 impl MipMapper {
     pub fn new(device: &wgpu::Device) -> Self {
-        let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("mipmap blit shader"),
-            source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/blit.wgsl").into()),
-        });
+        puffin::profile_function!();
+        let blit_shader = device.create_shader_module(slank::CompiledSlangShader::from_bytes(
+            "mipmap blit_shader", 
+            include_slang!("blit_shader")
+        ).create_wgpu_shader());
 
         // Keep this SRGB so we can render directly into the SRGB textures we create for materials.
         let blit_format = Texture::TEXTURE_FORMAT;
@@ -88,10 +91,12 @@ impl MipMapper {
             bind_group_layouts: &[&storage_texture_layout],
             push_constant_ranges: &[],
         });
+
         let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
             label: Some("mipmap compute shader"),
-            source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()),
+            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/mipmap.wgsl").into()),
         });
+
         let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
             label: Some("Mipmapper"),
             layout: Some(&pipeline_layout),
@@ -122,6 +127,7 @@ impl MipMapper {
         queue: &wgpu::Queue,
         texture: &Texture,
     ) -> anyhow::Result<()> {
+        puffin::profile_function!();
         let texture = &texture.texture;
 
         match texture.format() {
@@ -269,6 +275,7 @@ impl MipMapper {
         queue: &wgpu::Queue,
         texture: &Texture,
     ) -> anyhow::Result<()> {
+        puffin::profile_function!();
         let texture = &texture.texture;
 
         match texture.format() {
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 93d69f7..58837b3 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -1,13 +1,12 @@
-use crate::asset::AssetRegistry;
+use crate::asset::{AssetRegistry, Handle};
 use crate::buffer::UniformBuffer;
 use crate::{
-    asset::{ASSET_REGISTRY, AssetHandle},
     graphics::{SharedGraphicsContext},
     utils::{ResourceReference},
     texture::{Texture, TextureWrapMode}
 };
-use image::GenericImageView;
-use parking_lot::Mutex;
+// use image::GenericImageView;
+use parking_lot::{Mutex, RwLock};
 use rayon::prelude::*;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
@@ -16,178 +15,126 @@ use std::ops::Deref;
 use std::sync::{Arc, LazyLock};
 use std::time::Instant;
 use std::{mem, ops::Range, path::PathBuf};
-use glam::{Vec2, Vec3};
+use gltf::image::Format;
+use gltf::texture::MinFilter;
+use puffin::profile_scope;
 use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup};
 
-pub static MODEL_CACHE: LazyLock<Mutex<HashMap<String, Arc<Model>>>> =
-    LazyLock::new(|| Mutex::new(HashMap::new()));
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct ModelId(pub u64);
-
-impl ModelId {
-    pub fn raw(&self) -> u64 {
-        self.0
-    }
-}
-
 #[derive(Clone)]
 pub struct Model {
+    pub(crate) hash: u64, // also the id related to the handle
     pub label: String,
     pub path: ResourceReference,
     pub meshes: Vec<Mesh>,
     pub materials: Vec<Material>,
-    pub id: ModelId,
+    pub skins: Vec<Skin>,
+    pub animations: Vec<Animation>,
+    pub nodes: Vec<Node>,
 }
 
-/// A shared, GPU-backed renderable shape.
-///
-/// This is intentionally a lightweight wrapper around an `Arc<Model>`.
-/// It exists so other systems can treat both model-loaded and procedurally
-/// generated geometry uniformly, while retaining clone-on-write semantics.
 #[derive(Clone)]
-pub struct SharedShape {
-    model: Arc<Model>,
+pub struct Mesh {
+    pub name: String,
+    pub vertex_buffer: wgpu::Buffer,
+    pub index_buffer: wgpu::Buffer,
+    pub num_elements: u32,
+    pub material: usize,
+    pub vertices: Vec<ModelVertex>,
 }
 
-impl SharedShape {
-    pub fn new(model: Arc<Model>) -> Self {
-        Self { model }
-    }
-
-    pub fn get(&self) -> Arc<Model> {
-        Arc::clone(&self.model)
-    }
-
-    pub fn make_mut(&mut self) -> &mut Model {
-        Arc::make_mut(&mut self.model)
-    }
+#[derive(Clone)]
+pub struct Material {
+    pub name: String,
+    pub diffuse_texture: Texture,
+    pub normal_texture: Texture,
+    pub bind_group: wgpu::BindGroup,
+    pub tint: [f32; 4],
+    pub emissive_factor: [f32; 3],
+    pub metallic_factor: f32,
+    pub roughness_factor: f32,
+    pub alpha_mode: gltf::material::AlphaMode,
+    pub alpha_cutoff: Option<f32>,
+    pub double_sided: bool,
+    pub occlusion_strength: f32,
+    pub normal_scale: f32,
+    pub uv_tiling: [f32; 2],
+    pub tint_buffer: UniformBuffer<MaterialUniform>,
+    pub texture_tag: Option<String>,
+    pub wrap_mode: TextureWrapMode,
+    pub emissive_texture: Option<Texture>,
+    pub metallic_roughness_texture: Option<Texture>,
+    pub occlusion_texture: Option<Texture>,
 }
 
-impl Deref for SharedShape {
-    type Target = Model;
-
-    fn deref(&self) -> &Self::Target {
-        self.model.as_ref()
-    }
+/// Represents a node in the scene graph (can be a joint/bone or a mesh)
+#[derive(Clone, Debug)]
+pub struct Node {
+    pub name: String,
+    pub parent: Option<usize>,
+    pub children: Vec<usize>,
+    pub transform: NodeTransform,
 }
 
-#[derive(Clone)]
-pub struct LoadedModel {
-    pub(crate) inner: Arc<SharedShape>,
-    handle: AssetHandle,
+/// Local transform of a node relative to its parent
+#[derive(Clone, Debug)]
+pub struct NodeTransform {
+    pub translation: glam::Vec3,
+    pub rotation: glam::Quat,
+    pub scale: glam::Vec3,
 }
 
-impl LoadedModel {
-    pub fn new(inner: Arc<Model>) -> Self {
-        Self::new_raw(&ASSET_REGISTRY, inner)
-    }
-
-    pub fn new_raw(registry: &AssetRegistry, inner: Arc<Model>) -> Self {
-        let reference = inner.path.clone();
-        let handle = registry.register_model(reference, Arc::clone(&inner));
-        let inner = Arc::new(SharedShape::new(inner));
-        Self { inner, handle }
-    }
-
-    pub fn from_registered(handle: AssetHandle, inner: Arc<Model>) -> Self {
-        let inner = Arc::new(SharedShape::new(inner));
-        Self { inner, handle }
-    }
-
-    pub fn from_asset_handle_raw(registry: &AssetRegistry, handle: AssetHandle) -> Option<Self> {
-        registry
-            .get_model(handle)
-            .map(|model| Self::from_registered(handle, model))
-    }
-
-    pub fn from_asset_handle(handle: AssetHandle) -> Option<Arc<LoadedModel>> {
-        Self::from_asset_handle_raw(&ASSET_REGISTRY, handle).map(|model| Arc::new(model))
-    }
-
-    /// Returns the unique identifier of the underlying model asset.
-    pub fn id(&self) -> ModelId {
-        self.inner.id
-    }
-
-    /// Returns the asset handle associated with the underlying model.
-    pub fn asset_handle(&self) -> AssetHandle {
-        self.handle
+impl NodeTransform {
+    pub fn to_matrix(&self) -> glam::Mat4 {
+        glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
     }
 
-    pub fn matches_resource(&self, reference: &ResourceReference) -> bool {
-        self.inner.matches_resource(reference)
-    }
-
-    /// Provides shared access to the underlying model.
-    pub fn get(&self) -> Arc<Model> {
-        self.inner.get()
-    }
-
-    /// Provides mutable access to the underlying model data, cloning if shared.
-    pub fn make_mut(&mut self) -> &mut Model {
-        let shape = Arc::make_mut(&mut self.inner);
-        shape.make_mut()
-    }
-
-    /// Re-registers the model with the global asset registry, ensuring cached
-    /// sub-assets stay in sync after mutations.
-    pub fn refresh_registry(&mut self) {
-        self.refresh_registry_raw(&ASSET_REGISTRY);
-    }
-
-    pub fn refresh_registry_raw(&mut self, registry: &AssetRegistry) {
-        let reference = self.inner.path.clone();
-        let updated_handle = registry.register_model(reference, self.get());
-        self.handle = updated_handle;
-    }
-
-    pub fn contains_material_handle(&self, handle: AssetHandle) -> bool {
-        self.contains_material_handle_raw(&ASSET_REGISTRY, handle)
-    }
-
-    pub fn contains_material_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        handle: AssetHandle,
-    ) -> bool {
-        self.inner.contains_material_handle_raw(registry, handle)
-    }
-
-    pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool {
-        self.contains_material_reference_raw(&ASSET_REGISTRY, reference)
+    pub fn identity() -> Self {
+        Self {
+            translation: glam::Vec3::ZERO,
+            rotation: glam::Quat::IDENTITY,
+            scale: glam::Vec3::ONE,
+        }
     }
+}
 
-    pub fn contains_material_reference_raw(
-        &self,
-        registry: &AssetRegistry,
-        reference: &ResourceReference,
-    ) -> bool {
-        self.inner
-            .contains_material_reference_raw(registry, reference)
-    }
+/// A skin defines how a mesh is bound to a skeleton
+#[derive(Clone)]
+pub struct Skin {
+    pub name: String,
+    /// Indices of joints (nodes) in the Model's nodes array
+    pub joints: Vec<usize>,
+    /// Inverse bind matrices - one per joint
+    pub inverse_bind_matrices: Vec<glam::Mat4>,
+    /// Optional root joint index
+    pub skeleton_root: Option<usize>,
 }
 
-impl std::ops::Deref for LoadedModel {
-    type Target = Model;
+/// An animation that can be played on a skeleton
+#[derive(Clone)]
+pub struct Animation {
+    pub name: String,
+    pub channels: Vec<AnimationChannel>,
+    pub duration: f32,
+}
 
-    fn deref(&self) -> &Self::Target {
-        self.inner.deref()
-    }
+/// Describes how an animation affects a specific node
+#[derive(Clone)]
+pub struct AnimationChannel {
+    /// Target node index in the Model's nodes array
+    pub target_node: usize,
+    /// Keyframe times
+    pub times: Vec<f32>,
+    /// Animation data
+    pub values: ChannelValues,
+    /// Interpolation method
+    pub interpolation: AnimationInterpolation,
 }
 
 #[derive(Clone)]
-pub struct Material {
-    pub name: String,
-    pub diffuse_texture: Texture,
-    pub normal_texture: Texture,
-    pub bind_group: wgpu::BindGroup,
-    pub tint: [f32; 4],
-    pub uv_tiling: [f32; 2],
-    pub tint_buffer: UniformBuffer<MaterialUniform>,
-    pub tint_bind_group: wgpu::BindGroup,
-    pub texture_tag: Option<String>,
-    pub wrap_mode: TextureWrapMode,
+pub enum ChannelValues {
+    Translations(Vec<glam::Vec3>),
+    Rotations(Vec<glam::Quat>),
+    Scales(Vec<glam::Vec3>),
 }
 
 impl Material {
@@ -196,39 +143,30 @@ impl Material {
         name: impl Into<String>,
         diffuse_texture: Texture,
         normal_texture: Texture,
-    ) -> Self {
-        Self::new_with_tint(graphics, name, diffuse_texture, normal_texture, [1.0, 1.0, 1.0, 1.0], None)
-    }
-
-    pub fn new_with_tint(
-        graphics: Arc<SharedGraphicsContext>,
-        name: impl Into<String>,
-        diffuse_texture: Texture,
-        normal_texture: Texture,
         tint: [f32; 4],
         texture_tag: Option<String>,
     ) -> Self {
-        let name: String = name.into();
+        puffin::profile_function!();
+        let name = name.into();
 
         let uv_tiling = [1.0, 1.0];
         let uniform = MaterialUniform {
-            colour: tint,
+            base_colour: tint,
+            emissive: [0.0, 0.0, 0.0],
+            emissive_strength: 1.0,
+            metallic: 1.0,
+            roughness: 1.0,
+            normal_scale: 1.0,
+            occlusion_strength: 1.0,
+            alpha_cutoff: 0.5,
             uv_tiling,
-            _pad: [0.0, 0.0],
+            _pad: 0.0,
         };
-        
+
         let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform");
         tint_buffer.write(&graphics.queue, &uniform);
-        let tint_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
-            layout: &graphics.layouts.material_tint_bind_layout,
-            entries: &[wgpu::BindGroupEntry {
-                binding: 0,
-                resource: tint_buffer.buffer().as_entire_binding(),
-            }],
-            label: Some("material tint bind group"),
-        });
-
-        let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &name);
+        
+        let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &tint_buffer, &name);
 
         Self {
             name,
@@ -236,11 +174,21 @@ impl Material {
             normal_texture,
             bind_group,
             tint,
+            emissive_factor: [0.0, 0.0, 0.0],
+            metallic_factor: 1.0,
+            roughness_factor: 1.0,
+            alpha_mode: gltf::material::AlphaMode::Opaque,
+            alpha_cutoff: None,
+            double_sided: false,
+            occlusion_strength: 1.0,
+            normal_scale: 1.0,
             uv_tiling,
             tint_buffer,
-            tint_bind_group,
             texture_tag,
             wrap_mode: TextureWrapMode::Repeat,
+            emissive_texture: None,
+            metallic_roughness_texture: None,
+            occlusion_texture: None,
         }
     }
 
@@ -248,376 +196,748 @@ impl Material {
         graphics: &SharedGraphicsContext,
         diffuse: &Texture,
         normal: &Texture,
+        uniform_buffer: &UniformBuffer<MaterialUniform>,
         name: &str,
     ) -> BindGroup {
-        graphics.
-                device
-                .create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some(format!("{} mipmapped compute bind group", name).as_str()),
-                    layout: &graphics.layouts.texture_bind_layout,
-                    entries: &[
-                        wgpu::BindGroupEntry {
-                            binding: 0,
-                            resource: wgpu::BindingResource::TextureView(
-                                &diffuse.view,
-                            ),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 1,
-                            resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 2,
-                            resource: wgpu::BindingResource::TextureView(&normal.view),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 3,
-                            resource: wgpu::BindingResource::Sampler(&normal.sampler),
-                        },
-                    ],
-                })
+        puffin::profile_function!();
+        graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some(format!("{} texture bind group", name).as_str()),
+            layout: &graphics.layouts.material_bind_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&diffuse.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: wgpu::BindingResource::TextureView(&normal.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 3,
+                    resource: wgpu::BindingResource::Sampler(&normal.sampler),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 4,
+                    resource: uniform_buffer.buffer().as_entire_binding(),
+                },
+            ],
+        })
     }
 
-    pub fn set_tint(&mut self, graphics: &SharedGraphicsContext, tint: [f32; 4]) {
-        self.tint = tint;
+    pub fn sync_uniform(&self, graphics: &SharedGraphicsContext) {
         let uniform = MaterialUniform {
-            colour: tint,
+            base_colour: self.tint,
+            emissive: self.emissive_factor,
+            emissive_strength: 1.0,
+            metallic: self.metallic_factor,
+            roughness: self.roughness_factor,
+            normal_scale: self.normal_scale,
+            occlusion_strength: self.occlusion_strength,
+            alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5),
             uv_tiling: self.uv_tiling,
-            _pad: [0.0, 0.0],
+            _pad: 0.0,
         };
 
         self.tint_buffer.write(&graphics.queue, &uniform);
     }
+}
 
-    pub fn set_uv_tiling(&mut self, graphics: &SharedGraphicsContext, tiling: [f32; 2]) {
-        self.uv_tiling = tiling;
-        let uniform = MaterialUniform {
-            colour: self.tint,
-            uv_tiling: tiling,
-            _pad: [0.0, 0.0],
-        };
-        self.tint_buffer.write(&graphics.queue, &uniform);
-    }
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AnimationInterpolation {
+    /// The animated values are linearly interpolated between keyframes
+    Linear,
+    /// The animated values remain constant between keyframes
+    Step,
+    /// The animated values are interpolated using a cubic spline
+    CubicSpline,
 }
 
-#[derive(Clone)]
-pub struct Mesh {
-    pub name: String,
-    pub vertex_buffer: wgpu::Buffer,
-    pub index_buffer: wgpu::Buffer,
-    pub num_elements: u32,
-    pub material: usize,
-    pub vertices: Vec<ModelVertex>,
+struct GLTFTextureInformation {
+    sampler: wgpu::SamplerDescriptor<'static>,
+    pixels: Vec<u8>,
+    width: u32,
+    height: u32,
+    #[allow(dead_code)]
+    mip_level_count: u32,
+    #[allow(dead_code)]
+    format: wgpu::TextureFormat,
 }
 
-impl Model {
-    /// Replaces the material textures (diffuse + normal) for the material identified by `material_name`.
-    ///
-    /// Note: `bind_group` must match the provided textures.
-    pub fn set_material_texture(
-        &mut self,
-        material_name: &str,
-        diffuse_texture: Texture,
-        normal_texture: Texture,
-        bind_group: wgpu::BindGroup,
-        texture_tag: Option<String>,
-    ) -> bool {
-        if let Some(material) = self
-            .materials
-            .iter_mut()
-            .find(|mat| mat.name == material_name)
-        {
-            material.diffuse_texture = diffuse_texture;
-            material.normal_texture = normal_texture;
-            material.bind_group = bind_group;
-            material.texture_tag = texture_tag;
-            true
-        } else {
-            false
-        }
-    }
+impl GLTFTextureInformation {
+    fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation {
+        let sampler = tex.sampler();
 
-    /// Removes any stored texture tag for the supplied material.
-    pub fn clear_material_texture_tag(&mut self, material_name: &str) -> bool {
-        if let Some(material) = self
-            .materials
-            .iter_mut()
-            .find(|mat| mat.name == material_name)
-        {
-            material.texture_tag = None;
-            true
-        } else {
-            false
+        let mag_filter = match sampler.mag_filter() {
+            Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest,
+            _ => wgpu::FilterMode::Linear,
+        };
+
+        let (min_filter, mipmap_filter) = match sampler.min_filter() {
+            Some(MinFilter::Nearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
+            Some(MinFilter::Linear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
+            Some(MinFilter::NearestMipmapNearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
+            Some(MinFilter::LinearMipmapNearest) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
+            Some(MinFilter::NearestMipmapLinear) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear),
+            Some(MinFilter::LinearMipmapLinear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
+            None => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear),
+        };
+
+        fn map_wrap(wrap: gltf::texture::WrappingMode) -> wgpu::AddressMode {
+            match wrap {
+                gltf::texture::WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
+                gltf::texture::WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
+                gltf::texture::WrappingMode::Repeat => wgpu::AddressMode::Repeat,
+            }
         }
-    }
 
-    /// Returns `true` if a material with `material_name` exists within this model.
-    pub fn contains_material(&self, material_name: &str) -> bool {
-        self.materials.iter().any(|mat| mat.name == material_name)
-    }
+        let sampler = wgpu::SamplerDescriptor {
+            label: None,
+            address_mode_u: map_wrap(sampler.wrap_s()),
+            address_mode_v: map_wrap(sampler.wrap_t()),
+            address_mode_w: wgpu::AddressMode::Repeat,
+            mag_filter,
+            min_filter,
+            mipmap_filter,
+            lod_min_clamp: 0.0,
+            lod_max_clamp: 32.0,
+            compare: None,
+            anisotropy_clamp: 1,
+            border_color: None,
+        };
 
-    /// Returns the registered asset handle for this model, if available.
-    pub fn asset_handle(&self) -> Option<AssetHandle> {
-        self.asset_handle_raw(&ASSET_REGISTRY)
-    }
+        let image_index = tex.source().index();
+        let image_data = &images[image_index];
 
-    pub fn asset_handle_raw(&self, registry: &AssetRegistry) -> Option<AssetHandle> {
-        registry.model_handle_from_reference(&self.path)
-    }
+        let width = image_data.width;
+        let height = image_data.height;
 
-    /// Returns `true` if this model was loaded from the specified resource reference.
-    pub fn matches_resource(&self, reference: &ResourceReference) -> bool {
-        &self.path == reference
-    }
+        let mip_level_count = (width.max(height) as f32).log2().floor() as u32 + 1;
 
-    /// Returns `true` if this model owns the supplied material handle.
-    pub fn contains_material_handle(&self, material_handle: AssetHandle) -> bool {
-        self.contains_material_handle_raw(&ASSET_REGISTRY, material_handle)
-    }
+        let (pixels, format) = match image_data.format {
+            Format::R8 => (image_data.pixels.clone(), wgpu::TextureFormat::R8Unorm),
+            Format::R8G8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rg8Unorm),
+            Format::R8G8B8 => {
+                let mut rgba = Vec::with_capacity(image_data.pixels.len() / 3 * 4);
+                for chunk in image_data.pixels.chunks(3) {
+                    rgba.extend_from_slice(chunk);
+                    rgba.push(255);
+                }
+                (rgba, wgpu::TextureFormat::Rgba8Unorm)
+            },
+            Format::R8G8B8A8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rgba8Unorm),
+            _ => panic!("Unsupported format"),
+        };
 
-    pub fn contains_material_handle_raw(
-        &self,
-        registry: &AssetRegistry,
-        material_handle: AssetHandle,
-    ) -> bool {
-        registry.material_owner(material_handle) == Some(self.id)
+        GLTFTextureInformation {
+            sampler,
+            mip_level_count,
+            pixels,
+            format,
+            width,
+            height,
+        }
     }
+}
 
-    /// Returns `true` if this model owns a material registered under the provided resource reference.
-    pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool {
-        self.contains_material_reference_raw(&ASSET_REGISTRY, reference)
-    }
+struct GLTFMeshInformation {
+    name: String,
+    primitive_index: usize,
+    material_index: usize,
+    mode: gltf::mesh::Mode,
+    positions: Vec<[f32; 3]>,
+    indices: Vec<u32>,
+    normals: Vec<[f32; 3]>,
+    tangents: Vec<[f32; 4]>,
+    colors: Vec<[f32; 4]>,
+    joints: Vec<[u16; 4]>,
+    weights: Vec<[f32; 4]>,
+    tex_coords0: Vec<[f32; 2]>,
+    tex_coords1: Vec<[f32; 2]>,
+}
 
-    pub fn contains_material_reference_raw(
-        &self,
-        registry: &AssetRegistry,
-        reference: &ResourceReference,
-    ) -> bool {
-        registry
-            .material_handle_from_reference(reference)
-            .map_or(false, |handle| {
-                self.contains_material_handle_raw(registry, handle)
-            })
-    }
+struct GLTFMaterialInformation {
+    name: String,
+    diffuse_texture: Option<GLTFTextureInformation>,
+    normal_texture: Option<GLTFTextureInformation>,
+    emissive_texture: Option<GLTFTextureInformation>,
+    metallic_roughness_texture: Option<GLTFTextureInformation>,
+    occlusion_texture: Option<GLTFTextureInformation>,
+    tint: [f32; 4],
+    emissive_factor: [f32; 3],
+    metallic_factor: f32,
+    roughness_factor: f32,
+    alpha_mode: gltf::material::AlphaMode,
+    alpha_cutoff: Option<f32>,
+    double_sided: bool,
+    occlusion_strength: f32,
+    normal_scale: f32,
+}
 
-    /// Returns `true` if any material on this model is tagged with `texture_tag`.
-    pub fn contains_texture_tag(&self, texture_tag: &str) -> bool {
-        self.materials
-            .iter()
-            .any(|mat| mat.texture_tag.as_deref() == Some(texture_tag))
-    }
+struct ProcessedMaterialTextures {
+    name: String,
+    diffuse: Option<(Vec<u8>, (u32, u32))>,
+    normal: Option<(Vec<u8>, (u32, u32))>,
+    emissive: Option<(Vec<u8>, (u32, u32))>,
+    metallic_roughness: Option<(Vec<u8>, (u32, u32))>,
+    occlusion: Option<(Vec<u8>, (u32, u32))>,
+    diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    normal_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>,
+    tint: [f32; 4],
+    emissive_factor: [f32; 3],
+    metallic_factor: f32,
+    roughness_factor: f32,
+    alpha_mode: gltf::material::AlphaMode,
+    alpha_cutoff: Option<f32>,
+    double_sided: bool,
+    occlusion_strength: f32,
+    normal_scale: f32,
+}
 
-    /// Returns `true` if the specified material currently carries `texture_tag`.
-    pub fn material_has_texture_tag(&self, material_name: &str, texture_tag: &str) -> bool {
-        self.materials
-            .iter()
-            .find(|mat| mat.name == material_name)
-            .and_then(|mat| mat.texture_tag.as_deref())
-            == Some(texture_tag)
-    }
+impl Model {
+    fn load_materials(
+        gltf: &gltf::Document,
+        _buffers: &Vec<gltf::buffer::Data>,
+        images: &Vec<gltf::image::Data>,
+    ) -> Vec<GLTFMaterialInformation> {
+        puffin::profile_function!();
+        let process_texture = |texture: gltf::Texture<'_>| -> Option<GLTFTextureInformation> {
+            puffin::profile_scope!("reading texture bytes", texture.name().unwrap_or("Unnamed Texture"));
+            Some(GLTFTextureInformation::fetch(&texture, images))
+        };
 
-    pub async fn load_from_memory<B>(
-        graphics: Arc<SharedGraphicsContext>,
-        buffer: B,
-        label: Option<&str>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel>
-    where
-        B: AsRef<[u8]>,
-    {
-        Self::load_from_memory_raw(
-            graphics,
-            buffer,
-            label,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-            import_scale
-        )
-        .await
-    }
+        let mut material_data = Vec::new();
 
-    pub async fn load_from_memory_raw<B>(
-        graphics: Arc<SharedGraphicsContext>,
-        buffer: B,
-        label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel>
-    where
-        B: AsRef<[u8]>,
-    {
-        let start = Instant::now();
-        let mut hasher = DefaultHasher::new();
-
-        let scale_key = import_scale.unwrap_or(1.0);
-        let cache_key = format!(
-            "{}::import_scale={:.8}",
-            label.unwrap_or("default"),
-            scale_key
-        );
+        for material in gltf.materials() {
+            let material_name = material.name().unwrap_or("Unnamed Material").to_string();
+            puffin::profile_scope!("loading material", &material_name);
 
-        if let Some(cached_model) = {
-            let cache_guard = cache.lock();
-            cache_guard.get(&cache_key).cloned()
-        } {
-            log::debug!("Model loaded from memory cache: {:?}", cache_key);
-            return Ok(LoadedModel::new_raw(registry, cached_model));
+            let tint = material.pbr_metallic_roughness().base_color_factor();
+            let tint = [tint[0], tint[1], tint[2], tint[3]];
+
+            let pbr = material.pbr_metallic_roughness();
+            let diffuse_texture = pbr.base_color_texture();
+            let metallic_roughness_texture = pbr.metallic_roughness_texture();
+            let normal_texture = material.normal_texture();
+            let occlusion_texture = material.occlusion_texture();
+            let emissive_texture = material.emissive_texture();
+
+            let diffuse_texture_info = diffuse_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+            let metallic_roughness_texture_info = metallic_roughness_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+
+            let normal_texture_info = normal_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+            let occlusion_texture_info = occlusion_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+            let emissive_texture_info = emissive_texture
+                .as_ref()
+                .and_then(|info| process_texture(info.texture()));
+
+            let emissive_factor = material.emissive_factor();
+            let emissive_factor = [
+                emissive_factor[0],
+                emissive_factor[1],
+                emissive_factor[2],
+            ];
+            let metallic_factor = pbr.metallic_factor();
+            let roughness_factor = pbr.roughness_factor();
+            let alpha_mode = material.alpha_mode();
+            let alpha_cutoff = material.alpha_cutoff();
+            let double_sided = material.double_sided();
+            let occlusion_strength = occlusion_texture
+                .as_ref()
+                .map(|info| info.strength())
+                .unwrap_or(1.0);
+            let normal_scale = normal_texture
+                .as_ref()
+                .map(|info| info.scale())
+                .unwrap_or(1.0);
+
+            material_data.push(GLTFMaterialInformation {
+                name: material_name,
+                diffuse_texture: diffuse_texture_info,
+                normal_texture: normal_texture_info,
+                emissive_texture: emissive_texture_info,
+                metallic_roughness_texture: metallic_roughness_texture_info,
+                occlusion_texture: occlusion_texture_info,
+                tint,
+                emissive_factor,
+                metallic_factor,
+                roughness_factor,
+                alpha_mode,
+                alpha_cutoff,
+                double_sided,
+                occlusion_strength,
+                normal_scale,
+            });
         }
 
-        log::trace!(
-            "========== Benchmarking speed of loading {:?} ==========",
-            label
-        );
-        log::debug!("Loading from memory");
-        let res_ref = ResourceReference::from_bytes(buffer.as_ref());
+        if material_data.is_empty() {
+            material_data.push(GLTFMaterialInformation {
+                name: "Default".to_string(),
+                diffuse_texture: None,
+                normal_texture: None,
+                emissive_texture: None,
+                metallic_roughness_texture: None,
+                occlusion_texture: None,
+                tint: [1.0, 1.0, 1.0, 1.0],
+                emissive_factor: [0.0, 0.0, 0.0],
+                metallic_factor: 1.0,
+                roughness_factor: 1.0,
+                alpha_mode: gltf::material::AlphaMode::Opaque,
+                alpha_cutoff: None,
+                double_sided: false,
+                occlusion_strength: 1.0,
+                normal_scale: 1.0,
+            });
+        }
 
-        let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?;
-        let mut meshes = Vec::new();
+        material_data
+    }
 
-        let mut texture_data: Vec<(String, Option<Vec<u8>>, Option<Vec<u8>>, [f32; 4])> = Vec::new();
-        for material in gltf.materials() {
-            log::debug!("Processing material: {:?}", material.name());
-            let material_name = material.name().unwrap_or("Unnamed Material").to_string();
+    fn load_meshes(
+        mesh: &gltf::Mesh,
+        buffers: &Vec<gltf::buffer::Data>,
+        mesh_collector: &mut Vec<GLTFMeshInformation>,
+    ) -> anyhow::Result<()> {
+        let mesh_name = mesh.name().unwrap_or("Unnamed Mesh").to_string();
+        puffin::profile_function!(&mesh_name);
+
+        for (primitive_index, primitive) in mesh.primitives().enumerate() {
+            puffin::profile_scope!("reading primitive", &format!("{}[{}]", &mesh_name, primitive_index));
+            
+            let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
+
+            let positions: Vec<[f32; 3]> = reader
+                .read_positions()
+                .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))?
+                .collect();
+
+            let indices: Vec<u32> = reader
+                .read_indices()
+                .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))?
+                .into_u32()
+                .collect();
+
+            let normals: Vec<[f32; 3]> = reader
+                .read_normals()
+                .map(|iter| iter.collect())
+                .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
+
+            let tangents: Vec<[f32; 4]> = reader
+                .read_tangents()
+                .map(|iter| iter.collect())
+                .unwrap_or_else(|| vec![[0.0, 0.0, 0.0, 1.0]; positions.len()]);
+
+            let colors: Vec<[f32; 4]> = reader
+                .read_colors(0)
+                .map(|iter| iter.into_rgba_f32().collect())
+                .unwrap_or_else(|| vec![[1.0; 4]; positions.len()]);
+
+            let joints: Vec<[u16; 4]> = reader
+                .read_joints(0)
+                .map(|iter| iter.into_u16().collect())
+                .unwrap_or_else(|| vec![[0u16; 4]; positions.len()]);
+
+            let mut weights: Vec<[f32; 4]> = reader
+                .read_weights(0)
+                .map(|iter| iter.into_f32().collect())
+                .unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 0.0]; positions.len()]);
+
+            for weight in &mut weights {
+                let sum = weight[0] + weight[1] + weight[2] + weight[3];
+                if sum > 0.0 {
+                    weight[0] /= sum;
+                    weight[1] /= sum;
+                    weight[2] /= sum;
+                    weight[3] /= sum;
+                }
+            }
+
+            let tex_coords: Vec<[f32; 2]> = reader
+                .read_tex_coords(0)
+                .map(|iter| iter.into_f32().collect())
+                .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
+
+            let tex_coords1: Vec<[f32; 2]> = reader
+                .read_tex_coords(1)
+                .map(|iter| iter.into_f32().collect())
+                .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
+
+            let expected_len = positions.len();
+            let check_len = |label: &str, len: usize| -> anyhow::Result<()> {
+                if len == expected_len {
+                    Ok(())
+                } else {
+                    Err(anyhow::anyhow!(
+                        "Mesh attribute length mismatch for {}: expected {}, got {}",
+                        label,
+                        expected_len,
+                        len
+                    ))
+                }
+            };
 
-            let tint = material
-                .pbr_metallic_roughness()
-                .base_color_factor();
+            check_len("normals", normals.len())?;
+            check_len("tangents", tangents.len())?;
+            check_len("colors", colors.len())?;
+            check_len("joints", joints.len())?;
+            check_len("weights", weights.len())?;
+            check_len("tex_coords0", tex_coords.len())?;
+            check_len("tex_coords1", tex_coords1.len())?;
+
+            mesh_collector.push(GLTFMeshInformation {
+                name: mesh_name.clone(),
+                primitive_index,
+                material_index: primitive.material().index().unwrap_or(0),
+                mode: primitive.mode(),
+                positions,
+                indices,
+                normals,
+                tangents,
+                colors,
+                joints,
+                weights,
+                tex_coords0: tex_coords,
+                tex_coords1,
+            });
+        }
 
-            let tint = [tint[0], tint[1], tint[2], tint[3]];
+        Ok(())
+    }
 
-            let diffuse_bytes = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() {
-                let texture_info = pbr.texture();
-                let image = texture_info.source();
-                match image.source() {
-                    gltf::image::Source::View { view, mime_type: _ } => {
-                        let buffer_data = &buffers[view.buffer().index()];
-                        let start = view.offset();
-                        let end = start + view.length();
-                        Some(buffer_data[start..end].to_vec())
-                    }
-                    gltf::image::Source::Uri { uri, mime_type: _ } => {
-                        log::warn!("External URI textures not supported: {}", uri);
-                        None
+    fn load_nodes(gltf: &gltf::Document) -> Vec<Node> {
+        puffin::profile_function!("loading nodes");
+        let mut nodes = Vec::new();
+        
+        for node in gltf.nodes() {
+            profile_scope!("reading node", node.name().unwrap_or("Unnamed Node"));
+            let (translation, rotation, scale) = node.transform().decomposed();
+            
+            let transform = NodeTransform {
+                translation: glam::Vec3::from(translation),
+                rotation: glam::Quat::from_array(rotation),
+                scale: glam::Vec3::from(scale),
+            };
+            
+            nodes.push(Node {
+                name: node.name().unwrap_or("Unnamed Node").to_string(),
+                parent: None,
+                children: node.children().map(|n| n.index()).collect(),
+                transform,
+            });
+        }
+        
+        for (node_index, node) in gltf.nodes().enumerate() {
+            profile_scope!("second pass enumerating children", node.name().unwrap_or("Unnamed Node"));
+            for child in node.children() {
+                if let Some(child_node) = nodes.get_mut(child.index()) {
+                    child_node.parent = Some(node_index);
+                }
+            }
+        }
+        
+        nodes
+    }
+
+    fn load_skins(gltf: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec<Skin> {
+        puffin::profile_function!("loading skins");
+        let mut skins = Vec::new();
+        
+        for skin in gltf.skins() {
+            puffin::profile_scope!("reading skin", skin.name().unwrap_or("Unnamed Skin"));
+            let joints: Vec<usize> = skin.joints().map(|j| j.index()).collect();
+            
+            let inverse_bind_matrices = if let Some(accessor) = skin.inverse_bind_matrices() {
+                let view = accessor.view().expect("Accessor must have a buffer view");
+                let buffer_data = &buffers[view.buffer().index()];
+                let start = view.offset() + accessor.offset();
+                let stride = view.stride().unwrap_or(accessor.size());
+                
+                let mut matrices = Vec::with_capacity(accessor.count());
+                for i in 0..accessor.count() {
+                    let offset = start + i * stride;
+                    let matrix_bytes = &buffer_data[offset..offset + 64];
+                    
+                    let mut floats = [0f32; 16];
+                    for (j, chunk) in matrix_bytes.chunks_exact(4).enumerate() {
+                        floats[j] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
                     }
+                    
+                    matrices.push(glam::Mat4::from_cols_array(&floats));
                 }
+                matrices
             } else {
-                None
+                vec![glam::Mat4::IDENTITY; joints.len()]
             };
+            
+            skins.push(Skin {
+                name: skin.name().unwrap_or("Unnamed Skin").to_string(),
+                joints,
+                inverse_bind_matrices,
+                skeleton_root: skin.skeleton().map(|n| n.index()),
+            });
+        }
+        
+        skins
+    }
 
-            let normal_bytes = if let Some(info) = material.normal_texture() {
-                let image = info.texture().source();
-                match image.source() {
-                    gltf::image::Source::View { view, mime_type: _ } => {
-                        let buffer_data = &buffers[view.buffer().index()];
-                        let start = view.offset();
-                        let end = start + view.length();
-                        Some(buffer_data[start..end].to_vec())
+    fn load_animations(gltf: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec<Animation> {
+        puffin::profile_function!("loading animations");
+        let mut animations = Vec::new();
+        
+        for animation in gltf.animations() {
+            puffin::profile_scope!("reading animation", animation.name().unwrap_or("Unnamed Animation"));
+            let mut channels = Vec::new();
+            let mut max_time = 0.0f32;
+            
+            for channel in animation.channels() {
+                let target = channel.target();
+                let reader = channel.reader(|buffer| Some(&buffers[buffer.index()]));
+                let interpolation_mode = channel.sampler().interpolation();
+                
+                let times: Vec<f32> = if let Some(inputs) = reader.read_inputs() {
+                    inputs.collect()
+                } else {
+                    continue;
+                };
+                
+                if let Some(&last_time) = times.last() {
+                    max_time = max_time.max(last_time);
+                }
+                
+                let values = match target.property() {
+                    gltf::animation::Property::Translation => {
+                        puffin::profile_scope!("reading translation values");
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::Translations(iter) => {
+                                    let translations: Vec<glam::Vec3> = iter
+                                        .map(|t| glam::Vec3::from(t))
+                                        .collect();
+                                    ChannelValues::Translations(translations)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
                     }
-                    gltf::image::Source::Uri { uri, mime_type: _ } => {
-                        log::warn!("External URI textures not supported: {}", uri);
-                        None
+                    gltf::animation::Property::Rotation => {
+                        puffin::profile_scope!("reading rotation values");
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::Rotations(iter) => {
+                                    let rotations: Vec<glam::Quat> = if interpolation_mode == gltf::animation::Interpolation::CubicSpline {
+                                        iter.into_f32()
+                                            .enumerate()
+                                            .map(|(i, r)| {
+                                                let q = glam::Quat::from_array(r);
+                                                if i % 3 == 1 {
+                                                    q.normalize()
+                                                } else {
+                                                    q
+                                                }
+                                            })
+                                            .collect()
+                                    } else {
+                                        iter.into_f32()
+                                            .map(|r| glam::Quat::from_array(r).normalize())
+                                            .collect()
+                                    };
+                                    ChannelValues::Rotations(rotations)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
                     }
-                }
+                    gltf::animation::Property::Scale => {
+                        puffin::profile_scope!("reading scale values");
+                        if let Some(outputs) = reader.read_outputs() {
+                            match outputs {
+                                gltf::animation::util::ReadOutputs::Scales(iter) => {
+                                    let scales: Vec<glam::Vec3> = iter
+                                        .map(|s| glam::Vec3::from(s))
+                                        .collect();
+                                    ChannelValues::Scales(scales)
+                                }
+                                _ => continue,
+                            }
+                        } else {
+                            continue;
+                        }
+                    }
+                    gltf::animation::Property::MorphTargetWeights => {
+                        puffin::profile_scope!("reading morph target weights");
+                        // Skip morph targets for now
+                        continue;
+                    }
+                };
+                
+                let interpolation = match channel.sampler().interpolation() {
+                    gltf::animation::Interpolation::Linear => AnimationInterpolation::Linear,
+                    gltf::animation::Interpolation::Step => AnimationInterpolation::Step,
+                    gltf::animation::Interpolation::CubicSpline => AnimationInterpolation::CubicSpline,
+                };
+                
+                channels.push(AnimationChannel {
+                    target_node: target.node().index(),
+                    times,
+                    values,
+                    interpolation,
+                });
+            }
+            
+            animations.push(Animation {
+                name: animation.name().unwrap_or("Unnamed Animation").to_string(),
+                channels,
+                duration: max_time,
+            });
+        }
+        
+        animations
+    }
+
+    pub async fn load_from_memory_raw<B>(
+        graphics: Arc<SharedGraphicsContext>,
+        buffer: B,
+        label: Option<&str>,
+        registry: Arc<RwLock<AssetRegistry>>,
+    ) -> anyhow::Result<Handle<Model>>
+    where
+        B: AsRef<[u8]>,
+    {
+        puffin::profile_function!(label.unwrap_or("unlabelled model"));
+        let mut registry = registry.write();
+
+        let model_label = label.unwrap_or("No named model");
+        let hash = {
+            puffin::profile_scope!("hashing model");
+            let mut hasher = DefaultHasher::default();
+            if let Some(label) = label {
+                label.hash(&mut hasher);
             } else {
-                None
+                buffer.as_ref().hash(&mut hasher);
             };
+            hasher.finish()
+        };
 
-            texture_data.push((material_name, diffuse_bytes, normal_bytes, tint));
+        if let Some(model) = registry.model_handle_by_hash(hash) {
+            return Ok(model);
         }
 
-        if texture_data.is_empty() {
-            texture_data.push((
-                "Default".to_string(),
-                None,
-                None,
-                [1.0, 1.0, 1.0, 1.0],
-            ));
-        }
+        let (gltf, buffers, images) = gltf::import_slice(buffer.as_ref())?;
 
-        let parallel_start = Instant::now();
-        let processed_textures: Vec<_> = texture_data
-            .into_par_iter()
-            .map(|(material_name, diffuse_bytes, normal_bytes, tint)| {
-                let material_start = Instant::now();
-
-                let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| {
-                    let load_start = Instant::now();
-                    let image = match image::load_from_memory(bytes) {
-                        Ok(image) => image,
-                        Err(err) => {
-                            log::warn!(
-                                "Failed to decode diffuse texture for material '{}': {} ({} bytes)",
-                                material_name,
-                                err,
-                                bytes.len()
-                            );
-                            return None;
-                        }
-                    };
-                    log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed());
-
-                    let rgba_start = Instant::now();
-                    let rgba = image.to_rgba8();
-                    log::trace!(
-                        "Converting diffuse image to rgba8 took {:?}",
-                        rgba_start.elapsed()
-                    );
-
-                    let dimensions = image.dimensions();
-                    Some((rgba.into_raw(), dimensions))
-                });
+        let mut meshes = Vec::new();
+        for mesh in gltf.meshes() {
+            Self::load_meshes(&mesh, &buffers, &mut meshes)?;
+        }
 
-                let processed_normal = normal_bytes.as_ref().and_then(|bytes| {
-                    let load_start = Instant::now();
-                    let image = match image::load_from_memory(bytes) {
-                        Ok(image) => image,
-                        Err(err) => {
-                            log::warn!(
-                                "Failed to decode normal texture for material '{}': {} ({} bytes)",
-                                material_name,
-                                err,
-                                bytes.len()
-                            );
-                            return None;
-                        }
-                    };
-                    log::trace!("Loading normal image to memory: {:?}", load_start.elapsed());
-
-                    let rgba_start = Instant::now();
-                    let rgba = image.to_rgba8();
-                    log::trace!(
-                        "Converting normal image to rgba8 took {:?}",
-                        rgba_start.elapsed()
-                    );
-
-                    let dimensions = image.dimensions();
-                    Some((rgba.into_raw(), dimensions))
-                });
+        let nodes = Self::load_nodes(&gltf);
+        
+        let skins = Self::load_skins(&gltf, &buffers);
+        
+        let animations = Self::load_animations(&gltf, &buffers);
+        
+        log::debug!(
+            "Loaded {} nodes, {} skins, {} animations for model [{:?}]",
+            nodes.len(),
+            skins.len(),
+            animations.len(),
+            label
+        );
 
-                log::trace!(
-                    "Parallel processing of material '{}' took: {:?}",
-                    material_name,
-                    material_start.elapsed()
-                );
+        let material_data = Self::load_materials(&gltf, &buffers, &images);
 
-                (material_name, processed_diffuse, processed_normal, tint)
+        let processed_textures: Vec<ProcessedMaterialTextures> = material_data
+            .into_par_iter()
+            .map(|material_info| {
+                puffin::profile_scope!("processing material textures");
+                let material_name = material_info.name;
+
+                let extract = |info: Option<GLTFTextureInformation>| -> (Option<(Vec<u8>, (u32, u32))>, Option<wgpu::SamplerDescriptor<'static>>) {
+                    if let Some(info) = info {
+                         (Some((info.pixels, (info.width, info.height))), Some(info.sampler))
+                    } else {
+                         (None, None)
+                    }
+                };
+
+                let (processed_diffuse, diffuse_sampler) = extract(material_info.diffuse_texture);
+                let (processed_normal, normal_sampler) = extract(material_info.normal_texture);
+                let (processed_emissive, emissive_sampler) = extract(material_info.emissive_texture);
+                let (processed_metallic_roughness, metallic_roughness_sampler) = extract(material_info.metallic_roughness_texture);
+                let (processed_occlusion, occlusion_sampler) = extract(material_info.occlusion_texture);
+
+                let tint = material_info.tint;
+                let emissive_factor = material_info.emissive_factor;
+                let metallic_factor = material_info.metallic_factor;
+                let roughness_factor = material_info.roughness_factor;
+                let alpha_mode = material_info.alpha_mode;
+                let alpha_cutoff = material_info.alpha_cutoff;
+                let double_sided = material_info.double_sided;
+                let occlusion_strength = material_info.occlusion_strength;
+                let normal_scale = material_info.normal_scale;
+
+                ProcessedMaterialTextures {
+                    name: material_name,
+                    diffuse: processed_diffuse,
+                    normal: processed_normal,
+                    emissive: processed_emissive,
+                    metallic_roughness: processed_metallic_roughness,
+                    occlusion: processed_occlusion,
+                    diffuse_sampler,
+                    normal_sampler,
+                    emissive_sampler,
+                    metallic_roughness_sampler,
+                    occlusion_sampler,
+                    tint,
+                    emissive_factor,
+                    metallic_factor,
+                    roughness_factor,
+                    alpha_mode,
+                    alpha_cutoff,
+                    double_sided,
+                    occlusion_strength,
+                    normal_scale,
+                }
             })
             .collect();
 
-        log::trace!(
-            "Total parallel image processing took: {:?}",
-            parallel_start.elapsed()
-        );
-
         let mut materials = Vec::new();
 
         let grey_texture = registry.grey_texture(graphics.clone());
         let flat_normal_texture =
             registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
 
-        for (material_name, processed_diffuse, processed_normal, tint) in processed_textures {
-            let start = Instant::now();
+        for processed in processed_textures {
+            puffin::profile_scope!("creating material");
+
+            let material_name = processed.name;
+            let processed_diffuse = processed.diffuse;
+            let processed_normal = processed.normal;
+            let processed_emissive = processed.emissive;
+            let processed_metallic_roughness = processed.metallic_roughness;
+            let processed_occlusion = processed.occlusion;
+            let diffuse_sampler = processed.diffuse_sampler;
+            let normal_sampler = processed.normal_sampler;
+            let emissive_sampler = processed.emissive_sampler;
+            let metallic_roughness_sampler = processed.metallic_roughness_sampler;
+            let occlusion_sampler = processed.occlusion_sampler;
 
             let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse {
                 Texture::from_bytes_verbose_mipmapped(
@@ -625,11 +945,13 @@ impl Model {
                     &rgba_data,
                     Some(dimensions),
                     None,
-                    None,
+                    diffuse_sampler.clone(),
                     Some(material_name.as_str())
                 )
+            } else if let Some(grey) = registry.get_texture(grey_texture) {
+                (*grey).clone()
             } else {
-                (*grey_texture).clone()
+                anyhow::bail!("Unable to find processed diffuse or fetch fallback texture for model {:?}", label);
             };
 
             let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal {
@@ -638,269 +960,147 @@ impl Model {
                     &rgba_data,
                     Some(dimensions),
                     None,
-                    None,
+                    normal_sampler.clone(),
                     Some(material_name.as_str())
                 )
+            } else if let Some(tex) = registry.get_texture(flat_normal_texture) {
+                (*tex).clone()
             } else {
-                (*flat_normal_texture).clone()
+                anyhow::bail!("Unable to find processed normal or fetch fallback texture for model {:?}", label);
             };
+
+            let emissive_texture = processed_emissive.map(|(rgba_data, dimensions)| {
+                Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    emissive_sampler.clone(),
+                    Some(material_name.as_str())
+                )
+            });
+            let metallic_roughness_texture =
+                processed_metallic_roughness.map(|(rgba_data, dimensions)| {
+                    Texture::from_bytes_verbose_mipmapped(
+                        graphics.clone(),
+                        &rgba_data,
+                        Some(dimensions),
+                        None,
+                        metallic_roughness_sampler.clone(),
+                        Some(material_name.as_str())
+                    )
+                });
+            let occlusion_texture = processed_occlusion.map(|(rgba_data, dimensions)| {
+                Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    occlusion_sampler.clone(),
+                    Some(material_name.as_str())
+                )
+            });
             let texture_tag = Some(material_name.clone());
 
-            materials.push(Material::new_with_tint(
+            let mut material = Material::new(
                 graphics.clone(),
                 material_name,
                 diffuse_texture,
                 normal_texture,
-                tint,
+                processed.tint,
                 texture_tag,
-            ));
+            );
 
-            log::trace!("Time to create GPU texture: {:?}", start.elapsed());
+            material.emissive_factor = processed.emissive_factor;
+            material.metallic_factor = processed.metallic_factor;
+            material.roughness_factor = processed.roughness_factor;
+            material.alpha_mode = processed.alpha_mode;
+            material.alpha_cutoff = processed.alpha_cutoff;
+            material.double_sided = processed.double_sided;
+            material.occlusion_strength = processed.occlusion_strength;
+            material.normal_scale = processed.normal_scale;
+            material.emissive_texture = emissive_texture;
+            material.metallic_roughness_texture = metallic_roughness_texture;
+            material.occlusion_texture = occlusion_texture;
+            material.sync_uniform(&graphics);
+
+            materials.push(material);
         }
 
-        for mesh in gltf.meshes() {
-            log::debug!("Processing mesh: {:?}", mesh.name());
-            for primitive in mesh.primitives() {
-                let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
-
-                let positions: Vec<[f32; 3]> = reader
-                    .read_positions()
-                    .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))?
-                    .collect();
-
-                let normals: Vec<[f32; 3]> = reader
-                    .read_normals()
-                    .map(|iter| iter.collect())
-                    .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
-
-                let tex_coords: Vec<[f32; 2]> = reader
-                    .read_tex_coords(0)
-                    .map(|iter| iter.into_f32().collect())
-                    .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
-
-                let mut vertices: Vec<ModelVertex> = positions
-                    .iter()
-                    .zip(normals.iter())
-                    .zip(tex_coords.iter())
-                    .map(|((pos, norm), tex)| ModelVertex {
-                        position: *pos,
-                        normal: *norm,
-                        tex_coords: *tex,
-                        tangent: [0.0; 3],
-                        bitangent: [0.0; 3],
-                    })
-                    .collect();
-
-                // Apply import scaling at load time so downstream systems (bounds, physics, etc.)
-                // see the baked geometry rather than a render-time transform hack.
-                let scale = import_scale.unwrap_or(1.0) as f32;
-                if (scale - 1.0).abs() > f32::EPSILON {
-                    for vertex in &mut vertices {
-                        vertex.position[0] *= scale;
-                        vertex.position[1] *= scale;
-                        vertex.position[2] *= scale;
-                    }
-                }
-
-                for v in &vertices {
-                    let _ = v.position.iter().map(|v| (*v as i32).hash(&mut hasher));
-                    let _ = v.normal.iter().map(|v| (*v as i32).hash(&mut hasher));
-                    let _ = v.tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher));
-                }
-
-                let indices: Vec<u32> = reader
-                    .read_indices()
-                    .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))?
-                    .into_u32()
-                    .collect();
-                indices.hash(&mut hasher);
-
-                let mut triangles_included = vec![0; vertices.len()];
-                for c in indices.chunks(3) {
-                    let v0 = vertices[c[0] as usize];
-                    let v1 = vertices[c[1] as usize];
-                    let v2 = vertices[c[2] as usize];
-
-                    let pos0: Vec3 = v0.position.into();
-                    let pos1: Vec3 = v1.position.into();
-                    let pos2: Vec3 = v2.position.into();
-
-                    let uv0: Vec2 = v0.tex_coords.into();
-                    let uv1: Vec2 = v1.tex_coords.into();
-                    let uv2: Vec2 = v2.tex_coords.into();
-
-                    // Calculate the edges of the triangle
-                    let delta_pos1 = pos1 - pos0;
-                    let delta_pos2 = pos2 - pos0;
-
-                    // This will give us a direction to calculate the
-                    // tangent and bitangent
-                    let delta_uv1 = uv1 - uv0;
-                    let delta_uv2 = uv2 - uv0;
-
-                    // Solving the following system of equations will
-                    // give us the tangent and bitangent.
-                    //     delta_pos1 = delta_uv1.x * T + delta_u.y * B
-                    //     delta_pos2 = delta_uv2.x * T + delta_uv2.y * B
-                    // Luckily, the place I found this equation provided
-                    // the solution!
-                    let r = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x);
-                    let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r;
-                    // We flip the bitangent to enable right-handed normal
-                    // maps with wgpu texture coordinate system
-                    let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * -r;
-
-                    // We'll use the same tangent/bitangent for each vertex in the triangle
-                    vertices[c[0] as usize].tangent =
-                        (tangent + Vec3::from(vertices[c[0] as usize].tangent)).into();
-                    vertices[c[1] as usize].tangent =
-                        (tangent + Vec3::from(vertices[c[1] as usize].tangent)).into();
-                    vertices[c[2] as usize].tangent =
-                        (tangent + Vec3::from(vertices[c[2] as usize].tangent)).into();
-                    vertices[c[0] as usize].bitangent =
-                        (bitangent + Vec3::from(vertices[c[0] as usize].bitangent)).into();
-                    vertices[c[1] as usize].bitangent =
-                        (bitangent + Vec3::from(vertices[c[1] as usize].bitangent)).into();
-                    vertices[c[2] as usize].bitangent =
-                        (bitangent + Vec3::from(vertices[c[2] as usize].bitangent)).into();
-
-                    // Used to average the tangents/bitangents
-                    triangles_included[c[0] as usize] += 1;
-                    triangles_included[c[1] as usize] += 1;
-                    triangles_included[c[2] as usize] += 1;
-                }
-
-                for (i, n) in triangles_included.into_iter().enumerate() {
-                    let denom = 1.0 / n as f32;
-                    let v = &mut vertices[i];
-                    v.tangent = (Vec3::from(v.tangent) * denom).into();
-                    v.bitangent = (Vec3::from(v.bitangent) * denom).into();
-                }
+        let mut gpu_meshes = Vec::new();
+        for mesh_info in meshes {
+            if mesh_info.mode != gltf::mesh::Mode::Triangles {
+                return Err(anyhow::anyhow!(
+                    "Unsupported primitive mode {:?} for mesh '{}' (primitive {})",
+                    mesh_info.mode,
+                    mesh_info.name,
+                    mesh_info.primitive_index
+                ));
+            }
 
-                let vertex_buffer =
-                    graphics
-                        .device
-                        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                            label: Some(&format!("{:?} Vertex Buffer", label)),
-                            contents: bytemuck::cast_slice(&vertices),
-                            usage: wgpu::BufferUsages::VERTEX,
-                        });
-
-                let index_buffer =
-                    graphics
-                        .device
-                        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                            label: Some(&format!("{:?} Index Buffer", label)),
-                            contents: bytemuck::cast_slice(&indices),
-                            usage: wgpu::BufferUsages::INDEX,
-                        });
-
-                let material_index = primitive.material().index().unwrap_or(0);
-
-                meshes.push(Mesh {
-                    name: mesh.name().unwrap_or("Unnamed Mesh").to_string(),
-                    vertex_buffer,
-                    index_buffer,
-                    vertices,
-                    num_elements: indices.len() as u32,
-                    material: material_index,
+            let mut vertices = Vec::with_capacity(mesh_info.positions.len());
+            for index in 0..mesh_info.positions.len() {
+                vertices.push(ModelVertex {
+                    position: mesh_info.positions[index],
+                    normal: mesh_info.normals[index],
+                    tangent: mesh_info.tangents[index],
+                    tex_coords0: mesh_info.tex_coords0[index],
+                    tex_coords1: mesh_info.tex_coords1[index],
+                    colour0: mesh_info.colors[index],
+                    joints0: mesh_info.joints[index],
+                    weights0: mesh_info.weights[index],
                 });
             }
+
+            let vertex_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some(&format!("{} Vertex Buffer", model_label)),
+                        contents: bytemuck::cast_slice(&vertices),
+                        usage: wgpu::BufferUsages::VERTEX,
+                    });
+
+            let index_buffer =
+                graphics
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some(&format!("{} Index Buffer", model_label)),
+                        contents: bytemuck::cast_slice(&mesh_info.indices),
+                        usage: wgpu::BufferUsages::INDEX,
+                    });
+
+            gpu_meshes.push(Mesh {
+                name: mesh_info.name,
+                vertex_buffer,
+                index_buffer,
+                vertices,
+                num_elements: mesh_info.indices.len() as u32,
+                material: mesh_info.material_index,
+            });
         }
 
         log::debug!("Successfully loaded model [{:?}]", label);
 
-        let model = Arc::new(Model {
-            meshes,
+        let model = Model {
+            label: model_label.to_string(),
+            hash,
+            path: ResourceReference::from_bytes(buffer.as_ref()),
+            meshes: gpu_meshes,
             materials,
-            label: label.unwrap_or("No named model").to_string(),
-            path: res_ref,
-            id: ModelId(hasher.finish()),
-        });
-
-        let loaded = LoadedModel::new_raw(registry, Arc::clone(&model));
-
-        {
-            let mut cache_guard = cache.lock();
-            cache_guard.insert(cache_key.clone(), model);
-        }
-        log::trace!("==================== DONE ====================");
-        log::debug!("Model cached from memory: {:?}", label);
-        log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label);
-        log::trace!("==============================================");
-        Ok(loaded)
-    }
-
-    pub async fn load(
-        graphics: Arc<SharedGraphicsContext>,
-        path: &PathBuf,
-        label: Option<&str>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel> {
-        Self::load_raw(
-            graphics,
-            path,
-            label,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-            import_scale
-        )
-        .await
-    }
-
-    pub async fn load_raw(
-        graphics: Arc<SharedGraphicsContext>,
-        path: &PathBuf,
-        label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
-        import_scale: Option<f64>,
-    ) -> anyhow::Result<LoadedModel> {
-        let file_name = path.file_name();
-        log::debug!("Loading model [{:?}]", file_name);
-
-        let scale_key = import_scale.unwrap_or(1.0);
-        let path_str = format!("{}::import_scale={:.8}", path.to_string_lossy(), scale_key);
-
-        log::debug!("Checking if model exists in cache");
-        if let Some(cached_model) = {
-            let cache_guard = cache.lock();
-            cache_guard.get(&path_str).cloned()
-        } {
-            log::debug!("Model loaded from cache: {:?}", path_str);
-            return Ok(LoadedModel::new_raw(registry, cached_model));
-        }
-        log::debug!("Model does not exist in cache, loading memory...");
-
-        log::debug!("Path of model: {}", path.display());
-
-        let buffer = std::fs::read(path)?;
-        let loaded =
-            Self::load_from_memory_raw(graphics, buffer, label, registry, cache, import_scale)
-                .await?;
-
-        let mut model_clone: Model = (*loaded).clone();
-        if let Ok(reference) = ResourceReference::from_path(path) {
-            model_clone.path = reference;
-        }
-        if let Some(custom_label) = label {
-            model_clone.label = custom_label.to_string();
-        }
+            skins,
+            animations,
+            nodes,
+        };
 
-        let updated = Arc::new(model_clone);
-        {
-            let mut cache_guard = cache.lock();
-            cache_guard.insert(path_str.clone(), Arc::clone(&updated));
-            if let Some(custom_label) = label {
-                let label_key = format!("{}::import_scale={:.8}", custom_label, scale_key);
-                cache_guard.insert(label_key, Arc::clone(&updated));
-            }
-        }
+        let handle = if let Some(label) = label {
+            registry.add_model_with_label(label, model)
+        } else {
+            registry.add_model(model)
+        };
 
-        log::debug!("Model cached and loaded: {:?}", file_name);
-        Ok(LoadedModel::new_raw(registry, updated))
+        Ok(handle)
     }
-
 }
 
 pub trait DrawModel<'a> {
@@ -911,6 +1111,7 @@ pub trait DrawModel<'a> {
         material: &'a Material,
         camera_bind_group: &'a wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     );
     fn draw_mesh_instanced(
         &mut self,
@@ -919,6 +1120,7 @@ pub trait DrawModel<'a> {
         instances: Range<u32>,
         camera_bind_group: &'a wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     );
 
     #[allow(unused)]
@@ -947,8 +1149,9 @@ where
         material: &'b Material,
         camera_bind_group: &'b wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     ) {
-        self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group);
+        self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group, skin_bind_group);
     }
 
     fn draw_mesh_instanced(
@@ -958,13 +1161,18 @@ where
         instances: Range<u32>,
         camera_bind_group: &'b wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
+        skin_bind_group: Option<&'a wgpu::BindGroup>,
     ) {
         self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
         self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
         self.set_bind_group(0, &material.bind_group, &[]);
         self.set_bind_group(1, camera_bind_group, &[]);
         self.set_bind_group(2, light_bind_group, &[]);
-        self.set_bind_group(3, &material.tint_bind_group, &[]);
+        
+        if let Some(skin_bg) = skin_bind_group {
+            self.set_bind_group(4, skin_bg, &[]);
+        }
+
         self.draw_indexed(0..mesh.num_elements, 0, instances);
     }
 
@@ -992,6 +1200,7 @@ where
                 instances.clone(),
                 camera_bind_group,
                 light_bind_group,
+                None, // Provide an AnimationComponent if available in a future update
             );
         }
     }
@@ -1083,50 +1292,6 @@ where
     }
 }
 
-pub trait DrawShadow<'a> {
-    fn draw_shadow_mesh_instanced(
-        &mut self,
-        mesh: &'a Mesh,
-        instances: Range<u32>,
-        light_bind_group: &'a wgpu::BindGroup,
-    );
-
-    fn draw_shadow_model_instanced(
-        &mut self,
-        model: &'a Model,
-        instances: Range<u32>,
-        light_bind_group: &'a wgpu::BindGroup,
-    );
-}
-
-impl<'a, 'b> DrawShadow<'b> for wgpu::RenderPass<'a>
-where
-    'b: 'a,
-{
-    fn draw_shadow_mesh_instanced(
-        &mut self,
-        mesh: &'b Mesh,
-        instances: Range<u32>,
-        light_bind_group: &'b wgpu::BindGroup,
-    ) {
-        self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
-        self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
-        self.set_bind_group(0, light_bind_group, &[]);
-        self.draw_indexed(0..mesh.num_elements, 0, instances);
-    }
-
-    fn draw_shadow_model_instanced(
-        &mut self,
-        model: &'b Model,
-        instances: Range<u32>,
-        light_bind_group: &'b wgpu::BindGroup,
-    ) {
-        for mesh in &model.meshes {
-            self.draw_shadow_mesh_instanced(mesh, instances.clone(), light_bind_group);
-        }
-    }
-}
-
 pub trait Vertex {
     fn desc() -> VertexBufferLayout<'static>;
 }
@@ -1135,20 +1300,26 @@ pub trait Vertex {
 /// ```wgsl
 /// struct VertexInput {
 ///     @location(0) position: vec3<f32>,
-///     @location(1) tex_coords: vec2<f32>,
-///     @location(2) normal: vec3<f32>,
-///     @location(3) tangent: vec3<f32>,
-///     @location(4) bitangent: vec3<f32>,
+///     @location(1) normal: vec3<f32>,
+///     @location(2) tangent: vec4<f32>,
+///     @location(3) tex_coords0: vec2<f32>,
+///     @location(4) tex_coords1: vec2<f32>,
+///     @location(5) color0: vec4<f32>,
+///     @location(6) joints0: vec4<u32>,
+///     @location(7) weights0: vec4<f32>,
 /// };
 /// ```
 #[repr(C)]
-#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, serde::Serialize, serde::Deserialize)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct ModelVertex {
     pub position: [f32; 3],
-    pub tex_coords: [f32; 2],
     pub normal: [f32; 3],
-    pub tangent: [f32; 3],
-    pub bitangent: [f32; 3],
+    pub tangent: [f32; 4],      // xyz + handedness (w)
+    pub tex_coords0: [f32; 2],
+    pub tex_coords1: [f32; 2],  // optional, can be zeroed if missing
+    pub colour0: [f32; 4],       // optional, default to white
+    pub joints0: [u16; 4],
+    pub weights0: [f32; 4],
 }
 
 impl Vertex for ModelVertex {
@@ -1157,54 +1328,71 @@ impl Vertex for ModelVertex {
             array_stride: mem::size_of::<ModelVertex>() as BufferAddress,
             step_mode: wgpu::VertexStepMode::Vertex,
             attributes: &[
+                // position
                 VertexAttribute {
                     format: wgpu::VertexFormat::Float32x3,
                     offset: 0,
                     shader_location: 0,
                 },
+                // normal
                 wgpu::VertexAttribute {
                     offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
                     shader_location: 1,
-                    format: wgpu::VertexFormat::Float32x2,
+                    format: wgpu::VertexFormat::Float32x3,
                 },
+                // tangent
                 wgpu::VertexAttribute {
-                    offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
+                    offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
                     shader_location: 2,
-                    format: wgpu::VertexFormat::Float32x3,
+                    format: wgpu::VertexFormat::Float32x4,
                 },
+                // tex_coords0
                 wgpu::VertexAttribute {
-                    offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
+                    offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
                     shader_location: 3,
-                    format: wgpu::VertexFormat::Float32x3,
+                    format: wgpu::VertexFormat::Float32x2,
                 },
+                // tex_coords1
                 wgpu::VertexAttribute {
-                    offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress,
+                    offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
                     shader_location: 4,
-                    format: wgpu::VertexFormat::Float32x3,
+                    format: wgpu::VertexFormat::Float32x2,
+                },
+                // color0
+                wgpu::VertexAttribute {
+                    offset: mem::size_of::<[f32; 14]>() as wgpu::BufferAddress,
+                    shader_location: 5,
+                    format: wgpu::VertexFormat::Float32x4,
+                },
+                // joints0
+                wgpu::VertexAttribute {
+                    offset: mem::size_of::<[f32; 18]>() as wgpu::BufferAddress,
+                    shader_location: 6,
+                    format: wgpu::VertexFormat::Uint16x4,
+                },
+                // weights0
+                wgpu::VertexAttribute {
+                    offset: (mem::size_of::<[f32; 18]>() + mem::size_of::<[u16; 4]>())
+                        as wgpu::BufferAddress,
+                    shader_location: 7,
+                    format: wgpu::VertexFormat::Float32x4,
                 },
             ],
         }
     }
 }
 
-#[repr(C, align(16))]
+#[repr(C)]
 #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct MaterialUniform {
-    /// RGBA tint multiplier applied to the sampled base colour.
-    pub colour: [f32; 4],
-
-    /// Scales incoming UVs before sampling (repeat counts when using Repeat wrap mode).
+    pub base_colour: [f32; 4],
+    pub emissive: [f32; 3],
+    pub emissive_strength: f32,
+    pub metallic: f32,
+    pub roughness: f32,
+    pub normal_scale: f32,
+    pub occlusion_strength: f32,
+    pub alpha_cutoff: f32,
     pub uv_tiling: [f32; 2],
-
-    pub _pad: [f32; 2],
-}
-
-impl MaterialUniform {
-    pub fn new(colour: [f32; 4]) -> Self {
-        Self {
-            colour,
-            uv_tiling: [1.0, 1.0],
-            _pad: [0.0, 0.0],
-        }
-    }
+    pub _pad: f32,
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
index 6d63b61..28b0077 100644
--- a/crates/dropbear-engine/src/pipelines/globals.rs
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -19,7 +19,7 @@ impl Default for Globals {
     fn default() -> Self {
         Self {
             num_lights: 0,
-            ambient_strength: 0.1,
+            ambient_strength: 0.8,
             _padding: [0; 2],
         }
     }
diff --git a/crates/dropbear-engine/src/pipelines/hdr.rs b/crates/dropbear-engine/src/pipelines/hdr.rs
new file mode 100644
index 0000000..1753c14
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/hdr.rs
@@ -0,0 +1,168 @@
+use wgpu::Operations;
+use crate::pipelines::create_render_pipeline;
+use crate::texture::Texture;
+
+pub struct HdrPipeline {
+    pipeline: wgpu::RenderPipeline,
+    bind_group: wgpu::BindGroup,
+    texture: Texture,
+    width: u32,
+    height: u32,
+    format: wgpu::TextureFormat,
+    layout: wgpu::BindGroupLayout,
+}
+
+impl HdrPipeline {
+    pub fn new(
+        device: &wgpu::Device,
+        config: &wgpu::SurfaceConfiguration,
+        output_format: wgpu::TextureFormat,
+    ) -> Self {
+        let width = config.width;
+        let height = config.height;
+
+        // We could use `Rgba32Float`, but that requires some extra
+        // features to be enabled for rendering.
+        let format = wgpu::TextureFormat::Rgba16Float;
+
+        let texture = Texture::create_2d_texture(
+            device,
+            width,
+            height,
+            format,
+            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
+            wgpu::FilterMode::Nearest,
+            Some("Hdr::texture"),
+        );
+
+        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("Hdr::layout"),
+            entries: &[
+                // This is the HDR texture
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        multisampled: false,
+                    },
+                    count: None,
+                },
+                wgpu::BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                    count: None,
+                },
+            ],
+        });
+        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("Hdr::bind_group"),
+            layout: &layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&texture.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(&texture.sampler),
+                },
+            ],
+        });
+
+        // We'll cover the shader next
+        let shader = wgpu::include_wgsl!("../shaders/hdr.wgsl");
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: None,
+            bind_group_layouts: &[&layout],
+            push_constant_ranges: &[],
+        });
+
+        let pipeline = create_render_pipeline(
+            Some("hdr render pipeline"),
+            device,
+            &pipeline_layout,
+            output_format,
+            None,
+            // We'll use some math to generate the vertex data in
+            // the shader, so we don't need any vertex buffers
+            &[],
+            wgpu::PrimitiveTopology::TriangleList,
+            shader,
+        );
+
+        Self {
+            pipeline,
+            bind_group,
+            layout,
+            texture,
+            width,
+            height,
+            format,
+        }
+    }
+
+    /// Resize the HDR texture
+    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
+        self.texture = Texture::create_2d_texture(
+            device,
+            width,
+            height,
+            wgpu::TextureFormat::Rgba16Float,
+            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
+            wgpu::FilterMode::Nearest,
+            Some("Hdr::texture"),
+        );
+        self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("Hdr::bind_group"),
+            layout: &self.layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&self.texture.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(&self.texture.sampler),
+                },
+            ],
+        });
+        self.width = width;
+        self.height = height;
+    }
+
+    /// Exposes the HDR texture
+    pub fn view(&self) -> &wgpu::TextureView {
+        &self.texture.view
+    }
+
+    /// The format of the HDR texture
+    pub fn format(&self) -> wgpu::TextureFormat {
+        self.format
+    }
+
+    /// This renders the internal HDR texture to the [TextureView]
+    /// supplied as parameter.
+    pub fn process(&self, encoder: &mut wgpu::CommandEncoder, output: &wgpu::TextureView) {
+        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+            label: Some("Hdr::process"),
+            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                view: &output,
+                depth_slice: None,
+                resolve_target: None,
+                ops: Operations {
+                    load: wgpu::LoadOp::Load,
+                    store: wgpu::StoreOp::Store,
+                },
+            })],
+            depth_stencil_attachment: None,
+            timestamp_writes: None,
+            occlusion_query_set: None,
+        });
+        pass.set_pipeline(&self.pipeline);
+        pass.set_bind_group(0, &self.bind_group, &[]);
+        pass.draw(0..3, 0..1);
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 6a599a9..d19b34e 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -1,6 +1,7 @@
 use std::sync::Arc;
 use std::mem::size_of;
 use glam::DMat4;
+use slank::include_slang;
 use wgpu::{BufferAddress, VertexAttribute, VertexFormat};
 use crate::buffer::{StorageBuffer, UniformBuffer};
 use crate::entity::{EntityTransform, Transform};
@@ -9,7 +10,6 @@ use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS};
 use crate::model::Vertex;
 use crate::pipelines::DropbearShaderPipeline;
 use crate::shader::Shader;
-use crate::texture::Texture;
 
 pub struct LightCubePipeline {
     shader: Shader,
@@ -26,11 +26,7 @@ pub struct LightCubePipeline {
 
 impl DropbearShaderPipeline for LightCubePipeline {
     fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
-        let shader = Shader::new(
-            graphics.clone(),
-            include_str!("shaders/light.wgsl"),
-            Some("light cube shader"),
-        );
+        let shader = Shader::from_slang(graphics.clone(), &slank::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube")));
 
         let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
             label: Some("light cube pipeline layout"),
@@ -41,6 +37,7 @@ impl DropbearShaderPipeline for LightCubePipeline {
             push_constant_ranges: &[],
         });
 
+        let hdr_format = graphics.hdr.read().format();
         let pipeline = graphics.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
             label: Some("light cube pipeline"),
             layout: Some(&pipeline_layout),
@@ -59,7 +56,7 @@ impl DropbearShaderPipeline for LightCubePipeline {
                 module: &shader.module,
                 entry_point: Some("fs_main"),
                 targets: &[Some(wgpu::ColorTargetState {
-                    format: Texture::TEXTURE_FORMAT,
+                    format: hdr_format,
                     blend: Some(wgpu::BlendState {
                         alpha: wgpu::BlendComponent::REPLACE,
                         color: wgpu::BlendComponent::REPLACE,
@@ -96,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",
@@ -233,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/mod.rs b/crates/dropbear-engine/src/pipelines/mod.rs
index f151c40..5885750 100644
--- a/crates/dropbear-engine/src/pipelines/mod.rs
+++ b/crates/dropbear-engine/src/pipelines/mod.rs
@@ -5,6 +5,7 @@ use crate::shader::Shader;
 pub mod shader;
 pub mod light_cube;
 pub mod globals;
+pub mod hdr;
 
 pub use globals::{Globals, GlobalsUniform};
 
@@ -20,4 +21,90 @@ pub trait DropbearShaderPipeline {
     fn pipeline_layout(&self) -> &wgpu::PipelineLayout;
     /// Fetches the pipeline
     fn pipeline(&self) -> &wgpu::RenderPipeline;
-}
\ No newline at end of file
+}
+
+pub fn create_render_pipeline(
+    label: Option<&str>,
+    device: &wgpu::Device,
+    layout: &wgpu::PipelineLayout,
+    color_format: wgpu::TextureFormat,
+    depth_format: Option<wgpu::TextureFormat>,
+    vertex_layouts: &[wgpu::VertexBufferLayout],
+    topology: wgpu::PrimitiveTopology,
+    shader: wgpu::ShaderModuleDescriptor,
+) -> wgpu::RenderPipeline {
+    create_render_pipeline_ex(
+        label,
+        device,
+        layout,
+        color_format,
+        depth_format,
+        vertex_layouts,
+        topology,
+        shader,
+        true, // depth_write_enabled
+        wgpu::CompareFunction::LessEqual,
+    )
+}
+
+pub fn create_render_pipeline_ex(
+    label: Option<&str>,
+    device: &wgpu::Device,
+    layout: &wgpu::PipelineLayout,
+    color_format: wgpu::TextureFormat,
+    depth_format: Option<wgpu::TextureFormat>,
+    vertex_layouts: &[wgpu::VertexBufferLayout],
+    topology: wgpu::PrimitiveTopology,
+    shader: wgpu::ShaderModuleDescriptor,
+    depth_write_enabled: bool,
+    depth_compare: wgpu::CompareFunction,
+) -> wgpu::RenderPipeline {
+    let shader = device.create_shader_module(shader);
+
+    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+        label,
+        layout: Some(layout),
+        vertex: wgpu::VertexState {
+            module: &shader,
+            entry_point: Some("vs_main"),
+            buffers: vertex_layouts,
+            compilation_options: Default::default(),
+        },
+        fragment: Some(wgpu::FragmentState {
+            module: &shader,
+            entry_point: Some("fs_main"),
+            targets: &[Some(wgpu::ColorTargetState {
+                format: color_format,
+                blend: None,
+                write_mask: wgpu::ColorWrites::ALL,
+            })],
+            compilation_options: Default::default(),
+        }),
+        primitive: wgpu::PrimitiveState {
+            topology, // NEW!
+            strip_index_format: None,
+            front_face: wgpu::FrontFace::Ccw,
+            cull_mode: Some(wgpu::Face::Back),
+            // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
+            polygon_mode: wgpu::PolygonMode::Fill,
+            // Requires Features::DEPTH_CLIP_CONTROL
+            unclipped_depth: false,
+            // Requires Features::CONSERVATIVE_RASTERIZATION
+            conservative: false,
+        },
+        depth_stencil: depth_format.map(|format| wgpu::DepthStencilState {
+            format,
+            depth_write_enabled,
+            depth_compare,
+            stencil: wgpu::StencilState::default(),
+            bias: wgpu::DepthBiasState::default(),
+        }),
+        multisample: wgpu::MultisampleState {
+            count: 1,
+            mask: !0,
+            alpha_to_coverage_enabled: false,
+        },
+        cache: None,
+        multiview: None,
+    })
+}
diff --git a/crates/dropbear-engine/src/pipelines/shader.rs b/crates/dropbear-engine/src/pipelines/shader.rs
index e165009..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;
@@ -18,16 +19,16 @@ impl DropbearShaderPipeline for MainRenderPipeline {
     fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
         let shader = Shader::new(
             graphics.clone(),
-            include_str!("shaders/shader.wgsl"),
+            include_str!("../shaders/shader.wgsl"),
             Some("viewport shaders"),
         );
 
         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 =
@@ -38,6 +39,7 @@ impl DropbearShaderPipeline for MainRenderPipeline {
                     push_constant_ranges: &[],
                 });
 
+        let hdr_format = graphics.hdr.read().format();
         let pipeline =
             graphics.device
                 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
@@ -51,13 +53,13 @@ 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")
                         },
                         targets: &[Some(wgpu::ColorTargetState {
-                            format: Texture::TEXTURE_FORMAT,
+                            format: hdr_format,
                             blend: Some(wgpu::BlendState::REPLACE),
                             write_mask: wgpu::ColorWrites::ALL,
                         })],
diff --git a/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl b/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl
deleted file mode 100644
index aa10b01..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl
+++ /dev/null
@@ -1,28 +0,0 @@
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) uv: vec2<f32>,
-}
-
-@group(0) @binding(0)
-var tex: texture_2d<f32>;
-@group(0) @binding(1)
-var tex_sampler: sampler;
-
-@vertex
-fn vs_main(
-    @builtin(vertex_index) in_vertex_index: u32,
-) -> VertexOutput {
-    var out: VertexOutput;
-    // Create fullscreen triangle
-    let x = f32((in_vertex_index << 1u) & 2u);
-    let y = f32(in_vertex_index & 2u);
-    out.clip_position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
-    out.uv = vec2<f32>(x, 1.0 - y);
-    return out;
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let s = textureSample(tex, tex_sampler, in.uv);
-    return s;
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/light.wgsl b/crates/dropbear-engine/src/pipelines/shaders/light.wgsl
deleted file mode 100644
index d31ebc8..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/light.wgsl
+++ /dev/null
@@ -1,62 +0,0 @@
-// Shader for rendering the white cube for lighting (or diff depending on colour)
-
-struct Light {
-    position: vec4<f32>,
-    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
-    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
-    constant: f32,
-    lin: f32,
-    quadratic: f32,
-    cutoff: f32,
-}
-
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view_proj: mat4x4<f32>,
-};
-
-@group(0) @binding(0)
-var<uniform> camera: CameraUniform;
-
-@group(1) @binding(0)
-var<uniform> light: Light;
-
-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>,
-}
-
-struct VertexInput {
-    @location(0) position: vec3<f32>,
-};
-
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) color: vec3<f32>,
-};
-
-@vertex
-fn vs_main(
-    model: VertexInput,
-    instance: InstanceInput,
-) -> VertexOutput {
-    let model_matrix = mat4x4<f32>(
-        instance.model_matrix_0,
-        instance.model_matrix_1,
-        instance.model_matrix_2,
-        instance.model_matrix_3,
-    );
-    var out: VertexOutput;
-    out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0);
-    out.color = light.color.xyz;
-    return out;
-}
-
-// Fragment shaders
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    return vec4<f32>(in.color, 1.0);
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl b/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl
deleted file mode 100644
index cfc908b..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl
+++ /dev/null
@@ -1,31 +0,0 @@
-@group(0)
-@binding(0)
-var src: texture_storage_2d<rgba8unorm, read>;
-@group(0)
-@binding(1)
-var dst: texture_storage_2d<rgba8unorm, write>;
-
-@compute
-@workgroup_size(16, 16, 1)
-fn compute_mipmap(
-    @builtin(global_invocation_id) gid: vec3<u32>,
-) {
-    let dstPos = gid.xy;
-    let srcPos = gid.xy * 2;
-
-    let dim = textureDimensions(src);
-
-    if (dstPos.x >= dim.x || dstPos.y >= dim.y) {
-        return;
-    }
-
-    let t00 = textureLoad(src, srcPos);
-    let t01 = textureLoad(src, srcPos + vec2(0, 1));
-    let t10 = textureLoad(src, srcPos + vec2(1, 0));
-    let t11 = textureLoad(src, srcPos + vec2(1, 1));
-
-    // A simple linear average of 4 adjacent pixels
-    let t = (t00 + t01 + t10 + t11) * 0.25;
-
-    textureStore(dst, dstPos, t);
-}
diff --git a/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl b/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl
deleted file mode 100644
index dcf0012..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/shader.wgsl
+++ /dev/null
@@ -1,290 +0,0 @@
-// Main shaders for standard objects.
-
-struct Globals {
-    num_lights: u32,
-    ambient_strength: f32,
-}
-
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view_proj: mat4x4<f32>,
-};
-
-struct Light {
-    position: vec4<f32>,
-    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
-    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
-    constant: f32,
-    lin: f32,
-    quadratic: f32,
-    cutoff: f32,
-}
-
-struct MaterialUniform {
-    // for stuff like tinting
-    colour: vec4<f32>,
-
-    // scales incoming UVs before sampling
-    uv_tiling: vec2<f32>,
-}
-
-const c_max_lights: u32 = 10u;
-
-@group(0) @binding(0)
-var t_diffuse: texture_2d<f32>;
-@group(0) @binding(1)
-var s_diffuse: sampler;
-@group(0) @binding(2)
-var t_normal: texture_2d<f32>;
-@group(0) @binding(3)
-var s_normal: sampler;
-
-@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;
-
-@group(4) @binding(0)
-var<uniform> u_globals: Globals;
-
-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>,
-};
-
-struct VertexInput {
-    @location(0) position: vec3<f32>,
-    @location(1) tex_coords: vec2<f32>,
-    @location(2) normal: vec3<f32>,
-    @location(3) tangent: vec3<f32>,
-    @location(4) bitangent: vec3<f32>,
-};
-
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) tex_coords: vec2<f32>,
-    @location(1) world_normal: vec3<f32>,
-    @location(2) world_position: vec3<f32>,
-    @location(3) world_tangent: vec3<f32>,
-    @location(4) world_bitangent: vec3<f32>,
-};
-
-@vertex
-fn vs_main(
-    model: VertexInput,
-    instance: InstanceInput,
-) -> VertexOutput {
-    let model_matrix = mat4x4<f32>(
-        instance.model_matrix_0,
-        instance.model_matrix_1,
-        instance.model_matrix_2,
-        instance.model_matrix_3,
-    );
-    let normal_matrix = mat3x3<f32>(
-        instance.normal_matrix_0,
-        instance.normal_matrix_1,
-        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 out: VertexOutput;
-    out.clip_position = u_camera.view_proj * world_position;
-    out.tex_coords = model.tex_coords;
-    out.world_normal = world_normal;
-    out.world_position = world_position.xyz;
-    out.world_tangent = world_tangent;
-    out.world_bitangent = world_bitangent;
-    return out;
-}
-
-fn directional_light(
-    light: Light,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>,
-    world_pos: vec3<f32>
-) -> vec3<f32> {
-    let light_dir = normalize(-light.direction.xyz);
-
-    let ambient = light.color.xyz * u_globals.ambient_strength * tex_color;
-
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color;
-
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color;
-
-    return ambient + diffuse + specular;
-}
-
-fn point_light(
-    light: Light,
-    world_pos: vec3<f32>,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>
-) -> vec3<f32> {
-    let light_dir = normalize(light.position.xyz - world_pos);
-
-    let distance = length(light.position.xyz - world_pos);
-    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
-
-    let ambient = light.color.xyz * u_globals.ambient_strength * tex_color;
-
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color;
-
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color;
-
-    return (ambient + diffuse + specular) * attenuation;
-}
-
-fn spot_light(
-    light: Light,
-    world_pos: vec3<f32>,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>
-) -> vec3<f32> {
-    let light_dir = normalize(light.position.xyz - world_pos);
-    let theta = dot(light_dir, normalize(-light.direction.xyz));
-    let outer_cutoff = light.direction.w;
-
-    let epsilon = light.cutoff - outer_cutoff;
-    let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
-
-    let distance = length(light.position.xyz - world_pos);
-    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
-
-    let ambient = light.color.xyz * u_globals.ambient_strength * tex_color;
-
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color * intensity;
-
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color * intensity;
-
-    return (ambient + diffuse + specular) * attenuation;
-}
-
-fn apply_normal_map(
-    world_normal_in: vec3<f32>,
-    world_tangent_in: vec3<f32>,
-    world_bitangent_in: vec3<f32>,
-    normal_sample_rgb: vec3<f32>,
-) -> vec3<f32> {
-    let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0));
-
-    let n = normalize(world_normal_in);
-    var t = normalize(world_tangent_in);
-    t = normalize(t - n * dot(n, t));
-
-    let b_in = normalize(world_bitangent_in);
-    let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0);
-    let b = cross(n, t) * handedness;
-
-    let tbn = mat3x3<f32>(t, b, n);
-    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;
-
-    if (base_colour.a < 0.1) {
-        discard;
-    }
-
-    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
-
-    let world_normal = apply_normal_map(
-        in.world_normal,
-        in.world_tangent,
-        in.world_bitangent,
-        object_normal.xyz,
-    );
-
-    var final_color = vec3<f32>(0.0);
-
-    for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) {
-        let light = s_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);
-}
-
-// when storage is NOT supported
-@fragment
-fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let uv = in.tex_coords * u_material.uv_tiling;
-    var tex_color = textureSample(t_diffuse, s_diffuse, uv);
-    var object_normal = textureSample(t_normal, s_normal, uv);
-
-    let base_colour = tex_color * u_material.colour;
-
-    if (base_colour.a < 0.1) {
-        discard;
-    }
-
-    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
-
-    let world_normal = apply_normal_map(
-        in.world_normal,
-        in.world_tangent,
-        in.world_bitangent,
-        object_normal.xyz,
-    );
-
-    var final_color = vec3<f32>(0.0);
-
-    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);
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/procedural/cube.rs b/crates/dropbear-engine/src/procedural/cube.rs
index c31acfd..6dc4de9 100644
--- a/crates/dropbear-engine/src/procedural/cube.rs
+++ b/crates/dropbear-engine/src/procedural/cube.rs
@@ -5,48 +5,62 @@ impl ProcedurallyGeneratedObject {
     ///
     /// `size` is the full extents (width, height, depth).
     pub fn cuboid(size: glam::DVec3) -> Self {
+        puffin::profile_function!();
         let half = (size / 2.0).as_vec3();
 
         let uv_x = 1.0_f32;
         let uv_y = 1.0_f32;
         let uv_z = 1.0_f32;
 
+        let make_vertex = |position: [f32; 3], normal: [f32; 3], tangent: [f32; 3], uv: [f32; 2]| {
+            ModelVertex {
+                position,
+                normal,
+                tangent: [tangent[0], tangent[1], tangent[2], 1.0],
+                tex_coords0: uv,
+                tex_coords1: [0.0, 0.0],
+                colour0: [1.0, 1.0, 1.0, 1.0],
+                joints0: [0, 0, 0, 0],
+                weights0: [1.0, 0.0, 0.0, 0.0],
+            }
+        };
+
         let vertices = vec![
             // Front Face (Normal: 0, 0, 1)
-            ModelVertex { position: [-half.x, -half.y,  half.z], tex_coords: [0.0,  uv_y], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x, -half.y,  half.z], tex_coords: [uv_x, uv_y], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y,  half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y,  half.z], tex_coords: [0.0,  0.0],  normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([-half.x, -half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0,  uv_y]),
+            make_vertex([ half.x, -half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [uv_x, uv_y]),
+            make_vertex([ half.x,  half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([-half.x,  half.y,  half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Back Face (Normal: 0, 0, -1)
-            ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [0.0,  uv_y], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [uv_x, uv_y], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y, -half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y, -half.z], tex_coords: [0.0,  0.0],  normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([ half.x, -half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0,  uv_y]),
+            make_vertex([-half.x, -half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [uv_x, uv_y]),
+            make_vertex([-half.x,  half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([ half.x,  half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Top Face (Normal: 0, 1, 0)
-            ModelVertex { position: [-half.x,  half.y,  half.z], tex_coords: [0.0,  uv_z], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
-            ModelVertex { position: [ half.x,  half.y,  half.z], tex_coords: [uv_x, uv_z], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
-            ModelVertex { position: [ half.x,  half.y, -half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
-            ModelVertex { position: [-half.x,  half.y, -half.z], tex_coords: [0.0,  0.0],  normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] },
+            make_vertex([-half.x,  half.y,  half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  uv_z]),
+            make_vertex([ half.x,  half.y,  half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, uv_z]),
+            make_vertex([ half.x,  half.y, -half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([-half.x,  half.y, -half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Bottom Face (Normal: 0, -1, 0)
-            ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [0.0,  uv_z], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
-            ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [uv_x, uv_z], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
-            ModelVertex { position: [ half.x, -half.y,  half.z], tex_coords: [uv_x, 0.0],  normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
-            ModelVertex { position: [-half.x, -half.y,  half.z], tex_coords: [0.0,  0.0],  normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] },
+            make_vertex([-half.x, -half.y, -half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  uv_z]),
+            make_vertex([ half.x, -half.y, -half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, uv_z]),
+            make_vertex([ half.x, -half.y,  half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, 0.0]),
+            make_vertex([-half.x, -half.y,  half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0,  0.0]),
 
             // Right Face (Normal: 1, 0, 0)
-            ModelVertex { position: [ half.x, -half.y,  half.z], tex_coords: [0.0,  uv_y], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [uv_z, uv_y], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y, -half.z], tex_coords: [uv_z, 0.0],  normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [ half.x,  half.y,  half.z], tex_coords: [0.0,  0.0],  normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([ half.x, -half.y,  half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0,  uv_y]),
+            make_vertex([ half.x, -half.y, -half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [uv_z, uv_y]),
+            make_vertex([ half.x,  half.y, -half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [uv_z, 0.0]),
+            make_vertex([ half.x,  half.y,  half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0,  0.0]),
 
             // Left Face (Normal: -1, 0, 0)
-            ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [0.0,  uv_y], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x, -half.y,  half.z], tex_coords: [uv_z, uv_y], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y,  half.z], tex_coords: [uv_z, 0.0],  normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
-            ModelVertex { position: [-half.x,  half.y, -half.z], tex_coords: [0.0,  0.0],  normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] },
+            make_vertex([-half.x, -half.y, -half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0,  uv_y]),
+            make_vertex([-half.x, -half.y,  half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [uv_z, uv_y]),
+            make_vertex([-half.x,  half.y,  half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [uv_z, 0.0]),
+            make_vertex([-half.x,  half.y, -half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0,  0.0]),
         ];
 
         let indices: Vec<u32> = vec![
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index 695c172..9c6f6f2 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -1,20 +1,33 @@
 //! Starter objects like planes and primitive objects are that made during runtime with
 //! vertices rather than from a model.
 
-use crate::asset::{AssetRegistry, ASSET_REGISTRY};
+use crate::asset::{AssetRegistry, Handle};
 use crate::graphics::SharedGraphicsContext;
-use crate::model::{LoadedModel, Material, Mesh, Model, ModelId, MODEL_CACHE};
+use crate::model::{Material, Mesh, Model};
 use crate::utils::ResourceReference;
 use crate::model::ModelVertex;
-use parking_lot::Mutex;
-use std::collections::HashMap;
 use std::hash::{DefaultHasher, Hasher};
-use std::sync::{Arc, LazyLock};
+use std::sync::Arc;
+use parking_lot::RwLock;
+use serde::{Deserialize, Serialize};
 use wgpu::util::DeviceExt;
 
 pub mod cube;
 // pub mod plane;
 
+#[derive(
+    Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize
+)]
+pub enum ProcObj {
+    /// A parameterized cuboid (box) generated at runtime.
+    ///
+    /// Stored as IEEE-754 `f32` bit patterns so the reference remains hashable.
+    /// Values can be reconstructed with `f32::from_bits`.
+    ///
+    /// The `size_bits` represent the full extents (width, height, depth).
+    Cuboid { size_bits: [u32; 3] },
+}
+
 /// An object that comes with a template, and is generated through parameter input. 
 pub struct ProcedurallyGeneratedObject {
     pub vertices: Vec<ModelVertex>,
@@ -31,24 +44,9 @@ impl ProcedurallyGeneratedObject {
         graphics: Arc<SharedGraphicsContext>,
         material: Option<Material>,
         label: Option<&str>,
-    ) -> LoadedModel {
-        self.build_model_raw(
-            graphics,
-            material,
-            label,
-            &ASSET_REGISTRY,
-            LazyLock::force(&MODEL_CACHE),
-        )
-    }
-
-    pub fn build_model_raw(
-        self,
-        graphics: Arc<SharedGraphicsContext>,
-        material: Option<Material>,
-        label: Option<&str>,
-        registry: &AssetRegistry,
-        cache: &Mutex<HashMap<String, Arc<Model>>>,
-    ) -> LoadedModel {
+        registry: Arc<RwLock<AssetRegistry>>,
+    ) -> Handle<Model> {
+        puffin::profile_function!();
         let mut hasher = DefaultHasher::new();
         hasher.write(bytemuck::cast_slice(&self.vertices));
         hasher.write(bytemuck::cast_slice(&self.indices));
@@ -58,11 +56,10 @@ impl ProcedurallyGeneratedObject {
             .map(|s| s.to_string())
             .unwrap_or_else(|| format!("procedural_{hash:016x}"));
 
-        if let Some(cached_model) = {
-            let cache_guard = cache.lock();
-            cache_guard.get(&label).cloned()
-        } {
-            return LoadedModel::new_raw(registry, cached_model);
+        let mut _rguard = registry.write();
+
+        if let Some(handle) = _rguard.model_handle_by_hash(hash) {
+            return handle;
         }
 
         let vertices = self.vertices;
@@ -94,31 +91,38 @@ impl ProcedurallyGeneratedObject {
         };
 
         let material = material.unwrap_or_else(|| {
-            let grey = registry.grey_texture(graphics.clone());
-            let flat_normal = registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
-            Material::new_with_tint(
+            let grey_handle = _rguard.grey_texture(graphics.clone());
+            let flat_normal_handle =
+                _rguard.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]);
+            let grey = _rguard
+                .get_texture(grey_handle)
+                .expect("Grey texture handle missing")
+                .clone();
+            let flat_normal = _rguard
+                .get_texture(flat_normal_handle)
+                .expect("Flat normal texture handle missing")
+                .clone();
+            Material::new(
                 graphics.clone(),
                 "procedural_material",
-                (*grey).clone(),
-                (*flat_normal).clone(),
+                grey,
+                flat_normal,
                 [1.0, 1.0, 1.0, 1.0],
                 Some("procedural_material".to_string()),
             )
         });
 
-        let model = Arc::new(Model {
+        let model = Model {
             label: label.clone(),
+            hash,
             path: ResourceReference::from_bytes(hash.to_le_bytes()),
             meshes: vec![mesh],
             materials: vec![material],
-            id: ModelId(hash),
-        });
-
-        {
-            let mut cache_guard = cache.lock();
-            cache_guard.insert(label, Arc::clone(&model));
-        }
+            skins: Vec::new(),
+            animations: Vec::new(),
+            nodes: Vec::new(),
+        };
 
-        LoadedModel::new_raw(registry, model)
+        _rguard.add_model_with_label(label, model)
     }
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/scene.rs b/crates/dropbear-engine/src/scene.rs
index 5ceca77..b53e10b 100644
--- a/crates/dropbear-engine/src/scene.rs
+++ b/crates/dropbear-engine/src/scene.rs
@@ -4,6 +4,7 @@
 
 use winit::event_loop::ActiveEventLoop;
 use winit::window::WindowId;
+use winit::event::WindowEvent;
 
 use crate::{WindowData, graphics::{SharedGraphicsContext}, input};
 use parking_lot::RwLock;
@@ -15,6 +16,7 @@ pub trait Scene {
     fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>);
     fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>);
     fn exit(&mut self, event_loop: &ActiveEventLoop);
+    fn handle_event(&mut self, _event: &WindowEvent) {}
     /// By far a mess of a trait however it works.
     ///
     /// This struct allows you to add in a SceneCommand enum and send it to the scene management for them
@@ -91,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());
                 }
             }
@@ -113,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();
@@ -122,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());
 
@@ -153,21 +160,35 @@ 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);
+        }
+    }
+
     pub fn has_scene(&self) -> bool {
         self.current_scene.is_some()
     }
diff --git a/crates/dropbear-engine/src/shader.rs b/crates/dropbear-engine/src/shader.rs
index 772572f..5907024 100644
--- a/crates/dropbear-engine/src/shader.rs
+++ b/crates/dropbear-engine/src/shader.rs
@@ -3,6 +3,7 @@
 use std::ops::Deref;
 use crate::graphics::SharedGraphicsContext;
 use std::sync::Arc;
+use slank::{CompiledSlangShader, utils::WgpuUtils};
 use wgpu::ShaderModule;
 
 /// A nice little struct that stored basic information about a WGPU shaders.
@@ -38,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 {
@@ -47,6 +49,8 @@ impl Shader {
 
         log::debug!("Created new shaders under the label: {:?}", label);
 
+        CompiledSlangShader::from_bytes("light cube", slank::include_slang!("light_cube"));
+
         Self {
             label: match label {
                 Some(label) => label.into(),
@@ -56,4 +60,19 @@ impl Shader {
             content: shader_file_contents.to_string(),
         }
     }
+
+    pub fn from_slang(graphics: Arc<SharedGraphicsContext>, shader: &CompiledSlangShader) -> Self {
+        puffin::profile_function!();
+        let module = graphics
+            .device
+            .create_shader_module(shader.create_wgpu_shader());
+
+        log::debug!("Created new shaders [slang] under the label: {}", shader.label());
+
+        Self {
+            label: shader.label().clone(),
+            module,
+            content: String::from_utf8(shader.source.clone()).unwrap_or_default(),
+        }
+    }
 }
diff --git a/crates/dropbear-engine/src/shaders/blit.slang b/crates/dropbear-engine/src/shaders/blit.slang
new file mode 100644
index 0000000..4b14a95
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/blit.slang
@@ -0,0 +1,35 @@
+#include "dropbear.slang"
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+
+    [[vk_location(0)]]
+    float2 uv;
+};
+
+[[vk_binding(0, 0)]]
+Texture2D tex;
+
+[[vk_binding(1, 0)]]
+SamplerState tex_sampler;
+
+[shader("vertex")]
+VertexOutput vs_main(
+    uint in_vertex_index: SV_VulkanVertexID
+) {
+    VertexOutput output;
+
+    float x = float((in_vertex_index << 1u) & 2u);
+    float y = float(in_vertex_index & 2u);
+
+    output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
+    output.uv = float2(x, 1.0 - y);
+
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    float4 s = tex.Sample(tex_sampler, input.uv);
+    return s;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/dropbear.slang b/crates/dropbear-engine/src/shaders/dropbear.slang
new file mode 100644
index 0000000..203cd8c
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/dropbear.slang
@@ -0,0 +1,52 @@
+struct Globals {
+    uint num_lights;
+    float ambient_strength;
+};
+
+/// A standard light descriptor struct. 
+struct Light {
+    float4 position;
+    float4 direction; // x, y, z, outer_cutoff_angle
+    float4 color; // r, g, b, light_type (0, 1, 2)
+
+    float constant;
+    float lin;
+    float quadratic;
+
+    float cutoff;
+};
+
+struct CameraUniform {
+    float4 view_pos;
+    float4x4 view;
+    float4x4 view_proj;
+    float4x4 inv_proj;
+    float4x4 inv_view;
+};
+
+struct MaterialUniform {
+    float4 base_colour;
+    float3 emissive;
+    float emissive_strength;
+    float metallic;
+    float roughness;
+    float normal_scale;
+    float occlusion_strength;
+    float alpha_cutoff;
+    float2 uv_tiling;
+};
+
+/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. 
+float4x4 vs_input_matrix(
+    float4 mat1, 
+    float4 mat2, 
+    float4 mat3, 
+    float4 mat4
+) {
+    return transpose(float4x4(
+        mat1,
+        mat2,
+        mat3,
+        mat4
+    ));
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/equirectangular.wgsl b/crates/dropbear-engine/src/shaders/equirectangular.wgsl
new file mode 100644
index 0000000..440155f
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/equirectangular.wgsl
@@ -0,0 +1,86 @@
+const PI: f32 = 3.1415926535897932384626433832795;
+
+struct Face {
+    forward: vec3<f32>,
+    up: vec3<f32>,
+    right: vec3<f32>,
+}
+
+@group(0)
+@binding(0)
+var src: texture_2d<f32>;
+
+@group(0)
+@binding(1)
+var dst: texture_storage_2d_array<rgba32float, write>;
+
+@compute
+@workgroup_size(16, 16, 1)
+fn compute_equirect_to_cubemap(
+    @builtin(global_invocation_id)
+    gid: vec3<u32>,
+) {
+    // If texture size is not divisible by 32, we
+    // need to make sure we don't try to write to
+    // pixels that don't exist.
+    if gid.x >= u32(textureDimensions(dst).x) {
+        return;
+    }
+
+    var FACES: array<Face, 6> = array(
+        // FACES +X
+        Face(
+            vec3(1.0, 0.0, 0.0),  // forward
+            vec3(0.0, 1.0, 0.0),  // up
+            vec3(0.0, 0.0, -1.0), // right
+        ),
+        // FACES -X
+        Face (
+            vec3(-1.0, 0.0, 0.0),
+            vec3(0.0, 1.0, 0.0),
+            vec3(0.0, 0.0, 1.0),
+        ),
+        // FACES +Y
+        Face (
+            vec3(0.0, -1.0, 0.0),
+            vec3(0.0, 0.0, 1.0),
+            vec3(1.0, 0.0, 0.0),
+        ),
+        // FACES -Y
+        Face (
+            vec3(0.0, 1.0, 0.0),
+            vec3(0.0, 0.0, -1.0),
+            vec3(1.0, 0.0, 0.0),
+        ),
+        // FACES +Z
+        Face (
+            vec3(0.0, 0.0, 1.0),
+            vec3(0.0, 1.0, 0.0),
+            vec3(1.0, 0.0, 0.0),
+        ),
+        // FACES -Z
+        Face (
+            vec3(0.0, 0.0, -1.0),
+            vec3(0.0, 1.0, 0.0),
+            vec3(-1.0, 0.0, 0.0),
+        ),
+    );
+
+    // Get texture coords relative to cubemap face
+    let dst_dimensions = vec2<f32>(textureDimensions(dst));
+    let cube_uv = vec2<f32>(gid.xy) / dst_dimensions * 2.0 - 1.0;
+
+    // Get spherical coordinate from cube_uv
+    let face = FACES[gid.z];
+    let spherical = normalize(face.forward + face.right * cube_uv.x + face.up * cube_uv.y);
+
+    // Get coordinate on the equirectangular texture
+    let inv_atan = vec2(0.1591, 0.3183);
+    let eq_uv = vec2(atan2(spherical.z, spherical.x), asin(spherical.y)) * inv_atan + 0.5;
+    let eq_pixel = vec2<i32>(eq_uv * vec2<f32>(textureDimensions(src)));
+
+    // We use textureLoad() as textureSample() is not allowed in compute shaders
+    var smpl = textureLoad(src, eq_pixel, 0);
+
+    textureStore(dst, gid.xy, gid.z, smpl);
+}
diff --git a/crates/dropbear-engine/src/shaders/hdr.wgsl b/crates/dropbear-engine/src/shaders/hdr.wgsl
new file mode 100644
index 0000000..2dc5562
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/hdr.wgsl
@@ -0,0 +1,55 @@
+// Maps HDR values to linear values
+// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces
+fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> {
+    let m1 = mat3x3(
+        0.59719, 0.07600, 0.02840,
+        0.35458, 0.90834, 0.13383,
+        0.04823, 0.01566, 0.83777,
+    );
+    let m2 = mat3x3(
+        1.60475, -0.10208, -0.00327,
+        -0.53108,  1.10813, -0.07276,
+        -0.07367, -0.00605,  1.07602,
+    );
+    let v = m1 * hdr;
+    let a = v * (v + 0.0245786) - 0.000090537;
+    let b = v * (0.983729 * v + 0.4329510) + 0.238081;
+    return clamp(m2 * (a / b), vec3(0.0), vec3(1.0));
+}
+
+struct VertexOutput {
+    @location(0) uv: vec2<f32>,
+    @builtin(position) clip_position: vec4<f32>,
+};
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) vi: u32,
+) -> VertexOutput {
+    var out: VertexOutput;
+    // Generate a triangle that covers the whole screen
+    out.uv = vec2<f32>(
+        f32((vi << 1u) & 2u),
+        f32(vi & 2u),
+    );
+    out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0);
+    // We need to invert the y coordinate so the image
+    // is not upside down
+    out.uv.y = 1.0 - out.uv.y;
+    return out;
+}
+
+@group(0)
+@binding(0)
+var hdr_image: texture_2d<f32>;
+
+@group(0)
+@binding(1)
+var hdr_sampler: sampler;
+
+@fragment
+fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> {
+    let hdr = textureSample(hdr_image, hdr_sampler, vs.uv);
+    let sdr = aces_tone_map(hdr.rgb);
+    return vec4(sdr, hdr.a);
+}
diff --git a/crates/dropbear-engine/src/shaders/light.slang b/crates/dropbear-engine/src/shaders/light.slang
new file mode 100644
index 0000000..9a2a330
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/light.slang
@@ -0,0 +1,51 @@
+#include "dropbear.slang"
+
+[[vk::binding(0, 0)]]
+ConstantBuffer<CameraUniform> camera;
+
+[[vk::binding(0, 1)]]
+ConstantBuffer<Light> light;
+
+struct VertexInput {
+    [[vk::location(0)]]
+    float3 position : POSITION;
+    
+    [[vk::location(5)]]
+    float4 model_matrix_0 : TEXCOORD5;
+    
+    [[vk::location(6)]]
+    float4 model_matrix_1 : TEXCOORD6;
+    
+    [[vk::location(7)]]
+    float4 model_matrix_2 : TEXCOORD7;
+    
+    [[vk::location(8)]]
+    float4 model_matrix_3 : TEXCOORD8;
+};
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+    
+    [[vk::location(0)]]
+    float3 color : COLOR0;
+};
+
+[shader("vertex")]
+VertexOutput vs_main(VertexInput input) {
+    float4x4 model_matrix = vs_input_matrix(
+        input.model_matrix_0,
+        input.model_matrix_1,
+        input.model_matrix_2,
+        input.model_matrix_3,
+    );
+
+    VertexOutput output;
+    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
+    output.color = light.color.xyz;
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    return float4(input.color, 1.0);
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shaders/mipmap.wgsl b/crates/dropbear-engine/src/shaders/mipmap.wgsl
new file mode 100644
index 0000000..cfc908b
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/mipmap.wgsl
@@ -0,0 +1,31 @@
+@group(0)
+@binding(0)
+var src: texture_storage_2d<rgba8unorm, read>;
+@group(0)
+@binding(1)
+var dst: texture_storage_2d<rgba8unorm, write>;
+
+@compute
+@workgroup_size(16, 16, 1)
+fn compute_mipmap(
+    @builtin(global_invocation_id) gid: vec3<u32>,
+) {
+    let dstPos = gid.xy;
+    let srcPos = gid.xy * 2;
+
+    let dim = textureDimensions(src);
+
+    if (dstPos.x >= dim.x || dstPos.y >= dim.y) {
+        return;
+    }
+
+    let t00 = textureLoad(src, srcPos);
+    let t01 = textureLoad(src, srcPos + vec2(0, 1));
+    let t10 = textureLoad(src, srcPos + vec2(1, 0));
+    let t11 = textureLoad(src, srcPos + vec2(1, 1));
+
+    // A simple linear average of 4 adjacent pixels
+    let t = (t00 + t01 + t10 + t11) * 0.25;
+
+    textureStore(dst, dstPos, t);
+}
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
new file mode 100644
index 0000000..233aff0
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -0,0 +1,276 @@
+// Main shaders for standard objects.
+
+struct Globals {
+    num_lights: u32,
+    ambient_strength: f32,
+}
+
+struct CameraUniform {
+    view_pos: vec4<f32>,
+    view: mat4x4<f32>,
+    view_proj: mat4x4<f32>,
+    inv_proj: mat4x4<f32>,
+    inv_view: mat4x4<f32>,
+}
+struct Light {
+    position: vec4<f32>,
+    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
+    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
+    constant: f32,
+    lin: f32,
+    quadratic: f32,
+    cutoff: f32,
+}
+
+struct MaterialUniform {
+    base_colour: vec4<f32>,
+    emissive: vec3<f32>,
+    emissive_strength: f32,
+    metallic: f32,
+    roughness: f32,
+    normal_scale: f32,
+    occlusion_strength: f32,
+    alpha_cutoff: f32,
+    uv_tiling: vec2<f32>,
+}
+
+const c_max_lights: u32 = 10u;
+
+@group(0) @binding(0)
+var t_diffuse: texture_2d<f32>;
+@group(0) @binding(1)
+var s_diffuse: sampler;
+@group(0) @binding(2)
+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(3) @binding(0)
+var<uniform> u_globals: Globals;
+
+@group(4) @binding(0)
+var<storage, read> s_skinning: array<mat4x4<f32>>;
+
+struct InstanceInput {
+    @location(8) model_matrix_0: vec4<f32>,
+    @location(9) model_matrix_1: vec4<f32>,
+    @location(10) model_matrix_2: vec4<f32>,
+    @location(11) model_matrix_3: vec4<f32>,
+
+    @location(12) normal_matrix_0: vec3<f32>,
+    @location(13) normal_matrix_1: vec3<f32>,
+    @location(14) normal_matrix_2: vec3<f32>,
+};
+
+struct VertexInput {
+    @location(0) position: vec3<f32>,
+    @location(1) 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 {
+    @builtin(position) clip_position: vec4<f32>,
+    @location(0) tex_coords: vec2<f32>,
+    @location(1) world_normal: vec3<f32>,
+    @location(2) world_position: vec3<f32>,
+    @location(3) world_tangent: vec3<f32>,
+    @location(4) world_bitangent: vec3<f32>,
+};
+
+@vertex
+fn vs_main(
+    model: VertexInput,
+    instance: InstanceInput,
+) -> VertexOutput {
+    let model_matrix = mat4x4<f32>(
+        instance.model_matrix_0,
+        instance.model_matrix_1,
+        instance.model_matrix_2,
+        instance.model_matrix_3,
+    );
+    let normal_matrix = mat3x3<f32>(
+        instance.normal_matrix_0,
+        instance.normal_matrix_1,
+        instance.normal_matrix_2,
+    );
+
+    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_coords0;
+    out.world_normal = world_normal;
+    out.world_position = world_position.xyz;
+    out.world_tangent = world_tangent;
+    out.world_bitangent = world_bitangent;
+    return out;
+}
+
+fn directional_light(
+    light: Light,
+    world_normal: vec3<f32>,
+    view_dir: vec3<f32>,
+    tex_color: vec3<f32>,
+    world_pos: vec3<f32>
+) -> vec3<f32> {
+    let light_dir = normalize(-light.direction.xyz);
+
+    let diff = max(dot(world_normal, light_dir), 0.0);
+    let diffuse = light.color.xyz * diff * tex_color;
+
+    let reflect_dir = reflect(-light_dir, world_normal);
+    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+    let specular = light.color.xyz * spec * tex_color;
+
+    return diffuse + specular;
+}
+
+fn point_light(
+    light: Light,
+    world_pos: vec3<f32>,
+    world_normal: vec3<f32>,
+    view_dir: vec3<f32>,
+    tex_color: vec3<f32>
+) -> vec3<f32> {
+    let light_dir = normalize(light.position.xyz - world_pos);
+
+    let distance = length(light.position.xyz - world_pos);
+    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
+
+    let diff = max(dot(world_normal, light_dir), 0.0);
+    let diffuse = light.color.xyz * diff * tex_color;
+
+    let reflect_dir = reflect(-light_dir, world_normal);
+    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+    let specular = light.color.xyz * spec * tex_color;
+
+    return (diffuse + specular) * attenuation;
+}
+
+fn spot_light(
+    light: Light,
+    world_pos: vec3<f32>,
+    world_normal: vec3<f32>,
+    view_dir: vec3<f32>,
+    tex_color: vec3<f32>
+) -> vec3<f32> {
+    let light_dir = normalize(light.position.xyz - world_pos);
+    let theta = dot(light_dir, normalize(-light.direction.xyz));
+    let outer_cutoff = light.direction.w;
+
+    let epsilon = light.cutoff - outer_cutoff;
+    let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
+
+    let distance = length(light.position.xyz - world_pos);
+    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
+
+    let diff = max(dot(world_normal, light_dir), 0.0);
+    let diffuse = light.color.xyz * diff * tex_color * intensity;
+
+    let reflect_dir = reflect(-light_dir, world_normal);
+    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+    let specular = light.color.xyz * spec * tex_color * intensity;
+
+    return (diffuse + specular) * attenuation;
+}
+
+fn apply_normal_map(
+    world_normal_in: vec3<f32>,
+    world_tangent_in: vec3<f32>,
+    world_bitangent_in: vec3<f32>,
+    normal_sample_rgb: vec3<f32>,
+) -> vec3<f32> {
+    let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0));
+
+    let n = normalize(world_normal_in);
+    var t = normalize(world_tangent_in);
+    t = normalize(t - n * dot(n, t));
+
+    let b_in = normalize(world_bitangent_in);
+    let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0);
+    let b = cross(n, t) * handedness;
+
+    let tbn = mat3x3<f32>(t, b, n);
+    return normalize(tbn * normal_ts);
+}
+
+@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.base_colour;
+
+    if (base_colour.a < u_material.alpha_cutoff) {
+        discard;
+    }
+
+    let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
+
+    let world_normal = apply_normal_map(
+        in.world_normal,
+        in.world_tangent,
+        in.world_bitangent,
+        object_normal.xyz,
+    );
+
+    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz;
+    var final_color = ambient;
+
+    for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) {
+        let light = s_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/shaders/sky.wgsl b/crates/dropbear-engine/src/shaders/sky.wgsl
new file mode 100644
index 0000000..4469190
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/sky.wgsl
@@ -0,0 +1,46 @@
+struct Camera {
+    view_pos: vec4<f32>,
+    view: mat4x4<f32>,
+    view_proj: mat4x4<f32>,
+    inv_proj: mat4x4<f32>,
+    inv_view: mat4x4<f32>,
+}
+@group(0) @binding(0)
+var<uniform> camera: Camera;
+
+@group(1)
+@binding(0)
+var env_map: texture_cube<f32>;
+@group(1)
+@binding(1)
+var env_sampler: sampler;
+
+struct VertexOutput {
+    @builtin(position) frag_position: vec4<f32>,
+    @location(0) clip_position: vec4<f32>,
+}
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) id: u32,
+) -> VertexOutput {
+    let uv = vec2<f32>(vec2<u32>(
+        id & 1u,
+        (id >> 1u) & 1u,
+    ));
+    var out: VertexOutput;
+    let ndc = uv * 4.0 - 1.0;
+    out.frag_position = vec4(ndc, 0.0, 1.0);
+    out.clip_position = vec4(ndc, 1.0, 1.0);
+    return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    let view_pos_homogeneous = camera.inv_proj * in.clip_position;
+    let view_ray_direction = view_pos_homogeneous.xyz / view_pos_homogeneous.w;
+    var ray_direction = normalize((camera.inv_view * vec4(view_ray_direction, 0.0)).xyz);
+
+    let smpl = textureSample(env_map, env_sampler, ray_direction);
+    return smpl;
+}
diff --git a/crates/dropbear-engine/src/sky.rs b/crates/dropbear-engine/src/sky.rs
new file mode 100644
index 0000000..5451340
--- /dev/null
+++ b/crates/dropbear-engine/src/sky.rs
@@ -0,0 +1,304 @@
+use std::io::Cursor;
+use std::sync::Arc;
+use crate::texture::Texture;
+use image::codecs::hdr::HdrDecoder;
+use crate::graphics::SharedGraphicsContext;
+use crate::pipelines::{create_render_pipeline_ex};
+
+pub const DEFAULT_SKY_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr");
+
+pub struct CubeTexture {
+    texture: wgpu::Texture,
+    sampler: wgpu::Sampler,
+    view: wgpu::TextureView,
+}
+
+impl CubeTexture {
+    pub fn create_2d(
+        device: &wgpu::Device,
+        width: u32,
+        height: u32,
+        format: wgpu::TextureFormat,
+        mip_level_count: u32,
+        usage: wgpu::TextureUsages,
+        mag_filter: wgpu::FilterMode,
+        label: Option<&str>,
+    ) -> Self {
+        puffin::profile_function!();
+        let texture = device.create_texture(&wgpu::TextureDescriptor {
+            label,
+            size: wgpu::Extent3d {
+                width,
+                height,
+                // A cube has 6 sides, so we need 6 layers
+                depth_or_array_layers: 6,
+            },
+            mip_level_count,
+            sample_count: 1,
+            dimension: wgpu::TextureDimension::D2,
+            format,
+            usage,
+            view_formats: &[],
+        });
+
+        let view = texture.create_view(&wgpu::TextureViewDescriptor {
+            label,
+            dimension: Some(wgpu::TextureViewDimension::Cube),
+            array_layer_count: Some(6),
+            ..Default::default()
+        });
+
+        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+            label,
+            address_mode_u: wgpu::AddressMode::ClampToEdge,
+            address_mode_v: wgpu::AddressMode::ClampToEdge,
+            address_mode_w: wgpu::AddressMode::ClampToEdge,
+            mag_filter,
+            min_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Nearest,
+            ..Default::default()
+        });
+
+        Self {
+            texture,
+            sampler,
+            view,
+        }
+    }
+
+    pub fn texture(&self) -> &wgpu::Texture { &self.texture }
+
+    pub fn view(&self) -> &wgpu::TextureView { &self.view }
+
+    pub fn sampler(&self) -> &wgpu::Sampler { &self.sampler }
+
+}
+
+pub struct HdrLoader {
+    texture_format: wgpu::TextureFormat,
+    equirect_layout: wgpu::BindGroupLayout,
+    equirect_to_cubemap: wgpu::ComputePipeline,
+}
+
+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 {
+            label: Some("HdrLoader::equirect_layout"),
+            entries: &[
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::COMPUTE,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        multisampled: false,
+                    },
+                    count: None,
+                },
+                wgpu::BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: wgpu::ShaderStages::COMPUTE,
+                    ty: wgpu::BindingType::StorageTexture {
+                        access: wgpu::StorageTextureAccess::WriteOnly,
+                        format: texture_format,
+                        view_dimension: wgpu::TextureViewDimension::D2Array,
+                    },
+                    count: None,
+                },
+            ],
+        });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: None,
+            bind_group_layouts: &[&equirect_layout],
+            push_constant_ranges: &[],
+        });
+
+        let equirect_to_cubemap =
+            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
+                label: Some("equirect_to_cubemap"),
+                layout: Some(&pipeline_layout),
+                module: &module,
+                entry_point: Some("compute_equirect_to_cubemap"),
+                compilation_options: Default::default(),
+                cache: None,
+            });
+
+        Self {
+            equirect_to_cubemap,
+            texture_format,
+            equirect_layout,
+        }
+    }
+
+    pub fn from_equirectangular_bytes(
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        data: &[u8],
+        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))?;
+        let meta = hdr_decoder.metadata();
+
+        #[cfg(not(target_arch="wasm32"))]
+        let pixels = {
+            let mut pixels = vec![[0.0, 0.0, 0.0, 0.0]; meta.width as usize * meta.height as usize];
+            hdr_decoder.read_image_transform(
+                |pix| {
+                    let rgb = pix.to_hdr();
+                    [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32]
+                },
+                &mut pixels[..],
+            )?;
+            pixels
+        };
+        #[cfg(target_arch="wasm32")]
+        let pixels = hdr_decoder.read_image_native()?
+            .into_iter()
+            .map(|pix| {
+                let rgb = pix.to_hdr();
+                [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32]
+            })
+            .collect::<Vec<_>>();
+
+        let src = Texture::create_2d_texture(
+            device,
+            meta.width,
+            meta.height,
+            loader.texture_format,
+            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
+            wgpu::FilterMode::Linear,
+            None,
+        );
+
+        queue.write_texture(
+            wgpu::TexelCopyTextureInfo {
+                texture: &src.texture,
+                mip_level: 0,
+                origin: wgpu::Origin3d::ZERO,
+                aspect: wgpu::TextureAspect::All,
+            },
+            &bytemuck::cast_slice(&pixels),
+            wgpu::TexelCopyBufferLayout {
+                offset: 0,
+                bytes_per_row: Some(src.size.width * std::mem::size_of::<[f32; 4]>() as u32),
+                rows_per_image: Some(src.size.height),
+            },
+            src.size,
+        );
+
+        let dst = CubeTexture::create_2d(
+            device,
+            dst_size,
+            dst_size,
+            loader.texture_format,
+            1,
+            // We are going to write to `dst` texture so we
+            // need to use a `STORAGE_BINDING`.
+            wgpu::TextureUsages::STORAGE_BINDING
+                | wgpu::TextureUsages::TEXTURE_BINDING,
+            wgpu::FilterMode::Nearest,
+            label,
+        );
+
+        let dst_view = dst.texture().create_view(&wgpu::TextureViewDescriptor {
+            label,
+            // Normally, you'd use `TextureViewDimension::Cube`
+            // for a cube texture, but we can't use that
+            // view dimension with a `STORAGE_BINDING`.
+            // We need to access the cube texture layers
+            // directly.
+            dimension: Some(wgpu::TextureViewDimension::D2Array),
+            ..Default::default()
+        });
+
+        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label,
+            layout: &loader.equirect_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&src.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::TextureView(&dst_view),
+                },
+            ],
+        });
+
+        let mut encoder = device.create_command_encoder(&Default::default());
+        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label, timestamp_writes: None });
+
+        let num_workgroups = (dst_size + 15) / 16;
+        pass.set_pipeline(&loader.equirect_to_cubemap);
+        pass.set_bind_group(0, &bind_group, &[]);
+        pass.dispatch_workgroups(num_workgroups, num_workgroups, 6);
+
+        drop(pass);
+
+        queue.submit([encoder.finish()]);
+
+        Ok(dst)
+    }
+}
+
+pub struct SkyPipeline {
+    pub texture: CubeTexture,
+    pub pipeline: wgpu::RenderPipeline,
+    pub bind_group: wgpu::BindGroup,
+}
+
+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,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&sky_texture.view()),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(sky_texture.sampler()),
+                },
+            ],
+        });
+
+        let sky_pipeline = {
+            let layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Sky Pipeline Layout"),
+                bind_group_layouts: &[&graphics.layouts.camera_bind_group_layout, &graphics.layouts.environment_bind_group_layout],
+                push_constant_ranges: &[],
+            });
+            let shader = wgpu::include_wgsl!("shaders/sky.wgsl");
+            create_render_pipeline_ex(
+                Some("sky render pipeline"),
+                &graphics.device,
+                &layout,
+                graphics.hdr.read().format(),
+                Some(Texture::DEPTH_FORMAT),
+                &[],
+                wgpu::PrimitiveTopology::TriangleList,
+                shader,
+                false,
+                wgpu::CompareFunction::GreaterEqual,
+            )
+        };
+        
+        Self {
+            texture: sky_texture,
+            pipeline: sky_pipeline,
+            bind_group: environment_bind_group,
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 5cae83c..4b1dd30 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -2,7 +2,7 @@ use std::{fs, path::PathBuf, sync::Arc};
 
 use image::GenericImageView;
 use serde::{Deserialize, Serialize};
-
+use crate::asset::AssetRegistry;
 use crate::graphics::SharedGraphicsContext;
 use crate::utils::ToPotentialString;
 
@@ -68,6 +68,7 @@ pub struct Texture {
     pub sampler: wgpu::Sampler,
     pub size: wgpu::Extent3d,
     pub view: wgpu::TextureView,
+    pub(crate) hash: Option<u64>,
 }
 
 impl Texture {
@@ -75,12 +76,81 @@ impl Texture {
     pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
     pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
 
+    pub fn create_2d_texture(
+        device: &wgpu::Device,
+        width: u32,
+        height: u32,
+        format: wgpu::TextureFormat,
+        usage: wgpu::TextureUsages,
+        mag_filter: wgpu::FilterMode,
+        label: Option<&str>,
+    ) -> Self {
+        puffin::profile_function!(label.unwrap_or("create 2d texture"));
+        let size = wgpu::Extent3d {
+            width,
+            height,
+            depth_or_array_layers: 1,
+        };
+        Self::create_texture(
+            device,
+            label,
+            size,
+            format,
+            usage,
+            wgpu::TextureDimension::D2,
+            mag_filter,
+        )
+    }
+
+    pub fn create_texture(
+        device: &wgpu::Device,
+        label: Option<&str>,
+        size: wgpu::Extent3d,
+        format: wgpu::TextureFormat,
+        usage: wgpu::TextureUsages,
+        dimension: wgpu::TextureDimension,
+        mag_filter: wgpu::FilterMode,
+    ) -> Self {
+        puffin::profile_function!(label.unwrap_or("create texture"));
+        let texture = device.create_texture(&wgpu::TextureDescriptor {
+            label,
+            size,
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension,
+            format,
+            usage,
+            view_formats: &[],
+        });
+
+        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+            address_mode_u: wgpu::AddressMode::ClampToEdge,
+            address_mode_v: wgpu::AddressMode::ClampToEdge,
+            address_mode_w: wgpu::AddressMode::ClampToEdge,
+            mag_filter,
+            min_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Nearest,
+            ..Default::default()
+        });
+
+        Self {
+            label: label.and_then(|v| Some(v.to_string())),
+            texture,
+            view,
+            sampler,
+            size,
+            hash: None,
+        }
+    }
+
     /// Creates a new depth texture. This is an internal function.
     pub fn depth_texture(
         config: &wgpu::SurfaceConfiguration,
         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),
@@ -119,6 +189,7 @@ impl Texture {
             size,
             view,
             label: label.to_potential_string(),
+            hash: None,
         }
     }
 
@@ -130,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),
@@ -142,7 +214,7 @@ impl Texture {
             mip_level_count: 1, // leave me alone
             sample_count: 1,
             dimension: wgpu::TextureDimension::D2,
-            format: Texture::TEXTURE_FORMAT,
+            format: config.format.add_srgb_suffix(),
             usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
             view_formats: &[],
         };
@@ -157,6 +229,7 @@ impl Texture {
             sampler,
             size,
             view,
+            hash: None,
         }
     }
 
@@ -166,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))
     }
@@ -174,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)
     }
 
@@ -188,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,
@@ -220,21 +296,26 @@ impl Texture {
         sampler: Option<wgpu::SamplerDescriptor>,
         label: Option<&str>,
     ) -> Self {
-        let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) {
-            Ok(image) => {
-                let rgba = image.to_rgba8().into_raw();
-                let dims = dimensions.unwrap_or_else(|| image.dimensions());
-                (rgba, dims)
-            }
-            Err(err) => {
-                if let Some(dims) = dimensions {
-                    let expected_len = (dims.0 as usize)
-                        .saturating_mul(dims.1 as usize)
-                        .saturating_mul(4);
-                    if bytes.len() == expected_len {
-                        (bytes.to_vec(), dims)
-                    } else {
-                        log::error!(
+        puffin::profile_function!(label.unwrap_or(""));
+        let hash = AssetRegistry::hash_bytes(bytes);
+        
+        let (diffuse_rgba, dimensions) = {
+            puffin::profile_scope!("load from memory image");
+            match image::load_from_memory(bytes) {
+                Ok(image) => {
+                    let rgba = image.to_rgba8().into_raw();
+                    let dims = dimensions.unwrap_or_else(|| image.dimensions());
+                    (rgba, dims)
+                }
+                Err(err) => {
+                    if let Some(dims) = dimensions {
+                        let expected_len = (dims.0 as usize)
+                            .saturating_mul(dims.1 as usize)
+                            .saturating_mul(4);
+                        if bytes.len() == expected_len {
+                            (bytes.to_vec(), dims)
+                        } else {
+                            log::error!(
                             "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
                             label,
                             err,
@@ -243,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))
+                    }
                 }
             }
         };
@@ -284,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,
@@ -300,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;
@@ -351,6 +435,7 @@ impl Texture {
             sampler,
             size,
             view,
+            hash: Some(hash),
         }
     }
 
@@ -400,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 0c94d07..8a5acb9 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -3,6 +3,7 @@
 use serde::{Deserialize, Serialize};
 use std::fmt::{Display, Formatter};
 use std::path::Path;
+use crate::procedural::ProcObj;
 
 pub const EUCA_SCHEME: &str = "euca://";
 
@@ -105,14 +106,10 @@ pub enum ResourceReferenceType {
     /// The content in bytes. Sometimes, there is a model that is loaded into memory through the
     /// [`include_bytes!`] macro, this type stores it.
     Bytes(Vec<u8>),
-
-    /// A parameterized cuboid (box) generated at runtime.
-    ///
-    /// Stored as IEEE-754 `f32` bit patterns so the reference remains hashable.
-    /// Values can be reconstructed with `f32::from_bits`.
-    ///
-    /// The `size_bits` represent the full extents (width, height, depth).
-    Cuboid { size_bits: [u32; 3] },
+    
+    /// An object that can be generated at runtime with the usage of vertices and indices, as well
+    /// as a solid grey mesh. 
+    ProcObj(ProcObj),
 }
 
 impl Default for ResourceReferenceType {
@@ -155,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)
@@ -175,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),
@@ -215,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/dropbear-macro/src/lib.rs b/crates/dropbear-macro/src/lib.rs
index 75f2f72..eeae874 100644
--- a/crates/dropbear-macro/src/lib.rs
+++ b/crates/dropbear-macro/src/lib.rs
@@ -108,7 +108,7 @@ fn transform_function(mut func: ItemFn) -> ItemFn {
     let pointer_check = if !is_void {
         quote! {
             if out_result.is_null() {
-                return crate::scripting::native::DropbearNativeError::NullPointer as i32;
+                return crate::scripting::native::DropbearNativeError::NullPointer.code();
             }
         }
     } else {
@@ -118,11 +118,11 @@ fn transform_function(mut func: ItemFn) -> ItemFn {
     let success_handling = if !is_void {
         quote! {
             unsafe { *out_result = val; }
-            crate::scripting::native::DropbearNativeError::Success as i32
+            crate::scripting::native::DropbearNativeError::Success.code()
         }
     } else {
         quote! {
-            crate::scripting::native::DropbearNativeError::Success as i32
+            crate::scripting::native::DropbearNativeError::Success.code()
         }
     };
 
@@ -139,7 +139,7 @@ fn transform_function(mut func: ItemFn) -> ItemFn {
                     #success_handling
                 }
                 DropbearNativeResult::Err(e) => {
-                    e as i32
+                    e.code()
                 }
             }
         }
diff --git a/crates/euca-runner/Cargo.toml b/crates/euca-runner/Cargo.toml
index 5cbba10..0cd33f3 100644
--- a/crates/euca-runner/Cargo.toml
+++ b/crates/euca-runner/Cargo.toml
@@ -21,11 +21,11 @@ chrono.workspace = true
 env_logger.workspace = true
 colored.workspace = true
 ron.workspace = true
-hecs.workspace = true
-bytemuck.workspace = true
-log-once.workspace = true
 winit.workspace = true
 serde.workspace = true
-crossbeam-channel.workspace = true
-gilrs.workspace = true
-futures.workspace = true
+
+[features]
+default = ["jvm"]
+
+jvm = ["redback-runtime/jvm", "eucalyptus-core/jvm"]
+debug = ["eucalyptus-core/jvm_debug"]
diff --git a/crates/euca-runner/src/main.rs b/crates/euca-runner/src/main.rs
index b3bc66c..0761329 100644
--- a/crates/euca-runner/src/main.rs
+++ b/crates/euca-runner/src/main.rs
@@ -1,3 +1,5 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
 use app_dirs2::AppInfo;
 use dropbear_engine::future::FutureQueue;
 use eucalyptus_core::runtime::RuntimeProjectConfig;
@@ -8,13 +10,104 @@ use std::fs;
 use std::rc::Rc;
 use std::sync::Arc;
 use ron::ser::PrettyConfig;
-use winit::window::WindowAttributes;
+use winit::window::{Fullscreen, WindowAttributes};
 use dropbear_engine::{DropbearAppBuilder, DropbearWindowBuilder};
 use serde::{Deserialize, Serialize};
+use winit::dpi::PhysicalSize;
+use eucalyptus_core::scripting::jni::{RuntimeMode, RUNTIME_MODE};
 
 #[tokio::main]
 async fn main() {
-    env_logger::init();
+    // env_logger::init();
+
+    #[cfg(all(not(target_os = "android"), feature = "debug"))]
+    {
+        use colored::Colorize;
+        use env_logger::Builder;
+        use log::LevelFilter;
+        use std::fs::OpenOptions;
+
+        let log_dir =
+            app_dirs2::app_root(app_dirs2::AppDataType::UserData, &eucalyptus_core::APP_INFO)
+                .expect("Failed to get app data directory")
+                .join("logs");
+        fs::create_dir_all(&log_dir).expect("Failed to create log dir");
+
+        let datetime_str = chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S");
+        let log_filename = format!("{}.{}.log", "eucalyptus-editor", datetime_str);
+        let log_path = log_dir.join(log_filename);
+
+        let file = OpenOptions::new()
+            .create(true)
+            .append(true)
+            .open(&log_path)
+            .expect("Failed to open log file");
+        let file = parking_lot::Mutex::new(file);
+
+        let app_target = "eucalyptus-editor".replace('-', "_");
+        let log_config = format!("dropbear_engine=trace,{}=debug,warn", app_target);
+        unsafe { std::env::set_var("RUST_LOG", log_config) };
+
+        Builder::new()
+            .format(move |buf, record| {
+                use std::io::Write;
+
+                let ts = chrono::offset::Local::now().format("%Y-%m-%dT%H:%M:%S");
+
+                let colored_level = match record.level() {
+                    log::Level::Error => record.level().to_string().red().bold(),
+                    log::Level::Warn => record.level().to_string().yellow().bold(),
+                    log::Level::Info => record.level().to_string().green().bold(),
+                    log::Level::Debug => record.level().to_string().blue().bold(),
+                    log::Level::Trace => record.level().to_string().cyan().bold(),
+                };
+
+                let colored_timestamp = ts.to_string().bright_black();
+
+                let file_info = format!(
+                    "{}:{}",
+                    record.file().unwrap_or("unknown"),
+                    record.line().unwrap_or(0)
+                )
+                    .bright_black();
+
+                let console_line = format!(
+                    "{} {} [{}] - {}\n",
+                    file_info,
+                    colored_timestamp,
+                    colored_level,
+                    record.args()
+                );
+
+                let file_line = format!(
+                    "{}:{} {} [{}] - {}\n",
+                    record.file().unwrap_or("unknown"),
+                    record.line().unwrap_or(0),
+                    ts,
+                    record.level(),
+                    record.args()
+                );
+
+                write!(buf, "{}", console_line)?;
+
+                let mut fh = file.lock();
+                let _ = fh.write_all(file_line.as_bytes());
+
+                Ok(())
+            })
+            .filter_level(LevelFilter::Warn)
+            .filter(Some("dropbear_engine"), LevelFilter::Trace)
+            .filter(
+                Some("eucalyptus_editor"),
+                LevelFilter::Debug,
+            )
+            .filter(Some("eucalyptus_core"), LevelFilter::Debug)
+            .filter(Some("dropbear_traits"), LevelFilter::Debug)
+            .filter(Some("euca_runner"), LevelFilter::Debug)
+            .init();
+        log::info!("Initialised logger");
+    }
+
 
     dropbear_engine::panic::set_hook();
     log::debug!("Set panic hook");
@@ -65,10 +158,13 @@ async fn main() {
     log::debug!("Located scene config file: [{}] ({} bytes)", path.display(), scene_config.len());
 
     let scene_config: RuntimeProjectConfig = postcard::from_bytes(&scene_config).unwrap();
+    scene_config.populate().unwrap();
 
     log::debug!("Converted scene config file to RuntimeProjectConfig");
 
-    let runtime_scene = Rc::new(RwLock::new(PlayMode::new(None).unwrap()));
+    let _ = RUNTIME_MODE.set(RuntimeMode::Runtime);
+
+    let runtime_scene = Rc::new(RwLock::new(PlayMode::new(Some(scene_config.initial_scene)).unwrap()));
     let future_queue = Arc::new(FutureQueue::new());
 
     let authors = scene_config.authors.developer.clone();
@@ -81,11 +177,11 @@ async fn main() {
 
     let attributes = WindowAttributes::default();
 
-    match config.target_resolution {
-        WindowModes::Windowed(_, _) => {}
-        WindowModes::Maximised => {}
-        WindowModes::Fullscreen => {}
-    }
+    let attributes = match config.target_resolution {
+        WindowModes::Windowed(x, y) => {attributes.with_inner_size(PhysicalSize::new(x, y))}
+        WindowModes::Maximised => {attributes.with_maximized(true)}
+        WindowModes::Fullscreen => {attributes.with_fullscreen(Some(Fullscreen::Borderless(None)))}
+    };
 
     let window = DropbearWindowBuilder::new()
         .with_attributes(attributes)
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index 47772d6..cdcdfbb 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -7,7 +7,7 @@ repository.workspace = true
 readme = "README.md"
 
 [lib]
-crate-type = ["rlib", "cdylib"]
+crate-type = ["rlib", "dylib"]
 
 [dependencies]
 dropbear-traits = { path = "../dropbear-traits" }
@@ -43,6 +43,9 @@ semver.workspace = true
 rustc_version_runtime.workspace = true
 rapier3d.workspace = true
 bytemuck.workspace = true
+#yakui.workspace = true
+thiserror.workspace = true
+combine.workspace = true
 
 [features]
 default = []
diff --git a/crates/eucalyptus-core/src/engine.rs b/crates/eucalyptus-core/src/engine.rs
index 4229fbd..ec0c4ed 100644
--- a/crates/eucalyptus-core/src/engine.rs
+++ b/crates/eucalyptus-core/src/engine.rs
@@ -110,42 +110,4 @@ pub mod jni {
             );
         }
     }
-}
-
-#[dropbear_macro::impl_c_api]
-pub mod native {
-    use crate::command::CommandBuffer;
-    use crate::convert_ptr;
-    use crate::engine::shared::read_str;
-    use crate::ptr::{AssetRegistryPtr, CommandBufferPtr, WorldPtr};
-    use crate::scripting::result::DropbearNativeResult;
-    use dropbear_engine::asset::AssetRegistry;
-    use hecs::World;
-    use std::os::raw::c_char;
-
-    pub fn dropbear_get_entity(
-        world_ptr: WorldPtr,
-        label: *const c_char,
-    ) -> DropbearNativeResult<u64> {
-        let world = convert_ptr!(world_ptr => World);
-        let label_str = unsafe { read_str(label)? };
-
-        super::shared::get_entity(world, &label_str)
-    }
-
-    pub fn dropbear_get_asset(
-        asset_ptr: AssetRegistryPtr,
-        uri: *const c_char,
-    ) -> DropbearNativeResult<u64> {
-        let asset_registry = convert_ptr!(asset_ptr => AssetRegistry);
-        let uri_str = unsafe { read_str(uri)? };
-        super::shared::get_asset(&asset_registry, &uri_str)
-    }
-
-    pub fn dropbear_quit(
-        command_ptr: CommandBufferPtr,
-    ) -> DropbearNativeResult<()> {
-        let sender = convert_ptr!(command_ptr => crossbeam_channel::Sender<CommandBuffer>);
-        super::shared::quit(sender)
-    }
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index d0cc99e..e0af790 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -22,12 +22,14 @@ pub mod mesh;
 pub mod entity;
 pub mod engine;
 pub mod transform;
+pub mod ui;
 
 pub use dropbear_macro as macros;
 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;
@@ -37,6 +39,7 @@ use crate::physics::collider::ColliderGroup;
 use crate::physics::kcc::KCC;
 use crate::physics::rigidbody::RigidBody;
 use crate::states::{Camera3D, Light, Script, SerializedMeshRenderer};
+use crate::ui::UIComponent;
 
 /// The appdata directory for storing any information.
 ///
@@ -66,6 +69,8 @@ pub fn register_components(
     component_registry.register_with_default::<RigidBody>();
     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/physics/collider/shader.rs b/crates/eucalyptus-core/src/physics/collider/shader.rs
index e1067df..d20983d 100644
--- a/crates/eucalyptus-core/src/physics/collider/shader.rs
+++ b/crates/eucalyptus-core/src/physics/collider/shader.rs
@@ -34,6 +34,7 @@ impl DropbearShaderPipeline for ColliderWireframePipeline {
             push_constant_ranges: &[],
         });
 
+        let hdr_format = graphics.hdr.read().format();
         let pipeline = graphics.device.create_render_pipeline(&RenderPipelineDescriptor {
             label: Some("Collider Wireframe Pipeline"),
             layout: Some(&pipeline_layout),
@@ -60,7 +61,7 @@ impl DropbearShaderPipeline for ColliderWireframePipeline {
                 module: &shader.module,
                 entry_point: Some("fs_main"),
                 targets: &[Some(ColorTargetState {
-                    format: Texture::TEXTURE_FORMAT,
+                    format: hdr_format,
                     blend: Some(BlendState::ALPHA_BLENDING),
                     write_mask: ColorWrites::ALL,
                 })],
diff --git a/crates/eucalyptus-core/src/physics/kcc.rs b/crates/eucalyptus-core/src/physics/kcc.rs
index a6bae53..6500a5a 100644
--- a/crates/eucalyptus-core/src/physics/kcc.rs
+++ b/crates/eucalyptus-core/src/physics/kcc.rs
@@ -40,6 +40,7 @@ pub mod jni {
     use jni::objects::{JClass, JObject};
     use jni::sys::{jboolean, jdouble, jlong};
     use rapier3d::dynamics::RigidBodyType;
+    use rapier3d::math::Rotation;
     use rapier3d::prelude::QueryFilter;
     use crate::{convert_jlong_to_entity, convert_ptr};
     use crate::physics::kcc::KCC;
@@ -155,6 +156,72 @@ pub mod jni {
     }
 
     #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_physics_KinematicCharacterControllerNative_setRotation(
+        mut env: JNIEnv,
+        _: JClass,
+        world_handle: jlong,
+        physics_handle: jlong,
+        entity: jlong,
+        rotation: JObject,
+    ) {
+        let world = convert_ptr!(world_handle => World);
+        let physics_state = convert_ptr!(mut physics_handle => PhysicsState);
+        let entity = convert_jlong_to_entity!(entity);
+
+        let class = match env.find_class("com/dropbear/math/Quaterniond") {
+            Ok(cls) => cls,
+            Err(_) => {
+                let _ = env.throw_new("java/lang/RuntimeException", "Unable to find Quaterniond class");
+                return;
+            }
+        };
+
+        if let Ok(false) = env.is_instance_of(&rotation, &class) {
+            let _ = env.throw_new("java/lang/IllegalArgumentException", "rotation must be Quaterniond");
+            return;
+        }
+
+        let mut get_double = |field: &str| -> Option<f64> {
+            env.get_field(&rotation, field, "D").ok()?.d().ok()
+        };
+
+        let Some(rx) = get_double("x") else { return; };
+        let Some(ry) = get_double("y") else { return; };
+        let Some(rz) = get_double("z") else { return; };
+        let Some(rw) = get_double("w") else { return; };
+
+        let len = (rx * rx + ry * ry + rz * rz + rw * rw).sqrt();
+        let (x, y, z, w) = if len > 0.0 {
+            (rx / len, ry / len, rz / len, rw / len)
+        } else {
+            (0.0, 0.0, 0.0, 1.0)
+        };
+
+        if let Ok((label, _kcc)) = world.query_one::<(&Label, &KCC)>(entity).get() {
+            let Some(rigid_body_handle) = physics_state.bodies_entity_map.get(label) else {
+                return;
+            };
+
+            let body_type = {
+                let Some(body) = physics_state.bodies.get(*rigid_body_handle) else {
+                    return;
+                };
+                body.body_type()
+            };
+
+            match body_type {
+                RigidBodyType::KinematicPositionBased => {}
+                _ => return,
+            }
+
+            if let Some(body) = physics_state.bodies.get_mut(*rigid_body_handle) {
+                let rot = Rotation::from_xyzw(x as f32, y as f32, z as f32, w as f32);
+                body.set_next_kinematic_rotation(rot);
+            }
+        }
+    }
+
+    #[unsafe(no_mangle)]
     pub extern "system" fn Java_com_dropbear_physics_KinematicCharacterControllerNative_getHitNative(
         mut env: JNIEnv,
         _: JClass,
diff --git a/crates/eucalyptus-core/src/ptr.rs b/crates/eucalyptus-core/src/ptr.rs
index 5c3adab..86bae38 100644
--- a/crates/eucalyptus-core/src/ptr.rs
+++ b/crates/eucalyptus-core/src/ptr.rs
@@ -7,6 +7,7 @@ use hecs::World;
 use parking_lot::Mutex;
 use crate::physics::PhysicsState;
 use crate::scene::loading::SceneLoader;
+use crate::ui::{UiContext};
 
 /// A mutable pointer to a [`World`].
 ///
@@ -41,4 +42,10 @@ pub type SceneLoaderPtr = *const Mutex<SceneLoader>;
 /// A mutable pointer to a [`PhysicsState`].
 ///
 /// Defined in `dropbear_common.h` as `PhysicsEngine`
-pub type PhysicsStatePtr = *mut PhysicsState;
\ No newline at end of file
+pub type PhysicsStatePtr = *mut PhysicsState;
+
+/// A mutable pointer to a [`UiContext`], used for queueing UI components
+/// in the scripting module. 
+/// 
+/// Defined in `dropbear_common.h` as `UiBufferPtr`. 
+pub type UiBufferPtr = *const UiContext;
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/runtime.rs b/crates/eucalyptus-core/src/runtime.rs
index 9532ce5..609525d 100644
--- a/crates/eucalyptus-core/src/runtime.rs
+++ b/crates/eucalyptus-core/src/runtime.rs
@@ -1,10 +1,12 @@
 //! Configuration and metadata information about redback-runtime based data.
 
+use crate::config::ProjectConfig;
 use crate::scene::SceneConfig;
 use crate::states::{PROJECT, SCENES};
+use crate::utils::option::HistoricalOption;
 use anyhow::Context;
+use chrono::Utc;
 use semver::Version;
-use crate::utils::option::HistoricalOption;
 
 /// The settings of a project in its runtime.
 ///
@@ -119,4 +121,44 @@ impl RuntimeProjectConfig {
 
         Ok(result)
     }
+
+    /// Populates the states (such as [PROJECT]) with all the context from the RuntimeProjectConfig. 
+    pub fn populate(&self) -> anyhow::Result<()> {
+        let exe_dir = std::env::current_exe()
+            .context("Unable to locate runtime executable")?
+            .parent()
+            .ok_or_else(|| anyhow::anyhow!("Unable to locate parent directory of runtime executable"))?
+            .to_path_buf();
+
+        let now = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
+
+        let mut runtime_settings = self.runtime_settings.clone();
+        if runtime_settings.initial_scene.is_none() {
+            runtime_settings.initial_scene = Some(self.initial_scene.clone());
+        }
+
+        let project_config = ProjectConfig {
+            project_name: self.project_name.clone(),
+            project_path: exe_dir,
+            date_created: now.clone(),
+            date_last_accessed: now,
+            project_version: self.project_version.to_string(),
+            authors: self.authors.clone(),
+            runtime_settings,
+            last_opened_scene: Some(self.initial_scene.clone()),
+        };
+
+        {
+            let mut project = PROJECT.write();
+            *project = project_config;
+        }
+
+        {
+            let mut scenes = SCENES.write();
+            scenes.clear();
+            scenes.extend(self.scenes.iter().cloned());
+        }
+
+        Ok(())
+    }
 }
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 0edd530..11cbe5a 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -28,6 +28,7 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use crossbeam_channel::Sender;
 use hecs::Entity;
+use dropbear_engine::procedural::ProcObj;
 use crate::physics::collider::ColliderGroup;
 use crate::physics::kcc::KCC;
 use crate::physics::PhysicsState;
@@ -156,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)
@@ -185,28 +189,32 @@ impl SceneConfig {
                             .await?;
                     MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
                 }
-                ResourceReferenceType::Cuboid { size_bits } => {
-                    let size = [
-                        f32::from_bits(size_bits[0]),
-                        f32::from_bits(size_bits[1]),
-                        f32::from_bits(size_bits[2]),
-                    ];
-                    log::info!("Loading entity from cuboid: {:?}", size);
-                    {
-                        let mut cache_guard = MODEL_CACHE.lock();
-                        cache_guard.remove(label);
-                    }
+                ResourceReferenceType::ProcObj(obj) => {
+                    match obj {
+                        ProcObj::Cuboid { size_bits } => {
+                            let size = [
+                                f32::from_bits(size_bits[0]),
+                                f32::from_bits(size_bits[1]),
+                                f32::from_bits(size_bits[2]),
+                            ];
+                            log::info!("Loading entity from cuboid: {:?}", size);
+                            {
+                                let mut cache_guard = MODEL_CACHE.lock();
+                                cache_guard.remove(label);
+                            }
 
-                    let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
-                    let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec)
-                        .build_model(graphics.clone(), None, Some(label));
+                            let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
+                            let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec)
+                                .build_model(graphics.clone(), None, Some(label));
 
-                    let model = loaded_model.make_mut();
-                    model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits: *size_bits });
+                            let model = loaded_model.make_mut();
+                            model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits }));
 
-                    loaded_model.refresh_registry();
+                            loaded_model.refresh_registry();
 
-                    MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale)
+                            MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale)
+                        }
+                    }
                 }
             };
 
diff --git a/crates/eucalyptus-core/src/scene/scripting.rs b/crates/eucalyptus-core/src/scene/scripting.rs
index c76985c..239ebf0 100644
--- a/crates/eucalyptus-core/src/scene/scripting.rs
+++ b/crates/eucalyptus-core/src/scene/scripting.rs
@@ -358,193 +358,6 @@ pub mod jni {
     }
 }
 
-pub mod native {
-    use crate::ptr::{CommandBufferPtr, SceneLoaderPtr};
-    use crate::scripting::native::DropbearNativeError;
-    use std::ffi::c_char;
-    use std::ffi::CStr;
-
-    #[unsafe(no_mangle)]
-    pub extern "C" fn load_scene_async(
-        command_buffer_ptr: CommandBufferPtr,
-        scene_loader_ptr: SceneLoaderPtr,
-        scene_name: *const c_char,
-        loading_scene: *const c_char,
-        out_scene_id: *mut u64,
-    ) -> DropbearNativeError {
-        if command_buffer_ptr.is_null() || scene_loader_ptr.is_null() || scene_name.is_null() {
-            return DropbearNativeError::NullPointer;
-        }
-
-        let command_buffer = unsafe { &*command_buffer_ptr };
-        let scene_loader = unsafe { &*scene_loader_ptr };
-
-        let scene_name_cstr = unsafe { CStr::from_ptr(scene_name) };
-        let scene_name_str = match scene_name_cstr.to_str() {
-            Ok(s) => s.to_string(),
-            Err(_) => return DropbearNativeError::InvalidUTF8,
-        };
-
-        let loading_scene_option = if !loading_scene.is_null() {
-            let loading_scene_cstr = unsafe { CStr::from_ptr(loading_scene) };
-            match loading_scene_cstr.to_str() {
-                Ok(s) => Some(s.to_string()),
-                Err(_) => return DropbearNativeError::InvalidUTF8,
-            }
-        } else {
-            None
-        };
-
-        match super::shared::load_scene_async(command_buffer, scene_loader, scene_name_str, loading_scene_option) {
-            Ok(scene_id) => {
-                if !out_scene_id.is_null() {
-                    unsafe { *out_scene_id = scene_id };
-                }
-                DropbearNativeError::Success
-            }
-            Err(e) => e,
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "C" fn switch_to_scene_immediate(
-        command_buffer_ptr: CommandBufferPtr,
-        scene_name: *const c_char,
-    ) -> DropbearNativeError {
-        if command_buffer_ptr.is_null() || scene_name.is_null() {
-            return DropbearNativeError::NullPointer;
-        }
-
-        let command_buffer = unsafe { &*command_buffer_ptr };
-
-        let scene_name_cstr = unsafe { CStr::from_ptr(scene_name) };
-        let scene_name_str = match scene_name_cstr.to_str() {
-            Ok(s) => s.to_string(),
-            Err(_) => return DropbearNativeError::InvalidUTF8,
-        };
-
-        match super::shared::switch_to_scene_immediate(command_buffer, scene_name_str) {
-            Ok(_) => DropbearNativeError::Success,
-            Err(e) => e,
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "C" fn get_scene_load_handle_scene_name(
-        scene_loader_ptr: SceneLoaderPtr,
-        scene_id: u64,
-        out_buffer: *mut c_char,
-        buffer_size: usize,
-    ) -> DropbearNativeError {
-        if scene_loader_ptr.is_null() || out_buffer.is_null() {
-            return DropbearNativeError::NullPointer;
-        }
-
-        let scene_loader = unsafe { &*scene_loader_ptr };
-
-        match super::shared::get_scene_load_handle_scene_name(scene_loader, scene_id) {
-            Ok(scene_name) => {
-                let c_string = match std::ffi::CString::new(scene_name) {
-                    Ok(cstr) => cstr,
-                    Err(_) => return DropbearNativeError::CStringError,
-                };
-
-                let bytes = c_string.as_bytes_with_nul();
-                if bytes.len() > buffer_size {
-                    return DropbearNativeError::BufferTooSmall;
-                }
-
-                unsafe {
-                    std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buffer as *mut u8, bytes.len());
-                }
-
-                DropbearNativeError::Success
-            }
-            Err(e) => e,
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "C" fn switch_to_scene_async(
-        command_buffer_ptr: CommandBufferPtr,
-        scene_loader_ptr: SceneLoaderPtr,
-        scene_id: u64,
-    ) -> DropbearNativeError {
-        if command_buffer_ptr.is_null() || scene_loader_ptr.is_null() {
-            return DropbearNativeError::NullPointer;
-        }
-
-        let command_buffer = unsafe { &*command_buffer_ptr };
-        let scene_loader = unsafe { &*scene_loader_ptr };
-
-        match super::shared::switch_to_scene_async(command_buffer, scene_loader, scene_id) {
-            Ok(_) => DropbearNativeError::Success,
-            Err(e) => e,
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "C" fn get_scene_load_progress(
-        scene_loader_ptr: SceneLoaderPtr,
-        scene_id: u64,
-        out_current: *mut f64,
-        out_total: *mut f64,
-        out_message: *mut c_char,
-        message_buffer_size: usize,
-    ) -> DropbearNativeError {
-        if scene_loader_ptr.is_null() {
-            return DropbearNativeError::NullPointer;
-        }
-
-        let scene_loader = unsafe { &*scene_loader_ptr };
-
-        match super::shared::get_scene_load_progress(scene_loader, scene_id) {
-            Ok(progress) => {
-                if !out_current.is_null() {
-                    unsafe { *out_current = progress.current as f64 };
-                }
-                if !out_total.is_null() {
-                    unsafe { *out_total = progress.total as f64 };
-                }
-
-                if !out_message.is_null() && message_buffer_size > 0 {
-                    let message_cstr = match std::ffi::CString::new(progress.message) {
-                        Ok(cstr) => cstr,
-                        Err(_) => return DropbearNativeError::CStringError,
-                    };
-
-                    let bytes = message_cstr.as_bytes_with_nul();
-                    let copy_len = std::cmp::min(bytes.len(), message_buffer_size);
-
-                    unsafe {
-                        std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_message as *mut u8, copy_len);
-                    }
-                }
-
-                DropbearNativeError::Success
-            }
-            Err(e) => e,
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "C" fn get_scene_load_status(
-        scene_loader_ptr: SceneLoaderPtr,
-        scene_id: u64,
-    ) -> i32 {
-        if scene_loader_ptr.is_null() {
-            return DropbearNativeError::NullPointer as i32;
-        }
-
-        let scene_loader = unsafe { &*scene_loader_ptr };
-
-        match super::shared::get_scene_load_status(scene_loader, scene_id) {
-            Ok(status) => status as i32,
-            Err(e) => e as i32,
-        }
-    }
-}
-
 impl crate::scripting::jni::utils::ToJObject for crate::utils::Progress {
     fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> {
         let class = env.find_class("com/dropbear/utils/Progress")
diff --git a/crates/eucalyptus-core/src/scripting.rs b/crates/eucalyptus-core/src/scripting.rs
index f2a6965..f4e89c9 100644
--- a/crates/eucalyptus-core/src/scripting.rs
+++ b/crates/eucalyptus-core/src/scripting.rs
@@ -12,7 +12,7 @@ pub static JVM_ARGS: OnceLock<String> = OnceLock::new();
 pub static AWAIT_JDB: OnceLock<bool> = OnceLock::new();
 
 use std::sync::OnceLock;
-use crate::ptr::{AssetRegistryPtr, CommandBufferPtr, InputStatePtr, PhysicsStatePtr, SceneLoaderPtr, WorldPtr};
+use crate::ptr::{AssetRegistryPtr, CommandBufferPtr, InputStatePtr, PhysicsStatePtr, SceneLoaderPtr, UiBufferPtr, WorldPtr};
 use crate::scripting::jni::JavaContext;
 use crate::scripting::native::NativeLibrary;
 use crate::states::{Script};
@@ -29,6 +29,7 @@ use dropbear_engine::model::MODEL_CACHE;
 use magna_carta::Target;
 use crate::scene::loading::SCENE_LOADER;
 use crate::types::{CollisionEvent, ContactForceEvent};
+use crate::ui::UI_CONTEXT;
 
 /// The target of the script. This can be either a JVM or a native library.
 #[derive(Default, Clone, Debug)]
@@ -118,6 +119,9 @@ impl ScriptManager {
             log::debug!("Created new JVM instance");
         }
 
+        #[cfg(not(feature = "jvm"))]
+        log::debug!("JVM feature not enabled");
+
         Ok(result)
     }
 
@@ -225,6 +229,10 @@ impl ScriptManager {
         
         let model_cache_ptr = &raw const *MODEL_CACHE;
         ASSET_REGISTRY.add_pointer(Const("model_cache"), model_cache_ptr as usize);
+        
+        let ui_buf = UI_CONTEXT.with(|v| {
+            v.as_ptr()
+        });
 
         let context = DropbearContext {
             world,
@@ -233,6 +241,7 @@ impl ScriptManager {
             assets,
             scene_loader,
             physics_state,
+            ui_buf,
         };
 
         if world.is_null() { log::error!("World pointer is null"); }
@@ -966,4 +975,5 @@ pub struct DropbearContext {
     pub assets: AssetRegistryPtr,
     pub scene_loader: SceneLoaderPtr,
     pub physics_state: PhysicsStatePtr,
+    pub ui_buf: UiBufferPtr,
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/scripting/jni.rs b/crates/eucalyptus-core/src/scripting/jni.rs
index ca6ee72..81cd304 100644
--- a/crates/eucalyptus-core/src/scripting/jni.rs
+++ b/crates/eucalyptus-core/src/scripting/jni.rs
@@ -144,8 +144,6 @@ impl JavaContext {
         
         let mut args_log = Vec::new();
 
-
-
         if let Some(args) = external_vm_args {
             args_log.push(args.clone());
             jvm_args = jvm_args.option(args);
@@ -163,30 +161,8 @@ impl JavaContext {
                     }
                     RuntimeMode::Editor => {
                         log::debug!("JDB is not used in the editor (as there is no need for so)");
-
-                        // let (start_port, end_port) = (6741, 6761);
-                        // let mut port = 0000;
-                        // let mut debug_arg = String::new();
-                        //
-                        // for p in start_port..end_port {
-                        //     if is_port_available(p) {
-                        //         port = p;
-                        //         debug_arg = format!("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:{}", port);
-                        //         break;
-                        //     } else {
-                        //         log::debug!("Port {} is not available", p);
-                        //     }
-                        // }
-                        //
-                        // if debug_arg.is_empty() {
-                        //     log::warn!("Could not find an available port for JDWP debugger (tried 6741-6760). Debugging will be disabled.");
-                        // } else {
-                        //     args_log.push(debug_arg.clone());
-                        //     jvm_args = jvm_args.option(debug_arg);
-                        //     log::info!("JDWP debugger enabled on port {}", port);
-                        // }
                     }
-                    RuntimeMode::PlayMode => {
+                    RuntimeMode::PlayMode | RuntimeMode::Runtime => {
                         let (start_port, end_port) = (6751, 6771);
                         let mut port = 0000;
                         let mut debug_arg = String::new();
@@ -218,9 +194,6 @@ impl JavaContext {
                             log::info!("JDWP debugger enabled on port {}", port);
                         }
                     }
-                    RuntimeMode::Runtime => {
-                        log::warn!("Runtime mode JWDB not implemented yet...");
-                    }
                 }
             }
 
@@ -288,9 +261,6 @@ impl JavaContext {
         let jvm_init_args = jvm_args.build()?;
         let jvm = JavaVM::new(jvm_init_args)?;
 
-        #[cfg(feature = "jvm_debug")]
-        crate::success!("JDB debugger enabled on localhost:6741");
-
         log::info!("Created JVM instance");
 
         Ok(Self {
@@ -315,6 +285,7 @@ impl JavaContext {
             let asset_handle = context.assets as jlong;
             let scene_loader_handle = context.scene_loader as jlong;
             let physics_handle = context.physics_state as jlong;
+            let ui_handle = context.ui_buf as jlong;
 
             let args = [
                 JValue::Long(world_handle),
@@ -322,7 +293,8 @@ impl JavaContext {
                 JValue::Long(graphics_handle),
                 JValue::Long(asset_handle),
                 JValue::Long(scene_loader_handle),
-                JValue::Long(physics_handle)
+                JValue::Long(physics_handle),
+                JValue::Long(ui_handle),
             ];
 
             let mut sig = String::from("(");
@@ -388,8 +360,23 @@ impl JavaContext {
                 )?
                 .l()?;
 
-            let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?;
-            let log_writer_obj = env.new_object(std_out_writer_class, "()V", &[])?;
+            let log_writer_obj = match RUNTIME_MODE.get() {
+                Some(RuntimeMode::Editor) | Some(RuntimeMode::PlayMode) => {
+                    let port = 56624;
+                    if std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() {
+                        let socket_writer_class = env.find_class("com/dropbear/logging/SocketWriter")?;
+                        env.new_object(socket_writer_class, "()V", &[])?
+                    } else {
+                        log::debug!("Editor console not reachable at 127.0.0.1:{}. Falling back to StdoutWriter.", port);
+                        let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?;
+                        env.new_object(std_out_writer_class, "()V", &[])?
+                    }
+                }
+                _ => {
+                    let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?;
+                    env.new_object(std_out_writer_class, "()V", &[])?
+                }
+            };
 
             if self.system_manager_instance.is_none() {
                 let engine_ref = self
diff --git a/crates/eucalyptus-core/src/scripting/native.rs b/crates/eucalyptus-core/src/scripting/native.rs
index c70f5bf..dc0e7eb 100644
--- a/crates/eucalyptus-core/src/scripting/native.rs
+++ b/crates/eucalyptus-core/src/scripting/native.rs
@@ -10,10 +10,14 @@ use crate::scripting::native::sig::{CollisionEvent, ContactForceEvent, DestroyAl
 use anyhow::anyhow;
 use libloading::{Library, Symbol};
 use std::ffi::CString;
-use std::fmt::{Display, Formatter};
+// use std::fmt::{Display, Formatter}; // Display derived by thiserror
 use std::path::Path;
 use crate::scripting::DropbearContext;
 use crate::types::{CollisionEvent as CollisionEventFFI, ContactForceEvent as ContactForceEventFFI};
+use thiserror::Error;
+use jni::signature::TypeSignature;
+use jni::errors::JniError;
+
 
 pub struct NativeLibrary {
     #[allow(dead_code)]
@@ -442,150 +446,199 @@ impl LastErrorMessage for NativeLibrary {
     }
 }
 
-#[repr(C)]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Error)]
 /// Displays the types of errors that can be returned by the native library.
 pub enum DropbearNativeError {
     /// An error in the case the function returns an unsigned value.
-    ///
-    /// Subtract [`DropbearNativeError::UnsignedGenericError`] with another value
-    /// to get the alternative unsigned error.
-    UnsignedGenericError = 65535,
+    #[error("Unsigned generic error")]
+    UnsignedGenericError,
     /// An error that is thrown, but doesn't have any attached context.
-    GenericError = 1,
+    #[error("Generic error")]
+    GenericError,
     /// The default return code for a successful FFI operation.
-    Success = 0,
+    #[error("Success")]
+    Success,
     /// A null pointer was provided into the function, and cannot be read.
-    NullPointer = -1,
+    #[error("Null pointer")]
+    NullPointer,
     /// Attempting to query the current world failed for some cause.
-    QueryFailed = -2,
+    #[error("Query failed")]
+    QueryFailed,
     /// The entity does not exist in the world.
-    EntityNotFound = -3,
-
-    /// No such component exists **or** the component type id is not the same as the one in the
-    /// [`hecs::World`] database.
-    ///
-    /// # Causes
-    /// There are two potential causes for this error:
-    /// - If the component that the world is locating is not available within the entity, it will
-    ///   throw this.
-    /// - Due to Rust's compilation methods and the weird architecture of the dropbear project,
-    ///   if the `eucalyptus_core` library is not compiled with an executable
-    ///   (such as `eucalyptus-editor` or `redback-runtime`), it will throw this error.
-    ///
-    /// The querying system of `eucalyptus-core` is done with a [`hecs::World`] (which stored all the
-    /// entities), the component registry ([`dropbear_traits::registry::ComponentRegistry`]) that
-    /// stores all the potential component names (including ones from external plugins) and the
-    /// [`std::any::TypeId`] (which generates a hash of the components/types).
-    ///
-    /// If `eucalyptus-core` is externally compiled as its own thing (and not bundled with any executable),
-    /// a query will lead to a fail due to the hashes being completely different.
-    ///
-    /// When originally stumped with the issue of DLL's and EXE constantly throwing this error, a user
-    /// on the Rust discord provided me with this:
-    ///
-    /// ```txt
-    /// in short, the rules are as follows:
-    /// 1. If the compilers that produced two Rust binaries are different, or they were produced by
-    ///    compiling for different targets, or if one of them is a compilation root (not of crate
-    ///    type rlib or dylib), the two binaries have different Rust ABI
-    /// 2. If two binaries have different Rust ABIs, one cannot be used to satisfy a crate dependency
-    ///    of another (and thus, absent extern blocks and the associated unsafe, they cannot call each other's functions)
-    /// 3. If two binaries have different Rust ABI, their TypeIds will not be consistent
-    /// 4. If two Rust binaries are built from different source code, one cannot be substituted for
-    ///    another to satisfy the dependency of some other crate
-    /// ```
-    ///
-    /// Yeah, so likely if this error is thrown at you, either the compilation is wrong or you
-    /// **genuinely** messed up and didn't include the component.
-    ///
-    /// Anyhow, if you are able to confirm it was a compilation error, please open an issue
-    /// on the dropbear GitHub.
-    NoSuchComponent = -4,
-
+    #[error("Entity not found")]
+    EntityNotFound,
+    /// No such component exists.
+    #[error("No such component")]
+    NoSuchComponent,
     /// No such entity uses the specific component.
-    NoSuchEntity = -5,
+    #[error("No such entity")]
+    NoSuchEntity,
     /// Inserting something (like a component) into the world failed
-    WorldInsertError = -6,
+    #[error("World insert error")]
+    WorldInsertError,
     /// When the graphics queue fails to send its message to the receiver
-    SendError = -7,
+    #[error("Send error")]
+    SendError,
     /// Error while creating a new CString
-    CStringError = -8,
-    BufferTooSmall = -9,
+    #[error("CString error")]
+    CStringError,
+    #[error("Buffer too small")]
+    BufferTooSmall,
     /// Attempting to switch scenes before the world is loaded will throw this error.
-    PrematureSceneSwitch = -10,
+    #[error("Premature scene switch")]
+    PrematureSceneSwitch,
     /// When a gamepad is not found while querying the input state for so.
-    GamepadNotFound = -11,
+    #[error("Gamepad not found")]
+    GamepadNotFound,
     /// When the argument is invalid
-    InvalidArgument = -12,
+    #[error("Invalid argument")]
+    InvalidArgument,
     /// The handle provided does not exist. Could be for an asset, entity, or other handle type.
-    NoSuchHandle = -13,
+    #[error("No such handle")]
+    NoSuchHandle,
     /// Failed to create a Java object via JNI.
-    JNIFailedToCreateObject = -14,
+    #[error("JNI failed to create object")]
+    JNIFailedToCreateObject,
     /// Failed to get a field from a Java object via JNI.
-    JNIFailedToGetField = -15,
+    #[error("JNI failed to get field")]
+    JNIFailedToGetField,
     /// Failed to find a Java class via JNI.
-    JNIClassNotFound = -16,
+    #[error("JNI class not found")]
+    JNIClassNotFound,
     /// Failed to find a Java method via JNI.
-    JNIMethodNotFound = -17,
+    #[error("JNI method not found")]
+    JNIMethodNotFound,
     /// Failed to unwrap a Java object via JNI.
-    JNIUnwrapFailed = -18,
+    #[error("JNI unwrap failed")]
+    JNIUnwrapFailed,
     /// Generic asset error. There was an error thrown, however there is no context attached. 
-    GenericAssetError = -19,
+    #[error("Generic asset error")]
+    GenericAssetError,
     /// The provided uri (either euca:// or https) was invalid and formatted wrong.
-    InvalidURI = -20,
+    #[error("Invalid URI")]
+    InvalidURI,
     /// The asset provided by the handle is wrong.
-    AssetNotFound = -21,
+    #[error("Asset not found")]
+    AssetNotFound,
     /// When a handle has been inputted wrongly.
-    InvalidHandle = -22,
+    #[error("Invalid handle")]
+    InvalidHandle,
     /// When a physics object is not found
-    PhysicsObjectNotFound = -23,
-    
-    /// The entity provided was invalid, likely not from [hecs::Entity::from_bits].
-    InvalidEntity = -100,
-
-
-    /// The CString (or `*const c_char`) contained invalid UTF-8 while being decoded.
-    InvalidUTF8 = -108,
-    /// A generic error when the library doesn't know what happened or cannot find a
-    /// suitable error code.
-    ///
-    /// The number `1274` comes from the total sum of the word "UnknownError" in decimal
-    UnknownError = -1274,
+    #[error("Physics object not found")]
+    PhysicsObjectNotFound,
+    /// The entity provided was invalid.
+    #[error("Invalid entity")]
+    InvalidEntity,
+    /// The CString contained invalid UTF-8.
+    #[error("Invalid UTF-8")]
+    InvalidUTF8,
+    /// A generic error when the library doesn't know what happened.
+    #[error("Unknown error")]
+    UnknownError,
+
+    // JNI Errors impl
+    #[error("Invalid JValue type cast: {0}. Actual type: {1}")]
+    WrongJValueType(&'static str, &'static str),
+    #[error("Invalid constructor return type (must be void)")]
+    InvalidCtorReturn,
+    #[error("Invalid number or type of arguments passed to java method: {0}")]
+    InvalidArgList(TypeSignature),
+    #[error("Method not found: {name} {sig}")]
+    MethodNotFound { name: String, sig: String },
+    #[error("Field not found: {name} {sig}")]
+    FieldNotFound { name: String, sig: String },
+    #[error("Java exception was thrown")]
+    JavaException,
+    #[error("JNIEnv null method pointer for {0}")]
+    JNIEnvMethodNotFound(&'static str),
+    #[error("Null pointer in {0}")]
+    NullPtr(&'static str),
+    #[error("Null pointer deref in {0}")]
+    NullDeref(&'static str),
+    #[error("Mutex already locked")]
+    TryLock,
+    #[error("JavaVM null method pointer for {0}")]
+    JavaVMMethodNotFound(&'static str),
+    #[error("Field already set: {0}")]
+    FieldAlreadySet(String),
+    #[error("Throw failed with error code {0}")]
+    ThrowFailed(i32),
+    #[error("Parse failed for input: {1}")]
+    ParseFailed(#[source] combine::error::StringStreamError, String),
+    #[error("JNI call failed")]
+    JniCall(#[source] JniError),
+}
+
+impl DropbearNativeError {
+    pub fn code(&self) -> i32 {
+        match self {
+            DropbearNativeError::Success => 0,
+            DropbearNativeError::UnsignedGenericError => 65535,
+            DropbearNativeError::GenericError => 1,
+            DropbearNativeError::NullPointer => -1,
+            DropbearNativeError::QueryFailed => -2,
+            DropbearNativeError::EntityNotFound => -3,
+            DropbearNativeError::NoSuchComponent => -4,
+            DropbearNativeError::NoSuchEntity => -5,
+            DropbearNativeError::WorldInsertError => -6,
+            DropbearNativeError::SendError => -7,
+            DropbearNativeError::CStringError => -8,
+            DropbearNativeError::BufferTooSmall => -9,
+            DropbearNativeError::PrematureSceneSwitch => -10,
+            DropbearNativeError::GamepadNotFound => -11,
+            DropbearNativeError::InvalidArgument => -12,
+            DropbearNativeError::NoSuchHandle => -13,
+            DropbearNativeError::JNIFailedToCreateObject => -14,
+            DropbearNativeError::JNIFailedToGetField => -15,
+            DropbearNativeError::JNIClassNotFound => -16,
+            DropbearNativeError::JNIMethodNotFound => -17,
+            DropbearNativeError::JNIUnwrapFailed => -18,
+            DropbearNativeError::GenericAssetError => -19,
+            DropbearNativeError::InvalidURI => -20,
+            DropbearNativeError::AssetNotFound => -21,
+            DropbearNativeError::InvalidHandle => -22,
+            DropbearNativeError::PhysicsObjectNotFound => -23,
+            DropbearNativeError::InvalidEntity => -100,
+            DropbearNativeError::InvalidUTF8 => -108,
+            DropbearNativeError::UnknownError => -1274,
+            // New JNI errors start from -200 to separate them
+            DropbearNativeError::WrongJValueType(_, _) => -200,
+            DropbearNativeError::InvalidCtorReturn => -201,
+            DropbearNativeError::InvalidArgList(_) => -202,
+            DropbearNativeError::MethodNotFound { .. } => -203,
+            DropbearNativeError::FieldNotFound { .. } => -204,
+            DropbearNativeError::JavaException => -205,
+            DropbearNativeError::JNIEnvMethodNotFound(_) => -206,
+            DropbearNativeError::NullPtr(_) => -207,
+            DropbearNativeError::NullDeref(_) => -208,
+            DropbearNativeError::TryLock => -209,
+            DropbearNativeError::JavaVMMethodNotFound(_) => -210,
+            DropbearNativeError::FieldAlreadySet(_) => -211,
+            DropbearNativeError::ThrowFailed(_) => -212,
+            DropbearNativeError::ParseFailed(_, _) => -213,
+            DropbearNativeError::JniCall(_) => -214,
+        }
+    }
 }
 
-impl Display for DropbearNativeError {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        f.write_str(match self {
-            DropbearNativeError::NullPointer => "NullPointer (-1)",
-            DropbearNativeError::QueryFailed => "QueryFailed (-2)",
-            DropbearNativeError::EntityNotFound => "EntityNotFound (-3)",
-            DropbearNativeError::NoSuchComponent => "NoSuchComponent (-4)",
-            DropbearNativeError::NoSuchEntity => "NoSuchEntity (-5)",
-            DropbearNativeError::WorldInsertError => "WorldInsertError (-6)",
-            DropbearNativeError::SendError => "SendError (-7)",
-            DropbearNativeError::InvalidUTF8 => "InvalidUTF8 (-108)",
-            DropbearNativeError::UnknownError => "UnknownError (-1274)",
-            DropbearNativeError::UnsignedGenericError => "UnsignedGenericError (65535)",
-            DropbearNativeError::Success => "Success (0) [should not be displayed]",
-            DropbearNativeError::CStringError => "CStringError (-8)",
-            DropbearNativeError::BufferTooSmall => "BufferTooSmall (-9)",
-            DropbearNativeError::PrematureSceneSwitch => "PrematureSceneSwitch (-10)",
-            DropbearNativeError::GamepadNotFound => "GamepadNotFound (-11)",
-            DropbearNativeError::InvalidArgument => "InvalidArgument (-12)",
-            DropbearNativeError::NoSuchHandle => "NoSuchHandle (-13)",
-            DropbearNativeError::JNIFailedToCreateObject => "JNIFailedToCreateObject (-14)",
-            DropbearNativeError::JNIFailedToGetField => "JNIFailedToGetField (-15)",
-            DropbearNativeError::JNIClassNotFound => "JNIClassNotFound (-16)",
-            DropbearNativeError::JNIMethodNotFound => "JNIMethodNotFound (-17)",
-            DropbearNativeError::JNIUnwrapFailed => "JNIUnwrapFailed (-18)",
-            DropbearNativeError::InvalidEntity => "InvalidEntity (-100)",
-            DropbearNativeError::GenericAssetError => "GenericAssetError (-19)",
-            DropbearNativeError::InvalidURI => "InvalidURI (-20)",
-            DropbearNativeError::AssetNotFound => "AssetNotFound (-21)",
-            DropbearNativeError::InvalidHandle => "InvalidHandle (-22)",
-            DropbearNativeError::GenericError => "GenericError (1)",
-            DropbearNativeError::PhysicsObjectNotFound => "PhysicsObjectNotFound (-23)",
-        })
+impl From<jni::errors::Error> for DropbearNativeError {
+    fn from(err: jni::errors::Error) -> Self {
+        match err {
+            jni::errors::Error::WrongJValueType(a, b) => DropbearNativeError::WrongJValueType(a, b),
+            jni::errors::Error::InvalidCtorReturn => DropbearNativeError::InvalidCtorReturn,
+            jni::errors::Error::InvalidArgList(s) => DropbearNativeError::InvalidArgList(s),
+            jni::errors::Error::MethodNotFound { name, sig } => DropbearNativeError::MethodNotFound { name, sig },
+            jni::errors::Error::FieldNotFound { name, sig } => DropbearNativeError::FieldNotFound { name, sig },
+            jni::errors::Error::JavaException => DropbearNativeError::JavaException,
+            jni::errors::Error::JNIEnvMethodNotFound(s) => DropbearNativeError::JNIEnvMethodNotFound(s),
+            jni::errors::Error::NullPtr(s) => DropbearNativeError::NullPtr(s),
+            jni::errors::Error::NullDeref(s) => DropbearNativeError::NullDeref(s),
+            jni::errors::Error::TryLock => DropbearNativeError::TryLock,
+            jni::errors::Error::JavaVMMethodNotFound(s) => DropbearNativeError::JavaVMMethodNotFound(s),
+            jni::errors::Error::FieldAlreadySet(s) => DropbearNativeError::FieldAlreadySet(s),
+            jni::errors::Error::ThrowFailed(i) => DropbearNativeError::ThrowFailed(i),
+            jni::errors::Error::ParseFailed(e, s) => DropbearNativeError::ParseFailed(e, s),
+            jni::errors::Error::JniCall(e) => DropbearNativeError::JniCall(e),
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index a0e0ad7..83200aa 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -257,6 +257,7 @@ pub enum EditorTab {
     ModelEntityList,   // right side,
     Viewport,          // middle,
     ErrorConsole,
+    Console,
     Plugin(usize),
 }
 
diff --git a/crates/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rs
new file mode 100644
index 0000000..608ee11
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui.rs
@@ -0,0 +1,192 @@
+// mod button;
+// mod utils;
+// mod text;
+// mod align;
+// mod checkbox;
+
+use std::any::Any;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::fmt::Debug;
+use ::jni::JNIEnv;
+use ::jni::objects::JObject;
+use parking_lot::Mutex;
+use serde::{Deserialize, Serialize};
+// use yakui::{Alignment, MainAxisSize, Yakui};
+use dropbear_engine::utils::ResourceReference;
+use dropbear_macro::SerializableComponent;
+use dropbear_traits::SerializableComponent;
+use crate::scripting::jni::utils::{FromJObject};
+use crate::scripting::result::DropbearNativeResult;
+// use crate::ui::align::{AlignParser};
+// use crate::ui::button::ButtonParser;
+// use crate::ui::checkbox::CheckboxParser;
+// use crate::ui::text::TextParser;
+
+thread_local! {
+    pub static UI_CONTEXT: RefCell<UiContext> = RefCell::new(UiContext::new());
+}
+
+/// A component that can be attached to an entity that renders UI for the entire scene.
+///
+/// This UI is used in tandem with a `.kts` (Kotlin Script file) with the dropbear-engine scripting
+/// ui DSL.
+#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent, Default)]
+pub struct UIComponent {
+    /// Does not render the UI file.
+    pub disabled: bool,
+    /// The reference to the script file.
+    pub ui_file: ResourceReference,
+}
+
+#[derive(Clone, Copy, Debug, Default)]
+pub struct WidgetState {
+    pub clicked: bool,
+    pub hovering: bool,
+    pub checked: bool,
+}
+
+pub trait WidgetParser: Send + Sync {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>>;
+    fn name(&self) -> String;
+}
+
+pub struct UiContext {
+    // pub yakui_state: Mutex<Yakui>,
+    pub instruction_set: Mutex<Vec<UiInstructionType>>,
+    pub widget_states: Mutex<HashMap<i64, WidgetState>>,
+    pub parsers: Mutex<Vec<Box<dyn WidgetParser>>>,
+}
+
+pub fn poll() {
+    UI_CONTEXT.with(|v| {
+        let ctx = v.borrow();
+        // let mut instructions = ctx.instruction_set.lock();
+        let mut widget_states = ctx.widget_states.lock();
+
+        widget_states.clear();
+
+        // let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>();
+
+        // let tree = build_tree(current_instructions);
+
+        // yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| {
+        //     yakui::widgets::List::column()
+        //         .main_axis_size(MainAxisSize::Max)
+        //         .show(|| {
+        //             render_tree(tree, &mut widget_states);
+        //         });
+        // });
+    });
+}
+
+#[derive(Debug)]
+pub enum UiInstructionType {
+    Containered(ContaineredWidgetType),
+    Widget(Box<dyn NativeWidget>),
+}
+
+#[derive(Debug)]
+pub enum ContaineredWidgetType {
+    Start {
+        id: i64,
+        widget: Box<dyn ContaineredWidget>,
+    },
+    End {
+        id: i64,
+    }
+}
+
+pub trait NativeWidget: Send + Sync + Debug {
+    fn render(self: Box<Self>, state: &mut HashMap<i64, WidgetState>);
+    fn id(&self) -> i64;
+    fn as_any(&self) -> &dyn Any;
+}
+
+pub trait ContaineredWidget: Send + Sync + Debug {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut HashMap<i64, WidgetState>);
+    fn as_any(&self) -> &dyn Any;
+}
+
+pub struct UiNode {
+    pub instruction: UiInstructionType,
+    pub children: Vec<UiNode>,
+}
+
+impl UiContext {
+    pub fn new() -> Self {
+        let mut parsers: Vec<Box<dyn WidgetParser>> = Vec::new();
+
+        // parsers.push(Box::new(ButtonParser));
+        // parsers.push(Box::new(TextParser));
+        // parsers.push(Box::new(AlignParser));
+        // parsers.push(Box::new(CheckboxParser));
+
+        // let yakui = Yakui::new();
+
+        Self {
+            // yakui_state: Mutex::new(yakui),
+            instruction_set: Default::default(),
+            widget_states: Default::default(),
+            parsers: Mutex::new(parsers),
+        }
+    }
+}
+
+pub trait UiWidgetType: FromJObject {
+    type UIWidgetType;
+
+    fn as_id(&self) -> u32;
+    fn from_id(id: u32) -> Self::UIWidgetType;
+}
+
+pub mod jni {
+    #![allow(non_snake_case)]
+
+    use jni::sys::{jlong};
+    use jni::objects::{JClass, JObjectArray};
+    use jni::JNIEnv;
+    use crate::convert_ptr;
+    use crate::ui::{UiContext};
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_UINative_renderUI(
+        mut env: JNIEnv,
+        _class: JClass,
+        ui_buf_ptr: jlong,
+        instructions: JObjectArray,
+    ) {
+        // println!("[Java_com_dropbear_ui_UINative_renderUI] received new renderUI request");
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+        let mut rust_instructions = Vec::new();
+
+        let count = env.get_array_length(&instructions).unwrap_or(0);
+        let parsers_guard = ui.parsers.lock();
+
+        for i in 0..count {
+            let obj = match env.get_object_array_element(&instructions, i) {
+                Ok(o) => o,
+                Err(_) => continue,
+            };
+            if obj.is_null() { println!("[Java_com_dropbear_ui_UINative_renderUI] obj is null at index {}", i); continue; }
+
+            for parser in parsers_guard.iter() {
+                match parser.parse(&mut env, &obj) {
+                    Ok(Some(widget)) => {
+                        // println!("Received widget: {:?}", widget);
+                        rust_instructions.push(widget);
+                        break;
+                    },
+                    Ok(None) => continue,
+                    Err(e) => {
+                        eprintln!("[Java_com_dropbear_ui_UINative_renderUI] Error converting UI instruction: {:?}", e);
+                    }
+                }
+            }
+        }
+
+        let mut instruction_set = ui.instruction_set.lock();
+        instruction_set.clear();
+        instruction_set.extend(rust_instructions);
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/align.rs b/crates/eucalyptus-core/src/ui/align.rs
new file mode 100644
index 0000000..51fc973
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/align.rs
@@ -0,0 +1,81 @@
+use std::any::Any;
+use std::collections::HashMap;
+use jni::JNIEnv;
+use jni::objects::JObject;
+use yakui::Alignment;
+use crate::scripting::jni::utils::FromJObject;
+use crate::scripting::result::DropbearNativeResult;
+use crate::ui::{ContaineredWidget, ContaineredWidgetType, UiInstructionType, UiNode, WidgetParser, WidgetState};
+
+pub(crate) struct AlignParser;
+
+#[derive(Debug, Clone)]
+pub(crate) struct AlignWidget {
+    pub alignment: Alignment,
+}
+
+impl WidgetParser for AlignParser {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
+        let class = env.get_object_class(obj)?;
+        let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
+        let name_string: String = env.get_string(&name_str_obj.into())?.into();
+
+        if name_string.contains("AlignmentInstruction$StartAlignmentBlock") {
+            let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/widgets/Align;")?.l()?;
+            let align = yakui::widgets::Align::from_jobject(env, &align_obj)?;
+
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Containered(
+                ContaineredWidgetType::Start {
+                    id,
+                    widget: Box::new(AlignWidget {
+                        alignment: align.alignment,
+                    }),
+                }
+            )));
+        }
+
+        if name_string.contains("AlignmentInstruction$EndAlignmentBlock") {
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Containered(
+                ContaineredWidgetType::End { id }
+            )));
+        }
+
+        Ok(None)
+    }
+
+    fn name(&self) -> String {
+        String::from("AlignParser")
+    }
+}
+
+impl ContaineredWidget for AlignWidget {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut HashMap<i64, WidgetState>) {
+        yakui::widgets::Align::new(self.alignment).show(|| {
+            super::render_tree(children, state);
+        });
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+impl FromJObject for yakui::widgets::Align {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized
+    {
+        let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/styling/Alignment;")?.l()?;
+        let alignment = Alignment::from_jobject(env, &align_obj)?;
+
+        Ok(Self {
+            alignment,
+        })
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/button.rs b/crates/eucalyptus-core/src/ui/button.rs
new file mode 100644
index 0000000..f4bb622
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/button.rs
@@ -0,0 +1,134 @@
+use std::any::Any;
+use std::collections::HashMap;
+use yakui::{Alignment, BorderRadius};
+use yakui::widgets::{Button, Pad, DynamicButtonStyle};
+use crate::scripting::jni::utils::FromJObject;
+use crate::scripting::result::DropbearNativeResult;
+use std::borrow::Cow;
+use ::jni::JNIEnv;
+use ::jni::objects::JObject;
+use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState};
+
+pub(crate) struct ButtonParser;
+
+#[derive(Debug)]
+pub(crate) struct ButtonWidget {
+    pub id: i64,
+    pub button: yakui::widgets::Button,
+}
+
+impl WidgetParser for ButtonParser {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
+        let class = env.get_object_class(obj)?;
+        let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
+        let name_string: String = env.get_string(&name_str_obj.into())?.into();
+
+        if name_string.contains("ButtonInstruction$Button") {
+            let button_obj = env.get_field(obj, "button", "Lcom/dropbear/ui/widgets/Button;")?.l()?;
+            let button = yakui::widgets::Button::from_jobject(env, &button_obj)?;
+
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Widget(Box::new(ButtonWidget {
+                id,
+                button,
+            }))));
+        }
+
+        Ok(None)
+    }
+
+    fn name(&self) -> String {
+        String::from("ButtonParser")
+    }
+}
+
+impl NativeWidget for ButtonWidget {
+    fn render(self: Box<Self>, states: &mut HashMap<i64, WidgetState>) {
+        let res = self.button.show();
+        states.insert(self.id, WidgetState {
+            clicked: res.clicked,
+            hovering: res.hovering,
+            checked: false, // always be false because it is not a checkbox, obv
+        });
+    }
+
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+impl FromJObject for Button {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let text_field = env.get_field(obj, "text", "Ljava/lang/String;")?;
+        let text_jstring = text_field.l()?.into();
+        let text: String = env.get_string(&text_jstring)?.into();
+
+        let f = env.get_field(obj, "alignment", "Lcom/dropbear/ui/styling/Alignment;")?.l()?;
+        let alignment = Alignment::from_jobject(env, &f)?;
+
+        let f = env.get_field(obj, "padding", "Lcom/dropbear/ui/styling/Padding;")?.l()?;
+        let padding = Pad::from_jobject(env, &f)?;
+
+        let f = env.get_field(obj, "borderRadius", "Lcom/dropbear/ui/styling/BorderRadius;")?.l()?;
+        let border_radius = BorderRadius::from_jobject(env, &f)?;
+
+        let f = env.get_field(obj, "style", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?;
+        let style = DynamicButtonStyle::from_jobject(env, &f)?;
+
+        let f = env.get_field(obj, "hoverStyle", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?;
+        let hover_style = DynamicButtonStyle::from_jobject(env, &f)?;
+
+        let f = env.get_field(obj, "downStyle", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?;
+        let down_style = DynamicButtonStyle::from_jobject(env, &f)?;
+
+        Ok(Self {
+            text: Cow::Owned(text),
+            alignment,
+            padding,
+            border_radius,
+            style,
+            hover_style,
+            down_style,
+        })
+    }
+}
+
+pub mod jni {
+    #![allow(non_snake_case)]
+    
+    use jni::JNIEnv;
+    use jni::objects::JClass;
+    use jni::sys::{jboolean, jlong};
+    use crate::convert_ptr;
+    use crate::ui::UiContext;
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getClicked(
+        _env: JNIEnv,
+        _class: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+        let states = ui.widget_states.lock();
+        states.get(&id).map(|s| s.clicked).unwrap_or(false).into()
+    }
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getHovering(
+        _env: JNIEnv,
+        _class: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+        let states = ui.widget_states.lock();
+        states.get(&id).map(|s| s.hovering).unwrap_or(false).into()
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/checkbox.rs b/crates/eucalyptus-core/src/ui/checkbox.rs
new file mode 100644
index 0000000..17ff8ec
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/checkbox.rs
@@ -0,0 +1,99 @@
+use std::any::Any;
+use std::collections::HashMap;
+use ::jni::JNIEnv;
+use ::jni::objects::JObject;
+use yakui::widgets::Checkbox;
+use crate::scripting::result::DropbearNativeResult;
+use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState};
+
+pub(crate) struct CheckboxParser;
+
+#[derive(Debug)]
+pub(crate) struct CheckboxWidget {
+    pub id: i64,
+    pub checkbox: Checkbox,
+}
+
+impl WidgetParser for CheckboxParser {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
+        let class = env.get_object_class(obj)?;
+        let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
+        let name_string: String = env.get_string(&name_str_obj.into())?.into();
+
+        if name_string.contains("CheckboxInstruction$Checkbox") {
+            let checked = env.get_field(obj, "checked", "Z")?.z()?;
+            let checkbox = Checkbox::new(checked);
+
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Widget(Box::new(CheckboxWidget {
+                id,
+                checkbox,
+            }))));
+        }
+
+        Ok(None)
+    }
+
+    fn name(&self) -> String {
+        String::from("CheckboxParser")
+    }
+}
+
+impl NativeWidget for CheckboxWidget {
+    fn render(self: Box<Self>, state: &mut HashMap<i64, WidgetState>) {
+        let resp = self.checkbox.show();
+        state.insert(self.id, WidgetState {
+            clicked: false,
+            hovering: false,
+            checked: resp.checked,
+        });
+
+    }
+
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+pub mod jni {
+    #![allow(non_snake_case)]
+
+    use jni::JNIEnv;
+    use jni::objects::JClass;
+    use jni::sys::{jboolean, jlong};
+    use crate::convert_ptr;
+    use crate::ui::UiContext;
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_CheckboxNative_getChecked(
+        _env: JNIEnv,
+        _: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+
+        if let Some(v) = ui.widget_states.lock().get(&(id as i64)) {
+            return v.checked.into();
+        }
+        false.into()
+    }
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_CheckboxNative_hasCheckedState(
+        _env: JNIEnv,
+        _: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+
+        ui.widget_states.lock().contains_key(&(id as i64)).into()
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/text.rs b/crates/eucalyptus-core/src/ui/text.rs
new file mode 100644
index 0000000..1c5fce3
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/text.rs
@@ -0,0 +1,86 @@
+use std::any::Any;
+use std::borrow::Cow;
+use std::collections::HashMap;
+use jni::JNIEnv;
+use jni::objects::JObject;
+use yakui::style::TextStyle;
+use yakui::widgets::Pad;
+use crate::scripting::jni::utils::FromJObject;
+use crate::scripting::result::DropbearNativeResult;
+use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState};
+
+pub(crate) struct TextParser;
+
+#[derive(Debug)]
+pub(crate) struct TextWidget {
+    pub id: i64,
+    pub text: yakui::widgets::Text,
+}
+
+impl WidgetParser for TextParser {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
+        let class = env.get_object_class(obj)?;
+        let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
+        let name_string: String = env.get_string(&name_str_obj.into())?.into();
+        // println!("TextParser obj get_string result: {}", name_string);
+
+        if name_string.contains("TextInstruction$Text") {
+            let text_obj = env.get_field(obj, "text", "Lcom/dropbear/ui/widgets/Text;")?.l()?;
+            let text = yakui::widgets::Text::from_jobject(env, &text_obj)?;
+
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Widget(Box::new(TextWidget {
+                id,
+                text,
+            }))))
+        }
+
+        Ok(None)
+    }
+
+    fn name(&self) -> String {
+        String::from("TextParser")
+    }
+}
+
+impl NativeWidget for TextWidget {
+    fn render(self: Box<Self>, _state: &mut HashMap<i64, WidgetState>) {
+        let _ = self.text.show(); // no response
+    }
+
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+impl FromJObject for yakui::widgets::Text {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized
+    {
+        let text_field = env.get_field(obj, "text", "Ljava/lang/String;")?;
+        let text_jstring = text_field.l()?.into();
+        let text: String = env.get_string(&text_jstring)?.into();
+
+        let style_obj = env.get_field(obj, "style", "Lcom/dropbear/ui/styling/TextStyle;")?.l()?;
+        let style = TextStyle::from_jobject(env, &style_obj)?;
+
+        let f = env.get_field(obj, "padding", "Lcom/dropbear/ui/styling/Padding;")?.l()?;
+        let padding = Pad::from_jobject(
+            env,
+            &f
+        )?;
+        
+        Ok(Self {
+            text: Cow::Owned(text),
+            style,
+            padding,
+        })
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/utils.rs b/crates/eucalyptus-core/src/ui/utils.rs
new file mode 100644
index 0000000..7067d9d
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/utils.rs
@@ -0,0 +1,323 @@
+use jni::JNIEnv;
+use jni::objects::{JObject, JByteArray};
+use crate::scripting::jni::utils::FromJObject;
+use crate::scripting::result::DropbearNativeResult;
+use yakui::{Border, Color};
+use yakui::widgets::{Pad, DynamicButtonStyle};
+use yakui::{Alignment, BorderRadius};
+use yakui::style::{TextStyle, TextAlignment};
+use yakui::cosmic_text::{Attrs, AttrsOwned, FamilyOwned, Weight, Style, Stretch, CacheKeyFlags, FontFeatures, Feature, FeatureTag, Metrics, CacheMetrics};
+
+impl FromJObject for FeatureTag {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let tag_obj = env.call_method(obj, "asBytes", "()[B", &[])?.l()?;
+        let tag_bytes = env.convert_byte_array(JByteArray::from(tag_obj))?;
+        
+        let mut fixed_tag = [0u8; 4];
+        let len = tag_bytes.len().min(4);
+        fixed_tag[..len].copy_from_slice(&tag_bytes[..len]);
+        
+        Ok(FeatureTag::new(&fixed_tag))
+    }
+}
+
+impl FromJObject for Feature {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let tag_obj = env.get_field(obj, "tag", "Lcom/dropbear/ui/styling/fonts/FeatureTag;")?.l()?;
+        let tag = FeatureTag::from_jobject(env, &tag_obj)?;
+        
+        let value_obj = env.get_field(obj, "value", "Lcom/dropbear/ui/styling/fonts/UInt;")?.l()?;
+        let value = env.get_field(&value_obj, "value", "I")?.i()? as u32;
+
+        Ok(Feature {
+            tag,
+            value,
+        })
+    }
+}
+
+impl FromJObject for FontFeatures {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let features_list_obj = env.get_field(obj, "features", "Ljava/util/List;")?.l()?;
+        
+        let size = env.call_method(&features_list_obj, "size", "()I", &[])?.i()?;
+        let mut features = Vec::with_capacity(size as usize);
+        
+        for i in 0..size {
+            let item = env.call_method(&features_list_obj, "get", "(I)Ljava/lang/Object;", &[i.into()])?.l()?;
+            if !item.is_null() {
+                features.push(Feature::from_jobject(env, &item)?);
+            }
+        }
+        
+        Ok(FontFeatures { features })
+    }
+}
+
+impl FromJObject for CacheMetrics {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let font_size = env.get_field(obj, "fontSize", "D")?.d()? as f32;
+        let line_height = env.get_field(obj, "lineHeight", "D")?.d()? as f32;
+
+        Ok(CacheMetrics::from(Metrics {
+            font_size,
+            line_height,
+        }))
+    }
+}
+
+impl FromJObject for Border {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let color_obj = env.get_field(obj, "colour", "Lcom/dropbear/utils/Colour;")?.l()?;
+        let color = Color::from_jobject(env, &color_obj)?;
+        let width = env.get_field(obj, "width", "D")?.d()? as f32;
+        Ok(Border {
+            color,
+            width,
+        })
+    }
+}
+
+impl FromJObject for Color {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let r = env.get_field(obj, "r", "B")?.b()? as u8;
+        let g = env.get_field(obj, "g", "B")?.b()? as u8;
+        let b = env.get_field(obj, "b", "B")?.b()? as u8;
+        let a = env.get_field(obj, "a", "B")?.b()? as u8;
+        Ok(Color::rgba(r, g, b, a))
+    }
+}
+
+impl FromJObject for Pad {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let left = env.get_field(obj, "left", "D")?.d()? as f32;
+        let right = env.get_field(obj, "right", "D")?.d()? as f32;
+        let top = env.get_field(obj, "top", "D")?.d()? as f32;
+        let bottom = env.get_field(obj, "bottom", "D")?.d()? as f32;
+        Ok(Pad {
+            left,
+            right,
+            top,
+            bottom,
+        })
+    }
+}
+
+impl FromJObject for BorderRadius {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let top_left = env.get_field(obj, "topLeft", "D")?.d()? as f32;
+        let top_right = env.get_field(obj, "topRight", "D")?.d()? as f32;
+        let bottom_left = env.get_field(obj, "bottomLeft", "D")?.d()? as f32;
+        let bottom_right = env.get_field(obj, "bottomRight", "D")?.d()? as f32;
+        Ok(BorderRadius {
+            top_left,
+            top_right,
+            bottom_left,
+            bottom_right,
+        })
+    }
+}
+
+impl FromJObject for Alignment {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let x = env.get_field(obj, "x", "D")?.d()? as f32;
+        let y = env.get_field(obj, "y", "D")?.d()? as f32;
+        Ok(Alignment::new(x, y))
+    }
+}
+
+impl FromJObject for TextAlignment {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let name_obj = env.call_method(obj, "name", "()Ljava/lang/String;", &[])?.l()?;
+        let name: String = env.get_string(&name_obj.into())?.into();
+        match name.as_str() {
+            "Start" => Ok(TextAlignment::Start),
+            "Center" => Ok(TextAlignment::Center),
+            "End" => Ok(TextAlignment::End),
+            _ => Ok(TextAlignment::Start),
+        }
+    }
+}
+
+impl FromJObject for Weight {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let val = env.get_field(obj, "value", "I")?.i()? as u16;
+        Ok(Weight(val))
+    }
+}
+
+impl FromJObject for Style {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let name_obj = env.call_method(obj, "name", "()Ljava/lang/String;", &[])?.l()?;
+        let name: String = env.get_string(&name_obj.into())?.into();
+        match name.as_str() {
+            "Normal" => Ok(Style::Normal),
+            "Italic" => Ok(Style::Italic),
+            "Oblique" => Ok(Style::Oblique),
+            _ => Ok(Style::Normal),
+        }
+    }
+}
+
+impl FromJObject for Stretch {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let name_obj = env.call_method(obj, "name", "()Ljava/lang/String;", &[])?.l()?;
+        let name: String = env.get_string(&name_obj.into())?.into();
+        match name.as_str() {
+            "UltraCondensed" => Ok(Stretch::UltraCondensed),
+            "ExtraCondensed" => Ok(Stretch::ExtraCondensed),
+            "Condensed" => Ok(Stretch::Condensed),
+            "SemiCondensed" => Ok(Stretch::SemiCondensed),
+            "Normal" => Ok(Stretch::Normal),
+            "SemiExpanded" => Ok(Stretch::SemiExpanded),
+            "Expanded" => Ok(Stretch::Expanded),
+            "ExtraExpanded" => Ok(Stretch::ExtraExpanded),
+            "UltraExpanded" => Ok(Stretch::UltraExpanded),
+            _ => Ok(Stretch::Normal),
+        }
+    }
+}
+
+impl FromJObject for FamilyOwned {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Name")? {
+            let value_obj = env.call_method(obj, "getValue", "()Ljava/lang/String;", &[])?.l()?;
+            let val: String = env.get_string(&value_obj.into())?.into();
+            return Ok(FamilyOwned::Name(val.parse().unwrap()));
+        }
+        if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Serif")? { return Ok(FamilyOwned::Serif); }
+        if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$SansSerif")? { return Ok(FamilyOwned::SansSerif); }
+        if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Cursive")? { return Ok(FamilyOwned::Cursive); }
+        if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Fantasy")? { return Ok(FamilyOwned::Fantasy); }
+        if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Monospace")? { return Ok(FamilyOwned::Monospace); }
+        
+        Ok(FamilyOwned::SansSerif)
+    }
+}
+
+impl FromJObject for AttrsOwned {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let family_obj = env.get_field(obj, "family", "Lcom/dropbear/ui/styling/fonts/Family;")?.l()?;
+        let family_owned = FamilyOwned::from_jobject(env, &family_obj)?;
+        let family = family_owned.as_family();
+        
+        let color_obj = env.get_field(obj, "colourOptions", "Lcom/dropbear/utils/Colour;")?.l()?;
+        let color_opt = if !color_obj.is_null() {
+            let colour = Color::from_jobject(env, &color_obj)?;
+             Some(yakui::cosmic_text::Color::rgba(colour.r, colour.g, colour.b, colour.a))
+        } else {
+             None
+        };
+        
+        let stretch_obj = env.get_field(obj, "stretch", "Lcom/dropbear/ui/styling/fonts/Stretch;")?.l()?;
+        let stretch = Stretch::from_jobject(env, &stretch_obj)?;
+        
+        let style_obj = env.get_field(obj, "style", "Lcom/dropbear/ui/styling/fonts/FontStyle;")?.l()?;
+        let style = Style::from_jobject(env, &style_obj)?;
+        
+        let weight_obj = env.get_field(obj, "weight", "Lcom/dropbear/ui/styling/fonts/FontWeight;")?.l()?;
+        let weight = Weight::from_jobject(env, &weight_obj)?;
+        
+        let metadata = match env.get_field(obj, "metadata", "I") {
+             Ok(val) => val.i()? as usize,
+             Err(_) => 0, 
+        };
+
+        let letter_spacing_obj = env.get_field(obj, "letterSpacingOptions", "Ljava/lang/Double;")?.l()?;
+        let letter_spacing_opt = if !letter_spacing_obj.is_null() {
+             let val = env.call_method(&letter_spacing_obj, "doubleValue", "()D", &[])?.d()?;
+             Some(yakui::cosmic_text::LetterSpacing(val as f32))
+        } else {
+             None
+        };
+
+        let font_features_obj = env.get_field(obj, "fontFeatures", "Lcom/dropbear/ui/styling/fonts/FontFeatures;")?.l()?;
+        let font_features = if !font_features_obj.is_null() {
+            FontFeatures::from_jobject(env, &font_features_obj)?
+        } else {
+            FontFeatures::default()
+        };
+
+        let cache_key_flags_val = env.get_field(obj, "cacheKeyFlags", "I")?.i()? as u32;
+        let cache_key_flags = CacheKeyFlags::from_bits_truncate(cache_key_flags_val);
+        
+        let metrics_obj = env.get_field(obj, "metricsOptions", "Lcom/dropbear/ui/styling/fonts/CacheMetrics;")?.l()?;
+        let metrics_opt = if !metrics_obj.is_null() {
+            Some(CacheMetrics::from_jobject(env, &metrics_obj)?)
+        } else {
+            None
+        };
+
+        Ok(AttrsOwned::new(&Attrs {
+            family,
+            stretch,
+            style,
+            weight,
+            metadata,
+            color_opt,
+            cache_key_flags,
+            metrics_opt,
+            letter_spacing_opt,
+            font_features,
+        }))
+    }
+}
+
+impl FromJObject for TextStyle {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/styling/fonts/TextAlignment;")?.l()?;
+        let align = TextAlignment::from_jobject(env, &align_obj)?;
+
+        let font_size = env.get_field(obj, "fontSize", "D")?.d()? as f32;
+        
+        let color_obj = env.get_field(obj, "colour", "Lcom/dropbear/utils/Colour;")?.l()?;
+        let color = if !color_obj.is_null() {
+            Color::from_jobject(env, &color_obj)?
+        } else {
+            Color::WHITE
+        };
+
+        let attrs_obj = env.get_field(obj, "attrs", "Lcom/dropbear/ui/styling/fonts/FontAttributes;")?.l()?;
+        let attrs = AttrsOwned::from_jobject(env, &attrs_obj)?;
+
+        Ok(TextStyle {
+            align,
+            font_size,
+            line_height_override: None,
+            color,
+            attrs,
+        })
+    }
+}
+
+impl FromJObject for DynamicButtonStyle {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let text_obj = env.get_field(obj, "text", "Lcom/dropbear/ui/styling/TextStyle;")?.l()?;
+        let text = if !text_obj.is_null() {
+            TextStyle::from_jobject(env, &text_obj)?
+        } else {
+            println!("Text is null, setting to default");
+            TextStyle::default()
+        };
+
+        let fill_obj = env.get_field(obj, "fill", "Lcom/dropbear/utils/Colour;")?.l()?;
+        let fill = if !fill_obj.is_null() {
+            Color::from_jobject(env, &fill_obj)?
+        } else {
+            Color::GRAY
+        };
+
+        let border_obj = env.get_field(obj, "border", "Lcom/dropbear/ui/styling/Border;")?.l()?;
+        let border = if !border_obj.is_null() {
+            Some(Border::from_jobject(env, &border_obj)?)
+        } else {
+            None
+        };
+
+        Ok(DynamicButtonStyle {
+            text,
+            fill,
+            border,
+        })
+    }
+}
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 9a8dab0..0ceedb4 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -1,6 +1,7 @@
 //! Utility functions and helpers
 
 pub mod option;
+pub mod hashmap;
 
 use crate::states::Node;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca};
diff --git a/crates/eucalyptus-core/src/utils/hashmap.rs b/crates/eucalyptus-core/src/utils/hashmap.rs
new file mode 100644
index 0000000..cd0c7c4
--- /dev/null
+++ b/crates/eucalyptus-core/src/utils/hashmap.rs
@@ -0,0 +1,247 @@
+use std::collections::HashMap;
+
+/// A wrapper for a [HashMap] that iterates its generation for each access. It is able to check
+/// for any stale values, and remove them in [StaleTracker::remove_stale].
+///
+/// # Types
+/// * `K` - The key type, must implement `Eq` and `Hash`
+/// * `V` - The value type
+///
+/// # Examples
+///
+/// ```
+/// let mut tracker = eucalyptus_core::utils::hashmap::StaleTracker::new();
+///
+/// // Insert some values
+/// tracker.insert("session_1", "user_data");
+/// tracker.insert("session_2", "other_data");
+///
+/// // Access session_1 to keep it fresh
+/// tracker.get(&"session_1");
+///
+/// // Advance time
+/// tracker.tick();
+///
+/// // session_2 hasn't been accessed, remove entries older than 0 generations
+/// let removed = tracker.remove_stale(0);
+/// assert_eq!(removed, vec!["session_2"]);
+/// ```
+pub struct StaleTracker<K, V> {
+    map: HashMap<K, (V, usize)>, // (value, last_access_generation)
+    current_generation: usize,
+}
+
+impl<K: Eq + std::hash::Hash, V> StaleTracker<K, V> {
+    /// Creates a new empty `StaleTracker`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let tracker: StaleTracker<String, i32> = StaleTracker::new();
+    /// ```
+    pub fn new() -> Self {
+        Self {
+            map: HashMap::new(),
+            current_generation: 0,
+        }
+    }
+
+    /// Inserts a key-value pair into the tracker.
+    ///
+    /// The entry is marked as accessed at the current generation. If the key already
+    /// exists, the old value is replaced and its access time is reset to the current generation.
+    ///
+    /// # Arguments
+    ///
+    /// * `key` - The key to insert
+    /// * `value` - The value to associate with the key
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    /// tracker.insert("key", 42);
+    /// ```
+    pub fn insert(&mut self, key: K, value: V) {
+        self.map.insert(key, (value, self.current_generation));
+    }
+
+    /// Gets a reference to the value associated with the key.
+    ///
+    /// This method marks the entry as accessed at the current generation, preventing it
+    /// from being considered stale. Returns `None` if the key doesn't exist.
+    ///
+    /// # Arguments
+    ///
+    /// * `key` - The key to look up
+    ///
+    /// # Returns
+    ///
+    /// * `Some(&V)` if the key exists
+    /// * `None` if the key doesn't exist
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    /// tracker.insert("key", 42);
+    ///
+    /// assert_eq!(tracker.get(&"key"), Some(&42));
+    /// assert_eq!(tracker.get(&"missing"), None);
+    /// ```
+    pub fn get(&mut self, key: &K) -> Option<&V> {
+        self.map.get_mut(key).map(|(value, generation)| {
+            *generation = self.current_generation;
+            &*value
+        })
+    }
+
+    /// Gets a mutable reference to the value associated with the key.
+    ///
+    /// This method marks the entry as accessed at the current generation. Returns `None`
+    /// if the key doesn't exist.
+    ///
+    /// # Arguments
+    ///
+    /// * `key` - The key to look up
+    ///
+    /// # Returns
+    ///
+    /// * `Some(&mut V)` if the key exists
+    /// * `None` if the key doesn't exist
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    /// tracker.insert("counter", 0);
+    ///
+    /// if let Some(value) = tracker.get_mut(&"counter") {
+    ///     *value += 1;
+    /// }
+    /// ```
+    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
+        self.map.get_mut(key).map(|(value, generation)| {
+            *generation = self.current_generation;
+            value
+        })
+    }
+
+    /// Advances the generation counter by one.
+    ///
+    /// Call this method periodically (e.g., once per frame, once per request, etc.) to
+    /// mark a new time period. Entries that aren't accessed after calling `tick()` will
+    /// age and eventually become stale.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    /// tracker.insert("key", 42);
+    ///
+    /// // Simulate passage of time
+    /// tracker.tick();
+    /// tracker.tick();
+    ///
+    /// // "key" is now 2 generations old (hasn't been accessed since insertion)
+    /// ```
+    pub fn tick(&mut self) {
+        self.current_generation += 1;
+    }
+
+    /// Removes and returns all entries that haven't been accessed within `max_age` generations.
+    ///
+    /// An entry is considered stale if `(current_generation - last_access_generation) > max_age`.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_age` - Maximum number of generations an entry can go without access before removal
+    ///
+    /// # Returns
+    ///
+    /// A vector of keys that were removed
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    ///
+    /// tracker.insert("fresh", 1);
+    /// tracker.insert("stale", 2);
+    ///
+    /// tracker.get(&"fresh"); // Access this one
+    /// tracker.tick();        // Advance generation
+    /// tracker.tick();        // Advance again
+    ///
+    /// // "stale" hasn't been accessed in 2 generations
+    /// let removed = tracker.remove_stale(1);
+    /// assert!(removed.contains(&"stale"));
+    /// assert!(!removed.contains(&"fresh"));
+    /// ```
+    pub fn remove_stale(&mut self, max_age: usize) -> Vec<K>
+    where K: Clone
+    {
+        let current = self.current_generation;
+        let stale_keys: Vec<K> = self.map
+            .iter()
+            .filter(|(_, (_, generation))| current - generation > max_age)
+            .map(|(k, _)| k.clone())
+            .collect();
+
+        for key in &stale_keys {
+            self.map.remove(key);
+        }
+
+        stale_keys
+    }
+
+    /// Returns the number of entries currently in the tracker.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    /// assert_eq!(tracker.len(), 0);
+    ///
+    /// tracker.insert("key", 42);
+    /// assert_eq!(tracker.len(), 1);
+    /// ```
+    pub fn len(&self) -> usize {
+        self.map.len()
+    }
+
+    /// Returns `true` if the tracker contains no entries.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let tracker: StaleTracker<String, i32> = StaleTracker::new();
+    /// assert!(tracker.is_empty());
+    /// ```
+    pub fn is_empty(&self) -> bool {
+        self.map.is_empty()
+    }
+
+    /// Returns the current generation number.
+    ///
+    /// This can be useful for debugging or understanding how much time has passed.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let mut tracker = StaleTracker::new();
+    /// assert_eq!(tracker.current_generation(), 0);
+    ///
+    /// tracker.tick();
+    /// assert_eq!(tracker.current_generation(), 1);
+    /// ```
+    pub fn current_generation(&self) -> usize {
+        self.current_generation
+    }
+}
+
+impl<K: Eq + std::hash::Hash, V> Default for StaleTracker<K, V> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/Cargo.toml b/crates/eucalyptus-editor/Cargo.toml
index b43f599..19105ae 100644
--- a/crates/eucalyptus-editor/Cargo.toml
+++ b/crates/eucalyptus-editor/Cargo.toml
@@ -11,6 +11,7 @@ readme = "README.md"
 eucalyptus-core = { path = "../eucalyptus-core", features = ["editor"] }
 magna-carta = { path = "../magna-carta" }
 redback-runtime = { path = "../redback-runtime", features = ["debug"]}
+kino-ui = { path = "../kino-ui" }
 
 anyhow.workspace = true
 app_dirs2.workspace = true
@@ -28,7 +29,7 @@ glam.workspace = true
 hecs.workspace = true
 log.workspace = true
 log-once.workspace = true
-model_to_image.workspace = true
+#model_to_image.workspace = true
 parking_lot.workspace = true
 transform-gizmo-egui.workspace = true
 wgpu.workspace = true
diff --git a/crates/eucalyptus-editor/README.md b/crates/eucalyptus-editor/README.md
index 4d2c94f..d62f03c 100644
--- a/crates/eucalyptus-editor/README.md
+++ b/crates/eucalyptus-editor/README.md
@@ -5,8 +5,7 @@ supported.
 
 ## Other functions
 
-To run a local play test, run `eucalyptus-editor.exe play {project_dir}` with optional jvm args specified through
-`--jvm-args {jvm_args}`. 
+To run a local play test, run `eucalyptus-editor.exe play {project_dir}`. 
 
-eucalyptus-editor does not have a scripting interface, therefore can be run without the Java Virtual Machine. If you do
+eucalyptus-editor (the editor itself) does not have a scripting interface, therefore can be run without the Java Virtual Machine. If you do
 wish to enter into "Play Mode" or start testing your game, you will require a form of Java Development Kit (JDK).
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/build.rs b/crates/eucalyptus-editor/src/build.rs
index e86eb37..a6ff345 100644
--- a/crates/eucalyptus-editor/src/build.rs
+++ b/crates/eucalyptus-editor/src/build.rs
@@ -76,8 +76,7 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> {
 
     // export to .eupak
     let eupak_path = build_dir.join("data.eupak");
-    const SIZE_OF_RUNTIME_PROJECT_CONFIG: usize = size_of::<RuntimeProjectConfig>();
-    let config_bytes = postcard::to_vec::<RuntimeProjectConfig, SIZE_OF_RUNTIME_PROJECT_CONFIG>(&runtime_config)?;
+    let config_bytes = postcard::to_stdvec::<RuntimeProjectConfig>(&runtime_config)?;
     fs::write(&eupak_path, config_bytes)?;
     log::debug!("Exported scene config to {:?}", eupak_path);
 
diff --git a/crates/eucalyptus-editor/src/camera.rs b/crates/eucalyptus-editor/src/camera.rs
index 1d54411..8e53574 100644
--- a/crates/eucalyptus-editor/src/camera.rs
+++ b/crates/eucalyptus-editor/src/camera.rs
@@ -56,8 +56,7 @@ impl InspectableComponent for CameraComponent {
                 ui.label("Speed:");
                 ui.add(
                     egui::DragValue::new(&mut self.settings.speed)
-                        .speed(0.1)
-                        .range(0.1..=20.0),
+                        .speed(0.1),
                 );
             });
 
@@ -66,7 +65,7 @@ impl InspectableComponent for CameraComponent {
                 ui.add(
                     egui::DragValue::new(&mut self.settings.sensitivity)
                         .speed(0.0001)
-                        .range(0.0001..=1.0),
+                        .range(0.0001..=1.5),
                 );
             });
 
diff --git a/crates/eucalyptus-editor/src/editor/component.rs b/crates/eucalyptus-editor/src/editor/component.rs
index cdc18c6..0616ce2 100644
--- a/crates/eucalyptus-editor/src/editor/component.rs
+++ b/crates/eucalyptus-editor/src/editor/component.rs
@@ -6,7 +6,7 @@ use dropbear_engine::attenuation::ATTENUATION_PRESETS;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
 use dropbear_engine::lighting::LightType;
 use dropbear_engine::texture::TextureWrapMode;
-use dropbear_engine::utils::ResourceReferenceType;
+use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
 use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui, UiBuilder};
 use eucalyptus_core::camera::CameraType;
 use eucalyptus_core::states::{Camera3D, Light, Property, Script};
@@ -14,7 +14,9 @@ use eucalyptus_core::{fatal, warn};
 use glam::{DVec3, Vec3};
 use hecs::Entity;
 use std::time::Instant;
+use dropbear_engine::procedural::ProcObj;
 use eucalyptus_core::properties::{CustomProperties, Value};
+use eucalyptus_core::ui::UIComponent;
 
 /// A trait that can added to any component that allows you to inspect the value in the editor.
 pub trait InspectableComponent {
@@ -479,7 +481,7 @@ fn inspect_light_transform(
 
         if ui.button("Reset Rotation").clicked() {
             transform.rotation = glam::DQuat::IDENTITY;
-            cfg.transform_rotation_cache.insert(*entity, DVec3::ZERO);
+            cfg.transform_rotation_cache.insert(*entity, DVec3::NEG_Y);
         }
         ui.add_space(5.0);
     }
@@ -909,7 +911,7 @@ impl InspectableComponent for Script {
         _label: &mut String,
     ) {
         ui.vertical(|ui| {
-            CollapsingHeader::new("Tags")
+            CollapsingHeader::new("Logic")
                 .default_open(true)
                 .show(ui, |ui| {
                     let mut local_del: Option<usize> = None;
@@ -1008,8 +1010,12 @@ impl InspectableComponent for MeshRenderer {
 
         let model_reference = self.model().path.clone();
         let model_title = match &model_reference.ref_type {
-            ResourceReferenceType::Cuboid { .. } => "Cuboid".to_string(),
             ResourceReferenceType::Unassigned { .. } => "None".to_string(),
+            ResourceReferenceType::ProcObj(obj) => {
+                match obj {
+                    ProcObj::Cuboid { .. } => "Cuboid".to_string(),
+                }
+            }
             _ => self.handle().label.clone(),
         };
 
@@ -1084,7 +1090,7 @@ impl InspectableComponent for MeshRenderer {
 
                             if ui
                                 .selectable_label(
-                                    matches!(model_reference.ref_type, ResourceReferenceType::Cuboid { .. }),
+                                    matches!(model_reference.ref_type, ResourceReferenceType::ProcObj(ProcObj::Cuboid { .. })),
                                     "Cuboid",
                                 )
                                 .clicked()
@@ -1127,7 +1133,7 @@ impl InspectableComponent for MeshRenderer {
 
             if choose_proc_cuboid {
                 let default_size = match &model_reference.ref_type {
-                    ResourceReferenceType::Cuboid { size_bits } => [
+                    ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => [
                         f32::from_bits(size_bits[0]),
                         f32::from_bits(size_bits[1]),
                         f32::from_bits(size_bits[2]),
@@ -1169,7 +1175,7 @@ impl InspectableComponent for MeshRenderer {
                     }
                 }
 
-                if let ResourceReferenceType::Cuboid { size_bits } = &self.model().path.ref_type {
+                if let ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) = &self.model().path.ref_type {
                     let mut size = [
                         f32::from_bits(size_bits[0]),
                         f32::from_bits(size_bits[1]),
@@ -1544,3 +1550,66 @@ impl InspectableComponent for Camera3D {
         ui.separator();
     }
 }
+
+impl InspectableComponent for UIComponent {
+    fn inspect(
+        &mut self,
+        _entity: &mut Entity,
+        cfg: &mut StaticallyKept,
+        ui: &mut Ui,
+        _undo_stack: &mut Vec<UndoableAction>,
+        _signal: &mut Signal,
+        _label: &mut String,
+    ) {
+        ui.vertical(|ui| {
+            ui.checkbox(&mut self.disabled, "Disable rendering of UI");
+
+            ui.separator();
+
+            let (rect, response) = ui.allocate_exact_size(
+                egui::vec2(ui.available_width(), 72.0),
+                egui::Sense::click(),
+            );
+
+            let fill = if response.hovered() {
+                ui.visuals().widgets.hovered.bg_fill
+            } else {
+                ui.visuals().widgets.inactive.bg_fill
+            };
+
+            ui.painter().rect_filled(rect, 4.0, fill);
+            ui.painter().rect_stroke(
+                rect,
+                4.0,
+                ui.visuals().widgets.inactive.bg_stroke,
+                egui::StrokeKind::Inside,
+            );
+
+            let mut card_ui = ui.new_child(
+                UiBuilder::new()
+                    .layout(egui::Layout::centered_and_justified(egui::Direction::LeftToRight))
+                    .max_rect(rect),
+            );
+
+            if let Some(uri) = self.ui_file.as_uri() {
+                card_ui.label(uri);
+            } else {
+                card_ui.label("Drop .kts file here");
+            }
+
+            let pointer_released = ui.input(|i| i.pointer.any_released());
+            if pointer_released && response.hovered() {
+                if let Some(asset) = cfg.dragged_asset.clone() {
+                    if let Some(uri) = asset.path.as_uri() {
+                        if uri.ends_with(".kts") {
+                            if let Ok(new_ref) = ResourceReference::from_euca_uri(uri) {
+                                self.ui_file = new_ref;
+                            }
+                            cfg.dragged_asset = None;
+                        }
+                    }
+                }
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/console.rs b/crates/eucalyptus-editor/src/editor/console.rs
new file mode 100644
index 0000000..d8cabad
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/console.rs
@@ -0,0 +1,88 @@
+use std::io::{BufRead, BufReader};
+use std::net::{TcpListener, TcpStream};
+use std::sync::Arc;
+use parking_lot::Mutex;
+
+pub struct EucalyptusConsole {
+    pub buffer: Arc<Mutex<Vec<String>>>,
+    pub history: Vec<String>,
+
+    pub show_info: bool,
+    pub show_warning: bool,
+    pub show_error: bool,
+    pub show_debug: bool,
+    pub show_trace: bool,
+    pub auto_scroll: bool,
+}
+
+impl EucalyptusConsole {
+    /// Creates a new instance of a [EucalyptusConsole], and
+    pub fn new(port: Option<&str>) -> Self {
+        let result = Self {
+            buffer: Arc::new(Default::default()),
+            history: vec![],
+            show_info: true,
+            show_warning: true,
+            show_error: true,
+            show_debug: false,
+            show_trace: false,
+            auto_scroll: true,
+        };
+
+        let buf_clone = result.buffer.clone();
+        let addr = format!("127.0.0.1:{}", port.unwrap_or("56624"));
+        std::thread::spawn(move || {
+            let listener = TcpListener::bind(&addr).unwrap();
+
+            log::info!("eucalyptus-editor debug console started at {}", addr);
+
+            loop {
+                for stream in listener.incoming() {
+                    match stream {
+                        Ok(stream) => {
+                            let buf_clone = buf_clone.clone();
+                            std::thread::spawn(move || {
+                                EucalyptusConsole::handle_client(stream, buf_clone)
+                            });
+                        }
+                        Err(e) => {
+                            eprintln!("Connection failed: {}", e);
+                        }
+                    }
+                }
+            }
+        });
+
+        result
+    }
+
+    fn handle_client(stream: TcpStream, buf: Arc<Mutex<Vec<String>>>) {
+        let peer_addr = stream.peer_addr().unwrap();
+        println!("New connection from: {}", peer_addr);
+
+        let reader = BufReader::new(stream);
+
+        for line in reader.lines() {
+            match line {
+                Ok(text) => {
+                    buf.lock().push(text);
+                }
+                Err(e) => {
+                    buf.lock().push(format!("Error reading from {}: {}", peer_addr, e));
+                    break;
+                }
+            }
+        }
+
+        println!("Connection closed: {}", peer_addr);
+    }
+
+    /// Drains all from the thread-safe buffer and adds to the history, while returning that value.
+    ///
+    /// It is recommended to use this function.
+    pub fn take(&mut self) -> Vec<String> {
+        let buf = self.buffer.lock().drain(..).collect::<Vec<String>>();
+        buf.iter().for_each(|v| self.history.push(v.clone()));
+        buf
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/console_error.rs b/crates/eucalyptus-editor/src/editor/console_error.rs
index ed7362b..edf6c8f 100644
--- a/crates/eucalyptus-editor/src/editor/console_error.rs
+++ b/crates/eucalyptus-editor/src/editor/console_error.rs
@@ -1,6 +1,7 @@
 use std::path::PathBuf;
 
 pub enum ErrorLevel {
+    Info,
     Warn,
     Error,
 }
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index 04f0bb3..b09122e 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -24,7 +24,7 @@ use dropbear_engine::{
 use egui::{self, CollapsingHeader, Margin, RichText};
 use egui_dock::TabViewer;
 use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder};
-use eucalyptus_core::hierarchy::{Children, Hierarchy, Parent};
+use eucalyptus_core::{hierarchy::{Children, Hierarchy, Parent}, ui::UIComponent};
 use eucalyptus_core::states::{Label, Light, Script, PROJECT};
 use eucalyptus_core::traits::registry::ComponentRegistry;
 use hecs::{Entity, EntityBuilder, World};
@@ -36,6 +36,7 @@ use eucalyptus_core::physics::collider::{Collider, ColliderGroup};
 use eucalyptus_core::physics::kcc::KCC;
 use eucalyptus_core::physics::rigidbody::RigidBody;
 use eucalyptus_core::properties::CustomProperties;
+use crate::editor::console::EucalyptusConsole;
 
 pub struct EditorTabViewer<'a> {
     pub view: egui::TextureId,
@@ -53,6 +54,7 @@ pub struct EditorTabViewer<'a> {
     pub plugin_registry: &'a mut PluginRegistry,
     pub component_registry: &'a ComponentRegistry,
     pub build_logs: &'a mut Vec<String>,
+    pub eucalyptus_console: &'a mut EucalyptusConsole,
 
     // "wah wah its unsafe, its using raw pointers" shut the fuck up if it breaks i will know
     pub editor: *mut Editor,
@@ -191,7 +193,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                     "Unknown Plugin Name".into()
                 }
             }
-            EditorTab::ErrorConsole => "Error Console".into(),
+            EditorTab::ErrorConsole => "Build Output".into(),
+            EditorTab::Console => "Console".into(),
         }
     }
 
@@ -212,21 +215,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
         match tab {
             EditorTab::Viewport => {
                 log_once::debug_once!("Viewport focused");
-                // ------------------- Template for querying active camera -----------------
-                // if let Some(active_camera) = self.active_camera {
-                //     if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) {
-                //         if let Some((camera, component, follow_target)) = q.get() {
-
-                //         } else {
-                //             log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera)
-                //         }
-                //     } else {
-                //         log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera);
-                //     }
-                // } else {
-                //     log_once::warn_once!("No active camera found");
-                // }
-                // -------------------------------------------------------------------------
 
                 let available_rect = ui.available_rect_before_wrap();
                 let available_size = available_rect.size();
@@ -818,17 +806,32 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                             }
 
                             // script
-                            if let Ok(script) = world.query_one::<&mut Script>(*entity).get()
+                            if let Ok((script, ui_c)) = world.query_one::<(Option<&mut Script>, Option<&mut UIComponent>)>(*entity).get()
                             {
                                 CollapsingHeader::new("Script").default_open(true).show(ui, |ui| {
-                                    script.inspect(
-                                        entity,
-                                        &mut cfg,
-                                        ui,
-                                        self.undo_stack,
-                                        self.signal,
-                                        label.as_mut_string(),
-                                    );
+                                    if let Some(s) = script {
+                                        s.inspect(
+                                            entity,
+                                            &mut cfg,
+                                            ui,
+                                            self.undo_stack,
+                                            self.signal,
+                                            label.as_mut_string(),
+                                        );
+                                    }
+                                    
+                                    if let Some(ui_c) = ui_c {
+                                        CollapsingHeader::new("UI").default_open(true).show(ui, |ui| {
+                                            ui_c.inspect(
+                                                entity,
+                                                &mut cfg,
+                                                ui,
+                                                self.undo_stack,
+                                                self.signal,
+                                                label.as_mut_string(),
+                                            );
+                                        });
+                                    }
                                 });
                                 ui.separator();
                             }
@@ -997,29 +1000,32 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                     }
 
                     let mut list: Vec<ConsoleItem> = Vec::new();
-                    let index = 0;
-                    for line in log {
+                    for (index, line) in log.iter().enumerate() {
                         if line.contains("The required library") {
                             list.push(ConsoleItem {
                                 error_level: ErrorLevel::Error,
                                 msg: line.clone(),
                                 file_location: None,
                                 line_ref: None,
-                                id: index + 1,
+                                id: index as u64,
                             });
-                        }
-
-                        if let Some((error_level, path, loc)) = parse_compiler_location(line) {
+                        } else if let Some((error_level, path, loc)) = parse_compiler_location(line) {
                             list.push(ConsoleItem {
                                 error_level,
                                 msg: line.clone(),
                                 file_location: Some(path),
                                 line_ref: Some(loc),
-                                id: index + 1,
+                                id: index as u64,
+                            });
+                        } else {
+                            list.push(ConsoleItem {
+                                error_level: ErrorLevel::Info,
+                                msg: line.clone(),
+                                file_location: None,
+                                line_ref: None,
+                                id: index as u64,
                             });
                         }
-
-                        // thats it for now
                     }
                     list
                 }
@@ -1028,6 +1034,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
 
                 egui::ScrollArea::vertical()
                     .auto_shrink([false, false])
+                    .stick_to_bottom(true)
                     .show(ui, |ui| {
                         if logs.is_empty() {
                             ui.label("Build output will appear here once available.");
@@ -1035,64 +1042,141 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                         }
 
                         for item in &logs {
-                            let (bg_color, text_color) = match item.error_level {
+                            let (bg_color, text_color, stroke_color) = match item.error_level {
                                 ErrorLevel::Error => (
                                     egui::Color32::from_rgb(60, 20, 20),
                                     egui::Color32::from_rgb(255, 200, 200),
+                                    egui::Color32::from_rgb(255, 200, 200),
                                 ),
                                 ErrorLevel::Warn => (
                                     egui::Color32::from_rgb(40, 40, 10),
                                     egui::Color32::from_rgb(255, 255, 200),
+                                    egui::Color32::from_rgb(255, 255, 200),
+                                ),
+                                ErrorLevel::Info => (
+                                    egui::Color32::TRANSPARENT,
+                                    ui.style().visuals.text_color(),
+                                    egui::Color32::TRANSPARENT,
                                 ),
                             };
 
-                            let available_width = ui.available_width();
-                            let frame = egui::Frame::new()
-                                .inner_margin(Margin::symmetric(8, 6))
-                                .fill(bg_color)
-                                .stroke(egui::Stroke::new(1.0, text_color));
-
-                            let response = frame
-                                .show(ui, |ui| {
-                                    ui.set_width(available_width - 10.0);
-                                    ui.horizontal(|ui| {
-                                        ui.label(RichText::new(&item.msg).color(text_color));
-                                    });
-                                })
-                                .response;
-
-                            if response.clicked() {
-                                log::debug!("Log item clicked: {}", &item.id);
-                                if let (Some(path), Some(loc)) =
-                                    (&item.file_location, &item.line_ref)
-                                {
-                                    let location_arg = format!("{}:{}", path.display(), loc);
-
-                                    match std::process::Command::new("code")
-                                        .args(["-g", &location_arg])
-                                        .spawn()
-                                        .map(|_| ())
+                            if matches!(item.error_level, ErrorLevel::Info) {
+                                ui.label(RichText::new(&item.msg).monospace());
+                            } else {
+                                let available_width = ui.available_width();
+                                let frame = egui::Frame::new()
+                                    .inner_margin(Margin::symmetric(8, 6))
+                                    .fill(bg_color)
+                                    .stroke(egui::Stroke::new(1.0, stroke_color));
+
+                                let response = frame
+                                    .show(ui, |ui| {
+                                        ui.set_width(available_width - 10.0);
+                                        ui.horizontal(|ui| {
+                                            ui.label(RichText::new(&item.msg).color(text_color).monospace());
+                                        });
+                                    })
+                                    .response;
+
+                                if response.clicked() {
+                                    log::debug!("Log item clicked: {}", &item.id);
+                                    if let (Some(path), Some(loc)) =
+                                        (&item.file_location, &item.line_ref)
                                     {
-                                        Ok(()) => {
-                                            log::info!(
-                                                "Launched Visual Studio Code at the error: {}",
-                                                &location_arg
-                                            );
-                                        }
-                                        Err(e) => {
-                                            warn!(
-                                                "Failed to open '{}' in VS Code: {}",
-                                                location_arg, e
-                                            );
+                                        let location_arg = format!("{}:{}", path.display(), loc);
+
+                                        match std::process::Command::new("code")
+                                            .args(["-g", &location_arg])
+                                            .spawn()
+                                            .map(|_| ())
+                                        {
+                                            Ok(()) => {
+                                                log::info!(
+                                                    "Launched Visual Studio Code at the error: {}",
+                                                    &location_arg
+                                                );
+                                            }
+                                            Err(e) => {
+                                                warn!(
+                                                    "Failed to open '{}' in VS Code: {}",
+                                                    location_arg, e
+                                                );
+                                            }
                                         }
                                     }
                                 }
                             }
-
-                            ui.add_space(4.0);
                         }
                     });
             }
+            EditorTab::Console => {
+                ui.heading("Console");
+                ui.separator();
+
+                ui.horizontal(|ui| {
+                    if ui.button("Clear").clicked() {
+                        self.eucalyptus_console.history.clear();
+                    }
+
+                    ui.separator();
+
+                    ui.checkbox(&mut self.eucalyptus_console.show_info, "Info");
+                    ui.checkbox(&mut self.eucalyptus_console.show_warning, "Warning");
+                    ui.checkbox(&mut self.eucalyptus_console.show_error, "Error");
+                    ui.checkbox(&mut self.eucalyptus_console.show_debug, "Debug");
+                    ui.checkbox(&mut self.eucalyptus_console.show_trace, "Trace");
+
+                    ui.separator();
+
+                    ui.checkbox(&mut self.eucalyptus_console.auto_scroll, "Auto-scroll");
+
+                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                        ui.label(format!("Logs: {}", self.eucalyptus_console.history.len()));
+                    });
+                });
+
+                ui.separator();
+
+                let _ = self.eucalyptus_console.take();
+
+                let scroll = egui::ScrollArea::vertical()
+                    .auto_shrink([false, false])
+                    .stick_to_bottom(self.eucalyptus_console.auto_scroll);
+                
+                scroll.show(ui, |ui| {
+                    for log in &self.eucalyptus_console.history {
+                        let is_error = log.contains("[ERROR]") || log.contains("[FATAL]");
+                        let is_warn = log.contains("[WARN]");
+                        let is_debug = log.contains("[DEBUG]");
+                        let is_trace = log.contains("[TRACE]");
+                        let is_info = !is_error && !is_warn && !is_debug && !is_trace;
+
+                        if is_error && !self.eucalyptus_console.show_error { continue; }
+                        if is_warn && !self.eucalyptus_console.show_warning { continue; }
+                        if is_debug && !self.eucalyptus_console.show_debug { continue; }
+                        if is_trace && !self.eucalyptus_console.show_trace { continue; }
+                        if is_info && !self.eucalyptus_console.show_info { continue; }
+
+                        let color = if is_error {
+                            egui::Color32::from_rgb(255, 100, 100)
+                        } else if is_warn {
+                            egui::Color32::from_rgb(255, 200, 50)
+                        } else if is_debug {
+                            egui::Color32::from_rgb(100, 200, 255)
+                        } else if is_trace {
+                            egui::Color32::from_rgb(150, 150, 150)
+                        } else {
+                            egui::Color32::LIGHT_GRAY
+                        };
+
+                        ui.add(egui::Label::new(
+                            egui::RichText::new(log)
+                                .color(color)
+                                .monospace()
+                        ));
+                    }
+                });
+            }
         }
     }
 }
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 3b1cb2f..5303b74 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -4,6 +4,7 @@ pub mod dock;
 pub mod input;
 pub mod scene;
 pub mod settings;
+mod console;
 
 pub(crate) use crate::editor::dock::*;
 
@@ -16,7 +17,7 @@ use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::EntityTransform;
 use dropbear_engine::graphics::InstanceRaw;
 use dropbear_engine::pipelines::light_cube::LightCubePipeline;
-use dropbear_engine::texture::TextureWrapMode;
+use dropbear_engine::texture::{Texture, TextureWrapMode};
 use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData};
 use egui::{self, Context};
 use egui_dock::{DockArea, DockState, NodeIndex, Style};
@@ -50,7 +51,7 @@ use std::{
     time::Instant,
 };
 use std::rc::Rc;
-use log::debug;
+use log::{debug, error};
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
 use wgpu::{Color, Extent3d};
@@ -58,14 +59,16 @@ use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
 use winit::dpi::PhysicalSize;
 use dropbear_engine::mipmap::MipMapper;
-use dropbear_engine::pipelines::DropbearShaderPipeline;
+use dropbear_engine::pipelines::{create_render_pipeline, DropbearShaderPipeline};
 use dropbear_engine::pipelines::shader::MainRenderPipeline;
 use dropbear_engine::pipelines::GlobalsUniform;
+use dropbear_engine::sky::{HdrLoader, SkyPipeline, DEFAULT_SKY_TEXTURE};
 use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline;
 use eucalyptus_core::properties::CustomProperties;
 use crate::about::AboutWindow;
+use crate::editor::console::EucalyptusConsole;
 use crate::editor::settings::editor::{EditorSettingsWindow, EDITOR_SETTINGS};
 use crate::editor::settings::project::ProjectSettingsWindow;
 
@@ -86,6 +89,7 @@ pub struct Editor {
     pub shader_globals: Option<GlobalsUniform>,
     pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     pub mipmapper: Option<MipMapper>,
+    pub sky_pipeline: Option<SkyPipeline>,
 
     pub active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -110,6 +114,7 @@ pub struct Editor {
     pub(crate) editor_state: EditorState,
     pub gizmo_mode: EnumSet<GizmoMode>,
     pub gizmo_orientation: GizmoOrientation,
+    pub console: EucalyptusConsole,
 
     // might as well save some memory if its not required...
     // #[allow(unused)] // unused to allow for JVM to startup
@@ -182,10 +187,26 @@ impl Editor {
 
         eucalyptus_core::utils::start_deadlock_detector();
 
-        let plugin_registry = PluginRegistry::new();
+        let mut plugin_registry = PluginRegistry::new();
+        if let Err(e) = plugin_registry.load_plugins() {
+            warn!("Failed to load plugins: {e}");
+        }
+
         let mut component_registry = ComponentRegistry::new();
+        register_components(&mut component_registry);
 
-        register_components(/*&mut plugin_registry,*/ &mut component_registry);
+        for plugin in plugin_registry.plugins.values_mut() {
+            let plugin_id = plugin.id().to_string();
+            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+                plugin.register_component(&mut component_registry);
+            }));
+
+            if result.is_ok() {
+                log::info!("Registered components for plugin '{plugin_id}'");
+            } else {
+                warn!("Plugin '{plugin_id}' panicked during component registration");
+            }
+        }
 
         let component_registry = Arc::new(component_registry);
 
@@ -215,6 +236,7 @@ impl Editor {
             editor_state: EditorState::Editing,
             gizmo_mode: EnumSet::empty(),
             gizmo_orientation: GizmoOrientation::Global,
+            console: EucalyptusConsole::new(None),
             play_mode_backup: None,
             input_state: Box::new(InputState::new()),
             light_cube_pipeline: None,
@@ -251,6 +273,7 @@ impl Editor {
             collider_wireframe_geometry_cache: HashMap::new(),
             collider_instance_buffer: None,
             mipmapper: None,
+            sky_pipeline: None,
         })
     }
 
@@ -960,9 +983,12 @@ impl Editor {
                     if ui_window.button("Open Error Console").clicked() {
                         self.dock_state.push_to_focused_leaf(EditorTab::ErrorConsole);
                     }
+                    if ui_window.button("Open Debug Console").clicked() {
+                        self.dock_state.push_to_focused_leaf(EditorTab::Console);
+                    }
                     if self.plugin_registry.plugins.len() == 0 {
                         ui_window.label(
-                            egui::RichText::new("No plugins ")
+                            egui::RichText::new("No plugins")
                                 .color(ui_window.visuals().weak_text_color())
                         );
                     }
@@ -1084,6 +1110,7 @@ impl Editor {
                         editor: editor_ptr,
                         build_logs: &mut self.build_logs,
                         component_registry: &self.component_registry,
+                        eucalyptus_console: &mut self.console,
                     },
                 );
         });
@@ -1326,12 +1353,28 @@ impl Editor {
         self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone()));
         self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("editor shader globals")));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
-        // Mipmaps are generated by the engine during texture creation; keep this optional field unused for now.
         self.mipmapper = None;
 
         self.texture_id = Some((*graphics.texture_id).clone());
         self.window = Some(graphics.window.clone());
         self.is_world_loaded.mark_rendering_loaded();
+
+        let sky_texture = HdrLoader::from_equirectangular_bytes(
+            &graphics.device,
+            &graphics.queue,
+            DEFAULT_SKY_TEXTURE,
+            1080,
+            Some("sky texture")
+        );
+
+        match sky_texture {
+            Ok(sky_texture) => {
+                self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture));
+            }
+            Err(e) => {
+                error!("Failed to load sky texture: {}", e);
+            }
+        }
     }
 
     /// Initialises another eucalyptus-editor play mode app as a separate process and monitors it in a separate thread.
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 403c557..8910a14 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -232,12 +232,12 @@ impl Scene for Editor {
             {
                 for key in &self.input_state.pressed_keys {
                     match key {
-                        KeyCode::KeyW => camera.move_forwards(),
-                        KeyCode::KeyA => camera.move_left(),
-                        KeyCode::KeyD => camera.move_right(),
-                        KeyCode::KeyS => camera.move_back(),
-                        KeyCode::ShiftLeft => camera.move_down(),
-                        KeyCode::Space => camera.move_up(),
+                        KeyCode::KeyW => camera.move_forwards(dt),
+                        KeyCode::KeyA => camera.move_left(dt),
+                        KeyCode::KeyD => camera.move_right(dt),
+                        KeyCode::KeyS => camera.move_back(dt),
+                        KeyCode::ShiftLeft => camera.move_down(dt),
+                        KeyCode::Space => camera.move_up(dt),
                         _ => {}
                     }
                 }
@@ -351,6 +351,8 @@ impl Scene for Editor {
         self.texture_id = Some(*graphics.texture_id.clone());
         self.window = Some(graphics.window.clone());
 
+        let hdr = graphics.hdr.read();
+
         self.show_ui(&graphics.get_egui_context());
         let clear_color = Color {
             r: 100.0 / 255.0,
@@ -387,35 +389,34 @@ impl Scene for Editor {
         };
         log_once::debug_once!("Pipeline ready");
 
-        { // ensures clearing of the encoder is done correctly. 
-            let mut encoder = CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder"));
-
-            {
-                let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("viewport clear pass"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &graphics.viewport_texture.view,
-                        depth_slice: None,
-                        resolve_target: None,
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(wgpu::Color {
-                                r: 100.0 / 255.0,
-                                g: 149.0 / 255.0,
-                                b: 237.0 / 255.0,
-                                a: 1.0,
-                            }),
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: None,
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
-            }
-
-            if let Err(e) = encoder.submit(graphics.clone()) {
-                log_once::error_once!("{}", e);
-            }
+        {
+            let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("viewport clear pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    depth_slice: None,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(wgpu::Color {
+                            r: 100.0 / 255.0,
+                            g: 149.0 / 255.0,
+                            b: 237.0 / 255.0,
+                            a: 1.0,
+                        }),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &graphics.depth_texture.view,
+                    depth_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(0.0),
+                        store: wgpu::StoreOp::Store,
+                    }),
+                    stencil_ops: None,
+                }),
+                occlusion_query_set: None,
+                timestamp_writes: None,
+            });
         }
 
         let lights = {
@@ -503,18 +504,18 @@ impl Scene for Editor {
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("light cube render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &graphics.viewport_texture.view,
+                            view: hdr.view(),
                         depth_slice: None,
                         resolve_target: None,
                         ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(clear_color),
+                            load: wgpu::LoadOp::Load,
                             store: wgpu::StoreOp::Store,
                         },
                     })],
                     depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                         view: &graphics.depth_texture.view,
                         depth_ops: Some(wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(0.0),
+                            load: wgpu::LoadOp::Load,
                             store: wgpu::StoreOp::Store,
                         }),
                         stencil_ops: None,
@@ -537,8 +538,6 @@ impl Scene for Editor {
             }
         }
 
-                
-
         if let Some(lcp) = &self.light_cube_pipeline {
             for (model, instance_buffer, instance_count) in prepared_models {
                 let globals_bind_group = &self
@@ -551,7 +550,7 @@ impl Scene for Editor {
                     .begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("model render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                            view: &graphics.viewport_texture.view,
+                            view: hdr.view(),
                             depth_slice: None,
                             resolve_target: None,
                             ops: wgpu::Operations {
@@ -572,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,
@@ -601,7 +600,7 @@ impl Scene for Editor {
                         .begin_render_pass(&wgpu::RenderPassDescriptor {
                             label: Some("model render pass"),
                             color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                                view: &graphics.viewport_texture.view,
+                                view: hdr.view(),
                                 depth_slice: None,
                                 resolve_target: None,
                                 ops: wgpu::Operations {
@@ -715,9 +714,42 @@ impl Scene for Editor {
                             );
                         }
                     }
-                }
+}
             }
-            if let Err(e) = encoder.submit(graphics.clone()) {
+
+            if let Some(sky) = &self.sky_pipeline {
+                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                    label: Some("sky render pass"),
+                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                        view: hdr.view(),
+                        resolve_target: None,
+                        ops: wgpu::Operations {
+                            load: wgpu::LoadOp::Load,
+                            store: wgpu::StoreOp::Store,
+                        },
+                        depth_slice: None,
+                    })],
+                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                        view: &graphics.depth_texture.view,
+                        depth_ops: Some(wgpu::Operations {
+                            load: wgpu::LoadOp::Load,
+                            store: wgpu::StoreOp::Store,
+                        }),
+                        stencil_ops: None,
+                    }),
+                    timestamp_writes: None,
+                    occlusion_query_set: None,
+                });
+
+                render_pass.set_pipeline(&sky.pipeline);
+                render_pass.set_bind_group(0, &camera.bind_group, &[]);
+                render_pass.set_bind_group(1, &sky.bind_group, &[]);
+                render_pass.draw(0..3, 0..1);
+            }
+
+            hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
         }
diff --git a/crates/eucalyptus-editor/src/main.rs b/crates/eucalyptus-editor/src/main.rs
index 1d023c1..b5fbfa1 100644
--- a/crates/eucalyptus-editor/src/main.rs
+++ b/crates/eucalyptus-editor/src/main.rs
@@ -93,13 +93,15 @@ async fn main() -> anyhow::Result<()> {
 
                 Ok(())
             })
+            .filter_level(LevelFilter::Warn)
             .filter(Some("dropbear_engine"), LevelFilter::Trace)
             .filter(
-                Some("eucalyptus-editor".replace('-', "_").as_str()),
+                Some("eucalyptus_editor"),
                 LevelFilter::Debug,
             )
             .filter(Some("eucalyptus_core"), LevelFilter::Debug)
             .filter(Some("dropbear_traits"), LevelFilter::Debug)
+            .filter(Some("kino_ui"), LevelFilter::Debug)
             .init();
         log::info!("Initialised logger");
     }
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index f03b7e3..763150a 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -4,7 +4,7 @@ use dropbear_engine::entity::{MeshRenderer, Transform};
 use dropbear_engine::graphics::{SharedGraphicsContext};
 use dropbear_engine::lighting::{Light as EngineLight, LightComponent};
 use dropbear_engine::model::{LoadedModel, Material, Model, ModelId, MODEL_CACHE};
-use dropbear_engine::procedural::ProcedurallyGeneratedObject;
+use dropbear_engine::procedural::{ProcedurallyGeneratedObject, ProcObj};
 use dropbear_engine::texture::{Texture, TextureWrapMode};
 use dropbear_engine::utils::{relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType};
 use egui::Align2;
@@ -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);
 
@@ -653,7 +659,7 @@ impl SignalController for Editor {
                             .build_model(graphics_clone.clone(), None, Some("Cuboid"));
 
                         let model = loaded.make_mut();
-                        model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits });
+                        model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }));
                         loaded.refresh_registry();
                         loaded
                     } else {
@@ -692,7 +698,7 @@ impl SignalController for Editor {
 
                         {
                             let model = loaded_model.make_mut();
-                            model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits });
+                            model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }));
                             for material in &mut model.materials {
                                 material.set_tint(graphics_clone.as_ref(), [1.0, 1.0, 1.0, 1.0]);
                             }
@@ -775,7 +781,7 @@ impl SignalController for Editor {
 
                 {
                     let model = loaded_model.make_mut();
-                    model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits });
+                    model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }));
                 }
                 loaded_model.refresh_registry();
 
diff --git a/crates/eucalyptus-editor/src/spawn.rs b/crates/eucalyptus-editor/src/spawn.rs
index 9b1e98c..91d3e2a 100644
--- a/crates/eucalyptus-editor/src/spawn.rs
+++ b/crates/eucalyptus-editor/src/spawn.rs
@@ -8,6 +8,7 @@ use dropbear_engine::lighting::{Light, LightComponent};
 use dropbear_engine::model::{LoadedModel, Material, Model, ModelId};
 use dropbear_engine::texture::Texture;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
+use dropbear_engine::procedural::ProcObj;
 use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::scene::SceneEntity;
 pub(crate) use eucalyptus_core::spawn::{PendingSpawnController, PENDING_SPAWNS};
@@ -279,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);
@@ -296,7 +300,7 @@ async fn load_renderer_from_serialized(
                     .build_model(graphics.clone(), None, Some(&label));
 
                 let model = loaded_model.make_mut();
-                model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits });
+                model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }));
 
                 loaded_model.refresh_registry();
 
@@ -313,7 +317,7 @@ async fn load_renderer_from_serialized(
                     .await?;
             MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
         }
-        ResourceReferenceType::Cuboid { size_bits } => {
+        ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => {
             let size = [
                 f32::from_bits(size_bits[0]),
                 f32::from_bits(size_bits[1]),
@@ -324,7 +328,7 @@ async fn load_renderer_from_serialized(
                 .build_model(graphics.clone(), None, Some(&label));
 
             let model = loaded_model.make_mut();
-            model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits: *size_bits });
+            model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits }));
 
             loaded_model.refresh_registry();
 
diff --git a/crates/kino-ui/Cargo.toml b/crates/kino-ui/Cargo.toml
new file mode 100644
index 0000000..b59a6ce
--- /dev/null
+++ b/crates/kino-ui/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "kino-ui"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+readme.workspace = true
+authors.workspace = true
+
+[dependencies]
+wgpu.workspace = true
+parking_lot.workspace = true
+glam.workspace = true
+bytemuck.workspace = true
+image.workspace = true
+anyhow.workspace = true
+log.workspace = true
+winit.workspace = true
+
+glyphon.workspace = true
diff --git a/crates/kino-ui/README.md b/crates/kino-ui/README.md
new file mode 100644
index 0000000..c13900a
--- /dev/null
+++ b/crates/kino-ui/README.md
@@ -0,0 +1,45 @@
+# kino-ui
+
+Kino is a type of resin that is made by eucalyptus trees, and the name of the UI library. 
+
+Built with wgpu and winit, this UI library is inspired by the ui crate [wick3dr0se/egor](https://github.com/wick3dr0se/egor) and uses 
+Assembly-like instructions to render different components, including standard and contained widgets.
+
+# Example
+
+```rust
+pub fn init() {
+    let renderer = KinoWGPURenderer::new(/*yabba dabba doo*/);
+    let windowing = KinoWinitWindowing::new(window.clone());
+    
+    // keep this with you this is important. the rest are owned by KinoState
+    let kino = KinoState::new(renderer, windowing);
+}
+
+pub fn update(kino: &mut KinoState) {
+    // creating new widget
+    let new_rect = kino.add_widget(Box::new(
+        Rectangle::new("red patch".into())
+            .with(&Rect::new([50.0, 100.0].into(), [128.0, 100.0].into()))
+            .colour([255.0, 0.0, 0.0, 255.0]).into()
+    ));
+
+    // polling to build the widget tree
+    kino.poll();
+
+    // checking input response
+    if kino.response(new_rect).clicked {
+        println!("I have been clicked!");
+    }
+}
+
+pub fn render(kino: &mut KinoState) {
+    // ...
+    
+    // generally the last thing to render on your viewport if you want
+    // an overlay
+    kino.render(/**/);
+    
+    // ...
+}
+```
\ No newline at end of file
diff --git a/crates/kino-ui/src/asset.rs b/crates/kino-ui/src/asset.rs
new file mode 100644
index 0000000..a0f0ace
--- /dev/null
+++ b/crates/kino-ui/src/asset.rs
@@ -0,0 +1,96 @@
+use std::collections::HashMap;
+use std::hash::{Hash, Hasher};
+use std::collections::hash_map::DefaultHasher;
+use std::marker::PhantomData;
+use crate::rendering::texture::Texture;
+
+/// A handle with type [`T`] that provides an index to the [AssetServer] contents.
+#[derive(Hash, Eq, PartialEq, Debug)]
+pub struct Handle<T> {
+    pub id: u64,
+    _phanton: PhantomData<T>
+}
+
+impl<T> Copy for Handle<T> {}
+
+impl<T> Clone for Handle<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+impl<T> Handle<T> {
+    pub fn new(id: u64) -> Self {
+        Self { id, _phanton: Default::default() }
+    }
+}
+
+/// Serves assets as a [Handle], allowing you to reference any object.
+#[derive(Default)]
+pub struct AssetServer {
+    textures: HashMap<u64, Texture>,
+    texture_labels: HashMap<String, Handle<Texture>>,
+}
+
+impl AssetServer {
+    /// Creates a new instance of an AssetServer.
+    pub fn new() -> AssetServer {
+        AssetServer {
+            textures: Default::default(),
+            texture_labels: Default::default(),
+        }
+    }
+
+    /// Adds a texture and returns a handle.
+    ///
+    /// This assumes a [Texture] has already been created by you. To create a new texture,
+    /// you can use [`Texture::from_bytes`].
+    pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> {
+        let handle = Handle::new(texture.hash);
+        self.textures.entry(handle.id).or_insert(texture);
+        handle
+    }
+
+    /// Adds a texture with a label. If the texture already exists (by hash),
+    /// returns the existing handle and updates the label to point at it.
+    pub fn add_texture_with_label(&mut self, label: impl Into<String>, texture: Texture) -> Handle<Texture> {
+        let handle = self.add_texture(texture);
+        self.texture_labels.insert(label.into(), handle.clone());
+        handle
+    }
+
+    /// Maps a label to an existing texture handle.
+    pub fn label_texture(&mut self, label: impl Into<String>, handle: Handle<Texture>) {
+        self.texture_labels.insert(label.into(), handle.clone());
+    }
+
+    /// Updates the asset server by inserting the texture provided at the location of the handle,
+    /// and removing the old texture (by returning it back to you).
+    pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> {
+        self.textures.insert(handle.id, texture)
+    }
+
+    pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> {
+        self.textures.get(&handle.id)
+    }
+
+    pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> {
+        self.texture_labels
+            .get(label)
+            .and_then(|handle| self.textures.get(&handle.id))
+    }
+
+    pub fn get_texture_handle(&self, label: &str) -> Option<Handle<Texture>> {
+        self.texture_labels.get(label).cloned()
+    }
+
+    pub fn texture_handle_by_hash(&self, hash: u64) -> Option<Handle<Texture>> {
+        self.textures.contains_key(&hash).then(|| Handle::new(hash))
+    }
+
+    pub fn hash_bytes(data: &[u8]) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        data.hash(&mut hasher);
+        hasher.finish()
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/camera.rs b/crates/kino-ui/src/camera.rs
new file mode 100644
index 0000000..4b1c675
--- /dev/null
+++ b/crates/kino-ui/src/camera.rs
@@ -0,0 +1,83 @@
+    use bytemuck::{Pod, Zeroable};
+use glam::{Mat4, Vec2, Vec3};
+use crate::math::Rect;
+
+pub struct Camera2D {
+    position: Vec2,
+    zoom: f32,
+}
+
+impl Default for Camera2D {
+    fn default() -> Self {
+        Self {
+            position: Vec2::ZERO,
+            zoom: 1.0,
+        }
+    }
+}
+
+impl Camera2D {
+    /// Returns the orthographic view-projection matrix for the current camera state
+    pub fn view_proj(&self, screen_size: Vec2) -> Mat4 {
+        let (width, height) = (screen_size.x, screen_size.y);
+
+        let view = Mat4::look_at_rh(
+            self.position.extend(1.0),
+            self.position.extend(0.0),
+            Vec3::Y,
+        );
+
+        let proj = Mat4::orthographic_rh(
+            0.0,
+            width,
+            height,
+            0.0,
+            -1.0,
+            1.0,
+        );
+
+        proj * view
+    }
+
+    /// Set the camera's position (top-left corner of view)
+    pub fn target(&mut self, position: Vec2) {
+        self.position = position;
+    }
+
+    /// Center the camera on a position
+    pub fn center(&mut self, position: Vec2, screen_size: Vec2) {
+        self.position = position - screen_size / (2.0 * self.zoom);
+    }
+
+    /// Set zoom level, clamped between 0.1 & 10.0 to avoid insanity
+    pub fn set_zoom(&mut self, zoom: f32) {
+        self.zoom = zoom.clamp(0.1, 10.0);
+    }
+
+    /// Returns the viewport rectangle in world coordinates, factoring in zoom
+    /// Useful for culling or visibility checks
+    pub fn viewport(&self, screen_size: Vec2) -> Rect {
+        let size = screen_size / self.zoom;
+        Rect::new(self.position, size)
+    }
+    /// Converts a point from world space to screen space (pixels)
+    pub fn world_to_screen(&self, world: Vec2) -> Vec2 {
+        (world - self.position) * self.zoom
+    }
+
+    /// Converts a point from screen space back to world space
+    pub fn screen_to_world(&self, screen: Vec2) -> Vec2 {
+        screen / self.zoom + self.position
+    }
+}
+
+pub struct CameraRendering {
+    pub buffer: wgpu::Buffer,
+    pub bind_group: wgpu::BindGroup,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Pod, Zeroable)]
+pub struct CameraUniform {
+    pub view_proj: [[f32; 4]; 4],
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/lib.rs b/crates/kino-ui/src/lib.rs
new file mode 100644
index 0000000..ce54635
--- /dev/null
+++ b/crates/kino-ui/src/lib.rs
@@ -0,0 +1,567 @@
+#![doc = include_str!("../README.md")]
+
+pub mod resp;
+pub mod widgets;
+pub mod rendering;
+pub mod camera;
+pub mod asset;
+pub mod math;
+pub mod windowing;
+
+pub mod crates {
+    pub use wgpu;
+    pub use winit;
+    pub use glyphon;
+}
+
+pub use widgets::shorthand::*;
+
+use crate::asset::{AssetServer, Handle};
+use crate::camera::Camera2D;
+use crate::rendering::texture::Texture;
+use crate::rendering::vertex::Vertex;
+use crate::rendering::{KinoWGPURenderer};
+use crate::resp::WidgetResponse;
+use crate::widgets::{ContaineredWidget, NativeWidget};
+use crate::math::Rect;
+use std::borrow::Cow;
+use std::collections::{HashMap, VecDeque};
+use std::fmt::Debug;
+use std::hash::{DefaultHasher, Hash, Hasher};
+use wgpu::{LoadOp, StoreOp};
+use rendering::batching::VertexBatch;
+use crate::windowing::KinoWinitWindowing;
+use glam::Vec2;
+use crate::rendering::text::KinoTextRenderer;
+
+/// Holds the state of all the instructions, and the vertices+indices for rendering as well
+/// as the responses.
+pub struct KinoState {
+    renderer: KinoWGPURenderer,
+    windowing: KinoWinitWindowing,
+    instruction_set: VecDeque<UiInstructionType>,
+    widget_states: HashMap<WidgetId, WidgetResponse>,
+    assets: AssetServer,
+    batch: PrimitiveBatch,
+    camera: Camera2D,
+    container_stack: Vec<ContainerContext>,
+}
+
+// public stuff
+impl KinoState {
+    /// Returns a mutable reference to the current [`KinoTextRenderer`], used for text and font
+    /// management. 
+    pub fn text(&mut self) -> &mut KinoTextRenderer {
+        &mut self.renderer.text
+    }
+    
+    /// Returns a mutable reference to the current [`KinoWGPURenderer`], used for rendering and 
+    /// pipelines. 
+    pub fn renderer(&mut self) -> &mut KinoWGPURenderer {
+        &mut self.renderer
+    }
+    
+    /// Returns a mutable reference to the current [`KinoWinitWindowing`], used for handling events 
+    /// and windowing operations. 
+    pub fn windowing(&mut self) -> &mut KinoWinitWindowing {
+        &mut self.windowing
+    }
+    
+    /// Returns a mutable reference to the current [`Camera2D`], used for displaying the current 
+    /// viewport. 
+    pub fn camera(&mut self) -> &mut Camera2D {
+        &mut self.camera
+    }
+    
+    /// Returns a mutable reference to the [`AssetServer`], used for storing textures and
+    /// other assets. 
+    pub fn assets(&mut self) -> &mut AssetServer {
+        &mut self.assets
+    }
+}
+
+impl KinoState {
+    /// Creates a new instance of a [KinoState].
+    ///
+    /// This sits inside your `init()` function.
+    pub fn new(renderer: KinoWGPURenderer, windowing: KinoWinitWindowing) -> Self {
+        log::debug!("Created KinoState");
+        KinoState {
+            renderer,
+            windowing,
+            instruction_set: Default::default(),
+            widget_states: Default::default(),
+            assets: Default::default(),
+            batch: Default::default(),
+            camera: Camera2D::default(),
+            container_stack: Default::default(),
+        }
+    }
+
+    /// Adds a widget (a [`NativeWidget`]) to the instruction set as a
+    /// [`UiInstructionType::Widget`] and returns back the associated [`WidgetId`] for response
+    /// checking.
+    pub fn add_widget(&mut self, widget: Box<dyn NativeWidget>) -> WidgetId {
+        let id = widget.id();
+        self.instruction_set.push_back(UiInstructionType::Widget(widget));
+        id
+    }
+
+    /// Adds a widget (a [`ContaineredWidget`]) to the instruction set as a
+    /// [`UiInstructionType::Containered`] and returns the associated [`WidgetId`] for response
+    /// checking.
+    pub fn add_container(&mut self, container: Box<dyn ContaineredWidget>) -> WidgetId {
+        let id = container.id();
+        self.instruction_set.push_back(UiInstructionType::Containered(
+            ContaineredWidgetType::Start {
+                id,
+                widget: container,
+            }
+        ));
+        id
+    }
+
+    /// Ends the current container block.
+    pub fn end_container(&mut self, id: WidgetId) {
+        self.instruction_set.push_back(UiInstructionType::Containered(
+            ContaineredWidgetType::End { id }
+        ));
+    }
+
+    /// Adds a [UiInstructionType] to the instruction set.
+    pub fn add_instruction(&mut self, ui_instruction_type: UiInstructionType) {
+        self.instruction_set.push_back(ui_instruction_type);
+    }
+
+    /// Polls for changes by clearing the current instruction set, build the tree and
+    /// preparing them for rendering.
+    ///
+    /// If you create a widget and then check for a response before polling, you will not receive
+    /// back a response. You are required to poll/prepare the contents before being given access
+    /// to the response information.
+    ///
+    /// This sits inside your `update()` loop.
+    pub fn poll(&mut self) {
+        log::trace!("polling kinostate");
+        let current_instructions = {
+            self.instruction_set.drain(..).collect::<Vec<_>>()
+        };
+
+        self.widget_states.clear();
+
+        let tree = Self::build_tree(current_instructions);
+
+        self.render_tree(tree);
+    }
+
+    /// Fetches the [`WidgetResponse`] from an associated [`WidgetId`].
+    ///
+    /// This will only provide the proper information **after** you have
+    /// polled with [`KinoState::poll`].
+    pub fn response(&self, id: impl Into<WidgetId>) -> WidgetResponse {
+        self.widget_states.get(&id.into()).copied().unwrap_or_default()
+    }
+
+    pub fn set_viewport_offset(&mut self, offset: Vec2) {
+        self.windowing.viewport_offset = offset;
+    }
+
+    /// Sets both the viewport offset (top-left in screen space) and scale (screen->viewport).
+    pub fn set_viewport_transform(&mut self, offset: Vec2, scale: Vec2) {
+        self.windowing.viewport_offset = offset;
+        self.windowing.viewport_scale = scale;
+    }
+
+    /// Pushes the vertices and indices to the renderer.
+    ///
+    /// This is the recommended `render()` function and is used when you want
+    /// `kino_ui` to create the render pass and submit to the queue.
+    ///
+    /// This sits inside your `render()` loop.
+    pub fn render(
+        &mut self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        encoder: &mut wgpu::CommandEncoder,
+        view: &wgpu::TextureView,
+    ) {
+        log::trace!("rendering kinostate");
+        self.renderer.upload_camera_matrix(
+            queue,
+            self.camera
+                .view_proj(self.renderer.size)
+                .to_cols_array_2d(),
+        );
+        let batch = self.batch.take();
+
+        let (width, height) = {
+            let tex = view.texture();
+            (tex.width(), tex.height())
+        };
+
+        self.renderer.text.prepare(&device, &queue, width, height);
+
+        {
+            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("kino render pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view,
+                    depth_slice: None,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: LoadOp::Load,
+                        store: StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            for mut tg in batch {
+                // log::debug!("Rendering textured geometry: {:?}", tg);
+                let texture = tg.texture_id.and_then(|v| {
+                    self.assets.get_texture(v)
+                });
+                self.renderer.draw_batch(&mut pass, device, queue, &mut tg.batch, texture);
+            }
+
+            self.renderer.text.render(&mut pass);
+        }
+    }
+
+    /// Pushes the vertices and indices to the renderer.
+    ///
+    /// This is used when you want control on the [`wgpu::RenderPass`] and you want
+    /// `kino_ui` to only draw the widgets.
+    ///
+    /// This sits inside your `render()` loop.
+    pub fn render_into_pass<'a>(
+        &'a mut self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        pass: &mut wgpu::RenderPass<'a>,
+    ) {
+        self.renderer.upload_camera_matrix(
+            queue,
+            self.camera
+                .view_proj(self.renderer.size)
+                .to_cols_array_2d(),
+        );
+        let batch = self.batch.take();
+
+        for mut tg in batch {
+            // log::debug!("Rendering textured geometry: {:?}", tg);
+            let texture = tg.texture_id.and_then(|v| {
+                self.assets.get_texture(v)
+            });
+            self.renderer.draw_batch(pass, device, queue, &mut tg.batch, texture);
+        }
+
+        self.renderer.text.render(pass);
+    }
+
+    /// Handles the event into the internal input state.
+    ///
+    /// This is not required, however if you want reactivity, include it into your WindowEvent code.
+    pub fn handle_event(&mut self, event: &winit::event::WindowEvent) {
+        self.windowing.handle_event(event);
+    }
+
+    /// Creates (or reuses) a texture from raw RGBA bytes or encoded image bytes
+    /// (e.g. `include_bytes!()` PNG/JPEG) and stores it by label.
+    /// If a texture with the same content hash already exists, it reuses the handle
+    /// and just updates the label mapping.
+    pub fn add_texture_from_bytes(
+        &mut self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        label: impl Into<String>,
+        data: &[u8],
+        width: u32,
+        height: u32,
+    ) -> Handle<Texture> {
+        let mut raw: Cow<[u8]> = Cow::Borrowed(data);
+        let mut width = width;
+        let mut height = height;
+        let expected_len = (width as usize)
+            .saturating_mul(height as usize)
+            .saturating_mul(4);
+
+        if data.len() != expected_len {
+            match image::load_from_memory(data) {
+                Ok(img) => {
+                    let rgba = img.to_rgba8();
+                    let (w, h) = rgba.dimensions();
+                    width = w;
+                    height = h;
+                    raw = Cow::Owned(rgba.into_raw());
+                }
+                Err(e) => {
+                    log::error!("Failed to decode texture bytes: {}", e);
+                    let fallback = Texture::create_default(
+                        device,
+                        queue,
+                        self.renderer.texture_bind_group_layout(),
+                        self.renderer.texture_format(),
+                    );
+                    return self.assets.add_texture_with_label(label, fallback);
+                }
+            }
+        }
+
+        let hash = AssetServer::hash_bytes(raw.as_ref());
+        if let Some(handle) = self.assets.texture_handle_by_hash(hash) {
+            self.assets.label_texture(label, handle.clone());
+            return handle;
+        }
+
+        let texture = Texture::from_bytes(
+            device,
+            queue,
+            self.renderer.texture_bind_group_layout(),
+            raw.as_ref(),
+            width,
+            height,
+            self.renderer.texture_format(),
+        );
+        self.assets.add_texture_with_label(label, texture)
+    }
+
+    /// Fetch a texture handle by label.
+    pub fn texture_handle(&self, label: &str) -> Option<Handle<Texture>> {
+        self.assets.get_texture_handle(label)
+    }
+
+    /// Returns a reference to the windowing. Used for input detection.
+    pub(crate) fn input(&self) -> &KinoWinitWindowing {
+        &self.windowing
+    }
+
+    pub(crate) fn set_response(&mut self, id: WidgetId, response: WidgetResponse) {
+        self.widget_states.insert(id, response);
+    }
+
+    pub(crate) fn layout_offset(&self) -> Vec2 {
+        self.container_stack
+            .last()
+            .map(|ctx| ctx.offset)
+            .unwrap_or(Vec2::ZERO)
+    }
+
+    pub(crate) fn clip_contains(&self, point: Vec2) -> bool {
+        match self.container_stack.last().and_then(|ctx| ctx.clip) {
+            Some(rect) => rect.contains(point),
+            None => self.container_stack.is_empty(),
+        }
+    }
+
+    pub(crate) fn push_container(&mut self, rect: Rect) {
+        let parent_offset = self.layout_offset();
+        let world_rect = Rect::new(rect.position + parent_offset, rect.size);
+        let clip = match self.container_stack.last().and_then(|ctx| ctx.clip) {
+            Some(parent_clip) => intersect_rects(parent_clip, world_rect),
+            None => Some(world_rect),
+        };
+        self.container_stack.push(ContainerContext {
+            offset: world_rect.position,
+            clip,
+        });
+    }
+
+    pub(crate) fn pop_container(&mut self) {
+        self.container_stack.pop();
+    }
+
+    fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> {
+        let mut stack: Vec<UiNode> = Vec::new();
+        let mut root = Vec::new();
+
+        for instruction in instructions {
+            match &instruction {
+                UiInstructionType::Containered(container_ty) => {
+                    match container_ty {
+                        ContaineredWidgetType::Start { .. } => {
+                            stack.push(UiNode {
+                                instruction,
+                                children: Vec::new(),
+                            });
+                        }
+                        ContaineredWidgetType::End { .. } => {
+                            if let Some(node) = stack.pop() {
+                                if let Some(parent) = stack.last_mut() {
+                                    parent.children.push(node);
+                                } else {
+                                    root.push(node);
+                                }
+                            }
+                        }
+                    }
+                }
+                UiInstructionType::Widget(_) => {
+                    let node = UiNode {
+                        instruction,
+                        children: Vec::new(),
+                    };
+
+                    if let Some(parent) = stack.last_mut() {
+                        parent.children.push(node);
+                    } else {
+                        root.push(node);
+                    }
+                }
+            }
+        }
+
+        root
+    }
+
+    pub(crate) fn render_tree(&mut self, nodes: Vec<UiNode>) {
+        for node in nodes {
+            match node.instruction {
+                UiInstructionType::Containered(container_ty) => {
+                    match container_ty {
+                        ContaineredWidgetType::Start { widget, .. } => {
+                            log::trace!("Rendering containered widget START");
+                            widget.render(node.children, self);
+                        }
+                        ContaineredWidgetType::End { .. } => {
+                            log::trace!("Rendering end widget END");
+                            // already handled in tree building
+                        }
+                    }
+                }
+                UiInstructionType::Widget(widget) => {
+                    log::trace!("Rendering widget: {:?}", widget.id());
+                    widget.render(self);
+                }
+            }
+        }
+    }
+}
+
+/// The id of the widget, often being a hash.
+#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
+pub struct WidgetId(u64);
+
+impl Default for WidgetId {
+    fn default() -> Self {
+        WidgetId(0) // dummy value
+    }
+}
+
+impl WidgetId {
+    /// Creates a new [`WidgetId`] from an object that can be hashed.
+    pub fn new<H: Hash>(value: H) -> Self {
+        let mut hasher = DefaultHasher::new();
+        value.hash(&mut hasher);
+        WidgetId(hasher.finish())
+    }
+
+    /// Creates a new [`WidgetId`] from a simple u64 value.
+    pub fn from_raw(value: u64) -> Self {
+        WidgetId(value)
+    }
+
+    pub fn get_id(&self) -> u64 {
+        self.0
+    }
+}
+
+impl Into<WidgetId> for &str {
+    fn into(self) -> WidgetId {
+        let mut hasher = DefaultHasher::new();
+        self.hash(&mut hasher);
+        WidgetId(hasher.finish())
+    }
+}
+
+impl Into<WidgetId> for String {
+    fn into(self) -> WidgetId {
+        let mut hasher = DefaultHasher::new();
+        self.hash(&mut hasher);
+        WidgetId(hasher.finish())
+    }
+}
+
+impl Into<WidgetId> for u64 {
+    fn into(self) -> WidgetId {
+        WidgetId(self)
+    }
+}
+
+impl<T: Hash, U: Hash> Into<WidgetId> for (T, U) {
+    fn into(self) -> WidgetId {
+        let mut hasher = DefaultHasher::new();
+        self.0.hash(&mut hasher);
+        self.1.hash(&mut hasher);
+        WidgetId(hasher.finish())
+    }
+}
+
+pub enum UiInstructionType {
+    Containered(ContaineredWidgetType),
+    Widget(Box<dyn NativeWidget>),
+}
+
+pub enum ContaineredWidgetType {
+    Start {
+        id: WidgetId,
+        widget: Box<dyn ContaineredWidget>,
+    },
+    End {
+        id: WidgetId,
+    }
+}
+
+pub struct UiNode {
+    pub instruction: UiInstructionType,
+    pub children: Vec<UiNode>,
+}
+
+#[derive(Clone, Copy, Debug)]
+struct ContainerContext {
+    offset: Vec2,
+    clip: Option<Rect>,
+}
+
+#[derive(Debug, Default)]
+pub struct TexturedGeometry {
+    pub texture_id: Option<Handle<Texture>>,
+    pub batch: VertexBatch,
+}
+
+#[derive(Default)]
+pub struct PrimitiveBatch {
+    geometry: Vec<TexturedGeometry>,
+}
+
+impl PrimitiveBatch {
+    /// Add verts & indices to batch, preserving submission order & batching consecutive geometry per texture
+    pub fn push(&mut self, verts: &[Vertex], indices: &[u16], texture_id: Option<Handle<Texture>>) {
+        if let Some(tg) = self.geometry.last_mut()
+            && tg.texture_id == texture_id
+        {
+            tg.batch.push(verts, indices);
+            return;
+        }
+
+        let mut batch = VertexBatch::default();
+        batch.push(verts, indices);
+        self.geometry.push(TexturedGeometry { texture_id, batch });
+    }
+
+    pub(crate) fn take(&mut self) -> Vec<TexturedGeometry> {
+        std::mem::take(&mut self.geometry)
+    }
+}
+
+fn intersect_rects(a: Rect, b: Rect) -> Option<Rect> {
+    let min = a.min().max(b.min());
+    let max = a.max().min(b.max());
+    if max.cmpge(min).all() {
+        Some(Rect::new(min, max - min))
+    } else {
+        None
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/math.rs b/crates/kino-ui/src/math.rs
new file mode 100644
index 0000000..1f8561a
--- /dev/null
+++ b/crates/kino-ui/src/math.rs
@@ -0,0 +1,49 @@
+use glam::{vec2, Vec2};
+
+/// A rectangular shape consisting of a `position` and a `size`. 
+#[derive(Copy, Clone, PartialEq, Debug)]
+pub struct Rect {
+    pub position: Vec2,
+    pub size: Vec2,
+}
+
+impl Rect {
+    /// Create a new rectangle from position (top-left) & size
+    pub fn new(position: Vec2, size: Vec2) -> Self {
+        Self { position, size }
+    }
+
+    /// Returns the top-left corner (min coords)
+    pub fn min(&self) -> Vec2 {
+        self.position
+    }
+
+    /// Returns the bottom-right corner (max coords)
+    pub fn max(&self) -> Vec2 {
+        self.position + self.size
+    }
+
+    /// Returns the center point of the rectangle
+    pub fn center(&self) -> Vec2 {
+        self.position + self.size * 0.5
+    }
+
+    /// Move the rectangle by the given delta vector
+    pub fn translate(&mut self, delta: Vec2) {
+        self.position += delta;
+    }
+
+    /// Returns true if the point is inside of the rectangle
+    pub fn contains(&self, point: Vec2) -> bool {
+        point.cmpge(self.position).all() && point.cmple(self.position + self.size).all()
+    }
+
+    /// Returns the four corners in this order: top-left, top-right, bottom-right, bottom-left
+    pub fn corners(&self) -> [Vec2; 4] {
+        let tl = self.position;
+        let tr = vec2(tl.x + self.size.x, tl.y);
+        let br = vec2(tl.x + self.size.x, tl.y + self.size.y);
+        let bl = vec2(tl.x, tl.y + self.size.y);
+        [tl, tr, br, bl]
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering.rs b/crates/kino-ui/src/rendering.rs
new file mode 100644
index 0000000..2d74b9d
--- /dev/null
+++ b/crates/kino-ui/src/rendering.rs
@@ -0,0 +1,122 @@
+use glam::Vec2;
+use wgpu::util::{BufferInitDescriptor, DeviceExt};
+use batching::VertexBatch;
+use crate::camera::{CameraRendering, CameraUniform};
+use crate::rendering::pipeline::KinoRendererPipeline;
+use crate::rendering::text::KinoTextRenderer;
+
+pub mod pipeline;
+pub mod texture;
+pub mod vertex;
+pub mod batching;
+pub mod text;
+
+pub struct KinoWGPURenderer {
+    pipeline: KinoRendererPipeline,
+    default_texture: texture::Texture,
+    pub format: wgpu::TextureFormat,
+    texture_format: wgpu::TextureFormat,
+    pub size: Vec2,
+    pub text: KinoTextRenderer,
+
+    pub camera: CameraRendering,
+}
+
+impl KinoWGPURenderer {
+    /// Creates a new `wgpu` renderer for the kino ui system.
+    pub fn new(
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        surface_format: wgpu::TextureFormat,
+        size: [f32; 2],
+    ) -> Self {
+        log::debug!("Creating KinoWGPURenderer");
+        let pipeline = KinoRendererPipeline::new(device, surface_format);
+        let texture_format = wgpu::TextureFormat::Rgba8UnormSrgb;
+
+        let camera_buffer = device.create_buffer_init(&BufferInitDescriptor {
+            label: None,
+            contents: bytemuck::bytes_of(&CameraUniform {
+                view_proj: [
+                    [1.0, 0.0, 0.0, 0.0],
+                    [0.0, 1.0, 0.0, 0.0],
+                    [0.0, 0.0, 1.0, 0.0],
+                    [0.0, 0.0, 0.0, 1.0],
+                ],
+            }),
+            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+        });
+
+        let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: None,
+            layout: &pipeline.camera_bind_group_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: camera_buffer.as_entire_binding(),
+            }],
+        });
+
+        let default_texture = texture::Texture::create_default(
+            &device,
+            &queue,
+            &pipeline.texture_bind_group_layout,
+            texture_format,
+        );
+        let text = KinoTextRenderer::new(&device, &queue, surface_format);
+
+        log::debug!("Created KinoWGPURenderer");
+        Self {
+            pipeline,
+            default_texture,
+            format: surface_format,
+            texture_format,
+            size: Vec2::from_array(size),
+            text,
+            camera: CameraRendering {
+                buffer: camera_buffer,
+                bind_group: camera_bind_group,
+            },
+        }
+    }
+
+    pub fn texture_format(&self) -> wgpu::TextureFormat {
+        self.texture_format
+    }
+
+    pub fn upload_camera_matrix(&mut self, queue: &wgpu::Queue, view_proj: [[f32; 4]; 4]) {
+        queue.write_buffer(
+            &self.camera.buffer,
+            0,
+            bytemuck::bytes_of(&CameraUniform { view_proj }),
+        );
+    }
+
+    pub fn draw_batch(
+        &self,
+        r_pass: &mut wgpu::RenderPass<'_>,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        batch: &mut VertexBatch,
+        texture: Option<&texture::Texture>,
+    ) {
+        if batch.is_empty() {
+            return;
+        }
+        batch.upload(&device, &queue);
+
+        let texture = texture.unwrap_or(&self.default_texture);
+
+        texture.bind(r_pass, 0);
+
+        r_pass.set_pipeline(&self.pipeline.pipeline);
+        r_pass.set_bind_group(1, &self.camera.bind_group, &[]);
+
+        batch.draw(r_pass);
+        batch.clear();
+    }
+
+    pub(crate) fn texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
+        &self.pipeline.texture_bind_group_layout
+    }
+}
+
diff --git a/crates/kino-ui/src/rendering/batching.rs b/crates/kino-ui/src/rendering/batching.rs
new file mode 100644
index 0000000..9abca52
--- /dev/null
+++ b/crates/kino-ui/src/rendering/batching.rs
@@ -0,0 +1,120 @@
+use wgpu::{BufferUsages, IndexFormat};
+use crate::rendering::vertex::Vertex;
+
+/// Describes a primitive shape.
+#[derive(Debug)]
+pub struct VertexBatch {
+    vertices: Vec<Vertex>,
+    indices: Vec<u16>,
+    vertex_buffer: Option<wgpu::Buffer>,
+    index_buffer: Option<wgpu::Buffer>,
+    vertices_dirty: bool,
+    indices_dirty: bool,
+}
+
+impl Default for VertexBatch {
+    fn default() -> Self {
+        Self {
+            vertices: Vec::with_capacity(Self::MAX_VERTICES),
+            indices: Vec::with_capacity(Self::MAX_INDICES),
+            vertex_buffer: None,
+            index_buffer: None,
+            vertices_dirty: false,
+            indices_dirty: false,
+        }
+    }
+}
+
+impl VertexBatch {
+    const MAX_VERTICES: usize = u16::MAX as usize;
+    const MAX_INDICES: usize = Self::MAX_VERTICES * 6;
+
+    /// Returns true if adding verts/indices would exceed max allowed
+    fn would_overflow(&self, vert_count: usize, idx_count: usize) -> bool {
+        self.vertices.len() + vert_count > Self::MAX_VERTICES
+            || self.indices.len() + idx_count > Self::MAX_INDICES
+    }
+
+    /// Adds vertices/indices, returns false if it would overflow
+    pub fn push(&mut self, verts: &[Vertex], indices: &[u16]) -> bool {
+        if self.would_overflow(verts.len(), indices.len()) {
+            return false;
+        }
+
+        let idx_offset = self.vertices.len() as u16;
+        self.vertices.extend_from_slice(verts);
+        self.indices.extend(indices.iter().map(|i| *i + idx_offset));
+
+        self.vertices_dirty = true;
+        self.indices_dirty = true;
+
+        true
+    }
+
+    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
+        if self.is_empty() || (!self.vertices_dirty && !self.indices_dirty) {
+            return;
+        }
+
+        if self.vertex_buffer.is_none() {
+            self.vertex_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some("kino vertex buffer"),
+                size: (Self::MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
+                usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
+                mapped_at_creation: false,
+            }));
+        }
+        if self.index_buffer.is_none() {
+            self.index_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some("kino index buffer"),
+                size: (Self::MAX_INDICES * std::mem::size_of::<u16>()) as u64,
+                usage: BufferUsages::INDEX | BufferUsages::COPY_DST,
+                mapped_at_creation: false,
+            }));
+        }
+
+        if self.vertices_dirty {
+            queue.write_buffer(
+                self.vertex_buffer.as_ref().unwrap(),
+                0,
+                bytemuck::cast_slice(&self.vertices),
+            );
+            self.vertices_dirty = false;
+        }
+        if self.indices_dirty {
+            let mut indices_bytes: Vec<u8> = bytemuck::cast_slice(&self.indices).to_vec();
+            let remainder = indices_bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize;
+            if remainder != 0 {
+                let pad_len = wgpu::COPY_BUFFER_ALIGNMENT as usize - remainder;
+                indices_bytes.extend_from_slice(&vec![0u8; pad_len]);
+            }
+
+            queue.write_buffer(self.index_buffer.as_ref().unwrap(), 0, &indices_bytes);
+            self.indices_dirty = false;
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.vertices.is_empty() || self.indices.is_empty()
+    }
+
+    pub fn clear(&mut self) {
+        self.vertices.clear();
+        self.indices.clear();
+        self.vertices_dirty = true;
+        self.indices_dirty = true;
+    }
+
+    pub fn draw(&self, pass: &mut wgpu::RenderPass) {
+        if self.is_empty() {
+            return;
+        }
+
+        pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
+        pass.set_index_buffer(
+            self.index_buffer.as_ref().unwrap().slice(..),
+            IndexFormat::Uint16,
+        );
+        pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering/pipeline.rs b/crates/kino-ui/src/rendering/pipeline.rs
new file mode 100644
index 0000000..2ade512
--- /dev/null
+++ b/crates/kino-ui/src/rendering/pipeline.rs
@@ -0,0 +1,113 @@
+use crate::rendering::vertex::Vertex;
+
+pub struct KinoRendererPipeline {
+    pub(crate) pipeline: wgpu::RenderPipeline,
+    pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
+    pub(crate) camera_bind_group_layout: wgpu::BindGroupLayout,
+}
+
+impl KinoRendererPipeline {
+    pub fn new(
+        device: &wgpu::Device,
+        surface_format: wgpu::TextureFormat,
+    ) -> Self {
+        log::debug!("Initialising pipeline with format {:?}", surface_format);
+        let shader = device.create_shader_module(wgpu::include_wgsl!("../shaders/ui.wgsl"));
+
+        let texture_bind_group_layout = Self::create_texture_bind_group_layout(device);
+        let camera_bind_group_layout = Self::create_camera_bind_group_layout(device);
+
+        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("kino pipeline layout descriptor"),
+            bind_group_layouts: &[
+                &texture_bind_group_layout,
+                &camera_bind_group_layout,
+            ],
+            push_constant_ranges: &[],
+        });
+
+        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("kino render pipeline"),
+            layout: Some(&layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                compilation_options: Default::default(),
+                buffers: &[Vertex::desc()],
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &shader,
+                entry_point: Some("fs_main"),
+                compilation_options: Default::default(),
+                targets: &[Some(wgpu::ColorTargetState {
+                    format: surface_format,
+                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+                    write_mask: wgpu::ColorWrites::ALL,
+                })],
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleList,
+                cull_mode: None,
+                ..Default::default()
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState::default(),
+            multiview: None,
+            cache: None,
+        });
+        
+        log::debug!("Created pipeline");
+
+        Self {
+            pipeline,
+            texture_bind_group_layout,
+            camera_bind_group_layout,
+        }
+    }
+
+    fn create_texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
+        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("kino texture bind group layout"),
+            entries: &[
+                // texture binding
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        multisampled: false,
+                    },
+                    count: None,
+                },
+
+                // texture sampler
+                wgpu::BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                    count: None,
+                },
+            ],
+        })
+    }
+
+    fn create_camera_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
+        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("kino camera bind group layout"),
+            entries: &[
+                // camera uniform
+                wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::VERTEX,
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                }
+            ],
+        })
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering/text.rs b/crates/kino-ui/src/rendering/text.rs
new file mode 100644
index 0000000..dd5291d
--- /dev/null
+++ b/crates/kino-ui/src/rendering/text.rs
@@ -0,0 +1,97 @@
+use glam::Vec2;
+use glyphon::{Buffer, Cache, Color, FontSystem, Resolution, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport};
+
+pub struct TextEntry {
+    pub buffer: Buffer,
+    pub position: Vec2,
+}
+
+pub struct KinoTextRenderer {
+    pub font_system: FontSystem,
+    pub swash_cache: SwashCache,
+    pub atlas: TextAtlas,
+    pub renderer: TextRenderer,
+    pub viewport: Viewport,
+    pub entries: Vec<TextEntry>,
+}
+
+impl KinoTextRenderer {
+    pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
+        log::debug!("Creating KinoTextRenderer");
+        log::debug!("Loading system fonts");
+        let mut font_system = FontSystem::new();
+        font_system
+            .db_mut()
+            .load_system_fonts();
+        log::debug!("Loaded system fonts");
+
+        log::debug!("Loading \"Roboto-Regular.ttf\" as fallback");
+        font_system.db_mut().load_font_data(include_bytes!("../../../../resources/fonts/Roboto-Regular.ttf").to_vec());
+        log::debug!("Loaded fallback");
+        let swash_cache = SwashCache::new();
+        let cache = Cache::new(device);
+        let viewport = Viewport::new(device, &cache);
+        let mut atlas = TextAtlas::new(device, queue, &cache, format);
+        let renderer = TextRenderer::new(&mut atlas, device, Default::default(), None);
+
+        log::debug!("Created new KinoTextRenderer");
+
+        Self {
+            font_system,
+            swash_cache,
+            atlas,
+            renderer,
+            viewport,
+            entries: Vec::new(),
+        }
+    }
+
+    /// Prepare the text renderer for drawing
+    pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) {
+        self.viewport.update(queue, Resolution { width, height });
+        if self.entries.is_empty() {
+            return;
+        }
+
+        let text_areas: Vec<TextArea> = self
+            .entries
+            .iter()
+            .map(|entry| TextArea {
+                buffer: &entry.buffer,
+                left: entry.position.x,
+                top: entry.position.y,
+                bounds: TextBounds {
+                    right: width as i32,
+                    bottom: height as i32,
+                    ..Default::default()
+                },
+                scale: 1.0,
+                default_color: Color::rgb(255, 255, 255),
+                custom_glyphs: &[],
+            })
+            .collect();
+        self.renderer
+            .prepare(
+                device,
+                queue,
+                &mut self.font_system,
+                &mut self.atlas,
+                &self.viewport,
+                text_areas,
+                &mut self.swash_cache,
+            )
+            .unwrap();
+
+        self.entries.clear();
+    }
+
+    pub fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
+        self.renderer
+            .render(&self.atlas, &self.viewport, pass)
+            .unwrap();
+    }
+
+    pub fn resize(&mut self, width: u32, height: u32, queue: &wgpu::Queue) {
+        self.viewport.update(queue, Resolution { width, height });
+    }
+}
diff --git a/crates/kino-ui/src/rendering/texture.rs b/crates/kino-ui/src/rendering/texture.rs
new file mode 100644
index 0000000..e38ca8a
--- /dev/null
+++ b/crates/kino-ui/src/rendering/texture.rs
@@ -0,0 +1,140 @@
+use std::hash::{DefaultHasher, Hash, Hasher};
+use wgpu::{
+    BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindingResource, Device,
+    Extent3d, Origin3d, Queue, RenderPass, TexelCopyBufferLayout, TexelCopyTextureInfo,
+    TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
+};
+
+/// A GPU texture that can be bound in shaders for rendering
+///
+/// Wraps a `wgpu::Texture`, its view, sampler, & bind group
+#[derive(Debug, PartialEq)]
+pub struct Texture {
+    pub(crate) hash: u64,
+    bind_group: BindGroup,
+}
+
+impl Texture {
+    fn bytes_per_pixel(format: TextureFormat) -> Option<u32> {
+        match format {
+            TextureFormat::Rgba8Unorm
+            | TextureFormat::Rgba8UnormSrgb
+            | TextureFormat::Bgra8Unorm
+            | TextureFormat::Bgra8UnormSrgb => Some(4),
+            TextureFormat::Rgba16Float
+            | TextureFormat::Rgba16Unorm
+            | TextureFormat::Rgba16Snorm
+            | TextureFormat::Rgba16Uint
+            | TextureFormat::Rgba16Sint => Some(8),
+            TextureFormat::Rgba32Float
+            | TextureFormat::Rgba32Uint
+            | TextureFormat::Rgba32Sint => Some(16),
+            _ => None,
+        }
+    }
+
+    /// Creates a new texture from raw RGBA image data,
+    /// uploads the data, & builds the bind group using the layout
+    ///
+    /// - `data`: Must be in tightly packed 8-bit RGBA format
+    /// - `width`, `height`: Dimensions of the image in pixels
+    pub fn from_bytes(
+        device: &Device,
+        queue: &Queue,
+        bind_group_layout: &BindGroupLayout,
+        data: &[u8],
+        width: u32,
+        height: u32,
+        texture_format: TextureFormat,
+    ) -> Self {
+        log::debug!("Creating new texture");
+
+        let bytes_per_pixel = Self::bytes_per_pixel(texture_format).unwrap_or(4);
+        let expected_len = (width as usize)
+            .saturating_mul(height as usize)
+            .saturating_mul(bytes_per_pixel as usize);
+        if data.len() != expected_len {
+            log::error!(
+                "Texture data length {} does not match expected {} for {:?}",
+                data.len(),
+                expected_len,
+                texture_format
+            );
+        }
+        
+        let texture = device.create_texture(&TextureDescriptor {
+            label: None,
+            size: Extent3d {
+                width,
+                height,
+                depth_or_array_layers: 1,
+            },
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: TextureDimension::D2,
+            format: texture_format,
+            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
+            view_formats: &[],
+        });
+
+        queue.write_texture(
+            TexelCopyTextureInfo {
+                texture: &texture,
+                mip_level: 0,
+                origin: Origin3d::ZERO,
+                aspect: TextureAspect::All,
+            },
+            data,
+            TexelCopyBufferLayout {
+                offset: 0,
+                bytes_per_row: Some(bytes_per_pixel * width),
+                rows_per_image: Some(height),
+            },
+            Extent3d {
+                width,
+                height,
+                depth_or_array_layers: 1,
+            },
+        );
+
+        let view = texture.create_view(&Default::default());
+        let sampler = device.create_sampler(&Default::default());
+        let bind_group = device.create_bind_group(&BindGroupDescriptor {
+            label: None,
+            layout: bind_group_layout,
+            entries: &[
+                BindGroupEntry {
+                    binding: 0,
+                    resource: BindingResource::TextureView(&view),
+                },
+                BindGroupEntry {
+                    binding: 1,
+                    resource: BindingResource::Sampler(&sampler),
+                },
+            ],
+        });
+
+        let mut hasher = DefaultHasher::new();
+        data.hash(&mut hasher);
+        let hash = hasher.finish();
+        
+        log::debug!("Created new texture [{}]", hash);
+        
+        Self { hash, bind_group }
+    }
+
+    /// Creates a 1×1 white fallback texture
+    ///
+    /// Used when no valid texture is provided for a draw call
+    pub fn create_default(device: &Device, queue: &Queue, layout: &BindGroupLayout, texture_format: TextureFormat) -> Self {
+        log::debug!("Creating standard white texture");
+        Self::from_bytes(device, queue, layout, &[255u8, 255, 255, 255], 1, 1, texture_format)
+    }
+
+    /// Binds this texture at the given index in the render pass
+    ///
+    /// - `index` must match the bind group index used in the pipeline layout
+    pub fn bind(&self, pass: &mut RenderPass, index: u32) {
+        pass.set_bind_group(index, &self.bind_group, &[]);
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering/vertex.rs b/crates/kino-ui/src/rendering/vertex.rs
new file mode 100644
index 0000000..25836ca
--- /dev/null
+++ b/crates/kino-ui/src/rendering/vertex.rs
@@ -0,0 +1,48 @@
+use bytemuck::{Pod, Zeroable};
+
+#[repr(C)]
+#[derive(Copy, Clone, Pod, Zeroable, Debug)]
+pub struct Vertex {
+    pub position: [f32; 2],
+    pub colour: [f32; 4],
+    pub tex_coord: [f32; 2],
+}
+
+impl Vertex {
+    pub fn new(position: impl Into<[f32; 2]>, colour: impl Into<[f32; 4]>, tex_coord: impl Into<[f32; 2]>) -> Self {
+        Self {
+            position: position.into(),
+            colour: colour.into(),
+            tex_coord: tex_coord.into(),
+        }
+    }
+    
+    pub fn desc() -> wgpu::VertexBufferLayout<'static> {
+        wgpu::VertexBufferLayout {
+            array_stride: size_of::<Vertex>() as wgpu::BufferAddress,
+            step_mode: wgpu::VertexStepMode::Vertex,
+            attributes: &[
+                // position
+                wgpu::VertexAttribute {
+                    format: wgpu::VertexFormat::Float32x2,
+                    offset: 0,
+                    shader_location: 0,
+                },
+
+                // colour
+                wgpu::VertexAttribute {
+                    format: wgpu::VertexFormat::Float32x4,
+                    offset: size_of::<[f32; 2]>() as wgpu::BufferAddress,
+                    shader_location: 1,
+                },
+
+                // tex_coords
+                wgpu::VertexAttribute {
+                    format: wgpu::VertexFormat::Float32x2,
+                    offset: size_of::<[f32; 6]>() as wgpu::BufferAddress,
+                    shader_location: 2,
+                }
+            ],
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/resp.rs b/crates/kino-ui/src/resp.rs
new file mode 100644
index 0000000..dad460d
--- /dev/null
+++ b/crates/kino-ui/src/resp.rs
@@ -0,0 +1,8 @@
+use crate::{WidgetId};
+
+#[derive(Clone, Copy, Debug, Default)]
+pub struct WidgetResponse {
+    pub queried: WidgetId,
+    pub clicked: bool,
+    pub hovering: bool,
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/shaders/ui.wgsl b/crates/kino-ui/src/shaders/ui.wgsl
new file mode 100644
index 0000000..597bb39
--- /dev/null
+++ b/crates/kino-ui/src/shaders/ui.wgsl
@@ -0,0 +1,38 @@
+@group(0) @binding(0)
+var texture_binding: texture_2d<f32>;
+
+@group(0) @binding(1)
+var texture_sampler: sampler;
+
+struct CameraUniform {
+    view_proj: mat4x4<f32>,
+};
+
+@group(1) @binding(0)
+var<uniform> camera: CameraUniform;
+
+struct VertexInput {
+    @location(0) position: vec2<f32>,
+    @location(1) colour: vec4<f32>,
+    @location(2) tex_coords: vec2<f32>,
+};
+
+struct VertexOutput {
+    @builtin(position) position: vec4<f32>,
+    @location(0) colour: vec4<f32>,
+    @location(1) tex_coords: vec2<f32>,
+};
+
+@vertex
+fn vs_main(input: VertexInput) -> VertexOutput {
+    var output: VertexOutput;
+    output.position = camera.view_proj * vec4<f32>(input.position, 0.0, 1.0);
+    output.colour = input.colour;
+    output.tex_coords = input.tex_coords;
+    return output;
+}
+
+@fragment
+fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
+    return textureSample(texture_binding, texture_sampler, input.tex_coords) * input.colour;
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/layout.rs b/crates/kino-ui/src/widgets/layout.rs
new file mode 100644
index 0000000..5f17de2
--- /dev/null
+++ b/crates/kino-ui/src/widgets/layout.rs
@@ -0,0 +1,218 @@
+// In widgets/layout.rs
+
+use std::any::Any;
+use glam::{vec2, Vec2};
+use crate::{KinoState, UiNode, UiInstructionType, ContaineredWidgetType, WidgetId};
+use crate::widgets::{Anchor, ContaineredWidget};
+
+fn calculate_node_size(node: &UiNode) -> Vec2 {
+    match &node.instruction {
+        UiInstructionType::Widget(widget) => widget.size(),
+        UiInstructionType::Containered(ContaineredWidgetType::Start { widget, .. }) => {
+            if let Some(row) = widget.as_any().downcast_ref::<Row>() {
+                calculate_row_size(&node.children, row.spacing)
+            } else if let Some(column) = widget.as_any().downcast_ref::<Column>() {
+                calculate_column_size(&node.children, column.spacing)
+            } else {
+                Vec2::ZERO
+            }
+        }
+        _ => Vec2::ZERO,
+    }
+}
+
+fn calculate_row_size(children: &[UiNode], spacing: f32) -> Vec2 {
+    if children.is_empty() {
+        return Vec2::ZERO;
+    }
+
+    let mut total_width = 0.0;
+    let mut max_height: f32 = 0.0;
+
+    for (i, child) in children.iter().enumerate() {
+        let child_size = calculate_node_size(child);
+        total_width += child_size.x;
+        max_height = max_height.max(child_size.y);
+
+        if i < children.len() - 1 {
+            total_width += spacing;
+        }
+    }
+
+    vec2(total_width, max_height)
+}
+
+fn calculate_column_size(children: &[UiNode], spacing: f32) -> Vec2 {
+    if children.is_empty() {
+        return Vec2::ZERO;
+    }
+
+    let mut total_height = 0.0;
+    let mut max_width: f32 = 0.0;
+
+    for (i, child) in children.iter().enumerate() {
+        let child_size = calculate_node_size(child);
+        total_height += child_size.y;
+        max_width = max_width.max(child_size.x);
+
+        if i < children.len() - 1 {
+            total_height += spacing;
+        }
+    }
+
+    vec2(max_width, total_height)
+}
+
+pub struct Row {
+    pub id: WidgetId,
+    pub anchor: Anchor,
+    pub position: Vec2,
+    pub spacing: f32,
+}
+
+impl Row {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            spacing: 8.0,
+        }
+    }
+
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+}
+
+impl ContaineredWidget for Row {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let total_size = calculate_row_size(&children, self.spacing);
+
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => vec2(-total_size.x / 2.0, 0.0),
+        };
+
+        let start_pos = self.position + offset;
+        let mut x_offset = 0.0;
+
+        for child in children {
+            let child_size = calculate_node_size(&child);
+
+            state.push_container(crate::math::Rect::new(
+                start_pos + vec2(x_offset, 0.0),
+                vec2(child_size.x, total_size.y)
+            ));
+
+            state.render_tree(vec![child]);
+            state.pop_container();
+
+            x_offset += child_size.x + self.spacing;
+        }
+    }
+
+    fn size(&self, children: &[UiNode]) -> Vec2 {
+        calculate_row_size(children, self.spacing)
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+pub struct Column {
+    pub id: WidgetId,
+    pub anchor: Anchor,
+    pub position: Vec2,
+    pub spacing: f32,
+}
+
+impl Column {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            spacing: 8.0,
+        }
+    }
+
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+}
+
+impl ContaineredWidget for Column {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let total_size = calculate_column_size(&children, self.spacing);
+
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => vec2(0.0, -total_size.y / 2.0),
+        };
+
+        let start_pos = self.position + offset;
+        let mut y_offset = 0.0;
+
+        for child in children {
+            let child_size = calculate_node_size(&child);
+
+            state.push_container(crate::math::Rect::new(
+                start_pos + vec2(0.0, y_offset),
+                vec2(total_size.x, child_size.y)
+            ));
+
+            state.render_tree(vec![child]);
+            state.pop_container();
+
+            y_offset += child_size.y + self.spacing;
+        }
+    }
+
+    fn size(&self, children: &[UiNode]) -> Vec2 {
+        calculate_column_size(children, self.spacing)
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/mod.rs b/crates/kino-ui/src/widgets/mod.rs
new file mode 100644
index 0000000..0f23c9a
--- /dev/null
+++ b/crates/kino-ui/src/widgets/mod.rs
@@ -0,0 +1,75 @@
+pub mod rect;
+pub mod shorthand;
+pub mod text;
+pub mod layout;
+
+use std::any::Any;
+use glam::Vec2;
+use crate::{KinoState, UiNode, WidgetId};
+
+/// Determines how the object is anchored.
+pub enum Anchor {
+    /// A center anchor is when the position is based on the center of the object (such as the
+    /// center of a circle)
+    Center,
+    /// A top left anchor is when the position is based on the top left corner of the rectangle.
+    TopLeft,
+}
+
+/// Defines a widget with no children. 
+pub trait NativeWidget: Send + Sync {
+    /// Renders the widget. 
+    /// 
+    /// The state is provided for you to manipulate, such as adding a new response based on the
+    /// [`WidgetId`]. 
+    fn render(self: Box<Self>, state: &mut KinoState);
+    fn size(&self) -> Vec2;
+    fn id(&self) -> WidgetId;
+    fn as_any(&self) -> &dyn Any;
+}
+
+pub trait ContaineredWidget: Send + Sync {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState);
+    fn size(&self, children: &[UiNode]) -> Vec2;
+    fn id(&self) -> WidgetId;
+    fn as_any(&self) -> &dyn Any;
+}
+
+/// Describes the colour that the widget will be filled in with. 
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct Fill {
+    /// The colour of the fill described as RGBA between the range `0.0` <-> `1.0`.
+    ///
+    /// If a texture is applied to the colour, it will create a tinted texture on the quad.
+    pub colour: [f32; 4],
+}
+
+impl Fill {
+    /// Creates a new [`Fill`]
+    pub fn new(colour: [f32; 4]) -> Self {
+        Fill { colour }
+    }
+}
+
+impl Default for Fill {
+    fn default() -> Self {
+        Fill { colour: [1.0, 1.0, 1.0, 1.0] }
+    }
+}
+
+/// Describes the properties of the border/stroke of the widget. 
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct Border {
+    /// The colour of the border described as RGBA between the range `0.0` <-> `1.0`.
+    pub colour: [f32; 4],
+    
+    /// The width of the border. 
+    pub width: f32,
+}
+
+impl Border {
+    /// Creates a new [`Border`]. 
+    pub fn new(colour: [f32; 4], width: f32) -> Self {
+        Self { colour, width }
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/rect.rs b/crates/kino-ui/src/widgets/rect.rs
new file mode 100644
index 0000000..e1917d7
--- /dev/null
+++ b/crates/kino-ui/src/widgets/rect.rs
@@ -0,0 +1,256 @@
+//! Defines the primitive [`Rectangle`] widget.
+
+use std::any::Any;
+use glam::{vec2, Mat2, Vec2};
+use crate::{KinoState, UiNode, WidgetId};
+use crate::asset::Handle;
+use crate::math::Rect;
+use crate::rendering::texture::Texture;
+use crate::rendering::vertex::Vertex;
+use crate::widgets::{Anchor, Border, ContaineredWidget, Fill, NativeWidget};
+use crate::resp::WidgetResponse;
+use winit::event::{ElementState, MouseButton};
+
+/// A simple and humble rectangle.
+pub struct Rectangle {
+    /// The identifier of the widget.
+    ///
+    /// To make life easier, a text id works pretty well.
+    pub id: WidgetId,
+
+    /// The positioning of the rectangle.
+    ///
+    /// Default: [`Anchor::TopLeft`]
+    pub anchor: Anchor,
+
+    /// The position of the rectangle
+    ///
+    /// Default: [`Vec2::ZERO`]
+    pub position: Vec2,
+
+    /// The size of the rectangle
+    ///
+    /// Default: [`vec2(64.0, 64.0)`]
+    pub size: Vec2,
+
+    /// The texture that this
+    ///
+    /// Default: [`None`]
+    pub texture: Option<Handle<Texture>>,
+
+    /// Rotation of the rectangle in radians
+    ///
+    /// Default: `0.0` rad / `0.0` degrees
+    pub rotation: f32,
+
+    /// The UV of the textures.
+    ///
+    /// Default: `[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]`
+    pub uvs: [[f32; 2]; 4],
+
+    /// The fill properties of the rectangle
+    ///
+    /// Default: `[1.0, 1.0, 1.0, 1.0]`
+    pub fill: Fill,
+
+    /// The stroke/border properties of the rectangle
+    ///
+    /// Default: [`None`]
+    pub border: Option<Border>,
+}
+
+impl Rectangle {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            size: vec2(64.0, 128.0),
+            rotation: 0.0,
+            uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
+            fill: Fill::default(),
+            texture: None,
+            border: None,
+        }
+    }
+
+    /// Sets the position & size from a [`Rect`].
+    pub fn with(mut self, rect: &Rect) -> Self {
+        self.position = rect.position;
+        self.size = rect.size;
+        self
+    }
+
+    /// Sets the anchor point of the rectangle
+    ///
+    /// Defaults to [`Anchor::TopLeft`].
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    /// Sets the world-space position of the rectangle
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    /// Sets the size of the rectangle
+    pub fn size(mut self, size: Vec2) -> Self {
+        self.size = size;
+        self
+    }
+
+    /// Sets the fill properties of the rectangle
+    pub fn fill(mut self, fill: Fill) -> Self {
+        self.fill = fill;
+        self
+    }
+
+    /// Sets the border properties of the rectangle
+    pub fn border(mut self, border: Border) -> Self {
+        self.border = Some(border);
+        self
+    }
+
+    /// Sets rotation (in radians) around the rectangle's center
+    /// 0 radians points up (positive Y), increasing clockwise
+    pub fn rotate(mut self, angle: f32) -> Self {
+        self.rotation = angle + std::f32::consts::FRAC_PI_2;
+        self
+    }
+
+    /// Sets the texture ID for the rectangle
+    pub fn texture(mut self, id: Handle<Texture>) -> Self {
+        self.texture = Some(id);
+        self
+    }
+
+    /// Custom UV coordinates
+    ///
+    /// Defaults to covering the full texture ((0,0) - (1,1))
+    pub fn uv(mut self, coords: [[f32; 2]; 4]) -> Self {
+        self.uvs = coords;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+
+    fn compute_rect(&self, state: &KinoState) -> (Vec2, Rect, Mat2) {
+        let container_offset = state.layout_offset();
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => -self.size / 2.0,
+        };
+        let local_top_left = self.position + offset;
+        let top_left = local_top_left + container_offset;
+        let rect = Rect::new(top_left, self.size);
+        let rot = Mat2::from_angle(self.rotation);
+        (local_top_left, rect, rot)
+    }
+
+    fn render_body(&self, state: &mut KinoState, rect: &Rect, rot: Mat2) {
+        let input = state.input();
+        let hovering = rect.contains(input.mouse_position)
+            && state.clip_contains(input.mouse_position);
+        let clicked = hovering
+            && input.mouse_button == MouseButton::Left
+            && input.mouse_press_state == ElementState::Pressed;
+        state.set_response(
+            self.id,
+            WidgetResponse {
+                queried: self.id,
+                clicked,
+                hovering,
+            },
+        );
+
+        let fill_verts: Vec<_> = rect
+            .corners()
+            .iter()
+            .zip(self.uvs.iter())
+            .map(|(&corner, &uv)| {
+                let world = rot * (corner - rect.center()) + rect.center();
+                Vertex::new(world.to_array(), self.fill.colour, uv)
+            })
+            .collect();
+
+        state.batch.push(&fill_verts, &[0, 1, 2, 2, 3, 0], self.texture);
+
+        if let Some(border) = self.border {
+            let half_width = border.width / 2.0;
+            let outer_rect = Rect::new(
+                rect.position - Vec2::splat(half_width),
+                rect.size + Vec2::splat(border.width)
+            );
+            let inner_rect = Rect::new(
+                rect.position + Vec2::splat(half_width),
+                rect.size - Vec2::splat(border.width)
+            );
+
+            let outer_corners = outer_rect.corners();
+            let inner_corners = inner_rect.corners();
+
+            let mut border_verts = Vec::with_capacity(8);
+            for i in 0..4 {
+                let outer_world = rot * (outer_corners[i] - rect.center()) + rect.center();
+                border_verts.push(Vertex::new(outer_world.to_array(), border.colour, [0.0, 0.0]));
+
+                let inner_world = rot * (inner_corners[i] - rect.center()) + rect.center();
+                border_verts.push(Vertex::new(inner_world.to_array(), border.colour, [0.0, 0.0]));
+            }
+
+            let border_indices = [
+                0, 1, 3, 3, 2, 0,
+                2, 3, 5, 5, 4, 2,
+                4, 5, 7, 7, 6, 4,
+                6, 7, 1, 1, 0, 6,
+            ];
+
+            state.batch.push(&border_verts, &border_indices, None);
+        }
+    }
+}
+
+impl NativeWidget for Rectangle {
+    fn render(self: Box<Self>, state: &mut KinoState) {
+        let (_local_top_left, rect, rot) = self.compute_rect(state);
+        self.render_body(state, &rect, rot);
+    }
+
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+impl ContaineredWidget for Rectangle {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let (local_top_left, rect, rot) = self.compute_rect(state);
+        self.render_body(state, &rect, rot);
+        state.push_container(Rect::new(local_top_left, self.size));
+        state.render_tree(children);
+        state.pop_container();
+    }
+
+    fn size(&self, _children: &[UiNode]) -> Vec2 {
+        self.size
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/shorthand.rs b/crates/kino-ui/src/widgets/shorthand.rs
new file mode 100644
index 0000000..75f5262
--- /dev/null
+++ b/crates/kino-ui/src/widgets/shorthand.rs
@@ -0,0 +1,80 @@
+use crate::{KinoState, WidgetId};
+use crate::widgets::layout::{Column, Row};
+use crate::widgets::rect::Rectangle;
+use crate::widgets::text::Text;
+
+/// Shorthand for a standard rectangle widget.
+pub fn rect<F>(kino: &mut KinoState, id: impl Into<WidgetId>, configure: F) -> WidgetId
+where
+    F: FnOnce(&mut Rectangle),
+{
+    let id = id.into();
+    let mut rect = Rectangle::new(id);
+    configure(&mut rect);
+    kino.add_widget(Box::new(rect));
+    id
+}
+
+/// Shorthand for a rectangle container.
+///
+/// `configure` sets up the rectangle, and `contents` emits child widgets between
+/// start/end instructions.
+pub fn rect_container<C>(
+    kino: &mut KinoState,
+    rect: Rectangle,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = rect.id;
+
+    kino.add_container(Box::new(rect));
+    contents(kino);
+    kino.end_container(id);
+    id
+}
+
+/// Shorthand for a standard label. 
+pub fn label<F>(kino: &mut KinoState, text: impl ToString, configure: F) -> WidgetId
+where
+    F: FnOnce(&mut Text),
+{
+    let mut text = Text::new(text);
+    configure(&mut text);
+    kino.add_widget(Box::new(text))
+}
+
+/// Shorthand for [`Row`], used for displaying items that are displayed horizontally.
+pub fn row<C>(
+    kino: &mut KinoState,
+    row: Row,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = row.id;
+
+    kino.add_container(Box::new(row));
+    contents(kino);
+    kino.end_container(id);
+    id
+}
+
+/// Shorthand for [`Column`], used for displaying items that are displayed vertically.
+pub fn column<C>(
+    kino: &mut KinoState,
+    column: Column,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = column.id;
+
+    kino.add_container(Box::new(column));
+    contents(kino);
+    kino.end_container(id);
+    id
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/text.rs b/crates/kino-ui/src/widgets/text.rs
new file mode 100644
index 0000000..06a7dcb
--- /dev/null
+++ b/crates/kino-ui/src/widgets/text.rs
@@ -0,0 +1,140 @@
+use std::any::Any;
+use glam::Vec2;
+use glyphon::{Attrs, AttrsOwned, Buffer, Color, Metrics, Shaping};
+use crate::{KinoState, WidgetId};
+use crate::math::Rect;
+use crate::rendering::text::TextEntry;
+use crate::resp::WidgetResponse;
+use crate::widgets::NativeWidget;
+use winit::event::{ElementState, MouseButton};
+
+/// Creates a label with the specified text and properties. 
+/// 
+/// # Input
+/// Responses are weird for text, as it recognises the input when you touch the text itself. 
+/// 
+/// If you want an area, you might be interested in [`crate::rect_container`] (with a transparent colour). 
+pub struct Text {
+    pub id: WidgetId,
+    pub text: String,
+    pub position: Vec2,
+    pub size: Vec2,
+    pub metrics: Metrics,
+    pub attributes: AttrsOwned,
+}
+
+impl Text {
+    /// Create a new text builder that will push text to the renderer
+    pub fn new(text: impl ToString) -> Self {
+        Self {
+            id: text.to_string().into(),
+            text: text.to_string(),
+            position: Vec2::new(10.0, 10.0),
+            size: Vec2::ZERO,
+            metrics: Metrics::new(16.0, 1.0),
+            attributes: AttrsOwned::new(&Attrs::new().color(Color::rgb(0, 0, 0))),
+        }
+    }
+
+    /// Set a custom ID before sending the widget off
+    pub fn with_id(mut self, id: WidgetId) -> Self {
+        self.id = id;
+        self
+    }
+
+    /// Set the position of text in screen space
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    /// Sets the position & size from a [`Rect`].
+    pub fn with(mut self, rect: &Rect) -> Self {
+        self.position = rect.position;
+        self.size = rect.size;
+        self
+    }
+
+    pub fn with_attrs(mut self, attributes: AttrsOwned) -> Self {
+        self.attributes = attributes;
+        self
+    }
+
+    pub fn with_metrics(mut self, metrics: Metrics) -> Self {
+        self.metrics = metrics;
+        self
+    }
+
+}
+
+impl NativeWidget for Text {
+    fn render(self: Box<Self>, state: &mut KinoState) {
+        let mut buffer = Buffer::new(&mut state.renderer.text.font_system, self.metrics);
+        if self.size != Vec2::ZERO {
+            buffer.set_size(
+                &mut state.renderer.text.font_system,
+                Some(self.size.x),
+                Some(self.size.y),
+            );
+        }
+        buffer.set_text(
+            &mut state.renderer.text.font_system,
+            &self.text,
+            &self.attributes.as_attrs(),
+            Shaping::Basic,
+        );
+
+        let mut max_x = 0.0f32;
+        let mut max_y = 0.0f32;
+        for run in buffer.layout_runs() {
+            if let Some(last) = run.glyphs.last() {
+                max_x = max_x.max(last.x + last.w);
+            }
+            max_y = max_y.max(run.line_top + run.line_height);
+        }
+        let intrinsic_size = Vec2::new(
+            max_x.max(1.0),
+            max_y.max(self.metrics.line_height),
+        );
+        let size = if self.size == Vec2::ZERO {
+            intrinsic_size
+        } else {
+            self.size
+        };
+
+        let top_left = self.position + state.layout_offset();
+        let rect = Rect::new(top_left, size);
+
+        let input = state.input();
+        let hovering = rect.contains(input.mouse_position)
+            && state.clip_contains(input.mouse_position);
+        let clicked = hovering
+            && input.mouse_button == MouseButton::Left
+            && input.mouse_press_state == ElementState::Pressed;
+        state.set_response(
+            self.id,
+            WidgetResponse {
+                queried: self.id,
+                clicked,
+                hovering,
+            },
+        );
+
+        state.renderer.text.entries.push(TextEntry {
+            buffer,
+            position: top_left,
+        });
+    }
+
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/windowing.rs b/crates/kino-ui/src/windowing.rs
new file mode 100644
index 0000000..2731bdd
--- /dev/null
+++ b/crates/kino-ui/src/windowing.rs
@@ -0,0 +1,110 @@
+use std::sync::Arc;
+use glam::Vec2;
+use winit::event::{ElementState, MouseButton, WindowEvent};
+use winit::window::Window;
+use crate::KinoState;
+
+pub struct KinoWinitWindowing {
+    // mouse
+    pub mouse_position: Vec2,
+    pub mouse_button: MouseButton,
+    pub mouse_press_state: ElementState,
+
+    // windowing
+    window: Arc<Window>,
+    /// The top-left most pixel
+    pub viewport_offset: Vec2,
+    /// Scale from screen-space to viewport texture space
+    pub viewport_scale: Vec2,
+
+    pub scale_factor: f32,
+    pub auto_scale: bool,
+}
+
+impl KinoWinitWindowing {
+    /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset.
+    pub fn new(window: Arc<Window>, scale_factor: Option<f32>) -> Self {
+        let auto_scale = scale_factor.is_none();
+        let scale_factor = scale_factor.unwrap_or(window.scale_factor() as f32);
+        Self {
+            mouse_position: Default::default(),
+            mouse_button: MouseButton::Left,
+            mouse_press_state: ElementState::Released,
+            window,
+            viewport_offset: Default::default(),
+            viewport_scale: Vec2::ONE,
+            scale_factor,
+            auto_scale,
+        }
+    }
+
+    /// Get the physical size of the window in pixels
+    pub fn physical_size(&self) -> (u32, u32) {
+        let size = self.window.inner_size();
+        (size.width, size.height)
+    }
+
+    /// Get the logical size of the window (physical size / scale_factor)
+    pub fn logical_size(&self) -> Vec2 {
+        let (w, h) = self.physical_size();
+        Vec2::new(w as f32, h as f32) / self.scale_factor
+    }
+
+    pub(crate) fn handle_event(&mut self, event: &WindowEvent) {
+        match event {
+            WindowEvent::CursorMoved { position, .. } => {
+                let screen_pos = Vec2::new(position.x as f32, position.y as f32);
+                let local = screen_pos - self.viewport_offset;
+                self.mouse_position = local * self.viewport_scale;
+            }
+            WindowEvent::MouseInput { state, button, .. } => {
+                self.mouse_button = *button;
+                self.mouse_press_state = *state;
+            }
+            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
+                if self.auto_scale { return; }
+                self.scale_factor = *scale_factor as f32;
+            }
+            WindowEvent::Resized(_) => {}
+            WindowEvent::KeyboardInput { .. } => {}
+            WindowEvent::MouseWheel { .. } => {}
+            _ => {}
+        }
+    }
+}
+
+impl KinoState {
+    /// Get the current DPI scale factor
+    pub fn scale_factor(&self) -> f32 {
+        self.windowing.scale_factor
+    }
+
+    /// Scale a logical size to physical pixels
+    pub fn to_physical(&self, logical: f32) -> f32 {
+        logical * self.windowing.scale_factor
+    }
+
+    /// Scale a logical Vec2 to physical pixels
+    pub fn to_physical_vec(&self, logical: Vec2) -> Vec2 {
+        logical * self.windowing.scale_factor
+    }
+
+    /// Scale physical pixels to logical size
+    pub fn to_logical(&self, physical: f32) -> f32 {
+        physical / self.windowing.scale_factor
+    }
+
+    /// Scale physical Vec2 to logical size
+    pub fn to_logical_vec(&self, physical: Vec2) -> Vec2 {
+        physical / self.windowing.scale_factor
+    }
+
+    pub fn set_scale_factor(&mut self, factor: Option<f32>) {
+        if let Some(factor) = factor {
+            self.windowing.scale_factor = factor;
+            self.windowing.auto_scale = false;
+        } else {
+            self.windowing.auto_scale = true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/redback-runtime/Cargo.toml b/crates/redback-runtime/Cargo.toml
index 83de71d..6e4bd89 100644
--- a/crates/redback-runtime/Cargo.toml
+++ b/crates/redback-runtime/Cargo.toml
@@ -9,6 +9,7 @@ readme = "README.md"
 [dependencies]
 dropbear-engine = { path = "../dropbear-engine" }
 eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false }
+kino-ui = { path = "../kino-ui" }
 
 anyhow.workspace = true
 log.workspace = true
@@ -28,9 +29,13 @@ serde.workspace = true
 crossbeam-channel.workspace = true
 gilrs.workspace = true
 futures.workspace = true
+egui.workspace = true
+egui_extras.workspace = true
 glam.workspace = true
 wgpu.workspace = true
-egui.workspace = true
+#yakui-winit.workspace = true
+#yakui.workspace = true
+#yakui-wgpu.workspace = true
 
 [features]
 debug = []
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 816d008..389d6d4 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -12,29 +12,39 @@ use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use futures::executor;
 use hecs::{Entity, World};
-use dropbear_engine::future::FutureHandle;
+use dropbear_engine::future::{FutureHandle, FutureQueue};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::scene::SceneCommand;
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
-use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT};
-use eucalyptus_core::scene::loading::SCENE_LOADER;
+use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script};
+use eucalyptus_core::scene::loading::{SceneLoadResult, SCENE_LOADER};
 use eucalyptus_core::traits::registry::ComponentRegistry;
 use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, WorldPtr};
 use eucalyptus_core::command::COMMAND_BUFFER;
 use eucalyptus_core::scene::loading::IsSceneLoaded;
 use std::collections::HashMap;
 use std::path::PathBuf;
+use log::error;
+use wgpu::SurfaceConfiguration;
+use winit::window::Fullscreen;
+use dropbear_engine::sky::{HdrLoader, SkyPipeline, DEFAULT_SKY_TEXTURE};
+// use yakui_winit::YakuiWinit;
+use dropbear_engine::texture::Texture;
 use eucalyptus_core::physics::PhysicsState;
 use eucalyptus_core::rapier3d::prelude::*;
 use eucalyptus_core::register_components;
+use kino_ui::KinoState;
+use kino_ui::windowing::KinoWinitWindowing;
+use kino_ui::rendering::KinoWGPURenderer;
 
 mod scene;
 mod input;
 mod command;
 
+#[cfg(feature = "debug")]
 fn find_jvm_library_path() -> PathBuf {
-    let proj = PROJECT.read();
+    let proj = eucalyptus_core::states::PROJECT.read();
     let project_path = if !proj.project_path.is_dir() {
         proj.project_path
             .parent()
@@ -43,7 +53,7 @@ fn find_jvm_library_path() -> PathBuf {
     } else {
         proj.project_path.clone()
     }
-    .join("build/libs");
+        .join("build/libs");
 
     let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
 
@@ -72,6 +82,35 @@ fn find_jvm_library_path() -> PathBuf {
         .expect("No suitable candidate for a JVM targeted play mode session available")
 }
 
+#[cfg(not(feature = "debug"))]
+fn find_jvm_library_path() -> PathBuf {
+    let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
+
+    for entry in std::fs::read_dir(std::env::current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") {
+        let entry = entry.expect("Unable to get directory entry");
+        let path = entry.path();
+
+        if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
+            if filename.ends_with("-all.jar") {
+                let metadata = entry.metadata().expect("Unable to get file metadata");
+                let modified = metadata.modified().expect("Unable to get file modified time");
+
+                match latest_jar {
+                    None => latest_jar = Some((path.clone(), modified)),
+                    Some((_, latest_time)) if modified > latest_time => {
+                        latest_jar = Some((path.clone(), modified));
+                    }
+                    _ => {}
+                }
+            }
+        }
+    }
+
+    latest_jar
+        .map(|(path, _)| path)
+        .expect("No suitable candidate for a JVM targeted play mode session available")
+}
+
 pub struct PlayMode {
     scene_command: SceneCommand,
     input_state: InputState,
@@ -79,12 +118,14 @@ pub struct PlayMode {
     world: Box<World>,
     component_registry: Arc<ComponentRegistry>,
     active_camera: Option<Entity>,
+    display_settings: DisplaySettings,
     
     // rendering
     light_cube_pipeline: Option<LightCubePipeline>,
     main_pipeline: Option<MainRenderPipeline>,
     shader_globals: Option<GlobalsUniform>,
     collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
+    sky_pipeline: Option<SkyPipeline>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -108,10 +149,16 @@ pub struct PlayMode {
 
     collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>,
     collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>,
+    viewport_offset: (f32, f32),
+
+    // ui
+    // yakui_winit: Option<YakuiWinit>,
+    kino: Option<kino_ui::KinoState>,
 }
 
 impl PlayMode {
     pub fn new(initial_scene: Option<String>) -> anyhow::Result<Self> {
+        eucalyptus_core::utils::start_deadlock_detector();
 
         let mut component_registry = ComponentRegistry::new();
 
@@ -149,9 +196,21 @@ impl PlayMode {
             collider_wireframe_pipeline: None,
             collider_wireframe_geometry_cache: HashMap::new(),
             collider_instance_buffer: None,
+            viewport_offset: (0.0, 0.0),
             collision_event_receiver: Some(ce_r),
             collision_force_event_receiver: Some(cfe_r),
             event_collector,
+            // yakui_winit: None,
+            display_settings: DisplaySettings {
+                window_mode: WindowMode::Windowed,
+                maintain_aspect_ratio: true,
+                vsync: false,
+                last_window_mode: WindowMode::BorderlessFullscreen,
+                last_vsync: true,
+                last_size: (0, 0),
+            },
+            kino: None,
+            sky_pipeline: None,
         };
 
         log::debug!("Created new play mode instance");
@@ -164,6 +223,36 @@ impl PlayMode {
         self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone()));
         self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals")));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
+        
+        self.kino = Some(KinoState::new(
+            KinoWGPURenderer::new(
+                &graphics.device,
+                &graphics.queue,
+                graphics.hdr.read().format(),
+                [
+                    graphics.viewport_texture.size.width as f32,
+                    graphics.viewport_texture.size.height as f32,
+                ],
+            ),
+            KinoWinitWindowing::new(graphics.window.clone()),
+        ));
+
+        let sky_texture = HdrLoader::from_equirectangular_bytes(
+            &graphics.device,
+            &graphics.queue,
+            DEFAULT_SKY_TEXTURE,
+            1080,
+            Some("sky texture")
+        );
+
+        match sky_texture {
+            Ok(sky_texture) => {
+                self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture));
+            }
+            Err(e) => {
+                error!("Failed to load sky texture: {}", e);
+            }
+        }
     }
 
     fn reload_scripts_for_current_world(&mut self) {
@@ -208,6 +297,8 @@ impl PlayMode {
     }
 
     /// Requests an asynchronous scene load, returning immediately and loading the scene in the background.
+    ///
+    /// It will not request the scene load if the currently rendered scene is the same as the requested scene.
     pub fn request_async_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) {
         log::debug!("Requested async scene load: {}", requested_scene.requested_scene);
         let scene_name = requested_scene.requested_scene.clone();
@@ -225,8 +316,18 @@ impl PlayMode {
             if let Some(id) = progress.id {
                 let mut loader = SCENE_LOADER.lock();
                 if let Some(entry) = loader.get_entry_mut(id) {
-                    if entry.status.is_none() {
-                        entry.status = self.world_loading_progress.as_ref().cloned();
+                    if let Some(scene) = &self.current_scene && scene == &self.scene_progress.as_ref().unwrap().requested_scene {
+                        log::debug!("Load scene async request cancelled because scene name is current");
+                        entry.result = SceneLoadResult::Error("Currently rendered scene name is the same as the requested scene".to_string());
+                        self.world_loading_progress = None;
+                        self.world_receiver = None;
+                        self.physics_receiver = None;
+                        self.scene_progress = None;
+                        return
+                    } else {
+                        if entry.status.is_none() {
+                            entry.status = self.world_loading_progress.as_ref().cloned();
+                        }
                     }
                 }
             }
@@ -241,7 +342,7 @@ impl PlayMode {
         let graphics_cloned = graphics.clone();
         let component_registry = self.component_registry.clone();
 
-        let handle = graphics.future_queue.push(async move {
+        let handle = FutureQueue::push(&graphics.future_queue, async move {
             let mut temp_world = World::new();
             let load_status = scene_to_load.load_into_world(
                 &mut temp_world,
@@ -262,7 +363,7 @@ impl PlayMode {
 
                     v
                 }
-                Err(e) => {panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e);}
+                Err(e) => { panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e); }
             }
         });
 
@@ -276,6 +377,11 @@ impl PlayMode {
 
     /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded.
     pub fn request_immediate_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) {
+        if let Some(scene) = &self.current_scene && scene == &requested_scene.requested_scene {
+            log::debug!("Immediate scene load request cancelled because scene name is current");
+            return
+        }
+
         let scene_name = requested_scene.requested_scene.clone();
         log::debug!("Immediate scene load requested: {}", scene_name);
 
@@ -360,4 +466,90 @@ impl PlayMode {
             self.current_scene = Some(scene_progress.requested_scene.clone());
         }
     }
+}
+
+pub struct DisplaySettings {
+    pub window_mode: WindowMode,
+    pub maintain_aspect_ratio: bool,
+    pub vsync: bool,
+    last_window_mode: WindowMode,
+    last_vsync: bool,
+    last_size: (u32, u32),
+}
+
+impl DisplaySettings {
+    pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        let window = graphics.window.clone();
+        let size = (
+            graphics.viewport_texture.size.width,
+            graphics.viewport_texture.size.height,
+        );
+
+        let needs_update = self.window_mode != self.last_window_mode
+            || self.vsync != self.last_vsync
+            || size != self.last_size;
+
+        if !needs_update {
+            return;
+        }
+
+        match self.window_mode {
+            WindowMode::Windowed => {
+                window.set_fullscreen(None);
+                window.set_maximized(false);
+            }
+            WindowMode::Maximized => {
+                window.set_fullscreen(None);
+                window.set_maximized(true);
+            }
+            WindowMode::Fullscreen => {
+                let monitor = window.current_monitor();
+                let fullscreen = monitor
+                    .as_ref()
+                    .and_then(|m| m.video_modes().next())
+                    .map(Fullscreen::Exclusive)
+                    .or_else(|| Some(Fullscreen::Borderless(monitor)));
+
+                window.set_fullscreen(fullscreen);
+                window.set_maximized(false);
+            }
+            WindowMode::BorderlessFullscreen => {
+                let monitor = window.current_monitor();
+                window.set_fullscreen(Some(Fullscreen::Borderless(monitor)));
+                window.set_maximized(false);
+            }
+        }
+
+        let config = SurfaceConfiguration {
+            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+            format: graphics.surface_format,
+            width: graphics.viewport_texture.size.width,
+            height: graphics.viewport_texture.size.height,
+            present_mode: if self.vsync {
+                wgpu::PresentMode::AutoVsync
+            } else {
+                wgpu::PresentMode::AutoNoVsync
+            },
+            alpha_mode: wgpu::CompositeAlphaMode::Auto,
+            view_formats: vec![],
+            desired_maximum_frame_latency: 2,
+        };
+
+        {
+            let mut cfg = graphics.surface_config.write();
+            *cfg = config;
+        }
+
+        self.last_window_mode = self.window_mode;
+        self.last_vsync = self.vsync;
+        self.last_size = size;
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum WindowMode {
+    Windowed,
+    Maximized,
+    Fullscreen,
+    BorderlessFullscreen,
 }
\ No newline at end of file
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index cd2e8d8..a8989a5 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -7,11 +7,12 @@ use eucalyptus_core::egui::CentralPanel;
 use eucalyptus_core::physics::collider::ColliderGroup;
 use eucalyptus_core::physics::collider::ColliderShapeKey;
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
-use glam::{DMat4, DQuat, DVec3, Quat};
+use glam::{vec2, DMat4, DQuat, DVec3, Quat, Vec2};
 use hecs::Entity;
-use wgpu::{Color};
+use wgpu::Color;
 use wgpu::util::DeviceExt;
 use winit::event_loop::ActiveEventLoop;
+use winit::event::WindowEvent;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
@@ -29,11 +30,19 @@ use eucalyptus_core::rapier3d::geometry::SharedShape;
 use eucalyptus_core::states::{Label, PROJECT};
 use eucalyptus_core::states::SCENES;
 use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER};
-use crate::{PlayMode};
+use crate::PlayMode;
 use eucalyptus_core::physics::collider::shader::create_wireframe_geometry;
+use eucalyptus_core::ui::UI_CONTEXT;
+use kino_ui::widgets::{Anchor, Border, Fill};
+use kino_ui::widgets::rect::Rectangle;
 
 impl Scene for PlayMode {
     fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        // let mut yak = yakui_winit::YakuiWinit::new(&graphics.window);
+        // yak.set_automatic_viewport(false);
+        // yak.set_automatic_scale_factor(false);
+        // self.yakui_winit = Some(yak);
+        
         if self.current_scene.is_none() {
             let initial_scene = if let Some(s) = &self.initial_scene {
                 s.clone()
@@ -55,6 +64,12 @@ impl Scene for PlayMode {
             let _ = self.script_manager.physics_update_script(self.world.as_mut(), dt as f64);
         }
 
+        let world = self.world.iter().map(|e| (e.get::<&Label>().unwrap().to_string(), e.entity())).collect::<Vec<_>>();
+        log::info!("World contents [len={}]: ", world.len());
+        for (l, e) in world {
+            log::info!("{} -> {:?}", l, e);
+        }
+
         for kcc in self.world.query::<&mut KCC>().iter() {
             kcc.collisions.clear();
         }
@@ -233,6 +248,8 @@ impl Scene for PlayMode {
         graphics.future_queue.poll();
         self.poll(graphics.clone());
 
+        self.display_settings.update(graphics.clone());
+
         {
             if let Some(fps) = PROJECT.read().runtime_settings.target_fps.get() {
                 log_once::debug_once!("setting new fps for play mode session: {}", fps);
@@ -372,6 +389,40 @@ impl Scene for PlayMode {
         #[cfg(feature = "debug")]
         egui::TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| {
             egui::MenuBar::new().ui(ui, |ui| {
+                use crate::WindowMode;
+                ui.menu_button("Window", |ui| {
+                    ui.menu_button("Window Mode", |ui| {
+                        let is_windowed = matches!(self.display_settings.window_mode, WindowMode::Windowed);
+                        if ui.selectable_label(is_windowed, "Windowed").clicked() {
+                            self.display_settings.window_mode = WindowMode::Windowed;
+                            ui.close();
+                        }
+
+                        let is_maximized = matches!(self.display_settings.window_mode, WindowMode::Maximized);
+                        if ui.selectable_label(is_maximized, "Maximized").clicked() {
+                            self.display_settings.window_mode = WindowMode::Maximized;
+                            ui.close();
+                        }
+
+                        let is_fullscreen = matches!(self.display_settings.window_mode, WindowMode::Fullscreen);
+                        if ui.selectable_label(is_fullscreen, "Fullscreen").clicked() {
+                            self.display_settings.window_mode = WindowMode::Fullscreen;
+                            ui.close();
+                        }
+
+                        let is_borderless = matches!(self.display_settings.window_mode, WindowMode::BorderlessFullscreen);
+                        if ui.selectable_label(is_borderless, "Borderless Fullscreen").clicked() {
+                            self.display_settings.window_mode = WindowMode::BorderlessFullscreen;
+                            ui.close();
+                        }
+                    });
+
+                    ui.separator();
+
+                    ui.checkbox(&mut self.display_settings.maintain_aspect_ratio, "Maintain aspect ratio");
+                    ui.checkbox(&mut self.display_settings.vsync, "VSync").clicked();
+                });
+
                 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                     ui.group(|ui| {
                         ui.add_enabled_ui(true, |ui| {
@@ -394,9 +445,12 @@ impl Scene for PlayMode {
         CentralPanel::default().show(&graphics.get_egui_context(), |ui| {
             if let Some(p) = &self.scene_progress {
                 if !p.is_everything_loaded() && p.is_first_scene {
-                    // todo: change from label to "splashscreen"
                     ui.centered_and_justified(|ui| {
-                        ui.label("Loading scene...");
+                        egui_extras::install_image_loaders(&graphics.get_egui_context());
+                        ui.add(
+                            egui::Image::new(egui::include_image!("../../../resources/eucalyptus-editor.png"))
+                                .max_width(128.0)
+                        )
                     });
                     return;
                 }
@@ -415,20 +469,19 @@ impl Scene for PlayMode {
                         self.has_initial_resize_done = true;
                     }
 
-                    // if !self.display_settings.maintain_aspect_ratio {
-                    //     cam.aspect = (available_size.x / available_size.y) as f64;
-                    // }
+                    if !self.display_settings.maintain_aspect_ratio {
+                        cam.aspect = (available_size.x / available_size.y) as f64;
+                    }
                     cam.update_view_proj();
                     cam.update(graphics.clone());
 
-                    let (display_width, display_height) = (available_size.x, available_size.y);
-                    // if self.display_settings.maintain_aspect_ratio {
-                    //     let width = available_size.x;
-                    //     let height = width / cam.aspect as f32;
-                    //     (width, height)
-                    // } else {
-                        
-                    // };
+                    let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio {
+                        let width = available_size.x;
+                        let height = width / cam.aspect as f32;
+                        (width, height)
+                    } else {
+                        (available_size.x, available_size.y)
+                    };
 
                     let center_x = available_rect.center().x;
                     let center_y = available_rect.center().y;
@@ -438,6 +491,24 @@ impl Scene for PlayMode {
                         egui::vec2(display_width, display_height),
                     );
 
+                    self.viewport_offset = (image_rect.min.x, image_rect.min.y);
+                    if let Some(kino) = &mut self.kino {
+                        let scale_x = if display_width > 0.0 {
+                            graphics.viewport_texture.size.width as f32 / display_width
+                        } else {
+                            1.0
+                        };
+                        let scale_y = if display_height > 0.0 {
+                            graphics.viewport_texture.size.height as f32 / display_height
+                        } else {
+                            1.0
+                        };
+                        kino.set_viewport_transform(
+                            Vec2::new(image_rect.min.x, image_rect.min.y),
+                            Vec2::new(scale_x, scale_y),
+                        );
+                    }
+
                     ui.allocate_exact_size(available_size, egui::Sense::hover());
 
                     ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| {
@@ -446,6 +517,88 @@ impl Scene for PlayMode {
                             size: egui::vec2(display_width, display_height),
                         }));
                     });
+
+                    // overlay
+                    // UI_CONTEXT.with(|yakui_cell| {
+                    //     let yak = yakui_cell.borrow();
+                    //     let mut yakui = yak.yakui_state.lock();
+                    // 
+                    //     let tex_size = graphics.viewport_texture.size;
+                    //     let viewport_size = yakui::geometry::Vec2::new(
+                    //         tex_size.width as f32,
+                    //         tex_size.height as f32,
+                    //     );
+                    //     yakui.set_surface_size(viewport_size);
+                    //     yakui.set_unscaled_viewport(yakui::geometry::Rect::from_pos_size(
+                    //         yakui::geometry::Vec2::ZERO,
+                    //         viewport_size,
+                    //     ));
+                    //     yakui.set_scale_factor(graphics.window.scale_factor() as f32);
+                    // 
+                    //     yakui.start();
+                    // 
+                    //     // eucalyptus_core::ui::poll();
+                    // 
+                    //     yakui.finish();
+                    // });
+
+                    if let Some(kino) = &mut self.kino {
+                        // #[allow(dead_code)]
+                        let no_texture = kino.add_texture_from_bytes(
+                            &graphics.device, &graphics.queue,
+                            "no texture",
+                            include_bytes!("../../../resources/textures/no-texture.png"),
+                            256, 256
+                        );
+
+                        let parent = kino_ui::rect_container(
+                            kino,
+                            Rectangle::new("parent")
+                                .fill(Fill::new([1.0, 1.0, 1.0, 1.0]))
+                                .size(vec2(400.0, 400.0)),
+                            |kino| {
+                                kino.add_widget(Rectangle::new("rect")
+                                    .texture(no_texture)
+                                    .size(vec2(128.0, 100.0))
+                                    .border(Border::new([1.0, 0.0, 0.0, 1.0], 3.0))
+                                    .fill(Fill::new([1.0, 1.0, 1.0, 1.0]))
+                                    .texture(no_texture)
+                                    .build()
+                                );
+                            }
+                        );
+
+                        kino_ui::label(kino, "Hello World!", |l| {
+                            l.position = vec2(graphics.viewport_texture.size.width as f32 / 2.0, graphics.viewport_texture.size.height as f32 / 2.0);
+                            l.metrics.font_size = 30.0;
+                        });
+
+                        kino.poll();
+
+                        if kino.response(parent).clicked {
+                            println!("Parent clicked!");
+                        };
+
+                        // if kino.response(parent).hovering {
+                        //     println!("Parent hovering");
+                        // };
+
+                        if kino.response("rect").clicked {
+                            println!("child clicked!");
+                        };
+
+                        // if kino.response("rect").hovering {
+                        //     println!("child hovering");
+                        // };
+
+                        if kino.response("Hello World!").clicked {
+                            println!("text clicked")
+                        }
+
+                        if kino.response("Hello World!").hovering {
+                            println!("text hovering");
+                        };
+                    }
                 } else {
                     log::warn!("No such camera exists in the world");
                 }
@@ -458,12 +611,7 @@ impl Scene for PlayMode {
     }
 
     fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        let clear_color = Color {
-            r: 100.0 / 255.0,
-            g: 149.0 / 255.0,
-            b: 237.0 / 255.0,
-            a: 1.0,
-        };
+        let hdr = graphics.hdr.read();
 
         let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder"));
 
@@ -486,35 +634,34 @@ impl Scene for PlayMode {
         };
         log_once::debug_once!("Pipeline ready");
 
-        { // ensures clearing of the encoder is done correctly. 
-            let mut encoder = dropbear_engine::graphics::CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder"));
-
-            {
-                let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("viewport clear pass"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &graphics.viewport_texture.view,
-                        depth_slice: None,
-                        resolve_target: None,
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(wgpu::Color {
-                                r: 100.0 / 255.0,
-                                g: 149.0 / 255.0,
-                                b: 237.0 / 255.0,
-                                a: 1.0,
-                            }),
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: None,
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
-            }
-
-            if let Err(e) = encoder.submit(graphics.clone()) {
-                log_once::error_once!("{}", e);
-            }
+        {
+            let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("viewport clear pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    depth_slice: None,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(wgpu::Color {
+                            r: 100.0 / 255.0,
+                            g: 149.0 / 255.0,
+                            b: 237.0 / 255.0,
+                            a: 1.0,
+                        }),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &graphics.depth_texture.view,
+                    depth_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(0.0),
+                        store: wgpu::StoreOp::Store,
+                    }),
+                    stencil_ops: None,
+                }),
+                occlusion_query_set: None,
+                timestamp_writes: None,
+            });
         }
 
         let lights = {
@@ -602,18 +749,18 @@ impl Scene for PlayMode {
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("light cube render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &graphics.viewport_texture.view,
+                        view: hdr.view(),
                         depth_slice: None,
                         resolve_target: None,
                         ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(clear_color),
+                            load: wgpu::LoadOp::Load,
                             store: wgpu::StoreOp::Store,
                         },
                     })],
                     depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                         view: &graphics.depth_texture.view,
                         depth_ops: Some(wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(0.0),
+                            load: wgpu::LoadOp::Load,
                             store: wgpu::StoreOp::Store,
                         }),
                         stencil_ops: None,
@@ -649,7 +796,7 @@ impl Scene for PlayMode {
                     .begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("model render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                            view: &graphics.viewport_texture.view,
+                            view: hdr.view(),
                             depth_slice: None,
                             resolve_target: None,
                             ops: wgpu::Operations {
@@ -700,7 +847,7 @@ impl Scene for PlayMode {
                         .begin_render_pass(&wgpu::RenderPassDescriptor {
                             label: Some("model render pass"),
                             color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                                view: &graphics.viewport_texture.view,
+                                view: hdr.view(),
                                 depth_slice: None,
                                 resolve_target: None,
                                 ops: wgpu::Operations {
@@ -816,12 +963,84 @@ impl Scene for PlayMode {
                     }
                 }
             }
-            if let Err(e) = encoder.submit(graphics.clone()) {
-                log_once::error_once!("{}", e);
+
+            // UI_CONTEXT.with(|v| {
+            //     let commands = graphics.yakui_renderer.lock().paint(
+            //         &mut v.borrow().yakui_state.lock(),
+            //         &graphics.device,
+            //         &graphics.queue,
+            //         SurfaceInfo {
+            //             format: Texture::TEXTURE_FORMAT,
+            //             sample_count: 1,
+            //             color_attachment: &graphics.viewport_texture.view,
+            //             resolve_target: None,
+            //         }
+            //     );
+            //
+            //     graphics.queue.submit([commands]);
+            // });
+        }
+
+        if let Some(sky) = &self.sky_pipeline {
+            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("sky render pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                    depth_slice: None,
+                })],
+                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &graphics.depth_texture.view,
+                    depth_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    }),
+                    stencil_ops: None,
+                }),
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            render_pass.set_pipeline(&sky.pipeline);
+            render_pass.set_bind_group(0, &camera.bind_group, &[]);
+            render_pass.set_bind_group(1, &sky.bind_group, &[]);
+            render_pass.draw(0..3, 0..1);
+        }
+
+        hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
+        if let Err(e) = encoder.submit() {
+            log_once::error_once!("{}", e);
+        }
+
+        if let Some(kino) = &mut self.kino {
+            let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder"));
+            kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view());
+
+            if let Err(e) = encoder.submit() {
+                log_once::error_once!("Unable to submit kino: {}", e);
             }
         }
     }
 
+    fn handle_event(&mut self, event: &WindowEvent) {
+        // UI_CONTEXT.with(|yakui_cell| {
+        //     let yak = yakui_cell.borrow();
+        //     let mut yakui = yak.yakui_state.lock();
+        //     if let Some(yak) = &mut self.yakui_winit {
+        //         yak.handle_window_event(&mut yakui, event);
+        //     }
+        // });
+
+        if let Some(kino) = &mut self.kino {
+            kino.handle_event(event);
+        }
+    }
+
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
 
     fn run_command(&mut self) -> SceneCommand {
diff --git a/crates/slank/Cargo.toml b/crates/slank/Cargo.toml
new file mode 100644
index 0000000..40b1ac0
--- /dev/null
+++ b/crates/slank/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "slank"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+readme = "README.md"
+keywords = ["graphics", "shader", "slang", "wgpu", "vulkan"]
+authors.workspace = true
+description = "A build.rs slang shader compiler"
+
+[dependencies]
+
+wgpu = { workspace = true, optional = true }
+tempfile.workspace = true
+anyhow.workspace = true
+
+[features]
+# this feature downloads the slang compiler for the correct operating system and stores it for later.
+download-slang = []
+
+# allows for the wgpu dependency to be available.
+use-wgpu = ["wgpu/default", "wgpu/spirv"]
+
+[build-dependencies]
+dirs = "6.0"
+reqwest = { version = "0.13", features = ["blocking", "json"] }
+zip = "7.2"
+flate2 = "1.1"
+tar = "0.4"
+anyhow.workspace = true
+serde = { workspace = true }
\ No newline at end of file
diff --git a/crates/slank/README.md b/crates/slank/README.md
new file mode 100644
index 0000000..090bfad
--- /dev/null
+++ b/crates/slank/README.md
@@ -0,0 +1,53 @@
+# slank
+
+A rust version of the slang compiler, ready to be used by wgpu, vulkan or directx (or any graphics library really, i personally
+use wgpu).
+
+# Usage
+
+Add `slank` to your `[build-dependencies]`.
+
+In your `build.rs`:
+```rust
+use slank::{ShaderStage, SlangShaderBuilder, SlangTarget};
+use std::path::Path;
+
+fn main() {
+    let out_dir = std::env::var("OUT_DIR").unwrap();
+    let dest_path = Path::new(&out_dir).join("shader.spv");
+
+    SlangShaderBuilder::new("shader_label")
+        .add_source_path("src/shader.slang").unwrap()
+        .entry_with_stage("vs_main", ShaderStage::Vertex)
+        .entry_with_stage("fs_main", ShaderStage::Fragment)
+        .build(SlangTarget::SpirV).unwrap()
+        .output(&dest_path).unwrap();
+
+    println!("cargo:rerun-if-changed=src/shader.slang");
+}
+```
+
+Then in your main code:
+
+```rust,ignore
+let shader_bytes = slank::include_slang!("shader_label");
+
+// optionally
+slank::CompiledSlangShader::from_bytes("shader_label", shader_bytes);
+```
+
+# Features
+There are two main features currently available:
+
+- `download-slang` - Slank downloads the latest slangc compiler from the GitHub releases and stores it 
+                     in the user cache. The SLANG_DIR will be set to the directory of the slangc compiler.
+                     Note: Using this in CI will require you to add a `GITHUB_TOKEN` env var to avoid the rate limits. Locally, you are fine. 
+- `use-wgpu` - Enables wgpu as a dependency and unlocks utility traits for wgpu. 
+
+# Contribution
+
+This is part of the larger `dropbear-engine` project, however that shouldn't stop you from contributing to this
+repository. It is still missing a lot of different stuff, such as modules, more arguments and better Vulkan/DirectX
+library utility support. 
+
+Your help is appreciated. 
\ No newline at end of file
diff --git a/crates/slank/build.rs b/crates/slank/build.rs
new file mode 100644
index 0000000..4819473
--- /dev/null
+++ b/crates/slank/build.rs
@@ -0,0 +1,283 @@
+use std::path::PathBuf;
+use std::process::Command;
+
+fn main() -> anyhow::Result<()> {
+    println!("cargo:rerun-if-changed=build.rs");
+
+    let path = find_or_download_slangc()?;
+
+    println!("cargo:rustc-env=SLANG_DIR={}", path.display());
+    // println!("cargo:warning=Found slangc at: {}", path.display());
+    Ok(())
+}
+
+fn find_or_download_slangc() -> anyhow::Result<PathBuf> {
+    if let Ok(path) = std::env::var("SLANG_DIR") {
+        let path = PathBuf::from(path);
+        let exe_path = path.join("bin").join(if cfg!(target_os = "windows") {
+            "slangc.exe"
+        } else {
+            "slangc"
+        });
+        if is_valid_slangc(&exe_path)? {
+            return Ok(path);
+        } else {
+            anyhow::bail!("Not valid slangc installation at: {}", path.display());
+        }
+    }
+
+    if let Some(exe_path) = find_in_path("slangc") {
+        if is_valid_slangc(&exe_path)? {
+            if let Some(root) = exe_path.parent().and_then(|p| p.parent()) {
+                return Ok(root.to_path_buf());
+            }
+        }
+    }
+
+    if let Some(exe_path) = check_cached_download() {
+        if is_valid_slangc(&exe_path)? {
+            if let Some(root) = exe_path.parent().and_then(|p| p.parent()) {
+                return Ok(root.to_path_buf());
+            }
+        }
+    }
+
+    #[cfg(feature = "download-slang")]
+    {
+        return download_slang();
+    }
+
+    #[cfg(not(feature = "download-slang"))]
+    {
+        Err(anyhow::anyhow!(
+            "slangc not found. Either:\n\
+             1. Install slangc and add it to PATH\n\
+             2. Set SLANG_DIR environment variable\n\
+             3. Enable the 'download-slang' feature"
+        ))
+    }
+}
+
+fn is_valid_slangc(path: &PathBuf) -> anyhow::Result<bool> {
+    if !path.exists() {
+        return Ok(false);
+    }
+
+    Ok(Command::new(path)
+        .arg("-version")
+        .output()
+        .map(|output| output.status.success())
+        .unwrap_or(false))
+}
+
+fn find_in_path(name: &str) -> Option<PathBuf> {
+    std::env::var_os("PATH").and_then(|paths| {
+        std::env::split_paths(&paths)
+            .filter_map(|dir| {
+                let full_path = dir.join(name);
+                #[cfg(target_os = "windows")]
+                {
+                    if full_path.with_extension("exe").exists() {
+                        return Some(full_path.with_extension("exe"));
+                    }
+                }
+                if full_path.exists() {
+                    Some(full_path)
+                } else {
+                    None
+                }
+            })
+            .next()
+    })
+}
+
+fn check_cached_download() -> Option<PathBuf> {
+    if let Ok(out_dir) = std::env::var("OUT_DIR") {
+        let cached = PathBuf::from(out_dir)
+            .join("slangc")
+            .join("bin")
+            .join(if cfg!(target_os = "windows") {
+                "slangc.exe"
+            } else {
+                "slangc"
+            });
+
+        if cached.exists() {
+            return Some(cached);
+        }
+    }
+
+    if let Some(cache_dir) = dirs::cache_dir() {
+        let cached = cache_dir
+            .join("slank")
+            .join("slangc")
+            .join("bin")
+            .join(if cfg!(target_os = "windows") {
+                "slangc.exe"
+            } else {
+                "slangc"
+            });
+
+        if cached.exists() {
+            return Some(cached);
+        }
+    }
+
+    None
+}
+
+#[cfg(feature = "download-slang")]
+fn download_slang() -> anyhow::Result<PathBuf> {
+
+    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
+    let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
+
+    let version = get_latest_slang_version()?;
+
+    let (download_url, archive_name) = get_download_url(&version, &target_os, &target_arch)?;
+
+    let cache_dir = dirs::cache_dir()
+        .ok_or_else(|| anyhow::anyhow!("Could not find cache directory"))?
+        .join("slank")
+        .join("slangc");
+
+    std::fs::create_dir_all(&cache_dir)
+        .map_err(|e| anyhow::anyhow!("Failed to create cache directory: {}", e))?;
+
+    let archive_path = cache_dir.join(&archive_name);
+
+    if !archive_path.exists() {
+        download_file(&download_url, &archive_path)?;
+    }
+
+    extract_archive(&archive_path, &cache_dir)?;
+
+    let slangc_exe = cache_dir
+        .join("bin")
+        .join(if cfg!(target_os = "windows") {
+            "slangc.exe"
+        } else {
+            "slangc"
+        });
+
+    if !slangc_exe.exists() {
+        return Err(anyhow::anyhow!("slangc not found after extraction at: {}", slangc_exe.display()));
+    }
+
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::PermissionsExt;
+        let mut perms = std::fs::metadata(&slangc_exe)
+            .map_err(|e| anyhow::anyhow!("Failed to get permissions: {}", e))?
+            .permissions();
+        perms.set_mode(0o755);
+        std::fs::set_permissions(&slangc_exe, perms)
+            .map_err(|e| anyhow::anyhow!("Failed to set permissions: {}", e))?;
+    }
+
+    Ok(cache_dir)
+}
+
+#[cfg(feature = "download-slang")]
+fn get_latest_slang_version() -> anyhow::Result<String> {
+    use serde::Deserialize;
+
+    #[derive(Deserialize)]
+    struct Release {
+        tag_name: String,
+    }
+
+    let client = reqwest::blocking::Client::new();
+    let mut request = client
+        .get("https://api.github.com/repos/shader-slang/slang/releases/latest")
+        .header(reqwest::header::USER_AGENT, "dropbear-slank-build");
+
+    // Add GitHub token if available for authenticated requests (avoids rate limiting)
+    if let Ok(token) = std::env::var("GITHUB_TOKEN") {
+        request = request.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token));
+    }
+
+    let response = request
+        .send()
+        .map_err(|e| anyhow::anyhow!("Failed to fetch latest version from GitHub: {}", e))?;
+
+    if !response.status().is_success() {
+        return Err(anyhow::anyhow!(
+            "GitHub API failed with status: {}. If rate limited, set GITHUB_TOKEN environment variable.",
+            response.status()
+        ));
+    }
+
+    let release: Release = response.json()
+        .map_err(|e| anyhow::anyhow!("Failed to parse GitHub response: {}", e))?;
+
+    Ok(release.tag_name.trim_start_matches('v').to_string())
+}
+
+#[cfg(feature = "download-slang")]
+fn get_download_url(version: &str, os: &str, arch: &str) -> anyhow::Result<(String, String)> {
+    let (platform, ext) = match (os, arch) {
+        ("windows", "x86_64") => ("windows-x86_64", "zip"),
+        ("windows", "aarch64") => ("windows-aarch64", "zip"),
+        ("linux", "x86_64") => ("linux-x86_64", "tar.gz"),
+        ("linux", "aarch64") => ("linux-aarch64", "tar.gz"),
+        ("macos", "x86_64") => ("macos-x86_64", "zip"),
+        ("macos", "aarch64") => ("macos-aarch64", "zip"),
+        _ => return Err(anyhow::anyhow!("Unsupported platform: {}-{}", os, arch)),
+    };
+
+    let archive_name = format!("slang-{}-{}.{}", version, platform, ext);
+    let url = format!(
+        "https://github.com/shader-slang/slang/releases/download/v{}/{}",
+        version, archive_name
+    );
+
+    Ok((url, archive_name))
+}
+
+#[cfg(feature = "download-slang")]
+fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> {
+    let client = reqwest::blocking::Client::builder()
+        .timeout(std::time::Duration::from_secs(600))
+        .build()
+        .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?;
+
+    let mut response = client.get(url)
+        .header(reqwest::header::USER_AGENT, "dropbear-slank-build")
+        .send()
+        .map_err(|e| anyhow::anyhow!("Failed to download: {}", e))?;
+
+    if !response.status().is_success() {
+        return Err(anyhow::anyhow!("Download failed with status: {}", response.status()));
+    }
+
+    let mut file = std::fs::File::create(dest)
+        .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?;
+
+    response.copy_to(&mut file)
+        .map_err(|e| anyhow::anyhow!("Failed to save file content at {}: {}", dest.display(), e))?;
+
+    Ok(())
+}
+
+#[cfg(feature = "download-slang")]
+fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> {
+    let file = std::fs::File::open(archive)
+        .map_err(|e| anyhow::anyhow!("Failed to open archive: {}", e))?;
+
+    if archive.extension().and_then(|s| s.to_str()) == Some("zip") {
+        let mut archive = zip::ZipArchive::new(file)
+            .map_err(|e| anyhow::anyhow!("Failed to read zip: {}", e))?;
+
+        archive.extract(dest)
+            .map_err(|e| anyhow::anyhow!("Failed to extract zip: {}", e))?;
+    } else {
+        let tar = flate2::read::GzDecoder::new(file);
+        let mut archive = tar::Archive::new(tar);
+
+        archive.unpack(dest)
+            .map_err(|e| anyhow::anyhow!("Failed to extract tar.gz: {}", e))?;
+    }
+
+    Ok(())
+}
\ No newline at end of file
diff --git a/crates/slank/src/compiled.rs b/crates/slank/src/compiled.rs
new file mode 100644
index 0000000..573c5bb
--- /dev/null
+++ b/crates/slank/src/compiled.rs
@@ -0,0 +1,48 @@
+use std::path::Path;
+
+pub struct CompiledSlangShader {
+    pub(crate) label: String,
+    args: String,
+    pub source: Vec<u8>,
+}
+
+impl CompiledSlangShader {
+    /// Internal: Creates a new instance of [CompiledSlangShader] from the compiled args
+    /// of [crate::SlangShaderBuilder]
+    pub(crate) fn new(label: String, args: String, source: Vec<u8>) -> Self {
+        Self {
+            args,
+            source,
+            label,
+        }
+    }
+
+    /// Fetches the command line arguments provided into [crate::SlangShaderBuilder]
+    pub fn args(&self) -> String {
+        self.args.clone()
+    }
+    
+    /// Creates a [CompiledSlangShader] from raw bytes.
+    ///
+    /// This is useful when loading shaders compiled at build time using the 
+    /// `include_slang!` macro.
+    pub fn from_bytes(label: &str, source: &[u8]) -> Self {
+        Self {
+            label: label.to_string(),
+            args: String::new(),
+            source: source.to_vec(),
+        }
+    }
+
+    /// Returns the label of the shader. 
+    pub fn label(&self) -> String {
+        self.label.clone()
+    }
+
+    /// Writes the output to a file.
+    pub fn output(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
+        let path = path.as_ref();
+
+        std::fs::write(path, &self.source)
+    }
+}
\ No newline at end of file
diff --git a/crates/slank/src/lib.rs b/crates/slank/src/lib.rs
new file mode 100644
index 0000000..a31ccc9
--- /dev/null
+++ b/crates/slank/src/lib.rs
@@ -0,0 +1,583 @@
+//! slank - slangc for rust. 
+//! 
+//! Compiles slang code during build and stores the shaders locally (or in the crate with [`include_slang`])
+//!
+//! Check out [`SlangShaderBuilder`] to get started.
+
+/// Fetches the slang file (located in {OUT_DIR}/{label}.spv) (assuming it is compiled as .spv) 
+/// and includes the bytes of the file. 
+#[macro_export]
+macro_rules! include_slang {
+    ($label:expr) => {
+        include_bytes!(concat!(env!("OUT_DIR"), "/", $label, ".spv"))
+    };
+}
+
+/// Fetches the path of the shader (with the same label) and returns it to you. 
+#[macro_export]
+macro_rules! include_slang_path {
+    ($label:expr) => {
+        concat!(env!("OUT_DIR"), "/", $label, ".spv")
+    };
+}
+
+mod compiled;
+pub mod utils;
+
+pub use crate::compiled::*;
+
+use std::{fmt::Display, path::{Path, PathBuf}};
+
+#[derive(Debug, Clone)]
+pub struct SourceFile {
+    pub path: Option<PathBuf>,
+    pub content: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct EntryPoint {
+    pub name: String,
+    pub stage: Option<ShaderStage>,
+    /// Index into the sources vec - which file this entry point comes from
+    pub source_index: Option<usize>,
+}
+
+/// Allows you to pass arguments and make an argument builder to chuck into the slangc
+/// compiler.
+///
+/// This is the entry point of the library.
+/// # Usage
+/// 
+/// Add `slank` to your `[build-dependencies]`.
+///
+/// In your `build.rs`:
+/// ```rust,no_run
+/// use slank::{ShaderStage, SlangShaderBuilder, SlangTarget};
+/// use std::path::Path;
+///
+/// fn main() {
+///     let out_dir = std::env::var("OUT_DIR").unwrap();
+///     let dest_path = Path::new(&out_dir).join("shader.spv");
+///
+///     SlangShaderBuilder::new("shader_label")
+///         .add_source_path("src/shader.slang").unwrap()
+///         .entry_with_stage("vs_main", ShaderStage::Vertex)
+///         .entry_with_stage("fs_main", ShaderStage::Fragment)
+///         .build(SlangTarget::SpirV).unwrap()
+///         .output(&dest_path).unwrap();
+/// 
+///     println!("cargo:rerun-if-changed=src/shader.slang");
+/// }
+/// ```
+///
+/// Then in your main code:
+/// ```rust,ignore
+/// let shader_bytes = slank::include_slang!("shader_label")
+/// ```
+pub struct SlangShaderBuilder {
+    label: String,
+    sources: Vec<SourceFile>,
+    entries: Vec<EntryPoint>,
+    profile: Option<Profile>,
+    additional_args: Vec<String>,
+}
+
+impl SlangShaderBuilder {
+    /// Creates a new instance of a [SlangShaderBuilder].
+    pub fn new(label: &str) -> Self {
+        Self {
+            sources: Vec::new(),
+            entries: Vec::new(),
+            label: label.to_string(),
+            profile: None,
+            additional_args: Vec::new(),
+        }
+    }
+
+    /// Adds a source file by path.
+    ///
+    /// Returns the index of this source, which can be used with
+    /// `entry_from_source()` to associate entry points with specific files.
+    pub fn add_source_path(mut self, path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
+        let path_buf = path.as_ref().to_path_buf();
+        let content = std::fs::read_to_string(&path_buf)?;
+
+        self.sources.push(SourceFile {
+            path: Some(path_buf),
+            content,
+        });
+
+        Ok(self)
+    }
+
+    /// Adds a source file by path (non-consuming version).
+    pub fn source_path(&mut self, path: impl AsRef<Path>) -> Result<usize, std::io::Error> {
+        let path_buf = path.as_ref().to_path_buf();
+        let content = std::fs::read_to_string(&path_buf)?;
+
+        self.sources.push(SourceFile {
+            path: Some(path_buf),
+            content,
+        });
+
+        Ok(self.sources.len() - 1)
+    }
+
+    /// Adds source code as a string.
+    ///
+    /// Returns the index of this source.
+    pub fn add_source_str(mut self, content: &str) -> Self {
+        self.sources.push(SourceFile {
+            path: None,
+            content: content.to_string(),
+        });
+        self
+    }
+
+    /// Adds source code as a string (non-consuming version).
+    pub fn source_str(&mut self, content: &str) -> usize {
+        self.sources.push(SourceFile {
+            path: None,
+            content: content.to_string(),
+        });
+        self.sources.len() - 1
+    }
+
+    /// Adds multiple source files at once.
+    pub fn add_source_paths(mut self, paths: &[impl AsRef<Path>]) -> Result<Self, std::io::Error> {
+        for path in paths {
+            let path_buf = path.as_ref().to_path_buf();
+            let content = std::fs::read_to_string(&path_buf)?;
+
+            self.sources.push(SourceFile {
+                path: Some(path_buf),
+                content,
+            });
+        }
+        Ok(self)
+    }
+
+    /// Adds an entry point from the most recently added source.
+    ///
+    /// According to Slang docs: "the file associated with the entry point
+    /// will be the first one found when searching to the left in the command line."
+    pub fn entry(mut self, name: &str) -> Self {
+        let source_index = if self.sources.is_empty() {
+            None
+        } else {
+            Some(self.sources.len() - 1)
+        };
+
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: None,
+            source_index,
+        });
+        self
+    }
+
+    /// Adds an entry point with an explicit shader stage.
+    pub fn entry_with_stage(mut self, name: &str, stage: ShaderStage) -> Self {
+        let source_index = if self.sources.is_empty() {
+            None
+        } else {
+            Some(self.sources.len() - 1)
+        };
+
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: Some(stage),
+            source_index,
+        });
+        self
+    }
+
+    /// Adds an entry point from a specific source file (by index).
+    pub fn entry_from_source(mut self, name: &str, source_index: usize) -> Self {
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: None,
+            source_index: Some(source_index),
+        });
+        self
+    }
+
+    /// Adds an entry point with stage from a specific source file.
+    pub fn entry_from_source_with_stage(
+        mut self,
+        name: &str,
+        source_index: usize,
+        stage: ShaderStage
+    ) -> Self {
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: Some(stage),
+            source_index: Some(source_index),
+        });
+        self
+    }
+
+    pub fn with_profile(mut self, profile: Profile) -> Self {
+        self.profile = Some(profile);
+        self
+    }
+
+    /// In the case that there was an argument not available to this builder, you can
+    /// manually provide it here. 
+    pub fn with_additional_args(mut self, args: &[&str]) -> Self {
+        self.additional_args = args.to_vec().iter().map(|v| v.to_string()).collect::<Vec<_>>();
+        self
+    }
+
+    /// Compiles to the `OUT_DIR` env variable. It will return an [Err] if it is not ran in 
+    /// a `build.rs` script. 
+    /// 
+    /// Also assumes that this is for wgpu and a [`SlangTarget::SpirV`] target. 
+    pub fn compile_to_out_dir(self, target: SlangTarget) -> anyhow::Result<()> {
+        let label = self.label.clone();
+        let compiled = self.build(target)?;
+        let out_dir = std::env::var("OUT_DIR")
+            .map_err(|_| anyhow::anyhow!("OUT_DIR not set. Are you running this from build.rs?"))?;
+        let dest = Path::new(&out_dir).join(format!("{}.spv", label));
+        compiled.output(&dest).map_err(Into::into)
+    }
+
+    /// Builds the slang shader with the arguments provided. 
+    pub fn build(self, target: SlangTarget) -> anyhow::Result<CompiledSlangShader> {
+        let slang_dir = PathBuf::from(env!("SLANG_DIR"));
+        let slangc_path = slang_dir.join("bin").join(if cfg!(windows) { "slangc.exe" } else { "slangc" });
+
+        if !slangc_path.exists() {
+            anyhow::bail!("slangc executable not found at {}", slangc_path.display());
+        }
+
+        let mut cmd = std::process::Command::new(slangc_path);
+        let mut args_record = String::from("slangc");
+
+        let mut temp_files = Vec::new();
+
+        cmd.arg("-target").arg(target.as_arg());
+
+        use std::fmt::Write;
+        write!(&mut args_record, " -target {}", target.as_arg())?;
+
+        for (source_idx, source) in self.sources.iter().enumerate() {
+            let path_to_use = if let Some(path) = &source.path {
+                path.clone()
+            } else {
+                let mut temp = tempfile::Builder::new()
+                    .suffix(".slang")
+                    .tempfile()?;
+
+                use std::io::Write as IoWrite;
+                temp.write_all(source.content.as_bytes())?;
+
+                let p = temp.path().to_path_buf();
+                temp_files.push(temp);
+                p
+            };
+
+            cmd.arg(&path_to_use);
+            write!(&mut args_record, " {}", path_to_use.display())?;
+
+            for entry in &self.entries {
+                if entry.source_index == Some(source_idx) {
+                    cmd.arg("-entry").arg(&entry.name);
+                    write!(&mut args_record, " -entry {}", entry.name)?;
+
+                    if let Some(stage) = entry.stage {
+                        cmd.arg("-stage").arg(stage.as_arg());
+                        write!(&mut args_record, " -stage {}", stage.as_arg())?;
+                    }
+                }
+            }
+        }
+
+        cmd.args(&self.additional_args);
+        write!(&mut args_record, " {}", self.additional_args.join(" "))?;
+
+        let temp_dir = tempfile::tempdir()?;
+        let output_path = temp_dir.path().join("output");
+
+        cmd.arg("-o").arg(&output_path);
+        write!(&mut args_record, " -o {}", output_path.display())?;
+        println!("cmd: {:?}", cmd.get_args());
+        let output = cmd.output()?;
+
+        if !output.status.success() {
+            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
+            return Err(anyhow::anyhow!("Compilation error: {}", stderr));
+        }
+
+
+        let binary_output = std::fs::read(&output_path)?;
+
+        Ok(CompiledSlangShader::new(self.label, args_record, binary_output))
+    }
+}
+
+impl Default for SlangShaderBuilder {
+    fn default() -> Self {
+        Self::new("no named shader")
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ShaderStage {
+    Vertex,
+    Fragment,
+    Compute,
+    // wgpu doesnt support anything below this
+    Geometry,
+    Hull,
+    Domain,
+    RayGen,
+    Intersection,
+    AnyHit,
+    ClosestHit,
+    Miss,
+    Callable,
+}
+
+impl ShaderStage {
+    /// Convert to the slangc command-line argument
+    pub fn as_arg(&self) -> &'static str {
+        match self {
+            Self::Vertex => "vertex",
+            Self::Fragment => "fragment",
+            Self::Compute => "compute",
+            Self::Geometry => "geometry",
+            Self::Hull => "hull",
+            Self::Domain => "domain",
+            Self::RayGen => "raygeneration",
+            Self::Intersection => "intersection",
+            Self::AnyHit => "anyhit",
+            Self::ClosestHit => "closesthit",
+            Self::Miss => "miss",
+            Self::Callable => "callable",
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum SlangTarget {
+    Unknown,
+    None,
+
+    // HLSL/DirectX
+    Hlsl,
+    Dxbc,
+    DxbcAssembly,
+    Dxil,
+    DxilAssembly,
+
+    // GLSL/Vulkan/SPIR-V
+    Glsl,
+    /// Most optimal for WGPU and Vulkan/ 
+    SpirV,
+    SpirVAssembly,
+
+    // C/C++
+    C,
+    Cpp,
+    CppHeader,
+    TorchBinding,
+    HostCpp,
+
+    // Executables and Libraries
+    Executable,
+    ShaderSharedLibrary,
+    SharedLibrary,
+
+    // CUDA
+    Cuda,
+    CudaHeader,
+    Ptx,
+    CuBin,
+
+    // Host Callable
+    HostCallable,
+    ShaderObjectCode,
+    HostHostCallable,
+
+    // Metal
+    Metal,
+    MetalLib,
+    MetalLibAssembly,
+
+    // WebGPU
+    Wgsl,
+    WgslSpirVAssembly,
+    WgslSpirV,
+
+    // Slang VM
+    SlangVm,
+
+    // LLVM
+    HostObjectCode,
+    LlvmHostIr,
+    LlvmShaderIr,
+}
+
+impl SlangTarget {
+    /// Convert to the slangc command-line argument
+    pub fn as_arg(&self) -> &'static str {
+        match self {
+            Self::Unknown => "unknown",
+            Self::None => "none",
+            Self::Hlsl => "hlsl",
+            Self::Dxbc => "dxbc",
+            Self::DxbcAssembly => "dxbc-asm",
+            Self::Dxil => "dxil",
+            Self::DxilAssembly => "dxil-asm",
+            Self::Glsl => "glsl",
+            Self::SpirV => "spirv",
+            Self::SpirVAssembly => "spirv-asm",
+            Self::C => "c",
+            Self::Cpp => "cpp",
+            Self::CppHeader => "hpp",
+            Self::TorchBinding => "torch-binding",
+            Self::HostCpp => "host-cpp",
+            Self::Executable => "exe",
+            Self::ShaderSharedLibrary => "shader-dll",
+            Self::SharedLibrary => "dll",
+            Self::Cuda => "cuda",
+            Self::CudaHeader => "cuh",
+            Self::Ptx => "ptx",
+            Self::CuBin => "cubin",
+            Self::HostCallable => "host-callable",
+            Self::ShaderObjectCode => "shader-object-code",
+            Self::HostHostCallable => "host-host-callable",
+            Self::Metal => "metal",
+            Self::MetalLib => "metallib",
+            Self::MetalLibAssembly => "metallib-asm",
+            Self::Wgsl => "wgsl",
+            Self::WgslSpirVAssembly => "wgsl-spirv-asm",
+            Self::WgslSpirV => "wgsl-spirv",
+            Self::SlangVm => "slang-vm",
+            Self::HostObjectCode => "host-object-code",
+            Self::LlvmHostIr => "llvm-ir",
+            Self::LlvmShaderIr => "llvm-shader-ir",
+        }
+    }
+
+    /// Parse from a string (supports all aliases)
+    pub fn from_str(s: &str) -> Option<Self> {
+        match s {
+            "unknown" => Some(Self::Unknown),
+            "none" => Some(Self::None),
+            "hlsl" => Some(Self::Hlsl),
+            "dxbc" => Some(Self::Dxbc),
+            "dxbc-asm" | "dxbc-assembly" => Some(Self::DxbcAssembly),
+            "dxil" => Some(Self::Dxil),
+            "dxil-asm" | "dxil-assembly" => Some(Self::DxilAssembly),
+            "glsl" => Some(Self::Glsl),
+            "spirv" => Some(Self::SpirV),
+            "spirv-asm" | "spirv-assembly" => Some(Self::SpirVAssembly),
+            "c" => Some(Self::C),
+            "cpp" | "c++" | "cxx" => Some(Self::Cpp),
+            "hpp" => Some(Self::CppHeader),
+            "torch" | "torch-binding" | "torch-cpp" | "torch-cpp-binding" => Some(Self::TorchBinding),
+            "host-cpp" | "host-c++" | "host-cxx" => Some(Self::HostCpp),
+            "exe" | "executable" => Some(Self::Executable),
+            "shader-sharedlib" | "shader-sharedlibrary" | "shader-dll" => Some(Self::ShaderSharedLibrary),
+            "sharedlib" | "sharedlibrary" | "dll" => Some(Self::SharedLibrary),
+            "cuda" | "cu" => Some(Self::Cuda),
+            "cuh" => Some(Self::CudaHeader),
+            "ptx" => Some(Self::Ptx),
+            "cuobj" | "cubin" => Some(Self::CuBin),
+            "host-callable" | "callable" => Some(Self::HostCallable),
+            "object-code" | "shader-object-code" => Some(Self::ShaderObjectCode),
+            "host-host-callable" => Some(Self::HostHostCallable),
+            "metal" => Some(Self::Metal),
+            "metallib" => Some(Self::MetalLib),
+            "metallib-asm" => Some(Self::MetalLibAssembly),
+            "wgsl" => Some(Self::Wgsl),
+            "wgsl-spirv-asm" | "wgsl-spirv-assembly" => Some(Self::WgslSpirVAssembly),
+            "wgsl-spirv" => Some(Self::WgslSpirV),
+            "slangvm" | "slang-vm" => Some(Self::SlangVm),
+            "host-object-code" => Some(Self::HostObjectCode),
+            "llvm-host-ir" | "llvm-ir" => Some(Self::LlvmHostIr),
+            "llvm-shader-ir" => Some(Self::LlvmShaderIr),
+            _ => None,
+        }
+    }
+
+    /// Check if this target produces binary output
+    pub fn is_binary(&self) -> bool {
+        matches!(
+            self,
+            Self::Dxbc | Self::Dxil | Self::SpirV | Self::Executable
+            | Self::ShaderSharedLibrary | Self::SharedLibrary | Self::CuBin
+            | Self::MetalLib | Self::WgslSpirV | Self::SlangVm
+            | Self::HostObjectCode | Self::ShaderObjectCode
+        )
+    }
+
+    /// Check if this target is suitable for wgpu
+    pub fn is_wgpu_compatible(&self) -> bool {
+        matches!(self, Self::SpirV | Self::Wgsl)
+    }
+}
+
+impl std::fmt::Display for SlangTarget {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.as_arg())
+    }
+}
+
+impl std::str::FromStr for SlangTarget {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::from_str(s).ok_or_else(|| format!("Unknown Slang target: {}", s))
+    }
+}
+
+pub enum Profile {
+    Sm(String),
+    Glsl(String),
+    Vs(String),
+    Hs(String),
+    Ds(String),
+    Gs(String),
+    Ps(String),
+}
+
+impl Display for Profile {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let string = match self {
+            Profile::Sm(v) => format!("sm_{v}"),
+            Profile::Glsl(v) => format!("glsl_{v}"),
+            Profile::Vs(v) => format!("vs_{v}"),
+            Profile::Hs(v) => format!("hs_{v}"),
+            Profile::Ds(v) => format!("ds_{v}"),
+            Profile::Gs(v) => format!("gs_{v}"),
+            Profile::Ps(v) => format!("ps_{v}"),
+        };
+        write!(f, "{}", string)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_compile() {
+        let result = SlangShaderBuilder::new("light")
+            .add_source_str(include_str!("../test/light.slang"))
+            .entry_with_stage("vs_main", ShaderStage::Vertex)
+            .entry_with_stage("fs_main", ShaderStage::Fragment)
+            .build(SlangTarget::SpirV);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_from_bytes() {
+        let bytes = vec![1, 2, 3, 4];
+        let shader = CompiledSlangShader::from_bytes("idk", &bytes);
+        assert_eq!(shader.source, bytes);
+        assert_eq!(shader.label(), "idk");
+    }
+}
\ No newline at end of file
diff --git a/crates/slank/src/utils.rs b/crates/slank/src/utils.rs
new file mode 100644
index 0000000..0461cee
--- /dev/null
+++ b/crates/slank/src/utils.rs
@@ -0,0 +1,16 @@
+/// WGPU based traits that are useful for those using the [wgpu] crate.
+#[cfg(feature = "use-wgpu")]
+pub trait WgpuUtils {
+    /// Creates a new [`wgpu::ShaderModuleDescriptor`] for you to create your own shaders with.
+    fn create_wgpu_shader(&self) -> wgpu::ShaderModuleDescriptor<'_>;
+}
+
+#[cfg(feature = "use-wgpu")]
+impl WgpuUtils for crate::CompiledSlangShader {
+    fn create_wgpu_shader(&self) -> wgpu::ShaderModuleDescriptor<'_> {
+        wgpu::ShaderModuleDescriptor {
+            label: Some(self.label.as_str()),
+            source: wgpu::util::make_spirv(self.source.as_ref()),
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/slank/test/light.slang b/crates/slank/test/light.slang
new file mode 100644
index 0000000..af1dd42
--- /dev/null
+++ b/crates/slank/test/light.slang
@@ -0,0 +1,56 @@
+struct Light {
+    float4 position;
+    float4 direction; // x, y, z, outer_cutoff_angle
+    float4 color; // r, g, b, light_type (0, 1, 2)
+
+    float constant;
+    float lin;
+    float quadratic;
+
+    float cutoff;
+};
+
+struct CameraUniform {
+    float4 view_pos;
+    float4x4 view_proj;
+};
+
+[[vk::binding(0, 0)]]
+ConstantBuffer<CameraUniform> camera;
+
+[[vk::binding(1, 0)]]
+ConstantBuffer<Light> light;
+
+struct VertexInput {
+    float3 position : POSITION;
+    float4 model_matrix_0 : TEXCOORD5;
+    float4 model_matrix_1 : TEXCOORD6;
+    float4 model_matrix_2 : TEXCOORD7;
+    float4 model_matrix_3 : TEXCOORD8;
+};
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+    float3 color : COLOR0;
+};
+
+
+[shader("vertex")]
+VertexOutput vs_main(VertexInput input) {
+    float4x4 model_matrix = float4x4(
+        input.model_matrix_0,
+        input.model_matrix_1,
+        input.model_matrix_2,
+        input.model_matrix_3
+    );
+
+    VertexOutput output;
+    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
+    output.color = light.color.xyz;
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    return float4(input.color, 1.0);
+}
\ No newline at end of file
diff --git a/docs/player_hud.kts b/docs/player_hud.kts
new file mode 100644
index 0000000..a2add67
--- /dev/null
+++ b/docs/player_hud.kts
@@ -0,0 +1,75 @@
+package com.dropbear.sample
+
+DynamicBoundingBox {
+    Circle {
+        name = "status_backing"
+
+        align {
+            horizontal = Alignment.Left + 10
+            vertical = Alignment.Centered
+        }
+
+        radius = 40.0
+
+        Image {
+            name = "status_image"
+
+            when (Player.playerState) {
+                PlayerState.Gas -> {
+                    resource = "euca://player/status/gas.png"
+                }
+                PlayerState.Solid -> {
+                    resource = "euca://player/status/solid.png"
+                }
+                PlayerState.Liquid -> {
+                    resource = "euca://player/status/liquid.png"
+                }
+            }
+
+            align = Align.default()
+        }
+    }
+
+    bar("health_bar", Align.Center + 20.0)
+
+    bar("energy_bar", Align.Center - 20.0)
+}
+
+bar(val objectName: String, align: Align = Align.default()): DynamicBoundingBox {
+    Rectangle {
+        name = objectName + " backing"
+
+        size = Vector2d(30.0, 10.0)
+
+        align = align
+
+        style {
+            fill = null
+            stroke = Stroke(
+                colour = Colour.BLACK,
+                width = 1.0
+            )
+        }
+    }
+
+    Rectangle {
+        name = objectName + " fill"
+
+        size = Vector2d(Player.health.percentage() * 30.0, 10.0)
+
+        align = align
+
+        style {
+            fill = Fill(
+                colour = when (Player.currentZone) {
+                    CurrentZone.Freezing -> Colour.DARK_BLUE
+                    CurrentZone.Cold -> Colour.LIGHT_BLUE
+                    CurrentZone.Normal -> Colour.LIGHT_GREY
+                    CurrentZone.Hot -> Colour.ORANGE
+                    CurrentZone.Boiling -> Colour.SCARLET_RED
+                }
+            )
+            stroke = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/docs/renderdoc_wayland.cap b/docs/renderdoc_wayland.cap
new file mode 100644
index 0000000..0a432e3
--- /dev/null
+++ b/docs/renderdoc_wayland.cap
@@ -0,0 +1,40 @@
+{
+    "rdocCaptureSettings": 1,
+    "settings": {
+        "autoStart": false,
+        "commandLine": "",
+        "environment": [
+            {
+                "separator": "Platform style",
+                "type": "Set",
+                "value": "x11",
+                "variable": "WINIT_UNIX_BACKEND"
+            },
+            {
+                "separator": "Platform style",
+                "type": "Set",
+                "value": "",
+                "variable": "WAYLAND_DISPLAY"
+            }
+        ],
+        "executable": "/home/tirbofish/dropbear/target/debug/eucalyptus-editor",
+        "inject": false,
+        "numQueuedFrames": 0,
+        "options": {
+            "allowFullscreen": true,
+            "allowVSync": true,
+            "apiValidation": false,
+            "captureAllCmdLists": false,
+            "captureCallstacks": false,
+            "captureCallstacksOnlyDraws": false,
+            "debugOutputMute": true,
+            "delayForDebugger": 0,
+            "hookIntoChildren": false,
+            "refAllResources": false,
+            "softMemoryLimit": 0,
+            "verifyBufferAccess": false
+        },
+        "queuedFrameCap": 0,
+        "workingDir": ""
+    }
+}
diff --git a/resources/fonts/Roboto-Regular.ttf b/resources/fonts/Roboto-Regular.ttf
new file mode 100644
index 0000000..7e3bb2f
Binary files /dev/null and b/resources/fonts/Roboto-Regular.ttf differ
diff --git a/resources/models/cube.glb b/resources/models/cube.glb
deleted file mode 100644
index 88c5ec8..0000000
Binary files a/resources/models/cube.glb and /dev/null differ
diff --git a/resources/models/error.glb b/resources/models/error.glb
deleted file mode 100644
index 496e346..0000000
Binary files a/resources/models/error.glb and /dev/null differ
diff --git a/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr b/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr
new file mode 100644
index 0000000..25d9934
Binary files /dev/null and b/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr differ
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index de0043b..ced3eed 100644
--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -1,9 +1,12 @@
 package com.dropbear
 
-import com.dropbear.asset.AssetHandle
+import com.dropbear.asset.AssetType
+import com.dropbear.asset.Handle
 import com.dropbear.ffi.NativeEngine
 import com.dropbear.input.InputState
 import com.dropbear.scene.SceneManager
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.UIInstructionSet
 
 internal var exceptionOnError: Boolean = false
 var lastErrorMessage: String? = null
@@ -29,17 +32,6 @@ class DropbearEngine(val native: NativeEngine) {
         fun getLastErrMsg(): String? {
             return lastErrorMessage
         }
-
-        /**
-         * Globally sets whether exceptions should be thrown when an error occurs.
-         *
-         * This can be run in your update loop without consequences.
-         */
-        @Deprecated("Currently not supported anymore, automatically throws exception on error. " +
-                "Better to catch the exception instead", level = DeprecationLevel.HIDDEN)
-        fun callExceptionOnError(toggle: Boolean) {
-            exceptionOnError = toggle
-        }
     }
 
     /**
@@ -58,28 +50,46 @@ class DropbearEngine(val native: NativeEngine) {
      * ## Warning
      * The eucalyptus asset URI (or `euca://`) is case-sensitive.
      */
-    fun getAsset(eucaURI: String): AssetHandle? {
+    fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? {
         val id = com.dropbear.getAsset(eucaURI)
-        return if (id != null) AssetHandle(id) else null
+        if (id == null || id <= 0L) return null
+        return Handle(id)
     }
 
     /**
-     * Globally sets whether exceptions should be thrown when an error occurs.
+     * Renders a set of UI instructions to be displayed onto the screen.
+     *
+     * This uses the rust crate `yakui` to power the UI. You can get a [UIInstructionSet]
+     * by either doing one of two ways:
      *
-     * This can be run in your update loop without consequences.
+     * ## Method 1 (recommended)
+     * ```kt
+     * val instructions: UIInstructionSet = buildUI {
+     *      label("hello world!")
+     * }
+     * engine.renderUI(instructions)
+     * ```
+     *
+     * ## Method 2 (the non-dsl way)
+     * ```kt
+     * val builder = UIBuilder()
+     * builder.add(Text.label("hello world!").toInstruction())
+     * engine.renderUI(builder.build())
+     * ```
      */
-    @Deprecated("Currently not supported anymore, automatically throws exception on error. " +
-            "Better to catch the exception instead", level = DeprecationLevel.HIDDEN)
-    fun callExceptionOnError(toggle: Boolean) {
+    fun renderUI(uiInstructionSet: UIInstructionSet?) {
+        if (uiInstructionSet != null) {
+            renderUI(instructions = uiInstructionSet)
+        }
     }
 
     /**
      * Quits the currently running app or game elegantly.
      * 
-     * This function can have different behaviours depending on where it is ran. 
+     * This function can have different behaviours depending on where it is run.
      * - eucalyptus-editor - When called, this exits your Play Mode session and returns you back to
      *                       `EditorState::Editing`
-     * - redback-runtime - When called, this will exit your current process and kill the app as is. It will
+     * - euca-runner - When called, this will exit your current process and kill the app as is. It will
      *                     also drop any pointers and do any additional cleanup.
      */
     fun quit() {
@@ -89,4 +99,5 @@ class DropbearEngine(val native: NativeEngine) {
 
 internal expect fun getEntity(label: String): Long?
 internal expect fun getAsset(eucaURI: String): Long?
-internal expect fun quit()
\ No newline at end of file
+internal expect fun quit()
+internal expect fun renderUI(instructions: List<UIInstruction>)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/EntityId.kt b/scripting/commonMain/kotlin/com/dropbear/EntityId.kt
index 19dd8f7..6a1aec3 100644
--- a/scripting/commonMain/kotlin/com/dropbear/EntityId.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/EntityId.kt
@@ -1,8 +1,10 @@
 package com.dropbear
 
+import com.dropbear.utils.ID
+
 /**
  * The ID of an entity (represented as a [Long])
  *
  * @property raw The entity into bits.
  */
-data class EntityId(val raw: Long)
\ No newline at end of file
+data class EntityId(val raw: Long): ID(raw)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
new file mode 100644
index 0000000..a0f7dcf
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
@@ -0,0 +1,57 @@
+package com.dropbear.animation
+
+import com.dropbear.EntityId
+import com.dropbear.ecs.Component
+
+class AnimationComponent(parentEntity: EntityId) : Component(parentEntity, "AnimationComponent") {
+    var activeAnimationIndex: Int?
+        get() = getActiveAnimationIndex()
+        set(value) = setActiveAnimationIndex(value)
+
+    var time: Float
+        get() = getTime()
+        set(value) = setTime(value)
+
+    var speed: Float
+        get() = getSpeed()
+        set(value) = setSpeed(value)
+
+    var looping: Boolean
+        get() = getLooping()
+        set(value) = setLooping(value)
+
+    var isPlaying: Boolean
+        get() = getIsPlaying()
+        set(value) = setIsPlaying(value)
+
+    fun pause() {
+        isPlaying = false
+    }
+
+    fun play() {
+        isPlaying = true
+    }
+
+    fun stop() {
+        isPlaying = false
+        time = 0f
+        activeAnimationIndex = null
+    }
+
+    fun reset() {
+        time = 0f
+    }
+
+    fun setAnimation(index: Int, speed: Float = 1f) = setActiveAnimationIndex(index).let { setSpeed(speed) }
+}
+
+expect fun AnimationComponent.getActiveAnimationIndex(): Int?
+expect fun AnimationComponent.setActiveAnimationIndex(index: Int?)
+expect fun AnimationComponent.getTime(): Float
+expect fun AnimationComponent.setTime(value: Float)
+expect fun AnimationComponent.getSpeed(): Float
+expect fun AnimationComponent.setSpeed(value: Float)
+expect fun AnimationComponent.getLooping(): Boolean
+expect fun AnimationComponent.setLooping(value: Boolean)
+expect fun AnimationComponent.getIsPlaying(): Boolean
+expect fun AnimationComponent.setIsPlaying(value: Boolean)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetHandle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetHandle.kt
deleted file mode 100644
index 3a9459e..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/asset/AssetHandle.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.dropbear.asset
-
-import com.dropbear.DropbearEngine
-
-/**
- * A handle that describes the type of asset in the ASSET_REGISTRY
- */
-class AssetHandle(private val id: Long): Handle(id) {
-    /**
-     * Converts an [AssetHandle] to a [ModelHandle].
-     *
-     * It can return null if the asset is not a model.
-     */
-    fun asModelHandle(): ModelHandle? {
-        val result = isModelHandle(id)
-        return if (result) {
-            ModelHandle(id)
-        } else {
-            null
-        }
-    }
-
-    override fun asAssetHandle(): AssetHandle {
-        return this
-    }
-
-    /**
-     * Converts an [AssetHandle] to a [TextureHandle].
-     *
-     * It can return null if the asset is not a texture.
-     */
-    fun asTextureHandle(): TextureHandle? {
-        return if (isTextureHandle(id)) TextureHandle(id) else null
-    }
-
-    override fun toString(): String {
-        return "AssetHandle(id=$id)"
-    }
-}
-
-
-internal expect fun isTextureHandle(id: Long): Boolean
-internal expect fun isModelHandle(id: Long): Boolean
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
new file mode 100644
index 0000000..ac80dcd
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
@@ -0,0 +1,3 @@
+package com.dropbear.asset
+
+open class AssetType internal constructor(open val id: Long)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt
index 57f134c..d0183db 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Handle.kt
@@ -1,46 +1,28 @@
 package com.dropbear.asset
 
+import com.dropbear.utils.ID
+
 /**
  * Describes a handle of an asset, or anything really.
  *
- * Aims to allow people to group up different handle types ([AssetHandle], [ModelHandle] etc...)
+ * Aims to allow people to group up different handle types
  * into a list or a vector.
  *
  * All handles must be positive, non-zero values. If the id does not follow that rule, it is considered invalid.
  */
-abstract class Handle(private val id: Long) {
+class Handle<T: AssetType>(private val id: Long): ID(id) {
     init {
-        require(id > 0) { "Handle id must be a positive, non-zero value. Got: $id" }
+        require(id > 0) { "Handle id must be a positive value. Got: $id" }
     }
 
-    /**
-     * Returns the raw id of the handle
-     */
-    fun raw(): Long = id
-
-    /**
-     * Returns the handle as an [AssetHandle].
-     *
-     * This will not return null as all handles are a type of [AssetHandle].
-     */
-    abstract fun asAssetHandle(): AssetHandle
-
-    override fun toString(): String {
-        return "Handle(id=$id)"
+    companion object {
+        fun <T: AssetType> invalid() = Handle<T>(0)
     }
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-
-        val otherAsset = when (other) {
-            is Handle      -> other.asAssetHandle()
-            is AssetHandle -> other
-            else           -> return false
-        }
-
-        val thisAsset = asAssetHandle()
-        return thisAsset.raw() == otherAsset.raw()
+    fun raw(): Long {
+        return id
     }
+}
 
-    override fun hashCode(): Int = asAssetHandle().raw().hashCode()
-}
\ No newline at end of file
+typealias TextureHandle = Handle<Texture>
+typealias ModelHandle = Handle<Model>
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
new file mode 100644
index 0000000..fb70859
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
@@ -0,0 +1,34 @@
+package com.dropbear.asset
+
+import com.dropbear.asset.model.Mesh
+import com.dropbear.asset.model.Material
+import com.dropbear.asset.model.Skin
+import com.dropbear.asset.model.Animation
+import com.dropbear.asset.model.Node
+
+class Model(override val id: Long): AssetType(id) {
+    val label: String
+        get() = getLabel()
+
+    val meshes: List<Mesh>
+        get() = getMeshes()
+
+    val materials: List<Material>
+        get() = getMaterials()
+
+    val skins: List<Skin>
+        get() = getSkins()
+
+    val animations: List<Animation>
+        get() = getAnimations()
+
+    val nodes: List<Node>
+        get() = getNodes()
+}
+
+expect fun Model.getLabel(): String
+expect fun Model.getMeshes(): List<Mesh>
+expect fun Model.getMaterials(): List<Material>
+expect fun Model.getSkins(): List<Skin>
+expect fun Model.getAnimations(): List<Animation>
+expect fun Model.getNodes(): List<Node>
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/ModelHandle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/ModelHandle.kt
deleted file mode 100644
index a31f87e..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/asset/ModelHandle.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.dropbear.asset
-
-/**
- * Another type of asset handle, this wraps the id of the asset
- * into something that only models can access. 
- */
-class ModelHandle(private val id: Long): Handle(id) {
-    override fun asAssetHandle(): AssetHandle = AssetHandle(id)
-
-    override fun toString(): String {
-        return "ModelHandle(id=$id)"
-    }
-}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
new file mode 100644
index 0000000..a6b7f70
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
@@ -0,0 +1,22 @@
+package com.dropbear.asset
+
+class Texture(override val id: Long): AssetType(id) {
+    var label: String?
+        get() = getLabel()
+        set(value) = setLabel(value)
+
+    val width: Int
+        get() = getWidth()
+
+    val height: Int
+        get() = getHeight()
+
+    val depth: Int
+        get() = getDepth()
+}
+
+expect fun Texture.getLabel(): String?
+expect fun Texture.setLabel(value: String?)
+expect fun Texture.getWidth(): Int
+expect fun Texture.getHeight(): Int
+expect fun Texture.getDepth(): Int
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/TextureHandle.kt b/scripting/commonMain/kotlin/com/dropbear/asset/TextureHandle.kt
deleted file mode 100644
index a93257a..0000000
--- a/scripting/commonMain/kotlin/com/dropbear/asset/TextureHandle.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.dropbear.asset
-
-/**
- * Another type of asset handle, this wraps the id into 
- * another form that only texture related functions can use. 
- */
-class TextureHandle(private val id: Long): Handle(id) {
-    /**
-     * The name of the texture/material.
-     */
-    val name: String?
-        get() = getTextureName(id)
-
-    override fun asAssetHandle(): AssetHandle = AssetHandle(id)
-
-    override fun toString(): String {
-        return "TextureHandle(id=$id)"
-    }
-}
-
-internal expect fun TextureHandle.getTextureName(id: Long): String?
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
new file mode 100644
index 0000000..fe9649a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
@@ -0,0 +1,51 @@
+package com.dropbear.asset.model
+
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Quaternionf
+
+data class Animation(
+    val name: String,
+    val channels: List<AnimationChannel>,
+    val duration: Float
+)
+
+data class AnimationChannel(
+    val targetNode: Int,
+    val times: DoubleArray,
+    val values: ChannelValues,
+    val interpolation: AnimationInterpolation
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as AnimationChannel
+
+        if (targetNode != other.targetNode) return false
+        if (!times.contentEquals(other.times)) return false
+        if (values != other.values) return false
+        if (interpolation != other.interpolation) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = targetNode
+        result = 31 * result + times.contentHashCode()
+        result = 31 * result + values.hashCode()
+        result = 31 * result + interpolation.hashCode()
+        return result
+    }
+}
+
+enum class AnimationInterpolation {
+    LINEAR,
+    STEP,
+    CUBIC_SPLINE
+}
+
+sealed class ChannelValues {
+    data class Translations(val values: List<Vector3f>) : ChannelValues()
+    data class Rotations(val values: List<Quaternionf>) : ChannelValues()
+    data class Scales(val values: List<Vector3f>) : ChannelValues()
+}
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Material.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Material.kt
new file mode 100644
index 0000000..9e1c9ce
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Material.kt
@@ -0,0 +1,24 @@
+package com.dropbear.asset.model
+
+import com.dropbear.asset.Texture
+import com.dropbear.math.Vector2f
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Vector4f
+
+data class Material(
+    val name: String,
+    val diffuseTexture: Texture,
+    val normalTexture: Texture,
+    val tint: Vector4f,
+    val emissiveFactor: Vector3f,
+    val metallicFactor: Float,
+    val roughnessFactor: Float,
+    val alphaCutoff: Float?,
+    val doubleSided: Boolean,
+    val occlusionStrength: Float,
+    val normalScale: Float,
+    val uvTiling: Vector2f,
+    val emissiveTexture: Texture?,
+    val metallicRoughnessTexture: Texture?,
+    val occlusionTexture: Texture?
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
new file mode 100644
index 0000000..e93890e
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
@@ -0,0 +1,8 @@
+package com.dropbear.asset.model
+
+data class Mesh(
+    val name: String,
+    val numElements: Int,
+    val materialIndex: Int,
+    val vertices: List<ModelVertex>
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/ModelVertex.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/ModelVertex.kt
new file mode 100644
index 0000000..82f946c
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/ModelVertex.kt
@@ -0,0 +1,16 @@
+package com.dropbear.asset.model
+
+import com.dropbear.math.Vector2f
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Vector4f
+
+data class ModelVertex(
+    val position: Vector3f,
+    val normal: Vector3f,
+    val tangent: Vector4f,
+    val texCoords0: Vector2f,
+    val texCoords1: Vector2f,
+    val colour0: Vector4f,
+    val joints0: IntArray,
+    val weights0: Vector4f
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Node.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Node.kt
new file mode 100644
index 0000000..c9b3a6a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Node.kt
@@ -0,0 +1,17 @@
+package com.dropbear.asset.model
+
+import com.dropbear.math.Vector3f
+import com.dropbear.math.Quaternionf
+
+data class NodeTransform(
+    val translation: Vector3f,
+    val rotation: Quaternionf,
+    val scale: Vector3f
+)
+
+data class Node(
+    val name: String,
+    val parent: Int?,
+    val children: List<Int>,
+    val transform: NodeTransform
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Skin.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Skin.kt
new file mode 100644
index 0000000..5f31487
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Skin.kt
@@ -0,0 +1,8 @@
+package com.dropbear.asset.model
+
+data class Skin(
+    val name: String,
+    val joints: List<Int>,
+    val inverseBindMatrices: List<DoubleArray>,
+    val skeletonRoot: Int?
+)
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
new file mode 100644
index 0000000..218ee65
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
@@ -0,0 +1,50 @@
+package com.dropbear.components.camera
+
+import com.dropbear.math.Vector3d
+import com.dropbear.physics.ColliderShape
+import com.dropbear.physics.Physics
+
+/**
+ * A springy camera that is used for any camera. It uses ray casting to check if the distance from the player to the
+ * camera is obstructed by a physics object, and if so set the distance to the last "safe" spot.
+ *
+ * Inspired by Godot's SpringyCameraController (other engines probably have their own, never used them before).
+ */
+class SpringyCameraController {
+    private var currentDistance: Double = 5.0
+    private val margin = 0.3
+    private val sphereRadius = 0.2
+
+    fun getSpringyPosition(
+        playerPos: Vector3d,
+        targetPos: Vector3d,
+        deltaTime: Double
+    ): Vector3d {
+        val vectorToCam = targetPos - playerPos
+        val maxDist = vectorToCam.length()
+        val dir = vectorToCam.normalize()
+
+        val hit = Physics.shapeCast(
+            origin = playerPos,
+            shape = ColliderShape.Sphere(sphereRadius.toFloat()),
+            direction = dir,
+            maxDistance = maxDist,
+            solid = false
+        )
+
+        val targetDist = if (hit != null) {
+            (hit.distance - margin).coerceAtLeast(0.1)
+        } else {
+            maxDist
+        }
+
+        if (targetDist < currentDistance) {
+            currentDistance = targetDist
+        } else {
+            val returnSpeed = 5.0
+            currentDistance += (targetDist - currentDistance) * (returnSpeed * deltaTime).coerceIn(0.0, 1.0)
+        }
+
+        return playerPos + (dir * currentDistance)
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/logging/LogLevel.kt b/scripting/commonMain/kotlin/com/dropbear/logging/LogLevel.kt
index 91fed03..7bb0017 100644
--- a/scripting/commonMain/kotlin/com/dropbear/logging/LogLevel.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/logging/LogLevel.kt
@@ -1,9 +1,9 @@
 package com.dropbear.logging
 
-enum class LogLevel {
-    TRACE,
-    DEBUG,
-    INFO,
-    WARN,
-    ERROR
+enum class LogLevel(val ansi: String) {
+    TRACE("\u001B[38;5;81m"),
+    DEBUG("\u001B[38;5;220m"),
+    INFO("\u001B[38;5;46m"),
+    WARN("\u001B[38;5;214m"),
+    ERROR("\u001B[31m"),
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/logging/Logger.kt b/scripting/commonMain/kotlin/com/dropbear/logging/Logger.kt
index c80052d..51e655c 100644
--- a/scripting/commonMain/kotlin/com/dropbear/logging/Logger.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/logging/Logger.kt
@@ -5,7 +5,7 @@ import kotlin.jvm.JvmStatic
 
 @Suppress("unused")
 object Logger {
-    private var writer: LogWriter = StdoutWriter()
+    private var writer: LogWriter = SocketWriter()
     private var minLevel: LogLevel = LogLevel.INFO
     private var defaultTarget: String = "dropbear"
 
diff --git a/scripting/commonMain/kotlin/com/dropbear/logging/SocketWriter.kt b/scripting/commonMain/kotlin/com/dropbear/logging/SocketWriter.kt
new file mode 100644
index 0000000..42cc0e2
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/logging/SocketWriter.kt
@@ -0,0 +1,11 @@
+package com.dropbear.logging
+
+expect class SocketWriter(): LogWriter {
+    override fun log(
+        level: LogLevel,
+        target: String,
+        message: String,
+        file: String?,
+        line: Int?
+    )
+}
diff --git a/scripting/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt b/scripting/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt
index 845dfdc..c45640e 100644
--- a/scripting/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt
@@ -5,6 +5,8 @@ import kotlinx.datetime.toLocalDateTime
 import kotlin.time.Clock
 
 class StdoutWriter: LogWriter {
+    private val reset = "\u001B[0m"
+
     override fun log(
         level: LogLevel,
         target: String,
@@ -17,8 +19,9 @@ class StdoutWriter: LogWriter {
         val timestamp = now.toLocalDateTime(timeZone)
         val location = if (file != null && line != null) "[$file:$line] " else ""
         when (level) {
-            LogLevel.ERROR, LogLevel.WARN -> error("[$timestamp] [$level] $location[$target] $message")
-            else -> println("[$timestamp] [$level] $location[$target] $message")
+            LogLevel.ERROR -> println("${level.ansi}[$timestamp] [$level] $location[$target] $message$reset")
+            LogLevel.WARN -> println("${level.ansi}[$timestamp] [$level] $location[$target] $message$reset")
+            else -> println("[$timestamp] [${level.ansi}$level$reset] $location[$target] $message")
         }
     }
 }
\ 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/math/Vector3d.kt b/scripting/commonMain/kotlin/com/dropbear/math/Vector3d.kt
index 7f300c1..fd0275d 100644
--- a/scripting/commonMain/kotlin/com/dropbear/math/Vector3d.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/math/Vector3d.kt
@@ -73,6 +73,7 @@ class Vector3d(
     fun toFloat() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat())
     fun toInt() = Vector3i(x.toInt(), y.toInt(), z.toInt())
     fun toVector2d() = Vector2d(x, y)
+    fun toVector4d(w: Double) = Vector4d(x, y, z, w)
 
     fun toGeneric() = Vector3(x, y, z)
 
diff --git a/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt b/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
index 07fe528..bfc14f1 100644
--- a/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
@@ -3,6 +3,7 @@ package com.dropbear.physics
 import com.dropbear.EntityId
 import com.dropbear.ecs.Component
 import com.dropbear.ecs.ComponentType
+import com.dropbear.math.Quaterniond
 import com.dropbear.math.Vector3d
 import com.dropbear.math.degreesToRadians
 import kotlin.math.cos
@@ -28,6 +29,16 @@ class KinematicCharacterController(
     }
 
     /**
+     * Sets the kinematic rotation for this tick.
+     *
+     * This is useful for facing the character toward the camera direction while still
+     * using KCC movement.
+     */
+    fun setRotation(rotation: Quaterniond) {
+        setRotationNative(rotation)
+    }
+
+    /**
      * This function fetches the hits that are cached in the `KCC` struct.
      *
      * # Note
@@ -62,4 +73,5 @@ class KinematicCharacterController(
 internal expect fun kccExistsForEntity(entityId: EntityId): Boolean
 
 internal expect fun KinematicCharacterController.moveCharacter(dt: Double, translation: Vector3d)
+internal expect fun KinematicCharacterController.setRotationNative(rotation: Quaterniond)
 internal expect fun KinematicCharacterController.getHitsNative(): List<CharacterCollision>
\ 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
new file mode 100644
index 0000000..e98999e
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
@@ -0,0 +1,25 @@
+package com.dropbear.ui
+
+typealias UIInstructionSet = List<UIInstruction>
+
+class UIBuilder {
+    val instructions: MutableList<UIInstruction> = mutableListOf()
+    private var nextId = 0
+    internal fun generateId(): Int = nextId++
+
+    fun build(): UIInstructionSet = instructions.toList()
+}
+
+inline fun buildUI(block: UIBuilder.() -> Unit): UIInstructionSet {
+    return UIBuilder().apply(block).build()
+}
+
+fun UIBuilder.add(instruction: UIInstruction) {
+    instructions.add(instruction)
+}
+
+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/UIInstruction.kt b/scripting/commonMain/kotlin/com/dropbear/ui/UIInstruction.kt
new file mode 100644
index 0000000..0e3b19a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/UIInstruction.kt
@@ -0,0 +1,3 @@
+package com.dropbear.ui
+
+interface UIInstruction
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt b/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt
new file mode 100644
index 0000000..c197f9a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt
@@ -0,0 +1,20 @@
+package com.dropbear.ui
+
+/**
+ * Specifies a type of widget that is available. This class aims to create a more "unified" experience for
+ * describing a widget such as a button, or even a column.
+ */
+abstract class Widget {
+    /**
+     * The [WidgetId] used to differentiate between two different widgets.
+     *
+     * It is typically derived from the name of the widget.
+     */
+    abstract val id: WidgetId
+
+    /**
+     * Converts the [Widget] to a [UIInstruction] list, allowing to be added
+     * by a [UIBuilder].
+     */
+    abstract fun toInstruction(): List<UIInstruction>
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt b/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt
new file mode 100644
index 0000000..391bb2e
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt
@@ -0,0 +1,7 @@
+package com.dropbear.ui
+
+import com.dropbear.utils.ID
+
+class WidgetId(val id: Long) : ID(id) {
+    constructor(id: String) : this(id.hashCode().toLong())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Alignment.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Alignment.kt
new file mode 100644
index 0000000..56ee8f9
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Alignment.kt
@@ -0,0 +1,26 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.math.Vector2d
+
+class Alignment(
+    var x: Double,
+    var y: Double,
+) {
+    companion object {
+        val TOP_LEFT = Alignment(0.0, 0.0)
+        val TOP_CENTER = Alignment(0.5, 0.0)
+        val TOP_RIGHT = Alignment(1.0, 0.0)
+
+        val CENTER_LEFT = Alignment(0.0, 0.5)
+        val CENTER = Alignment(0.5, 0.5)
+        val CENTER_RIGHT = Alignment(1.0, 0.5)
+
+        val BOTTOM_LEFT = Alignment(0.0, 1.0)
+        val BOTTOM_CENTER = Alignment(0.5, 1.0)
+        val BOTTOM_RIGHT = Alignment(1.0, 1.0)
+    }
+
+    fun asVector2d(): Vector2d {
+        return Vector2d(x, y)
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt
new file mode 100644
index 0000000..5e9d853
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt
@@ -0,0 +1,14 @@
+package com.dropbear.ui.styling
+
+enum class Anchor {
+    /**
+     * A center anchor is when the position is based on the center of the object (such as the
+     * center of a circle)
+     */
+    Center,
+
+    /**
+     * A top left anchor is when the position is based on the top left corner of the object.
+     */
+    TopLeft,
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Border.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Border.kt
new file mode 100644
index 0000000..9d481aa
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Border.kt
@@ -0,0 +1,9 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.utils.Colour
+
+data class Border(
+    val colour: Colour,
+    val width: Double,
+) {
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/BorderRadius.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/BorderRadius.kt
new file mode 100644
index 0000000..e8390d1
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/BorderRadius.kt
@@ -0,0 +1,55 @@
+package com.dropbear.ui.styling
+
+data class BorderRadius(
+    var topLeft: Double = 0.0,
+    var topRight: Double = 0.0,
+    var bottomLeft: Double = 0.0,
+    var bottomRight: Double = 0.0
+) {
+    companion object {
+        fun uniform(value: Double): BorderRadius {
+            return BorderRadius(
+                value,
+                value,
+                value,
+                value,
+            )
+        }
+
+        fun top(value: Double): BorderRadius {
+            return BorderRadius(
+                value,
+                value,
+                0.0,
+                0.0,
+            )
+        }
+
+        fun bottom(value: Double): BorderRadius {
+            return BorderRadius(
+                0.0,
+                0.0,
+                value,
+                value,
+            )
+        }
+
+        fun left(value: Double): BorderRadius {
+            return BorderRadius(
+                value,
+                0.0,
+                value,
+                0.0,
+            )
+        }
+
+        fun right(value: Double): BorderRadius {
+            return BorderRadius(
+                0.0,
+                value,
+                0.0,
+                value,
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/DynamicButtonStyle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/DynamicButtonStyle.kt
new file mode 100644
index 0000000..e2d2e00
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/DynamicButtonStyle.kt
@@ -0,0 +1,10 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.ui.styling.fonts.TextAlignment
+import com.dropbear.utils.Colour
+
+data class DynamicButtonStyle(
+    var text: TextStyle = TextStyle(align = TextAlignment.Center),
+    var fill: Colour = Colour.GRAY,
+    var border: Border? = null,
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt
new file mode 100644
index 0000000..c564aad
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt
@@ -0,0 +1,5 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.utils.Colour
+
+data class Fill(var colour: Colour)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Padding.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Padding.kt
new file mode 100644
index 0000000..f3f72f3
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Padding.kt
@@ -0,0 +1,42 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.math.Vector2d
+
+data class Padding(
+    var left: Double,
+    var right: Double,
+    var top: Double,
+    var bottom: Double,
+) {
+    constructor(value: Double): this(value, value, value, value)
+    constructor(horizontal: Double, vertical: Double) : this(horizontal, horizontal, vertical, vertical)
+
+    companion object {
+        fun zero() = Padding.all(0.0)
+
+        fun all(value: Double) = Padding(value, value, value, value)
+        fun balanced(horizontal: Double, vertical: Double) = Padding(horizontal, horizontal, vertical, vertical)
+        fun horizontal(horizontal: Double) = Padding(horizontal, horizontal, 0.0, 0.0)
+        fun vertical(vertical: Double) = Padding(0.0, 0.0, vertical, vertical)
+    }
+
+    fun offset(): Vector2d = Vector2d(left, top)
+
+    override fun toString() = "($left, $right, $top, $bottom)"
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Padding) return false
+        if (left != other.left) return false
+        if (right != other.right) return false
+        if (top != other.top) return false
+        if (bottom != other.bottom) return false
+        return true
+    }
+    override fun hashCode(): Int {
+        var result = left.hashCode()
+        result = 31 * result + right.hashCode()
+        result = 31 * result + top.hashCode()
+        result = 31 * result + bottom.hashCode()
+        return result
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/TextStyle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/TextStyle.kt
new file mode 100644
index 0000000..23329b1
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/TextStyle.kt
@@ -0,0 +1,23 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.ui.styling.fonts.Family
+import com.dropbear.ui.styling.fonts.FontAttributes
+import com.dropbear.ui.styling.fonts.FontName
+import com.dropbear.ui.styling.fonts.TextAlignment
+import com.dropbear.utils.Colour
+
+data class TextStyle(
+    var fontSize: Double = 14.0,
+    var lineHeightOverride: Double? = null,
+    var colour: Colour = Colour.WHITE,
+    var align: TextAlignment = TextAlignment.Start,
+    var attrs: FontAttributes = FontAttributes(
+        family = Family.SansSerif,
+    ),
+) {
+    companion object {
+        fun label(): TextStyle {
+            return TextStyle()
+        }
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/CacheKeyFlags.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/CacheKeyFlags.kt
new file mode 100644
index 0000000..4da6aef
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/CacheKeyFlags.kt
@@ -0,0 +1,16 @@
+package com.dropbear.ui.styling.fonts
+
+import kotlin.jvm.JvmInline
+
+@JvmInline
+value class CacheKeyFlags(val bits: Int = 0) {
+    operator fun plus(flag: CacheKeyFlags): CacheKeyFlags =
+        CacheKeyFlags(bits or flag.bits)
+
+    operator fun contains(flag: CacheKeyFlags): Boolean =
+        (bits and flag.bits) != 0
+
+    companion object {
+        val NONE = CacheKeyFlags(0)
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/CacheMetrics.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/CacheMetrics.kt
new file mode 100644
index 0000000..fcb6c48
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/CacheMetrics.kt
@@ -0,0 +1,6 @@
+package com.dropbear.ui.styling.fonts
+
+class CacheMetrics(
+    val fontSize: Double,
+    val lineHeight: Double,
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/Family.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/Family.kt
new file mode 100644
index 0000000..c6d50bd
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/Family.kt
@@ -0,0 +1,10 @@
+package com.dropbear.ui.styling.fonts
+
+sealed interface Family {
+    data class Name(val value: String) : Family
+    data object Serif : Family
+    data object SansSerif : Family
+    data object Cursive : Family
+    data object Fantasy : Family
+    data object Monospace : Family
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FeatureTag.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FeatureTag.kt
new file mode 100644
index 0000000..aeb1497
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FeatureTag.kt
@@ -0,0 +1,33 @@
+package com.dropbear.ui.styling.fonts
+
+import kotlin.jvm.JvmInline
+
+@JvmInline
+value class FeatureTag(val tag: ByteArray) {
+    init {
+        require(tag.size == 4) { "Tag must be exactly 4 bytes" }
+    }
+
+    companion object {
+        /** Kerning adjusts spacing between specific character pairs */
+        val KERNING = FeatureTag("kern".encodeToByteArray())
+        /** Standard ligatures (fi, fl, etc.) */
+        val STANDARD_LIGATURES = FeatureTag("liga".encodeToByteArray())
+        /** Contextual ligatures (context-dependent ligatures) */
+        val CONTEXTUAL_LIGATURES = FeatureTag("clig".encodeToByteArray())
+        /** Contextual alternates (glyph substitutions based on context) */
+        val CONTEXTUAL_ALTERNATES = FeatureTag("calt".encodeToByteArray())
+        /** Discretionary ligatures (optional stylistic ligatures) */
+        val DISCRETIONARY_LIGATURES = FeatureTag("dlig".encodeToByteArray())
+        /** Small caps (lowercase to small capitals) */
+        val SMALL_CAPS = FeatureTag("smcp".encodeToByteArray())
+        /** All small caps (uppercase and lowercase to small capitals) */
+        val ALL_SMALL_CAPS = FeatureTag("c2sc".encodeToByteArray())
+        /** Stylistic Set 1 (font-specific alternate glyphs) */
+        val STYLISTIC_SET_1 = FeatureTag("ss01".encodeToByteArray())
+        /** Stylistic Set 2 (font-specific alternate glyphs) */
+        val STYLISTIC_SET_2 = FeatureTag("ss02".encodeToByteArray())
+    }
+
+    fun asBytes(): ByteArray = tag
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontAttributes.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontAttributes.kt
new file mode 100644
index 0000000..a033d4b
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontAttributes.kt
@@ -0,0 +1,16 @@
+package com.dropbear.ui.styling.fonts
+
+import com.dropbear.utils.Colour
+
+class FontAttributes(
+    var colourOptions: Colour? = null,
+    var family: Family = Family.Name("default"),
+    var stretch: Stretch = Stretch.Normal,
+    var style: FontStyle = FontStyle.Normal,
+    var weight: FontWeight = FontWeight.NORMAL,
+    var metadata: UInt = 0u,
+    var cacheKeyFlags: CacheKeyFlags = CacheKeyFlags.NONE,
+    var metricsOptions: CacheMetrics? = null,
+    var letterSpacingOptions: Double? = null,
+    var fontFeatures: FontFeatures = FontFeatures(),
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontFeature.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontFeature.kt
new file mode 100644
index 0000000..250e209
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontFeature.kt
@@ -0,0 +1,6 @@
+package com.dropbear.ui.styling.fonts
+
+class FontFeature(
+    val tag: FeatureTag,
+    val value: UInt,
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontFeatures.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontFeatures.kt
new file mode 100644
index 0000000..cdb38b1
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontFeatures.kt
@@ -0,0 +1,15 @@
+package com.dropbear.ui.styling.fonts
+
+class FontFeatures(val features: MutableList<FontFeature> = mutableListOf()) {
+    fun set(tag: FeatureTag, value: UInt) {
+        features.add(FontFeature(tag, value))
+    }
+
+    fun enable(tag: FeatureTag) {
+        set(tag, 1u)
+    }
+
+    fun disable(tag: FeatureTag) {
+        set(tag, 0u)
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontName.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontName.kt
new file mode 100644
index 0000000..5f4998f
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontName.kt
@@ -0,0 +1,3 @@
+package com.dropbear.ui.styling.fonts
+
+data class FontName(val name: String)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontStyle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontStyle.kt
new file mode 100644
index 0000000..bbd4aa4
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontStyle.kt
@@ -0,0 +1,7 @@
+package com.dropbear.ui.styling.fonts
+
+enum class FontStyle {
+    Normal,
+    Italic,
+    Oblique,
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontWeight.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontWeight.kt
new file mode 100644
index 0000000..4e3832a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/FontWeight.kt
@@ -0,0 +1,15 @@
+package com.dropbear.ui.styling.fonts
+
+class FontWeight(val value: Int) {
+    companion object {
+        val THIN = FontWeight(100)
+        val EXTRA_LIGHT = FontWeight(200)
+        val LIGHT = FontWeight(300)
+        val NORMAL = FontWeight(400)
+        val MEDIUM = FontWeight(500)
+        val SEMIBOLD = FontWeight(600)
+        val BOLD = FontWeight(700)
+        val EXTRA_BOLD = FontWeight(800)
+        val BLACK = FontWeight(900)
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/Stretch.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/Stretch.kt
new file mode 100644
index 0000000..319e4f3
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/Stretch.kt
@@ -0,0 +1,13 @@
+package com.dropbear.ui.styling.fonts
+
+enum class Stretch {
+    UltraCondensed,
+    ExtraCondensed,
+    Condensed,
+    SemiCondensed,
+    Normal,
+    SemiExpanded,
+    Expanded,
+    ExtraExpanded,
+    UltraExpanded,
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/TextAlignment.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/TextAlignment.kt
new file mode 100644
index 0000000..ceb3ea7
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/fonts/TextAlignment.kt
@@ -0,0 +1,7 @@
+package com.dropbear.ui.styling.fonts
+
+enum class TextAlignment {
+    Start,
+    Center,
+    End,
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt
new file mode 100644
index 0000000..8a0bab7
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt
@@ -0,0 +1,70 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.styling.Anchor
+
+class Column(
+	override var id: WidgetId,
+	var anchor: Anchor = Anchor.TopLeft,
+	var position: Vector2d = Vector2d.zero(),
+	var spacing: Double = 8.0,
+): Widget() {
+	sealed class ColumnInstruction: UIInstruction {
+		data class StartColumnBlock(val id: WidgetId, val column: com.dropbear.ui.widgets.Column) : ColumnInstruction()
+		data class EndColumnBlock(val id: WidgetId) : ColumnInstruction()
+	}
+
+	override fun toInstruction(): List<UIInstruction> {
+		return listOf(ColumnInstruction.StartColumnBlock(id, this), ColumnInstruction.EndColumnBlock(id))
+	}
+
+	fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+		return listOf(ColumnInstruction.StartColumnBlock(id, this)) +
+			children +
+			ColumnInstruction.EndColumnBlock(id)
+	}
+
+	fun startInstruction(): UIInstruction = ColumnInstruction.StartColumnBlock(id, this)
+
+	fun endInstruction(): UIInstruction = ColumnInstruction.EndColumnBlock(id)
+
+	fun at(position: Vector2d): Column {
+		this.position = position
+		return this
+	}
+
+	fun spacing(spacing: Double): Column {
+		this.spacing = spacing
+		return this
+	}
+
+	fun withAnchor(anchor: Anchor): Column {
+		this.anchor = anchor
+		return this
+	}
+}
+
+fun UIBuilder.column(id: WidgetId = WidgetId(generateId().toLong()), block: Column.() -> Unit = {}): Column {
+	val column = Column(id = id).apply(block)
+	column.toInstruction().forEach { instructions.add(it) }
+	return column
+}
+
+fun UIBuilder.column(id: WidgetId, content: UIBuilder.(Column) -> Unit) {
+	column(id, columnBlock = {}, content = content)
+}
+
+fun UIBuilder.column(
+	id: WidgetId,
+	columnBlock: Column.() -> Unit = {},
+	content: UIBuilder.(Column) -> Unit
+) {
+	val column = Column(id = id).apply(columnBlock)
+	instructions.add(column.startInstruction())
+	this.content(column)
+	instructions.add(column.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt
new file mode 100644
index 0000000..fd1efd4
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt
@@ -0,0 +1,118 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.asset.Handle
+import com.dropbear.asset.Texture
+import com.dropbear.math.Angle
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.styling.Anchor
+import com.dropbear.ui.styling.Border
+import com.dropbear.ui.styling.Fill
+import com.dropbear.utils.Colour
+
+class Rectangle(
+    override var id: WidgetId,
+    var anchor: Anchor = Anchor.TopLeft,
+    var position: Vector2d = Vector2d.zero(),
+    var size: Vector2d = Vector2d(64.0, 128.0),
+    var texture: Handle<Texture>? = null,
+    var rotation: Angle = Angle.ZERO,
+    var uv: List<Vector2d> = listOf(Vector2d.zero(), Vector2d(1.0, 0.0), Vector2d.one(), Vector2d(0.0, 1.0)),
+    var fill: Fill = Fill(Colour.WHITE),
+    var border: Border? = null
+): Widget() {
+    init {
+        require(uv.size == 4) { "Rectangle uv must have 4 coordinates." }
+    }
+
+    sealed class RectangleInstruction: UIInstruction {
+        data class Rectangle(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction()
+        data class StartRectangleBlock(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction()
+        data class EndRectangleBlock(val id: WidgetId) : RectangleInstruction()
+    }
+
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(RectangleInstruction.Rectangle(id, this))
+    }
+
+    fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+        return listOf(RectangleInstruction.StartRectangleBlock(id, this)) +
+            children +
+            RectangleInstruction.EndRectangleBlock(id)
+    }
+
+    fun startInstruction(): UIInstruction = RectangleInstruction.StartRectangleBlock(id, this)
+
+    fun endInstruction(): UIInstruction = RectangleInstruction.EndRectangleBlock(id)
+
+    fun with(position: Vector2d, size: Vector2d): Rectangle {
+        this.position = position
+        this.size = size
+        return this
+    }
+
+    fun withAnchor(anchor: Anchor): Rectangle {
+        this.anchor = anchor
+        return this
+    }
+
+    fun at(position: Vector2d): Rectangle {
+        this.position = position
+        return this
+    }
+
+    fun size(size: Vector2d): Rectangle {
+        this.size = size
+        return this
+    }
+
+    fun fill(fill: Fill): Rectangle {
+        this.fill = fill
+        return this
+    }
+
+    fun border(border: Border?): Rectangle {
+        this.border = border
+        return this
+    }
+
+    fun rotate(angle: Angle): Rectangle {
+        this.rotation = angle
+        return this
+    }
+
+    fun texture(texture: Handle<Texture>?): Rectangle {
+        this.texture = texture
+        return this
+    }
+
+    fun uv(coords: List<Vector2d>): Rectangle {
+        require(coords.size == 4) { "Rectangle uv must have 4 coordinates." }
+        this.uv = coords
+        return this
+    }
+}
+
+fun UIBuilder.rectangle(id: WidgetId = WidgetId(generateId().toLong()), block: Rectangle.() -> Unit = {}): Rectangle {
+    val rect = Rectangle(id = id).apply(block)
+    rect.toInstruction().forEach { instructions.add(it) }
+    return rect
+}
+
+fun UIBuilder.container(id: WidgetId, block: UIBuilder.(Rectangle) -> Unit) {
+    container(id, rectBlock = {}, content = block)
+}
+
+fun UIBuilder.container(
+    id: WidgetId,
+    rectBlock: Rectangle.() -> Unit = {},
+    content: UIBuilder.(Rectangle) -> Unit
+) {
+    val rect = Rectangle(id = id).apply(rectBlock)
+    instructions.add(rect.startInstruction())
+    this.content(rect)
+    instructions.add(rect.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt
new file mode 100644
index 0000000..955ee08
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt
@@ -0,0 +1,70 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.styling.Anchor
+
+class Row(
+	override var id: WidgetId,
+	var anchor: Anchor = Anchor.TopLeft,
+	var position: Vector2d = Vector2d.zero(),
+	var spacing: Double = 8.0,
+): Widget() {
+	sealed class RowInstruction: UIInstruction {
+		data class StartRowBlock(val id: WidgetId, val row: com.dropbear.ui.widgets.Row) : RowInstruction()
+		data class EndRowBlock(val id: WidgetId) : RowInstruction()
+	}
+
+	override fun toInstruction(): List<UIInstruction> {
+		return listOf(RowInstruction.StartRowBlock(id, this), RowInstruction.EndRowBlock(id))
+	}
+
+	fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+		return listOf(RowInstruction.StartRowBlock(id, this)) +
+			children +
+			RowInstruction.EndRowBlock(id)
+	}
+
+	fun startInstruction(): UIInstruction = RowInstruction.StartRowBlock(id, this)
+
+	fun endInstruction(): UIInstruction = RowInstruction.EndRowBlock(id)
+
+	fun at(position: Vector2d): Row {
+		this.position = position
+		return this
+	}
+
+	fun spacing(spacing: Double): Row {
+		this.spacing = spacing
+		return this
+	}
+
+	fun withAnchor(anchor: Anchor): Row {
+		this.anchor = anchor
+		return this
+	}
+}
+
+fun UIBuilder.row(id: WidgetId = WidgetId(generateId().toLong()), block: Row.() -> Unit = {}): Row {
+	val row = Row(id = id).apply(block)
+	row.toInstruction().forEach { instructions.add(it) }
+	return row
+}
+
+fun UIBuilder.row(id: WidgetId, content: UIBuilder.(Row) -> Unit) {
+	row(id, rowBlock = {}, content = content)
+}
+
+fun UIBuilder.row(
+	id: WidgetId,
+	rowBlock: Row.() -> Unit = {},
+	content: UIBuilder.(Row) -> Unit
+) {
+	val row = Row(id = id).apply(rowBlock)
+	instructions.add(row.startInstruction())
+	this.content(row)
+	instructions.add(row.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt
new file mode 100644
index 0000000..7beb2e1
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt
@@ -0,0 +1,70 @@
+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.Padding
+import com.dropbear.ui.styling.TextStyle
+
+class Text(
+    var text: String,
+    var style: TextStyle = TextStyle(),
+    var padding: Padding,
+    id: WidgetId = WidgetId(text.hashCode().toLong()),
+) : Widget() {
+    override var id: WidgetId
+
+    init {
+        this.id = id
+    }
+
+    companion object {
+        fun withStyle(text: String, style: TextStyle, id: String = text): Text {
+            val text = Text(text, style, padding = Padding.zero())
+            text.id = WidgetId(id.hashCode().toLong())
+            return text
+        }
+
+        fun label(text: String, id: String = text): Text {
+            val text = Text(text, TextStyle(), Padding.all(8.0))
+            text.id = WidgetId(id.hashCode().toLong())
+            return text
+        }
+    }
+
+    sealed class TextInstruction: UIInstruction {
+        data class Text(val id: WidgetId, val text: com.dropbear.ui.widgets.Text) : TextInstruction()
+    }
+
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(TextInstruction.Text(this.id, this))
+    }
+}
+
+fun UIBuilder.label(text: String, block: Text.() -> Unit = {}): Text {
+    val style = TextStyle()
+    val text = Text(
+        text = text,
+        style = style,
+        padding = Padding.zero()
+    ).apply(block)
+    text.toInstruction().forEach {
+        instructions.add(it)
+    }
+    return text
+}
+
+fun UIBuilder.text(size: Double, text: String, block: Text.() -> Unit = {}): Text {
+    val style = TextStyle()
+    style.fontSize = size
+    val text = Text(
+        text = text,
+        style = style,
+        padding = Padding.zero()
+    ).apply(block)
+    text.toInstruction().forEach {
+        instructions.add(it)
+    }
+    return text
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt b/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt
index 009014b..626bbeb 100644
--- a/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt
@@ -17,15 +17,64 @@ class Colour(
     var b: UByte,
     var a: UByte,
 ) {
+    companion object {
+        val RED = Colour.rgb(255u, 0u, 0u)
+        val GREEN = Colour.rgb(0u, 255u, 0u)
+        val BLUE = Colour.rgb(0u, 0u, 255u)
+        val YELLOW = Colour.rgb(255u, 255u, 0u)
+        val CYAN = Colour.rgb(0u, 255u, 255u)
+        val FUCHSIA = Colour.rgb(255u, 255u, 0u)
+        val GRAY = Colour.rgb(127u, 127u, 127u)
+        val TRANSPARENT = Colour(0u, 0u, 0u, 0u)
+        val WHITE = Colour.rgb(255u, 255u, 255u)
+        val BLACK = Colour.rgb(0u, 0u, 0u)
+
+        val BACKGROUND_1 = Colour.rgb(31u, 31u, 31u)
+        val BACKGROUND_2 = Colour.rgb(42u, 42u, 42u)
+        val BACKGROUND_3 = Colour.rgb(54u, 54u, 54u)
+
+        val TEXT = Colour.rgb(255u, 255u, 255u)
+        val TEXT_MUTED = Colour.rgb(147u, 147u, 147u)
+
+        fun hex(colour: UInt): Colour {
+            val r = ((colour shr 16) and 255u).toByte().toUByte()
+            val g = ((colour shr 8) and 255u).toByte().toUByte()
+            val b = (colour and 255u).toByte().toUByte()
+
+            return Colour(r, g, b, 255u)
+        }
+
+        fun rgb(r: UByte, g: UByte, b: UByte): Colour {
+            return Colour(r, g, b, 255u)
+        }
+
+        fun fromNormalized(norm: Vector4d): Colour {
+            return Colour(
+                r = (norm.x * 255).toInt().toUByte(),
+                g = (norm.y * 255).toInt().toUByte(),
+                b = (norm.z * 255).toInt().toUByte(),
+                a = (norm.w * 255).toInt().toUByte()
+            )
+        }
+    }
+
     /**
      * Divides all values by `255` and creates a new [Vector4d] object.
      */
     fun normalize(): Vector4d {
         return Vector4d(
-            x=(r/ 255u).toDouble(),
-            y=(g/ 255u).toDouble(),
-            z=(b/ 255u).toDouble(),
-            w=(a/ 255u).toDouble(),
+            x=(r.toDouble() / 255.0),
+            y=(g.toDouble() / 255.0),
+            z=(b.toDouble() / 255.0),
+            w=(a.toDouble() / 255.0),
         )
     }
+
+    fun adjust(factor: Double): Colour {
+        val linear = normalize()
+        val colour = linear.toVector3d() * factor
+        val adjusted = colour.toVector4d(linear.w)
+
+        return Colour.fromNormalized(adjusted)
+    }
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/utils/ID.kt b/scripting/commonMain/kotlin/com/dropbear/utils/ID.kt
new file mode 100644
index 0000000..5a09d29
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/utils/ID.kt
@@ -0,0 +1,32 @@
+package com.dropbear.utils
+
+/**
+ * Describes all classes that can be supplied as a form of identification, such as that of a hashcode
+ * or a number.
+ *
+ * This is different to [com.dropbear.asset.Handle] as a handle is used for assets, while an [ID] is used
+ * for identifying something.
+ *
+ * @param id The raw id value
+ */
+open class ID(id: Long) {
+    companion object {
+        fun fromString(id: String): ID {
+            return ID(hashCode().toLong())
+        }
+    }
+
+    // it has to be like this EntityId already took `raw`.
+    private val rawId: Long = id
+
+    override fun toString(): String {
+        return "ID(raw=${rawId})"
+    }
+}
+
+/**
+ * Converts a [String] into an [ID], allowing to be used for UI.
+ */
+fun String.asId(): ID {
+    return ID(hashCode().toLong())
+}
\ 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/java/com/dropbear/physics/KinematicCharacterControllerNative.java b/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
index fbf3e1a..cccdaa1 100644
--- a/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
+++ b/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
@@ -2,6 +2,7 @@ package com.dropbear.physics;
 
 import com.dropbear.EucalyptusCoreLoader;
 import com.dropbear.math.Vector3d;
+import com.dropbear.math.Quaterniond;
 
 // fuck, this got a long ass name
 public class KinematicCharacterControllerNative {
@@ -12,5 +13,6 @@ public class KinematicCharacterControllerNative {
     public static native boolean existsForEntity(long worldHandle, long entityHandle);
 
     public static native void moveCharacter(long worldHandle, long physicsHandle, long entityHandle, Vector3d translation, double deltaTime);
+    public static native void setRotation(long worldHandle, long physicsHandle, long entityHandle, Quaterniond rotation);
     public static native CharacterCollision[] getHitNative(long worldHandle, long entity);
 }
diff --git a/scripting/jvmMain/java/com/dropbear/ui/UINative.java b/scripting/jvmMain/java/com/dropbear/ui/UINative.java
new file mode 100644
index 0000000..a1b2fc8
--- /dev/null
+++ b/scripting/jvmMain/java/com/dropbear/ui/UINative.java
@@ -0,0 +1,11 @@
+package com.dropbear.ui;
+
+import com.dropbear.EucalyptusCoreLoader;
+
+public class UINative {
+    static {
+        new EucalyptusCoreLoader().ensureLoaded();
+    }
+
+    public static native void renderUI(long uiBufHandle, UIInstruction[] instructions);
+}
\ No newline at end of file
diff --git a/scripting/jvmMain/java/com/dropbear/ui/widgets/ButtonNative.java b/scripting/jvmMain/java/com/dropbear/ui/widgets/ButtonNative.java
new file mode 100644
index 0000000..f61d102
--- /dev/null
+++ b/scripting/jvmMain/java/com/dropbear/ui/widgets/ButtonNative.java
@@ -0,0 +1,12 @@
+package com.dropbear.ui.widgets;
+
+import com.dropbear.EucalyptusCoreLoader;
+
+public class ButtonNative {
+    static {
+        new EucalyptusCoreLoader().ensureLoaded();
+    }
+
+    public static native boolean getClicked(long uiBufferHandle, long id);
+    public static native boolean getHovering(long uiBufferHandle, long id);
+}
diff --git a/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java b/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java
new file mode 100644
index 0000000..e32abf2
--- /dev/null
+++ b/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java
@@ -0,0 +1,12 @@
+package com.dropbear.ui.widgets;
+
+import com.dropbear.EucalyptusCoreLoader;
+
+public class CheckboxNative {
+    static {
+        new EucalyptusCoreLoader().ensureLoaded();
+    }
+
+    public static native boolean getChecked(long uiBufferHandle, long id);
+    public static native boolean hasCheckedState(long uiBufferHandle, long id);
+}
diff --git a/scripting/jvmMain/kotlin/com/dropbear/DropbearEngine.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/DropbearEngine.jvm.kt
index 83a72f2..bf95bcd 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/DropbearEngine.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/DropbearEngine.jvm.kt
@@ -1,6 +1,8 @@
 package com.dropbear
 
-import com.dropbear.components.Camera
+import com.dropbear.logging.Logger
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.UINative
 
 internal actual fun getEntity(label: String): Long? {
     return DropbearEngineNative.getEntity(DropbearEngine.native.worldHandle, label)
@@ -12,4 +14,9 @@ internal actual fun getAsset(eucaURI: String): Long? {
 
 internal actual fun quit() {
     DropbearEngineNative.quit(DropbearEngine.native.commandBufferHandle)
+}
+
+internal actual fun renderUI(instructions: List<UIInstruction>) {
+    Logger.debug("instructions: $instructions")
+    UINative.renderUI(DropbearEngine.native.uiBufferHandle, instructions.toTypedArray())
 }
\ No newline at end of file
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/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt b/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt
index 3804406..4ed40fd 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt
@@ -10,4 +10,5 @@ class DropbearContext(
     val assetHandle: Long,
     val sceneLoaderHandle: Long,
     val physicsEngineHandle: Long,
+    val uiHandle: Long,
 )
\ No newline at end of file
diff --git a/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
index 9bcb833..5acd8f6 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
@@ -9,6 +9,7 @@ actual class NativeEngine {
     internal var assetHandle: Long = 0L
     internal var sceneLoaderHandle: Long = 0L
     internal var physicsEngineHandle: Long = 0L
+    internal var uiBufferHandle: Long = 0L
 
     @JvmName("init")
     fun init(ctx: DropbearContext) {
@@ -18,6 +19,7 @@ actual class NativeEngine {
         this.assetHandle = ctx.assetHandle
         this.sceneLoaderHandle = ctx.sceneLoaderHandle
         this.physicsEngineHandle = ctx.physicsEngineHandle
+        this.uiBufferHandle = ctx.uiHandle
 
         if (this.worldHandle <= 0L) {
             Logger.error("NativeEngine: Error - Invalid world handle received!")
@@ -43,5 +45,9 @@ actual class NativeEngine {
             Logger.error("NativeEngine: Error - Invalid physics handle received!")
             return
         }
+        if (this.uiBufferHandle <= 0L) {
+            Logger.error("NativeEngine: Error - Invalid ui command buffer handle received!")
+            return
+        }
     }
 }
\ No newline at end of file
diff --git a/scripting/jvmMain/kotlin/com/dropbear/logging/SocketWriter.kt b/scripting/jvmMain/kotlin/com/dropbear/logging/SocketWriter.kt
new file mode 100644
index 0000000..8934e07
--- /dev/null
+++ b/scripting/jvmMain/kotlin/com/dropbear/logging/SocketWriter.kt
@@ -0,0 +1,57 @@
+package com.dropbear.logging
+
+import java.net.Socket
+import java.io.PrintWriter
+import java.util.concurrent.Executors
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import kotlin.time.Clock
+
+actual class SocketWriter actual constructor() : LogWriter {
+    private val executor = Executors.newSingleThreadExecutor()
+    private var writer: PrintWriter? = null
+    
+    init {
+        Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
+            val stackTrace = java.io.StringWriter().apply {
+                throwable.printStackTrace(java.io.PrintWriter(this))
+            }.toString()
+            
+            log(LogLevel.ERROR, "UncaughtException", "Exception in thread \"${thread.name}\": $throwable\n$stackTrace", null, null)
+        }
+
+        executor.submit {
+            try {
+                // Connect to the editor console
+                val socket = Socket("127.0.0.1", 56624)
+                writer = PrintWriter(socket.getOutputStream(), true)
+            } catch (e: Exception) {
+                // Silently fail or log to stderr if connection fails
+                System.err.println("Failed to connect to logging socket: ${e.message}")
+            }
+        }
+    }
+
+    actual override fun log(
+        level: LogLevel,
+        target: String,
+        message: String,
+        file: String?,
+        line: Int?
+    ) {
+        val now = Clock.System.now()
+        val timeZone = TimeZone.currentSystemDefault()
+        val timestamp = now.toLocalDateTime(timeZone)
+        val location = if (file != null && line != null) "[$file:$line] " else ""
+        
+        val formatted = "[$timestamp] [$level] $location[$target] $message"
+        
+        executor.submit {
+            try {
+                writer?.println(formatted)
+            } catch (e: Exception) {
+                // Ignore write errors to avoid crashing
+            }
+        }
+    }
+}
diff --git a/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt
index dc306ce..a549ce0 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt
@@ -18,6 +18,15 @@ internal actual fun KinematicCharacterController.moveCharacter(dt: Double, trans
     )
 }
 
+internal actual fun KinematicCharacterController.setRotationNative(rotation: com.dropbear.math.Quaterniond) {
+    return KinematicCharacterControllerNative.setRotation(
+        DropbearEngine.native.worldHandle,
+        DropbearEngine.native.physicsEngineHandle,
+        entity.raw,
+        rotation
+    )
+}
+
 internal actual fun KinematicCharacterController.getHitsNative(): List<CharacterCollision> {
     return KinematicCharacterControllerNative.getHitNative(
         DropbearEngine.native.worldHandle,
diff --git a/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Button.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Button.jvm.kt
new file mode 100644
index 0000000..aa062c5
--- /dev/null
+++ b/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Button.jvm.kt
@@ -0,0 +1,11 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.DropbearEngine
+
+actual fun Button.getClicked(): Boolean {
+    return ButtonNative.getClicked(DropbearEngine.native.uiBufferHandle, this.id.id)
+}
+
+actual fun Button.getHovering(): Boolean {
+    return ButtonNative.getHovering(DropbearEngine.native.uiBufferHandle, this.id.id)
+}
\ No newline at end of file
diff --git a/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Checkbox.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Checkbox.jvm.kt
new file mode 100644
index 0000000..528277d
--- /dev/null
+++ b/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Checkbox.jvm.kt
@@ -0,0 +1,11 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.DropbearEngine
+
+actual fun Checkbox.getChecked(): Boolean {
+    return CheckboxNative.getChecked(DropbearEngine.native.uiBufferHandle, id.id)
+}
+
+actual fun Checkbox.hasCheckedState(): Boolean {
+    return CheckboxNative.hasCheckedState(DropbearEngine.native.uiBufferHandle, id.id)
+}
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/DropbearEngine.native.kt b/scripting/nativeMain/kotlin/com/dropbear/DropbearEngine.native.kt
index c739dbf..6b2a1ca 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/DropbearEngine.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/DropbearEngine.native.kt
@@ -1,6 +1,7 @@
 package com.dropbear
 
 import com.dropbear.components.Camera
+import com.dropbear.ui.UIInstruction
 
 internal actual fun getEntity(label: String): Long? {
     TODO("Not yet implemented")
@@ -11,4 +12,7 @@ internal actual fun getAsset(eucaURI: String): Long? {
 }
 
 internal actual fun quit() {
+}
+
+internal actual fun renderUI(instructions: List<UIInstruction>) {
 }
\ No newline at end of file
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? {
diff --git a/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
index 61d52e1..2663baa 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
@@ -19,6 +19,7 @@ actual class NativeEngine {
     private var assetHandle: COpaquePointer? = null
     private var sceneLoaderHandle: COpaquePointer? = null
     private var physicsEngineHandle: COpaquePointer? = null
+    private var uiBufferHandle: COpaquePointer? = null
 
     @Suppress("unused")
     fun init(
@@ -30,6 +31,9 @@ actual class NativeEngine {
         this.assetHandle = ctx?.assets?.rawValue?.let { interpretCPointer(it) }
         this.sceneLoaderHandle = ctx?.scene_loader?.rawValue?.let { interpretCPointer(it) }
         this.physicsEngineHandle = ctx?.physics_engine?.rawValue?.let { interpretCPointer(it) }
+        this.uiBufferHandle = ctx?.graphics?.rawValue?.let { interpretCPointer(it) }
+
+        Logger.init(com.dropbear.logging.SocketWriter())
 
         // if release, always enable exceptionOnError
         if (!Platform.isDebugBinary) {
@@ -66,5 +70,11 @@ actual class NativeEngine {
                 throw DropbearNativeException("init failed - Invalid physics engine handle received!")
             }
         }
+        if (this.uiBufferHandle == null) {
+            Logger.error("NativeEngine: Error - Invalid ui command buffer engine handle received!")
+            if (exceptionOnError) {
+                throw DropbearNativeException("init failed - Invalid ui command buffer engine handle received!")
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/logging/SocketWriter.kt b/scripting/nativeMain/kotlin/com/dropbear/logging/SocketWriter.kt
new file mode 100644
index 0000000..62ce3de
--- /dev/null
+++ b/scripting/nativeMain/kotlin/com/dropbear/logging/SocketWriter.kt
@@ -0,0 +1,14 @@
+package com.dropbear.logging
+
+actual class SocketWriter actual constructor() : LogWriter {
+    actual override fun log(
+        level: LogLevel,
+        target: String,
+        message: String,
+        file: String?,
+        line: Int?
+    ) {
+        TODO("Not yet implemented")
+    }
+
+}
diff --git a/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt b/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt
index eec669a..ed0e696 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt
@@ -1,11 +1,16 @@
 package com.dropbear.physics
 
 import com.dropbear.EntityId
+import com.dropbear.math.Quaterniond
 import com.dropbear.math.Vector3d
 
 internal actual fun KinematicCharacterController.moveCharacter(dt: Double, translation: Vector3d) {
 }
 
+internal actual fun KinematicCharacterController.setRotationNative(rotation: Quaterniond) {
+    TODO("Not yet implemented")
+}
+
 internal actual fun kccExistsForEntity(entityId: EntityId): Boolean {
     TODO("Not yet implemented")
 }
diff --git a/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Button.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Button.native.kt
new file mode 100644
index 0000000..56cf6af
--- /dev/null
+++ b/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Button.native.kt
@@ -0,0 +1,9 @@
+package com.dropbear.ui.widgets
+
+actual fun Button.getClicked(): Boolean {
+    TODO("Not yet implemented")
+}
+
+actual fun Button.getHovering(): Boolean {
+    TODO("Not yet implemented")
+}
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Checkbox.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Checkbox.native.kt
new file mode 100644
index 0000000..aaaf684
--- /dev/null
+++ b/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Checkbox.native.kt
@@ -0,0 +1,9 @@
+package com.dropbear.ui.widgets
+
+actual fun Checkbox.getChecked(): Boolean {
+    TODO("Not yet implemented")
+}
+
+actual fun Checkbox.hasCheckedState(): Boolean {
+    return false
+}
\ No newline at end of file