use crate::palette::WHITE;
use crate::runner::MATERIAL_PREFIX;
use nightshade::prelude::nalgebra_glm::Mat4;
use nightshade::prelude::*;
pub use nightshade::prelude::despawn_recursive_immediate as despawn;
pub use nightshade::prelude::spawn_cone_at as spawn_cone;
pub use nightshade::prelude::spawn_cube_at as spawn_cube;
pub use nightshade::prelude::spawn_cylinder_at as spawn_cylinder;
pub use nightshade::prelude::spawn_plane_at as spawn_plane;
pub use nightshade::prelude::spawn_sphere_at as spawn_sphere;
pub use nightshade::prelude::spawn_torus_at as spawn_torus;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Shape {
#[default]
Cube,
Sphere,
Cylinder,
Cone,
Torus,
Plane,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Body {
#[default]
None,
Static,
Dynamic {
mass: f32,
},
}
pub struct Object {
pub shape: Shape,
pub position: Vec3,
pub scale: Vec3,
pub color: [f32; 4],
pub body: Body,
}
impl Default for Object {
fn default() -> Self {
Self {
shape: Shape::Cube,
position: Vec3::zeros(),
scale: Vec3::new(1.0, 1.0, 1.0),
color: WHITE,
body: Body::None,
}
}
}
pub fn spawn_object(world: &mut World, object: Object) -> Entity {
let entity = spawn_mesh_at(
world,
mesh_name(object.shape),
object.position,
object.scale,
);
crate::appearance::set_color(world, entity, object.color);
match object.body {
Body::None => {}
#[cfg(feature = "physics")]
Body::Static => {
let collider = static_collider(world, object.shape, object.scale)
.with_friction(0.8)
.with_restitution(0.1);
attach_body(
world,
entity,
RigidBodyComponent::new_static().with_translation(
object.position.x,
object.position.y,
object.position.z,
),
collider,
false,
);
}
#[cfg(feature = "physics")]
Body::Dynamic { mass } => {
let collider = dynamic_collider(world, object.shape, object.scale)
.with_friction(0.7)
.with_restitution(0.2);
attach_body(
world,
entity,
RigidBodyComponent::new_dynamic()
.with_translation(object.position.x, object.position.y, object.position.z)
.with_mass(mass),
collider,
true,
);
}
#[cfg(not(feature = "physics"))]
Body::Static | Body::Dynamic { .. } => {}
}
entity
}
pub fn spawn_objects(world: &mut World, object: Object, positions: &[Vec3]) -> Vec<Entity> {
let mut entities = Vec::with_capacity(positions.len());
let mut shared_material: Option<String> = None;
#[cfg(feature = "physics")]
let mut collider_template: Option<ColliderComponent> = None;
for &position in positions {
let entity = spawn_mesh_at(world, mesh_name(object.shape), position, object.scale);
match shared_material.as_deref() {
None => {
crate::appearance::set_color(world, entity, object.color);
shared_material = world
.core
.get_material_ref(entity)
.map(|material_ref| material_ref.name.clone());
}
Some(name) => {
let name = name.to_string();
adopt_shared_material(world, entity, &name);
}
}
#[cfg(feature = "physics")]
match object.body {
Body::None => {}
Body::Static => {
let collider = collider_template
.get_or_insert_with(|| {
static_collider(world, object.shape, object.scale)
.with_friction(0.8)
.with_restitution(0.1)
})
.clone();
attach_body(
world,
entity,
RigidBodyComponent::new_static()
.with_translation(position.x, position.y, position.z),
collider,
false,
);
}
Body::Dynamic { mass } => {
let collider = collider_template
.get_or_insert_with(|| {
dynamic_collider(world, object.shape, object.scale)
.with_friction(0.7)
.with_restitution(0.2)
})
.clone();
attach_body(
world,
entity,
RigidBodyComponent::new_dynamic()
.with_translation(position.x, position.y, position.z)
.with_mass(mass),
collider,
true,
);
}
}
entities.push(entity);
}
entities
}
pub fn spawn_instanced(
world: &mut World,
shape: Shape,
transforms: Vec<InstanceTransform>,
color: [f32; 4],
) -> Entity {
let shape_mesh_name = mesh_name(shape);
ensure_primitive_mesh(world, shape_mesh_name);
let fallback = format!(
"api::material::instanced::{:.4}_{:.4}_{:.4}_{:.4}",
color[0], color[1], color[2], color[3]
);
let material_name = nightshade::ecs::material::resources::material_registry_find_or_insert(
&mut world.resources.assets.material_registry,
fallback,
Material {
base_color: color,
..Default::default()
},
);
spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
}
fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
let previous = world
.core
.get_material_ref(entity)
.map(|material_ref| material_ref.name.clone());
if let Some(previous_name) = previous
&& let Some((index, _)) = registry_lookup_index(
&world.resources.assets.material_registry.registry,
&previous_name,
)
{
registry_remove_reference(
&mut world.resources.assets.material_registry.registry,
index,
);
}
if let Some((index, _)) =
registry_lookup_index(&world.resources.assets.material_registry.registry, name)
{
registry_add_reference(
&mut world.resources.assets.material_registry.registry,
index,
);
}
world
.core
.set_material_ref(entity, MaterialRef::new(name.to_string()));
world.resources.mesh_render_state.mark_entity_added(entity);
}
pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
use nightshade::ecs::mesh::components::{
create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
create_sphere_mesh, create_torus_mesh,
};
if !world
.resources
.assets
.mesh_cache
.registry
.name_to_index
.contains_key(mesh_name)
{
let mesh = match mesh_name {
"Cube" => create_cube_mesh(),
"Sphere" => create_sphere_mesh(1.0, 16),
"Plane" => create_plane_mesh(2.0),
"Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
"Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
_ => create_cone_mesh(0.5, 1.0, 16),
};
mesh_cache_insert(
&mut world.resources.assets.mesh_cache,
mesh_name.to_string(),
mesh,
);
}
if let Some((index, _)) =
registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
{
registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
}
}
pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
spawn_cloth(
world,
Cloth {
width,
height,
..Default::default()
},
position,
"Cloth".to_string(),
)
}
pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
if let Some(visibility) = world.core.get_visibility_mut(entity) {
visibility.visible = visible;
}
}
pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
let entity = spawn_mesh_at(
world,
"Plane",
Vec3::zeros(),
Vec3::new(half_extent, 1.0, half_extent),
);
#[cfg(feature = "physics")]
attach_body(
world,
entity,
RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
.with_friction(0.8)
.with_restitution(0.1),
false,
);
entity
}
pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
let mut result =
import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
nightshade::ecs::loading::queue_gltf_load(world, &mut result);
let prefab = &result.prefabs[0];
nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
world,
prefab,
&result.animations,
&result.skins,
position,
)
}
pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
if let Some(player) = world.core.get_animation_player_mut(entity) {
player.play(clip_index);
}
}
pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
if let Some(player) = world.core.get_animation_player_mut(entity) {
player.looping = looping;
}
}
pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
1,
)[0];
world.core.set_name(entity, Name("Group".to_string()));
assign_local_transform(
world,
entity,
LocalTransform {
translation: position,
..Default::default()
},
);
entity
}
pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
let child_world = crate::placement::world_matrix(world, child);
let parent_world = parent
.map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
.unwrap_or_else(Mat4::identity);
let local = nalgebra_glm::inverse(&parent_world) * child_world;
let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
let scale = nalgebra_glm::vec3(
basis_x.magnitude(),
basis_y.magnitude(),
basis_z.magnitude(),
);
let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
basis_x / scale.x.max(f32::EPSILON),
basis_y / scale.y.max(f32::EPSILON),
basis_z / scale.z.max(f32::EPSILON),
]);
let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
if parent.is_some() {
world.core.add_components(child, PARENT);
}
update_parent(
world,
child,
parent.map(|parent_entity| Parent(Some(parent_entity))),
);
assign_local_transform(
world,
child,
LocalTransform {
translation,
rotation,
scale,
},
);
}
#[cfg(feature = "picking")]
pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
world
.core
.get_name(entity)
.is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
}
pub(crate) fn api_material_name(entity: Entity) -> String {
format!("{MATERIAL_PREFIX}{}", entity.id)
}
fn mesh_name(shape: Shape) -> &'static str {
match shape {
Shape::Cube => "Cube",
Shape::Sphere => "Sphere",
Shape::Cylinder => "Cylinder",
Shape::Cone => "Cone",
Shape::Torus => "Torus",
Shape::Plane => "Plane",
}
}
#[cfg(feature = "physics")]
fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
match shape {
Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
Shape::Sphere => ColliderComponent::new_ball(scale.x),
Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
Shape::Torus => ColliderComponent {
shape: ColliderShape::ConvexMesh {
vertices: scaled_mesh_points(world, "Torus", scale),
},
..Default::default()
},
Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
}
}
#[cfg(feature = "physics")]
fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
match shape {
Shape::Torus | Shape::Plane => {
let shape_mesh_name = mesh_name(shape);
ColliderComponent {
shape: ColliderShape::TriMesh {
vertices: scaled_mesh_points(world, shape_mesh_name, scale),
indices: mesh_triangles(world, shape_mesh_name),
},
..Default::default()
}
}
_ => dynamic_collider(world, shape, scale),
}
}
#[cfg(feature = "physics")]
fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
.map(|mesh| {
mesh.vertices
.iter()
.map(|vertex| {
[
vertex.position[0] * scale.x,
vertex.position[1] * scale.y,
vertex.position[2] * scale.z,
]
})
.collect()
})
.unwrap_or_default()
}
#[cfg(feature = "physics")]
fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
.map(|mesh| {
mesh.indices
.chunks_exact(3)
.map(|triangle| [triangle[0], triangle[1], triangle[2]])
.collect()
})
.unwrap_or_default()
}
#[cfg(feature = "physics")]
fn attach_body(
world: &mut World,
entity: Entity,
body: RigidBodyComponent,
collider: ColliderComponent,
dynamic: bool,
) {
let mut flags = RIGID_BODY | COLLIDER;
if dynamic {
flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
}
world.core.add_components(entity, flags);
world.core.set_rigid_body(entity, body);
world.core.set_collider(entity, collider);
if dynamic {
reset_physics_interpolation(world, entity);
if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
interpolation.enabled = true;
}
}
}