kitgit

tirbofish/dropbear · commit

5734766f3d3179f7c4e327a5466317c40a6851a4

fix: general editor fixes, improvements to components and optimised the model rendering (as well as animations playing).

Unverified

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

view full diff

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index 6acbbe7..de27737 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -3,7 +3,7 @@ use std::sync::Arc;
 use glam::Mat4;
 use wgpu::util::DeviceExt;
 use crate::graphics::SharedGraphicsContext;
-use crate::model::{Animation, AnimationInterpolation, ChannelValues, Model, NodeTransform};
+use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct AnimationComponent {
diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index de2dddf..1cffb43 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -245,6 +245,13 @@ impl AssetRegistry {
     }
 
     pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Model> {
+        if let Some(existing) = self.models.get(&handle.id) {
+            if existing.label.eq_ignore_ascii_case("light cube") {
+                log::warn!("Attempted to update protected model '{}'", existing.label);
+                return None;
+            }
+        }
+
         self.models.insert(handle.id, model)
     }
 
@@ -252,6 +259,10 @@ impl AssetRegistry {
         self.models.get(&handle.id)
     }
 
+    pub fn get_model_mut(&mut self, handle: Handle<Model>) -> Option<&mut Model> {
+        self.models.get_mut(&handle.id)
+    }
+
     pub fn get_model_by_label(&self, label: &str) -> Option<&Model> {
         self.model_labels
             .get(label)
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index f533dab..22278c4 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -7,10 +7,10 @@ use std::{
 };
 
 use crate::{
-    asset::{ASSET_REGISTRY},
+    asset::ASSET_REGISTRY,
     graphics::{Instance, SharedGraphicsContext},
     model::Model,
-    texture::Texture,
+    texture::Texture, utils::ResourceReference,
 };
 use egui::{Ui};
 use crate::asset::Handle;
@@ -292,7 +292,8 @@ impl MeshRenderer {
         let path = path.as_ref().to_path_buf();
         let handle = Model::load_from_memory_raw(
             graphics.clone(),
-            std::fs::read(path)?,
+            std::fs::read(&path)?,
+            Some(ResourceReference::from_path(&path)?),
             label,
             ASSET_REGISTRY.clone(),
         ).await?;
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 2f3d18f..0edbf32 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -275,6 +275,7 @@ pub struct Light {
     pub buffer: UniformBuffer<LightUniform>,
     pub bind_group: BindGroup,
     pub instance_buffer: ResizableBuffer<InstanceInput>,
+    pub component: LightComponent,
 }
 
 impl Light {
@@ -364,12 +365,15 @@ impl Light {
             buffer,
             bind_group,
             instance_buffer,
+            component: light.clone(),
         }
     }
 
-    pub fn update(&mut self, graphics: &SharedGraphicsContext, light: &LightComponent) {
+    pub fn update(&mut self, graphics: &SharedGraphicsContext) {
         puffin::profile_function!();
 
+        let light = &mut self.component;
+
         self.uniform.position = dvec3_to_uniform_array(light.position);
 
         self.uniform.direction =
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index e451684..977d384 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -16,7 +16,8 @@ use gltf::texture::MinFilter;
 use puffin::profile_scope;
 use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup};
 
-#[derive(Clone)]
+// do not derive clone otherwise it wil take too much memory
+// #[derive(Clone)]
 pub struct Model {
     pub hash: u64, // also the id related to the handle
     pub label: String,
@@ -28,7 +29,7 @@ pub struct Model {
     pub nodes: Vec<Node>,
 }
 
-#[derive(Clone)]
+// #[derive(Clone)]
 pub struct Mesh {
     pub name: String,
     pub vertex_buffer: wgpu::Buffer,
@@ -43,6 +44,9 @@ pub struct Material {
     pub name: String,
     pub diffuse_texture: Texture,
     pub normal_texture: Texture,
+    pub emissive_texture: Option<Texture>,
+    pub metallic_roughness_texture: Option<Texture>,
+    pub occlusion_texture: Option<Texture>,
     pub bind_group: wgpu::BindGroup,
     pub tint: [f32; 4],
     pub emissive_factor: [f32; 3],
@@ -57,9 +61,6 @@ pub struct Material {
     pub tint_buffer: UniformBuffer<MaterialUniform>,
     pub texture_tag: Option<String>,
     pub wrap_mode: TextureWrapMode,
-    pub emissive_texture: Option<Texture>,
-    pub metallic_roughness_texture: Option<Texture>,
-    pub occlusion_texture: Option<Texture>,
 }
 
 #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)]
@@ -283,6 +284,7 @@ struct GLTFTextureInformation {
 
 impl GLTFTextureInformation {
     fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation {
+        puffin::profile_function!();
         let sampler = tex.sampler();
 
         let mag_filter = match sampler.mag_filter() {
@@ -460,11 +462,6 @@ impl Model {
                 .and_then(|info| process_texture(info.texture()));
 
             let emissive_factor = material.emissive_factor();
-            let emissive_factor = [
-                emissive_factor[0],
-                emissive_factor[1],
-                emissive_factor[2],
-            ];
             let metallic_factor = pbr.metallic_factor();
             let roughness_factor = pbr.roughness_factor();
             let alpha_mode = material.alpha_mode();
@@ -829,6 +826,7 @@ impl Model {
     pub async fn load_from_memory_raw<B>(
         graphics: Arc<SharedGraphicsContext>,
         buffer: B,
+        optional_resref: Option<ResourceReference>,
         label: Option<&str>,
         registry: Arc<RwLock<AssetRegistry>>,
     ) -> anyhow::Result<Handle<Model>>
@@ -885,9 +883,9 @@ impl Model {
 
                 let extract = |info: Option<GLTFTextureInformation>| -> (Option<(Vec<u8>, (u32, u32))>, Option<wgpu::SamplerDescriptor<'static>>) {
                     if let Some(info) = info {
-                         (Some((info.pixels, (info.width, info.height))), Some(info.sampler))
+                        (Some((info.pixels, (info.width, info.height))), Some(info.sampler))
                     } else {
-                         (None, None)
+                        (None, None)
                     }
                 };
 
@@ -1094,12 +1092,35 @@ impl Model {
             });
         }
 
+        if let Some(resref) = optional_resref.clone() {
+            for material in &mut materials {
+                material.diffuse_texture.reference = Some(resref.clone());
+                material.normal_texture.reference = Some(resref.clone());
+
+                if let Some(texture) = material.emissive_texture.as_mut() {
+                    texture.reference = Some(resref.clone());
+                }
+
+                if let Some(texture) = material.metallic_roughness_texture.as_mut() {
+                    texture.reference = Some(resref.clone());
+                }
+
+                if let Some(texture) = material.occlusion_texture.as_mut() {
+                    texture.reference = Some(resref.clone());
+                }
+            }
+        }
+
         log::debug!("Successfully loaded model [{:?}]", label);
 
+        let model_path = optional_resref
+            .clone()
+            .unwrap_or_else(|| ResourceReference::from_bytes(buffer.as_ref()));
+
         let model = Model {
             label: model_label.to_string(),
             hash,
-            path: ResourceReference::from_bytes(buffer.as_ref()),
+            path: model_path,
             meshes: gpu_meshes,
             materials,
             skins,
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index bf25e48..c65ea00 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -6,7 +6,7 @@ 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::lighting::{Light, LightArrayUniform, MAX_LIGHTS};
 use crate::model::{ModelVertex, Vertex};
 use crate::pipelines::DropbearShaderPipeline;
 use crate::shader::Shader;
@@ -135,22 +135,24 @@ impl LightCubePipeline {
 
         let mut light_index: usize = 0;
 
-        for (light_component, s_trans, e_trans, light) in world
-            .query::<(&LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>()
+        for (s_trans, e_trans, light) in world
+            .query::<(Option<&Transform>, Option<&EntityTransform>, &mut Light)>()
             .iter()
         {
+            light.update(graphics.as_ref());
+
             let instance: InstanceInput = if let Some(transform) = e_trans {
                 let sync_transform = transform.sync();
                 sync_transform.matrix().into()
             } else if let Some(transform) = s_trans {
                 transform.matrix().into()
             } else {
-                light_component.to_transform().matrix().into()
+                panic!("No Transform or EntityTransform available for this light cube");
             };
 
             light.instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
 
-            if light_component.enabled && light_index < MAX_LIGHTS {
+            if light.component.enabled && light_index < MAX_LIGHTS {
                 let uniform = *light.uniform();
 
                 light.buffer.write(&graphics.queue, &uniform);
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index 233aff0..90b5601 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -34,8 +34,6 @@ struct MaterialUniform {
     uv_tiling: vec2<f32>,
 }
 
-const c_max_lights: u32 = 10u;
-
 @group(0) @binding(0)
 var t_diffuse: texture_2d<f32>;
 @group(0) @binding(1)
@@ -257,7 +255,7 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
     let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz;
     var final_color = ambient;
 
-    for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) {
+    for(var i = 0u; i < u_globals.num_lights; i += 1u) {
         let light = s_light_array[i];
 
         let light_type = i32(light.color.w + 0.1);
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 4d36ed9..36d8f8a 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -303,6 +303,10 @@ impl Texture {
         label: Option<&str>,
     ) -> Self {
         puffin::profile_function!(label.unwrap_or(""));
+        if let Some(l) = label {
+            log::debug!("Loading texture: {l}");
+        }
+
         let hash = AssetRegistry::hash_bytes(bytes);
         
         let (diffuse_rgba, dimensions) = {
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index cae4d24..b5b0d83 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -146,14 +146,12 @@ impl ResourceReference {
 
     /// Creates a new `ResourceReference` from bytes
     pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
-        puffin::profile_function!();
         Self {
             ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()),
         }
     }
 
     pub fn from_reference(ref_type: ResourceReferenceType) -> Self {
-        puffin::profile_function!(format!("{:?}", ref_type));
         match ref_type {
             ResourceReferenceType::File(reference) => {
                 let canonical = canonicalize_euca_uri(&reference)
@@ -168,7 +166,6 @@ impl ResourceReference {
 
     /// Creates a [`ResourceReference`] directly from an euca URI (e.g. `euca://models/cube.glb`).
     pub fn from_euca_uri(uri: impl AsRef<str>) -> anyhow::Result<Self> {
-        puffin::profile_function!(uri.as_ref());
         let canonical = canonicalize_euca_uri(uri.as_ref())?;
         Ok(Self {
             ref_type: ResourceReferenceType::File(canonical),
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 32d55b9..4c4e708 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -485,11 +485,12 @@ impl Component for MeshRenderer {
                 }
                 ResourceReferenceType::File(reference) => {
                     log::debug!("Loading model from file: {:?}", ser.handle);
-                    let path = ser.handle.resolve()?;
+                    let path = ser.handle.clone().resolve()?;
                     let buffer = std::fs::read(&path)?;
                     Model::load_from_memory_raw(
                         graphics.clone(),
                         buffer,
+                        Some(ser.handle.clone()),
                         Some(reference),
                         ASSET_REGISTRY.clone(),
                     )
@@ -500,6 +501,7 @@ impl Component for MeshRenderer {
                     Model::load_from_memory_raw(
                         graphics.clone(),
                         bytes,
+                        Some(ser.handle.clone()),
                         None,
                         ASSET_REGISTRY.clone(),
                     )
@@ -598,7 +600,6 @@ impl Component for MeshRenderer {
 
         let mut texture_override: HashMap<String, SerializedMaterialCustomisation> = HashMap::new();
         for (label, mat) in &self.material_snapshot {
-            // 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());
@@ -748,9 +749,8 @@ impl InspectableComponent for MeshRenderer {
 
         CollapsingHeader::new("Mesh Renderer").show(ui, |ui| {
             let registry = ASSET_REGISTRY.read();
-            let current_model = registry.get_model(self.model()).cloned();
+            let current_model = registry.get_model(self.model());
             let model_list = registry.list_models();
-            drop(registry);
 
             let model_reference = current_model
                 .as_ref()
@@ -866,6 +866,9 @@ impl InspectableComponent for MeshRenderer {
                                     if handle.is_null() {
                                         continue;
                                     }
+                                    if label.eq_ignore_ascii_case("light cube") {
+                                        continue;
+                                    }
                                     let is_selected = self.model() == *handle;
                                     if ui.selectable_label(is_selected, label).clicked() {
                                         selected_model = Some(*handle);
@@ -887,6 +890,14 @@ impl InspectableComponent for MeshRenderer {
                                 if let Some(handle) =
                                     ASSET_REGISTRY.read().get_model_handle_by_reference(&reference)
                                 {
+                                    if let Some(model) = ASSET_REGISTRY.read().get_model(handle) {
+                                        if model.label.eq_ignore_ascii_case("light cube") {
+                                            ui.ctx().data_mut(|d| {
+                                                d.insert_temp(drag_id, None::<ResourceReference>)
+                                            });
+                                            return;
+                                        }
+                                    }
                                     self.set_model(handle);
                                     self.reset_texture_override();
                                 }
diff --git a/crates/eucalyptus-core/src/lighting.rs b/crates/eucalyptus-core/src/lighting.rs
index 185256f..cf0ed8d 100644
--- a/crates/eucalyptus-core/src/lighting.rs
+++ b/crates/eucalyptus-core/src/lighting.rs
@@ -1,14 +1,13 @@
 use std::sync::Arc;
-use egui::{CollapsingHeader, DragValue, Ui};
+use egui::{CollapsingHeader, ComboBox, DragValue, Ui};
 use crate::ptr::WorldPtr;
 use crate::scripting::jni::utils::{FromJObject, ToJObject};
 use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
 use crate::types::NVector3;
-use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::entity::{EntityTransform, Transform};
-use dropbear_engine::lighting::{Light, LightComponent, LightType};
-use glam::{DQuat, DVec3};
+use dropbear_engine::lighting::{Light, LightType};
+use glam::{DQuat, DVec3, Vec3};
 use hecs::{Entity, World};
 use jni::objects::{JObject, JValue};
 use jni::JNIEnv;
@@ -21,7 +20,7 @@ impl SerializedComponent for SerializedLight {}
 
 impl Component for Light {
     type SerializedForm = SerializedLight;
-    type RequiredComponentTypes = (Self, LightComponent, Transform);
+    type RequiredComponentTypes = (Self, Transform);
 
     fn descriptor() -> ComponentDescriptor {
         ComponentDescriptor {
@@ -45,124 +44,134 @@ impl Component for Light {
             ).await;
             let transform = light_component.to_transform();
 
-            Ok((light, light_component, transform))
+            Ok((light, transform))
         })
     }
 
     fn update_component(&mut self, world: &World, _physics: &mut crate::physics::PhysicsState, entity: Entity, _dt: f32, graphics: Arc<SharedGraphicsContext>) {
-        if let Ok(comp) = world.query_one::<&LightComponent>(entity).get() {
-            let mut synced = comp.clone();
-            if let Ok(entity_transform) = world.query_one::<&EntityTransform>(entity).get() {
-                let transform = entity_transform.sync();
-                synced.position = transform.position;
-                synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
-            } else if let Ok(transform) = world.query_one::<&Transform>(entity).get() {
-                synced.position = transform.position;
-                synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
-            }
-
-            self.update(&graphics, &synced);
+        let synced = &mut self.component;
+        if let Ok(entity_transform) = world.query_one::<&EntityTransform>(entity).get() {
+            let transform = entity_transform.sync();
+            synced.position = transform.position;
+            synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
+        } else if let Ok(transform) = world.query_one::<&Transform>(entity).get() {
+            synced.position = transform.position;
+            synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
         }
+
+        self.update(&graphics);
     }
 
-    fn save(&self, world: &World, entity: Entity) -> Box<dyn SerializedComponent> {
-        if let Ok(comp) = world.query_one::<&LightComponent>(entity).get() {
-            Box::new(SerializedLight {
-                label: self.label.clone(),
-                light_component: comp.clone(),
-                enabled: comp.enabled,
-                entity_id: Some(entity),
-            })
-        } else {
-            Box::new(SerializedLight::default())
-        }
+    fn save(&self, _: &World, entity: Entity) -> Box<dyn SerializedComponent> {
+        Box::new(SerializedLight {
+            label: self.label.clone(),
+            light_component: self.component.clone(),
+            entity_id: Some(entity),
+        })
     }
 }
 
 impl InspectableComponent for Light {
     fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
         CollapsingHeader::new("Light").default_open(true).show(ui, |ui| {
-            ui.horizontal(|ui| {
-                ui.label("Label");
-                ui.text_edit_singleline(&mut self.label);
-            });
-
-            let registry = ASSET_REGISTRY.read();
-            let current_label = registry
-                .get_label_from_model_handle(self.cube_model)
-                .unwrap_or_else(|| "(unlabeled)".to_string());
-
-            ui.horizontal(|ui| {
-                ui.label("Cube Model");
-                ui.label(current_label.as_str());
-            });
-
-            let label_id = ui.make_persistent_id("light_cube_model_label");
-            let mut model_label = ui
-                .data_mut(|d| d.get_temp::<String>(label_id))
-                .unwrap_or_else(|| current_label.clone());
-
-            let mut invalid_label = false;
-            ui.horizontal(|ui| {
-                ui.label("Model Label");
-                let response = ui.text_edit_singleline(&mut model_label);
-                if response.changed() {
-                    ui.data_mut(|d| d.insert_temp(label_id, model_label.clone()));
-                }
+            ui.add_space(6.0);
+            ui.label("Uniform");
 
-                if ui.button("Apply").clicked() {
-                    if let Some(handle) = registry.get_model_handle_from_label(&model_label) {
-                        self.cube_model = handle;
-                        ui.data_mut(|d| d.insert_temp(label_id, model_label.clone()));
-                    } else {
-                        invalid_label = true;
-                    }
-                }
+            ui.label("Light Type");
+            ComboBox::from_id_salt("Light Type").show_ui(ui, |ui| {
+                ui.selectable_value(&mut self.component.light_type, LightType::Directional, "Directional");
+                ui.selectable_value(&mut self.component.light_type, LightType::Point, "Point");
+                ui.selectable_value(&mut self.component.light_type, LightType::Spot, "Spot");
             });
 
-            if invalid_label {
-                ui.label("Unknown model label");
+            let mut display_pos = |yueye: &mut Ui| {
+                yueye.horizontal(|yueye| {
+                    yueye.label("Position");
+                    yueye.add(DragValue::new(&mut self.component.position.x).speed(0.01));
+                    yueye.add(DragValue::new(&mut self.component.position.y).speed(0.01));
+                    yueye.add(DragValue::new(&mut self.component.position.z).speed(0.01));
+                });
+            };
+
+            let mut display_dir = |yueye: &mut Ui| {
+                yueye.horizontal(|yueye| {
+                    yueye.label("Direction");
+                    yueye.add(DragValue::new(&mut self.component.direction.x).speed(0.01));
+                    yueye.add(DragValue::new(&mut self.component.direction.y).speed(0.01));
+                    yueye.add(DragValue::new(&mut self.component.direction.z).speed(0.01));
+                });
+            };
+
+            match self.component.light_type {
+                LightType::Directional => {
+                    display_dir(ui);
+                },
+                LightType::Point => {
+                    display_pos(ui);
+                },
+                LightType::Spot => {
+                    display_pos(ui);
+                    display_dir(ui);
+                },
             }
 
-            ui.add_space(6.0);
-            ui.label("Uniform");
+            let mut colour_rgb = [
+                self.component.colour[0] as f32,
+                self.component.colour[1] as f32,
+                self.component.colour[2] as f32,
+            ];
+            egui::color_picker::color_edit_button_rgb(ui, &mut colour_rgb);
+            self.component.colour = Vec3::from_array(colour_rgb).as_dvec3();
 
             ui.horizontal(|ui| {
-                ui.label("Position");
-                ui.add(DragValue::new(&mut self.uniform.position[0]).speed(0.01));
-                ui.add(DragValue::new(&mut self.uniform.position[1]).speed(0.01));
-                ui.add(DragValue::new(&mut self.uniform.position[2]).speed(0.01));
+                ui.label("Intensity");
+                ui.add(DragValue::new(&mut self.component.intensity).speed(0.05));
             });
 
+            if matches!(self.component.light_type, LightType::Point | LightType::Spot) {
+                ui.horizontal(|ui| {
+                    ui.label("Attenuation");
+                    ui.add(DragValue::new(&mut self.component.attenuation.constant).speed(0.01));
+                    ui.add(DragValue::new(&mut self.component.attenuation.linear).speed(0.01));
+                    ui.add(DragValue::new(&mut self.component.attenuation.quadratic).speed(0.01));
+                });
+            }
+
             ui.horizontal(|ui| {
-                ui.label("Direction");
-                ui.add(DragValue::new(&mut self.uniform.direction[0]).speed(0.01));
-                ui.add(DragValue::new(&mut self.uniform.direction[1]).speed(0.01));
-                ui.add(DragValue::new(&mut self.uniform.direction[2]).speed(0.01));
+                ui.checkbox(&mut self.component.enabled, "Enabled");
+                ui.checkbox(&mut self.component.visible, "Visible");
             });
 
-            let mut colour_rgb = [
-                self.uniform.colour[0],
-                self.uniform.colour[1],
-                self.uniform.colour[2],
-            ];
-            if egui::color_picker::color_edit_button_rgb(ui, &mut colour_rgb).changed() {
-                self.uniform.colour[0] = colour_rgb[0];
-                self.uniform.colour[1] = colour_rgb[1];
-                self.uniform.colour[2] = colour_rgb[2];
+            if matches!(self.component.light_type, LightType::Spot) {
+                ui.horizontal(|ui| {
+                    ui.label("Cutoff");
+                    ui.add(DragValue::new(&mut self.component.cutoff_angle).speed(0.1));
+                });
+                ui.horizontal(|ui| {
+                    ui.label("Outer Cutoff");
+                    ui.add(DragValue::new(&mut self.component.outer_cutoff_angle).speed(0.1));
+                });
+
+                if self.component.outer_cutoff_angle <= self.component.cutoff_angle {
+                    self.component.outer_cutoff_angle = self.component.cutoff_angle + 1.0;
+                }
             }
 
+            ui.separator();
+            ui.label("Shadows");
+            ui.checkbox(&mut self.component.cast_shadows, "Cast Shadows");
             ui.horizontal(|ui| {
-                ui.label("Attenuation");
-                ui.add(DragValue::new(&mut self.uniform.constant).speed(0.01));
-                ui.add(DragValue::new(&mut self.uniform.linear).speed(0.01));
-                ui.add(DragValue::new(&mut self.uniform.quadratic).speed(0.01));
+                ui.label("Depth");
+                ui.add(DragValue::new(&mut self.component.depth.start).speed(0.1));
+                ui.label("..");
+                ui.add(DragValue::new(&mut self.component.depth.end).speed(0.1));
             });
 
-            ui.horizontal(|ui| {
-                ui.label("Cutoff");
-                ui.add(DragValue::new(&mut self.uniform.cutoff).speed(0.01));
-            });
+            if self.component.depth.end < self.component.depth.start {
+                self.component.depth.end = self.component.depth.start;
+            }
+
+            
         });
     }
 }
@@ -505,9 +514,9 @@ fn get_colour(
     entity: Entity,
 ) -> DropbearNativeResult<NColour> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(NColour::from_linear_rgb(light.colour))
+    Ok(NColour::from_linear_rgb(light.component.colour))
 }
 
 #[dropbear_macro::export(
@@ -522,9 +531,9 @@ fn set_colour(
     colour: &NColour,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.colour = (*colour).to_linear_rgb();
+    light.component.colour = (*colour).to_linear_rgb();
     Ok(())
 }
 
@@ -539,9 +548,9 @@ fn get_light_type(
     entity: Entity,
 ) -> DropbearNativeResult<i32> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.light_type as i32)
+    Ok(light.component.light_type as i32)
 }
 
 #[dropbear_macro::export(
@@ -556,10 +565,10 @@ fn set_light_type(
     light_type: i32,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
 
-    light.light_type = match light_type {
+    light.component.light_type = match light_type {
         0 => LightType::Directional,
         1 => LightType::Point,
         2 => LightType::Spot,
@@ -580,9 +589,9 @@ fn get_intensity(
     entity: Entity,
 ) -> DropbearNativeResult<f64> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.intensity as f64)
+    Ok(light.component.intensity as f64)
 }
 
 #[dropbear_macro::export(
@@ -597,9 +606,9 @@ fn set_intensity(
     intensity: f64,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.intensity = intensity as f32;
+    light.component.intensity = intensity as f32;
     Ok(())
 }
 
@@ -614,13 +623,13 @@ fn get_attenuation(
     entity: Entity,
 ) -> DropbearNativeResult<NAttenuation> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
 
     Ok(NAttenuation {
-        constant: light.attenuation.constant,
-        linear: light.attenuation.linear,
-        quadratic: light.attenuation.quadratic,
+        constant: light.component.attenuation.constant,
+        linear: light.component.attenuation.linear,
+        quadratic: light.component.attenuation.quadratic,
     })
 }
 
@@ -636,12 +645,12 @@ fn set_attenuation(
     attenuation: &NAttenuation,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
 
-    light.attenuation.constant = attenuation.constant;
-    light.attenuation.linear = attenuation.linear;
-    light.attenuation.quadratic = attenuation.quadratic;
+    light.component.attenuation.constant = attenuation.constant;
+    light.component.attenuation.linear = attenuation.linear;
+    light.component.attenuation.quadratic = attenuation.quadratic;
     Ok(())
 }
 
@@ -656,9 +665,9 @@ fn get_enabled(
     entity: Entity,
 ) -> DropbearNativeResult<bool> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.enabled)
+    Ok(light.component.enabled)
 }
 
 #[dropbear_macro::export(
@@ -673,9 +682,9 @@ fn set_enabled(
     enabled: bool,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.enabled = enabled;
+    light.component.enabled = enabled;
     Ok(())
 }
 
@@ -690,9 +699,9 @@ fn get_cutoff_angle(
     entity: Entity,
 ) -> DropbearNativeResult<f64> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.cutoff_angle as f64)
+    Ok(light.component.cutoff_angle as f64)
 }
 
 #[dropbear_macro::export(
@@ -707,9 +716,9 @@ fn set_cutoff_angle(
     cutoff_angle: f64,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.cutoff_angle = cutoff_angle as f32;
+    light.component.cutoff_angle = cutoff_angle as f32;
     Ok(())
 }
 
@@ -724,9 +733,9 @@ fn get_outer_cutoff_angle(
     entity: Entity,
 ) -> DropbearNativeResult<f64> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.outer_cutoff_angle as f64)
+    Ok(light.component.outer_cutoff_angle as f64)
 }
 
 #[dropbear_macro::export(
@@ -741,9 +750,9 @@ fn set_outer_cutoff_angle(
     outer_cutoff_angle: f64,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.outer_cutoff_angle = outer_cutoff_angle as f32;
+    light.component.outer_cutoff_angle = outer_cutoff_angle as f32;
     Ok(())
 }
 
@@ -758,9 +767,9 @@ fn get_casts_shadows(
     entity: Entity,
 ) -> DropbearNativeResult<bool> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.cast_shadows)
+    Ok(light.component.cast_shadows)
 }
 
 #[dropbear_macro::export(
@@ -775,9 +784,9 @@ fn set_casts_shadows(
     casts_shadows: bool,
 ) -> DropbearNativeResult<()> {
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.cast_shadows = casts_shadows;
+    light.component.cast_shadows = casts_shadows;
     Ok(())
 }
 
@@ -792,12 +801,12 @@ fn get_depth(
     entity: Entity,
 ) -> DropbearNativeResult<NRange> {
     let light = world
-        .get::<&LightComponent>(entity)
+        .get::<&Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
 
     Ok(NRange {
-        start: light.depth.start,
-        end: light.depth.end,
+        start: light.component.depth.start,
+        end: light.component.depth.end,
     })
 }
 
@@ -820,8 +829,8 @@ fn set_depth(
     }
 
     let mut light = world
-        .get::<&mut LightComponent>(entity)
+        .get::<&mut Light>(entity)
         .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.depth = depth.start..depth.end;
+    light.component.depth = depth.start..depth.end;
     Ok(())
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/mesh.rs b/crates/eucalyptus-core/src/mesh.rs
index b87c86c..f3f6165 100644
--- a/crates/eucalyptus-core/src/mesh.rs
+++ b/crates/eucalyptus-core/src/mesh.rs
@@ -242,26 +242,17 @@ fn set_material_tint(
     b: f32,
     a: f32,
 ) -> DropbearNativeResult<()> {
-    let renderer = world
-        .get::<&MeshRenderer>(entity)
+    let _ = asset;
+    let mut renderer = world
+        .get::<&mut MeshRenderer>(entity)
         .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    let handle = renderer.model();
 
-    let mut registry = asset.write();
-    let model = registry
-        .get_model(handle)
-        .cloned()
-        .ok_or(DropbearNativeError::AssetNotFound)?;
-    let mut model = model;
-
-    let index = shared::resolve_target_material_index(&model, &material_name)
+    let material = renderer
+        .material_snapshot
+        .get_mut(&material_name)
         .ok_or(DropbearNativeError::InvalidArgument)?;
 
-    if let Some(material) = model.materials.get_mut(index) {
-        material.tint = [r, g, b, a];
-        material.sync_uniform(graphics);
-    }
-
-    registry.update_model(handle, model);
+    material.tint = [r, g, b, a];
+    material.sync_uniform(graphics);
     Ok(())
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 53c6ff5..ccc939b 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -381,7 +381,7 @@ impl SceneConfig {
     ) -> anyhow::Result<()> {
         let mut has_light = false;
         if world
-            .query::<(&LightComponent, &Light)>()
+            .query::<&Light>()
             .iter()
             .next()
             .is_some()
@@ -400,9 +400,6 @@ impl SceneConfig {
                 });
             }
             let comp = LightComponent::directional(glam::DVec3::ONE, 1.0);
-            let light_direction = LightComponent::default_direction();
-            let rotation =
-                DQuat::from_rotation_arc(DVec3::new(0.0, 0.0, -1.0), light_direction);
             let light =
                 Light::new(graphics.clone(), comp.clone(), Some("Default Light"))
                     .await;
@@ -410,7 +407,6 @@ impl SceneConfig {
             let light_config = SerializedLight {
                 label: "Default Light".to_string(),
                 light_component: comp.clone(),
-                enabled: true,
                 entity_id: None,
             };
 
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 7f120b3..cb583fc 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -298,7 +298,6 @@ pub struct Property {
 pub struct SerializedLight {
     pub label: String,
     pub light_component: LightComponent,
-    pub enabled: bool,
 
     #[serde(skip)]
     pub entity_id: Option<hecs::Entity>,
@@ -309,7 +308,6 @@ impl Default for SerializedLight {
         Self {
             label: "Default Light".to_string(),
             light_component: LightComponent::default(),
-            enabled: true,
             entity_id: None,
         }
     }
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 6214acc..c27eb2a 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -279,6 +279,15 @@ impl ResolveReference for ResourceReference {
 
                     if !project_path.as_os_str().is_empty() {
                         let root = project_path.join("resources");
+                        if let Some(resolved) = try_resolve_resource_from_root(relative, &root) {
+                            return Ok(resolved);
+                        }
+
+                        let fallback = project_path.join(relative);
+                        if fallback.exists() {
+                            return Ok(fallback);
+                        }
+
                         return resolve_resource_from_root(relative, &root);
                     }
                 }
@@ -293,6 +302,11 @@ impl ResolveReference for ResourceReference {
     }
 }
 
+fn try_resolve_resource_from_root(relative: &str, root: &Path) -> Option<PathBuf> {
+    let resolved = root.join(relative);
+    resolved.exists().then_some(resolved)
+}
+
 fn runtime_resources_dir() -> anyhow::Result<PathBuf> {
     let current_exe = std::env::current_exe()?;
     let dir = current_exe
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index aece78d..d903a0c 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -11,7 +11,11 @@ use eucalyptus_core::states::PROJECT;
 use hecs::Entity;
 use log::{info, warn};
 
-use crate::editor::{ComponentNodeSelection, DraggedAsset, EditorTabViewer, FsEntry, StaticallyKept, TABS_GLOBAL};
+use crate::editor::{
+    AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset,
+    EditorTabViewer, FsEntry, ResourceDivision, SceneDivision, ScriptDivision, StaticallyKept,
+    Signal, TABS_GLOBAL,
+};
 use eucalyptus_core::component::DRAGGED_ASSET_ID;
 
 #[derive(Clone, Copy, Debug)]
@@ -26,6 +30,41 @@ enum TextureSlot {
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) {
         let mut cfg = TABS_GLOBAL.lock();
+        cfg.asset_node_assets.clear();
+        cfg.asset_node_info.clear();
+        if let Some(rename) = &cfg.asset_rename {
+            if !rename.original_path.exists() {
+                cfg.asset_rename = None;
+            }
+        }
+
+        if let Some(mut rename) = cfg.asset_rename.take() {
+            let rename_id = egui::Id::new("asset_rename_input");
+            let mut should_apply = false;
+
+            ui.horizontal(|ui| {
+                ui.label("Rename");
+                let response = ui.add(
+                    egui::TextEdit::singleline(&mut rename.buffer).id(rename_id),
+                );
+                if rename.just_started {
+                    ui.ctx().memory_mut(|m| m.request_focus(rename_id));
+                    rename.just_started = false;
+                }
+
+                let enter = ui.input(|input| input.key_pressed(egui::Key::Enter));
+                should_apply = (enter && response.has_focus()) || response.lost_focus();
+            });
+
+            if should_apply {
+                let is_dir = rename.original_path.is_dir();
+                self.apply_asset_rename(&rename, is_dir);
+            } else {
+                cfg.asset_rename = Some(rename);
+            }
+
+            ui.separator();
+        }
 
         let project_root = {
             let project = PROJECT.read();
@@ -39,8 +78,8 @@ 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_scripts_branch(&mut cfg, builder, &project_root);
-            Self::build_scene_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();
         });
 
@@ -51,6 +90,7 @@ impl<'a> EditorTabViewer<'a> {
                 }
                 Action::Move(moved) => {
                     log_once::debug_once!("Moved: {:?}", moved);
+                    self.handle_asset_move(&mut cfg, &moved);
                 }
                 Action::Drag(dragged) => {
                     log_once::debug_once!("Dragged: {:?}", dragged);
@@ -80,12 +120,29 @@ impl<'a> EditorTabViewer<'a> {
         project_root: &Path,
     ) {
         let label = "euca://resources";
-        builder.node(Self::dir_node_labeled(label, "resources"));
         let resources_root = project_root.join("resources");
+        let root_info = AssetNodeInfo {
+            path: resources_root.clone(),
+            division: AssetDivision::Resources,
+            kind: AssetNodeKind::Resource(ResourceDivision::Folder),
+            is_dir: true,
+            is_division_root: true,
+            allow_add_folder: true,
+        };
+        Self::register_asset_node(cfg, label, root_info.clone());
+        let node_id = Self::asset_node_id(label);
+        let menu = Self::dir_node_kind(label, "resources", root_info.kind)
+            .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder"));
+        builder.node(menu);
         if resources_root.exists() {
             self.walk_resource_directory(cfg, builder, &resources_root, &resources_root);
         } else {
-            Self::add_placeholder_leaf(builder, "euca://resources/missing", "missing");
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://resources/missing",
+                "missing",
+                AssetNodeKind::Resource(ResourceDivision::File),
+            );
         }
         builder.close_dir();
     }
@@ -112,7 +169,19 @@ impl<'a> EditorTabViewer<'a> {
         for entry in entries {
             let full_label = Self::resource_label(base_path, &entry.path);
             if entry.is_dir {
-                builder.node(Self::dir_node_labeled(&full_label, &entry.name));
+                let dir_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Resources,
+                    kind: AssetNodeKind::Resource(ResourceDivision::Folder),
+                    is_dir: true,
+                    is_division_root: false,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &full_label, dir_info.clone());
+                let node_id = Self::asset_node_id(&full_label);
+                let menu = Self::dir_node_kind(&full_label, &entry.name, dir_info.kind)
+                    .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder"));
+                builder.node(menu);
                 self.walk_resource_directory(cfg, builder, base_path, &entry.path);
                 builder.close_dir();
             } else {
@@ -133,7 +202,20 @@ impl<'a> EditorTabViewer<'a> {
                 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| {
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Resources,
+                    kind: AssetNodeKind::Resource(ResourceDivision::File),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &full_label, file_info.clone());
+                let node_id = Self::asset_node_id(&full_label);
+                let menu = Self::leaf_node_kind(&full_label, &entry.name, file_info.kind).context_menu(|ui| {
+                        self.asset_file_context_menu(cfg, ui, node_id, &file_info);
+                        ui.separator();
+
                         if is_model {
                             if ui.button("Load to memory").clicked() {
                                 ui.close();
@@ -205,15 +287,33 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn build_scripts_branch(
-        _cfg: &mut StaticallyKept,
+        &mut self,
+        cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         project_root: &Path,
     ) {
         let label = "euca://scripts";
-        builder.node(Self::dir_node_labeled(label, "scripts"));
         let scripts_root = project_root.join("src");
+        let root_info = AssetNodeInfo {
+            path: scripts_root.clone(),
+            division: AssetDivision::Scripts,
+            kind: AssetNodeKind::Script(ScriptDivision::Package),
+            is_dir: true,
+            is_division_root: true,
+            allow_add_folder: false,
+        };
+        Self::register_asset_node(cfg, label, root_info.clone());
+        let node_id = Self::asset_node_id(label);
+        let menu = Self::dir_node_kind(label, "scripts", root_info.kind)
+            .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Package"));
+        builder.node(menu);
         if !scripts_root.exists() {
-            Self::add_placeholder_leaf(builder, "euca://scripts/missing", "missing");
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scripts/missing",
+                "missing",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
             builder.close_dir();
             return;
         }
@@ -235,26 +335,58 @@ impl<'a> EditorTabViewer<'a> {
         for entry in entries {
             if entry.is_dir {
                 let source_label = format!("{}/{}", label, entry.name);
-                builder.node(Self::dir_node_labeled(&source_label, &entry.name));
-                if Self::build_script_source_set(builder, &entry.path, &source_label) {
+                let kotlin_root = entry.path.join("kotlin");
+                let source_info = AssetNodeInfo {
+                    path: kotlin_root,
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Package),
+                    is_dir: true,
+                    is_division_root: true,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &source_label, source_info.clone());
+                let node_id = Self::asset_node_id(&source_label);
+                let menu = Self::dir_node_kind(&source_label, &entry.name, source_info.kind)
+                    .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &source_info, "New Package"));
+                builder.node(menu);
+                if self.build_script_source_set(cfg, builder, &entry.path, &source_label) {
                     had_content = true;
                 }
                 builder.close_dir();
             } else if !entry.name.eq_ignore_ascii_case("source.eucc") {
                 let file_label = format!("{}/{}", label, entry.name);
-                builder.node(Self::leaf_node_labeled(&file_label, &entry.name));
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_label, file_info.clone());
+                let node_id = Self::asset_node_id(&file_label);
+                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
                 had_content = true;
             }
         }
 
         if !had_content {
-            Self::add_placeholder_leaf(builder, "euca://scripts/empty", "empty");
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scripts/empty",
+                "empty",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
         }
 
         builder.close_dir();
     }
 
     fn build_script_source_set(
+        &mut self,
+        cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         source_path: &Path,
         source_label: &str,
@@ -271,6 +403,7 @@ impl<'a> EditorTabViewer<'a> {
                     builder,
                     &format!("{source_label}/unreadable"),
                     "unreadable",
+                    AssetNodeKind::Script(ScriptDivision::Script),
                 );
                 return true;
             }
@@ -280,25 +413,63 @@ impl<'a> EditorTabViewer<'a> {
         for entry in entries {
             if entry.is_dir {
                 if entry.name.eq_ignore_ascii_case("kotlin") {
-                    if Self::build_kotlin_tree(builder, &entry.path, source_label) {
+                    if self.build_kotlin_tree(cfg, builder, &entry.path, source_label) {
                         had_content = true;
                     }
                 } else {
                     let child_label = format!("{}/{}", source_label, entry.name);
-                    builder.node(Self::dir_node_labeled(&child_label, &entry.name));
-                    Self::build_plain_directory(builder, &entry.path, &child_label);
+                    let dir_info = AssetNodeInfo {
+                        path: entry.path.clone(),
+                        division: AssetDivision::Scripts,
+                        kind: AssetNodeKind::Script(ScriptDivision::Package),
+                        is_dir: true,
+                        is_division_root: false,
+                        allow_add_folder: true,
+                    };
+                    Self::register_asset_node(cfg, &child_label, dir_info.clone());
+                    let node_id = Self::asset_node_id(&child_label);
+                    let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
+                        .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package"));
+                    builder.node(menu);
+                    self.build_plain_directory(
+                        cfg,
+                        builder,
+                        &entry.path,
+                        &child_label,
+                        AssetDivision::Scripts,
+                        AssetNodeKind::Script(ScriptDivision::Package),
+                        AssetNodeKind::Script(ScriptDivision::Script),
+                        "New Package",
+                    );
                     builder.close_dir();
                     had_content = true;
                 }
             } else if !entry.name.eq_ignore_ascii_case("source.eucc") {
                 let file_label = format!("{}/{}", source_label, entry.name);
-                builder.node(Self::leaf_node_labeled(&file_label, &entry.name));
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_label, file_info.clone());
+                let node_id = Self::asset_node_id(&file_label);
+                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
                 had_content = true;
             }
         }
 
         if !had_content {
-            Self::add_placeholder_leaf(builder, &format!("{source_label}/empty"), "empty");
+            Self::add_placeholder_leaf(
+                builder,
+                &format!("{source_label}/empty"),
+                "empty",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
             had_content = true;
         }
 
@@ -306,9 +477,15 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn build_plain_directory(
+        &mut self,
+        cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         dir_path: &Path,
         parent_label: &str,
+        division: AssetDivision,
+        dir_kind: AssetNodeKind,
+        file_kind: AssetNodeKind,
+        new_folder_label: &str,
     ) {
         let entries = match Self::sorted_entries(dir_path) {
             Ok(entries) => entries,
@@ -322,29 +499,70 @@ impl<'a> EditorTabViewer<'a> {
                     builder,
                     &format!("{parent_label}/unreadable"),
                     "unreadable",
+                    file_kind,
                 );
                 return;
             }
         };
 
         if entries.is_empty() {
-            Self::add_placeholder_leaf(builder, &format!("{parent_label}/empty"), "empty");
+            Self::add_placeholder_leaf(
+                builder,
+                &format!("{parent_label}/empty"),
+                "empty",
+                file_kind,
+            );
             return;
         }
 
         for entry in entries {
             let child_label = format!("{}/{}", parent_label, entry.name);
             if entry.is_dir {
-                builder.node(Self::dir_node_labeled(&child_label, &entry.name));
-                Self::build_plain_directory(builder, &entry.path, &child_label);
+                let dir_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division,
+                    kind: dir_kind,
+                    is_dir: true,
+                    is_division_root: false,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &child_label, dir_info.clone());
+                let node_id = Self::asset_node_id(&child_label);
+                let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
+                    .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, new_folder_label));
+                builder.node(menu);
+                self.build_plain_directory(
+                    cfg,
+                    builder,
+                    &entry.path,
+                    &child_label,
+                    division,
+                    dir_kind,
+                    file_kind,
+                    new_folder_label,
+                );
                 builder.close_dir();
             } else {
-                builder.node(Self::leaf_node_labeled(&child_label, &entry.name));
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division,
+                    kind: file_kind,
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &child_label, file_info.clone());
+                let node_id = Self::asset_node_id(&child_label);
+                let menu = Self::leaf_node_kind(&child_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
             }
         }
     }
 
     fn build_kotlin_tree(
+        &mut self,
+        cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         kotlin_path: &Path,
         source_label: &str,
@@ -361,6 +579,7 @@ impl<'a> EditorTabViewer<'a> {
                     builder,
                     &format!("{source_label}/unreadable"),
                     "unreadable",
+                    AssetNodeKind::Script(ScriptDivision::Script),
                 );
                 return true;
             }
@@ -371,6 +590,7 @@ impl<'a> EditorTabViewer<'a> {
                 builder,
                 &format!("{source_label}/no_kotlin_files"),
                 "no kotlin files",
+                AssetNodeKind::Script(ScriptDivision::Script),
             );
             return true;
         }
@@ -378,7 +598,8 @@ impl<'a> EditorTabViewer<'a> {
         let mut had_entries = false;
         for entry in entries {
             if entry.is_dir {
-                Self::build_kotlin_package_collapsed(
+                self.build_kotlin_package_collapsed(
+                    cfg,
                     builder,
                     &entry.path,
                     source_label,
@@ -387,7 +608,19 @@ impl<'a> EditorTabViewer<'a> {
                 had_entries = true;
             } else {
                 let file_id = format!("{}/{}", source_label, entry.name);
-                builder.node(Self::leaf_node_labeled(&file_id, &entry.name));
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_id, file_info.clone());
+                let node_id = Self::asset_node_id(&file_id);
+                let menu = Self::leaf_node_kind(&file_id, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
                 had_entries = true;
             }
         }
@@ -396,6 +629,8 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn build_kotlin_package_collapsed(
+        &mut self,
+        cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         dir_path: &Path,
         parent_path_str: &str,
@@ -416,6 +651,7 @@ impl<'a> EditorTabViewer<'a> {
                     builder,
                     &format!("{full_path_str}/unreadable"),
                     "unreadable",
+                    AssetNodeKind::Script(ScriptDivision::Script),
                 );
                 return;
             }
@@ -428,20 +664,51 @@ impl<'a> EditorTabViewer<'a> {
             let subdir = subdirs[0];
             let mut new_parts = accumulated_parts;
             new_parts.push(subdir.name.clone());
-            Self::build_kotlin_package_collapsed(builder, &subdir.path, parent_path_str, new_parts);
+            self.build_kotlin_package_collapsed(
+                cfg,
+                builder,
+                &subdir.path,
+                parent_path_str,
+                new_parts,
+            );
         } else {
             let package_suffix = accumulated_parts.join(".");
             let full_path_str = format!("{}/{}", parent_path_str, package_suffix);
 
-            builder.node(Self::dir_node_labeled(&full_path_str, &package_suffix));
+            let dir_info = AssetNodeInfo {
+                path: dir_path.to_path_buf(),
+                division: AssetDivision::Scripts,
+                kind: AssetNodeKind::Script(ScriptDivision::Package),
+                is_dir: true,
+                is_division_root: false,
+                allow_add_folder: true,
+            };
+            Self::register_asset_node(cfg, &full_path_str, dir_info.clone());
+            let node_id = Self::asset_node_id(&full_path_str);
+            let menu = Self::dir_node_kind(&full_path_str, &package_suffix, dir_info.kind)
+                .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package"));
+            builder.node(menu);
 
             for file in files {
                 let file_id = format!("{}/{}", full_path_str, file.name);
-                builder.node(Self::leaf_node_labeled(&file_id, &file.name));
+                let file_info = AssetNodeInfo {
+                    path: file.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_id, file_info.clone());
+                let node_id = Self::asset_node_id(&file_id);
+                let menu = Self::leaf_node_kind(&file_id, &file.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
             }
 
             for subdir in subdirs {
-                Self::build_kotlin_package_collapsed(
+                self.build_kotlin_package_collapsed(
+                    cfg,
                     builder,
                     &subdir.path,
                     &full_path_str,
@@ -454,15 +721,33 @@ impl<'a> EditorTabViewer<'a> {
     }
 
     fn build_scene_branch(
-        _cfg: &mut StaticallyKept,
+        &mut self,
+        cfg: &mut StaticallyKept,
         builder: &mut TreeViewBuilder<u64>,
         project_root: &Path,
     ) {
         let label = "euca://scenes";
-        builder.node(Self::dir_node_labeled(label, "scenes"));
         let scenes_root = project_root.join("scenes");
+        let root_info = AssetNodeInfo {
+            path: scenes_root.clone(),
+            division: AssetDivision::Scenes,
+            kind: AssetNodeKind::Scene(SceneDivision::Folder),
+            is_dir: true,
+            is_division_root: true,
+            allow_add_folder: true,
+        };
+        Self::register_asset_node(cfg, label, root_info.clone());
+        let node_id = Self::asset_node_id(label);
+        let menu = Self::dir_node_kind(label, "scenes", root_info.kind)
+            .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder"));
+        builder.node(menu);
         if !scenes_root.exists() {
-            Self::add_placeholder_leaf(builder, "euca://scenes/missing", "missing");
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scenes/missing",
+                "missing",
+                AssetNodeKind::Scene(SceneDivision::Scene),
+            );
             builder.close_dir();
             return;
         }
@@ -475,7 +760,12 @@ impl<'a> EditorTabViewer<'a> {
                     scenes_root.display(),
                     err
                 );
-                Self::add_placeholder_leaf(builder, "euca://scenes/unreadable", "unreadable");
+                Self::add_placeholder_leaf(
+                    builder,
+                    "euca://scenes/unreadable",
+                    "unreadable",
+                    AssetNodeKind::Scene(SceneDivision::Scene),
+                );
                 builder.close_dir();
                 return;
             }
@@ -485,8 +775,29 @@ impl<'a> EditorTabViewer<'a> {
         for entry in entries {
             if entry.is_dir {
                 let child_label = format!("{}/{}", label, entry.name);
-                builder.node(Self::dir_node_labeled(&child_label, &entry.name));
-                Self::build_plain_directory(builder, &entry.path, &child_label);
+                let dir_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scenes,
+                    kind: AssetNodeKind::Scene(SceneDivision::Folder),
+                    is_dir: true,
+                    is_division_root: false,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &child_label, dir_info.clone());
+                let node_id = Self::asset_node_id(&child_label);
+                let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
+                    .context_menu(|ui| self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder"));
+                builder.node(menu);
+                self.build_plain_directory(
+                    cfg,
+                    builder,
+                    &entry.path,
+                    &child_label,
+                    AssetDivision::Scenes,
+                    AssetNodeKind::Scene(SceneDivision::Folder),
+                    AssetNodeKind::Scene(SceneDivision::Scene),
+                    "New Folder",
+                );
                 builder.close_dir();
                 had_entries = true;
             } else if entry
@@ -497,13 +808,30 @@ impl<'a> EditorTabViewer<'a> {
                 .unwrap_or(false)
             {
                 let file_label = format!("{}/{}", label, entry.name);
-                builder.node(Self::leaf_node_labeled(&file_label, &entry.name));
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scenes,
+                    kind: AssetNodeKind::Scene(SceneDivision::Scene),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_label, file_info.clone());
+                let node_id = Self::asset_node_id(&file_label);
+                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
                 had_entries = true;
             }
         }
 
         if !had_entries {
-            Self::add_placeholder_leaf(builder, "euca://scenes/no_scenes", "no scenes");
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scenes/no_scenes",
+                "no scenes",
+                AssetNodeKind::Scene(SceneDivision::Scene),
+            );
         }
 
         builder.close_dir();
@@ -541,33 +869,320 @@ impl<'a> EditorTabViewer<'a> {
         id
     }
 
+    fn register_asset_node(
+        cfg: &mut StaticallyKept,
+        id_source: &str,
+        info: AssetNodeInfo,
+    ) -> u64 {
+        let node_id = Self::asset_node_id(id_source);
+        cfg.asset_node_info.insert(node_id, info);
+        node_id
+    }
+
     fn dir_node<'ui>(label: &str) -> NodeBuilder<'ui, u64> {
-        Self::with_icon(NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()))
+        Self::with_icon_kind(
+            NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()),
+            AssetNodeKind::Resource(ResourceDivision::Folder),
+        )
     }
 
-    fn dir_node_labeled<'ui>(id_source: &str, label: &str) -> NodeBuilder<'ui, u64> {
-        Self::with_icon(NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string()))
+    fn dir_node_kind<'ui>(id_source: &str, label: &str, kind: AssetNodeKind) -> NodeBuilder<'ui, u64> {
+        Self::with_icon_kind(
+            NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string()),
+            kind,
+        )
     }
 
-    fn leaf_node_labeled<'ui>(id_source: &str, label: &str) -> NodeBuilder<'ui, u64> {
-        Self::with_icon(NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string()))
+    fn leaf_node_kind<'ui>(id_source: &str, label: &str, kind: AssetNodeKind) -> NodeBuilder<'ui, u64> {
+        Self::with_icon_kind(
+            NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string()),
+            kind,
+        )
     }
 
-    fn with_icon<'ui>(builder: NodeBuilder<'ui, u64>) -> NodeBuilder<'ui, u64> {
-        builder.icon(|ui| {
+    fn with_icon_kind<'ui>(builder: NodeBuilder<'ui, u64>, kind: AssetNodeKind) -> NodeBuilder<'ui, u64> {
+        builder.icon(move |ui| {
             egui_extras::install_image_loaders(ui.ctx());
-            Self::draw_asset_icon(ui)
+            Self::draw_asset_icon(ui, kind)
         })
     }
 
-    fn draw_asset_icon(ui: &mut egui::Ui) {
+    fn draw_asset_icon(ui: &mut egui::Ui, _kind: AssetNodeKind) {
         let image = egui::Image::from_bytes("bytes://asset-viewer-icon", NO_TEXTURE)
             .max_size(egui::vec2(14.0, 14.0));
         ui.add(image);
     }
 
-    fn add_placeholder_leaf(builder: &mut TreeViewBuilder<u64>, id_source: &str, label: &str) {
-        builder.node(Self::leaf_node_labeled(id_source, label));
+    fn add_placeholder_leaf(
+        builder: &mut TreeViewBuilder<u64>,
+        id_source: &str,
+        label: &str,
+        kind: AssetNodeKind,
+    ) {
+        builder.node(Self::leaf_node_kind(id_source, label, kind));
+    }
+
+    fn asset_dir_context_menu(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        ui: &mut egui::Ui,
+        node_id: u64,
+        info: &AssetNodeInfo,
+        new_folder_label: &str,
+    ) {
+        if info.allow_add_folder {
+            if ui.button(new_folder_label).clicked() {
+                ui.close();
+                let base_name = if info.division == AssetDivision::Scripts {
+                    "newpackage"
+                } else {
+                    "New Folder"
+                };
+                self.create_asset_folder(&info.path, base_name);
+            }
+        }
+
+        if ui.button("Paste").clicked() {
+            ui.close();
+            if !info.path.exists() {
+                if let Err(err) = fs::create_dir_all(&info.path) {
+                    warn!("Unable to create folder '{}': {}", info.path.display(), err);
+                    return;
+                }
+            }
+            *self.signal = Signal::AssetPaste {
+                target_dir: info.path.clone(),
+                division: info.division,
+            };
+        }
+
+        if ui.button("Reveal Folder").clicked() {
+            ui.close();
+            if let Err(err) = open::that(&info.path) {
+                warn!("Unable to reveal folder: {}", err);
+            }
+        }
+
+        if !info.is_division_root && ui.button("Rename").clicked() {
+            let current_name = info
+                .path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .unwrap_or("")
+                .to_string();
+            cfg.asset_rename = Some(crate::editor::AssetRenameState {
+                node_id,
+                original_path: info.path.clone(),
+                buffer: current_name,
+                just_started: true,
+            });
+        }
+
+        if !info.is_division_root && ui.button("Delete").clicked() {
+            ui.close();
+            self.delete_asset_entry(info);
+        }
+    }
+
+    fn asset_file_context_menu(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        ui: &mut egui::Ui,
+        node_id: u64,
+        info: &AssetNodeInfo,
+    ) {
+        if matches!(info.kind, AssetNodeKind::Script(ScriptDivision::Script))
+            && ui.button("Open Script").clicked()
+        {
+            ui.close();
+            if let Err(err) = open::that(&info.path) {
+                warn!("Unable to open script: {}", err);
+            }
+        }
+
+        if ui.button("Rename").clicked() {
+            let current_name = info
+                .path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .unwrap_or("")
+                .to_string();
+            cfg.asset_rename = Some(crate::editor::AssetRenameState {
+                node_id,
+                original_path: info.path.clone(),
+                buffer: current_name,
+                just_started: true,
+            });
+        }
+
+        if ui.button("Copy").clicked() {
+            ui.close();
+            *self.signal = Signal::AssetCopy {
+                source: info.path.clone(),
+                division: info.division,
+            };
+        }
+
+        if ui.button("Delete").clicked() {
+            ui.close();
+            self.delete_asset_entry(info);
+        }
+    }
+
+    fn apply_asset_rename(&self, rename: &crate::editor::AssetRenameState, is_dir: bool) {
+        let trimmed = rename.buffer.trim();
+        if trimmed.is_empty() {
+            warn!("Rename cancelled: empty name");
+            return;
+        }
+
+        let Some(parent) = rename.original_path.parent() else {
+            warn!("Unable to rename: missing parent directory");
+            return;
+        };
+
+        let mut new_name = trimmed.to_string();
+        if !is_dir {
+            if let Some(ext) = rename.original_path.extension().and_then(|e| e.to_str()) {
+                let has_ext = Path::new(&new_name)
+                    .extension()
+                    .and_then(|e| e.to_str())
+                    .is_some();
+                if !has_ext {
+                    new_name = format!("{new_name}.{ext}");
+                }
+            }
+        }
+
+        let target_path = parent.join(&new_name);
+        if target_path == rename.original_path {
+            return;
+        }
+
+        if target_path.exists() {
+            warn!("Rename target already exists: {}", target_path.display());
+            return;
+        }
+
+        if let Err(err) = fs::rename(&rename.original_path, &target_path) {
+            warn!("Failed to rename '{}': {}", rename.original_path.display(), err);
+        } else {
+            info!("Renamed to {}", target_path.display());
+        }
+    }
+
+    fn delete_asset_entry(&self, info: &AssetNodeInfo) {
+        if info.is_division_root {
+            warn!("Cannot delete division root");
+            return;
+        }
+
+        let result = if info.is_dir {
+            fs::remove_dir_all(&info.path)
+        } else {
+            fs::remove_file(&info.path)
+        };
+
+        if let Err(err) = result {
+            warn!("Failed to delete '{}': {}", info.path.display(), err);
+        } else {
+            info!("Deleted {}", info.path.display());
+        }
+    }
+
+    fn create_asset_folder(&self, base_dir: &Path, base_name: &str) {
+        if let Err(err) = fs::create_dir_all(base_dir) {
+            warn!("Unable to create folder '{}': {}", base_dir.display(), err);
+            return;
+        }
+
+        let mut candidate = base_dir.join(base_name);
+        if candidate.exists() {
+            let mut index = 1;
+            loop {
+                let suffix = if base_name.contains(' ') {
+                    format!(" {}", index)
+                } else {
+                    format!("{}", index)
+                };
+                candidate = base_dir.join(format!("{base_name}{suffix}"));
+                if !candidate.exists() {
+                    break;
+                }
+                index += 1;
+            }
+        }
+
+        if let Err(err) = fs::create_dir_all(&candidate) {
+            warn!("Unable to create folder '{}': {}", candidate.display(), err);
+        } else {
+            info!("Created folder {}", candidate.display());
+        }
+    }
+
+    fn handle_asset_move(&mut self, cfg: &mut StaticallyKept, drag: &egui_ltreeview::DragAndDrop<u64>) {
+        let Some(&source_id) = drag.source.first() else {
+            return;
+        };
+        let Some(source_info) = cfg.asset_node_info.get(&source_id).cloned() else {
+            return;
+        };
+        let Some(target_info) = cfg.asset_node_info.get(&drag.target).cloned() else {
+            return;
+        };
+
+        if source_info.is_division_root {
+            warn!("Cannot move division root");
+            return;
+        }
+
+        if source_info.division != target_info.division {
+            warn!("Cannot move assets across divisions");
+            return;
+        }
+
+        let target_dir = if target_info.is_dir {
+            target_info.path.clone()
+        } else {
+            target_info
+                .path
+                .parent()
+                .unwrap_or(&target_info.path)
+                .to_path_buf()
+        };
+
+        if !target_dir.exists() {
+            if let Err(err) = fs::create_dir_all(&target_dir) {
+                warn!("Target directory does not exist: {}", err);
+                return;
+            }
+        }
+
+        if source_info.is_dir && target_dir.starts_with(&source_info.path) {
+            warn!("Cannot move a folder into itself");
+            return;
+        }
+
+        let Some(name) = source_info.path.file_name() else {
+            warn!("Unable to move: invalid file name");
+            return;
+        };
+
+        let target_path = target_dir.join(name);
+        if target_path == source_info.path {
+            return;
+        }
+
+        if target_path.exists() {
+            warn!("Target already exists: {}", target_path.display());
+            return;
+        }
+
+        if let Err(err) = fs::rename(&source_info.path, &target_path) {
+            warn!("Failed to move '{}': {}", source_info.path.display(), err);
+        } else {
+            info!("Moved to {}", target_path.display());
+        }
     }
 
 
@@ -604,19 +1219,18 @@ impl<'a> EditorTabViewer<'a> {
             let handle = Model::load_from_memory_raw(
                 graphics.clone(),
                 buffer,
+                Some(reference.clone()),
                 None,
                 ASSET_REGISTRY.clone(),
             )
             .await?;
 
             let mut registry = ASSET_REGISTRY.write();
-            if let Some(model) = registry.get_model(handle).cloned() {
-                let mut model = model;
+            if let Some(model) = registry.get_model_mut(handle) {
                 model.path = reference.clone();
                 model.label = label.clone();
-                registry.update_model(handle, model);
-                registry.label_model(label.clone(), handle);
             }
+            registry.label_model(label.clone(), handle);
 
             Ok::<(), anyhow::Error>(())
         });
diff --git a/crates/eucalyptus-editor/src/editor/component.rs b/crates/eucalyptus-editor/src/editor/component.rs
index 7dbea57..42a2764 100644
--- a/crates/eucalyptus-editor/src/editor/component.rs
+++ b/crates/eucalyptus-editor/src/editor/component.rs
@@ -926,6 +926,9 @@ impl InspectableComponent for MeshRenderer {
                                 if i.path.as_uri().is_none() {
                                     continue;
                                 }
+                                if i.label.eq_ignore_ascii_case("light cube") {
+                                    continue;
+                                }
 
                                 let is_selected = self.asset_handle() == *i.key();
                                 if ui
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index 221f15c..6574818 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -114,6 +114,8 @@ pub struct StaticallyKept {
 
     pub(crate) dragged_asset: Option<DraggedAsset>,
     pub(crate) asset_node_assets: HashMap<u64, DraggedAsset>,
+    pub(crate) asset_node_info: HashMap<u64, AssetNodeInfo>,
+    pub(crate) asset_rename: Option<AssetRenameState>,
 
     pub(crate) component_node_ids: HashMap<ComponentNodeKey, u64>,
     pub(crate) component_node_lookup: HashMap<u64, ComponentNodeKey>,
@@ -302,6 +304,56 @@ pub(crate) struct FsEntry {
     pub(crate) is_dir: bool,
 }
 
+#[derive(Clone, Debug)]
+pub(crate) struct AssetRenameState {
+    pub(crate) node_id: u64,
+    pub(crate) original_path: PathBuf,
+    pub(crate) buffer: String,
+    pub(crate) just_started: bool,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AssetDivision {
+    Resources,
+    Scripts,
+    Scenes,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ResourceDivision {
+    File,
+    Folder,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ScriptDivision {
+    Package,
+    Script,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SceneDivision {
+    Scene,
+    Folder,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AssetNodeKind {
+    Resource(ResourceDivision),
+    Script(ScriptDivision),
+    Scene(SceneDivision),
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct AssetNodeInfo {
+    pub(crate) path: PathBuf,
+    pub(crate) division: AssetDivision,
+    pub(crate) kind: AssetNodeKind,
+    pub(crate) is_dir: bool,
+    pub(crate) is_division_root: bool,
+    pub(crate) allow_add_folder: bool,
+}
+
 #[derive(Debug, Clone, Copy)]
 pub enum EditorTabMenuAction {
     ImportResource,
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 7171f17..7c5e60c 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -84,6 +84,7 @@ pub struct Editor {
     pub texture_id: Option<egui::TextureId>,
     pub size: Extent3d,
     pub instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>,
+    pub animated_instance_buffer: Option<ResizableBuffer<InstanceRaw>>,
     pub collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>,
     pub collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>,
     pub color: Color,
@@ -177,6 +178,8 @@ pub struct Editor {
     pub(crate) play_mode_process: Option<std::process::Child>,
     pub(crate) play_mode_pid: Option<u32>,
     pub(crate) play_mode_exit_rx: Option<std::sync::mpsc::Receiver<()>>,
+
+    pub(crate) asset_clipboard: Option<AssetClipboard>,
 }
 
 impl Editor {
@@ -275,8 +278,10 @@ impl Editor {
             play_mode_process: None,
             play_mode_pid: None,
             play_mode_exit_rx: None,
+            asset_clipboard: None,
             collider_wireframe_pipeline: None,
             instance_buffer_cache: HashMap::new(),
+            animated_instance_buffer: None,
             collider_wireframe_geometry_cache: HashMap::new(),
             collider_instance_buffer: None,
             mipmapper: None,
@@ -1521,6 +1526,14 @@ pub enum Signal {
     None,
     Copy(SceneEntity),
     Paste(SceneEntity),
+    AssetCopy {
+        source: PathBuf,
+        division: AssetDivision,
+    },
+    AssetPaste {
+        target_dir: PathBuf,
+        division: AssetDivision,
+    },
     Delete,
     Undo,
     Play,
@@ -1531,6 +1544,12 @@ pub enum Signal {
     UpdateViewportSize((f32, f32)),
 }
 
+#[derive(Clone, Debug)]
+pub struct AssetClipboard {
+    pub source: PathBuf,
+    pub division: AssetDivision,
+}
+
 #[derive(Debug)]
 pub enum EditorState {
     Editing,
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 09cb0d2..94b7231 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -12,7 +12,7 @@ use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
 use dropbear_engine::graphics::{CommandEncoder, InstanceRaw};
 use dropbear_engine::{
     entity::{EntityTransform, MeshRenderer, Transform},
-    lighting::{Light, LightComponent, MAX_LIGHTS},
+    lighting::{Light, MAX_LIGHTS},
     model::{DrawLight, DrawModel},
     scene::{Scene, SceneCommand},
 };
@@ -191,7 +191,6 @@ impl Scene for Editor {
                     self.world.spawn((
                         label_component,
                         l,
-                        LightComponent::default(),
                         Transform::default(),
                         CustomProperties::default(),
                     ));
@@ -346,11 +345,15 @@ impl Scene for Editor {
             });
         }
 
+        if let Some(light_pipeline) = &mut self.light_cube_pipeline {
+            light_pipeline.update(graphics.clone(), &self.world);
+        }
+
         let lights = {
             let mut lights = Vec::new();
-            let mut query = self.world.query::<(&Light, &LightComponent)>();
-            for (light, comp) in query.iter() {
-                lights.push((light.clone(), comp.clone()));
+            let mut query = self.world.query::<&Light>();
+            for light in query.iter() {
+                lights.push(light.clone());
             }
             lights
         };
@@ -358,8 +361,7 @@ impl Scene for Editor {
             if let Some(globals) = &mut self.shader_globals {
                 let enabled_count = lights
                     .iter()
-                    .filter(|(_, comp)| comp.enabled)
-                    .take(MAX_LIGHTS)
+                    .filter(|light| light.component.enabled)
                     .count() as u32;
                 globals.set_num_lights(enabled_count);
                 globals.write(&graphics.queue);
@@ -391,23 +393,27 @@ impl Scene for Editor {
         let registry = ASSET_REGISTRY.read();
         let mut prepared_models = Vec::new();
         for (handle, instances) in static_batches {
-            let Some(model) = registry.get_model(Handle::new(handle)).cloned() else {
+            let Some(model) = registry.get_model(Handle::new(handle)) else {
                 log_once::error_once!("Missing model handle {} in registry", handle);
                 continue;
             };
 
-            let instance_buffer = graphics.device.create_buffer_init(
-                &wgpu::util::BufferInitDescriptor {
-                    label: Some("Runtime Instance Buffer"),
-                    contents: bytemuck::cast_slice(&instances),
-                    usage: wgpu::BufferUsages::VERTEX,
-                },
-            );
+            let instance_buffer = self
+                .instance_buffer_cache
+                .entry(handle)
+                .or_insert_with(|| {
+                    ResizableBuffer::new(
+                        &graphics.device,
+                        instances.len().max(1),
+                        wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                        "Runtime Instance Buffer",
+                    )
+                });
+            instance_buffer.write(&graphics.device, &graphics.queue, &instances);
 
-            prepared_models.push((model, instance_buffer, instances.len() as u32));
+            prepared_models.push((model, handle, instances.len() as u32));
         }
 
-        let registry = ASSET_REGISTRY.read();
         {
             let mut render_pass = encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -434,9 +440,9 @@ impl Scene for Editor {
                 });
             if let Some(light_pipeline) = &self.light_cube_pipeline {
                 render_pass.set_pipeline(light_pipeline.pipeline());
-                for (light, component) in &lights {
+                for light in &lights {
                     render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
-                    if !component.visible {
+                    if !light.component.visible {
                         continue;
                     }
 
@@ -485,7 +491,7 @@ impl Scene for Editor {
 
         // model rendering
         if let Some(lcp) = &self.light_cube_pipeline {
-            for (model, instance_buffer, instance_count) in prepared_models {
+            for (model, handle, instance_count) in prepared_models {
                 let globals_bind_group = &self
                     .shader_globals
                     .as_ref()
@@ -516,7 +522,14 @@ impl Scene for Editor {
                         timestamp_writes: None,
                     });
                 render_pass.set_pipeline(pipeline.pipeline());
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
+                    render_pass.set_vertex_buffer(
+                        1,
+                        instance_buffer.slice(instance_count as usize),
+                    );
+                } else {
+                    continue;
+                }
                 render_pass.set_bind_group(3, globals_bind_group, &[]);
 
                 for mesh in &model.meshes {
@@ -541,18 +554,22 @@ impl Scene for Editor {
                 .bind_group;
 
             for (handle, instance, skin_bind_group) in animated_instances {
-                let Some(model) = registry.get_model(Handle::new(handle)).cloned() else {
+                let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
                 };
 
-                let instance_buffer = graphics.device.create_buffer_init(
-                    &wgpu::util::BufferInitDescriptor {
-                        label: Some("Runtime Animated Instance Buffer"),
-                        contents: bytemuck::cast_slice(&[instance]),
-                        usage: wgpu::BufferUsages::VERTEX,
-                    },
-                );
+                let instance_buffer = self
+                    .animated_instance_buffer
+                    .get_or_insert_with(|| {
+                        ResizableBuffer::new(
+                            &graphics.device,
+                            1,
+                            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                            "Runtime Animated Instance Buffer",
+                        )
+                    });
+                instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
 
                 let mut render_pass = encoder
                     .begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -579,7 +596,7 @@ impl Scene for Editor {
                     });
 
                 render_pass.set_pipeline(pipeline.pipeline());
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
                 render_pass.set_bind_group(3, globals_bind_group, &[]);
 
                 for mesh in &model.meshes {
diff --git a/crates/eucalyptus-editor/src/editor/viewport.rs b/crates/eucalyptus-editor/src/editor/viewport.rs
index d8b9801..46fa78f 100644
--- a/crates/eucalyptus-editor/src/editor/viewport.rs
+++ b/crates/eucalyptus-editor/src/editor/viewport.rs
@@ -1,7 +1,7 @@
 use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation};
 use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, Transform};
-use dropbear_engine::lighting::LightComponent;
+use dropbear_engine::lighting::{Light};
 use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::utils::ViewportMode;
 use crate::editor::{EditorTabViewer, Signal, TABS_GLOBAL, UndoableAction};
@@ -226,10 +226,10 @@ impl<'a> EditorTabViewer<'a> {
             }
 
             if let Some(updated_transform) = updated_light_transform {
-                if let Ok(mut light_component) = self.world.get::<&mut LightComponent>(*entity_id) {
+                if let Ok(mut light) = self.world.get::<&mut Light>(*entity_id) {
                     let forward = DVec3::new(0.0, 0.0, -1.0);
-                    light_component.position = updated_transform.position;
-                    light_component.direction = (updated_transform.rotation * forward).normalize_or_zero();
+                    light.component.position = updated_transform.position;
+                    light.component.direction = (updated_transform.rotation * forward).normalize_or_zero();
                 }
             }
         }
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 100fbaa..0bb0573 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -1,4 +1,4 @@
-use crate::editor::{Editor, EditorState, Signal};
+use crate::editor::{AssetClipboard, Editor, EditorState, Signal};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::utils::{
     relative_path_from_euca, EUCA_SCHEME, 
@@ -9,6 +9,7 @@ use eucalyptus_core::camera::{CameraComponent, CameraType};
 use eucalyptus_core::scripting::{build_jvm, BuildStatus};
 use eucalyptus_core::states::{EditorTab, PROJECT};
 use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console};
+use std::fs;
 use std::path::PathBuf;
 use std::sync::Arc;
 use winit::keyboard::KeyCode;
@@ -44,6 +45,64 @@ impl SignalController for Editor {
                 Ok::<(), anyhow::Error>(())
             }
             Signal::Copy(_) => Ok(()),
+            Signal::AssetCopy { source, division } => {
+                self.asset_clipboard = Some(AssetClipboard {
+                    source: source.clone(),
+                    division: *division,
+                });
+                self.signal = Signal::None;
+                Ok(())
+            }
+            Signal::AssetPaste { target_dir, division } => {
+                let clipboard = self.asset_clipboard.clone();
+                if clipboard.is_none() {
+                    warn!("Nothing copied to paste");
+                    self.signal = Signal::None;
+                    return Ok(());
+                }
+
+                let clipboard = clipboard.unwrap();
+                if clipboard.division != *division {
+                    warn!("Cannot paste across different asset divisions");
+                    self.signal = Signal::None;
+                    return Ok(());
+                }
+
+                if !clipboard.source.is_file() {
+                    warn!("Copied asset is not a file");
+                    self.signal = Signal::None;
+                    return Ok(());
+                }
+
+                if !target_dir.exists() {
+                    warn!("Target directory does not exist");
+                    self.signal = Signal::None;
+                    return Ok(());
+                }
+
+                let Some(file_name) = clipboard.source.file_name() else {
+                    warn!("Unable to paste: invalid file name");
+                    self.signal = Signal::None;
+                    return Ok(());
+                };
+
+                let target_path = target_dir.join(file_name);
+                if target_path.exists() {
+                    warn!("Target already exists: {}", target_path.display());
+                    self.signal = Signal::None;
+                    return Ok(());
+                }
+
+                if let Err(err) = fs::copy(&clipboard.source, &target_path) {
+                    warn!("Unable to paste file: {}", err);
+                    self.signal = Signal::None;
+                    return Ok(());
+                }
+
+                info!("Pasted asset to {}", target_path.display());
+                self.signal = Signal::None;
+                Ok(())
+            }
             Signal::Paste(scene_entity) => {
                 let mut scene_entity = scene_entity.clone();
                 scene_entity.label = Editor::unique_label_for_world(
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 5d128eb..ad46d52 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -14,7 +14,7 @@ use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use futures::executor;
 use hecs::{Entity, World};
 use dropbear_engine::future::{FutureHandle, FutureQueue};
-use dropbear_engine::graphics::SharedGraphicsContext;
+use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext};
 use dropbear_engine::scene::SceneCommand;
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
@@ -122,6 +122,8 @@ pub struct PlayMode {
     light_cube_pipeline: Option<LightCubePipeline>,
     main_pipeline: Option<MainRenderPipeline>,
     shader_globals: Option<GlobalsUniform>,
+    instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>,
+    animated_instance_buffer: Option<ResizableBuffer<InstanceRaw>>,
     collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     sky_pipeline: Option<SkyPipeline>,
     default_skinning_buffer: Option<wgpu::Buffer>,
@@ -187,6 +189,8 @@ impl PlayMode {
             main_pipeline: None,
             light_cube_pipeline: None,
             shader_globals: None,
+            instance_buffer_cache: HashMap::new(),
+            animated_instance_buffer: None,
             scripts_ready: false,
             has_initial_resize_done: false,
             physics_pipeline: Default::default(),
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 7b3a49f..dcd141a 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -9,7 +9,6 @@ use eucalyptus_core::physics::collider::ColliderShapeKey;
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use glam::{vec2, DMat4, DQuat, DVec3, Mat4, Quat, Vec2};
 use hecs::Entity;
-use wgpu::Color;
 use wgpu::util::DeviceExt;
 use winit::event_loop::ActiveEventLoop;
 use winit::event::WindowEvent;
@@ -19,9 +18,9 @@ use dropbear_engine::camera::Camera;
 use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
 use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext};
-use dropbear_engine::lighting::{Light, LightComponent};
+use dropbear_engine::lighting::{Light};
 use dropbear_engine::lighting::MAX_LIGHTS;
-use dropbear_engine::model::{DrawLight, DrawModel, Model};
+use dropbear_engine::model::{DrawLight, DrawModel};
 use dropbear_engine::scene::{Scene, SceneCommand};
 use eucalyptus_core::command::CommandBufferPoller;
 use eucalyptus_core::hierarchy::{EntityTransformExt, Parent};
@@ -33,7 +32,7 @@ use eucalyptus_core::states::SCENES;
 use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER};
 use crate::PlayMode;
 use eucalyptus_core::physics::collider::shader::create_wireframe_geometry;
-use kino_ui::widgets::{Anchor, Border, Fill};
+use kino_ui::widgets::{Border, Fill};
 use kino_ui::widgets::rect::Rectangle;
 
 impl Scene for PlayMode {
@@ -619,11 +618,15 @@ impl Scene for PlayMode {
             });
         }
 
+        if let Some(light_pipeline) = &mut self.light_cube_pipeline {
+            light_pipeline.update(graphics.clone(), &self.world);
+        }
+
         let lights = {
             let mut lights = Vec::new();
-            let mut query = self.world.query::<(&Light, &LightComponent)>();
-            for (light, comp) in query.iter() {
-                lights.push((light.clone(), comp.clone()));
+            let mut query = self.world.query::<&Light>();
+            for light in query.iter() {
+                lights.push(light.clone());
             }
             lights
         };
@@ -631,8 +634,7 @@ impl Scene for PlayMode {
             if let Some(globals) = &mut self.shader_globals {
                 let enabled_count = lights
                     .iter()
-                    .filter(|(_, comp)| comp.enabled)
-                    .take(MAX_LIGHTS)
+                    .filter(|light| light.component.enabled)
                     .count() as u32;
                 globals.set_num_lights(enabled_count);
                 globals.write(&graphics.queue);
@@ -664,31 +666,25 @@ impl Scene for PlayMode {
         let registry = ASSET_REGISTRY.read();
         let mut prepared_models = Vec::new();
         for (handle, instances) in static_batches {
-            let Some(model) = registry.get_model(Handle::new(handle)).cloned() else {
+            let Some(model) = registry.get_model(Handle::new(handle)) else {
                 log_once::error_once!("Missing model handle {} in registry", handle);
                 continue;
             };
 
-            let instance_buffer = graphics.device.create_buffer_init(
-                &wgpu::util::BufferInitDescriptor {
-                    label: Some("Runtime Instance Buffer"),
-                    contents: bytemuck::cast_slice(&instances),
-                    usage: wgpu::BufferUsages::VERTEX,
-                },
-            );
-
-            prepared_models.push((model, instance_buffer, instances.len() as u32));
-        }
-
-        {
-            let mut query = self.world.query::<(
-                &mut LightComponent,
-                &mut Light,
-            )>();
+            let instance_buffer = self
+                .instance_buffer_cache
+                .entry(handle)
+                .or_insert_with(|| {
+                    ResizableBuffer::new(
+                        &graphics.device,
+                        instances.len().max(1),
+                        wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                        "Runtime Instance Buffer",
+                    )
+                });
+            instance_buffer.write(&graphics.device, &graphics.queue, &instances);
 
-            for (light_component, light) in query.iter() {
-                light.update(graphics.as_ref(), light_component);
-            }
+            prepared_models.push((model, handle, instances.len() as u32));
         }
 
         let registry = ASSET_REGISTRY.read();
@@ -718,9 +714,9 @@ impl Scene for PlayMode {
                 });
             if let Some(light_pipeline) = &self.light_cube_pipeline {
                 render_pass.set_pipeline(light_pipeline.pipeline());
-                for (light, component) in &lights {
+                for light in &lights {
                     render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
-                    if !component.visible {
+                    if !light.component.visible {
                         continue;
                     }
 
@@ -769,7 +765,7 @@ impl Scene for PlayMode {
 
         // model rendering
         if let Some(lcp) = &self.light_cube_pipeline {
-            for (model, instance_buffer, instance_count) in prepared_models {
+            for (model, handle, instance_count) in prepared_models {
                 let globals_bind_group = &self
                     .shader_globals
                     .as_ref()
@@ -800,7 +796,14 @@ impl Scene for PlayMode {
                         timestamp_writes: None,
                     });
                 render_pass.set_pipeline(pipeline.pipeline());
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
+                    render_pass.set_vertex_buffer(
+                        1,
+                        instance_buffer.slice(instance_count as usize),
+                    );
+                } else {
+                    continue;
+                }
                 render_pass.set_bind_group(3, globals_bind_group, &[]);
 
                 for mesh in &model.meshes {
@@ -825,18 +828,22 @@ impl Scene for PlayMode {
                 .bind_group;
 
             for (handle, instance, skin_bind_group) in animated_instances {
-                let Some(model) = registry.get_model(Handle::new(handle)).cloned() else {
+                let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
                 };
 
-                let instance_buffer = graphics.device.create_buffer_init(
-                    &wgpu::util::BufferInitDescriptor {
-                        label: Some("Runtime Animated Instance Buffer"),
-                        contents: bytemuck::cast_slice(&[instance]),
-                        usage: wgpu::BufferUsages::VERTEX,
-                    },
-                );
+                let instance_buffer = self
+                    .animated_instance_buffer
+                    .get_or_insert_with(|| {
+                        ResizableBuffer::new(
+                            &graphics.device,
+                            1,
+                            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                            "Runtime Animated Instance Buffer",
+                        )
+                    });
+                instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
 
                 let mut render_pass = encoder
                     .begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -863,7 +870,7 @@ impl Scene for PlayMode {
                     });
 
                 render_pass.set_pipeline(pipeline.pipeline());
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
                 render_pass.set_bind_group(3, globals_bind_group, &[]);
 
                 for mesh in &model.meshes {