1use std::cell::Cell;
2use std::path::Path;
3
4use bytemuck::{Pod, Zeroable};
5use pfx_bake::{
6 AnchorBlend, Artifact, Grid, Manifest, blend_anchors, read_artifact, with_emitters,
7};
8use wgpu::util::DeviceExt;
9
10pub const PROBE_CORNER_FLOOR: f32 = 0.05;
11pub const PROBE_LOCALS: usize = 4;
12pub const PROBE_WGSL: &str = r#"
13override use_probes: bool = true;
14struct ProbeLocal {
15 min_spacing: vec4f,
16 dims: vec4u,
17}
18struct ProbeGridInfo {
19 min_spacing: vec4f,
20 dims: vec4u,
21 fallback: vec4f,
22 blend: vec4f,
23 shade: vec4f,
24 locals: array<ProbeLocal, 4>,
25}
26@group(3) @binding(6) var<storage, read> probe_lower: array<u32>;
27@group(3) @binding(7) var<storage, read> probe_upper: array<u32>;
28@group(3) @binding(8) var<uniform> probe_grid: ProbeGridInfo;
29
30fn probe_word(anchor: u32, index: u32) -> u32 {
31 if (anchor == 0u) { return probe_lower[index]; }
32 return probe_upper[index];
33}
34
35fn probe_block(anchor: u32, offset: u32) -> array<u32, 18> {
36 var block: array<u32, 18>;
37 if (anchor == 0u) {
38 for (var k = 0u; k < 18u; k++) { block[k] = probe_lower[offset + k]; }
39 } else {
40 for (var k = 0u; k < 18u; k++) { block[k] = probe_upper[offset + k]; }
41 }
42 return block;
43}
44
45fn probe_index(xyz: vec3u, dims: vec4u) -> u32 {
46 return (xyz.z * dims.y + xyz.y) * dims.x + xyz.x;
47}
48
49fn sample_probe_volume(position: vec3f, n: vec3f, point: vec3f, anchor: u32, origin: vec4f, dims: vec4u, words: u32, toward: vec3f, seen: ptr<function, f32>) -> vec4f {
50 let last = vec3f(dims.xyz - vec3u(1u));
51 let surface = (position - origin.xyz) / origin.w;
52 let shifted = (point - origin.xyz) / origin.w;
53 let surface_out = any(surface < vec3f(0.0)) || any(surface > last);
54 let shifted_out = any(shifted < vec3f(0.0)) || any(shifted > last);
55 if (surface_out && shifted_out) { return vec4f(0.0); }
56 let coord = clamp(shifted, vec3f(0.0), last);
57 let base = min(vec3u(floor(coord)), max(dims.xyz, vec3u(2u)) - vec3u(2u));
58 var t = clamp(coord - vec3f(base), vec3f(0.0), vec3f(1.0));
59 for (var axis = 0u; axis < 3u; axis++) {
60 if (dims[axis] == 1u) { t[axis] = 0.0; }
61 }
62 let version_two = dims.w == 2u;
63 var color = vec3f(0.0);
64 var toward_luma = 0.0;
65 var total = 0.0;
66 for (var corner = 0u; corner < 8u; corner++) {
67 var xyz = base;
68 var weight = 1.0;
69 for (var axis = 0u; axis < 3u; axis++) {
70 let high = (corner & (1u << axis)) != 0u;
71 if (high && dims[axis] > 1u) { xyz[axis] += 1u; }
72 weight *= select(1.0 - t[axis], t[axis], high);
73 }
74 weight = max(weight - probe_grid.blend.z, 0.0);
75 if (weight == 0.0) { continue; }
76 let offset = words + probe_index(xyz, dims) * select(15u, 20u, version_two);
77 var center = origin.xyz + vec3f(xyz) * origin.w;
78 if (version_two) {
79 let last = probe_word(anchor, offset + 19u);
80 if (((last >> 16u) & 1u) == 0u) { continue; }
81 center += vec3f(unpack2x16float(probe_word(anchor, offset + 18u)), unpack2x16float(last).x) * origin.w;
82 let toward_probe = center - position + n * 0.0001;
83 let facing = clamp((dot(toward_probe / max(length(toward_probe), 0.000001), n) + 1.0) * 0.5, 0.0, 1.0);
84 weight *= facing * facing * facing;
85 if (weight == 0.0) { continue; }
86 let block = probe_block(anchor, offset);
87 let toward_point = point - center;
88 let distance = length(toward_point);
89 let direction = toward_point / max(distance, 0.000001);
90 var irradiance = vec3f(0.0);
91 var toward_irradiance = 0.0;
92 var mean = 0.0;
93 var second = 0.0;
94 for (var axis = 0u; axis < 3u; axis++) {
95 let normal_component = n[axis];
96 let up = normal_component >= 0.0;
97 let rg = unpack2x16float(select(block[axis * 4u + 2u], block[axis * 4u], up));
98 let b = unpack2x16float(select(block[axis * 4u + 3u], block[axis * 4u + 1u], up)).x;
99 irradiance += vec3f(rg, b) * normal_component * normal_component;
100 let toward_component = toward[axis];
101 let toward_up = toward_component >= 0.0;
102 let toward_rg = unpack2x16float(select(block[axis * 4u + 2u], block[axis * 4u], toward_up));
103 let toward_b = unpack2x16float(select(block[axis * 4u + 3u], block[axis * 4u + 1u], toward_up)).x;
104 toward_irradiance += dot(vec3f(toward_rg, toward_b), vec3f(0.2126, 0.7152, 0.0722)) * toward_component * toward_component;
105 let ray_component = direction[axis];
106 let pair = unpack2x16float(block[12u + axis]);
107 let depth = select(pair.y, pair.x, ray_component >= 0.0);
108 let ray_weight = ray_component * ray_component;
109 let squared = unpack2x16float(block[15u + axis]);
110 mean += ray_weight * depth * origin.w;
111 second += ray_weight * select(squared.y, squared.x, ray_component >= 0.0) * origin.w * origin.w;
112 }
113 let variance = max(second - mean * mean, origin.w * origin.w * 0.01);
114 let delta = max(distance - mean - origin.w * 0.1, 0.0);
115 let chance = variance / (variance + delta * delta);
116 weight *= chance * chance;
117 color += irradiance * weight;
118 toward_luma += toward_irradiance * weight;
119 total += weight;
120 continue;
121 }
122 let toward_probe = center - position + n * 0.0001;
123 let facing = clamp((dot(toward_probe / max(length(toward_probe), 0.000001), n) + 1.0) * 0.5, 0.0, 1.0);
124 weight *= facing * facing * facing;
125 if (weight == 0.0) { continue; }
126 let toward_point = point - center;
127 let distance = length(toward_point);
128 let direction = toward_point / max(distance, 0.000001);
129 var irradiance = vec3f(0.0);
130 var toward_irradiance = 0.0;
131 var visibility = 0.0;
132 for (var axis = 0u; axis < 3u; axis++) {
133 let normal_component = n[axis];
134 let normal_lobe = axis * 2u + select(1u, 0u, normal_component >= 0.0);
135 let rg = unpack2x16float(probe_word(anchor, offset + normal_lobe * 2u));
136 let b = unpack2x16float(probe_word(anchor, offset + normal_lobe * 2u + 1u)).x;
137 irradiance += vec3f(rg, b) * normal_component * normal_component;
138 let toward_component = toward[axis];
139 if (toward_component != 0.0) {
140 let toward_lobe = axis * 2u + select(1u, 0u, toward_component >= 0.0);
141 let toward_rg = unpack2x16float(probe_word(anchor, offset + toward_lobe * 2u));
142 let toward_b = unpack2x16float(probe_word(anchor, offset + toward_lobe * 2u + 1u)).x;
143 toward_irradiance += dot(vec3f(toward_rg, toward_b), vec3f(0.2126, 0.7152, 0.0722)) * toward_component * toward_component;
144 }
145 let ray_component = direction[axis];
146 let pair = unpack2x16float(probe_word(anchor, offset + 12u + axis));
147 let depth = select(pair.y, pair.x, ray_component >= 0.0);
148 let ray_weight = ray_component * ray_component;
149 let reach = depth + origin.w * 0.25;
150 visibility += ray_weight * clamp((reach - distance) / origin.w, 0.0, 1.0);
151 }
152 weight *= select(1.0, visibility, distance > 0.000001);
153 color += irradiance * weight;
154 toward_luma += toward_irradiance * weight;
155 total += weight;
156 }
157 if (total <= 0.000001) { return vec4f(0.0); }
158 *seen = toward_luma / total;
159 return vec4f(color / total, 1.0);
160}
161
162fn probe_local_fade(point: vec3f, local: ProbeLocal) -> f32 {
163 let far = local.min_spacing.xyz + vec3f(local.dims.xyz - vec3u(1u)) * local.min_spacing.w;
164 let inside = min(point - local.min_spacing.xyz, far - point);
165 return clamp(min(inside.x, min(inside.y, inside.z)) / local.min_spacing.w, 0.0, 1.0);
166}
167
168fn sample_probe_anchor(position: vec3f, normal: vec3f, anchor: u32) -> vec3f {
169 var unused = 0.0;
170 return sample_probe_anchor_toward(position, normal, vec3f(0.0), anchor, &unused);
171}
172
173fn sample_probe_anchor_toward(position: vec3f, normal: vec3f, toward: vec3f, anchor: u32, seen: ptr<function, f32>) -> vec3f {
174 let normal_length = length(normal);
175 if (normal_length < 0.000001 || normal_length != normal_length || normal_length > 1e20 || any(position != position) || any(abs(position) > vec3f(1e20))) { return probe_grid.fallback.xyz; }
176 let n = normal / normal_length;
177 let point = position + n * 0.006;
178 var local = vec4f(0.0);
179 var local_seen = 0.0;
180 var fade = 0.0;
181 let count = min(u32(probe_grid.blend.w), 4u);
182 for (var k = 0u; k < count; k++) {
183 let inner = probe_local_fade(point, probe_grid.locals[k]);
184 if (inner <= 0.0) { continue; }
185 let sampled = sample_probe_volume(position, n, point, anchor, probe_grid.locals[k].min_spacing, vec4u(probe_grid.locals[k].dims.xyz, 2u), probe_grid.locals[k].dims.w, toward, &local_seen);
186 if (sampled.w > 0.0) {
187 local = sampled;
188 fade = inner;
189 break;
190 }
191 }
192 if (fade >= 1.0) {
193 *seen = local_seen;
194 return local.xyz;
195 }
196 var main_seen = 0.0;
197 let main = sample_probe_volume(position, n, point, anchor, probe_grid.min_spacing, probe_grid.dims, 0u, toward, &main_seen);
198 if (main.w <= 0.0) {
199 if (fade > 0.0) {
200 *seen = local_seen;
201 return local.xyz;
202 }
203 return probe_grid.fallback.xyz;
204 }
205 if (fade <= 0.0) {
206 *seen = main_seen;
207 return main.xyz;
208 }
209 *seen = mix(main_seen, local_seen, fade);
210 return mix(main.xyz, local.xyz, fade);
211}
212
213fn probe_reflection_occlusion(grid: vec3f, reference: vec3f) -> f32 {
214 let threshold = probe_grid.shade.x;
215 if (threshold <= 0.0 || grid.x < 0.0) { return 1.0; }
216 let open = dot(reference, vec3f(0.2126, 0.7152, 0.0722)) * threshold;
217 if (!(open > 0.000001)) { return 1.0; }
218 return clamp(dot(grid, vec3f(0.2126, 0.7152, 0.0722)) / open, 0.0, 1.0);
219}
220
221fn sample_live_irradiance(position: vec3f, normal: vec3f) -> vec3f {
222 if (!use_probes || probe_grid.blend.y < 0.5) { return probe_grid.fallback.xyz; }
223 let lower = sample_probe_anchor(position, normal, 0u);
224 if (probe_grid.blend.x <= 0.0) { return lower; }
225 let upper = sample_probe_anchor(position, normal, 1u);
226 return mix(lower, upper, probe_grid.blend.x);
227}
228
229fn sample_live_irradiance_toward(position: vec3f, normal: vec3f, toward: vec3f, seen: ptr<function, f32>) -> vec3f {
230 if (!use_probes || probe_grid.blend.y < 0.5) { return probe_grid.fallback.xyz; }
231 var lower_seen = -1.0;
232 let lower = sample_probe_anchor_toward(position, normal, toward, 0u, &lower_seen);
233 if (probe_grid.blend.x <= 0.0) {
234 *seen = lower_seen;
235 return lower;
236 }
237 var upper_seen = -1.0;
238 let upper = sample_probe_anchor_toward(position, normal, toward, 1u, &upper_seen);
239 if (lower_seen >= 0.0 && upper_seen >= 0.0) {
240 *seen = mix(lower_seen, upper_seen, probe_grid.blend.x);
241 }
242 return mix(lower, upper, probe_grid.blend.x);
243}
244"#;
245
246#[repr(C)]
247#[derive(Clone, Copy, Pod, Zeroable)]
248struct LocalUniform {
249 min_spacing: [f32; 4],
250 dims: [u32; 4],
251}
252
253#[repr(C)]
254#[derive(Clone, Copy, Pod, Zeroable)]
255struct GridUniform {
256 min_spacing: [f32; 4],
257 dims: [u32; 4],
258 fallback: [f32; 4],
259 blend: [f32; 4],
260 shade: [f32; 4],
261 locals: [LocalUniform; PROBE_LOCALS],
262}
263
264pub fn verified_artifact(path: &Path) -> Result<Artifact, String> {
265 read_artifact(path)
266}
267
268const BAND_ZERO: f32 = 0.282095;
269
270#[derive(Clone, Copy, Debug, Default, PartialEq)]
271pub enum Ambient {
272 #[default]
273 Sky,
274 Constant([f32; 3]),
275 Sh([[f32; 3]; 9]),
276}
277
278impl Ambient {
279 pub fn valid(&self) -> bool {
280 match self {
281 Self::Sky => true,
282 Self::Constant(irradiance) => irradiance.iter().all(|v| v.is_finite() && *v >= 0.0),
283 Self::Sh(sh) => sh.iter().flatten().all(|v| v.is_finite()),
284 }
285 }
286
287 pub fn sh(&self, sky: [[f32; 3]; 9]) -> [[f32; 3]; 9] {
288 match self {
289 Self::Sky => sky,
290 Self::Constant(irradiance) => {
291 let mut sh = [[0.0; 3]; 9];
292 sh[0] = irradiance.map(|v| v / BAND_ZERO);
293 sh
294 }
295 Self::Sh(sh) => *sh,
296 }
297 }
298}
299
300#[derive(Clone, Copy, Debug, PartialEq)]
301pub struct ContactDesc {
302 pub size: [u32; 2],
303 pub min: [f32; 2],
304 pub max: [f32; 2],
305 pub tint: [f32; 3],
306}
307
308impl ContactDesc {
309 fn valid(&self) -> bool {
310 self.size.iter().all(|&n| n > 0)
311 && self.min.iter().chain(&self.max).all(|v| v.is_finite())
312 && self.max[0] > self.min[0]
313 && self.max[1] > self.min[1]
314 && self.tint.iter().all(|v| v.is_finite() && *v >= 0.0)
315 }
316}
317
318#[repr(C)]
319#[derive(Clone, Copy, Pod, Zeroable)]
320pub(crate) struct ContactUniform {
321 rect: [f32; 4],
322 tint: [f32; 4],
323}
324
325impl ContactUniform {
326 pub(crate) fn off() -> Self {
327 Self::zeroed()
328 }
329
330 pub(crate) fn on(desc: &ContactDesc) -> Self {
331 Self {
332 rect: [
333 desc.min[0],
334 desc.min[1],
335 1.0 / (desc.max[0] - desc.min[0]),
336 1.0 / (desc.max[1] - desc.min[1]),
337 ],
338 tint: [desc.tint[0], desc.tint[1], desc.tint[2], 1.0],
339 }
340 }
341}
342
343pub struct ContactField {
344 desc: ContactDesc,
345 texture: wgpu::Texture,
346 view: wgpu::TextureView,
347}
348
349impl ContactField {
350 pub fn new(
351 device: &wgpu::Device,
352 queue: &wgpu::Queue,
353 desc: ContactDesc,
354 ) -> Result<Self, String> {
355 if !desc.valid() {
356 return Err("a contact field needs a nonzero size, a finite box with min below max and a finite nonnegative tint".into());
357 }
358 let texture = device.create_texture(&wgpu::TextureDescriptor {
359 label: Some("live contact field"),
360 size: wgpu::Extent3d {
361 width: desc.size[0],
362 height: desc.size[1],
363 depth_or_array_layers: 1,
364 },
365 mip_level_count: 1,
366 sample_count: 1,
367 dimension: wgpu::TextureDimension::D2,
368 format: wgpu::TextureFormat::R16Float,
369 usage: wgpu::TextureUsages::TEXTURE_BINDING
370 | wgpu::TextureUsages::COPY_DST
371 | wgpu::TextureUsages::RENDER_ATTACHMENT,
372 view_formats: &[],
373 });
374 let field = Self {
375 view: texture.create_view(&Default::default()),
376 texture,
377 desc,
378 };
379 field.write(queue, &vec![1.0; (desc.size[0] * desc.size[1]) as usize])?;
380 Ok(field)
381 }
382
383 pub fn desc(&self) -> &ContactDesc {
384 &self.desc
385 }
386
387 pub fn texture(&self) -> &wgpu::Texture {
388 &self.texture
389 }
390
391 pub fn view(&self) -> &wgpu::TextureView {
392 &self.view
393 }
394
395 pub fn write(&self, queue: &wgpu::Queue, values: &[f32]) -> Result<(), String> {
396 let [width, height] = self.desc.size;
397 if values.len() != (width * height) as usize {
398 return Err(format!(
399 "a {width}x{height} contact field takes {} values, not {}",
400 width * height,
401 values.len()
402 ));
403 }
404 let bytes: Vec<u8> = values
405 .iter()
406 .flat_map(|v| half::f16::from_f32(v.clamp(0.0, 1.0)).to_le_bytes())
407 .collect();
408 queue.write_texture(
409 self.texture.as_image_copy(),
410 &bytes,
411 wgpu::TexelCopyBufferLayout {
412 offset: 0,
413 bytes_per_row: Some(width * 2),
414 rows_per_image: Some(height),
415 },
416 wgpu::Extent3d {
417 width,
418 height,
419 depth_or_array_layers: 1,
420 },
421 );
422 Ok(())
423 }
424}
425
426pub struct ProbeLighting {
427 pub manifest: Option<Manifest>,
428 layout: wgpu::BindGroupLayout,
429 group: wgpu::BindGroup,
430 uniform: wgpu::Buffer,
431 buffers: Vec<wgpu::Buffer>,
432 selected: Option<AnchorBlend>,
433 data: GridUniform,
434 floor: Cell<f32>,
435 fading: Option<Fading>,
436 emission: f32,
437}
438
439struct Fading {
440 grids: Vec<Grid>,
441 emitters: Grid,
442 scales: Vec<f32>,
443}
444
445impl ProbeLighting {
446 pub fn fallback(device: &wgpu::Device, queue: &wgpu::Queue, fallback: [f32; 3]) -> Self {
447 let layout = Self::make_layout(device);
448 let uniform = device.create_buffer(&wgpu::BufferDescriptor {
449 label: Some("live probe uniform"),
450 size: std::mem::size_of::<GridUniform>() as u64,
451 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
452 mapped_at_creation: false,
453 });
454 let dummy = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
455 label: Some("live probe fallback"),
456 contents: &[0; 60],
457 usage: wgpu::BufferUsages::STORAGE,
458 });
459 let data = GridUniform {
460 min_spacing: [0.0, 0.0, 0.0, 1.0],
461 dims: [1, 1, 1, 0],
462 fallback: [fallback[0], fallback[1], fallback[2], 0.0],
463 blend: [0.0; 4],
464 shade: [0.0, 1.0, 0.0, 0.0],
465 locals: [LocalUniform::zeroed(); PROBE_LOCALS],
466 };
467 queue.write_buffer(&uniform, 0, bytemuck::bytes_of(&data));
468 let group = Self::make_group(device, &layout, &dummy, &dummy, &uniform);
469 Self {
470 manifest: None,
471 layout,
472 group,
473 uniform,
474 buffers: vec![dummy],
475 selected: None,
476 data,
477 floor: Cell::new(PROBE_CORNER_FLOOR),
478 fading: None,
479 emission: 0.0,
480 }
481 }
482
483 pub fn load(
484 device: &wgpu::Device,
485 queue: &wgpu::Queue,
486 path: &Path,
487 hour: f32,
488 fallback: [f32; 3],
489 ) -> Result<Self, String> {
490 Self::from_artifact(device, queue, &verified_artifact(path)?, hour, fallback)
491 }
492
493 pub fn from_artifact(
494 device: &wgpu::Device,
495 queue: &wgpu::Queue,
496 artifact: &Artifact,
497 hour: f32,
498 fallback: [f32; 3],
499 ) -> Result<Self, String> {
500 Self::from_artifacts(device, queue, artifact, &[], hour, fallback)
501 }
502
503 pub fn from_artifacts(
504 device: &wgpu::Device,
505 queue: &wgpu::Queue,
506 artifact: &Artifact,
507 locals: &[Artifact],
508 hour: f32,
509 fallback: [f32; 3],
510 ) -> Result<Self, String> {
511 check_locals(artifact, locals)?;
512 let mut lighting = Self::fallback(device, queue, fallback);
513 lighting.data.min_spacing = [
514 artifact.manifest.grid.min[0],
515 artifact.manifest.grid.min[1],
516 artifact.manifest.grid.min[2],
517 artifact.manifest.grid.spacing,
518 ];
519 lighting.data.dims = [
520 artifact.manifest.dimensions[0],
521 artifact.manifest.dimensions[1],
522 artifact.manifest.dimensions[2],
523 artifact.manifest.schema_version,
524 ];
525 let mut words = (artifact.grids[0].bytes().len() - 64) / 4;
526 for (slot, local) in lighting.data.locals.iter_mut().zip(locals) {
527 let grid = &local.manifest;
528 *slot = LocalUniform {
529 min_spacing: [
530 grid.grid.min[0],
531 grid.grid.min[1],
532 grid.grid.min[2],
533 grid.grid.spacing,
534 ],
535 dims: [
536 grid.dimensions[0],
537 grid.dimensions[1],
538 grid.dimensions[2],
539 words as u32,
540 ],
541 };
542 words += (local.grids[0].bytes().len() - 64) / 4;
543 }
544 lighting.data.blend[3] = locals.len() as f32;
545 lighting.buffers = artifact
546 .grids
547 .iter()
548 .enumerate()
549 .map(|(anchor, grid)| {
550 let mut bytes = grid.bytes().split_off(64);
551 for local in locals {
552 bytes.extend_from_slice(&local.grids[anchor].bytes()[64..]);
553 }
554 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
555 label: Some("live probe anchor"),
556 contents: &bytes,
557 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
558 })
559 })
560 .collect();
561 if let (Some(emitters), Some(entry)) = (&artifact.emitters, &artifact.manifest.emitters) {
562 lighting.fading = Some(Fading {
563 grids: artifact.grids.clone(),
564 emitters: emitters.clone(),
565 scales: entry.scales.clone(),
566 });
567 lighting.set_emission(queue, 0.0)?;
568 }
569 lighting.manifest = Some(artifact.manifest.clone());
570 lighting.set_hour(device, queue, hour)?;
571 Ok(lighting)
572 }
573
574 pub fn set_hour(
575 &mut self,
576 device: &wgpu::Device,
577 queue: &wgpu::Queue,
578 hour: f32,
579 ) -> Result<(), String> {
580 let Some(manifest) = &self.manifest else {
581 return Err("no probe artifact is loaded".into());
582 };
583 let selection = blend_anchors(&manifest.anchors, hour)?;
584 if self
585 .selected
586 .is_none_or(|old| old.lower != selection.lower || old.upper != selection.upper)
587 {
588 self.group = Self::make_group(
589 device,
590 &self.layout,
591 &self.buffers[selection.lower],
592 &self.buffers[selection.upper],
593 &self.uniform,
594 );
595 }
596 self.data.blend = [
597 selection.upper_weight,
598 1.0,
599 self.floor.get(),
600 self.data.blend[3],
601 ];
602 queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data));
603 self.selected = Some(selection);
604 Ok(())
605 }
606
607 pub fn set_emission(&mut self, queue: &wgpu::Queue, scale: f32) -> Result<(), String> {
608 if !scale.is_finite() || scale < 0.0 {
609 return Err("emission scale must be finite and nonnegative".into());
610 }
611 if let Some(fading) = &self.fading {
612 for ((grid, baked), buffer) in
613 fading.grids.iter().zip(&fading.scales).zip(&self.buffers)
614 {
615 let faded = with_emitters(grid, &fading.emitters, scale - baked)?;
616 queue.write_buffer(buffer, 0, &faded.bytes()[64..]);
617 }
618 }
619 self.emission = scale;
620 Ok(())
621 }
622
623 pub fn emission(&self) -> f32 {
624 self.emission
625 }
626
627 pub fn has_emitters(&self) -> bool {
628 self.fading.is_some()
629 }
630
631 pub fn set_fallback(&mut self, queue: &wgpu::Queue, fallback: [f32; 3]) {
632 self.data.fallback = [fallback[0], fallback[1], fallback[2], 0.0];
633 queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data));
634 }
635
636 pub fn set_corner_floor(&self, queue: &wgpu::Queue, floor: f32) -> Result<(), String> {
637 if !floor.is_finite() || !(0.0..=0.25).contains(&floor) {
638 return Err("the probe corner floor must be between 0 and 0.25".into());
639 }
640 self.floor.set(floor);
641 let blend = [
642 self.data.blend[0],
643 self.data.blend[1],
644 floor,
645 self.data.blend[3],
646 ];
647 queue.write_buffer(&self.uniform, 48, bytemuck::bytes_of(&blend));
648 Ok(())
649 }
650
651 pub fn set_reflection_occlusion(
652 &mut self,
653 queue: &wgpu::Queue,
654 threshold: f32,
655 ) -> Result<(), String> {
656 if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
657 return Err("the reflection occlusion threshold must be between 0 and 1".into());
658 }
659 self.data.shade[0] = threshold;
660 queue.write_buffer(&self.uniform, 64, bytemuck::bytes_of(&self.data.shade));
661 Ok(())
662 }
663
664 pub fn reflection_occlusion(&self) -> f32 {
665 self.data.shade[0]
666 }
667
668 pub fn set_sky_visibility(&mut self, queue: &wgpu::Queue, on: bool) {
669 self.data.shade[1] = if on { 1.0 } else { 0.0 };
670 queue.write_buffer(&self.uniform, 64, bytemuck::bytes_of(&self.data.shade));
671 }
672
673 pub fn sky_visibility(&self) -> bool {
674 self.data.shade[1] > 0.0
675 }
676
677 pub fn corner_floor(&self) -> f32 {
678 self.floor.get()
679 }
680
681 pub fn local_volumes(&self) -> usize {
682 self.data.blend[3] as usize
683 }
684
685 pub fn live(&self) -> bool {
686 let off = self.data.blend[1] < 0.5;
687 !off
688 }
689
690 pub fn selection(&self) -> Option<AnchorBlend> {
691 self.selected
692 }
693
694 pub fn layout(&self) -> &wgpu::BindGroupLayout {
695 &self.layout
696 }
697
698 pub fn group(&self) -> &wgpu::BindGroup {
699 &self.group
700 }
701
702 pub fn bindings(&self) -> [&wgpu::Buffer; 3] {
703 match self.selected {
704 Some(selection) => [
705 &self.buffers[selection.lower],
706 &self.buffers[selection.upper],
707 &self.uniform,
708 ],
709 None => [&self.buffers[0], &self.buffers[0], &self.uniform],
710 }
711 }
712
713 fn make_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
714 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
715 label: Some("live probe layout"),
716 entries: &[
717 wgpu::BindGroupLayoutEntry {
718 binding: 6,
719 visibility: wgpu::ShaderStages::FRAGMENT,
720 ty: wgpu::BindingType::Buffer {
721 ty: wgpu::BufferBindingType::Storage { read_only: true },
722 has_dynamic_offset: false,
723 min_binding_size: None,
724 },
725 count: None,
726 },
727 wgpu::BindGroupLayoutEntry {
728 binding: 7,
729 visibility: wgpu::ShaderStages::FRAGMENT,
730 ty: wgpu::BindingType::Buffer {
731 ty: wgpu::BufferBindingType::Storage { read_only: true },
732 has_dynamic_offset: false,
733 min_binding_size: None,
734 },
735 count: None,
736 },
737 wgpu::BindGroupLayoutEntry {
738 binding: 8,
739 visibility: wgpu::ShaderStages::FRAGMENT,
740 ty: wgpu::BindingType::Buffer {
741 ty: wgpu::BufferBindingType::Uniform,
742 has_dynamic_offset: false,
743 min_binding_size: None,
744 },
745 count: None,
746 },
747 ],
748 })
749 }
750
751 fn make_group(
752 device: &wgpu::Device,
753 layout: &wgpu::BindGroupLayout,
754 lower: &wgpu::Buffer,
755 upper: &wgpu::Buffer,
756 uniform: &wgpu::Buffer,
757 ) -> wgpu::BindGroup {
758 device.create_bind_group(&wgpu::BindGroupDescriptor {
759 label: Some("live probe binding"),
760 layout,
761 entries: &[
762 wgpu::BindGroupEntry {
763 binding: 6,
764 resource: lower.as_entire_binding(),
765 },
766 wgpu::BindGroupEntry {
767 binding: 7,
768 resource: upper.as_entire_binding(),
769 },
770 wgpu::BindGroupEntry {
771 binding: 8,
772 resource: uniform.as_entire_binding(),
773 },
774 ],
775 })
776 }
777}
778
779fn check_locals(artifact: &Artifact, locals: &[Artifact]) -> Result<(), String> {
780 if artifact.grids.is_empty() {
781 return Err("a probe artifact needs at least one anchor".into());
782 }
783 if locals.is_empty() {
784 return Ok(());
785 }
786 if locals.len() > PROBE_LOCALS {
787 return Err(format!(
788 "at most {PROBE_LOCALS} local probe volumes, not {}",
789 locals.len()
790 ));
791 }
792 if artifact.emitters.is_some() || locals.iter().any(|local| local.emitters.is_some()) {
793 return Err("local probe volumes take no emitter layer".into());
794 }
795 for local in locals {
796 if local.manifest.schema_version != 2 {
797 return Err("a local probe volume needs schema 2, with depth moments".into());
798 }
799 if local.manifest.anchors != artifact.manifest.anchors
800 || local.grids.len() != artifact.grids.len()
801 {
802 return Err("a local probe volume needs the main volume's anchors".into());
803 }
804 if local.manifest.dimensions.iter().any(|&count| count < 3) {
805 return Err("a local probe volume needs at least three probes on every axis".into());
806 }
807 }
808 Ok(())
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use half::f16;
815 use pfx_bake::{Anchor, BakeScene, FileEntry, Grid, GridSpec, Probe, bake, write_artifact};
816 use pfx_gpu::Gpu;
817 use pfx_load::Sky;
818 use pfx_materials::Material;
819 use pfx_trace::bvh::Triangle;
820 use pfx_trace::{Camera, Scene, Sun, Trace};
821 use sha2::{Digest, Sha256};
822 use std::fs;
823
824 fn test_root(name: &str) -> std::path::PathBuf {
825 Path::new(env!("CARGO_MANIFEST_DIR"))
826 .join("../../tmp")
827 .join(name)
828 }
829
830 fn digest(bytes: &[u8]) -> String {
831 Sha256::digest(bytes)
832 .iter()
833 .map(|byte| format!("{byte:02x}"))
834 .collect()
835 }
836
837 #[test]
838 fn loader_rejects_bad_hash_and_version() {
839 let path = test_root("live-probe-loader-test");
840 if path.exists() {
841 fs::remove_dir_all(&path).unwrap();
842 }
843 fs::create_dir_all(&path).unwrap();
844 let spec = GridSpec {
845 min: [0.0; 3],
846 max: [0.0; 3],
847 spacing: 1.0,
848 };
849 let grid = Grid::new(spec, vec![Probe::default()]).unwrap();
850 let bytes = grid.bytes();
851 fs::write(path.join("probe-000.bin"), &bytes).unwrap();
852 let mut manifest = Manifest {
853 schema_version: 1,
854 scene_hash: "test".into(),
855 anchors: vec![12.0],
856 sample_count: 1,
857 seed: 0,
858 grid: spec,
859 dimensions: [1; 3],
860 format: "ambient_cube_rgb16f_visibility_f16_le_v1".into(),
861 color_space: "linear_rec709_irradiance".into(),
862 coordinates: "right_handed_y_up_xyz_x_fastest".into(),
863 files: vec![FileEntry {
864 path: "probe-000.bin".into(),
865 bytes: bytes.len() as u64,
866 sha256: digest(&bytes),
867 }],
868 emitters: None,
869 };
870 let save = |manifest: &Manifest| {
871 let json = serde_json::to_vec(manifest).unwrap();
872 fs::write(path.join("manifest.json"), &json).unwrap();
873 fs::write(path.join("manifest.sha256"), digest(&json)).unwrap();
874 };
875 save(&manifest);
876 assert!(verified_artifact(&path).is_ok());
877 let mut corrupt = bytes.clone();
878 corrupt[64] ^= 1;
879 fs::write(path.join("probe-000.bin"), &corrupt).unwrap();
880 assert!(verified_artifact(&path).err().unwrap().contains("checksum"));
881 fs::write(path.join("probe-000.bin"), &bytes).unwrap();
882 manifest.schema_version = 2;
883 save(&manifest);
884 assert!(verified_artifact(&path).err().unwrap().contains("manifest"));
885 fs::remove_dir_all(path).unwrap();
886 }
887
888 fn artifact(spacing: f32, count: u32, anchors: &[f32], version_two: bool) -> Artifact {
889 let spec = GridSpec {
890 min: [0.0; 3],
891 max: [spacing * (count - 1) as f32; 3],
892 spacing,
893 };
894 let probes = vec![Probe::default(); (count * count * count) as usize];
895 let grid = if version_two {
896 let extra = pfx_bake::ProbeExtra {
897 second: [0.0; 6],
898 offset: [0.0; 3],
899 enabled: true,
900 backfaces: 0,
901 };
902 Grid::with_extras(spec, probes.clone(), vec![extra; probes.len()]).unwrap()
903 } else {
904 Grid::new(spec, probes).unwrap()
905 };
906 Artifact {
907 manifest: Manifest {
908 schema_version: grid.version(),
909 scene_hash: "test".into(),
910 anchors: anchors.to_vec(),
911 sample_count: 1,
912 seed: 0,
913 grid: spec,
914 dimensions: [count; 3],
915 format: String::new(),
916 color_space: String::new(),
917 coordinates: String::new(),
918 files: Vec::new(),
919 emitters: None,
920 },
921 grids: vec![grid; anchors.len()],
922 emitters: None,
923 rounds: None,
924 }
925 }
926
927 #[test]
928 fn local_volumes_need_the_main_anchors_and_depth_moments() {
929 let main = artifact(0.05, 4, &[12.0, 16.0], true);
930 let fine = || artifact(0.01, 3, &[12.0, 16.0], true);
931 let local = fine();
932 assert!(check_locals(&main, &[]).is_ok());
933 assert!(check_locals(&main, std::slice::from_ref(&local)).is_ok());
934 assert!(
935 check_locals(
936 &main,
937 &(0..PROBE_LOCALS).map(|_| fine()).collect::<Vec<_>>()
938 )
939 .is_ok()
940 );
941 assert!(
942 check_locals(
943 &main,
944 &(0..=PROBE_LOCALS).map(|_| fine()).collect::<Vec<_>>()
945 )
946 .unwrap_err()
947 .contains("at most")
948 );
949 let other_hours = artifact(0.01, 3, &[12.0, 19.0], true);
950 assert!(
951 check_locals(&main, &[other_hours])
952 .unwrap_err()
953 .contains("anchors")
954 );
955 let flat = artifact(0.01, 3, &[12.0, 16.0], false);
956 assert!(
957 check_locals(&main, &[flat])
958 .unwrap_err()
959 .contains("schema 2")
960 );
961 let thin = artifact(0.01, 2, &[12.0, 16.0], true);
962 assert!(
963 check_locals(&main, &[thin])
964 .unwrap_err()
965 .contains("three probes")
966 );
967 }
968
969 #[test]
970 fn anchor_selection_tracks_live_hour() {
971 let hours = [8.0, 12.0, 16.0];
972 assert_eq!(blend_anchors(&hours, 10.0).unwrap().upper_weight, 0.5);
973 assert_eq!(blend_anchors(&hours, 14.0).unwrap().lower, 1);
974 assert_eq!(blend_anchors(&hours, 17.0).unwrap().upper, 2);
975 assert!(blend_anchors(&hours, f32::NAN).is_err());
976 }
977
978 #[test]
979 fn probe_shader_validates() {
980 let source = format!(
981 "{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); }}"
982 );
983 let module = naga::front::wgsl::parse_str(&source).unwrap();
984 naga::valid::Validator::new(
985 naga::valid::ValidationFlags::all(),
986 naga::valid::Capabilities::all(),
987 )
988 .validate(&module)
989 .unwrap();
990 }
991
992 fn quad(
993 triangles: &mut Vec<Triangle>,
994 a: [f32; 3],
995 b: [f32; 3],
996 c: [f32; 3],
997 d: [f32; 3],
998 material: u32,
999 ) {
1000 triangles.push(Triangle {
1001 vertices: [a, b, c],
1002 material,
1003 });
1004 triangles.push(Triangle {
1005 vertices: [a, c, d],
1006 material,
1007 });
1008 }
1009
1010 #[test]
1011 #[ignore = "needs a GPU; run under pgpu"]
1012 fn sunlit_floor_agrees_with_path_trace() {
1013 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1014 let mut triangles = Vec::new();
1015 quad(
1016 &mut triangles,
1017 [-1.0, 0.0, -1.0],
1018 [1.0, 0.0, -1.0],
1019 [1.0, 0.0, 1.0],
1020 [-1.0, 0.0, 1.0],
1021 0,
1022 );
1023 quad(
1024 &mut triangles,
1025 [-1.0, 0.0, -1.0],
1026 [-1.0, 1.5, -1.0],
1027 [1.0, 1.5, -1.0],
1028 [1.0, 0.0, -1.0],
1029 1,
1030 );
1031 quad(
1032 &mut triangles,
1033 [1.0, 0.0, -1.0],
1034 [1.0, 1.5, -1.0],
1035 [1.0, 1.5, 1.0],
1036 [1.0, 0.0, 1.0],
1037 1,
1038 );
1039 quad(
1040 &mut triangles,
1041 [1.0, 0.0, 1.0],
1042 [1.0, 1.5, 1.0],
1043 [-1.0, 1.5, 1.0],
1044 [-1.0, 0.0, 1.0],
1045 1,
1046 );
1047 quad(
1048 &mut triangles,
1049 [-1.0, 0.0, 1.0],
1050 [-1.0, 1.5, 1.0],
1051 [-1.0, 1.5, -1.0],
1052 [-1.0, 0.0, -1.0],
1053 1,
1054 );
1055 let material = Material {
1056 base: [0.8; 3],
1057 specular: 0.0,
1058 roughness: 1.0,
1059 ..Material::default()
1060 };
1061 let wall = Material {
1062 base: [0.2; 3],
1063 specular: 0.0,
1064 roughness: 1.0,
1065 ..Material::default()
1066 };
1067 let anchor = Anchor {
1068 hour: 12.0,
1069 sky: Sky {
1070 width: 1,
1071 height: 1,
1072 texels: vec![[0.5, 0.5, 0.5, 1.0]],
1073 },
1074 sun: Sun {
1075 direction: [0.0, 1.0, 0.0],
1076 color: [1.0; 3],
1077 intensity: 0.1,
1078 },
1079 };
1080 let scene = BakeScene {
1081 triangles: triangles.clone(),
1082 shapes: Vec::new(),
1083 materials: vec![material, wall],
1084 anchors: vec![anchor.clone()],
1085 };
1086 let spec = GridSpec {
1087 min: [0.0, 0.01, 0.0],
1088 max: [0.0, 0.01, 0.0],
1089 spacing: 1.0,
1090 };
1091 let grids = bake(&gpu, &scene, spec, 16, 73).unwrap();
1092 let root = test_root("live-probe-gpu-test");
1093 let path = root.join("artifact");
1094 if root.exists() {
1095 fs::remove_dir_all(&root).unwrap();
1096 }
1097 fs::create_dir_all(&root).unwrap();
1098 write_artifact(
1099 root.parent().unwrap(),
1100 Path::new("live-probe-gpu-test/artifact"),
1101 &scene,
1102 &grids,
1103 16,
1104 73,
1105 )
1106 .unwrap();
1107 let lighting = ProbeLighting::load(&gpu.device, &gpu.queue, &path, 12.0, [0.0; 3]).unwrap();
1108 let empty = gpu
1109 .device
1110 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1111 label: None,
1112 entries: &[],
1113 });
1114 let empty_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1115 label: None,
1116 layout: &empty,
1117 entries: &[],
1118 });
1119 let layout = gpu
1120 .device
1121 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1122 label: None,
1123 bind_group_layouts: &[
1124 Some(&empty),
1125 Some(&empty),
1126 Some(&empty),
1127 Some(lighting.layout()),
1128 ],
1129 immediate_size: 0,
1130 });
1131 let source = format!(
1132 "{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 {{ let indirect = sample_live_irradiance(vec3f(0.0, 0.01, 0.0), vec3f(0.0, 1.0, 0.0)) * 0.8 / 3.14159265; return vec4f(indirect + vec3f(0.1 * 0.8 / 3.14159265), 1.0); }}"
1133 );
1134 let shader = gpu
1135 .device
1136 .create_shader_module(wgpu::ShaderModuleDescriptor {
1137 label: None,
1138 source: wgpu::ShaderSource::Wgsl(source.into()),
1139 });
1140 let pipeline = gpu
1141 .device
1142 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1143 label: None,
1144 layout: Some(&layout),
1145 vertex: wgpu::VertexState {
1146 module: &shader,
1147 entry_point: Some("vs"),
1148 compilation_options: Default::default(),
1149 buffers: &[],
1150 },
1151 primitive: wgpu::PrimitiveState::default(),
1152 depth_stencil: None,
1153 multisample: wgpu::MultisampleState::default(),
1154 fragment: Some(wgpu::FragmentState {
1155 module: &shader,
1156 entry_point: Some("fs"),
1157 compilation_options: Default::default(),
1158 targets: &[Some(wgpu::ColorTargetState {
1159 format: wgpu::TextureFormat::Rgba16Float,
1160 blend: None,
1161 write_mask: wgpu::ColorWrites::ALL,
1162 })],
1163 }),
1164 multiview_mask: None,
1165 cache: None,
1166 });
1167 let target = gpu
1168 .offscreen(1, 1, wgpu::TextureFormat::Rgba16Float)
1169 .unwrap();
1170 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1171 {
1172 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1173 multiview_mask: None,
1174 label: None,
1175 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1176 view: &target.view,
1177 resolve_target: None,
1178 ops: wgpu::Operations {
1179 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1180 store: wgpu::StoreOp::Store,
1181 },
1182 depth_slice: None,
1183 })],
1184 depth_stencil_attachment: None,
1185 timestamp_writes: None,
1186 occlusion_query_set: None,
1187 });
1188 pass.set_pipeline(&pipeline);
1189 pass.set_bind_group(0, &empty_group, &[]);
1190 pass.set_bind_group(1, &empty_group, &[]);
1191 pass.set_bind_group(2, &empty_group, &[]);
1192 pass.set_bind_group(3, lighting.group(), &[]);
1193 pass.draw(0..3, 0..1);
1194 }
1195 gpu.queue.submit(Some(encoder.finish()));
1196 let live = f16::from_bits(gpu.readback_rgba16(&target).unwrap()[0]).to_f32();
1197 let reference_scene = Scene {
1198 triangles,
1199 shapes: Vec::new(),
1200 materials: vec![material, scene.materials[1]],
1201 sky: anchor.sky,
1202 camera: Camera {
1203 origin: [0.0, 1.0, 0.0],
1204 forward: [0.0, -1.0, 0.0],
1205 right: [1.0, 0.0, 0.0],
1206 up: [0.0, 0.0, 1.0],
1207 },
1208 sun: anchor.sun,
1209 };
1210 let mut trace = Trace::new(&gpu, &reference_scene, 1, 1).unwrap();
1211 trace.sample(&gpu, 4096, 74).unwrap();
1212 let bytes = trace.readback(&gpu).unwrap().color;
1213 let reference = f32::from_le_bytes(bytes[..4].try_into().unwrap());
1214 let error = (live - reference).abs() / reference.max(0.01);
1215 println!("sunlit floor live={live:.4} path={reference:.4} relative_error={error:.3}");
1216 assert!(
1217 error < 0.3,
1218 "live={live} reference={reference} error={error}"
1219 );
1220 fs::remove_dir_all(root).unwrap();
1221 }
1222
1223 fn irradiance_pixel(gpu: &Gpu, lighting: &ProbeLighting) -> Vec<u16> {
1224 irradiance_at(gpu, lighting, [0.0, 0.01, 0.0], [0.0, 1.0, 0.0])
1225 }
1226
1227 fn irradiance_at(
1228 gpu: &Gpu,
1229 lighting: &ProbeLighting,
1230 position: [f32; 3],
1231 normal: [f32; 3],
1232 ) -> Vec<u16> {
1233 let empty = gpu
1234 .device
1235 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1236 label: None,
1237 entries: &[],
1238 });
1239 let empty_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1240 label: None,
1241 layout: &empty,
1242 entries: &[],
1243 });
1244 let layout = gpu
1245 .device
1246 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1247 label: None,
1248 bind_group_layouts: &[
1249 Some(&empty),
1250 Some(&empty),
1251 Some(&empty),
1252 Some(lighting.layout()),
1253 ],
1254 immediate_size: 0,
1255 });
1256 let source = format!(
1257 "{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({:?}, {:?}, {:?}), vec3f({:?}, {:?}, {:?})), 1.0); }}",
1258 position[0], position[1], position[2], normal[0], normal[1], normal[2]
1259 );
1260 let shader = gpu
1261 .device
1262 .create_shader_module(wgpu::ShaderModuleDescriptor {
1263 label: None,
1264 source: wgpu::ShaderSource::Wgsl(source.into()),
1265 });
1266 let pipeline = gpu
1267 .device
1268 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1269 label: None,
1270 layout: Some(&layout),
1271 vertex: wgpu::VertexState {
1272 module: &shader,
1273 entry_point: Some("vs"),
1274 compilation_options: Default::default(),
1275 buffers: &[],
1276 },
1277 primitive: wgpu::PrimitiveState::default(),
1278 depth_stencil: None,
1279 multisample: wgpu::MultisampleState::default(),
1280 fragment: Some(wgpu::FragmentState {
1281 module: &shader,
1282 entry_point: Some("fs"),
1283 compilation_options: Default::default(),
1284 targets: &[Some(wgpu::ColorTargetState {
1285 format: wgpu::TextureFormat::Rgba16Float,
1286 blend: None,
1287 write_mask: wgpu::ColorWrites::ALL,
1288 })],
1289 }),
1290 multiview_mask: None,
1291 cache: None,
1292 });
1293 let target = gpu
1294 .offscreen(1, 1, wgpu::TextureFormat::Rgba16Float)
1295 .unwrap();
1296 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1297 {
1298 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1299 multiview_mask: None,
1300 label: None,
1301 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1302 view: &target.view,
1303 resolve_target: None,
1304 ops: wgpu::Operations {
1305 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1306 store: wgpu::StoreOp::Store,
1307 },
1308 depth_slice: None,
1309 })],
1310 depth_stencil_attachment: None,
1311 timestamp_writes: None,
1312 occlusion_query_set: None,
1313 });
1314 pass.set_pipeline(&pipeline);
1315 pass.set_bind_group(0, &empty_group, &[]);
1316 pass.set_bind_group(1, &empty_group, &[]);
1317 pass.set_bind_group(2, &empty_group, &[]);
1318 pass.set_bind_group(3, lighting.group(), &[]);
1319 pass.draw(0..3, 0..1);
1320 }
1321 gpu.queue.submit(Some(encoder.finish()));
1322 gpu.readback_rgba16(&target).unwrap()
1323 }
1324
1325 #[test]
1326 #[ignore = "needs a GPU; run under pgpu"]
1327 fn from_artifact_lights_a_scene_as_load_does() {
1328 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1329 let anchors: Vec<Anchor> = [12.0, 18.0]
1330 .into_iter()
1331 .map(|hour| Anchor {
1332 hour,
1333 sky: Sky {
1334 width: 1,
1335 height: 1,
1336 texels: vec![[0.5, 0.5, 0.5, 1.0]],
1337 },
1338 sun: Sun {
1339 direction: [0.0, 1.0, 0.0],
1340 color: [1.0; 3],
1341 intensity: 0.1,
1342 },
1343 })
1344 .collect();
1345 let scene = BakeScene {
1346 triangles: Vec::new(),
1347 shapes: Vec::new(),
1348 materials: vec![Material::default()],
1349 anchors,
1350 };
1351 let spec = GridSpec {
1352 min: [0.0, 0.01, 0.0],
1353 max: [0.0, 0.01, 0.0],
1354 spacing: 1.0,
1355 };
1356 let grid = |level: f32| {
1357 let mut probe = Probe::default();
1358 for (index, lobe) in probe.lobes.iter_mut().enumerate() {
1359 *lobe = [level * (index + 1) as f32, level, level * 0.5];
1360 }
1361 Grid::new(spec, vec![probe]).unwrap()
1362 };
1363 let grids = [grid(0.25), grid(0.75)];
1364 let emitters = pfx_bake::Emitters {
1365 grid: grid(0.125),
1366 scales: vec![0.0, 0.0],
1367 direct: false,
1368 };
1369 let root = test_root("live-probe-bytes-test");
1370 if root.exists() {
1371 fs::remove_dir_all(&root).unwrap();
1372 }
1373 fs::create_dir_all(&root).unwrap();
1374 pfx_bake::write_artifact_with_emitters(
1375 root.parent().unwrap(),
1376 Path::new("live-probe-bytes-test/artifact"),
1377 &scene,
1378 &grids,
1379 &emitters,
1380 4,
1381 3,
1382 )
1383 .unwrap();
1384 let path = root.join("artifact");
1385 let files: std::collections::HashMap<String, Vec<u8>> = fs::read_dir(&path)
1386 .unwrap()
1387 .map(|entry| {
1388 let entry = entry.unwrap();
1389 (
1390 entry.file_name().to_string_lossy().into_owned(),
1391 fs::read(entry.path()).unwrap(),
1392 )
1393 })
1394 .collect();
1395 let artifact = pfx_bake::read_artifact_from(&|name| files.get(name).cloned()).unwrap();
1396 let mut loaded =
1397 ProbeLighting::load(&gpu.device, &gpu.queue, &path, 15.0, [0.0; 3]).unwrap();
1398 let mut built =
1399 ProbeLighting::from_artifact(&gpu.device, &gpu.queue, &artifact, 15.0, [0.0; 3])
1400 .unwrap();
1401 let base = irradiance_pixel(&gpu, &loaded);
1402 assert!(f16::from_bits(base[0]).to_f32() > 0.0);
1403 assert_eq!(irradiance_pixel(&gpu, &built), base);
1404 for scale in [0.5, 1.0] {
1405 loaded.set_emission(&gpu.queue, scale).unwrap();
1406 built.set_emission(&gpu.queue, scale).unwrap();
1407 assert_eq!(
1408 irradiance_pixel(&gpu, &built),
1409 irradiance_pixel(&gpu, &loaded)
1410 );
1411 }
1412 assert_ne!(irradiance_pixel(&gpu, &built), base);
1413 fs::remove_dir_all(root).unwrap();
1414 }
1415
1416 fn box_with_a_slot() -> Vec<Triangle> {
1417 let mut triangles = Vec::new();
1418 quad(
1419 &mut triangles,
1420 [-1.0, 0.0, -1.0],
1421 [-1.0, 0.0, 1.0],
1422 [1.0, 0.0, 1.0],
1423 [1.0, 0.0, -1.0],
1424 0,
1425 );
1426 let (low, high, top) = (0.005, 0.045, 0.03);
1427 for (a, b) in [
1428 ([low, low], [high, low]),
1429 ([high, low], [high, high]),
1430 ([high, high], [low, high]),
1431 ([low, high], [low, low]),
1432 ] {
1433 quad(
1434 &mut triangles,
1435 [a[0], 0.0, a[1]],
1436 [b[0], 0.0, b[1]],
1437 [b[0], top, b[1]],
1438 [a[0], top, a[1]],
1439 0,
1440 );
1441 }
1442 quad(
1443 &mut triangles,
1444 [low, top, low],
1445 [low, top, high],
1446 [0.035, top, high],
1447 [0.035, top, low],
1448 0,
1449 );
1450 triangles
1451 }
1452
1453 fn baked_artifact(
1454 gpu: &Gpu,
1455 scene: &BakeScene,
1456 spec: GridSpec,
1457 root: &Path,
1458 name: &str,
1459 ) -> Artifact {
1460 let grids = bake(gpu, scene, spec, 32, 11).unwrap();
1461 write_artifact(
1462 root.parent().unwrap(),
1463 &Path::new(root.file_name().unwrap()).join(name),
1464 scene,
1465 &grids,
1466 32,
1467 11,
1468 )
1469 .unwrap();
1470 verified_artifact(&root.join(name)).unwrap()
1471 }
1472
1473 fn rgb(pixel: &[u16]) -> [f32; 3] {
1474 std::array::from_fn(|channel| f16::from_bits(pixel[channel]).to_f32())
1475 }
1476
1477 fn luma(rgb: [f32; 3]) -> f32 {
1478 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
1479 }
1480
1481 #[test]
1482 #[ignore = "needs a GPU; run under pgpu"]
1483 fn a_local_volume_keeps_the_light_out_of_a_slotted_box() {
1484 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1485 let white = Material {
1486 base: [0.8; 3],
1487 specular: 0.0,
1488 roughness: 1.0,
1489 ..Material::default()
1490 };
1491 let scene = BakeScene {
1492 triangles: box_with_a_slot(),
1493 shapes: Vec::new(),
1494 materials: vec![white],
1495 anchors: vec![Anchor {
1496 hour: 12.0,
1497 sky: Sky {
1498 width: 1,
1499 height: 1,
1500 texels: vec![[1.0, 1.0, 1.0, 1.0]],
1501 },
1502 sun: Sun {
1503 direction: [0.0, 1.0, 0.0],
1504 color: [1.0; 3],
1505 intensity: 0.0,
1506 },
1507 }],
1508 };
1509 let root = test_root("live-probe-slot-test");
1510 if root.exists() {
1511 fs::remove_dir_all(&root).unwrap();
1512 }
1513 fs::create_dir_all(&root).unwrap();
1514 let main = baked_artifact(
1515 &gpu,
1516 &scene,
1517 GridSpec {
1518 min: [-0.15, -0.025, -0.15],
1519 max: [0.15, 0.125, 0.15],
1520 spacing: 0.05,
1521 },
1522 &root,
1523 "main",
1524 );
1525 let local = baked_artifact(
1526 &gpu,
1527 &scene,
1528 GridSpec {
1529 min: [-0.01, -0.015, -0.01],
1530 max: [0.06, 0.045, 0.06],
1531 spacing: 0.01,
1532 },
1533 &root,
1534 "local",
1535 );
1536 let inside = [0.015, 0.0, 0.025];
1537 let up = [0.0, 1.0, 0.0];
1538 let traced = bake(
1539 &gpu,
1540 &scene,
1541 GridSpec {
1542 min: [inside[0], 0.002, inside[2]],
1543 max: [inside[0], 0.002, inside[2]],
1544 spacing: 0.01,
1545 },
1546 64,
1547 12,
1548 )
1549 .unwrap()[0]
1550 .probes[0]
1551 .lobes[2];
1552 let coarse =
1553 ProbeLighting::from_artifact(&gpu.device, &gpu.queue, &main, 12.0, [0.0; 3]).unwrap();
1554 let fine = ProbeLighting::from_artifacts(
1555 &gpu.device,
1556 &gpu.queue,
1557 &main,
1558 std::slice::from_ref(&local),
1559 12.0,
1560 [0.0; 3],
1561 )
1562 .unwrap();
1563 assert_eq!(fine.local_volumes(), 1);
1564 let leaked = luma(rgb(&irradiance_at(&gpu, &coarse, inside, up))) / luma(traced);
1565 let kept = luma(rgb(&irradiance_at(&gpu, &fine, inside, up))) / luma(traced);
1566 println!(
1567 "slotted box interior: traced {:.4}, main grid {leaked:.2}x, with the local volume {kept:.2}x",
1568 luma(traced)
1569 );
1570 assert!(leaked > 1.5, "the 5 cm grid alone reads {leaked}x");
1571 assert!(
1572 (1.0 / 1.5..1.5).contains(&kept),
1573 "the local volume reads {kept}x"
1574 );
1575 for (position, normal) in [
1576 ([-0.1, 0.0, -0.1], up),
1577 ([0.12, 0.05, 0.0], [-1.0, 0.0, 0.0]),
1578 ([0.0, 0.0, 0.1], up),
1579 ] {
1580 assert_eq!(
1581 irradiance_at(&gpu, &fine, position, normal),
1582 irradiance_at(&gpu, &coarse, position, normal),
1583 "{position:?} is outside the local volume"
1584 );
1585 }
1586 fs::remove_dir_all(root).unwrap();
1587 }
1588}