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::*;
8use serde::{Deserialize, Serialize};
9
10pub use nightshade::prelude::despawn_recursive_immediate as despawn;
11pub use nightshade::prelude::spawn_cone_at as spawn_cone;
12pub use nightshade::prelude::spawn_cube_at as spawn_cube;
13pub use nightshade::prelude::spawn_cylinder_at as spawn_cylinder;
14pub use nightshade::prelude::spawn_plane_at as spawn_plane;
15pub use nightshade::prelude::spawn_sphere_at as spawn_sphere;
16pub use nightshade::prelude::spawn_torus_at as spawn_torus;
17
18/// The primitive shapes [`spawn_object`] can produce.
19#[derive(
20    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, enum2schema::Schema,
21)]
22pub enum Shape {
23    #[default]
24    Cube,
25    Sphere,
26    Cylinder,
27    Cone,
28    Torus,
29    Plane,
30}
31
32/// Physics participation for [`spawn_object`]. Requires the `physics`
33/// feature, which is on by default.
34///
35/// Dynamic cubes, spheres, cylinders, and cones get exact colliders. A
36/// dynamic torus gets a convex hull of its mesh, which fills the hole. Static
37/// toruses and planes collide against the exact triangle mesh.
38#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, enum2schema::Schema)]
39pub enum Body {
40    #[default]
41    None,
42    Static,
43    Dynamic {
44        mass: f32,
45    },
46}
47
48/// Everything about one object in a single struct literal.
49///
50/// ```ignore
51/// let ball = spawn_object(world, Object {
52///     shape: Shape::Sphere,
53///     position: vec3(0.0, 4.0, 0.0),
54///     color: RED,
55///     body: Body::Dynamic { mass: 2.0 },
56///     ..Object::default()
57/// });
58/// ```
59pub struct Object {
60    pub shape: Shape,
61    pub position: Vec3,
62    pub scale: Vec3,
63    pub color: [f32; 4],
64    pub body: Body,
65}
66
67impl Default for Object {
68    fn default() -> Self {
69        Self {
70            shape: Shape::Cube,
71            position: Vec3::zeros(),
72            scale: Vec3::new(1.0, 1.0, 1.0),
73            color: WHITE,
74            body: Body::None,
75        }
76    }
77}
78
79/// Spawns an [`Object`]: mesh, color, and optional physics body in one call.
80pub fn spawn_object(world: &mut World, object: Object) -> Entity {
81    let entity = spawn_mesh_at(
82        world,
83        mesh_name(object.shape),
84        object.position,
85        object.scale,
86    );
87    crate::appearance::set_color(world, entity, object.color);
88    match object.body {
89        Body::None => {}
90        #[cfg(feature = "physics")]
91        Body::Static => {
92            let collider = static_collider(world, object.shape, object.scale)
93                .with_friction(0.8)
94                .with_restitution(0.1);
95            attach_body(
96                world,
97                entity,
98                RigidBodyComponent::new_static().with_translation(
99                    object.position.x,
100                    object.position.y,
101                    object.position.z,
102                ),
103                collider,
104                false,
105            );
106        }
107        #[cfg(feature = "physics")]
108        Body::Dynamic { mass } => {
109            let collider = dynamic_collider(world, object.shape, object.scale)
110                .with_friction(0.7)
111                .with_restitution(0.2);
112            attach_body(
113                world,
114                entity,
115                RigidBodyComponent::new_dynamic()
116                    .with_translation(object.position.x, object.position.y, object.position.z)
117                    .with_mass(mass),
118                collider,
119                true,
120            );
121        }
122        #[cfg(not(feature = "physics"))]
123        Body::Static | Body::Dynamic { .. } => {}
124    }
125    entity
126}
127
128/// Spawns a dynamic capsule physics body at `position`: a `radius` pill with a
129/// `half_height` cylindrical section, `mass`, and a linear RGBA `color`. The
130/// capsule is the upright character shape. The visual is a cylinder, the
131/// collider is the true capsule. Capsule is not a [`Shape`], so it has its own
132/// spawn rather than coming through [`spawn_object`].
133#[cfg(feature = "physics")]
134pub fn spawn_capsule_body(
135    world: &mut World,
136    position: Vec3,
137    half_height: f32,
138    radius: f32,
139    mass: f32,
140    color: [f32; 4],
141) -> Entity {
142    let scale = Vec3::new(radius * 2.0, (half_height + radius) * 2.0, radius * 2.0);
143    let entity = spawn_mesh_at(world, mesh_name(Shape::Cylinder), position, scale);
144    crate::appearance::set_color(world, entity, color);
145    let collider = ColliderComponent::new_capsule(half_height, radius)
146        .with_friction(0.7)
147        .with_restitution(0.2);
148    attach_body(
149        world,
150        entity,
151        RigidBodyComponent::new_dynamic()
152            .with_translation(position.x, position.y, position.z)
153            .with_mass(mass),
154        collider,
155        true,
156    );
157    entity
158}
159
160/// Spawns a static, collidable mesh from raw geometry at `position`: it renders
161/// the mesh and gives it an exact triangle collider, so dynamic bodies collide
162/// with arbitrary level geometry. Each vertex is a (position, normal, uv) triple
163/// and `indices` lists triangle corners three at a time. Heavy, so use it for
164/// static level geometry, not movers.
165#[cfg(feature = "physics")]
166pub fn spawn_collidable_mesh(
167    world: &mut World,
168    name: &str,
169    vertices: &[([f32; 3], [f32; 3], [f32; 2])],
170    indices: &[u32],
171    position: Vec3,
172) -> Entity {
173    let entity = crate::mesh::spawn_custom_mesh(world, name, vertices, indices, position);
174    let positions: Vec<[f32; 3]> = vertices.iter().map(|(point, _, _)| *point).collect();
175    let triangles: Vec<[u32; 3]> = indices
176        .chunks_exact(3)
177        .map(|chunk| [chunk[0], chunk[1], chunk[2]])
178        .collect();
179    let collider = ColliderComponent {
180        shape: nightshade::ecs::physics::components::ColliderShape::TriMesh {
181            vertices: positions,
182            indices: triangles,
183        },
184        ..Default::default()
185    };
186    attach_body(
187        world,
188        entity,
189        RigidBodyComponent::new_static().with_translation(position.x, position.y, position.z),
190        collider,
191        false,
192    );
193    entity
194}
195
196/// Spawns one [`Object`] at each position, all sharing a single registered
197/// material, which is what writing it longhand for a crowd looks like. A
198/// thousand entities through this hold one material entry, not a thousand.
199pub fn spawn_objects(world: &mut World, object: Object, positions: &[Vec3]) -> Vec<Entity> {
200    let mut entities = Vec::with_capacity(positions.len());
201    let mut shared_material: Option<String> = None;
202    #[cfg(feature = "physics")]
203    let mut collider_template: Option<ColliderComponent> = None;
204
205    for &position in positions {
206        let entity = spawn_mesh_at(world, mesh_name(object.shape), position, object.scale);
207        match shared_material.as_deref() {
208            None => {
209                crate::appearance::set_color(world, entity, object.color);
210                shared_material = world
211                    .core
212                    .get_material_ref(entity)
213                    .map(|material_ref| material_ref.name.clone());
214            }
215            Some(name) => {
216                let name = name.to_string();
217                adopt_shared_material(world, entity, &name);
218            }
219        }
220
221        #[cfg(feature = "physics")]
222        match object.body {
223            Body::None => {}
224            Body::Static => {
225                let collider = collider_template
226                    .get_or_insert_with(|| {
227                        static_collider(world, object.shape, object.scale)
228                            .with_friction(0.8)
229                            .with_restitution(0.1)
230                    })
231                    .clone();
232                attach_body(
233                    world,
234                    entity,
235                    RigidBodyComponent::new_static()
236                        .with_translation(position.x, position.y, position.z),
237                    collider,
238                    false,
239                );
240            }
241            Body::Dynamic { mass } => {
242                let collider = collider_template
243                    .get_or_insert_with(|| {
244                        dynamic_collider(world, object.shape, object.scale)
245                            .with_friction(0.7)
246                            .with_restitution(0.2)
247                    })
248                    .clone();
249                attach_body(
250                    world,
251                    entity,
252                    RigidBodyComponent::new_dynamic()
253                        .with_translation(position.x, position.y, position.z)
254                        .with_mass(mass),
255                    collider,
256                    true,
257                );
258            }
259        }
260
261        entities.push(entity);
262    }
263    entities
264}
265
266/// Spawns one entity that renders `transforms.len()` copies of the shape in a
267/// single draw call, the cheapest way to put thousands of identical things on
268/// screen. Per copy control afterward goes through the entity's
269/// `InstancedMesh` component.
270pub fn spawn_instanced(
271    world: &mut World,
272    shape: Shape,
273    transforms: Vec<InstanceTransform>,
274    color: [f32; 4],
275) -> Entity {
276    let shape_mesh_name = mesh_name(shape);
277    ensure_primitive_mesh(world, shape_mesh_name);
278    let fallback = format!(
279        "api::material::instanced::{:.4}_{:.4}_{:.4}_{:.4}",
280        color[0], color[1], color[2], color[3]
281    );
282    let material_name = nightshade::ecs::material::resources::material_registry_find_or_insert(
283        &mut world.resources.assets.material_registry,
284        fallback,
285        Material {
286            base_color: color,
287            ..Default::default()
288        },
289    );
290    spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
291}
292
293fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
294    let previous = world
295        .core
296        .get_material_ref(entity)
297        .map(|material_ref| material_ref.name.clone());
298    if let Some(previous_name) = previous
299        && let Some((index, _)) = registry_lookup_index(
300            &world.resources.assets.material_registry.registry,
301            &previous_name,
302        )
303    {
304        registry_remove_reference(
305            &mut world.resources.assets.material_registry.registry,
306            index,
307        );
308    }
309    if let Some((index, _)) =
310        registry_lookup_index(&world.resources.assets.material_registry.registry, name)
311    {
312        registry_add_reference(
313            &mut world.resources.assets.material_registry.registry,
314            index,
315        );
316    }
317    world
318        .core
319        .set_material_ref(entity, MaterialRef::new(name.to_string()));
320    world.resources.mesh_render_state.mark_entity_added(entity);
321}
322
323pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
324    use nightshade::ecs::mesh::components::{
325        create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
326        create_sphere_mesh, create_torus_mesh,
327    };
328    if !world
329        .resources
330        .assets
331        .mesh_cache
332        .registry
333        .name_to_index
334        .contains_key(mesh_name)
335    {
336        let mesh = match mesh_name {
337            "Cube" => create_cube_mesh(),
338            "Sphere" => create_sphere_mesh(1.0, 16),
339            "Plane" => create_plane_mesh(2.0),
340            "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
341            "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
342            _ => create_cone_mesh(0.5, 1.0, 16),
343        };
344        mesh_cache_insert(
345            &mut world.resources.assets.mesh_cache,
346            mesh_name.to_string(),
347            mesh,
348        );
349    }
350    if let Some((index, _)) =
351        registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
352    {
353        registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
354    }
355}
356
357/// Spawns a simulated cloth sheet hanging from `position`, pinned along its
358/// top row. It drapes, collides with the ground, and responds to
359/// `world.resources.wind`. Color it like anything else with
360/// [`set_color`](crate::prelude::set_color).
361pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
362    spawn_cloth(
363        world,
364        Cloth {
365            width,
366            height,
367            ..Default::default()
368        },
369        position,
370        "Cloth".to_string(),
371    )
372}
373
374/// Shows or hides the entity without despawning it.
375pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
376    if let Some(visibility) = world.core.get_visibility_mut(entity) {
377        visibility.visible = visible;
378    }
379}
380
381/// Spawns a flat ground plane reaching `half_extent` in each direction. With
382/// the `physics` feature it carries a static collider so dynamic objects land
383/// on it.
384pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
385    let entity = spawn_mesh_at(
386        world,
387        "Plane",
388        Vec3::zeros(),
389        Vec3::new(half_extent, 1.0, half_extent),
390    );
391    #[cfg(feature = "physics")]
392    attach_body(
393        world,
394        entity,
395        RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
396        ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
397            .with_friction(0.8)
398            .with_restitution(0.1),
399        false,
400    );
401    entity
402}
403
404/// Spawns a glb model with its textures, materials, skins, and animations.
405/// Panics with the import error when the bytes are not a valid glb.
406pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
407    let mut result =
408        import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
409    nightshade::ecs::loading::queue_gltf_load(world, &mut result);
410    let prefab = &result.prefabs[0];
411    nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
412        world,
413        prefab,
414        &result.animations,
415        &result.skins,
416        position,
417    )
418}
419
420/// Starts playing the model's animation clip at `clip_index`.
421pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
422    if let Some(player) = world.core.get_animation_player_mut(entity) {
423        player.play(clip_index);
424    }
425}
426
427/// Sets whether the entity's current animation repeats.
428pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
429    if let Some(player) = world.core.get_animation_player_mut(entity) {
430        player.looping = looping;
431    }
432}
433
434/// Plays the animation clip named `clip_name`. Returns whether a clip with that
435/// name exists on the model.
436pub fn play_animation_named(world: &mut World, entity: Entity, clip_name: &str) -> bool {
437    if let Some(player) = world.core.get_animation_player_mut(entity)
438        && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
439    {
440        player.play(index);
441        return true;
442    }
443    false
444}
445
446/// Sets the playback speed of the entity's animation. 1.0 is normal, 2.0 doubles
447/// it, a negative value plays backward.
448pub fn set_animation_speed(world: &mut World, entity: Entity, speed: f32) {
449    if let Some(player) = world.core.get_animation_player_mut(entity) {
450        player.speed = speed;
451    }
452}
453
454/// Cross-fades from the current clip to `clip_index` over `seconds`, a smooth
455/// transition rather than a hard cut.
456pub fn blend_to_animation(world: &mut World, entity: Entity, clip_index: usize, seconds: f32) {
457    if let Some(player) = world.core.get_animation_player_mut(entity) {
458        player.blend_to(clip_index, seconds);
459    }
460}
461
462/// Cross-fades to the clip named `clip_name` over `seconds`. Returns whether a
463/// clip with that name exists.
464pub fn blend_to_animation_named(
465    world: &mut World,
466    entity: Entity,
467    clip_name: &str,
468    seconds: f32,
469) -> bool {
470    if let Some(player) = world.core.get_animation_player_mut(entity)
471        && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
472    {
473        player.blend_to(index, seconds);
474        return true;
475    }
476    false
477}
478
479/// Pauses the entity's animation at the current frame.
480pub fn pause_animation(world: &mut World, entity: Entity) {
481    if let Some(player) = world.core.get_animation_player_mut(entity) {
482        player.pause();
483    }
484}
485
486/// Resumes a paused animation from where it left off.
487pub fn resume_animation(world: &mut World, entity: Entity) {
488    if let Some(player) = world.core.get_animation_player_mut(entity) {
489        player.resume();
490    }
491}
492
493/// Stops the entity's animation and resets it to the first frame.
494pub fn stop_animation(world: &mut World, entity: Entity) {
495    if let Some(player) = world.core.get_animation_player_mut(entity) {
496        player.stop();
497    }
498}
499
500/// The names of the entity's animation clips, in clip-index order.
501pub fn animation_clips(world: &World, entity: Entity) -> Vec<String> {
502    world
503        .core
504        .get_animation_player(entity)
505        .map(|player| player.clips.iter().map(|clip| clip.name.clone()).collect())
506        .unwrap_or_default()
507}
508
509/// Adds a named marker at `time` seconds on the clip at `clip_index`. While that
510/// clip plays, crossing the marker publishes an `Event::AnimationEvent` carrying
511/// `name`, the cue to spawn a footstep, land a hit, or trigger an effect. Read
512/// it with [`drain_events`](crate::prelude::drain_events). Returns false if the
513/// entity has no animation player or the clip index is out of range.
514pub fn add_animation_event(
515    world: &mut World,
516    entity: Entity,
517    clip_index: usize,
518    time: f32,
519    name: &str,
520) -> bool {
521    if let Some(player) = world.core.get_animation_player_mut(entity)
522        && let Some(clip) = player.clips.get_mut(clip_index)
523    {
524        clip.events
525            .push(nightshade::ecs::animation::components::AnimationEvent {
526                time,
527                name: name.to_string(),
528            });
529        return true;
530    }
531    false
532}
533
534/// Adds a named marker at `time` seconds on the clip named `clip_name`, the
535/// by-name form of [`add_animation_event`].
536pub fn add_animation_event_named(
537    world: &mut World,
538    entity: Entity,
539    clip_name: &str,
540    time: f32,
541    name: &str,
542) -> bool {
543    if let Some(player) = world.core.get_animation_player_mut(entity)
544        && let Some(clip) = player.clips.iter_mut().find(|clip| clip.name == clip_name)
545    {
546        clip.events
547            .push(nightshade::ecs::animation::components::AnimationEvent {
548                time,
549                name: name.to_string(),
550            });
551        return true;
552    }
553    false
554}
555
556/// Plays an extra clip on top of the base animation at `weight` (0.0 to 1.0),
557/// blended over the bones the clip targets. Returns the layer's index for
558/// [`set_animation_layer_weight`], or `None` if the entity has no animation
559/// player. A reload, a hit reaction, anything composited over a walk or idle.
560pub fn add_animation_layer(
561    world: &mut World,
562    entity: Entity,
563    clip_index: usize,
564    weight: f32,
565) -> Option<usize> {
566    let player = world.core.get_animation_player_mut(entity)?;
567    let index = player.layers.len();
568    player.layers.push(
569        nightshade::ecs::animation::components::AnimationLayer::new(clip_index).with_weight(weight),
570    );
571    Some(index)
572}
573
574/// Plays an extra clip limited to the named bones, the per-bone mask form of
575/// [`add_animation_layer`]. Pass the bone names the layer should drive, like the
576/// spine and arms for an upper-body wave over a full-body walk.
577pub fn add_animation_layer_masked(
578    world: &mut World,
579    entity: Entity,
580    clip_index: usize,
581    weight: f32,
582    bones: &[&str],
583) -> Option<usize> {
584    let player = world.core.get_animation_player_mut(entity)?;
585    let index = player.layers.len();
586    let mask = bones.iter().map(|bone| bone.to_string()).collect();
587    player.layers.push(
588        nightshade::ecs::animation::components::AnimationLayer::new(clip_index)
589            .with_weight(weight)
590            .with_mask(mask),
591    );
592    Some(index)
593}
594
595/// Sets the blend weight of a layer added by [`add_animation_layer`], for fading
596/// a composited animation in and out. 0.0 disables it without removing it.
597pub fn set_animation_layer_weight(
598    world: &mut World,
599    entity: Entity,
600    layer_index: usize,
601    weight: f32,
602) {
603    if let Some(player) = world.core.get_animation_player_mut(entity)
604        && let Some(layer) = player.layers.get_mut(layer_index)
605    {
606        layer.weight = weight;
607    }
608}
609
610/// Removes every animation layer, leaving only the base animation.
611pub fn clear_animation_layers(world: &mut World, entity: Entity) {
612    if let Some(player) = world.core.get_animation_player_mut(entity) {
613        player.layers.clear();
614    }
615}
616
617/// Registers `entity` under `name` so it can be looked up by name, including
618/// through the `named` map a rhai script reads. Use it to hand specific entities
619/// (a HUD label, a player) to scripts that drive them.
620pub fn name_entity(world: &mut World, name: &str, entity: Entity) {
621    world
622        .resources
623        .entities
624        .names
625        .insert(name.to_string(), entity);
626}
627
628/// Spawns an invisible group at `position` for building hierarchies. Parent
629/// things to it and move, rotate, or animate the group to drive them all.
630pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
631    let entity = spawn_entities(
632        world,
633        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
634        1,
635    )[0];
636    world.core.set_name(entity, Name("Group".to_string()));
637    assign_local_transform(
638        world,
639        entity,
640        LocalTransform {
641            translation: position,
642            ..Default::default()
643        },
644    );
645    entity
646}
647
648/// Parents `child` to `parent`, or unparents it with `None`, keeping the
649/// child exactly where it is in world space.
650pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
651    let child_world = crate::placement::world_matrix(world, child);
652    let parent_world = parent
653        .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
654        .unwrap_or_else(Mat4::identity);
655    let local = nalgebra_glm::inverse(&parent_world) * child_world;
656
657    let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
658    let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
659    let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
660    let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
661    let scale = nalgebra_glm::vec3(
662        basis_x.magnitude(),
663        basis_y.magnitude(),
664        basis_z.magnitude(),
665    );
666    let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
667        basis_x / scale.x.max(f32::EPSILON),
668        basis_y / scale.y.max(f32::EPSILON),
669        basis_z / scale.z.max(f32::EPSILON),
670    ]);
671    let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
672
673    if parent.is_some() {
674        world.core.add_components(child, PARENT);
675    }
676    update_parent(
677        world,
678        child,
679        parent.map(|parent_entity| Parent(Some(parent_entity))),
680    );
681    assign_local_transform(
682        world,
683        child,
684        LocalTransform {
685            translation,
686            rotation,
687            scale,
688        },
689    );
690}
691
692#[cfg(feature = "picking")]
693pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
694    world
695        .core
696        .get_name(entity)
697        .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
698}
699
700pub(crate) fn api_material_name(entity: Entity) -> String {
701    format!("{MATERIAL_PREFIX}{}", entity.id)
702}
703
704fn mesh_name(shape: Shape) -> &'static str {
705    match shape {
706        Shape::Cube => "Cube",
707        Shape::Sphere => "Sphere",
708        Shape::Cylinder => "Cylinder",
709        Shape::Cone => "Cone",
710        Shape::Torus => "Torus",
711        Shape::Plane => "Plane",
712    }
713}
714
715#[cfg(feature = "physics")]
716fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
717    match shape {
718        Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
719        Shape::Sphere => ColliderComponent::new_ball(scale.x),
720        Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
721        Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
722        Shape::Torus => ColliderComponent {
723            shape: ColliderShape::ConvexMesh {
724                vertices: scaled_mesh_points(world, "Torus", scale),
725            },
726            ..Default::default()
727        },
728        Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
729    }
730}
731
732#[cfg(feature = "physics")]
733fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
734    match shape {
735        Shape::Torus | Shape::Plane => {
736            let shape_mesh_name = mesh_name(shape);
737            ColliderComponent {
738                shape: ColliderShape::TriMesh {
739                    vertices: scaled_mesh_points(world, shape_mesh_name, scale),
740                    indices: mesh_triangles(world, shape_mesh_name),
741                },
742                ..Default::default()
743            }
744        }
745        _ => dynamic_collider(world, shape, scale),
746    }
747}
748
749#[cfg(feature = "physics")]
750fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
751    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
752        .map(|mesh| {
753            mesh.vertices
754                .iter()
755                .map(|vertex| {
756                    [
757                        vertex.position[0] * scale.x,
758                        vertex.position[1] * scale.y,
759                        vertex.position[2] * scale.z,
760                    ]
761                })
762                .collect()
763        })
764        .unwrap_or_default()
765}
766
767#[cfg(feature = "physics")]
768fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
769    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
770        .map(|mesh| {
771            mesh.indices
772                .chunks_exact(3)
773                .map(|triangle| [triangle[0], triangle[1], triangle[2]])
774                .collect()
775        })
776        .unwrap_or_default()
777}
778
779#[cfg(feature = "physics")]
780fn attach_body(
781    world: &mut World,
782    entity: Entity,
783    body: RigidBodyComponent,
784    collider: ColliderComponent,
785    dynamic: bool,
786) {
787    let mut flags = RIGID_BODY | COLLIDER;
788    if dynamic {
789        flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
790    }
791    world.core.add_components(entity, flags);
792    world.core.set_rigid_body(entity, body);
793    world.core.set_collider(entity, collider);
794    if dynamic {
795        reset_physics_interpolation(world, entity);
796        if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
797            interpolation.enabled = true;
798        }
799    }
800}