1use crate::ecs::material::components::MaterialRef;
2use crate::ecs::material::resources::{material_registry_insert, material_registry_remove_unused};
3use crate::ecs::world::{Entity, NAME, World};
4#[cfg(feature = "physics")]
5use crate::plugins::physics::resources::{
6 physics_world_add_collider, physics_world_add_rigid_body,
7};
8use crate::render::generational_registry::registry_entry_by_name;
9use crate::render::mesh_cache::{mesh_cache_insert, mesh_cache_remove_unused};
10use crate::render::procedural_meshes::{
11 create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
12 create_sphere_mesh, create_subdivided_plane_mesh, create_torus_mesh,
13};
14use crate::render::wgpu::texture_cache::{
15 TextureOwner, texture_cache_acquire, texture_cache_release_entity, texture_cache_remove_unused,
16};
17
18use crate::prelude::*;
19#[derive(Debug, Clone)]
20pub enum EcsCommand {
21 DespawnRecursive {
22 entity: Entity,
23 },
24 ReloadMaterial {
25 name: String,
26 material: Box<crate::render::material::Material>,
27 },
28}
29
30#[derive(Debug, Clone)]
31pub enum RenderCommand {
32 UploadUiImageLayer {
33 layer: u32,
34 rgba_data: Vec<u8>,
35 width: u32,
36 height: u32,
37 },
38 LoadHdrSkybox {
39 hdr_data: Vec<u8>,
40 },
41 LoadHdrSkyboxFromPath {
42 path: std::path::PathBuf,
43 },
44 CaptureScreenshot {
45 path: Option<std::path::PathBuf>,
46 max_dimension: Option<u32>,
50 },
51 ReloadTexture {
52 name: String,
53 rgba_data: Vec<u8>,
54 width: u32,
55 height: u32,
56 },
57 SetColorLut {
62 data: Vec<u8>,
63 },
64}
65
66#[derive(Default)]
67pub struct CommandQueues {
68 pub ecs: Vec<EcsCommand>,
69 pub render: Vec<RenderCommand>,
70}
71
72pub fn spawn_entities(world: &mut World, core_mask: u64, count: usize) -> Vec<Entity> {
74 world.ecs.spawn_entities(CORE, core_mask, count)
75}
76
77pub fn spawn_entities_ui(world: &mut World, ui_mask: u64, count: usize) -> Vec<Entity> {
79 world.ecs.spawn_entities(UI, ui_mask, count)
80}
81
82pub fn queue_ecs_command(world: &mut World, command: EcsCommand) {
84 world
85 .res_mut::<crate::ecs::world::commands::CommandQueues>()
86 .ecs
87 .push(command);
88}
89
90pub fn queue_render_command(world: &mut World, command: RenderCommand) {
92 world
93 .res_mut::<crate::ecs::world::commands::CommandQueues>()
94 .render
95 .push(command);
96}
97
98pub fn set_cursor_locked(world: &mut World, locked: bool) {
100 if let Some(window_handle) = &world.res::<crate::ecs::window::resources::Window>().handle {
101 if locked {
102 if window_handle
103 .set_cursor_grab(winit::window::CursorGrabMode::Locked)
104 .is_err()
105 {
106 let _ = window_handle.set_cursor_grab(winit::window::CursorGrabMode::Confined);
107 }
108 } else {
109 let _ = window_handle.set_cursor_grab(winit::window::CursorGrabMode::None);
110 }
111 }
112 #[cfg(not(target_arch = "wasm32"))]
113 {
114 world
115 .res_mut::<crate::ecs::window::resources::Window>()
116 .cursor_locked = locked;
117 }
118}
119
120#[cfg(target_arch = "wasm32")]
126pub fn sync_cursor_lock_state(world: &mut World) {
127 let locked = web_sys::window()
128 .and_then(|window| window.document())
129 .and_then(|document| document.pointer_lock_element())
130 .is_some();
131 world
132 .res_mut::<crate::ecs::window::resources::Window>()
133 .cursor_locked = locked;
134}
135
136pub fn set_cursor_visible(world: &mut World, visible: bool) {
138 if let Some(window_handle) = &world.res::<crate::ecs::window::resources::Window>().handle {
139 window_handle.set_cursor_visible(visible);
140 }
141}
142
143pub fn set_time_scale(world: &mut World, scale: f32) {
148 world.res_mut::<crate::ecs::time::Time>().time_speed = scale.max(0.0);
149}
150
151pub fn time_scale(world: &World) -> f32 {
153 world.res::<crate::ecs::time::Time>().time_speed
154}
155
156pub fn pause(world: &mut World) {
159 world.res_mut::<crate::ecs::time::Time>().paused = true;
160}
161
162pub fn unpause(world: &mut World) {
164 world.res_mut::<crate::ecs::time::Time>().paused = false;
165}
166
167pub fn set_paused(world: &mut World, paused: bool) {
169 world.res_mut::<crate::ecs::time::Time>().paused = paused;
170}
171
172pub fn is_paused(world: &World) -> bool {
174 world.res::<crate::ecs::time::Time>().paused
175}
176
177pub fn register_material(
179 world: &mut World,
180 entity: Entity,
181 name: String,
182 material: crate::render::material::Material,
183) {
184 material_registry_insert(
185 &mut world
186 .res_mut::<crate::ecs::asset_state::AssetState>()
187 .material_registry,
188 name.clone(),
189 material,
190 );
191 if let Some(&index) = world
192 .res::<crate::ecs::asset_state::AssetState>()
193 .material_registry
194 .registry
195 .name_to_index
196 .get(&name)
197 {
198 registry_add_reference(
199 &mut world
200 .res_mut::<crate::ecs::asset_state::AssetState>()
201 .material_registry
202 .registry,
203 index,
204 );
205 }
206 world.set(entity, MaterialRef::new(name));
207 world
208 .res_mut::<crate::render::mesh_state::MeshRenderState>()
209 .mark_entity_added(render_entity(entity));
210}
211
212#[cfg(feature = "physics")]
213pub fn spawn_physics_body(world: &mut World, entity: Entity) {
214 let rigid_body_comp = world
215 .get::<crate::plugins::physics::components::RigidBodyComponent>(entity)
216 .cloned();
217 let collider_comp = world
218 .get::<crate::plugins::physics::components::ColliderComponent>(entity)
219 .cloned();
220
221 if let Some(rigid_body_comp) = rigid_body_comp {
222 let rapier_body = rigid_body_comp.to_rapier_rigid_body();
223 let rapier_handle =
224 physics_world_add_rigid_body(world.plugin_resource_mut::<PhysicsWorld>(), rapier_body);
225
226 if let Some(collider_comp) = collider_comp {
227 let rapier_collider = collider_comp.to_rapier_collider();
228 physics_world_add_collider(
229 world.plugin_resource_mut::<PhysicsWorld>(),
230 rapier_collider,
231 rapier_handle,
232 );
233 }
234
235 if let Some(rigid_body_mut) =
236 world.get_mut::<crate::plugins::physics::components::RigidBodyComponent>(entity)
237 {
238 rigid_body_mut.handle =
239 Some(crate::plugins::physics::types::rigid_body_handle_from_rapier(rapier_handle));
240 }
241
242 world
243 .plugin_resource_mut::<PhysicsWorld>()
244 .handle_to_entity
245 .insert(rapier_handle, entity);
246 }
247}
248
249pub fn process_commands_system(world: &mut World) {
250 let commands = std::mem::take(
251 &mut world
252 .res_mut::<crate::ecs::world::commands::CommandQueues>()
253 .ecs,
254 );
255 let _span = tracing::info_span!("ecs_commands", count = commands.len()).entered();
256 for command in commands {
257 match command {
258 EcsCommand::DespawnRecursive { entity } => {
259 despawn_recursive_immediate(world, entity);
260 }
261 EcsCommand::ReloadMaterial { name, material } => {
262 reload_material_command(world, name, *material);
263 }
264 }
265 }
266}
267
268fn reload_material_command(
269 world: &mut World,
270 name: String,
271 new_material: crate::render::material::Material,
272) {
273 use crate::ecs::world::MATERIAL_REF;
274
275 let new_textures: Vec<String> = new_material.texture_names().map(str::to_string).collect();
276
277 crate::ecs::material::resources::material_registry_mutate(
278 &mut world
279 .res_mut::<crate::ecs::asset_state::AssetState>()
280 .material_registry,
281 &name,
282 |existing| *existing = new_material,
283 );
284
285 let referencing_entities: Vec<Entity> = world.ecs.worlds[CORE]
286 .query_entities(MATERIAL_REF)
287 .filter(|&entity| {
288 world
289 .get::<crate::ecs::material::components::MaterialRef>(entity)
290 .is_some_and(|mat_ref| mat_ref.name == name)
291 })
292 .collect();
293
294 for entity in referencing_entities {
295 texture_cache_acquire(
296 world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(),
297 TextureOwner::EntityMaterial(render_entity(entity)),
298 new_textures.clone(),
299 );
300 world
301 .res_mut::<crate::render::mesh_state::MeshRenderState>()
302 .mark_material_dirty(render_entity(entity));
303 }
304}
305
306pub fn despawn_entities_with_cache_cleanup(world: &mut World, entities: &[Entity]) {
309 for &entity in entities {
310 world
311 .res_mut::<crate::ecs::entity_registry::EntityRegistry>()
312 .tags
313 .remove(&entity);
314 if let Some(guid) = world.get::<crate::ecs::primitives::Guid>(entity).copied() {
315 world
316 .res_mut::<crate::ecs::entity_registry::EntityRegistry>()
317 .guid_index
318 .remove(&guid.0);
319 }
320
321 if let Some(render_mesh) = world.get::<crate::ecs::mesh::components::RenderMesh>(entity) {
322 let mesh_name = render_mesh.name.clone();
323 if let Some(&index) = world
324 .res::<crate::ecs::asset_state::AssetState>()
325 .mesh_cache
326 .registry
327 .name_to_index
328 .get(&mesh_name)
329 {
330 registry_remove_reference(
331 &mut world
332 .res_mut::<crate::ecs::asset_state::AssetState>()
333 .mesh_cache
334 .registry,
335 index,
336 );
337 }
338 world
339 .res_mut::<crate::render::mesh_state::MeshRenderState>()
340 .mark_entity_removed(render_entity(entity));
341 }
342
343 if let Some(instanced_mesh) =
344 world.get::<crate::ecs::mesh::components::InstancedMesh>(entity)
345 {
346 let mesh_name = instanced_mesh.mesh_name.clone();
347 if let Some(&index) = world
348 .res::<crate::ecs::asset_state::AssetState>()
349 .mesh_cache
350 .registry
351 .name_to_index
352 .get(&mesh_name)
353 {
354 registry_remove_reference(
355 &mut world
356 .res_mut::<crate::ecs::asset_state::AssetState>()
357 .mesh_cache
358 .registry,
359 index,
360 );
361 }
362 world
363 .res_mut::<crate::render::mesh_state::MeshRenderState>()
364 .mark_instanced_meshes_changed();
365 }
366
367 texture_cache_release_entity(
368 world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(),
369 render_entity(entity),
370 );
371
372 if let Some(material_ref) =
373 world.get::<crate::ecs::material::components::MaterialRef>(entity)
374 {
375 let material_name = material_ref.name.clone();
376 if let Some(&index) = world
377 .res::<crate::ecs::asset_state::AssetState>()
378 .material_registry
379 .registry
380 .name_to_index
381 .get(&material_name)
382 {
383 registry_remove_reference(
384 &mut world
385 .res_mut::<crate::ecs::asset_state::AssetState>()
386 .material_registry
387 .registry,
388 index,
389 );
390 }
391 }
392 }
393
394 world.despawn_entities(entities);
395}
396
397pub fn despawn_recursive_immediate(world: &mut World, entity: Entity) {
399 crate::ecs::transform::systems::sync_hierarchy_index(world);
400 let mut targets = world
401 .res::<freecs::dynamic::HierarchyIndex>()
402 .descendants(entity);
403 targets.insert(0, entity);
404 despawn_entities_with_cache_cleanup(world, &targets);
405}
406
407pub(crate) fn setup_entity_transforms(
411 world: &mut World,
412 entity: Entity,
413 local_transform: crate::ecs::world::components::LocalTransform,
414) {
415 world.set(entity, local_transform);
416 world.set(
417 entity,
418 crate::ecs::world::components::GlobalTransform::default(),
419 );
420}
421
422pub fn spawn_sun(world: &mut World) -> Entity {
423 use crate::ecs::world::components;
424
425 world.ecs.spawn_with((
426 components::Name("Sun".to_string()),
427 components::LocalTransform {
428 translation: nalgebra_glm::Vec3::new(5.0, 10.0, 5.0),
429 rotation: nalgebra_glm::quat_angle_axis(
430 std::f32::consts::FRAC_PI_4,
431 &nalgebra_glm::Vec3::new(0.0, 1.0, 0.0),
432 ) * nalgebra_glm::quat_angle_axis(
433 -std::f32::consts::FRAC_PI_6,
434 &nalgebra_glm::Vec3::new(1.0, 0.0, 0.0),
435 ),
436 scale: nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
437 },
438 components::GlobalTransform::default(),
439 components::Light {
440 light_type: components::LightType::Directional,
441 color: nalgebra_glm::Vec3::new(1.0, 0.95, 0.8),
442 intensity: 5.0,
443 range: 100.0,
444 inner_cone_angle: std::f32::consts::PI / 6.0,
445 outer_cone_angle: std::f32::consts::PI / 4.0,
446 cast_shadows: true,
447 shadow_bias: 0.0005,
448 shadow_resolution: 0,
449 shadow_distance: 0.0,
450 cookie_texture: None,
451 ..Default::default()
452 },
453 ))
454}
455
456pub fn spawn_sun_without_shadows(world: &mut World) -> Entity {
458 use crate::ecs::world::components;
459
460 world.ecs.spawn_with((
461 components::Name("Sun".to_string()),
462 components::LocalTransform {
463 translation: nalgebra_glm::Vec3::new(5.0, 10.0, 5.0),
464 rotation: nalgebra_glm::quat_angle_axis(
465 -std::f32::consts::FRAC_PI_4,
466 &nalgebra_glm::Vec3::new(1.0, 0.0, 0.0),
467 ),
468 scale: nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
469 },
470 components::GlobalTransform::default(),
471 components::Light {
472 light_type: components::LightType::Directional,
473 color: nalgebra_glm::Vec3::new(1.0, 0.95, 0.8),
474 intensity: 5.0,
475 range: 100.0,
476 inner_cone_angle: std::f32::consts::PI / 6.0,
477 outer_cone_angle: std::f32::consts::PI / 4.0,
478 cast_shadows: false,
479 shadow_bias: 0.0,
480 shadow_resolution: 0,
481 shadow_distance: 0.0,
482 cookie_texture: None,
483 ..Default::default()
484 },
485 ))
486}
487
488pub fn spawn_light_entity(world: &mut World, position: nalgebra_glm::Vec3, name: &str) -> Entity {
490 use crate::ecs::world::components;
491 use crate::ecs::world::{GLOBAL_TRANSFORM, LIGHT, LOCAL_TRANSFORM, NAME};
492
493 let entity = spawn_entities(world, NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LIGHT, 1)[0];
494 world.set(entity, components::Name(name.to_string()));
495 setup_entity_transforms(
496 world,
497 entity,
498 components::LocalTransform {
499 translation: position,
500 rotation: nalgebra_glm::quat_identity(),
501 scale: nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
502 },
503 );
504 entity
505}
506
507pub fn spawn_mesh_at(
509 world: &mut World,
510 mesh_name: &str,
511 position: nalgebra_glm::Vec3,
512 scale: nalgebra_glm::Vec3,
513) -> Entity {
514 use crate::ecs::world::components;
515
516 let render_mesh = components::RenderMesh::new(mesh_name);
517
518 if !world
519 .res::<crate::ecs::asset_state::AssetState>()
520 .mesh_cache
521 .registry
522 .name_to_index
523 .contains_key(&render_mesh.name)
524 {
525 let mesh = match mesh_name {
526 "Cube" => Some(create_cube_mesh()),
527 "Sphere" => Some(create_sphere_mesh(1.0, 16)),
528 "Plane" => Some(create_plane_mesh(2.0)),
529 "SubdividedPlane" => Some(create_subdivided_plane_mesh(2.0, 20)),
530 "Torus" => Some(create_torus_mesh(1.0, 0.3, 32, 16)),
531 "Cylinder" => Some(create_cylinder_mesh(0.5, 1.0, 16)),
532 "Cone" => Some(create_cone_mesh(0.5, 1.0, 16)),
533 _ => None,
534 };
535 if let Some(mesh) = mesh {
536 mesh_cache_insert(
537 &mut world
538 .res_mut::<crate::ecs::asset_state::AssetState>()
539 .mesh_cache,
540 mesh_name.to_string(),
541 mesh,
542 );
543 }
544 }
545
546 if let Some(&index) = world
547 .res::<crate::ecs::asset_state::AssetState>()
548 .mesh_cache
549 .registry
550 .name_to_index
551 .get(&render_mesh.name)
552 {
553 registry_add_reference(
554 &mut world
555 .res_mut::<crate::ecs::asset_state::AssetState>()
556 .mesh_cache
557 .registry,
558 index,
559 );
560 }
561
562 if let Some(&index) = world
563 .res::<crate::ecs::asset_state::AssetState>()
564 .material_registry
565 .registry
566 .name_to_index
567 .get("Default")
568 {
569 registry_add_reference(
570 &mut world
571 .res_mut::<crate::ecs::asset_state::AssetState>()
572 .material_registry
573 .registry,
574 index,
575 );
576 }
577
578 let entity = world.ecs.spawn_with((
579 components::Name(mesh_name.to_string()),
580 components::LocalTransform {
581 translation: position,
582 scale,
583 rotation: nalgebra_glm::Quat::identity(),
584 },
585 components::GlobalTransform::default(),
586 render_mesh,
587 components::MaterialRef::new("Default"),
588 components::BoundingVolume::from_mesh_type(mesh_name),
589 components::CastsShadow,
590 crate::ecs::primitives::Visibility { visible: true },
591 ));
592 world
593 .res_mut::<crate::render::mesh_state::MeshRenderState>()
594 .mark_entity_added(render_entity(entity));
595
596 entity
597}
598
599pub fn spawn_cube_at(world: &mut World, position: nalgebra_glm::Vec3) -> Entity {
601 spawn_mesh_at(
602 world,
603 "Cube",
604 position,
605 nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
606 )
607}
608
609pub fn spawn_sphere_at(world: &mut World, position: nalgebra_glm::Vec3) -> Entity {
611 spawn_mesh_at(
612 world,
613 "Sphere",
614 position,
615 nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
616 )
617}
618
619pub fn spawn_cylinder_at(world: &mut World, position: nalgebra_glm::Vec3) -> Entity {
621 spawn_mesh_at(
622 world,
623 "Cylinder",
624 position,
625 nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
626 )
627}
628
629pub fn spawn_torus_at(world: &mut World, position: nalgebra_glm::Vec3) -> Entity {
631 spawn_mesh_at(
632 world,
633 "Torus",
634 position,
635 nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
636 )
637}
638
639pub fn spawn_cone_at(world: &mut World, position: nalgebra_glm::Vec3) -> Entity {
641 spawn_mesh_at(
642 world,
643 "Cone",
644 position,
645 nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
646 )
647}
648
649pub fn spawn_plane_at(world: &mut World, position: nalgebra_glm::Vec3) -> Entity {
651 spawn_mesh_at(
652 world,
653 "Plane",
654 position,
655 nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
656 )
657}
658
659pub fn spawn_3d_text_with_properties(
661 world: &mut World,
662 text: &str,
663 position: nalgebra_glm::Vec3,
664 properties: crate::ecs::text::components::TextProperties,
665) -> Entity {
666 spawn_3d_text_impl(world, text, position, properties, false)
667}
668
669pub fn spawn_3d_billboard_text_with_properties(
671 world: &mut World,
672 text: &str,
673 position: nalgebra_glm::Vec3,
674 properties: crate::ecs::text::components::TextProperties,
675) -> Entity {
676 spawn_3d_text_impl(world, text, position, properties, true)
677}
678
679fn spawn_3d_text_impl(
680 world: &mut World,
681 text: &str,
682 position: nalgebra_glm::Vec3,
683 properties: crate::ecs::text::components::TextProperties,
684 billboard: bool,
685) -> Entity {
686 use crate::ecs::world::components;
687 use crate::ecs::world::{GLOBAL_TRANSFORM, LOCAL_TRANSFORM, NAME, TEXT, VISIBILITY};
688
689 let text_index = world
690 .res_mut::<crate::ecs::text::resources::TextState>()
691 .cache
692 .add_text(text);
693
694 let entity = spawn_entities(
695 world,
696 NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | TEXT | VISIBILITY,
697 1,
698 )[0];
699
700 world.set(entity, components::Name(text.to_string()));
701 world.set(
702 entity,
703 components::LocalTransform {
704 translation: position,
705 rotation: nalgebra_glm::Quat::identity(),
706 scale: nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
707 },
708 );
709 world.set(entity, components::GlobalTransform::default());
710 world.set(
711 entity,
712 components::Text {
713 text_index,
714 properties,
715 dirty: true,
716 cached_mesh: None,
717 billboard,
718 },
719 );
720 world.set(entity, components::Visibility { visible: true });
721
722 entity
723}
724
725const CLEANUP_INTERVAL_FRAMES: u64 = 300;
726
727pub fn cleanup_unused_resources_system(world: &mut World) {
728 let _span = tracing::info_span!("cleanup").entered();
729 world
730 .res_mut::<crate::ecs::world::cleanup::CleanupState>()
731 .frame_counter += 1;
732
733 if world
734 .res::<crate::ecs::world::cleanup::CleanupState>()
735 .frame_counter
736 >= CLEANUP_INTERVAL_FRAMES
737 {
738 world
739 .res_mut::<crate::ecs::world::cleanup::CleanupState>()
740 .frame_counter = 0;
741 mesh_cache_remove_unused(
742 &mut world
743 .res_mut::<crate::ecs::asset_state::AssetState>()
744 .mesh_cache,
745 );
746 material_registry_remove_unused(
747 &mut world
748 .res_mut::<crate::ecs::asset_state::AssetState>()
749 .material_registry,
750 );
751 texture_cache_remove_unused(
752 world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(),
753 );
754 }
755}
756
757pub fn set_material_with_textures(
758 world: &mut World,
759 entity: Entity,
760 material: crate::render::material::Material,
761) {
762 let material_ref = world
763 .get::<crate::ecs::material::components::MaterialRef>(entity)
764 .cloned();
765
766 let texture_names: Vec<String> = material.texture_names().map(str::to_string).collect();
767 texture_cache_acquire(
768 world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(),
769 TextureOwner::EntityMaterial(render_entity(entity)),
770 texture_names,
771 );
772
773 if let Some(ref mat_ref) = material_ref {
774 crate::ecs::material::resources::material_registry_mutate(
775 &mut world
776 .res_mut::<crate::ecs::asset_state::AssetState>()
777 .material_registry,
778 &mat_ref.name,
779 |existing_mat| *existing_mat = material,
780 );
781 world
782 .res_mut::<crate::render::mesh_state::MeshRenderState>()
783 .mark_material_dirty(render_entity(entity));
784 } else {
785 let material_name = format!("TexturedMaterial_{}", entity.id);
786 material_registry_insert(
787 &mut world
788 .res_mut::<crate::ecs::asset_state::AssetState>()
789 .material_registry,
790 material_name.clone(),
791 material,
792 );
793 if let Some(&index) = world
794 .res::<crate::ecs::asset_state::AssetState>()
795 .material_registry
796 .registry
797 .name_to_index
798 .get(&material_name)
799 {
800 registry_add_reference(
801 &mut world
802 .res_mut::<crate::ecs::asset_state::AssetState>()
803 .material_registry
804 .registry,
805 index,
806 );
807 }
808 world.set(
809 entity,
810 crate::ecs::material::components::MaterialRef::new(material_name),
811 );
812 }
813}
814
815pub fn load_procedural_textures(world: &mut World) {
816 let default_sampler = crate::render::texture_data::SamplerSettings::DEFAULT;
817 let color = crate::render::texture_data::TextureUsage::Color;
818
819 let checkerboard = generate_checkerboard_texture();
820 crate::ecs::loading::queue_decoded_texture(
821 world,
822 "checkerboard".to_string(),
823 checkerboard.0,
824 checkerboard.1,
825 checkerboard.2,
826 color,
827 default_sampler,
828 );
829
830 let gradient = generate_gradient_texture();
831 crate::ecs::loading::queue_decoded_texture(
832 world,
833 "gradient".to_string(),
834 gradient.0,
835 gradient.1,
836 gradient.2,
837 color,
838 default_sampler,
839 );
840
841 let uv_test = generate_uv_test_texture();
842 crate::ecs::loading::queue_decoded_texture(
843 world,
844 "uv_test".to_string(),
845 uv_test.0,
846 uv_test.1,
847 uv_test.2,
848 color,
849 default_sampler,
850 );
851
852 for name in ["checkerboard", "gradient", "uv_test"] {
853 crate::render::wgpu::texture_cache::texture_cache_protect(
854 world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(),
855 name.to_string(),
856 );
857 }
858}
859
860fn generate_checkerboard_texture() -> (Vec<u8>, u32, u32) {
861 let width = 256;
862 let height = 256;
863 let checker_size = 32;
864
865 let mut pixels = Vec::new();
866 for y in 0..height {
867 for x in 0..width {
868 let checker_x = (x / checker_size) % 2;
869 let checker_y = (y / checker_size) % 2;
870 let is_white = (checker_x + checker_y) % 2 == 0;
871
872 if is_white {
873 pixels.extend_from_slice(&[255, 255, 255, 255]);
874 } else {
875 pixels.extend_from_slice(&[64, 64, 64, 255]);
876 }
877 }
878 }
879
880 (pixels, width, height)
881}
882
883fn generate_gradient_texture() -> (Vec<u8>, u32, u32) {
884 let width = 256;
885 let height = 256;
886
887 let mut pixels = Vec::new();
888 for y in 0..height {
889 for x in 0..width {
890 let r = (x * 255 / width) as u8;
891 let g = (y * 255 / height) as u8;
892 let b = 128u8;
893 pixels.extend_from_slice(&[r, g, b, 255]);
894 }
895 }
896
897 (pixels, width, height)
898}
899
900fn generate_uv_test_texture() -> (Vec<u8>, u32, u32) {
901 let width = 256;
902 let height = 256;
903
904 let mut pixels = Vec::new();
905 for y in 0..height {
906 for x in 0..width {
907 let u = x as f32 / width as f32;
908 let v = y as f32 / height as f32;
909
910 let r = (u * 255.0) as u8;
911 let g = (v * 255.0) as u8;
912 let b = ((1.0 - u) * (1.0 - v) * 255.0) as u8;
913
914 pixels.extend_from_slice(&[r, g, b, 255]);
915 }
916 }
917
918 (pixels, width, height)
919}
920
921pub fn load_hdr_skybox(world: &mut World, hdr_data: Vec<u8>) {
922 queue_render_command(world, RenderCommand::LoadHdrSkybox { hdr_data });
923}
924
925pub fn load_hdr_skybox_from_path(world: &mut World, path: std::path::PathBuf) {
926 queue_render_command(world, RenderCommand::LoadHdrSkyboxFromPath { path });
927}
928
929#[cfg(not(target_arch = "wasm32"))]
930pub fn capture_screenshot(world: &mut World) {
931 queue_render_command(
932 world,
933 RenderCommand::CaptureScreenshot {
934 path: None,
935 max_dimension: None,
936 },
937 );
938}
939
940#[cfg(not(target_arch = "wasm32"))]
941pub fn capture_screenshot_to_path(world: &mut World, path: impl Into<std::path::PathBuf>) {
942 queue_render_command(
943 world,
944 RenderCommand::CaptureScreenshot {
945 path: Some(path.into()),
946 max_dimension: None,
947 },
948 );
949}
950
951#[cfg(not(target_arch = "wasm32"))]
952pub fn capture_thumbnail_to_path(
953 world: &mut World,
954 path: impl Into<std::path::PathBuf>,
955 max_dimension: u32,
956) {
957 queue_render_command(
958 world,
959 RenderCommand::CaptureScreenshot {
960 path: Some(path.into()),
961 max_dimension: Some(max_dimension),
962 },
963 );
964}
965
966pub fn spawn_instanced_mesh(
968 world: &mut World,
969 mesh_name: &str,
970 instances: Vec<crate::ecs::mesh::components::InstanceTransform>,
971) -> Entity {
972 use crate::ecs::world::components;
973 use crate::ecs::world::{
974 CASTS_SHADOW, GLOBAL_TRANSFORM, INSTANCED_MESH, LOCAL_TRANSFORM, MATERIAL_REF, NAME,
975 VISIBILITY,
976 };
977
978 let entity = spawn_entities(
979 world,
980 NAME | LOCAL_TRANSFORM
981 | GLOBAL_TRANSFORM
982 | INSTANCED_MESH
983 | MATERIAL_REF
984 | VISIBILITY
985 | CASTS_SHADOW,
986 1,
987 )[0];
988
989 world.set(entity, components::Name(format!("Instanced {}", mesh_name)));
990 world.set(
991 entity,
992 components::LocalTransform {
993 translation: nalgebra_glm::Vec3::new(0.0, 0.0, 0.0),
994 scale: nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
995 rotation: nalgebra_glm::Quat::identity(),
996 },
997 );
998 world.set(entity, components::GlobalTransform::default());
999
1000 world.set(
1001 entity,
1002 crate::ecs::mesh::components::InstancedMesh::with_instances(mesh_name, instances),
1003 );
1004
1005 if let Some(&index) = world
1006 .res::<crate::ecs::asset_state::AssetState>()
1007 .material_registry
1008 .registry
1009 .name_to_index
1010 .get("Default")
1011 {
1012 registry_add_reference(
1013 &mut world
1014 .res_mut::<crate::ecs::asset_state::AssetState>()
1015 .material_registry
1016 .registry,
1017 index,
1018 );
1019 }
1020 world.set(entity, components::MaterialRef::new("Default"));
1021
1022 world.set(entity, components::Visibility { visible: true });
1023 world.set(entity, components::CastsShadow);
1024
1025 entity
1026}
1027
1028pub fn spawn_instanced_mesh_with_material(
1030 world: &mut World,
1031 mesh_name: &str,
1032 instances: Vec<crate::ecs::mesh::components::InstanceTransform>,
1033 material_name: &str,
1034) -> Entity {
1035 use crate::ecs::world::components;
1036 use crate::ecs::world::{
1037 CASTS_SHADOW, GLOBAL_TRANSFORM, INSTANCED_MESH, LOCAL_TRANSFORM, MATERIAL_REF, NAME,
1038 VISIBILITY,
1039 };
1040
1041 let entity = spawn_entities(
1042 world,
1043 NAME | LOCAL_TRANSFORM
1044 | GLOBAL_TRANSFORM
1045 | INSTANCED_MESH
1046 | MATERIAL_REF
1047 | VISIBILITY
1048 | CASTS_SHADOW,
1049 1,
1050 )[0];
1051
1052 world.set(entity, components::Name(format!("Instanced {}", mesh_name)));
1053 world.set(
1054 entity,
1055 components::LocalTransform {
1056 translation: nalgebra_glm::Vec3::new(0.0, 0.0, 0.0),
1057 scale: nalgebra_glm::Vec3::new(1.0, 1.0, 1.0),
1058 rotation: nalgebra_glm::Quat::identity(),
1059 },
1060 );
1061 world.set(entity, components::GlobalTransform::default());
1062
1063 world.set(
1064 entity,
1065 crate::ecs::mesh::components::InstancedMesh::with_instances(mesh_name, instances),
1066 );
1067
1068 if let Some(&index) = world
1069 .res::<crate::ecs::asset_state::AssetState>()
1070 .material_registry
1071 .registry
1072 .name_to_index
1073 .get(material_name)
1074 {
1075 registry_add_reference(
1076 &mut world
1077 .res_mut::<crate::ecs::asset_state::AssetState>()
1078 .material_registry
1079 .registry,
1080 index,
1081 );
1082 }
1083
1084 let texture_names: Vec<String> = registry_entry_by_name(
1085 &world
1086 .res::<crate::ecs::asset_state::AssetState>()
1087 .material_registry
1088 .registry,
1089 material_name,
1090 )
1091 .map(|mat| mat.texture_names().map(str::to_string).collect())
1092 .unwrap_or_default();
1093 texture_cache_acquire(
1094 world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(),
1095 TextureOwner::EntityMaterial(render_entity(entity)),
1096 texture_names,
1097 );
1098
1099 world.set(entity, components::MaterialRef::new(material_name));
1100
1101 world.set(entity, components::Visibility { visible: true });
1102 world.set(entity, components::CastsShadow);
1103
1104 entity
1105}
1106
1107pub fn find_entity_by_name(world: &World, name: &str) -> Option<Entity> {
1108 world.ecs.worlds[CORE].query_entities(NAME).find(|&entity| {
1109 world
1110 .get::<crate::ecs::primitives::Name>(entity)
1111 .is_some_and(|n| n.0 == name)
1112 })
1113}
1114
1115pub fn spawn_material(
1117 world: &mut World,
1118 entity: Entity,
1119 name: String,
1120 material: crate::render::material::Material,
1121) {
1122 material_registry_insert(
1123 &mut world
1124 .res_mut::<crate::ecs::asset_state::AssetState>()
1125 .material_registry,
1126 name.clone(),
1127 material,
1128 );
1129 if let Some(&index) = world
1130 .res::<crate::ecs::asset_state::AssetState>()
1131 .material_registry
1132 .registry
1133 .name_to_index
1134 .get(&name)
1135 {
1136 registry_add_reference(
1137 &mut world
1138 .res_mut::<crate::ecs::asset_state::AssetState>()
1139 .material_registry
1140 .registry,
1141 index,
1142 );
1143 }
1144 world.set(entity, MaterialRef::new(name));
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149 use super::*;
1150
1151 #[test]
1152 fn process_commands_system_drains_ecs_and_leaves_render() {
1153 let mut world = World::default();
1154 let entity = world.spawn_count(1)[0];
1155
1156 queue_ecs_command(&mut world, EcsCommand::DespawnRecursive { entity });
1157 queue_render_command(
1158 &mut world,
1159 RenderCommand::LoadHdrSkybox {
1160 hdr_data: vec![0u8; 4],
1161 },
1162 );
1163
1164 process_commands_system(&mut world);
1165
1166 assert!(
1167 world
1168 .res::<crate::ecs::world::commands::CommandQueues>()
1169 .ecs
1170 .is_empty(),
1171 "process_commands_system should drain the ecs queue"
1172 );
1173 assert_eq!(
1174 world
1175 .res::<crate::ecs::world::commands::CommandQueues>()
1176 .render
1177 .len(),
1178 1,
1179 "process_commands_system must not touch the render queue"
1180 );
1181 assert!(matches!(
1182 world
1183 .res::<crate::ecs::world::commands::CommandQueues>()
1184 .render[0],
1185 RenderCommand::LoadHdrSkybox { .. }
1186 ));
1187 }
1188}