kitgit

tirbofish/dropbear · diff

d57ac54 · Thribhu K

a bunch of fixes amirite

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index ca1cbaa..e53fdc7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -88,6 +88,7 @@ features = "0.10"
 puffin_http = "0.16"
 dyn-clone = "1.0"
 downcast-rs = "2.0"
+float-derive = "0.1"
 
 [workspace.dependencies.image]
 version = "0.24"
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index d3b0e2d..19a0a30 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -47,6 +47,7 @@ image.workspace = true
 puffin.workspace = true
 bitflags.workspace = true
 puffin_http.workspace = true
+float-derive.workspace = true
 
 #yakui-wgpu.workspace = true
 #yakui.workspace = true
diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index 2177c1f..05e9851 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -112,9 +112,9 @@ impl AnimationComponent {
                     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)
+                            let start = values[prev_idx];
+                            let end = values[next_idx];
+                            start.lerp(end, factor)
                         },
                         AnimationInterpolation::CubicSpline => {
                             let t = factor;
diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index 69e47f5..de2dddf 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -283,6 +283,26 @@ impl AssetRegistry {
     pub fn get_label_from_model_handle(&self, handle: Handle<Model>) -> Option<String> {
         self.model_labels.iter().find_map(|(label, h)| if *h == handle { Some(label.clone()) } else { None })
     }
+
+    /// Returns a list of all loaded textures with their handles, labels, and references.
+    pub fn list_textures(&self) -> Vec<(Handle<Texture>, Option<String>, Option<ResourceReference>)> {
+        self.textures
+            .values()
+            .map(|texture| (
+                texture.hash.map(Handle::new).unwrap_or(Handle::NULL),
+                texture.label.clone(),
+                texture.reference.clone(),
+            ))
+            .collect()
+    }
+
+    /// Finds a texture handle by its resource reference.
+    pub fn get_texture_handle_by_reference(&self, reference: &ResourceReference) -> Option<Handle<Texture>> {
+        self.textures
+            .values()
+            .find(|texture| texture.reference.as_ref() == Some(reference))
+            .and_then(|texture| texture.hash.map(Handle::new))
+    }
 }
 
 impl Default for AssetRegistry {
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index c3ef642..1fccf7d 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -5,7 +5,6 @@ use crate::{
     utils::{ResourceReference},
     texture::{Texture, TextureWrapMode}
 };
-// use image::GenericImageView;
 use parking_lot::{RwLock};
 use rayon::prelude::*;
 use serde::{Deserialize, Serialize};
@@ -1325,7 +1324,7 @@ pub trait Vertex {
 /// };
 /// ```
 #[repr(C)]
-#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize)]
 pub struct ModelVertex {
     pub position: [f32; 3],
     pub normal: [f32; 3],
@@ -1397,6 +1396,35 @@ impl Vertex for ModelVertex {
     }
 }
 
+impl PartialEq for ModelVertex {
+    fn eq(&self, other: &Self) -> bool {
+        self.position.map(f32::to_bits) == other.position.map(f32::to_bits)
+            && self.normal.map(f32::to_bits) == other.normal.map(f32::to_bits)
+            && self.tangent.map(f32::to_bits) == other.tangent.map(f32::to_bits)
+            && self.tex_coords0.map(f32::to_bits) == other.tex_coords0.map(f32::to_bits)
+            && self.tex_coords1.map(f32::to_bits) == other.tex_coords1.map(f32::to_bits)
+            && self.colour0.map(f32::to_bits) == other.colour0.map(f32::to_bits)
+            && self.joints0 == other.joints0
+            && self.weights0.map(f32::to_bits) == other.weights0.map(f32::to_bits)
+    }
+}
+
+// Eq is just a marker trait — no methods needed
+impl Eq for ModelVertex {}
+
+impl Hash for ModelVertex {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        for v in &self.position   { v.to_bits().hash(state); }
+        for v in &self.normal     { v.to_bits().hash(state); }
+        for v in &self.tangent    { v.to_bits().hash(state); }
+        for v in &self.tex_coords0{ v.to_bits().hash(state); }
+        for v in &self.tex_coords1{ v.to_bits().hash(state); }
+        for v in &self.colour0    { v.to_bits().hash(state); }
+        self.joints0.hash(state);
+        for v in &self.weights0   { v.to_bits().hash(state); }
+    }
+}
+
 #[repr(C)]
 #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct MaterialUniform {
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index f772ebe..578669f 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -2,12 +2,12 @@ use std::sync::Arc;
 use std::mem::size_of;
 use glam::DMat4;
 use slank::include_slang;
-use wgpu::{BufferAddress, CompareFunction, DepthBiasState, StencilState, VertexAttribute, VertexFormat};
+use wgpu::{BufferAddress, CompareFunction, DepthBiasState, StencilState};
 use crate::buffer::{StorageBuffer};
 use crate::entity::{EntityTransform, Transform};
 use crate::graphics::SharedGraphicsContext;
 use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS};
-use crate::model::Vertex;
+use crate::model::{ModelVertex, Vertex};
 use crate::pipelines::DropbearShaderPipeline;
 use crate::shader::Shader;
 use crate::texture::Texture;
@@ -44,7 +44,7 @@ impl DropbearShaderPipeline for LightCubePipeline {
                 compilation_options: Default::default(),
                 buffers: &[
                     // model
-                    VertexInput::desc(),
+                    ModelVertex::desc(),
                     // instance
                     InstanceInput::desc(),
                 ],
@@ -178,29 +178,6 @@ impl LightCubePipeline {
     }
 }
 
-#[repr(C)]
-#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct VertexInput {
-    position: [f32; 3],
-}
-
-impl Vertex for VertexInput {
-    fn desc() -> wgpu::VertexBufferLayout<'static> {
-        wgpu::VertexBufferLayout {
-            array_stride: size_of::<VertexInput>() as BufferAddress,
-            step_mode: wgpu::VertexStepMode::Vertex,
-            attributes: &[
-                // position
-                VertexAttribute {
-                    format: VertexFormat::Float32x3,
-                    offset: 0,
-                    shader_location: 0,
-                }
-            ],
-        }
-    }
-}
-
 /// As mapped in `shaders/light.slang` as
 /// ```wgsl
 /// struct InstanceInput {
diff --git a/crates/dropbear-engine/src/procedural/cube.rs b/crates/dropbear-engine/src/procedural/cube.rs
index 6dc4de9..49270b7 100644
--- a/crates/dropbear-engine/src/procedural/cube.rs
+++ b/crates/dropbear-engine/src/procedural/cube.rs
@@ -72,6 +72,6 @@ impl ProcedurallyGeneratedObject {
             20, 21, 22, 22, 23, 20, // left
         ];
 
-        Self { vertices, indices }
+        Self { vertices, indices, ty: crate::procedural::ProcObjType::Cuboid }
     }
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index 9c6f6f2..66c748a 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -13,57 +13,51 @@ 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] },
+pub enum ProcObjType {
+    Cuboid,
 }
 
 /// An object that comes with a template, and is generated through parameter input. 
+#[derive(
+    Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize
+)]
 pub struct ProcedurallyGeneratedObject {
     pub vertices: Vec<ModelVertex>,
     pub indices: Vec<u32>,
+    pub ty: ProcObjType,
 }
 
 impl ProcedurallyGeneratedObject {
-    /// Builds a GPU-backed model from this procedural mesh.
-    ///
-    /// - `material`: optional material; when `None`, a cached 1x1 grey texture is used.
-    /// - `label`: optional cache label; when `None`, a stable hash-based label is generated.
-    pub fn build_model(
-        self,
+    /// Constructs a [`Model`] and returns the model itself instead of adding to the registry. 
+    pub fn construct(
+        &self,
         graphics: Arc<SharedGraphicsContext>,
         material: Option<Material>,
         label: Option<&str>,
+        hash: Option<u64>,
         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));
-        let hash = hasher.finish();
+    ) -> Model {
+        let mut _rguard = registry.write();
+
+        let hash = if let Some(hash) = hash {
+            hash
+        } else {
+            let mut hasher = DefaultHasher::new();
+            hasher.write(bytemuck::cast_slice(&self.vertices));
+            hasher.write(bytemuck::cast_slice(&self.indices));
+            hasher.finish()
+        };
 
         let label = label
             .map(|s| s.to_string())
             .unwrap_or_else(|| format!("procedural_{hash:016x}"));
 
-        let mut _rguard = registry.write();
-
-        if let Some(handle) = _rguard.model_handle_by_hash(hash) {
-            return handle;
-        }
-
-        let vertices = self.vertices;
-        let indices = self.indices;
+        let vertices = self.vertices.clone();
+        let indices = self.indices.clone();
 
         let vertex_buffer = graphics
             .device
@@ -115,7 +109,7 @@ impl ProcedurallyGeneratedObject {
         let model = Model {
             label: label.clone(),
             hash,
-            path: ResourceReference::from_bytes(hash.to_le_bytes()),
+            path: ResourceReference::from_reference(crate::utils::ResourceReferenceType::ProcObj(self.clone())),
             meshes: vec![mesh],
             materials: vec![material],
             skins: Vec::new(),
@@ -123,6 +117,43 @@ impl ProcedurallyGeneratedObject {
             nodes: Vec::new(),
         };
 
-        _rguard.add_model_with_label(label, model)
+        model
+    }
+
+    /// Builds a GPU-backed model from this procedural mesh.
+    ///
+    /// - `material`: optional material; when `None`, a cached 1x1 grey texture is used.
+    /// - `label`: optional cache label; when `None`, a stable hash-based label is generated.
+    pub fn build_model(
+        &self,
+        graphics: Arc<SharedGraphicsContext>,
+        material: Option<Material>,
+        label: Option<&str>,
+        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));
+        let hash = hasher.finish();
+
+        let label_str = label
+            .map(|s| s.to_string())
+            .unwrap_or_else(|| format!("procedural_{hash:016x}"));
+
+        {
+            let mut _rguard = registry.read();
+
+            if let Some(handle) = _rguard.model_handle_by_hash(hash) {
+                return handle;
+            }
+        }
+
+        let model = Self::construct(&self, graphics, material, label, Some(hash), registry.clone());
+
+        {
+            let mut _rguard = registry.write();
+            _rguard.add_model_with_label(label_str, model)
+        }
     }
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index d5558b1..cae4d24 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -3,7 +3,7 @@
 use serde::{Deserialize, Serialize};
 use std::fmt::{Display, Formatter};
 use std::path::Path;
-use crate::procedural::ProcObj;
+use crate::procedural::{ProcedurallyGeneratedObject};
 
 pub const EUCA_SCHEME: &str = "euca://";
 
