1use 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#[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#[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
48pub 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
79pub 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#[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#[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
196pub 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
266pub 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
357pub 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
374pub 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
381pub 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
404pub 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
420pub 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
427pub 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
434pub 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
446pub 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
454pub 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
462pub 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
479pub 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
486pub 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
493pub 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
500pub 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
509pub 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
534pub 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
556pub 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
574pub 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
595pub 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
610pub 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
617pub 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
628pub 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
648pub 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}