khora_lanes/render_lane/
world.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines the intermediate `RenderWorld` and its associated data structures.
16//!
17//! The `RenderWorld` is a temporary, frame-by-frame representation of the scene,
18//! optimized for consumption by the rendering pipelines (`RenderLane`s). It is
19//! populated by an "extraction" phase that reads data from the main ECS `World`.
20
21use khora_core::{
22    asset::AssetUUID,
23    math::{affine_transform::AffineTransform, Vec3},
24    renderer::light::LightType,
25};
26
27/// A flat, GPU-friendly representation of a single mesh to be rendered.
28///
29/// This struct contains all the necessary information, copied from various ECS
30/// components, required to issue a draw call for a mesh.
31pub struct ExtractedMesh {
32    /// The world-space transformation matrix of the mesh, derived from `GlobalTransform`.
33    pub transform: AffineTransform,
34    /// The unique identifier of the GpuMesh asset to be rendered.
35    pub gpu_mesh_uuid: AssetUUID,
36    /// The unique identifier of the material to be used for rendering.
37    /// If `None`, a default material should be used.
38    pub material_uuid: Option<AssetUUID>,
39}
40
41/// A flat, GPU-friendly representation of a light source for rendering.
42///
43/// This struct contains the light's properties along with its world-space
44/// position and direction, extracted from the ECS.
45#[derive(Debug, Clone)]
46pub struct ExtractedLight {
47    /// The type and properties of the light source.
48    pub light_type: LightType,
49    /// The world-space position of the light (from `GlobalTransform`).
50    ///
51    /// For directional lights, this is typically ignored.
52    pub position: Vec3,
53    /// The world-space direction of the light.
54    ///
55    /// For point lights, this is typically ignored.
56    /// For directional and spot lights, this is the direction the light is pointing.
57    pub direction: Vec3,
58}
59
60/// A collection of all data extracted from the main `World` needed for rendering a single frame.
61///
62/// This acts as the primary input to the entire rendering system. By decoupling
63/// from the main ECS, the render thread can work on this data without contention
64/// while the simulation thread advances the next frame.
65#[derive(Default)]
66pub struct RenderWorld {
67    /// A list of all meshes to be rendered in the current frame.
68    pub meshes: Vec<ExtractedMesh>,
69    /// A list of all active lights affecting the current frame.
70    pub lights: Vec<ExtractedLight>,
71}
72
73impl RenderWorld {
74    /// Creates a new, empty `RenderWorld`.
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Clears all the data in the `RenderWorld`, preparing it for the next frame's extraction.
80    pub fn clear(&mut self) {
81        self.meshes.clear();
82        self.lights.clear();
83    }
84
85    /// Returns the number of directional lights in the render world.
86    pub fn directional_light_count(&self) -> usize {
87        self.lights
88            .iter()
89            .filter(|l| matches!(l.light_type, LightType::Directional(_)))
90            .count()
91    }
92
93    /// Returns the number of point lights in the render world.
94    pub fn point_light_count(&self) -> usize {
95        self.lights
96            .iter()
97            .filter(|l| matches!(l.light_type, LightType::Point(_)))
98            .count()
99    }
100
101    /// Returns the number of spot lights in the render world.
102    pub fn spot_light_count(&self) -> usize {
103        self.lights
104            .iter()
105            .filter(|l| matches!(l.light_type, LightType::Spot(_)))
106            .count()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use khora_core::renderer::light::{DirectionalLight, PointLight, SpotLight};
114
115    #[test]
116    fn test_render_world_default() {
117        let world = RenderWorld::default();
118        assert!(world.meshes.is_empty());
119        assert!(world.lights.is_empty());
120    }
121
122    #[test]
123    fn test_render_world_clear() {
124        let mut world = RenderWorld::new();
125        world.lights.push(ExtractedLight {
126            light_type: LightType::Directional(DirectionalLight::default()),
127            position: Vec3::ZERO,
128            direction: Vec3::new(0.0, -1.0, 0.0),
129        });
130        assert_eq!(world.lights.len(), 1);
131
132        world.clear();
133        assert!(world.lights.is_empty());
134        assert!(world.meshes.is_empty());
135    }
136
137    #[test]
138    fn test_light_count_methods() {
139        let mut world = RenderWorld::new();
140
141        // Add lights
142        world.lights.push(ExtractedLight {
143            light_type: LightType::Directional(DirectionalLight::default()),
144            position: Vec3::ZERO,
145            direction: Vec3::new(0.0, -1.0, 0.0),
146        });
147        world.lights.push(ExtractedLight {
148            light_type: LightType::Point(PointLight::default()),
149            position: Vec3::new(1.0, 2.0, 3.0),
150            direction: Vec3::ZERO,
151        });
152        world.lights.push(ExtractedLight {
153            light_type: LightType::Point(PointLight::default()),
154            position: Vec3::new(-1.0, 2.0, 3.0),
155            direction: Vec3::ZERO,
156        });
157        world.lights.push(ExtractedLight {
158            light_type: LightType::Spot(SpotLight::default()),
159            position: Vec3::ZERO,
160            direction: Vec3::new(0.0, -1.0, 0.0),
161        });
162
163        assert_eq!(world.directional_light_count(), 1);
164        assert_eq!(world.point_light_count(), 2);
165        assert_eq!(world.spot_light_count(), 1);
166    }
167}