Skip to main content

nightshade_api/
scene.rs

1//! Retained scene content: primitives, models, hierarchy, and the [`Object`]
2//! descriptor for spawning shape, color, and physics in one call.
3
4use crate::palette::WHITE;
5use crate::runner::MATERIAL_PREFIX;
6use nightshade::prelude::nalgebra_glm::Mat4;
7use nightshade::prelude::*;
8
9pub use nightshade::prelude::despawn_recursive_immediate as despawn;
10pub use nightshade::prelude::spawn_cone_at as spawn_cone;
11pub use nightshade::prelude::spawn_cube_at as spawn_cube;
12pub use nightshade::prelude::spawn_cylinder_at as spawn_cylinder;
13pub use nightshade::prelude::spawn_plane_at as spawn_plane;
14pub use nightshade::prelude::spawn_sphere_at as spawn_sphere;
15pub use nightshade::prelude::spawn_torus_at as spawn_torus;
16
17/// The primitive shapes [`spawn_object`] can produce.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Shape {
20    #[default]
21    Cube,
22    Sphere,
23    Cylinder,
24    Cone,
25    Torus,
26    Plane,
27}
28
29/// Physics participation for [`spawn_object`]. Requires the `physics`
30/// feature, which is on by default.
31///
32/// Dynamic cubes, spheres, cylinders, and cones get exact colliders. A
33/// dynamic torus gets a convex hull of its mesh, which fills the hole. Static
34/// toruses and planes collide against the exact triangle mesh.
35#[derive(Debug, Clone, Copy, PartialEq, Default)]
36pub enum Body {
37    #[default]
38    None,
39    Static,
40    Dynamic {
41        mass: f32,
42    },
43}
44
45/// Everything about one object in a single struct literal.
46///
47/// ```ignore
48/// let ball = spawn_object(world, Object {
49///     shape: Shape::Sphere,
50///     position: vec3(0.0, 4.0, 0.0),
51///     color: RED,
52///     body: Body::Dynamic { mass: 2.0 },
53///     ..Object::default()
54/// });
55/// ```
56pub struct Object {
57    pub shape: Shape,
58    pub position: Vec3,
59    pub scale: Vec3,
60    pub color: [f32; 4],
61    pub body: Body,
62}
63
64impl Default for Object {
65    fn default() -> Self {
66        Self {
67            shape: Shape::Cube,
68            position: Vec3::zeros(),
69            scale: Vec3::new(1.0, 1.0, 1.0),
70            color: WHITE,
71            body: Body::None,
72        }
73    }
74}
75
76/// Spawns an [`Object`]: mesh, color, and optional physics body in one call.
77pub fn spawn_object(world: &mut World, object: Object) -> Entity {
78    let entity = spawn_mesh_at(
79        world,
80        mesh_name(object.shape),
81        object.position,
82        object.scale,
83    );
84    crate::appearance::set_color(world, entity, object.color);
85    match object.body {
86        Body::None => {}
87        #[cfg(feature = "physics")]
88        Body::Static => {
89            let collider = static_collider(world, object.shape, object.scale)
90                .with_friction(0.8)
91                .with_restitution(0.1);
92            attach_body(
93                world,
94                entity,
95                RigidBodyComponent::new_static().with_translation(
96                    object.position.x,
97                    object.position.y,
98                    object.position.z,
99                ),
100                collider,
101                false,
102            );
103        }
104        #[cfg(feature = "physics")]
105        Body::Dynamic { mass } => {
106            let collider = dynamic_collider(world, object.shape, object.scale)
107                .with_friction(0.7)
108                .with_restitution(0.2);
109            attach_body(
110                world,
111                entity,
112                RigidBodyComponent::new_dynamic()
113                    .with_translation(object.position.x, object.position.y, object.position.z)
114                    .with_mass(mass),
115                collider,
116                true,
117            );
118        }
119        #[cfg(not(feature = "physics"))]
120        Body::Static | Body::Dynamic { .. } => {}
121    }
122    entity
123}
124
125/// Spawns a flat ground plane reaching `half_extent` in each direction. With
126/// the `physics` feature it carries a static collider so dynamic objects land
127/// on it.
128pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
129    let entity = spawn_mesh_at(
130        world,
131        "Plane",
132        Vec3::zeros(),
133        Vec3::new(half_extent, 1.0, half_extent),
134    );
135    #[cfg(feature = "physics")]
136    attach_body(
137        world,
138        entity,
139        RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
140        ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
141            .with_friction(0.8)
142            .with_restitution(0.1),
143        false,
144    );
145    entity
146}
147
148/// Spawns a glb model with its textures, materials, skins, and animations.
149/// Panics with the import error when the bytes are not a valid glb.
150pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
151    let mut result =
152        import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
153    nightshade::ecs::loading::queue_gltf_load(world, &mut result);
154    let prefab = &result.prefabs[0];
155    nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
156        world,
157        prefab,
158        &result.animations,
159        &result.skins,
160        position,
161    )
162}
163
164/// Starts playing the model's animation clip at `clip_index`.
165pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
166    if let Some(player) = world.core.get_animation_player_mut(entity) {
167        player.play(clip_index);
168    }
169}
170
171/// Sets whether the entity's current animation repeats.
172pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
173    if let Some(player) = world.core.get_animation_player_mut(entity) {
174        player.looping = looping;
175    }
176}
177
178/// Parents `child` to `parent`, or unparents it with `None`, keeping the
179/// child exactly where it is in world space.
180pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
181    let child_world = crate::placement::world_matrix(world, child);
182    let parent_world = parent
183        .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
184        .unwrap_or_else(Mat4::identity);
185    let local = nalgebra_glm::inverse(&parent_world) * child_world;
186
187    let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
188    let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
189    let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
190    let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
191    let scale = nalgebra_glm::vec3(
192        basis_x.magnitude(),
193        basis_y.magnitude(),
194        basis_z.magnitude(),
195    );
196    let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
197        basis_x / scale.x.max(f32::EPSILON),
198        basis_y / scale.y.max(f32::EPSILON),
199        basis_z / scale.z.max(f32::EPSILON),
200    ]);
201    let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
202
203    if parent.is_some() {
204        world.core.add_components(child, PARENT);
205    }
206    update_parent(
207        world,
208        child,
209        parent.map(|parent_entity| Parent(Some(parent_entity))),
210    );
211    assign_local_transform(
212        world,
213        child,
214        LocalTransform {
215            translation,
216            rotation,
217            scale,
218        },
219    );
220}
221
222#[cfg(feature = "picking")]
223pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
224    world
225        .core
226        .get_name(entity)
227        .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
228}
229
230pub(crate) fn api_material_name(entity: Entity) -> String {
231    format!("{MATERIAL_PREFIX}{}", entity.id)
232}
233
234fn mesh_name(shape: Shape) -> &'static str {
235    match shape {
236        Shape::Cube => "Cube",
237        Shape::Sphere => "Sphere",
238        Shape::Cylinder => "Cylinder",
239        Shape::Cone => "Cone",
240        Shape::Torus => "Torus",
241        Shape::Plane => "Plane",
242    }
243}
244
245#[cfg(feature = "physics")]
246fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
247    match shape {
248        Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
249        Shape::Sphere => ColliderComponent::new_ball(scale.x),
250        Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
251        Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
252        Shape::Torus => ColliderComponent {
253            shape: ColliderShape::ConvexMesh {
254                vertices: scaled_mesh_points(world, "Torus", scale),
255            },
256            ..Default::default()
257        },
258        Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
259    }
260}
261
262#[cfg(feature = "physics")]
263fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
264    match shape {
265        Shape::Torus | Shape::Plane => {
266            let shape_mesh_name = mesh_name(shape);
267            ColliderComponent {
268                shape: ColliderShape::TriMesh {
269                    vertices: scaled_mesh_points(world, shape_mesh_name, scale),
270                    indices: mesh_triangles(world, shape_mesh_name),
271                },
272                ..Default::default()
273            }
274        }
275        _ => dynamic_collider(world, shape, scale),
276    }
277}
278
279#[cfg(feature = "physics")]
280fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
281    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
282        .map(|mesh| {
283            mesh.vertices
284                .iter()
285                .map(|vertex| {
286                    [
287                        vertex.position[0] * scale.x,
288                        vertex.position[1] * scale.y,
289                        vertex.position[2] * scale.z,
290                    ]
291                })
292                .collect()
293        })
294        .unwrap_or_default()
295}
296
297#[cfg(feature = "physics")]
298fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
299    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
300        .map(|mesh| {
301            mesh.indices
302                .chunks_exact(3)
303                .map(|triangle| [triangle[0], triangle[1], triangle[2]])
304                .collect()
305        })
306        .unwrap_or_default()
307}
308
309#[cfg(feature = "physics")]
310fn attach_body(
311    world: &mut World,
312    entity: Entity,
313    body: RigidBodyComponent,
314    collider: ColliderComponent,
315    dynamic: bool,
316) {
317    let mut flags = RIGID_BODY | COLLIDER;
318    if dynamic {
319        flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
320    }
321    world.core.add_components(entity, flags);
322    world.core.set_rigid_body(entity, body);
323    world.core.set_collider(entity, collider);
324    if dynamic {
325        reset_physics_interpolation(world, entity);
326        if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
327            interpolation.enabled = true;
328        }
329    }
330}