@@ -103,7 +103,7 @@ pub enum ResourceReferenceType {
     
     /// An object that can be generated at runtime with the usage of vertices and indices, as well
     /// as a solid grey mesh. 
-    ProcObj(ProcObj),
+    ProcObj(ProcedurallyGeneratedObject),
 }
 
 impl Default for ResourceReferenceType {
diff --git a/crates/eucalyptus-core/src/camera.rs b/crates/eucalyptus-core/src/camera.rs
index 2380214..5d0e2d0 100644
--- a/crates/eucalyptus-core/src/camera.rs
+++ b/crates/eucalyptus-core/src/camera.rs
@@ -58,6 +58,7 @@ impl Component for Camera {
         if let Ok((cam, comp)) = world.query_one::<(&Camera, &CameraComponent)>(entity).get() {
             Box::new(SerializableCamera::from_ecs_camera(cam, comp))
         } else {
+            crate::warn!("Unable to save Camera3D's properties: Not found within world");
             Box::new(SerializableCamera::default())
         }
     }
@@ -69,11 +70,6 @@ impl InspectableComponent for Camera {
             let mut changed = false;
 
             ui.horizontal(|ui| {
-                ui.label("Label");
-                changed |= ui.text_edit_singleline(&mut self.label).changed();
-            });
-
-            ui.horizontal(|ui| {
                 ui.label("Eye");
                 changed |= ui.add(egui::DragValue::new(&mut self.eye.x).speed(0.1)).changed();
                 changed |= ui.add(egui::DragValue::new(&mut self.eye.y).speed(0.1)).changed();
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index ccc2279..32d55b9 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -1,16 +1,19 @@
 use std::any::TypeId;
 use std::collections::HashMap;
+use std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
+use std::time::{SystemTime, UNIX_EPOCH};
 use std::future::Future;
 use std::pin::Pin;
 use std::sync::Arc;
-use egui::{CollapsingHeader, ComboBox, DragValue, RichText, Ui, UiBuilder};
+use egui::{CollapsingHeader, ComboBox, DragValue, RichText, UiBuilder};
 use hecs::{Entity, World};
-use serde::{Deserialize, Serialize};
+pub use serde::{Deserialize, Serialize};
 use dropbear_engine::asset::{Handle, ASSET_REGISTRY};
 use dropbear_engine::entity::{EntityTransform, MeshRenderer};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::model::{Model};
-use dropbear_engine::procedural::{ProcObj, ProcedurallyGeneratedObject};
+use dropbear_engine::procedural::{ProcObjType, ProcedurallyGeneratedObject};
 use dropbear_engine::texture::Texture;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
 use crate::hierarchy::EntityTransformExt;
@@ -502,27 +505,8 @@ impl Component for MeshRenderer {
                     )
                     .await?
                 }
-                ResourceReferenceType::ProcObj(obj) => match obj {
-                    dropbear_engine::procedural::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::debug!("Loading model from cuboid: {:?}", size);
-
-                        let size_vec = glam::DVec3::new(
-                            size[0] as f64,
-                            size[1] as f64,
-                            size[2] as f64,
-                        );
-                        ProcedurallyGeneratedObject::cuboid(size_vec).build_model(
-                            graphics.clone(),
-                            None,
-                            Some(ser.label.as_str()),
-                            ASSET_REGISTRY.clone(),
-                        )
-                    }
+                ResourceReferenceType::ProcObj(obj) => {
+                    obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone())
                 },
             };
 
@@ -596,76 +580,30 @@ impl Component for MeshRenderer {
 
     fn update_component(&mut self, world: &World, _physics: &mut PhysicsState, entity: Entity, _dt: f32, _graphics: Arc<SharedGraphicsContext>) {
         if let Ok(transform) = world.query_one::<&EntityTransform>(entity).get() {
-            self.update(&transform.propagate(&world, entity))
+            self.update(&transform.propagate(&world, entity));
         }
     }
 
     fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> {
         let asset = ASSET_REGISTRY.read();
         let model = asset.get_model(self.model());
+        let (label, handle) = if let Some(model) = model {
+            (model.label.clone(), model.path.clone())
+        } else {
+            if !self.model().is_null() {
+                log::warn!("MeshRenderer save: missing model handle {} in registry", self.model().id);
+            }
+            ("None".to_string(), ResourceReference::default())
+        };
 
         let mut texture_override: HashMap<String, SerializedMaterialCustomisation> = HashMap::new();
         for (label, mat) in &self.material_snapshot {
-            let diffuse_texture = model
-                .and_then(|m| m.materials.iter()
-                    .find(|material|
-                        material.diffuse_texture.label.as_ref() == Some(&label) &&
-                            material.diffuse_texture.reference == mat.diffuse_texture.reference
-                    )
-                    .and_then(|material| material.diffuse_texture.reference.clone())
-                );
-
-            let normal_texture = model
-                .and_then(|m| m.materials.iter()
-                    .find(|material|
-                        material.normal_texture.label.as_ref() == Some(&label) &&
-                            material.normal_texture.reference == mat.normal_texture.reference
-                    )
-                    .and_then(|material| material.normal_texture.reference.clone())
-                );
-
-            let emissive_texture = model
-                .and_then(|m| {
-                    let mat_ref = mat.emissive_texture.as_ref()?.reference.as_ref()?;
-                    m.materials.iter()
-                        .find_map(|material| {
-                            let nt = material.emissive_texture.as_ref()?;
-                            if nt.label.as_ref() == Some(&label) && nt.reference.as_ref() == Some(mat_ref) {
-                                nt.reference.clone()
-                            } else {
-                                None
-                            }
-                        })
-                });
-
-            let occlusion_texture = model
-                .and_then(|m| {
-                    let mat_ref = mat.occlusion_texture.as_ref()?.reference.as_ref()?;
-                    m.materials.iter()
-                        .find_map(|material| {
-                            let nt = material.occlusion_texture.as_ref()?;
-                            if nt.label.as_ref() == Some(&label) && nt.reference.as_ref() == Some(mat_ref) {
-                                nt.reference.clone()
-                            } else {
-                                None
-                            }
-                        })
-                });
-
-            let metallic_roughness_texture = model
-                .and_then(|m| {
-                    let mat_ref = mat.metallic_roughness_texture.as_ref()?.reference.as_ref()?;
-                    m.materials.iter()
-                        .find_map(|material| {
-                            let nt = material.metallic_roughness_texture.as_ref()?;
-                            if nt.label.as_ref() == Some(&label) && nt.reference.as_ref() == Some(mat_ref) {
-                                nt.reference.clone()
-                            } else {
-                                None
-                            }
-                        })
-                });
-
+            // Save texture references directly from the material snapshot
+            let diffuse_texture = mat.diffuse_texture.reference.clone();
+            let normal_texture = mat.normal_texture.reference.clone();
+            let emissive_texture = mat.emissive_texture.as_ref().and_then(|t| t.reference.clone());
+            let occlusion_texture = mat.occlusion_texture.as_ref().and_then(|t| t.reference.clone());
+            let metallic_roughness_texture = mat.metallic_roughness_texture.as_ref().and_then(|t| t.reference.clone());
 
             texture_override.insert(label.to_string(), SerializedMaterialCustomisation {
                 label: label.clone(),
@@ -690,9 +628,9 @@ impl Component for MeshRenderer {
         }
 
         Box::new(SerializedMeshRenderer {
-            label: model.unwrap().label.clone(),
-            handle: Default::default(),
-            import_scale: None,
+            label,
+            handle,
+            import_scale: Some(self.import_scale()),
             texture_override,
         })
     }
@@ -708,28 +646,103 @@ impl InspectableComponent for MeshRenderer {
                 || uri.ends_with(".fbx")
         }
 
-        let apply_cuboid = |renderer: &mut MeshRenderer, size: [f32; 3]| {
-            let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
-            let handle = ProcedurallyGeneratedObject::cuboid(size_vec).build_model(
-                graphics.clone(),
-                None,
-                Some("Cuboid"),
-                ASSET_REGISTRY.clone(),
-            );
-
-            let size_bits = [size[0].to_bits(), size[1].to_bits(), size[2].to_bits()];
-            {
-                let mut registry = ASSET_REGISTRY.write();
-                if let Some(model) = registry.get_model(handle).cloned() {
-                    let mut model = model;
-                    model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(
-                        ProcObj::Cuboid { size_bits },
-                    ));
-                    registry.update_model(handle, model);
+        fn is_probably_texture_uri(uri: &str) -> bool {
+            let uri = uri.to_ascii_lowercase();
+            uri.ends_with(".png")
+                || uri.ends_with(".jpg")
+                || uri.ends_with(".jpeg")
+                || uri.ends_with(".tga")
+                || uri.ends_with(".bmp")
+                || uri.ends_with(".webp")
+        }
+
+        fn proc_obj_size(obj: &ProcedurallyGeneratedObject) -> Option<[f32; 3]> {
+            if obj.ty != ProcObjType::Cuboid {
+                return None;
+            }
+
+            let mut min = [f32::INFINITY; 3];
+            let mut max = [f32::NEG_INFINITY; 3];
+            for v in &obj.vertices {
+                let pos = v.position;
+                for i in 0..3 {
+                    min[i] = min[i].min(pos[i]);
+                    max[i] = max[i].max(pos[i]);
                 }
             }
 
-            renderer.set_model(handle);
+            if min.iter().any(|v| !v.is_finite()) || max.iter().any(|v| !v.is_finite()) {
+                return None;
+            }
+
+            Some([max[0] - min[0], max[1] - min[1], max[2] - min[2]])
+        }
+
+        let apply_cuboid = |renderer: &mut MeshRenderer, size: [f32; 3], force_new: bool| {
+            let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
+            let current_model = renderer.model();
+
+            let proc_obj = ProcedurallyGeneratedObject::cuboid(size_vec);
+            if force_new {
+                let mut model = proc_obj.construct(
+                    graphics.clone(),
+                    None,
+                    None,
+                    None,
+                    ASSET_REGISTRY.clone(),
+                );
+                let mut hasher = DefaultHasher::new();
+                model.hash.hash(&mut hasher);
+                if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
+                    duration.as_nanos().hash(&mut hasher);
+                }
+                let new_hash = hasher.finish();
+                model.hash = new_hash;
+                model.label = format!("Cuboid {:016x}", new_hash);
+
+                let handle = {
+                    let mut asset = ASSET_REGISTRY.write();
+                    asset.add_model(model)
+                };
+                renderer.set_model(handle);
+            } else if current_model.is_null() {
+                let handle = proc_obj.build_model(
+                    graphics.clone(),
+                    None,
+                    None,
+                    ASSET_REGISTRY.clone(),
+                );
+                renderer.set_model(handle);
+            } else {
+                let existing_label = {
+                    let asset = ASSET_REGISTRY.read();
+                    asset.get_model(current_model).map(|model| model.label.clone())
+                };
+
+                if let Some(existing_label) = existing_label {
+                    let mut model = proc_obj.construct(
+                        graphics.clone(),
+                        None,
+                        None,
+                        None,
+                        ASSET_REGISTRY.clone(),
+                    );
+                    model.hash = current_model.id;
+                    model.label = existing_label;
+
+                    let mut asset = ASSET_REGISTRY.write();
+                    asset.update_model(current_model, model);
+                } else {
+                    let handle = proc_obj.build_model(
+                        graphics.clone(),
+                        None,
+                        None,
+                        ASSET_REGISTRY.clone(),
+                    );
+                    renderer.set_model(handle);
+                }
+            }
+            
             renderer.reset_texture_override();
         };
 
@@ -750,12 +763,14 @@ impl InspectableComponent for MeshRenderer {
 
             let model_title = match &model_reference.ref_type {
                 ResourceReferenceType::None => "None".to_string(),
-                ResourceReferenceType::ProcObj(ProcObj::Cuboid { .. }) => "Cuboid".to_string(),
+                ResourceReferenceType::ProcObj(obj) => match obj.ty {
+                    ProcObjType::Cuboid => "Cuboid".to_string(),
+                },
                 _ => model_label,
             };
 
             ui.vertical(|ui| {
-                let expand_id = ui.make_persistent_id(format!("mesh_renderer_expand_{}", self.model().id));
+                let expand_id = ui.make_persistent_id("mesh_renderer_expand");
                 let mut expanded = ui
                     .ctx()
                     .data_mut(|d| d.get_temp::<bool>(expand_id).unwrap_or(false));
@@ -764,13 +779,25 @@ impl InspectableComponent for MeshRenderer {
                 let mut choose_proc_cuboid = false;
                 let mut choose_none = false;
 
+                let drag_id = egui::Id::new(DRAGGED_ASSET_ID);
+                let dragging_valid_model = ui.ctx().data_mut(|d| {
+                    d.get_temp::<Option<ResourceReference>>(drag_id)
+                        .unwrap_or(None)
+                        .and_then(|r| r.as_uri().map(|u| is_probably_model_uri(u)))
+                        .unwrap_or(false)
+                });
+
                 let (rect, response) = ui.allocate_exact_size(
                     egui::vec2(ui.available_width(), 72.0),
                     egui::Sense::click(),
                 );
 
-                let fill = if response.hovered() {
+                let fill = if dragging_valid_model && response.hovered() {
+                    ui.visuals().selection.bg_fill
+                } else if response.hovered() {
                     ui.visuals().widgets.hovered.bg_fill
+                } else if dragging_valid_model {
+                    ui.visuals().widgets.active.bg_fill
                 } else {
                     ui.visuals().widgets.inactive.bg_fill
                 };
@@ -824,7 +851,7 @@ impl InspectableComponent for MeshRenderer {
                                     .selectable_label(
                                         matches!(
                                             model_reference.ref_type,
-                                            ResourceReferenceType::ProcObj(ProcObj::Cuboid { .. })
+                                            ResourceReferenceType::ProcObj(_)
                                         ),
                                         "Cuboid",
                                     )
@@ -875,14 +902,12 @@ impl InspectableComponent for MeshRenderer {
 
                 if choose_proc_cuboid {
                     let default_size = match &model_reference.ref_type {
-                        ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => [
-                            f32::from_bits(size_bits[0]),
-                            f32::from_bits(size_bits[1]),
-                            f32::from_bits(size_bits[2]),
-                        ],
+                        ResourceReferenceType::ProcObj(obj) => {
+                            proc_obj_size(obj).unwrap_or([1.0, 1.0, 1.0])
+                        }
                         _ => [1.0, 1.0, 1.0],
                     };
-                    apply_cuboid(self, default_size);
+                    apply_cuboid(self, default_size, true);
                 } else if choose_none {
                     self.set_model(Handle::NULL);
                     self.reset_texture_override();
@@ -917,15 +942,10 @@ impl InspectableComponent for MeshRenderer {
                         }
                     }
 
-                    if let ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) = &model_reference.ref_type {
-                        let mut size = [
-                            f32::from_bits(size_bits[0]),
-                            f32::from_bits(size_bits[1]),
-                            f32::from_bits(size_bits[2]),
-                        ];
-
-                        ui.label(RichText::new("Cuboid").strong());
-                        ui.horizontal(|ui| {
+                    if let ResourceReferenceType::ProcObj(obj) = &model_reference.ref_type {
+                        if let Some(mut size) = proc_obj_size(obj) {
+                            ui.label(RichText::new("Cuboid").strong());
+                            ui.horizontal(|ui| {
                             ui.label("Extents:");
                             let mut changed = false;
                             ui.label("X");
@@ -942,11 +962,31 @@ impl InspectableComponent for MeshRenderer {
                                 .changed();
 
                             if changed {
-                                apply_cuboid(self, size);
+                                // Preserve material customizations across cuboid size change
+                                let saved_materials = self.material_snapshot.clone();
+                                apply_cuboid(self, size, false);
+                                // Re-apply material customizations to the new model
+                                for (name, saved_mat) in saved_materials {
+                                    if let Some(mat) = self.material_snapshot.get_mut(&name) {
+                                        mat.tint = saved_mat.tint;
+                                        mat.emissive_factor = saved_mat.emissive_factor;
+                                        mat.metallic_factor = saved_mat.metallic_factor;
+                                        mat.roughness_factor = saved_mat.roughness_factor;
+                                        mat.alpha_mode = saved_mat.alpha_mode;
+                                        mat.alpha_cutoff = saved_mat.alpha_cutoff;
+                                        mat.double_sided = saved_mat.double_sided;
+                                        mat.occlusion_strength = saved_mat.occlusion_strength;
+                                        mat.normal_scale = saved_mat.normal_scale;
+                                        mat.uv_tiling = saved_mat.uv_tiling;
+                                        mat.wrap_mode = saved_mat.wrap_mode;
+                                        mat.texture_tag = saved_mat.texture_tag.clone();
+                                    }
+                                }
                             }
                         });
 
                         ui.separator();
+                        }
                     }
 
                     ui.label("Materials");
@@ -1089,6 +1129,339 @@ impl InspectableComponent for MeshRenderer {
                                 Some(texture_tag)
                             };
                         }
+
+                        // Texture customization slots
+                        ui.add_space(8.0);
+                        ui.label(RichText::new("Textures").strong());
+
+                        // Check if a valid texture is being dragged
+                        let drag_id = egui::Id::new(DRAGGED_ASSET_ID);
+                        let dragging_valid_texture = ui.ctx().data_mut(|d| {
+                            d.get_temp::<Option<ResourceReference>>(drag_id)
+                                .unwrap_or(None)
+                                .and_then(|r| r.as_uri().map(|u| is_probably_texture_uri(u)))
+                                .unwrap_or(false)
+                        });
+
+                        // Helper to create texture slot UI
+                        let texture_slot = |ui: &mut egui::Ui,
+                                           label: &str,
+                                           _id_salt: &str,
+                                           current_texture: &Texture,
+                                           _graphics: Arc<SharedGraphicsContext>,
+                                           dragging_valid: bool| -> Option<Texture> {
+                            let mut result = None;
+                            ui.horizontal(|ui| {
+                                ui.label(label);
+                                
+                                let texture_label = current_texture.label.clone()
+                                    .unwrap_or_else(|| "Default".to_string());
+                                
+                                let (rect, response) = ui.allocate_exact_size(
+                                    egui::vec2(ui.available_width().min(150.0), 24.0),
+                                    egui::Sense::click(),
+                                );
+
+                                let fill = if dragging_valid && response.hovered() {
+                                    ui.visuals().selection.bg_fill
+                                } else if response.hovered() {
+                                    ui.visuals().widgets.hovered.bg_fill
+                                } else if dragging_valid {
+                                    ui.visuals().widgets.active.bg_fill
+                                } else {
+                                    ui.visuals().widgets.inactive.bg_fill
+                                };
+                                ui.painter().rect_filled(rect, 2.0, fill);
+                                ui.painter().rect_stroke(
+                                    rect, 2.0,
+                                    ui.visuals().widgets.inactive.bg_stroke,
+                                    egui::StrokeKind::Inside,
+                                );
+                                ui.painter().text(
+                                    rect.center(),
+                                    egui::Align2::CENTER_CENTER,
+                                    &texture_label,
+                                    egui::FontId::default(),
+                                    ui.visuals().text_color(),
+                                );
+
+                                // Handle texture drag-drop
+                                let pointer_released = ui.input(|i| i.pointer.any_released());
+                                if pointer_released && response.hovered() {
+                                    let drag_id = egui::Id::new(DRAGGED_ASSET_ID);
+                                    let dragged_reference = ui
+                                        .ctx()
+                                        .data_mut(|d| d.get_temp::<Option<ResourceReference>>(drag_id).unwrap_or(None));
+                                    if let Some(reference) = dragged_reference {
+                                        if let Some(uri) = reference.as_uri() {
+                                            if is_probably_texture_uri(uri) {
+                                                // Try to find the texture in the registry
+                                                if let Some(handle) = ASSET_REGISTRY.read()
+                                                    .get_texture_handle_by_reference(&reference)
+                                                {
+                                                    if let Some(tex) = ASSET_REGISTRY.read()
+                                                        .get_texture(handle)
+                                                        .cloned()
+                                                    {
+                                                        result = Some(tex);
+                                                    }
+                                                }
+                                                ui.ctx().data_mut(|d| {
+                                                    d.insert_temp(drag_id, None::<ResourceReference>)
+                                                });
+                                            }
+                                        }
+                                    }
+                                }
+
+                                if response.hovered() {
+                                    response.on_hover_text("Drop a texture here");
+                                }
+                            });
+                            result
+                        };
+
+                        // Diffuse texture slot
+                        if let Some(new_tex) = texture_slot(
+                            ui, "Diffuse", 
+                            &format!("diffuse_tex_{}", material_name),
+                            &material.diffuse_texture,
+                            graphics.clone(),
+                            dragging_valid_texture,
+                        ) {
+                            material.diffuse_texture = new_tex;
+                        }
+
+                        // Normal texture slot
+                        if let Some(new_tex) = texture_slot(
+                            ui, "Normal",
+                            &format!("normal_tex_{}", material_name),
+                            &material.normal_texture,
+                            graphics.clone(),
+                            dragging_valid_texture,
+                        ) {
+                            material.normal_texture = new_tex;
+                        }
+
+                        // Emissive texture slot (optional)
+                        ui.horizontal(|ui| {
+                            ui.label("Emissive");
+                            
+                            let texture_label = material.emissive_texture
+                                .as_ref()
+                                .and_then(|t| t.label.clone())
+                                .unwrap_or_else(|| "None".to_string());
+                            
+                            let (rect, response) = ui.allocate_exact_size(
+                                egui::vec2(ui.available_width().min(150.0), 24.0),
+                                egui::Sense::click(),
+                            );
+
+                            let fill = if dragging_valid_texture && response.hovered() {
+                                ui.visuals().selection.bg_fill
+                            } else if response.hovered() {
+                                ui.visuals().widgets.hovered.bg_fill
+                            } else if dragging_valid_texture {
+                                ui.visuals().widgets.active.bg_fill
+                            } else {
+                                ui.visuals().widgets.inactive.bg_fill
+                            };
+                            ui.painter().rect_filled(rect, 2.0, fill);
+                            ui.painter().rect_stroke(
+                                rect, 2.0,
+                                ui.visuals().widgets.inactive.bg_stroke,
+                                egui::StrokeKind::Inside,
+                            );
+                            ui.painter().text(
+                                rect.center(),
+                                egui::Align2::CENTER_CENTER,
+                                &texture_label,
+                                egui::FontId::default(),
+                                ui.visuals().text_color(),
+                            );
+
+                            let pointer_released = ui.input(|i| i.pointer.any_released());
+                            if pointer_released && response.hovered() {
+                                let drag_id = egui::Id::new(DRAGGED_ASSET_ID);
+                                let dragged_reference = ui
+                                    .ctx()
+                                    .data_mut(|d| d.get_temp::<Option<ResourceReference>>(drag_id).unwrap_or(None));
+                                if let Some(reference) = dragged_reference {
+                                    if let Some(uri) = reference.as_uri() {
+                                        if is_probably_texture_uri(uri) {
+                                            if let Some(handle) = ASSET_REGISTRY.read()
+                                                .get_texture_handle_by_reference(&reference)
+                                            {
+                                                if let Some(tex) = ASSET_REGISTRY.read()
+                                                    .get_texture(handle)
+                                                    .cloned()
+                                                {
+                                                    material.emissive_texture = Some(tex);
+                                                }
+                                            }
+                                            ui.ctx().data_mut(|d| {
+                                                d.insert_temp(drag_id, None::<ResourceReference>)
+                                            });
+                                        }
+                                    }
+                                }
+                            }
+
+                            if response.hovered() {
+                                response.on_hover_text("Drop a texture here");
+                            }
+
+                            // Clear button for optional texture
+                            if material.emissive_texture.is_some() && ui.small_button("X").clicked() {
+                                material.emissive_texture = None;
+                            }
+                        });
+
+                        // Metallic/Roughness texture slot (optional)
+                        ui.horizontal(|ui| {
+                            ui.label("Metal/Rough");
+                            
+                            let texture_label = material.metallic_roughness_texture
+                                .as_ref()
+                                .and_then(|t| t.label.clone())
+                                .unwrap_or_else(|| "None".to_string());
+                            
+                            let (rect, response) = ui.allocate_exact_size(
+                                egui::vec2(ui.available_width().min(150.0), 24.0),
+                                egui::Sense::click(),
+                            );
+
+                            let fill = if dragging_valid_texture && response.hovered() {
+                                ui.visuals().selection.bg_fill
+                            } else if response.hovered() {
+                                ui.visuals().widgets.hovered.bg_fill
+                            } else if dragging_valid_texture {
+                                ui.visuals().widgets.active.bg_fill
+                            } else {
+                                ui.visuals().widgets.inactive.bg_fill
+                            };
+                            ui.painter().rect_filled(rect, 2.0, fill);
+                            ui.painter().rect_stroke(
+                                rect, 2.0,
+                                ui.visuals().widgets.inactive.bg_stroke,
+                                egui::StrokeKind::Inside,
+                            );
+                            ui.painter().text(
+                                rect.center(),
+                                egui::Align2::CENTER_CENTER,
+                                &texture_label,
+                                egui::FontId::default(),
+                                ui.visuals().text_color(),
+                            );
+
+                            let pointer_released = ui.input(|i| i.pointer.any_released());
+                            if pointer_released && response.hovered() {
+                                let drag_id = egui::Id::new(DRAGGED_ASSET_ID);
+                                let dragged_reference = ui
+                                    .ctx()
+                                    .data_mut(|d| d.get_temp::<Option<ResourceReference>>(drag_id).unwrap_or(None));
+                                if let Some(reference) = dragged_reference {
+                                    if let Some(uri) = reference.as_uri() {
+                                        if is_probably_texture_uri(uri) {
+                                            if let Some(handle) = ASSET_REGISTRY.read()
+                                                .get_texture_handle_by_reference(&reference)
+                                            {
+                                                if let Some(tex) = ASSET_REGISTRY.read()
+                                                    .get_texture(handle)
+                                                    .cloned()
+                                                {
+                                                    material.metallic_roughness_texture = Some(tex);
+                                                }
+                                            }
+                                            ui.ctx().data_mut(|d| {
+                                                d.insert_temp(drag_id, None::<ResourceReference>)
+                                            });
+                                        }
+                                    }
+                                }
+                            }
+
+                            if response.hovered() {
+                                response.on_hover_text("Drop a texture here");
+                            }
+
+                            if material.metallic_roughness_texture.is_some() && ui.small_button("X").clicked() {
+                                material.metallic_roughness_texture = None;
+                            }
+                        });
+
+                        // Occlusion texture slot (optional)
+                        ui.horizontal(|ui| {
+                            ui.label("Occlusion");
+                            
+                            let texture_label = material.occlusion_texture
+                                .as_ref()
+                                .and_then(|t| t.label.clone())
+                                .unwrap_or_else(|| "None".to_string());
+                            
+                            let (rect, response) = ui.allocate_exact_size(
+                                egui::vec2(ui.available_width().min(150.0), 24.0),
+                                egui::Sense::click(),
+                            );
+
+                            let fill = if dragging_valid_texture && response.hovered() {
+                                ui.visuals().selection.bg_fill
+                            } else if response.hovered() {
+                                ui.visuals().widgets.hovered.bg_fill
+                            } else if dragging_valid_texture {
+                                ui.visuals().widgets.active.bg_fill
+                            } else {
+                                ui.visuals().widgets.inactive.bg_fill
+                            };
+                            ui.painter().rect_filled(rect, 2.0, fill);
+                            ui.painter().rect_stroke(
+                                rect, 2.0,
+                                ui.visuals().widgets.inactive.bg_stroke,
+                                egui::StrokeKind::Inside,
+                            );
+                            ui.painter().text(
+                                rect.center(),
+                                egui::Align2::CENTER_CENTER,
+                                &texture_label,
+                                egui::FontId::default(),
+                                ui.visuals().text_color(),
+                            );
+
+                            let pointer_released = ui.input(|i| i.pointer.any_released());
+                            if pointer_released && response.hovered() {
+                                let drag_id = egui::Id::new(DRAGGED_ASSET_ID);
+                                let dragged_reference = ui
+                                    .ctx()
+                                    .data_mut(|d| d.get_temp::<Option<ResourceReference>>(drag_id).unwrap_or(None));
+                                if let Some(reference) = dragged_reference {
+                                    if let Some(uri) = reference.as_uri() {
+                                        if is_probably_texture_uri(uri) {
+                                            if let Some(handle) = ASSET_REGISTRY.read()
+                                                .get_texture_handle_by_reference(&reference)
+                                            {
+                                                if let Some(tex) = ASSET_REGISTRY.read()
+                                                    .get_texture(handle)
+                                                    .cloned()
+                                                {
+                                                    material.occlusion_texture = Some(tex);
+                                                }
+                                            }
+                                            ui.ctx().data_mut(|d| {
+                                                d.insert_temp(drag_id, None::<ResourceReference>)
+                                            });
+                                        }
+                                    }
+                                }
+                            }
+
+                            if response.hovered() {
+                                response.on_hover_text("Drop a texture here");
+                            }
+
+                            if material.occlusion_texture.is_some() && ui.small_button("X").clicked() {
+                                material.occlusion_texture = None;
+                            }
+                        });
                     }
                 }
             });
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 63a365b..1caf018 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -265,12 +265,12 @@ impl SceneConfig {
 
     fn register_physics_for_entity(&mut self, world: &mut hecs::World, entity: hecs::Entity) {
         if let Ok((
-              label,
-              e_trans,
-              rigid,
-              col_group,
-              kcc
-          )) = world.query_one::<(
+            label,
+            e_trans,
+            rigid,
+            col_group,
+            kcc
+        )) = world.query_one::<(
             &Label,
             &EntityTransform,
             Option<&mut RigidBody>,
@@ -392,33 +392,6 @@ impl SceneConfig {
         if !has_light {
             log::info!("No lights in scene, spawning default light");
 
-            let legacy_lights: Vec<hecs::Entity> = world
-                .query::<(Entity, &Label)>()
-                .iter()
-                .filter_map(|(entity, label)| {
-                    if label.as_str() == "Default Light" {
-                        Some(entity)
-                    } else {
-                        None
-                    }
-                })
-                .collect();
-
-            for entity in legacy_lights {
-                if let Err(err) = world.despawn(entity) {
-                    log::warn!(
-                        "Failed to remove legacy 'Default Light' entity {:?}: {}",
-                        entity,
-                        err
-                    );
-                } else {
-                    log::debug!(
-                        "Removed legacy 'Default Light' placeholder entity {:?}",
-                        entity
-                    );
-                }
-            }
-
             if let Some(s) = progress_sender {
                 let _ = s.send(WorldLoadingStatus::LoadingEntity {
                     index: 0,
@@ -503,34 +476,6 @@ impl SceneConfig {
             } else {
                 log::info!("No debug or starting camera found, creating viewport camera for editor");
 
-                let legacy_cameras: Vec<hecs::Entity> = world
-                    .query::<(Entity, &Label)>()
-                    .iter()
-                    .filter_map(|(entity, label)| {
-                        if label.as_str() == "Viewport Camera" {
-                            log::debug!("Found 'Viewport Camera' entity: {:?}", entity);
-                            Some(entity)
-                        } else {
-                            None
-                        }
-                    })
-                    .collect();
-
-                for entity in legacy_cameras {
-                    if let Err(err) = world.despawn(entity) {
-                        log::warn!(
-                            "Failed to remove legacy 'Viewport Camera' entity {:?}: {}",
-                            entity,
-                            err
-                        );
-                    } else {
-                        log::debug!(
-                            "Removed legacy 'Viewport Camera' placeholder entity {:?}",
-                            entity
-                        );
-                    }
-                }
-
                 if let Some(s) = progress_sender {
                     let _ = s.send(WorldLoadingStatus::LoadingEntity {
                         index: 0,
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 265bee0..7f120b3 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -279,8 +279,8 @@ impl SerializableCamera {
             fov: camera.settings.fov_y as f32,
             near: camera.znear as f32,
             far: camera.zfar as f32,
-            speed: component.settings.speed as f32,
-            sensitivity: component.settings.sensitivity as f32,
+            speed: camera.settings.speed as f32,
+            sensitivity: camera.settings.sensitivity as f32,
             starting_camera: component.starting_camera,
         }
     }
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index adb9553..aece78d 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -1,13 +1,28 @@
 use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::Path};
 use std::hash::{Hash, Hasher};
 use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference};
+use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::entity::MeshRenderer;
+use dropbear_engine::model::Model;
+use dropbear_engine::texture::Texture;
+use eucalyptus_core::utils::ResolveReference;
 use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder};
 use eucalyptus_core::states::PROJECT;
 use hecs::Entity;
+use log::{info, warn};
 
 use crate::editor::{ComponentNodeSelection, DraggedAsset, EditorTabViewer, FsEntry, StaticallyKept, TABS_GLOBAL};
 use eucalyptus_core::component::DRAGGED_ASSET_ID;
 
+#[derive(Clone, Copy, Debug)]
+enum TextureSlot {
+    Diffuse,
+    Normal,
+    Emissive,
+    MetallicRoughness,
+    Occlusion,
+}
+
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) {
         let mut cfg = TABS_GLOBAL.lock();
@@ -23,7 +38,7 @@ impl<'a> EditorTabViewer<'a> {
 
         let (_resp, action) = egui_ltreeview::TreeView::new(egui::Id::new("asset_viewer")).show(ui, |builder| {
             builder.node(Self::dir_node("euca://"));
-            Self::build_resource_branch(&mut cfg, builder, &project_root);
+            self.build_resource_branch(&mut cfg, builder, &project_root);
             Self::build_scripts_branch(&mut cfg, builder, &project_root);
             Self::build_scene_branch(&mut cfg, builder, &project_root);
             builder.close_dir();
@@ -59,6 +74,7 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn build_resource_branch(
+        &mut self,
         cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         project_root: &Path,
@@ -67,7 +83,7 @@ impl<'a> EditorTabViewer<'a> {
         builder.node(Self::dir_node_labeled(label, "resources"));
         let resources_root = project_root.join("resources");
         if resources_root.exists() {
-            Self::walk_resource_directory(cfg, builder, &resources_root, &resources_root);
+            self.walk_resource_directory(cfg, builder, &resources_root, &resources_root);
         } else {
             Self::add_placeholder_leaf(builder, "euca://resources/missing", "missing");
         }
@@ -75,6 +91,7 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn walk_resource_directory(
+        &mut self,
         cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         base_path: &Path,
@@ -96,22 +113,81 @@ impl<'a> EditorTabViewer<'a> {
             let full_label = Self::resource_label(base_path, &entry.path);
             if entry.is_dir {
                 builder.node(Self::dir_node_labeled(&full_label, &entry.name));
-                Self::walk_resource_directory(cfg, builder, base_path, &entry.path);
+                self.walk_resource_directory(cfg, builder, base_path, &entry.path);
                 builder.close_dir();
             } else {
                 if entry.name.eq_ignore_ascii_case("resources.eucc") {
                     continue;
                 }
 
+                let reference = ResourceReference::from_euca_uri(&full_label)
+                    .unwrap_or_else(|_| ResourceReference::default());
                 cfg.asset_node_assets.insert(
                     Self::asset_node_id(&full_label),
                     DraggedAsset {
                         name: entry.name.clone(),
-                        path: ResourceReference::from_euca_uri(&full_label)
-                            .unwrap_or_else(|_| ResourceReference::default()),
+                        path: reference.clone(),
                     },
                 );
-                builder.node(Self::leaf_node_labeled(&full_label, &entry.name));
+                let is_model = Self::is_model_file(&entry.name);
+                let is_texture = Self::is_texture_file(&entry.name);
+                let entry_name = entry.name.clone();
+                let reference_for_menu = reference.clone();
+                let menu = Self::leaf_node_labeled(&full_label, &entry.name).context_menu(|ui| {
+                        if is_model {
+                            if ui.button("Load to memory").clicked() {
+                                ui.close();
+                                self.queue_model_load(reference_for_menu.clone(), entry_name.clone());
+                            }
+                        }
+
+                        if is_texture {
+                            if ui.button("Load to memory").clicked() {
+                                ui.close();
+                                self.queue_texture_load(reference_for_menu.clone(), entry_name.clone());
+                            }
+
+                            ui.separator();
+                            ui.menu_button("Choose", |ui| {
+                                if ui.button("Diffuse").clicked() {
+                                    ui.close();
+                                    self.apply_texture_slot(
+                                        reference_for_menu.clone(),
+                                        TextureSlot::Diffuse,
+                                    );
+                                }
+                                if ui.button("Normal").clicked() {
+                                    ui.close();
+                                    self.apply_texture_slot(
+                                        reference_for_menu.clone(),
+                                        TextureSlot::Normal,
+                                    );
+                                }
+                                if ui.button("Emissive").clicked() {
+                                    ui.close();
+                                    self.apply_texture_slot(
+                                        reference_for_menu.clone(),
+                                        TextureSlot::Emissive,
+                                    );
+                                }
+                                if ui.button("Metal/Rough").clicked() {
+                                    ui.close();
+                                    self.apply_texture_slot(
+                                        reference_for_menu.clone(),
+                                        TextureSlot::MetallicRoughness,
+                                    );
+                                }
+                                if ui.button("Occlusion").clicked() {
+                                    ui.close();
+                                    self.apply_texture_slot(
+                                        reference_for_menu.clone(),
+                                        TextureSlot::Occlusion,
+                                    );
+                                }
+                            });
+                        }
+                    });
+                builder.node(menu);
             }
         }
     }
@@ -129,7 +205,7 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn build_scripts_branch(
-        cfg: &mut StaticallyKept,
+        _cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         project_root: &Path,
     ) {
@@ -137,7 +213,7 @@ impl<'a> EditorTabViewer<'a> {
         builder.node(Self::dir_node_labeled(label, "scripts"));
         let scripts_root = project_root.join("src");
         if !scripts_root.exists() {
-            Self::walk_resource_directory(cfg, builder, &scripts_root, &scripts_root);
+            Self::add_placeholder_leaf(builder, "euca://scripts/missing", "missing");
             builder.close_dir();
             return;
         }
@@ -494,6 +570,120 @@ impl<'a> EditorTabViewer<'a> {
         builder.node(Self::leaf_node_labeled(id_source, label));
     }
 
+
+    fn is_model_file(name: &str) -> bool {
+        let name = name.to_ascii_lowercase();
+        name.ends_with(".glb") || name.ends_with(".gltf")
+    }
+
+    fn is_texture_file(name: &str) -> bool {
+        let name = name.to_ascii_lowercase();
+        name.ends_with(".png")
+            || name.ends_with(".jpg")
+            || name.ends_with(".jpeg")
+            || name.ends_with(".tga")
+            || name.ends_with(".bmp")
+            || name.ends_with(".webp")
+    }
+
+    fn queue_model_load(&self, reference: ResourceReference, label: String) {
+        if ASSET_REGISTRY
+            .read()
+            .get_model_handle_by_reference(&reference)
+            .is_some()
+        {
+            info!("Model already loaded: {}", label);
+            return;
+        }
+
+        let graphics = self.graphics.clone();
+        let queue = graphics.future_queue.clone();
+        queue.push(async move {
+            let path = reference.resolve()?;
+            let buffer = fs::read(&path)?;
+            let handle = Model::load_from_memory_raw(
+                graphics.clone(),
+                buffer,
+                None,
+                ASSET_REGISTRY.clone(),
+            )
+            .await?;
+
+            let mut registry = ASSET_REGISTRY.write();
+            if let Some(model) = registry.get_model(handle).cloned() {
+                let mut model = model;
+                model.path = reference.clone();
+                model.label = label.clone();
+                registry.update_model(handle, model);
+                registry.label_model(label.clone(), handle);
+            }
+
+            Ok::<(), anyhow::Error>(())
+        });
+    }
+
+    fn queue_texture_load(&self, reference: ResourceReference, label: String) {
+        if ASSET_REGISTRY
+            .read()
+            .get_texture_handle_by_reference(&reference)
+            .is_some()
+        {
+            info!("Texture already loaded: {}", label);
+            return;
+        }
+
+        let graphics = self.graphics.clone();
+        let queue = graphics.future_queue.clone();
+        queue.push(async move {
+            let path = reference.resolve()?;
+            let texture = Texture::from_file(graphics.clone(), &path, Some(&label)).await?;
+            let mut registry = ASSET_REGISTRY.write();
+            registry.add_texture_with_label(label.clone(), texture);
+            Ok::<(), anyhow::Error>(())
+        });
+    }
+
+    fn apply_texture_slot(&mut self, reference: ResourceReference, slot: TextureSlot) {
+        let Some(entity) = *self.selected_entity else {
+            warn!("Unable to apply texture: no entity selected");
+            return;
+        };
+
+        let Some(handle) = ASSET_REGISTRY
+            .read()
+            .get_texture_handle_by_reference(&reference)
+        else {
+            warn!("Texture not loaded in memory, load it first");
+            return;
+        };
+
+        let texture = {
+            let registry = ASSET_REGISTRY.read();
+            registry.get_texture(handle).cloned()
+        };
+
+        let Some(texture) = texture else {
+            warn!("Texture handle missing from registry");
+            return;
+        };
+
+        if let Ok(renderer) = self.world.query_one::<&mut MeshRenderer>(entity).get() {
+            for material in renderer.material_snapshot.values_mut() {
+                match slot {
+                    TextureSlot::Diffuse => material.diffuse_texture = texture.clone(),
+                    TextureSlot::Normal => material.normal_texture = texture.clone(),
+                    TextureSlot::Emissive => material.emissive_texture = Some(texture.clone()),
+                    TextureSlot::MetallicRoughness => {
+                        material.metallic_roughness_texture = Some(texture.clone())
+                    }
+                    TextureSlot::Occlusion => material.occlusion_texture = Some(texture.clone()),
+                }
+            }
+        } else {
+            warn!("Selected entity has no MeshRenderer");
+        }
+    }
+
     pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) {
         for node_id in items {
             self.resolve_tree_node(cfg, *node_id);
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index b8a1dac..63b42ba 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -2,7 +2,7 @@ use egui_ltreeview::{NodeBuilder, TreeViewBuilder};
 use eucalyptus_core::{component::ComponentRegistry, hierarchy::{Children, Hierarchy, Parent}, physics::{collider::ColliderGroup, rigidbody::RigidBody}, states::{Label, PROJECT}};
 use hecs::{Entity, World};
 
-use crate::editor::{EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL};
+use crate::editor::{Editor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL};
 
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) {
@@ -24,7 +24,8 @@ impl<'a> EditorTabViewer<'a> {
                         .label(format!("Scene: {}", current_scene_name))
                         .context_menu(|ui| {
                             if ui.button("New Empty Entity").clicked() {
-                                self.world.spawn((Label::new("Blank Entity"),));
+                                let label = Editor::unique_label_for_world(self.world, "Blank Entity");
+                                self.world.spawn((label,));
                                 ui.close();
                             }
                         }),
@@ -56,7 +57,8 @@ impl<'a> EditorTabViewer<'a> {
                             .context_menu(|ui| {
                                 ui.menu_button("New", |ui| {
                                     if ui.button("Child").clicked() {
-                                        let child = world.spawn((Label::new("New Entity"),));
+                                        let label = Editor::unique_label_for_world(world, "New Entity");
+                                        let child = world.spawn((label,));
                                         Hierarchy::set_parent(world, child, entity);
                                         ui.close();
                                     }
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index b0dd5c5..7171f17 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -30,6 +30,7 @@ use eucalyptus_core::{register_components, APP_INFO};
 use eucalyptus_core::hierarchy::{Children, SceneHierarchy};
 use eucalyptus_core::scene::{SceneConfig, SceneEntity};
 use eucalyptus_core::states::Label;
+use std::collections::HashSet;
 use eucalyptus_core::{
     camera::{CameraComponent, CameraType, DebugCamera},
     fatal, info,
@@ -285,6 +286,55 @@ impl Editor {
         })
     }
 
+    pub(crate) fn unique_label_for_world(world: &World, base: &str) -> Label {
+        fn parse_suffix(name: &str) -> Option<(&str, u32)> {
+            let mut parts = name.rsplitn(2, '.');
+            let suffix = parts.next()?;
+            let root = parts.next()?;
+            if suffix.len() == 3 && suffix.chars().all(|c| c.is_ascii_digit()) {
+                if let Ok(value) = suffix.parse::<u32>() {
+                    return Some((root, value));
+                }
+            }
+            None
+        }
+
+        let mut existing = HashSet::new();
+        for (_, label) in world.query::<(Entity, &Label)>().iter() {
+            existing.insert(label.as_str().to_string());
+        }
+
+        if !existing.contains(base) {
+            return Label::new(base);
+        }
+
+        let (root, base_suffix) = match parse_suffix(base) {
+            Some((root, suffix)) => (root.to_string(), suffix),
+            None => (base.to_string(), 0),
+        };
+
+        let mut max_suffix = base_suffix;
+        for name in &existing {
+            if name == &root {
+                continue;
+            }
+            if let Some((candidate_root, suffix)) = parse_suffix(name) {
+                if candidate_root == root {
+                    max_suffix = max_suffix.max(suffix);
+                }
+            }
+        }
+
+        let mut next = max_suffix.max(1);
+        loop {
+            let candidate = format!("{root}.{next:03}");
+            if !existing.contains(&candidate) {
+                return Label::new(candidate.as_str());
+            }
+            next += 1;
+        }
+    }
+
     fn double_key_pressed(&mut self, key: KeyCode) -> bool {
         let now = Instant::now();
 
@@ -1076,6 +1126,64 @@ impl Editor {
             });
         });
 
+        egui::TopBottomPanel::bottom("status bar")
+            .resizable(false)
+            .show(ctx, |ui| {
+                let active_camera = *self.active_camera.lock();
+                let (label, is_debug, active_entity) = if let Some(entity) = active_camera {
+                    let camera_label = self
+                        .world
+                        .get::<&Label>(entity)
+                        .map(|label| label.as_str().to_string())
+                        .ok()
+                        .or_else(|| {
+                            self.world
+                                .get::<&Camera>(entity)
+                                .map(|camera| camera.label.clone())
+                                .ok()
+                        })
+                        .unwrap_or_else(|| "Camera".to_string());
+                    let is_debug = self
+                        .world
+                        .get::<&CameraComponent>(entity)
+                        .map(|comp| matches!(comp.camera_type, CameraType::Debug))
+                        .unwrap_or(false);
+                    (camera_label, is_debug, Some(entity))
+                } else {
+                    ("Viewport Camera".to_string(), true, None)
+                };
+
+                let selected_entity = self.selected_entity;
+                let selected_has_camera = selected_entity
+                    .and_then(|entity| {
+                        self.world
+                            .query_one::<(&Camera, &CameraComponent)>(entity)
+                            .get()
+                            .ok()
+                            .map(|_| entity)
+                    })
+                    .is_some();
+
+                let is_selected_active = selected_entity
+                    .and_then(|entity| active_entity.map(|active| (entity, active)))
+                    .map(|(selected, active)| selected == active)
+                    .unwrap_or(false);
+
+                let text_color = if is_debug {
+                    ui.visuals().weak_text_color()
+                } else if selected_has_camera && is_selected_active {
+                    egui::Color32::from_rgb(80, 200, 120)
+                } else if selected_has_camera {
+                    egui::Color32::from_rgb(245, 230, 160)
+                } else {
+                    ui.visuals().weak_text_color()
+                };
+
+                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                    ui.colored_label(text_color, format!("Viewing through {label}"));
+                });
+        });
+
         let editor_ptr = self as *mut Editor;
 
         let Some(view) = self.texture_id else {
diff --git a/crates/eucalyptus-editor/src/editor/resource.rs b/crates/eucalyptus-editor/src/editor/resource.rs
index cdac237..2e3afcf 100644
--- a/crates/eucalyptus-editor/src/editor/resource.rs
+++ b/crates/eucalyptus-editor/src/editor/resource.rs
@@ -1,4 +1,6 @@
 use crate::editor::{EditorTabViewer, TABS_GLOBAL};
+use dropbear_engine::camera::Camera;
+use eucalyptus_core::camera::CameraComponent;
 
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) {
@@ -13,6 +15,30 @@ impl<'a> EditorTabViewer<'a> {
                 ui.label(format!("Entity ID: {}", inspect_entity.id()));
                 ui.separator();
 
+                if self
+                    .world
+                    .query_one::<(&Camera, &CameraComponent)>(inspect_entity)
+                    .get()
+                    .is_ok()
+                {
+                    let is_active = self
+                        .active_camera
+                        .lock()
+                        .map_or(false, |active| active == inspect_entity);
+                    ui.horizontal(|ui| {
+                        let label = if is_active {
+                            "Viewing Through This Camera"
+                        } else {
+                            "View Through This Camera"
+                        };
+                        if ui.add_enabled(!is_active, egui::Button::new(label)).clicked() {
+                            let mut active_camera = self.active_camera.lock();
+                            *active_camera = Some(inspect_entity);
+                        }
+                    });
+                    ui.separator();
+                }
+
                 self.component_registry
                     .inspect_components(self.world, inspect_entity, ui, self.graphics.clone());
             }
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index c8c3c85..09cb0d2 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -610,11 +610,13 @@ impl Scene for Editor {
                 })
                 .unwrap_or(false);
 
+            log_once::debug_once!("show_hitboxes = {}, current_scene_name = {:?}", show_hitboxes, self.current_scene_name);
+
             if show_hitboxes {
                 if let Some(collider_pipeline) = &self.collider_wireframe_pipeline {
                     let mut render_pass = encoder
                         .begin_render_pass(&wgpu::RenderPassDescriptor {
-                            label: Some("model render pass"),
+                            label: Some("collider wireframe render pass"),
                             color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                                 view: hdr.view(),
                                 depth_slice: None,
@@ -643,8 +645,12 @@ impl Scene for Editor {
                         HashMap::new();
 
                     let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>();
+                    let mut entity_count = 0;
+                    let mut collider_count = 0;
                     for (entity_transform, group) in q.iter() {
+                        entity_count += 1;
                         for collider in &group.colliders {
+                            collider_count += 1;
                             let world_tf = entity_transform.sync();
 
                             let entity_matrix = DMat4::from_rotation_translation(
@@ -659,6 +665,7 @@ impl Scene for Editor {
 
                             let final_matrix = entity_matrix * offset_matrix;
 
+                            
                             let color = [0.0, 1.0, 0.0, 1.0];
                             let instance = ColliderInstanceRaw::from_matrix(final_matrix, color);
 
@@ -673,6 +680,7 @@ impl Scene for Editor {
                             });
                         }
                     }
+                    log_once::debug_once!("Collider wireframe: {} entities with colliders, {} total colliders", entity_count, collider_count);
 
                     if !instances_by_shape.is_empty() {
                         let total_instances: usize =
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 8c0a5c8..100fbaa 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -45,6 +45,11 @@ impl SignalController for Editor {
             }
             Signal::Copy(_) => Ok(()),
             Signal::Paste(scene_entity) => {
+                let mut scene_entity = scene_entity.clone();
+                scene_entity.label = Editor::unique_label_for_world(
+                    self.world.as_ref(),
+                    scene_entity.label.as_str(),
+                );
                 let spawn = PendingSpawn {
                     scene_entity: scene_entity.clone(),
                     handle: None,
diff --git a/include/dropbear.h b/include/dropbear.h
index 85f3ff8..630fb92 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -68,39 +68,6 @@ typedef struct ColliderShapeFfi {
 
 typedef ColliderShapeFfi ColliderShape;
 
-typedef enum NShapeCastStatusTag {
-    NShapeCastStatusTag_OutOfIterations = 0,
-    NShapeCastStatusTag_Converged = 1,
-    NShapeCastStatusTag_Failed = 2,
-    NShapeCastStatusTag_PenetratingOrWithinTargetDist = 3,
-} NShapeCastStatusTag;
-
-typedef struct NShapeCastStatusOutOfIterations {
-} NShapeCastStatusOutOfIterations;
-
-typedef struct NShapeCastStatusConverged {
-} NShapeCastStatusConverged;
-
-typedef struct NShapeCastStatusFailed {
-} NShapeCastStatusFailed;
-
-typedef struct NShapeCastStatusPenetratingOrWithinTargetDist {
-} NShapeCastStatusPenetratingOrWithinTargetDist;
-
-typedef union NShapeCastStatusData {
-    NShapeCastStatusOutOfIterations OutOfIterations;
-    NShapeCastStatusConverged Converged;
-    NShapeCastStatusFailed Failed;
-    NShapeCastStatusPenetratingOrWithinTargetDist PenetratingOrWithinTargetDist;
-} NShapeCastStatusData;
-
-typedef struct NShapeCastStatusFfi {
-    NShapeCastStatusTag tag;
-    NShapeCastStatusData data;
-} NShapeCastStatusFfi;
-
-typedef NShapeCastStatusFfi NShapeCastStatus;
-
 typedef enum NAnimationInterpolationTag {
     NAnimationInterpolationTag_Linear = 0,
     NAnimationInterpolationTag_Step = 1,
@@ -179,59 +146,141 @@ typedef struct NChannelValuesFfi {
 
 typedef NChannelValuesFfi NChannelValues;
 
+typedef enum NShapeCastStatusTag {
+    NShapeCastStatusTag_OutOfIterations = 0,
+    NShapeCastStatusTag_Converged = 1,
+    NShapeCastStatusTag_Failed = 2,
+    NShapeCastStatusTag_PenetratingOrWithinTargetDist = 3,
+} NShapeCastStatusTag;
+
+typedef struct NShapeCastStatusOutOfIterations {
+} NShapeCastStatusOutOfIterations;
+
+typedef struct NShapeCastStatusConverged {
+} NShapeCastStatusConverged;
+
+typedef struct NShapeCastStatusFailed {
+} NShapeCastStatusFailed;
+
+typedef struct NShapeCastStatusPenetratingOrWithinTargetDist {
+} NShapeCastStatusPenetratingOrWithinTargetDist;
+
+typedef union NShapeCastStatusData {
+    NShapeCastStatusOutOfIterations OutOfIterations;
+    NShapeCastStatusConverged Converged;
+    NShapeCastStatusFailed Failed;
+    NShapeCastStatusPenetratingOrWithinTargetDist PenetratingOrWithinTargetDist;
+} NShapeCastStatusData;
+
+typedef struct NShapeCastStatusFfi {
+    NShapeCastStatusTag tag;
+    NShapeCastStatusData data;
+} NShapeCastStatusFfi;
+
+typedef NShapeCastStatusFfi NShapeCastStatus;
+
+typedef struct NRange {
+    float start;
+    float end;
+} NRange;
+
 typedef struct IndexNative {
     uint32_t index;
     uint32_t generation;
 } IndexNative;
 
-typedef struct IndexNativeArray {
-    IndexNative* values;
-    size_t length;
-    size_t capacity;
-} IndexNativeArray;
-
-typedef struct CharacterCollisionArray {
-    uint64_t entity_id;
-    IndexNativeArray collisions;
-} CharacterCollisionArray;
-
 typedef struct NCollider {
     IndexNative index;
     uint64_t entity_id;
     uint32_t id;
 } NCollider;
 
-typedef struct NShapeCastHit {
+typedef struct RayHit {
     NCollider collider;
     double distance;
-    NVector3 witness1;
-    NVector3 witness2;
-    NVector3 normal1;
-    NVector3 normal2;
-    NShapeCastStatus status;
-} NShapeCastHit;
+} RayHit;
 
-typedef void* PhysicsStatePtr;
+typedef struct AxisLock {
+    bool x;
+    bool y;
+    bool z;
+} AxisLock;
+
+typedef struct i32Array {
+    int32_t* values;
+    size_t length;
+    size_t capacity;
+} i32Array;
+
+typedef struct f64ArrayArray {
+    double* values;
+    size_t length;
+    size_t capacity;
+} f64ArrayArray;
+
+typedef struct NSkin {
+    const char* name;
+    i32Array joints;
+    f64ArrayArray inverse_bind_matrices;
+    const int32_t* skeleton_root;
+} NSkin;
+
+typedef struct NSkinArray {
+    NSkin* values;
+    size_t length;
+    size_t capacity;
+} NSkinArray;
+
+typedef void* AssetRegistryPtr;
 
 typedef struct NVector2 {
     double x;
     double y;
 } NVector2;
 
+typedef void* InputStatePtr;
+
+typedef struct u64Array {
+    uint64_t* values;
+    size_t length;
+    size_t capacity;
+} u64Array;
+
+typedef struct NAttenuation {
+    float constant;
+    float linear;
+    float quadratic;
+} NAttenuation;
+
+typedef struct NColour {
+    uint8_t r;
+    uint8_t g;
+    uint8_t b;
+    uint8_t a;
+} NColour;
+
 typedef struct NTransform {
     NVector3 position;
     NQuaternion rotation;
     NVector3 scale;
 } NTransform;
 
-typedef struct NRange {
-    float start;
-    float end;
-} NRange;
+typedef struct Progress {
+    size_t current;
+    size_t total;
+    const char* message;
+} Progress;
 
-typedef void* InputStatePtr;
+typedef struct IndexNativeArray {
+    IndexNative* values;
+    size_t length;
+    size_t capacity;
+} IndexNativeArray;
 
-typedef void* SceneLoaderPtr;
+typedef struct CharacterCollisionArray {
+    uint64_t entity_id;
+    IndexNativeArray collisions;
+} CharacterCollisionArray;
 
 typedef struct f64Array {
     double* values;
@@ -264,66 +313,17 @@ typedef struct NAnimationArray {
     size_t capacity;
 } NAnimationArray;
 
-typedef struct i32Array {
-    int32_t* values;
-    size_t length;
-    size_t capacity;
-} i32Array;
-
-typedef struct NNodeTransform {
-    NVector3 translation;
-    NQuaternion rotation;
-    NVector3 scale;
-} NNodeTransform;
-
-typedef struct NNode {
-    const char* name;
-    const int32_t* parent;
-    i32Array children;
-    NNodeTransform transform;
-} NNode;
-
-typedef struct NNodeArray {
-    NNode* values;
-    size_t length;
-    size_t capacity;
-} NNodeArray;
-
-typedef void* GraphicsContextPtr;
-
-typedef struct f64ArrayArray {
-    double* values;
-    size_t length;
-    size_t capacity;
-} f64ArrayArray;
-
-typedef struct NSkin {
-    const char* name;
-    i32Array joints;
-    f64ArrayArray inverse_bind_matrices;
-    const int32_t* skeleton_root;
-} NSkin;
-
-typedef struct NSkinArray {
-    NSkin* values;
-    size_t length;
-    size_t capacity;
-} NSkinArray;
-
-typedef struct NAttenuation {
-    float constant;
-    float linear;
-    float quadratic;
-} NAttenuation;
-
-typedef void* AssetRegistryPtr;
+typedef void* WorldPtr;
 
-typedef struct NColour {
-    uint8_t r;
-    uint8_t g;
-    uint8_t b;
-    uint8_t a;
-} NColour;
+typedef struct NShapeCastHit {
+    NCollider collider;
+    double distance;
+    NVector3 witness1;
+    NVector3 witness2;
+    NVector3 normal1;
+    NVector3 normal2;
+    NShapeCastStatus status;
+} NShapeCastHit;
 
 typedef struct NVector4 {
     double x;
@@ -362,35 +362,6 @@ typedef struct NMeshArray {
     size_t capacity;
 } NMeshArray;
 
-typedef struct u64Array {
-    uint64_t* values;
-    size_t length;
-    size_t capacity;
-} u64Array;
-
-typedef struct ConnectedGamepadIds {
-    u64Array ids;
-} ConnectedGamepadIds;
-
-typedef void* CommandBufferPtr;
-
-typedef struct NColliderArray {
-    NCollider* values;
-    size_t length;
-    size_t capacity;
-} NColliderArray;
-
-typedef struct Progress {
-    size_t current;
-    size_t total;
-    const char* message;
-} Progress;
-
-typedef struct RayHit {
-    NCollider collider;
-    double distance;
-} RayHit;
-
 typedef struct NMaterial {
     const char* name;
     uint64_t diffuse_texture;
@@ -415,18 +386,47 @@ typedef struct NMaterialArray {
     size_t capacity;
 } NMaterialArray;
 
+typedef void* SceneLoaderPtr;
+
 typedef struct RigidBodyContext {
     IndexNative index;
     uint64_t entity_id;
 } RigidBodyContext;
 
-typedef struct AxisLock {
-    bool x;
-    bool y;
-    bool z;
-} AxisLock;
+typedef struct ConnectedGamepadIds {
+    u64Array ids;
+} ConnectedGamepadIds;
 
-typedef void* WorldPtr;
+typedef struct NColliderArray {
+    NCollider* values;
+    size_t length;
+    size_t capacity;
+} NColliderArray;
+
+typedef void* CommandBufferPtr;
+
+typedef void* GraphicsContextPtr;
+
+typedef struct NNodeTransform {
+    NVector3 translation;
+    NQuaternion rotation;
+    NVector3 scale;
+} NNodeTransform;
+
+typedef struct NNode {
+    const char* name;
+    const int32_t* parent;
+    i32Array children;
+    NNodeTransform transform;
+} NNode;
+
+typedef struct NNodeArray {
+    NNode* values;
+    size_t length;
+    size_t capacity;
+} NNodeArray;
+
+typedef void* PhysicsStatePtr;
 
 int32_t dropbear_gamepad_is_button_pressed(InputStatePtr input, uint64_t gamepad_id, int32_t button_ordinal, bool* out0);
 int32_t dropbear_gamepad_get_left_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
@@ -459,6 +459,10 @@ int32_t dropbear_collider_get_collider_translation(PhysicsStatePtr physics, cons
 int32_t dropbear_collider_set_collider_translation(PhysicsStatePtr physics, const NCollider* collider, const NVector3* translation);
 int32_t dropbear_collider_get_collider_rotation(PhysicsStatePtr physics, const NCollider* collider, NVector3* out0);
 int32_t dropbear_collider_set_collider_rotation(PhysicsStatePtr physics, const NCollider* collider, const NVector3* rotation);
+int32_t dropbear_kcc_kcc_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
+int32_t dropbear_kcc_move_character(WorldPtr world, PhysicsStatePtr physics_state, uint64_t entity, const NVector3* translation, double delta_time);
+int32_t dropbear_kcc_set_rotation(WorldPtr world, PhysicsStatePtr physics_state, uint64_t entity, const NQuaternion* rotation);
+int32_t dropbear_kcc_get_hit(WorldPtr world, uint64_t entity, CharacterCollisionArray* out0);
 int32_t dropbear_rigidbody_exists_for_entity(WorldPtr world, PhysicsStatePtr physics, uint64_t entity, IndexNative* out0, bool* out0_present);
 int32_t dropbear_rigidbody_get_rigidbody_mode(WorldPtr _world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, int32_t* out0);
 int32_t dropbear_rigidbody_set_rigidbody_mode(WorldPtr world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, int32_t mode);
@@ -483,10 +487,6 @@ int32_t dropbear_rigidbody_set_rigidbody_lock_rotation(WorldPtr world, PhysicsSt
 int32_t dropbear_rigidbody_get_rigidbody_children(WorldPtr _world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, NColliderArray* out0);
 int32_t dropbear_rigidbody_apply_impulse(WorldPtr world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, double x, double y, double z);
 int32_t dropbear_rigidbody_apply_torque_impulse(WorldPtr world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, double x, double y, double z);
-int32_t dropbear_kcc_kcc_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
-int32_t dropbear_kcc_move_character(WorldPtr world, PhysicsStatePtr physics_state, uint64_t entity, const NVector3* translation, double delta_time);
-int32_t dropbear_kcc_set_rotation(WorldPtr world, PhysicsStatePtr physics_state, uint64_t entity, const NQuaternion* rotation);
-int32_t dropbear_kcc_get_hit(WorldPtr world, uint64_t entity, CharacterCollisionArray* out0);
 int32_t dropbear_scripting_load_scene_async(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, const char* scene_name, uint64_t* out0);
 int32_t dropbear_scripting_load_scene_async_with_loading(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, const char* scene_name, const char* loading_scene, uint64_t* out0);
 int32_t dropbear_scripting_switch_to_scene_immediate(CommandBufferPtr command_buffer, const char* scene_name);
@@ -494,6 +494,18 @@ int32_t dropbear_scripting_get_scene_load_handle_scene_name(SceneLoaderPtr scene
 int32_t dropbear_scripting_switch_to_scene_async(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, uint64_t scene_id);
 int32_t dropbear_scripting_get_scene_load_progress(SceneLoaderPtr scene_loader, uint64_t scene_id, Progress* out0);
 int32_t dropbear_scripting_get_scene_load_status(SceneLoaderPtr scene_loader, uint64_t scene_id, uint32_t* out0);
+int32_t dropbear_engine_get_asset(AssetRegistryPtr asset, const char* label, const AssetKind* kind, uint64_t* out0, bool* out0_present);
+int32_t dropbear_asset_model_get_label(AssetRegistryPtr asset, uint64_t model_handle, char** out0);
+int32_t dropbear_asset_model_get_meshes(AssetRegistryPtr asset, uint64_t model_handle, NMeshArray* out0);
+int32_t dropbear_asset_model_get_materials(AssetRegistryPtr asset, uint64_t model_handle, NMaterialArray* out0);
+int32_t dropbear_asset_model_get_skins(AssetRegistryPtr asset, uint64_t model_handle, NSkinArray* out0);
+int32_t dropbear_asset_model_get_animations(AssetRegistryPtr asset, uint64_t model_handle, NAnimationArray* out0);
+int32_t dropbear_asset_model_get_nodes(AssetRegistryPtr asset, uint64_t model_handle, NNodeArray* out0);
+int32_t dropbear_asset_texture_get_label(AssetRegistryPtr asset_manager, uint64_t texture_handle, char** out0, bool* out0_present);
+int32_t dropbear_asset_texture_get_width(AssetRegistryPtr asset_manager, uint64_t texture_handle, uint32_t* out0);
+int32_t dropbear_asset_texture_get_height(AssetRegistryPtr asset_manager, uint64_t texture_handle, uint32_t* out0);
+int32_t dropbear_engine_get_entity(WorldPtr world, const char* label, uint64_t* out0);
+int32_t dropbear_engine_quit(CommandBufferPtr command_buffer);
 int32_t dropbear_entity_label_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
 int32_t dropbear_entity_get_label(WorldPtr world, uint64_t entity, char** out0);
 int32_t dropbear_entity_get_children(WorldPtr world, uint64_t entity, u64Array* out0);
@@ -510,6 +522,38 @@ int32_t dropbear_input_get_last_mouse_pos(InputStatePtr input, NVector2* out0);
 int32_t dropbear_input_is_cursor_hidden(InputStatePtr input, bool* out0);
 int32_t dropbear_input_set_cursor_hidden(CommandBufferPtr command_buffer, InputStatePtr input, bool hidden);
 int32_t dropbear_input_get_connected_gamepads(InputStatePtr input, ConnectedGamepadIds* out0);
+int32_t dropbear_physics_get_gravity(PhysicsStatePtr physics, NVector3* out0);
+int32_t dropbear_physics_set_gravity(PhysicsStatePtr physics, const NVector3* gravity);
+int32_t dropbear_physics_raycast(PhysicsStatePtr physics, const NVector3* origin, const NVector3* dir, double time_of_impact, bool solid, RayHit* out0, bool* out0_present);
+int32_t dropbear_physics_shape_cast(PhysicsStatePtr physics, const NVector3* origin, const NVector3* direction, const ColliderShape* shape, double time_of_impact, bool solid, NShapeCastHit* out0, bool* out0_present);
+int32_t dropbear_physics_is_overlapping(PhysicsStatePtr physics, const NCollider* collider1, const NCollider* collider2, bool* out0);
+int32_t dropbear_physics_is_triggering(PhysicsStatePtr physics, const NCollider* collider1, const NCollider* collider2, bool* out0);
+int32_t dropbear_physics_is_touching(PhysicsStatePtr physics, uint64_t entity1, uint64_t entity2, bool* out0);
+int32_t dropbear_mesh_get_texture(WorldPtr world, AssetRegistryPtr asset, uint64_t entity, const char* material_name, uint64_t* out0, bool* out0_present);
+int32_t dropbear_mesh_set_texture_override(WorldPtr world, AssetRegistryPtr asset, uint64_t entity, const char* material_name, uint64_t texture_handle);
+int32_t dropbear_mesh_set_material_tint(WorldPtr world, AssetRegistryPtr asset, GraphicsContextPtr graphics, uint64_t entity, const char* material_name, float r, float g, float b, float a);
+int32_t dropbear_camera_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
+int32_t dropbear_camera_get_eye(WorldPtr world, uint64_t entity, NVector3* out0);
+int32_t dropbear_camera_set_eye(WorldPtr world, uint64_t entity, const NVector3* eye);
+int32_t dropbear_camera_get_target(WorldPtr world, uint64_t entity, NVector3* out0);
+int32_t dropbear_camera_set_target(WorldPtr world, uint64_t entity, const NVector3* target);
+int32_t dropbear_camera_get_up(WorldPtr world, uint64_t entity, NVector3* out0);
+int32_t dropbear_camera_set_up(WorldPtr world, uint64_t entity, const NVector3* up);
+int32_t dropbear_camera_get_aspect(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_get_fovy(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_fovy(WorldPtr world, uint64_t entity, double fovy);
+int32_t dropbear_camera_get_znear(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_znear(WorldPtr world, uint64_t entity, double znear);
+int32_t dropbear_camera_get_zfar(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_zfar(WorldPtr world, uint64_t entity, double zfar);
+int32_t dropbear_camera_get_yaw(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_yaw(WorldPtr world, uint64_t entity, double yaw);
+int32_t dropbear_camera_get_pitch(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_pitch(WorldPtr world, uint64_t entity, double pitch);
+int32_t dropbear_camera_get_speed(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_speed(WorldPtr world, uint64_t entity, double speed);
+int32_t dropbear_camera_get_sensitivity(WorldPtr world, uint64_t entity, double* out0);
+int32_t dropbear_camera_set_sensitivity(WorldPtr world, uint64_t entity, double sensitivity);
 int32_t dropbear_lighting_light_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
 int32_t dropbear_lighting_get_position(WorldPtr world, uint64_t entity, NVector3* out0);
 int32_t dropbear_lighting_set_position(WorldPtr world, uint64_t entity, const NVector3* position);
@@ -533,13 +577,6 @@ int32_t dropbear_lighting_get_casts_shadows(WorldPtr world, uint64_t entity, boo
 int32_t dropbear_lighting_set_casts_shadows(WorldPtr world, uint64_t entity, bool casts_shadows);
 int32_t dropbear_lighting_get_depth(WorldPtr world, uint64_t entity, NRange* out0);
 int32_t dropbear_lighting_set_depth(WorldPtr world, uint64_t entity, const NRange* depth);
-int32_t dropbear_physics_get_gravity(PhysicsStatePtr physics, NVector3* out0);
-int32_t dropbear_physics_set_gravity(PhysicsStatePtr physics, const NVector3* gravity);
-int32_t dropbear_physics_raycast(PhysicsStatePtr physics, const NVector3* origin, const NVector3* dir, double time_of_impact, bool solid, RayHit* out0, bool* out0_present);
-int32_t dropbear_physics_shape_cast(PhysicsStatePtr physics, const NVector3* origin, const NVector3* direction, const ColliderShape* shape, double time_of_impact, bool solid, NShapeCastHit* out0, bool* out0_present);
-int32_t dropbear_physics_is_overlapping(PhysicsStatePtr physics, const NCollider* collider1, const NCollider* collider2, bool* out0);
-int32_t dropbear_physics_is_triggering(PhysicsStatePtr physics, const NCollider* collider1, const NCollider* collider2, bool* out0);
-int32_t dropbear_physics_is_touching(PhysicsStatePtr physics, uint64_t entity1, uint64_t entity2, bool* out0);
 int32_t dropbear_properties_custom_properties_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
 int32_t dropbear_properties_get_string_property(WorldPtr world, uint64_t entity, const char* key, char** out0, bool* out0_present);
 int32_t dropbear_properties_get_int_property(WorldPtr world, uint64_t entity, const char* key, int32_t* out0, bool* out0_present);
@@ -561,42 +598,5 @@ int32_t dropbear_transform_set_local_transform(WorldPtr world, uint64_t entity, 
 int32_t dropbear_transform_get_world_transform(WorldPtr world, uint64_t entity, NTransform* out0);
 int32_t dropbear_transform_set_world_transform(WorldPtr world, uint64_t entity, const NTransform* transform);
 int32_t dropbear_transform_propogate_transform(WorldPtr world, uint64_t entity, NTransform* out0);
-int32_t dropbear_engine_get_entity(WorldPtr world, const char* label, uint64_t* out0);
-int32_t dropbear_engine_quit(CommandBufferPtr command_buffer);
-int32_t dropbear_engine_get_asset(AssetRegistryPtr asset, const char* label, const AssetKind* kind, uint64_t* out0, bool* out0_present);
-int32_t dropbear_asset_model_get_label(AssetRegistryPtr asset, uint64_t model_handle, char** out0);
-int32_t dropbear_asset_model_get_meshes(AssetRegistryPtr asset, uint64_t model_handle, NMeshArray* out0);
-int32_t dropbear_asset_model_get_materials(AssetRegistryPtr asset, uint64_t model_handle, NMaterialArray* out0);
-int32_t dropbear_asset_model_get_skins(AssetRegistryPtr asset, uint64_t model_handle, NSkinArray* out0);
-int32_t dropbear_asset_model_get_animations(AssetRegistryPtr asset, uint64_t model_handle, NAnimationArray* out0);
-int32_t dropbear_asset_model_get_nodes(AssetRegistryPtr asset, uint64_t model_handle, NNodeArray* out0);
-int32_t dropbear_asset_texture_get_label(AssetRegistryPtr asset_manager, uint64_t texture_handle, char** out0, bool* out0_present);
-int32_t dropbear_asset_texture_get_width(AssetRegistryPtr asset_manager, uint64_t texture_handle, uint32_t* out0);
-int32_t dropbear_asset_texture_get_height(AssetRegistryPtr asset_manager, uint64_t texture_handle, uint32_t* out0);
-int32_t dropbear_mesh_get_texture(WorldPtr world, AssetRegistryPtr asset, uint64_t entity, const char* material_name, uint64_t* out0, bool* out0_present);
-int32_t dropbear_mesh_set_texture_override(WorldPtr world, AssetRegistryPtr asset, uint64_t entity, const char* material_name, uint64_t texture_handle);
-int32_t dropbear_mesh_set_material_tint(WorldPtr world, AssetRegistryPtr asset, GraphicsContextPtr graphics, uint64_t entity, const char* material_name, float r, float g, float b, float a);
-int32_t dropbear_camera_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
-int32_t dropbear_camera_get_eye(WorldPtr world, uint64_t entity, NVector3* out0);
-int32_t dropbear_camera_set_eye(WorldPtr world, uint64_t entity, const NVector3* eye);
-int32_t dropbear_camera_get_target(WorldPtr world, uint64_t entity, NVector3* out0);
-int32_t dropbear_camera_set_target(WorldPtr world, uint64_t entity, const NVector3* target);
-int32_t dropbear_camera_get_up(WorldPtr world, uint64_t entity, NVector3* out0);
-int32_t dropbear_camera_set_up(WorldPtr world, uint64_t entity, const NVector3* up);
-int32_t dropbear_camera_get_aspect(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_get_fovy(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_fovy(WorldPtr world, uint64_t entity, double fovy);
-int32_t dropbear_camera_get_znear(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_znear(WorldPtr world, uint64_t entity, double znear);
-int32_t dropbear_camera_get_zfar(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_zfar(WorldPtr world, uint64_t entity, double zfar);
-int32_t dropbear_camera_get_yaw(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_yaw(WorldPtr world, uint64_t entity, double yaw);
-int32_t dropbear_camera_get_pitch(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_pitch(WorldPtr world, uint64_t entity, double pitch);
-int32_t dropbear_camera_get_speed(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_speed(WorldPtr world, uint64_t entity, double speed);
-int32_t dropbear_camera_get_sensitivity(WorldPtr world, uint64_t entity, double* out0);
-int32_t dropbear_camera_set_sensitivity(WorldPtr world, uint64_t entity, double sensitivity);
 
 #endif /* DROPBEAR_H */