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::{AssetHandle, AssetUUID, Material},
23    math::{affine_transform::AffineTransform, Vec3},
24    renderer::{api::scene::GpuMesh, 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 UUID of the loaded CPU Mesh. Used for debugging or mapping.
35    pub cpu_mesh_uuid: AssetUUID,
36    /// A handle to the uploaded GPU mesh data.
37    pub gpu_mesh: AssetHandle<GpuMesh>,
38    /// A handle to the material to be used for rendering.
39    /// If `None`, a default material should be used.
40    pub material: Option<AssetHandle<Box<dyn Material>>>,
41}
42
43/// A flat, GPU-friendly representation of a light source for rendering.
44///
45/// This struct contains the light's properties along with its world-space
46/// position and direction, extracted from the ECS.
47#[derive(Debug, Clone)]
48pub struct ExtractedLight {
49    /// The type and properties of the light source.
50    pub light_type: LightType,
51    /// The world-space position of the light (from `GlobalTransform`).
52    ///
53    /// For directional lights, this is typically ignored.
54    pub position: Vec3,
55    /// The world-space direction of the light.
56    ///
57    /// For point lights, this is typically ignored.
58    /// For directional and spot lights, this is the direction the light is pointing.
59    pub direction: Vec3,
60    /// View-projection matrix for the shadow map.
61    pub shadow_view_proj: khora_core::math::Mat4,
62    /// Index into the shadow atlas, or None if no shadow.
63    pub shadow_atlas_index: Option<i32>,
64}
65
66/// A flat representation of a camera view for rendering.
67#[derive(Debug, Clone)]
68pub struct ExtractedView {
69    /// The view-projection matrix for this camera.
70    pub view_proj: khora_core::math::Mat4,
71    /// The world-space position of the camera.
72    pub position: Vec3,
73}
74
75/// A collection of all data extracted from the main `World` needed for rendering a single frame.
76///
77/// This acts as the primary input to the entire rendering system. By decoupling
78/// from the main ECS, the render thread can work on this data without contention
79/// while the simulation thread advances the next frame.
80#[derive(Default)]
81pub struct RenderWorld {
82    /// A list of all meshes to be rendered in the current frame.
83    pub meshes: Vec<ExtractedMesh>,
84    /// A list of all active lights affecting the current frame.
85    pub lights: Vec<ExtractedLight>,
86    /// A list of all active camera views for the current frame.
87    pub views: Vec<ExtractedView>,
88}
89
90impl RenderWorld {
91    /// Creates a new, empty `RenderWorld`.
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Clears all the data in the `RenderWorld`, preparing it for the next frame's extraction.
97    pub fn clear(&mut self) {
98        self.meshes.clear();
99        self.lights.clear();
100        self.views.clear();
101    }
102
103    /// Returns the number of directional lights in the render world.
104    pub fn directional_light_count(&self) -> usize {
105        self.lights
106            .iter()
107            .filter(|l| matches!(l.light_type, LightType::Directional(_)))
108            .count()
109    }
110
111    /// Returns the number of point lights in the render world.
112    pub fn point_light_count(&self) -> usize {
113        self.lights
114            .iter()
115            .filter(|l| matches!(l.light_type, LightType::Point(_)))
116            .count()
117    }
118
119    /// Returns the number of spot lights in the render world.
120    pub fn spot_light_count(&self) -> usize {
121        self.lights
122            .iter()
123            .filter(|l| matches!(l.light_type, LightType::Spot(_)))
124            .count()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use khora_core::renderer::light::{DirectionalLight, PointLight, SpotLight};
132
133    #[test]
134    fn test_render_world_default() {
135        let world = RenderWorld::default();
136        assert!(world.meshes.is_empty());
137        assert!(world.lights.is_empty());
138    }
139
140    #[test]
141    fn test_render_world_clear() {
142        let mut world = RenderWorld::new();
143        world.lights.push(ExtractedLight {
144            light_type: LightType::Directional(DirectionalLight::default()),
145            position: Vec3::ZERO,
146            direction: Vec3::new(0.0, -1.0, 0.0),
147            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
148            shadow_atlas_index: None,
149        });
150        assert_eq!(world.lights.len(), 1);
151
152        world.clear();
153        assert!(world.lights.is_empty());
154        assert!(world.meshes.is_empty());
155    }
156
157    #[test]
158    fn test_light_count_methods() {
159        let mut world = RenderWorld::new();
160
161        // Add lights
162        world.lights.push(ExtractedLight {
163            light_type: LightType::Directional(DirectionalLight::default()),
164            position: Vec3::ZERO,
165            direction: Vec3::new(0.0, -1.0, 0.0),
166            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
167            shadow_atlas_index: None,
168        });
169        world.lights.push(ExtractedLight {
170            light_type: LightType::Point(PointLight::default()),
171            position: Vec3::new(1.0, 2.0, 3.0),
172            direction: Vec3::ZERO,
173            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
174            shadow_atlas_index: None,
175        });
176        world.lights.push(ExtractedLight {
177            light_type: LightType::Point(PointLight::default()),
178            position: Vec3::new(-1.0, 2.0, 3.0),
179            direction: Vec3::ZERO,
180            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
181            shadow_atlas_index: None,
182        });
183        world.lights.push(ExtractedLight {
184            light_type: LightType::Spot(SpotLight::default()),
185            position: Vec3::ZERO,
186            direction: Vec3::new(0.0, -1.0, 0.0),
187            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
188            shadow_atlas_index: None,
189        });
190
191        assert_eq!(world.directional_light_count(), 1);
192        assert_eq!(world.point_light_count(), 2);
193        assert_eq!(world.spot_light_count(), 1);
194    }
195}