kitgit

tirbofish/dropbear · diff

66651ff · Thribhu K

feature: improved lighting feature: changed to uuid-based id system for assets

Unverified

diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index 7fab0c5..f76d848 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -48,6 +48,7 @@ image = {workspace = true, features = ["serde"]}
 puffin.workspace = true
 bitflags.workspace = true
 puffin_http.workspace = true
+uuid.workspace = true
 float-derive.workspace = true
 rkyv.workspace = true
 bevy_mikktspace.workspace = true
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index bdaf899..d05d814 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -79,7 +79,7 @@ impl Default for LightArrayUniform {
         Self {
             lights: [LightUniform::default(); MAX_LIGHTS],
             light_count: 0,
-            ambient_strength: 0.1,
+            ambient_strength: 0.5,
             _padding: [0; 2],
         }
     }
diff --git a/crates/dropbear-engine/src/pipelines/hdr.rs b/crates/dropbear-engine/src/pipelines/hdr.rs
index 4257e36..bfab1c1 100644
--- a/crates/dropbear-engine/src/pipelines/hdr.rs
+++ b/crates/dropbear-engine/src/pipelines/hdr.rs
@@ -1,7 +1,15 @@
 use crate::pipelines::create_render_pipeline;
 use crate::texture::{Texture, TextureBuilder};
+use wgpu::util::DeviceExt;
 use wgpu::Operations;
 
+#[repr(C)]
+#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+struct PostProcessUniforms {
+    gamma: f32,
+    _pad: [f32; 3],
+}
+
 pub struct HdrPipeline {
     pipeline: wgpu::RenderPipeline,
     bind_group: wgpu::BindGroup,
@@ -12,6 +20,8 @@ pub struct HdrPipeline {
     format: wgpu::TextureFormat,
     layout: wgpu::BindGroupLayout,
     antialiasing: crate::multisampling::AntiAliasingMode,
+    gamma: f32,
+    gamma_buffer: wgpu::Buffer,
 }
 
 impl HdrPipeline {
@@ -49,6 +59,16 @@ impl HdrPipeline {
             ),
         };
 
+        let default_gamma = 2.2_f32;
+        let gamma_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+            label: Some("Hdr::gamma_buffer"),
+            contents: bytemuck::bytes_of(&PostProcessUniforms {
+                gamma: default_gamma,
+                _pad: [0.0; 3],
+            }),
+            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+        });
+
         let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
             label: Some("Hdr::layout"),
             entries: &[
@@ -69,6 +89,16 @@ impl HdrPipeline {
                     ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                     count: None,
                 },
+                wgpu::BindGroupLayoutEntry {
+                    binding: 2,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                },
             ],
         });
         let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -83,6 +113,10 @@ impl HdrPipeline {
                     binding: 1,
                     resource: wgpu::BindingResource::Sampler(&texture.sampler),
                 },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: gamma_buffer.as_entire_binding(),
+                },
             ],
         });
 
@@ -118,9 +152,33 @@ impl HdrPipeline {
             height,
             format,
             antialiasing,
+            gamma: default_gamma,
+            gamma_buffer,
         }
     }
 
+    /// Returns the current gamma exponent.
+    pub fn gamma(&self) -> f32 {
+        self.gamma
+    }
+
+    /// Sets the gamma correction exponent applied after ACES tonemapping.
+    ///
+    /// - `2.2` — standard gamma for non-sRGB render targets (default).
+    /// - `1.0` — no correction; use when the render target is an sRGB-format
+    ///   texture so the GPU handles gamma encoding automatically.
+    pub fn set_gamma(&mut self, queue: &wgpu::Queue, gamma: f32) {
+        self.gamma = gamma;
+        queue.write_buffer(
+            &self.gamma_buffer,
+            0,
+            bytemuck::bytes_of(&PostProcessUniforms {
+                gamma,
+                _pad: [0.0; 3],
+            }),
+        );
+    }
+
     /// Resize the HDR texture
     pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32, antialiasing: Option<crate::multisampling::AntiAliasingMode>) {
         self.antialiasing = antialiasing.unwrap_or(self.antialiasing);
@@ -157,6 +215,10 @@ impl HdrPipeline {
                     binding: 1,
                     resource: wgpu::BindingResource::Sampler(&self.texture.sampler),
                 },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: self.gamma_buffer.as_entire_binding(),
+                },
             ],
         });
         self.width = width;
