//! Renderable scene components: [`MeshRenderer`] and [`PrimitiveShape`]. //! //! A [`MeshRenderer`] is the component that makes a scene entity show up in the //! 3D viewport: it pairs a mesh source with a [`Material`]. Stage 4 ships the //! built-in [`PrimitiveShape`] source (cube/sphere/plane) — lightweight and //! serializable, so the editor (and later scripts/AI agents) can author what an //! entity renders. Imported meshes attach later via a mesh-asset handle. use serde::{Deserialize, Serialize}; use super::{Material, Mesh}; use crate::math::{Aabb, Vec3}; /// A built-in primitive mesh an entity can render. /// /// This names a shape rather than embedding vertex data, so it stays tiny, /// serializable, and cheap to edit; the renderer resolves it to a (cached) /// [`Mesh`]/GPU buffer. #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, crate::reflect::ReflectEnum, )] pub enum PrimitiveShape { /// Unit cube centered at the origin. #[default] Cube, /// Unit-radius UV sphere. Sphere, /// A 1×1 ground plane on the XZ axes, facing `+Y`. Plane, } impl PrimitiveShape { /// All shapes, for building caches / editor menus. pub const ALL: [PrimitiveShape; 3] = [ PrimitiveShape::Cube, PrimitiveShape::Sphere, PrimitiveShape::Plane, ]; /// A human-readable label. pub fn label(self) -> &'static str { match self { PrimitiveShape::Cube => "Cube", PrimitiveShape::Sphere => "Sphere", PrimitiveShape::Plane => "Plane", } } /// Builds the CPU [`Mesh`] for this shape. pub fn mesh(self) -> Mesh { match self { PrimitiveShape::Cube => Mesh::cube(), PrimitiveShape::Sphere => Mesh::uv_sphere(1.0, 32, 16), PrimitiveShape::Plane => Mesh::plane(1.0), } } /// The object-space bounds of this shape, without building a mesh — used for /// ray-picking and culling. pub fn local_bounds(self) -> Aabb { let half = match self { PrimitiveShape::Cube => Vec3::splat(0.5), PrimitiveShape::Sphere => Vec3::ONE, PrimitiveShape::Plane => Vec3::new(0.5, 0.0, 0.5), }; Aabb::from_center_half_extents(Vec3::ZERO, half) } } /// Component: what an entity renders. /// /// Attach to a scene entity (via the ECS) to make it appear in a forward pass. /// Stage 4 sources the mesh from a [`PrimitiveShape`]; the [`Material`] is /// edited in the inspector. Both are serializable, supporting the engine's /// dual-editable (editor + script/AI) component goal. #[derive( Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, crate::reflect::Reflect, )] pub struct MeshRenderer { /// The mesh to draw. pub shape: PrimitiveShape, /// The surface material. pub material: Material, } impl MeshRenderer { /// A renderer for `shape` with the default material. pub fn new(shape: PrimitiveShape) -> Self { Self { shape, material: Material::default(), } } /// A renderer for `shape` with an explicit `material`. pub fn with_material(shape: PrimitiveShape, material: Material) -> Self { Self { shape, material } } } #[cfg(test)] mod tests { use super::*; use crate::math::Color; #[test] fn every_shape_builds_a_nonempty_mesh() { for shape in PrimitiveShape::ALL { let mesh = shape.mesh(); assert!(!mesh.vertices.is_empty(), "{shape:?} has no vertices"); assert!(mesh.triangle_count() > 0, "{shape:?} has no triangles"); } } #[test] fn mesh_renderer_round_trips_through_ron() { let mr = MeshRenderer::with_material( PrimitiveShape::Sphere, Material::metal(Color::rgb(0.2, 0.4, 0.8), 0.25), ); let ron = ron::to_string(&mr).unwrap(); let back: MeshRenderer = ron::from_str(&ron).unwrap(); assert_eq!(mr, back); } }