1use super::{ExtractedLight, ExtractedMesh, RenderWorld};
18use khora_core::{
19 math::Vec3,
20 renderer::{light::LightType, GpuMesh},
21};
22use khora_data::ecs::{GlobalTransform, HandleComponent, Light, MaterialComponent, World};
23
24#[derive(Default)]
30pub struct ExtractRenderablesLane;
31
32impl ExtractRenderablesLane {
33 pub fn new() -> Self {
35 Self
36 }
37
38 pub fn run(&self, world: &World, render_world: &mut RenderWorld) {
44 render_world.clear();
46
47 let query = world.query::<(&GlobalTransform, &HandleComponent<GpuMesh>)>();
51
52 for (entity_id, (transform, gpu_mesh_handle_comp)) in query.enumerate() {
54 let material_uuid = world
56 .query::<&MaterialComponent>()
57 .nth(entity_id)
58 .map(|material_comp| material_comp.uuid);
59
60 let extracted_mesh = ExtractedMesh {
61 transform: transform.0,
63 gpu_mesh_uuid: gpu_mesh_handle_comp.uuid,
65 material_uuid,
67 };
68 render_world.meshes.push(extracted_mesh);
69 }
70
71 self.extract_lights(world, render_world);
73 }
74
75 fn extract_lights(&self, world: &World, render_world: &mut RenderWorld) {
77 let light_query = world.query::<(&Light, &GlobalTransform)>();
79
80 for (light_comp, global_transform) in light_query {
81 if !light_comp.enabled {
83 continue;
84 }
85
86 let position = global_transform.0.translation();
88
89 let direction = match &light_comp.light_type {
94 LightType::Directional(dir_light) => dir_light.direction,
95 LightType::Spot(spot_light) => {
96 let rotation = global_transform.0.rotation();
98 rotation * spot_light.direction
99 }
100 LightType::Point(_) => Vec3::ZERO, };
102
103 let extracted = ExtractedLight {
104 light_type: light_comp.light_type,
105 position,
106 direction,
107 };
108
109 render_world.lights.push(extracted);
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use khora_core::{
118 asset::{AssetHandle, AssetUUID},
119 math::{affine_transform::AffineTransform, Vec3},
120 renderer::GpuMesh,
121 };
122
123 fn create_dummy_gpu_mesh() -> GpuMesh {
125 use khora_core::renderer::{api::PrimitiveTopology, BufferId, IndexFormat};
126 GpuMesh {
127 vertex_buffer: BufferId(0),
128 index_buffer: BufferId(0),
129 index_count: 0,
130 index_format: IndexFormat::Uint32,
131 primitive_topology: PrimitiveTopology::TriangleList,
132 }
133 }
134
135 #[derive(Clone)]
137 struct DummyMaterial;
138
139 impl khora_core::asset::Material for DummyMaterial {}
140 impl khora_core::asset::Asset for DummyMaterial {}
141
142 #[test]
143 fn test_extract_lane_creation() {
144 let lane = ExtractRenderablesLane::new();
145 let _ = lane;
147 }
148
149 #[test]
150 fn test_extract_lane_default() {
151 let _lane = ExtractRenderablesLane;
152 }
153
154 #[test]
155 fn test_extract_empty_world() {
156 let lane = ExtractRenderablesLane::new();
157 let world = World::new();
158 let mut render_world = RenderWorld::default();
159
160 lane.run(&world, &mut render_world);
161
162 assert_eq!(
163 render_world.meshes.len(),
164 0,
165 "Empty world should extract no meshes"
166 );
167 }
168
169 #[test]
170 fn test_extract_single_entity_without_material() {
171 let lane = ExtractRenderablesLane::new();
172 let mut world = World::new();
173 let mut render_world = RenderWorld::default();
174
175 let transform =
177 GlobalTransform(AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0)));
178
179 let mesh_uuid = AssetUUID::new();
181 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
182 handle: AssetHandle::new(create_dummy_gpu_mesh()),
183 uuid: mesh_uuid,
184 };
185
186 world.spawn((transform, gpu_mesh_handle));
188
189 lane.run(&world, &mut render_world);
190
191 assert_eq!(render_world.meshes.len(), 1, "Should extract 1 mesh");
192
193 let extracted = &render_world.meshes[0];
194 assert_eq!(extracted.gpu_mesh_uuid, mesh_uuid);
195 assert_eq!(extracted.material_uuid, None);
196 assert_eq!(extracted.transform.translation(), Vec3::new(1.0, 2.0, 3.0));
197 }
198
199 #[test]
200 fn test_extract_single_entity_with_material() {
201 let lane = ExtractRenderablesLane::new();
202 let mut world = World::new();
203 let mut render_world = RenderWorld::default();
204
205 let transform = GlobalTransform(AffineTransform::from_translation(Vec3::new(
207 5.0, 10.0, 15.0,
208 )));
209 let mesh_uuid = AssetUUID::new();
210 let material_uuid = AssetUUID::new();
211
212 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
213 handle: AssetHandle::new(create_dummy_gpu_mesh()),
214 uuid: mesh_uuid,
215 };
216
217 let dummy_material: Box<dyn khora_core::asset::Material> = Box::new(DummyMaterial);
219 let material = MaterialComponent {
220 handle: AssetHandle::new(dummy_material),
221 uuid: material_uuid,
222 };
223
224 world.spawn((transform, gpu_mesh_handle, material));
226
227 lane.run(&world, &mut render_world);
228
229 assert_eq!(render_world.meshes.len(), 1, "Should extract 1 mesh");
230
231 let extracted = &render_world.meshes[0];
232 assert_eq!(extracted.gpu_mesh_uuid, mesh_uuid);
233 assert_eq!(extracted.material_uuid, Some(material_uuid));
234 assert_eq!(
235 extracted.transform.translation(),
236 Vec3::new(5.0, 10.0, 15.0)
237 );
238 }
239
240 #[test]
241 fn test_extract_multiple_entities() {
242 let lane = ExtractRenderablesLane::new();
243 let mut world = World::new();
244 let mut render_world = RenderWorld::default();
245
246 let mut with_material_count = 0;
247 let mut without_material_count = 0;
248
249 for i in 0..5 {
251 let transform = GlobalTransform(AffineTransform::from_translation(Vec3::new(
252 i as f32,
253 i as f32 * 2.0,
254 i as f32 * 3.0,
255 )));
256 let mesh_uuid = AssetUUID::new();
257 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
258 handle: AssetHandle::new(create_dummy_gpu_mesh()),
259 uuid: mesh_uuid,
260 };
261
262 if i % 2 == 0 {
264 let dummy_material: Box<dyn khora_core::asset::Material> = Box::new(DummyMaterial);
265 let material = MaterialComponent {
266 handle: AssetHandle::new(dummy_material),
267 uuid: AssetUUID::new(),
268 };
269 world.spawn((transform, gpu_mesh_handle, material));
270 with_material_count += 1;
271 } else {
272 world.spawn((transform, gpu_mesh_handle));
273 without_material_count += 1;
274 }
275 }
276
277 lane.run(&world, &mut render_world);
278
279 assert_eq!(render_world.meshes.len(), 5, "Should extract 5 meshes");
280
281 let extracted_with_material = render_world
283 .meshes
284 .iter()
285 .filter(|m| m.material_uuid.is_some())
286 .count();
287 let extracted_without_material = render_world
288 .meshes
289 .iter()
290 .filter(|m| m.material_uuid.is_none())
291 .count();
292
293 assert_eq!(
294 extracted_with_material, with_material_count,
295 "Should have {} entities with materials",
296 with_material_count
297 );
298 assert_eq!(
299 extracted_without_material, without_material_count,
300 "Should have {} entities without materials",
301 without_material_count
302 );
303 }
304
305 #[test]
306 fn test_extract_with_different_transforms() {
307 let lane = ExtractRenderablesLane::new();
308 let mut world = World::new();
309 let mut render_world = RenderWorld::default();
310
311 let transform1 = GlobalTransform(AffineTransform::from_translation(Vec3::new(
313 10.0, 20.0, 30.0,
314 )));
315 world.spawn((
316 transform1,
317 HandleComponent::<GpuMesh> {
318 handle: AssetHandle::new(create_dummy_gpu_mesh()),
319 uuid: AssetUUID::new(),
320 },
321 ));
322
323 use khora_core::math::Quaternion;
325 let transform2 = GlobalTransform(AffineTransform::from_quat(Quaternion::from_axis_angle(
326 Vec3::Y,
327 std::f32::consts::PI / 2.0,
328 )));
329 world.spawn((
330 transform2,
331 HandleComponent::<GpuMesh> {
332 handle: AssetHandle::new(create_dummy_gpu_mesh()),
333 uuid: AssetUUID::new(),
334 },
335 ));
336
337 let transform3 = GlobalTransform(AffineTransform::from_scale(Vec3::new(2.0, 3.0, 4.0)));
339 world.spawn((
340 transform3,
341 HandleComponent::<GpuMesh> {
342 handle: AssetHandle::new(create_dummy_gpu_mesh()),
343 uuid: AssetUUID::new(),
344 },
345 ));
346
347 lane.run(&world, &mut render_world);
348
349 assert_eq!(render_world.meshes.len(), 3, "Should extract 3 meshes");
350
351 assert_eq!(
353 render_world.meshes[0].transform.translation(),
354 Vec3::new(10.0, 20.0, 30.0)
355 );
356
357 let mat0 = render_world.meshes[0].transform.to_matrix();
359 let mat1 = render_world.meshes[1].transform.to_matrix();
360 let mat2 = render_world.meshes[2].transform.to_matrix();
361 assert_ne!(mat0, mat1);
362 assert_ne!(mat1, mat2);
363 }
364
365 #[test]
366 fn test_extract_clears_previous_data() {
367 let lane = ExtractRenderablesLane::new();
368 let mut world = World::new();
369 let mut render_world = RenderWorld::default();
370
371 for _ in 0..3 {
373 let transform = GlobalTransform(AffineTransform::IDENTITY);
374 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
375 handle: AssetHandle::new(create_dummy_gpu_mesh()),
376 uuid: AssetUUID::new(),
377 };
378 world.spawn((transform, gpu_mesh_handle));
379 }
380
381 lane.run(&world, &mut render_world);
382 assert_eq!(
383 render_world.meshes.len(),
384 3,
385 "First run should extract 3 meshes"
386 );
387
388 let mut world2 = World::new();
390 let transform = GlobalTransform(AffineTransform::IDENTITY);
391 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
392 handle: AssetHandle::new(create_dummy_gpu_mesh()),
393 uuid: AssetUUID::new(),
394 };
395 world2.spawn((transform, gpu_mesh_handle));
396
397 lane.run(&world2, &mut render_world);
399 assert_eq!(
400 render_world.meshes.len(),
401 1,
402 "Second run should extract only 1 mesh (cleared previous)"
403 );
404 }
405
406 #[test]
407 fn test_extract_entities_without_mesh_component() {
408 let lane = ExtractRenderablesLane::new();
409 let mut world = World::new();
410 let mut render_world = RenderWorld::default();
411
412 use khora_data::ecs::Transform;
415 for _ in 0..3 {
416 let transform = GlobalTransform(AffineTransform::IDENTITY);
417 world.spawn((transform, Transform::default()));
419 }
420
421 let transform = GlobalTransform(AffineTransform::IDENTITY);
423 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
424 handle: AssetHandle::new(create_dummy_gpu_mesh()),
425 uuid: AssetUUID::new(),
426 };
427 world.spawn((transform, gpu_mesh_handle));
428
429 lane.run(&world, &mut render_world);
430
431 assert_eq!(
433 render_world.meshes.len(),
434 1,
435 "Should only extract entities with both components"
436 );
437 }
438
439 #[test]
440 fn test_extract_preserves_mesh_uuids() {
441 let lane = ExtractRenderablesLane::new();
442 let mut world = World::new();
443 let mut render_world = RenderWorld::default();
444
445 let uuid1 = AssetUUID::new();
447 let uuid2 = AssetUUID::new();
448 let uuid3 = AssetUUID::new();
449
450 let transform = GlobalTransform(AffineTransform::IDENTITY);
451
452 world.spawn((
453 transform,
454 HandleComponent::<GpuMesh> {
455 handle: AssetHandle::new(create_dummy_gpu_mesh()),
456 uuid: uuid1,
457 },
458 ));
459 world.spawn((
460 transform,
461 HandleComponent::<GpuMesh> {
462 handle: AssetHandle::new(create_dummy_gpu_mesh()),
463 uuid: uuid2,
464 },
465 ));
466 world.spawn((
467 transform,
468 HandleComponent::<GpuMesh> {
469 handle: AssetHandle::new(create_dummy_gpu_mesh()),
470 uuid: uuid3,
471 },
472 ));
473
474 lane.run(&world, &mut render_world);
475
476 assert_eq!(render_world.meshes.len(), 3);
477
478 let extracted_uuids: Vec<AssetUUID> = render_world
480 .meshes
481 .iter()
482 .map(|m| m.gpu_mesh_uuid)
483 .collect();
484
485 assert!(extracted_uuids.contains(&uuid1), "Should contain uuid1");
486 assert!(extracted_uuids.contains(&uuid2), "Should contain uuid2");
487 assert!(extracted_uuids.contains(&uuid3), "Should contain uuid3");
488 }
489
490 #[test]
491 fn test_extract_with_identity_transform() {
492 let lane = ExtractRenderablesLane::new();
493 let mut world = World::new();
494 let mut render_world = RenderWorld::default();
495
496 let transform = GlobalTransform(AffineTransform::IDENTITY);
497 let mesh_uuid = AssetUUID::new();
498 world.spawn((
499 transform,
500 HandleComponent::<GpuMesh> {
501 handle: AssetHandle::new(create_dummy_gpu_mesh()),
502 uuid: mesh_uuid,
503 },
504 ));
505
506 lane.run(&world, &mut render_world);
507
508 assert_eq!(render_world.meshes.len(), 1);
509
510 let extracted = &render_world.meshes[0];
511 use khora_core::math::Mat4;
512 assert_eq!(extracted.transform.to_matrix(), Mat4::IDENTITY);
513 }
514
515 #[test]
516 fn test_extract_multiple_runs() {
517 let lane = ExtractRenderablesLane::new();
518 let mut world = World::new();
519 let mut render_world = RenderWorld::default();
520
521 let transform = GlobalTransform(AffineTransform::IDENTITY);
523 world.spawn((
524 transform,
525 HandleComponent::<GpuMesh> {
526 handle: AssetHandle::new(create_dummy_gpu_mesh()),
527 uuid: AssetUUID::new(),
528 },
529 ));
530
531 lane.run(&world, &mut render_world);
533 assert_eq!(render_world.meshes.len(), 1);
534
535 world.spawn((
537 transform,
538 HandleComponent::<GpuMesh> {
539 handle: AssetHandle::new(create_dummy_gpu_mesh()),
540 uuid: AssetUUID::new(),
541 },
542 ));
543 world.spawn((
544 transform,
545 HandleComponent::<GpuMesh> {
546 handle: AssetHandle::new(create_dummy_gpu_mesh()),
547 uuid: AssetUUID::new(),
548 },
549 ));
550
551 lane.run(&world, &mut render_world);
553 assert_eq!(
554 render_world.meshes.len(),
555 3,
556 "Should extract all 3 entities"
557 );
558
559 lane.run(&world, &mut render_world);
561 assert_eq!(
562 render_world.meshes.len(),
563 3,
564 "Should consistently extract 3 entities"
565 );
566 }
567}