Skip to main content

khora_data/render/
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//! The intermediate `RenderWorld` and its associated extracted-data types.
16//!
17//! `RenderWorld` is a per-frame, GPU-friendly snapshot of the scene used by
18//! the rendering lanes.  It is populated by [`extract_scene`](super::extract_scene)
19//! once per frame in the engine's hot loop.
20
21use khora_core::{
22    asset::{AssetHandle, AssetUUID, Material},
23    math::{affine_transform::AffineTransform, Vec3},
24    renderer::{
25        api::scene::{GpuMaterial, GpuMesh},
26        light::LightType,
27    },
28};
29
30/// Flat, GPU-friendly representation of a single mesh to render.
31#[derive(Clone)]
32pub struct ExtractedMesh {
33    /// World-space transform derived from `GlobalTransform`.
34    pub transform: AffineTransform,
35    /// UUID of the loaded CPU mesh — useful for debugging or mapping.
36    pub cpu_mesh_uuid: AssetUUID,
37    /// Handle to the uploaded GPU mesh data.
38    pub gpu_mesh: AssetHandle<GpuMesh>,
39    /// Optional CPU-side material handle.  `None` means use a default material.
40    ///
41    /// Still consumed by lanes that branch on the concrete material type
42    /// (e.g. `SimpleUnlitLane` pipeline selection); lit lanes read the
43    /// projected [`gpu_material`](Self::gpu_material) instead.
44    pub material: Option<AssetHandle<Box<dyn Material>>>,
45    /// Handle to the projected GPU material (uniform buffer + textures +
46    /// group-2 bind group), produced by the material projection.  `None`
47    /// until the projection has uploaded it, or when the entity carries no
48    /// resolved material handle (lit lanes fall back to a default material).
49    pub gpu_material: Option<AssetHandle<GpuMaterial>>,
50}
51
52/// Flat, GPU-friendly representation of a light source.
53#[derive(Debug, Clone)]
54pub struct ExtractedLight {
55    /// Light type and its parameters.
56    pub light_type: LightType,
57    /// World-space position (ignored for purely directional lights).
58    pub position: Vec3,
59    /// World-space direction (ignored for point lights).
60    pub direction: Vec3,
61    /// View-projection matrix used for shadow mapping.
62    pub shadow_view_proj: khora_core::math::Mat4,
63    /// Index into the shadow atlas, or `None` if the light casts no shadow.
64    pub shadow_atlas_index: Option<i32>,
65}
66
67/// Flat representation of a camera view.
68#[derive(Debug, Clone)]
69pub struct ExtractedView {
70    /// View-projection matrix.
71    pub view_proj: khora_core::math::Mat4,
72    /// World-space camera position.
73    pub position: Vec3,
74}
75
76/// All scene data needed to render one frame.
77///
78/// Populated by [`extract_scene`](super::extract_scene).  Consumed by the
79/// render lanes through the shared [`RenderWorldStore`](super::RenderWorldStore).
80#[derive(Default, Clone)]
81pub struct RenderWorld {
82    /// Meshes to draw this frame.
83    pub meshes: Vec<ExtractedMesh>,
84    /// Active lights affecting the frame.
85    pub lights: Vec<ExtractedLight>,
86    /// Active camera views.
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 extracted data.  Called at the start of each 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.
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.
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.
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 render_world_default_is_empty() {
135        let world = RenderWorld::default();
136        assert!(world.meshes.is_empty());
137        assert!(world.lights.is_empty());
138        assert!(world.views.is_empty());
139    }
140
141    #[test]
142    fn render_world_clear_drains_collections() {
143        let mut world = RenderWorld::new();
144        world.lights.push(ExtractedLight {
145            light_type: LightType::Directional(DirectionalLight::default()),
146            position: Vec3::ZERO,
147            direction: Vec3::new(0.0, -1.0, 0.0),
148            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
149            shadow_atlas_index: None,
150        });
151        assert_eq!(world.lights.len(), 1);
152
153        world.clear();
154        assert!(world.lights.is_empty());
155        assert!(world.meshes.is_empty());
156    }
157
158    #[test]
159    fn light_count_methods_filter_by_type() {
160        let mut world = RenderWorld::new();
161        world.lights.push(ExtractedLight {
162            light_type: LightType::Directional(DirectionalLight::default()),
163            position: Vec3::ZERO,
164            direction: Vec3::new(0.0, -1.0, 0.0),
165            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
166            shadow_atlas_index: None,
167        });
168        world.lights.push(ExtractedLight {
169            light_type: LightType::Point(PointLight::default()),
170            position: Vec3::new(1.0, 2.0, 3.0),
171            direction: Vec3::ZERO,
172            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
173            shadow_atlas_index: None,
174        });
175        world.lights.push(ExtractedLight {
176            light_type: LightType::Point(PointLight::default()),
177            position: Vec3::new(-1.0, 2.0, 3.0),
178            direction: Vec3::ZERO,
179            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
180            shadow_atlas_index: None,
181        });
182        world.lights.push(ExtractedLight {
183            light_type: LightType::Spot(SpotLight::default()),
184            position: Vec3::ZERO,
185            direction: Vec3::new(0.0, -1.0, 0.0),
186            shadow_view_proj: khora_core::math::Mat4::IDENTITY,
187            shadow_atlas_index: None,
188        });
189
190        assert_eq!(world.directional_light_count(), 1);
191        assert_eq!(world.point_light_count(), 2);
192        assert_eq!(world.spot_light_count(), 1);
193    }
194}