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 one [`Object`] at each position, all sharing a single registered
126/// material, which is what writing it longhand for a crowd looks like. A
127/// thousand entities through this hold one material entry, not a thousand.
128pub fn spawn_objects(world: &mut World, object: Object, positions: &[Vec3]) -> Vec<Entity> {
129    let mut entities = Vec::with_capacity(positions.len());
130    let mut shared_material: Option<String> = None;
131    #[cfg(feature = "physics")]
132    let mut collider_template: Option<ColliderComponent> = None;
133
134    for &position in positions {
135        let entity = spawn_mesh_at(world, mesh_name(object.shape), position, object.scale);
136        match shared_material.as_deref() {
137            None => {
138                crate::appearance::set_color(world, entity, object.color);
139                shared_material = world
140                    .core
141                    .get_material_ref(entity)
142                    .map(|material_ref| material_ref.name.clone());
143            }
144            Some(name) => {
145                let name = name.to_string();
146                adopt_shared_material(world, entity, &name);
147            }
148        }
149
150        #[cfg(feature = "physics")]
151        match object.body {
152            Body::None => {}
153            Body::Static => {
154                let collider = collider_template
155                    .get_or_insert_with(|| {
156                        static_collider(world, object.shape, object.scale)
157                            .with_friction(0.8)
158                            .with_restitution(0.1)
159                    })
160                    .clone();
161                attach_body(
162                    world,
163                    entity,
164                    RigidBodyComponent::new_static()
165                        .with_translation(position.x, position.y, position.z),
166                    collider,
167                    false,
168                );
169            }
170            Body::Dynamic { mass } => {
171                let collider = collider_template
172                    .get_or_insert_with(|| {
173                        dynamic_collider(world, object.shape, object.scale)
174                            .with_friction(0.7)
175                            .with_restitution(0.2)
176                    })
177                    .clone();
178                attach_body(
179                    world,
180                    entity,
181                    RigidBodyComponent::new_dynamic()
182                        .with_translation(position.x, position.y, position.z)
183                        .with_mass(mass),
184                    collider,
185                    true,
186                );
187            }
188        }
189
190        entities.push(entity);
191    }
192    entities
193}
194
195/// Spawns one entity that renders `transforms.len()` copies of the shape in a
196/// single draw call, the cheapest way to put thousands of identical things on
197/// screen. Per copy control afterward goes through the entity's
198/// `InstancedMesh` component.
199pub fn spawn_instanced(
200    world: &mut World,
201    shape: Shape,
202    transforms: Vec<InstanceTransform>,
203    color: [f32; 4],
204) -> Entity {
205    let shape_mesh_name = mesh_name(shape);
206    ensure_primitive_mesh(world, shape_mesh_name);
207    let fallback = format!(
208        "api::material::instanced::{:.4}_{:.4}_{:.4}_{:.4}",
209        color[0], color[1], color[2], color[3]
210    );
211    let material_name = nightshade::ecs::material::resources::material_registry_find_or_insert(
212        &mut world.resources.assets.material_registry,
213        fallback,
214        Material {
215            base_color: color,
216            ..Default::default()
217        },
218    );
219    spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
220}
221
222fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
223    let previous = world
224        .core
225        .get_material_ref(entity)
226        .map(|material_ref| material_ref.name.clone());
227    if let Some(previous_name) = previous
228        && let Some((index, _)) = registry_lookup_index(
229            &world.resources.assets.material_registry.registry,
230            &previous_name,
231        )
232    {
233        registry_remove_reference(
234            &mut world.resources.assets.material_registry.registry,
235            index,
236        );
237    }
238    if let Some((index, _)) =
239        registry_lookup_index(&world.resources.assets.material_registry.registry, name)
240    {
241        registry_add_reference(
242            &mut world.resources.assets.material_registry.registry,
243            index,
244        );
245    }
246    world
247        .core
248        .set_material_ref(entity, MaterialRef::new(name.to_string()));
249    world.resources.mesh_render_state.mark_entity_added(entity);
250}
251
252pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
253    use nightshade::ecs::mesh::components::{
254        create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
255        create_sphere_mesh, create_torus_mesh,
256    };
257    if !world
258        .resources
259        .assets
260        .mesh_cache
261        .registry
262        .name_to_index
263        .contains_key(mesh_name)
264    {
265        let mesh = match mesh_name {
266            "Cube" => create_cube_mesh(),
267            "Sphere" => create_sphere_mesh(1.0, 16),
268            "Plane" => create_plane_mesh(2.0),
269            "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
270            "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
271            _ => create_cone_mesh(0.5, 1.0, 16),
272        };
273        mesh_cache_insert(
274            &mut world.resources.assets.mesh_cache,
275            mesh_name.to_string(),
276            mesh,
277        );
278    }
279    if let Some((index, _)) =
280        registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
281    {
282        registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
283    }
284}
285
286/// Spawns a simulated cloth sheet hanging from `position`, pinned along its
287/// top row. It drapes, collides with the ground, and responds to
288/// `world.resources.wind`. Color it like anything else with
289/// [`set_color`](crate::prelude::set_color).
290pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
291    spawn_cloth(
292        world,
293        Cloth {
294            width,
295            height,
296            ..Default::default()
297        },
298        position,
299        "Cloth".to_string(),
300    )
301}
302
303/// Shows or hides the entity without despawning it.
304pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
305    if let Some(visibility) = world.core.get_visibility_mut(entity) {
306        visibility.visible = visible;
307    }
308}
309
310/// Spawns a flat ground plane reaching `half_extent` in each direction. With
311/// the `physics` feature it carries a static collider so dynamic objects land
312/// on it.
313pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
314    let entity = spawn_mesh_at(
315        world,
316        "Plane",
317        Vec3::zeros(),
318        Vec3::new(half_extent, 1.0, half_extent),
319    );
320    #[cfg(feature = "physics")]
321    attach_body(
322        world,
323        entity,
324        RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
325        ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
326            .with_friction(0.8)
327            .with_restitution(0.1),
328        false,
329    );
330    entity
331}
332
333/// Spawns a glb model with its textures, materials, skins, and animations.
334/// Panics with the import error when the bytes are not a valid glb.
335pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
336    let mut result =
337        import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
338    nightshade::ecs::loading::queue_gltf_load(world, &mut result);
339    let prefab = &result.prefabs[0];
340    nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
341        world,
342        prefab,
343        &result.animations,
344        &result.skins,
345        position,
346    )
347}
348
349/// Starts playing the model's animation clip at `clip_index`.
350pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
351    if let Some(player) = world.core.get_animation_player_mut(entity) {
352        player.play(clip_index);
353    }
354}
355
356/// Sets whether the entity's current animation repeats.
357pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
358    if let Some(player) = world.core.get_animation_player_mut(entity) {
359        player.looping = looping;
360    }
361}
362
363/// Spawns an invisible group at `position` for building hierarchies. Parent
364/// things to it and move, rotate, or animate the group to drive them all.
365pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
366    let entity = spawn_entities(
367        world,
368        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
369        1,
370    )[0];
371    world.core.set_name(entity, Name("Group".to_string()));
372    assign_local_transform(
373        world,
374        entity,
375        LocalTransform {
376            translation: position,
377            ..Default::default()
378        },
379    );
380    entity
381}
382
383/// Parents `child` to `parent`, or unparents it with `None`, keeping the
384/// child exactly where it is in world space.
385pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
386    let child_world = crate::placement::world_matrix(world, child);
387    let parent_world = parent
388        .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
389        .unwrap_or_else(Mat4::identity);
390    let local = nalgebra_glm::inverse(&parent_world) * child_world;
391
392    let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
393    let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
394    let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
395    let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
396    let scale = nalgebra_glm::vec3(
397        basis_x.magnitude(),
398        basis_y.magnitude(),
399        basis_z.magnitude(),
400    );
401    let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
402        basis_x / scale.x.max(f32::EPSILON),
403        basis_y / scale.y.max(f32::EPSILON),
404        basis_z / scale.z.max(f32::EPSILON),
405    ]);
406    let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
407
408    if parent.is_some() {
409        world.core.add_components(child, PARENT);
410    }
411    update_parent(
412        world,
413        child,
414        parent.map(|parent_entity| Parent(Some(parent_entity))),
415    );
416    assign_local_transform(
417        world,
418        child,
419        LocalTransform {
420            translation,
421            rotation,
422            scale,
423        },
424    );
425}
426
427#[cfg(feature = "picking")]
428pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
429    world
430        .core
431        .get_name(entity)
432        .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
433}
434
435pub(crate) fn api_material_name(entity: Entity) -> String {
436    format!("{MATERIAL_PREFIX}{}", entity.id)
437}
438
439fn mesh_name(shape: Shape) -> &'static str {
440    match shape {
441        Shape::Cube => "Cube",
442        Shape::Sphere => "Sphere",
443        Shape::Cylinder => "Cylinder",
444        Shape::Cone => "Cone",
445        Shape::Torus => "Torus",
446        Shape::Plane => "Plane",
447    }
448}
449
450#[cfg(feature = "physics")]
451fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
452    match shape {
453        Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
454        Shape::Sphere => ColliderComponent::new_ball(scale.x),
455        Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
456        Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
457        Shape::Torus => ColliderComponent {
458            shape: ColliderShape::ConvexMesh {
459                vertices: scaled_mesh_points(world, "Torus", scale),
460            },
461            ..Default::default()
462        },
463        Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
464    }
465}
466
467#[cfg(feature = "physics")]
468fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
469    match shape {
470        Shape::Torus | Shape::Plane => {
471            let shape_mesh_name = mesh_name(shape);
472            ColliderComponent {
473                shape: ColliderShape::TriMesh {
474                    vertices: scaled_mesh_points(world, shape_mesh_name, scale),
475                    indices: mesh_triangles(world, shape_mesh_name),
476                },
477                ..Default::default()
478            }
479        }
480        _ => dynamic_collider(world, shape, scale),
481    }
482}
483
484#[cfg(feature = "physics")]
485fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
486    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
487        .map(|mesh| {
488            mesh.vertices
489                .iter()
490                .map(|vertex| {
491                    [
492                        vertex.position[0] * scale.x,
493                        vertex.position[1] * scale.y,
494                        vertex.position[2] * scale.z,
495                    ]
496                })
497                .collect()
498        })
499        .unwrap_or_default()
500}
501
502#[cfg(feature = "physics")]
503fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
504    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
505        .map(|mesh| {
506            mesh.indices
507                .chunks_exact(3)
508                .map(|triangle| [triangle[0], triangle[1], triangle[2]])
509                .collect()
510        })
511        .unwrap_or_default()
512}
513
514#[cfg(feature = "physics")]
515fn attach_body(
516    world: &mut World,
517    entity: Entity,
518    body: RigidBodyComponent,
519    collider: ColliderComponent,
520    dynamic: bool,
521) {
522    let mut flags = RIGID_BODY | COLLIDER;
523    if dynamic {
524        flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
525    }
526    world.core.add_components(entity, flags);
527    world.core.set_rigid_body(entity, body);
528    world.core.set_collider(entity, collider);
529    if dynamic {
530        reset_physics_interpolation(world, entity);
531        if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
532            interpolation.enabled = true;
533        }
534    }
535}