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
293pub fn spawn_instanced_with_material(
299 world: &mut World,
300 shape: Shape,
301 transforms: Vec<InstanceTransform>,
302 material: &str,
303) -> Entity {
304 let shape_mesh_name = mesh_name(shape);
305 ensure_primitive_mesh(world, shape_mesh_name);
306 spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, material)
307}
308
309pub fn set_instances(world: &mut World, batch: Entity, transforms: Vec<InstanceTransform>) {
313 if let Some(instanced) = world.core.get_instanced_mesh_mut(batch) {
314 instanced.set_instances(transforms);
315 }
316 world
317 .resources
318 .mesh_render_state
319 .mark_instanced_meshes_changed();
320}
321
322fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
323 let previous = world
324 .core
325 .get_material_ref(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
347 .core
348 .set_material_ref(entity, MaterialRef::new(name.to_string()));
349 world.resources.mesh_render_state.mark_entity_added(entity);
350}
351
352pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
353 use nightshade::ecs::mesh::components::{
354 create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
355 create_sphere_mesh, create_torus_mesh,
356 };
357 if !world
358 .resources
359 .assets
360 .mesh_cache
361 .registry
362 .name_to_index
363 .contains_key(mesh_name)
364 {
365 let mesh = match mesh_name {
366 "Cube" => create_cube_mesh(),
367 "Sphere" => create_sphere_mesh(1.0, 16),
368 "Plane" => create_plane_mesh(2.0),
369 "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
370 "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
371 _ => create_cone_mesh(0.5, 1.0, 16),
372 };
373 mesh_cache_insert(
374 &mut world.resources.assets.mesh_cache,
375 mesh_name.to_string(),
376 mesh,
377 );
378 }
379 if let Some((index, _)) =
380 registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
381 {
382 registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
383 }
384}
385
386pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
391 spawn_cloth(
392 world,
393 Cloth {
394 width,
395 height,
396 ..Default::default()
397 },
398 position,
399 "Cloth".to_string(),
400 )
401}
402
403pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
405 if let Some(visibility) = world.core.get_visibility_mut(entity) {
406 visibility.visible = visible;
407 }
408}
409
410pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
414 let entity = spawn_mesh_at(
415 world,
416 "Plane",
417 Vec3::zeros(),
418 Vec3::new(half_extent, 1.0, half_extent),
419 );
420 #[cfg(feature = "physics")]
421 attach_body(
422 world,
423 entity,
424 RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
425 ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
426 .with_friction(0.8)
427 .with_restitution(0.1),
428 false,
429 );
430 entity
431}
432
433pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
436 let mut result =
437 import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
438 nightshade::ecs::loading::queue_gltf_load(world, &mut result);
439 let prefab = &result.prefabs[0];
440 nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
441 world,
442 prefab,
443 &result.animations,
444 &result.skins,
445 position,
446 )
447}
448
449pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
451 if let Some(player) = world.core.get_animation_player_mut(entity) {
452 player.play(clip_index);
453 }
454}
455
456pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
458 if let Some(player) = world.core.get_animation_player_mut(entity) {
459 player.looping = looping;
460 }
461}
462
463pub fn play_animation_named(world: &mut World, entity: Entity, clip_name: &str) -> bool {
466 if let Some(player) = world.core.get_animation_player_mut(entity)
467 && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
468 {
469 player.play(index);
470 return true;
471 }
472 false
473}
474
475pub fn set_animation_speed(world: &mut World, entity: Entity, speed: f32) {
478 if let Some(player) = world.core.get_animation_player_mut(entity) {
479 player.speed = speed;
480 }
481}
482
483pub fn blend_to_animation(world: &mut World, entity: Entity, clip_index: usize, seconds: f32) {
486 if let Some(player) = world.core.get_animation_player_mut(entity) {
487 player.blend_to(clip_index, seconds);
488 }
489}
490
491pub fn blend_to_animation_named(
494 world: &mut World,
495 entity: Entity,
496 clip_name: &str,
497 seconds: f32,
498) -> bool {
499 if let Some(player) = world.core.get_animation_player_mut(entity)
500 && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
501 {
502 player.blend_to(index, seconds);
503 return true;
504 }
505 false
506}
507
508pub fn pause_animation(world: &mut World, entity: Entity) {
510 if let Some(player) = world.core.get_animation_player_mut(entity) {
511 player.pause();
512 }
513}
514
515pub fn resume_animation(world: &mut World, entity: Entity) {
517 if let Some(player) = world.core.get_animation_player_mut(entity) {
518 player.resume();
519 }
520}
521
522pub fn stop_animation(world: &mut World, entity: Entity) {
524 if let Some(player) = world.core.get_animation_player_mut(entity) {
525 player.stop();
526 }
527}
528
529pub fn animation_clips(world: &World, entity: Entity) -> Vec<String> {
531 world
532 .core
533 .get_animation_player(entity)
534 .map(|player| player.clips.iter().map(|clip| clip.name.clone()).collect())
535 .unwrap_or_default()
536}
537
538pub fn add_animation_event(
544 world: &mut World,
545 entity: Entity,
546 clip_index: usize,
547 time: f32,
548 name: &str,
549) -> bool {
550 if let Some(player) = world.core.get_animation_player_mut(entity)
551 && let Some(clip) = player.clips.get_mut(clip_index)
552 {
553 clip.events
554 .push(nightshade::ecs::animation::components::AnimationEvent {
555 time,
556 name: name.to_string(),
557 });
558 return true;
559 }
560 false
561}
562
563pub fn add_animation_event_named(
566 world: &mut World,
567 entity: Entity,
568 clip_name: &str,
569 time: f32,
570 name: &str,
571) -> bool {
572 if let Some(player) = world.core.get_animation_player_mut(entity)
573 && let Some(clip) = player.clips.iter_mut().find(|clip| clip.name == clip_name)
574 {
575 clip.events
576 .push(nightshade::ecs::animation::components::AnimationEvent {
577 time,
578 name: name.to_string(),
579 });
580 return true;
581 }
582 false
583}
584
585pub fn add_animation_layer(
590 world: &mut World,
591 entity: Entity,
592 clip_index: usize,
593 weight: f32,
594) -> Option<usize> {
595 let player = world.core.get_animation_player_mut(entity)?;
596 let index = player.layers.len();
597 player.layers.push(
598 nightshade::ecs::animation::components::AnimationLayer::new(clip_index).with_weight(weight),
599 );
600 Some(index)
601}
602
603pub fn add_animation_layer_masked(
607 world: &mut World,
608 entity: Entity,
609 clip_index: usize,
610 weight: f32,
611 bones: &[&str],
612) -> Option<usize> {
613 let player = world.core.get_animation_player_mut(entity)?;
614 let index = player.layers.len();
615 let mask = bones.iter().map(|bone| bone.to_string()).collect();
616 player.layers.push(
617 nightshade::ecs::animation::components::AnimationLayer::new(clip_index)
618 .with_weight(weight)
619 .with_mask(mask),
620 );
621 Some(index)
622}
623
624pub fn set_animation_layer_weight(
627 world: &mut World,
628 entity: Entity,
629 layer_index: usize,
630 weight: f32,
631) {
632 if let Some(player) = world.core.get_animation_player_mut(entity)
633 && let Some(layer) = player.layers.get_mut(layer_index)
634 {
635 layer.weight = weight;
636 }
637}
638
639pub fn clear_animation_layers(world: &mut World, entity: Entity) {
641 if let Some(player) = world.core.get_animation_player_mut(entity) {
642 player.layers.clear();
643 }
644}
645
646pub fn name_entity(world: &mut World, name: &str, entity: Entity) {
650 world
651 .resources
652 .entities
653 .names
654 .insert(name.to_string(), entity);
655}
656
657pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
660 let entity = spawn_entities(
661 world,
662 NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
663 1,
664 )[0];
665 world.core.set_name(entity, Name("Group".to_string()));
666 assign_local_transform(
667 world,
668 entity,
669 LocalTransform {
670 translation: position,
671 ..Default::default()
672 },
673 );
674 entity
675}
676
677pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
680 let child_world = crate::placement::world_matrix(world, child);
681 let parent_world = parent
682 .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
683 .unwrap_or_else(Mat4::identity);
684 let local = nalgebra_glm::inverse(&parent_world) * child_world;
685
686 let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
687 let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
688 let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
689 let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
690 let scale = nalgebra_glm::vec3(
691 basis_x.magnitude(),
692 basis_y.magnitude(),
693 basis_z.magnitude(),
694 );
695 let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
696 basis_x / scale.x.max(f32::EPSILON),
697 basis_y / scale.y.max(f32::EPSILON),
698 basis_z / scale.z.max(f32::EPSILON),
699 ]);
700 let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
701
702 if parent.is_some() {
703 world.core.add_components(child, PARENT);
704 }
705 update_parent(
706 world,
707 child,
708 parent.map(|parent_entity| Parent(Some(parent_entity))),
709 );
710 assign_local_transform(
711 world,
712 child,
713 LocalTransform {
714 translation,
715 rotation,
716 scale,
717 },
718 );
719}
720
721#[cfg(feature = "picking")]
722pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
723 world
724 .core
725 .get_name(entity)
726 .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
727}
728
729pub(crate) fn api_material_name(entity: Entity) -> String {
730 format!("{MATERIAL_PREFIX}{}", entity.id)
731}
732
733fn mesh_name(shape: Shape) -> &'static str {
734 match shape {
735 Shape::Cube => "Cube",
736 Shape::Sphere => "Sphere",
737 Shape::Cylinder => "Cylinder",
738 Shape::Cone => "Cone",
739 Shape::Torus => "Torus",
740 Shape::Plane => "Plane",
741 }
742}
743
744#[cfg(feature = "physics")]
745fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
746 match shape {
747 Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
748 Shape::Sphere => ColliderComponent::new_ball(scale.x),
749 Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
750 Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
751 Shape::Torus => ColliderComponent {
752 shape: ColliderShape::ConvexMesh {
753 vertices: scaled_mesh_points(world, "Torus", scale),
754 },
755 ..Default::default()
756 },
757 Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
758 }
759}
760
761#[cfg(feature = "physics")]
762fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
763 match shape {
764 Shape::Torus | Shape::Plane => {
765 let shape_mesh_name = mesh_name(shape);
766 ColliderComponent {
767 shape: ColliderShape::TriMesh {
768 vertices: scaled_mesh_points(world, shape_mesh_name, scale),
769 indices: mesh_triangles(world, shape_mesh_name),
770 },
771 ..Default::default()
772 }
773 }
774 _ => dynamic_collider(world, shape, scale),
775 }
776}
777
778#[cfg(feature = "physics")]
779fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
780 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
781 .map(|mesh| {
782 mesh.vertices
783 .iter()
784 .map(|vertex| {
785 [
786 vertex.position[0] * scale.x,
787 vertex.position[1] * scale.y,
788 vertex.position[2] * scale.z,
789 ]
790 })
791 .collect()
792 })
793 .unwrap_or_default()
794}
795
796#[cfg(feature = "physics")]
797fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
798 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
799 .map(|mesh| {
800 mesh.indices
801 .chunks_exact(3)
802 .map(|triangle| [triangle[0], triangle[1], triangle[2]])
803 .collect()
804 })
805 .unwrap_or_default()
806}
807
808#[cfg(feature = "physics")]
809fn attach_body(
810 world: &mut World,
811 entity: Entity,
812 body: RigidBodyComponent,
813 collider: ColliderComponent,
814 dynamic: bool,
815) {
816 let mut flags = RIGID_BODY | COLLIDER;
817 if dynamic {
818 flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
819 }
820 world.core.add_components(entity, flags);
821 world.core.set_rigid_body(entity, body);
822 world.core.set_collider(entity, collider);
823 if dynamic {
824 reset_physics_interpolation(world, entity);
825 if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
826 interpolation.enabled = true;
827 }
828 }
829}