kitgit

tirbofish/dropbear · diff

9a516a7 · tk

attempted to make a plane (didn't work, will try later), created easier pathing with "ResourceReferences" (though i do need to improve performance).

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index 6a59752..7b17222 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,7 @@ bincode = { version = "2.0", features = ["serde"] }
 bytemuck = { version = "1.23", features = ["derive"] }
 chrono = "0.4"
 clap = "4.5"
+colored = "3.0"
 dropbear-engine = { path = "dropbear-engine" }
 egui = "0.32"
 egui-toast-fork = "0.18"
diff --git a/dropbear-engine/Cargo.toml b/dropbear-engine/Cargo.toml
index 925f12b..7ffd453 100644
--- a/dropbear-engine/Cargo.toml
+++ b/dropbear-engine/Cargo.toml
@@ -13,6 +13,7 @@ anyhow.workspace = true
 app_dirs2.workspace = true
 bytemuck.workspace = true
 chrono.workspace = true
+colored.workspace = true
 egui_wgpu_backend.workspace = true
 egui_winit_platform.workspace = true
 egui.workspace = true
@@ -30,6 +31,8 @@ spin_sleep.workspace = true
 wgpu.workspace = true
 winit.workspace = true
 hecs.workspace = true
+once_cell.workspace = true
+parking_lot.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
diff --git a/dropbear-engine/src/entity.rs b/dropbear-engine/src/entity.rs
index 41422b1..b0c481b 100644
--- a/dropbear-engine/src/entity.rs
+++ b/dropbear-engine/src/entity.rs
@@ -1,13 +1,15 @@
+use std::collections::HashMap;
 use std::path::PathBuf;
-
 use glam::{DMat4, DQuat, DVec3, Mat4};
+use once_cell::sync::Lazy;
+use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
-// use nalgebra::{Matrix4, UnitQuaternion, Vector3};
 use wgpu::{util::DeviceExt, BindGroup, Buffer};
 
 use crate::{
     graphics::{Graphics, Instance}, model::Model
 };
+use crate::model::Mesh;
 
 #[derive(Debug, Clone, Deserialize, Serialize, Copy, PartialEq)]
 pub struct Transform {
diff --git a/dropbear-engine/src/graphics.rs b/dropbear-engine/src/graphics.rs
index 3519964..2c2c160 100644
--- a/dropbear-engine/src/graphics.rs
+++ b/dropbear-engine/src/graphics.rs
@@ -4,14 +4,7 @@ use egui::Context;
 use glam::{DMat4, DQuat, DVec3, Mat3};
 use image::GenericImageView;
 // use nalgebra::{Matrix4, UnitQuaternion, Vector3};
-use wgpu::{
-    BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder,
-    CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, RenderPass,
-    RenderPassDepthStencilAttachment, RenderPipeline, Sampler, ShaderModule, StencilState,
-    SurfaceConfiguration, TextureDescriptor, TextureFormat, TextureUsages, TextureView,
-    TextureViewDescriptor, VertexBufferLayout,
-    util::{BufferInitDescriptor, DeviceExt},
-};
+use wgpu::{BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder, CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, RenderPass, RenderPassDepthStencilAttachment, RenderPipeline, Sampler, ShaderModule, StencilState, SurfaceConfiguration, TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, VertexBufferLayout, util::{BufferInitDescriptor, DeviceExt}};
 
 use crate::{
     State,
@@ -92,7 +85,8 @@ impl<'a> Graphics<'a> {
                         topology: wgpu::PrimitiveTopology::TriangleList,
                         strip_index_format: None,
                         front_face: wgpu::FrontFace::Ccw,
-                        cull_mode: None, // todo: change for improved performance
+                        // cull_mode: Some(wgpu::Face::Back), // todo: change for improved performance
+                        cull_mode: None,
                         polygon_mode: wgpu::PolygonMode::Fill,
                         unclipped_depth: false,
                         conservative: false,
diff --git a/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index e4cec26..ddee3d2 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -11,6 +11,8 @@ pub mod resources;
 pub mod scene;
 pub mod attenuation;
 pub mod panic;
+pub mod starter;
+pub mod utils;
 
 use chrono::Local;
 use egui::TextureId;
@@ -27,6 +29,7 @@ use std::{
 use std::fs::OpenOptions;
 use std::sync::Mutex;
 use app_dirs2::{AppDataType, AppInfo};
+use colored::Colorize;
 use env_logger::Builder;
 use wgpu::{
     BindGroupLayout, Device, Instance, Queue, Surface, SurfaceConfiguration, SurfaceError, TextureFormat
@@ -429,7 +432,31 @@ impl App {
             Builder::new()
                 .format(move |buf, record| {
                     let ts = Local::now().format("%Y-%m-%dT%H:%M:%S");
-                    let line = format!(
+
+                    let colored_level = match record.level() {
+                        log::Level::Error => record.level().to_string().red().bold(),
+                        log::Level::Warn => record.level().to_string().yellow().bold(),
+                        log::Level::Info => record.level().to_string().green().bold(),
+                        log::Level::Debug => record.level().to_string().blue().bold(),
+                        log::Level::Trace => record.level().to_string().cyan().bold(),
+                    };
+
+                    let colored_timestamp = ts.to_string().bright_black();
+
+                    let file_info = format!("{}:{}",
+                                            record.file().unwrap_or("unknown"),
+                                            record.line().unwrap_or(0)
+                    ).bright_black();
+
+                    let console_line = format!(
+                        "{} {} [{}] - {}\n",
+                        file_info,
+                        colored_timestamp,
+                        colored_level,
+                        record.args()
+                    );
+
+                    let file_line = format!(
                         "{}:{} {} [{}] - {}\n",
                         record.file().unwrap_or("unknown"),
                         record.line().unwrap_or(0),
@@ -438,10 +465,10 @@ impl App {
                         record.args()
                     );
 
-                    write!(buf, "{}", line)?;
+                    write!(buf, "{}", console_line)?;
 
                     if let Ok(mut fh) = file.lock() {
-                        let _ = fh.write_all(line.as_bytes());
+                        let _ = fh.write_all(file_line.as_bytes());
                     }
                     Ok(())
                 })
diff --git a/dropbear-engine/src/lighting.rs b/dropbear-engine/src/lighting.rs
index e06c29b..c2c2387 100644
--- a/dropbear-engine/src/lighting.rs
+++ b/dropbear-engine/src/lighting.rs
@@ -201,7 +201,7 @@ impl Light {
 
         let cube_model = Model::load_from_memory(
             graphics, 
-            include_bytes!("../../resources/cube.obj"), 
+            include_bytes!("../../resources/cube.obj").to_vec(), 
             label.clone()
         ).unwrap();
 
diff --git a/dropbear-engine/src/model.rs b/dropbear-engine/src/model.rs
index 35855b1..7fb9567 100644
--- a/dropbear-engine/src/model.rs
+++ b/dropbear-engine/src/model.rs
@@ -7,7 +7,10 @@ use russimp_ng::{
 };
 use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt};
 
-use crate::graphics::{Graphics, NO_MODEL, NO_TEXTURE, Texture};
+use crate::graphics::{Graphics, NO_MODEL, Texture};
+use crate::utils::ResourceReference;
+
+pub const GREY_TEXTURE_BYTES: &'static [u8] = include_bytes!("../../resources/grey.png");
 
 pub trait Vertex {
     fn desc() -> VertexBufferLayout<'static>;
@@ -50,7 +53,7 @@ impl Vertex for ModelVertex {
 #[derive(Clone)]
 pub struct Model {
     pub label: String,
-    pub path: PathBuf,
+    pub path: ResourceReference,
     pub meshes: Vec<Mesh>,
     pub materials: Vec<Material>,
 }
@@ -74,13 +77,14 @@ pub struct Mesh {
 impl Model {
     pub fn load_from_memory(
         graphics: &Graphics<'_>,
-        buffer: &[u8],
+        buffer: Vec<u8>,
         label: Option<&str>,
     ) -> anyhow::Result<Model> {
         log::debug!("Loading from memory");
+        let res_ref = ResourceReference::from_bytes(buffer.clone());
 
         let scene = match Scene::from_buffer(
-            buffer,
+            buffer.as_slice(),
             vec![
                 PostProcess::Triangulate,
                 PostProcess::FlipUVs,
@@ -129,7 +133,7 @@ impl Model {
                 } else {
                     log::warn!("Error loading material, using default missing texture");
                 }
-                Texture::new(graphics, NO_TEXTURE)
+                Texture::new(graphics, GREY_TEXTURE_BYTES)
             };
 
             let bind_group = diffuse_texture.bind_group().to_owned();
@@ -206,9 +210,9 @@ impl Model {
             label: if let Some(l) = label {
                 l.to_string()
             } else {
-                String::from("Model")
+                String::from("No named model")
             },
-            path: PathBuf::new(),
+            path: res_ref,
         })
     }
 
@@ -217,8 +221,8 @@ impl Model {
         path: &PathBuf,
         label: Option<&str>,
     ) -> anyhow::Result<Model> {
-        let file_name = path.file_name().unwrap().to_str().unwrap();
-        log::debug!("Loading model [{}]", file_name);
+        let file_name = path.file_name();
+        log::debug!("Loading model [{:?}]", file_name);
 
         let scene = match Scene::from_file(
             path.to_str().unwrap(),
@@ -269,7 +273,7 @@ impl Model {
                 } else {
                     log::warn!("Error loading material, using default missing texture");
                 }
-                Texture::new(graphics, NO_TEXTURE)
+                Texture::new(graphics, GREY_TEXTURE_BYTES)
             };
 
             let bind_group = diffuse_texture.bind_group().to_owned();
@@ -339,16 +343,16 @@ impl Model {
                 material: mesh.material_index as usize,
             });
         }
-        log::debug!("Successfully loaded model [{}]", file_name);
+        log::debug!("Successfully loaded model [{:?}]", file_name);
         Ok(Model {
             meshes,
             materials,
             label: if let Some(l) = label {
                 l.to_string()
             } else {
-                String::from(file_name.split(".").into_iter().next().unwrap())
+                String::from(file_name.unwrap().to_str().unwrap().split(".").into_iter().next().unwrap())
             },
-            path: path.clone(),
+            path: ResourceReference::from_path(path).unwrap(),
         })
     }
 }
diff --git a/dropbear-engine/src/starter/mod.rs b/dropbear-engine/src/starter/mod.rs
new file mode 100644
index 0000000..3f04ee2
--- /dev/null
+++ b/dropbear-engine/src/starter/mod.rs
@@ -0,0 +1,3 @@
+//! Starter objects like planes, players and more
+
+pub mod plane;
\ No newline at end of file
diff --git a/dropbear-engine/src/starter/plane.rs b/dropbear-engine/src/starter/plane.rs
new file mode 100644
index 0000000..5446021
--- /dev/null
+++ b/dropbear-engine/src/starter/plane.rs
@@ -0,0 +1,9 @@
+pub struct Plane {
+
+}
+
+impl Plane {
+    pub fn new() {
+
+    }
+}
\ No newline at end of file
diff --git a/dropbear-engine/src/utils.rs b/dropbear-engine/src/utils.rs
new file mode 100644
index 0000000..02157d4
--- /dev/null
+++ b/dropbear-engine/src/utils.rs
@@ -0,0 +1,193 @@
+//! Utilities and helper functions for the dropbear renderer.
+
+use std::fmt::{Display, Formatter};
+use std::{fs, io};
+use std::path::{Path, PathBuf};
+use serde::{Deserialize, Serialize};
+
+/// A struct used to "point" to the resource relative to
+/// the executable directory or the project directory.
+///
+/// # Example
+/// `/home/tk/project/resources/models/cube.obj` is the file path to `cube.obj`.
+///
+/// The resource reference will be `models/cube.obj`.
+///
+/// - If ran in the editor, it translates to
+/// `/home/tk/project/resources/models/cube.obj`.
+///
+/// - In the runtime (with redback-runtime), it
+/// translates to `/home/tk/Downloads/Maze/resources/models/cube.obj`
+/// _(assuming the executable is at `/home/tk/Downloads/Maze/maze_runner.exe`)_.
+#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct ResourceReference {
+    resource_ref_path: String,
+    bytes: Vec<u8>
+}
+
+impl Display for ResourceReference {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.resource_ref_path)
+    }
+}
+
+impl ResourceReference {
+    /// Creates a new `ResourceReference` struct.
+    pub fn new(resource_path: impl Into<String>) -> Self {
+        Self {
+            resource_ref_path: resource_path.into(),
+            bytes: Vec::new()
+        }
+    }
+    
+    pub fn from_bytes(bytes: Vec<u8>) -> Self {
+        Self {
+            resource_ref_path: String::from(""),
+            bytes
+        }
+    }
+
+    /// Creates a `ResourceReference` from a full path by extracting the part after "resources/".
+    ///
+    /// # Examples
+    /// ```
+    /// use dropbear_engine::utils::ResourceReference;
+    ///
+    /// let path = "/home/tk/project/resources/models/cube.obj";
+    /// let resource_ref = ResourceReference::from_path(path).unwrap();
+    /// assert_eq!(resource_ref.as_str(), "models/cube.obj");
+    /// ```
+    ///
+    /// Returns `None` if the path doesn't contain "resources" or if the path after resources is empty.
+    pub fn from_path(full_path: impl AsRef<Path>) -> Option<Self> {
+        let path = full_path.as_ref();
+
+        let components: Vec<_> = path.components().collect();
+
+        for (i, component) in components.iter().enumerate() {
+            if let std::path::Component::Normal(name) = component {
+                if *name == "resources" {
+                    let remaining_components = &components[i + 1..];
+                    if remaining_components.is_empty() {
+                        return None;
+                    }
+
+                    let resource_path = remaining_components
+                        .iter()
+                        .map(|c| match c {
+                            std::path::Component::Normal(name) => name.to_str().unwrap_or(""),
+                            _ => "",
+                        })
+                        .collect::<Vec<_>>()
+                        .join("/");
+
+                    return Some(Self {
+                        resource_ref_path: resource_path,
+                        bytes: Vec::new()
+                    });
+                }
+            }
+        }
+
+        None
+    }
+
+    /// Get the raw resource reference path
+    pub fn as_str(&self) -> &str {
+        self.resource_ref_path.as_str()
+    }
+
+    /// Creates a PathBuf relative to the given project path.
+    ///
+    /// If the project_path has a parent directory, it uses that parent + "resources" + resource_path.
+    /// Otherwise, it uses project_path + resource_path directly.
+    pub fn to_project_path(&self, project_path: impl AsRef<Path>) -> PathBuf {
+        let path = project_path.as_ref();
+        match path.parent() {
+            Some(parent) => parent.join("resources").join(self.resource_ref_path.as_str()),
+            None => path.join("resources").join(self.resource_ref_path.as_str()),
+        }
+    }
+
+    /// Creates a PathBuf that points to the resource relative to the executable directory.
+    ///
+    /// Returns an error if the executable path cannot be determined.
+    pub fn to_executable_path(&self) -> anyhow::Result<PathBuf> {
+        let exe_path = std::env::current_exe()?;
+        let exe_dir = exe_path.parent()
+            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not get executable directory"))?;
+        Ok(exe_dir.join("resources").join(self.resource_ref_path.as_str()))
+    }
+
+    /// Creates a PathBuf that points to the resource, with fallback logic.
+    ///
+    /// First tries to resolve relative to executable, then falls back to current directory + resources.
+    pub fn resolve_path(&self) -> PathBuf {
+        if let Ok(exe_path) = self.to_executable_path() {
+            if exe_path.exists() {
+                return exe_path;
+            }
+        }
+
+        std::env::current_dir()
+            .unwrap_or_else(|_| PathBuf::from("."))
+            .join("resources")
+            .join(self.resource_ref_path.as_str())
+    }
+
+    /// Check if the resource exists when resolved via executable path
+    pub fn exists_in_executable_dir(&self) -> bool {
+        self.to_executable_path()
+            .map(|path| path.exists())
+            .unwrap_or(false)
+    }
+
+    /// Check if the resource exists when resolved via project path
+    pub fn exists_in_project_dir(&self, project_path: impl AsRef<Path>) -> bool {
+        self.to_project_path(project_path).exists()
+    }
+
+    /// Check if the resource exists using the fallback resolution logic
+    pub fn exists(&self) -> bool {
+        self.resolve_path().exists()
+    }
+
+    /// Read the resource as a string using fallback resolution
+    pub fn read_to_string(&self) -> io::Result<String> {
+        fs::read_to_string(self.resolve_path())
+    }
+
+    /// Read the resource as bytes using fallback resolution
+    pub fn read_to_bytes(&self) -> io::Result<Vec<u8>> {
+        fs::read(self.resolve_path())
+    }
+
+    /// Read from a specific resolved path (project or executable)
+    pub fn read_to_string_from_project(&self, project_path: impl AsRef<Path>) -> io::Result<String> {
+        fs::read_to_string(self.to_project_path(project_path))
+    }
+
+    /// Read from the executable directory
+    pub fn read_to_string_from_executable(&self) -> anyhow::Result<String> {
+        let path = self.to_executable_path()?;
+        Ok(fs::read_to_string(path)?)
+    }
+
+    /// Get the file name of the resource
+    pub fn file_name(&self) -> Option<&str> {
+        Path::new(self.resource_ref_path.as_str()).file_name()?.to_str()
+    }
+
+    /// Get the file extension of the resource
+    pub fn extension(&self) -> Option<&str> {
+        Path::new(self.resource_ref_path.as_str()).extension()?.to_str()
+    }
+}
+
+/// Neat lil macro to create a resource reference easier
+#[macro_export]
+macro_rules! resource {
+    ($path:literal) => {
+        ResourceReference::new($path)
+    };
+}
\ No newline at end of file
diff --git a/eucalyptus/src/editor/mod.rs b/eucalyptus/src/editor/mod.rs
index fd606be..68720a6 100644
--- a/eucalyptus/src/editor/mod.rs
+++ b/eucalyptus/src/editor/mod.rs
@@ -348,6 +348,8 @@ impl Editor {
     pub fn load_project_config(&mut self, graphics: &Graphics) -> anyhow::Result<()> {
         let config = PROJECT.read().unwrap();
 
+        self.project_path = Some(config.project_path.clone());
+
         if let Some(layout) = &config.dock_layout {
             self.dock_state = layout.clone();
         }
diff --git a/eucalyptus/src/editor/scene.rs b/eucalyptus/src/editor/scene.rs
index 27aafc7..fc99ecf 100644
--- a/eucalyptus/src/editor/scene.rs
+++ b/eucalyptus/src/editor/scene.rs
@@ -1,4 +1,3 @@
-use std::path::PathBuf;
 use egui::Align2;
 use dropbear_engine::{
     entity::{AdoptedEntity, Transform}, graphics::{Graphics, Shader}, lighting::{Light, LightComponent}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}
@@ -7,10 +6,8 @@ use log;
 use parking_lot::Mutex;
 use wgpu::Color;
 use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode};
-
 use super::*;
 use crate::{
-    states::{Node, RESOURCES},
     utils::PendingSpawn,
 };
 
@@ -26,44 +23,6 @@ impl Scene for Editor {
             include_str!("../shader.wgsl"),
             Some("viewport_shader"),
         );
-        if self.world.len() == 0 {
-            let cube_path = {
-                #[allow(unused_assignments)]
-                let mut path = PathBuf::new();
-                let resources = RESOURCES.read().unwrap();
-                let mut matches = Vec::new();
-                crate::utils::search_nodes_recursively(
-                    &resources.nodes,
-                    &|node| match node {
-                        Node::File(file) => file.name.contains("cube"),
-                        Node::Folder(folder) => folder.name.contains("cube"),
-                    },
-                    &mut matches,
-                );
-                match matches.get(0) {
-                    Some(thing) => match thing {
-                        Node::File(file) => path = file.path.clone(),
-                        Node::Folder(folder) => path = folder.path.clone(),
-                    },
-                    None => path = PathBuf::new(),
-                }
-                path
-            };
-
-            if cube_path != PathBuf::new() {
-                let cube = AdoptedEntity::new(graphics, &cube_path, Some("default_cube")).unwrap();
-                self.world
-                    .spawn((cube, Transform::default(), ModelProperties::default()));
-                log::info!("Added default cube since no entities were loaded from scene");
-            } else {
-                log::warn!("cube path is empty :(")
-            }
-        } else {
-            log::info!(
-                "Scene loaded with {} entities, skipping default cube",
-                self.world.len()
-            );
-        }
 
         self.light_manager.create_light_array_resources(graphics);
 
@@ -177,7 +136,7 @@ impl Scene for Editor {
             Signal::Paste(scene_entity) => {
                         match AdoptedEntity::new(
                             graphics,
-                            &scene_entity.model_path,
+                            &scene_entity.model_path.to_project_path(self.project_path.clone().unwrap()),
                             Some(&scene_entity.label),
                         ) {
                             Ok(adopted) => {
@@ -573,6 +532,19 @@ impl Scene for Editor {
                             // always ensure the signal is reset after action is dun
                             self.signal = Signal::None;
                         }
+
+                        // if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() {
+                        //     log::debug!("Creating new plane");
+                        //     let plane = PlaneBuilder::new()
+                        //         .with_size(5000.0, 2000.0)
+                        //         .with_texture_size(1024, 1024)
+                        //         .build(graphics, include_bytes!("../../../resources/proto.png"), Some("Plane")).unwrap();
+                        //     let transform = Transform::new();
+                        //     self.world.spawn((plane, transform));
+                        //     crate::success!("Created new plane");
+                        //
+                        //     self.signal = Signal::None;
+                        // }
                     });
                 if !show {
                     self.signal = Signal::None;
diff --git a/eucalyptus/src/scripting/mod.rs b/eucalyptus/src/scripting/mod.rs
index 6f38092..15cae7e 100644
--- a/eucalyptus/src/scripting/mod.rs
+++ b/eucalyptus/src/scripting/mod.rs
@@ -248,7 +248,7 @@ impl ScriptManager {
                     script_name,
                     entity_id,
                     adopted.label(),
-                    adopted.model().path.display()
+                    adopted.model().path
                 );
             }
         } else {
diff --git a/eucalyptus/src/states.rs b/eucalyptus/src/states.rs
index a41da6d..6afb118 100644
--- a/eucalyptus/src/states.rs
+++ b/eucalyptus/src/states.rs
@@ -28,7 +28,7 @@ use log;
 use once_cell::sync::Lazy;
 use ron::ser::PrettyConfig;
 use serde::{Deserialize, Serialize};
-
+use dropbear_engine::utils::ResourceReference;
 use crate::camera::CameraType;
 #[cfg(feature = "editor")]
 use crate::editor::EditorTab;
@@ -559,18 +559,6 @@ impl EntityNode {
     }
 }
 
-#[derive(Default, Debug, Serialize, Deserialize, Clone)]
-pub struct SceneConfig {
-    pub scene_name: String,
-    pub entities: Vec<SceneEntity>,
-    pub camera: HashMap<CameraType, SceneCameraConfig>, // TODO: Change to component
-    pub lights: Vec<LightConfig>,
-    // todo later
-    // pub settings: SceneSettings,
-    #[serde(skip)]
-    pub path: PathBuf,
-}
-
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct SceneCameraConfig {
     pub position: [f64; 3],
@@ -603,13 +591,13 @@ impl Default for SceneCameraConfig {
 
 impl SceneCameraConfig {
     pub fn camera(&self, graphics: &mut Graphics) -> Camera {
-        Camera::new(graphics, self.position.into(), self.target.into(), self.up.into(), self.aspect.into(), self.fov.into(), self.near.into(), self.far.into(), 5.0, 0.0125)
+        Camera::new(graphics, self.position.into(), self.target.into(), self.up.into(), self.aspect.into(), self.fov.into(), self.near.into(), self.far.into(), 10.0, 0.0125)
     }
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone, Default)]
 pub struct SceneEntity {
-    pub model_path: PathBuf,
+    pub model_path: ResourceReference,
     pub label: String,
     pub transform: Transform,
     pub properties: ModelProperties,
@@ -656,6 +644,18 @@ impl Default for ModelProperties {
     }
 }
 
+#[derive(Default, Debug, Serialize, Deserialize, Clone)]
+pub struct SceneConfig {
+    pub scene_name: String,
+    pub entities: Vec<SceneEntity>,
+    pub camera: HashMap<CameraType, SceneCameraConfig>, // TODO: Change to component
+    pub lights: Vec<LightConfig>,
+    // todo later
+    // pub settings: SceneSettings,
+    #[serde(skip)]
+    pub path: PathBuf,
+}
+
 impl SceneConfig {
     /// Creates a new instance of the scene config
     pub fn new(scene_name: String, path: PathBuf) -> Self {
@@ -733,14 +733,29 @@ impl SceneConfig {
         );
         world.clear();
 
+        let project_config = if cfg!(feature = "data-only") {
+            if let Ok(cfg) = PROJECT.read() {
+                cfg.project_path.clone()
+            } else {
+                PathBuf::new()
+            }
+        } else {
+            PathBuf::new()
+        };
+
         log::info!("World cleared, now has {} entities", world.len());
 
         for entity_config in &self.entities {
             log::debug!("Loading entity: {}", entity_config.label);
+            let model_path = if !cfg!(feature = "data-only") {
+                entity_config.model_path.to_project_path(project_config.clone())
+            } else {
+                entity_config.model_path.to_executable_path()?
+            };
 
             let adopted = AdoptedEntity::new(
                 graphics,
-                &entity_config.model_path,
+                &model_path,
                 Some(&entity_config.label),
             )?;
 
@@ -803,7 +818,7 @@ impl SceneConfig {
                 debug_config.fov as f64,
                 debug_config.near as f64,
                 debug_config.far as f64,
-                0.125,
+                5.0,
                 0.002,
             );
             let debug_controller = Box::new(DebugCameraController::new());
@@ -820,7 +835,7 @@ impl SceneConfig {
                 player_config.fov as f64,
                 player_config.near as f64,
                 player_config.far as f64,
-                0.1,
+                5.0,
                 0.001,
             );
             let player_controller = Box::new(PlayerCameraController::new());
@@ -835,7 +850,7 @@ impl SceneConfig {
                         "World entity {:?} -> label='{}' path='{}'",
                         entity_id,
                         adopted_entity.label(),
-                        adopted_entity.model().path.display()
+                        adopted_entity.model().path
                     );
                 }
 
@@ -846,13 +861,22 @@ impl SceneConfig {
                         if adopted_entity.label() == target_label {
                             Some(entity_id)
                         } else {
-                            let stem_match = adopted_entity
-                                .model()
-                                .path
-                                .file_stem()
+                            let stem_match = if cfg!(feature = "data-only") {
+                                  adopted_entity.model().path.to_executable_path().unwrap()
+                            } else {
+                                let project_path = if let Ok(cfg) = PROJECT.read() {
+                                    cfg.project_path.clone()
+                                } else {
+                                    panic!("Unable to get project path to use with camera manager");
+                                };
+                                adopted_entity.model().path.to_project_path(project_path)
+                            };
+
+                            let stem_match = stem_match.file_stem()
                                 .and_then(|s| s.to_str())
                                 .map(|s| s == target_label)
                                 .unwrap_or(false);
+
                             if stem_match {
                                 Some(entity_id)
                             } else {
diff --git a/resources/grey.png b/resources/grey.png
new file mode 100644
index 0000000..0ca3354
Binary files /dev/null and b/resources/grey.png differ
diff --git a/resources/proto.png b/resources/proto.png
new file mode 100644
index 0000000..3fd2e56
Binary files /dev/null and b/resources/proto.png differ