1use std::hash::{Hash, Hasher};
2
3use bytemuck::{Pod, Zeroable};
4use pfx_gpu::{GpuProfiler, wgpu};
5use pfx_load::scene::Light;
6use pfx_materials::Material;
7
8use crate::frame::{Matrix, multiply};
9use crate::shadow::{CASCADE_COUNT, FaceTiles};
10
11pub const MAX_LIGHTS: usize = 4;
12pub const FACES: usize = 6;
13pub const CASTER_SLOTS: usize = CASCADE_COUNT + MAX_LIGHTS * FACES;
14pub const CASTER_BYTES: u64 = 80;
15pub const DEFAULT_FACE_RESOLUTION: u32 = 1024;
16const MARGIN_TEXELS: f32 = 8.0;
17const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
18const FACE_AXES: [([f32; 3], [f32; 3]); FACES] = [
19 ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
20 ([-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
21 ([0.0, 1.0, 0.0], [0.0, 0.0, -1.0]),
22 ([0.0, -1.0, 0.0], [0.0, 0.0, 1.0]),
23 ([0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
24 ([0.0, 0.0, -1.0], [0.0, 1.0, 0.0]),
25];
26
27pub const LIGHTS_WGSL: &str = r#"
28const FIRST_TILE_LAYER: u32 = 3u;
29struct LocalLight {
30 position_radius: vec4f,
31 colour_range: vec4f,
32 shadow: vec4f,
33 bias: vec4f,
34}
35struct LocalLights {
36 info: vec4u,
37 lights: array<LocalLight, 4>,
38 faces: array<mat4x4f, 24>,
39}
40@group(1) @binding(4) var shadow_transmission: texture_2d_array<u32>;
41@group(1) @binding(5) var<uniform> local_lights: LocalLights;
42@group(1) @binding(6) var light_faces: texture_depth_2d_array;
43@group(1) @binding(7) var light_sampler: sampler_comparison;
44@group(1) @binding(8) var<storage, read> caster_tints: array<vec4f>;
45
46fn caster_skips(material: u32) -> bool {
47 return caster.mode.x > 0.5 && caster_tints[material].w > 0.5;
48}
49
50fn light_face(d: vec3f) -> u32 {
51 let a = abs(d);
52 if (a.x >= a.y && a.x >= a.z) {
53 return select(1u, 0u, d.x > 0.0);
54 }
55 if (a.y >= a.z) {
56 return select(3u, 2u, d.y > 0.0);
57 }
58 return select(5u, 4u, d.z > 0.0);
59}
60
61fn light_shadow(light: LocalLight, world: vec3f, normal: vec3f, toward: vec3f) -> f32 {
62 if (light.shadow.x < 0.0) {
63 return 1.0;
64 }
65 let offset = world - light.position_radius.xyz;
66 let reach = max(abs(offset.x), max(abs(offset.y), abs(offset.z)));
67 let texel = reach * light.shadow.y;
68 let shifted = world + normal * texel * light.bias.x + toward * texel * light.bias.y;
69 let layer = u32(light.shadow.x) + light_face(shifted - light.position_radius.xyz);
70 let clip = local_lights.faces[layer] * vec4f(shifted, 1.0);
71 if (clip.w <= 0.0) {
72 return 1.0;
73 }
74 let ndc = clip.xyz / clip.w;
75 let uv = vec2f(ndc.x, -ndc.y) * 0.5 + vec2f(0.5);
76 let step = light.shadow.w / vec2f(textureDimensions(light_faces));
77 var lit = 0.0;
78 for (var y = -1; y <= 1; y++) {
79 for (var x = -1; x <= 1; x++) {
80 lit += textureSampleCompareLevel(light_faces, light_sampler, uv + vec2f(f32(x), f32(y)) * step, layer, ndc.z);
81 }
82 }
83 return lit / 9.0;
84}
85
86fn light_tint(light: LocalLight, world: vec3f, normal: vec3f, toward: vec3f) -> vec3f {
87 let tile = local_lights.info.z;
88 if (tile == 0u || light.shadow.x < 0.0) {
89 return vec3f(1.0);
90 }
91 let offset = world - light.position_radius.xyz;
92 let reach = max(abs(offset.x), max(abs(offset.y), abs(offset.z)));
93 let texel = reach * light.shadow.y;
94 let shifted = world + normal * texel * light.bias.x + toward * texel * light.bias.y;
95 let face = u32(light.shadow.x) + light_face(shifted - light.position_radius.xyz);
96 let clip = local_lights.faces[face] * vec4f(shifted, 1.0);
97 if (clip.w <= 0.0) {
98 return vec3f(1.0);
99 }
100 let ndc = clip.xyz / clip.w;
101 let uv = clamp(vec2f(ndc.x, -ndc.y) * 0.5 + vec2f(0.5), vec2f(0.0), vec2f(1.0));
102 let columns = max(local_lights.info.w, 1u);
103 let per = columns * columns;
104 let slot = face % per;
105 let corner = vec2i(vec2u(slot % columns, slot / columns) * tile);
106 let at = corner + min(vec2i(uv * f32(tile)), vec2i(i32(tile) - 1));
107 let stored = textureLoad(shadow_transmission, at, FIRST_TILE_LAYER + face / per, 0);
108 return select(vec3f(1.0), unpack4x8unorm(stored.x).xyz, ndc.z > bitcast<f32>(stored.y));
109}
110
111fn local_light_radiance(surface: Shaded, wo: vec3f, t: vec3f, b: vec3f, n: vec3f, world: vec3f, geometric: vec3f, film: vec3f) -> vec3f {
112 var total = vec3f(0.0);
113 let count = min(local_lights.info.x, 4u);
114 for (var index = 0u; index < count; index++) {
115 let light = local_lights.lights[index];
116 let to_light = light.position_radius.xyz - world;
117 let distance = length(to_light);
118 let range = light.colour_range.w;
119 if (distance >= range || distance <= 1e-6) {
120 continue;
121 }
122 let l = to_light / distance;
123 let ndl = dot(n, l);
124 if (ndl <= 0.0 || dot(geometric, l) <= 0.0) {
125 continue;
126 }
127 let ratio = distance / range;
128 let window = clamp(1.0 - ratio * ratio * ratio * ratio, 0.0, 1.0);
129 let reach = max(distance, light.position_radius.w);
130 var lit = surface;
131 lit.roughness = max(surface.roughness, sqrt(clamp(light.position_radius.w / (2.0 * distance), 0.0, 1.0)));
132 let wi = vec3f(dot(l, t), dot(l, b), dot(l, n));
133 let radiance = light.colour_range.xyz * window * window / (reach * reach);
134 total += eval_filmed(lit, wo, wi, film) * radiance * ndl * light_shadow(light, world, geometric, l) * light_tint(light, world, geometric, l);
135 }
136 return total;
137}
138
139fn shadow_tint_one(world: vec3f, normal: vec3f, instance: u32, cascade: i32) -> vec3f {
140 var view_proj = shadow_setting.view_proj_0;
141 var texel = shadow_setting.texels.x;
142 if (cascade == 1) {
143 view_proj = shadow_setting.view_proj_1;
144 texel = shadow_setting.texels.y;
145 } else if (cascade == 2) {
146 view_proj = shadow_setting.view_proj_2;
147 texel = shadow_setting.texels.z;
148 }
149 let shifted = shadow_offset(world, normal, shadow_setting.sun.xyz, shadow_setting.filter_params, texel, -1.0);
150 let projected = shadow_project(view_proj, shifted);
151 if (any(projected < vec3f(0.0)) || any(projected > vec3f(1.0))) {
152 return vec3f(1.0);
153 }
154 let size = vec2i(textureDimensions(shadow_transmission));
155 let at = clamp(vec2i(projected.xy * vec2f(size)), vec2i(0), size - vec2i(1));
156 let stored = textureLoad(shadow_transmission, at, cascade, 0);
157 let behind = projected.z - shadow_setting.sun.w > bitcast<f32>(stored.y) && stored.z != instance + 1u;
158 return select(vec3f(1.0), unpack4x8unorm(stored.x).xyz, behind);
159}
160
161fn shadow_tint(world: vec3f, normal: vec3f, instance: u32) -> vec3f {
162 if (local_lights.info.y == 0u || shadow_setting.texels.w > 0.5) {
163 return vec3f(1.0);
164 }
165 let distance = length(world - frame.camera_position.xyz);
166 let choice = shadow_select(distance, shadow_setting.splits, shadow_setting.filter_params.w);
167 let tint = shadow_tint_one(world, normal, instance, i32(choice.x));
168 if (choice.z <= 0.0) {
169 return tint;
170 }
171 return mix(tint, shadow_tint_one(world, normal, instance, i32(choice.y)), choice.z);
172}
173
174struct TransmitOutput {
175 @builtin(position) position: vec4f,
176 @location(0) world_position: vec3f,
177 @location(1) @interpolate(flat) instance: u32,
178 @location(2) @interpolate(flat) tint: vec4f,
179}
180
181@vertex fn transmit_vertex(input: VertexInput) -> TransmitOutput {
182 let instance = instances[input.instance_index];
183 var output: TransmitOutput;
184 output.tint = caster_tints[instance.fields.x];
185 output.instance = input.instance_index;
186 if (output.tint.w < 0.5) {
187 output.position = vec4f(2.0, 2.0, 2.0, 1.0);
188 return output;
189 }
190 let position = caster_position(input, instance);
191 let world = instance.model * vec4f(position, 1.0);
192 output.position = caster.view_proj * world;
193 output.world_position = world.xyz;
194 return output;
195}
196
197@fragment fn transmit_fragment(input: TransmitOutput) -> @location(0) vec4u {
198 if (clipped(input.instance, input.world_position)) {
199 discard;
200 }
201 return vec4u(pack4x8unorm(vec4f(input.tint.xyz, 1.0)), bitcast<u32>(input.position.z), input.instance + 1u, 0u);
202}
203"#;
204
205#[derive(Clone, Copy, Debug, PartialEq)]
206pub struct LocalLight {
207 pub position: [f32; 3],
208 pub colour: [f32; 3],
209 pub intensity: f32,
210 pub radius: f32,
211 pub range: f32,
212 pub shadow: bool,
213}
214
215pub fn local_light(light: &Light) -> LocalLight {
216 LocalLight {
217 position: light.position,
218 colour: light.color,
219 intensity: light.intensity,
220 radius: light.radius,
221 range: light.range,
222 shadow: light.shadow,
223 }
224}
225
226impl LocalLight {
227 pub fn faded(self, fade: f32) -> Self {
228 Self {
229 intensity: self.intensity * fade.max(0.0),
230 ..self
231 }
232 }
233
234 pub fn lit(&self) -> bool {
235 self.intensity > 0.0 && self.range > 0.0 && self.colour.iter().any(|&value| value > 0.0)
236 }
237
238 fn valid(&self) -> bool {
239 self.position
240 .iter()
241 .chain(&self.colour)
242 .chain([&self.intensity, &self.radius, &self.range])
243 .all(|value| value.is_finite())
244 && self.colour.iter().all(|&value| value >= 0.0)
245 && self.intensity >= 0.0
246 && self.radius >= 0.0
247 && self.range >= 0.0
248 }
249
250 pub fn near(&self) -> f32 {
251 (self.radius * 0.5).clamp(0.002, (self.range * 0.5).max(0.002))
252 }
253}
254
255#[derive(Clone, Copy, Debug, PartialEq)]
256pub struct LightQuality {
257 pub resolution: u32,
258 pub pcf_radius: f32,
259 pub normal_bias: f32,
260 pub depth_bias: f32,
261}
262
263impl Default for LightQuality {
264 fn default() -> Self {
265 Self {
266 resolution: DEFAULT_FACE_RESOLUTION,
267 pcf_radius: 1.5,
268 normal_bias: 1.5,
269 depth_bias: 1.5,
270 }
271 }
272}
273
274pub fn sheet_tint(material: &Material) -> Option<[f32; 3]> {
275 (material.subsurface > 0.0).then(|| {
276 let share = (2.0 * material.subsurface).min(1.0);
277 material
278 .base
279 .map(|channel| (channel * share).clamp(0.0, 1.0))
280 })
281}
282
283pub fn tan_half(resolution: u32) -> f32 {
284 let margin = (2.0 * MARGIN_TEXELS / resolution.max(32) as f32).min(0.5);
285 1.0 / (1.0 - margin)
286}
287
288pub fn face_view_proj(light: &LocalLight, face: usize, resolution: u32) -> Matrix {
289 let (forward, up) = FACE_AXES[face % FACES];
290 let right = cross(forward, up);
291 let eye = light.position;
292 let view = [
293 [right[0], up[0], -forward[0], 0.0],
294 [right[1], up[1], -forward[1], 0.0],
295 [right[2], up[2], -forward[2], 0.0],
296 [-dot(right, eye), -dot(up, eye), dot(forward, eye), 1.0],
297 ];
298 let near = light.near();
299 let far = light.range.max(near * 2.0);
300 let f = 1.0 / tan_half(resolution);
301 let projection = [
302 [f, 0.0, 0.0, 0.0],
303 [0.0, f, 0.0, 0.0],
304 [0.0, 0.0, far / (near - far), -1.0],
305 [0.0, 0.0, far * near / (near - far), 0.0],
306 ];
307 multiply(projection, view)
308}
309
310pub fn face_of(direction: [f32; 3]) -> usize {
311 let a = direction.map(f32::abs);
312 if a[0] >= a[1] && a[0] >= a[2] {
313 return if direction[0] > 0.0 { 0 } else { 1 };
314 }
315 if a[1] >= a[2] {
316 return if direction[1] > 0.0 { 2 } else { 3 };
317 }
318 if direction[2] > 0.0 { 4 } else { 5 }
319}
320
321pub fn falloff(light: &LocalLight, distance: f32) -> f32 {
322 if distance >= light.range || distance <= 1e-6 {
323 return 0.0;
324 }
325 let ratio = distance / light.range;
326 let window = (1.0 - ratio.powi(4)).clamp(0.0, 1.0);
327 let reach = distance.max(light.radius);
328 light.intensity * window * window / (reach * reach)
329}
330
331fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
332 [
333 a[1] * b[2] - a[2] * b[1],
334 a[2] * b[0] - a[0] * b[2],
335 a[0] * b[1] - a[1] * b[0],
336 ]
337}
338
339fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
340 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
341}
342
343#[repr(C)]
344#[derive(Clone, Copy, Pod, Zeroable)]
345struct GpuLight {
346 position_radius: [f32; 4],
347 colour_range: [f32; 4],
348 shadow: [f32; 4],
349 bias: [f32; 4],
350}
351
352#[repr(C)]
353#[derive(Clone, Copy, Pod, Zeroable)]
354struct GpuLights {
355 info: [u32; 4],
356 lights: [GpuLight; MAX_LIGHTS],
357 faces: [Matrix; MAX_LIGHTS * FACES],
358}
359
360struct FaceMaps {
361 resolution: u32,
362 layers: u32,
363 texture: wgpu::Texture,
364 backup: wgpu::Texture,
365 array: wgpu::TextureView,
366 views: Vec<wgpu::TextureView>,
367}
368
369pub type FaceDraw<'d> = dyn FnMut(&mut wgpu::RenderPass<'_>, usize, bool) + 'd;
370
371pub struct Lights {
372 lights: Vec<LocalLight>,
373 quality: LightQuality,
374 uniform: wgpu::Buffer,
375 maps: Option<FaceMaps>,
376 empty_faces: wgpu::TextureView,
377 empty_transmission: wgpu::TextureView,
378 sampler: wgpu::Sampler,
379 tints: Vec<Option<[f32; 3]>>,
380 table: wgpu::Buffer,
381 table_capacity: usize,
382 written: Vec<[f32; 4]>,
383 cache: Option<u64>,
384 clean: bool,
385 reused: bool,
386 extended: bool,
387}
388
389impl Lights {
390 pub fn new(device: &wgpu::Device) -> Self {
391 let uniform = device.create_buffer(&wgpu::BufferDescriptor {
392 label: Some("live local lights"),
393 size: std::mem::size_of::<GpuLights>() as u64,
394 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
395 mapped_at_creation: false,
396 });
397 let empty_faces = device
398 .create_texture(&wgpu::TextureDescriptor {
399 label: Some("live empty light faces"),
400 size: wgpu::Extent3d {
401 width: 1,
402 height: 1,
403 depth_or_array_layers: 1,
404 },
405 mip_level_count: 1,
406 sample_count: 1,
407 dimension: wgpu::TextureDimension::D2,
408 format: DEPTH_FORMAT,
409 usage: wgpu::TextureUsages::TEXTURE_BINDING
410 | wgpu::TextureUsages::RENDER_ATTACHMENT,
411 view_formats: &[],
412 })
413 .create_view(&wgpu::TextureViewDescriptor {
414 dimension: Some(wgpu::TextureViewDimension::D2Array),
415 ..Default::default()
416 });
417 let empty_transmission = device
418 .create_texture(&wgpu::TextureDescriptor {
419 label: Some("live empty transmission"),
420 size: wgpu::Extent3d {
421 width: 1,
422 height: 1,
423 depth_or_array_layers: 1,
424 },
425 mip_level_count: 1,
426 sample_count: 1,
427 dimension: wgpu::TextureDimension::D2,
428 format: crate::shadow::TRANSMISSION_FORMAT,
429 usage: wgpu::TextureUsages::TEXTURE_BINDING,
430 view_formats: &[],
431 })
432 .create_view(&wgpu::TextureViewDescriptor {
433 dimension: Some(wgpu::TextureViewDimension::D2Array),
434 ..Default::default()
435 });
436 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
437 label: Some("live light compare"),
438 address_mode_u: wgpu::AddressMode::ClampToEdge,
439 address_mode_v: wgpu::AddressMode::ClampToEdge,
440 address_mode_w: wgpu::AddressMode::ClampToEdge,
441 mag_filter: wgpu::FilterMode::Linear,
442 min_filter: wgpu::FilterMode::Linear,
443 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
444 compare: Some(wgpu::CompareFunction::LessEqual),
445 ..Default::default()
446 });
447 let table_capacity = 1;
448 Self {
449 lights: Vec::new(),
450 quality: LightQuality::default(),
451 uniform,
452 maps: None,
453 empty_faces,
454 empty_transmission,
455 sampler,
456 tints: Vec::new(),
457 table: table_buffer(device, table_capacity),
458 table_capacity,
459 written: Vec::new(),
460 cache: None,
461 clean: false,
462 reused: false,
463 extended: false,
464 }
465 }
466
467 pub fn set(&mut self, lights: &[LocalLight]) -> Result<(), String> {
468 if lights.len() > MAX_LIGHTS {
469 return Err(format!("at most {MAX_LIGHTS} local lights"));
470 }
471 if !lights.iter().all(LocalLight::valid) {
472 return Err("local lights need finite, nonnegative values".into());
473 }
474 self.lights = lights.to_vec();
475 Ok(())
476 }
477
478 pub fn lights(&self) -> &[LocalLight] {
479 &self.lights
480 }
481
482 pub fn set_quality(&mut self, quality: LightQuality) -> Result<(), String> {
483 if quality.resolution < 32
484 || ![quality.pcf_radius, quality.normal_bias, quality.depth_bias]
485 .iter()
486 .all(|value| value.is_finite() && *value >= 0.0)
487 {
488 return Err("light shadow quality needs at least 32 texels and finite biases".into());
489 }
490 self.quality = quality;
491 Ok(())
492 }
493
494 pub fn quality(&self) -> LightQuality {
495 self.quality
496 }
497
498 pub fn set_transmission(&mut self, tints: &[Option<[f32; 3]>]) -> Result<(), String> {
499 if tints
500 .iter()
501 .flatten()
502 .flatten()
503 .any(|value| !value.is_finite() || !(0.0..=1.0).contains(value))
504 {
505 return Err("transmission tints must be fractions".into());
506 }
507 self.tints = tints.to_vec();
508 Ok(())
509 }
510
511 pub fn transmissive(&self, material: u32) -> bool {
512 self.tints
513 .get(material as usize)
514 .is_some_and(Option::is_some)
515 }
516
517 pub fn any_transmissive(&self) -> bool {
518 self.tints.iter().any(Option::is_some)
519 }
520
521 pub fn table(&self) -> &wgpu::Buffer {
522 &self.table
523 }
524
525 pub fn table_key(&self) -> u64 {
526 let mut hasher = std::collections::hash_map::DefaultHasher::new();
527 for tint in &self.tints {
528 tint.map(|value| value.map(f32::to_bits)).hash(&mut hasher);
529 }
530 hasher.finish()
531 }
532
533 pub fn prepare(
534 &mut self,
535 device: &wgpu::Device,
536 queue: &wgpu::Queue,
537 materials: usize,
538 ) -> bool {
539 let rows: Vec<[f32; 4]> = (0..materials.max(1))
540 .map(|index| match self.tints.get(index).copied().flatten() {
541 Some(tint) => [tint[0], tint[1], tint[2], 1.0],
542 None => [1.0, 1.0, 1.0, 0.0],
543 })
544 .collect();
545 let mut grown = false;
546 if rows.len() > self.table_capacity {
547 self.table_capacity = rows.len().next_power_of_two();
548 self.table = table_buffer(device, self.table_capacity);
549 self.written.clear();
550 grown = true;
551 }
552 if rows != self.written {
553 queue.write_buffer(&self.table, 0, bytemuck::cast_slice(&rows));
554 self.written = rows;
555 }
556 grown
557 }
558
559 pub fn shadowed(&self) -> Vec<(usize, u32)> {
560 self.lights
561 .iter()
562 .enumerate()
563 .filter(|(_, light)| light.lit() && light.shadow)
564 .enumerate()
565 .map(|(slot, (index, _))| (index, (slot * FACES) as u32))
566 .collect()
567 }
568
569 pub fn faces(&self) -> Vec<(u32, Matrix)> {
570 let resolution = self.quality.resolution;
571 self.shadowed()
572 .into_iter()
573 .flat_map(|(index, first)| {
574 let light = self.lights[index];
575 (0..FACES).map(move |face| {
576 (
577 first + face as u32,
578 face_view_proj(&light, face, resolution),
579 )
580 })
581 })
582 .collect()
583 }
584
585 pub fn uniform(&self) -> &wgpu::Buffer {
586 &self.uniform
587 }
588
589 pub fn faces_view(&self) -> &wgpu::TextureView {
590 self.maps
591 .as_ref()
592 .map_or(&self.empty_faces, |maps| &maps.array)
593 }
594
595 pub fn empty_transmission(&self) -> &wgpu::TextureView {
596 &self.empty_transmission
597 }
598
599 pub fn sampler(&self) -> &wgpu::Sampler {
600 &self.sampler
601 }
602
603 pub fn reused(&self) -> bool {
604 self.reused
605 }
606
607 pub fn extended(&self) -> bool {
608 self.extended
609 }
610
611 pub fn write_uniform(&mut self, queue: &wgpu::Queue, transmission: bool, tiles: FaceTiles) {
612 let mut data = GpuLights::zeroed();
613 let shadowed = self.shadowed();
614 let resolution = self.quality.resolution;
615 let mut count = 0;
616 for (index, light) in self.lights.iter().enumerate() {
617 if !light.lit() {
618 continue;
619 }
620 let layer = shadowed
621 .iter()
622 .find(|(shadowed, _)| *shadowed == index)
623 .map_or(-1.0, |(_, first)| *first as f32);
624 data.lights[count] = GpuLight {
625 position_radius: [
626 light.position[0],
627 light.position[1],
628 light.position[2],
629 light.radius,
630 ],
631 colour_range: [
632 light.colour[0] * light.intensity,
633 light.colour[1] * light.intensity,
634 light.colour[2] * light.intensity,
635 light.range,
636 ],
637 shadow: [
638 layer,
639 2.0 * tan_half(resolution) / resolution as f32,
640 0.0,
641 self.quality.pcf_radius,
642 ],
643 bias: [self.quality.normal_bias, self.quality.depth_bias, 0.0, 0.0],
644 };
645 count += 1;
646 }
647 for (layer, matrix) in self.faces() {
648 data.faces[layer as usize] = matrix;
649 }
650 let tiles = if transmission {
651 tiles
652 } else {
653 FaceTiles::default()
654 };
655 data.info = [
656 count as u32,
657 u32::from(transmission),
658 tiles.tile,
659 tiles.columns,
660 ];
661 self.extended = count > 0 || transmission;
662 queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&data));
663 }
664
665 pub fn render(
666 &mut self,
667 device: &wgpu::Device,
668 encoder: &mut wgpu::CommandEncoder,
669 content: u64,
670 moving: bool,
671 draw: &mut FaceDraw<'_>,
672 profiler: Option<&mut GpuProfiler>,
673 ) {
674 let shadowed = self.shadowed();
675 if shadowed.is_empty() {
676 self.reused = false;
677 return;
678 }
679 let layers = (shadowed.len() * FACES) as u32;
680 let resolution = self
681 .quality
682 .resolution
683 .min(device.limits().max_texture_dimension_2d);
684 if self
685 .maps
686 .as_ref()
687 .is_none_or(|maps| maps.layers != layers || maps.resolution != resolution)
688 {
689 self.maps = Some(face_maps(device, resolution, layers));
690 self.cache = None;
691 self.clean = false;
692 }
693 let mut hasher = std::collections::hash_map::DefaultHasher::new();
694 content.hash(&mut hasher);
695 resolution.hash(&mut hasher);
696 for (index, _) in &shadowed {
697 let light = self.lights[*index];
698 for value in light.position.iter().chain([&light.range, &light.radius]) {
699 value.to_bits().hash(&mut hasher);
700 }
701 }
702 let key = hasher.finish();
703 let hit = self.cache == Some(key);
704 if hit && self.clean && !moving {
705 self.reused = true;
706 return;
707 }
708 self.reused = hit;
709 let Some(maps) = &self.maps else {
710 return;
711 };
712 let total = (usize::from(!hit) + usize::from(moving)) * maps.views.len();
713 let mut profiler = profiler;
714 let timing = if total > 0 {
715 profiler
716 .as_deref_mut()
717 .and_then(|timer| timer.pass("light shadows"))
718 } else {
719 None
720 };
721 let mut faces = Faces {
722 done: 0,
723 total,
724 timing,
725 profiler: profiler.as_deref(),
726 };
727 if hit {
728 if !self.clean {
729 copy_layers(encoder, &maps.backup, &maps.texture, resolution, layers);
730 }
731 } else {
732 faces.encode(encoder, maps, true, draw);
733 copy_layers(encoder, &maps.texture, &maps.backup, resolution, layers);
734 }
735 if moving {
736 faces.encode(encoder, maps, false, draw);
737 }
738 self.cache = Some(key);
739 self.clean = !moving;
740 }
741}
742
743struct Faces<'p> {
744 done: usize,
745 total: usize,
746 timing: Option<u32>,
747 profiler: Option<&'p GpuProfiler>,
748}
749
750impl Faces<'_> {
751 fn encode(
752 &mut self,
753 encoder: &mut wgpu::CommandEncoder,
754 maps: &FaceMaps,
755 statics: bool,
756 draw: &mut FaceDraw<'_>,
757 ) {
758 for (layer, view) in maps.views.iter().enumerate() {
759 let writes = self
760 .profiler
761 .and_then(|timer| timer.render_writes(self.timing))
762 .map(|writes| wgpu::RenderPassTimestampWrites {
763 query_set: writes.query_set,
764 beginning_of_pass_write_index: writes
765 .beginning_of_pass_write_index
766 .filter(|_| self.done == 0),
767 end_of_pass_write_index: writes
768 .end_of_pass_write_index
769 .filter(|_| self.done + 1 == self.total),
770 })
771 .filter(|writes| {
772 writes.beginning_of_pass_write_index.is_some()
773 || writes.end_of_pass_write_index.is_some()
774 });
775 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
776 multiview_mask: None,
777 label: Some("light shadow face"),
778 color_attachments: &[],
779 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
780 view,
781 depth_ops: Some(wgpu::Operations {
782 load: if statics {
783 wgpu::LoadOp::Clear(1.0)
784 } else {
785 wgpu::LoadOp::Load
786 },
787 store: wgpu::StoreOp::Store,
788 }),
789 stencil_ops: None,
790 }),
791 timestamp_writes: writes,
792 occlusion_query_set: None,
793 });
794 draw(&mut pass, layer, statics);
795 self.done += 1;
796 }
797 }
798}
799
800fn table_buffer(device: &wgpu::Device, rows: usize) -> wgpu::Buffer {
801 device.create_buffer(&wgpu::BufferDescriptor {
802 label: Some("live caster tints"),
803 size: (rows.max(1) * 16) as u64,
804 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
805 mapped_at_creation: false,
806 })
807}
808
809fn face_maps(device: &wgpu::Device, resolution: u32, layers: u32) -> FaceMaps {
810 let size = wgpu::Extent3d {
811 width: resolution,
812 height: resolution,
813 depth_or_array_layers: layers,
814 };
815 let make = |label| {
816 device.create_texture(&wgpu::TextureDescriptor {
817 label: Some(label),
818 size,
819 mip_level_count: 1,
820 sample_count: 1,
821 dimension: wgpu::TextureDimension::D2,
822 format: DEPTH_FORMAT,
823 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
824 | wgpu::TextureUsages::TEXTURE_BINDING
825 | wgpu::TextureUsages::COPY_SRC
826 | wgpu::TextureUsages::COPY_DST,
827 view_formats: &[],
828 })
829 };
830 let texture = make("live light faces");
831 let backup = make("live light faces static");
832 let array = texture.create_view(&wgpu::TextureViewDescriptor {
833 dimension: Some(wgpu::TextureViewDimension::D2Array),
834 ..Default::default()
835 });
836 let views = (0..layers)
837 .map(|layer| {
838 texture.create_view(&wgpu::TextureViewDescriptor {
839 dimension: Some(wgpu::TextureViewDimension::D2),
840 base_array_layer: layer,
841 array_layer_count: Some(1),
842 ..Default::default()
843 })
844 })
845 .collect();
846 FaceMaps {
847 resolution,
848 layers,
849 texture,
850 backup,
851 array,
852 views,
853 }
854}
855
856fn copy_layers(
857 encoder: &mut wgpu::CommandEncoder,
858 from: &wgpu::Texture,
859 to: &wgpu::Texture,
860 resolution: u32,
861 layers: u32,
862) {
863 encoder.copy_texture_to_texture(
864 from.as_image_copy(),
865 to.as_image_copy(),
866 wgpu::Extent3d {
867 width: resolution,
868 height: resolution,
869 depth_or_array_layers: layers,
870 },
871 );
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::frame::{Camera, Frame, Instance, MeshData, Scene, SceneWind, Sun, transform};
878 use crate::probes::{PROBE_WGSL, ProbeLighting};
879 use crate::shadow::{Quality, Shadows, View};
880 use half::f16;
881 use pfx_bake::{BakeScene, Emitters, Grid, GridSpec, Probe, write_artifact_with_emitters};
882 use pfx_gpu::Gpu;
883 use std::path::Path;
884
885 const SIZE: u32 = 256;
886
887 #[test]
888 fn light_tiles_start_after_the_cascades() {
889 assert!(LIGHTS_WGSL.contains(&format!("const FIRST_TILE_LAYER: u32 = {CASCADE_COUNT}u;")));
890 }
891
892 fn light() -> LocalLight {
893 LocalLight {
894 position: [0.5, 1.0, 0.0],
895 colour: [1.0; 3],
896 intensity: 1.0,
897 radius: 0.0,
898 range: 10.0,
899 shadow: true,
900 }
901 }
902
903 #[test]
904 fn each_face_sees_its_own_axis_inside_the_margin() {
905 let light = light();
906 let resolution = 512;
907 for direction in [
908 [1.0, 0.2, -0.3],
909 [-0.7, 0.69, 0.1],
910 [0.1, 0.9, 0.3],
911 [0.2, -1.0, 0.999],
912 [0.4, -0.2, 0.6],
913 [-0.3, 0.3, -0.31],
914 [0.577, 0.577, 0.577],
915 ] {
916 let face = face_of(direction);
917 let matrix = face_view_proj(&light, face, resolution);
918 let mut previous = -1.0;
919 for distance in [0.05, 0.5, 2.0, 9.0] {
920 let length = dot(direction, direction).sqrt();
921 let point = [0, 1, 2]
922 .map(|axis| light.position[axis] + direction[axis] / length * distance);
923 let clip = transform(matrix, [point[0], point[1], point[2], 1.0]);
924 let ndc = [clip[0] / clip[3], clip[1] / clip[3], clip[2] / clip[3]];
925 let inside = 1.0 / tan_half(resolution) + 1e-5;
926 assert!(
927 ndc[0].abs() <= inside && ndc[1].abs() <= inside,
928 "{direction:?} {ndc:?}"
929 );
930 assert!(ndc[2] > previous && ndc[2] < 1.0, "{direction:?} {ndc:?}");
931 previous = ndc[2];
932 }
933 }
934 let margin = (1.0 - 1.0 / tan_half(resolution)) * 0.5 * resolution as f32;
935 assert!((margin - MARGIN_TEXELS).abs() < 1e-3);
936 }
937
938 #[test]
939 fn falloff_is_inverse_square_and_ends_at_the_range() {
940 let light = LocalLight {
941 intensity: 0.88,
942 radius: 0.06,
943 range: 3.0,
944 ..light()
945 };
946 let near = falloff(&light, 0.5);
947 assert!((near - 0.88 / 0.25).abs() / near < 2e-3);
948 assert_eq!(falloff(&light, 3.0), 0.0);
949 assert!(falloff(&light, 0.01) <= 0.88 / (0.06 * 0.06));
950 assert!(!light.faded(0.0).lit());
951 assert_eq!(light.faded(4.0).intensity, 0.88 * 4.0);
952 }
953
954 #[test]
955 fn sheet_tint_is_base_times_twice_the_translucency() {
956 let paper = Material {
957 base: [0.9, 0.85, 0.7],
958 subsurface: 0.22,
959 ..Material::default()
960 };
961 let tint = sheet_tint(&paper).unwrap();
962 for (channel, base) in tint.iter().zip(paper.base) {
963 assert!((channel - base * 0.44).abs() < 1e-6);
964 }
965 let thick = Material {
966 subsurface: 0.7,
967 ..paper
968 };
969 assert_eq!(sheet_tint(&thick).unwrap(), paper.base);
970 assert_eq!(sheet_tint(&Material::default()), None);
971 }
972
973 #[test]
974 fn light_settings_are_checked() {
975 let broken = LocalLight {
976 intensity: f32::NAN,
977 ..light()
978 };
979 assert!(!broken.valid());
980 assert!(light().valid());
981 assert!(
982 !LocalLight {
983 range: -1.0,
984 ..light()
985 }
986 .valid()
987 );
988 }
989
990 fn quad(centre: [f32; 3], half: [f32; 2]) -> (Vec<[f32; 3]>, Vec<u32>) {
991 let [x, y, z] = centre;
992 (
993 vec![
994 [x - half[0], y, z + half[1]],
995 [x + half[0], y, z + half[1]],
996 [x + half[0], y, z - half[1]],
997 [x - half[0], y, z - half[1]],
998 ],
999 vec![0, 1, 2, 0, 2, 3],
1000 )
1001 }
1002
1003 fn upload(frame: &mut Frame, centre: [f32; 3], half: [f32; 2]) -> crate::frame::MeshHandle {
1004 let (positions, indices) = quad(centre, half);
1005 frame
1006 .upload_mesh(MeshData {
1007 positions: &positions,
1008 normals: &[[0.0, 1.0, 0.0]; 4],
1009 tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
1010 uvs: &[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
1011 uvs1: None,
1012 alpha: None,
1013 indices: &indices,
1014 })
1015 .unwrap()
1016 }
1017
1018 fn identity() -> Matrix {
1019 [
1020 [1.0, 0.0, 0.0, 0.0],
1021 [0.0, 1.0, 0.0, 0.0],
1022 [0.0, 0.0, 1.0, 0.0],
1023 [0.0, 0.0, 0.0, 1.0],
1024 ]
1025 }
1026
1027 fn camera() -> Camera {
1028 let near = 0.1;
1029 let far = 100.0;
1030 let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
1031 let projection = [
1032 [f, 0.0, 0.0, 0.0],
1033 [0.0, f, 0.0, 0.0],
1034 [0.0, 0.0, far / (near - far), -1.0],
1035 [0.0, 0.0, far * near / (near - far), 0.0],
1036 ];
1037 let view = [
1038 [1.0, 0.0, 0.0, 0.0],
1039 [0.0, 0.0, 1.0, 0.0],
1040 [0.0, -1.0, 0.0, 0.0],
1041 [0.0, 0.0, -2.0, 1.0],
1042 ];
1043 Camera {
1044 view,
1045 projection,
1046 previous_view_projection: multiply(projection, view),
1047 position: [0.0, 2.0, 0.0],
1048 }
1049 }
1050
1051 fn pixel(x: f32, z: f32) -> usize {
1052 let extent = 2.0 * (25.0_f32).to_radians().tan();
1053 let column = ((x / extent * 0.5 + 0.5) * SIZE as f32) as usize;
1054 let row = ((z / extent * 0.5 + 0.5) * SIZE as f32) as usize;
1055 row * SIZE as usize + column
1056 }
1057
1058 fn matte(base: [f32; 3]) -> Material {
1059 Material {
1060 base,
1061 roughness: 1.0,
1062 specular: 0.0,
1063 ..Material::default()
1064 }
1065 }
1066
1067 struct Bench {
1068 frame: Frame,
1069 shadows: Shadows,
1070 instances: Vec<Instance>,
1071 materials: Vec<Material>,
1072 }
1073
1074 impl Bench {
1075 fn render(&mut self, sun: f32) -> Vec<[f32; 3]> {
1076 let view = View {
1077 eye: [0.0, 2.0, 0.0],
1078 forward: [0.0, -1.0, 0.0],
1079 up: [0.0, 0.0, -1.0],
1080 fov_y: 50.0_f32.to_radians(),
1081 aspect: 1.0,
1082 near: 0.1,
1083 far: 100.0,
1084 };
1085 let toward = [0.70710677, 0.70710677, 0.0];
1086 let fit = self.shadows.fit(&view, toward);
1087 let scene = Scene {
1088 camera: camera(),
1089 time: 0.0,
1090 seed: 7,
1091 sun: Sun {
1092 direction: toward,
1093 colour: [1.0; 3],
1094 intensity: sun,
1095 },
1096 instances: &self.instances,
1097 materials: &self.materials,
1098 deformers: &[],
1099 wind: SceneWind::default(),
1100 };
1101 let mut encoder = self
1102 .frame
1103 .gpu
1104 .device
1105 .create_command_encoder(&Default::default());
1106 self.frame
1107 .encode(&scene, &mut encoder, Some((&mut self.shadows, &fit)), None)
1108 .unwrap();
1109 self.frame.gpu.queue.submit(Some(encoder.finish()));
1110 self.frame
1111 .gpu
1112 .readback_rgba16(&self.frame.targets.hdr)
1113 .unwrap()
1114 .chunks_exact(4)
1115 .map(|texel| [0, 1, 2].map(|k| f16::from_bits(texel[k]).to_f32()))
1116 .collect()
1117 }
1118 }
1119
1120 fn bench() -> Bench {
1121 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1122 let mut frame = Frame::new(gpu, SIZE, SIZE).unwrap();
1123 let shadows = Shadows::new(
1124 &frame.gpu.device,
1125 Quality {
1126 resolution: 1024,
1127 receiver: Some(crate::shadow::ReceiverBox {
1128 min: [-1.0, -0.1, -1.0],
1129 max: [1.0, 1.0, 1.0],
1130 }),
1131 ..Default::default()
1132 },
1133 );
1134 let floor = upload(&mut frame, [0.0, 0.0, 0.0], [2.0, 2.0]);
1135 Bench {
1136 frame,
1137 shadows,
1138 instances: vec![Instance::new(floor, identity(), 0, 1)],
1139 materials: vec![matte([0.5; 3])],
1140 }
1141 }
1142
1143 fn lambert(albedo: f32, light: &LocalLight, point: [f32; 3]) -> f32 {
1144 let to = [0, 1, 2].map(|k| light.position[k] - point[k]);
1145 let distance = dot(to, to).sqrt();
1146 albedo / std::f32::consts::PI * falloff(light, distance) * to[1] / distance
1147 }
1148
1149 #[test]
1150 #[ignore = "needs a GPU; run under pgpu"]
1151 fn a_point_light_shadows_a_plane() {
1152 let mut bench = bench();
1153 let blocker = upload(&mut bench.frame, [0.25, 0.5, 0.0], [0.1, 0.1]);
1154 bench
1155 .instances
1156 .push(Instance::new(blocker, identity(), 0, 2));
1157 let dark = bench.render(0.0);
1158 bench.frame.set_lights(&[light()]).unwrap();
1159 let lit = bench.render(0.0);
1160 let again = bench.render(0.0);
1161 assert!(bench.frame.lights().reused());
1162 assert_eq!(lit, again);
1163 bench
1164 .frame
1165 .set_lights(&[LocalLight {
1166 shadow: false,
1167 ..light()
1168 }])
1169 .unwrap();
1170 let open = bench.render(0.0);
1171 let lamp =
1172 |image: &[[f32; 3]], x: f32, z: f32| image[pixel(x, z)][1] - dark[pixel(x, z)][1];
1173 let side = lambert(0.5, &light(), [-0.5, 0.0, 0.0]);
1174 let centre = lambert(0.5, &light(), [0.0, 0.0, 0.0]);
1175 println!(
1176 "lamp at the side {:.4} (expected {side:.4}), centre shadowed {:.4}, centre open {:.4} (expected {centre:.4})",
1177 lamp(&lit, -0.5, 0.0),
1178 lamp(&lit, 0.0, 0.0),
1179 lamp(&open, 0.0, 0.0)
1180 );
1181 assert!((lamp(&lit, -0.5, 0.0) - side).abs() < side * 0.03);
1182 assert!((lamp(&open, 0.0, 0.0) - centre).abs() < centre * 0.03);
1183 assert!(lamp(&lit, 0.0, 0.0).abs() < centre * 0.03);
1184 assert!(lamp(&lit, 0.0, 0.3).abs() > centre * 0.5);
1185 bench.frame.set_lights(&[]).unwrap();
1186 assert_eq!(bench.render(0.0), dark);
1187 }
1188
1189 #[test]
1190 #[ignore = "needs a GPU; run under pgpu"]
1191 fn thin_casters_tint_the_sun_and_opaque_casters_still_block_it() {
1192 let mut bench = bench();
1193 let paper = upload(&mut bench.frame, [0.3, 0.5, -0.35], [0.2, 0.25]);
1194 let card = upload(&mut bench.frame, [0.3, 0.5, 0.35], [0.2, 0.25]);
1195 let stock = Material {
1196 subsurface: 0.3,
1197 ..matte([0.9, 0.6, 0.3])
1198 };
1199 bench.materials.extend([stock, matte([0.4; 3])]);
1200 bench.instances.extend([
1201 Instance::new(paper, identity(), 1, 2),
1202 Instance::new(card, identity(), 2, 3),
1203 ]);
1204 let ambient = bench.render(0.0);
1205 let opaque = bench.render(1.0);
1206 let tints: Vec<Option<[f32; 3]>> = bench.materials.iter().map(sheet_tint).collect();
1207 bench.frame.set_transmission(&tints).unwrap();
1208 let through = bench.render(1.0);
1209 let sun = |image: &[[f32; 3]], x: f32, z: f32| {
1210 let at = pixel(x, z);
1211 [0, 1, 2].map(|k| image[at][k] - ambient[at][k])
1212 };
1213 let open = sun(&through, -0.7, -0.35);
1214 let behind_paper = sun(&through, -0.2, -0.35);
1215 let behind_card = sun(&through, -0.2, 0.35);
1216 let expected = sheet_tint(&stock).unwrap();
1217 println!(
1218 "open {open:?}, behind paper {behind_paper:?} (tint {expected:?}), behind card {behind_card:?}"
1219 );
1220 for k in 0..3 {
1221 assert!((behind_paper[k] / open[k] - expected[k]).abs() < 0.02);
1222 assert!(behind_card[k].abs() < open[k] * 0.02);
1223 assert!(sun(&opaque, -0.2, -0.35)[k].abs() < open[k] * 0.02);
1224 }
1225 for (x, z) in [(-0.7, -0.35), (-0.2, 0.35), (-0.7, 0.6), (0.8, -0.8)] {
1226 assert_eq!(sun(&opaque, x, z), sun(&through, x, z), "{x} {z}");
1227 }
1228 bench.frame.set_transmission(&[]).unwrap();
1229 assert_eq!(bench.render(1.0), opaque);
1230 }
1231
1232 #[test]
1233 #[ignore = "needs a GPU; run under pgpu"]
1234 fn emission_scale_fades_the_baked_lamp() {
1235 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1236 let spec = GridSpec {
1237 min: [0.0; 3],
1238 max: [0.0; 3],
1239 spacing: 1.0,
1240 };
1241 let grid = |value: f32| {
1242 Grid::new(
1243 spec,
1244 vec![Probe {
1245 lobes: [[value; 3]; 6],
1246 visibility: [100.0; 6],
1247 }],
1248 )
1249 .unwrap()
1250 };
1251 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp");
1252 std::fs::create_dir_all(&root).unwrap();
1253 let sample = |baked: f32, live: f32| {
1254 let name = format!("live-emission-test-{baked}-{live}");
1255 let path = root.join(&name);
1256 if path.exists() {
1257 std::fs::remove_dir_all(&path).unwrap();
1258 }
1259 let scene = BakeScene {
1260 triangles: Vec::new(),
1261 shapes: Vec::new(),
1262 materials: vec![Material::default()],
1263 anchors: vec![pfx_bake::Anchor {
1264 hour: 22.0,
1265 sky: pfx_load::Sky {
1266 width: 1,
1267 height: 1,
1268 texels: vec![[0.0; 4]],
1269 },
1270 sun: pfx_trace::Sun {
1271 direction: [0.0, 1.0, 0.0],
1272 color: [0.0; 3],
1273 intensity: 0.0,
1274 },
1275 }],
1276 };
1277 write_artifact_with_emitters(
1278 &root,
1279 Path::new(&name),
1280 &scene,
1281 &[grid(0.1 + 0.5 * baked)],
1282 &Emitters {
1283 grid: grid(0.5),
1284 scales: vec![baked],
1285 direct: false,
1286 },
1287 1,
1288 1,
1289 )
1290 .unwrap();
1291 let mut lighting =
1292 ProbeLighting::load(&gpu.device, &gpu.queue, &path, 22.0, [0.0; 3]).unwrap();
1293 lighting.set_emission(&gpu.queue, live).unwrap();
1294 let value = probe_value(&gpu, &lighting);
1295 std::fs::remove_dir_all(&path).unwrap();
1296 value
1297 };
1298 let off = sample(0.0, 0.0);
1299 let on = sample(0.0, 2.0);
1300 let kept = sample(1.0, 1.0);
1301 let removed = sample(1.0, 0.0);
1302 println!("off {off:.4}, on {on:.4}, kept {kept:.4}, removed {removed:.4}");
1303 assert!((off - 0.1).abs() < 1e-3);
1304 assert!((on - 1.1).abs() < 1e-2);
1305 assert!((kept - 0.6).abs() < 1e-2);
1306 assert!((removed - 0.1).abs() < 1e-3);
1307 }
1308
1309 fn probe_value(gpu: &Gpu, lighting: &ProbeLighting) -> f32 {
1310 let empty = gpu
1311 .device
1312 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1313 label: None,
1314 entries: &[],
1315 });
1316 let empty_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1317 label: None,
1318 layout: &empty,
1319 entries: &[],
1320 });
1321 let layout = gpu
1322 .device
1323 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1324 label: None,
1325 bind_group_layouts: &[
1326 Some(&empty),
1327 Some(&empty),
1328 Some(&empty),
1329 Some(lighting.layout()),
1330 ],
1331 immediate_size: 0,
1332 });
1333 let source = format!(
1334 "{PROBE_WGSL}\n@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {{ var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); return vec4f(p[i], 0.0, 1.0); }}\n@fragment fn fs() -> @location(0) vec4f {{ return vec4f(sample_live_irradiance(vec3f(0.0), vec3f(0.0, 1.0, 0.0)), 1.0); }}"
1335 );
1336 let shader = gpu
1337 .device
1338 .create_shader_module(wgpu::ShaderModuleDescriptor {
1339 label: None,
1340 source: wgpu::ShaderSource::Wgsl(source.into()),
1341 });
1342 let pipeline = gpu
1343 .device
1344 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1345 label: None,
1346 layout: Some(&layout),
1347 vertex: wgpu::VertexState {
1348 module: &shader,
1349 entry_point: Some("vs"),
1350 compilation_options: Default::default(),
1351 buffers: &[],
1352 },
1353 primitive: wgpu::PrimitiveState::default(),
1354 depth_stencil: None,
1355 multisample: wgpu::MultisampleState::default(),
1356 fragment: Some(wgpu::FragmentState {
1357 module: &shader,
1358 entry_point: Some("fs"),
1359 compilation_options: Default::default(),
1360 targets: &[Some(wgpu::ColorTargetState {
1361 format: wgpu::TextureFormat::Rgba16Float,
1362 blend: None,
1363 write_mask: wgpu::ColorWrites::ALL,
1364 })],
1365 }),
1366 multiview_mask: None,
1367 cache: None,
1368 });
1369 let target = gpu
1370 .offscreen(1, 1, wgpu::TextureFormat::Rgba16Float)
1371 .unwrap();
1372 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1373 {
1374 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1375 multiview_mask: None,
1376 label: None,
1377 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1378 view: &target.view,
1379 resolve_target: None,
1380 ops: wgpu::Operations {
1381 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1382 store: wgpu::StoreOp::Store,
1383 },
1384 depth_slice: None,
1385 })],
1386 depth_stencil_attachment: None,
1387 timestamp_writes: None,
1388 occlusion_query_set: None,
1389 });
1390 pass.set_pipeline(&pipeline);
1391 pass.set_bind_group(0, &empty_group, &[]);
1392 pass.set_bind_group(1, &empty_group, &[]);
1393 pass.set_bind_group(2, &empty_group, &[]);
1394 pass.set_bind_group(3, lighting.group(), &[]);
1395 pass.draw(0..3, 0..1);
1396 }
1397 gpu.queue.submit(Some(encoder.finish()));
1398 f16::from_bits(gpu.readback_rgba16(&target).unwrap()[0]).to_f32()
1399 }
1400}