tirbofish/dropbear · commit
9a516a70c6df7fad670d49ea2cc5fb614a3f65e9
attempted to make a plane (didn't work, will try later), created easier pathing with "ResourceReferences" (though i do need to improve performance).
Signature present but could not be verified.
Unverified
@@ -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" @@ -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 @@ -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 { @@ -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, @@ -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(()) }) @@ -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(); @@ -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(), }) } } @@ -0,0 +1,3 @@ +//! Starter objects like planes, players and more + +pub mod plane; @@ -0,0 +1,9 @@ +pub struct Plane { + +} + +impl Plane { + pub fn new() { + + } +} @@ -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) + }; +} @@ -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(); } @@ -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; @@ -248,7 +248,7 @@ impl ScriptManager { script_name, entity_id, adopted.label(), - adopted.model().path.display() + adopted.model().path ); } } else { @@ -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 { Binary files /dev/null and b/resources/grey.png differ Binary files /dev/null and b/resources/proto.png differ