1use super::{ExtractedLight, ExtractedMesh, ExtractedView, RenderWorld};
18use khora_core::{
19 math::Vec3,
20 renderer::{api::scene::GpuMesh, light::LightType},
21};
22use khora_data::ecs::{Camera, 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 = world
56 .query::<&MaterialComponent>()
57 .nth(entity_id)
58 .map(|material_comp| material_comp.handle.clone());
59
60 let extracted_mesh = ExtractedMesh {
61 transform: transform.0,
63 cpu_mesh_uuid: gpu_mesh_handle_comp.uuid,
64 gpu_mesh: gpu_mesh_handle_comp.handle.clone(),
66 material,
67 };
68 render_world.meshes.push(extracted_mesh);
69 }
70
71 self.extract_lights(world, render_world);
73
74 self.extract_views(world, render_world);
76 }
77
78 fn extract_views(&self, world: &World, render_world: &mut RenderWorld) {
80 let camera_query = world.query::<(&Camera, &GlobalTransform)>();
82 let cameras: Vec<_> = camera_query.collect();
83 log::debug!("ExtractViews: Found {} cameras", cameras.len());
84
85 for (camera, global_transform) in cameras {
86 log::trace!(
87 "ExtractViews: Checking camera is_active={}",
88 camera.is_active
89 );
90 if !camera.is_active {
91 continue;
92 }
93
94 let position = global_transform.0.translation();
99 let rotation = global_transform.0.rotation();
100
101 let rotation_matrix = khora_core::math::Mat4::from_quat(rotation.inverse());
103 let translation_matrix = khora_core::math::Mat4::from_translation(-position);
104 let view_matrix = rotation_matrix * translation_matrix;
105
106 let proj_matrix = camera.projection_matrix();
108
109 let view_proj = proj_matrix * view_matrix;
111
112 let forward = global_transform.0.forward();
113 log::trace!(
114 "ExtractViews: Camera at pos={:?} forward={:?}",
115 position,
116 forward
117 );
118
119 let extracted_view = ExtractedView {
120 view_proj,
121 position,
122 };
123
124 render_world.views.push(extracted_view);
125 }
126 }
127
128 fn extract_lights(&self, world: &World, render_world: &mut RenderWorld) {
130 let light_query = world.query::<(&Light, &GlobalTransform)>();
132
133 for (light_comp, global_transform) in light_query {
134 if !light_comp.enabled {
136 continue;
137 }
138
139 let position = global_transform.0.translation();
141
142 let direction = match &light_comp.light_type {
147 LightType::Directional(dir_light) => {
148 let rotation = global_transform.0.rotation();
149 rotation * dir_light.direction
150 }
151 LightType::Spot(spot_light) => {
152 let rotation = global_transform.0.rotation();
154 rotation * spot_light.direction
155 }
156 LightType::Point(_) => Vec3::ZERO, };
158
159 let extracted = ExtractedLight {
160 light_type: light_comp.light_type,
161 position,
162 direction,
163 shadow_view_proj: khora_core::math::Mat4::IDENTITY,
164 shadow_atlas_index: None,
165 };
166
167 render_world.lights.push(extracted);
168 }
169 }
170}
171
172impl khora_core::lane::Lane for ExtractRenderablesLane {
173 fn strategy_name(&self) -> &'static str {
174 "ExtractRenderables"
175 }
176
177 fn lane_kind(&self) -> khora_core::lane::LaneKind {
178 khora_core::lane::LaneKind::Render
179 }
180
181 fn execute(
182 &self,
183 ctx: &mut khora_core::lane::LaneContext,
184 ) -> Result<(), khora_core::lane::LaneError> {
185 use khora_core::lane::{LaneError, Slot};
186
187 let world = ctx
188 .get::<Slot<World>>()
189 .ok_or(LaneError::missing("Slot<World>"))?
190 .get_ref();
191 let render_world = ctx
192 .get::<Slot<super::RenderWorld>>()
193 .ok_or(LaneError::missing("Slot<RenderWorld>"))?
194 .get();
195
196 self.run(world, render_world);
197 Ok(())
198 }
199
200 fn as_any(&self) -> &dyn std::any::Any {
201 self
202 }
203
204 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
205 self
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use khora_core::{
213 asset::{AssetHandle, AssetUUID},
214 math::{affine_transform::AffineTransform, Vec3},
215 renderer::api::scene::GpuMesh,
216 };
217
218 fn create_dummy_gpu_mesh() -> GpuMesh {
220 use khora_core::renderer::api::{
221 pipeline::enums::PrimitiveTopology, resource::BufferId, util::IndexFormat,
222 };
223 GpuMesh {
224 vertex_buffer: BufferId(0),
225 index_buffer: BufferId(0),
226 index_count: 0,
227 index_format: IndexFormat::Uint32,
228 primitive_topology: PrimitiveTopology::TriangleList,
229 }
230 }
231
232 #[derive(Clone)]
234 struct DummyMaterial;
235
236 impl khora_core::asset::Material for DummyMaterial {}
237 impl khora_core::asset::Asset for DummyMaterial {}
238
239 #[test]
240 fn test_extract_lane_creation() {
241 let lane = ExtractRenderablesLane::new();
242 let _ = lane;
244 }
245
246 #[test]
247 fn test_extract_lane_default() {
248 let _lane = ExtractRenderablesLane;
249 }
250
251 #[test]
252 fn test_extract_empty_world() {
253 let lane = ExtractRenderablesLane::new();
254 let world = World::new();
255 let mut render_world = RenderWorld::default();
256
257 lane.run(&world, &mut render_world);
258
259 assert_eq!(
260 render_world.meshes.len(),
261 0,
262 "Empty world should extract no meshes"
263 );
264 }
265
266 #[test]
267 fn test_extract_single_entity_without_material() {
268 let lane = ExtractRenderablesLane::new();
269 let mut world = World::new();
270 let mut render_world = RenderWorld::default();
271
272 let transform =
274 GlobalTransform(AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0)));
275
276 let mesh_uuid = AssetUUID::new();
278 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
279 handle: AssetHandle::new(create_dummy_gpu_mesh()),
280 uuid: mesh_uuid,
281 };
282
283 world.spawn((transform, gpu_mesh_handle));
285
286 lane.run(&world, &mut render_world);
287
288 assert_eq!(render_world.meshes.len(), 1, "Should extract 1 mesh");
289
290 let extracted = &render_world.meshes[0];
291 assert_eq!(extracted.cpu_mesh_uuid, mesh_uuid);
292 assert!(extracted.material.is_none());
293 assert_eq!(extracted.transform.translation(), Vec3::new(1.0, 2.0, 3.0));
294 }
295
296 #[test]
297 fn test_extract_single_entity_with_material() {
298 let lane = ExtractRenderablesLane::new();
299 let mut world = World::new();
300 let mut render_world = RenderWorld::default();
301
302 let transform = GlobalTransform(AffineTransform::from_translation(Vec3::new(
304 5.0, 10.0, 15.0,
305 )));
306 let mesh_uuid = AssetUUID::new();
307 let material_uuid = AssetUUID::new();
308
309 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
310 handle: AssetHandle::new(create_dummy_gpu_mesh()),
311 uuid: mesh_uuid,
312 };
313
314 let dummy_material: Box<dyn khora_core::asset::Material> = Box::new(DummyMaterial);
316 let material = MaterialComponent {
317 handle: AssetHandle::new(dummy_material),
318 uuid: material_uuid,
319 };
320
321 world.spawn((transform, gpu_mesh_handle, material));
323
324 lane.run(&world, &mut render_world);
325
326 assert_eq!(render_world.meshes.len(), 1, "Should extract 1 mesh");
327
328 let extracted = &render_world.meshes[0];
329 assert_eq!(extracted.cpu_mesh_uuid, mesh_uuid);
330 assert!(extracted.material.is_some());
331 assert_eq!(
332 extracted.transform.translation(),
333 Vec3::new(5.0, 10.0, 15.0)
334 );
335 }
336
337 #[test]
338 fn test_extract_multiple_entities() {
339 let lane = ExtractRenderablesLane::new();
340 let mut world = World::new();
341 let mut render_world = RenderWorld::default();
342
343 let mut with_material_count = 0;
344 let mut without_material_count = 0;
345
346 for i in 0..5 {
348 let transform = GlobalTransform(AffineTransform::from_translation(Vec3::new(
349 i as f32,
350 i as f32 * 2.0,
351 i as f32 * 3.0,
352 )));
353 let mesh_uuid = AssetUUID::new();
354 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
355 handle: AssetHandle::new(create_dummy_gpu_mesh()),
356 uuid: mesh_uuid,
357 };
358
359 if i % 2 == 0 {
361 let dummy_material: Box<dyn khora_core::asset::Material> = Box::new(DummyMaterial);
362 let material = MaterialComponent {
363 handle: AssetHandle::new(dummy_material),
364 uuid: AssetUUID::new(),
365 };
366 world.spawn((transform, gpu_mesh_handle, material));
367 with_material_count += 1;
368 } else {
369 world.spawn((transform, gpu_mesh_handle));
370 without_material_count += 1;
371 }
372 }
373
374 lane.run(&world, &mut render_world);
375
376 assert_eq!(render_world.meshes.len(), 5, "Should extract 5 meshes");
377
378 let extracted_with_material = render_world
380 .meshes
381 .iter()
382 .filter(|m| m.material.is_some())
383 .count();
384 let extracted_without_material = render_world
385 .meshes
386 .iter()
387 .filter(|m| m.material.is_none())
388 .count();
389
390 assert_eq!(
391 extracted_with_material, with_material_count,
392 "Should have {} entities with materials",
393 with_material_count
394 );
395 assert_eq!(
396 extracted_without_material, without_material_count,
397 "Should have {} entities without materials",
398 without_material_count
399 );
400 }
401
402 #[test]
403 fn test_extract_with_different_transforms() {
404 let lane = ExtractRenderablesLane::new();
405 let mut world = World::new();
406 let mut render_world = RenderWorld::default();
407
408 let transform1 = GlobalTransform(AffineTransform::from_translation(Vec3::new(
410 10.0, 20.0, 30.0,
411 )));
412 world.spawn((
413 transform1,
414 HandleComponent::<GpuMesh> {
415 handle: AssetHandle::new(create_dummy_gpu_mesh()),
416 uuid: AssetUUID::new(),
417 },
418 ));
419
420 use khora_core::math::Quaternion;
422 let transform2 = GlobalTransform(AffineTransform::from_quat(Quaternion::from_axis_angle(
423 Vec3::Y,
424 std::f32::consts::PI / 2.0,
425 )));
426 world.spawn((
427 transform2,
428 HandleComponent::<GpuMesh> {
429 handle: AssetHandle::new(create_dummy_gpu_mesh()),
430 uuid: AssetUUID::new(),
431 },
432 ));
433
434 let transform3 = GlobalTransform(AffineTransform::from_scale(Vec3::new(2.0, 3.0, 4.0)));
436 world.spawn((
437 transform3,
438 HandleComponent::<GpuMesh> {
439 handle: AssetHandle::new(create_dummy_gpu_mesh()),
440 uuid: AssetUUID::new(),
441 },
442 ));
443
444 lane.run(&world, &mut render_world);
445
446 assert_eq!(render_world.meshes.len(), 3, "Should extract 3 meshes");
447
448 assert_eq!(
450 render_world.meshes[0].transform.translation(),
451 Vec3::new(10.0, 20.0, 30.0)
452 );
453
454 let mat0 = render_world.meshes[0].transform.to_matrix();
456 let mat1 = render_world.meshes[1].transform.to_matrix();
457 let mat2 = render_world.meshes[2].transform.to_matrix();
458 assert_ne!(mat0, mat1);
459 assert_ne!(mat1, mat2);
460 }
461
462 #[test]
463 fn test_extract_clears_previous_data() {
464 let lane = ExtractRenderablesLane::new();
465 let mut world = World::new();
466 let mut render_world = RenderWorld::default();
467
468 for _ in 0..3 {
470 let transform = GlobalTransform(AffineTransform::IDENTITY);
471 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
472 handle: AssetHandle::new(create_dummy_gpu_mesh()),
473 uuid: AssetUUID::new(),
474 };
475 world.spawn((transform, gpu_mesh_handle));
476 }
477
478 lane.run(&world, &mut render_world);
479 assert_eq!(
480 render_world.meshes.len(),
481 3,
482 "First run should extract 3 meshes"
483 );
484
485 let mut world2 = World::new();
487 let transform = GlobalTransform(AffineTransform::IDENTITY);
488 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
489 handle: AssetHandle::new(create_dummy_gpu_mesh()),
490 uuid: AssetUUID::new(),
491 };
492 world2.spawn((transform, gpu_mesh_handle));
493
494 lane.run(&world2, &mut render_world);
496 assert_eq!(
497 render_world.meshes.len(),
498 1,
499 "Second run should extract only 1 mesh (cleared previous)"
500 );
501 }
502
503 #[test]
504 fn test_extract_entities_without_mesh_component() {
505 let lane = ExtractRenderablesLane::new();
506 let mut world = World::new();
507 let mut render_world = RenderWorld::default();
508
509 use khora_data::ecs::Transform;
512 for _ in 0..3 {
513 let transform = GlobalTransform(AffineTransform::IDENTITY);
514 world.spawn((transform, Transform::default()));
516 }
517
518 let transform = GlobalTransform(AffineTransform::IDENTITY);
520 let gpu_mesh_handle = HandleComponent::<GpuMesh> {
521 handle: AssetHandle::new(create_dummy_gpu_mesh()),
522 uuid: AssetUUID::new(),
523 };
524 world.spawn((transform, gpu_mesh_handle));
525
526 lane.run(&world, &mut render_world);
527
528 assert_eq!(
530 render_world.meshes.len(),
531 1,
532 "Should only extract entities with both components"
533 );
534 }
535
536 #[test]
537 fn test_extract_preserves_mesh_uuids() {
538 let lane = ExtractRenderablesLane::new();
539 let mut world = World::new();
540 let mut render_world = RenderWorld::default();
541
542 let uuid1 = AssetUUID::new();
544 let uuid2 = AssetUUID::new();
545 let uuid3 = AssetUUID::new();
546
547 let transform = GlobalTransform(AffineTransform::IDENTITY);
548
549 world.spawn((
550 transform,
551 HandleComponent::<GpuMesh> {
552 handle: AssetHandle::new(create_dummy_gpu_mesh()),
553 uuid: uuid1,
554 },
555 ));
556 world.spawn((
557 transform,
558 HandleComponent::<GpuMesh> {
559 handle: AssetHandle::new(create_dummy_gpu_mesh()),
560 uuid: uuid2,
561 },
562 ));
563 world.spawn((
564 transform,
565 HandleComponent::<GpuMesh> {
566 handle: AssetHandle::new(create_dummy_gpu_mesh()),
567 uuid: uuid3,
568 },
569 ));
570
571 lane.run(&world, &mut render_world);
572
573 assert_eq!(render_world.meshes.len(), 3);
574
575 let extracted_uuids: Vec<AssetUUID> = render_world
577 .meshes
578 .iter()
579 .map(|m| m.cpu_mesh_uuid)
580 .collect();
581
582 assert!(extracted_uuids.contains(&uuid1), "Should contain uuid1");
583 assert!(extracted_uuids.contains(&uuid2), "Should contain uuid2");
584 assert!(extracted_uuids.contains(&uuid3), "Should contain uuid3");
585 }
586
587 #[test]
588 fn test_extract_with_identity_transform() {
589 let lane = ExtractRenderablesLane::new();
590 let mut world = World::new();
591 let mut render_world = RenderWorld::default();
592
593 let transform = GlobalTransform(AffineTransform::IDENTITY);
594 let mesh_uuid = AssetUUID::new();
595 world.spawn((
596 transform,
597 HandleComponent::<GpuMesh> {
598 handle: AssetHandle::new(create_dummy_gpu_mesh()),
599 uuid: mesh_uuid,
600 },
601 ));
602
603 lane.run(&world, &mut render_world);
604
605 assert_eq!(render_world.meshes.len(), 1);
606
607 let extracted = &render_world.meshes[0];
608 use khora_core::math::Mat4;
609 assert_eq!(extracted.transform.to_matrix(), Mat4::IDENTITY);
610 }
611
612 #[test]
613 fn test_extract_multiple_runs() {
614 let lane = ExtractRenderablesLane::new();
615 let mut world = World::new();
616 let mut render_world = RenderWorld::default();
617
618 let transform = GlobalTransform(AffineTransform::IDENTITY);
620 world.spawn((
621 transform,
622 HandleComponent::<GpuMesh> {
623 handle: AssetHandle::new(create_dummy_gpu_mesh()),
624 uuid: AssetUUID::new(),
625 },
626 ));
627
628 lane.run(&world, &mut render_world);
630 assert_eq!(render_world.meshes.len(), 1);
631
632 world.spawn((
634 transform,
635 HandleComponent::<GpuMesh> {
636 handle: AssetHandle::new(create_dummy_gpu_mesh()),
637 uuid: AssetUUID::new(),
638 },
639 ));
640 world.spawn((
641 transform,
642 HandleComponent::<GpuMesh> {
643 handle: AssetHandle::new(create_dummy_gpu_mesh()),
644 uuid: AssetUUID::new(),
645 },
646 ));
647
648 lane.run(&world, &mut render_world);
650 assert_eq!(
651 render_world.meshes.len(),
652 3,
653 "Should extract all 3 entities"
654 );
655
656 lane.run(&world, &mut render_world);
658 assert_eq!(
659 render_world.meshes.len(),
660 3,
661 "Should consistently extract 3 entities"
662 );
663 }
664}