diff --git a/crates/dropbear-engine/src/shaders/hdr.wgsl b/crates/dropbear-engine/src/shaders/hdr.wgsl
index 2dc5562..db32074 100644
--- a/crates/dropbear-engine/src/shaders/hdr.wgsl
+++ b/crates/dropbear-engine/src/shaders/hdr.wgsl
@@ -47,9 +47,21 @@ var hdr_image: texture_2d<f32>;
 @binding(1)
 var hdr_sampler: sampler;
 
+struct PostProcessSettings {
+    gamma: f32,
+    _pad0: f32,
+    _pad1: f32,
+    _pad2: f32,
+}
+
+@group(0)
+@binding(2)
+var<uniform> post_process: PostProcessSettings;
+
 @fragment
 fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> {
     let hdr = textureSample(hdr_image, hdr_sampler, vs.uv);
     let sdr = aces_tone_map(hdr.rgb);
-    return vec4(sdr, hdr.a);
+    let gamma_corrected = pow(sdr, vec3<f32>(1.0 / post_process.gamma));
+    return vec4(gamma_corrected, hdr.a);
 }
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index bf45610..af3e4f6 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -429,6 +429,6 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
 
     // combine
     let colour = lo + ambient_ibl + emissive;
-
+    
     return vec4<f32>(colour, albedo_sample.a);
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 2e754a1..d8ff172 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -4,6 +4,7 @@ use crate::asset::AssetRegistry;
 use crate::graphics::SharedGraphicsContext;
 use crate::utils::{ResourceReference};
 use image::{DynamicImage, GenericImageView, RgbaImage};
+use uuid::Uuid;
 use rkyv::Archive;
 use serde::{Deserialize, Serialize};
 use wgpu::{SamplerDescriptor, TextureAspect, TextureFormat, TextureUsages, TextureViewDescriptor, TextureViewDimension};
