1use super::physics::*;
2use crate::core::World;
3use crate::math::{Mat4, Vec3};
4use crate::renderer::{
5 components::{Camera, Material, Mesh, MeshRenderer},
6 Renderer,
7};
8use bytemuck;
9use wgpu;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub struct WireframeConfig {
13 pub global: bool,
14}
15
16#[derive(Default)]
17pub struct RenderCache {
18 pub(crate) batches: std::collections::HashMap<BatchKey, BatchData>,
19 pub instances: Vec<crate::renderer::gpu_types::InstanceRaw>,
20 pub draw_items: Vec<DrawItem>,
21}
22
23thread_local! {
24 static RENDER_CACHE: std::cell::RefCell<RenderCache> = std::cell::RefCell::new(RenderCache::default());
25}
26
27pub fn clear_render_cache() {
28 RENDER_CACHE.with(|rc| {
29 let mut cache = rc.borrow_mut();
30 cache.batches.clear();
31 cache.instances.clear();
32 cache.draw_items.clear();
33 });
34}
35
36#[derive(Debug, Clone)]
37pub struct DrawItem {
38 vbuf: std::sync::Arc<wgpu::Buffer>,
39 vertex_count: u32,
40 bind_group: std::sync::Arc<wgpu::BindGroup>,
41 unlit: bool,
42 is_skybox: bool,
43 skeleton_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
44 is_transparent: bool,
45 first_instance: u32,
46 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
51 instance_count: u32,
52 camera_count: u32,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, Hash)]
57pub(crate) struct BatchKey {
58 vbuf_id: usize,
59 mat_id: usize,
60 skeleton_id: Option<usize>,
61 is_transparent: bool,
71 unlit: bool,
72 is_skybox: bool,
73}
74
75pub(crate) struct BatchData {
76 vbuf: std::sync::Arc<wgpu::Buffer>,
77 bind_group: std::sync::Arc<wgpu::BindGroup>,
78 vertex_count: u32,
79 unlit: bool,
80 is_skybox: bool,
81 skeleton_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
82 is_transparent: bool,
83 instances: Vec<crate::renderer::gpu_types::InstanceRaw>,
84 shadow_instances: Vec<crate::renderer::gpu_types::InstanceRaw>,
87}
88
89fn ensure_global_transforms(world: &mut World) {
100 use crate::core::query::Without;
101 use crate::core::system::System;
102 use gizmo_physics_core::components::{GlobalTransform, Transform};
103
104 let mut missing = Vec::new();
107 if let Some(q) = world.query::<(&Mesh, &Transform, Without<GlobalTransform>)>() {
108 for (id, _) in q.iter() {
109 missing.push(id);
110 }
111 }
112 for id in missing {
113 if let Some(e) = world.get_entity(id) {
114 world.add_component(e, GlobalTransform::default());
115 }
116 }
117
118 let mut sync = crate::systems::transform::TransformSyncSystem;
119 let mut propagate = crate::systems::transform::TransformPropagateSystem;
120 sync.run(world, 0.0);
121 propagate.run(world, 0.0);
122}
123
124pub trait AppSceneRenderExt {
138 fn with_scene_render(self) -> Self;
140}
141
142impl<State: 'static> AppSceneRenderExt for gizmo_app::App<State> {
143 fn with_scene_render(self) -> Self {
144 self.set_render(|world, _state, encoder, view, renderer, _light_time| {
145 renderer.gpu_fluid = None;
146 renderer.gpu_physics = None;
147 renderer.ssr = None;
148 renderer.ssgi = None;
149 renderer.volumetric = None;
150 renderer.taa = None;
151 default_render_pass(world, encoder, view, renderer);
152 })
153 }
154}
155
156#[tracing::instrument(skip_all, name = "render_system")]
160pub fn default_render_pass(
161 world: &mut World,
162 encoder: &mut wgpu::CommandEncoder,
163 view: &wgpu::TextureView,
164 renderer: &mut Renderer,
165) {
166 ensure_global_transforms(world);
173
174 let aspect = if renderer.size.height > 0 {
181 renderer.size.width as f32 / renderer.size.height as f32
182 } else {
183 1.0
184 };
185 let mut proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, aspect, 0.1, 2000.0);
186 let mut view_mat = Mat4::from_translation(Vec3::ZERO);
187 let mut cam_pos = Vec3::ZERO;
188 let mut cam_forward = Vec3::new(0.0, 0.0, -1.0);
189
190 gpu_physics_submit_system(world, renderer);
195 gpu_physics_readback_system(world, renderer);
196
197 let mut cam_exposure = 1.0;
198 let mut cam_near = 0.1f32;
201 let mut cam_far = 2000.0f32;
202 let mut cam_fov = std::f32::consts::FRAC_PI_4;
203
204 let cameras = world.borrow::<Camera>();
206 let global_transforms = world.borrow::<gizmo_physics_core::components::GlobalTransform>();
207 let local_transforms = world.borrow::<gizmo_physics_core::components::Transform>();
208 {
209 let active_cam = cameras
215 .iter()
216 .find(|(_, c)| c.primary)
217 .or_else(|| cameras.iter().next())
218 .map(|(id, _)| id);
219 if let Some(active_cam) = active_cam {
220 if let Some(cam) = cameras.get(active_cam) {
221 let pos = global_transforms
231 .get(active_cam)
232 .map(|g| g.matrix.to_scale_rotation_translation().2)
233 .or_else(|| local_transforms.get(active_cam).map(|t| t.position))
234 .unwrap_or(Vec3::ZERO);
235 proj = cam.get_projection(aspect);
236 view_mat = cam.get_view(pos);
237 cam_pos = pos;
238 cam_forward = cam.get_front();
239 cam_exposure = cam.exposure;
240 cam_near = cam.near;
241 cam_far = cam.far;
242 cam_fov = cam.fov;
243 }
244 }
245 }
246
247 let water_sample = world
258 .get_resource::<crate::physics::world::PhysicsWorld>()
259 .and_then(|pw| pw.water_at(cam_pos));
260 let (uw, fog_r, fog_g, fog_b, fog_density) = match water_sample {
261 Some(s) => (1.0, s.fog_color[0], s.fog_color[1], s.fog_color[2], s.fog_density),
262 None => (0.0, 0.0, 0.0, 0.0, 0.0),
263 };
264
265 renderer.update_post_process(
266 &renderer.queue,
267 crate::renderer::gpu_types::PostProcessUniforms {
268 bloom_intensity: renderer.bloom_intensity,
269 bloom_threshold: renderer.bloom_threshold,
270 exposure: cam_exposure,
271 chromatic_aberration: renderer.chromatic_aberration,
272 vignette_intensity: 0.25,
273 film_grain_intensity: renderer.film_grain_intensity,
274 dof_focus_dist: renderer.dof_focus_dist,
275 dof_focus_range: renderer.dof_focus_range,
276 dof_blur_size: if renderer.dof_enabled { renderer.dof_blur_size } else { 0.0 },
277 cam_near,
278 cam_far,
279 underwater: uw,
280 fog_r,
281 fog_g,
282 fog_b,
283 fog_density,
284 },
285 );
286
287 let unjittered_proj = proj;
289
290 if let Some(ref taa) = renderer.taa {
292 if taa.enabled {
293 let jp = crate::renderer::taa::TaaState::get_jitter(taa.frame_index);
294 let jx = jp[0] * 2.0 / renderer.size.width as f32;
296 let jy = jp[1] * 2.0 / renderer.size.height as f32;
297 proj.z_axis.x -= jx;
300 proj.z_axis.y -= jy;
301 }
302 }
303
304 let view_proj = proj * view_mat; let unjittered_view_proj = unjittered_proj * view_mat; let scene_lights = collect_scene_lights(world);
310 let sun_dir = scene_lights.sun_dir;
311 let sun_col = scene_lights.sun_col;
312
313 let cascades =
317 crate::renderer::compute_directional_cascades(cam_pos, cam_forward, aspect, cam_fov, cam_near, cam_far, sun_dir);
318 let cascade_splits = cascades.splits;
319 let cascade_vp = cascades.view_projs;
320 let light_view_projs: [[[f32; 4]; 4]; 4] = cascade_vp.map(|m| m.to_cols_array_2d());
321
322 let lights_data = scene_lights.lights;
324 let num_lights = scene_lights.num_lights;
325
326 #[allow(unused_assignments)]
327 let mut point_light_view_projs = [gizmo_math::Mat4::IDENTITY; 6];
328 if renderer.point_shadows_enabled && scene_lights.shadow_point_index >= 0 {
333 let idx = scene_lights.shadow_point_index as usize;
334 let lp = lights_data[idx].position;
335 let pos = gizmo_math::Vec3::new(lp[0], lp[1], lp[2]);
336 let radius = lights_data[idx].color[3].max(1.0);
338 let proj = gizmo_math::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, radius);
339 point_light_view_projs = [
340 proj * gizmo_math::Mat4::look_to_rh(pos, gizmo_math::Vec3::X, -gizmo_math::Vec3::Y),
341 proj * gizmo_math::Mat4::look_to_rh(pos, gizmo_math::Vec3::NEG_X, -gizmo_math::Vec3::Y),
342 proj * gizmo_math::Mat4::look_to_rh(pos, gizmo_math::Vec3::Y, gizmo_math::Vec3::Z),
343 proj * gizmo_math::Mat4::look_to_rh(pos, gizmo_math::Vec3::NEG_Y, gizmo_math::Vec3::NEG_Z),
344 proj * gizmo_math::Mat4::look_to_rh(pos, gizmo_math::Vec3::Z, -gizmo_math::Vec3::Y),
345 proj * gizmo_math::Mat4::look_to_rh(pos, gizmo_math::Vec3::NEG_Z, -gizmo_math::Vec3::Y),
346 ];
347
348 for (i, view_proj) in point_light_view_projs.iter().enumerate() {
349 renderer.queue.write_buffer(
350 &renderer.scene.point_shadow_uniform_buffers[i],
351 0,
352 bytemuck::bytes_of(&crate::renderer::gpu_types::ShadowVsUniform {
353 light_view_proj: view_proj.to_cols_array_2d(),
354 }),
355 );
356 }
357 }
358
359
360 let elapsed_time = world
363 .get_resource::<gizmo_core::time::Time>()
364 .map(|t| t.elapsed() as f32)
365 .unwrap_or(0.0);
366 let scene_uniform_data = crate::renderer::gpu_types::SceneUniforms {
367 view_proj: view_proj.to_cols_array_2d(),
368 camera_pos: [cam_pos.x, cam_pos.y, cam_pos.z, 1.0],
369 sun_direction: [sun_dir.x, sun_dir.y, sun_dir.z, if scene_lights.has_sun { 1.0 } else { 0.0 }],
374 sun_color: [sun_col.x, sun_col.y, sun_col.z, sun_col.w],
375 lights: lights_data,
376 light_view_proj: light_view_projs,
377 cascade_splits,
378 camera_forward: [cam_forward.x, cam_forward.y, cam_forward.z, 0.0],
379 cascade_params: [
382 0.1,
383 1.0 / crate::renderer::SHADOW_MAP_RES as f32,
384 elapsed_time,
385 (scene_lights.shadow_point_index + 1).max(0) as f32,
386 ],
387 num_lights,
388 exposure: cam_exposure,
389 _pre_align_pad: [0; 2],
390 _align_pad: [0; 3],
391 environment_blend_t: renderer.environment_blend_t,
392 environment_preset: renderer.environment_preset,
393 point_shadows_enabled: renderer.point_shadows_enabled as u32,
394 environment_preset_2: renderer.environment_preset_2,
395 shading_mode: renderer.shading_mode,
396 };
397 renderer.queue.write_buffer(
398 &renderer.scene.global_uniform_buffer,
399 0,
400 bytemuck::cast_slice(&[scene_uniform_data]),
401 );
402 for (i, light_view_proj) in light_view_projs.iter().enumerate() {
403 renderer.queue.write_buffer(
404 &renderer.scene.shadow_cascade_uniform_buffers[i],
405 0,
406 bytemuck::bytes_of(&crate::renderer::gpu_types::ShadowVsUniform {
407 light_view_proj: *light_view_proj,
408 }),
409 );
410 }
411
412 if let Some(ref mut taa) = renderer.taa {
414 if taa.enabled {
415 let jp = crate::renderer::taa::TaaState::get_jitter(taa.frame_index);
416 let jx = jp[0] * 2.0 / renderer.size.width as f32;
417 let jy = jp[1] * 2.0 / renderer.size.height as f32;
418 let alpha = if taa.frame_index == 0 { 1.0f32 } else { 0.1f32 };
419 taa.update_params(&renderer.queue, [jx, jy], alpha);
420 taa.store_prev_vp(unjittered_view_proj.to_cols_array_2d());
421 }
422 }
423
424 if let Some(ref mut ssgi) = renderer.ssgi {
428 let alpha = if ssgi.frame_index == 0 { 1.0f32 } else { 0.1f32 };
429 ssgi.update_params(&renderer.queue, alpha);
430 ssgi.store_prev_vp(unjittered_view_proj.to_cols_array_2d());
431 }
432
433 let renderers = world.borrow::<MeshRenderer>();
436
437 let frustum = crate::math::Frustum::from_matrix(&unjittered_view_proj);
439 let cascade_frusta: [crate::math::Frustum; 4] =
442 cascade_vp.map(|m| crate::math::Frustum::from_matrix(&m));
443
444 let (draw_items, uploaded_instances) = RENDER_CACHE.with(|rc| {
445 let mut cache = rc.borrow_mut();
446
447 for batch in cache.batches.values_mut() {
456 batch.instances.clear();
457 batch.shadow_instances.clear();
458 }
459 cache.instances.clear();
460 cache.draw_items.clear();
461
462 let pooled_storage = world.borrow::<gizmo_core::pool::Pooled>();
463
464 macro_rules! process_mesh {
465 ($e:expr, $mesh:expr, $trans:expr, $mat:expr, $skeleton:expr) => {
466 if renderers.get($e).is_none() {
467 continue;
468 }
469
470 if pooled_storage.get($e).is_some() {
472 continue;
473 }
474
475 let center_mat = Mat4::from_translation($mesh.center_offset);
476 let model = $trans.matrix * center_mat;
477
478 let local_cx = ($mesh.bounds.min.x + $mesh.bounds.max.x) * 0.5;
480 let local_cy = ($mesh.bounds.min.y + $mesh.bounds.max.y) * 0.5;
481 let local_cz = ($mesh.bounds.min.z + $mesh.bounds.max.z) * 0.5;
482 let world_c = model.transform_point3(Vec3::new(local_cx, local_cy, local_cz));
483 let hx = ($mesh.bounds.max.x - $mesh.bounds.min.x) * 0.5;
484 let hy = ($mesh.bounds.max.y - $mesh.bounds.min.y) * 0.5;
485 let hz = ($mesh.bounds.max.z - $mesh.bounds.min.z) * 0.5;
486 let local_r = (hx * hx + hy * hy + hz * hz).sqrt();
487 let sx = model.x_axis.truncate().length();
488 let sy = model.y_axis.truncate().length();
489 let sz = model.z_axis.truncate().length();
490 let world_r = local_r * sx.max(sy).max(sz);
491
492 let camera_visible = match crate::renderer::classify_visibility(
498 &frustum,
499 &cascade_frusta,
500 &model,
501 $mesh.bounds,
502 $mat.material_type,
503 $mat.is_transparent,
504 $mat.albedo.w,
505 ) {
506 crate::renderer::Visibility::Culled => continue,
507 crate::renderer::Visibility::Camera => true,
508 crate::renderer::Visibility::ShadowOnly => false,
509 };
510
511 let dist_to_cam = (world_c - cam_pos).length();
513 let use_lod1 = if !$mesh.lod_vbufs.is_empty() {
514 dist_to_cam > world_r * 15.0 } else {
516 false
517 };
518
519 let active_vbuf = if use_lod1 {
520 $mesh.lod_vbufs[0].clone()
521 } else {
522 $mesh.vbuf.clone()
523 };
524 let active_vertex_count = if use_lod1 {
525 $mesh.lod_vertex_counts[0]
526 } else {
527 $mesh.vertex_count
528 };
529
530 let packed_params = (($mat.anisotropy * 1000.0).floor() + 1000.0 * ($mat.clear_coat * 1000.0).floor() + 1000000.0 * ($mat.subsurface * 100.0).floor()) as f32;
531
532 let instance_data = crate::renderer::gpu_types::InstanceRaw {
533 model: model.to_cols_array_2d(),
534 albedo_color: [$mat.albedo.x, $mat.albedo.y, $mat.albedo.z, $mat.albedo.w],
535 roughness: $mat.roughness,
536 metallic: $mat.metallic,
537 unlit: match $mat.material_type {
538 crate::renderer::components::MaterialType::Skybox => 2.0,
539 crate::renderer::components::MaterialType::Unlit => 1.0,
540 _ => 0.0,
541 },
542 _padding: packed_params,
543 };
544 let skel_bg = $skeleton.map(|s: &crate::renderer::components::Skeleton| s.bind_group.clone());
545
546 let is_skybox = $mat.material_type == crate::renderer::components::MaterialType::Skybox;
549 let unlit = is_skybox
550 || $mat.material_type == crate::renderer::components::MaterialType::Unlit;
551 let is_transparent = $mat.is_transparent || $mat.albedo.w < 0.99;
552
553 let key = BatchKey {
554 vbuf_id: std::sync::Arc::as_ptr(&active_vbuf) as usize,
555 mat_id: std::sync::Arc::as_ptr(&$mat.bind_group) as usize,
556 skeleton_id: skel_bg.as_ref().map(|bg| std::sync::Arc::as_ptr(bg) as usize),
557 is_transparent,
558 unlit,
559 is_skybox,
560 };
561
562 let batch = cache.batches.entry(key).or_insert_with(|| BatchData {
563 vbuf: active_vbuf.clone(),
564 bind_group: $mat.bind_group.clone(),
565 vertex_count: active_vertex_count,
566 unlit,
567 is_skybox,
568 skeleton_bind_group: skel_bg,
569 is_transparent,
570 instances: Vec::new(),
571 shadow_instances: Vec::new(),
572 });
573 if camera_visible {
574 batch.instances.push(instance_data);
575 } else {
576 batch.shadow_instances.push(instance_data);
578 }
579 };
580 }
581
582 let skeletons = world.borrow::<crate::renderer::components::Skeleton>();
583
584 if let Some(mut q) = world.query::<(&Mesh, &gizmo_physics_core::components::GlobalTransform, &Material)>() {
585 for (e, (mesh, trans, mat)) in q.iter_mut() {
586 process_mesh!(e, mesh, trans, mat, skeletons.get(e));
587 }
588 }
589
590 let meshes = world.try_get_resource::<gizmo_core::asset::Assets<Mesh>>().ok();
591 let materials = world.try_get_resource::<gizmo_core::asset::Assets<Material>>().ok();
592
593 if let (Some(meshes), Some(materials)) = (meshes, materials) {
594 if let Some(mut q) = world.query::<(&gizmo_core::asset::Handle<Mesh>, &gizmo_physics_core::components::GlobalTransform, &gizmo_core::asset::Handle<Material>)>() {
595 for (e, (h_mesh, trans, h_mat)) in q.iter_mut() {
596 if let (Some(mesh), Some(mat)) = (meshes.get(h_mesh), materials.get(h_mat)) {
597 process_mesh!(e, mesh, trans, mat, skeletons.get(e));
598 }
599 }
600 }
601 }
602
603 let mut local_instances: Vec<crate::renderer::gpu_types::InstanceRaw> = std::mem::take(&mut cache.instances);
604 let mut local_draw_items: Vec<DrawItem> = std::mem::take(&mut cache.draw_items);
605
606 for batch in cache.batches.values() {
607 if batch.instances.is_empty() && batch.shadow_instances.is_empty() {
608 continue;
609 }
610 let first_instance = local_instances.len() as u32;
611 let camera_count = batch.instances.len() as u32;
614 local_instances.extend(&batch.instances);
615 local_instances.extend(&batch.shadow_instances);
616 let instance_count = camera_count + batch.shadow_instances.len() as u32;
617
618 local_draw_items.push(DrawItem {
619 vbuf: batch.vbuf.clone(),
620 vertex_count: batch.vertex_count,
621 bind_group: batch.bind_group.clone(),
622 unlit: batch.unlit,
623 is_skybox: batch.is_skybox,
624 skeleton_bind_group: batch.skeleton_bind_group.clone(),
625 is_transparent: batch.is_transparent,
626 first_instance,
627 instance_count,
628 camera_count,
629 });
630 }
631
632 cache.instances = local_instances;
633 cache.draw_items = local_draw_items;
634
635 let max_instances = renderer.scene.instance_capacity;
637 let instances_slice = if cache.instances.len() > max_instances {
638 &cache.instances[..max_instances]
639 } else {
640 &cache.instances
641 };
642
643 if !instances_slice.is_empty() {
644 renderer.queue.write_buffer(
645 &renderer.scene.instance_buffer,
646 0,
647 bytemuck::cast_slice(instances_slice),
648 );
649 }
650
651 (cache.draw_items.clone(), instances_slice.len() as u32)
655 });
656 if let Some(physics) = &renderer.gpu_physics {
659 physics.request_readback(encoder);
661
662 physics.compute_pass(encoder);
663 physics.debug_compute_pass(encoder);
664 physics.cull_pass(encoder, &renderer.scene.global_bind_group);
665 }
666
667 let fluid_pos = Vec3::new(0.0, 5.0, 0.0);
674 let dist_to_fluid = (cam_pos - fluid_pos).length();
675 let fluid_lod = if !renderer.fluid_enabled {
676 0.0
677 } else if dist_to_fluid < 40.0 {
678 1.0
679 } else if dist_to_fluid < 80.0 {
680 0.5
681 } else if dist_to_fluid < 150.0 {
682 0.1
683 } else {
684 0.0
685 };
686
687 let dist_to_origin = cam_pos.length();
688 let particle_lod = if dist_to_origin < 50.0 {
689 1.0
690 } else if dist_to_origin < 100.0 {
691 0.5
692 } else if dist_to_origin < 200.0 {
693 0.1
694 } else {
695 0.0
696 };
697
698 if let Some(fluid) = &renderer.gpu_fluid {
700 let active_fluid = (fluid.num_particles as f32 * fluid_lod) as u32;
701 fluid.compute_pass(encoder, &renderer.queue, true, active_fluid);
702 }
703
704 if let Some(particles) = &renderer.gpu_particles {
706 let active_parts = (particles.max_particles as f32 * particle_lod) as u32;
707 let (dt, time) = world
708 .get_resource::<gizmo_core::time::Time>()
709 .map(|t| (t.dt(), t.elapsed() as f32))
710 .unwrap_or((0.016, 0.0));
711 particles.update_params(&renderer.queue, dt, time); particles.compute_pass(encoder, active_parts);
713 }
714
715 if let Some(ref mut def) = renderer.deferred {
719 def.resize(&renderer.device, renderer.size.width, renderer.size.height);
720 }
721 {
722 let w = renderer.size.width;
723 let h = renderer.size.height;
724 if let (Some(ssao), Some(def)) = (&mut renderer.ssao, &renderer.deferred) {
725 if ssao.width != w || ssao.height != h {
726 ssao.resize(&renderer.device, def, w, h);
727 }
728 }
729 if let (Some(ssr), Some(def)) = (&mut renderer.ssr, &renderer.deferred) {
730 if ssr.width != w || ssr.height != h {
731 ssr.resize(&renderer.device, def, &renderer.post.hdr_texture_view, w, h);
732 }
733 }
734 if let (Some(volumetric), Some(def)) = (&mut renderer.volumetric, &renderer.deferred) {
735 if volumetric.width != w || volumetric.height != h {
736 volumetric.resize(&renderer.device, def, w, h);
737 }
738 }
739 }
740 {
741 let w = renderer.size.width;
742 let h = renderer.size.height;
743 if let (Some(taa), Some(def)) = (&mut renderer.taa, &renderer.deferred) {
744 if taa.width != w || taa.height != h {
745 taa.resize(
746 &renderer.device,
747 &renderer.post.hdr_texture_view,
748 &def.world_position_view,
749 w,
750 h,
751 );
752 }
753 }
754 }
755
756 #[cfg(not(target_arch = "wasm32"))]
759 passes::record_shadow_passes(encoder, renderer, &draw_items, uploaded_instances);
760 passes::record_deferred_geometry(encoder, renderer, world, &draw_items, uploaded_instances);
761 passes::record_ssao(encoder, renderer);
762 passes::record_forward_and_fluid(
763 encoder, renderer, world, &draw_items, uploaded_instances, particle_lod, fluid_lod,
764 );
765 passes::record_screen_space_effects(encoder, renderer);
766 if let Some(ref mut ssgi) = renderer.ssgi {
768 ssgi.advance_frame();
769 }
770 passes::record_taa_and_overlays(encoder, renderer, world);
771
772 renderer.run_post_processing(encoder, view);
773}
774
775pub trait RenderContextExt {
783 fn default_render(&mut self, world: &mut crate::core::World);
793}
794
795impl<'a> RenderContextExt for crate::renderer::RenderContext<'a> {
796 fn default_render(&mut self, world: &mut crate::core::World) {
797 let (encoder, view, renderer) = self.parts_mut();
798 default_render_pass(world, encoder, view, renderer);
799 }
800}
801
802mod passes;
803
804mod shared;
805pub use shared::{collect_scene_lights, SceneLights};
806
807#[cfg(test)]
815mod batch_key_tests {
816 use super::BatchKey;
817
818 #[test]
826 fn routing_flags_distinguish_batches_sharing_a_texture() {
827 let base = BatchKey {
828 vbuf_id: 1,
829 mat_id: 42, skeleton_id: None,
831 is_transparent: false,
832 unlit: false,
833 is_skybox: false,
834 };
835 let transparent = BatchKey {
836 is_transparent: true,
837 ..base.clone()
838 };
839 let unlit = BatchKey {
840 unlit: true,
841 ..base.clone()
842 };
843 let skybox = BatchKey {
844 is_skybox: true,
845 ..base.clone()
846 };
847
848 assert_ne!(base, transparent, "opaque and transparent must be separate batches");
849 assert_ne!(base, unlit, "PBR and unlit must be separate batches");
850 assert_ne!(base, skybox, "PBR and skybox must be separate batches");
851
852 assert_eq!(base, base.clone(), "identical materials must still batch together");
854 }
855}
856
857#[cfg(test)]
858mod golden_render_tests {
859 use super::default_render_pass;
860 use crate::bundles::{CameraBundle, DirectionalLightBundle};
861 use crate::core::World;
862 use crate::math::{Vec3, Vec4};
863 use crate::physics::components::{GlobalTransform, Transform};
864 use crate::renderer::asset::AssetManager;
865 use crate::renderer::components::{Material, MeshRenderer};
866 use crate::renderer::Renderer;
867
868 #[test]
869 fn default_render_pass_draws_a_cube_distinct_from_background() {
870 if !pollster::block_on(Renderer::headless_adapter_available()) {
871 eprintln!(
872 "skipping default_render_pass_draws_a_cube_distinct_from_background: \
873 no GPU adapter available (headless render requires a GPU)"
874 );
875 return;
876 }
877 pollster::block_on(async {
878 const W: u32 = 128;
879 const H: u32 = 128;
880 const BPP: u32 = 4; let mut renderer = Renderer::new_headless(W, H, None).await;
883 let mut asset_manager = AssetManager::new();
884 let mut world = World::new();
885
886 let mesh = AssetManager::create_cube(&renderer.device);
888 let tex = asset_manager.create_white_texture(
889 &renderer.device,
890 &renderer.queue,
891 &renderer.scene.texture_bind_group_layout,
892 );
893 let mat = Material::new(tex).with_pbr(Vec4::new(0.9, 0.15, 0.15, 1.0), 0.0, 1.0);
894 let cube = world.spawn();
898 world.add_component(cube, Transform::new(Vec3::ZERO));
899 world.add_component(cube, mesh);
900 world.add_component(cube, mat);
901 world.add_component(cube, MeshRenderer::new());
902
903 world.spawn_bundle(CameraBundle {
905 position: Vec3::new(-6.0, 0.0, 0.0),
906 yaw: 0.0,
907 pitch: 0.0,
908 primary: true,
909 ..Default::default()
910 });
911 world.spawn_bundle(DirectionalLightBundle::default());
913
914 let format = renderer.config.format;
916 let target = renderer.device.create_texture(&wgpu::TextureDescriptor {
917 label: Some("golden-target"),
918 size: wgpu::Extent3d {
919 width: W,
920 height: H,
921 depth_or_array_layers: 1,
922 },
923 mip_level_count: 1,
924 sample_count: 1,
925 dimension: wgpu::TextureDimension::D2,
926 format,
927 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
928 view_formats: &[],
929 });
930 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
931 let mut encoder = renderer
932 .device
933 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
934
935 default_render_pass(&mut world, &mut encoder, &view, &mut renderer);
936
937 let staging = renderer.device.create_buffer(&wgpu::BufferDescriptor {
939 label: Some("golden-readback"),
940 size: (W * H * BPP) as u64,
941 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
942 mapped_at_creation: false,
943 });
944 encoder.copy_texture_to_buffer(
945 wgpu::TexelCopyTextureInfo {
946 texture: &target,
947 mip_level: 0,
948 origin: wgpu::Origin3d::ZERO,
949 aspect: wgpu::TextureAspect::All,
950 },
951 wgpu::TexelCopyBufferInfo {
952 buffer: &staging,
953 layout: wgpu::TexelCopyBufferLayout {
954 offset: 0,
955 bytes_per_row: Some(W * BPP),
956 rows_per_image: Some(H),
957 },
958 },
959 wgpu::Extent3d {
960 width: W,
961 height: H,
962 depth_or_array_layers: 1,
963 },
964 );
965 renderer.queue.submit(Some(encoder.finish()));
966
967 let slice = staging.slice(..);
968 let (tx, rx) = std::sync::mpsc::channel();
969 slice.map_async(wgpu::MapMode::Read, move |v| tx.send(v).unwrap());
970 let _ = renderer.device.poll(wgpu::PollType::Wait {
971 submission_index: None,
972 timeout: None,
973 });
974 rx.recv().unwrap().unwrap();
975 let data = slice.get_mapped_range();
976
977 let px = |x: u32, y: u32| -> [u8; 4] {
978 let i = ((y * W + x) * BPP) as usize;
979 [data[i], data[i + 1], data[i + 2], data[i + 3]]
980 };
981 let background = px(2, 2); let centre = px(W / 2, H / 2);
983 assert_ne!(
984 centre, background,
985 "centre pixel equals the corner/background — default_render_pass drew no geometry"
986 );
987
988 let mut differing = 0u32;
990 for y in 0..H {
991 for x in 0..W {
992 if px(x, y) != background {
993 differing += 1;
994 }
995 }
996 }
997 let frac = differing as f32 / (W * H) as f32;
998 assert!(
999 frac > 0.05,
1000 "only {:.1}% of pixels differ from the background; the lit cube should fill a \
1001 sizeable central region (regression dropping geometry?)",
1002 frac * 100.0
1003 );
1004 });
1005 }
1006
1007 async fn render_mean_brightness(exposure: f32) -> f32 {
1010 const W: u32 = 128;
1011 const H: u32 = 128;
1012 const BPP: u32 = 4;
1013
1014 let mut renderer = Renderer::new_headless(W, H, None).await;
1015 let mut asset_manager = AssetManager::new();
1016 let mut world = World::new();
1017
1018 let mesh = AssetManager::create_cube(&renderer.device);
1019 let tex = asset_manager.create_white_texture(
1020 &renderer.device,
1021 &renderer.queue,
1022 &renderer.scene.texture_bind_group_layout,
1023 );
1024 let mat = Material::new(tex).with_pbr(Vec4::new(0.9, 0.15, 0.15, 1.0), 0.0, 1.0);
1025 let cube = world.spawn();
1026 world.add_component(cube, Transform::new(Vec3::ZERO));
1027 world.add_component(cube, GlobalTransform::default());
1028 world.add_component(cube, mesh);
1029 world.add_component(cube, mat);
1030 world.add_component(cube, MeshRenderer::new());
1031
1032 world.spawn_bundle(CameraBundle {
1033 position: Vec3::new(-6.0, 0.0, 0.0),
1034 yaw: 0.0,
1035 pitch: 0.0,
1036 primary: true,
1037 exposure,
1038 ..Default::default()
1039 });
1040 world.spawn_bundle(DirectionalLightBundle::default());
1041
1042 let format = renderer.config.format;
1043 let target = renderer.device.create_texture(&wgpu::TextureDescriptor {
1044 label: Some("exposure-target"),
1045 size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
1046 mip_level_count: 1,
1047 sample_count: 1,
1048 dimension: wgpu::TextureDimension::D2,
1049 format,
1050 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1051 view_formats: &[],
1052 });
1053 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
1054 let mut encoder = renderer
1055 .device
1056 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
1057 default_render_pass(&mut world, &mut encoder, &view, &mut renderer);
1058
1059 let staging = renderer.device.create_buffer(&wgpu::BufferDescriptor {
1060 label: Some("exposure-readback"),
1061 size: (W * H * BPP) as u64,
1062 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1063 mapped_at_creation: false,
1064 });
1065 encoder.copy_texture_to_buffer(
1066 wgpu::TexelCopyTextureInfo {
1067 texture: &target,
1068 mip_level: 0,
1069 origin: wgpu::Origin3d::ZERO,
1070 aspect: wgpu::TextureAspect::All,
1071 },
1072 wgpu::TexelCopyBufferInfo {
1073 buffer: &staging,
1074 layout: wgpu::TexelCopyBufferLayout {
1075 offset: 0,
1076 bytes_per_row: Some(W * BPP),
1077 rows_per_image: Some(H),
1078 },
1079 },
1080 wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
1081 );
1082 renderer.queue.submit(Some(encoder.finish()));
1083
1084 let slice = staging.slice(..);
1085 let (tx, rx) = std::sync::mpsc::channel();
1086 slice.map_async(wgpu::MapMode::Read, move |v| tx.send(v).unwrap());
1087 let _ = renderer.device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
1088 rx.recv().unwrap().unwrap();
1089 let data = slice.get_mapped_range();
1090
1091 let mut sum = 0u64;
1093 for i in (0..(W * H * BPP) as usize).step_by(BPP as usize) {
1094 sum += data[i] as u64 + data[i + 1] as u64 + data[i + 2] as u64;
1095 }
1096 sum as f32 / (W * H * 3) as f32
1097 }
1098
1099 #[test]
1105 fn camera_exposure_brightens_the_frame() {
1106 if !pollster::block_on(Renderer::headless_adapter_available()) {
1107 eprintln!("skipping camera_exposure_brightens_the_frame: no GPU adapter available");
1108 return;
1109 }
1110 pollster::block_on(async {
1111 let dim = render_mean_brightness(1.0).await;
1112 let bright = render_mean_brightness(2.0).await;
1113 assert!(
1114 bright > dim + 1.0,
1115 "higher camera exposure must brighten the scene, but exp=1.0 mean={dim:.2} \
1116 vs exp=2.0 mean={bright:.2} (exposure not applied / detached from post?)"
1117 );
1118 });
1119 }
1120}