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}