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}