#![allow(clippy::doc_markdown)]
use gloss_hecs::{
CommandBuffer, Component, ComponentError, ComponentRef, DynamicBundle, Entity, EntityBuilder, EntityRef, NoSuchEntity, Query, QueryMut, World,
};
use log::{error, trace};
use crate::{
actor::Actor,
builders,
camera::Camera,
components::{
BoundingBox, CamController, ColorsGPU, DiffuseImg, DiffuseTex, EdgesGPU, EnvironmentMapGpu, FacesGPU, ImgConfig, LightEmit, MeshColorType,
MetalnessTex, ModelMatrix, Name, NormalTex, NormalsGPU, OnGui, PosLookat, Projection, ProjectionWithFov, Renderable, RoughnessTex,
ShadowCaster, ShadowMap, TangentsGPU, UVsGPU, Verts, VertsGPU, VisLines, VisMesh, VisPoints,
},
config::{Config, FloorTexture, FloorType, LightConfig},
light::Light,
network::{ComponentTypeId, SceneSender},
selector::Selectable,
};
use gloss_geometry::geom;
use gloss_utils::abi_stable_aliases::std_types::{RHashMap, RString};
#[cfg(not(target_arch = "wasm32"))]
use gloss_utils::abi_stable_aliases::StableAbi;
use nalgebra as na;
pub static GLOSS_FLOOR_NAME: &str = "floor";
pub static GLOSS_CAM_NAME: &str = "gloss_camera";
static CHECKERBOARD_BYTES: &[u8; 2324] = include_bytes!("../data/checkerboard.png");
#[repr(C)]
#[cfg_attr(not(target_arch = "wasm32"), derive(StableAbi))]
#[allow(non_upper_case_globals, non_camel_case_types)]
pub struct Scene {
world: World,
name2entity: RHashMap<RString, Entity>,
pub command_buffer: CommandBuffer, entity_resource: Entity, }
impl Default for Scene {
fn default() -> Self {
Self::new()
}
}
impl Scene {
pub fn new() -> Self {
let mut world = World::default();
let name2entity = RHashMap::<RString, Entity>::new();
let command_buffer = CommandBuffer::new();
let entity_resource = world.spawn((Name("entity_resource".to_string()),));
Self {
world,
name2entity,
command_buffer,
entity_resource,
}
}
pub fn get_entity_resource(&self) -> Entity {
self.entity_resource
}
pub fn get_unused_name(&self) -> String {
let mut cur_nr = self.get_renderables(false).len();
loop {
let name = String::from("ent_") + &cur_nr.to_string();
let r_name = RString::from(name.clone());
if !self.name2entity.contains_key(&r_name) {
return name;
}
cur_nr += 1;
}
}
pub fn get_or_create_hidden_entity(&mut self, name: &str) -> EntityMut<'_> {
let r_name = RString::from(name.to_string());
let entity_ref = self
.name2entity
.entry(r_name)
.or_insert_with(|| self.world.spawn((Name(name.to_string()),))); EntityMut::new(&mut self.world, *entity_ref)
}
pub fn get_or_create_entity(&mut self, name: &str) -> EntityMut<'_> {
let r_name = RString::from(name.to_string());
let entity_ref = self
.name2entity
.entry(r_name)
.or_insert_with(|| self.world.spawn((Name(name.to_string()), Renderable, Selectable)));
if let Ok(mut sender) = self.world.get::<&mut SceneSender>(self.entity_resource) {
sender.send_entity_spawn(name, true, false);
}
EntityMut::new(&mut self.world, *entity_ref)
}
pub fn get_or_create_gui_entity(&mut self, name: &str) -> EntityMut<'_> {
let r_name = RString::from(name.to_string());
let entity_ref = self
.name2entity
.entry(r_name)
.or_insert_with(|| self.world.spawn((Name(name.to_string()), OnGui)));
if let Ok(mut sender) = self.world.get::<&mut SceneSender>(self.entity_resource) {
sender.send_entity_spawn(name, false, true);
}
EntityMut::new(&mut self.world, *entity_ref)
}
pub fn despawn_with_name(&mut self, name: &str) {
if let Some(entity) = self.get_entity_with_name(name) {
let _ = self.world.despawn(entity);
let r_name = RString::from(name.to_string());
let _ = self.name2entity.remove(&r_name);
}
}
pub fn despawn(&mut self, entity: Entity) {
let name = self.get_comp::<&Name>(&entity).map(|x| RString::from(x.0.to_string()));
if let Ok(name) = name {
let _ = self.name2entity.remove(&name);
}
let _ = self.world.despawn(entity);
}
pub fn get_entity_with_name(&self, name: &str) -> Option<Entity> {
self.name2entity.get(name).copied()
}
pub fn get_entity_mut_with_name(&mut self, name: &str) -> Option<EntityMut<'_>> {
let entity_opt = self.name2entity.get(name);
entity_opt.map(|ent| EntityMut::new(&mut self.world, *ent))
}
pub fn find_entity_with_id(&mut self, id: u8) -> Option<EntityRef<'_>> {
let entities = self.get_all_entities(false);
for entity in entities {
if u32::from(id) == entity.id() {
let e_ref = self.world.entity(entity).unwrap();
return Some(e_ref);
}
}
None
}
pub fn get_current_cam(&self) -> Option<Camera> {
let entity_opt = self.name2entity.get(GLOSS_CAM_NAME);
entity_opt.map(|ent| Camera::from_entity(*ent))
}
pub fn add_resource<T: gloss_hecs::Component>(&mut self, component: T) {
self.world.insert_one(self.entity_resource, component).unwrap();
}
pub fn remove_resource<T: gloss_hecs::Component>(&mut self) -> Result<T, gloss_hecs::ComponentError> {
self.world.remove_one::<T>(self.entity_resource) }
pub fn has_resource<T: gloss_hecs::Component>(&self) -> bool {
self.world.has::<T>(self.entity_resource).unwrap()
}
pub fn get_resource<'a, T: gloss_hecs::ComponentRef<'a>>(&'a self) -> Result<<T as ComponentRef<'a>>::Ref, gloss_hecs::ComponentError> {
self.world.get::<T>(self.entity_resource)
}
pub fn insert_if_doesnt_exist<T: gloss_hecs::Component + Default>(&mut self, entity: Entity) {
if !self.world.has::<T>(entity).unwrap() {
let _ = self.world.insert_one(entity, T::default());
}
}
pub fn get_comp<'a, T: gloss_hecs::ComponentRef<'a>>(
&'a self,
entity: &Entity,
) -> Result<<T as ComponentRef<'a>>::Ref, gloss_hecs::ComponentError> {
self.world.get::<T>(*entity)
}
pub fn get_lights(&self, sorted_by_name: bool) -> Vec<Entity> {
let mut entities_with_name = Vec::new();
for (entity_light, (name, _)) in self.world.query::<(&Name, &LightEmit)>().iter() {
entities_with_name.push((entity_light, name.0.clone()));
}
if sorted_by_name {
entities_with_name.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
}
let entities = entities_with_name.iter().map(|x| x.0).collect();
entities
}
#[allow(clippy::missing_panics_doc)]
pub fn get_all_entities(&self, sorted_by_name: bool) -> Vec<Entity> {
let mut entities_with_name = Vec::new();
for (entity, name) in self.world.query::<&Name>().iter() {
entities_with_name.push((entity, name.0.clone()));
}
if sorted_by_name {
entities_with_name.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
}
let entities = entities_with_name.iter().map(|x| x.0).collect();
entities
}
#[allow(clippy::missing_panics_doc)]
pub fn get_renderables(&self, sorted_by_name: bool) -> Vec<Entity> {
let mut entities_with_name = Vec::new();
for (entity, (name, _)) in self.world.query::<(&Name, &Renderable)>().iter() {
entities_with_name.push((entity, name.0.clone()));
}
if sorted_by_name {
entities_with_name.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
}
let entities = entities_with_name.iter().map(|x| x.0).collect();
entities
}
pub fn get_renderable_names(&self) -> Vec<String> {
self.world
.query::<(&Name, &Renderable)>()
.iter()
.map(|(_, (name, _))| name.0.clone())
.collect()
}
#[allow(clippy::cast_possible_truncation)]
pub fn nr_renderables(&self) -> u32 {
self.get_renderables(false).len() as u32
}
pub fn get_bounding_points(&self) -> (na::Point3<f32>, na::Point3<f32>) {
let mut min_point_global = na::Point3::<f32>::new(f32::MAX, f32::MAX, f32::MAX);
let mut max_point_global = na::Point3::<f32>::new(f32::MIN, f32::MIN, f32::MIN);
for (_entity, (verts, model_matrix_opt, name, _)) in self.world.query::<(&Verts, Option<&ModelMatrix>, &Name, &Renderable)>().iter() {
if name.0 == GLOSS_FLOOR_NAME {
continue;
}
let model_matrix = if let Some(mm) = model_matrix_opt {
mm.clone()
} else {
ModelMatrix::default()
};
trace!("scale for mesh {}", name.0);
let min_coord_vec: Vec<f32> = verts.0.column_iter().map(|c| c.min()).collect();
let max_coord_vec: Vec<f32> = verts.0.column_iter().map(|c| c.max()).collect();
let min_point = na::Point3::<f32>::from_slice(&min_coord_vec);
let max_point = na::Point3::<f32>::from_slice(&max_coord_vec);
let min_point_w = model_matrix.0 * min_point;
let max_point_w = model_matrix.0 * max_point;
min_point_global = min_point_global.inf(&min_point_w);
max_point_global = max_point_global.sup(&max_point_w);
}
for (_entity, (bbox, name, _)) in self.world.query::<(&BoundingBox, &Name, &Renderable)>().without::<&Verts>().iter() {
if name.0 == GLOSS_FLOOR_NAME {
continue;
}
min_point_global = min_point_global.inf(&bbox.min);
max_point_global = max_point_global.sup(&bbox.max);
}
(min_point_global, max_point_global)
}
pub fn get_min_y(&self) -> f32 {
let mut min_y_global = f32::MAX;
for (_entity, (verts, model_matrix_opt, name, _)) in self.world.query::<(&Verts, Option<&ModelMatrix>, &Name, &Renderable)>().iter() {
if name.0 == GLOSS_FLOOR_NAME {
continue;
}
let model_matrix = if let Some(mm) = model_matrix_opt {
mm.clone()
} else {
ModelMatrix::default()
};
let v_world = geom::transform_verts(&verts.0, &model_matrix.0);
let min_y_cur = v_world.column(1).min();
min_y_global = min_y_global.min(min_y_cur);
}
for (_entity, (bbox, name, _)) in self.world.query::<(&BoundingBox, &Name, &Renderable)>().without::<&Verts>().iter() {
if name.0 == GLOSS_FLOOR_NAME {
continue;
}
min_y_global = min_y_global.min(bbox.min.y);
}
min_y_global
}
pub fn get_scale(&self) -> f32 {
if self.get_renderables(false).is_empty() {
error!("scale: no renderables, returning 1.0");
return 1.0;
}
let (min_point_global, max_point_global) = self.get_bounding_points();
let scale = (max_point_global - min_point_global).abs().max();
if scale.is_infinite() {
1.0
} else {
scale
}
}
pub fn get_centroid(&self) -> na::Point3<f32> {
if self.get_renderables(false).is_empty() {
error!("centroid: no renderables, returning 1.0");
return na::Point3::<f32>::origin();
}
let (min_point_global, max_point_global) = self.get_bounding_points();
min_point_global.lerp(&max_point_global, 0.5)
}
pub fn init_3_point_light(&self, light_config: &mut LightConfig, idx: usize, scale: f32, centroid: &na::Point3<f32>, flip_y: bool) {
let (mut dir_movement, intensity_at_point) = match idx {
0 => {
let dir_movement = na::Vector3::new(0.5, 0.6, 0.5);
let intensity_at_point = 2.9;
(dir_movement, intensity_at_point)
}
1 => {
let dir_movement = na::Vector3::new(-0.5, 0.6, 0.5);
let intensity_at_point = 1.0;
(dir_movement, intensity_at_point)
}
2 => {
let dir_movement = na::Vector3::new(-0.1, 0.6, -0.5);
let intensity_at_point = 3.5;
(dir_movement, intensity_at_point)
}
_ => {
let dir_movement = na::Vector3::new(0.0, 0.6, 0.5);
let intensity_at_point = 2.9;
(dir_movement, intensity_at_point)
}
};
if flip_y {
dir_movement[1] *= -1.0;
}
dir_movement = dir_movement.normalize();
let lookat = centroid;
let position = centroid + dir_movement * 8.0 * scale; let intensity = Light::intensity_for_point(&position, lookat, intensity_at_point);
if light_config.position.is_none() {
light_config.position = Some(position);
}
if light_config.lookat.is_none() {
light_config.lookat = Some(*lookat);
}
if light_config.near.is_none() {
light_config.near = Some(scale * 0.5);
}
if light_config.far.is_none() {
light_config.far = Some(scale * 30.0);
}
if light_config.intensity.is_none() {
light_config.intensity = Some(intensity);
}
if light_config.range.is_none() {
light_config.range = Some(scale * 30.0);
}
if light_config.radius.is_none() {
light_config.radius = Some(scale * 1.5);
}
if light_config.shadow_res.is_none() {
light_config.shadow_res = Some(2048);
}
if light_config.shadow_bias_fixed.is_none() {
light_config.shadow_bias_fixed = Some(2e-6);
}
if light_config.shadow_bias.is_none() {
light_config.shadow_bias = Some(2e-6);
}
if light_config.shadow_bias_normal.is_none() {
light_config.shadow_bias_normal = Some(2e-6);
}
}
pub fn make_concrete_config(&self, config: &mut Config) {
let scale = self.get_scale();
let centroid = self.get_centroid();
let min_y = self.get_min_y();
let floor_scale_multiplier = 300.0;
if config.core.floor_scale.is_none() {
config.core.floor_scale = Some(scale * floor_scale_multiplier); }
if config.core.floor_origin.is_none() {
config.core.floor_origin = Some(na::Point3::<f32>::new(centroid.x, min_y, centroid.z));
}
if config.core.floor_uv_scale.is_none() {
config.core.floor_uv_scale = Some(scale * floor_scale_multiplier * 1.4);
}
let position = centroid + na::Vector3::z_axis().scale(2.0 * scale) + na::Vector3::y_axis().scale(0.5 * scale);
if config.scene.cam.position.is_none() {
config.scene.cam.position = Some(position);
}
if config.scene.cam.lookat.is_none() {
config.scene.cam.lookat = Some(centroid);
}
if config.scene.cam.near.is_none() {
let near = (centroid - position).norm() * 0.02;
config.scene.cam.near = Some(near);
}
if config.scene.cam.far.is_none() {
let far = (centroid - position).norm() * 50.0; config.scene.cam.far = Some(far);
}
if config.render.distance_fade_center.is_none() {
config.render.distance_fade_center = Some(centroid);
}
if config.render.distance_fade_start.is_none() {
config.render.distance_fade_start = Some(scale * 1.0);
}
if config.render.distance_fade_end.is_none() {
config.render.distance_fade_end = Some(scale * 8.5);
}
for (idx, light_config) in config.scene.lights.iter_mut().enumerate() {
self.init_3_point_light(light_config, idx, scale, ¢roid, config.render.flip_light_position_y);
}
config.set_concrete();
}
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::needless_update)]
#[allow(clippy::too_many_lines)]
pub fn from_config(&mut self, config: &mut Config, width: u32, height: u32) {
let cam = self.get_current_cam().expect("Camera should be created");
if !self.world.has::<PosLookat>(cam.entity).unwrap() {
self.world
.insert(
cam.entity,
(
PosLookat::new(config.scene.cam.position.unwrap(), config.scene.cam.lookat.unwrap()),
CamController::new(
config.scene.cam.limit_max_dist,
config.scene.cam.limit_max_vertical_angle,
config.scene.cam.limit_min_vertical_angle,
),
),
)
.unwrap();
}
if !self.world.has::<Projection>(cam.entity).unwrap() {
let aspect_ratio = width as f32 / height as f32;
self.world
.insert(
cam.entity,
(Projection::WithFov(ProjectionWithFov {
aspect_ratio,
fovy: config.scene.cam.fovy,
near: config.scene.cam.near.unwrap(),
far: config.scene.cam.far.unwrap(),
..Default::default()
}),),
)
.unwrap();
}
for (idx, light_config) in config.scene.lights.iter_mut().enumerate() {
let entity = self
.get_or_create_hidden_entity(("light_".to_owned() + idx.to_string().as_str()).as_str())
.insert(PosLookat::new(light_config.position.unwrap(), light_config.lookat.unwrap()))
.insert(Projection::WithFov(ProjectionWithFov {
aspect_ratio: 1.0,
fovy: light_config.fovy, near: light_config.near.unwrap(),
far: light_config.far.unwrap(),
..Default::default()
}))
.insert(LightEmit {
color: light_config.color,
intensity: light_config.intensity.unwrap(),
range: light_config.range.unwrap(),
radius: light_config.radius.unwrap(),
..Default::default()
})
.entity;
let shadow_res = light_config.shadow_res.unwrap_or(0);
if shadow_res != 0 {
self.world
.insert_one(
entity,
ShadowCaster {
shadow_res: light_config.shadow_res.unwrap(),
shadow_bias_fixed: light_config.shadow_bias_fixed.unwrap(),
shadow_bias: light_config.shadow_bias.unwrap(),
shadow_bias_normal: light_config.shadow_bias_normal.unwrap(),
},
)
.ok();
}
}
if config.core.auto_add_floor {
self.create_floor(config);
}
config.set_consumed();
}
pub fn create_floor(&mut self, config: &Config) {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let floor_builder = match config.core.floor_type {
FloorType::Solid => builders::build_plane(
config.core.floor_origin.unwrap(),
na::Vector3::<f32>::new(0.0, 1.0, 0.0),
config.core.floor_scale.unwrap(),
config.core.floor_scale.unwrap(),
false,
),
FloorType::Grid => builders::build_grid(
config.core.floor_origin.unwrap(),
na::Vector3::<f32>::new(0.0, 1.0, 0.0),
(config.core.floor_scale.unwrap() / 1.0) as u32,
(config.core.floor_scale.unwrap() / 1.0) as u32,
config.core.floor_scale.unwrap(),
config.core.floor_scale.unwrap(),
false,
),
};
#[allow(clippy::cast_possible_truncation)]
let floor_ent = self
.get_or_create_entity(GLOSS_FLOOR_NAME)
.insert_builder(floor_builder)
.insert(VisMesh {
show_mesh: config.core.floor_type == FloorType::Solid,
solid_color: na::Vector4::<f32>::new(0.08, 0.08, 0.08, 1.0), perceptual_roughness: 0.70,
uv_scale: config.core.floor_uv_scale.unwrap(),
color_type: MeshColorType::Texture,
..Default::default()
})
.insert(VisPoints::default())
.insert(VisLines {
show_lines: config.core.floor_type == FloorType::Grid,
line_color: na::Vector4::<f32>::new(0.2, 0.2, 0.2, 1.0),
line_width: config.core.floor_grid_line_width,
antialias_edges: true,
..Default::default()
})
.entity();
if config.core.floor_texture == FloorTexture::Checkerboard {
let texture_checkerboard = DiffuseImg::new_from_buf(
CHECKERBOARD_BYTES,
&ImgConfig {
mipmap_generation_cpu: true,
..Default::default()
},
);
let _ = self.world.insert_one(floor_ent, texture_checkerboard);
}
}
#[allow(clippy::missing_panics_doc)]
pub fn get_floor(&self) -> Option<Actor> {
let ent_opt = self.name2entity.get(GLOSS_FLOOR_NAME);
ent_opt.map(|ent| Actor::from_entity(*ent))
}
pub fn has_floor(&self) -> bool {
self.name2entity.get(GLOSS_FLOOR_NAME).is_some()
}
pub fn create_camera(&mut self) {
Camera::new(GLOSS_CAM_NAME, self, false);
}
pub fn remove_all_gpu_components(&mut self) {
let mut command_buffer = CommandBuffer::new();
for (entity, ()) in self.world.query::<()>().iter() {
command_buffer.remove_one::<VertsGPU>(entity);
command_buffer.remove_one::<UVsGPU>(entity);
command_buffer.remove_one::<NormalsGPU>(entity);
command_buffer.remove_one::<ColorsGPU>(entity);
command_buffer.remove_one::<EdgesGPU>(entity);
command_buffer.remove_one::<FacesGPU>(entity);
command_buffer.remove_one::<TangentsGPU>(entity);
command_buffer.remove_one::<DiffuseTex>(entity);
command_buffer.remove_one::<NormalTex>(entity);
command_buffer.remove_one::<MetalnessTex>(entity);
command_buffer.remove_one::<RoughnessTex>(entity);
command_buffer.remove_one::<EnvironmentMapGpu>(entity);
command_buffer.remove_one::<ShadowMap>(entity);
}
command_buffer.run_on(&mut self.world);
}
pub fn update_name2entity(&mut self, name_new_entity: &str) {
for entity in self.get_all_entities(false) {
let name = self.get_comp::<&Name>(&entity).unwrap().0.clone();
if name == name_new_entity {
self.name2entity.insert(name_new_entity.to_string().into(), entity);
}
}
}
pub fn world(&self) -> &World {
&self.world
}
pub fn world_mut(&mut self) -> WorldMut<'_> {
WorldMut {
world: &mut self.world,
entity_resource: self.entity_resource,
}
}
}
pub struct WorldMut<'w> {
world: &'w mut World,
entity_resource: Entity,
}
impl WorldMut<'_> {
pub fn run_command_buffer(&mut self, command_buffer: &mut CommandBuffer) {
command_buffer.run_on(self.world);
}
pub fn query_mut<Q: Query>(&mut self) -> QueryMut<'_, Q> {
self.world.query_mut::<Q>()
}
#[allow(clippy::missing_errors_doc)]
pub fn insert_one<T: Component>(&mut self, entity: Entity, component: T) -> Result<(), NoSuchEntity> {
let type_id = ComponentTypeId::of::<T>();
if let Ok(mut sender) = self.world.get::<&mut SceneSender>(self.entity_resource) {
if sender.is_component_sendable::<T>() {
let entity_name = self.world.get::<&Name>(entity).ok().map(|n| n.0.clone()).unwrap_or_default();
if let Some(serialized_component) = sender.try_serialize_component(&type_id, &component as &dyn std::any::Any) {
if let Err(e) = sender.send_component(&entity_name, serialized_component) {
log::warn!("Failed to send component: {e}");
}
}
}
}
self.world.insert_one(entity, component)
}
#[allow(clippy::missing_errors_doc)]
pub fn insert(&mut self, entity: Entity, component: impl DynamicBundle) -> Result<(), NoSuchEntity> {
self.world.insert(entity, component)
}
#[allow(clippy::missing_errors_doc)]
pub fn remove_one<T: Component>(&mut self, entity: Entity) -> Result<T, ComponentError> {
self.world.remove_one::<T>(entity)
}
pub fn set_trackers_changed(&mut self) {
self.world.set_trackers_changed();
}
pub fn clear_trackers(&mut self) {
self.world.clear_trackers();
}
}
pub struct EntityMut<'w> {
world: &'w mut World,
entity: Entity,
}
impl<'w> EntityMut<'w> {
pub(crate) fn new(world: &'w mut World, entity: Entity) -> Self {
EntityMut { world, entity }
}
pub fn insert<T: Component>(&mut self, component: T) -> &mut Self {
self.insert_bundle((component,))
}
pub fn insert_bundle(&mut self, bundle: impl DynamicBundle) -> &mut Self {
let _ = self.world.insert(self.entity, bundle);
self
}
pub fn remove<T: Component>(&mut self) -> &mut Self {
let _ = self.world.remove_one::<T>(self.entity);
self
}
pub fn insert_builder(&mut self, mut builder: EntityBuilder) -> &mut Self {
let _ = self.world.insert(self.entity, builder.build());
self
}
pub fn get_comp<'a, T: gloss_hecs::ComponentRef<'a>>(&'a self) -> <T as ComponentRef<'a>>::Ref {
let comp = self.world.get::<T>(self.entity).unwrap();
comp
}
pub fn entity(&self) -> Entity {
self.entity
}
}