use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::interaction::select::selection::{NodeId, Selection};
use crate::renderer::{PickId, SceneRenderItem};
use crate::resources::mesh::mesh_store::MeshId;
use crate::scene::aabb::Aabb;
use crate::scene::material::Material;
use crate::scene::spatial_index::SpatialIndex;
use crate::scene::traits::ViewportObject;
use crate::{LightKind, LightSource};
const SPATIAL_THRESHOLD: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LayerId(pub u32);
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Layer {
pub id: LayerId,
pub name: String,
pub visible: bool,
pub locked: bool,
pub colour: [f32; 4],
pub order: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GroupId(pub u32);
pub struct Group {
pub id: GroupId,
pub name: String,
pub members: HashSet<NodeId>,
}
pub struct SceneNode {
id: NodeId,
name: String,
mesh_id: Option<MeshId>,
material: Material,
appearance: crate::scene::material::ItemSettings,
visible: bool,
show_normals: bool,
local_transform: glam::Mat4,
world_transform: glam::Mat4,
parent: Option<NodeId>,
children: Vec<NodeId>,
layer: LayerId,
dirty: bool,
deform_instance: Option<u32>,
receives_decals: bool,
lod_group: Option<crate::resources::LodGroupId>,
pub light: Option<LightSource>,
}
impl SceneNode {
pub fn id(&self) -> NodeId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn mesh_id(&self) -> Option<MeshId> {
self.mesh_id
}
pub fn lod_group(&self) -> Option<crate::resources::LodGroupId> {
self.lod_group
}
pub fn material(&self) -> &Material {
&self.material
}
pub fn appearance(&self) -> &crate::scene::material::ItemSettings {
&self.appearance
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn show_normals(&self) -> bool {
self.show_normals
}
pub fn local_transform(&self) -> glam::Mat4 {
self.local_transform
}
pub fn world_transform(&self) -> glam::Mat4 {
self.world_transform
}
pub fn parent(&self) -> Option<NodeId> {
self.parent
}
pub fn children(&self) -> &[NodeId] {
&self.children
}
pub fn layer(&self) -> LayerId {
self.layer
}
pub fn receives_decals(&self) -> bool {
self.receives_decals
}
pub fn set_receives_decals(&mut self, v: bool) {
self.receives_decals = v;
}
}
impl ViewportObject for SceneNode {
fn id(&self) -> u64 {
self.id
}
fn mesh_id(&self) -> Option<u64> {
self.mesh_id.map(|m| m.index() as u64)
}
fn model_matrix(&self) -> glam::Mat4 {
self.world_transform
}
fn position(&self) -> glam::Vec3 {
self.world_transform.col(3).truncate()
}
fn rotation(&self) -> glam::Quat {
let (_scale, rotation, _translation) = self.world_transform.to_scale_rotation_translation();
rotation
}
fn scale(&self) -> glam::Vec3 {
let (scale, _rotation, _translation) = self.world_transform.to_scale_rotation_translation();
scale
}
fn is_visible(&self) -> bool {
self.visible
}
fn colour(&self) -> glam::Vec3 {
glam::Vec3::from(self.material.base_colour)
}
fn show_normals(&self) -> bool {
self.show_normals
}
fn material(&self) -> Material {
self.material
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SceneStats {
pub spatial_index_nodes: u32,
pub frustum_cull_traversal_ms: f32,
}
fn resolve_light_to_world(src: &LightSource, world: glam::Mat4) -> LightSource {
let translation = world.col(3).truncate();
let kind = match &src.kind {
LightKind::Directional { direction } => {
let rotated = world
.transform_vector3(glam::Vec3::from(*direction))
.normalize();
LightKind::Directional {
direction: rotated.into(),
}
}
LightKind::Point { range, .. } => LightKind::Point {
position: translation.into(),
range: *range,
},
LightKind::Spot {
direction,
range,
inner_angle,
outer_angle,
..
} => {
let rotated = world
.transform_vector3(glam::Vec3::from(*direction))
.normalize();
LightKind::Spot {
position: translation.into(),
direction: rotated.into(),
range: *range,
inner_angle: *inner_angle,
outer_angle: *outer_angle,
}
}
};
LightSource {
kind,
colour: src.colour,
intensity: src.intensity,
importance: src.importance,
cast_shadows: src.cast_shadows,
}
}
const DEFAULT_LAYER: LayerId = LayerId(0);
pub struct Scene {
nodes: HashMap<NodeId, SceneNode>,
roots: Vec<NodeId>,
layers: Vec<Layer>,
next_id: u64,
next_layer_id: u32,
groups: Vec<Group>,
next_group_id: u32,
version: u64,
spatial: SpatialIndex,
spatial_built: bool,
last_scene_stats: SceneStats,
live_decals: Vec<LiveDecal>,
next_decal_id: u64,
}
static SCENE_VERSION_CLOCK: AtomicU64 = AtomicU64::new(0);
impl Scene {
pub fn new() -> Self {
let base = SCENE_VERSION_CLOCK.fetch_add(1 << 20, Ordering::Relaxed);
Self {
nodes: HashMap::new(),
roots: Vec::new(),
layers: vec![Layer {
id: DEFAULT_LAYER,
name: "Default".to_string(),
visible: true,
locked: false,
colour: [1.0, 1.0, 1.0, 1.0],
order: 0,
}],
next_id: 1,
next_layer_id: 1,
groups: Vec::new(),
next_group_id: 0,
version: base,
spatial: SpatialIndex::new(),
spatial_built: false,
last_scene_stats: SceneStats::default(),
live_decals: Vec::new(),
next_decal_id: 0,
}
}
pub fn version(&self) -> u64 {
self.version
}
pub fn add(
&mut self,
mesh_id: Option<MeshId>,
transform: glam::Mat4,
material: Material,
) -> NodeId {
self.add_named("", mesh_id, transform, material)
}
pub fn add_named(
&mut self,
name: &str,
mesh_id: Option<MeshId>,
transform: glam::Mat4,
material: Material,
) -> NodeId {
let id = self.next_id;
self.next_id += 1;
let node = SceneNode {
id,
name: name.to_string(),
mesh_id,
material,
appearance: crate::scene::material::ItemSettings::default(),
visible: true,
show_normals: false,
local_transform: transform,
world_transform: transform,
parent: None,
children: Vec::new(),
layer: DEFAULT_LAYER,
dirty: true,
deform_instance: None,
receives_decals: true,
lod_group: None,
light: None,
};
self.nodes.insert(id, node);
self.roots.push(id);
self.version = self.version.wrapping_add(1);
if self.spatial_built {
self.spatial.mark_dirty(id);
}
id
}
pub fn remove(&mut self, id: NodeId) -> Vec<NodeId> {
let mut removed = Vec::new();
self.remove_recursive(id, &mut removed);
if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
if let Some(parent) = self.nodes.get_mut(&parent_id) {
parent.children.retain(|c| *c != id);
}
} else {
self.roots.retain(|r| *r != id);
}
if self.spatial_built {
for &rid in &removed {
self.spatial.remove_node(rid);
}
}
for &rid in &removed {
self.nodes.remove(&rid);
}
self.roots.retain(|r| !removed.contains(r));
for group in &mut self.groups {
for &rid in &removed {
group.members.remove(&rid);
}
}
self.version = self.version.wrapping_add(1);
removed
}
fn remove_recursive(&self, id: NodeId, out: &mut Vec<NodeId>) {
out.push(id);
if let Some(node) = self.nodes.get(&id) {
for &child in &node.children {
self.remove_recursive(child, out);
}
}
}
pub fn remove_many(&mut self, ids: &[NodeId]) -> Vec<NodeId> {
let mut removed = Vec::new();
let mut removed_set = HashSet::new();
for &id in ids {
if self.nodes.contains_key(&id) {
self.collect_subtree(id, &mut removed_set, &mut removed);
}
}
if removed.is_empty() {
return removed;
}
let mut parents_to_fix = HashSet::new();
for &id in ids {
if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
if !removed_set.contains(&parent_id) {
parents_to_fix.insert(parent_id);
}
}
}
for parent_id in parents_to_fix {
if let Some(parent) = self.nodes.get_mut(&parent_id) {
parent.children.retain(|c| !removed_set.contains(c));
}
}
if self.spatial_built {
for &rid in &removed {
self.spatial.remove_node(rid);
}
}
for &rid in &removed {
self.nodes.remove(&rid);
}
self.roots.retain(|r| !removed_set.contains(r));
for group in &mut self.groups {
group.members.retain(|m| !removed_set.contains(m));
}
self.version = self.version.wrapping_add(1);
removed
}
fn collect_subtree(&self, id: NodeId, seen: &mut HashSet<NodeId>, out: &mut Vec<NodeId>) {
if !seen.insert(id) {
return;
}
out.push(id);
if let Some(node) = self.nodes.get(&id) {
for &child in &node.children {
self.collect_subtree(child, seen, out);
}
}
}
pub fn set_parent(&mut self, child_id: NodeId, new_parent: Option<NodeId>) {
let old_parent = self.nodes.get(&child_id).and_then(|n| n.parent);
if let Some(old_pid) = old_parent {
if let Some(old_p) = self.nodes.get_mut(&old_pid) {
old_p.children.retain(|c| *c != child_id);
}
} else {
self.roots.retain(|r| *r != child_id);
}
if let Some(new_pid) = new_parent {
if let Some(new_p) = self.nodes.get_mut(&new_pid) {
new_p.children.push(child_id);
}
} else {
self.roots.push(child_id);
}
if let Some(node) = self.nodes.get_mut(&child_id) {
node.parent = new_parent;
node.dirty = true;
}
self.version = self.version.wrapping_add(1);
if self.spatial_built {
self.mark_spatial_dirty_subtree(child_id);
}
}
pub fn children(&self, id: NodeId) -> &[NodeId] {
self.nodes
.get(&id)
.map(|n| n.children.as_slice())
.unwrap_or(&[])
}
pub fn parent(&self, id: NodeId) -> Option<NodeId> {
self.nodes.get(&id).and_then(|n| n.parent)
}
pub fn roots(&self) -> &[NodeId] {
&self.roots
}
pub fn set_local_transform(&mut self, id: NodeId, transform: glam::Mat4) {
if let Some(node) = self.nodes.get_mut(&id) {
node.local_transform = transform;
node.dirty = true;
}
self.mark_descendants_dirty(id);
self.version = self.version.wrapping_add(1);
if self.spatial_built {
self.mark_spatial_dirty_subtree(id);
}
}
pub fn set_visible(&mut self, id: NodeId, visible: bool) {
if let Some(node) = self.nodes.get_mut(&id) {
node.visible = visible;
}
self.version = self.version.wrapping_add(1);
if self.spatial_built {
self.spatial.mark_dirty(id);
}
}
pub fn set_material(&mut self, id: NodeId, material: Material) {
if let Some(node) = self.nodes.get_mut(&id) {
node.material = material;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_appearance(&mut self, id: NodeId, appearance: crate::scene::material::ItemSettings) {
if let Some(node) = self.nodes.get_mut(&id) {
node.appearance = appearance;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_mesh(&mut self, id: NodeId, mesh_id: Option<MeshId>) {
if let Some(node) = self.nodes.get_mut(&id) {
node.mesh_id = mesh_id;
}
self.version = self.version.wrapping_add(1);
if self.spatial_built {
self.spatial.mark_dirty(id);
}
}
pub fn set_lod_group(&mut self, id: NodeId, lod_group: Option<crate::resources::LodGroupId>) {
if let Some(node) = self.nodes.get_mut(&id) {
node.lod_group = lod_group;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_name(&mut self, id: NodeId, name: &str) {
if let Some(node) = self.nodes.get_mut(&id) {
node.name = name.to_string();
}
self.version = self.version.wrapping_add(1);
}
pub fn set_deform_instance(&mut self, id: NodeId, instance: Option<u32>) {
if let Some(node) = self.nodes.get_mut(&id) {
node.deform_instance = instance;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_receives_decals(&mut self, id: NodeId, v: bool) {
if let Some(node) = self.nodes.get_mut(&id) {
node.receives_decals = v;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_show_normals(&mut self, id: NodeId, show: bool) {
if let Some(node) = self.nodes.get_mut(&id) {
node.show_normals = show;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_layer(&mut self, id: NodeId, layer: LayerId) {
if let Some(node) = self.nodes.get_mut(&id) {
node.layer = layer;
}
self.version = self.version.wrapping_add(1);
}
pub fn node(&self, id: NodeId) -> Option<&SceneNode> {
self.nodes.get(&id)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn nodes(&self) -> impl Iterator<Item = &SceneNode> {
self.nodes.values()
}
pub fn add_layer(&mut self, name: &str) -> LayerId {
let id = LayerId(self.next_layer_id);
let order = self.next_layer_id;
self.next_layer_id += 1;
self.layers.push(Layer {
id,
name: name.to_string(),
visible: true,
locked: false,
colour: [1.0, 1.0, 1.0, 1.0],
order,
});
self.version = self.version.wrapping_add(1);
id
}
pub fn remove_layer(&mut self, id: LayerId) {
if id == DEFAULT_LAYER {
return;
}
for node in self.nodes.values_mut() {
if node.layer == id {
node.layer = DEFAULT_LAYER;
}
}
self.layers.retain(|l| l.id != id);
self.version = self.version.wrapping_add(1);
}
pub fn set_layer_visible(&mut self, id: LayerId, visible: bool) {
if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
layer.visible = visible;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_layer_locked(&mut self, id: LayerId, locked: bool) {
if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
layer.locked = locked;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_layer_colour(&mut self, id: LayerId, colour: [f32; 4]) {
if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
layer.colour = colour;
}
self.version = self.version.wrapping_add(1);
}
pub fn set_layer_order(&mut self, id: LayerId, order: u32) {
if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
layer.order = order;
}
self.version = self.version.wrapping_add(1);
}
pub fn is_layer_locked(&self, id: LayerId) -> bool {
self.layers
.iter()
.find(|l| l.id == id)
.map(|l| l.locked)
.unwrap_or(false)
}
pub fn layers(&self) -> Vec<&Layer> {
let mut sorted: Vec<&Layer> = self.layers.iter().collect();
sorted.sort_by_key(|l| l.order);
sorted
}
pub fn create_group(&mut self, name: &str) -> GroupId {
let id = GroupId(self.next_group_id);
self.next_group_id += 1;
self.groups.push(Group {
id,
name: name.to_string(),
members: HashSet::new(),
});
self.version = self.version.wrapping_add(1);
id
}
pub fn remove_group(&mut self, id: GroupId) {
self.groups.retain(|g| g.id != id);
self.version = self.version.wrapping_add(1);
}
pub fn add_to_group(&mut self, node: NodeId, group: GroupId) {
if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
g.members.insert(node);
}
self.version = self.version.wrapping_add(1);
}
pub fn remove_from_group(&mut self, node: NodeId, group: GroupId) {
if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
g.members.remove(&node);
}
self.version = self.version.wrapping_add(1);
}
pub fn get_group(&self, id: GroupId) -> Option<&Group> {
self.groups.iter().find(|g| g.id == id)
}
pub fn groups(&self) -> &[Group] {
&self.groups
}
pub fn node_groups(&self, node: NodeId) -> Vec<GroupId> {
self.groups
.iter()
.filter(|g| g.members.contains(&node))
.map(|g| g.id)
.collect()
}
pub fn update_transforms(&mut self) {
let roots: Vec<NodeId> = self.roots.clone();
for &root_id in &roots {
self.propagate_transform(root_id, glam::Mat4::IDENTITY);
}
}
fn propagate_transform(&mut self, id: NodeId, parent_world: glam::Mat4) {
let (dirty, local, children) = {
let Some(node) = self.nodes.get(&id) else {
return;
};
(node.dirty, node.local_transform, node.children.clone())
};
if dirty {
let world = parent_world * local;
let node = self.nodes.get_mut(&id).unwrap();
node.world_transform = world;
node.dirty = false;
for &child_id in &children {
self.mark_dirty(child_id);
self.propagate_transform(child_id, world);
}
} else {
let world = self.nodes[&id].world_transform;
for &child_id in &children {
self.propagate_transform(child_id, world);
}
}
}
fn mark_dirty(&mut self, id: NodeId) {
if let Some(node) = self.nodes.get_mut(&id) {
node.dirty = true;
}
}
fn mark_descendants_dirty(&mut self, id: NodeId) {
let children = self
.nodes
.get(&id)
.map(|n| n.children.clone())
.unwrap_or_default();
for child_id in children {
self.mark_dirty(child_id);
self.mark_descendants_dirty(child_id);
}
}
fn mark_spatial_dirty_subtree(&mut self, id: NodeId) {
self.spatial.mark_dirty(id);
let children = self
.nodes
.get(&id)
.map(|n| n.children.clone())
.unwrap_or_default();
for child_id in children {
self.mark_spatial_dirty_subtree(child_id);
}
}
fn build_spatial_index_from(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
let entries: Vec<(NodeId, Aabb)> = self
.nodes
.values()
.filter(|n| n.visible && n.mesh_id.is_some())
.filter_map(|n| {
let local_aabb = mesh_aabb_fn(n.mesh_id.unwrap())?;
let world_aabb = local_aabb.transformed(&n.world_transform);
Some((n.id, world_aabb))
})
.collect();
self.spatial.rebuild(&entries);
self.spatial_built = true;
}
fn flush_spatial_dirty(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
let dirty = self.spatial.drain_dirty();
for id in dirty {
self.spatial.remove_entry(id);
let new_aabb = self.nodes.get(&id).and_then(|node| {
if !node.visible {
return None;
}
let mesh_id = node.mesh_id?;
let local_aabb = mesh_aabb_fn(mesh_id)?;
Some(local_aabb.transformed(&node.world_transform))
});
if let Some(aabb) = new_aabb {
self.spatial.insert_entry(id, aabb);
}
}
}
pub fn rebuild_spatial_index(&mut self, mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>) {
self.update_transforms();
self.build_spatial_index_from(&mesh_aabb_fn);
}
pub fn scene_stats(&self) -> SceneStats {
self.last_scene_stats
}
pub fn collect_render_items(&mut self, selection: &Selection) -> Vec<SceneRenderItem> {
self.update_transforms();
let layer_visible: HashMap<LayerId, bool> =
self.layers.iter().map(|l| (l.id, l.visible)).collect();
let layer_locked: HashMap<LayerId, bool> =
self.layers.iter().map(|l| (l.id, l.locked)).collect();
let mut items = Vec::new();
for node in self.nodes.values() {
if !node.visible {
continue;
}
if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
continue;
}
let Some(mesh_id) = node.mesh_id else {
continue;
};
let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
let mut item_settings = node.appearance;
item_settings.pick_id = PickId(node.id);
item_settings.selected = if locked {
false
} else {
selection.contains(node.id)
};
items.push(SceneRenderItem {
mesh_id,
model: node.world_transform.to_cols_array_2d(),
settings: item_settings,
show_normals: node.show_normals,
material: node.material,
active_attribute: None,
scalar_range: None,
colourmap_id: None,
nan_colour: None,
warp_attribute: None,
warp_scale: 1.0,
deform_instance: node.deform_instance,
receives_decals: node.receives_decals,
lic: None,
lod_group: node.lod_group,
});
}
items
}
pub fn collect_render_items_culled(
&mut self,
selection: &Selection,
frustum: &crate::camera::frustum::Frustum,
mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>,
) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
self.update_transforms();
let layer_visible: HashMap<LayerId, bool> =
self.layers.iter().map(|l| (l.id, l.visible)).collect();
let layer_locked: HashMap<LayerId, bool> =
self.layers.iter().map(|l| (l.id, l.locked)).collect();
let mut items = Vec::new();
let mut stats = crate::camera::frustum::CullStats::default();
if self.nodes.len() >= SPATIAL_THRESHOLD {
if !self.spatial_built {
self.build_spatial_index_from(&mesh_aabb_fn);
} else {
self.flush_spatial_dirty(&mesh_aabb_fn);
}
let t0 = std::time::Instant::now();
let mut candidate_ids = Vec::new();
self.spatial
.collect_visible(frustum, &mut candidate_ids, &mut stats);
let traversal_ms = t0.elapsed().as_secs_f32() * 1000.0;
for id in candidate_ids {
let Some(node) = self.nodes.get(&id) else {
continue;
};
if !node.visible {
continue;
}
if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
continue;
}
let Some(mesh_id) = node.mesh_id else {
continue;
};
let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
let mut item_settings = node.appearance;
item_settings.pick_id = PickId(node.id);
item_settings.selected = if locked {
false
} else {
selection.contains(node.id)
};
items.push(SceneRenderItem {
mesh_id,
model: node.world_transform.to_cols_array_2d(),
settings: item_settings,
show_normals: node.show_normals,
material: node.material,
active_attribute: None,
scalar_range: None,
colourmap_id: None,
nan_colour: None,
warp_attribute: None,
warp_scale: 1.0,
deform_instance: node.deform_instance,
receives_decals: node.receives_decals,
lic: None,
lod_group: node.lod_group,
});
}
self.last_scene_stats = SceneStats {
spatial_index_nodes: self.spatial.node_count(),
frustum_cull_traversal_ms: traversal_ms,
};
} else {
let t0 = std::time::Instant::now();
for node in self.nodes.values() {
if !node.visible {
continue;
}
if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
continue;
}
let Some(mesh_id) = node.mesh_id else {
continue;
};
stats.total += 1;
if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
let world_aabb = local_aabb.transformed(&node.world_transform);
if frustum.cull_aabb(&world_aabb) {
stats.culled += 1;
continue;
}
}
let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
stats.visible += 1;
let mut item_settings = node.appearance;
item_settings.pick_id = PickId(node.id);
item_settings.selected = if locked {
false
} else {
selection.contains(node.id)
};
items.push(SceneRenderItem {
mesh_id,
model: node.world_transform.to_cols_array_2d(),
settings: item_settings,
show_normals: node.show_normals,
material: node.material,
active_attribute: None,
scalar_range: None,
colourmap_id: None,
nan_colour: None,
warp_attribute: None,
warp_scale: 1.0,
deform_instance: node.deform_instance,
receives_decals: node.receives_decals,
lic: None,
lod_group: node.lod_group,
});
}
self.last_scene_stats = SceneStats {
spatial_index_nodes: 0,
frustum_cull_traversal_ms: t0.elapsed().as_secs_f32() * 1000.0,
};
}
(items, stats)
}
pub fn add_light(&mut self, light: LightSource) -> NodeId {
let id = self.next_id;
self.next_id += 1;
let node = SceneNode {
id,
name: String::new(),
mesh_id: None,
material: crate::scene::material::Material::default(),
appearance: crate::scene::material::ItemSettings::default(),
visible: true,
show_normals: false,
local_transform: glam::Mat4::IDENTITY,
world_transform: glam::Mat4::IDENTITY,
parent: None,
children: Vec::new(),
layer: DEFAULT_LAYER,
dirty: true,
deform_instance: None,
receives_decals: false,
lod_group: None,
light: Some(light),
};
self.nodes.insert(id, node);
self.roots.push(id);
self.version = self.version.wrapping_add(1);
id
}
pub fn collect_lights(&mut self) -> Vec<LightSource> {
self.update_transforms();
let layer_visible: HashMap<LayerId, bool> =
self.layers.iter().map(|l| (l.id, l.visible)).collect();
let mut out = Vec::new();
for node in self.nodes.values() {
if !node.visible {
continue;
}
if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
continue;
}
let Some(src) = &node.light else {
continue;
};
out.push(resolve_light_to_world(src, node.world_transform));
}
out
}
pub fn set_light(&mut self, id: NodeId, light: Option<LightSource>) {
if let Some(node) = self.nodes.get_mut(&id) {
node.light = light;
}
self.version = self.version.wrapping_add(1);
}
pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
self.nodes
.values()
.filter(|n| n.mesh_id == Some(mesh_id))
.count()
}
pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
let mut result = Vec::new();
for &root_id in &self.roots {
self.walk_recursive(root_id, 0, &mut result);
}
result
}
fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
out.push((id, depth));
if let Some(node) = self.nodes.get(&id) {
for &child_id in &node.children {
self.walk_recursive(child_id, depth + 1, out);
}
}
}
}
impl Default for Scene {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DecalHandle(u64);
pub struct LiveDecal {
id: u64,
pub item: crate::renderer::DecalItem,
pub animation: Option<crate::renderer::DecalAnimation>,
pub lifetime: Option<f32>,
pub fade_duration: f32,
pub age: f32,
}
impl Scene {
pub fn add_decal(&mut self, item: crate::renderer::DecalItem) -> DecalHandle {
self.add_live_decal(item, None, None, 0.0)
}
pub fn add_decal_with_lifetime(
&mut self,
item: crate::renderer::DecalItem,
lifetime: f32,
fade_duration: f32,
) {
self.add_live_decal(item, Some(lifetime), None, fade_duration);
}
pub fn add_decal_animated(
&mut self,
item: crate::renderer::DecalItem,
animation: crate::renderer::DecalAnimation,
lifetime: Option<f32>,
) -> DecalHandle {
self.add_live_decal(item, lifetime, Some(animation), 0.0)
}
pub fn remove_decal(&mut self, handle: DecalHandle) {
self.live_decals.retain(|d| d.id != handle.0);
}
pub fn update_decals(&mut self, dt: f32) {
for ld in &mut self.live_decals {
ld.age += dt;
}
self.live_decals
.retain(|ld| ld.lifetime.map_or(true, |lt| ld.age < lt));
}
pub fn collect_decal_items(&self) -> Vec<crate::renderer::DecalItem> {
self.live_decals
.iter()
.map(|ld| {
let mut item = ld.item.clone();
if let Some(lt) = ld.lifetime {
let fade = if ld.fade_duration > 0.0 {
ld.fade_duration.min(lt)
} else {
lt * 0.2
};
let time_left = lt - ld.age;
if time_left < fade {
item.alpha *= (time_left / fade).clamp(0.0, 1.0);
}
}
if let Some(anim) = &ld.animation {
match anim {
crate::renderer::DecalAnimation::UvScroll { vx, vy } => {
item.uv_offset[0] =
(ld.item.uv_offset[0] + vx * ld.age).rem_euclid(1.0);
item.uv_offset[1] =
(ld.item.uv_offset[1] + vy * ld.age).rem_euclid(1.0);
}
crate::renderer::DecalAnimation::SpriteSheet { cols, rows, fps } => {
let total = cols * rows;
let frame = ((ld.age * fps) as u32).rem_euclid(total.max(1));
let col = frame % cols;
let row = frame / cols;
item.uv_scale = [1.0 / *cols as f32, 1.0 / *rows as f32];
item.uv_offset = [col as f32 / *cols as f32, row as f32 / *rows as f32];
}
}
}
item
})
.collect()
}
fn add_live_decal(
&mut self,
item: crate::renderer::DecalItem,
lifetime: Option<f32>,
animation: Option<crate::renderer::DecalAnimation>,
fade_duration: f32,
) -> DecalHandle {
let id = self.next_decal_id;
self.next_decal_id += 1;
self.live_decals.push(LiveDecal {
id,
item,
animation,
lifetime,
fade_duration,
age: 0.0,
});
DecalHandle(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lod_group_round_trips_onto_render_items() {
let mut scene = Scene::new();
let id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
assert_eq!(scene.node(id).unwrap().lod_group(), None);
let group = crate::resources::LodGroupId::new(7, 0);
scene.set_lod_group(id, Some(group));
assert_eq!(scene.node(id).unwrap().lod_group(), Some(group));
let items = scene.collect_render_items(&Selection::new());
assert_eq!(items.len(), 1);
assert_eq!(items[0].lod_group, Some(group));
scene.set_lod_group(id, None);
let items = scene.collect_render_items(&Selection::new());
assert_eq!(items[0].lod_group, None);
}
#[test]
fn test_add_and_remove() {
let mut scene = Scene::new();
let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
assert!(scene.node(id).is_some());
assert_eq!(scene.node_count(), 1);
let removed = scene.remove(id);
assert_eq!(removed, vec![id]);
assert!(scene.node(id).is_none());
assert_eq!(scene.node_count(), 0);
}
#[test]
fn test_remove_cascades_to_children() {
let mut scene = Scene::new();
let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.set_parent(child1, Some(parent));
scene.set_parent(child2, Some(parent));
let removed = scene.remove(parent);
assert_eq!(removed.len(), 3);
assert!(removed.contains(&parent));
assert!(removed.contains(&child1));
assert!(removed.contains(&child2));
assert_eq!(scene.node_count(), 0);
}
#[test]
fn test_remove_many_clears_roots_and_descendants() {
let mut scene = Scene::new();
let a = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let b = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let b_child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.set_parent(b_child, Some(b));
let keep = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let removed = scene.remove_many(&[a, b]);
assert_eq!(removed.len(), 3);
assert!(removed.contains(&a));
assert!(removed.contains(&b));
assert!(removed.contains(&b_child));
assert!(scene.node(a).is_none());
assert!(scene.node(b_child).is_none());
assert!(scene.node(keep).is_some());
assert_eq!(scene.node_count(), 1);
}
#[test]
fn test_remove_many_dedups_overlapping_and_missing() {
let mut scene = Scene::new();
let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.set_parent(child, Some(parent));
let removed = scene.remove_many(&[parent, child, parent, 9999_u64]);
assert_eq!(removed.len(), 2);
assert!(removed.contains(&parent));
assert!(removed.contains(&child));
assert_eq!(scene.node_count(), 0);
}
#[test]
fn test_remove_many_detaches_surviving_parent() {
let mut scene = Scene::new();
let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.set_parent(child1, Some(parent));
scene.set_parent(child2, Some(parent));
scene.remove_many(&[child1, child2]);
assert!(scene.node(parent).is_some());
assert!(scene.node(parent).unwrap().children().is_empty());
}
#[test]
fn test_remove_many_empty_is_noop() {
let mut scene = Scene::new();
scene.add(None, glam::Mat4::IDENTITY, Material::default());
let before = scene.version();
let removed = scene.remove_many(&[]);
assert!(removed.is_empty());
assert_eq!(scene.version(), before);
assert_eq!(scene.node_count(), 1);
}
#[test]
fn test_set_parent_updates_world_transform() {
let mut scene = Scene::new();
let parent = scene.add(
None,
glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
Material::default(),
);
let child = scene.add(
None,
glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
Material::default(),
);
scene.set_parent(child, Some(parent));
scene.update_transforms();
let world = scene.node(child).unwrap().world_transform();
let pos = world.col(3).truncate();
assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
}
#[test]
fn test_dirty_propagation() {
let mut scene = Scene::new();
let parent = scene.add(
None,
glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
Material::default(),
);
let child = scene.add(
None,
glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
Material::default(),
);
scene.set_parent(child, Some(parent));
scene.update_transforms();
scene.set_local_transform(
parent,
glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
);
scene.update_transforms();
let child_pos = scene
.node(child)
.unwrap()
.world_transform()
.col(3)
.truncate();
assert!(
(child_pos.x - 12.0).abs() < 1e-5,
"expected x=12.0, got {}",
child_pos.x
);
}
#[test]
fn test_layer_visibility_hides_nodes() {
let mut scene = Scene::new();
let layer = scene.add_layer("Hidden");
let id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
scene.set_layer(id, layer);
scene.set_layer_visible(layer, false);
let items = scene.collect_render_items(&Selection::new());
assert!(items.is_empty());
}
#[test]
fn test_collect_skips_invisible_nodes() {
let mut scene = Scene::new();
let id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
scene.set_visible(id, false);
let items = scene.collect_render_items(&Selection::new());
assert!(items.is_empty());
}
#[test]
fn test_collect_skips_meshless_nodes() {
let mut scene = Scene::new();
scene.add(None, glam::Mat4::IDENTITY, Material::default());
let items = scene.collect_render_items(&Selection::new());
assert!(items.is_empty());
}
#[test]
fn test_collect_marks_selected() {
let mut scene = Scene::new();
let id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
let mut sel = Selection::new();
sel.select_one(id);
let items = scene.collect_render_items(&sel);
assert_eq!(items.len(), 1);
assert!(items[0].settings.selected);
}
#[test]
fn test_unparent_makes_root() {
let mut scene = Scene::new();
let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.set_parent(child, Some(parent));
assert!(!scene.roots().contains(&child));
scene.set_parent(child, None);
assert!(scene.roots().contains(&child));
assert!(scene.node(child).unwrap().parent().is_none());
}
#[test]
fn test_collect_culled_filters_offscreen() {
let mut scene = Scene::new();
let visible_id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
let _behind = scene.add(
Some(MeshId::new(1, 0)),
glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
Material::default(),
);
let sel = Selection::new();
let view = glam::Mat4::look_at_rh(
glam::Vec3::new(0.0, 0.0, 5.0),
glam::Vec3::ZERO,
glam::Vec3::Y,
);
let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
let unit_aabb = crate::scene::aabb::Aabb {
min: glam::Vec3::splat(-0.5),
max: glam::Vec3::splat(0.5),
};
let (items, stats) =
scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
assert_eq!(stats.total, 2);
assert_eq!(stats.visible, 1);
assert_eq!(stats.culled, 1);
assert_eq!(items.len(), 1);
assert_eq!(items[0].mesh_id.index(), visible_id as usize - 1); let _ = visible_id; }
#[test]
fn test_layer_locked_field_default_false() {
let scene = Scene::new();
let layers = scene.layers();
let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
assert!(!default_layer.locked);
}
#[test]
fn test_add_layer_has_locked_false_colour_white_and_order() {
let mut scene = Scene::new();
let layer_id = scene.add_layer("Test");
let layers = scene.layers();
let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
assert!(!layer.locked);
assert_eq!(layer.colour, [1.0, 1.0, 1.0, 1.0]);
assert!(layer.order > 0); }
#[test]
fn test_set_layer_locked() {
let mut scene = Scene::new();
let layer_id = scene.add_layer("Locked");
scene.set_layer_locked(layer_id, true);
assert!(scene.is_layer_locked(layer_id));
scene.set_layer_locked(layer_id, false);
assert!(!scene.is_layer_locked(layer_id));
}
#[test]
fn test_set_layer_colour() {
let mut scene = Scene::new();
let layer_id = scene.add_layer("Coloured");
scene.set_layer_colour(layer_id, [1.0, 0.0, 0.0, 1.0]);
let layers = scene.layers();
let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
assert_eq!(layer.colour, [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn test_set_layer_order() {
let mut scene = Scene::new();
let layer_id = scene.add_layer("Orderly");
scene.set_layer_order(layer_id, 99);
let layers = scene.layers();
let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
assert_eq!(layer.order, 99);
}
#[test]
fn test_locked_layer_suppresses_selection_in_render_items() {
let mut scene = Scene::new();
let layer_id = scene.add_layer("Locked");
let node_id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
scene.set_layer(node_id, layer_id);
scene.set_layer_locked(layer_id, true);
let mut sel = Selection::new();
sel.select_one(node_id);
let items = scene.collect_render_items(&sel);
assert_eq!(items.len(), 1, "locked layer nodes still render");
assert!(
!items[0].settings.selected,
"locked layer nodes must not appear selected"
);
}
#[test]
fn test_layers_sorted_by_order() {
let mut scene = Scene::new();
let a = scene.add_layer("A");
let b = scene.add_layer("B");
scene.set_layer_order(a, 10);
scene.set_layer_order(b, 5);
let layers = scene.layers();
let pos_b = layers.iter().position(|l| l.id == b).unwrap();
let pos_a = layers.iter().position(|l| l.id == a).unwrap();
assert!(
pos_b < pos_a,
"layer B (order=5) should appear before A (order=10)"
);
}
#[test]
fn test_create_group_returns_id() {
let mut scene = Scene::new();
let gid = scene.create_group("MyGroup");
let group = scene.get_group(gid).unwrap();
assert_eq!(group.name, "MyGroup");
assert!(group.members.is_empty());
}
#[test]
fn test_add_to_group_and_remove_from_group() {
let mut scene = Scene::new();
let gid = scene.create_group("G");
let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.add_to_group(node_id, gid);
assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
scene.remove_from_group(node_id, gid);
assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
}
#[test]
fn test_groups_returns_all_groups() {
let mut scene = Scene::new();
scene.create_group("G1");
scene.create_group("G2");
assert_eq!(scene.groups().len(), 2);
}
#[test]
fn test_node_groups_returns_containing_groups() {
let mut scene = Scene::new();
let g1 = scene.create_group("G1");
let g2 = scene.create_group("G2");
let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.add_to_group(node_id, g1);
scene.add_to_group(node_id, g2);
let groups = scene.node_groups(node_id);
assert_eq!(groups.len(), 2);
assert!(groups.contains(&g1));
assert!(groups.contains(&g2));
}
#[test]
fn test_remove_node_cleans_up_group_membership() {
let mut scene = Scene::new();
let gid = scene.create_group("G");
let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
scene.add_to_group(node_id, gid);
scene.remove(node_id);
assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
}
#[test]
fn test_mesh_ref_count_zero_for_unused_mesh() {
let scene = Scene::new();
assert_eq!(scene.mesh_ref_count(MeshId::new(42, 0)), 0);
}
#[test]
fn test_mesh_ref_count_correct_for_nodes() {
let mut scene = Scene::new();
scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
scene.add(
Some(MeshId::new(1, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
assert_eq!(scene.mesh_ref_count(MeshId::new(0, 0)), 2);
assert_eq!(scene.mesh_ref_count(MeshId::new(1, 0)), 1);
}
#[test]
fn test_mesh_ref_count_decreases_after_remove() {
let mut scene = Scene::new();
let node_a = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
assert_eq!(scene.mesh_ref_count(MeshId::new(0, 0)), 2);
scene.remove(node_a);
assert_eq!(scene.mesh_ref_count(MeshId::new(0, 0)), 1);
}
#[test]
fn test_remove_group() {
let mut scene = Scene::new();
let gid = scene.create_group("G");
scene.remove_group(gid);
assert!(scene.get_group(gid).is_none());
assert!(scene.groups().is_empty());
}
#[test]
fn test_walk_depth_first_order() {
let mut scene = Scene::new();
let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
scene.set_parent(child_a, Some(root));
scene.set_parent(child_b, Some(root));
scene.set_parent(grandchild, Some(child_a));
let walk = scene.walk_depth_first();
assert_eq!(walk.len(), 4);
assert_eq!(walk[0], (root, 0));
assert_eq!(walk[1], (child_a, 1));
assert_eq!(walk[2], (grandchild, 2));
assert_eq!(walk[3], (child_b, 1));
}
fn make_test_frustum() -> crate::camera::frustum::Frustum {
let view = glam::Mat4::look_at_rh(
glam::Vec3::new(0.0, 0.0, 300.0),
glam::Vec3::ZERO,
glam::Vec3::Y,
);
let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 1.0, 400.0);
crate::camera::frustum::Frustum::from_view_proj(&(proj * view))
}
fn unit_aabb() -> crate::scene::aabb::Aabb {
crate::scene::aabb::Aabb {
min: glam::Vec3::splat(-0.5),
max: glam::Vec3::splat(0.5),
}
}
#[test]
fn test_octree_parity_with_known_counts() {
let mut scene = Scene::new();
let frustum = make_test_frustum();
for _ in 0..100 {
scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
}
for _ in 0..500 {
let mat = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
scene.add(Some(MeshId::new(0, 0)), mat, Material::default());
}
assert!(scene.node_count() >= SPATIAL_THRESHOLD);
let sel = Selection::new();
let (items, stats) =
scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
assert_eq!(
items.len(),
100,
"expected 100 visible items, got {}",
items.len()
);
assert_eq!(stats.visible, 100);
let scene_stats = scene.scene_stats();
assert!(
scene_stats.spatial_index_nodes > 0,
"spatial index should be active"
);
}
#[test]
fn test_octree_incremental_transform_update() {
let mut scene = Scene::new();
let frustum = make_test_frustum();
let mut ids = Vec::new();
for _ in 0..600 {
let id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
ids.push(id);
}
let sel = Selection::new();
let (items, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
assert_eq!(items.len(), 600, "all 600 should be visible initially");
let behind = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
for &id in &ids[..300] {
scene.set_local_transform(id, behind);
}
let (items2, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
assert_eq!(
items2.len(),
300,
"300 nodes should be visible after moving the others out"
);
}
#[test]
fn test_octree_node_removal() {
let mut scene = Scene::new();
let frustum = make_test_frustum();
let mut ids = Vec::new();
for _ in 0..600 {
let id = scene.add(
Some(MeshId::new(0, 0)),
glam::Mat4::IDENTITY,
Material::default(),
);
ids.push(id);
}
let sel = Selection::new();
let (items, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
assert_eq!(items.len(), 600);
for &id in &ids[..150] {
scene.remove(id);
}
let (items2, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
assert_eq!(items2.len(), 450, "should have 450 after removing 150");
}
}