@@ -143,8 +144,10 @@ impl Image {
 
 #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
 pub enum TextureReference {
-    Resource(ResourceReference),
+    /// A solid RGBA colour literal — no file backing needed.
     RGBAColour([f32; 4]),
+    /// UUID of an asset tracked by a `.eucmeta` sidecar file.
+    AssetUuid(Uuid),
 }
 
 impl<'a> TextureBuilder<'a> {
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 5ab7dc2..176f098 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -654,31 +654,43 @@ impl Component for MeshRenderer {
 
                     let get_tex_handle = async |resource: &Option<TextureReference>| -> Option<anyhow::Result<Handle<Texture>>> {
                         match resource {
-                            Some(TextureReference::Resource(dif)) => match dif {
-                                ResourceReference::File(file_path) if !file_path.is_empty() => {
-                                    let path = dif.resolve().ok()?;
-                                    let bytes = std::fs::read(&path).ok()?;
-                                    let mut texture = TextureBuilder::new(&graphics.device)
-                                        .with_bytes(graphics.clone(), bytes.as_slice())
-                                        .label(file_path.as_str())
-                                        .build();
-                                    texture.reference = Some(dif.clone());
-                                    let mut registry = ASSET_REGISTRY.write();
-                                    Some(Ok(registry.add_texture_with_label(file_path.clone(), texture)))
-                                }
-                                ResourceReference::Embedded(bytes) => {
-                                    let texture = TextureBuilder::new(&graphics.device)
-                                        .with_bytes(graphics.clone(), bytes)
-                                        .label(label.as_str())
-                                        .build();
-                                    let mut registry = ASSET_REGISTRY.write();
-                                    Some(Ok(registry.add_texture_with_label(label.clone(), texture)))
-                                }
-                                ResourceReference::Procedural(_) => {
-                                    Some(Err(anyhow::anyhow!("Using a Procedural object as a texture is not valid, for texture with label {}", label)))
+                            Some(TextureReference::AssetUuid(uuid)) => {
+                                let project_root = crate::states::PROJECT.read().project_path.clone();
+                                match crate::metadata::find_asset_by_uuid(&project_root, *uuid) {
+                                    Ok(entry) => {
+                                        if let crate::resource::ResourceReference::File(rel) = &entry.location {
+                                            let abs = project_root.join(rel);
+                                            let path_str = abs.to_string_lossy().to_string();
+                                            // return early if already loaded
+                                            {
+                                                let engine_ref = ResourceReference::from_path(&abs).ok();
+                                                if let Some(ref r) = engine_ref {
+                                                    let registry = ASSET_REGISTRY.read();
+                                                    if let Some(h) = registry.get_texture_handle_by_reference(r) {
+                                                        return Some(Ok(h));
+                                                    }
+                                                }
+                                            }
+                                            match std::fs::read(&abs) {
+                                                Ok(bytes) => {
+                                                    let engine_ref = ResourceReference::from_path(&abs).ok();
+                                                    let mut texture = TextureBuilder::new(&graphics.device)
+                                                        .with_bytes(graphics.clone(), bytes.as_slice())
+                                                        .label(path_str.as_str())
+                                                        .build();
+                                                    texture.reference = engine_ref;
+                                                    let mut registry = ASSET_REGISTRY.write();
+                                                    Some(Ok(registry.add_texture_with_label(entry.name.clone(), texture)))
+                                                }
+                                                Err(e) => Some(Err(anyhow::anyhow!("Failed to read texture for UUID {}: {}", uuid, e))),
+                                            }
+                                        } else {
+                                            Some(Err(anyhow::anyhow!("Texture asset {} has no file-backed location", uuid)))
+                                        }
+                                    }
+                                    Err(e) => Some(Err(anyhow::anyhow!("UUID {} not found for texture: {}", uuid, e))),
                                 }
-                                _ => None,
-                            },
+                            }
                             Some(TextureReference::RGBAColour(rgba)) => {
                                 let to_u8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
                                 let mut registry = ASSET_REGISTRY.write();
@@ -745,64 +757,18 @@ impl Component for MeshRenderer {
     }
 
     fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> {
-        // let entity_label_raw = world
-        //     .query_one::<&Label>(entity)
-        //     .get()
-        //     .map(|label| label.as_str().to_string())
-        //     .unwrap_or_else(|_| "unnamed_entity".to_string());
-        //
-        // let sanitize_segment = |segment: &str| -> String {
-        //     let mut result = String::with_capacity(segment.len());
-        //     for ch in segment.chars() {
-        //         if ch.is_ascii_alphanumeric() {
-        //             result.push(ch.to_ascii_lowercase());
-        //         } else if ch == '_' || ch == '-' {
-        //             result.push(ch);
-        //         } else {
-        //             result.push('_');
-        //         }
-        //     }
-        //
-        //     let trimmed = result.trim_matches('_').to_string();
-        //     if trimmed.is_empty() {
-        //         "unnamed_entity".to_string()
-        //     } else {
-        //         trimmed
-        //     }
-        // };
-
-        // let proc_obj_type_name = |ty: &ProcObjType| -> &'static str {
-        //     match ty {
-        //         ProcObjType::Cuboid => "cuboid",
-        //     }
-        // };
-        //
-        // let entity_label = sanitize_segment(entity_label_raw.as_str());
-
-        let save_reference = |reference: ResourceReference, context: &str| -> ResourceReference {
-            match &reference {
-                ResourceReference::File(_) | ResourceReference::Procedural(_) => reference,
-                ResourceReference::Embedded(_) => {
-                    match reference
-                        .as_file(None)
-                        .and_then(|path| ResourceReference::from_path(path.as_path()))
-                    {
-                        Ok(file_ref) => file_ref,
-                        Err(err) => {
-                            log::warn!(
-                        "Failed to save {} as file-backed reference: {}",
-                        context,
-                        err
-                    );
-                            ResourceReference::default()
-                        }
+        let save_optional_texture_reference = |tex_reference: Option<ResourceReference>| -> Option<TextureReference> {
+            let resource = tex_reference?;
+            if let ResourceReference::File(rel) = &resource {
+                if !rel.is_empty() {
+                    let project_root = crate::states::PROJECT.read().project_path.clone();
+                    let abs = project_root.join("resources").join(rel);
+                    if let Ok(entry) = crate::metadata::generate_eucmeta(&abs, &project_root) {
+                        return Some(TextureReference::AssetUuid(entry.uuid));
                     }
                 }
             }
-        };
-
-        let save_optional_texture_reference = |reference: Option<ResourceReference>, context: &str| {
-            reference.map(|resource| TextureReference::Resource(save_reference(resource, context)))
+            None
         };
 
         let asset = ASSET_REGISTRY.read();
@@ -848,10 +814,7 @@ impl Component for MeshRenderer {
                     .get_texture(mat.diffuse_texture)
                     .and_then(|t| t.reference.clone())
             };
-            let diffuse_texture = save_optional_texture_reference(
-                diffuse_texture,
-                "mesh renderer diffuse texture",
-            );
+            let diffuse_texture = save_optional_texture_reference(diffuse_texture);
 
             let normal_texture = if default_material.map(|default| default.normal_texture)
                 == Some(mat.normal_texture)
@@ -861,10 +824,7 @@ impl Component for MeshRenderer {
                 mat.normal_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let normal_texture = save_optional_texture_reference(
-                normal_texture,
-                "mesh renderer normal texture",
-            );
+            let normal_texture = save_optional_texture_reference(normal_texture);
 
             let emissive_texture = if default_material.map(|default| default.emissive_texture)
                 == Some(mat.emissive_texture)
@@ -874,10 +834,7 @@ impl Component for MeshRenderer {
                 mat.emissive_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let emissive_texture = save_optional_texture_reference(
-                emissive_texture,
-                "mesh renderer emissive texture",
-            );
+            let emissive_texture = save_optional_texture_reference(emissive_texture);
 
             let occlusion_texture = if default_material.map(|default| default.occlusion_texture)
                 == Some(mat.occlusion_texture)
@@ -887,10 +844,7 @@ impl Component for MeshRenderer {
                 mat.occlusion_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let occlusion_texture = save_optional_texture_reference(
-                occlusion_texture,
-                "mesh renderer occlusion texture",
-            );
+            let occlusion_texture = save_optional_texture_reference(occlusion_texture);
 
             let metallic_roughness_texture = if default_material
                 .map(|default| default.metallic_roughness_texture)
@@ -901,10 +855,7 @@ impl Component for MeshRenderer {
                 mat.metallic_roughness_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let metallic_roughness_texture = save_optional_texture_reference(
-                metallic_roughness_texture,
-                "mesh renderer metallic-roughness texture",
-            );
+            let metallic_roughness_texture = save_optional_texture_reference(metallic_roughness_texture);
 
             texture_override.insert(
                 label.to_string(),
diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index dcf8e7e..8345b38 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -297,6 +297,8 @@ impl ProjectConfig {
             self.last_opened_scene = Some(first.scene_name.clone());
         }
 
+        crate::metadata::scan_and_generate_eucmeta(&project_root);
+
         Ok(())
     }
 
@@ -322,28 +324,6 @@ impl ProjectConfig {
     }
 }
 
-/// The resource config.
-#[derive(Default, Debug, Serialize, Deserialize)]
-pub struct ResourceConfig {
-    /// The path to the resource folder.
-    pub path: PathBuf,
-    /// The files and folders of the assets
-    pub nodes: Vec<Node>,
-}
-
-impl ResourceConfig {
-    /// Updates the in-memory ResourceConfig by re-scanning the resource directory.
-    pub fn update_mem(&mut self) -> anyhow::Result<ResourceConfig> {
-        let resource_dir = self.path.clone();
-        let project_path = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf();
-        let updated_config = ResourceConfig {
-            path: resource_dir.clone(),
-            nodes: collect_nodes(&resource_dir, &project_path, vec!["thumbnails"].as_slice()),
-        };
-        Ok(updated_config)
-    }
-}
-
 #[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct SourceConfig {
     /// The path to the resource folder.
diff --git a/crates/eucalyptus-core/src/metadata.rs b/crates/eucalyptus-core/src/metadata.rs
index b77bd87..b2a5252 100644
--- a/crates/eucalyptus-core/src/metadata.rs
+++ b/crates/eucalyptus-core/src/metadata.rs
@@ -222,6 +222,43 @@ pub struct PackedAssetEntry {
     pub dependencies: Vec<UuidV4>,
 }
 
+/// generates `.eucmeta` files for resources
+pub fn scan_and_generate_eucmeta(project_root: &Path) -> usize {
+    let resources_dir = project_root.join("resources");
+    let mut created = 0usize;
+    fn walk(dir: &Path, project_root: &Path, created: &mut usize) {
+        let Ok(read) = std::fs::read_dir(dir) else { return };
+        for entry in read.flatten() {
+            let path = entry.path();
+            if path.is_dir() {
+                walk(&path, project_root, created);
+                continue;
+            }
+            // skip sidecars themselves
+            if path.extension().and_then(|e| e.to_str()) == Some("eucmeta") {
+                continue;
+            }
+            // only generate for known asset types
+            if detect_asset_type(&path).is_none() {
+                continue;
+            }
+            let meta_path = PathBuf::from(format!("{}.eucmeta", path.display()));
+            if meta_path.exists() {
+                continue;
+            }
+            match generate_eucmeta(&path, project_root) {
+                Ok(_) => *created += 1,
+                Err(e) => log::warn!("Failed to generate .eucmeta for '{}': {}", path.display(), e),
+            }
+        }
+    }
+    walk(&resources_dir, project_root, &mut created);
+    if created > 0 {
+        log::info!("Generated {} .eucmeta sidecar(s) during project scan", created);
+    }
+    created
+}
+
 /// Scans all `.eucmeta` files under `<project_root>/resources/` and returns
 /// the [`AssetEntry`] whose UUID matches `uuid`.
 ///
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 534d063..e170a18 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -81,6 +81,10 @@ pub struct SceneSettings {
     /// Overlays all billboard ui for all entities if it exists as a component.
     #[serde(default = "SceneSettings::default_overlay_billboard")]
     pub overlay_billboard: bool,
+
+    /// Controls the strength of ambient/IBL lighting for this scene.
+    #[serde(default = "SceneSettings::default_ambient_strength")]
+    pub ambient_strength: f32,
 }
 
 impl SceneSettings {
@@ -91,6 +95,7 @@ impl SceneSettings {
             show_hitboxes: false,
             overlay_hud: false,
             overlay_billboard: true,
+            ambient_strength: 1.0,
         }
     }
 
@@ -101,6 +106,10 @@ impl SceneSettings {
     pub(crate) const fn default_overlay_billboard() -> bool {
         true
     }
+
+    pub(crate) const fn default_ambient_strength() -> f32 {
+        1.0
+    }
 }
 
 /// Specifies the configuration of a scene, such as its entities, hierarchies and any settings that
diff --git a/crates/eucalyptus-core/src/ser/model.rs b/crates/eucalyptus-core/src/ser/model.rs
index 0f787e3..bf0940c 100644
--- a/crates/eucalyptus-core/src/ser/model.rs
+++ b/crates/eucalyptus-core/src/ser/model.rs
@@ -8,8 +8,30 @@ use dropbear_engine::texture::{Texture, TextureWrapMode};
 use dropbear_engine::utils::ResourceReference;
 use dropbear_engine::wgpu::util::DeviceExt;
 use dropbear_engine::wgpu;
+use uuid::Uuid;
 
-use crate::utils::ResolveReference;
+use crate::uuid::UuidV4;
+
+/// How a texture is referenced inside a compiled model (`.eucmdl`).
+///
+/// `AssetUuid` is the canonical form for any texture that lives on disk and has
+/// a `.eucmeta` sidecar. `Embedded` is used for textures that were packed
+/// directly inside a source file (e.g. GLTF-embedded data) and have not yet
+/// been extracted as standalone assets.
+#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub enum EucalyptusTextureRef {
+    /// UUID of a file-backed texture tracked by a `.eucmeta` sidecar.
+    AssetUuid(UuidV4),
+    /// Raw image bytes embedded directly in the model file.
+    Embedded(Arc<[u8]>),
+}
+
+impl EucalyptusTextureRef {
+    /// Constructs from a `uuid::Uuid`.
+    pub fn from_uuid(uuid: Uuid) -> Self {
+        Self::AssetUuid(UuidV4::from(uuid))
+    }
+}
 
 /// The serialized format for a Model without all the buffers and stuff.
 ///
@@ -163,11 +185,11 @@ impl EucalyptusMesh {
 #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)]
 pub struct EucalyptusMaterial {
     pub name: String,
-    pub diffuse_texture: ResourceReference, // the file can either be a eucalyptus texture or a standard file
-    pub normal_texture: Option<ResourceReference>, // same
-    pub emissive_texture: Option<ResourceReference>, // same
-    pub metallic_roughness_texture: Option<ResourceReference>, // same
-    pub occlusion_texture: Option<ResourceReference>, // same
+    pub diffuse_texture: Option<EucalyptusTextureRef>,
+    pub normal_texture: Option<EucalyptusTextureRef>,
+    pub emissive_texture: Option<EucalyptusTextureRef>,
+    pub metallic_roughness_texture: Option<EucalyptusTextureRef>,
+    pub occlusion_texture: Option<EucalyptusTextureRef>,
     pub tint: [f32; 4],
     pub emissive_factor: [f32; 3],
     pub metallic_factor: f32,
@@ -184,19 +206,29 @@ pub struct EucalyptusMaterial {
 
 impl From<Material> for EucalyptusMaterial {
     fn from(value: Material) -> Self {
-        let get_texture = |tex: Option<Handle<Texture>>| -> Option<ResourceReference> {
-            if let Some(tex) = tex {
-                if let Some(t) = ASSET_REGISTRY.read().get_texture(tex) {
-                    return t.reference.clone();
+        let project_root = crate::states::PROJECT.read().project_path.clone();
+
+        let get_texture = |tex: Option<Handle<Texture>>| -> Option<EucalyptusTextureRef> {
+            let tex = tex?;
+            let registry = ASSET_REGISTRY.read();
+            let t = registry.get_texture(tex)?;
+            match t.reference.clone()? {
+                ResourceReference::File(rel) if !rel.is_empty() => {
+                    let abs = project_root.join("resources").join(&rel);
+                    crate::metadata::generate_eucmeta(&abs, &project_root)
+                        .ok()
+                        .map(|entry| EucalyptusTextureRef::from_uuid(entry.uuid))
                 }
+                ResourceReference::Embedded(bytes) => {
+                    Some(EucalyptusTextureRef::Embedded(bytes))
+                }
+                _ => None,
             }
-
-            None
         };
 
         Self {
             name: value.name,
-            diffuse_texture: get_texture(Some(value.diffuse_texture)).unwrap_or_default(),
+            diffuse_texture: get_texture(Some(value.diffuse_texture)),
             normal_texture: get_texture(value.normal_texture),
             emissive_texture: get_texture(value.emissive_texture),
             metallic_roughness_texture: get_texture(value.metallic_roughness_texture),
@@ -221,40 +253,58 @@ impl EucalyptusMaterial {
     fn load_texture(
         &self,
         graphics: Arc<SharedGraphicsContext>,
-        reference: &ResourceReference,
+        reference: &EucalyptusTextureRef,
         suffix: &str,
     ) -> Option<Handle<Texture>> {
         match reference {
-            ResourceReference::File(path) if !path.is_empty() => {
-                let path = reference.resolve().ok()?;
-                let bytes = std::fs::read(path).ok()?;
-                let label = format!("{}_{}", self.name, suffix);
-                let mut texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device)
-                    .with_bytes(graphics.clone(), bytes.as_slice())
-                    .label(label.as_str())
-                    .build();
-                texture.reference = Some(reference.clone());
-
-                let mut registry = ASSET_REGISTRY.write();
-                Some(registry.add_texture(texture))
+            EucalyptusTextureRef::AssetUuid(uuid_v4) => {
+                let uuid = uuid_v4.as_uuid();
+                let project_root = crate::states::PROJECT.read().project_path.clone();
+                let entry = crate::metadata::find_asset_by_uuid(&project_root, uuid)
+                    .map_err(|e| log::warn!("load_texture: UUID {} not found: {}", uuid, e))
+                    .ok()?;
+                if let crate::resource::ResourceReference::File(rel) = &entry.location {
+                    let abs = project_root.join(rel);
+                    // Dedup: return cached handle if already loaded.
+                    if let Ok(engine_ref) = ResourceReference::from_path(&abs) {
+                        let registry = ASSET_REGISTRY.read();
+                        if let Some(h) = registry.get_texture_handle_by_reference(&engine_ref) {
+                            return Some(h);
+                        }
+                    }
+                    let bytes = std::fs::read(&abs)
+                        .map_err(|e| log::warn!("load_texture: failed to read '{}': {}", abs.display(), e))
+                        .ok()?;
+                    let label = format!("{}_{}", self.name, suffix);
+                    let engine_ref = ResourceReference::from_path(&abs).ok();
+                    let mut texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device)
+                        .with_bytes(graphics.clone(), bytes.as_slice())
+                        .label(label.as_str())
+                        .build();
+                    texture.reference = engine_ref;
+                    let mut registry = ASSET_REGISTRY.write();
+                    Some(registry.add_texture_with_label(entry.name, texture))
+                } else {
+                    log::warn!("load_texture: UUID {} has no file-backed location", uuid);
+                    None
+                }
             }
-            ResourceReference::Embedded(bytes) => {
+            EucalyptusTextureRef::Embedded(bytes) => {
                 let label = format!("{}_{}", self.name, suffix);
                 let texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device)
                     .with_bytes(graphics.clone(), bytes)
                     .label(label.as_str())
                     .build();
-
                 let mut registry = ASSET_REGISTRY.write();
                 Some(registry.add_texture(texture))
             }
-            _ => None,
         }
     }
 
     fn load(&self, graphics: Arc<SharedGraphicsContext>) -> Material {
         let diffuse_texture = {
-            let maybe = self.load_texture(graphics.clone(), &self.diffuse_texture, "diffuse");
+            let maybe = self.diffuse_texture.as_ref()
+                .and_then(|r| self.load_texture(graphics.clone(), r, "diffuse"));
             if let Some(handle) = maybe {
                 handle
             } else {
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 038d1f7..211a2aa 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -394,7 +394,7 @@ impl AsFile for ResourceReference {
                     }],
                     materials: vec![EucalyptusMaterial {
                         name: "procedural_material".to_string(),
-                        diffuse_texture: ResourceReference::default(),
+                        diffuse_texture: None,
                         normal_texture: None,
                         emissive_texture: None,
                         metallic_roughness_texture: None,
diff --git a/crates/eucalyptus-editor/src/build.rs b/crates/eucalyptus-editor/src/build.rs
index 38e1586..e5d36e1 100644
--- a/crates/eucalyptus-editor/src/build.rs
+++ b/crates/eucalyptus-editor/src/build.rs
@@ -17,6 +17,8 @@ use tokio::{fs as tokio_fs, process::Command, task};
 ///
 /// Returns the path of the build directory
 pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> {
+    // todo: remake this entire function
+    return Err(anyhow::anyhow!("Not implemented yet"));
     log::info!("Started project building");
     // create a build directory
     let project_root = project_config
@@ -101,6 +103,9 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> {
         if ty.is_dir() {
             copy_dir_recursive(&entry.path(), &dst.join(entry.file_name()))?;
         } else {
+            if entry.path().extension().and_then(|e| e.to_str()) == Some("eucmeta") {
+                continue;
+            }
             fs::copy(entry.path(), dst.join(entry.file_name()))?;
         }
     }
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index a556391..cf422c3 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -9,7 +9,7 @@ use eucalyptus_core::utils::ResolveReference;
 use hecs::Entity;
 use log::{info, warn};
 use std::hash::{Hash, Hasher};
-use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::Path};
+use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::{Path, PathBuf}};
 
 use crate::editor::{
     AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset,
@@ -1088,6 +1088,13 @@ impl<'a> EditorTabViewer<'a> {
             );
         } else {
             info!("Renamed to {}", target_path.display());
+            let old_meta = PathBuf::from(format!("{}.eucmeta", rename.original_path.display()));
+            if old_meta.exists() {
+                let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display()));
+                if let Err(e) = fs::rename(&old_meta, &new_meta) {
+                    warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e);
+                }
+            }
         }
     }
 
@@ -1107,6 +1114,14 @@ impl<'a> EditorTabViewer<'a> {
             warn!("Failed to delete '{}': {}", info.path.display(), err);
         } else {
             info!("Deleted {}", info.path.display());
+            if !info.is_dir {
+                let meta = PathBuf::from(format!("{}.eucmeta", info.path.display()));
+                if meta.exists() {
+                    if let Err(e) = fs::remove_file(&meta) {
+                        warn!("Failed to remove .eucmeta sidecar '{}': {}", meta.display(), e);
+                    }
+                }
+            }
         }
     }
 
@@ -1206,6 +1221,15 @@ impl<'a> EditorTabViewer<'a> {
             warn!("Failed to move '{}': {}", source_info.path.display(), err);
         } else {
             info!("Moved to {}", target_path.display());
+            if !source_info.is_dir {
+                let old_meta = PathBuf::from(format!("{}.eucmeta", source_info.path.display()));
+                if old_meta.exists() {
+                    let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display()));
+                    if let Err(e) = fs::rename(&old_meta, &new_meta) {
+                        warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e);
+                    }
+                }
+            }
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index b84ce3f..bf2a525 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -39,7 +39,7 @@ impl<'a> EditorTabViewer<'a> {
                                 self.world.spawn((label,));
                                 ui.close();
                             }
-                            ui.menu_button("Import Template", |ui| {
+                            ui.menu_button("Import Template", |_| {
 
                             });
                         }),
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 0fad404..75202c5 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -479,6 +479,12 @@ impl Scene for Editor {
         if let Some(globals) = &mut self.shader_globals {
             puffin::profile_scope!("Fetching globals");
             globals.set_num_lights(enabled_light_count);
+            if let Some(scene_name) = &self.current_scene_name {
+                let scenes = SCENES.read();
+                if let Some(scene) = scenes.iter().find(|s| s.scene_name == *scene_name) {
+                    globals.set_ambient_strength(scene.settings.ambient_strength);
+                }
+            }
             globals.write(&graphics.queue);
         }
 
diff --git a/crates/eucalyptus-editor/src/editor/settings.rs b/crates/eucalyptus-editor/src/editor/settings.rs
index 3227070..d85be0a 100644
--- a/crates/eucalyptus-editor/src/editor/settings.rs
+++ b/crates/eucalyptus-editor/src/editor/settings.rs
@@ -39,6 +39,14 @@ impl<'a> EditorTabViewer<'a> {
                     scene.settings.overlay_hud = overlay_hud;
                 }
                 ui.label("Renders the HUD UI overlay on top of the viewport");
+
+                ui.separator();
+                ui.label("Ambient Strength");
+                let mut ambient = scene.settings.ambient_strength;
+                if ui.add(egui::Slider::new(&mut ambient, 0.0..=2.0).step_by(0.01)).changed() {
+                    scene.settings.ambient_strength = ambient;
+                }
+                ui.label("Controls the intensity of ambient/IBL lighting");
             } else {
                 ui.label("Scene not found");
             }
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index d467bea..fa9ff67 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -690,6 +690,12 @@ impl Scene for PlayMode {
         if let Some(globals) = &mut self.shader_globals {
             puffin::profile_scope!("Fetching globals");
             globals.set_num_lights(enabled_light_count);
+            if let Some(scene_name) = &self.current_scene {
+                let scenes = SCENES.read();
+                if let Some(scene) = scenes.iter().find(|s| &s.scene_name == scene_name) {
+                    globals.set_ambient_strength(scene.settings.ambient_strength);
+                }
+            }
             globals.write(&graphics.queue);
         }