9eead719b0
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
4.0 KiB
Rust
135 lines
4.0 KiB
Rust
//! 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);
|
||
}
|
||
}
|