From eb4e0529eb26a82d99f398c74b970d84391551ae Mon Sep 17 00:00:00 2001 From: gituser12981u2 Date: Sun, 6 Apr 2025 15:28:02 -0700 Subject: [PATCH] feat!: add scene graph and higher level scene object API BREAKING CHANGE: now one must instantiate objects for the scene graph with scene object in scenes --- examples/simple_scene.rs | 122 +++++--- src/renderer/factory.rs | 197 ++++++++++++ src/renderer/mod.rs | 9 + src/renderer/render_core.rs | 99 +++++- src/renderer/scene_graph.rs | 574 +++++++++++++++++++++++++++++----- src/renderer/scene_objects.rs | 84 +++++ 6 files changed, 954 insertions(+), 131 deletions(-) create mode 100644 src/renderer/factory.rs create mode 100644 src/renderer/scene_objects.rs diff --git a/examples/simple_scene.rs b/examples/simple_scene.rs index 60a4f03..0060a86 100644 --- a/examples/simple_scene.rs +++ b/examples/simple_scene.rs @@ -1,64 +1,88 @@ +use std::time::Instant; + use env_logger::Builder; -use glam::{Mat4, Quat, Vec3}; +use glam::{Quat, Vec3}; use log::LevelFilter; -use render_engine::{Color, RendererSystem, ShapeBuilder}; -use std::time::Instant; +use render_engine::{renderer::ShapeFactory, Color, RendererSystem, ShapeBuilder}; fn main() -> Result<(), Box> { Builder::new().filter_level(LevelFilter::Debug).init(); - let mut renderer_system = RendererSystem::new(800, 600, "Metal Renderer")?; + let mut renderer_system = RendererSystem::new(800, 600, "Scene Graph Demo")?; let start_time = Instant::now(); - renderer_system.set_render_callback(move |r| { + let mut pyramid = None; + let mut cube = None; + + renderer_system.set_render_callback(move |renderer| { let elapsed = start_time.elapsed().as_secs_f32(); - // Define pyramid dimensions - let base_width = 1.0; - let height = 1.5; - let half_width = base_width / 2.0; + if pyramid.is_none() { + // Create a pyramid + let pyramid_vertices = vec![ + // Apex + (Vec3::new(0.0, 1.5, 0.0), Color::new(1.0, 0.0, 0.0, 1.0)), + // Base vertices + (Vec3::new(-0.5, 0.0, -0.5), Color::new(0.0, 1.0, 0.0, 1.0)), + (Vec3::new(0.5, 0.0, -0.5), Color::new(0.0, 0.0, 1.0, 1.0)), + (Vec3::new(0.5, 0.0, 0.5), Color::new(1.0, 1.0, 0.0, 1.0)), + (Vec3::new(-0.5, 0.0, 0.5), Color::new(0.0, 1.0, 1.0, 1.0)), + ]; + + let pyramid_indices = vec![ + 0, 1, 2, // Front face + 0, 2, 3, // Right face + 0, 3, 4, // Back face + 0, 4, 1, // Left face + 1, 3, 2, // Base (part 1) + 1, 4, 3, // Base (part 2) + ]; + + // Create the mesh builder and add to scene + let pyramid_mesh = renderer + .create_shape(pyramid_vertices) + .as_mesh() + .with_indices(pyramid_indices); + + pyramid = Some(renderer.create_object( + pyramid_mesh, + Vec3::new(-2.0, 0.0, 0.0), + Quat::IDENTITY, + Vec3::ONE, + )?); + + // Create a cube + cube = Some(ShapeFactory::create_cube( + renderer, + 1.0, + Color::new(0.2, 0.5, 0.8, 1.0), + Vec3::new(0.0, 0.5, 0.0), + Quat::IDENTITY, + Vec3::ONE, + )?); + } + + if let Some(ref pyramid_obj) = pyramid { + pyramid_obj.set_rotation(renderer, Quat::from_rotation_y(elapsed * 1.0))?; + } + + if let Some(ref obj) = cube { + obj.set_rotation( + renderer, + Quat::from_rotation_y(elapsed * 0.7) * Quat::from_rotation_x(elapsed * 0.5), + )?; + } - // Define pyramid vertices - let pyramid_vertices = vec![ - // Apex - (Vec3::new(0.0, height, 0.0), Color::new(1.0, 0.0, 0.0, 1.0)), - // Base vertices - ( - Vec3::new(-half_width, 0.0, -half_width), - Color::new(0.0, 1.0, 0.0, 1.0), - ), - ( - Vec3::new(half_width, 0.0, -half_width), - Color::new(0.0, 0.0, 1.0, 1.0), - ), - ( - Vec3::new(half_width, 0.0, half_width), - Color::new(1.0, 1.0, 0.0, 1.0), - ), - ( - Vec3::new(-half_width, 0.0, half_width), - Color::new(0.0, 1.0, 1.0, 1.0), - ), - ]; - // Define indices for the pyramid faces - let pyramid_indices = vec![ - 0, 1, 2, // Front face - 0, 2, 3, // Right face - 0, 3, 4, // Back face - 0, 4, 1, // Left face - 1, 3, 2, // Base (part 1) - 1, 4, 3, // Base (part 2) - ]; - r.create_shape(pyramid_vertices) - .as_mesh() - .with_indices(pyramid_indices) - .with_transform(Mat4::from_rotation_translation( - Quat::from_rotation_y(elapsed), - Vec3::new(0.0, -0.5, 0.0), - )) - .draw(r); + // r.create_shape(pyramid_vertices) + // .as_mesh() + // .with_indices(pyramid_indices) + // .with_transform(Mat4::from_rotation_translation( + // Quat::from_rotation_y(elapsed), + // Vec3::new(0.0, -0.5, 0.0), + // )) + // .draw(r); - r.render() + renderer.render() }); renderer_system.run()?; diff --git a/src/renderer/factory.rs b/src/renderer/factory.rs new file mode 100644 index 0000000..238ccad --- /dev/null +++ b/src/renderer/factory.rs @@ -0,0 +1,197 @@ +use glam::{Quat, Vec3}; +use std::f32::consts::PI; + +use crate::{ + renderer::{render_core::Renderer, scene_objects::SceneObject, Color, RendererError}, + ShapeBuilder, +}; + +/// Factory methods for creating common 3D shapes as scene objects +pub struct ShapeFactory; + +impl ShapeFactory { + /// Create a cube with the given properties + pub fn create_cube( + renderer: &mut Renderer, + size: f32, + color: Color, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result { + let half_size = size / 2.0; + + // Define the 8 vertices of the cube + let vertices = vec![ + // Front face + (Vec3::new(-half_size, -half_size, half_size), color), + (Vec3::new(half_size, -half_size, half_size), color), + (Vec3::new(half_size, half_size, half_size), color), + (Vec3::new(-half_size, half_size, half_size), color), + // Back face + (Vec3::new(-half_size, -half_size, -half_size), color), + (Vec3::new(half_size, -half_size, -half_size), color), + (Vec3::new(half_size, half_size, -half_size), color), + (Vec3::new(-half_size, half_size, -half_size), color), + ]; + + // Define indices for the triangles + let indices = vec![ + // Front face + 0, 1, 2, 0, 2, 3, // Back face + 4, 6, 5, 4, 7, 6, // Left face + 0, 3, 7, 0, 7, 4, // Right face + 1, 5, 6, 1, 6, 2, // Top face + 3, 2, 6, 3, 6, 7, // Bottom face + 0, 4, 5, 0, 5, 1, + ]; + + let mesh_builder = renderer + .create_shape(vertices) + .as_mesh() + .with_indices(indices); + + renderer.create_object(mesh_builder, position, rotation, scale) + } + + /// Create a sphere with the given properties + pub fn create_sphere( + renderer: &mut Renderer, + radius: f32, + segments: u32, + rings: u32, + color: Color, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result { + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + // Create vertices + for ring in 0..=rings { + let phi = PI * (ring as f32) / (rings as f32); + let sin_phi = phi.sin(); + let cos_phi = phi.cos(); + + for segment in 0..=segments { + let theta = 2.0 * PI * (segment as f32) / (segments as f32); + let sin_theta = theta.sin(); + let cos_theta = theta.cos(); + + let x = radius * sin_phi * cos_theta; + let y = radius * cos_phi; + let z = radius * sin_phi * sin_theta; + + vertices.push((Vec3::new(x, y, z), color)); + } + } + + // Create indices + for ring in 0..rings { + let ring_start = ring * (segments + 1); + let next_ring_start = (ring + 1) * (segments + 1); + + for segment in 0..segments { + // Upper triangle + indices.push(ring_start + segment); + indices.push(next_ring_start + segment); + indices.push(next_ring_start + segment + 1); + + // Lower triangle + indices.push(ring_start + segment); + indices.push(next_ring_start + segment + 1); + indices.push(ring_start + segment + 1); + } + } + + let mesh_builder = renderer + .create_shape(vertices) + .as_mesh() + .with_indices(indices); + + renderer.create_object(mesh_builder, position, rotation, scale) + } + + /// Create a cone with the given properties + pub fn create_cone( + renderer: &mut Renderer, + radius: f32, + height: f32, + segments: u32, + color: Color, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result { + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + // Apex vertex (at the top) + vertices.push((Vec3::new(0.0, height / 2.0, 0.0), color)); + + // Base vertices + for i in 0..segments { + let angle = 2.0 * PI * (i as f32) / (segments as f32); + let x = radius * angle.cos(); + let z = radius * angle.sin(); + vertices.push((Vec3::new(x, -height / 2.0, z), color)); + } + + // Side faces + for i in 0..segments { + let next = (i + 1) % segments; + indices.push(0); // Apex + indices.push(i + 1); + indices.push(next + 1); + } + + // Base faces (triangulate the base) + let center_idx = vertices.len(); + vertices.push((Vec3::new(0.0, -height / 2.0, 0.0), color)); + + for i in 0..segments { + let next = (i + 1) % segments; + indices.push(center_idx.try_into().unwrap()); + indices.push(next + 1); + indices.push(i + 1); + } + + let mesh_builder = renderer + .create_shape(vertices) + .as_mesh() + .with_indices(indices); + + renderer.create_object(mesh_builder, position, rotation, scale) + } + + /// Create a plane (flat rectangle) with the given properties + pub fn create_plane( + renderer: &mut Renderer, + width: f32, + depth: f32, + color: Color, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result { + let half_width = width / 2.0; + let half_depth = depth / 2.0; + + let vertices = vec![ + (Vec3::new(-half_width, 0.0, -half_depth), color), + (Vec3::new(half_width, 0.0, -half_depth), color), + (Vec3::new(half_width, 0.0, half_depth), color), + (Vec3::new(-half_width, 0.0, half_depth), color), + ]; + + let indices = vec![0, 2, 1, 0, 3, 2]; + + let mesh_builder = renderer + .create_shape(vertices) + .as_mesh() + .with_indices(indices); + + renderer.create_object(mesh_builder, position, rotation, scale) + } +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index e3b6b10..5b0b2ae 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -9,8 +9,11 @@ //! - `backend`: Handles the low-level graphics API interactions (e.g., Metal, Vulkan). //! - `camera`: Provides a camera system for 3D scene navigation and projection. //! - `common`: Contains common data structures and types used throughout the renderer. +//! - `factory`: Provides factory methods for creating common 3D shapes. //! - `render_core`: Implements the core rendering logic and system management. //! - `render_queue`: Handles the queuing and processing of draw commands. +//! - `scene_graph`: Manages the hierarchical organization of objects in the scene. +//! - `scene_objects`: Provides high-level API for managing scene objects. //! - `shape_builders`: Offers utilities for creating various 3D shapes programmatically. //! //! This module abstracts away much of the complexity of 3D rendering, providing a @@ -20,12 +23,18 @@ mod backend; mod camera; mod common; +pub mod factory; mod mesh; mod render_core; mod render_queue; +pub mod scene_graph; +pub mod scene_objects; pub mod shape_builders; pub use self::common::{Color, RendererError}; pub use camera::Camera; +pub use factory::ShapeFactory; pub use render_core::RendererSystem; pub use render_queue::{DrawCommandBuilder, InstanceData}; +pub use scene_graph::NodeId; +pub use scene_objects::SceneObject; diff --git a/src/renderer/render_core.rs b/src/renderer/render_core.rs index dfa9a13..4085d57 100644 --- a/src/renderer/render_core.rs +++ b/src/renderer/render_core.rs @@ -3,11 +3,12 @@ use super::{ common::{BackendDrawCommand, IndexType, PrimitiveType, Uniforms, Vertex}, mesh::{Mesh, MeshStorage}, render_queue::DrawCommand, + scene_graph::{NodeId, SceneGraph}, shape_builders::{ shape_builder::{vec3_color_to_vertex, ShapeData}, MeshBuilder, TriangleBuilder, }, - Camera, Color, RendererError, + Camera, Color, RendererError, SceneObject, }; use crate::{ debug_trace, @@ -17,7 +18,7 @@ use crate::{ render_queue::RenderQueue, }, }; -use glam::Vec3; +use glam::{Mat4, Quat, Vec3}; use log::info; use std::{cell::RefCell, mem, rc::Rc, time::Instant}; use winit::{ @@ -32,9 +33,9 @@ pub struct Renderer { backend: MetalBackend, mesh_storage: MeshStorage, render_queue: RenderQueue, - // TODO: implement Material Manager and Scene Graph + // TODO: implement Material Manager // material_manager: MaterialManager, - // scene_graph: SceneGraph, + scene_graph: SceneGraph, window: Window, camera: Camera, last_frame_time: std::time::Instant, @@ -68,6 +69,7 @@ impl Renderer { backend, mesh_storage: MeshStorage::new(), render_queue: RenderQueue::new(), + scene_graph: SceneGraph::new(), window, camera, last_frame_time: std::time::Instant::now(), @@ -77,6 +79,11 @@ impl Renderer { pub fn render(&mut self) -> Result<(), RendererError> { // TODO: sort batches in an efficient manner // TODO: Implement Frustum Culling + self.render_queue.draw_commands.clear(); + + // Generate a draw commands from the scene graph + self.scene_graph + .generate_draw_commands(&mut self.render_queue, &self.mesh_storage)?; let render_start = Instant::now(); debug_trace!("Starting render at {:?}", render_start); @@ -86,7 +93,7 @@ impl Renderer { // Implicitly clear the render queue by taking ownership of the draw commands let draw_commands = mem::take(&mut self.render_queue.draw_commands); - debug_trace!("Clearing RenderQueue at {:?}", Instant::now()); + debug_trace!("Processing {} draw commands", draw_commands.len()); for draw_command in draw_commands { match &draw_command { @@ -251,6 +258,61 @@ impl Renderer { self.render_queue.add_draw_command(draw_command); } + pub fn create_node(&mut self) -> NodeId { + self.scene_graph.create_node() + } + + pub fn create_mesh_node(&mut self, mesh_id: usize) -> NodeId { + self.scene_graph.create_mesh_node(mesh_id) + } + + pub fn set_node_parent( + &mut self, + node_id: NodeId, + parent_id: Option, + ) -> Result<(), RendererError> { + self.scene_graph.set_parent(node_id, parent_id) + } + + pub fn set_node_transform( + &mut self, + node_id: NodeId, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result<(), RendererError> { + self.scene_graph + .set_transform(node_id, position, rotation, scale) + } + + pub fn set_node_mesh( + &mut self, + node_id: NodeId, + mesh_id: Option, + ) -> Result<(), RendererError> { + self.scene_graph.set_mesh(node_id, mesh_id) + } + + pub fn set_node_color(&mut self, node_id: NodeId, color: Color) -> Result<(), RendererError> { + self.scene_graph.set_color(node_id, color) + } + + pub fn set_node_visible( + &mut self, + node_id: NodeId, + visible: bool, + ) -> Result<(), RendererError> { + self.scene_graph.set_visible(node_id, visible) + } + + pub fn remove_node(&mut self, node_id: NodeId, recursive: bool) -> Result<(), RendererError> { + self.scene_graph.remove_node(node_id, recursive) + } + + pub fn get_node_world_transform(&self, node_id: NodeId) -> Result { + self.scene_graph.get_world_transform(node_id) + } + // TODO: implement resize in the backend pub fn resize(&mut self, new_size: PhysicalSize) { self.camera @@ -278,6 +340,27 @@ impl Renderer { .collect(); ShapeData::new(vertices, PrimitiveType::Triangle) } + + /// Creates a complete object with mesh and adds it to the scene + pub fn create_object( + &mut self, + mesh_builder: MeshBuilder, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result { + // Add the mesh to storage + let mesh_id = self.add_mesh(mesh_builder); + + // Create a node for it + let node_id = self.create_mesh_node(mesh_id); + + // Set its transform + self.set_node_transform(node_id, position, rotation, scale)?; + + // Return a SceneObject wrapping the node + Ok(SceneObject::from_node_id(node_id)) + } } pub type RenderCallback = dyn Fn(&mut Renderer) -> Result<(), RendererError>; @@ -285,7 +368,7 @@ pub type RenderCallback = dyn Fn(&mut Renderer) -> Result<(), RendererError>; pub struct RendererSystem { renderer: Rc>, event_loop: EventLoop<()>, - render_callback: Box, + render_callback: Box Result<(), RendererError>>, } /// Configuration options for the renderer @@ -339,12 +422,12 @@ impl RendererSystem { pub fn set_render_callback(&mut self, callback: F) where - F: Fn(&mut Renderer) -> Result<(), RendererError> + 'static, + F: FnMut(&mut Renderer) -> Result<(), RendererError> + 'static, { self.render_callback = Box::new(callback); } - pub fn run(self) -> Result<(), RendererError> { + pub fn run(mut self) -> Result<(), RendererError> { let window_size = self.renderer.borrow().window.inner_size(); let center_x = window_size.width as f64 / 2.0; let center_y = window_size.height as f64 / 2.0; diff --git a/src/renderer/scene_graph.rs b/src/renderer/scene_graph.rs index e22455a..399adca 100644 --- a/src/renderer/scene_graph.rs +++ b/src/renderer/scene_graph.rs @@ -1,74 +1,500 @@ -// use super::{material_manager::MaterialId, mesh_manager::MeshId, render_core::ObjectId}; -// use glam::{Mat4, Quat, Vec3}; - -// pub struct SceneGraph { -// object_ids: Vec, -// mesh_ids: Vec, -// material_ids: Vec, -// positions: Vec, -// rotations: Vec, -// scales: Vec, -// // colors: Vec, -// // pub mesh_indices: Vec, -// } - -// impl SceneGraph { -// pub fn new() -> Self { -// SceneGraph { -// object_ids: Vec::new(), -// mesh_ids: Vec::new(), -// material_ids: Vec::new(), -// positions: Vec::new(), -// rotations: Vec::new(), -// scales: Vec::new(), -// } -// } - -// pub fn add_object( -// &mut self, -// mesh_id: MeshId, -// material_id: MaterialId, -// transform: Mat4, -// ) -> ObjectId { -// let object_id = ObjectId(self.object_ids.len()); - -// let (scale, rotation, position) = transform.to_scale_rotation_translation(); - -// self.object_ids.push(object_id); -// self.mesh_ids.push(mesh_id); -// self.material_ids.push(material_id); -// self.positions.push(position); -// self.rotations.push(rotation); -// self.scales.push(scale); - -// object_id -// } - -// pub fn update_object(&mut self, object_id: ObjectId, transform: Mat4) { -// let index = self -// .object_ids -// .iter() -// .position(|&id| id == object_id) -// .unwrap(); -// let (scale, rotation, position) = transform.to_scale_rotation_translation(); - -// self.positions[index] = position; -// self.rotations[index] = rotation; -// self.scales[index] = scale; -// } - -// pub fn remove_object(&mut self, object_id: ObjectId) { -// let index = self -// .object_ids -// .iter() -// .position(|&id| id == object_id) -// .unwrap(); - -// self.object_ids.swap_remove(index); -// self.mesh_ids.swap_remove(index); -// self.material_ids.swap_remove(index); -// self.positions.swap_remove(index); -// self.rotations.swap_remove(index); -// self.scales.swap_remove(index); -// } -// } +use super::{ + mesh::MeshStorage, render_queue::RenderQueue, Color, DrawCommandBuilder, RendererError, +}; +use glam::{Mat4, Quat, Vec3}; +use log::{debug, trace}; + +/// Node ID for the scene graph +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct NodeId(pub usize); + +/// Represents a node in the scene graph. +pub struct SceneNode { + /// The unique ID of this node + pub id: NodeId, + /// The parent node ID, if any + pub parent: Option, + /// The mesh ID associated with this node, if any + pub mesh_id: Option, + /// The local position relative to the parent + pub position: Vec3, + /// The local rotation relative to the parent + pub rotation: Quat, + /// The local scale relative to the parent + pub scale: Vec3, + /// The color of this node + pub color: Color, + /// Children of this node + pub children: Vec, + /// Whether this node is visible + pub visible: bool, +} + +impl SceneNode { + /// Creates a new scene node + pub fn new(id: NodeId) -> Self { + Self { + id, + parent: None, + mesh_id: None, + position: Vec3::ZERO, + rotation: Quat::IDENTITY, + scale: Vec3::ONE, + color: Color::new(1.0, 1.0, 1.0, 1.0), + children: Vec::new(), + visible: true, + } + } + + /// Calculate the local transformation matrix for this node + pub fn local_transform(&self) -> Mat4 { + Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position) + } +} + +/// Manages a hierarchical scene graph of objects +pub struct SceneGraph { + /// All nodes in the scene graph + nodes: Vec, + /// Root nodes + root_nodes: Vec, +} + +impl SceneGraph { + pub fn new() -> Self { + debug!("Creating new SceneGraph"); + Self { + nodes: Vec::new(), + root_nodes: Vec::new(), + } + } + + /// Creates a new node in the scene graph + pub fn create_node(&mut self) -> NodeId { + let id = NodeId(self.nodes.len()); + let node = SceneNode::new(id); + self.nodes.push(node); + self.root_nodes.push(id); + debug!("Created new node with ID {:?}", id); + id + } + + /// Creates a new node with a mesh + pub fn create_mesh_node(&mut self, mesh_id: usize) -> NodeId { + let id = self.create_node(); + self.nodes[id.0].mesh_id = Some(mesh_id); + id + } + + /// Sets teh parent of a node + pub fn set_parent( + &mut self, + node_id: NodeId, + parent_id: Option, + ) -> Result<(), RendererError> { + // Ensure node exists + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + // If the node already has a parent, remove it from the parent's children + if let Some(old_parent) = self.nodes[node_id.0].parent { + if let Some(parent_idx) = self.nodes.iter().position(|n| n.id == old_parent) { + let children = &mut self.nodes[parent_idx].children; + if let Some(idx) = children.iter().position(|&id| id == node_id) { + children.swap_remove(idx); + } + } + } else { + // If it was a root node, remove from root_nodes + if let Some(idx) = self.root_nodes.iter().position(|&id| id == node_id) { + self.root_nodes.swap_remove(idx); + } + } + + // Update parent reference + self.nodes[node_id.0].parent = parent_id; + + // Add to new parent's children if provided, or add to root_nodes if None + if let Some(parent_id) = parent_id { + if parent_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + // Prevent circular reference + if self.would_create_cycle(node_id, parent_id) { + self.nodes[node_id.0].parent = None; + self.root_nodes.push(node_id); + return Err(RendererError::InvalidMeshId); + } + + self.nodes[parent_id.0].children.push(node_id); + } else { + // If no parent, add to root nodes + self.root_nodes.push(node_id); + } + + trace!("Set parent of node {:?} to {:?}", node_id, parent_id); + Ok(()) + } + + /// Checks if setting a parent would create a cycle in the graph + fn would_create_cycle(&self, node_id: NodeId, parent_id: NodeId) -> bool { + // If the node that is to be set as parent is the same as the node, then it is a cycle + if node_id == parent_id { + return true; + } + + // Check if the potential parent is a child of the node + let mut to_check = vec![node_id]; + while let Some(current) = to_check.pop() { + let children = &self.nodes[current.0].children; + for &child in children { + if child == parent_id { + return true; + } + to_check.push(child); + } + } + + false + } + + /// Sets the affine transformation of a node + pub fn set_transform( + &mut self, + node_id: NodeId, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result<(), RendererError> { + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + let node = &mut self.nodes[node_id.0]; + node.position = position; + node.rotation = rotation; + node.scale = scale; + + trace!("Set transform of node {:?}", node_id); + Ok(()) + } + + /// Sets the mesh ID for a node + pub fn set_mesh( + &mut self, + node_id: NodeId, + mesh_id: Option, + ) -> Result<(), RendererError> { + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + self.nodes[node_id.0].mesh_id = mesh_id; + trace!("Set mesh of node {:?} to {:?}", node_id, mesh_id); + Ok(()) + } + + /// Sets the color for a node + pub fn set_color(&mut self, node_id: NodeId, color: Color) -> Result<(), RendererError> { + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + self.nodes[node_id.0].color = color; + trace!("Set color of node {:?} to {:?}", node_id, color); + Ok(()) + } + + /// Sets teh visibility of a node and its children + pub fn set_visible(&mut self, node_id: NodeId, visible: bool) -> Result<(), RendererError> { + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + self.nodes[node_id.0].visible = visible; + trace!("Set visibility of node {:?} tp {visible}", node_id); + Ok(()) + } + + /// Calculate the world transform for a node + pub fn get_world_transform(&self, node_id: NodeId) -> Result { + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + let mut transform = self.nodes[node_id.0].local_transform(); + let mut current_node = &self.nodes[node_id.0]; + + // Apply parent transforms + while let Some(parent_id) = current_node.parent { + let parent = &self.nodes[parent_id.0]; + transform = parent.local_transform() * transform; + current_node = parent; + } + + Ok(transform) + } + + /// Removes a node and optionally all its children + pub fn remove_node(&mut self, node_id: NodeId, recursive: bool) -> Result<(), RendererError> { + if node_id.0 >= self.nodes.len() { + return Err(RendererError::InvalidMeshId); + } + + // First collect nodes to remove + let mut to_remove = vec![node_id]; + + if recursive { + let mut i = 0; + while i < to_remove.len() { + let children = self.nodes[to_remove[i].0].children.clone(); + i += 1; + } + } else { + // Reparent children to this node's parent + let parent = self.nodes[node_id.0].parent; + for &child in &self.nodes[node_id.0].children.clone() { + self.set_parent(child, parent)?; + } + } + + // Remove from parent's children list + if let Some(parent_id) = self.nodes[node_id.0].parent { + let parent = &mut self.nodes[parent_id.0]; + if let Some(idx) = parent.children.iter().position(|&id| id == node_id) { + parent.children.swap_remove(idx); + } + } else { + // Remove from root nodes + if let Some(idx) = self.root_nodes.iter().position(|&id| id == node_id) { + self.root_nodes.swap_remove(idx); + } + } + + // Mark the nodes as "removed" by setting mesh_id to None + for &id in &to_remove { + self.nodes[id.0].visible = false; + self.nodes[id.0].mesh_id = None; + self.nodes[id.0].children.clear(); + } + + debug!("Removed node {:?} (recursive: {})", node_id, recursive); + Ok(()) + } + + /// Generate draw commands for the entire scene + pub fn generate_draw_commands( + &self, + render_queue: &mut RenderQueue, + mesh_storage: &MeshStorage, + ) -> Result<(), RendererError> { + debug!("Generating draw commands for scene graph"); + + // Process all root nodes + for &root_id in &self.root_nodes { + self.process_node(root_id, Mat4::IDENTITY, render_queue, mesh_storage)?; + } + + Ok(()) + } + + /// Recursively process a node and its children + fn process_node( + &self, + node_id: NodeId, + parent_transform: Mat4, + render_queue: &mut RenderQueue, + mesh_storage: &MeshStorage, + ) -> Result<(), RendererError> { + let node = &self.nodes[node_id.0]; + + if !node.visible { + return Ok(()); + } + + // Calculate combined transform + let local_transform = node.local_transform(); + let world_transform = parent_transform * local_transform; + + // if has mesh, add draw command + if let Some(mesh_id) = node.mesh_id { + if let Some(mesh) = mesh_storage.get_mesh(mesh_id) { + let command = DrawCommandBuilder::new_mesh(mesh_id) + .with_transform(world_transform) + .build(); + + render_queue.add_draw_command(command); + trace!( + "Added draw command for node {:?} with mesh {:?}", + node_id, + mesh_id + ); + } + } + + // Process children + for &child_id in &node.children { + self.process_node(child_id, world_transform, render_queue, mesh_storage)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use glam::{Quat, Vec3}; + + use crate::{ + renderer::{ + common::Vertex, + mesh::{Mesh, MeshStorage}, + render_queue::{DrawCommand, RenderQueue}, + scene_graph::SceneGraph, + }, + MeshBuilder, + }; + + fn create_test_mesh() -> Mesh { + let vertices = vec![ + Vertex { + position: [0.0, 0.5, 0.0], + color: [1.0, 0.0, 0.0, 1.0], + }, + Vertex { + position: [-0.5, -0.5, 0.0], + color: [0.0, 1.0, 0.0, 1.0], + }, + Vertex { + position: [0.5, -0.5, 0.0], + color: [0.0, 0.0, 1.0, 1.0], + }, + ]; + + let mesh_builder = + MeshBuilder::new(vertices, crate::renderer::common::PrimitiveType::Triangle); + Mesh::new(mesh_builder) + } + + #[test] + fn test_create_node() { + let mut graph = SceneGraph::new(); + let node_id = graph.create_node(); + + assert_eq!(node_id.0, 0); + assert_eq!(graph.nodes.len(), 1); + assert_eq!(graph.root_nodes.len(), 1); + assert_eq!(graph.root_nodes[0], node_id); + } + + #[test] + fn test_parent_child_relationship() { + let mut graph = SceneGraph::new(); + let parent_id = graph.create_node(); + let child_id = graph.create_node(); + + assert_eq!(graph.root_nodes.len(), 2); + + graph.set_parent(child_id, Some(parent_id)).unwrap(); + + assert_eq!(graph.nodes[child_id.0].parent, Some(parent_id)); + assert!(graph.nodes[parent_id.0].children.contains(&child_id)); + assert_eq!(graph.root_nodes.len(), 1); + assert_eq!(graph.root_nodes[0], parent_id); + } + + #[test] + fn test_world_transform() { + let mut graph = SceneGraph::new(); + let parent_id = graph.create_node(); + let child_id = graph.create_node(); + + // Parent at (1, 0, 0) with identity rotation and scale + graph + .set_transform( + parent_id, + Vec3::new(1.0, 0.0, 0.0), + Quat::IDENTITY, + Vec3::ONE, + ) + .unwrap(); + + // Child at (0, 1, 0) relative to parent + graph + .set_transform( + child_id, + Vec3::new(0.0, 1.0, 0.0), + Quat::IDENTITY, + Vec3::ONE, + ) + .unwrap(); + + // Set parent-child relationship + graph.set_parent(child_id, Some(parent_id)).unwrap(); + + // World transform of child be at (1, 1, 0) + let world_transform = graph.get_world_transform(child_id).unwrap(); + let world_pos = world_transform.transform_point3(Vec3::ZERO); + + assert!((world_pos - Vec3::new(1.0, 1.0, 0.0)).length() < 1e-15); + } + + #[test] + fn test_cycle_prevention() { + let mut graph = SceneGraph::new(); + let node1 = graph.create_node(); + let node2 = graph.create_node(); + let node3 = graph.create_node(); + + // Create a valid chain: node1 -> node2 -> node3 + graph.set_parent(node2, Some(node1)).unwrap(); + graph.set_parent(node3, Some(node2)).unwrap(); + + // Try to create an invalid cycle: node1 -> node2 -> node3 -> node1 + let result = graph.set_parent(node1, Some(node3)); + + // Should fail + assert!(result.is_err()); + + // Original structure should remain + assert_eq!(graph.nodes[node1.0].parent, None); + assert_eq!(graph.nodes[node2.0].parent, Some(node1)); + assert_eq!(graph.nodes[node3.0].parent, Some(node2)); + } + + #[test] + fn test_generate_draw_commands() { + let mut graph = SceneGraph::new(); + let mut mesh_storage = MeshStorage::new(); + let mut render_queue = RenderQueue::new(); + + // Add a test mesh to storage + let mesh = create_test_mesh(); + let mesh_id = + mesh_storage.add_mesh(MeshBuilder::new(mesh.vertices.clone(), mesh.primitive_type)); + + // Create a node with the mesh + let node_id = graph.create_mesh_node(mesh_id); + + // Generate draw commands + graph + .generate_draw_commands(&mut render_queue, &mesh_storage) + .unwrap(); + + // Should have one draw command + assert_eq!(render_queue.draw_commands.len(), 1); + + // Check it is the right type + match &render_queue.draw_commands[0] { + DrawCommand::Mesh { + mesh_id: cmd_mesh_id, + .. + } => { + assert_eq!(*cmd_mesh_id, mesh_id); + } + _ => panic!("Wrong draw command type"), + } + } +} diff --git a/src/renderer/scene_objects.rs b/src/renderer/scene_objects.rs new file mode 100644 index 0000000..e4901ec --- /dev/null +++ b/src/renderer/scene_objects.rs @@ -0,0 +1,84 @@ +use super::{render_core::Renderer, Color, NodeId, RendererError}; +use glam::{Quat, Vec3}; + +/// High level API wrapper around a scene node +pub struct SceneObject { + /// The node ID in the scene group + node_id: NodeId, +} + +impl SceneObject { + /// Create a new object from an existing node ID + pub(crate) fn from_node_id(node_id: NodeId) -> Self { + Self { node_id } + } + + /// Get the internal node ID + pub fn node_id(&self) -> NodeId { + self.node_id + } + + /// Set the position of this object + pub fn set_position( + &self, + renderer: &mut Renderer, + position: Vec3, + ) -> Result<(), RendererError> { + // Get current transform + let world_transform = renderer.get_node_world_transform(self.node_id)?; + let (scale, rotation, _) = world_transform.to_scale_rotation_translation(); + + // Update with new position + renderer.set_node_transform(self.node_id, position, rotation, scale) + } + + /// Set the rotation of this object + pub fn set_rotation( + &self, + renderer: &mut crate::renderer::render_core::Renderer, + rotation: Quat, + ) -> Result<(), RendererError> { + // Get current transform + let world_transform = renderer.get_node_world_transform(self.node_id)?; + let (scale, _, position) = world_transform.to_scale_rotation_translation(); + + // Update with new rotation + renderer.set_node_transform(self.node_id, position, rotation, scale) + } + + /// Set the scale of this object + pub fn set_scale(&self, renderer: &mut Renderer, scale: Vec3) -> Result<(), RendererError> { + // Get current transform + let world_transform = renderer.get_node_world_transform(self.node_id)?; + let (_, rotation, position) = world_transform.to_scale_rotation_translation(); + + // Update with new scale + renderer.set_node_transform(self.node_id, position, rotation, scale) + } + + pub fn set_transform( + &self, + renderer: &mut Renderer, + position: Vec3, + rotation: Quat, + scale: Vec3, + ) -> Result<(), RendererError> { + renderer.set_node_transform(self.node_id, position, rotation, scale) + } + + pub fn set_color(&self, renderer: &mut Renderer, color: Color) -> Result<(), RendererError> { + renderer.set_node_color(self.node_id, color) + } + + pub fn set_visible(&self, renderer: &mut Renderer, visible: bool) -> Result<(), RendererError> { + renderer.set_node_visible(self.node_id, visible) + } + + pub fn set_parent( + &self, + renderer: &mut Renderer, + parent: Option<&SceneObject>, + ) -> Result<(), RendererError> { + renderer.set_node_parent(self.node_id, parent.map(|p| p.node_id)) + } +}