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_custom_mesh_collider(
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::plugins::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 .get::<nightshade::ecs::material::components::MaterialRef>(entity)
212 .map(|material_ref| material_ref.name.clone());
213 }
214 Some(name) => {
215 let name = name.to_string();
216 adopt_shared_material(world, entity, &name);
217 }
218 }
219
220 #[cfg(feature = "physics")]
221 match object.body {
222 Body::None => {}
223 Body::Static => {
224 let collider = collider_template
225 .get_or_insert_with(|| {
226 static_collider(world, object.shape, object.scale)
227 .with_friction(0.8)
228 .with_restitution(0.1)
229 })
230 .clone();
231 attach_body(
232 world,
233 entity,
234 RigidBodyComponent::new_static()
235 .with_translation(position.x, position.y, position.z),
236 collider,
237 false,
238 );
239 }
240 Body::Dynamic { mass } => {
241 let collider = collider_template
242 .get_or_insert_with(|| {
243 dynamic_collider(world, object.shape, object.scale)
244 .with_friction(0.7)
245 .with_restitution(0.2)
246 })
247 .clone();
248 attach_body(
249 world,
250 entity,
251 RigidBodyComponent::new_dynamic()
252 .with_translation(position.x, position.y, position.z)
253 .with_mass(mass),
254 collider,
255 true,
256 );
257 }
258 }
259
260 entities.push(entity);
261 }
262 entities
263}
264
265pub fn spawn_instanced(
270 world: &mut World,
271 shape: Shape,
272 transforms: Vec<InstanceTransform>,
273 color: [f32; 4],
274) -> Entity {
275 let shape_mesh_name = mesh_name(shape);
276 ensure_primitive_mesh(world, shape_mesh_name);
277 let fallback = format!(
278 "api::material::instanced::{:.4}_{:.4}_{:.4}_{:.4}",
279 color[0], color[1], color[2], color[3]
280 );
281 let material_name = nightshade::ecs::material::resources::material_registry_find_or_insert(
282 &mut world.resources.assets.material_registry,
283 fallback,
284 Material {
285 base_color: color,
286 ..Default::default()
287 },
288 );
289 spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
290}
291
292pub fn spawn_instanced_with_material(
298 world: &mut World,
299 shape: Shape,
300 transforms: Vec<InstanceTransform>,
301 material: &str,
302) -> Entity {
303 let shape_mesh_name = mesh_name(shape);
304 ensure_primitive_mesh(world, shape_mesh_name);
305 spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, material)
306}
307
308pub fn set_instances(world: &mut World, batch: Entity, transforms: Vec<InstanceTransform>) {
312 if let Some(instanced) =
313 world.get_mut::<nightshade::ecs::mesh::components::InstancedMesh>(batch)
314 {
315 instanced.set_instances(transforms);
316 }
317 world
318 .resources
319 .mesh_render_state
320 .mark_instanced_meshes_changed();
321}
322
323fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
324 let previous = world
325 .get::<nightshade::ecs::material::components::MaterialRef>(entity)
326 .map(|material_ref| material_ref.name.clone());
327 if let Some(previous_name) = previous
328 && let Some((index, _)) = registry_lookup_index(
329 &world.resources.assets.material_registry.registry,
330 &previous_name,
331 )
332 {
333 registry_remove_reference(
334 &mut world.resources.assets.material_registry.registry,
335 index,
336 );
337 }
338 if let Some((index, _)) =
339 registry_lookup_index(&world.resources.assets.material_registry.registry, name)
340 {
341 registry_add_reference(
342 &mut world.resources.assets.material_registry.registry,
343 index,
344 );
345 }
346 world.set(entity, MaterialRef::new(name.to_string()));
347 world
348 .resources
349 .mesh_render_state
350 .mark_entity_added(render_entity(entity));
351}
352
353pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
354 use nightshade::render::procedural_meshes::{
355 create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
356 create_sphere_mesh, create_torus_mesh,
357 };
358 if !world
359 .resources
360 .assets
361 .mesh_cache
362 .registry
363 .name_to_index
364 .contains_key(mesh_name)
365 {
366 let mesh = match mesh_name {
367 "Cube" => create_cube_mesh(),
368 "Sphere" => create_sphere_mesh(1.0, 16),
369 "Plane" => create_plane_mesh(2.0),
370 "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
371 "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
372 _ => create_cone_mesh(0.5, 1.0, 16),
373 };
374 mesh_cache_insert(
375 &mut world.resources.assets.mesh_cache,
376 mesh_name.to_string(),
377 mesh,
378 );
379 }
380 if let Some((index, _)) =
381 registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
382 {
383 registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
384 }
385}
386
387pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
392 spawn_cloth(
393 world,
394 Cloth {
395 width,
396 height,
397 ..Default::default()
398 },
399 position,
400 "Cloth".to_string(),
401 )
402}
403
404pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
406 if let Some(visibility) = world.get_mut::<nightshade::ecs::primitives::Visibility>(entity) {
407 visibility.visible = visible;
408 }
409}
410
411pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
415 let entity = spawn_mesh_at(
416 world,
417 "Plane",
418 Vec3::zeros(),
419 Vec3::new(half_extent, 1.0, half_extent),
420 );
421 #[cfg(feature = "physics")]
422 attach_body(
423 world,
424 entity,
425 RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
426 ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
427 .with_friction(0.8)
428 .with_restitution(0.1),
429 false,
430 );
431 entity
432}
433
434pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
437 let mut result =
438 import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
439 nightshade::ecs::loading::queue_gltf_load(world, &mut result);
440 let prefab = &result.prefabs[0];
441 nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
442 world,
443 prefab,
444 &result.animations,
445 &result.skins,
446 position,
447 )
448}
449
450pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
452 if let Some(player) =
453 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
454 {
455 player.play(clip_index);
456 }
457}
458
459pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
461 if let Some(player) =
462 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
463 {
464 player.looping = looping;
465 }
466}
467
468pub fn play_animation_named(world: &mut World, entity: Entity, clip_name: &str) -> bool {
471 if let Some(player) =
472 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
473 && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
474 {
475 player.play(index);
476 return true;
477 }
478 false
479}
480
481pub fn set_animation_speed(world: &mut World, entity: Entity, speed: f32) {
484 if let Some(player) =
485 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
486 {
487 player.speed = speed;
488 }
489}
490
491pub fn blend_to_animation(world: &mut World, entity: Entity, clip_index: usize, seconds: f32) {
494 if let Some(player) =
495 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
496 {
497 player.blend_to(clip_index, seconds);
498 }
499}
500
501pub fn blend_to_animation_named(
504 world: &mut World,
505 entity: Entity,
506 clip_name: &str,
507 seconds: f32,
508) -> bool {
509 if let Some(player) =
510 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
511 && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
512 {
513 player.blend_to(index, seconds);
514 return true;
515 }
516 false
517}
518
519pub fn pause_animation(world: &mut World, entity: Entity) {
521 if let Some(player) =
522 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
523 {
524 player.pause();
525 }
526}
527
528pub fn resume_animation(world: &mut World, entity: Entity) {
530 if let Some(player) =
531 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
532 {
533 player.resume();
534 }
535}
536
537pub fn stop_animation(world: &mut World, entity: Entity) {
539 if let Some(player) =
540 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
541 {
542 player.stop();
543 }
544}
545
546pub fn animation_clips(world: &World, entity: Entity) -> Vec<String> {
548 world
549 .get::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
550 .map(|player| player.clips.iter().map(|clip| clip.name.clone()).collect())
551 .unwrap_or_default()
552}
553
554pub fn add_animation_event(
560 world: &mut World,
561 entity: Entity,
562 clip_index: usize,
563 time: f32,
564 name: &str,
565) -> bool {
566 if let Some(player) =
567 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
568 && let Some(clip) = player.clips.get_mut(clip_index)
569 {
570 clip.events
571 .push(nightshade::ecs::animation::components::AnimationEvent {
572 time,
573 name: name.to_string(),
574 });
575 return true;
576 }
577 false
578}
579
580pub fn add_animation_event_named(
583 world: &mut World,
584 entity: Entity,
585 clip_name: &str,
586 time: f32,
587 name: &str,
588) -> bool {
589 if let Some(player) =
590 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
591 && let Some(clip) = player.clips.iter_mut().find(|clip| clip.name == clip_name)
592 {
593 clip.events
594 .push(nightshade::ecs::animation::components::AnimationEvent {
595 time,
596 name: name.to_string(),
597 });
598 return true;
599 }
600 false
601}
602
603pub fn add_animation_layer(
608 world: &mut World,
609 entity: Entity,
610 clip_index: usize,
611 weight: f32,
612) -> Option<usize> {
613 let player =
614 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)?;
615 let index = player.layers.len();
616 player.layers.push(
617 nightshade::ecs::animation::components::AnimationLayer::new(clip_index).with_weight(weight),
618 );
619 Some(index)
620}
621
622pub fn add_animation_layer_masked(
626 world: &mut World,
627 entity: Entity,
628 clip_index: usize,
629 weight: f32,
630 bones: &[&str],
631) -> Option<usize> {
632 let player =
633 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)?;
634 let index = player.layers.len();
635 let mask = bones.iter().map(|bone| bone.to_string()).collect();
636 player.layers.push(
637 nightshade::ecs::animation::components::AnimationLayer::new(clip_index)
638 .with_weight(weight)
639 .with_mask(mask),
640 );
641 Some(index)
642}
643
644pub fn set_animation_layer_weight(
647 world: &mut World,
648 entity: Entity,
649 layer_index: usize,
650 weight: f32,
651) {
652 if let Some(player) =
653 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
654 && let Some(layer) = player.layers.get_mut(layer_index)
655 {
656 layer.weight = weight;
657 }
658}
659
660pub fn clear_animation_layers(world: &mut World, entity: Entity) {
662 if let Some(player) =
663 world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
664 {
665 player.layers.clear();
666 }
667}
668
669pub fn name_entity(world: &mut World, name: &str, entity: Entity) {
673 world
674 .resources
675 .entities
676 .names
677 .insert(name.to_string(), entity);
678}
679
680pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
683 let entity = spawn_entities(world, NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
684 world.set(entity, Name("Group".to_string()));
685 world.set(
686 entity,
687 LocalTransform {
688 translation: position,
689 ..Default::default()
690 },
691 );
692 entity
693}
694
695pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
698 let child_world = crate::placement::world_matrix(world, child);
699 let parent_world = parent
700 .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
701 .unwrap_or_else(Mat4::identity);
702 let local = nalgebra_glm::inverse(&parent_world) * child_world;
703
704 let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
705 let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
706 let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
707 let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
708 let scale = nalgebra_glm::vec3(
709 basis_x.magnitude(),
710 basis_y.magnitude(),
711 basis_z.magnitude(),
712 );
713 let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
714 basis_x / scale.x.max(f32::EPSILON),
715 basis_y / scale.y.max(f32::EPSILON),
716 basis_z / scale.z.max(f32::EPSILON),
717 ]);
718 let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
719
720 if parent.is_some() {
721 world.ecs.worlds[CORE].add_components(child, PARENT);
722 }
723 update_parent(world, child, parent);
724 world.set(
725 child,
726 LocalTransform {
727 translation,
728 rotation,
729 scale,
730 },
731 );
732}
733
734#[cfg(feature = "picking")]
735pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
736 world
737 .get::<nightshade::ecs::primitives::Name>(entity)
738 .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
739}
740
741pub(crate) fn api_material_name(entity: Entity) -> String {
742 format!("{MATERIAL_PREFIX}{}", entity.id)
743}
744
745fn mesh_name(shape: Shape) -> &'static str {
746 match shape {
747 Shape::Cube => "Cube",
748 Shape::Sphere => "Sphere",
749 Shape::Cylinder => "Cylinder",
750 Shape::Cone => "Cone",
751 Shape::Torus => "Torus",
752 Shape::Plane => "Plane",
753 }
754}
755
756#[cfg(feature = "physics")]
757fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
758 match shape {
759 Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
760 Shape::Sphere => ColliderComponent::new_ball(scale.x),
761 Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
762 Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
763 Shape::Torus => ColliderComponent {
764 shape: ColliderShape::ConvexMesh {
765 vertices: scaled_mesh_points(world, "Torus", scale),
766 },
767 ..Default::default()
768 },
769 Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
770 }
771}
772
773#[cfg(feature = "physics")]
774fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
775 match shape {
776 Shape::Torus | Shape::Plane => {
777 let shape_mesh_name = mesh_name(shape);
778 ColliderComponent {
779 shape: ColliderShape::TriMesh {
780 vertices: scaled_mesh_points(world, shape_mesh_name, scale),
781 indices: mesh_triangles(world, shape_mesh_name),
782 },
783 ..Default::default()
784 }
785 }
786 _ => dynamic_collider(world, shape, scale),
787 }
788}
789
790#[cfg(feature = "physics")]
791fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
792 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
793 .map(|mesh| {
794 mesh.vertices
795 .iter()
796 .map(|vertex| {
797 [
798 vertex.position[0] * scale.x,
799 vertex.position[1] * scale.y,
800 vertex.position[2] * scale.z,
801 ]
802 })
803 .collect()
804 })
805 .unwrap_or_default()
806}
807
808#[cfg(feature = "physics")]
809fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
810 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
811 .map(|mesh| {
812 mesh.indices
813 .chunks_exact(3)
814 .map(|triangle| [triangle[0], triangle[1], triangle[2]])
815 .collect()
816 })
817 .unwrap_or_default()
818}
819
820#[cfg(feature = "physics")]
821fn attach_body(
822 world: &mut World,
823 entity: Entity,
824 body: RigidBodyComponent,
825 collider: ColliderComponent,
826 dynamic: bool,
827) {
828 let mut flags = RIGID_BODY | COLLIDER;
829 if dynamic {
830 flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
831 }
832 world.ecs.worlds[CORE].add_components(entity, flags);
833 world.set(entity, body);
834 world.set(entity, collider);
835 if dynamic {
836 reset_physics_interpolation(world, entity);
837 if let Some(interpolation) =
838 world.get_mut::<nightshade::plugins::physics::components::PhysicsInterpolation>(entity)
839 {
840 interpolation.enabled = true;
841 }
842 }
843}