use crate::engine::ecs::ComponentId;
use crate::engine::ecs::component::BackgroundColorComponent;
use crate::engine::ecs::component::OverlayComponent;
use crate::engine::ecs::component::{
BackgroundComponent, BoundsComponent, ColorComponent, EmissiveComponent,
LightQuantizationComponent, MeshComponent, OpacityComponent, RenderableComponent,
RendererSettingsComponent, TransparentCutoutComponent, UVComponent,
};
use crate::engine::ecs::World;
use crate::engine::ecs::system::System;
use crate::engine::ecs::system::TransformSystem;
use crate::engine::graphics::bounds::Aabb;
use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Transform};
use crate::engine::graphics::{GpuRenderable, VisualWorld};
use crate::engine::graphics::{MeshUploader, RenderAssets};
use crate::engine::user_input::InputState;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Default)]
pub struct RenderableSystem {
renderables: Vec<ComponentId>,
pending: HashMap<ComponentId, PendingRenderable>,
pending_uv: HashMap<ComponentId, Vec<[f32; 2]>>,
uv_mesh_cache: HashMap<UvMeshCacheKey, CpuMeshHandle>,
pending_color: HashMap<ComponentId, [f32; 4]>,
pending_opacity: HashMap<ComponentId, PendingOpacity>,
pending_cutout: HashMap<ComponentId, bool>,
pending_emissive: HashMap<ComponentId, f32>,
pending_quant_steps: HashMap<ComponentId, f32>,
pending_normal_vis: Vec<(ComponentId, ComponentId, CpuMeshHandle, f32)>,
}
#[derive(Debug, Clone, Copy)]
struct PendingOpacity {
opacity: f32,
multiple_layers: bool,
}
impl Default for PendingOpacity {
fn default() -> Self {
Self {
opacity: 1.0,
multiple_layers: false,
}
}
}
#[derive(Debug, Clone, Copy)]
struct EffectiveRenderableStyle {
color: [f32; 4],
opacity: PendingOpacity,
transparent_cutout: bool,
background: bool,
background_occluded_lit: bool,
overlay: bool,
}
impl Default for EffectiveRenderableStyle {
fn default() -> Self {
Self {
color: [1.0, 1.0, 1.0, 1.0],
opacity: PendingOpacity::default(),
transparent_cutout: false,
background: false,
background_occluded_lit: false,
overlay: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UvMeshCacheKey {
base_mesh: CpuMeshHandle,
uv_bits: [u32; 8],
}
#[derive(Debug, Clone)]
struct PendingRenderable {
cpu_mesh: CpuMeshHandle,
material: MaterialHandle,
renderable_cid: ComponentId,
effective_style: EffectiveRenderableStyle,
mesh_key: Option<String>,
}
fn clone_mesh_with_uv_overrides(
render_assets: &mut RenderAssets,
base_mesh: CpuMeshHandle,
uvs: &[[f32; 2]],
) -> Option<CpuMeshHandle> {
let mut mesh = render_assets.cpu_mesh(base_mesh)?.clone();
for (i, v) in mesh.vertices.iter_mut().enumerate() {
v.uv = uvs.get(i).copied().unwrap_or([0.0, 0.0]);
}
Some(render_assets.register_mesh(mesh))
}
impl RenderableSystem {
fn uv_clone_audit_enabled() -> bool {
std::env::var("CAT_DEBUG_RENDERABLE_UV_CLONES")
.ok()
.map(|s| {
let s = s.trim().to_ascii_lowercase();
s == "1" || s == "true" || s == "on" || s == "yes"
})
.unwrap_or(false)
}
fn material_with_emissive(material: MaterialHandle, emissive_intensity: f32) -> MaterialHandle {
let is_emissive = emissive_intensity > 0.0;
match (material, is_emissive) {
(MaterialHandle::TOON_MESH, true) => MaterialHandle::EMISSIVE_TOON_MESH,
(MaterialHandle::SKINNED_TOON_MESH, true) => MaterialHandle::SKINNED_EMISSIVE_TOON_MESH,
(MaterialHandle::EMISSIVE_TOON_MESH, false) => MaterialHandle::TOON_MESH,
(MaterialHandle::SKINNED_EMISSIVE_TOON_MESH, false) => {
MaterialHandle::SKINNED_TOON_MESH
}
_ => material,
}
}
fn immediate_color_child(world: &World, node: ComponentId) -> Option<[f32; 4]> {
world.children_of(node).iter().find_map(|&ch| {
world
.get_component_by_id_as::<ColorComponent>(ch)
.map(|c| c.rgba)
})
}
fn immediate_opacity_child(world: &World, node: ComponentId) -> Option<PendingOpacity> {
world.children_of(node).iter().find_map(|&ch| {
world
.get_component_by_id_as::<OpacityComponent>(ch)
.map(|o| PendingOpacity {
opacity: o.opacity,
multiple_layers: o.multiple_layers,
})
})
}
fn immediate_cutout_child(world: &World, node: ComponentId) -> Option<bool> {
world.children_of(node).iter().find_map(|&ch| {
world
.get_component_by_id_as::<TransparentCutoutComponent>(ch)
.map(|c| c.enabled)
})
}
fn immediate_emissive_child(world: &World, node: ComponentId) -> Option<f32> {
world.children_of(node).iter().find_map(|&ch| {
world
.get_component_by_id_as::<EmissiveComponent>(ch)
.map(|e| e.intensity.max(0.0))
})
}
fn resolve_effective_renderable_style(
world: &World,
renderable_cid: ComponentId,
) -> EffectiveRenderableStyle {
let mut style = EffectiveRenderableStyle::default();
let mut color_resolved = false;
let mut opacity_resolved = false;
let mut cutout_resolved = false;
if let Some(rgba) = Self::immediate_color_child(world, renderable_cid) {
style.color = rgba;
color_resolved = true;
}
if let Some(opacity) = Self::immediate_opacity_child(world, renderable_cid) {
style.opacity = opacity;
opacity_resolved = true;
}
if let Some(enabled) = Self::immediate_cutout_child(world, renderable_cid) {
style.transparent_cutout = enabled;
cutout_resolved = true;
}
let mut cur = renderable_cid;
while let Some(parent) = world.parent_of(cur) {
if !color_resolved {
if let Some(rgba) = Self::immediate_color_child(world, parent) {
style.color = rgba;
color_resolved = true;
}
}
if !opacity_resolved {
if let Some(opacity) = Self::immediate_opacity_child(world, parent) {
style.opacity = opacity;
opacity_resolved = true;
}
}
if !cutout_resolved {
if let Some(enabled) = Self::immediate_cutout_child(world, parent) {
style.transparent_cutout = enabled;
cutout_resolved = true;
}
}
if !style.background {
if let Some(bg) = world.get_component_by_id_as::<BackgroundComponent>(parent) {
style.background = true;
style.background_occluded_lit = bg.occlusion_and_lighting;
}
}
if !style.overlay
&& world
.get_component_by_id_as::<OverlayComponent>(parent)
.is_some()
{
style.overlay = true;
}
cur = parent;
}
style
}
fn clone_mesh_with_uv_overrides_cached(
&mut self,
render_assets: &mut RenderAssets,
base_mesh: CpuMeshHandle,
uvs: &[[f32; 2]],
) -> Option<CpuMeshHandle> {
let vertex_count = render_assets.cpu_mesh(base_mesh)?.vertices.len();
if vertex_count == 4 && uvs.len() >= 4 {
let mut uv_bits = [0u32; 8];
for i in 0..4 {
uv_bits[i * 2] = uvs[i][0].to_bits();
uv_bits[i * 2 + 1] = uvs[i][1].to_bits();
}
let key = UvMeshCacheKey { base_mesh, uv_bits };
if let Some(&cached) = self.uv_mesh_cache.get(&key) {
return Some(cached);
}
let new_mesh = clone_mesh_with_uv_overrides(render_assets, base_mesh, uvs)?;
self.uv_mesh_cache.insert(key, new_mesh);
return Some(new_mesh);
}
clone_mesh_with_uv_overrides(render_assets, base_mesh, uvs)
}
}
impl RenderableSystem {
fn apply_pending_emissive_updates_to_registered_renderables(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
) {
let keys: Vec<ComponentId> = self.pending_emissive.keys().copied().collect();
for renderable_cid in keys {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_emissive.remove(&renderable_cid);
continue;
};
let Some(handle) = renderable_comp.get_handle() else {
continue;
};
let Some(emissive) = self.pending_emissive.get(&renderable_cid).copied() else {
continue;
};
let _ = visuals.update_emissive(handle, emissive);
if let Some(inst) = visuals.instance(handle) {
let material = Self::material_with_emissive(inst.renderable.material, emissive);
let _ = visuals.update_material(handle, material);
}
let _ = self.pending_emissive.remove(&renderable_cid);
}
}
fn apply_pending_quant_updates_to_registered_renderables(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
) {
let keys: Vec<ComponentId> = self.pending_quant_steps.keys().copied().collect();
for renderable_cid in keys {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_quant_steps.remove(&renderable_cid);
continue;
};
let Some(handle) = renderable_comp.get_handle() else {
continue;
};
let Some(quant_steps) = self.pending_quant_steps.get(&renderable_cid).copied() else {
continue;
};
let _ = visuals.update_quant_steps(handle, quant_steps);
let _ = self.pending_quant_steps.remove(&renderable_cid);
}
}
fn apply_pending_color_updates_to_registered_renderables(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
) {
let color_keys: Vec<ComponentId> = self.pending_color.keys().copied().collect();
for renderable_cid in color_keys {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_color.remove(&renderable_cid);
continue;
};
let Some(handle) = renderable_comp.get_handle() else {
continue;
};
let Some(color) = self.pending_color.get(&renderable_cid).copied() else {
continue;
};
let _ = visuals.update_color(handle, color);
let _ = self.pending_color.remove(&renderable_cid);
}
}
fn apply_pending_opacity_updates_to_registered_renderables(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
) {
let keys: Vec<ComponentId> = self.pending_opacity.keys().copied().collect();
for renderable_cid in keys {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_opacity.remove(&renderable_cid);
continue;
};
let Some(handle) = renderable_comp.get_handle() else {
continue;
};
let Some(pending) = self.pending_opacity.get(&renderable_cid).copied() else {
continue;
};
let _ = visuals.update_opacity_state(handle, pending.opacity, pending.multiple_layers);
let _ = self.pending_opacity.remove(&renderable_cid);
}
}
fn apply_pending_cutout_updates_to_registered_renderables(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
) {
let keys: Vec<ComponentId> = self.pending_cutout.keys().copied().collect();
for renderable_cid in keys {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_cutout.remove(&renderable_cid);
continue;
};
let Some(handle) = renderable_comp.get_handle() else {
continue;
};
let Some(enabled) = self.pending_cutout.get(&renderable_cid).copied() else {
continue;
};
let _ = visuals.update_transparent_cutout(handle, enabled);
let _ = self.pending_cutout.remove(&renderable_cid);
}
}
fn apply_pending_uv_updates_to_registered_renderables(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
render_assets: &mut RenderAssets,
uploader: &mut dyn MeshUploader,
) {
let uv_keys: Vec<ComponentId> = self.pending_uv.keys().copied().collect();
for renderable_cid in uv_keys {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_uv.remove(&renderable_cid);
continue;
};
let Some(handle) = renderable_comp.get_handle() else {
continue;
};
let base_mesh = renderable_comp.renderable.mesh;
let material = renderable_comp.renderable.material;
let Some(uvs) = self.pending_uv.get(&renderable_cid).cloned() else {
continue;
};
let Some(new_mesh) =
self.clone_mesh_with_uv_overrides_cached(render_assets, base_mesh, &uvs)
else {
continue;
};
let mesh = match render_assets.gpu_mesh_handle(uploader, new_mesh) {
Ok(h) => h,
Err(_err) => continue,
};
let Some(model) = TransformSystem::world_model(world, renderable_cid) else {
continue;
};
let transform = Transform {
model,
matrix_world: model,
..Default::default()
};
let gpu_r = GpuRenderable { mesh, material };
let _ = visuals.update(handle, gpu_r, transform);
if let Some(renderable_comp) =
world.get_component_by_id_as_mut::<RenderableComponent>(renderable_cid)
{
renderable_comp.renderable.mesh = new_mesh;
}
let _ = self.pending_uv.remove(&renderable_cid);
}
}
pub fn register_color(
&mut self,
world: &mut World,
_visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(color_comp) = world.get_component_by_id_as::<ColorComponent>(component) else {
return;
};
let mut cur = component;
let mut renderable_cid: Option<ComponentId> = None;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
renderable_cid = Some(parent);
break;
}
cur = parent;
}
if let Some(renderable_cid) = renderable_cid {
self.pending_color.insert(renderable_cid, color_comp.rgba);
return;
}
let mut q = VecDeque::new();
let start = world.parent_of(component).unwrap_or(component);
q.push_back(start);
while let Some(node) = q.pop_front() {
for &ch in world.children_of(node).iter() {
q.push_back(ch);
}
if world
.get_component_by_id_as::<RenderableComponent>(node)
.is_none()
{
continue;
}
if Self::immediate_color_child(world, node).is_some() {
continue;
}
self.pending_color.insert(node, color_comp.rgba);
}
}
pub fn register_opacity(
&mut self,
world: &mut World,
_visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(opacity_comp) = world.get_component_by_id_as::<OpacityComponent>(component) else {
return;
};
let pending = PendingOpacity {
opacity: opacity_comp.opacity,
multiple_layers: opacity_comp.multiple_layers,
};
let mut cur = component;
let mut renderable_cid: Option<ComponentId> = None;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
renderable_cid = Some(parent);
break;
}
cur = parent;
}
if let Some(renderable_cid) = renderable_cid {
self.pending_opacity.insert(renderable_cid, pending);
return;
}
let mut q = VecDeque::new();
q.push_back(component);
while let Some(node) = q.pop_front() {
for &ch in world.children_of(node).iter() {
q.push_back(ch);
}
if world
.get_component_by_id_as::<RenderableComponent>(node)
.is_none()
{
continue;
}
if Self::immediate_opacity_child(world, node).is_some() {
continue;
}
self.pending_opacity.insert(node, pending);
}
}
pub fn register_transparent_cutout(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(cutout_comp) =
world.get_component_by_id_as::<TransparentCutoutComponent>(component)
else {
return;
};
let pending = cutout_comp.enabled;
let mut cur = component;
let mut renderable_cid: Option<ComponentId> = None;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
renderable_cid = Some(parent);
break;
}
cur = parent;
}
if let Some(renderable_cid) = renderable_cid {
self.pending_cutout.insert(renderable_cid, pending);
if let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
{
if let Some(handle) = renderable_comp.get_handle() {
let _ = visuals.update_transparent_cutout(handle, pending);
let _ = self.pending_cutout.remove(&renderable_cid);
}
}
return;
}
let mut q = VecDeque::new();
q.push_back(component);
while let Some(node) = q.pop_front() {
for &ch in world.children_of(node).iter() {
q.push_back(ch);
}
if world
.get_component_by_id_as::<RenderableComponent>(node)
.is_none()
{
continue;
}
if Self::immediate_cutout_child(world, node).is_some() {
continue;
}
self.pending_cutout.insert(node, pending);
if let Some(renderable_comp) = world.get_component_by_id_as::<RenderableComponent>(node)
{
if let Some(handle) = renderable_comp.get_handle() {
let _ = visuals.update_transparent_cutout(handle, pending);
let _ = self.pending_cutout.remove(&node);
}
}
}
}
pub fn register_light_quantization(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(q_comp) = world.get_component_by_id_as::<LightQuantizationComponent>(component)
else {
return;
};
let mut cur = component;
let mut renderable_cid: Option<ComponentId> = None;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
renderable_cid = Some(parent);
break;
}
cur = parent;
}
let Some(renderable_cid) = renderable_cid else {
return;
};
self.pending_quant_steps
.insert(renderable_cid, q_comp.quant_steps);
if let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
{
if let Some(handle) = renderable_comp.get_handle() {
let _ = visuals.update_quant_steps(handle, q_comp.quant_steps);
let _ = self.pending_quant_steps.remove(&renderable_cid);
}
}
}
pub fn register_emissive(
&mut self,
world: &mut World,
_visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(emissive_comp) = world.get_component_by_id_as::<EmissiveComponent>(component)
else {
return;
};
let emissive = emissive_comp.intensity.max(0.0);
if let Some(parent) = world.parent_of(component) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
self.pending_emissive.insert(parent, emissive);
return;
}
}
let start = world.parent_of(component).unwrap_or(component);
let mut q = VecDeque::new();
q.push_back(start);
while let Some(node) = q.pop_front() {
for &ch in world.children_of(node).iter() {
q.push_back(ch);
}
if world
.get_component_by_id_as::<RenderableComponent>(node)
.is_none()
{
continue;
}
if Self::immediate_emissive_child(world, node).is_some() {
continue;
}
self.pending_emissive.insert(node, emissive);
}
}
pub fn register_uv(
&mut self,
world: &mut World,
_visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(uv_comp) = world.get_component_by_id_as::<UVComponent>(component) else {
return;
};
let mut cur = component;
let mut renderable_cid: Option<ComponentId> = None;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
renderable_cid = Some(parent);
break;
}
cur = parent;
}
let Some(renderable_cid) = renderable_cid else {
return;
};
self.pending_uv.insert(renderable_cid, uv_comp.uvs.clone());
}
pub fn register_background_color(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
if world
.get_component_by_id_as::<BackgroundColorComponent>(component)
.is_none()
{
return;
}
const DEFAULT: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
let rgba = world
.children_of(component)
.iter()
.find_map(|&ch| {
world
.get_component_by_id_as::<ColorComponent>(ch)
.map(|c| c.rgba)
})
.unwrap_or(DEFAULT);
visuals.set_clear_color(rgba);
}
pub fn register_renderer_settings(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(settings) = world.get_component_by_id_as::<RendererSettingsComponent>(component)
else {
return;
};
visuals.set_renderer_msaa_mode(settings.msaa_mode());
visuals.set_preferred_window_size(settings.window_size);
}
pub fn register_normal_vis(&mut self, world: &World, component: ComponentId) {
use crate::engine::ecs::component::{NormalVisualisationComponent, RenderableComponent};
let Some(nv) = world.get_component_by_id_as::<NormalVisualisationComponent>(component)
else {
return;
};
let thickness = nv.thickness;
let mut cur = component;
let mut parent_renderable: Option<(ComponentId, CpuMeshHandle)> = None;
while let Some(p) = world.parent_of(cur) {
if let Some(r) = world.get_component_by_id_as::<RenderableComponent>(p) {
parent_renderable = Some((p, r.renderable.base_mesh));
break;
}
cur = p;
}
let Some((renderable_id, base_mesh)) = parent_renderable else {
return;
};
self.pending_normal_vis
.push((component, renderable_id, base_mesh, thickness));
}
pub fn register_renderable(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
if !self.renderables.iter().any(|c| *c == component) {
self.renderables.push(component);
}
self.register_renderable_from_world(world, visuals, component);
}
pub fn remove_renderable(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
self.renderables.retain(|&c| c != component);
let _ = self.pending.remove(&component);
let _ = self.pending_uv.remove(&component);
let _ = self.pending_color.remove(&component);
let _ = self.pending_opacity.remove(&component);
let _ = self.pending_emissive.remove(&component);
let _ = self.pending_quant_steps.remove(&component);
if let Some(r) = world.get_component_by_id_as_mut::<RenderableComponent>(component) {
if let Some(handle) = r.handle.take() {
let _ = visuals.remove(handle);
}
}
}
pub fn register_renderable_from_world(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
component: ComponentId,
) {
{
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(component)
else {
return;
};
if renderable_comp.get_handle().is_some() {
return;
}
}
let Some(renderable_comp) = world.get_component_by_id_as::<RenderableComponent>(component)
else {
return;
};
let mesh_key = world
.children_of(component)
.iter()
.copied()
.find_map(|cid| {
world
.get_component_by_id_as::<MeshComponent>(cid)
.map(|m| m.key.clone())
});
self.pending.insert(
component,
PendingRenderable {
cpu_mesh: renderable_comp.renderable.mesh,
material: renderable_comp.renderable.material,
renderable_cid: component,
effective_style: Self::resolve_effective_renderable_style(world, component),
mesh_key,
},
);
let _ = visuals;
}
pub fn flush_pending(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
render_assets: &mut RenderAssets,
uploader: &mut dyn MeshUploader,
queue: &mut crate::engine::ecs::CommandQueue,
) -> bool {
let parse_bool_env = |name: &str| {
std::env::var(name)
.ok()
.map(|s| {
let s = s.trim().to_ascii_lowercase();
s == "1" || s == "true" || s == "on" || s == "yes"
})
.unwrap_or(false)
};
let debug_mesh_stats = parse_bool_env("CAT_DEBUG_RENDERABLE_MESH_STATS");
let debug_mesh_stats_all = parse_bool_env("CAT_DEBUG_RENDERABLE_MESH_STATS_ALL");
static MESH_STATS_LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
let mut inserted_any = false;
let keys: Vec<ComponentId> = self.pending.keys().copied().collect();
for key in keys {
let Some(p) = self.pending.get(&key).cloned() else {
continue;
};
let effective_style = Self::resolve_effective_renderable_style(world, p.renderable_cid);
if let Some(pending) = self.pending.get_mut(&key) {
pending.effective_style = effective_style;
}
let mut cpu_mesh = p.cpu_mesh;
if let Some(mesh_key) = p.mesh_key.as_deref() {
let Some(imported) = render_assets.imported_mesh(mesh_key) else {
continue;
};
cpu_mesh = imported;
if let Some(pending) = self.pending.get_mut(&key) {
pending.cpu_mesh = cpu_mesh;
}
if let Some(renderable_comp) =
world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
{
renderable_comp.renderable.mesh = cpu_mesh;
renderable_comp.renderable.base_mesh = cpu_mesh;
}
}
if let Some(uvs) = self.pending_uv.get(&p.renderable_cid).cloned() {
if let Some(new_mesh) =
self.clone_mesh_with_uv_overrides_cached(render_assets, cpu_mesh, &uvs)
{
let uv_base_mesh = cpu_mesh;
if Self::uv_clone_audit_enabled() {
let (verts, indices) = render_assets
.cpu_mesh(new_mesh)
.map(|m| (m.vertices.len(), m.indices_u32.len()))
.unwrap_or((0, 0));
println!(
"[RenderableSystem][audit] uv_clone renderable={:?} base_mesh={:?} new_mesh={:?} verts={} indices={} repeated_work=true",
p.renderable_cid, uv_base_mesh, new_mesh, verts, indices
);
}
cpu_mesh = new_mesh;
if let Some(pending) = self.pending.get_mut(&key) {
pending.cpu_mesh = cpu_mesh;
}
if let Some(renderable_comp) =
world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
{
renderable_comp.renderable.mesh = cpu_mesh;
renderable_comp.renderable.base_mesh = uv_base_mesh;
}
}
}
cache_resolved_mesh_bounds(world, render_assets, p.renderable_cid, cpu_mesh);
let mesh = match render_assets.gpu_mesh_handle(uploader, cpu_mesh) {
Ok(h) => h,
Err(_err) => continue,
};
if debug_mesh_stats {
let (vcount, icount, has_skin) = render_assets
.cpu_mesh(cpu_mesh)
.map(|m| {
(
m.vertices.len(),
m.indices_u32.len(),
m.joints0.is_some() && m.weights0.is_some(),
)
})
.unwrap_or((0, 0, false));
let key_str = p.mesh_key.as_deref().unwrap_or("<no mesh_key>");
let should_log = debug_mesh_stats_all || key_str != "<no mesh_key>" || has_skin;
if should_log {
let limit = std::env::var("CAT_DEBUG_RENDERABLE_MESH_STATS_LIMIT")
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.unwrap_or(50);
let n = MESH_STATS_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
if n < limit {
println!(
"[RenderableSystem] renderable={:?} material={:?} mesh_key='{}' cpu_mesh={:?} gpu_mesh={:?} verts={} indices={} skinned_attrs={}",
p.renderable_cid,
p.material,
key_str,
cpu_mesh,
mesh,
vcount,
icount,
has_skin
);
}
}
}
let gpu_r = GpuRenderable {
mesh,
material: p.material,
};
let model = match TransformSystem::world_model(world, p.renderable_cid) {
Some(m) => m,
None => {
self.pending.remove(&key);
continue;
}
};
let transform = Transform {
model,
matrix_world: model,
..Default::default()
};
let color = self
.pending_color
.get(&p.renderable_cid)
.copied()
.unwrap_or(effective_style.color);
let opacity = self
.pending_opacity
.get(&p.renderable_cid)
.copied()
.unwrap_or(effective_style.opacity);
let transparent_cutout = self
.pending_cutout
.get(&p.renderable_cid)
.copied()
.unwrap_or(effective_style.transparent_cutout);
let emissive = self
.pending_emissive
.get(&p.renderable_cid)
.copied()
.unwrap_or(0.0);
let gpu_r = GpuRenderable::new(
gpu_r.mesh,
Self::material_with_emissive(gpu_r.material, emissive),
);
let quant_steps = self
.pending_quant_steps
.get(&p.renderable_cid)
.copied()
.unwrap_or_else(|| match p.material {
MaterialHandle::TOON_MESH => 3.0,
MaterialHandle::UNLIT_MESH => 1.0,
_ => 3.0,
});
let handle = visuals.register(
p.renderable_cid,
gpu_r,
transform,
color,
opacity.opacity,
opacity.multiple_layers,
transparent_cutout,
effective_style.background,
effective_style.background_occluded_lit,
effective_style.overlay,
emissive,
None,
quant_steps,
);
if let Some(renderable_comp) =
world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
{
renderable_comp.handle = Some(handle);
}
let _ = self.pending_uv.remove(&p.renderable_cid);
let _ = self.pending_color.remove(&p.renderable_cid);
let _ = self.pending_opacity.remove(&p.renderable_cid);
let _ = self.pending_cutout.remove(&p.renderable_cid);
let _ = self.pending_emissive.remove(&p.renderable_cid);
let _ = self.pending_quant_steps.remove(&p.renderable_cid);
self.pending.remove(&key);
inserted_any = true;
}
self.apply_pending_uv_updates_to_registered_renderables(
world,
visuals,
render_assets,
uploader,
);
self.apply_pending_color_updates_to_registered_renderables(world, visuals);
self.apply_pending_opacity_updates_to_registered_renderables(world, visuals);
self.apply_pending_cutout_updates_to_registered_renderables(world, visuals);
self.apply_pending_emissive_updates_to_registered_renderables(world, visuals);
self.apply_pending_quant_updates_to_registered_renderables(world, visuals);
self.spawn_pending_normal_vis(world, render_assets, queue);
inserted_any
}
fn spawn_pending_normal_vis(
&mut self,
world: &mut World,
render_assets: &RenderAssets,
queue: &mut crate::engine::ecs::CommandQueue,
) {
use crate::engine::ecs::component::{
ColorComponent, EmissiveComponent, NormalVisualisationComponent, RenderableComponent,
TransformComponent,
};
use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Renderable};
let pending = std::mem::take(&mut self.pending_normal_vis);
for (nv_id, _renderable_id, base_mesh, thickness) in pending {
if let Some(nv) = world.get_component_by_id_as::<NormalVisualisationComponent>(nv_id) {
if !nv.spawned_roots.is_empty() {
continue;
}
} else {
continue;
}
let Some(cpu_mesh) = render_assets.cpu_mesh(base_mesh) else {
self.pending_normal_vis
.push((nv_id, _renderable_id, base_mesh, thickness));
continue;
};
let half_height = thickness * 5.0;
let mut spawned_roots: Vec<ComponentId> = Vec::new();
for vertex in &cpu_mesh.vertices {
let pos = vertex.pos;
let n = vertex.normal;
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
let n = if len > 1e-6 {
[n[0] / len, n[1] / len, n[2] / len]
} else {
[0.0, 1.0, 0.0]
};
let cx = pos[0] + n[0] * half_height;
let cy = pos[1] + n[1] * half_height;
let cz = pos[2] + n[2] * half_height;
let quat = crate::utils::math::shortest_arc_quat([0.0, 1.0, 0.0], n);
let t_id = world.add_component(
TransformComponent::new()
.with_position(cx, cy, cz)
.with_rotation_quat(quat)
.with_scale(thickness, thickness * 10.0, thickness),
);
let r_id = world.add_component(RenderableComponent::new(Renderable::new(
CpuMeshHandle::CUBE,
MaterialHandle::TOON_MESH,
)));
let c_id = world.add_component(ColorComponent::rgba(0.0, 1.0, 1.0, 1.0));
let e_id = world.add_component(EmissiveComponent::on());
let _ = world.add_child(nv_id, t_id);
let _ = world.add_child(t_id, r_id);
let _ = world.add_child(r_id, c_id);
let _ = world.add_child(r_id, e_id);
world.init_component_tree(t_id, queue);
spawned_roots.push(t_id);
}
if let Some(nv) =
world.get_component_by_id_as_mut::<NormalVisualisationComponent>(nv_id)
{
nv.spawned_roots = spawned_roots;
}
}
}
}
fn cache_resolved_mesh_bounds(
world: &mut World,
render_assets: &RenderAssets,
renderable: ComponentId,
mesh: CpuMeshHandle,
) {
let Some(local) = render_assets.cpu_mesh(mesh).and_then(|cpu_mesh| {
let positions: Vec<[f32; 3]> = cpu_mesh.vertices.iter().map(|vertex| vertex.pos).collect();
Aabb::from_points(&positions)
}) else {
return;
};
let existing_bounds = world
.children_of(renderable)
.iter()
.copied()
.find(|&child| {
world
.get_component_by_id_as::<BoundsComponent>(child)
.is_some()
});
if let Some(bounds) =
existing_bounds.and_then(|child| world.get_component_by_id_as_mut::<BoundsComponent>(child))
{
bounds.local = local;
return;
}
let bounds = world.add_component(BoundsComponent::new(local));
let _ = world.add_child(renderable, bounds);
}
impl System for RenderableSystem {
fn tick(
&mut self,
_world: &mut World,
_visuals: &mut VisualWorld,
_input: &InputState,
_dt_sec: f32,
) {
}
}
#[cfg(test)]
mod tests {
use super::RenderableSystem;
use crate::engine::ecs::CommandQueue;
use crate::engine::ecs::World;
use crate::engine::ecs::component::{
BackgroundComponent, ColorComponent, EmissiveComponent, OpacityComponent, OverlayComponent,
RenderableComponent, TextComponent, TransformComponent, TransparentCutoutComponent,
};
use crate::engine::graphics::primitives::MeshHandle;
use crate::engine::graphics::{CpuMesh, MeshUploader, RenderAssets, VisualWorld};
#[derive(Default)]
struct TestUploader {
next_mesh: u32,
}
impl MeshUploader for TestUploader {
fn upload_mesh(
&mut self,
_mesh: &CpuMesh,
) -> Result<MeshHandle, Box<dyn std::error::Error>> {
let handle = MeshHandle(self.next_mesh);
self.next_mesh += 1;
Ok(handle)
}
}
#[test]
fn effective_style_preserves_renderable_local_and_ancestor_semantics() {
let mut world = World::default();
let text = world.add_component(TextComponent::new("item"));
let text_color = world.add_component(ColorComponent::rgba(0.2, 0.3, 0.4, 1.0));
let text_opacity = world.add_component(OpacityComponent::new().with_opacity(0.4));
let overlay = world.add_component(OverlayComponent::new());
let background =
world.add_component(BackgroundComponent::new().with_occlusion_and_lighting());
let overlay_host = world.add_component(TransformComponent::new());
let glyph_t = world.add_component(TransformComponent::new());
let glyph_r = world.add_component(RenderableComponent::square());
let local_color = world.add_component(ColorComponent::rgba(0.9, 0.8, 0.7, 1.0));
let cutout = world.add_component(TransparentCutoutComponent::new());
let _ = world.add_child(text, text_color);
let _ = world.add_child(text, text_opacity);
let _ = world.add_child(text, background);
let _ = world.add_child(background, overlay);
let _ = world.add_child(overlay, overlay_host);
let _ = world.add_child(overlay_host, glyph_t);
let _ = world.add_child(glyph_t, glyph_r);
let _ = world.add_child(glyph_r, local_color);
let _ = world.add_child(glyph_r, cutout);
let style = RenderableSystem::resolve_effective_renderable_style(&world, glyph_r);
assert_eq!(style.color, [0.9, 0.8, 0.7, 1.0]);
assert_eq!(style.opacity.opacity, 0.4);
assert!(!style.opacity.multiple_layers);
assert!(style.transparent_cutout);
assert!(style.background);
assert!(style.background_occluded_lit);
assert!(style.overlay);
}
#[test]
fn effective_style_defaults_when_no_style_ancestors_exist() {
let mut world = World::default();
let text = world.add_component(TextComponent::new("item"));
let glyph_t = world.add_component(TransformComponent::new());
let glyph_r = world.add_component(RenderableComponent::square());
let _ = world.add_child(text, glyph_t);
let _ = world.add_child(glyph_t, glyph_r);
let style = RenderableSystem::resolve_effective_renderable_style(&world, glyph_r);
assert_eq!(style.color, [1.0, 1.0, 1.0, 1.0]);
assert_eq!(style.opacity.opacity, 1.0);
assert!(!style.opacity.multiple_layers);
assert!(!style.transparent_cutout);
assert!(!style.background);
assert!(!style.background_occluded_lit);
assert!(!style.overlay);
}
#[test]
fn flush_pending_recomputes_style_after_attach() {
let mut world = World::default();
let mut visuals = VisualWorld::default();
let mut renderable_system = RenderableSystem::default();
let mut render_assets = RenderAssets::new();
let mut uploader = TestUploader::default();
let mut queue = CommandQueue::new();
let overlay_root = world.add_component(OverlayComponent::new());
let item_root = world.add_component(TransformComponent::new());
let renderable_root = world.add_component(TransformComponent::new());
let renderable = world.add_component(RenderableComponent::square());
let _ = world.add_child(item_root, renderable_root);
let _ = world.add_child(renderable_root, renderable);
renderable_system.register_renderable_from_world(&mut world, &mut visuals, renderable);
let _ = world.add_child(overlay_root, item_root);
let inserted = renderable_system.flush_pending(
&mut world,
&mut visuals,
&mut render_assets,
&mut uploader,
&mut queue,
);
assert!(inserted);
let handle = world
.get_component_by_id_as::<RenderableComponent>(renderable)
.and_then(|r| r.get_handle())
.expect("renderable handle after flush");
let instance = visuals.instance(handle).expect("visual instance");
assert!(instance.overlay);
}
#[test]
fn text_style_emissive_targets_descendants_not_ancestor_renderable() {
let mut world = World::default();
let mut visuals = VisualWorld::default();
let mut renderable = RenderableSystem::default();
let plane = world.add_component(RenderableComponent::square());
let text = world.add_component(TextComponent::new("item"));
let glyph_t = world.add_component(TransformComponent::new());
let glyph_r = world.add_component(RenderableComponent::square());
let emissive = world.add_component(EmissiveComponent::on());
let _ = world.add_child(plane, text);
let _ = world.add_child(text, glyph_t);
let _ = world.add_child(glyph_t, glyph_r);
let _ = world.add_child(text, emissive);
renderable.register_emissive(&mut world, &mut visuals, emissive);
assert_eq!(renderable.pending_emissive.get(&plane), None);
assert_eq!(renderable.pending_emissive.get(&glyph_r), Some(&1.0));
}
}