1use crate::canopy::{Canopy, CanopyFrame, CanopyMode, Gobo, canopy_light_wgsl};
2use crate::deform::{self, Deformer, DeformerBuffer, DeformerId, WindFrame};
3use crate::lights::{CASTER_BYTES, CASTER_SLOTS, LIGHTS_WGSL, LightQuality, Lights, LocalLight};
4use crate::lod::LOD_WGSL;
5use crate::maps::{
6 self, CAUSTIC_BINDING, CONTENT_BINDING, CONTENT_SLOTS, Caustic, ContentFormat, ContentSlots,
7 GpuMapMaterial, MapImages, MapIndices, MapTextures,
8};
9use crate::pipeline_cache::{CacheLoad, PipelineCache};
10use crate::probes::{
11 Ambient, ContactDesc, ContactField, ContactUniform, PROBE_WGSL, ProbeLighting,
12};
13use crate::ranges::FreeRanges;
14use crate::reflect;
15use crate::shadow::{CASCADE_COUNT, Fit, SAMPLE_UNIFORM_BYTES, SHADOW_WGSL, Shadows};
16use crate::skin::{
17 InstancePose, NO_SKIN, PoseLayout, SKIN_BINDING, SKIN_WGSL, SkinData, SkinStore,
18};
19use crate::sky::{AnalyticSky, SkyPass, SkySource};
20use crate::warm::{self, Arrival, Warming};
21use bytemuck::{Pod, Zeroable};
22use pfx_bake::detail::atlas::Atlas;
23use pfx_bake::reflection::{
24 ProbeRecord, ReflectionArray, ReflectionArtifact, nearest_two, upload_reflections,
25};
26use pfx_core::daylight::{Daylight, REFERENCE_HOUR};
27use pfx_core::sky::SUN_RADIUS;
28use pfx_geom::tree::{SwayVertex, TREE_SWAY_WGSL, Tree};
29use pfx_gpu::{Gpu, GpuProfiler, OffscreenTarget, PassTiming};
30use pfx_materials::{BRDF, Material, PackedMaterial};
31use pfx_physics::BEND_WGSL;
32use std::cell::{Cell, RefCell};
33use std::collections::{HashMap, HashSet};
34use std::hash::{Hash, Hasher};
35use std::num::NonZeroU64;
36use std::ops::{Deref, Range};
37use std::sync::Arc;
38use std::sync::atomic::{AtomicUsize, Ordering};
39use std::sync::mpsc::{Receiver, Sender, channel};
40use wgpu::util::DeviceExt;
41
42pub type Matrix = [[f32; 4]; 4];
43
44pub const NO_SWAY: u32 = u32::MAX;
45pub const SKY_EDGE: u32 = 128;
46pub const CONTACT_BINDING: u32 = CONTENT_BINDING + CONTENT_SLOTS as u32 + 1;
47pub const NORMAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
48
49pub(crate) const MESH_SLOT_BITS: u32 = 20;
50pub(crate) const MESH_SLOT_MASK: u32 = (1 << MESH_SLOT_BITS) - 1;
51pub(crate) const MESH_GENERATION_LAST: u32 = u32::MAX >> MESH_SLOT_BITS;
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub struct MeshHandle(pub u32);
55
56impl MeshHandle {
57 pub fn slot(self) -> usize {
58 (self.0 & MESH_SLOT_MASK) as usize
59 }
60
61 pub fn generation(self) -> u32 {
62 self.0 >> MESH_SLOT_BITS
63 }
64
65 fn pack(slot: usize, generation: u32) -> Self {
66 Self(slot as u32 | generation << MESH_SLOT_BITS)
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct MeshStats {
72 pub live: usize,
73 pub slots: usize,
74 pub buffers: usize,
75 pub glass_live: usize,
76 pub glass_slots: usize,
77 pub glass_buffers: usize,
78}
79
80pub struct MeshData<'a> {
81 pub positions: &'a [[f32; 3]],
82 pub normals: &'a [[f32; 3]],
83 pub tangents: &'a [[f32; 4]],
84 pub uvs: &'a [[f32; 2]],
85 pub uvs1: Option<&'a [[f32; 2]]>,
86 pub alpha: Option<&'a [f32]>,
87 pub indices: &'a [u32],
88}
89
90pub struct MeshBuffers<'a> {
91 pub vertices: &'a wgpu::Buffer,
92 pub positions: &'a wgpu::Buffer,
93 pub indices: &'a wgpu::Buffer,
94 pub count: u32,
95}
96
97#[derive(Clone, Copy)]
98pub struct Camera {
99 pub view: Matrix,
100 pub projection: Matrix,
101 pub previous_view_projection: Matrix,
102 pub position: [f32; 3],
103}
104
105#[derive(Clone, Copy)]
106pub struct Sun {
107 pub direction: [f32; 3],
108 pub colour: [f32; 3],
109 pub intensity: f32,
110}
111
112impl Sun {
113 pub fn from_daylight(daylight: Daylight, reference_hour: f64) -> Self {
114 let sun = daylight.sun();
115 let light = daylight.light(reference_hour);
116 Self {
117 direction: sun.y_up.map(|value| value as f32),
118 colour: light.colour.map(|value| value as f32),
119 intensity: light.intensity as f32,
120 }
121 }
122}
123
124#[derive(Clone, Copy)]
125pub struct Instance {
126 pub mesh: MeshHandle,
127 pub model: Matrix,
128 pub previous_model: Matrix,
129 pub material: u32,
130 pub id: u32,
131 pub opacity: f32,
132 pub alpha_cutoff: f32,
133 pub deformer: DeformerId,
134 pub age: f32,
135 pub coverage: f32,
136 pub shadow_only: bool,
137 pub casts_shadow: bool,
138 pub two_sided: bool,
139}
140
141impl Instance {
142 pub fn new(mesh: MeshHandle, model: Matrix, material: u32, id: u32) -> Self {
143 Self {
144 mesh,
145 model,
146 previous_model: model,
147 material,
148 id,
149 opacity: 1.0,
150 alpha_cutoff: 0.5,
151 deformer: DeformerId::NONE,
152 age: 0.0,
153 coverage: 1.0,
154 shadow_only: false,
155 casts_shadow: true,
156 two_sided: false,
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum ContentFace {
163 #[default]
164 Both,
165 Front,
166 Back,
167}
168
169impl ContentFace {
170 fn code(self) -> u32 {
171 match self {
172 Self::Both => 0,
173 Self::Front => 1,
174 Self::Back => 2,
175 }
176 }
177
178 pub fn shows(self, front: bool) -> bool {
179 match self {
180 Self::Both => true,
181 Self::Front => front,
182 Self::Back => !front,
183 }
184 }
185}
186
187#[derive(Clone, Copy, Debug, PartialEq)]
188pub struct InstanceSurface {
189 pub clip: [[f32; 4]; 2],
190 pub uv_offset: [f32; 2],
191 pub uv_scale: [f32; 2],
192 pub crop: [f32; 4],
193 pub face: ContentFace,
194 pub contact: bool,
195 pub authored_ambient: bool,
196 pub cutout: bool,
197 pub reflection_occlusion: f32,
198}
199
200impl Default for InstanceSurface {
201 fn default() -> Self {
202 Self {
203 clip: [[0.0; 4]; 2],
204 uv_offset: [0.0; 2],
205 uv_scale: [1.0; 2],
206 crop: [0.0, 0.0, 1.0, 1.0],
207 face: ContentFace::Both,
208 contact: false,
209 authored_ambient: false,
210 cutout: false,
211 reflection_occlusion: 1.0,
212 }
213 }
214}
215
216impl InstanceSurface {
217 pub fn clipped(&self) -> bool {
218 self.clip
219 .iter()
220 .any(|plane| plane[..3].iter().any(|&value| value != 0.0))
221 }
222
223 pub fn cuts(&self, point: [f32; 3]) -> bool {
224 self.clip.iter().any(|plane| {
225 plane[..3].iter().any(|&value| value != 0.0)
226 && plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] > plane[3]
227 })
228 }
229
230 pub fn content_uv(&self, uv: [f32; 2]) -> Option<[f32; 2]> {
231 pfx_materials::content_uv(uv, self.uv_offset, self.uv_scale, self.crop)
232 }
233
234 fn valid(&self) -> bool {
235 self.clip.iter().flatten().all(|value| value.is_finite())
236 && self.uv_offset.iter().all(|value| value.is_finite())
237 && self.uv_scale.iter().all(|value| value.is_finite())
238 && self.crop.iter().all(|value| value.is_finite())
239 && (0.0..=1.0).contains(&self.reflection_occlusion)
240 }
241
242 fn occlusion_release(&self) -> u32 {
243 ((1.0 - self.reflection_occlusion) * 255.0).round() as u32
244 }
245}
246
247#[derive(Clone, Copy, Debug, PartialEq)]
248pub struct SceneWind {
249 pub current: WindFrame,
250 pub previous: WindFrame,
251}
252
253impl Default for SceneWind {
254 fn default() -> Self {
255 let calm = WindFrame {
256 time: 0.0,
257 strength: 0.0,
258 direction: [1.0, 0.0, 0.0],
259 };
260 Self {
261 current: calm,
262 previous: calm,
263 }
264 }
265}
266
267pub struct Scene<'a> {
268 pub camera: Camera,
269 pub time: f32,
270 pub seed: u32,
271 pub sun: Sun,
272 pub instances: &'a [Instance],
273 pub materials: &'a [Material],
274 pub deformers: &'a [Deformer],
275 pub wind: SceneWind,
276}
277
278pub struct FrameTargets {
279 pub hdr: OffscreenTarget,
280 pub depth: wgpu::Texture,
281 pub depth_view: wgpu::TextureView,
282 pub velocity: wgpu::Texture,
283 pub velocity_view: wgpu::TextureView,
284 pub ids: wgpu::Texture,
285 pub ids_view: wgpu::TextureView,
286 pub normal_roughness: wgpu::Texture,
287 pub normal_roughness_view: wgpu::TextureView,
288 pub width: u32,
289 pub height: u32,
290}
291
292pub(crate) struct CountedBuffer {
293 buffer: wgpu::Buffer,
294 live: Arc<AtomicUsize>,
295}
296
297impl CountedBuffer {
298 pub(crate) fn new(buffer: wgpu::Buffer, live: &Arc<AtomicUsize>) -> Self {
299 live.fetch_add(1, Ordering::Relaxed);
300 Self {
301 buffer,
302 live: live.clone(),
303 }
304 }
305}
306
307impl Deref for CountedBuffer {
308 type Target = wgpu::Buffer;
309
310 fn deref(&self) -> &wgpu::Buffer {
311 &self.buffer
312 }
313}
314
315impl Drop for CountedBuffer {
316 fn drop(&mut self) {
317 self.live.fetch_sub(1, Ordering::Relaxed);
318 }
319}
320
321struct Mesh {
322 vertices: CountedBuffer,
323 positions: CountedBuffer,
324 indices: CountedBuffer,
325 vertex_capacity: usize,
326 index_capacity: usize,
327 count: u32,
328 sway: u32,
329 sway_len: u32,
330 skin: u32,
331 skin_len: u32,
332 skin_joints: u32,
333}
334
335struct MeshSlot {
336 generation: u32,
337 mesh: Option<Mesh>,
338}
339
340fn live_mesh(meshes: &[MeshSlot], handle: MeshHandle) -> Option<&Mesh> {
341 let slot = meshes.get(handle.slot())?;
342 if slot.generation == handle.generation() {
343 slot.mesh.as_ref()
344 } else {
345 None
346 }
347}
348
349fn check_mesh(data: &MeshData<'_>) -> Result<(), String> {
350 let count = data.positions.len();
351 if count == 0 || data.indices.is_empty() || !data.indices.len().is_multiple_of(3) {
352 return Err("mesh needs vertices and triangle indices".into());
353 }
354 if data.normals.len() != count
355 || data.tangents.len() != count
356 || data.uvs.len() != count
357 || data.uvs1.is_some_and(|uvs1| uvs1.len() != count)
358 || data.alpha.is_some_and(|alpha| alpha.len() != count)
359 {
360 return Err("mesh attributes have different vertex counts".into());
361 }
362 if data.indices.iter().any(|&index| index as usize >= count) {
363 return Err("mesh index is outside the vertex buffer".into());
364 }
365 Ok(())
366}
367
368fn mesh_vertices(data: &MeshData<'_>) -> Vec<Vertex> {
369 (0..data.positions.len())
370 .map(|index| {
371 let uv1 = data.uvs1.map_or([0.0; 2], |uvs1| uvs1[index]);
372 Vertex {
373 position: [
374 data.positions[index][0],
375 data.positions[index][1],
376 data.positions[index][2],
377 1.0,
378 ],
379 normal: [
380 data.normals[index][0],
381 data.normals[index][1],
382 data.normals[index][2],
383 uv1[1],
384 ],
385 tangent: data.tangents[index],
386 uv_alpha: [
387 data.uvs[index][0],
388 data.uvs[index][1],
389 data.alpha.map_or(1.0, |alpha| alpha[index]),
390 uv1[0],
391 ],
392 }
393 })
394 .collect()
395}
396
397fn mesh_buffer(
398 device: &wgpu::Device,
399 live: &Arc<AtomicUsize>,
400 label: &str,
401 contents: &[u8],
402 usage: wgpu::BufferUsages,
403) -> CountedBuffer {
404 CountedBuffer::new(
405 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
406 label: Some(label),
407 contents,
408 usage: usage | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE,
409 }),
410 live,
411 )
412}
413
414#[derive(Clone, Debug, PartialEq, Eq)]
415pub struct Batch {
416 pub mesh: MeshHandle,
417 pub two_sided: bool,
418 pub moving: bool,
419 pub clipped: bool,
420 pub cut: bool,
421 pub shadow_only: bool,
422 pub casts_shadow: bool,
423 pub range: Range<u32>,
424}
425
426#[repr(C)]
427#[derive(Clone, Copy, Pod, Zeroable)]
428struct Vertex {
429 position: [f32; 4],
430 normal: [f32; 4],
431 tangent: [f32; 4],
432 uv_alpha: [f32; 4],
433}
434
435#[repr(C)]
436#[derive(Clone, Copy, Pod, Zeroable)]
437struct GpuSway {
438 pivot_level: [f32; 4],
439 stiffness: [f32; 4],
440}
441
442#[repr(C)]
443#[derive(Clone, Copy, Pod, Zeroable)]
444struct GpuInstance {
445 model: Matrix,
446 previous_model: Matrix,
447 normal: [[f32; 4]; 3],
448 meta: [u32; 4],
449 extra: [u32; 4],
450 clip: [[f32; 4]; 2],
451 content_uv: [f32; 4],
452 content_crop: [f32; 4],
453 content_face: [u32; 4],
454 skin: [u32; 4],
455}
456
457#[repr(C)]
458#[derive(Clone, Copy, Pod, Zeroable)]
459struct Uniforms {
460 view_projection: Matrix,
461 motion_view_projection: Matrix,
462 previous_view_projection: Matrix,
463 view: Matrix,
464 camera_position: [f32; 4],
465 sun_direction: [f32; 4],
466 sun_colour_intensity: [f32; 4],
467 time_size: [f32; 4],
468 flags: [u32; 4],
469 wind: [f32; 4],
470 wind_direction: [f32; 4],
471 previous_wind_direction: [f32; 4],
472}
473
474#[repr(C)]
475#[derive(Clone, Copy, Pod, Zeroable)]
476struct SkyLight {
477 sh: [[f32; 4]; 9],
478 info: [f32; 4],
479}
480
481#[repr(C)]
482#[derive(Clone, Copy, Pod, Zeroable)]
483struct GpuReflectionProbe {
484 center: [f32; 3],
485 layer: i32,
486 box_min: [f32; 3],
487 priority: f32,
488 box_max: [f32; 3],
489 fade: f32,
490}
491
492impl From<ProbeRecord> for GpuReflectionProbe {
493 fn from(value: ProbeRecord) -> Self {
494 Self {
495 center: value.center,
496 layer: value.layer,
497 box_min: value.box_min,
498 priority: value.priority,
499 box_max: value.box_max,
500 fade: value.fade,
501 }
502 }
503}
504
505#[repr(C)]
506#[derive(Clone, Copy, Pod, Zeroable)]
507struct ReflectionSelection {
508 first: GpuReflectionProbe,
509 second: GpuReflectionProbe,
510 weights_mip: [f32; 4],
511}
512
513fn local_candidates(records: &[ProbeRecord], camera: [f32; 3]) -> [usize; 2] {
514 let chosen = nearest_two(records, camera);
515 if chosen[0].1 > 0.0 {
516 let second = if chosen[1].1 > 0.0 {
517 chosen[1].0
518 } else {
519 chosen[0].0
520 };
521 return [chosen[0].0, second];
522 }
523 let mut order: Vec<(usize, f32)> = records
524 .iter()
525 .enumerate()
526 .map(|(index, record)| {
527 let outside = (0..3)
528 .map(|axis| {
529 let below = record.box_min[axis] - camera[axis];
530 let above = camera[axis] - record.box_max[axis];
531 below.max(above).max(0.0).powi(2)
532 })
533 .sum::<f32>();
534 (index, outside)
535 })
536 .collect();
537 order.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
538 let first = order.first().map_or(0, |entry| entry.0);
539 [first, order.get(1).map_or(first, |entry| entry.0)]
540}
541
542struct LocalReflections {
543 artifacts: Vec<ReflectionArtifact>,
544 array: ReflectionArray,
545}
546
547struct Foliage {
548 canopy: Canopy,
549 origin: [f32; 3],
550 receiver_y: f32,
551 id: u32,
552 prepass: wgpu::RenderPipeline,
553 opaque: wgpu::RenderPipeline,
554}
555
556#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
557pub struct OpaqueFeatures {
558 pub sun_shadow: bool,
559 pub soft_search: bool,
560 pub soft_filter: bool,
561 pub cascade_blend: bool,
562 pub probes: bool,
563 pub local_reflections: bool,
564 pub ssr: bool,
565 pub canopy: bool,
566 pub contact: bool,
567 pub authored_ambient: bool,
568 pub caustics: bool,
569 pub content: bool,
570 pub maps: bool,
571 pub detail: bool,
572 pub ageing: bool,
573 pub fine_noise: bool,
574 pub uv1: bool,
575 pub content_cutout: bool,
576 pub lacquer: bool,
577 pub procedural_lacquer: bool,
578 pub scratch: bool,
579 pub grime: bool,
580 pub wood: bool,
581 pub wall: bool,
582 pub fibre: bool,
583 pub crinkle: bool,
584 pub procedural_detail: bool,
585 pub film: bool,
586}
587
588const OPAQUE_FEATURES: usize = 28;
589
590impl OpaqueFeatures {
591 pub const NAMES: [&'static str; OPAQUE_FEATURES] = [
592 "sun_shadow",
593 "soft_search",
594 "soft_filter",
595 "cascade_blend",
596 "probes",
597 "local_reflections",
598 "ssr",
599 "canopy",
600 "contact",
601 "authored_ambient",
602 "caustics",
603 "content",
604 "maps",
605 "detail",
606 "ageing",
607 "fine_noise",
608 "uv1",
609 "content_cutout",
610 "lacquer",
611 "procedural_lacquer",
612 "scratch",
613 "grime",
614 "wood",
615 "wall",
616 "fibre",
617 "crinkle",
618 "procedural_detail",
619 "film",
620 ];
621
622 pub const ALL: Self = Self::from_flags([true; OPAQUE_FEATURES]);
623
624 pub const NONE: Self = Self::from_flags([false; OPAQUE_FEATURES]);
625
626 pub const fn from_flags(flags: [bool; OPAQUE_FEATURES]) -> Self {
627 Self {
628 sun_shadow: flags[0],
629 soft_search: flags[1],
630 soft_filter: flags[2],
631 cascade_blend: flags[3],
632 probes: flags[4],
633 local_reflections: flags[5],
634 ssr: flags[6],
635 canopy: flags[7],
636 contact: flags[8],
637 authored_ambient: flags[9],
638 caustics: flags[10],
639 content: flags[11],
640 maps: flags[12],
641 detail: flags[13],
642 ageing: flags[14],
643 fine_noise: flags[15],
644 uv1: flags[16],
645 content_cutout: flags[17],
646 lacquer: flags[18],
647 procedural_lacquer: flags[19],
648 scratch: flags[20],
649 grime: flags[21],
650 wood: flags[22],
651 wall: flags[23],
652 fibre: flags[24],
653 crinkle: flags[25],
654 procedural_detail: flags[26],
655 film: flags[27],
656 }
657 }
658
659 pub const fn flags(self) -> [bool; OPAQUE_FEATURES] {
660 [
661 self.sun_shadow,
662 self.soft_search,
663 self.soft_filter,
664 self.cascade_blend,
665 self.probes,
666 self.local_reflections,
667 self.ssr,
668 self.canopy,
669 self.contact,
670 self.authored_ambient,
671 self.caustics,
672 self.content,
673 self.maps,
674 self.detail,
675 self.ageing,
676 self.fine_noise,
677 self.uv1,
678 self.content_cutout,
679 self.lacquer,
680 self.procedural_lacquer,
681 self.scratch,
682 self.grime,
683 self.wood,
684 self.wall,
685 self.fibre,
686 self.crinkle,
687 self.procedural_detail,
688 self.film,
689 ]
690 }
691
692 pub fn named(name: &str) -> Option<Self> {
693 let index = Self::NAMES.iter().position(|known| *known == name)?;
694 let mut flags = [false; OPAQUE_FEATURES];
695 flags[index] = true;
696 Some(Self::from_flags(flags))
697 }
698
699 pub fn union(self, other: Self) -> Self {
700 let (a, b) = (self.flags(), other.flags());
701 Self::from_flags(std::array::from_fn(|i| a[i] || b[i]))
702 }
703
704 pub fn without(self, other: Self) -> Self {
705 let (a, b) = (self.flags(), other.flags());
706 Self::from_flags(std::array::from_fn(|i| a[i] && !b[i]))
707 }
708
709 pub fn names(self) -> Vec<&'static str> {
710 Self::NAMES
711 .iter()
712 .zip(self.flags())
713 .filter_map(|(name, on)| on.then_some(*name))
714 .collect()
715 }
716
717 fn constants(self) -> Vec<(String, f64)> {
718 Self::NAMES
719 .iter()
720 .zip(self.flags())
721 .map(|(name, on)| (format!("use_{name}"), f64::from(u8::from(on))))
722 .collect()
723 }
724}
725
726#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
727pub struct OpaqueKey {
728 pub features: OpaqueFeatures,
729 pub lit: bool,
730 pub two_sided: bool,
731}
732
733impl OpaqueKey {
734 pub fn both_sides(features: OpaqueFeatures, lit: bool) -> [Self; 2] {
735 [false, true].map(|two_sided| Self {
736 features,
737 lit,
738 two_sided,
739 })
740 }
741}
742
743pub(crate) fn opaque_pipeline(
744 device: &wgpu::Device,
745 layout: &wgpu::PipelineLayout,
746 module: &wgpu::ShaderModule,
747 cache: Option<&wgpu::PipelineCache>,
748 key: OpaqueKey,
749) -> wgpu::RenderPipeline {
750 let constants = key.features.constants();
751 let constants: Vec<(&str, f64)> = constants
752 .iter()
753 .map(|(name, value)| (name.as_str(), *value))
754 .collect();
755 let buffers = [vertex_layout()];
756 pipeline_with(
757 device,
758 PipelineSpec {
759 label: if key.lit {
760 "live opaque PBR with local lights"
761 } else {
762 "live opaque PBR"
763 },
764 layout,
765 module,
766 vertex: "vertex_main",
767 fragment: Some(if key.lit {
768 "opaque_lit_main"
769 } else {
770 "opaque_main"
771 }),
772 buffers: &buffers,
773 targets: &opaque_targets(),
774 cull: (!key.two_sided).then_some(wgpu::Face::Back),
775 write_depth: false,
776 cache,
777 },
778 &constants,
779 )
780}
781
782struct CutPipelines {
783 prepass: [wgpu::RenderPipeline; 2],
784 shadow: wgpu::RenderPipeline,
785}
786
787struct Pipelines {
788 prepass: [wgpu::RenderPipeline; 2],
789 shadow: [wgpu::RenderPipeline; 2],
790 cut: Option<CutPipelines>,
791 cut_layout: wgpu::PipelineLayout,
792 transmit: wgpu::RenderPipeline,
793 layout: wgpu::PipelineLayout,
794 module: wgpu::ShaderModule,
795 opaque: HashMap<OpaqueKey, wgpu::RenderPipeline>,
796 cache: Option<PipelineCache>,
797 pending: HashSet<OpaqueKey>,
798 arrivals: Receiver<Arrival>,
799 sender: Sender<Arrival>,
800 made_here: usize,
801 workers: usize,
802}
803
804impl Pipelines {
805 fn collect(&mut self) {
806 while let Ok(arrival) = self.arrivals.try_recv() {
807 self.arrive(arrival);
808 }
809 }
810
811 fn retarget(&mut self, module: wgpu::ShaderModule) {
812 while !self.pending.is_empty() {
813 match self.arrivals.recv() {
814 Ok((key, _)) => {
815 self.pending.remove(&key);
816 }
817 Err(_) => break,
818 }
819 }
820 self.opaque.clear();
821 self.module = module;
822 }
823
824 fn arrive(&mut self, (key, made): Arrival) {
825 self.pending.remove(&key);
826 if let Some(made) = made {
827 self.opaque.entry(key).or_insert(made);
828 }
829 }
830
831 fn ensure_cut(&mut self, device: &wgpu::Device) {
832 if self.cut.is_some() {
833 return;
834 }
835 let buffers = [vertex_layout()];
836 let prepass = prepass_pipelines(device, &self.layout, &self.module, true);
837 let shadow = pipeline(
838 device,
839 PipelineSpec {
840 label: "live cut shadow casters",
841 layout: &self.cut_layout,
842 module: &self.module,
843 vertex: "shadow_cut_vertex",
844 fragment: Some("shadow_cut_fragment"),
845 buffers: &buffers,
846 targets: &[],
847 cull: None,
848 write_depth: true,
849 cache: None,
850 },
851 );
852 self.cut = Some(CutPipelines { prepass, shadow });
853 }
854
855 fn ensure_opaque(&mut self, device: &wgpu::Device, key: OpaqueKey) {
856 self.collect();
857 while !self.opaque.contains_key(&key) && self.pending.contains(&key) {
858 match self.arrivals.recv() {
859 Ok(arrival) => self.arrive(arrival),
860 Err(_) => break,
861 }
862 }
863 if self.opaque.contains_key(&key) {
864 return;
865 }
866 let made = opaque_pipeline(
867 device,
868 &self.layout,
869 &self.module,
870 self.cache.as_ref().map(PipelineCache::cache),
871 key,
872 );
873 self.made_here += 1;
874 self.opaque.insert(key, made);
875 }
876
877 fn warm(&mut self, device: &wgpu::Device, first: &[OpaqueKey], later: &[OpaqueKey]) -> Warming {
878 self.collect();
879 let mut queued: Vec<OpaqueKey> = Vec::new();
880 let mut first_count = 0;
881 for (index, key) in first.iter().chain(later).enumerate() {
882 if self.opaque.contains_key(key) || self.pending.contains(key) || queued.contains(key) {
883 continue;
884 }
885 queued.push(*key);
886 if index < first.len() {
887 first_count += 1;
888 }
889 }
890 self.pending.extend(queued.iter().copied());
891 warm::spawn(
892 warm::Job {
893 device: device.clone(),
894 layout: self.layout.clone(),
895 module: self.module.clone(),
896 cache: self.cache.as_ref().map(|cache| cache.cache().clone()),
897 },
898 queued,
899 first_count,
900 self.workers,
901 self.sender.clone(),
902 )
903 }
904}
905
906struct Fallbacks {
907 black: wgpu::TextureView,
908 local_probes: wgpu::TextureView,
909 canopy_params: wgpu::Buffer,
910 linear: wgpu::Sampler,
911}
912
913pub struct Frame {
914 pub gpu: Gpu,
915 pub targets: FrameTargets,
916 meshes: Vec<MeshSlot>,
917 free_meshes: Vec<usize>,
918 mesh_buffers_live: Arc<AtomicUsize>,
919 mesh_epoch: u64,
920 sway_free: FreeRanges,
921 uniform: wgpu::Buffer,
922 instances: wgpu::Buffer,
923 materials: wgpu::Buffer,
924 map_materials: wgpu::Buffer,
925 sway: wgpu::Buffer,
926 sway_data: Vec<GpuSway>,
927 skins: SkinStore,
928 instance_capacity: usize,
929 material_capacity: usize,
930 scene_layout: wgpu::BindGroupLayout,
931 scene_group: wgpu::BindGroup,
932 shadow_group: wgpu::BindGroup,
933 shadow_layout: wgpu::BindGroupLayout,
934 shadow_uniform: wgpu::Buffer,
935 _shadow_atlas: wgpu::Texture,
936 _shadow_view: wgpu::TextureView,
937 _shadow_sampler: wgpu::Sampler,
938 cascade_uniform: wgpu::Buffer,
939 cascade_group: wgpu::BindGroup,
940 cascade_layout: wgpu::BindGroupLayout,
941 cascade_align: u32,
942 lights: Lights,
943 lighting_layout: wgpu::BindGroupLayout,
944 lighting_group: Option<wgpu::BindGroup>,
945 sky_light: wgpu::Buffer,
946 ambient: Ambient,
947 ambient_light: wgpu::Buffer,
948 contact: Option<ContactField>,
949 contact_uniform: wgpu::Buffer,
950 reflection_selection: wgpu::Buffer,
951 local_reflections: RefCell<Option<LocalReflections>>,
952 deformers: DeformerBuffer,
953 maps: MapTextures,
954 map_layers: [usize; 4],
955 content: RefCell<ContentSlots>,
956 content_epoch: Cell<u64>,
957 caustic: RefCell<Option<Caustic>>,
958 surfaces: RefCell<Vec<InstanceSurface>>,
959 probes: ProbeLighting,
960 sky: SkyPass,
961 ssr: Option<reflect::Reflections>,
962 viewport: [u32; 2],
963 foliage: Option<Foliage>,
964 fallbacks: Fallbacks,
965 pipelines: Pipelines,
966 canopy_gobo: Gobo,
967 shadow_sample: [u8; SAMPLE_UNIFORM_BYTES],
968 instance_features: Vec<OpaqueFeatures>,
969 reflections_planned: bool,
970 opaque_on: Cell<OpaqueFeatures>,
971 opaque_off: Cell<OpaqueFeatures>,
972 opaque_drawn: Vec<OpaqueFeatures>,
973 jitter: [f32; 2],
974 profiler: GpuProfiler,
975}
976
977pub fn multiply(a: Matrix, b: Matrix) -> Matrix {
978 let mut result = [[0.0; 4]; 4];
979 for column in 0..4 {
980 for row in 0..4 {
981 result[column][row] = (0..4).map(|k| a[k][row] * b[column][k]).sum();
982 }
983 }
984 result
985}
986
987pub fn transform(matrix: Matrix, point: [f32; 4]) -> [f32; 4] {
988 std::array::from_fn(|row| {
989 (0..4)
990 .map(|column| matrix[column][row] * point[column])
991 .sum()
992 })
993}
994
995pub fn velocity_uv(current: [f32; 4], previous: [f32; 4]) -> [f32; 2] {
996 if current[3].abs() < 1e-8 || previous[3].abs() < 1e-8 {
997 return [0.0; 2];
998 }
999 [
1000 (previous[0] / previous[3] - current[0] / current[3]) * 0.5,
1001 (current[1] / current[3] - previous[1] / previous[3]) * 0.5,
1002 ]
1003}
1004
1005pub fn velocity_pixels(current: [f32; 4], previous: [f32; 4], size: [f32; 2]) -> [f32; 2] {
1006 let uv = velocity_uv(current, previous);
1007 [uv[0] * size[0], uv[1] * size[1]]
1008}
1009
1010pub fn jitter_matrix(jitter: [f32; 2]) -> Matrix {
1011 let mut shift = [[0.0; 4]; 4];
1012 for (i, column) in shift.iter_mut().enumerate() {
1013 column[i] = 1.0;
1014 }
1015 shift[3][0] = jitter[0];
1016 shift[3][1] = jitter[1];
1017 shift
1018}
1019
1020fn same_bits(a: Matrix, b: Matrix) -> bool {
1021 a.iter()
1022 .flatten()
1023 .zip(b.iter().flatten())
1024 .all(|(x, y)| x.to_bits() == y.to_bits())
1025}
1026
1027pub fn batches(
1028 instances: &[Instance],
1029 swaying: impl Fn(MeshHandle) -> bool,
1030 clipped: impl Fn(usize) -> bool,
1031) -> Vec<Batch> {
1032 surface_batches(instances, swaying, clipped, |_| false)
1033}
1034
1035pub fn surface_batches(
1036 instances: &[Instance],
1037 swaying: impl Fn(MeshHandle) -> bool,
1038 clipped: impl Fn(usize) -> bool,
1039 cut: impl Fn(usize) -> bool,
1040) -> Vec<Batch> {
1041 let mut out: Vec<Batch> = Vec::new();
1042 for (index, instance) in instances.iter().enumerate() {
1043 let two_sided = instance.two_sided || instance.deformer != DeformerId::NONE;
1044 let moving = instance.model != instance.previous_model
1045 || instance.deformer != DeformerId::NONE
1046 || swaying(instance.mesh);
1047 let clipped = clipped(index);
1048 let cut = cut(index);
1049 let index = index as u32;
1050 if let Some(last) = out.last_mut()
1051 && last.mesh == instance.mesh
1052 && last.two_sided == two_sided
1053 && last.moving == moving
1054 && last.clipped == clipped
1055 && last.cut == cut
1056 && last.shadow_only == instance.shadow_only
1057 && last.casts_shadow == instance.casts_shadow
1058 && last.range.end == index
1059 {
1060 last.range.end += 1;
1061 continue;
1062 }
1063 out.push(Batch {
1064 mesh: instance.mesh,
1065 two_sided,
1066 moving,
1067 clipped,
1068 cut,
1069 shadow_only: instance.shadow_only,
1070 casts_shadow: instance.casts_shadow,
1071 range: index..index + 1,
1072 });
1073 }
1074 out
1075}
1076
1077fn opaque_instance_features(
1078 instance: &GpuInstance,
1079 material: &GpuMapMaterial,
1080 filmed: bool,
1081) -> OpaqueFeatures {
1082 let packed = material.seeds[2] == 3 && material.indices[3] >= 0;
1083 let unbaked = material.seeds[3] == 0;
1084 let layered = material
1085 .layers
1086 .iter()
1087 .any(|layer| layer[0] != 0.0 && layer[2] != 0.0);
1088 let bump = material.seeds[2] == 1 && material.factors[3] != 0.0;
1089 let lacquered = !packed
1090 && material
1091 .layers
1092 .iter()
1093 .any(|layer| layer[0] == 18.0 && layer[2] != 0.0);
1094 let carries = |kinds: &[f32]| {
1095 unbaked
1096 && !packed
1097 && material
1098 .layers
1099 .iter()
1100 .any(|layer| kinds.contains(&layer[0]) && layer[2] != 0.0)
1101 };
1102 let unplaced = material.content[0] < -0.5;
1103 OpaqueFeatures {
1104 contact: instance.content_face[2] & 1 != 0,
1105 caustics: instance.content_face[2] & 2 != 0,
1106 authored_ambient: instance.content_face[3] != 0,
1107 content: !unplaced,
1108 maps: material.indices.iter().any(|&index| index >= 0),
1109 detail: unbaked && ((!packed && layered) || bump),
1110 lacquer: unbaked && (lacquered || bump),
1111 scratch: carries(&[17.0]),
1112 grime: carries(&[5.0]),
1113 wood: carries(&[3.0]),
1114 wall: carries(&[4.0]),
1115 fibre: carries(&[1.0, 11.0]),
1116 crinkle: carries(&[2.0]),
1117 ageing: f32::from_bits(instance.extra[2]) > 0.0,
1118 uv1: material.seeds[3] >= 2 && material.packed[1] != 0.0,
1119 content_cutout: instance.content_face[2] & 4 != 0,
1120 film: filmed,
1121 ..OpaqueFeatures::NONE
1122 }
1123}
1124
1125fn scene_instance_features(
1126 packed: &[GpuInstance],
1127 map_materials: &[GpuMapMaterial],
1128 materials: &[Material],
1129) -> Vec<OpaqueFeatures> {
1130 packed
1131 .iter()
1132 .map(|instance| {
1133 let index = instance.meta[0] as usize;
1134 opaque_instance_features(
1135 instance,
1136 &map_materials[index],
1137 materials[index].thin_film_amount > 0.0,
1138 )
1139 })
1140 .collect()
1141}
1142
1143pub fn shadow_batches(batches: &[Batch]) -> Vec<&Batch> {
1144 batches.iter().filter(|batch| batch.casts_shadow).collect()
1145}
1146
1147pub fn transmission_batches<'a>(
1148 batches: &'a [Batch],
1149 instances: &[Instance],
1150 transmissive: impl Fn(u32) -> bool,
1151) -> Vec<&'a Batch> {
1152 batches
1153 .iter()
1154 .filter(|batch| {
1155 batch.casts_shadow
1156 && batch
1157 .range
1158 .clone()
1159 .any(|index| transmissive(instances[index as usize].material))
1160 })
1161 .collect()
1162}
1163
1164#[allow(clippy::too_many_arguments)]
1165fn draw_casters(
1166 pass: &mut wgpu::RenderPass<'_>,
1167 casters: &[&Batch],
1168 statics: bool,
1169 deformers: &wgpu::BindGroup,
1170 lighting: Option<&wgpu::BindGroup>,
1171 pipelines: &Pipelines,
1172 meshes: &[MeshSlot],
1173) {
1174 pass.set_bind_group(2, deformers, &[]);
1175 if casters.iter().any(|batch| batch.cut)
1176 && let Some(lighting) = lighting
1177 {
1178 pass.set_bind_group(3, lighting, &[]);
1179 }
1180 let mut current = None;
1181 for batch in casters.iter().filter(|batch| batch.moving != statics) {
1182 let kind = if batch.cut {
1183 2
1184 } else {
1185 usize::from(batch.clipped)
1186 };
1187 if current != Some(kind) {
1188 pass.set_pipeline(match &pipelines.cut {
1189 Some(cut) if batch.cut => &cut.shadow,
1190 _ => &pipelines.shadow[usize::from(batch.clipped)],
1191 });
1192 current = Some(kind);
1193 }
1194 let Some(mesh) = live_mesh(meshes, batch.mesh) else {
1195 continue;
1196 };
1197 pass.set_vertex_buffer(0, mesh.vertices.slice(..));
1198 pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
1199 pass.draw_indexed(0..mesh.count, 0, batch.range.clone());
1200 }
1201}
1202
1203fn content_row_key(material: &Material) -> (bool, i32) {
1204 let layer = material.content_layer;
1205 (layer.active(), layer.slot)
1206}
1207
1208fn mix_key(key: u64, other: u64) -> u64 {
1209 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1210 key.hash(&mut hasher);
1211 other.hash(&mut hasher);
1212 hasher.finish()
1213}
1214
1215pub fn static_key(instances: &[Instance], batches: &[Batch], surfaces: &[InstanceSurface]) -> u64 {
1216 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1217 for batch in shadow_batches(batches)
1218 .into_iter()
1219 .filter(|batch| !batch.moving)
1220 {
1221 batch.mesh.hash(&mut hasher);
1222 for index in batch.range.start as usize..batch.range.end as usize {
1223 for value in instances[index].model.iter().flatten() {
1224 value.to_bits().hash(&mut hasher);
1225 }
1226 if batch.clipped {
1227 for value in surfaces[index].clip.iter().flatten() {
1228 value.to_bits().hash(&mut hasher);
1229 }
1230 }
1231 }
1232 }
1233 hasher.finish()
1234}
1235
1236pub fn sky_irradiance(sh: &[[f32; 3]; 9], normal: [f32; 3]) -> [f32; 3] {
1237 let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2])
1238 .sqrt()
1239 .max(1e-8);
1240 let d = normal.map(|value| value / length);
1241 let basis = [
1242 0.282095,
1243 0.488603 * d[1],
1244 0.488603 * d[2],
1245 0.488603 * d[0],
1246 1.092548 * d[0] * d[1],
1247 1.092548 * d[1] * d[2],
1248 0.315392 * (3.0 * d[2] * d[2] - 1.0),
1249 1.092548 * d[0] * d[2],
1250 0.546274 * (d[0] * d[0] - d[1] * d[1]),
1251 ];
1252 [0, 1, 2].map(|channel| {
1253 (0..9)
1254 .map(|i| sh[i][channel] * basis[i])
1255 .sum::<f32>()
1256 .max(0.0)
1257 })
1258}
1259
1260pub fn ambient_irradiance(
1261 grid: Option<[f32; 3]>,
1262 sh: &[[f32; 3]; 9],
1263 normal: [f32; 3],
1264) -> [f32; 3] {
1265 match grid {
1266 Some(value) if value[0] >= 0.0 => value,
1267 _ => sky_irradiance(sh, normal),
1268 }
1269}
1270
1271fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
1272 [
1273 a[1] * b[2] - a[2] * b[1],
1274 a[2] * b[0] - a[0] * b[2],
1275 a[0] * b[1] - a[1] * b[0],
1276 ]
1277}
1278
1279fn normal_columns(model: Matrix) -> Result<[[f32; 4]; 3], String> {
1280 let a = [model[0][0], model[0][1], model[0][2]];
1281 let b = [model[1][0], model[1][1], model[1][2]];
1282 let c = [model[2][0], model[2][1], model[2][2]];
1283 let columns = [cross(b, c), cross(c, a), cross(a, b)];
1284 let determinant = a.iter().zip(columns[0]).map(|(x, y)| x * y).sum::<f32>();
1285 if !determinant.is_finite() || determinant.abs() < 1e-8 {
1286 return Err("instance model has a singular normal transform".into());
1287 }
1288 Ok(columns.map(|v| {
1289 [
1290 v[0] / determinant,
1291 v[1] / determinant,
1292 v[2] / determinant,
1293 0.0,
1294 ]
1295 }))
1296}
1297
1298pub fn shader_source() -> String {
1299 shader_source_with(&Gobo::default())
1300}
1301
1302pub fn shader_source_with(gobo: &Gobo) -> String {
1303 [
1304 BRDF,
1305 SHADOW_WGSL,
1306 LOD_WGSL,
1307 &deform_source(),
1308 SKIN_WGSL,
1309 TREE_SWAY_WGSL,
1310 pfx_materials::NOISE,
1311 pfx_materials::AGEING,
1312 maps::MAPS_WGSL,
1313 &maps::content_wgsl(),
1314 PROBE_WGSL,
1315 reflect::PROBE_WGSL,
1316 &canopy_light_wgsl(gobo),
1317 SHADER,
1318 LIGHTS_WGSL,
1319 ]
1320 .join("\n")
1321}
1322
1323pub fn deform_source() -> String {
1324 format!(
1325 "{}\n{}\n{}",
1326 pfx_geom::strip::ROLL_WGSL,
1327 BEND_WGSL.replace("pcg(", "bend_pcg("),
1328 deform::DEFORM_WGSL
1329 )
1330}
1331
1332pub fn card_source() -> String {
1333 format!("{SHADOW_WGSL}\n{LOD_WGSL}\n{CARD_SHADER}")
1334}
1335
1336fn target(
1337 device: &wgpu::Device,
1338 width: u32,
1339 height: u32,
1340 format: wgpu::TextureFormat,
1341 label: &str,
1342) -> (wgpu::Texture, wgpu::TextureView) {
1343 let texture = device.create_texture(&wgpu::TextureDescriptor {
1344 label: Some(label),
1345 size: wgpu::Extent3d {
1346 width,
1347 height,
1348 depth_or_array_layers: 1,
1349 },
1350 mip_level_count: 1,
1351 sample_count: 1,
1352 dimension: wgpu::TextureDimension::D2,
1353 format,
1354 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1355 | wgpu::TextureUsages::TEXTURE_BINDING
1356 | wgpu::TextureUsages::COPY_SRC,
1357 view_formats: &[],
1358 });
1359 let view = texture.create_view(&Default::default());
1360 (texture, view)
1361}
1362
1363fn storage(device: &wgpu::Device, label: &str, bytes: usize) -> wgpu::Buffer {
1364 device.create_buffer(&wgpu::BufferDescriptor {
1365 label: Some(label),
1366 size: bytes.max(16) as u64,
1367 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1368 mapped_at_creation: false,
1369 })
1370}
1371
1372fn buffer_entry(
1373 binding: u32,
1374 visibility: wgpu::ShaderStages,
1375 ty: wgpu::BufferBindingType,
1376) -> wgpu::BindGroupLayoutEntry {
1377 wgpu::BindGroupLayoutEntry {
1378 binding,
1379 visibility,
1380 ty: wgpu::BindingType::Buffer {
1381 ty,
1382 has_dynamic_offset: false,
1383 min_binding_size: None,
1384 },
1385 count: None,
1386 }
1387}
1388
1389fn texture_entry(
1390 binding: u32,
1391 view_dimension: wgpu::TextureViewDimension,
1392) -> wgpu::BindGroupLayoutEntry {
1393 wgpu::BindGroupLayoutEntry {
1394 binding,
1395 visibility: wgpu::ShaderStages::FRAGMENT,
1396 ty: wgpu::BindingType::Texture {
1397 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1398 view_dimension,
1399 multisampled: false,
1400 },
1401 count: None,
1402 }
1403}
1404
1405fn sampler_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
1406 wgpu::BindGroupLayoutEntry {
1407 binding,
1408 visibility: wgpu::ShaderStages::FRAGMENT,
1409 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1410 count: None,
1411 }
1412}
1413
1414pub fn scene_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1415 let read = wgpu::BufferBindingType::Storage { read_only: true };
1416 vec![
1417 buffer_entry(
1418 0,
1419 wgpu::ShaderStages::VERTEX_FRAGMENT,
1420 wgpu::BufferBindingType::Uniform,
1421 ),
1422 buffer_entry(1, wgpu::ShaderStages::VERTEX_FRAGMENT, read),
1423 buffer_entry(2, wgpu::ShaderStages::FRAGMENT, read),
1424 buffer_entry(3, wgpu::ShaderStages::VERTEX, read),
1425 buffer_entry(SKIN_BINDING, wgpu::ShaderStages::VERTEX, read),
1426 ]
1427}
1428
1429pub fn shadow_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1430 vec![
1431 buffer_entry(
1432 0,
1433 wgpu::ShaderStages::FRAGMENT,
1434 wgpu::BufferBindingType::Uniform,
1435 ),
1436 wgpu::BindGroupLayoutEntry {
1437 binding: 1,
1438 visibility: wgpu::ShaderStages::FRAGMENT,
1439 ty: wgpu::BindingType::Texture {
1440 sample_type: wgpu::TextureSampleType::Depth,
1441 view_dimension: wgpu::TextureViewDimension::D2Array,
1442 multisampled: false,
1443 },
1444 count: None,
1445 },
1446 wgpu::BindGroupLayoutEntry {
1447 binding: 2,
1448 visibility: wgpu::ShaderStages::FRAGMENT,
1449 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
1450 count: None,
1451 },
1452 wgpu::BindGroupLayoutEntry {
1453 binding: 4,
1454 visibility: wgpu::ShaderStages::FRAGMENT,
1455 ty: wgpu::BindingType::Texture {
1456 sample_type: wgpu::TextureSampleType::Uint,
1457 view_dimension: wgpu::TextureViewDimension::D2Array,
1458 multisampled: false,
1459 },
1460 count: None,
1461 },
1462 buffer_entry(
1463 5,
1464 wgpu::ShaderStages::FRAGMENT,
1465 wgpu::BufferBindingType::Uniform,
1466 ),
1467 wgpu::BindGroupLayoutEntry {
1468 binding: 6,
1469 visibility: wgpu::ShaderStages::FRAGMENT,
1470 ty: wgpu::BindingType::Texture {
1471 sample_type: wgpu::TextureSampleType::Depth,
1472 view_dimension: wgpu::TextureViewDimension::D2Array,
1473 multisampled: false,
1474 },
1475 count: None,
1476 },
1477 wgpu::BindGroupLayoutEntry {
1478 binding: 7,
1479 visibility: wgpu::ShaderStages::FRAGMENT,
1480 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
1481 count: None,
1482 },
1483 ]
1484}
1485
1486pub fn cascade_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1487 vec![
1488 wgpu::BindGroupLayoutEntry {
1489 binding: 3,
1490 visibility: wgpu::ShaderStages::VERTEX,
1491 ty: wgpu::BindingType::Buffer {
1492 ty: wgpu::BufferBindingType::Uniform,
1493 has_dynamic_offset: true,
1494 min_binding_size: NonZeroU64::new(CASTER_BYTES),
1495 },
1496 count: None,
1497 },
1498 buffer_entry(
1499 8,
1500 wgpu::ShaderStages::VERTEX_FRAGMENT,
1501 wgpu::BufferBindingType::Storage { read_only: true },
1502 ),
1503 ]
1504}
1505
1506pub fn deformer_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1507 (0..2)
1508 .map(|binding| {
1509 buffer_entry(
1510 binding,
1511 wgpu::ShaderStages::VERTEX,
1512 wgpu::BufferBindingType::Storage { read_only: true },
1513 )
1514 })
1515 .collect()
1516}
1517
1518pub fn card_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1519 vec![
1520 buffer_entry(
1521 0,
1522 wgpu::ShaderStages::VERTEX_FRAGMENT,
1523 wgpu::BufferBindingType::Uniform,
1524 ),
1525 buffer_entry(
1526 1,
1527 wgpu::ShaderStages::VERTEX,
1528 wgpu::BufferBindingType::Storage { read_only: true },
1529 ),
1530 texture_entry(2, wgpu::TextureViewDimension::D2),
1531 sampler_entry(3),
1532 ]
1533}
1534
1535pub fn lighting_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1536 let read = wgpu::BufferBindingType::Storage { read_only: true };
1537 let uniform = wgpu::BufferBindingType::Uniform;
1538 let fragment = wgpu::ShaderStages::FRAGMENT;
1539 vec![
1540 texture_entry(0, wgpu::TextureViewDimension::D2Array),
1541 texture_entry(1, wgpu::TextureViewDimension::D2Array),
1542 texture_entry(2, wgpu::TextureViewDimension::D2Array),
1543 texture_entry(3, wgpu::TextureViewDimension::D2Array),
1544 sampler_entry(4),
1545 buffer_entry(5, fragment, read),
1546 buffer_entry(6, fragment, read),
1547 buffer_entry(7, fragment, read),
1548 buffer_entry(8, fragment, uniform),
1549 texture_entry(9, wgpu::TextureViewDimension::D2),
1550 sampler_entry(10),
1551 buffer_entry(11, fragment, uniform),
1552 buffer_entry(12, fragment, uniform),
1553 texture_entry(13, wgpu::TextureViewDimension::Cube),
1554 texture_entry(14, wgpu::TextureViewDimension::CubeArray),
1555 sampler_entry(15),
1556 texture_entry(16, wgpu::TextureViewDimension::D2),
1557 buffer_entry(17, fragment, uniform),
1558 texture_entry(CONTENT_BINDING, wgpu::TextureViewDimension::D2),
1559 texture_entry(CONTENT_BINDING + 1, wgpu::TextureViewDimension::D2),
1560 texture_entry(CONTENT_BINDING + 2, wgpu::TextureViewDimension::D2),
1561 texture_entry(CONTENT_BINDING + 3, wgpu::TextureViewDimension::D2),
1562 sampler_entry(CONTENT_BINDING + CONTENT_SLOTS as u32),
1563 texture_entry(CONTACT_BINDING, wgpu::TextureViewDimension::D2),
1564 buffer_entry(CONTACT_BINDING + 1, fragment, uniform),
1565 buffer_entry(CONTACT_BINDING + 2, fragment, uniform),
1566 texture_entry(reflect::GUIDE_BINDING, wgpu::TextureViewDimension::D2),
1567 buffer_entry(CAUSTIC_BINDING, fragment, uniform),
1568 ]
1569}
1570
1571fn layout(
1572 device: &wgpu::Device,
1573 label: &str,
1574 entries: &[wgpu::BindGroupLayoutEntry],
1575) -> wgpu::BindGroupLayout {
1576 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1577 label: Some(label),
1578 entries,
1579 })
1580}
1581
1582fn vertex_layout() -> Option<wgpu::VertexBufferLayout<'static>> {
1583 const ATTRIBUTES: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1584 0 => Float32x4,
1585 1 => Float32x4,
1586 2 => Float32x4,
1587 3 => Float32x4,
1588 ];
1589 Some(wgpu::VertexBufferLayout {
1590 array_stride: std::mem::size_of::<Vertex>() as u64,
1591 step_mode: wgpu::VertexStepMode::Vertex,
1592 attributes: &ATTRIBUTES,
1593 })
1594}
1595
1596fn prepass_targets() -> [Option<wgpu::ColorTargetState>; 2] {
1597 [
1598 Some(wgpu::ColorTargetState {
1599 format: wgpu::TextureFormat::Rg16Float,
1600 blend: None,
1601 write_mask: wgpu::ColorWrites::ALL,
1602 }),
1603 Some(wgpu::ColorTargetState {
1604 format: wgpu::TextureFormat::R32Uint,
1605 blend: None,
1606 write_mask: wgpu::ColorWrites::ALL,
1607 }),
1608 ]
1609}
1610
1611fn opaque_targets() -> [Option<wgpu::ColorTargetState>; 2] {
1612 [
1613 Some(wgpu::ColorTargetState {
1614 format: wgpu::TextureFormat::Rgba16Float,
1615 blend: None,
1616 write_mask: wgpu::ColorWrites::ALL,
1617 }),
1618 Some(wgpu::ColorTargetState {
1619 format: NORMAL_FORMAT,
1620 blend: None,
1621 write_mask: wgpu::ColorWrites::ALL,
1622 }),
1623 ]
1624}
1625
1626struct PipelineSpec<'a> {
1627 label: &'a str,
1628 layout: &'a wgpu::PipelineLayout,
1629 module: &'a wgpu::ShaderModule,
1630 vertex: &'a str,
1631 fragment: Option<&'a str>,
1632 buffers: &'a [Option<wgpu::VertexBufferLayout<'a>>],
1633 targets: &'a [Option<wgpu::ColorTargetState>],
1634 cull: Option<wgpu::Face>,
1635 write_depth: bool,
1636 cache: Option<&'a wgpu::PipelineCache>,
1637}
1638
1639fn prepass_pipelines(
1640 device: &wgpu::Device,
1641 layout: &wgpu::PipelineLayout,
1642 module: &wgpu::ShaderModule,
1643 cut: bool,
1644) -> [wgpu::RenderPipeline; 2] {
1645 let buffers = [vertex_layout()];
1646 [Some(wgpu::Face::Back), None].map(|cull| {
1647 pipeline_with(
1648 device,
1649 PipelineSpec {
1650 label: "live depth and velocity",
1651 layout,
1652 module,
1653 vertex: "vertex_main",
1654 fragment: Some("prepass_main"),
1655 buffers: &buffers,
1656 targets: &prepass_targets(),
1657 cull,
1658 write_depth: true,
1659 cache: None,
1660 },
1661 &[("use_content_cutout", f64::from(u8::from(cut)))],
1662 )
1663 })
1664}
1665
1666fn pipeline(device: &wgpu::Device, spec: PipelineSpec<'_>) -> wgpu::RenderPipeline {
1667 pipeline_with(device, spec, &[])
1668}
1669
1670fn pipeline_with(
1671 device: &wgpu::Device,
1672 spec: PipelineSpec<'_>,
1673 constants: &[(&str, f64)],
1674) -> wgpu::RenderPipeline {
1675 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1676 label: Some(spec.label),
1677 layout: Some(spec.layout),
1678 vertex: wgpu::VertexState {
1679 module: spec.module,
1680 entry_point: Some(spec.vertex),
1681 buffers: spec.buffers,
1682 compilation_options: Default::default(),
1683 },
1684 primitive: wgpu::PrimitiveState {
1685 cull_mode: spec.cull,
1686 ..Default::default()
1687 },
1688 depth_stencil: Some(wgpu::DepthStencilState {
1689 format: wgpu::TextureFormat::Depth32Float,
1690 depth_write_enabled: Some(spec.write_depth),
1691 depth_compare: Some(if spec.write_depth {
1692 wgpu::CompareFunction::Less
1693 } else {
1694 wgpu::CompareFunction::LessEqual
1695 }),
1696 stencil: Default::default(),
1697 bias: Default::default(),
1698 }),
1699 multisample: Default::default(),
1700 fragment: spec.fragment.map(|entry| wgpu::FragmentState {
1701 module: spec.module,
1702 entry_point: Some(entry),
1703 targets: spec.targets,
1704 compilation_options: wgpu::PipelineCompilationOptions {
1705 constants,
1706 ..Default::default()
1707 },
1708 }),
1709 multiview_mask: None,
1710 cache: spec.cache,
1711 })
1712}
1713
1714fn default_sky() -> SkySource {
1715 SkySource::Analytic(AnalyticSky::new(
1716 Daylight {
1717 hour: 12.0,
1718 day: Daylight::DAY,
1719 latitude: Daylight::LATITUDE,
1720 heading: Daylight::HEADING,
1721 },
1722 REFERENCE_HOUR,
1723 2.5,
1724 [0.3; 3],
1725 ))
1726}
1727
1728fn new_targets(gpu: &Gpu, width: u32, height: u32) -> Result<FrameTargets, String> {
1729 let hdr = gpu.offscreen(width, height, wgpu::TextureFormat::Rgba16Float)?;
1730 let device = &gpu.device;
1731 let (depth, depth_view) = target(
1732 device,
1733 width,
1734 height,
1735 wgpu::TextureFormat::Depth32Float,
1736 "live depth",
1737 );
1738 let (velocity, velocity_view) = target(
1739 device,
1740 width,
1741 height,
1742 wgpu::TextureFormat::Rg16Float,
1743 "live velocity",
1744 );
1745 let (ids, ids_view) = target(
1746 device,
1747 width,
1748 height,
1749 wgpu::TextureFormat::R32Uint,
1750 "live ids",
1751 );
1752 let (normal_roughness, normal_roughness_view) =
1753 target(device, width, height, NORMAL_FORMAT, "live normals");
1754 Ok(FrameTargets {
1755 hdr,
1756 depth,
1757 depth_view,
1758 velocity,
1759 velocity_view,
1760 ids,
1761 ids_view,
1762 normal_roughness,
1763 normal_roughness_view,
1764 width,
1765 height,
1766 })
1767}
1768
1769fn solid(
1770 device: &wgpu::Device,
1771 queue: &wgpu::Queue,
1772 layers: u32,
1773 dimension: wgpu::TextureViewDimension,
1774 label: &str,
1775) -> wgpu::TextureView {
1776 let texture = device.create_texture_with_data(
1777 queue,
1778 &wgpu::TextureDescriptor {
1779 label: Some(label),
1780 size: wgpu::Extent3d {
1781 width: 1,
1782 height: 1,
1783 depth_or_array_layers: layers,
1784 },
1785 mip_level_count: 1,
1786 sample_count: 1,
1787 dimension: wgpu::TextureDimension::D2,
1788 format: wgpu::TextureFormat::Rgba16Float,
1789 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1790 view_formats: &[],
1791 },
1792 wgpu::util::TextureDataOrder::LayerMajor,
1793 &vec![0; 8 * layers as usize],
1794 );
1795 texture.create_view(&wgpu::TextureViewDescriptor {
1796 dimension: Some(dimension),
1797 ..Default::default()
1798 })
1799}
1800
1801impl Frame {
1802 pub fn new(gpu: Gpu, width: u32, height: u32) -> Result<Self, String> {
1803 if width == 0 || height == 0 {
1804 return Err("viewport is empty".into());
1805 }
1806 let targets = new_targets(&gpu, width, height)?;
1807 let device = &gpu.device;
1808 let uniform = device.create_buffer(&wgpu::BufferDescriptor {
1809 label: Some("live frame uniforms"),
1810 size: std::mem::size_of::<Uniforms>() as u64,
1811 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1812 mapped_at_creation: false,
1813 });
1814 let instances = storage(device, "live instances", std::mem::size_of::<GpuInstance>());
1815 let materials = storage(device, "live materials", PackedMaterial::SIZE);
1816 let map_materials = storage(
1817 device,
1818 "live map materials",
1819 std::mem::size_of::<GpuMapMaterial>(),
1820 );
1821 let sway = storage(device, "live sway", std::mem::size_of::<GpuSway>());
1822 let skins = SkinStore::new(device);
1823 let scene_layout = layout(device, "live scene layout", &scene_entries());
1824 let scene_group = Self::make_scene_group(
1825 device,
1826 &scene_layout,
1827 [&uniform, &instances, &materials, &sway, skins.buffer()],
1828 );
1829 let shadow_layout = layout(device, "live shadow seam", &shadow_entries());
1830 let mut fallback_sample = [0u8; SAMPLE_UNIFORM_BYTES];
1831 fallback_sample[60..64].copy_from_slice(&1.0f32.to_le_bytes());
1832 let shadow_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1833 label: Some("live shadow sample"),
1834 contents: &fallback_sample,
1835 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1836 });
1837 let shadow_atlas = device.create_texture(&wgpu::TextureDescriptor {
1838 label: Some("live fallback shadow"),
1839 size: wgpu::Extent3d {
1840 width: 1,
1841 height: 1,
1842 depth_or_array_layers: 3,
1843 },
1844 mip_level_count: 1,
1845 sample_count: 1,
1846 dimension: wgpu::TextureDimension::D2,
1847 format: wgpu::TextureFormat::Depth32Float,
1848 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
1849 view_formats: &[],
1850 });
1851 let shadow_view = shadow_atlas.create_view(&wgpu::TextureViewDescriptor {
1852 dimension: Some(wgpu::TextureViewDimension::D2Array),
1853 ..Default::default()
1854 });
1855 let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1856 compare: Some(wgpu::CompareFunction::LessEqual),
1857 ..Default::default()
1858 });
1859 let lights = Lights::new(device);
1860 let shadow_group = Self::make_shadow_group(
1861 device,
1862 &shadow_layout,
1863 &shadow_uniform,
1864 &shadow_view,
1865 &shadow_sampler,
1866 lights.empty_transmission(),
1867 &lights,
1868 );
1869 let cascade_layout = layout(device, "live cascade", &cascade_entries());
1870 let cascade_align = device.limits().min_uniform_buffer_offset_alignment.max(256);
1871 let cascade_uniform = device.create_buffer(&wgpu::BufferDescriptor {
1872 label: Some("live cascades"),
1873 size: u64::from(cascade_align) * CASTER_SLOTS as u64,
1874 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1875 mapped_at_creation: false,
1876 });
1877 let cascade_group =
1878 Self::make_cascade_group(device, &cascade_layout, &cascade_uniform, lights.table());
1879 let lighting_layout = layout(device, "live lighting", &lighting_entries());
1880 let sky_light = device.create_buffer(&wgpu::BufferDescriptor {
1881 label: Some("live sky light"),
1882 size: std::mem::size_of::<SkyLight>() as u64,
1883 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1884 mapped_at_creation: false,
1885 });
1886 let ambient_light = device.create_buffer(&wgpu::BufferDescriptor {
1887 label: Some("live authored ambient"),
1888 size: std::mem::size_of::<SkyLight>() as u64,
1889 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1890 mapped_at_creation: false,
1891 });
1892 let contact_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1893 label: Some("live contact field"),
1894 contents: bytemuck::bytes_of(&ContactUniform::off()),
1895 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1896 });
1897 let reflection_selection = device.create_buffer(&wgpu::BufferDescriptor {
1898 label: Some("live reflection selection"),
1899 size: std::mem::size_of::<ReflectionSelection>() as u64,
1900 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1901 mapped_at_creation: false,
1902 });
1903 let deformers = DeformerBuffer::new(device);
1904 let maps = MapTextures::new(device, &gpu.queue, &MapImages::default())?;
1905 let content = ContentSlots::new(device, &gpu.queue);
1906 let probes = ProbeLighting::fallback(device, &gpu.queue, [-1.0; 3]);
1907 let mut sky = SkyPass::new(
1908 device,
1909 &gpu.queue,
1910 wgpu::TextureFormat::Rgba16Float,
1911 Some(wgpu::TextureFormat::Depth32Float),
1912 default_sky(),
1913 SKY_EDGE,
1914 );
1915 while !sky.lighting.complete() {
1916 sky.update(
1917 &gpu.queue,
1918 [0.0, 0.0, -1.0],
1919 [1.0, 0.0, 0.0],
1920 [0.0, 1.0, 0.0],
1921 );
1922 }
1923 let fallbacks = Fallbacks {
1924 black: solid(
1925 device,
1926 &gpu.queue,
1927 1,
1928 wgpu::TextureViewDimension::D2,
1929 "live black",
1930 ),
1931 local_probes: solid(
1932 device,
1933 &gpu.queue,
1934 6,
1935 wgpu::TextureViewDimension::CubeArray,
1936 "live local probes",
1937 ),
1938 canopy_params: device.create_buffer(&wgpu::BufferDescriptor {
1939 label: Some("live canopy fallback"),
1940 size: 96,
1941 usage: wgpu::BufferUsages::UNIFORM,
1942 mapped_at_creation: false,
1943 }),
1944 linear: device.create_sampler(&wgpu::SamplerDescriptor {
1945 label: Some("live reflection sampler"),
1946 mag_filter: wgpu::FilterMode::Linear,
1947 min_filter: wgpu::FilterMode::Linear,
1948 mipmap_filter: wgpu::MipmapFilterMode::Linear,
1949 ..Default::default()
1950 }),
1951 };
1952 let pipelines = Self::make_pipelines(
1953 device,
1954 [
1955 &scene_layout,
1956 &shadow_layout,
1957 &deformers.layout,
1958 &lighting_layout,
1959 ],
1960 &cascade_layout,
1961 );
1962 let profiler = GpuProfiler::new(device, &gpu.queue);
1963 Ok(Self {
1964 targets,
1965 meshes: Vec::new(),
1966 uniform,
1967 instances,
1968 materials,
1969 map_materials,
1970 sway,
1971 sway_data: Vec::new(),
1972 skins,
1973 free_meshes: Vec::new(),
1974 mesh_buffers_live: Arc::new(AtomicUsize::new(0)),
1975 mesh_epoch: 0,
1976 sway_free: FreeRanges::default(),
1977 instance_capacity: 1,
1978 material_capacity: 1,
1979 scene_layout,
1980 scene_group,
1981 shadow_group,
1982 shadow_layout,
1983 shadow_uniform,
1984 _shadow_atlas: shadow_atlas,
1985 _shadow_view: shadow_view,
1986 _shadow_sampler: shadow_sampler,
1987 cascade_uniform,
1988 cascade_group,
1989 cascade_layout,
1990 cascade_align,
1991 lights,
1992 lighting_layout,
1993 lighting_group: None,
1994 sky_light,
1995 ambient: Ambient::Sky,
1996 ambient_light,
1997 contact: None,
1998 contact_uniform,
1999 reflection_selection,
2000 local_reflections: RefCell::new(None),
2001 deformers,
2002 maps,
2003 map_layers: [0; 4],
2004 content: RefCell::new(content),
2005 content_epoch: Cell::new(0),
2006 caustic: RefCell::new(None),
2007 surfaces: RefCell::new(Vec::new()),
2008 probes,
2009 sky,
2010 ssr: None,
2011 viewport: [width, height],
2012 foliage: None,
2013 fallbacks,
2014 pipelines,
2015 canopy_gobo: Gobo::default(),
2016 shadow_sample: fallback_sample,
2017 instance_features: Vec::new(),
2018 reflections_planned: false,
2019 opaque_on: Cell::new(OpaqueFeatures::NONE),
2020 opaque_off: Cell::new(OpaqueFeatures::NONE),
2021 opaque_drawn: Vec::new(),
2022 jitter: [0.0; 2],
2023 profiler,
2024 gpu,
2025 })
2026 }
2027
2028 fn make_pipelines(
2029 device: &wgpu::Device,
2030 layouts: [&wgpu::BindGroupLayout; 4],
2031 cascade: &wgpu::BindGroupLayout,
2032 ) -> Pipelines {
2033 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2034 label: Some("live frame layout"),
2035 bind_group_layouts: &layouts.map(Some),
2036 immediate_size: 0,
2037 });
2038 let shadow_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2039 label: Some("live shadow layout"),
2040 bind_group_layouts: &[Some(layouts[0]), Some(cascade), Some(layouts[2])],
2041 immediate_size: 0,
2042 });
2043 let cut_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2044 label: Some("live cut shadow layout"),
2045 bind_group_layouts: &[
2046 Some(layouts[0]),
2047 Some(cascade),
2048 Some(layouts[2]),
2049 Some(layouts[3]),
2050 ],
2051 immediate_size: 0,
2052 });
2053 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2054 label: Some("live frame shader"),
2055 source: wgpu::ShaderSource::Wgsl(shader_source().into()),
2056 });
2057 let buffers = [vertex_layout()];
2058 let prepass = prepass_pipelines(device, &pipeline_layout, &module, false);
2059 let shadow = [
2060 ("shadow_vertex", None),
2061 ("shadow_clip_vertex", Some("shadow_clip_fragment")),
2062 ]
2063 .map(|(vertex, fragment)| {
2064 pipeline(
2065 device,
2066 PipelineSpec {
2067 label: "live shadow casters",
2068 layout: &shadow_layout,
2069 module: &module,
2070 vertex,
2071 fragment,
2072 buffers: &buffers,
2073 targets: &[],
2074 cull: None,
2075 write_depth: true,
2076 cache: None,
2077 },
2078 )
2079 });
2080 let transmit = pipeline(
2081 device,
2082 PipelineSpec {
2083 label: "live transmissive casters",
2084 layout: &shadow_layout,
2085 module: &module,
2086 vertex: "transmit_vertex",
2087 fragment: Some("transmit_fragment"),
2088 buffers: &buffers,
2089 targets: &[Some(wgpu::ColorTargetState {
2090 format: crate::shadow::TRANSMISSION_FORMAT,
2091 blend: None,
2092 write_mask: wgpu::ColorWrites::ALL,
2093 })],
2094 cull: None,
2095 write_depth: true,
2096 cache: None,
2097 },
2098 );
2099 let (sender, arrivals) = channel();
2100 Pipelines {
2101 prepass,
2102 shadow,
2103 cut: None,
2104 cut_layout,
2105 transmit,
2106 layout: pipeline_layout,
2107 module,
2108 opaque: HashMap::new(),
2109 cache: None,
2110 pending: HashSet::new(),
2111 arrivals,
2112 sender,
2113 made_here: 0,
2114 workers: warm::default_workers(),
2115 }
2116 }
2117
2118 fn make_cascade_group(
2119 device: &wgpu::Device,
2120 layout: &wgpu::BindGroupLayout,
2121 uniform: &wgpu::Buffer,
2122 tints: &wgpu::Buffer,
2123 ) -> wgpu::BindGroup {
2124 device.create_bind_group(&wgpu::BindGroupDescriptor {
2125 label: Some("live cascades"),
2126 layout,
2127 entries: &[
2128 wgpu::BindGroupEntry {
2129 binding: 3,
2130 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2131 buffer: uniform,
2132 offset: 0,
2133 size: NonZeroU64::new(CASTER_BYTES),
2134 }),
2135 },
2136 wgpu::BindGroupEntry {
2137 binding: 8,
2138 resource: tints.as_entire_binding(),
2139 },
2140 ],
2141 })
2142 }
2143
2144 fn make_scene_group(
2145 device: &wgpu::Device,
2146 layout: &wgpu::BindGroupLayout,
2147 buffers: [&wgpu::Buffer; 5],
2148 ) -> wgpu::BindGroup {
2149 let entries: Vec<wgpu::BindGroupEntry<'_>> = buffers
2150 .iter()
2151 .enumerate()
2152 .map(|(binding, buffer)| wgpu::BindGroupEntry {
2153 binding: binding as u32,
2154 resource: buffer.as_entire_binding(),
2155 })
2156 .collect();
2157 device.create_bind_group(&wgpu::BindGroupDescriptor {
2158 label: Some("live scene"),
2159 layout,
2160 entries: &entries,
2161 })
2162 }
2163
2164 fn rebuild_scene_group(&mut self) {
2165 self.scene_group = Self::make_scene_group(
2166 &self.gpu.device,
2167 &self.scene_layout,
2168 [
2169 &self.uniform,
2170 &self.instances,
2171 &self.materials,
2172 &self.sway,
2173 self.skins.buffer(),
2174 ],
2175 );
2176 }
2177
2178 fn make_shadow_group(
2179 device: &wgpu::Device,
2180 layout: &wgpu::BindGroupLayout,
2181 uniform: &wgpu::Buffer,
2182 view: &wgpu::TextureView,
2183 sampler: &wgpu::Sampler,
2184 transmission: &wgpu::TextureView,
2185 lights: &Lights,
2186 ) -> wgpu::BindGroup {
2187 device.create_bind_group(&wgpu::BindGroupDescriptor {
2188 label: Some("live shadow sample"),
2189 layout,
2190 entries: &[
2191 wgpu::BindGroupEntry {
2192 binding: 0,
2193 resource: uniform.as_entire_binding(),
2194 },
2195 wgpu::BindGroupEntry {
2196 binding: 1,
2197 resource: wgpu::BindingResource::TextureView(view),
2198 },
2199 wgpu::BindGroupEntry {
2200 binding: 2,
2201 resource: wgpu::BindingResource::Sampler(sampler),
2202 },
2203 wgpu::BindGroupEntry {
2204 binding: 4,
2205 resource: wgpu::BindingResource::TextureView(transmission),
2206 },
2207 wgpu::BindGroupEntry {
2208 binding: 5,
2209 resource: lights.uniform().as_entire_binding(),
2210 },
2211 wgpu::BindGroupEntry {
2212 binding: 6,
2213 resource: wgpu::BindingResource::TextureView(lights.faces_view()),
2214 },
2215 wgpu::BindGroupEntry {
2216 binding: 7,
2217 resource: wgpu::BindingResource::Sampler(lights.sampler()),
2218 },
2219 ],
2220 })
2221 }
2222
2223 pub fn set_shadows(&mut self, shadows: &Shadows, fit: &Fit) {
2224 self.shadow_sample = shadows.sample_uniform(fit);
2225 self.gpu
2226 .queue
2227 .write_buffer(&self.shadow_uniform, 0, &self.shadow_sample);
2228 self.shadow_group = Self::make_shadow_group(
2229 &self.gpu.device,
2230 &self.shadow_layout,
2231 &self.shadow_uniform,
2232 shadows.atlas_view(),
2233 shadows.sampler(),
2234 shadows
2235 .transmission_view()
2236 .unwrap_or(self.lights.empty_transmission()),
2237 &self.lights,
2238 );
2239 }
2240
2241 pub fn set_probe_corner_floor(&self, floor: f32) -> Result<(), String> {
2242 self.probes.set_corner_floor(&self.gpu.queue, floor)
2243 }
2244
2245 pub fn probe_corner_floor(&self) -> f32 {
2246 self.probes.corner_floor()
2247 }
2248
2249 pub fn set_reflection_occlusion(&mut self, threshold: f32) -> Result<(), String> {
2250 self.probes
2251 .set_reflection_occlusion(&self.gpu.queue, threshold)
2252 }
2253
2254 pub fn set_sky_visibility(&mut self, on: bool) {
2255 self.probes.set_sky_visibility(&self.gpu.queue, on);
2256 }
2257
2258 pub fn set_opaque_on(&self, on: OpaqueFeatures) {
2259 self.opaque_on.set(on);
2260 }
2261
2262 pub fn set_opaque_off(&self, off: OpaqueFeatures) {
2263 self.opaque_off.set(off);
2264 }
2265
2266 pub fn warm_opaque(&mut self, features: OpaqueFeatures, lit: bool) {
2267 for key in OpaqueKey::both_sides(features, lit) {
2268 self.pipelines.ensure_opaque(&self.gpu.device, key);
2269 }
2270 }
2271
2272 pub fn warm(&mut self, first: &[OpaqueKey], later: &[OpaqueKey]) -> Warming {
2273 self.pipelines.warm(&self.gpu.device, first, later)
2274 }
2275
2276 pub fn set_warm_workers(&mut self, workers: usize) {
2277 self.pipelines.workers = workers.max(1);
2278 }
2279
2280 pub fn opaque_pipelines(&self) -> Vec<OpaqueKey> {
2281 self.pipelines.opaque.keys().copied().collect()
2282 }
2283
2284 pub fn opaque_made_on_render_thread(&self) -> usize {
2285 self.pipelines.made_here
2286 }
2287
2288 pub fn use_pipeline_cache(&mut self, path: &std::path::Path) -> CacheLoad {
2289 let (cache, load) = PipelineCache::open(&self.gpu.device, &self.gpu.info, path);
2290 if cache.is_some() {
2291 self.pipelines.cache = cache;
2292 }
2293 load
2294 }
2295
2296 pub(crate) fn pipeline_cache(&self) -> Option<&wgpu::PipelineCache> {
2297 self.pipelines.cache.as_ref().map(PipelineCache::cache)
2298 }
2299
2300 pub fn save_pipeline_cache(&self) -> Result<Option<usize>, String> {
2301 self.pipelines
2302 .cache
2303 .as_ref()
2304 .map_or(Ok(None), |cache| cache.save().map(Some))
2305 }
2306
2307 pub fn set_reflections_planned(&mut self, planned: bool) {
2308 self.reflections_planned = planned;
2309 }
2310
2311 pub fn opaque_keys(
2312 &self,
2313 scene: &Scene<'_>,
2314 shadows: Option<(&Shadows, &Fit)>,
2315 ) -> Result<Vec<OpaqueKey>, String> {
2316 if scene.materials.is_empty() {
2317 return Err("scene needs at least one material".into());
2318 }
2319 let surfaces = self.scene_surfaces(scene.instances.len());
2320 let packed = self.pack_instances(scene, &surfaces, &self.skins.layout())?;
2321 let map_materials = self.pack_map_materials(scene);
2322 let instance_features = scene_instance_features(&packed, &map_materials, scene.materials);
2323 let batches = surface_batches(
2324 scene.instances,
2325 |mesh| self.deforms(mesh),
2326 |index| surfaces[index].clipped(),
2327 |index| surfaces[index].cutout,
2328 );
2329 let sample = shadows.map_or(self.shadow_sample, |(shadows, fit)| {
2330 shadows.sample_uniform(fit)
2331 });
2332 let features = self.batch_features(
2333 &batches,
2334 self.scene_features_from(&sample),
2335 &instance_features,
2336 );
2337 let lit = self.lights.extended();
2338 let mut keys: Vec<OpaqueKey> = Vec::new();
2339 for (batch, features) in batches.iter().zip(features) {
2340 let key = OpaqueKey {
2341 features,
2342 lit,
2343 two_sided: batch.two_sided,
2344 };
2345 if !batch.shadow_only && !keys.contains(&key) {
2346 keys.push(key);
2347 }
2348 }
2349 Ok(keys)
2350 }
2351
2352 pub fn opaque_drawn(&self) -> &[OpaqueFeatures] {
2353 &self.opaque_drawn
2354 }
2355
2356 pub fn scene_features(&self) -> OpaqueFeatures {
2357 self.scene_features_from(&self.shadow_sample)
2358 }
2359
2360 fn scene_features_from(&self, sample: &[u8; SAMPLE_UNIFORM_BYTES]) -> OpaqueFeatures {
2361 let read = |offset: usize| {
2362 f32::from_le_bytes(sample[offset..offset + 4].try_into().expect("four bytes"))
2363 };
2364 let unshadowed = read(60) > 0.5;
2365 let shadowed = !unshadowed;
2366 let soft = shadowed && read(256) > 0.0;
2367 OpaqueFeatures {
2368 sun_shadow: shadowed,
2369 soft_search: soft,
2370 soft_filter: soft,
2371 cascade_blend: shadowed && read(28) > 0.0,
2372 probes: self.probes.live(),
2373 local_reflections: self.local_reflections.borrow().is_some(),
2374 ssr: self.ssr.is_some() || self.reflections_planned,
2375 canopy: self.foliage.is_some(),
2376 ..OpaqueFeatures::NONE
2377 }
2378 }
2379
2380 pub fn opaque_features(&self, batches: &[Batch]) -> Vec<OpaqueFeatures> {
2381 self.batch_features(batches, self.scene_features(), &self.instance_features)
2382 }
2383
2384 fn batch_features(
2385 &self,
2386 batches: &[Batch],
2387 scene: OpaqueFeatures,
2388 instance_features: &[OpaqueFeatures],
2389 ) -> Vec<OpaqueFeatures> {
2390 batches
2391 .iter()
2392 .map(|batch| {
2393 let used = batch
2394 .range
2395 .clone()
2396 .filter_map(|index| instance_features.get(index as usize))
2397 .fold(scene, |all, features| all.union(*features));
2398 used.union(self.opaque_on.get())
2399 .without(self.opaque_off.get())
2400 })
2401 .collect()
2402 }
2403
2404 fn set_unshadowed(&mut self) {
2405 self.shadow_group = Self::make_shadow_group(
2406 &self.gpu.device,
2407 &self.shadow_layout,
2408 &self.shadow_uniform,
2409 &self._shadow_view,
2410 &self._shadow_sampler,
2411 self.lights.empty_transmission(),
2412 &self.lights,
2413 );
2414 }
2415
2416 pub fn set_lights(&mut self, lights: &[LocalLight]) -> Result<(), String> {
2417 self.lights.set(lights)
2418 }
2419
2420 pub fn set_light_quality(&mut self, quality: LightQuality) -> Result<(), String> {
2421 self.lights.set_quality(quality)
2422 }
2423
2424 pub fn set_transmission(&mut self, tints: &[Option<[f32; 3]>]) -> Result<(), String> {
2425 self.lights.set_transmission(tints)
2426 }
2427
2428 pub fn lights(&self) -> &Lights {
2429 &self.lights
2430 }
2431
2432 pub fn set_probe_emission(&mut self, scale: f32) -> Result<(), String> {
2433 self.probes.set_emission(&self.gpu.queue, scale)
2434 }
2435
2436 pub fn set_jitter(&mut self, jitter: [f32; 2]) {
2437 self.jitter = jitter;
2438 }
2439
2440 pub fn set_reflections(&mut self, history: Option<reflect::Reflections>) {
2441 self.ssr = history;
2442 }
2443
2444 pub fn set_local_reflections(
2445 &self,
2446 artifacts: Option<Vec<ReflectionArtifact>>,
2447 hour: f32,
2448 ) -> Result<(), String> {
2449 let reflections = artifacts
2450 .map(|artifacts| {
2451 let array =
2452 upload_reflections(&self.gpu.device, &self.gpu.queue, &artifacts, hour)?;
2453 Ok::<_, String>(LocalReflections { artifacts, array })
2454 })
2455 .transpose()?;
2456 *self.local_reflections.borrow_mut() = reflections;
2457 Ok(())
2458 }
2459
2460 pub fn set_local_reflection_hour(&self, hour: f32) -> Result<(), String> {
2461 let mut reflections = self.local_reflections.borrow_mut();
2462 if let Some(ref mut current) = *reflections {
2463 current.array =
2464 upload_reflections(&self.gpu.device, &self.gpu.queue, ¤t.artifacts, hour)?;
2465 }
2466 Ok(())
2467 }
2468
2469 pub fn set_sky(&mut self, source: SkySource, settle: bool) -> u64 {
2470 if !settle {
2471 return self.sky.request_source(source);
2472 }
2473 self.sky
2474 .set_source(&self.gpu.device, &self.gpu.queue, source);
2475 while !self.sky.lighting.complete() {
2476 self.sky.update(
2477 &self.gpu.queue,
2478 [0.0, 0.0, -1.0],
2479 [1.0, 0.0, 0.0],
2480 [0.0, 1.0, 0.0],
2481 );
2482 }
2483 self.sky.shown()
2484 }
2485
2486 pub fn sky(&self) -> &SkyPass {
2487 &self.sky
2488 }
2489
2490 pub fn sky_mut(&mut self) -> &mut SkyPass {
2491 &mut self.sky
2492 }
2493
2494 pub fn set_ambient(&mut self, ambient: Ambient) -> Result<(), String> {
2495 if !ambient.valid() {
2496 return Err("an ambient must be finite, and a constant nonnegative".into());
2497 }
2498 self.ambient = ambient;
2499 Ok(())
2500 }
2501
2502 pub fn ambient(&self) -> Ambient {
2503 self.ambient
2504 }
2505
2506 pub fn set_contact(&mut self, desc: Option<ContactDesc>) -> Result<(), String> {
2507 let uniform = match desc {
2508 Some(desc) => {
2509 self.contact = Some(ContactField::new(&self.gpu.device, &self.gpu.queue, desc)?);
2510 ContactUniform::on(&desc)
2511 }
2512 None => {
2513 self.contact = None;
2514 ContactUniform::off()
2515 }
2516 };
2517 self.gpu
2518 .queue
2519 .write_buffer(&self.contact_uniform, 0, bytemuck::bytes_of(&uniform));
2520 Ok(())
2521 }
2522
2523 pub fn contact(&self) -> Option<&ContactField> {
2524 self.contact.as_ref()
2525 }
2526
2527 pub fn write_contact(&self, values: &[f32]) -> Result<(), String> {
2528 match &self.contact {
2529 Some(field) => field.write(&self.gpu.queue, values),
2530 None => Err("no contact field is set".into()),
2531 }
2532 }
2533
2534 pub fn set_probes(&mut self, probes: ProbeLighting) {
2535 self.probes = probes;
2536 self.probes.set_fallback(&self.gpu.queue, [-1.0; 3]);
2537 }
2538
2539 pub fn set_probe_hour(&mut self, hour: f32) -> Result<(), String> {
2540 self.set_local_reflection_hour(hour)?;
2541 if self.probes.manifest.is_none() {
2542 return Ok(());
2543 }
2544 self.probes
2545 .set_hour(&self.gpu.device, &self.gpu.queue, hour)
2546 }
2547
2548 pub fn probes(&self) -> &ProbeLighting {
2549 &self.probes
2550 }
2551
2552 pub fn set_maps(&mut self, images: &MapImages<'_>) -> Result<(), String> {
2553 let filter = self.maps.filter;
2554 self.maps = MapTextures::new(&self.gpu.device, &self.gpu.queue, images)?;
2555 self.maps.set_filter(&self.gpu.device, filter);
2556 self.map_layers = [
2557 images.base.len(),
2558 images.normal.len(),
2559 images.roughness.len(),
2560 images.metal.len(),
2561 ];
2562 Ok(())
2563 }
2564
2565 pub fn set_detail(&mut self, atlas: &Atlas) -> Result<(), String> {
2566 let filter = self.maps.filter;
2567 self.maps = MapTextures::detail(&self.gpu.device, &self.gpu.queue, atlas)?;
2568 self.maps.set_filter(&self.gpu.device, filter);
2569 let parts = atlas.parts.len();
2570 self.map_layers = [parts, parts, parts, 0];
2571 Ok(())
2572 }
2573
2574 pub fn set_texture_filter(&mut self, filter: maps::TextureFilter) -> Result<(), String> {
2575 filter.check()?;
2576 let anisotropy = self.maps.filter.anisotropy;
2577 self.maps.set_filter(&self.gpu.device, filter);
2578 if filter.anisotropy != anisotropy {
2579 self.content
2580 .borrow_mut()
2581 .set_filter(&self.gpu.device, filter);
2582 }
2583 Ok(())
2584 }
2585
2586 pub fn texture_filter(&self) -> maps::TextureFilter {
2587 self.maps.filter
2588 }
2589
2590 pub fn maps(&self) -> &MapTextures {
2591 &self.maps
2592 }
2593
2594 pub fn set_content(
2595 &self,
2596 slot: usize,
2597 width: u32,
2598 height: u32,
2599 format: ContentFormat,
2600 ) -> Result<(), String> {
2601 self.content_changed();
2602 self.content
2603 .borrow_mut()
2604 .allocate(&self.gpu.device, slot, width, height, format)
2605 }
2606
2607 pub fn write_content(&self, slot: usize, rect: [u32; 4], bytes: &[u8]) -> Result<(), String> {
2608 self.content_changed();
2609 self.content
2610 .borrow_mut()
2611 .write(&self.gpu.device, &self.gpu.queue, slot, rect, bytes)
2612 }
2613
2614 fn content_changed(&self) {
2615 self.content_epoch
2616 .set(self.content_epoch.get().wrapping_add(1));
2617 }
2618
2619 pub fn copy_content(
2620 &self,
2621 encoder: &mut wgpu::CommandEncoder,
2622 slot: usize,
2623 source: &wgpu::Texture,
2624 ) -> Result<maps::ContentCopy, String> {
2625 self.content_changed();
2626 self.content
2627 .borrow_mut()
2628 .copy(&self.gpu.device, encoder, slot, source)
2629 }
2630
2631 pub fn regenerate_content(
2632 &self,
2633 encoder: &mut wgpu::CommandEncoder,
2634 slot: usize,
2635 ) -> Result<(), String> {
2636 self.content_changed();
2637 self.content.borrow_mut().regenerate(encoder, slot)
2638 }
2639
2640 pub fn regenerate_content_regions(
2641 &self,
2642 encoder: &mut wgpu::CommandEncoder,
2643 slot: usize,
2644 regions: &[maps::MipRegion],
2645 ) -> Result<(), String> {
2646 self.content_changed();
2647 self.content
2648 .borrow_mut()
2649 .regenerate_regions(&self.gpu.device, encoder, slot, regions)
2650 }
2651
2652 pub fn set_caustic(&self, caustic: Option<Caustic>) -> Result<(), String> {
2653 if let Some(caustic) = &caustic {
2654 caustic.check()?;
2655 }
2656 *self.caustic.borrow_mut() = caustic;
2657 Ok(())
2658 }
2659
2660 pub fn caustic(&self) -> Option<Caustic> {
2661 self.caustic.borrow().clone()
2662 }
2663
2664 pub fn release_content(&self, slot: usize) {
2665 self.content_changed();
2666 self.content.borrow_mut().release(slot);
2667 }
2668
2669 pub fn content(&self) -> std::cell::Ref<'_, ContentSlots> {
2670 self.content.borrow()
2671 }
2672
2673 pub fn set_surfaces(&self, surfaces: &[InstanceSurface]) -> Result<(), String> {
2674 if surfaces.iter().any(|surface| !surface.valid()) {
2675 return Err(
2676 "instance surface planes, offsets and crops must be finite and reflection occlusion between 0 and 1"
2677 .into(),
2678 );
2679 }
2680 let mut current = self.surfaces.borrow_mut();
2681 current.clear();
2682 current.extend_from_slice(surfaces);
2683 Ok(())
2684 }
2685
2686 pub fn surfaces(&self) -> std::cell::Ref<'_, Vec<InstanceSurface>> {
2687 self.surfaces.borrow()
2688 }
2689
2690 pub fn lit_bindings(&self) -> Option<crate::text::LitBindings<'_>> {
2691 Some(crate::text::LitBindings {
2692 scene: &self.scene_group,
2693 shadow: &self.shadow_group,
2694 lighting: self.lighting_group.as_ref()?,
2695 deformers: self.deformers.buffer(),
2696 plan: self.deformers.plan_buffer(),
2697 })
2698 }
2699
2700 pub fn deformer_buffer(&self) -> &DeformerBuffer {
2701 &self.deformers
2702 }
2703
2704 pub fn has_tree(&self) -> bool {
2705 self.foliage.is_some()
2706 }
2707
2708 pub fn set_canopy_mode(&mut self, mode: CanopyMode) -> Result<(), String> {
2709 let foliage = self.foliage.as_mut().ok_or("no tree is set")?;
2710 foliage.canopy.set_mode(mode);
2711 Ok(())
2712 }
2713
2714 pub fn set_canopy_sharpness(&mut self, sharpness: f32) -> Result<(), String> {
2715 let foliage = self.foliage.as_mut().ok_or("no tree is set")?;
2716 foliage.canopy.set_sharpness(sharpness)
2717 }
2718
2719 pub fn set_canopy_strength(&mut self, strength: f32) -> Result<(), String> {
2720 let foliage = self.foliage.as_mut().ok_or("no tree is set")?;
2721 foliage.canopy.set_strength(strength)
2722 }
2723
2724 pub fn set_canopy_gobo(&mut self, gobo: Gobo) -> Result<(), String> {
2725 gobo.validate()?;
2726 if gobo == self.canopy_gobo {
2727 return Ok(());
2728 }
2729 let module = self
2730 .gpu
2731 .device
2732 .create_shader_module(wgpu::ShaderModuleDescriptor {
2733 label: Some("live frame shader"),
2734 source: wgpu::ShaderSource::Wgsl(shader_source_with(&gobo).into()),
2735 });
2736 self.pipelines.retarget(module);
2737 self.canopy_gobo = gobo;
2738 if let Some(foliage) = self.foliage.as_mut() {
2739 foliage.canopy.set_weight(gobo.weight);
2740 }
2741 Ok(())
2742 }
2743
2744 pub fn canopy_gobo(&self) -> Gobo {
2745 self.canopy_gobo
2746 }
2747
2748 pub fn visible_card_count(&self) -> u32 {
2749 self.foliage
2750 .as_ref()
2751 .map_or(0, |foliage| foliage.canopy.card_count)
2752 }
2753
2754 pub fn set_tree(
2755 &mut self,
2756 tree: &Tree,
2757 origin: [f32; 3],
2758 receiver_y: f32,
2759 id: u32,
2760 ) -> Result<MeshHandle, String> {
2761 let wood = &tree.wood;
2762 let handle = self.upload_swaying_mesh(
2763 MeshData {
2764 positions: &wood.positions,
2765 normals: &wood.normals,
2766 tangents: &wood.tangents,
2767 uvs: &wood.uvs,
2768 uvs1: None,
2769 alpha: None,
2770 indices: &wood.indices,
2771 },
2772 &tree.sway,
2773 )?;
2774 let device = &self.gpu.device;
2775 let mut canopy = Canopy::new(
2776 device,
2777 &self.gpu.queue,
2778 tree,
2779 crate::canopy::DEFAULT_RESOLUTION,
2780 );
2781 canopy.set_weight(self.canopy_gobo.weight);
2782 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2783 label: Some("live cards layout"),
2784 bind_group_layouts: &[
2785 Some(&self.scene_layout),
2786 Some(&self.shadow_layout),
2787 Some(canopy.cookie_layout()),
2788 Some(&self.lighting_layout),
2789 ],
2790 immediate_size: 0,
2791 });
2792 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2793 label: Some("live cards"),
2794 source: wgpu::ShaderSource::Wgsl(card_source().into()),
2795 });
2796 let prepass = pipeline(
2797 device,
2798 PipelineSpec {
2799 label: "live card depth",
2800 layout: &pipeline_layout,
2801 module: &module,
2802 vertex: "card_vertex",
2803 fragment: Some("card_prepass"),
2804 buffers: &[],
2805 targets: &prepass_targets(),
2806 cull: None,
2807 write_depth: true,
2808 cache: None,
2809 },
2810 );
2811 let opaque = pipeline(
2812 device,
2813 PipelineSpec {
2814 label: "live card shading",
2815 layout: &pipeline_layout,
2816 module: &module,
2817 vertex: "card_vertex",
2818 fragment: Some("card_opaque"),
2819 buffers: &[],
2820 targets: &opaque_targets(),
2821 cull: None,
2822 write_depth: false,
2823 cache: None,
2824 },
2825 );
2826 self.foliage = Some(Foliage {
2827 canopy,
2828 origin,
2829 receiver_y,
2830 id,
2831 prepass,
2832 opaque,
2833 });
2834 Ok(handle)
2835 }
2836
2837 pub fn upload_mesh(&mut self, data: MeshData<'_>) -> Result<MeshHandle, String> {
2838 self.upload(data, None, None)
2839 }
2840
2841 pub fn upload_skinned_mesh(
2842 &mut self,
2843 data: MeshData<'_>,
2844 skin: SkinData<'_>,
2845 ) -> Result<MeshHandle, String> {
2846 if skin.joints.len() != data.positions.len() || skin.weights.len() != data.positions.len() {
2847 return Err("skin joints and weights need one entry per vertex".into());
2848 }
2849 if skin
2850 .weights
2851 .iter()
2852 .flatten()
2853 .any(|weight| !weight.is_finite())
2854 {
2855 return Err("a skin weight is not finite".into());
2856 }
2857 self.upload(data, None, Some(skin))
2858 }
2859
2860 pub fn upload_swaying_mesh(
2861 &mut self,
2862 data: MeshData<'_>,
2863 sway: &[SwayVertex],
2864 ) -> Result<MeshHandle, String> {
2865 if sway.len() != data.positions.len() {
2866 return Err("sway attributes need one entry per vertex".into());
2867 }
2868 self.upload(data, Some(sway), None)
2869 }
2870
2871 fn upload(
2872 &mut self,
2873 data: MeshData<'_>,
2874 sway: Option<&[SwayVertex]>,
2875 skin: Option<SkinData<'_>>,
2876 ) -> Result<MeshHandle, String> {
2877 check_mesh(&data)?;
2878 let slot = match self.free_meshes.last() {
2879 Some(&slot) => slot,
2880 None if self.meshes.len() <= MESH_SLOT_MASK as usize => self.meshes.len(),
2881 None => return Err("too many meshes".into()),
2882 };
2883 let generation = self.meshes.get(slot).map_or(0, |slot| slot.generation);
2884 let vertices = mesh_vertices(&data);
2885 let device = &self.gpu.device;
2886 let live = &self.mesh_buffers_live;
2887 let vertex_buffer = mesh_buffer(
2888 device,
2889 live,
2890 "live mesh vertices",
2891 bytemuck::cast_slice(&vertices),
2892 wgpu::BufferUsages::VERTEX,
2893 );
2894 let positions = mesh_buffer(
2895 device,
2896 live,
2897 "live mesh positions",
2898 bytemuck::cast_slice(data.positions),
2899 wgpu::BufferUsages::VERTEX,
2900 );
2901 let indices = mesh_buffer(
2902 device,
2903 live,
2904 "live mesh indices",
2905 bytemuck::cast_slice(data.indices),
2906 wgpu::BufferUsages::INDEX,
2907 );
2908 let (base, sway_len) = match sway {
2909 Some(sway) => (self.allocate_sway(sway)?, sway.len() as u32),
2910 None => (NO_SWAY, 0),
2911 };
2912 let (skin_base, skin_len, skin_joints) = match skin {
2913 Some(skin) => {
2914 let range = self.skins.allocate(&self.gpu.device, skin)?;
2915 self.rebuild_scene_group();
2916 (range.start, range.end - range.start, skin.palette() as u32)
2917 }
2918 None => (NO_SKIN, 0, 0),
2919 };
2920 let mesh = Mesh {
2921 vertices: vertex_buffer,
2922 positions,
2923 indices,
2924 vertex_capacity: data.positions.len(),
2925 index_capacity: data.indices.len(),
2926 count: data.indices.len() as u32,
2927 sway: base,
2928 sway_len,
2929 skin: skin_base,
2930 skin_len,
2931 skin_joints,
2932 };
2933 if slot == self.meshes.len() {
2934 self.meshes.push(MeshSlot {
2935 generation,
2936 mesh: Some(mesh),
2937 });
2938 } else {
2939 self.free_meshes.pop();
2940 self.meshes[slot].mesh = Some(mesh);
2941 }
2942 Ok(MeshHandle::pack(slot, generation))
2943 }
2944
2945 fn allocate_sway(&mut self, sway: &[SwayVertex]) -> Result<u32, String> {
2946 let base = self.sway_free.take(
2947 &mut self.sway_data,
2948 sway.len(),
2949 GpuSway {
2950 pivot_level: [0.0; 4],
2951 stiffness: [0.0; 4],
2952 },
2953 "sway vertices",
2954 )?;
2955 for (slot, vertex) in self.sway_data[base as usize..].iter_mut().zip(sway) {
2956 *slot = GpuSway {
2957 pivot_level: [
2958 vertex.pivot[0],
2959 vertex.pivot[1],
2960 vertex.pivot[2],
2961 vertex.level,
2962 ],
2963 stiffness: [vertex.stiffness, 0.0, 0.0, 0.0],
2964 };
2965 }
2966 self.sway = self
2967 .gpu
2968 .device
2969 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
2970 label: Some("live sway"),
2971 contents: bytemuck::cast_slice(&self.sway_data),
2972 usage: wgpu::BufferUsages::STORAGE,
2973 });
2974 self.rebuild_scene_group();
2975 Ok(base)
2976 }
2977
2978 fn check_handle(&self, handle: MeshHandle) -> Result<usize, String> {
2979 match live_mesh(&self.meshes, handle) {
2980 Some(_) => Ok(handle.slot()),
2981 None => Err("mesh handle is unknown or already released".into()),
2982 }
2983 }
2984
2985 pub fn release_mesh(&mut self, handle: MeshHandle) -> Result<(), String> {
2986 let slot = self.check_handle(handle)?;
2987 let Some(mesh) = self.meshes[slot].mesh.take() else {
2988 return Err("mesh handle is unknown or already released".into());
2989 };
2990 if mesh.sway != NO_SWAY {
2991 self.sway_free.give(mesh.sway..mesh.sway + mesh.sway_len);
2992 }
2993 if mesh.skin != NO_SKIN {
2994 self.skins.release(mesh.skin..mesh.skin + mesh.skin_len);
2995 }
2996 let entry = &mut self.meshes[slot];
2997 entry.generation += 1;
2998 if entry.generation < MESH_GENERATION_LAST {
2999 self.free_meshes.push(slot);
3000 }
3001 self.mesh_epoch += 1;
3002 Ok(())
3003 }
3004
3005 pub fn replace_mesh(&mut self, handle: MeshHandle, data: MeshData<'_>) -> Result<(), String> {
3006 check_mesh(&data)?;
3007 let slot = self.check_handle(handle)?;
3008 let Some(mesh) = self.meshes[slot].mesh.as_mut() else {
3009 return Err("mesh handle is unknown or already released".into());
3010 };
3011 if mesh.sway != NO_SWAY {
3012 return Err("a swaying mesh cannot be replaced".into());
3013 }
3014 if mesh.skin != NO_SKIN {
3015 return Err("a skinned mesh cannot be replaced".into());
3016 }
3017 let device = &self.gpu.device;
3018 let queue = &self.gpu.queue;
3019 let live = &self.mesh_buffers_live;
3020 let vertices = mesh_vertices(&data);
3021 if data.positions.len() <= mesh.vertex_capacity {
3022 queue.write_buffer(&mesh.vertices, 0, bytemuck::cast_slice(&vertices));
3023 queue.write_buffer(&mesh.positions, 0, bytemuck::cast_slice(data.positions));
3024 } else {
3025 mesh.vertices = mesh_buffer(
3026 device,
3027 live,
3028 "live mesh vertices",
3029 bytemuck::cast_slice(&vertices),
3030 wgpu::BufferUsages::VERTEX,
3031 );
3032 mesh.positions = mesh_buffer(
3033 device,
3034 live,
3035 "live mesh positions",
3036 bytemuck::cast_slice(data.positions),
3037 wgpu::BufferUsages::VERTEX,
3038 );
3039 mesh.vertex_capacity = data.positions.len();
3040 }
3041 if data.indices.len() <= mesh.index_capacity {
3042 queue.write_buffer(&mesh.indices, 0, bytemuck::cast_slice(data.indices));
3043 } else {
3044 mesh.indices = mesh_buffer(
3045 device,
3046 live,
3047 "live mesh indices",
3048 bytemuck::cast_slice(data.indices),
3049 wgpu::BufferUsages::INDEX,
3050 );
3051 mesh.index_capacity = data.indices.len();
3052 }
3053 mesh.count = data.indices.len() as u32;
3054 self.mesh_epoch += 1;
3055 Ok(())
3056 }
3057
3058 pub fn mesh_stats(&self) -> MeshStats {
3059 let live = self
3060 .meshes
3061 .iter()
3062 .filter(|slot| slot.mesh.is_some())
3063 .count();
3064 MeshStats {
3065 live,
3066 slots: self.meshes.len(),
3067 buffers: self.mesh_buffers_live.load(Ordering::Relaxed),
3068 glass_live: 0,
3069 glass_slots: 0,
3070 glass_buffers: 0,
3071 }
3072 }
3073
3074 pub fn shadow_mesh(&self, handle: MeshHandle) -> Option<(&wgpu::Buffer, &wgpu::Buffer, u32)> {
3075 live_mesh(&self.meshes, handle)
3076 .map(|mesh| (&mesh.positions.buffer, &mesh.indices.buffer, mesh.count))
3077 }
3078
3079 pub fn mesh_buffers(&self, handle: MeshHandle) -> Option<MeshBuffers<'_>> {
3080 live_mesh(&self.meshes, handle).map(|mesh| MeshBuffers {
3081 vertices: &mesh.vertices,
3082 positions: &mesh.positions,
3083 indices: &mesh.indices,
3084 count: mesh.count,
3085 })
3086 }
3087
3088 pub fn swaying(&self, handle: MeshHandle) -> bool {
3089 live_mesh(&self.meshes, handle).is_some_and(|mesh| mesh.sway != NO_SWAY)
3090 }
3091
3092 pub fn skinned(&self, handle: MeshHandle) -> bool {
3093 live_mesh(&self.meshes, handle).is_some_and(|mesh| mesh.skin != NO_SKIN)
3094 }
3095
3096 fn deforms(&self, handle: MeshHandle) -> bool {
3097 live_mesh(&self.meshes, handle)
3098 .is_some_and(|mesh| mesh.sway != NO_SWAY || mesh.skin != NO_SKIN)
3099 }
3100
3101 pub fn set_poses(&self, poses: &[InstancePose<'_>]) -> Result<(), String> {
3102 self.skins.set_poses(poses)
3103 }
3104
3105 pub fn same_poses(&self, poses: &[InstancePose<'_>]) -> bool {
3106 self.skins.same(poses)
3107 }
3108
3109 pub fn render(&mut self, scene: &Scene<'_>) -> Result<&FrameTargets, String> {
3110 let mut encoder = self
3111 .gpu
3112 .device
3113 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3114 label: Some("live frame"),
3115 });
3116 let mut profiler = std::mem::replace(
3117 &mut self.profiler,
3118 GpuProfiler::new(&self.gpu.device, &self.gpu.queue),
3119 );
3120 let result = self.encode(scene, &mut encoder, None, Some(&mut profiler));
3121 if let Err(error) = result {
3122 self.profiler = profiler;
3123 return Err(error);
3124 }
3125 let slot = profiler.finish(&mut encoder);
3126 self.gpu.queue.submit(Some(encoder.finish()));
3127 if let Some(slot) = slot {
3128 profiler.submitted(slot);
3129 }
3130 self.profiler = profiler;
3131 Ok(&self.targets)
3132 }
3133
3134 pub fn resize(&mut self, width: u32, height: u32) -> Result<(), String> {
3135 if width == 0 || height == 0 {
3136 return Err("viewport is empty".into());
3137 }
3138 if (width, height) == (self.targets.width, self.targets.height) {
3139 self.viewport = [width, height];
3140 return Ok(());
3141 }
3142 self.targets = new_targets(&self.gpu, width, height)?;
3143 self.ssr = None;
3144 self.viewport = [width, height];
3145 Ok(())
3146 }
3147
3148 pub fn viewport(&self) -> [u32; 2] {
3149 self.viewport
3150 }
3151
3152 pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
3153 crate::viewport::check([width, height], [self.targets.width, self.targets.height])?;
3154 self.viewport = [width, height];
3155 Ok(())
3156 }
3157
3158 fn scene_surfaces(&self, count: usize) -> Vec<InstanceSurface> {
3159 let given = self.surfaces.borrow();
3160 (0..count)
3161 .map(|index| given.get(index).copied().unwrap_or_default())
3162 .collect()
3163 }
3164
3165 fn pack_instances(
3166 &self,
3167 scene: &Scene<'_>,
3168 surfaces: &[InstanceSurface],
3169 poses: &PoseLayout,
3170 ) -> Result<Vec<GpuInstance>, String> {
3171 let mut packed_instances = Vec::with_capacity(scene.instances.len());
3172 let caustic = self.caustic.borrow();
3173 for (index, (instance, surface)) in scene.instances.iter().zip(surfaces).enumerate() {
3174 let Some(mesh) = live_mesh(&self.meshes, instance.mesh) else {
3175 return Err("instance references a released or unknown mesh".into());
3176 };
3177 if instance.material as usize >= scene.materials.len() {
3178 return Err("instance references an unknown mesh or material".into());
3179 }
3180 if !(0.0..=1.0).contains(&instance.opacity)
3181 || !(0.0..=1.0).contains(&instance.alpha_cutoff)
3182 || !(0.0..=1.0).contains(&instance.coverage)
3183 || !instance.age.is_finite()
3184 {
3185 return Err("instance alpha, coverage and age must be finite fractions".into());
3186 }
3187 if instance.deformer != DeformerId::NONE
3188 && instance.deformer.0 as usize >= scene.deformers.len()
3189 {
3190 return Err("instance references an unknown deformer".into());
3191 }
3192 let skin = match poses.get(index).filter(|_| mesh.skin != NO_SKIN) {
3193 Some(placed) if placed.joints < mesh.skin_joints => {
3194 return Err(format!(
3195 "instance {index} is posed with {} joints where its mesh needs {}",
3196 placed.joints, mesh.skin_joints
3197 ));
3198 }
3199 Some(placed) => [mesh.skin, placed.current, placed.previous, placed.joints],
3200 None => [NO_SKIN, 0, 0, 0],
3201 };
3202 let still = u32::from(
3203 instance.deformer == DeformerId::NONE
3204 && mesh.sway == NO_SWAY
3205 && skin[0] == NO_SKIN
3206 && same_bits(instance.model, instance.previous_model),
3207 );
3208 packed_instances.push(GpuInstance {
3209 model: instance.model,
3210 previous_model: instance.previous_model,
3211 normal: normal_columns(instance.model)?,
3212 meta: [
3213 instance.material,
3214 instance.id,
3215 instance.opacity.to_bits(),
3216 instance.alpha_cutoff.to_bits(),
3217 ],
3218 extra: [
3219 instance.deformer.0,
3220 mesh.sway,
3221 instance.age.clamp(0.0, 1.0).to_bits(),
3222 instance.coverage.to_bits(),
3223 ],
3224 clip: surface.clip,
3225 content_uv: [
3226 surface.uv_offset[0],
3227 surface.uv_offset[1],
3228 surface.uv_scale[0],
3229 surface.uv_scale[1],
3230 ],
3231 content_crop: surface.crop,
3232 content_face: [
3233 surface.face.code(),
3234 still,
3235 u32::from(surface.contact)
3236 | u32::from(caustic.as_ref().is_some_and(|c| c.receives(index))) << 1
3237 | u32::from(surface.cutout) << 2
3238 | surface.occlusion_release() << 8,
3239 u32::from(surface.authored_ambient),
3240 ],
3241 skin,
3242 });
3243 }
3244 Ok(packed_instances)
3245 }
3246
3247 fn pack_map_materials(&self, scene: &Scene<'_>) -> Vec<GpuMapMaterial> {
3248 scene
3249 .materials
3250 .iter()
3251 .map(|material| {
3252 let indices = MapIndices::from_material(material)
3253 .ok()
3254 .filter(|indices| {
3255 [
3256 indices.base,
3257 indices.normal,
3258 indices.roughness,
3259 indices.metal,
3260 ]
3261 .iter()
3262 .zip(self.map_layers)
3263 .all(|(index, count)| index.is_none_or(|index| (index as usize) < count))
3264 })
3265 .unwrap_or_default();
3266 self.maps.material(material, indices)
3267 })
3268 .collect()
3269 }
3270
3271 fn prepare(&mut self, scene: &Scene<'_>) -> Result<(Vec<Batch>, Vec<InstanceSurface>), String> {
3272 if scene.materials.is_empty() {
3273 return Err("scene needs at least one material".into());
3274 }
3275 let surfaces = self.scene_surfaces(scene.instances.len());
3276 self.content.borrow().write_caustic(
3277 &self.gpu.queue,
3278 Caustic::rows(self.caustic.borrow().as_ref(), scene.sun.direction),
3279 );
3280 let packed_instances = self.pack_instances(scene, &surfaces, &self.skins.layout())?;
3281 if self.skins.upload(&self.gpu.device, &self.gpu.queue) {
3282 self.rebuild_scene_group();
3283 }
3284 self.deformers
3285 .upload(&self.gpu.device, &self.gpu.queue, scene.deformers)?;
3286 let materials: Vec<PackedMaterial> =
3287 scene.materials.iter().map(PackedMaterial::pack).collect();
3288 self.maps.fit_coat(&self.gpu.queue, scene.materials);
3289 let map_materials = self.pack_map_materials(scene);
3290 self.instance_features =
3291 scene_instance_features(&packed_instances, &map_materials, scene.materials);
3292 let mut resized = false;
3293 if packed_instances.len() > self.instance_capacity {
3294 self.instance_capacity = packed_instances.len().next_power_of_two();
3295 self.instances = storage(
3296 &self.gpu.device,
3297 "live instances",
3298 self.instance_capacity * std::mem::size_of::<GpuInstance>(),
3299 );
3300 resized = true;
3301 }
3302 if materials.len() > self.material_capacity {
3303 self.material_capacity = materials.len().next_power_of_two();
3304 self.materials = storage(
3305 &self.gpu.device,
3306 "live materials",
3307 self.material_capacity * PackedMaterial::SIZE,
3308 );
3309 self.map_materials = storage(
3310 &self.gpu.device,
3311 "live map materials",
3312 self.material_capacity * std::mem::size_of::<GpuMapMaterial>(),
3313 );
3314 resized = true;
3315 }
3316 if resized {
3317 self.rebuild_scene_group();
3318 }
3319 if self
3320 .lights
3321 .prepare(&self.gpu.device, &self.gpu.queue, scene.materials.len())
3322 {
3323 self.cascade_group = Self::make_cascade_group(
3324 &self.gpu.device,
3325 &self.cascade_layout,
3326 &self.cascade_uniform,
3327 self.lights.table(),
3328 );
3329 }
3330 let [width, height] = self
3331 .ssr
3332 .as_ref()
3333 .map_or(self.viewport, |history| history.size);
3334 let motion_view_projection = multiply(scene.camera.projection, scene.camera.view);
3335 let wind = scene.wind;
3336 let uniforms = Uniforms {
3337 view_projection: multiply(jitter_matrix(self.jitter), motion_view_projection),
3338 motion_view_projection,
3339 previous_view_projection: scene.camera.previous_view_projection,
3340 view: scene.camera.view,
3341 camera_position: [
3342 scene.camera.position[0],
3343 scene.camera.position[1],
3344 scene.camera.position[2],
3345 if same_bits(
3346 motion_view_projection,
3347 scene.camera.previous_view_projection,
3348 ) {
3349 1.0
3350 } else {
3351 0.0
3352 },
3353 ],
3354 sun_direction: [
3355 scene.sun.direction[0],
3356 scene.sun.direction[1],
3357 scene.sun.direction[2],
3358 0.0,
3359 ],
3360 sun_colour_intensity: [
3361 scene.sun.colour[0],
3362 scene.sun.colour[1],
3363 scene.sun.colour[2],
3364 scene.sun.intensity,
3365 ],
3366 time_size: [
3367 scene.time,
3368 width as f32,
3369 height as f32,
3370 (self.sky.lighting.mip_count - 1) as f32,
3371 ],
3372 flags: [
3373 scene.seed,
3374 u32::from(self.ssr.is_some()),
3375 u32::from(self.foliage.is_some()),
3376 self.foliage.as_ref().map_or(0, |foliage| foliage.id),
3377 ],
3378 wind: [
3379 wind.current.time,
3380 wind.current.strength,
3381 wind.previous.time,
3382 wind.previous.strength,
3383 ],
3384 wind_direction: [
3385 wind.current.direction[0],
3386 wind.current.direction[1],
3387 wind.current.direction[2],
3388 0.0,
3389 ],
3390 previous_wind_direction: [
3391 wind.previous.direction[0],
3392 wind.previous.direction[1],
3393 wind.previous.direction[2],
3394 0.0,
3395 ],
3396 };
3397 let queue = &self.gpu.queue;
3398 queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&uniforms));
3399 if !packed_instances.is_empty() {
3400 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&packed_instances));
3401 }
3402 queue.write_buffer(&self.materials, 0, bytemuck::cast_slice(&materials));
3403 queue.write_buffer(&self.map_materials, 0, bytemuck::cast_slice(&map_materials));
3404 let mut light = SkyLight {
3405 sh: [[0.0; 4]; 9],
3406 info: [(self.sky.lighting.mip_count - 1) as f32, 0.0, 0.0, 0.0],
3407 };
3408 for (out, value) in light.sh.iter_mut().zip(self.sky.lighting.sh) {
3409 *out = [value[0], value[1], value[2], 0.0];
3410 }
3411 queue.write_buffer(&self.sky_light, 0, bytemuck::bytes_of(&light));
3412 let mut authored = light;
3413 for (out, value) in authored
3414 .sh
3415 .iter_mut()
3416 .zip(self.ambient.sh(self.sky.lighting.sh))
3417 {
3418 *out = [value[0], value[1], value[2], 0.0];
3419 }
3420 queue.write_buffer(&self.ambient_light, 0, bytemuck::bytes_of(&authored));
3421 let mut selection = ReflectionSelection::zeroed();
3422 selection.first.box_min = [-1.0; 3];
3423 selection.first.box_max = [1.0; 3];
3424 selection.second.box_min = [-1.0; 3];
3425 selection.second.box_max = [1.0; 3];
3426 let sky_mip = (self.sky.lighting.mip_count - 1) as f32;
3427 selection.weights_mip = [0.0, 0.0, sky_mip, sky_mip];
3428 if let Some(ref reflections) = *self.local_reflections.borrow() {
3429 let records = &reflections.array.records;
3430 let [first, second] = local_candidates(records, scene.camera.position);
3431 selection.first = records[first].into();
3432 selection.second = records[second].into();
3433 selection.weights_mip = [
3434 1.0,
3435 if second == first { 0.0 } else { 1.0 },
3436 reflections.array.max_mip,
3437 sky_mip,
3438 ];
3439 }
3440 queue.write_buffer(
3441 &self.reflection_selection,
3442 0,
3443 bytemuck::bytes_of(&selection),
3444 );
3445 self.lighting_group = Some(self.make_lighting_group());
3446 let batches = surface_batches(
3447 scene.instances,
3448 |mesh| self.deforms(mesh),
3449 |index| surfaces[index].clipped(),
3450 |index| surfaces[index].cutout,
3451 );
3452 Ok((batches, surfaces))
3453 }
3454
3455 fn make_lighting_group(&self) -> wgpu::BindGroup {
3456 let [lower, upper, grid] = self.probes.bindings();
3457 let cube = self.sky.cube_view();
3458 let (ssr, ssr_guide) = self
3459 .ssr
3460 .as_ref()
3461 .map_or((&self.fallbacks.black, &self.fallbacks.black), |ssr| {
3462 (&ssr.colour, &ssr.guide)
3463 });
3464 let reflections = self.local_reflections.borrow();
3465 let local = reflections
3466 .as_ref()
3467 .map_or(&self.fallbacks.local_probes, |current| ¤t.array.view);
3468 let (cookie, cookie_sampler, canopy_params) = match &self.foliage {
3469 Some(foliage) => (
3470 &foliage.canopy.view,
3471 &foliage.canopy.sampler,
3472 foliage.canopy.params(),
3473 ),
3474 None => (
3475 &self.fallbacks.black,
3476 &self.fallbacks.linear,
3477 &self.fallbacks.canopy_params,
3478 ),
3479 };
3480 let view = wgpu::BindingResource::TextureView;
3481 let content = self.content.borrow();
3482 let entries = [
3483 view(&self.maps.base.view),
3484 view(&self.maps.normal.view),
3485 view(&self.maps.roughness.view),
3486 view(&self.maps.metal.view),
3487 wgpu::BindingResource::Sampler(&self.maps.sampler),
3488 self.map_materials.as_entire_binding(),
3489 lower.as_entire_binding(),
3490 upper.as_entire_binding(),
3491 grid.as_entire_binding(),
3492 view(cookie),
3493 wgpu::BindingResource::Sampler(cookie_sampler),
3494 canopy_params.as_entire_binding(),
3495 self.sky_light.as_entire_binding(),
3496 view(&cube),
3497 view(local),
3498 wgpu::BindingResource::Sampler(&self.fallbacks.linear),
3499 view(ssr),
3500 self.reflection_selection.as_entire_binding(),
3501 view(content.view(0)),
3502 view(content.view(1)),
3503 view(content.view(2)),
3504 view(content.view(3)),
3505 wgpu::BindingResource::Sampler(&content.sampler),
3506 view(
3507 self.contact
3508 .as_ref()
3509 .map_or(&self.fallbacks.black, ContactField::view),
3510 ),
3511 self.contact_uniform.as_entire_binding(),
3512 self.ambient_light.as_entire_binding(),
3513 view(ssr_guide),
3514 content.caustic.as_entire_binding(),
3515 ];
3516 let entries: Vec<wgpu::BindGroupEntry<'_>> = entries
3517 .into_iter()
3518 .enumerate()
3519 .map(|(binding, resource)| wgpu::BindGroupEntry {
3520 binding: binding as u32,
3521 resource,
3522 })
3523 .collect();
3524 self.gpu
3525 .device
3526 .create_bind_group(&wgpu::BindGroupDescriptor {
3527 label: Some("live lighting"),
3528 layout: &self.lighting_layout,
3529 entries: &entries,
3530 })
3531 }
3532
3533 pub fn encode(
3534 &mut self,
3535 scene: &Scene<'_>,
3536 encoder: &mut wgpu::CommandEncoder,
3537 shadows: Option<(&mut Shadows, &Fit)>,
3538 profiler: Option<&mut GpuProfiler>,
3539 ) -> Result<(), String> {
3540 self.encode_with_shadow_hook(scene, encoder, shadows, profiler, |_, _, _, _| Ok(()))
3541 }
3542
3543 pub fn encode_with_shadow_hook(
3544 &mut self,
3545 scene: &Scene<'_>,
3546 encoder: &mut wgpu::CommandEncoder,
3547 shadows: Option<(&mut Shadows, &Fit)>,
3548 profiler: Option<&mut GpuProfiler>,
3549 shadow_hook: impl FnOnce(
3550 &Gpu,
3551 &mut wgpu::CommandEncoder,
3552 &mut Shadows,
3553 &Fit,
3554 ) -> Result<(), String>,
3555 ) -> Result<(), String> {
3556 self.encode_with_casts(scene, encoder, shadows, profiler, None, shadow_hook)
3557 }
3558
3559 pub fn encode_with_casts(
3560 &mut self,
3561 scene: &Scene<'_>,
3562 encoder: &mut wgpu::CommandEncoder,
3563 shadows: Option<(&mut Shadows, &Fit)>,
3564 mut profiler: Option<&mut GpuProfiler>,
3565 casts: Option<&mut crate::shadow::LayerDraw<'_>>,
3566 shadow_hook: impl FnOnce(
3567 &Gpu,
3568 &mut wgpu::CommandEncoder,
3569 &mut Shadows,
3570 &Fit,
3571 ) -> Result<(), String>,
3572 ) -> Result<(), String> {
3573 let (batches, surfaces) = self.prepare(scene)?;
3574 if batches.iter().any(|batch| batch.cut) {
3575 self.pipelines.ensure_cut(&self.gpu.device);
3576 }
3577 if let Some(foliage) = &mut self.foliage {
3578 foliage.canopy.select_lod(
3579 &self.gpu.queue,
3580 scene.camera,
3581 self.viewport[1],
3582 foliage.origin,
3583 );
3584 let timing = profiler
3585 .as_deref_mut()
3586 .and_then(|timer| timer.pass("canopy cookie"));
3587 let sun = scene.sun.direction;
3588 foliage.canopy.update(
3589 &self.gpu.queue,
3590 encoder,
3591 CanopyFrame {
3592 origin: foliage.origin,
3593 time: scene.wind.current.time,
3594 strength: scene.wind.current.strength,
3595 direction: scene.wind.current.direction,
3596 sun,
3597 receiver_y: foliage.receiver_y,
3598 angular_radius: SUN_RADIUS,
3599 },
3600 profiler
3601 .as_deref()
3602 .and_then(|timer| timer.render_writes(timing)),
3603 );
3604 }
3605 let mut key = mix_key(
3606 static_key(scene.instances, &batches, &surfaces),
3607 self.mesh_epoch,
3608 );
3609 if batches.iter().any(|batch| batch.cut && batch.casts_shadow) {
3610 key = mix_key(key, self.cut_key(scene, &batches, &surfaces));
3611 }
3612 if let Some((shadows, fit)) = shadows {
3613 let cascades = mix_key(key, self.lights.table_key());
3614 self.encode_shadows(
3615 cascades,
3616 &batches,
3617 shadows,
3618 fit,
3619 encoder,
3620 profiler.as_deref_mut(),
3621 );
3622 let transmitting = self.encode_transmission(
3623 cascades,
3624 &batches,
3625 scene.instances,
3626 shadows,
3627 fit,
3628 encoder,
3629 casts,
3630 profiler.as_deref_mut(),
3631 );
3632 shadow_hook(&self.gpu, encoder, shadows, fit)?;
3633 self.encode_lights(key, &batches, encoder, profiler.as_deref_mut());
3634 self.lights
3635 .write_uniform(&self.gpu.queue, transmitting, shadows.face_tiles());
3636 self.set_shadows(shadows, fit);
3637 } else {
3638 self.encode_lights(key, &batches, encoder, profiler.as_deref_mut());
3639 self.lights
3640 .write_uniform(&self.gpu.queue, false, Default::default());
3641 self.set_unshadowed();
3642 }
3643 let features = self.opaque_features(&batches);
3644 let lit = self.lights.extended();
3645 for (batch, used) in batches.iter().zip(&features) {
3646 if batch.shadow_only {
3647 continue;
3648 }
3649 self.pipelines.ensure_opaque(
3650 &self.gpu.device,
3651 OpaqueKey {
3652 features: *used,
3653 lit,
3654 two_sided: batch.two_sided,
3655 },
3656 );
3657 }
3658 self.encode_surfaces(&batches, &features, encoder, profiler)?;
3659 self.opaque_drawn = batches
3660 .iter()
3661 .zip(features)
3662 .filter(|(batch, _)| !batch.shadow_only)
3663 .map(|(_, used)| used)
3664 .collect();
3665 Ok(())
3666 }
3667
3668 fn cut_key(&self, scene: &Scene<'_>, batches: &[Batch], surfaces: &[InstanceSurface]) -> u64 {
3669 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3670 self.content_epoch.get().hash(&mut hasher);
3671 for batch in batches
3672 .iter()
3673 .filter(|batch| batch.cut && batch.casts_shadow)
3674 {
3675 for index in batch.range.clone() {
3676 let index = index as usize;
3677 let instance = &scene.instances[index];
3678 let surface = &surfaces[index];
3679 instance.alpha_cutoff.to_bits().hash(&mut hasher);
3680 instance.opacity.to_bits().hash(&mut hasher);
3681 content_row_key(&scene.materials[instance.material as usize]).hash(&mut hasher);
3682 for value in surface
3683 .uv_offset
3684 .iter()
3685 .chain(&surface.uv_scale)
3686 .chain(&surface.crop)
3687 {
3688 value.to_bits().hash(&mut hasher);
3689 }
3690 }
3691 }
3692 hasher.finish()
3693 }
3694
3695 #[allow(clippy::too_many_arguments)]
3696 fn encode_transmission(
3697 &self,
3698 key: u64,
3699 batches: &[Batch],
3700 instances: &[Instance],
3701 shadows: &mut Shadows,
3702 fit: &Fit,
3703 encoder: &mut wgpu::CommandEncoder,
3704 mut casts: Option<&mut crate::shadow::LayerDraw<'_>>,
3705 profiler: Option<&mut GpuProfiler>,
3706 ) -> bool {
3707 let chosen = if self.lights.any_transmissive() {
3708 transmission_batches(batches, instances, |material| {
3709 self.lights.transmissive(material)
3710 })
3711 } else {
3712 Vec::new()
3713 };
3714 if chosen.is_empty() && casts.is_none() {
3715 return false;
3716 }
3717 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3718 key.hash(&mut hasher);
3719 for batch in &chosen {
3720 batch.range.hash(&mut hasher);
3721 for index in batch.range.clone() {
3722 instances[index as usize].material.hash(&mut hasher);
3723 }
3724 }
3725 let moving = casts.is_some() || chosen.iter().any(|batch| batch.moving);
3726 let faces = if casts.is_some() {
3727 self.lights.faces().len()
3728 } else {
3729 0
3730 };
3731 let mut draw = |pass: &mut wgpu::RenderPass<'_>, cascade: usize, statics: bool| {
3732 if !chosen.is_empty() && cascade < CASCADE_COUNT {
3733 pass.set_pipeline(&self.pipelines.transmit);
3734 pass.set_bind_group(0, &self.scene_group, &[]);
3735 pass.set_bind_group(
3736 1,
3737 &self.cascade_group,
3738 &[self.cascade_align * cascade as u32],
3739 );
3740 pass.set_bind_group(2, &self.deformers.group, &[]);
3741 for batch in chosen.iter().filter(|batch| batch.moving != statics) {
3742 let Some(mesh) = live_mesh(&self.meshes, batch.mesh) else {
3743 continue;
3744 };
3745 pass.set_vertex_buffer(0, mesh.vertices.slice(..));
3746 pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
3747 pass.draw_indexed(0..mesh.count, 0, batch.range.clone());
3748 }
3749 }
3750 if !statics && let Some(casts) = casts.as_mut() {
3751 casts(pass, cascade, statics);
3752 }
3753 };
3754 shadows.render_transmission(
3755 &self.gpu.device,
3756 encoder,
3757 fit,
3758 hasher.finish(),
3759 moving,
3760 (faces, self.lights.quality().resolution),
3761 &mut draw,
3762 profiler,
3763 );
3764 true
3765 }
3766
3767 fn encode_lights(
3768 &mut self,
3769 key: u64,
3770 batches: &[Batch],
3771 encoder: &mut wgpu::CommandEncoder,
3772 profiler: Option<&mut GpuProfiler>,
3773 ) {
3774 let faces = self.lights.faces();
3775 if faces.is_empty() {
3776 return;
3777 }
3778 for (layer, matrix) in &faces {
3779 let mut slot = [0u8; CASTER_BYTES as usize];
3780 slot[..64].copy_from_slice(bytemuck::cast_slice(matrix));
3781 self.gpu.queue.write_buffer(
3782 &self.cascade_uniform,
3783 u64::from(self.cascade_align) * (CASCADE_COUNT as u64 + u64::from(*layer)),
3784 &slot,
3785 );
3786 }
3787 let casters = shadow_batches(batches);
3788 let moving = casters.iter().any(|batch| batch.moving);
3789 let scene_group = &self.scene_group;
3790 let cascade_group = &self.cascade_group;
3791 let deformers = &self.deformers.group;
3792 let lighting = self.lighting_group.as_ref();
3793 let pipelines = &self.pipelines;
3794 let meshes = &self.meshes;
3795 let align = self.cascade_align;
3796 let mut draw = |pass: &mut wgpu::RenderPass<'_>, layer: usize, statics: bool| {
3797 pass.set_bind_group(0, scene_group, &[]);
3798 pass.set_bind_group(1, cascade_group, &[align * (CASCADE_COUNT + layer) as u32]);
3799 draw_casters(
3800 pass, &casters, statics, deformers, lighting, pipelines, meshes,
3801 );
3802 };
3803 self.lights
3804 .render(&self.gpu.device, encoder, key, moving, &mut draw, profiler);
3805 }
3806
3807 fn encode_shadows(
3808 &self,
3809 key: u64,
3810 batches: &[Batch],
3811 shadows: &mut Shadows,
3812 fit: &Fit,
3813 encoder: &mut wgpu::CommandEncoder,
3814 profiler: Option<&mut GpuProfiler>,
3815 ) {
3816 let mode = if self.lights.any_transmissive() {
3817 1.0f32
3818 } else {
3819 0.0
3820 };
3821 let near_map = shadows.quality().splits_near_map();
3822 for (index, cascade) in fit.cascades.iter().enumerate() {
3823 let pancake = if near_map && index == 1 { 0.0f32 } else { 1.0 };
3824 let mut slot = [0u8; CASTER_BYTES as usize];
3825 slot[..64].copy_from_slice(bytemuck::cast_slice(&cascade.view_proj.columns));
3826 slot[64..68].copy_from_slice(&mode.to_le_bytes());
3827 slot[68..72].copy_from_slice(&pancake.to_le_bytes());
3828 self.gpu.queue.write_buffer(
3829 &self.cascade_uniform,
3830 u64::from(self.cascade_align) * index as u64,
3831 &slot,
3832 );
3833 }
3834 let casters = shadow_batches(batches);
3835 let moving = casters.iter().any(|batch| batch.moving);
3836 let mut draw = |pass: &mut wgpu::RenderPass<'_>, cascade: usize, statics: bool| {
3837 pass.set_bind_group(0, &self.scene_group, &[]);
3838 pass.set_bind_group(
3839 1,
3840 &self.cascade_group,
3841 &[self.cascade_align * cascade as u32],
3842 );
3843 self.draw_casters(pass, &casters, statics);
3844 };
3845 shadows.render_with(encoder, fit, key, moving, &mut draw, profiler);
3846 }
3847
3848 fn draw_casters(&self, pass: &mut wgpu::RenderPass<'_>, casters: &[&Batch], statics: bool) {
3849 draw_casters(
3850 pass,
3851 casters,
3852 statics,
3853 &self.deformers.group,
3854 self.lighting_group.as_ref(),
3855 &self.pipelines,
3856 &self.meshes,
3857 );
3858 }
3859
3860 fn encode_surfaces(
3861 &self,
3862 batches: &[Batch],
3863 features: &[OpaqueFeatures],
3864 encoder: &mut wgpu::CommandEncoder,
3865 mut profiler: Option<&mut GpuProfiler>,
3866 ) -> Result<(), String> {
3867 let lighting = self
3868 .lighting_group
3869 .as_ref()
3870 .ok_or("frame lighting is not prepared")?;
3871 let depth_timing = profiler
3872 .as_deref_mut()
3873 .and_then(|timer| timer.pass("depth and velocity"));
3874 {
3875 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3876 multiview_mask: None,
3877 label: Some("depth and velocity"),
3878 color_attachments: &[
3879 Some(wgpu::RenderPassColorAttachment {
3880 view: &self.targets.velocity_view,
3881 resolve_target: None,
3882 depth_slice: None,
3883 ops: wgpu::Operations {
3884 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3885 store: wgpu::StoreOp::Store,
3886 },
3887 }),
3888 Some(wgpu::RenderPassColorAttachment {
3889 view: &self.targets.ids_view,
3890 resolve_target: None,
3891 depth_slice: None,
3892 ops: wgpu::Operations {
3893 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3894 store: wgpu::StoreOp::Store,
3895 },
3896 }),
3897 ],
3898 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3899 view: &self.targets.depth_view,
3900 depth_ops: Some(wgpu::Operations {
3901 load: wgpu::LoadOp::Clear(1.0),
3902 store: wgpu::StoreOp::Store,
3903 }),
3904 stencil_ops: None,
3905 }),
3906 occlusion_query_set: None,
3907 timestamp_writes: profiler
3908 .as_deref()
3909 .and_then(|timer| timer.render_writes(depth_timing)),
3910 });
3911 crate::viewport::apply(&mut pass, self.viewport);
3912 self.draw_with(&mut pass, batches, lighting, |index, two_sided| {
3913 let pair = match &self.pipelines.cut {
3914 Some(cut) if batches[index].cut => &cut.prepass,
3915 _ => &self.pipelines.prepass,
3916 };
3917 &pair[usize::from(two_sided)]
3918 });
3919 if let Some(foliage) = &self.foliage {
3920 self.draw_cards(&mut pass, foliage, &foliage.prepass, lighting);
3921 }
3922 }
3923 let opaque_timing = profiler
3924 .as_deref_mut()
3925 .and_then(|timer| timer.pass("opaque PBR"));
3926 {
3927 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3928 multiview_mask: None,
3929 label: Some("opaque PBR"),
3930 color_attachments: &[
3931 Some(wgpu::RenderPassColorAttachment {
3932 view: &self.targets.hdr.view,
3933 resolve_target: None,
3934 depth_slice: None,
3935 ops: wgpu::Operations {
3936 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3937 store: wgpu::StoreOp::Store,
3938 },
3939 }),
3940 Some(wgpu::RenderPassColorAttachment {
3941 view: &self.targets.normal_roughness_view,
3942 resolve_target: None,
3943 depth_slice: None,
3944 ops: wgpu::Operations {
3945 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3946 store: wgpu::StoreOp::Store,
3947 },
3948 }),
3949 ],
3950 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3951 view: &self.targets.depth_view,
3952 depth_ops: Some(wgpu::Operations {
3953 load: wgpu::LoadOp::Load,
3954 store: wgpu::StoreOp::Store,
3955 }),
3956 stencil_ops: None,
3957 }),
3958 occlusion_query_set: None,
3959 timestamp_writes: profiler
3960 .as_deref()
3961 .and_then(|timer| timer.render_writes(opaque_timing)),
3962 });
3963 crate::viewport::apply(&mut pass, self.viewport);
3964 let lit = self.lights.extended();
3965 self.draw_with(&mut pass, batches, lighting, |index, two_sided| {
3966 &self.pipelines.opaque[&OpaqueKey {
3967 features: features[index],
3968 lit,
3969 two_sided,
3970 }]
3971 });
3972 if let Some(foliage) = &self.foliage {
3973 self.draw_cards(&mut pass, foliage, &foliage.opaque, lighting);
3974 }
3975 }
3976 Ok(())
3977 }
3978
3979 fn draw_with<'p>(
3980 &self,
3981 pass: &mut wgpu::RenderPass<'_>,
3982 batches: &[Batch],
3983 lighting: &wgpu::BindGroup,
3984 pipelines: impl Fn(usize, bool) -> &'p wgpu::RenderPipeline,
3985 ) {
3986 pass.set_bind_group(0, &self.scene_group, &[]);
3987 pass.set_bind_group(1, &self.shadow_group, &[]);
3988 pass.set_bind_group(2, &self.deformers.group, &[]);
3989 pass.set_bind_group(3, lighting, &[]);
3990 let mut current: Option<&wgpu::RenderPipeline> = None;
3991 for (index, batch) in batches
3992 .iter()
3993 .enumerate()
3994 .filter(|(_, batch)| !batch.shadow_only)
3995 {
3996 let chosen = pipelines(index, batch.two_sided);
3997 if !current.is_some_and(|last| std::ptr::eq(last, chosen)) {
3998 pass.set_pipeline(chosen);
3999 current = Some(chosen);
4000 }
4001 let Some(mesh) = live_mesh(&self.meshes, batch.mesh) else {
4002 continue;
4003 };
4004 pass.set_vertex_buffer(0, mesh.vertices.slice(..));
4005 pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
4006 pass.draw_indexed(0..mesh.count, 0, batch.range.clone());
4007 }
4008 }
4009
4010 fn draw_cards(
4011 &self,
4012 pass: &mut wgpu::RenderPass<'_>,
4013 foliage: &Foliage,
4014 pipeline: &wgpu::RenderPipeline,
4015 lighting: &wgpu::BindGroup,
4016 ) {
4017 if foliage.canopy.card_count == 0 {
4018 return;
4019 }
4020 pass.set_pipeline(pipeline);
4021 pass.set_bind_group(0, &self.scene_group, &[]);
4022 pass.set_bind_group(1, &self.shadow_group, &[]);
4023 pass.set_bind_group(2, foliage.canopy.cookie_bind(), &[]);
4024 pass.set_bind_group(3, lighting, &[]);
4025 pass.draw(0..6, 0..foliage.canopy.card_count);
4026 }
4027
4028 pub fn collect_timings(&mut self) -> Vec<Vec<PassTiming>> {
4029 self.profiler.collect(&self.gpu.device)
4030 }
4031
4032 pub fn readback_velocity(&self) -> Result<Vec<[u16; 2]>, String> {
4033 let [width, height] = self.viewport;
4034 let row = (width * 4).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
4035 * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
4036 let size = u64::from(row) * u64::from(height);
4037 let buffer = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
4038 label: Some("live velocity readback"),
4039 size,
4040 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4041 mapped_at_creation: false,
4042 });
4043 let mut encoder = self
4044 .gpu
4045 .device
4046 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4047 label: Some("live velocity readback"),
4048 });
4049 encoder.copy_texture_to_buffer(
4050 self.targets.velocity.as_image_copy(),
4051 wgpu::TexelCopyBufferInfo {
4052 buffer: &buffer,
4053 layout: wgpu::TexelCopyBufferLayout {
4054 offset: 0,
4055 bytes_per_row: Some(row),
4056 rows_per_image: Some(height),
4057 },
4058 },
4059 wgpu::Extent3d {
4060 width,
4061 height,
4062 depth_or_array_layers: 1,
4063 },
4064 );
4065 self.gpu.queue.submit(Some(encoder.finish()));
4066 let (send, receive) = std::sync::mpsc::channel();
4067 let slice = buffer.slice(..);
4068 slice.map_async(wgpu::MapMode::Read, move |result| {
4069 let _ = send.send(result);
4070 });
4071 self.gpu
4072 .device
4073 .poll(wgpu::PollType::wait_indefinitely())
4074 .map_err(|error| format!("{error:?}"))?;
4075 receive
4076 .recv()
4077 .map_err(|error| error.to_string())?
4078 .map_err(|error| error.to_string())?;
4079 let mapped = slice.get_mapped_range().expect("a mapped range");
4080 let mut values = Vec::with_capacity((width * height) as usize);
4081 for padded in mapped.chunks_exact(row as usize) {
4082 for pixel in padded[..width as usize * 4].chunks_exact(4) {
4083 values.push([
4084 u16::from_le_bytes([pixel[0], pixel[1]]),
4085 u16::from_le_bytes([pixel[2], pixel[3]]),
4086 ]);
4087 }
4088 }
4089 drop(mapped);
4090 buffer.unmap();
4091 Ok(values)
4092 }
4093}
4094
4095const SHADER: &str = r#"
4096override use_sun_shadow: bool = true;
4097override use_ssr: bool = true;
4098override use_canopy: bool = true;
4099override use_contact: bool = true;
4100override use_authored_ambient: bool = true;
4101override use_caustics: bool = true;
4102struct FrameUniforms {
4103 view_projection: mat4x4f,
4104 motion_view_projection: mat4x4f,
4105 previous_view_projection: mat4x4f,
4106 view: mat4x4f,
4107 camera_position: vec4f,
4108 sun_direction: vec4f,
4109 sun_colour_intensity: vec4f,
4110 time_size: vec4f,
4111 flags: vec4u,
4112 wind: vec4f,
4113 wind_direction: vec4f,
4114 previous_wind_direction: vec4f,
4115}
4116struct InstanceData {
4117 model: mat4x4f,
4118 previous_model: mat4x4f,
4119 normal0: vec4f,
4120 normal1: vec4f,
4121 normal2: vec4f,
4122 fields: vec4u,
4123 extra: vec4u,
4124 clip0: vec4f,
4125 clip1: vec4f,
4126 content_uv: vec4f,
4127 content_crop: vec4f,
4128 content_face: vec4u,
4129 skin: vec4u,
4130}
4131struct SwayData {
4132 pivot_level: vec4f,
4133 stiffness: vec4f,
4134}
4135struct SkyLight {
4136 sh: array<vec4f, 9>,
4137 info: vec4f,
4138}
4139struct MaterialData {
4140 base_roughness: vec4f,
4141 metal_spec_coat: vec4f,
4142 sheen_trans_ior_disp: vec4f,
4143 thick_sub_film: vec4f,
4144 tint_absorption: vec4f,
4145 emission_film: vec4f,
4146 normal_maps: vec4f,
4147 maps_content: vec4f,
4148 age_a: vec4f,
4149 age_b: vec4f,
4150 layer0: vec4f,
4151 layer1: vec4f,
4152 layer2: vec4f,
4153 layer3: vec4f,
4154 tail: vec4f,
4155 params: array<vec4f, 16>,
4156}
4157@group(0) @binding(0) var<uniform> frame: FrameUniforms;
4158@group(0) @binding(1) var<storage, read> instances: array<InstanceData>;
4159@group(0) @binding(2) var<storage, read> materials: array<MaterialData>;
4160@group(0) @binding(3) var<storage, read> sway_data: array<SwayData>;
4161@group(1) @binding(0) var<uniform> shadow_setting: ShadowSample;
4162@group(1) @binding(1) var shadow_map: texture_depth_2d_array;
4163@group(1) @binding(2) var shadow_sampler: sampler_comparison;
4164struct CasterView {
4165 view_proj: mat4x4f,
4166 mode: vec4f,
4167}
4168@group(1) @binding(3) var<uniform> caster: CasterView;
4169@group(3) @binding(12) var<uniform> sky_light: SkyLight;
4170@group(3) @binding(13) var sky_cube: texture_cube<f32>;
4171@group(3) @binding(14) var local_probes: texture_cube_array<f32>;
4172@group(3) @binding(15) var reflect_sampler: sampler;
4173@group(3) @binding(16) var ssr_history: texture_2d<f32>;
4174@group(3) @binding(26) var ssr_guide: texture_2d<f32>;
4175struct ReflectionSelection {
4176 first: ReflectionProbe,
4177 second: ReflectionProbe,
4178 weights_mip: vec4f,
4179}
4180@group(3) @binding(17) var<uniform> selected_reflections: ReflectionSelection;
4181struct ContactParams {
4182 rect: vec4f,
4183 tint: vec4f,
4184}
4185@group(3) @binding(23) var contact_field: texture_2d<f32>;
4186@group(3) @binding(24) var<uniform> contact_params: ContactParams;
4187@group(3) @binding(25) var<uniform> authored_light: SkyLight;
4188struct VertexInput {
4189 @location(0) position: vec4f,
4190 @location(1) normal: vec4f,
4191 @location(2) tangent: vec4f,
4192 @location(3) uv_alpha: vec4f,
4193 @builtin(instance_index) instance_index: u32,
4194 @builtin(vertex_index) vertex_index: u32,
4195}
4196struct VertexOutput {
4197 @builtin(position) position: vec4f,
4198 @location(0) world_position: vec3f,
4199 @location(1) normal: vec4f,
4200 @location(2) tangent: vec4f,
4201 @location(3) uv_alpha: vec4f,
4202 @location(4) previous_clip: vec4f,
4203 @location(5) current_clip: vec4f,
4204 @location(6) @interpolate(flat) material: u32,
4205 @location(7) @interpolate(flat) id: u32,
4206 @location(8) @interpolate(flat) settings: vec4f,
4207 @location(9) object_position: vec3f,
4208 @location(10) @interpolate(flat) instance: u32,
4209}
4210fn surface_shape(input: VertexInput, instance: InstanceData) -> DeformedVertex {
4211 if (instance.skin.x != 0xffffffffu) {
4212 return skin_shape(input.position.xyz, input.normal.xyz, input.tangent, input.vertex_index, instance.skin);
4213 }
4214 var shape = deform_vertex(input.position.xyz, input.normal.xyz, input.tangent, instance.extra.x);
4215 if (instance.extra.y != 0xffffffffu) {
4216 let sway = sway_data[instance.extra.y + input.vertex_index];
4217 shape.position = tree_sway(shape.position, sway.pivot_level.xyz, sway.pivot_level.w, sway.stiffness.x, frame.wind.x, frame.wind.y, frame.wind_direction.xyz);
4218 shape.previous_position = tree_sway(shape.previous_position, sway.pivot_level.xyz, sway.pivot_level.w, sway.stiffness.x, frame.wind.z, frame.wind.w, frame.previous_wind_direction.xyz);
4219 }
4220 return shape;
4221}
4222@vertex fn vertex_main(input: VertexInput) -> VertexOutput {
4223 let instance = instances[input.instance_index];
4224 let shape = surface_shape(input, instance);
4225 let world = instance.model * vec4f(shape.position, 1.0);
4226 let normal_matrix = mat3x3f(instance.normal0.xyz, instance.normal1.xyz, instance.normal2.xyz);
4227 var output: VertexOutput;
4228 output.position = frame.view_projection * world;
4229 output.current_clip = frame.motion_view_projection * world;
4230 let previous_world = select(instance.previous_model * vec4f(shape.previous_position, 1.0), world, instance.content_face.y != 0u);
4231 output.previous_clip = frame.previous_view_projection * previous_world;
4232 output.world_position = world.xyz;
4233 output.normal = vec4f(normalize(normal_matrix * shape.normal), input.normal.w);
4234 output.tangent = vec4f(normalize((instance.model * vec4f(shape.tangent.xyz, 0.0)).xyz), shape.tangent.w);
4235 output.uv_alpha = input.uv_alpha;
4236 output.material = instance.fields.x;
4237 output.id = instance.fields.y;
4238 output.settings = vec4f(bitcast<f32>(instance.fields.z), bitcast<f32>(instance.fields.w), bitcast<f32>(instance.extra.z), bitcast<f32>(instance.extra.w));
4239 output.object_position = input.position.xyz;
4240 output.instance = input.instance_index;
4241 return output;
4242}
4243fn caster_position(input: VertexInput, instance: InstanceData) -> vec3f {
4244 if (instance.skin.x != 0xffffffffu) {
4245 return skin_position(input.position.xyz, input.vertex_index, instance.skin);
4246 }
4247 var position = deform_position(input.position.xyz, instance.extra.x, false);
4248 if (instance.extra.y != 0xffffffffu) {
4249 let sway = sway_data[instance.extra.y + input.vertex_index];
4250 position = tree_sway(position, sway.pivot_level.xyz, sway.pivot_level.w, sway.stiffness.x, frame.wind.x, frame.wind.y, frame.wind_direction.xyz);
4251 }
4252 return position;
4253}
4254@vertex fn shadow_vertex(input: VertexInput) -> @builtin(position) vec4f {
4255 let instance = instances[input.instance_index];
4256 if (caster_skips(instance.fields.x)) {
4257 return vec4f(2.0, 2.0, 2.0, 1.0);
4258 }
4259 let position = caster_position(input, instance);
4260 return pancake(caster.view_proj * instance.model * vec4f(position, 1.0));
4261}
4262fn pancake(clip: vec4f) -> vec4f {
4263 if (caster.mode.y > 0.5) {
4264 return vec4f(clip.xy, max(clip.z, 0.0), clip.w);
4265 }
4266 return clip;
4267}
4268fn clip_plane(plane: vec4f, world: vec3f) -> bool {
4269 return any(plane.xyz != vec3f(0.0)) && dot(plane.xyz, world) > plane.w;
4270}
4271fn clipped(instance: u32, world: vec3f) -> bool {
4272 let data = instances[instance];
4273 return clip_plane(data.clip0, world) || clip_plane(data.clip1, world);
4274}
4275struct ShadowClipOutput {
4276 @builtin(position) position: vec4f,
4277 @location(0) world_position: vec3f,
4278 @location(1) @interpolate(flat) instance: u32,
4279}
4280@vertex fn shadow_clip_vertex(input: VertexInput) -> ShadowClipOutput {
4281 let instance = instances[input.instance_index];
4282 if (caster_skips(instance.fields.x)) {
4283 var skipped: ShadowClipOutput;
4284 skipped.position = vec4f(2.0, 2.0, 2.0, 1.0);
4285 return skipped;
4286 }
4287 let position = caster_position(input, instance);
4288 let world = instance.model * vec4f(position, 1.0);
4289 var output: ShadowClipOutput;
4290 output.position = pancake(caster.view_proj * world);
4291 output.world_position = world.xyz;
4292 output.instance = input.instance_index;
4293 return output;
4294}
4295@fragment fn shadow_clip_fragment(input: ShadowClipOutput) {
4296 if (clipped(input.instance, input.world_position)) {
4297 discard;
4298 }
4299}
4300struct ShadowCutOutput {
4301 @builtin(position) position: vec4f,
4302 @location(0) world_position: vec3f,
4303 @location(1) @interpolate(flat) instance: u32,
4304 @location(2) uv_alpha: vec4f,
4305}
4306@vertex fn shadow_cut_vertex(input: VertexInput) -> ShadowCutOutput {
4307 let instance = instances[input.instance_index];
4308 var output: ShadowCutOutput;
4309 if (caster_skips(instance.fields.x)) {
4310 output.position = vec4f(2.0, 2.0, 2.0, 1.0);
4311 return output;
4312 }
4313 let position = caster_position(input, instance);
4314 let world = instance.model * vec4f(position, 1.0);
4315 output.position = pancake(caster.view_proj * world);
4316 output.world_position = world.xyz;
4317 output.instance = input.instance_index;
4318 output.uv_alpha = input.uv_alpha;
4319 return output;
4320}
4321@fragment fn shadow_cut_fragment(input: ShadowCutOutput) {
4322 if (clipped(input.instance, input.world_position)) {
4323 discard;
4324 }
4325 let data = instances[input.instance];
4326 let cutoff = bitcast<f32>(data.fields.w);
4327 if (input.uv_alpha.z * bitcast<f32>(data.fields.z) < cutoff) {
4328 discard;
4329 }
4330 if (cut_by_content(input.instance, data.fields.x, input.uv_alpha.xy, cutoff)) {
4331 discard;
4332 }
4333}
4334fn cut_by_content(instance: u32, material: u32, uv: vec2f, cutoff: f32) -> bool {
4335 let data = instances[instance];
4336 return (data.content_face.z & 4u) != 0u && content_cut(material, uv, data.content_uv, data.content_crop, cutoff);
4337}
4338fn cutout(input: VertexOutput) {
4339 if (clipped(input.instance, input.world_position)) {
4340 discard;
4341 }
4342 if (input.uv_alpha.z * input.settings.x < input.settings.y) {
4343 discard;
4344 }
4345 if (use_content_cutout && cut_by_content(input.instance, input.material, input.uv_alpha.xy, input.settings.y)) {
4346 discard;
4347 }
4348 if (input.settings.w < 1.0 && !lod_keep(vec2u(input.position.xy), input.id, input.settings.w)) {
4349 discard;
4350 }
4351}
4352struct PrepassOutput {
4353 @location(0) velocity: vec2f,
4354 @location(1) id: u32,
4355}
4356fn motion(current_clip: vec4f, previous_clip: vec4f) -> vec2f {
4357 let current = current_clip.xy / current_clip.w;
4358 let previous = previous_clip.xy / previous_clip.w;
4359 let moved = (previous - current) * vec2f(0.5, -0.5);
4360 return select(moved, vec2f(0.0), abs(moved) < vec2f(1e-8));
4361}
4362@fragment fn prepass_main(input: VertexOutput) -> PrepassOutput {
4363 cutout(input);
4364 var output: PrepassOutput;
4365 if (instances[input.instance].content_face.y != 0u && frame.camera_position.w != 0.0) {
4366 output.velocity = vec2f(0.0);
4367 } else {
4368 output.velocity = motion(input.current_clip, input.previous_clip);
4369 }
4370 output.id = input.id;
4371 return output;
4372}
4373fn shadow_visibility(world_position: vec3f, normal: vec3f) -> f32 {
4374 if (!use_sun_shadow || shadow_setting.texels.w > 0.5) { return 1.0; }
4375 let distance = length(world_position - frame.camera_position.xyz);
4376 return shadow_sample(shadow_map, shadow_sampler, reflect_sampler, shadow_setting, world_position, normal, distance);
4377}
4378fn sky_irradiance(n: vec3f) -> vec3f {
4379 return sh_irradiance(sky_light.sh, n);
4380}
4381fn sh_irradiance(sh: array<vec4f, 9>, n: vec3f) -> vec3f {
4382 let d = normalize(n);
4383 let basis0 = vec3f(0.282095, 0.488603 * d.y, 0.488603 * d.z);
4384 let basis1 = vec3f(0.488603 * d.x, 1.092548 * d.x * d.y, 1.092548 * d.y * d.z);
4385 let basis2 = vec3f(0.315392 * (3.0 * d.z * d.z - 1.0), 1.092548 * d.x * d.z, 0.546274 * (d.x * d.x - d.y * d.y));
4386 let value = sh[0].xyz * basis0.x + sh[1].xyz * basis0.y + sh[2].xyz * basis0.z
4387 + sh[3].xyz * basis1.x + sh[4].xyz * basis1.y + sh[5].xyz * basis1.z
4388 + sh[6].xyz * basis2.x + sh[7].xyz * basis2.y + sh[8].xyz * basis2.z;
4389 return max(value, vec3f(0.0));
4390}
4391fn ambient_irradiance(position: vec3f, normal: vec3f) -> vec3f {
4392 let grid = sample_live_irradiance(position, normal);
4393 if (grid.x >= 0.0) {
4394 return grid;
4395 }
4396 return sky_irradiance(normal);
4397}
4398fn surface_irradiance(grid: vec3f, normal: vec3f, authored: bool) -> vec3f {
4399 if (grid.x >= 0.0) {
4400 return grid;
4401 }
4402 if (authored) {
4403 return sh_irradiance(authored_light.sh, normal);
4404 }
4405 return sky_irradiance(normal);
4406}
4407fn reflection_reflective(surface: Shaded) -> bool {
4408 return surface.specular > 0.0 || surface.metalness > 0.0 || surface.clearcoat > 0.0 || surface.transmission > 0.0 || surface.thin_film_amount > 0.0;
4409}
4410fn reflection_wants_probes(surface: Shaded, release: f32) -> bool {
4411 if (!use_probes || probe_grid.shade.y <= 0.0 || probe_grid.blend.y < 0.5 || release >= 1.0) {
4412 return false;
4413 }
4414 return reflection_reflective(surface);
4415}
4416fn reflection_sky_visibility(seen: f32, direction: vec3f, lookup: LocalLookup, grid: vec3f, release: f32) -> f32 {
4417 if (seen < 0.0 || grid.x < 0.0 || lookup.weights.x + lookup.weights.y >= 1.0) {
4418 return 1.0;
4419 }
4420 let reference = dot(sky_irradiance(direction), vec3f(0.2126, 0.7152, 0.0722));
4421 if (!(reference > 0.000001)) {
4422 return 1.0;
4423 }
4424 return mix(clamp(seen / reference, 0.0, 1.0), 1.0, release);
4425}
4426fn reflection_irradiance(n: vec3f, world: vec3f, sky_visible: f32) -> vec3f {
4427 var a = 0.0;
4428 var b = 0.0;
4429 if (use_local_reflections) {
4430 let weights = local_probe_weights(selected_reflections.first, selected_reflections.second, selected_reflections.weights_mip.xy, world);
4431 a = clamp(weights.x, 0.0, 1.0);
4432 b = clamp(weights.y, 0.0, 1.0 - a);
4433 }
4434 var reference = sky_irradiance(n) * ((1.0 - a - b) * sky_visible);
4435 if (a != 0.0) {
4436 reference += local_probe_reference(selected_reflections.first, world, n, local_probes, reflect_sampler, selected_reflections.weights_mip.z) * (PI * a);
4437 }
4438 if (b != 0.0) {
4439 reference += local_probe_reference(selected_reflections.second, world, n, local_probes, reflect_sampler, selected_reflections.weights_mip.z) * (PI * b);
4440 }
4441 return reference;
4442}
4443fn contact_factor(world_position: vec3f) -> vec3f {
4444 if (contact_params.tint.w < 0.5) { return vec3f(1.0); }
4445 let uv = (world_position.xz - contact_params.rect.xy) * contact_params.rect.zw;
4446 if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) { return vec3f(1.0); }
4447 let open = textureSampleLevel(contact_field, reflect_sampler, uv, 0.0).r;
4448 return mix(contact_params.tint.xyz, vec3f(1.0), open * open);
4449}
4450fn shaded(material: MaterialData, normal: vec3f) -> Shaded {
4451 return Shaded(
4452 material.base_roughness.xyz,
4453 material.base_roughness.w,
4454 material.metal_spec_coat.x,
4455 material.metal_spec_coat.y,
4456 material.metal_spec_coat.z,
4457 material.metal_spec_coat.w,
4458 material.sheen_trans_ior_disp.x,
4459 material.sheen_trans_ior_disp.y,
4460 material.sheen_trans_ior_disp.z,
4461 material.sheen_trans_ior_disp.w,
4462 material.thick_sub_film.x,
4463 material.thick_sub_film.y,
4464 material.tint_absorption.xyz,
4465 material.tint_absorption.w,
4466 material.thick_sub_film.z,
4467 material.thick_sub_film.w,
4468 material.emission_film.w,
4469 material.emission_film.xyz,
4470 material.age_b.w,
4471 normal,
4472 );
4473}
4474struct OpaqueOutput {
4475 @location(0) colour: vec4f,
4476 @location(1) normal_roughness: vec4f,
4477}
4478@fragment fn opaque_main(input: VertexOutput, @builtin(front_facing) front: bool) -> OpaqueOutput {
4479 return opaque_shade(input, front, false);
4480}
4481@fragment fn opaque_lit_main(input: VertexOutput, @builtin(front_facing) front: bool) -> OpaqueOutput {
4482 return opaque_shade(input, front, true);
4483}
4484fn opaque_shade(input: VertexOutput, front: bool, lit: bool) -> OpaqueOutput {
4485 let uv_dx = dpdx(input.uv_alpha.xy);
4486 let uv_dy = dpdy(input.uv_alpha.xy);
4487 let caustic_at = caustic_uv(input.world_position);
4488 let caustic_dx = dpdx(caustic_at);
4489 let caustic_dy = dpdy(caustic_at);
4490 map_footprint(input.object_position);
4491 cutout(input);
4492 let material = materials[input.material];
4493 var geometric = normalize(input.normal.xyz);
4494 if (!front) {
4495 geometric = -geometric;
4496 }
4497 let mapped = map_evaluate_with(material.base_roughness.xyz, material.base_roughness.w, material.metal_spec_coat.x, material.metal_spec_coat.w, material.thick_sub_film.z, geometric, input.tangent, input.uv_alpha.xy, vec2f(input.uv_alpha.w, input.normal.w), input.object_position, 0.0, input.settings.z, frame.time_size.x, input.material);
4498 var surface = shaded(material, mapped.normal);
4499 surface.base = mapped.base;
4500 surface.roughness = mapped.roughness;
4501 surface.metalness = mapped.metalness;
4502 surface.clearcoat_roughness = mapped.coat_roughness;
4503 surface.thin_film = mapped.thin_film;
4504 let placed = instances[input.instance];
4505 let face = placed.content_face.x;
4506 let shown = face == 0u || (face == 1u && front) || (face == 2u && !front);
4507 let content_crop = select(vec4f(2.0, 2.0, -1.0, -1.0), placed.content_crop, shown);
4508 let content_t = normalize(input.tangent.xyz - mapped.normal * dot(input.tangent.xyz, mapped.normal));
4509 let content_b = cross(mapped.normal, content_t) * input.tangent.w;
4510 surface = content_apply(surface, input.material, input.uv_alpha.xy, uv_dx, uv_dy, placed.content_uv, content_crop, content_t, content_b, geometric);
4511 let n = surface.normal;
4512 let t = normalize(input.tangent.xyz - n * dot(input.tangent.xyz, n));
4513 let b = cross(n, t) * input.tangent.w;
4514 let view = normalize(frame.camera_position.xyz - input.world_position);
4515 let light = normalize(frame.sun_direction.xyz);
4516 let wo = vec3f(dot(view, t), dot(view, b), dot(view, n));
4517 let wi = vec3f(dot(light, t), dot(light, b), dot(light, n));
4518 let facing = max(dot(n, light), 0.0);
4519 var sun = frame.sun_colour_intensity.xyz * frame.sun_colour_intensity.w * facing;
4520 if (facing > 0.0) {
4521 sun *= shadow_visibility(input.world_position, geometric);
4522 if (lit && any(sun > vec3f(0.0))) {
4523 sun *= shadow_tint(input.world_position, geometric, input.instance);
4524 }
4525 let shaded = all(sun == vec3f(0.0));
4526 if (use_canopy && frame.flags.z != 0u && !shaded) {
4527 sun *= canopy_light(input.world_position);
4528 }
4529 if (use_caustics && (placed.content_face.z & 2u) != 0u && !shaded) {
4530 sun *= caustic_gain(caustic_at, caustic_dx, caustic_dy);
4531 }
4532 }
4533 let contacted = use_contact && (placed.content_face.z & 1u) != 0u;
4534 var contact = vec3f(1.0);
4535 if (contacted) {
4536 contact = contact_factor(input.world_position);
4537 sun *= contact;
4538 }
4539 var film = vec3f(0.0);
4540 var film_normal = vec3f(0.0);
4541 if (filmed(surface)) {
4542 film = film_colour(surface, clamp(wo.z, 0.0, 1.0));
4543 film_normal = film_colour(surface, 1.0);
4544 }
4545 var direct = eval_filmed(surface, wo, wi, film) * sun;
4546 if (lit) {
4547 var local = local_light_radiance(surface, wo, t, b, n, input.world_position, geometric, film);
4548 if (contacted) {
4549 local *= contact;
4550 }
4551 direct += local;
4552 }
4553 let release = f32((placed.content_face.z >> 8u) & 255u) / 255.0;
4554 let sky_direction = reflect(-view, n);
4555 var seen = -1.0;
4556 let toward = select(vec3f(0.0), sky_direction, reflection_wants_probes(surface, release));
4557 let grid = sample_live_irradiance_toward(input.world_position, n, toward, &seen);
4558 var irradiance = surface_irradiance(grid, n, use_authored_ambient && placed.content_face.w != 0u);
4559 if (contacted) {
4560 irradiance *= contact;
4561 }
4562 let diffuse = surface.base * irradiance / PI * reflection_diffuse_weight(surface, view, n, film, film_normal);
4563 let previous_uv = input.previous_clip.xy / input.previous_clip.w * vec2f(0.5, -0.5) + vec2f(0.5);
4564 let view_normal = normalize((frame.view * vec4f(n, 0.0)).xyz);
4565 var ssr = vec4f(0.0);
4566 if (use_ssr) {
4567 ssr = ssr_resolve(ssr_history, ssr_guide, reflect_sampler, previous_uv, frame.time_size.yz, input.previous_clip.w, view_normal);
4568 }
4569 var lookup = LocalLookup(vec2f(0.0), sky_direction, sky_direction);
4570 if (reflection_reflective(surface)) {
4571 lookup = local_lookup(selected_reflections.first, selected_reflections.second, selected_reflections.weights_mip.xy, input.world_position, sky_direction, local_probes, reflect_sampler, selected_reflections.weights_mip.z);
4572 }
4573 let sky_visible = select(1.0, reflection_sky_visibility(seen, sky_direction, lookup, grid, release), any(toward != vec3f(0.0)));
4574 var specular = reflection_radiance(surface, view, n, ssr, sky_cube, local_probes, selected_reflections.first, selected_reflections.second, lookup, reflect_sampler, selected_reflections.weights_mip.z, selected_reflections.weights_mip.w, film, film_normal, sky_visible);
4575 if (probe_grid.shade.x > 0.0 && grid.x >= 0.0) {
4576 let occlusion = mix(probe_reflection_occlusion(grid, reflection_irradiance(n, input.world_position, sky_visible)), 1.0, release);
4577 if (occlusion < 1.0) {
4578 specular *= mix(occlusion, 1.0, clamp(ssr.a, 0.0, 1.0));
4579 }
4580 }
4581 var output: OpaqueOutput;
4582 output.colour = vec4f(max(direct + diffuse + specular + surface.emission, vec3f(0.0)), 1.0);
4583 output.normal_roughness = vec4f(view_normal, surface.roughness);
4584 return output;
4585}
4586"#;
4587
4588const CARD_SHADER: &str = r#"
4589struct FrameUniforms {
4590 view_projection: mat4x4f,
4591 motion_view_projection: mat4x4f,
4592 previous_view_projection: mat4x4f,
4593 view: mat4x4f,
4594 camera_position: vec4f,
4595 sun_direction: vec4f,
4596 sun_colour_intensity: vec4f,
4597 time_size: vec4f,
4598 flags: vec4u,
4599 wind: vec4f,
4600 wind_direction: vec4f,
4601 previous_wind_direction: vec4f,
4602}
4603struct SkyLight {
4604 sh: array<vec4f, 9>,
4605 info: vec4f,
4606}
4607struct CanopyParams {
4608 center_extent: vec4f,
4609 sun_receiver: vec4f,
4610 wind: vec4f,
4611 direction: vec4f,
4612 projected_extent: vec4f,
4613 mode_sharpness: vec4f,
4614}
4615struct Card {
4616 position_size: vec4f,
4617 right_size: vec4f,
4618 up: vec4f,
4619 pivot_level: vec4f,
4620 motion_color: vec4f,
4621 atlas: vec4f,
4622 lod: vec4f,
4623}
4624@group(0) @binding(0) var<uniform> frame: FrameUniforms;
4625@group(1) @binding(0) var<uniform> shadow_setting: ShadowSample;
4626@group(1) @binding(1) var shadow_map: texture_depth_2d_array;
4627@group(1) @binding(2) var shadow_sampler: sampler_comparison;
4628@group(2) @binding(0) var<uniform> params: CanopyParams;
4629@group(2) @binding(1) var<storage, read> cards: array<Card>;
4630@group(2) @binding(2) var alpha_atlas: texture_2d<f32>;
4631@group(2) @binding(3) var alpha_sampler: sampler;
4632@group(3) @binding(9) var canopy_cookie: texture_2d<f32>;
4633@group(3) @binding(10) var canopy_sampler: sampler;
4634@group(3) @binding(12) var<uniform> sky_light: SkyLight;
4635fn tree_sway(rest: vec3f, pivot: vec3f, level: f32, stiffness: f32, wind_time: f32, strength: f32, direction: vec3f) -> vec3f {
4636 let d = normalize(direction.xz + vec2f(0.00001, 0.0));
4637 let arm = length(rest - pivot);
4638 let phase = wind_time * (0.7 + level * 0.41) + pivot.x * 0.83 + pivot.z * 0.67;
4639 let bend = sin(phase) * strength * (1.0 - stiffness) * arm * (0.012 + 0.009 * level);
4640 return rest + vec3f(d.x * bend, -abs(bend) * 0.12, d.y * bend);
4641}
4642struct CardOut {
4643 @builtin(position) clip: vec4f,
4644 @location(0) uv: vec2f,
4645 @location(1) @interpolate(flat) atlas: vec4f,
4646 @location(2) @interpolate(flat) color: vec3f,
4647 @location(3) world: vec3f,
4648 @location(4) @interpolate(flat) normal: vec3f,
4649 @location(5) current_clip: vec4f,
4650 @location(6) previous_clip: vec4f,
4651 @location(7) @interpolate(flat) lod: vec4f,
4652}
4653@vertex fn card_vertex(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> CardOut {
4654 let card = cards[instance];
4655 let corners = array<vec2f, 6>(vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0), vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0));
4656 let corner = corners[vertex];
4657 let rest = card.position_size.xyz + card.right_size.xyz * card.position_size.w * corner.x + card.up.xyz * card.right_size.w * corner.y;
4658 let origin = vec3f(params.center_extent.x, params.wind.z, params.center_extent.y);
4659 let moved = tree_sway(rest, card.pivot_level.xyz, card.pivot_level.w, card.motion_color.x, frame.wind.x, frame.wind.y, frame.wind_direction.xyz) + origin;
4660 let before = tree_sway(rest, card.pivot_level.xyz, card.pivot_level.w, card.motion_color.x, frame.wind.z, frame.wind.w, frame.previous_wind_direction.xyz) + origin;
4661 var out: CardOut;
4662 out.clip = frame.view_projection * vec4f(moved, 1.0);
4663 out.current_clip = frame.motion_view_projection * vec4f(moved, 1.0);
4664 out.previous_clip = frame.previous_view_projection * vec4f(before, 1.0);
4665 out.uv = (corner + vec2f(1.0)) * 0.5;
4666 out.atlas = card.atlas;
4667 out.color = card.motion_color.yzw;
4668 out.world = moved;
4669 out.normal = normalize(cross(card.right_size.xyz, card.up.xyz));
4670 out.lod = card.lod;
4671 return out;
4672}
4673fn card_texel(input: CardOut) -> vec4f {
4674 return textureSample(alpha_atlas, alpha_sampler, input.atlas.xy + input.uv * input.atlas.zw);
4675}
4676fn card_keep(input: CardOut) -> bool {
4677 if (input.lod.x >= 1.0) { return true; }
4678 let near = lod_keep(vec2u(input.clip.xy), u32(input.lod.y), input.lod.x);
4679 return near == (input.lod.z > 0.5);
4680}
4681struct PrepassOutput {
4682 @location(0) velocity: vec2f,
4683 @location(1) id: u32,
4684}
4685@fragment fn card_prepass(input: CardOut) -> PrepassOutput {
4686 if (card_texel(input).r < 0.5 || !card_keep(input)) {
4687 discard;
4688 }
4689 let current = input.current_clip.xy / input.current_clip.w;
4690 let previous = input.previous_clip.xy / input.previous_clip.w;
4691 var output: PrepassOutput;
4692 output.velocity = (previous - current) * vec2f(0.5, -0.5);
4693 output.id = frame.flags.w;
4694 return output;
4695}
4696fn card_sky(n: vec3f) -> vec3f {
4697 let d = normalize(n);
4698 let sh = sky_light.sh;
4699 let value = sh[0].xyz * 0.282095 + sh[1].xyz * (0.488603 * d.y) + sh[2].xyz * (0.488603 * d.z)
4700 + sh[3].xyz * (0.488603 * d.x) + sh[4].xyz * (1.092548 * d.x * d.y) + sh[5].xyz * (1.092548 * d.y * d.z)
4701 + sh[6].xyz * (0.315392 * (3.0 * d.z * d.z - 1.0)) + sh[7].xyz * (1.092548 * d.x * d.z) + sh[8].xyz * (0.546274 * (d.x * d.x - d.y * d.y));
4702 return max(value, vec3f(0.0));
4703}
4704struct OpaqueOutput {
4705 @location(0) colour: vec4f,
4706 @location(1) normal_roughness: vec4f,
4707}
4708@fragment fn card_opaque(input: CardOut, @builtin(front_facing) front: bool) -> OpaqueOutput {
4709 let texel = card_texel(input);
4710 if (texel.r < 0.5 || !card_keep(input)) {
4711 discard;
4712 }
4713 let n = select(-input.normal, input.normal, front);
4714 let light = normalize(frame.sun_direction.xyz);
4715 let facing = dot(n, light);
4716 let diffuse = max(facing, 0.0);
4717 let transmission = max(-facing, 0.0);
4718 var lit = 1.0;
4719 if (shadow_setting.texels.w < 0.5) {
4720 lit = shadow_sample(shadow_map, shadow_sampler, canopy_sampler, shadow_setting, input.world, n, length(input.world - frame.camera_position.xyz));
4721 }
4722 let projected = input.world.xz - light.xz / max(light.y, 0.05) * (input.world.y - params.sun_receiver.w);
4723 let cookie_uv = (projected - params.projected_extent.xy) / params.projected_extent.zw + vec2f(0.5);
4724 if (all(cookie_uv >= vec2f(0.0)) && all(cookie_uv <= vec2f(1.0))) {
4725 let occlusion = textureSample(canopy_cookie, canopy_sampler, cookie_uv).r;
4726 let depth = clamp((params.wind.w - input.world.y) / max(params.wind.w - params.wind.z, 0.01), 0.0, 1.0);
4727 lit *= 1.0 - occlusion * depth * 0.75;
4728 }
4729 let pigment = mix(input.color, vec3f(1.0, 0.44, 0.61) * length(input.color) * 0.58, texel.g * 0.7);
4730 let petal = mix(pigment, vec3f(1.0, 0.72, 0.38) * length(input.color) * 0.58, texel.b * 0.68);
4731 let sun = frame.sun_colour_intensity.xyz * frame.sun_colour_intensity.w * lit;
4732 let colour = petal * (card_sky(n) + sun * diffuse) * 0.31830988 + vec3f(1.0, 0.47, 0.64) * sun * transmission * 0.17;
4733 var output: OpaqueOutput;
4734 output.colour = vec4f(colour, 1.0);
4735 output.normal_roughness = vec4f(normalize((frame.view * vec4f(n, 0.0)).xyz), 0.8);
4736 return output;
4737}
4738"#;
4739
4740#[cfg(test)]
4741mod tests {
4742 use super::*;
4743 use crate::deform::{Deformer, RollerFrame};
4744 use crate::lod::{LodSet, MeshUploader};
4745 use pfx_geom::shapes::Shape;
4746 use std::f32::consts::PI;
4747
4748 fn identity() -> Matrix {
4749 [
4750 [1.0, 0.0, 0.0, 0.0],
4751 [0.0, 1.0, 0.0, 0.0],
4752 [0.0, 0.0, 1.0, 0.0],
4753 [0.0, 0.0, 0.0, 1.0],
4754 ]
4755 }
4756
4757 fn perspective(aspect: f32) -> Matrix {
4758 let near = 0.1;
4759 let far = 100.0;
4760 let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
4761 [
4762 [f / aspect, 0.0, 0.0, 0.0],
4763 [0.0, f, 0.0, 0.0],
4764 [0.0, 0.0, far / (near - far), -1.0],
4765 [0.0, 0.0, far * near / (near - far), 0.0],
4766 ]
4767 }
4768
4769 fn look_down(height: f32) -> Matrix {
4770 [
4771 [1.0, 0.0, 0.0, 0.0],
4772 [0.0, 0.0, 1.0, 0.0],
4773 [0.0, -1.0, 0.0, 0.0],
4774 [0.0, 0.0, -height, 1.0],
4775 ]
4776 }
4777
4778 #[test]
4779 fn matrix_and_velocity() {
4780 let mut moved = identity();
4781 moved[3] = [2.0, -3.0, 4.0, 1.0];
4782 assert_eq!(
4783 transform(moved, [1.0, 2.0, 3.0, 1.0]),
4784 [3.0, -1.0, 7.0, 1.0]
4785 );
4786 assert_eq!(multiply(identity(), moved), moved);
4787 assert_eq!(multiply(moved, identity()), moved);
4788 let previous = [0.0, 0.0, 0.5, 1.0];
4789 let current = [0.5, -0.5, 0.5, 1.0];
4790 assert_eq!(velocity_uv(current, previous), [-0.25, -0.25]);
4791 assert_eq!(
4792 velocity_pixels(current, previous, [200.0, 100.0]),
4793 [-50.0, -25.0]
4794 );
4795 assert_eq!(
4796 velocity_pixels(previous, previous, [200.0, 100.0]),
4797 [0.0, 0.0]
4798 );
4799 let mut scaled = identity();
4800 scaled[0][0] = 2.0;
4801 scaled[1][1] = 4.0;
4802 let normals = normal_columns(scaled).unwrap();
4803 assert_eq!(normals[0][0], 0.5);
4804 assert_eq!(normals[1][1], 0.25);
4805 let shift = jitter_matrix([0.25, -0.5]);
4806 assert_eq!(
4807 transform(shift, [0.0, 0.0, 0.3, 1.0]),
4808 [0.25, -0.5, 0.3, 1.0]
4809 );
4810 }
4811
4812 #[test]
4813 fn gpu_layouts_and_shader_entry_points() {
4814 assert_eq!(std::mem::size_of::<Vertex>(), 64);
4815 assert_eq!(std::mem::size_of::<GpuInstance>(), 304);
4816 assert_eq!(std::mem::size_of::<Uniforms>(), 384);
4817 assert_eq!(std::mem::size_of::<SkyLight>(), 160);
4818 assert_eq!(std::mem::size_of::<GpuSway>(), 32);
4819 assert_eq!(std::mem::size_of::<PackedMaterial>(), PackedMaterial::SIZE);
4820 let material = Material {
4821 base: [0.2, 0.4, 0.6],
4822 roughness: 0.3,
4823 ..Default::default()
4824 };
4825 let packed = PackedMaterial::pack(&material);
4826 assert_eq!(packed.base_roughness, [0.2, 0.4, 0.6, 0.3]);
4827 assert_eq!(packed.metal_spec_coat[1], material.specular);
4828 let shader = shader_source();
4829 for entry in [
4830 "fn eval(",
4831 "@vertex fn vertex_main",
4832 "@vertex fn shadow_vertex",
4833 "@vertex fn shadow_clip_vertex",
4834 "@fragment fn shadow_clip_fragment",
4835 "@vertex fn shadow_cut_vertex",
4836 "@fragment fn shadow_cut_fragment",
4837 "fn content_cut(",
4838 "fn content_apply(",
4839 "fn content_composite(",
4840 "@fragment fn prepass_main",
4841 "@fragment fn opaque_main",
4842 "fn shadow_visibility",
4843 "fn map_evaluate(",
4844 "fn sample_live_irradiance(",
4845 "fn reflection_radiance(",
4846 "fn canopy_light(",
4847 "fn deform_vertex(",
4848 "fn tree_sway(",
4849 "fn lod_keep(",
4850 "fn local_light_radiance(",
4851 "fn shadow_tint(",
4852 "@vertex fn transmit_vertex",
4853 "@fragment fn transmit_fragment",
4854 ] {
4855 assert!(shader.contains(entry), "{entry}");
4856 }
4857 }
4858
4859 fn binding_kind(module: &naga::Module, variable: &naga::GlobalVariable) -> String {
4860 match (&variable.space, &module.types[variable.ty].inner) {
4861 (naga::AddressSpace::Uniform, _) => "uniform".into(),
4862 (naga::AddressSpace::Storage { access }, _) => {
4863 if access.contains(naga::StorageAccess::STORE) {
4864 "storage".into()
4865 } else {
4866 "read storage".into()
4867 }
4868 }
4869 (_, naga::TypeInner::Sampler { comparison }) => {
4870 if *comparison {
4871 "comparison sampler".into()
4872 } else {
4873 "sampler".into()
4874 }
4875 }
4876 (
4877 _,
4878 naga::TypeInner::Image {
4879 dim,
4880 arrayed,
4881 class,
4882 },
4883 ) => {
4884 let class = match class {
4885 naga::ImageClass::Depth { .. } => "depth",
4886 naga::ImageClass::Sampled {
4887 kind: naga::ScalarKind::Uint,
4888 ..
4889 } => "uint",
4890 naga::ImageClass::Sampled { .. } => "float",
4891 _ => "storage",
4892 };
4893 format!("{class} {dim:?} {arrayed}")
4894 }
4895 other => format!("{other:?}"),
4896 }
4897 }
4898
4899 fn entry_kind(entry: &wgpu::BindGroupLayoutEntry) -> String {
4900 match entry.ty {
4901 wgpu::BindingType::Buffer {
4902 ty: wgpu::BufferBindingType::Uniform,
4903 ..
4904 } => "uniform".into(),
4905 wgpu::BindingType::Buffer {
4906 ty: wgpu::BufferBindingType::Storage { read_only },
4907 ..
4908 } => if read_only { "read storage" } else { "storage" }.into(),
4909 wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison) => {
4910 "comparison sampler".into()
4911 }
4912 wgpu::BindingType::Sampler(_) => "sampler".into(),
4913 wgpu::BindingType::Texture {
4914 sample_type,
4915 view_dimension,
4916 ..
4917 } => {
4918 let class = match sample_type {
4919 wgpu::TextureSampleType::Depth => "depth",
4920 wgpu::TextureSampleType::Uint => "uint",
4921 _ => "float",
4922 };
4923 let (dim, arrayed) = match view_dimension {
4924 wgpu::TextureViewDimension::D2 => ("D2", false),
4925 wgpu::TextureViewDimension::D2Array => ("D2", true),
4926 wgpu::TextureViewDimension::Cube => ("Cube", false),
4927 wgpu::TextureViewDimension::CubeArray => ("Cube", true),
4928 wgpu::TextureViewDimension::D3 => ("D3", false),
4929 wgpu::TextureViewDimension::D1 => ("D1", false),
4930 };
4931 format!("{class} {dim} {arrayed}")
4932 }
4933 _ => "other".into(),
4934 }
4935 }
4936
4937 pub fn check_pipeline(
4938 source: &str,
4939 entries: &[&str],
4940 layouts: &[Vec<wgpu::BindGroupLayoutEntry>],
4941 ) -> usize {
4942 let module = naga::front::wgsl::parse_str(source).unwrap();
4943 let info = naga::valid::Validator::new(
4944 naga::valid::ValidationFlags::all(),
4945 naga::valid::Capabilities::all(),
4946 )
4947 .validate(&module)
4948 .unwrap();
4949 let mut checked = 0;
4950 for name in entries {
4951 let index = module
4952 .entry_points
4953 .iter()
4954 .position(|entry| entry.name == *name)
4955 .unwrap_or_else(|| panic!("missing entry {name}"));
4956 let stage = match module.entry_points[index].stage {
4957 naga::ShaderStage::Vertex => wgpu::ShaderStages::VERTEX,
4958 naga::ShaderStage::Fragment => wgpu::ShaderStages::FRAGMENT,
4959 _ => wgpu::ShaderStages::COMPUTE,
4960 };
4961 let function = info.get_entry_point(index);
4962 for (handle, variable) in module.global_variables.iter() {
4963 let Some(binding) = &variable.binding else {
4964 continue;
4965 };
4966 if function[handle].is_empty() {
4967 continue;
4968 }
4969 let layout = layouts
4970 .get(binding.group as usize)
4971 .unwrap_or_else(|| panic!("{name} uses group {}", binding.group));
4972 let entry = layout
4973 .iter()
4974 .find(|entry| entry.binding == binding.binding)
4975 .unwrap_or_else(|| {
4976 panic!(
4977 "{name} uses @group({}) @binding({}) missing from the layout",
4978 binding.group, binding.binding
4979 )
4980 });
4981 assert_eq!(
4982 binding_kind(&module, variable),
4983 entry_kind(entry),
4984 "{name} @group({}) @binding({})",
4985 binding.group,
4986 binding.binding
4987 );
4988 assert!(
4989 entry.visibility.contains(stage),
4990 "{name} @group({}) @binding({}) is not visible to its stage",
4991 binding.group,
4992 binding.binding
4993 );
4994 checked += 1;
4995 }
4996 }
4997 checked
4998 }
4999
5000 #[test]
5001 fn bind_layout_validates_against_every_pipeline() {
5002 let frame_layouts = [
5003 scene_entries(),
5004 shadow_entries(),
5005 deformer_entries(),
5006 lighting_entries(),
5007 ];
5008 let checked = check_pipeline(
5009 &shader_source(),
5010 &[
5011 "vertex_main",
5012 "prepass_main",
5013 "opaque_main",
5014 "opaque_lit_main",
5015 ],
5016 &frame_layouts,
5017 );
5018 assert!(checked >= 20, "{checked} frame bindings checked");
5019 check_pipeline(
5020 &shader_source(),
5021 &[
5022 "shadow_vertex",
5023 "shadow_clip_vertex",
5024 "shadow_clip_fragment",
5025 "transmit_vertex",
5026 "transmit_fragment",
5027 ],
5028 &[scene_entries(), cascade_entries(), deformer_entries()],
5029 );
5030 check_pipeline(
5031 &shader_source(),
5032 &["shadow_cut_vertex", "shadow_cut_fragment"],
5033 &[
5034 scene_entries(),
5035 cascade_entries(),
5036 deformer_entries(),
5037 lighting_entries(),
5038 ],
5039 );
5040 check_pipeline(
5041 &card_source(),
5042 &["card_vertex", "card_prepass", "card_opaque"],
5043 &[
5044 scene_entries(),
5045 shadow_entries(),
5046 card_entries(),
5047 lighting_entries(),
5048 ],
5049 );
5050 let maps = format!(
5051 "{}\n{}",
5052 maps::shader_source(),
5053 "@fragment fn probe() -> @location(0) vec4f { let r = map_evaluate(vec3f(1.0), 0.5, 0.0, 0.1, 0.0, vec3f(0.0, 1.0, 0.0), vec4f(1.0, 0.0, 0.0, 1.0), vec2f(0.5), vec3f(0.0), 0.0, 0.5, 0.0, 0u); return vec4f(r.base, 1.0); }"
5054 );
5055 check_pipeline(
5056 &maps,
5057 &["probe"],
5058 &[vec![], vec![], vec![], lighting_entries()],
5059 );
5060 let probes = format!(
5061 "{PROBE_WGSL}\n@fragment fn probe() -> @location(0) vec4f {{ return vec4f(sample_live_irradiance(vec3f(0.0), vec3f(0.0, 1.0, 0.0)), 1.0); }}"
5062 );
5063 check_pipeline(
5064 &probes,
5065 &["probe"],
5066 &[vec![], vec![], vec![], lighting_entries()],
5067 );
5068 let canopy = format!(
5069 "{}\n@fragment fn probe() -> @location(0) vec4f {{ return vec4f(canopy_light(vec3f(0.0))); }}",
5070 canopy_light_wgsl(&Gobo::default())
5071 );
5072 check_pipeline(
5073 &canopy,
5074 &["probe"],
5075 &[vec![], vec![], vec![], lighting_entries()],
5076 );
5077 }
5078
5079 struct Counter(u32);
5080
5081 impl MeshUploader for Counter {
5082 fn upload_mesh(&mut self, _data: MeshData<'_>) -> Result<MeshHandle, String> {
5083 self.0 += 1;
5084 Ok(MeshHandle(self.0 + 100))
5085 }
5086 }
5087
5088 fn lod_camera(z: f32) -> Camera {
5089 let mut projection = identity();
5090 projection[1][1] = 1.0 / (0.75_f32 * 0.5).tan();
5091 Camera {
5092 view: identity(),
5093 projection,
5094 previous_view_projection: identity(),
5095 position: [0.0, 0.0, z],
5096 }
5097 }
5098
5099 #[test]
5100 fn lod_output_feeds_the_draws() {
5101 let slab = Shape::Slab {
5102 half_length: 2.8,
5103 radius: 1.8,
5104 height: 0.16,
5105 fillet: 0.12,
5106 };
5107 let mut set = LodSet::new(2_000_000);
5108 let near = Instance::new(MeshHandle(0), identity(), 0, 1);
5109 let mut far_model = identity();
5110 far_model[3][2] = -30.0;
5111 let far = Instance::new(MeshHandle(0), far_model, 0, 2);
5112 set.add_shape(near, slab);
5113 set.add_shape(far, slab);
5114 let mut uploader = Counter(0);
5115 set.select(lod_camera(5.0), 2160);
5116 let close = set.upload_selected(&mut uploader).unwrap();
5117 let drawn = batches(&close, |_| false, |_| false);
5118 assert_eq!(drawn.len(), 2);
5119 assert_eq!(drawn[0].mesh, close[0].mesh);
5120 assert_eq!(drawn[1].mesh, close[1].mesh);
5121 assert_ne!(drawn[0].mesh, MeshHandle(0));
5122 assert_ne!(drawn[0].mesh, drawn[1].mesh);
5123 assert_eq!(drawn[0].range, 0..1);
5124 set.select(lod_camera(-25.0), 2160);
5125 let moved = set.upload_selected(&mut uploader).unwrap();
5126 let redrawn = batches(&moved, |_| false, |_| false);
5127 assert_ne!(redrawn[0].mesh, drawn[0].mesh);
5128 assert_ne!(redrawn[1].mesh, drawn[1].mesh);
5129 assert!(redrawn.iter().all(|batch| batch.mesh.0 > 100));
5130 }
5131
5132 #[test]
5133 fn batches_split_on_mesh_sidedness_and_motion() {
5134 let mut instances = vec![Instance::new(MeshHandle(1), identity(), 0, 1); 3];
5135 instances[1].deformer = DeformerId(0);
5136 instances.push(Instance::new(MeshHandle(2), identity(), 0, 4));
5137 let drawn = batches(&instances, |mesh| mesh == MeshHandle(2), |_| false);
5138 assert_eq!(drawn.len(), 4);
5139 assert!(!drawn[0].two_sided && !drawn[0].moving);
5140 assert!(drawn[1].two_sided && drawn[1].moving);
5141 assert_eq!(drawn[2].range, 2..3);
5142 assert!(drawn[3].moving && !drawn[3].two_sided);
5143 let mut surfaces = vec![InstanceSurface::default(); instances.len()];
5144 let key = static_key(&instances, &drawn, &surfaces);
5145 instances[3].model[3][0] = 4.0;
5146 assert_eq!(static_key(&instances, &drawn, &surfaces), key);
5147 instances[0].model[3][0] = 4.0;
5148 let moved = static_key(&instances, &drawn, &surfaces);
5149 assert_ne!(moved, key);
5150 surfaces[2].clip[0] = [0.0, 1.0, 0.0, 0.1];
5151 let cut = batches(
5152 &instances,
5153 |mesh| mesh == MeshHandle(2),
5154 |i| surfaces[i].clipped(),
5155 );
5156 assert_eq!(cut.len(), 4);
5157 assert!(cut[2].clipped && !cut[0].clipped);
5158 assert_ne!(static_key(&instances, &cut, &surfaces), moved);
5159 let unclipped = vec![Instance::new(MeshHandle(1), identity(), 0, 1); 3];
5160 let split = batches(&unclipped, |_| false, |i| i == 1);
5161 assert_eq!(split.len(), 3);
5162 assert_eq!(split[1].range, 1..2);
5163 }
5164
5165 #[test]
5166 fn instances_that_cast_no_shadow_stay_out_of_every_caster_list() {
5167 let mut instances = vec![
5168 Instance::new(MeshHandle(1), identity(), 0, 1),
5169 Instance::new(MeshHandle(1), identity(), 1, 2),
5170 Instance::new(MeshHandle(1), identity(), 1, 3),
5171 ];
5172 let surfaces = vec![InstanceSurface::default(); instances.len()];
5173 let all = batches(&instances, |_| false, |_| false);
5174 assert_eq!(all.len(), 1);
5175 assert!(all[0].casts_shadow);
5176 assert_eq!(shadow_batches(&all).len(), 1);
5177 let key = static_key(&instances, &all, &surfaces);
5178 instances[1].casts_shadow = false;
5179 let split = batches(&instances, |_| false, |_| false);
5180 assert_eq!(split.len(), 3);
5181 assert_eq!(
5182 split
5183 .iter()
5184 .map(|batch| batch.casts_shadow)
5185 .collect::<Vec<_>>(),
5186 [true, false, true]
5187 );
5188 let casters = shadow_batches(&split);
5189 assert_eq!(casters.len(), 2);
5190 assert!(casters.iter().all(|batch| batch.range != (1..2)));
5191 let transmitting = transmission_batches(&split, &instances, |material| material == 1);
5192 assert_eq!(transmitting.len(), 1);
5193 assert_eq!(transmitting[0].range, 2..3);
5194 assert_ne!(static_key(&instances, &split, &surfaces), key);
5195 let before = static_key(&instances, &split, &surfaces);
5196 instances[1].model[3][0] = 9.0;
5197 let moved = batches(&instances, |_| false, |_| false);
5198 assert_eq!(static_key(&instances, &moved, &surfaces), before);
5199 instances[1].previous_model = identity();
5200 let restless = batches(&instances, |_| false, |_| false);
5201 assert!(restless[1].moving);
5202 assert!(shadow_batches(&restless).iter().all(|batch| !batch.moving));
5203 assert_eq!(static_key(&instances, &restless, &surfaces), before);
5204 instances[1].previous_model = instances[1].model;
5205 instances[1].casts_shadow = true;
5206 let back = batches(&instances, |_| false, |_| false);
5207 assert_ne!(static_key(&instances, &back, &surfaces), before);
5208 }
5209
5210 #[test]
5211 fn instance_surfaces_clip_and_place_content() {
5212 let plain = InstanceSurface::default();
5213 assert!(!plain.clipped());
5214 assert!(!plain.cuts([5.0, 5.0, 5.0]));
5215 assert_eq!(plain.content_uv([0.3, 0.7]), Some([0.3, 0.7]));
5216 let strip = InstanceSurface {
5217 clip: [[0.0, 1.0, 0.0, 0.1449], [0.0, -1.0, 0.0, 0.0916]],
5218 ..Default::default()
5219 };
5220 assert!(strip.clipped());
5221 assert!(strip.cuts([0.0, 0.2, 0.0]));
5222 assert!(strip.cuts([0.0, -0.1, 0.0]));
5223 assert!(!strip.cuts([0.0, 0.0, 0.0]));
5224 let tape = InstanceSurface {
5225 uv_offset: [0.0, 0.25],
5226 crop: [0.0, 0.0, 1.0, 2000.0 / 2048.0],
5227 ..Default::default()
5228 };
5229 assert_eq!(tape.content_uv([0.5, 0.5]), Some([0.5, 0.75]));
5230 assert_eq!(tape.content_uv([0.5, 0.9]), None);
5231 let broken = InstanceSurface {
5232 crop: [0.0, f32::NAN, 1.0, 1.0],
5233 ..Default::default()
5234 };
5235 assert!(!broken.valid());
5236 }
5237
5238 #[test]
5239 fn an_instance_takes_all_of_the_reflection_occlusion_unless_it_says_otherwise() {
5240 let plain = InstanceSurface::default();
5241 assert_eq!(plain.reflection_occlusion, 1.0);
5242 assert!(plain.valid());
5243 assert_eq!(plain.occlusion_release(), 0);
5244 let release = |scale: f32| {
5245 InstanceSurface {
5246 reflection_occlusion: scale,
5247 ..Default::default()
5248 }
5249 .occlusion_release()
5250 };
5251 assert_eq!(release(0.0), 255);
5252 assert_eq!(release(0.5), 128);
5253 assert_eq!(release(0.25), 191);
5254 for scale in [-0.1, 1.1, f32::NAN, f32::INFINITY] {
5255 let surface = InstanceSurface {
5256 reflection_occlusion: scale,
5257 ..Default::default()
5258 };
5259 assert!(!surface.valid(), "{scale}");
5260 }
5261 }
5262
5263 #[test]
5264 fn ambient_prefers_the_grid_then_the_sky() {
5265 let mut sh = [[0.0; 3]; 9];
5266 sh[0] = [1.0, 2.0, 3.0];
5267 sh[1] = [0.5, 0.5, 0.5];
5268 let up = sky_irradiance(&sh, [0.0, 1.0, 0.0]);
5269 let down = sky_irradiance(&sh, [0.0, -1.0, 0.0]);
5270 assert!(up[0] > down[0]);
5271 assert_eq!(ambient_irradiance(None, &sh, [0.0, 1.0, 0.0]), up);
5272 assert_eq!(
5273 ambient_irradiance(Some([-1.0; 3]), &sh, [0.0, 1.0, 0.0]),
5274 up
5275 );
5276 assert_eq!(
5277 ambient_irradiance(Some([0.2, 0.3, 0.4]), &sh, [0.0, 1.0, 0.0]),
5278 [0.2, 0.3, 0.4]
5279 );
5280 let lighting = crate::sky::SkyLighting::new(default_sky(), 4);
5281 let mut settled = lighting.clone();
5282 while !settled.complete() {
5283 settled.step();
5284 }
5285 for normal in [[0.0, 1.0, 0.0], [0.6, 0.0, 0.8], [0.0, -1.0, 0.0]] {
5286 let reference = settled.irradiance(normal);
5287 let ours = sky_irradiance(&settled.sh, normal);
5288 for channel in 0..3 {
5289 assert!((reference[channel] - ours[channel]).abs() < 1e-5);
5290 }
5291 }
5292 }
5293
5294 fn sphere(frame: &mut Frame) -> MeshHandle {
5295 let mut positions = Vec::new();
5296 let mut normals = Vec::new();
5297 let mut tangents = Vec::new();
5298 let mut uvs = Vec::new();
5299 let mut indices = Vec::new();
5300 let rings = 20;
5301 let sides = 32;
5302 for ring in 0..=rings {
5303 let phi = std::f32::consts::PI * ring as f32 / rings as f32;
5304 for side in 0..=sides {
5305 let theta = std::f32::consts::TAU * side as f32 / sides as f32;
5306 let normal = [phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin()];
5307 positions.push([normal[0] * 0.75, normal[1] * 0.75, normal[2] * 0.75]);
5308 normals.push(normal);
5309 tangents.push([-theta.sin(), 0.0, theta.cos(), 1.0]);
5310 uvs.push([side as f32 / sides as f32, ring as f32 / rings as f32]);
5311 }
5312 }
5313 for ring in 0..rings {
5314 for side in 0..sides {
5315 let a = ring * (sides + 1) + side;
5316 let b = a + sides + 1;
5317 indices.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
5318 }
5319 }
5320 frame
5321 .upload_mesh(MeshData {
5322 positions: &positions,
5323 normals: &normals,
5324 tangents: &tangents,
5325 uvs: &uvs,
5326 uvs1: None,
5327 alpha: None,
5328 indices: &indices,
5329 })
5330 .unwrap()
5331 }
5332
5333 fn quad(frame: &mut Frame, corners: [[f32; 3]; 4]) -> MeshHandle {
5334 frame
5335 .upload_mesh(MeshData {
5336 positions: &corners,
5337 normals: &[[0.0, 1.0, 0.0]; 4],
5338 tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5339 uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5340 uvs1: None,
5341 alpha: None,
5342 indices: &[0, 2, 1, 1, 2, 3],
5343 })
5344 .unwrap()
5345 }
5346
5347 fn plane(frame: &mut Frame) -> MeshHandle {
5348 quad(
5349 frame,
5350 [
5351 [-3.0, -1.0, -1.0],
5352 [3.0, -1.0, -1.0],
5353 [-3.0, -1.0, -9.0],
5354 [3.0, -1.0, -9.0],
5355 ],
5356 )
5357 }
5358
5359 fn half(value: u16) -> f32 {
5360 half::f16::from_bits(value).to_f32()
5361 }
5362
5363 fn calm_scene<'a>(
5364 camera: Camera,
5365 instances: &'a [Instance],
5366 materials: &'a [Material],
5367 sun: Sun,
5368 ) -> Scene<'a> {
5369 Scene {
5370 camera,
5371 time: 0.0,
5372 seed: 7,
5373 sun,
5374 instances,
5375 materials,
5376 deformers: &[],
5377 wind: SceneWind::default(),
5378 }
5379 }
5380
5381 #[test]
5382 #[ignore = "needs a GPU; run under pgpu"]
5383 fn warm_opaque_compiles_a_pipeline_on_the_frame_before_any_draw() {
5384 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5385 let mut renderer = crate::renderer::Renderer::new(gpu, 32, 32).unwrap();
5386 assert!(renderer.frame().pipelines.opaque.is_empty());
5387 renderer.warm_opaque(OpaqueFeatures::NONE, false);
5388 assert_eq!(renderer.frame().pipelines.opaque.len(), 2);
5389 for key in OpaqueKey::both_sides(OpaqueFeatures::NONE, false) {
5390 assert!(renderer.frame().pipelines.opaque.contains_key(&key));
5391 }
5392 renderer.warm_opaque(OpaqueFeatures::NONE, false);
5393 assert_eq!(renderer.frame().pipelines.opaque.len(), 2);
5394 renderer.warm_opaque(OpaqueFeatures::NONE, true);
5395 assert_eq!(renderer.frame().pipelines.opaque.len(), 4);
5396 }
5397
5398 #[test]
5399 #[ignore = "needs a GPU; run under pgpu"]
5400 fn a_shadow_only_scene_compiles_no_opaque_pipeline() {
5401 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5402 let mut frame = Frame::new(gpu, 32, 32).unwrap();
5403 let mesh = quad(
5404 &mut frame,
5405 [
5406 [-1.0, 0.0, -2.0],
5407 [1.0, 0.0, -2.0],
5408 [-1.0, 0.0, -4.0],
5409 [1.0, 0.0, -4.0],
5410 ],
5411 );
5412 let sun = Sun {
5413 direction: [0.2, 0.9, 0.1],
5414 colour: [1.0; 3],
5415 intensity: 1.0,
5416 };
5417 let materials = [Material::default()];
5418 let mut hidden = Instance::new(mesh, identity(), 0, 1);
5419 hidden.shadow_only = true;
5420 let mut shifted = identity();
5421 shifted[3][0] = 0.4;
5422 let mut other = Instance::new(mesh, shifted, 0, 2);
5423 other.shadow_only = true;
5424 let draw = |frame: &mut Frame, instances: &[Instance]| {
5425 let camera = Camera {
5426 view: look_down(4.0),
5427 projection: perspective(1.0),
5428 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5429 position: [0.0, 4.0, 0.0],
5430 };
5431 let scene = calm_scene(camera, instances, &materials, sun);
5432 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5433 frame.encode(&scene, &mut encoder, None, None).unwrap();
5434 frame.gpu.queue.submit(Some(encoder.finish()));
5435 };
5436 assert!(frame.pipelines.opaque.is_empty());
5437 draw(&mut frame, &[hidden, other]);
5438 assert!(frame.pipelines.opaque.is_empty());
5439 assert!(frame.opaque_drawn().is_empty());
5440 draw(&mut frame, &[Instance::new(mesh, identity(), 0, 3)]);
5441 assert_eq!(frame.pipelines.opaque.len(), 1);
5442 }
5443
5444 #[test]
5445 #[ignore = "needs a GPU; run under pgpu"]
5446 fn shadow_only_cube_casts_without_colour_or_id() {
5447 use crate::shadow::{Quality, View};
5448 use pfx_geom::shapes::Shape;
5449
5450 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5451 let mut frame = Frame::new(gpu, 128, 128).unwrap();
5452 let floor = frame
5453 .upload_mesh(MeshData {
5454 positions: &[
5455 [-3.0, -1.0, -1.0],
5456 [3.0, -1.0, -1.0],
5457 [-3.0, -1.0, -9.0],
5458 [3.0, -1.0, -9.0],
5459 ],
5460 normals: &[[0.0, -1.0, 0.0]; 4],
5461 tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5462 uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5463 uvs1: None,
5464 alpha: None,
5465 indices: &[0, 2, 1, 1, 2, 3],
5466 })
5467 .unwrap();
5468 let cube_mesh = Shape::RoundBox {
5469 half: [0.35; 3],
5470 radius: 0.01,
5471 }
5472 .mesh(0.05);
5473 let cube = frame
5474 .upload_mesh(MeshData {
5475 positions: &cube_mesh.positions,
5476 normals: &cube_mesh.normals,
5477 tangents: &cube_mesh.tangents,
5478 uvs: &cube_mesh.uvs,
5479 uvs1: None,
5480 alpha: None,
5481 indices: &cube_mesh.indices,
5482 })
5483 .unwrap();
5484 let mut shadows = Shadows::new(
5485 &frame.gpu.device,
5486 Quality {
5487 resolution: 512,
5488 ..Default::default()
5489 },
5490 );
5491 let view = View {
5492 eye: [0.0, 4.0, 0.0],
5493 forward: [0.0, -1.0, 0.0],
5494 up: [0.0, 0.0, -1.0],
5495 fov_y: 0.9,
5496 aspect: 1.0,
5497 near: 0.1,
5498 far: 12.0,
5499 };
5500 let fit = shadows.fit(&view, [0.4, 0.9, 0.1]);
5501 let camera = Camera {
5502 view: look_down(4.0),
5503 projection: perspective(1.0),
5504 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5505 position: [0.0, 4.0, 0.0],
5506 };
5507 let sun = Sun {
5508 direction: [0.4, 0.9, 0.1],
5509 colour: [1.0; 3],
5510 intensity: 2.0,
5511 };
5512 let materials = [Material::default()];
5513 let mut floor_instance = Instance::new(floor, identity(), 0, 1);
5514 floor_instance.two_sided = true;
5515 let mut cube_model = identity();
5516 cube_model[3][2] = -3.0;
5517 let mut cube_instance = Instance::new(cube, cube_model, 0, 2);
5518 cube_instance.shadow_only = true;
5519 let mut capture = |instances: &[Instance]| {
5520 let scene = calm_scene(camera, instances, &materials, sun);
5521 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5522 frame
5523 .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
5524 .unwrap();
5525 frame.gpu.queue.submit(Some(encoder.finish()));
5526 (
5527 read_target(&frame, &frame.targets.ids, 4),
5528 frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap(),
5529 read_atlas(&frame, &shadows),
5530 )
5531 };
5532 let empty = capture(&[]);
5533 let hidden = capture(&[cube_instance]);
5534 assert_eq!(empty.0, hidden.0);
5535 assert_eq!(empty.1, hidden.1);
5536 let baseline = capture(&[floor_instance]);
5537 let shadowed = capture(&[floor_instance, cube_instance]);
5538 let ids = shadowed
5539 .0
5540 .chunks_exact(4)
5541 .map(|pixel| u32::from_le_bytes(pixel.try_into().unwrap()));
5542 assert!(ids.into_iter().all(|id| id != 2));
5543 assert!(baseline.2.iter().zip(&shadowed.2).any(|(a, b)| a != b));
5544 }
5545
5546 #[test]
5547 #[ignore = "needs a GPU; run under pgpu"]
5548 fn a_sphere_that_casts_no_shadow_leaves_the_floor_as_if_it_were_gone() {
5549 use crate::shadow::{Quality, View};
5550
5551 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5552 let mut frame = Frame::new(gpu, 128, 128).unwrap();
5553 let floor = quad(
5554 &mut frame,
5555 [
5556 [-3.0, -1.0, -3.0],
5557 [3.0, -1.0, -3.0],
5558 [-3.0, -1.0, 3.0],
5559 [3.0, -1.0, 3.0],
5560 ],
5561 );
5562 let ball = sphere(&mut frame);
5563 let mut shadows = Shadows::new(
5564 &frame.gpu.device,
5565 Quality {
5566 resolution: 512,
5567 ..Default::default()
5568 },
5569 );
5570 let view = View {
5571 eye: [0.0, 4.0, 0.0],
5572 forward: [0.0, -1.0, 0.0],
5573 up: [0.0, 0.0, -1.0],
5574 fov_y: 0.9,
5575 aspect: 1.0,
5576 near: 0.1,
5577 far: 12.0,
5578 };
5579 let fit = shadows.fit(&view, [0.4, 0.9, 0.1]);
5580 let camera = Camera {
5581 view: look_down(4.0),
5582 projection: perspective(1.0),
5583 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5584 position: [0.0, 4.0, 0.0],
5585 };
5586 let sun = Sun {
5587 direction: [0.4, 0.9, 0.1],
5588 colour: [1.0; 3],
5589 intensity: 2.0,
5590 };
5591 let materials = [Material::default()];
5592 let mut floor_instance = Instance::new(floor, identity(), 0, 1);
5593 floor_instance.two_sided = true;
5594 let ball_model = identity();
5595 let casting = Instance::new(ball, ball_model, 0, 2);
5596 let mut lit_only = casting;
5597 lit_only.casts_shadow = false;
5598 let mut capture = |instances: &[Instance]| {
5599 let scene = calm_scene(camera, instances, &materials, sun);
5600 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5601 frame
5602 .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
5603 .unwrap();
5604 frame.gpu.queue.submit(Some(encoder.finish()));
5605 (
5606 read_target(&frame, &frame.targets.ids, 4),
5607 frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap(),
5608 read_atlas(&frame, &shadows),
5609 )
5610 };
5611 let removed = capture(&[floor_instance]);
5612 let unshadowed = capture(&[floor_instance, lit_only]);
5613 let shadowed = capture(&[floor_instance, casting]);
5614 let mut nothing = lit_only;
5615 nothing.shadow_only = true;
5616 let gone = capture(&[floor_instance, nothing]);
5617 assert!(gone.0 == removed.0 && gone.1 == removed.1 && gone.2 == removed.2);
5618 let sphere_pixels = |ids: &[u8]| -> Vec<bool> {
5619 ids.chunks_exact(4)
5620 .map(|pixel| u32::from_le_bytes(pixel.try_into().unwrap()) == 2)
5621 .collect()
5622 };
5623 let covered = sphere_pixels(&unshadowed.0);
5624 assert!(covered.iter().filter(|hit| **hit).count() > 200);
5625 assert_eq!(covered, sphere_pixels(&shadowed.0));
5626 assert!(removed.2 == unshadowed.2);
5627 assert!(removed.2.iter().zip(&shadowed.2).any(|(a, b)| a != b));
5628 let mut worst = 0.0f32;
5629 let mut shadow_depth = 0.0f32;
5630 for (pixel, hit) in covered.iter().enumerate() {
5631 if *hit {
5632 continue;
5633 }
5634 for channel in 0..3 {
5635 let at = pixel * 4 + channel;
5636 worst = worst.max((half(removed.1[at]) - half(unshadowed.1[at])).abs());
5637 shadow_depth = shadow_depth.max((half(removed.1[at]) - half(shadowed.1[at])).abs());
5638 }
5639 }
5640 assert!(worst <= 1.0 / 255.0, "floor moved by {worst}");
5641 assert!(shadow_depth > 0.05, "control shadow only {shadow_depth}");
5642 }
5643
5644 #[test]
5645 #[ignore = "needs a GPU; run under pgpu"]
5646 fn a_caustic_lights_its_receiver_only_where_the_sun_reaches() {
5647 use crate::shadow::{Quality, View};
5648 use pfx_geom::shapes::Shape;
5649
5650 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5651 let mut frame = Frame::new(gpu, 128, 128).unwrap();
5652 let floor = frame
5653 .upload_mesh(MeshData {
5654 positions: &[
5655 [-3.0, -1.0, -3.0],
5656 [3.0, -1.0, -3.0],
5657 [-3.0, -1.0, 3.0],
5658 [3.0, -1.0, 3.0],
5659 ],
5660 normals: &[[0.0, 1.0, 0.0]; 4],
5661 tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5662 uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5663 uvs1: None,
5664 alpha: None,
5665 indices: &[0, 2, 1, 1, 2, 3],
5666 })
5667 .unwrap();
5668 let cube_mesh = Shape::RoundBox {
5669 half: [0.35; 3],
5670 radius: 0.01,
5671 }
5672 .mesh(0.05);
5673 let cube = frame
5674 .upload_mesh(MeshData {
5675 positions: &cube_mesh.positions,
5676 normals: &cube_mesh.normals,
5677 tangents: &cube_mesh.tangents,
5678 uvs: &cube_mesh.uvs,
5679 uvs1: None,
5680 alpha: None,
5681 indices: &cube_mesh.indices,
5682 })
5683 .unwrap();
5684 frame.set_content(1, 4, 4, ContentFormat::Linear16).unwrap();
5685 let texel: Vec<u8> = [1.0f32, 0.5, 0.0, 1.0]
5686 .iter()
5687 .flat_map(|v| half::f16::from_f32(*v).to_bits().to_le_bytes())
5688 .collect();
5689 frame
5690 .write_content(1, [0, 0, 4, 4], &texel.repeat(16))
5691 .unwrap();
5692 let mut shadows = Shadows::new(
5693 &frame.gpu.device,
5694 Quality {
5695 resolution: 512,
5696 ..Default::default()
5697 },
5698 );
5699 let toward = [0.1, 1.0, 0.05];
5700 let fit = shadows.fit(
5701 &View {
5702 eye: [0.0, 4.0, 0.0],
5703 forward: [0.0, -1.0, 0.0],
5704 up: [0.0, 0.0, -1.0],
5705 fov_y: 0.9,
5706 aspect: 1.0,
5707 near: 0.1,
5708 far: 12.0,
5709 },
5710 toward,
5711 );
5712 let camera = Camera {
5713 view: look_down(4.0),
5714 projection: perspective(1.0),
5715 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5716 position: [0.0, 4.0, 0.0],
5717 };
5718 let materials = [Material::default()];
5719 let mut floor_instance = Instance::new(floor, identity(), 0, 1);
5720 floor_instance.two_sided = true;
5721 let mut caster = Instance::new(cube, identity(), 0, 2);
5722 caster.shadow_only = true;
5723 let instances = [floor_instance, caster];
5724 let caustic = Caustic {
5725 slot: 1,
5726 rect: [-3.0, -3.0, 6.0, 6.0],
5727 height: -1.0,
5728 strength: 1.0,
5729 receivers: vec![0],
5730 };
5731 let mut capture = |caustic: Option<Caustic>, intensity: f32| {
5732 frame.set_caustic(caustic).unwrap();
5733 let sun = Sun {
5734 direction: toward,
5735 colour: [1.0; 3],
5736 intensity,
5737 };
5738 let scene = calm_scene(camera, &instances, &materials, sun);
5739 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5740 frame
5741 .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
5742 .unwrap();
5743 frame.gpu.queue.submit(Some(encoder.finish()));
5744 frame
5745 .gpu
5746 .readback_rgba16(&frame.targets.hdr)
5747 .unwrap()
5748 .chunks_exact(4)
5749 .map(|p| std::array::from_fn::<f32, 3, _>(|c| half::f16::from_bits(p[c]).to_f32()))
5750 .collect::<Vec<_>>()
5751 };
5752 let plain = capture(None, 2.0);
5753 let lit = capture(Some(caustic.clone()), 2.0);
5754 let ambient = capture(Some(caustic.clone()), 0.0);
5755 let elsewhere = capture(
5756 Some(Caustic {
5757 receivers: vec![1],
5758 ..caustic.clone()
5759 }),
5760 2.0,
5761 );
5762 let off = capture(
5763 Some(Caustic {
5764 rect: [10.0, 10.0, 1.0, 1.0],
5765 ..caustic
5766 }),
5767 2.0,
5768 );
5769 assert_eq!(plain, elsewhere, "a caustic lights only its receivers");
5770 assert_eq!(plain, off, "a caustic lights only inside its rectangle");
5771 let at = |x: usize, y: usize| y * 128 + x;
5772 let (open, shade) = (at(104, 62), at(61, 62));
5773 let sun_open: [f32; 3] = std::array::from_fn(|c| plain[open][c] - ambient[open][c]);
5774 let sun_shade: [f32; 3] = std::array::from_fn(|c| plain[shade][c] - ambient[shade][c]);
5775 assert!(
5776 sun_open.iter().all(|v| *v > 0.1),
5777 "open floor is sunlit: {sun_open:?}"
5778 );
5779 assert!(
5780 (0..3).all(|c| sun_shade[c] < 0.05 * sun_open[c]),
5781 "the caster shades the floor: {sun_shade:?} against {sun_open:?}"
5782 );
5783 for (c, gain) in [2.0f32, 1.5, 1.0].into_iter().enumerate() {
5784 let got = (lit[open][c] - ambient[open][c]) / sun_open[c];
5785 assert!(
5786 (got - gain).abs() < 0.02,
5787 "channel {c}: gain {got}, want {gain}"
5788 );
5789 assert!(
5790 (lit[shade][c] - plain[shade][c]).abs() <= 0.01 * plain[shade][c].max(0.05),
5791 "channel {c}: the caustic shows in the shadow: {} against {}",
5792 lit[shade][c],
5793 plain[shade][c]
5794 );
5795 }
5796 assert!(
5797 frame
5798 .set_caustic(Some(Caustic {
5799 slot: 9,
5800 rect: [0.0, 0.0, 1.0, 1.0],
5801 height: 0.0,
5802 strength: 1.0,
5803 receivers: vec![0],
5804 }))
5805 .is_err()
5806 );
5807 }
5808
5809 #[test]
5810 #[ignore = "needs a GPU; run under pgpu"]
5811 fn two_sided_back_face_flips_lit_normal() {
5812 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5813 let mut frame = Frame::new(gpu, 64, 64).unwrap();
5814 let positions = [
5815 [-0.8, -0.8, -3.0],
5816 [0.8, -0.8, -3.0],
5817 [-0.8, 0.8, -3.0],
5818 [0.8, 0.8, -3.0],
5819 ];
5820 let quad = frame
5821 .upload_mesh(MeshData {
5822 positions: &positions,
5823 normals: &[[0.0, 0.0, -1.0]; 4],
5824 tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5825 uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5826 uvs1: None,
5827 alpha: None,
5828 indices: &[0, 2, 1, 1, 2, 3],
5829 })
5830 .unwrap();
5831 let camera = Camera {
5832 view: identity(),
5833 projection: perspective(1.0),
5834 previous_view_projection: perspective(1.0),
5835 position: [0.0; 3],
5836 };
5837 let material = [Material::default()];
5838 let sun = Sun {
5839 direction: [0.0, 0.0, 1.0],
5840 colour: [1.0; 3],
5841 intensity: 4.0,
5842 };
5843 let mut instance = Instance::new(quad, identity(), 0, 7);
5844 let mut capture = |instance: Instance| {
5845 frame
5846 .render(&calm_scene(camera, &[instance], &material, sun))
5847 .unwrap();
5848 let center = 32 * 64 + 32;
5849 let ids = read_target(&frame, &frame.targets.ids, 4);
5850 let normals = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
5851 (
5852 u32::from_le_bytes(ids[center * 4..center * 4 + 4].try_into().unwrap()),
5853 half(normals[center * 4]),
5854 )
5855 };
5856 let one_sided = capture(instance);
5857 instance.two_sided = true;
5858 let two_sided = capture(instance);
5859 assert_eq!(one_sided.0, 0);
5860 assert_eq!(two_sided.0, 7);
5861 assert!(two_sided.1 > one_sided.1 + 0.1);
5862 }
5863
5864 #[test]
5865 #[ignore = "needs a GPU; run under pgpu"]
5866 fn chrome_sphere_reflects_nearby_red_box() {
5867 use pfx_bake::reflection::{ReflectionManifest, ReflectionSpec, bake_reflection};
5868 use pfx_bake::{Anchor, BakeScene};
5869 use pfx_load::Sky;
5870 use pfx_trace::shapes::Shape as TraceShape;
5871
5872 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5873 let bake_scene = BakeScene {
5874 triangles: Vec::new(),
5875 shapes: vec![TraceShape::RoundedBox {
5876 center: [1.6, 0.0, -3.0],
5877 half: [0.4; 3],
5878 radius: 0.02,
5879 material: 0,
5880 }],
5881 materials: vec![Material {
5882 base: [1.0, 0.0, 0.0],
5883 emission: [6.0, 0.0, 0.0],
5884 ..Default::default()
5885 }],
5886 anchors: vec![Anchor {
5887 hour: 12.0,
5888 sky: Sky {
5889 width: 1,
5890 height: 1,
5891 texels: vec![[0.0, 0.0, 0.0, 1.0]],
5892 },
5893 sun: pfx_trace::Sun {
5894 direction: [0.0, 1.0, 0.0],
5895 color: [0.0; 3],
5896 intensity: 0.0,
5897 },
5898 }],
5899 };
5900 let spec = ReflectionSpec {
5901 name: Some("red-box".into()),
5902 position: [0.0, 0.0, -3.0],
5903 min: [-2.0, -2.0, -5.0],
5904 max: [2.0, 2.0, 1.0],
5905 resolution: 16,
5906 anchors: None,
5907 fade: 0.25,
5908 priority: 0.0,
5909 };
5910 let cube = bake_reflection(&gpu, &bake_scene, &spec, 4, 7, 0).unwrap();
5911 let mut frame = Frame::new(gpu, 128, 128).unwrap();
5912 let sphere_handle = sphere(&mut frame);
5913 let box_mesh = Shape::RoundBox {
5914 half: [0.4; 3],
5915 radius: 0.02,
5916 }
5917 .mesh(0.02);
5918 let box_handle = frame
5919 .upload_mesh(MeshData {
5920 positions: &box_mesh.positions,
5921 normals: &box_mesh.normals,
5922 tangents: &box_mesh.tangents,
5923 uvs: &box_mesh.uvs,
5924 uvs1: None,
5925 alpha: None,
5926 indices: &box_mesh.indices,
5927 })
5928 .unwrap();
5929 let mut sphere_model = identity();
5930 sphere_model[3][2] = -3.0;
5931 let mut box_model = identity();
5932 box_model[3] = [1.6, 0.0, -3.0, 1.0];
5933 let instances = [
5934 Instance::new(sphere_handle, sphere_model, 0, 1),
5935 Instance::new(box_handle, box_model, 1, 2),
5936 ];
5937 let materials = [
5938 Material {
5939 base: [0.95; 3],
5940 metalness: 1.0,
5941 roughness: 0.04,
5942 ..Default::default()
5943 },
5944 bake_scene.materials[0],
5945 ];
5946 let camera = Camera {
5947 view: identity(),
5948 projection: perspective(1.0),
5949 previous_view_projection: perspective(1.0),
5950 position: [0.0; 3],
5951 };
5952 let scene = calm_scene(
5953 camera,
5954 &instances,
5955 &materials,
5956 Sun {
5957 direction: [0.0, 1.0, 0.0],
5958 colour: [0.0; 3],
5959 intensity: 0.0,
5960 },
5961 );
5962 frame.render(&scene).unwrap();
5963 let without = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
5964 let artifact = ReflectionArtifact {
5965 manifest: ReflectionManifest {
5966 schema_version: 1,
5967 scene_hash: String::new(),
5968 spec,
5969 anchors: vec![12.0],
5970 sample_count: 4,
5971 seed: 7,
5972 format: String::new(),
5973 color_space: String::new(),
5974 coordinates: String::new(),
5975 files: Vec::new(),
5976 },
5977 cubes: vec![cube],
5978 };
5979 frame
5980 .set_local_reflections(Some(vec![artifact]), 12.0)
5981 .unwrap();
5982 frame.render(&scene).unwrap();
5983 let with = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
5984 let red = |pixels: &[u16]| {
5985 (45..80)
5986 .flat_map(|y| (85..95).map(move |x| (y * 128 + x) * 4))
5987 .map(|index| half(pixels[index]))
5988 .sum::<f32>()
5989 };
5990 assert!(
5991 red(&with) > red(&without) + 20.0,
5992 "without: {}, with: {}",
5993 red(&without),
5994 red(&with)
5995 );
5996 }
5997
5998 fn velocity_capture(
5999 model: Matrix,
6000 previous_model: Matrix,
6001 view: Matrix,
6002 previous_view: Matrix,
6003 ) -> (Vec<[u16; 2]>, Vec<bool>) {
6004 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6005 let mut frame = Frame::new(gpu, 128, 128).unwrap();
6006 let sphere_handle = sphere(&mut frame);
6007 let mut instance = Instance::new(sphere_handle, model, 0, 7);
6008 instance.previous_model = previous_model;
6009 let materials = [Material::default()];
6010 let camera = Camera {
6011 view,
6012 projection: perspective(1.0),
6013 previous_view_projection: multiply(perspective(1.0), previous_view),
6014 position: [0.0; 3],
6015 };
6016 let sun = Sun {
6017 direction: [0.4, 0.8, 0.7],
6018 colour: [1.0; 3],
6019 intensity: 2.0,
6020 };
6021 let instances = [instance];
6022 frame
6023 .render(&calm_scene(camera, &instances, &materials, sun))
6024 .unwrap();
6025 let ids = read_target(&frame, &frame.targets.ids, 4);
6026 let covered = ids
6027 .chunks_exact(4)
6028 .map(|pixel| u32::from_le_bytes(pixel.try_into().unwrap()) == 7)
6029 .collect();
6030 (frame.readback_velocity().unwrap(), covered)
6031 }
6032
6033 fn posed(x: f32, y: f32, z: f32) -> Matrix {
6034 let mut model = identity();
6035 model[3] = [x, y, z, 1.0];
6036 model
6037 }
6038
6039 #[test]
6040 #[ignore = "needs a GPU; run under pgpu"]
6041 fn unmoved_instance_under_static_camera_has_exact_zero_velocity() {
6042 let model = posed(0.37, -0.21, -3.3);
6043 let (velocity, covered) = velocity_capture(model, model, identity(), identity());
6044 assert!(covered.iter().filter(|&&covered| covered).count() > 500);
6045 let nonzero: Vec<_> = velocity
6046 .iter()
6047 .enumerate()
6048 .filter(|(_, pair)| **pair != [0, 0])
6049 .map(|(index, pair)| (index % 128, index / 128, covered[index], *pair))
6050 .collect();
6051 assert!(
6052 nonzero.is_empty(),
6053 "{} nonzero, covered {}, first: {:?}",
6054 nonzero.len(),
6055 nonzero.iter().filter(|entry| entry.2).count(),
6056 &nonzero[..nonzero.len().min(8)]
6057 );
6058 }
6059
6060 #[test]
6061 #[ignore = "needs a GPU; run under pgpu"]
6062 fn unmoved_instance_under_moving_camera_reads_camera_motion() {
6063 let model = posed(0.37, -0.21, -3.3);
6064 let mut previous_view = identity();
6065 previous_view[3][0] = 0.05;
6066 let (velocity, covered) = velocity_capture(model, model, identity(), previous_view);
6067 let focal = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
6068 let near = 0.05 * focal * 0.5 / (3.3 + 0.75);
6069 let far = 0.05 * focal * 0.5 / (3.3 - 0.75);
6070 let mut seen = 0;
6071 for (pair, _) in velocity
6072 .iter()
6073 .zip(&covered)
6074 .filter(|(_, covered)| **covered)
6075 {
6076 let x = half(pair[0]);
6077 assert!(
6078 x > near * 0.9 && x < far * 1.1,
6079 "x velocity {x} outside {near}..{far}"
6080 );
6081 assert!(half(pair[1]).abs() < far * 0.01);
6082 seen += 1;
6083 }
6084 assert!(seen > 500);
6085 }
6086
6087 #[test]
6088 #[ignore = "needs a GPU; run under pgpu"]
6089 fn slowly_moving_instance_keeps_its_velocity() {
6090 let (velocity, covered) = velocity_capture(
6091 posed(0.37, -0.21, -3.3),
6092 posed(0.37 - 1e-5, -0.21, -3.3),
6093 identity(),
6094 identity(),
6095 );
6096 assert!(covered.iter().filter(|&&covered| covered).count() > 500);
6097 assert!(
6098 velocity
6099 .iter()
6100 .zip(&covered)
6101 .any(|(pair, covered)| *covered && pair[0] != 0 && half(pair[0]) < 0.0)
6102 );
6103 }
6104
6105 #[test]
6106 #[ignore = "needs a GPU; run under pgpu"]
6107 fn headless_sphere_plane_and_4k_timing() {
6108 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6109 let mut frame = Frame::new(gpu, 128, 128).unwrap();
6110 let sphere_handle = sphere(&mut frame);
6111 let plane_handle = plane(&mut frame);
6112 let mut sphere_model = identity();
6113 sphere_model[3][2] = -3.0;
6114 let instances = [
6115 Instance::new(sphere_handle, sphere_model, 0, 17),
6116 Instance::new(plane_handle, identity(), 1, 23),
6117 ];
6118 let materials = [
6119 Material {
6120 base: [0.8, 0.2, 0.1],
6121 roughness: 0.6,
6122 ..Default::default()
6123 },
6124 Material {
6125 base: [0.4, 0.4, 0.4],
6126 ..Default::default()
6127 },
6128 ];
6129 let camera = Camera {
6130 view: identity(),
6131 projection: perspective(1.0),
6132 previous_view_projection: perspective(1.0),
6133 position: [0.0, 0.0, 0.0],
6134 };
6135 let sun = Sun {
6136 direction: [0.4, 0.8, 0.7],
6137 colour: [1.0; 3],
6138 intensity: 2.0,
6139 };
6140 let scene = calm_scene(camera, &instances, &materials, sun);
6141 frame.render(&scene).unwrap();
6142 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6143 let center = (64 * 128 + 64) * 4;
6144 let luminance = 0.2126 * half(hdr[center])
6145 + 0.7152 * half(hdr[center + 1])
6146 + 0.0722 * half(hdr[center + 2]);
6147 assert!(
6148 (0.02..3.0).contains(&luminance),
6149 "sphere luminance: {luminance}"
6150 );
6151 assert_eq!(half(hdr[0]), 0.0);
6152 let velocity = frame.readback_velocity().unwrap();
6153 assert_eq!(velocity[64 * 128 + 64], [0, 0]);
6154 drop(frame);
6155 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6156 let mut frame = Frame::new(gpu, 3840, 2160).unwrap();
6157 let sphere = sphere(&mut frame);
6158 let plane = plane(&mut frame);
6159 let instances = [
6160 Instance {
6161 mesh: sphere,
6162 ..instances[0]
6163 },
6164 Instance {
6165 mesh: plane,
6166 ..instances[1]
6167 },
6168 ];
6169 let camera = Camera {
6170 projection: perspective(3840.0 / 2160.0),
6171 previous_view_projection: perspective(3840.0 / 2160.0),
6172 ..camera
6173 };
6174 let scene = calm_scene(camera, &instances, &materials, sun);
6175 frame.render(&scene).unwrap();
6176 frame
6177 .gpu
6178 .device
6179 .poll(wgpu::PollType::wait_indefinitely())
6180 .unwrap();
6181 let timings = frame.collect_timings();
6182 assert!(!timings.is_empty(), "GPU timestamps unavailable");
6183 for pass in &timings[0] {
6184 println!("3840x2160 {}: {:.3} ms", pass.label, pass.milliseconds);
6185 }
6186 }
6187
6188 fn write_grid(root: &std::path::Path, irradiance: [f32; 3]) {
6189 use pfx_bake::{FileEntry, Grid, GridSpec, Manifest, Probe};
6190 use sha2::{Digest, Sha256};
6191 let digest = |bytes: &[u8]| -> String {
6192 Sha256::digest(bytes)
6193 .iter()
6194 .map(|byte| format!("{byte:02x}"))
6195 .collect()
6196 };
6197 if root.exists() {
6198 std::fs::remove_dir_all(root).unwrap();
6199 }
6200 std::fs::create_dir_all(root).unwrap();
6201 let spec = GridSpec {
6202 min: [-1.0, -0.5, -1.0],
6203 max: [0.0, 0.5, 1.0],
6204 spacing: 0.5,
6205 };
6206 let dims = spec.dimensions().unwrap();
6207 let count = dims.iter().product::<u32>() as usize;
6208 let probe = Probe {
6209 lobes: [irradiance; 6],
6210 visibility: [100.0; 6],
6211 };
6212 let grid = Grid::new(spec, vec![probe; count]).unwrap();
6213 let bytes = grid.bytes();
6214 std::fs::write(root.join("probe-000.bin"), &bytes).unwrap();
6215 let manifest = Manifest {
6216 schema_version: 1,
6217 scene_hash: "ambient-test".into(),
6218 anchors: vec![12.0],
6219 sample_count: 1,
6220 seed: 0,
6221 grid: spec,
6222 dimensions: dims,
6223 format: "ambient_cube_rgb16f_visibility_f16_le_v1".into(),
6224 color_space: "linear_rec709_irradiance".into(),
6225 coordinates: "right_handed_y_up_xyz_x_fastest".into(),
6226 files: vec![FileEntry {
6227 path: "probe-000.bin".into(),
6228 bytes: bytes.len() as u64,
6229 sha256: digest(&bytes),
6230 }],
6231 emitters: None,
6232 };
6233 let json = serde_json::to_vec(&manifest).unwrap();
6234 std::fs::write(root.join("manifest.json"), &json).unwrap();
6235 std::fs::write(root.join("manifest.sha256"), digest(&json)).unwrap();
6236 }
6237
6238 #[test]
6239 #[ignore = "needs a GPU; run under pgpu"]
6240 fn ambient_uses_the_grid_where_bound_and_the_sky_elsewhere() {
6241 let size = 128;
6242 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6243 let mut frame = Frame::new(gpu, size, size).unwrap();
6244 let floor = quad(
6245 &mut frame,
6246 [
6247 [-2.0, 0.0, -2.0],
6248 [2.0, 0.0, -2.0],
6249 [-2.0, 0.0, 2.0],
6250 [2.0, 0.0, 2.0],
6251 ],
6252 );
6253 let instances = [Instance::new(floor, identity(), 0, 1)];
6254 let base = 0.8;
6255 let materials = [Material {
6256 base: [base; 3],
6257 roughness: 1.0,
6258 specular: 0.0,
6259 ..Default::default()
6260 }];
6261 let projection = perspective(1.0);
6262 let view = look_down(3.0);
6263 let camera = Camera {
6264 view,
6265 projection,
6266 previous_view_projection: multiply(projection, view),
6267 position: [0.0, 3.0, 0.0],
6268 };
6269 let dark = Sun {
6270 direction: [0.0, 1.0, 0.0],
6271 colour: [1.0; 3],
6272 intensity: 0.0,
6273 };
6274 let read = |frame: &mut Frame| {
6275 let scene = calm_scene(camera, &instances, &materials, dark);
6276 frame.render(&scene).unwrap();
6277 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6278 let at = |x: u32, y: u32| {
6279 let i = ((y * size + x) * 4) as usize;
6280 [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6281 };
6282 (at(size / 4, size / 2), at(size * 3 / 4, size / 2))
6283 };
6284 let sky_only = read(&mut frame);
6285 let sh = frame.sky().lighting.sh;
6286 let expected_sky = sky_irradiance(&sh, [0.0, 1.0, 0.0]).map(|e| base * e / PI);
6287 for side in [sky_only.0, sky_only.1] {
6288 for channel in 0..3 {
6289 let ratio = side[channel] / expected_sky[channel];
6290 assert!(
6291 (0.85..1.2).contains(&ratio),
6292 "sky ambient {side:?} against {expected_sky:?}"
6293 );
6294 }
6295 }
6296 let grid = [0.6, 0.2, 0.1];
6297 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp/ambient-grid");
6298 write_grid(&root, grid);
6299 let probes =
6300 ProbeLighting::load(&frame.gpu.device, &frame.gpu.queue, &root, 12.0, [-1.0; 3])
6301 .unwrap();
6302 frame.set_probes(probes);
6303 let (inside, outside) = read(&mut frame);
6304 let expected_grid = grid.map(|e| base * e / PI);
6305 for channel in 0..3 {
6306 let ratio = inside[channel] / expected_grid[channel];
6307 assert!(
6308 (0.85..1.2).contains(&ratio),
6309 "grid ambient {inside:?} against {expected_grid:?}"
6310 );
6311 assert!((outside[channel] - sky_only.1[channel]).abs() < 0.01);
6312 }
6313 assert!(inside[0] > inside[2] * 3.0);
6314 assert!(outside[2] > outside[0]);
6315 println!(
6316 "grid side {inside:?}, sky side {outside:?}, expected {expected_grid:?} and {expected_sky:?}"
6317 );
6318 std::fs::remove_dir_all(root).unwrap();
6319 }
6320
6321 #[test]
6322 #[ignore = "needs a GPU; run under pgpu"]
6323 fn reflection_occlusion_dims_a_dark_grid_and_an_instance_can_keep_its_reflection() {
6324 let size = 64;
6325 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6326 let mut frame = Frame::new(gpu, size, size).unwrap();
6327 let floor = quad(
6328 &mut frame,
6329 [
6330 [-2.0, 0.0, -2.0],
6331 [2.0, 0.0, -2.0],
6332 [-2.0, 0.0, 2.0],
6333 [2.0, 0.0, 2.0],
6334 ],
6335 );
6336 let instances = [Instance::new(floor, identity(), 0, 1)];
6337 let materials = [Material {
6338 base: [0.9; 3],
6339 metalness: 1.0,
6340 roughness: 0.3,
6341 ..Default::default()
6342 }];
6343 let projection = perspective(1.0);
6344 let view = look_down(3.0);
6345 let camera = Camera {
6346 view,
6347 projection,
6348 previous_view_projection: multiply(projection, view),
6349 position: [0.0, 3.0, 0.0],
6350 };
6351 let dark = Sun {
6352 direction: [0.0, 1.0, 0.0],
6353 colour: [1.0; 3],
6354 intensity: 0.0,
6355 };
6356 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
6357 .join("../../tmp/reflection-occlusion-grid");
6358 write_grid(&root, [0.01; 3]);
6359 let probes =
6360 ProbeLighting::load(&frame.gpu.device, &frame.gpu.queue, &root, 12.0, [-1.0; 3])
6361 .unwrap();
6362 frame.set_probes(probes);
6363 frame.set_sky_visibility(false);
6364 let read = |frame: &mut Frame, scale: f32| {
6365 frame
6366 .set_surfaces(&[InstanceSurface {
6367 reflection_occlusion: scale,
6368 ..Default::default()
6369 }])
6370 .unwrap();
6371 let scene = calm_scene(camera, &instances, &materials, dark);
6372 frame.render(&scene).unwrap();
6373 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6374 let at = |x: u32, y: u32| {
6375 let i = ((y * size + x) * 4) as usize;
6376 [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6377 };
6378 (at(size / 4, size / 2), at(size * 3 / 4, size / 2))
6379 };
6380 let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
6381 let open = read(&mut frame, 1.0);
6382 for scale in [0.0, 0.5] {
6383 assert_eq!(
6384 read(&mut frame, scale),
6385 open,
6386 "occlusion off, scale {scale}"
6387 );
6388 }
6389 frame.set_reflection_occlusion(1.0).unwrap();
6390 assert_eq!(frame.probes().reflection_occlusion(), 1.0);
6391 let full = read(&mut frame, 1.0);
6392 let half_kept = read(&mut frame, 0.5);
6393 let kept = read(&mut frame, 0.0);
6394 println!(
6395 "under the dark grid: open {:.4}, scale 1 {:.4}, scale 0.5 {:.4}, scale 0 {:.4}",
6396 luma(open.0),
6397 luma(full.0),
6398 luma(half_kept.0),
6399 luma(kept.0)
6400 );
6401 assert!(luma(full.0) < luma(open.0) * 0.5);
6402 assert!(luma(full.0) < luma(half_kept.0) && luma(half_kept.0) < luma(open.0));
6403 assert_eq!(kept, open);
6404 assert_eq!(full.1, open.1);
6405 assert!(frame.set_reflection_occlusion(1.5).is_err());
6406 assert!(
6407 frame
6408 .set_surfaces(&[InstanceSurface {
6409 reflection_occlusion: -0.5,
6410 ..Default::default()
6411 }])
6412 .is_err()
6413 );
6414 std::fs::remove_dir_all(root).unwrap();
6415 }
6416
6417 #[test]
6418 fn an_authored_ambient_is_flat_and_the_default_is_the_sky() {
6419 let mut sh = [[0.0; 3]; 9];
6420 sh[0] = [1.0, 2.0, 3.0];
6421 sh[1] = [0.5, 0.5, 0.5];
6422 assert_eq!(Ambient::default(), Ambient::Sky);
6423 assert_eq!(Ambient::Sky.sh(sh), sh);
6424 let constant = [0.31 * PI, 0.305 * PI, 0.43 * PI];
6425 let flat = Ambient::Constant(constant).sh(sh);
6426 for normal in [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.6, 0.0, 0.8]] {
6427 let got = sky_irradiance(&flat, normal);
6428 for channel in 0..3 {
6429 assert!((got[channel] - constant[channel]).abs() < 1e-5);
6430 }
6431 }
6432 let own = [[0.25; 3]; 9];
6433 assert_eq!(Ambient::Sh(own).sh(sh), own);
6434 assert!(Ambient::Constant(constant).valid());
6435 assert!(!Ambient::Constant([-0.1, 0.0, 0.0]).valid());
6436 assert!(!Ambient::Constant([f32::NAN, 0.0, 0.0]).valid());
6437 assert!(!Ambient::Sh([[f32::INFINITY; 3]; 9]).valid());
6438 }
6439
6440 #[test]
6441 #[ignore = "needs a GPU; run under pgpu"]
6442 fn an_authored_ambient_survives_set_probes_and_the_default_is_unchanged() {
6443 let size = 128;
6444 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6445 let mut frame = Frame::new(gpu, size, size).unwrap();
6446 let floor = quad(
6447 &mut frame,
6448 [
6449 [-2.0, 0.0, -2.0],
6450 [2.0, 0.0, -2.0],
6451 [-2.0, 0.0, 2.0],
6452 [2.0, 0.0, 2.0],
6453 ],
6454 );
6455 let instances = [Instance::new(floor, identity(), 0, 1)];
6456 let base = 0.8;
6457 let materials = [Material {
6458 base: [base; 3],
6459 roughness: 1.0,
6460 specular: 0.0,
6461 ..Default::default()
6462 }];
6463 let projection = perspective(1.0);
6464 let view = look_down(3.0);
6465 let camera = Camera {
6466 view,
6467 projection,
6468 previous_view_projection: multiply(projection, view),
6469 position: [0.0, 3.0, 0.0],
6470 };
6471 let dark = Sun {
6472 direction: [0.0, 1.0, 0.0],
6473 colour: [1.0; 3],
6474 intensity: 0.0,
6475 };
6476 let read = |frame: &mut Frame| {
6477 let scene = calm_scene(camera, &instances, &materials, dark);
6478 frame.render(&scene).unwrap();
6479 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6480 let i = (((size / 2) * size + size / 2) * 4) as usize;
6481 [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6482 };
6483 let sky_first = read(&mut frame);
6484 let fresh = ProbeLighting::fallback(&frame.gpu.device, &frame.gpu.queue, [-1.0; 3]);
6485 frame.set_probes(fresh);
6486 let sky_after = read(&mut frame);
6487 for channel in 0..3 {
6488 assert_eq!(sky_first[channel], sky_after[channel]);
6489 }
6490 let authored = [0.31 * PI, 0.305 * PI, 0.43 * PI];
6491 frame.set_ambient(Ambient::Constant(authored)).unwrap();
6492 let fresh = ProbeLighting::fallback(&frame.gpu.device, &frame.gpu.queue, [-1.0; 3]);
6493 frame.set_probes(fresh);
6494 assert_eq!(frame.ambient(), Ambient::Constant(authored));
6495 let unflagged = read(&mut frame);
6496 for channel in 0..3 {
6497 assert_eq!(sky_first[channel], unflagged[channel]);
6498 }
6499 frame
6500 .set_surfaces(&[InstanceSurface {
6501 authored_ambient: true,
6502 ..Default::default()
6503 }])
6504 .unwrap();
6505 let flat = read(&mut frame);
6506 for channel in 0..3 {
6507 let expected = base * authored[channel] / PI;
6508 assert!(
6509 (flat[channel] / expected - 1.0).abs() < 0.03,
6510 "authored ambient {flat:?} against {authored:?}"
6511 );
6512 }
6513 assert!(flat[2] > flat[0]);
6514 assert!(
6515 frame
6516 .set_ambient(Ambient::Constant([-1.0, 0.0, 0.0]))
6517 .is_err()
6518 );
6519 assert_eq!(frame.ambient(), Ambient::Constant(authored));
6520 frame.set_ambient(Ambient::Sky).unwrap();
6521 let back = read(&mut frame);
6522 for channel in 0..3 {
6523 assert_eq!(sky_first[channel], back[channel]);
6524 }
6525 println!("sky {sky_first:?}, authored {flat:?}");
6526 }
6527
6528 #[test]
6529 #[ignore = "needs a GPU; run under pgpu"]
6530 fn contact_darkens_flagged_receivers_by_mix_of_tint_and_one_over_the_field_squared() {
6531 let size = 128;
6532 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6533 let mut frame = Frame::new(gpu, size, size).unwrap();
6534 let floor = quad(
6535 &mut frame,
6536 [
6537 [-2.0, 0.0, -2.0],
6538 [2.0, 0.0, -2.0],
6539 [-2.0, 0.0, 2.0],
6540 [2.0, 0.0, 2.0],
6541 ],
6542 );
6543 let instances = [Instance::new(floor, identity(), 0, 1)];
6544 let materials = [Material {
6545 base: [0.8; 3],
6546 roughness: 1.0,
6547 specular: 0.0,
6548 ..Default::default()
6549 }];
6550 let projection = perspective(1.0);
6551 let view = look_down(3.0);
6552 let camera = Camera {
6553 view,
6554 projection,
6555 previous_view_projection: multiply(projection, view),
6556 position: [0.0, 3.0, 0.0],
6557 };
6558 let dark = Sun {
6559 direction: [0.0, 1.0, 0.0],
6560 colour: [1.0; 3],
6561 intensity: 0.0,
6562 };
6563 let read = |frame: &mut Frame| {
6564 let scene = calm_scene(camera, &instances, &materials, dark);
6565 frame.render(&scene).unwrap();
6566 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6567 let at = |x: u32, y: u32| {
6568 let i = ((y * size + x) * 4) as usize;
6569 [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6570 };
6571 (at(size / 4, size / 2), at(size * 3 / 4, size / 2))
6572 };
6573 let open = read(&mut frame);
6574 let tint = [0.34, 0.34, 0.50];
6575 frame
6576 .set_contact(Some(ContactDesc {
6577 size: [64, 2],
6578 min: [-2.0, -2.0],
6579 max: [2.0, 2.0],
6580 tint,
6581 }))
6582 .unwrap();
6583 let field: Vec<f32> = (0..128)
6584 .map(|i| if i % 64 < 32 { 0.5 } else { 1.0 })
6585 .collect();
6586 frame.write_contact(&field).unwrap();
6587 let unflagged = read(&mut frame);
6588 for channel in 0..3 {
6589 assert_eq!(unflagged.0[channel], open.0[channel]);
6590 assert_eq!(unflagged.1[channel], open.1[channel]);
6591 }
6592 frame
6593 .set_surfaces(&[InstanceSurface {
6594 contact: true,
6595 ..Default::default()
6596 }])
6597 .unwrap();
6598 let (dim, clear) = read(&mut frame);
6599 for channel in 0..3 {
6600 let wanted = tint[channel] + (1.0 - tint[channel]) * 0.25;
6601 let got = dim[channel] / open.0[channel];
6602 assert!(
6603 (got / wanted - 1.0).abs() < 0.02,
6604 "contact {got} against {wanted} in channel {channel}"
6605 );
6606 assert!((clear[channel] / open.1[channel] - 1.0).abs() < 0.01);
6607 }
6608 println!("open {open:?}, flagged {dim:?} and {clear:?}");
6609 frame.set_contact(None).unwrap();
6610 let removed = read(&mut frame);
6611 for channel in 0..3 {
6612 assert_eq!(removed.0[channel], open.0[channel]);
6613 }
6614 assert!(frame.write_contact(&field).is_err());
6615 }
6616
6617 fn read_atlas(frame: &Frame, shadows: &Shadows) -> Vec<f32> {
6618 let resolution = shadows.resolution();
6619 let row = resolution * 4;
6620 let buffer = frame.gpu.device.create_buffer(&wgpu::BufferDescriptor {
6621 label: Some("atlas readback"),
6622 size: u64::from(row * resolution) * CASCADE_COUNT as u64,
6623 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6624 mapped_at_creation: false,
6625 });
6626 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6627 encoder.copy_texture_to_buffer(
6628 wgpu::TexelCopyTextureInfo {
6629 texture: shadows.atlas(),
6630 mip_level: 0,
6631 origin: wgpu::Origin3d::ZERO,
6632 aspect: wgpu::TextureAspect::DepthOnly,
6633 },
6634 wgpu::TexelCopyBufferInfo {
6635 buffer: &buffer,
6636 layout: wgpu::TexelCopyBufferLayout {
6637 offset: 0,
6638 bytes_per_row: Some(row),
6639 rows_per_image: Some(resolution),
6640 },
6641 },
6642 wgpu::Extent3d {
6643 width: resolution,
6644 height: resolution,
6645 depth_or_array_layers: CASCADE_COUNT as u32,
6646 },
6647 );
6648 frame.gpu.queue.submit(Some(encoder.finish()));
6649 buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
6650 frame
6651 .gpu
6652 .device
6653 .poll(wgpu::PollType::wait_indefinitely())
6654 .unwrap();
6655 let values = bytemuck::cast_slice::<u8, f32>(
6656 &buffer.slice(..).get_mapped_range().expect("a mapped range"),
6657 )
6658 .to_vec();
6659 buffer.unmap();
6660 values
6661 }
6662
6663 #[test]
6664 #[ignore = "needs a GPU; run under pgpu"]
6665 fn deformation_reaches_the_shadow_pass() {
6666 use crate::shadow::{Quality, View};
6667 use pfx_geom::strip::{Roller, Strip};
6668 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6669 let mut frame = Frame::new(gpu, 256, 256).unwrap();
6670 let strip = Strip {
6671 length: 3.0,
6672 width: 1.2,
6673 };
6674 let mesh = strip.mesh(0.12, 0.002, None, 4);
6675 let handle = frame
6676 .upload_mesh(MeshData {
6677 positions: &mesh.positions,
6678 normals: &mesh.normals,
6679 tangents: &mesh.tangents,
6680 uvs: &mesh.uvs,
6681 uvs1: None,
6682 alpha: None,
6683 indices: &mesh.indices,
6684 })
6685 .unwrap();
6686 let mut shadows = Shadows::new(
6687 &frame.gpu.device,
6688 Quality {
6689 resolution: 512,
6690 ..Default::default()
6691 },
6692 );
6693 let view = View {
6694 eye: [1.5, 4.0, 4.0],
6695 forward: [0.0, -0.7, -0.7],
6696 up: [0.0, 1.0, 0.0],
6697 fov_y: 0.9,
6698 aspect: 1.0,
6699 near: 0.1,
6700 far: 12.0,
6701 };
6702 let fit = shadows.fit(&view, [0.3, 0.9, 0.2]);
6703 let mut instance = Instance::new(handle, identity(), 0, 1);
6704 instance.deformer = DeformerId(0);
6705 let instances = [instance];
6706 let materials = [Material::default()];
6707 let camera = Camera {
6708 view: look_down(4.0),
6709 projection: perspective(1.0),
6710 previous_view_projection: perspective(1.0),
6711 position: [0.0, 4.0, 0.0],
6712 };
6713 let mut render = |at: f32| {
6714 let roller = Roller {
6715 at,
6716 core: 0.12,
6717 thickness: 0.01,
6718 };
6719 let state = RollerFrame {
6720 at,
6721 outer: roller.outer(&strip),
6722 thickness: roller.thickness,
6723 };
6724 let deformers = [Deformer::Roller {
6725 current: state,
6726 previous: state,
6727 }];
6728 let scene = Scene {
6729 deformers: &deformers,
6730 ..calm_scene(
6731 camera,
6732 &instances,
6733 &materials,
6734 Sun {
6735 direction: [0.3, 0.9, 0.2],
6736 colour: [1.0; 3],
6737 intensity: 1.0,
6738 },
6739 )
6740 };
6741 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6742 frame
6743 .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
6744 .unwrap();
6745 frame.gpu.queue.submit(Some(encoder.finish()));
6746 read_atlas(&frame, &shadows)
6747 };
6748 let flat = render(3.0);
6749 let rolled = render(1.2);
6750 let flat_again = render(3.0);
6751 assert_eq!(flat, flat_again);
6752 let covered = |depths: &[f32]| depths.iter().filter(|depth| **depth < 1.0).count();
6753 let changed = flat
6754 .iter()
6755 .zip(&rolled)
6756 .filter(|(a, b)| (**a - **b).abs() > 1e-4)
6757 .count();
6758 println!(
6759 "flat casters {} texels, rolled casters {} texels, {changed} texels changed",
6760 covered(&flat),
6761 covered(&rolled)
6762 );
6763 assert!(covered(&flat) > 1000);
6764 assert!(changed > 500);
6765 }
6766 fn read_target(frame: &Frame, texture: &wgpu::Texture, texel: u32) -> Vec<u8> {
6767 let size = texture.size();
6768 let row = (size.width * texel).div_ceil(256) * 256;
6769 let buffer = frame.gpu.device.create_buffer(&wgpu::BufferDescriptor {
6770 label: Some("test readback"),
6771 size: u64::from(row * size.height),
6772 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6773 mapped_at_creation: false,
6774 });
6775 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6776 encoder.copy_texture_to_buffer(
6777 wgpu::TexelCopyTextureInfo {
6778 texture,
6779 mip_level: 0,
6780 origin: wgpu::Origin3d::ZERO,
6781 aspect: wgpu::TextureAspect::All,
6782 },
6783 wgpu::TexelCopyBufferInfo {
6784 buffer: &buffer,
6785 layout: wgpu::TexelCopyBufferLayout {
6786 offset: 0,
6787 bytes_per_row: Some(row),
6788 rows_per_image: Some(size.height),
6789 },
6790 },
6791 wgpu::Extent3d {
6792 width: size.width,
6793 height: size.height,
6794 depth_or_array_layers: 1,
6795 },
6796 );
6797 frame.gpu.queue.submit(Some(encoder.finish()));
6798 buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
6799 frame
6800 .gpu
6801 .device
6802 .poll(wgpu::PollType::wait_indefinitely())
6803 .unwrap();
6804 let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
6805 let mut out = Vec::new();
6806 for y in 0..size.height {
6807 let start = (y * row) as usize;
6808 out.extend_from_slice(&mapped[start..start + (size.width * texel) as usize]);
6809 }
6810 drop(mapped);
6811 buffer.unmap();
6812 out
6813 }
6814
6815 struct Outputs {
6816 ids: Vec<u32>,
6817 depth: Vec<f32>,
6818 colour: Vec<[f32; 3]>,
6819 atlas: Vec<f32>,
6820 }
6821
6822 #[test]
6823 #[ignore = "needs a GPU; run under pgpu"]
6824 fn clip_planes_cut_prepass_opaque_shadow_and_ids_alike() {
6825 use crate::shadow::{Quality, View};
6826 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6827 let size = 256;
6828 let mut frame = Frame::new(gpu, size, size).unwrap();
6829 let floor = quad(
6830 &mut frame,
6831 [
6832 [-4.0, 0.0, -4.0],
6833 [4.0, 0.0, -4.0],
6834 [-4.0, 0.0, 4.0],
6835 [4.0, 0.0, 4.0],
6836 ],
6837 );
6838 let card = |x0: f32, x1: f32| {
6839 [
6840 [x0, 1.0, -1.0],
6841 [x1, 1.0, -1.0],
6842 [x0, 1.0, 1.0],
6843 [x1, 1.0, 1.0],
6844 ]
6845 };
6846 let whole = quad(&mut frame, card(-1.0, 1.0));
6847 let half_card = quad(&mut frame, card(-1.0, 0.0));
6848 let mut shadows = Shadows::new(
6849 &frame.gpu.device,
6850 Quality {
6851 resolution: 1024,
6852 ..Default::default()
6853 },
6854 );
6855 let sun_toward = {
6856 let v: [f32; 3] = [0.6, 0.8, 0.0];
6857 let length = v.iter().map(|x| x * x).sum::<f32>().sqrt();
6858 v.map(|x| x / length)
6859 };
6860 let view = View {
6861 eye: [0.0, 4.0, 0.0],
6862 forward: [0.0, -1.0, 0.0],
6863 up: [0.0, 0.0, -1.0],
6864 fov_y: 50.0_f32.to_radians(),
6865 aspect: 1.0,
6866 near: 0.1,
6867 far: 100.0,
6868 };
6869 let fit = shadows.fit(&view, sun_toward);
6870 let materials = [
6871 Material {
6872 base: [0.6, 0.6, 0.6],
6873 roughness: 0.8,
6874 ..Default::default()
6875 },
6876 Material {
6877 base: [0.8, 0.1, 0.1],
6878 roughness: 0.8,
6879 ..Default::default()
6880 },
6881 ];
6882 let camera = Camera {
6883 view: look_down(4.0),
6884 projection: perspective(1.0),
6885 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
6886 position: [0.0, 4.0, 0.0],
6887 };
6888 let sun = Sun {
6889 direction: sun_toward,
6890 colour: [1.0; 3],
6891 intensity: 3.0,
6892 };
6893 let mut render = |card: MeshHandle, surfaces: &[InstanceSurface]| {
6894 let instances = [
6895 Instance::new(floor, identity(), 0, 1),
6896 Instance::new(card, identity(), 1, 2),
6897 ];
6898 frame.set_surfaces(surfaces).unwrap();
6899 let scene = calm_scene(camera, &instances, &materials, sun);
6900 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6901 frame
6902 .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
6903 .unwrap();
6904 frame.gpu.queue.submit(Some(encoder.finish()));
6905 let ids = read_target(&frame, &frame.targets.ids, 4)
6906 .chunks_exact(4)
6907 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
6908 .collect();
6909 let depth = read_target(&frame, &frame.targets.depth, 4)
6910 .chunks_exact(4)
6911 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
6912 .collect();
6913 let colour = frame
6914 .gpu
6915 .readback_rgba16(&frame.targets.hdr)
6916 .unwrap()
6917 .chunks_exact(4)
6918 .map(|p| [half(p[0]), half(p[1]), half(p[2])])
6919 .collect();
6920 Outputs {
6921 ids,
6922 depth,
6923 colour,
6924 atlas: read_atlas(&frame, &shadows),
6925 }
6926 };
6927 let cut = InstanceSurface {
6928 clip: [[1.0, 0.0, 0.0, 0.0], [0.0; 4]],
6929 ..Default::default()
6930 };
6931 let clipped = render(whole, &[InstanceSurface::default(), cut]);
6932 let reference = render(half_card, &[]);
6933 let uncut = render(whole, &[]);
6934 let differ = |a: &[f32], b: &[f32], tolerance: f32| {
6935 a.iter()
6936 .zip(b)
6937 .filter(|(x, y)| (**x - **y).abs() > tolerance)
6938 .count()
6939 };
6940 let id_mismatch = clipped
6941 .ids
6942 .iter()
6943 .zip(&reference.ids)
6944 .filter(|(a, b)| a != b)
6945 .count();
6946 let depth_mismatch = differ(&clipped.depth, &reference.depth, 1e-6);
6947 let colour_mismatch = clipped
6948 .colour
6949 .iter()
6950 .zip(&reference.colour)
6951 .filter(|(a, b)| (0..3).any(|c| (a[c] - b[c]).abs() > 0.02))
6952 .count();
6953 let atlas_mismatch = differ(&clipped.atlas, &reference.atlas, 1e-4);
6954 let atlas_cut = differ(&uncut.atlas, &reference.atlas, 1e-4);
6955 let id_cut = uncut
6956 .ids
6957 .iter()
6958 .zip(&reference.ids)
6959 .filter(|(a, b)| a != b)
6960 .count();
6961 println!(
6962 "clip against a half card: ids {id_mismatch}, depth {depth_mismatch}, colour {colour_mismatch} of {} pixels, shadow {atlas_mismatch} of {} texels; the uncut card differs in {id_cut} ids and {atlas_cut} shadow texels",
6963 clipped.ids.len(),
6964 clipped.atlas.len()
6965 );
6966 assert!(id_cut > 2000 && atlas_cut > 1000);
6967 let seam = size as usize * 2;
6968 assert!(id_mismatch <= seam, "{id_mismatch} ids differ");
6969 assert!(depth_mismatch <= seam, "{depth_mismatch} depths differ");
6970 assert!(
6971 colour_mismatch <= seam * 2,
6972 "{colour_mismatch} colours differ"
6973 );
6974 assert!(
6975 atlas_mismatch * 20 <= atlas_cut,
6976 "{atlas_mismatch} shadow texels differ"
6977 );
6978 let at = |x: f32, z: f32| {
6979 let ndc = transform(multiply(perspective(1.0), look_down(4.0)), [x, 0.0, z, 1.0]);
6980 let px = ((ndc[0] / ndc[3] * 0.5 + 0.5) * size as f32) as usize;
6981 let py = ((0.5 - ndc[1] / ndc[3] * 0.5) * size as f32) as usize;
6982 py * size as usize + px
6983 };
6984 let shown = at(0.6, 0.0);
6985 assert_eq!(clipped.ids[shown], 1);
6986 assert_eq!(uncut.ids[shown], 2);
6987 assert_eq!(clipped.ids[at(-0.6, 0.0)], 2);
6988 let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
6989 let open_floor = luma(clipped.colour[at(1.6, 0.0)]);
6990 let under_cut = luma(clipped.colour[at(0.15, 0.0)]);
6991 let under_card = luma(clipped.colour[at(-1.5, 0.0)]);
6992 println!(
6993 "floor luma: open {open_floor}, under the cut half {under_cut}, in the card's shadow {under_card}"
6994 );
6995 assert!((under_cut - open_floor).abs() < 0.1 * open_floor);
6996 assert!(under_card < 0.6 * open_floor);
6997 }
6998
6999 const RING: u32 = 64;
7000
7001 fn ring_alpha(i: u32, j: u32) -> u8 {
7002 let u = (i as f32 + 0.5) / RING as f32 - 0.5;
7003 let v = (j as f32 + 0.5) / RING as f32 - 0.5;
7004 let r = (u * u + v * v).sqrt();
7005 if (0.25..=0.45).contains(&r) { 255 } else { 0 }
7006 }
7007
7008 fn traced_alpha(uv: [f32; 2]) -> f32 {
7009 let size = RING as f32;
7010 let p = [0, 1].map(|k| (uv[k] * size - 0.5).clamp(0.0, size - 1.0));
7011 let lo = p.map(|value| value.floor() as u32);
7012 let hi = lo.map(|value| (value + 1).min(RING - 1));
7013 let f = [p[0] - p[0].floor(), p[1] - p[1].floor()];
7014 let a = |i: u32, j: u32| f32::from(ring_alpha(i, j)) / 255.0;
7015 let top = a(lo[0], lo[1]) * (1.0 - f[0]) + a(hi[0], lo[1]) * f[0];
7016 let bottom = a(lo[0], hi[1]) * (1.0 - f[0]) + a(hi[0], hi[1]) * f[0];
7017 top * (1.0 - f[1]) + bottom * f[1]
7018 }
7019
7020 #[test]
7021 #[ignore = "needs a GPU; run under pgpu"]
7022 fn content_alpha_cuts_a_ring_out_of_one_quad_in_every_pass() {
7023 use crate::maps::ContentFormat;
7024 use crate::shadow::{Quality, View};
7025 use pfx_materials::ContentLayer;
7026 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7027 let size = 256;
7028 let height = 6.0;
7029 let half_card = 0.6;
7030 let mut frame = Frame::new(gpu, size, size).unwrap();
7031 let floor = quad(
7032 &mut frame,
7033 [
7034 [-4.0, 0.0, -4.0],
7035 [4.0, 0.0, -4.0],
7036 [-4.0, 0.0, 4.0],
7037 [4.0, 0.0, 4.0],
7038 ],
7039 );
7040 let card = quad(
7041 &mut frame,
7042 [
7043 [-half_card, 1.0, -half_card],
7044 [half_card, 1.0, -half_card],
7045 [-half_card, 1.0, half_card],
7046 [half_card, 1.0, half_card],
7047 ],
7048 );
7049 let texels: Vec<u8> = (0..RING * RING)
7050 .flat_map(|index| [230, 40, 30, ring_alpha(index % RING, index / RING)])
7051 .collect();
7052 frame
7053 .set_content(0, RING, RING, ContentFormat::Srgb8)
7054 .unwrap();
7055 frame.write_content(0, [0, 0, RING, RING], &texels).unwrap();
7056 let mut shadows = Shadows::new(
7057 &frame.gpu.device,
7058 Quality {
7059 resolution: 1024,
7060 ..Default::default()
7061 },
7062 );
7063 let sun_toward = [0.8, 0.6, 0.0];
7064 let view = View {
7065 eye: [0.0, height, 0.0],
7066 forward: [0.0, -1.0, 0.0],
7067 up: [0.0, 0.0, -1.0],
7068 fov_y: 50.0_f32.to_radians(),
7069 aspect: 1.0,
7070 near: 0.1,
7071 far: 100.0,
7072 };
7073 let fit = shadows.fit(&view, sun_toward);
7074 let materials = [
7075 Material {
7076 base: [0.6, 0.6, 0.6],
7077 roughness: 0.8,
7078 ..Default::default()
7079 },
7080 Material {
7081 base: [0.2, 0.2, 0.8],
7082 roughness: 0.8,
7083 content_layer: ContentLayer {
7084 slot: 0,
7085 ..Default::default()
7086 },
7087 ..Default::default()
7088 },
7089 ];
7090 let camera = Camera {
7091 view: look_down(height),
7092 projection: perspective(1.0),
7093 previous_view_projection: multiply(perspective(1.0), look_down(height)),
7094 position: [0.0, height, 0.0],
7095 };
7096 let sun = Sun {
7097 direction: sun_toward,
7098 colour: [1.0; 3],
7099 intensity: 3.0,
7100 };
7101 let mut render = |cutout: bool| {
7102 let instances = [
7103 Instance::new(floor, identity(), 0, 1),
7104 Instance::new(card, identity(), 1, 2),
7105 ];
7106 let surfaces = [
7107 InstanceSurface::default(),
7108 InstanceSurface {
7109 cutout,
7110 ..Default::default()
7111 },
7112 ];
7113 frame.set_surfaces(&surfaces).unwrap();
7114 let scene = calm_scene(camera, &instances, &materials, sun);
7115 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
7116 frame
7117 .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
7118 .unwrap();
7119 frame.gpu.queue.submit(Some(encoder.finish()));
7120 let ids: Vec<u32> = read_target(&frame, &frame.targets.ids, 4)
7121 .chunks_exact(4)
7122 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
7123 .collect();
7124 let colour: Vec<[f32; 3]> = frame
7125 .gpu
7126 .readback_rgba16(&frame.targets.hdr)
7127 .unwrap()
7128 .chunks_exact(4)
7129 .map(|p| [half(p[0]), half(p[1]), half(p[2])])
7130 .collect();
7131 (ids, colour, frame.opaque_drawn().to_vec())
7132 };
7133 let (ids, colour, drawn) = render(true);
7134 let (whole_ids, whole_colour, whole_drawn) = render(false);
7135 assert!(drawn.iter().any(|used| used.content_cutout));
7136 assert!(whole_drawn.iter().all(|used| !used.content_cutout));
7137 let at = |x: f32, y: f32, z: f32| {
7138 let ndc = transform(
7139 multiply(perspective(1.0), look_down(height)),
7140 [x, y, z, 1.0],
7141 );
7142 let px = ((ndc[0] / ndc[3] * 0.5 + 0.5) * size as f32) as usize;
7143 let py = ((0.5 - ndc[1] / ndc[3] * 0.5) * size as f32) as usize;
7144 py * size as usize + px
7145 };
7146 assert_eq!(ids[at(0.0, 1.0, 0.0)], 1, "the hole shows the floor");
7147 assert_eq!(ids[at(0.42, 1.0, 0.0)], 2, "the ring shows");
7148 assert_eq!(
7149 ids[at(0.55, 1.0, 0.55)],
7150 1,
7151 "the clear corner shows the floor"
7152 );
7153 assert_eq!(whole_ids[at(0.0, 1.0, 0.0)], 2);
7154 let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
7155 let mut checked = 0;
7156 let mut wrong = 0;
7157 for py in 0..size {
7158 for px in 0..size {
7159 let ndc = [
7160 (px as f32 + 0.5) / size as f32 * 2.0 - 1.0,
7161 1.0 - (py as f32 + 0.5) / size as f32 * 2.0,
7162 ];
7163 let x = ndc[0] * (height - 1.0) / f;
7164 let z = -ndc[1] * (height - 1.0) / f;
7165 if x.abs() > half_card || z.abs() > half_card {
7166 continue;
7167 }
7168 let uv = [
7169 (x + half_card) / (2.0 * half_card),
7170 (z + half_card) / (2.0 * half_card),
7171 ];
7172 let alpha = traced_alpha(uv);
7173 if (alpha - 0.5).abs() < 0.05 {
7174 continue;
7175 }
7176 checked += 1;
7177 let shown = ids[(py * size + px) as usize] == 2;
7178 if shown != (alpha >= 0.5) {
7179 wrong += 1;
7180 }
7181 }
7182 }
7183 println!("{wrong} of {checked} card pixels disagree with the tracer's cutout");
7184 assert!(checked > 3000, "{checked}");
7185 assert_eq!(wrong, 0);
7186 let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
7187 let shift = sun_toward[0] / sun_toward[1];
7188 let shadow_of = |x: f32, z: f32| at(x - shift, 0.0, z);
7189 let open = luma(colour[at(1.5, 0.0, -1.5)]);
7190 let through_hole = luma(colour[shadow_of(0.0, 0.0)]);
7191 let under_ring = luma(colour[shadow_of(-0.42, 0.0)]);
7192 let past_corner = luma(colour[shadow_of(-0.55, 0.55)]);
7193 let whole_hole = luma(whole_colour[shadow_of(0.0, 0.0)]);
7194 println!(
7195 "floor luma: open {open}, through the hole {through_hole}, under the ring {under_ring}, past the clear corner {past_corner}; the uncut card shades the hole's spot to {whole_hole}"
7196 );
7197 assert!((through_hole - open).abs() < 0.05 * open);
7198 assert!((past_corner - open).abs() < 0.05 * open);
7199 assert!(under_ring < 0.6 * open);
7200 assert!(whole_hole < 0.6 * open);
7201 }
7202
7203 #[test]
7204 #[ignore = "needs a GPU; run under pgpu"]
7205 fn content_updates_keep_the_arrays_and_regenerate_mips() {
7206 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7207 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7208 let paper = pfx_load::Image {
7209 width: 4,
7210 height: 4,
7211 space: pfx_load::ColorSpace::Srgb,
7212 pixels: pfx_load::Pixels::Eight([200, 190, 170, 255].repeat(16)),
7213 };
7214 frame
7215 .set_maps(&MapImages {
7216 base: std::slice::from_ref(&paper),
7217 ..Default::default()
7218 })
7219 .unwrap();
7220 let arrays = [
7221 frame.maps.base.texture.clone(),
7222 frame.maps.normal.texture.clone(),
7223 frame.maps.roughness.texture.clone(),
7224 frame.maps.metal.texture.clone(),
7225 ];
7226 frame
7227 .set_content(3, 1280, 720, ContentFormat::Srgb8)
7228 .unwrap();
7229 let video = frame.content().get(3).unwrap().texture.clone();
7230 assert_eq!(frame.content().get(3).unwrap().mips, 11);
7231 let started = std::time::Instant::now();
7232 for (index, colour) in [[255u8, 0, 0, 255], [0, 0, 255, 255], [0, 255, 0, 255]]
7233 .iter()
7234 .enumerate()
7235 {
7236 frame
7237 .write_content(3, [0, 0, 1280, 720], &colour.repeat(1280 * 720))
7238 .unwrap();
7239 frame
7240 .set_content(3, 1280, 720, ContentFormat::Srgb8)
7241 .unwrap();
7242 let content = frame.content();
7243 let slot = content.get(3).unwrap();
7244 assert_eq!(slot.texture, video);
7245 assert_eq!(slot.writes, index as u64 + 1);
7246 let last = slot.mips - 1;
7247 let buffer = frame.gpu.device.create_buffer(&wgpu::BufferDescriptor {
7248 label: Some("smallest mip"),
7249 size: 256,
7250 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
7251 mapped_at_creation: false,
7252 });
7253 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
7254 encoder.copy_texture_to_buffer(
7255 wgpu::TexelCopyTextureInfo {
7256 texture: &slot.texture,
7257 mip_level: last,
7258 origin: wgpu::Origin3d::ZERO,
7259 aspect: wgpu::TextureAspect::All,
7260 },
7261 wgpu::TexelCopyBufferInfo {
7262 buffer: &buffer,
7263 layout: wgpu::TexelCopyBufferLayout {
7264 offset: 0,
7265 bytes_per_row: Some(256),
7266 rows_per_image: Some(1),
7267 },
7268 },
7269 wgpu::Extent3d {
7270 width: 1,
7271 height: 1,
7272 depth_or_array_layers: 1,
7273 },
7274 );
7275 frame.gpu.queue.submit(Some(encoder.finish()));
7276 buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
7277 frame
7278 .gpu
7279 .device
7280 .poll(wgpu::PollType::wait_indefinitely())
7281 .unwrap();
7282 let texel = buffer.slice(..).get_mapped_range().expect("a mapped range")[..4].to_vec();
7283 assert_eq!(texel, colour.to_vec(), "frame {index}: the smallest mip");
7284 }
7285 println!(
7286 "three 1280x720 frames written with mips in {:.2} ms",
7287 started.elapsed().as_secs_f64() * 1000.0
7288 );
7289 assert_eq!(frame.maps.base.texture, arrays[0]);
7290 assert_eq!(frame.maps.normal.texture, arrays[1]);
7291 assert_eq!(frame.maps.roughness.texture, arrays[2]);
7292 assert_eq!(frame.maps.metal.texture, arrays[3]);
7293 assert!(frame.write_content(2, [0, 0, 1, 1], &[0; 4]).is_err());
7294 assert!(frame.write_content(3, [1279, 0, 2, 1], &[0; 8]).is_err());
7295 assert!(frame.set_content(4, 4, 4, ContentFormat::Linear16).is_err());
7296 frame
7297 .set_content(3, 640, 360, ContentFormat::Srgb8)
7298 .unwrap();
7299 assert_ne!(frame.content().get(3).unwrap().texture, video);
7300 }
7301
7302 #[test]
7303 #[ignore = "needs a GPU; run under pgpu"]
7304 fn screen_content_emits_through_the_frame() {
7305 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7306 let size = 64;
7307 let mut frame = Frame::new(gpu, size, size).unwrap();
7308 let screen = quad(
7309 &mut frame,
7310 [
7311 [-2.0, 0.0, -2.0],
7312 [2.0, 0.0, -2.0],
7313 [-2.0, 0.0, 2.0],
7314 [2.0, 0.0, 2.0],
7315 ],
7316 );
7317 frame.set_content(0, 4, 4, ContentFormat::Srgb8).unwrap();
7318 frame
7319 .write_content(0, [0, 0, 4, 4], &[0u8, 255, 0, 255].repeat(16))
7320 .unwrap();
7321 let dark = Material {
7322 base: [0.01; 3],
7323 roughness: 0.3,
7324 ..Default::default()
7325 };
7326 let lit = Material {
7327 content_layer: pfx_materials::ContentLayer::from_kind(
7328 pfx_materials::Content::Screen,
7329 1.0,
7330 0,
7331 &pfx_materials::ContentLook {
7332 screen_gain: 1.5,
7333 ..Default::default()
7334 },
7335 ),
7336 ..dark
7337 };
7338 let camera = Camera {
7339 view: look_down(4.0),
7340 projection: perspective(1.0),
7341 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
7342 position: [0.0, 4.0, 0.0],
7343 };
7344 let sun = Sun {
7345 direction: [0.0, 1.0, 0.0],
7346 colour: [1.0; 3],
7347 intensity: 0.0,
7348 };
7349 let mut centre = |materials: &[Material]| {
7350 let instances = [Instance::new(screen, identity(), 0, 1)];
7351 frame
7352 .render(&calm_scene(camera, &instances, materials, sun))
7353 .unwrap();
7354 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
7355 let i = ((size / 2) * size + size / 2) as usize * 4;
7356 [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
7357 };
7358 let off = centre(&[dark]);
7359 let on = centre(&[lit]);
7360 println!("screen off {off:?}, on {on:?}");
7361 assert!((on[1] - off[1] - 1.5).abs() < 0.02);
7362 assert!((on[0] - off[0]).abs() < 0.01 && (on[2] - off[2]).abs() < 0.01);
7363 }
7364
7365 fn screen_materials() -> [Material; 2] {
7366 let dark = Material {
7367 base: [0.01; 3],
7368 roughness: 0.3,
7369 ..Default::default()
7370 };
7371 let lit = Material {
7372 content_layer: pfx_materials::ContentLayer::from_kind(
7373 pfx_materials::Content::Screen,
7374 1.0,
7375 0,
7376 &pfx_materials::ContentLook {
7377 screen_gain: 1.5,
7378 ..Default::default()
7379 },
7380 ),
7381 ..dark
7382 };
7383 [dark, lit]
7384 }
7385
7386 fn green_screen(frame: &mut Frame) {
7387 frame.set_content(0, 4, 4, ContentFormat::Srgb8).unwrap();
7388 frame
7389 .write_content(0, [0, 0, 4, 4], &[0u8, 255, 0, 255].repeat(16))
7390 .unwrap();
7391 }
7392
7393 fn hdr_green(frame: &Frame) -> Vec<f32> {
7394 frame
7395 .gpu
7396 .readback_rgba16(&frame.targets.hdr)
7397 .unwrap()
7398 .chunks_exact(4)
7399 .map(|pixel| half(pixel[1]))
7400 .collect()
7401 }
7402
7403 #[test]
7404 fn content_faces_choose_the_side_that_shows() {
7405 assert!(ContentFace::Both.shows(true) && ContentFace::Both.shows(false));
7406 assert!(ContentFace::Front.shows(true) && !ContentFace::Front.shows(false));
7407 assert!(!ContentFace::Back.shows(true) && ContentFace::Back.shows(false));
7408 assert_eq!(ContentFace::default(), ContentFace::Both);
7409 assert_eq!(InstanceSurface::default().face, ContentFace::Both);
7410 assert_eq!(ContentFace::Both.code(), 0);
7411 assert_eq!(ContentFace::Front.code(), 1);
7412 assert_eq!(ContentFace::Back.code(), 2);
7413 }
7414
7415 #[test]
7416 #[ignore = "needs a GPU; run under pgpu"]
7417 fn two_sided_content_shows_on_the_chosen_face_only() {
7418 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7419 let size = 64;
7420 let mut frame = Frame::new(gpu, size, size).unwrap();
7421 let screen = quad(
7422 &mut frame,
7423 [
7424 [-2.0, 0.0, -2.0],
7425 [2.0, 0.0, -2.0],
7426 [-2.0, 0.0, 2.0],
7427 [2.0, 0.0, 2.0],
7428 ],
7429 );
7430 green_screen(&mut frame);
7431 let materials = screen_materials();
7432 let camera = Camera {
7433 view: look_down(4.0),
7434 projection: perspective(1.0),
7435 previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
7436 position: [0.0, 4.0, 0.0],
7437 };
7438 let sun = Sun {
7439 direction: [0.0, 1.0, 0.0],
7440 colour: [1.0; 3],
7441 intensity: 0.0,
7442 };
7443 let mut flipped = identity();
7444 flipped[1][1] = -1.0;
7445 flipped[2][2] = -1.0;
7446 let centre = (size / 2 * size + size / 2) as usize;
7447 let mut shown = |material: u32, model: Matrix, surface: Option<InstanceSurface>| {
7448 let mut instance = Instance::new(screen, model, material, 1);
7449 instance.two_sided = true;
7450 frame
7451 .set_surfaces(&surface.map_or_else(Vec::new, |surface| vec![surface]))
7452 .unwrap();
7453 frame
7454 .render(&calm_scene(camera, &[instance], &materials, sun))
7455 .unwrap();
7456 hdr_green(&frame)
7457 };
7458 let off = shown(0, identity(), None)[centre];
7459 let gain = 1.5;
7460 let faced = |face| {
7461 Some(InstanceSurface {
7462 face,
7463 ..Default::default()
7464 })
7465 };
7466 let default_front = shown(1, identity(), None);
7467 let default_back = shown(1, flipped, None);
7468 assert!((default_front[centre] - off - gain).abs() < 0.02);
7469 assert!((default_back[centre] - off - gain).abs() < 0.02);
7470 for (face, front_shows, back_shows) in [
7471 (ContentFace::Both, true, true),
7472 (ContentFace::Front, true, false),
7473 (ContentFace::Back, false, true),
7474 ] {
7475 let upright = shown(1, identity(), faced(face));
7476 let turned = shown(1, flipped, faced(face));
7477 for (image, expected) in [(&upright, front_shows), (&turned, back_shows)] {
7478 let lifted = image[centre] - off;
7479 if expected {
7480 assert!((lifted - gain).abs() < 0.02, "{face:?}: {lifted}");
7481 } else {
7482 assert!(lifted.abs() < 0.02, "{face:?}: {lifted}");
7483 }
7484 }
7485 if face == ContentFace::Both {
7486 assert_eq!(upright, default_front);
7487 assert_eq!(turned, default_back);
7488 }
7489 }
7490 }
7491
7492 #[test]
7493 #[ignore = "needs a GPU; run under pgpu"]
7494 fn front_content_keeps_a_rolled_strip_outside_blank() {
7495 use pfx_geom::strip::{Roller, Strip};
7496 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7497 let size = 128;
7498 let mut frame = Frame::new(gpu, size, size).unwrap();
7499 let strip = Strip {
7500 length: 3.0,
7501 width: 1.2,
7502 };
7503 let mesh = strip.mesh(0.12, 0.002, None, 4);
7504 let handle = frame
7505 .upload_mesh(MeshData {
7506 positions: &mesh.positions,
7507 normals: &mesh.normals,
7508 tangents: &mesh.tangents,
7509 uvs: &mesh.uvs,
7510 uvs1: None,
7511 alpha: None,
7512 indices: &mesh.indices,
7513 })
7514 .unwrap();
7515 green_screen(&mut frame);
7516 let materials = screen_materials();
7517 let mut instance = Instance::new(handle, identity(), 1, 1);
7518 instance.deformer = DeformerId(0);
7519 let camera = Camera {
7520 view: look_down(6.0),
7521 projection: perspective(1.0),
7522 previous_view_projection: multiply(perspective(1.0), look_down(6.0)),
7523 position: [0.0, 6.0, 0.0],
7524 };
7525 let sun = Sun {
7526 direction: [0.0, 1.0, 0.0],
7527 colour: [1.0; 3],
7528 intensity: 0.0,
7529 };
7530 let roller = Roller {
7531 at: 1.2,
7532 core: 0.12,
7533 thickness: 0.01,
7534 };
7535 let state = RollerFrame {
7536 at: roller.at,
7537 outer: roller.outer(&strip),
7538 thickness: roller.thickness,
7539 };
7540 let deformers = [Deformer::Roller {
7541 current: state,
7542 previous: state,
7543 }];
7544 let instances = [instance];
7545 let mut lit = |face: ContentFace| {
7546 frame
7547 .set_surfaces(&[InstanceSurface {
7548 face,
7549 ..Default::default()
7550 }])
7551 .unwrap();
7552 let scene = Scene {
7553 deformers: &deformers,
7554 ..calm_scene(camera, &instances, &materials, sun)
7555 };
7556 frame.render(&scene).unwrap();
7557 hdr_green(&frame)
7558 .into_iter()
7559 .map(|green| green > 0.8)
7560 .collect::<Vec<bool>>()
7561 };
7562 let both = lit(ContentFace::Both);
7563 let front = lit(ContentFace::Front);
7564 let back = lit(ContentFace::Back);
7565 let count = |image: &[bool]| image.iter().filter(|&&on| on).count();
7566 println!(
7567 "printed pixels: both {}, front {}, back {}",
7568 count(&both),
7569 count(&front),
7570 count(&back)
7571 );
7572 assert!(count(&front) > 0 && count(&back) > 0);
7573 assert!(count(&front) < count(&both));
7574 assert!(count(&back) < count(&both));
7575 for ((both, front), back) in both.iter().zip(&front).zip(&back) {
7576 assert_eq!(*both, *front || *back);
7577 assert!(!(*front && *back));
7578 }
7579 }
7580
7581 fn floor_quad(frame: &mut Frame, centre: [f32; 2], half: f32) -> MeshHandle {
7582 quad(frame, floor_corners(centre, half))
7583 }
7584
7585 fn floor_corners(centre: [f32; 2], half: f32) -> [[f32; 3]; 4] {
7586 [
7587 [centre[0] - half, 0.0, centre[1] - half],
7588 [centre[0] + half, 0.0, centre[1] - half],
7589 [centre[0] - half, 0.0, centre[1] + half],
7590 [centre[0] + half, 0.0, centre[1] + half],
7591 ]
7592 }
7593
7594 fn quad_data(corners: &[[f32; 3]; 4]) -> MeshData<'_> {
7595 MeshData {
7596 positions: corners,
7597 normals: &[[0.0, 1.0, 0.0]; 4],
7598 tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
7599 uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
7600 uvs1: None,
7601 alpha: None,
7602 indices: &[0, 2, 1, 1, 2, 3],
7603 }
7604 }
7605
7606 fn top_camera() -> Camera {
7607 Camera {
7608 view: look_down(8.0),
7609 projection: perspective(1.0),
7610 previous_view_projection: multiply(perspective(1.0), look_down(8.0)),
7611 position: [0.0, 8.0, 0.0],
7612 }
7613 }
7614
7615 fn lit_pixels(frame: &mut Frame, instances: &[Instance]) -> Result<usize, String> {
7616 let materials = [Material {
7617 base: [0.8; 3],
7618 roughness: 0.5,
7619 ..Default::default()
7620 }];
7621 let sun = Sun {
7622 direction: [0.0, 1.0, 0.0],
7623 colour: [1.0; 3],
7624 intensity: 3.0,
7625 };
7626 frame.render(&calm_scene(top_camera(), instances, &materials, sun))?;
7627 let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
7628 Ok(hdr
7629 .chunks_exact(4)
7630 .filter(|pixel| half(pixel[1]) > 0.1)
7631 .count())
7632 }
7633
7634 #[test]
7635 fn mesh_handles_pack_a_slot_and_a_generation() {
7636 let first = MeshHandle(7);
7637 assert_eq!((first.slot(), first.generation()), (7, 0));
7638 let later = MeshHandle::pack(7, 3);
7639 assert_eq!((later.slot(), later.generation()), (7, 3));
7640 assert_ne!(first, later);
7641 assert_eq!(
7642 MeshHandle::pack(MESH_SLOT_MASK as usize, 1).slot(),
7643 MESH_SLOT_MASK as usize
7644 );
7645 }
7646
7647 #[test]
7648 #[ignore = "needs a GPU; run under pgpu"]
7649 fn released_mesh_stops_drawing_and_the_rest_still_render() {
7650 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7651 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7652 let big = floor_quad(&mut frame, [0.0, 0.0], 2.0);
7653 let small = floor_quad(&mut frame, [3.0, 3.0], 0.5);
7654 let both = [
7655 Instance::new(big, identity(), 0, 1),
7656 Instance::new(small, identity(), 0, 2),
7657 ];
7658 let before = lit_pixels(&mut frame, &both).unwrap();
7659 frame.release_mesh(big).unwrap();
7660 let after = lit_pixels(&mut frame, &both[1..]).unwrap();
7661 println!("lit pixels before {before}, after release {after}");
7662 assert!(before > after * 4);
7663 assert!(after > 0);
7664 assert_eq!(frame.mesh_stats().live, 1);
7665 }
7666
7667 #[test]
7668 #[ignore = "needs a GPU; run under pgpu"]
7669 fn stale_handles_are_refused_and_never_alias_a_newer_mesh() {
7670 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7671 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7672 let old = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7673 frame.release_mesh(old).unwrap();
7674 assert!(frame.release_mesh(old).is_err());
7675 let corners = floor_corners([0.0, 0.0], 1.0);
7676 assert!(frame.replace_mesh(old, quad_data(&corners)).is_err());
7677 assert!(frame.shadow_mesh(old).is_none());
7678 let fresh = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7679 assert_eq!(fresh.slot(), old.slot());
7680 assert_ne!(fresh, old);
7681 assert!(frame.shadow_mesh(fresh).is_some());
7682 assert!(frame.shadow_mesh(old).is_none());
7683 assert!(frame.release_mesh(old).is_err());
7684 assert!(frame.replace_mesh(old, quad_data(&corners)).is_err());
7685 assert!(
7686 frame
7687 .replace_mesh(MeshHandle(900), quad_data(&corners))
7688 .is_err()
7689 );
7690 assert_eq!(frame.mesh_stats().live, 1);
7691 assert!(frame.release_mesh(fresh).is_ok());
7692 }
7693
7694 #[test]
7695 #[ignore = "needs a GPU; run under pgpu"]
7696 fn instance_with_a_released_mesh_is_an_error_from_render() {
7697 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7698 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7699 let kept = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7700 let dropped = floor_quad(&mut frame, [2.0, 2.0], 1.0);
7701 let instances = [
7702 Instance::new(kept, identity(), 0, 1),
7703 Instance::new(dropped, identity(), 0, 2),
7704 ];
7705 assert!(lit_pixels(&mut frame, &instances).is_ok());
7706 frame.release_mesh(dropped).unwrap();
7707 let error = lit_pixels(&mut frame, &instances).unwrap_err();
7708 assert!(error.contains("released"), "{error}");
7709 let replaced = floor_quad(&mut frame, [-2.0, -2.0], 1.0);
7710 assert_eq!(replaced.slot(), dropped.slot());
7711 let error = lit_pixels(&mut frame, &instances).unwrap_err();
7712 assert!(error.contains("released"), "{error}");
7713 assert!(lit_pixels(&mut frame, &instances[..1]).is_ok());
7714 }
7715
7716 #[test]
7717 #[ignore = "needs a GPU; run under pgpu"]
7718 fn replacing_a_mesh_keeps_the_handle_and_reuses_buffers_that_fit() {
7719 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7720 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7721 let handle = floor_quad(&mut frame, [0.0, 0.0], 2.0);
7722 let instances = [Instance::new(handle, identity(), 0, 1)];
7723 let first = lit_pixels(&mut frame, &instances).unwrap();
7724 let buffer = frame.shadow_mesh(handle).unwrap().0.clone();
7725
7726 let smaller = floor_corners([0.0, 0.0], 1.0);
7727 frame.replace_mesh(handle, quad_data(&smaller)).unwrap();
7728 let same_size = lit_pixels(&mut frame, &instances).unwrap();
7729 println!("lit pixels {first} then {same_size} with the same buffers");
7730 assert!(same_size * 3 < first * 2 && same_size > 0);
7731 assert!(*frame.shadow_mesh(handle).unwrap().0 == buffer);
7732 assert_eq!(frame.mesh_stats().live, 1);
7733
7734 let large = sphere(&mut frame);
7735 let mut positions = Vec::new();
7736 let mut normals = Vec::new();
7737 let mut tangents = Vec::new();
7738 let mut uvs = Vec::new();
7739 let mut indices = Vec::new();
7740 for row in 0..10u32 {
7741 for column in 0..10u32 {
7742 let at = [
7743 column as f32 * 4.0 / 9.0 - 2.0,
7744 0.0,
7745 row as f32 * 4.0 / 9.0 - 2.0,
7746 ];
7747 positions.push(at);
7748 normals.push([0.0, 1.0, 0.0]);
7749 tangents.push([1.0, 0.0, 0.0, 1.0]);
7750 uvs.push([column as f32 / 9.0, row as f32 / 9.0]);
7751 if row < 9 && column < 9 {
7752 let a = row * 10 + column;
7753 indices.extend_from_slice(&[a, a + 10, a + 1, a + 1, a + 10, a + 11]);
7754 }
7755 }
7756 }
7757 frame.release_mesh(large).unwrap();
7758 frame
7759 .replace_mesh(
7760 handle,
7761 MeshData {
7762 positions: &positions,
7763 normals: &normals,
7764 tangents: &tangents,
7765 uvs: &uvs,
7766 uvs1: None,
7767 alpha: None,
7768 indices: &indices,
7769 },
7770 )
7771 .unwrap();
7772 let grown = lit_pixels(&mut frame, &instances).unwrap();
7773 println!(
7774 "lit pixels {grown} after growing to {} vertices",
7775 positions.len()
7776 );
7777 assert!(grown.abs_diff(first) * 20 < first);
7778 assert!(*frame.shadow_mesh(handle).unwrap().0 != buffer);
7779 assert_eq!(frame.shadow_mesh(handle).unwrap().2, indices.len() as u32);
7780 assert_eq!(frame.mesh_stats().live, 1);
7781
7782 let tiny = floor_corners([0.0, 0.0], 0.5);
7783 frame.replace_mesh(handle, quad_data(&tiny)).unwrap();
7784 let shrunk = lit_pixels(&mut frame, &instances).unwrap();
7785 assert!(shrunk < same_size && shrunk > 0);
7786 assert_eq!(frame.shadow_mesh(handle).unwrap().2, 6);
7787 let broken = [[0.0; 3]; 3];
7788 let error = frame.replace_mesh(
7789 handle,
7790 MeshData {
7791 positions: &broken,
7792 normals: &broken,
7793 tangents: &[[0.0; 4]; 3],
7794 uvs: &[[0.0; 2]; 3],
7795 uvs1: None,
7796 alpha: None,
7797 indices: &[0, 1, 7],
7798 },
7799 );
7800 assert!(error.is_err());
7801 assert_eq!(lit_pixels(&mut frame, &instances).unwrap(), shrunk);
7802 }
7803
7804 #[test]
7805 #[ignore = "needs a GPU; run under pgpu"]
7806 fn upload_and_release_cycles_keep_the_buffers_bounded() {
7807 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7808 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7809 let keep = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7810 let corners = floor_corners([0.0, 0.0], 1.0);
7811 let mut seen = std::collections::HashSet::new();
7812 for cycle in 0..1000 {
7813 let handle = frame.upload_mesh(quad_data(&corners)).unwrap();
7814 assert!(seen.insert(handle), "cycle {cycle} reused a handle");
7815 let stats = frame.mesh_stats();
7816 assert_eq!((stats.live, stats.slots, stats.buffers), (2, 2, 6));
7817 frame.release_mesh(handle).unwrap();
7818 }
7819 let stats = frame.mesh_stats();
7820 println!("after 1000 cycles: {stats:?}");
7821 assert_eq!((stats.live, stats.slots, stats.buffers), (1, 2, 3));
7822 println!(
7823 "allocator: {:?}",
7824 frame
7825 .gpu
7826 .device
7827 .generate_allocator_report()
7828 .map(|report| (report.total_allocated_bytes, report.total_reserved_bytes))
7829 );
7830 for _ in 0..5000 {
7831 let handle = frame.upload_mesh(quad_data(&corners)).unwrap();
7832 frame.release_mesh(handle).unwrap();
7833 }
7834 let stats = frame.mesh_stats();
7835 println!("after 6000 cycles: {stats:?}");
7836 assert_eq!(stats.live, 1);
7837 assert!(stats.slots <= 4);
7838 assert!(frame.shadow_mesh(keep).is_some());
7839 }
7840
7841 #[test]
7842 #[ignore = "needs a GPU; run under pgpu"]
7843 fn replace_cycles_count_buffers_by_their_own_life() {
7844 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7845 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7846 let start = frame.mesh_stats().buffers;
7847 let keep = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7848 assert_eq!(frame.mesh_stats().buffers, start + 3);
7849 let grid = |cells: u32| {
7850 let side = cells + 1;
7851 let mut positions = Vec::new();
7852 let mut indices = Vec::new();
7853 for row in 0..side {
7854 for column in 0..side {
7855 positions.push([column as f32, 0.0, row as f32]);
7856 if row < cells && column < cells {
7857 let a = row * side + column;
7858 indices.extend_from_slice(&[
7859 a,
7860 a + side,
7861 a + 1,
7862 a + 1,
7863 a + side,
7864 a + side + 1,
7865 ]);
7866 }
7867 }
7868 }
7869 let count = positions.len();
7870 (
7871 positions,
7872 vec![[0.0, 1.0, 0.0]; count],
7873 vec![[1.0, 0.0, 0.0, 1.0]; count],
7874 vec![[0.0, 0.0]; count],
7875 indices,
7876 )
7877 };
7878 let handle = floor_quad(&mut frame, [2.0, 2.0], 1.0);
7879 let mut most = 0;
7880 for cycle in 0..1000u32 {
7881 let cells = 1 + (cycle * 7) % 23;
7882 let (positions, normals, tangents, uvs, indices) = grid(cells);
7883 frame
7884 .replace_mesh(
7885 handle,
7886 MeshData {
7887 positions: &positions,
7888 normals: &normals,
7889 tangents: &tangents,
7890 uvs: &uvs,
7891 uvs1: None,
7892 alpha: None,
7893 indices: &indices,
7894 },
7895 )
7896 .unwrap();
7897 let stats = frame.mesh_stats();
7898 most = most.max(stats.buffers);
7899 assert_eq!(stats.live, 2);
7900 assert_eq!(stats.buffers, start + 6, "cycle {cycle} with {cells} cells");
7901 }
7902 frame.release_mesh(handle).unwrap();
7903 let stats = frame.mesh_stats();
7904 println!(
7905 "buffers: start {start}, most {most}, after release {}",
7906 stats.buffers
7907 );
7908 assert_eq!(stats.buffers, start + 3);
7909 frame.release_mesh(keep).unwrap();
7910 assert_eq!(frame.mesh_stats().buffers, start);
7911 }
7912
7913 #[test]
7914 #[ignore = "needs a GPU; run under pgpu"]
7915 fn releasing_a_swaying_mesh_frees_its_sway_range_for_the_next() {
7916 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7917 let mut frame = Frame::new(gpu, 64, 64).unwrap();
7918 let corners = floor_corners([0.0, 0.0], 1.0);
7919 let sway = [SwayVertex {
7920 pivot: [0.0; 3],
7921 level: 1.0,
7922 stiffness: 0.5,
7923 }; 4];
7924 let first = frame
7925 .upload_swaying_mesh(quad_data(&corners), &sway)
7926 .unwrap();
7927 let second = frame
7928 .upload_swaying_mesh(quad_data(&corners), &sway)
7929 .unwrap();
7930 assert_eq!(frame.sway_data.len(), 8);
7931 assert!(frame.swaying(first));
7932 assert!(frame.replace_mesh(first, quad_data(&corners)).is_err());
7933 frame.release_mesh(first).unwrap();
7934 assert!(!frame.swaying(first));
7935 let third = frame
7936 .upload_swaying_mesh(quad_data(&corners), &sway)
7937 .unwrap();
7938 assert_eq!(frame.sway_data.len(), 8);
7939 assert!(frame.swaying(third) && frame.swaying(second));
7940 frame.release_mesh(second).unwrap();
7941 frame.release_mesh(third).unwrap();
7942 assert_eq!(frame.sway_free.0, vec![0..8]);
7943 }
7944}