1use 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#[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#[derive(Debug, Clone, Copy, PartialEq, Default)]
36pub enum Body {
37 #[default]
38 None,
39 Static,
40 Dynamic {
41 mass: f32,
42 },
43}
44
45pub 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
76pub 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
125pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
129 let entity = spawn_mesh_at(
130 world,
131 "Plane",
132 Vec3::zeros(),
133 Vec3::new(half_extent, 1.0, half_extent),
134 );
135 #[cfg(feature = "physics")]
136 attach_body(
137 world,
138 entity,
139 RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
140 ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
141 .with_friction(0.8)
142 .with_restitution(0.1),
143 false,
144 );
145 entity
146}
147
148pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
151 let mut result =
152 import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
153 nightshade::ecs::loading::queue_gltf_load(world, &mut result);
154 let prefab = &result.prefabs[0];
155 nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
156 world,
157 prefab,
158 &result.animations,
159 &result.skins,
160 position,
161 )
162}
163
164pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
166 if let Some(player) = world.core.get_animation_player_mut(entity) {
167 player.play(clip_index);
168 }
169}
170
171pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
173 if let Some(player) = world.core.get_animation_player_mut(entity) {
174 player.looping = looping;
175 }
176}
177
178pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
181 let child_world = crate::placement::world_matrix(world, child);
182 let parent_world = parent
183 .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
184 .unwrap_or_else(Mat4::identity);
185 let local = nalgebra_glm::inverse(&parent_world) * child_world;
186
187 let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
188 let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
189 let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
190 let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
191 let scale = nalgebra_glm::vec3(
192 basis_x.magnitude(),
193 basis_y.magnitude(),
194 basis_z.magnitude(),
195 );
196 let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
197 basis_x / scale.x.max(f32::EPSILON),
198 basis_y / scale.y.max(f32::EPSILON),
199 basis_z / scale.z.max(f32::EPSILON),
200 ]);
201 let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
202
203 if parent.is_some() {
204 world.core.add_components(child, PARENT);
205 }
206 update_parent(
207 world,
208 child,
209 parent.map(|parent_entity| Parent(Some(parent_entity))),
210 );
211 assign_local_transform(
212 world,
213 child,
214 LocalTransform {
215 translation,
216 rotation,
217 scale,
218 },
219 );
220}
221
222#[cfg(feature = "picking")]
223pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
224 world
225 .core
226 .get_name(entity)
227 .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
228}
229
230pub(crate) fn api_material_name(entity: Entity) -> String {
231 format!("{MATERIAL_PREFIX}{}", entity.id)
232}
233
234fn mesh_name(shape: Shape) -> &'static str {
235 match shape {
236 Shape::Cube => "Cube",
237 Shape::Sphere => "Sphere",
238 Shape::Cylinder => "Cylinder",
239 Shape::Cone => "Cone",
240 Shape::Torus => "Torus",
241 Shape::Plane => "Plane",
242 }
243}
244
245#[cfg(feature = "physics")]
246fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
247 match shape {
248 Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
249 Shape::Sphere => ColliderComponent::new_ball(scale.x),
250 Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
251 Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
252 Shape::Torus => ColliderComponent {
253 shape: ColliderShape::ConvexMesh {
254 vertices: scaled_mesh_points(world, "Torus", scale),
255 },
256 ..Default::default()
257 },
258 Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
259 }
260}
261
262#[cfg(feature = "physics")]
263fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
264 match shape {
265 Shape::Torus | Shape::Plane => {
266 let shape_mesh_name = mesh_name(shape);
267 ColliderComponent {
268 shape: ColliderShape::TriMesh {
269 vertices: scaled_mesh_points(world, shape_mesh_name, scale),
270 indices: mesh_triangles(world, shape_mesh_name),
271 },
272 ..Default::default()
273 }
274 }
275 _ => dynamic_collider(world, shape, scale),
276 }
277}
278
279#[cfg(feature = "physics")]
280fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
281 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
282 .map(|mesh| {
283 mesh.vertices
284 .iter()
285 .map(|vertex| {
286 [
287 vertex.position[0] * scale.x,
288 vertex.position[1] * scale.y,
289 vertex.position[2] * scale.z,
290 ]
291 })
292 .collect()
293 })
294 .unwrap_or_default()
295}
296
297#[cfg(feature = "physics")]
298fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
299 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
300 .map(|mesh| {
301 mesh.indices
302 .chunks_exact(3)
303 .map(|triangle| [triangle[0], triangle[1], triangle[2]])
304 .collect()
305 })
306 .unwrap_or_default()
307}
308
309#[cfg(feature = "physics")]
310fn attach_body(
311 world: &mut World,
312 entity: Entity,
313 body: RigidBodyComponent,
314 collider: ColliderComponent,
315 dynamic: bool,
316) {
317 let mut flags = RIGID_BODY | COLLIDER;
318 if dynamic {
319 flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
320 }
321 world.core.add_components(entity, flags);
322 world.core.set_rigid_body(entity, body);
323 world.core.set_collider(entity, collider);
324 if dynamic {
325 reset_physics_interpolation(world, entity);
326 if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
327 interpolation.enabled = true;
328 }
329 }
330}