1use bytemuck::{Pod, Zeroable};
2#[cfg(test)]
3use pfx_core::daylight::Daylight;
4use pfx_core::daylight::{self, Probe};
5pub use pfx_core::sky::AnalyticSky;
6use pfx_core::sky::SKY_MODEL_WGSL;
7use pfx_load::Sky as HdrSky;
8use std::f32::consts::{PI, TAU};
9use std::sync::{Arc, Condvar, LazyLock, Mutex, MutexGuard};
10use std::thread::JoinHandle;
11
12const SH_SAMPLES: usize = 4096;
13
14#[derive(Clone, Debug)]
15pub enum SkySource {
16 Analytic(AnalyticSky),
17 Hdr(HdrSky),
18}
19
20impl SkySource {
21 pub fn from_probes(hour: f64, skies: &[HdrSky], probes: &[Probe]) -> Result<Self, String> {
22 let first = skies.first().ok_or("sky probes need at least one HDR")?;
23 if skies.iter().any(|s| {
24 s.width != first.width
25 || s.height != first.height
26 || s.texels.len() != first.texels.len()
27 }) {
28 return Err("sky probes need matching HDR dimensions".into());
29 }
30 let weights = daylight::probe_weights(hour, probes, skies.len());
31 if weights.iter().sum::<f64>() <= 0.0 {
32 return Err("sky probes have no valid index".into());
33 }
34 let mut texels = vec![[0.0; 4]; first.texels.len()];
35 for (sky, weight) in skies.iter().zip(weights) {
36 for (out, input) in texels.iter_mut().zip(&sky.texels) {
37 for (out_channel, input_channel) in out[..3].iter_mut().zip(&input[..3]) {
38 *out_channel += *input_channel * weight as f32;
39 }
40 }
41 }
42 for pixel in &mut texels {
43 pixel[3] = 1.0;
44 }
45 Ok(Self::Hdr(HdrSky {
46 width: first.width,
47 height: first.height,
48 texels,
49 }))
50 }
51
52 pub fn radiance(&self, direction: [f32; 3]) -> [f32; 3] {
53 match self {
54 Self::Analytic(model) => model.radiance(direction),
55 Self::Hdr(hdr) => {
56 let d = unit(direction);
57 let u = (d[0].atan2(-d[2]) / TAU + 0.5).rem_euclid(1.0);
58 let v = d[1].clamp(-1.0, 1.0).acos() / PI;
59 let x = u * hdr.width as f32 - 0.5;
60 let y = v * hdr.height as f32 - 0.5;
61 let x0 = x.floor();
62 let y0 = y.floor();
63 let tx = x - x0;
64 let ty = y - y0;
65 let at = |ix: f32, iy: f32| {
66 let col = (ix as i32).rem_euclid(hdr.width as i32) as usize;
67 let row = (iy as i32).clamp(0, hdr.height as i32 - 1) as usize;
68 hdr.texels[row * hdr.width as usize + col]
69 };
70 let a = at(x0, y0);
71 let b = at(x0 + 1.0, y0);
72 let c = at(x0, y0 + 1.0);
73 let d = at(x0 + 1.0, y0 + 1.0);
74 [0, 1, 2].map(|i| {
75 (a[i] * (1.0 - tx) + b[i] * tx) * (1.0 - ty)
76 + (c[i] * (1.0 - tx) + d[i] * tx) * ty
77 })
78 }
79 }
80 }
81
82 fn diffuse(&self, direction: [f32; 3]) -> [f32; 3] {
83 match self {
84 Self::Analytic(model) => model.diffuse(direction),
85 Self::Hdr(_) => self.radiance(direction),
86 }
87 }
88}
89
90#[derive(Clone, Debug)]
91pub struct SkyLighting {
92 pub sh: [[f32; 3]; 9],
93 pub cube: Vec<Vec<[f32; 4]>>,
94 pub edge: u32,
95 pub mip_count: u32,
96 source: SkySource,
97 sh_next: usize,
98 cube_next: u32,
99}
100
101impl SkyLighting {
102 pub fn new(source: SkySource, edge: u32) -> Self {
103 let edge = edge.max(1).next_power_of_two();
104 let mip_count = edge.ilog2() + 1;
105 let cube = (0..mip_count)
106 .map(|mip| vec![[0.0; 4]; (6 * (edge >> mip).pow(2)) as usize])
107 .collect();
108 Self {
109 sh: [[0.0; 3]; 9],
110 cube,
111 edge,
112 mip_count,
113 source,
114 sh_next: 0,
115 cube_next: 0,
116 }
117 }
118
119 pub fn set_source(&mut self, source: SkySource) {
120 self.source = source;
121 self.sh = [[0.0; 3]; 9];
122 self.sh_next = 0;
123 self.cube_next = 0;
124 }
125
126 pub fn source(&self) -> &SkySource {
127 &self.source
128 }
129
130 pub fn step(&mut self) -> Option<(u32, u32)> {
131 if self.sh_next < SH_SAMPLES {
132 self.sh_chunk();
133 }
134 if self.cube_next >= self.mip_count * 6 {
135 return None;
136 }
137 let mip = self.cube_next / 6;
138 let face = self.cube_next % 6;
139 let texels = self.face(mip, face);
140 let size = self.edge >> mip;
141 let at = (face * size * size) as usize;
142 self.cube[mip as usize][at..at + texels.len()].copy_from_slice(&texels);
143 self.cube_next += 1;
144 Some((mip, face))
145 }
146
147 pub fn settle(&mut self) {
148 while self.sh_next < SH_SAMPLES {
149 self.sh_chunk();
150 }
151 let first = self.cube_next;
152 let mip_count = self.mip_count;
153 let edge = self.edge;
154 let lighting = &*self;
155 let faces: Vec<Vec<(u32, Vec<[f32; 4]>)>> = std::thread::scope(|scope| {
156 let workers: Vec<_> = (0..6)
157 .map(|face| {
158 scope.spawn(move || {
159 (0..mip_count)
160 .filter(|mip| mip * 6 + face >= first)
161 .map(|mip| (mip, lighting.face(mip, face)))
162 .collect::<Vec<_>>()
163 })
164 })
165 .collect();
166 workers
167 .into_iter()
168 .map(|worker| worker.join().expect("sky face worker"))
169 .collect()
170 });
171 for (face, mips) in faces.into_iter().enumerate() {
172 for (mip, texels) in mips {
173 let size = edge >> mip;
174 let at = face * (size * size) as usize;
175 self.cube[mip as usize][at..at + texels.len()].copy_from_slice(&texels);
176 }
177 }
178 self.cube_next = self.mip_count * 6;
179 }
180
181 fn sh_chunk(&mut self) {
182 for index in self.sh_next..(self.sh_next + 256).min(SH_SAMPLES) {
183 let z = 1.0 - 2.0 * (index as f32 + 0.5) / SH_SAMPLES as f32;
184 let angle = index as f32 * 2.399_963_1;
185 let radius = (1.0 - z * z).sqrt();
186 let d = [radius * angle.cos(), z, radius * angle.sin()];
187 let color = self.source.diffuse(d);
188 let basis = sh_basis(d);
189 let scale = 4.0 * PI / SH_SAMPLES as f32;
190 for (l, basis_value) in basis.iter().enumerate() {
191 let convolution = if l == 0 {
192 PI
193 } else if l < 4 {
194 2.0 * PI / 3.0
195 } else {
196 PI / 4.0
197 };
198 for (coefficient, value) in self.sh[l].iter_mut().zip(color) {
199 *coefficient += value * basis_value * scale * convolution;
200 }
201 }
202 }
203 self.sh_next = (self.sh_next + 256).min(SH_SAMPLES);
204 }
205
206 fn face(&self, mip: u32, face: u32) -> Vec<[f32; 4]> {
207 let size = self.edge >> mip;
208 let roughness = mip as f32 / (self.mip_count - 1).max(1) as f32;
209 let mut texels = Vec::with_capacity((size * size) as usize);
210 for y in 0..size {
211 for x in 0..size {
212 let n = cube_direction(
213 face,
214 (x as f32 + 0.5) / size as f32,
215 (y as f32 + 0.5) / size as f32,
216 );
217 let color = prefilter(&self.source, n, roughness);
218 texels.push([color[0], color[1], color[2], 1.0]);
219 }
220 }
221 texels
222 }
223
224 pub fn complete(&self) -> bool {
225 self.sh_next == SH_SAMPLES && self.cube_next == self.mip_count * 6
226 }
227
228 pub fn irradiance(&self, normal: [f32; 3]) -> [f32; 3] {
229 let basis = sh_basis(unit(normal));
230 [0, 1, 2].map(|channel| {
231 (0..9)
232 .map(|i| self.sh[i][channel] * basis[i])
233 .sum::<f32>()
234 .max(0.0)
235 })
236 }
237}
238
239pub struct SettledSky {
240 pub request: u64,
241 pub lighting: SkyLighting,
242 textures: Option<(wgpu::Texture, wgpu::Texture)>,
243}
244
245struct SettlerState {
246 request: Option<(u64, SkySource)>,
247 result: Option<SettledSky>,
248 busy: bool,
249 closed: bool,
250 built: u64,
251}
252
253struct SettlerShared {
254 state: Mutex<SettlerState>,
255 wake: Condvar,
256}
257
258impl SettlerShared {
259 fn lock(&self) -> MutexGuard<'_, SettlerState> {
260 self.state
261 .lock()
262 .unwrap_or_else(|poisoned| poisoned.into_inner())
263 }
264}
265
266pub struct SkySettler {
267 edge: u32,
268 gpu: Option<(wgpu::Device, wgpu::Queue)>,
269 shared: Arc<SettlerShared>,
270 worker: Option<JoinHandle<()>>,
271}
272
273impl SkySettler {
274 pub fn new(edge: u32, gpu: Option<(wgpu::Device, wgpu::Queue)>) -> Self {
275 Self {
276 edge,
277 gpu,
278 shared: Arc::new(SettlerShared {
279 state: Mutex::new(SettlerState {
280 request: None,
281 result: None,
282 busy: false,
283 closed: false,
284 built: 0,
285 }),
286 wake: Condvar::new(),
287 }),
288 worker: None,
289 }
290 }
291
292 pub fn request(&mut self, request: u64, source: SkySource) {
293 if self.worker.is_none() {
294 let shared = self.shared.clone();
295 let edge = self.edge;
296 let gpu = self.gpu.clone();
297 self.worker = Some(
298 std::thread::Builder::new()
299 .name("pito sky settle".into())
300 .spawn(move || settle_worker(&shared, edge, gpu))
301 .expect("spawn the sky settle thread"),
302 );
303 }
304 self.shared.lock().request = Some((request, source));
305 self.shared.wake.notify_all();
306 }
307
308 pub fn cancel(&self) {
309 let mut state = self.shared.lock();
310 state.request = None;
311 state.result = None;
312 }
313
314 pub fn take(&self) -> Option<SettledSky> {
315 self.shared.lock().result.take()
316 }
317
318 pub fn idle(&self) -> bool {
319 let state = self.shared.lock();
320 state.request.is_none() && !state.busy
321 }
322
323 pub fn built(&self) -> u64 {
324 self.shared.lock().built
325 }
326
327 pub fn wait(&self) {
328 let mut state = self.shared.lock();
329 while state.request.is_some() || state.busy {
330 state = self
331 .shared
332 .wake
333 .wait(state)
334 .unwrap_or_else(|poisoned| poisoned.into_inner());
335 }
336 }
337}
338
339impl Drop for SkySettler {
340 fn drop(&mut self) {
341 self.shared.lock().closed = true;
342 self.shared.wake.notify_all();
343 if let Some(worker) = self.worker.take() {
344 let _ = worker.join();
345 }
346 }
347}
348
349fn settle_worker(shared: &SettlerShared, edge: u32, gpu: Option<(wgpu::Device, wgpu::Queue)>) {
350 loop {
351 let (request, source) = {
352 let mut state = shared.lock();
353 loop {
354 if state.closed {
355 return;
356 }
357 if let Some(next) = state.request.take() {
358 state.busy = true;
359 break next;
360 }
361 state = shared
362 .wake
363 .wait(state)
364 .unwrap_or_else(|poisoned| poisoned.into_inner());
365 }
366 };
367 let mut lighting = SkyLighting::new(source, edge);
368 lighting.settle();
369 let textures = gpu.as_ref().map(|(device, queue)| {
370 (
371 sky_hdr(device, queue, &lighting.source),
372 sky_cube(device, queue, &lighting),
373 )
374 });
375 let mut state = shared.lock();
376 state.busy = false;
377 state.built += 1;
378 if state
379 .result
380 .as_ref()
381 .is_none_or(|result| result.request < request)
382 {
383 state.result = Some(SettledSky {
384 request,
385 lighting,
386 textures,
387 });
388 }
389 drop(state);
390 shared.wake.notify_all();
391 }
392}
393
394pub struct SkySteps {
395 settler: SkySettler,
396 pending: Option<(u64, SkySource)>,
397 next: u64,
398 shown: u64,
399}
400
401impl SkySteps {
402 pub fn new(edge: u32, gpu: Option<(wgpu::Device, wgpu::Queue)>) -> Self {
403 Self {
404 settler: SkySettler::new(edge, gpu),
405 pending: None,
406 next: 0,
407 shown: 0,
408 }
409 }
410
411 pub fn request(&mut self, source: SkySource) -> u64 {
412 self.next += 1;
413 self.pending = Some((self.next, source));
414 self.next
415 }
416
417 pub fn settled(&mut self) -> u64 {
418 self.next += 1;
419 self.shown = self.next;
420 self.pending = None;
421 self.settler.cancel();
422 self.next
423 }
424
425 pub fn dispatch(&mut self) {
426 if let Some((request, source)) = self.pending.take() {
427 self.settler.request(request, source);
428 }
429 }
430
431 pub fn take(&mut self) -> Option<SettledSky> {
432 let settled = self.settler.take()?;
433 if settled.request <= self.shown {
434 return None;
435 }
436 self.shown = settled.request;
437 Some(settled)
438 }
439
440 pub fn shown(&self) -> u64 {
441 self.shown
442 }
443
444 pub fn requested(&self) -> u64 {
445 self.next
446 }
447
448 pub fn settling(&self) -> bool {
449 self.pending.is_some() || !self.settler.idle() || self.shown < self.next
450 }
451
452 pub fn built(&self) -> u64 {
453 self.settler.built()
454 }
455
456 pub fn wait(&mut self) {
457 self.dispatch();
458 self.settler.wait();
459 }
460}
461
462fn f16_bytes(texels: &[[f32; 4]]) -> Vec<u8> {
463 let mut bytes = Vec::with_capacity(texels.len() * 8);
464 for pixel in texels {
465 for channel in pixel {
466 bytes.extend_from_slice(&half::f16::from_f32(*channel).to_bits().to_le_bytes());
467 }
468 }
469 bytes
470}
471
472fn hdr_texture(device: &wgpu::Device, source: &SkySource) -> wgpu::Texture {
473 let (width, height) = match source {
474 SkySource::Hdr(hdr) => (hdr.width, hdr.height),
475 _ => (1, 1),
476 };
477 device.create_texture(&wgpu::TextureDescriptor {
478 label: Some("sky HDR"),
479 size: wgpu::Extent3d {
480 width,
481 height,
482 depth_or_array_layers: 1,
483 },
484 mip_level_count: 1,
485 sample_count: 1,
486 dimension: wgpu::TextureDimension::D2,
487 format: wgpu::TextureFormat::Rgba16Float,
488 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
489 view_formats: &[],
490 })
491}
492
493fn sky_hdr(device: &wgpu::Device, queue: &wgpu::Queue, source: &SkySource) -> wgpu::Texture {
494 let texture = hdr_texture(device, source);
495 if let SkySource::Hdr(hdr) = source {
496 queue.write_texture(
497 texture.as_image_copy(),
498 &f16_bytes(&hdr.texels),
499 wgpu::TexelCopyBufferLayout {
500 offset: 0,
501 bytes_per_row: Some(8 * hdr.width),
502 rows_per_image: Some(hdr.height),
503 },
504 texture.size(),
505 );
506 }
507 texture
508}
509
510fn cube_texture(device: &wgpu::Device, lighting: &SkyLighting) -> wgpu::Texture {
511 device.create_texture(&wgpu::TextureDescriptor {
512 label: Some("sky GGX cube"),
513 size: wgpu::Extent3d {
514 width: lighting.edge,
515 height: lighting.edge,
516 depth_or_array_layers: 6,
517 },
518 mip_level_count: lighting.mip_count,
519 sample_count: 1,
520 dimension: wgpu::TextureDimension::D2,
521 format: wgpu::TextureFormat::Rgba16Float,
522 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
523 view_formats: &[],
524 })
525}
526
527fn sky_cube(device: &wgpu::Device, queue: &wgpu::Queue, lighting: &SkyLighting) -> wgpu::Texture {
528 let texture = cube_texture(device, lighting);
529 for (mip, texels) in lighting.cube.iter().enumerate() {
530 let size = lighting.edge >> mip;
531 queue.write_texture(
532 wgpu::TexelCopyTextureInfo {
533 texture: &texture,
534 mip_level: mip as u32,
535 origin: wgpu::Origin3d::ZERO,
536 aspect: wgpu::TextureAspect::All,
537 },
538 &f16_bytes(texels),
539 wgpu::TexelCopyBufferLayout {
540 offset: 0,
541 bytes_per_row: Some(size * 8),
542 rows_per_image: Some(size),
543 },
544 wgpu::Extent3d {
545 width: size,
546 height: size,
547 depth_or_array_layers: 6,
548 },
549 );
550 }
551 texture
552}
553
554fn sh_basis(d: [f32; 3]) -> [f32; 9] {
555 [
556 0.282095,
557 0.488603 * d[1],
558 0.488603 * d[2],
559 0.488603 * d[0],
560 1.092548 * d[0] * d[1],
561 1.092548 * d[1] * d[2],
562 0.315392 * (3.0 * d[2] * d[2] - 1.0),
563 1.092548 * d[0] * d[2],
564 0.546274 * (d[0] * d[0] - d[1] * d[1]),
565 ]
566}
567
568fn prefilter(source: &SkySource, n: [f32; 3], roughness: f32) -> [f32; 3] {
569 if roughness == 0.0 {
570 return source.radiance(n);
571 }
572 let up = if n[1].abs() < 0.99 {
573 [0.0, 1.0, 0.0]
574 } else {
575 [1.0, 0.0, 0.0]
576 };
577 let right = unit(cross(up, n));
578 let up = cross(n, right);
579 let alpha = roughness * roughness;
580 let mut sum = [0.0; 3];
581 let mut weight = 0.0;
582 for i in 0..64_u32 {
583 let xi = (i as f32 + 0.5) / 64.0;
584 let phi = TAU * radical_inverse(i);
585 let cos_theta = ((1.0 - xi) / (1.0 + (alpha * alpha - 1.0) * xi)).sqrt();
586 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
587 let h = add(
588 add(
589 mul(right, sin_theta * phi.cos()),
590 mul(up, sin_theta * phi.sin()),
591 ),
592 mul(n, cos_theta),
593 );
594 let l = unit(add(mul(h, 2.0 * dot(n, h)), mul(n, -1.0)));
595 let ndotl = dot(n, l).max(0.0);
596 if ndotl > 0.0 {
597 let value = source.radiance(l);
598 for (channel, incoming) in sum.iter_mut().zip(value) {
599 *channel += incoming * ndotl;
600 }
601 weight += ndotl;
602 }
603 }
604 sum.map(|v| v / weight.max(1e-6))
605}
606
607fn radical_inverse(value: u32) -> f32 {
608 value.reverse_bits() as f32 * 2.328_306_4e-10
609}
610
611fn cube_direction(face: u32, u: f32, v: f32) -> [f32; 3] {
612 let x = 2.0 * u - 1.0;
613 let y = 1.0 - 2.0 * v;
614 unit(match face {
615 0 => [1.0, y, -x],
616 1 => [-1.0, y, x],
617 2 => [x, 1.0, -y],
618 3 => [x, -1.0, y],
619 4 => [x, y, 1.0],
620 _ => [-x, y, -1.0],
621 })
622}
623
624fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
625 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
626}
627fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
628 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
629}
630fn mul(a: [f32; 3], s: f32) -> [f32; 3] {
631 a.map(|v| v * s)
632}
633fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
634 [
635 a[1] * b[2] - a[2] * b[1],
636 a[2] * b[0] - a[0] * b[2],
637 a[0] * b[1] - a[1] * b[0],
638 ]
639}
640fn unit(a: [f32; 3]) -> [f32; 3] {
641 mul(a, 1.0 / dot(a, a).sqrt().max(1e-8))
642}
643
644pub static SKY_WGSL: LazyLock<String> = LazyLock::new(|| {
645 format!(
646 "{}\n{}",
647 SKY_MODEL_WGSL.replace("@group(1)", "@group(0)"),
648 SKY_PASS_WGSL
649 )
650});
651
652const SKY_PASS_WGSL: &str = r#"
653struct SkyVertex {
654 @builtin(position) position: vec4f,
655 @location(0) uv: vec2f,
656};
657@vertex
658fn sky_vs(@builtin(vertex_index) index: u32) -> SkyVertex {
659 var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
660 var output: SkyVertex;
661 output.position = vec4f(positions[index], 1.0, 1.0);
662 output.uv = positions[index]*vec2f(0.5, -0.5)+vec2f(0.5, 0.5);
663 return output;
664}
665@fragment
666fn sky_fs(input: SkyVertex) -> @location(0) vec4f {
667 let direction = normalize(sky.forward.xyz + sky.right.xyz*(input.uv.x*2.0-1.0) + sky.up.xyz*(1.0-input.uv.y*2.0));
668 return vec4f(sky_radiance(direction), 1.0);
669}
670"#;
671
672#[repr(C)]
673#[derive(Clone, Copy, Pod, Zeroable)]
674struct SkyUniform {
675 sun: [f32; 4],
676 sun_colour_intensity: [f32; 4],
677 ambient_turbidity_albedo: [f32; 4],
678 albedo_mode: [f32; 4],
679 forward: [f32; 4],
680 right: [f32; 4],
681 up: [f32; 4],
682}
683
684pub struct SkyPass {
685 pipeline: wgpu::RenderPipeline,
686 layout: wgpu::BindGroupLayout,
687 uniform: wgpu::Buffer,
688 bind: wgpu::BindGroup,
689 hdr: wgpu::Texture,
690 sampler: wgpu::Sampler,
691 cube: wgpu::Texture,
692 device: wgpu::Device,
693 steps: SkySteps,
694 pub lighting: SkyLighting,
695}
696
697impl SkyPass {
698 pub fn new(
699 device: &wgpu::Device,
700 queue: &wgpu::Queue,
701 color: wgpu::TextureFormat,
702 depth: Option<wgpu::TextureFormat>,
703 source: SkySource,
704 cube_edge: u32,
705 ) -> Self {
706 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
707 label: Some("sky layout"),
708 entries: &[
709 wgpu::BindGroupLayoutEntry {
710 binding: 0,
711 visibility: wgpu::ShaderStages::FRAGMENT,
712 ty: wgpu::BindingType::Buffer {
713 ty: wgpu::BufferBindingType::Uniform,
714 has_dynamic_offset: false,
715 min_binding_size: None,
716 },
717 count: None,
718 },
719 wgpu::BindGroupLayoutEntry {
720 binding: 1,
721 visibility: wgpu::ShaderStages::FRAGMENT,
722 ty: wgpu::BindingType::Texture {
723 sample_type: wgpu::TextureSampleType::Float { filterable: true },
724 view_dimension: wgpu::TextureViewDimension::D2,
725 multisampled: false,
726 },
727 count: None,
728 },
729 wgpu::BindGroupLayoutEntry {
730 binding: 2,
731 visibility: wgpu::ShaderStages::FRAGMENT,
732 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
733 count: None,
734 },
735 ],
736 });
737 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
738 label: Some("sky shader"),
739 source: wgpu::ShaderSource::Wgsl(SKY_WGSL.as_str().into()),
740 });
741 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
742 label: Some("sky pipeline layout"),
743 bind_group_layouts: &[Some(&layout)],
744 immediate_size: 0,
745 });
746 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
747 label: Some("sky pass"),
748 layout: Some(&pipeline_layout),
749 vertex: wgpu::VertexState {
750 module: &shader,
751 entry_point: Some("sky_vs"),
752 compilation_options: Default::default(),
753 buffers: &[],
754 },
755 primitive: Default::default(),
756 depth_stencil: depth.map(|format| wgpu::DepthStencilState {
757 format,
758 depth_write_enabled: Some(false),
759 depth_compare: Some(wgpu::CompareFunction::LessEqual),
760 stencil: Default::default(),
761 bias: Default::default(),
762 }),
763 multisample: Default::default(),
764 fragment: Some(wgpu::FragmentState {
765 module: &shader,
766 entry_point: Some("sky_fs"),
767 compilation_options: Default::default(),
768 targets: &[Some(wgpu::ColorTargetState {
769 format: color,
770 blend: None,
771 write_mask: wgpu::ColorWrites::ALL,
772 })],
773 }),
774 multiview_mask: None,
775 cache: None,
776 });
777 let uniform = device.create_buffer(&wgpu::BufferDescriptor {
778 label: Some("sky uniform"),
779 size: std::mem::size_of::<SkyUniform>() as u64,
780 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
781 mapped_at_creation: false,
782 });
783 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
784 address_mode_u: wgpu::AddressMode::Repeat,
785 address_mode_v: wgpu::AddressMode::ClampToEdge,
786 mag_filter: wgpu::FilterMode::Linear,
787 min_filter: wgpu::FilterMode::Linear,
788 ..Default::default()
789 });
790 let hdr = hdr_texture(device, &source);
791 let bind = Self::bind(device, &layout, &uniform, &hdr, &sampler);
792 let lighting = SkyLighting::new(source, cube_edge);
793 let cube = cube_texture(device, &lighting);
794 let steps = SkySteps::new(lighting.edge, Some((device.clone(), queue.clone())));
795 let mut pass = Self {
796 pipeline,
797 layout,
798 uniform,
799 bind,
800 hdr,
801 sampler,
802 cube,
803 device: device.clone(),
804 steps,
805 lighting,
806 };
807 pass.set_source(device, queue, pass.lighting.source.clone());
808 pass
809 }
810
811 fn bind(
812 device: &wgpu::Device,
813 layout: &wgpu::BindGroupLayout,
814 uniform: &wgpu::Buffer,
815 hdr: &wgpu::Texture,
816 sampler: &wgpu::Sampler,
817 ) -> wgpu::BindGroup {
818 device.create_bind_group(&wgpu::BindGroupDescriptor {
819 label: Some("sky binding"),
820 layout,
821 entries: &[
822 wgpu::BindGroupEntry {
823 binding: 0,
824 resource: uniform.as_entire_binding(),
825 },
826 wgpu::BindGroupEntry {
827 binding: 1,
828 resource: wgpu::BindingResource::TextureView(
829 &hdr.create_view(&Default::default()),
830 ),
831 },
832 wgpu::BindGroupEntry {
833 binding: 2,
834 resource: wgpu::BindingResource::Sampler(sampler),
835 },
836 ],
837 })
838 }
839
840 pub fn set_source(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, source: SkySource) {
841 self.steps.settled();
842 self.hdr = sky_hdr(device, queue, &source);
843 self.bind = Self::bind(
844 device,
845 &self.layout,
846 &self.uniform,
847 &self.hdr,
848 &self.sampler,
849 );
850 self.lighting.set_source(source);
851 }
852
853 pub fn request_source(&mut self, source: SkySource) -> u64 {
854 self.steps.request(source)
855 }
856
857 pub fn swap_settled(&mut self) -> Option<u64> {
858 self.steps.dispatch();
859 let settled = self.steps.take()?;
860 let (hdr, cube) = settled.textures?;
861 self.hdr = hdr;
862 self.cube = cube;
863 self.lighting = settled.lighting;
864 self.bind = Self::bind(
865 &self.device,
866 &self.layout,
867 &self.uniform,
868 &self.hdr,
869 &self.sampler,
870 );
871 Some(settled.request)
872 }
873
874 pub fn settling(&self) -> bool {
875 self.steps.settling()
876 }
877
878 pub fn shown(&self) -> u64 {
879 self.steps.shown()
880 }
881
882 pub fn wait_settled(&mut self) -> Option<u64> {
883 self.steps.wait();
884 self.swap_settled()
885 }
886
887 pub fn update(
888 &mut self,
889 queue: &wgpu::Queue,
890 forward: [f32; 3],
891 right: [f32; 3],
892 up: [f32; 3],
893 ) {
894 self.swap_settled();
895 let (model, mode) = match self.lighting.source() {
896 SkySource::Analytic(model) => (*model, 0.0),
897 SkySource::Hdr(_) => (
898 AnalyticSky {
899 sun: [0.0, 1.0, 0.0],
900 sun_colour: [1.0; 3],
901 sun_intensity: 0.0,
902 ambient: 0.0,
903 turbidity: 2.0,
904 ground_albedo: [0.0; 3],
905 },
906 1.0,
907 ),
908 };
909 let data = SkyUniform {
910 sun: [model.sun[0], model.sun[1], model.sun[2], 0.0],
911 sun_colour_intensity: [
912 model.sun_colour[0],
913 model.sun_colour[1],
914 model.sun_colour[2],
915 model.sun_intensity,
916 ],
917 ambient_turbidity_albedo: [
918 model.ambient,
919 model.turbidity,
920 model.ground_albedo[0],
921 model.ground_albedo[1],
922 ],
923 albedo_mode: [model.ground_albedo[2], mode, 0.0, 0.0],
924 forward: [forward[0], forward[1], forward[2], 0.0],
925 right: [right[0], right[1], right[2], 0.0],
926 up: [up[0], up[1], up[2], 0.0],
927 };
928 queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&data));
929 if let Some((mip, face)) = self.lighting.step() {
930 let size = self.lighting.edge >> mip;
931 let start = (face * size * size) as usize;
932 let end = start + (size * size) as usize;
933 let bytes = f16_bytes(&self.lighting.cube[mip as usize][start..end]);
934 queue.write_texture(
935 wgpu::TexelCopyTextureInfo {
936 texture: &self.cube,
937 mip_level: mip,
938 origin: wgpu::Origin3d {
939 x: 0,
940 y: 0,
941 z: face,
942 },
943 aspect: wgpu::TextureAspect::All,
944 },
945 &bytes,
946 wgpu::TexelCopyBufferLayout {
947 offset: 0,
948 bytes_per_row: Some(size * 8),
949 rows_per_image: Some(size),
950 },
951 wgpu::Extent3d {
952 width: size,
953 height: size,
954 depth_or_array_layers: 1,
955 },
956 );
957 }
958 }
959
960 pub fn cube_view(&self) -> wgpu::TextureView {
961 self.cube.create_view(&wgpu::TextureViewDescriptor {
962 dimension: Some(wgpu::TextureViewDimension::Cube),
963 ..Default::default()
964 })
965 }
966
967 pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
968 pass.set_pipeline(&self.pipeline);
969 pass.set_bind_group(0, &self.bind, &[]);
970 pass.draw(0..3, 0..1);
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 fn daylight(hour: f64) -> Daylight {
979 Daylight {
980 hour,
981 day: 172.0,
982 latitude: 45.0,
983 heading: 180.0,
984 }
985 }
986
987 fn model(hour: f64) -> AnalyticSky {
988 AnalyticSky::new(
989 daylight(hour),
990 daylight::REFERENCE_HOUR,
991 2.5,
992 [0.2, 0.25, 0.2],
993 )
994 }
995
996 #[test]
997 fn sun_is_brightest_and_night_is_dark() {
998 let noon = model(12.0);
999 let near = noon.radiance(noon.sun);
1000 let away = noon.radiance([-noon.sun[0], noon.sun[1], -noon.sun[2]]);
1001 assert!(near[0] > away[0] * 100.0);
1002 let night = model(0.0);
1003 let above = [0.0, 1.0, 0.0];
1004 assert!(noon.radiance(above)[2] > night.radiance(above)[2] * 5.0);
1005 }
1006
1007 #[test]
1008 fn sh_reconstructs_diffuse_irradiance() {
1009 let source = SkySource::Analytic(model(12.0));
1010 let mut lighting = SkyLighting::new(source.clone(), 2);
1011 while !lighting.complete() {
1012 lighting.step();
1013 }
1014 for normal in [[0.0, 1.0, 0.0], [0.8, 0.6, 0.0], [0.0, 0.5, 0.866_025_4]] {
1015 let mut reference = [0.0; 3];
1016 let count = 16_384;
1017 for i in 0..count {
1018 let y = 1.0 - 2.0 * (i as f32 + 0.5) / count as f32;
1019 let phi = i as f32 * 2.399_963_1;
1020 let r = (1.0 - y * y).sqrt();
1021 let dir = [r * phi.cos(), y, r * phi.sin()];
1022 let weight = dot(dir, normal).max(0.0) * 4.0 * PI / count as f32;
1023 let color = source.diffuse(dir);
1024 for (channel, incoming) in reference.iter_mut().zip(color) {
1025 *channel += incoming * weight;
1026 }
1027 }
1028 let sh = lighting.irradiance(normal);
1029 for c in 0..3 {
1030 let error = (sh[c] - reference[c]).abs() / reference[c].max(0.01);
1031 assert!(
1032 error < 0.18,
1033 "{normal:?} channel {c}: {sh:?} versus {reference:?}"
1034 );
1035 }
1036 }
1037 }
1038
1039 #[test]
1040 fn hdr_blend_and_ggx_cube() {
1041 let red = HdrSky {
1042 width: 2,
1043 height: 1,
1044 texels: vec![[2.0, 0.0, 0.0, 1.0]; 2],
1045 };
1046 let blue = HdrSky {
1047 width: 2,
1048 height: 1,
1049 texels: vec![[0.0, 0.0, 4.0, 1.0]; 2],
1050 };
1051 let probes = [
1052 Probe {
1053 hour: 6.0,
1054 index: 0,
1055 },
1056 Probe {
1057 hour: 18.0,
1058 index: 1,
1059 },
1060 ];
1061 let source = SkySource::from_probes(12.0, &[red, blue], &probes).unwrap();
1062 let color = source.radiance([0.0, 1.0, 0.0]);
1063 assert!((color[0] - 1.0).abs() < 1e-5 && (color[2] - 2.0).abs() < 1e-5);
1064 let mut lighting = SkyLighting::new(source, 4);
1065 while !lighting.complete() {
1066 lighting.step();
1067 }
1068 for mip in &lighting.cube {
1069 for texel in mip {
1070 assert!((texel[0] - 1.0).abs() < 1e-3 && (texel[2] - 2.0).abs() < 1e-3);
1071 }
1072 }
1073 }
1074
1075 fn same_bits(a: &SkyLighting, b: &SkyLighting) -> bool {
1076 let sh = |l: &SkyLighting| {
1077 l.sh.iter()
1078 .flatten()
1079 .map(|v| v.to_bits())
1080 .collect::<Vec<_>>()
1081 };
1082 let cube = |l: &SkyLighting| {
1083 l.cube
1084 .iter()
1085 .flatten()
1086 .flatten()
1087 .map(|v| v.to_bits())
1088 .collect::<Vec<_>>()
1089 };
1090 a.complete() && b.complete() && sh(a) == sh(b) && cube(a) == cube(b)
1091 }
1092
1093 fn stepped(source: SkySource, edge: u32) -> SkyLighting {
1094 let mut lighting = SkyLighting::new(source, edge);
1095 while !lighting.complete() {
1096 lighting.step();
1097 }
1098 lighting
1099 }
1100
1101 fn banded_hdr() -> HdrSky {
1102 let (width, height) = (32, 16);
1103 HdrSky {
1104 width,
1105 height,
1106 texels: (0..width * height)
1107 .map(|i| {
1108 let (x, y) = (i % width, i / width);
1109 [
1110 0.2 + x as f32 * 0.05,
1111 0.1 + y as f32 * 0.3,
1112 if (x + y) % 5 == 0 { 6.0 } else { 0.4 },
1113 1.0,
1114 ]
1115 })
1116 .collect(),
1117 }
1118 }
1119
1120 #[test]
1121 fn settling_off_thread_equals_stepping_bit_for_bit() {
1122 for source in [
1123 SkySource::Analytic(model(9.5)),
1124 SkySource::Hdr(banded_hdr()),
1125 ] {
1126 let reference = stepped(source.clone(), 16);
1127 let mut settled = SkyLighting::new(source.clone(), 16);
1128 settled.settle();
1129 assert!(same_bits(&reference, &settled));
1130 let mut partly = SkyLighting::new(source.clone(), 16);
1131 for _ in 0..9 {
1132 partly.step();
1133 }
1134 partly.settle();
1135 assert!(same_bits(&reference, &partly));
1136 let mut steps = SkySteps::new(16, None);
1137 let request = steps.request(source);
1138 steps.wait();
1139 let result = steps.take().unwrap();
1140 assert_eq!(result.request, request);
1141 assert!(same_bits(&reference, &result.lighting));
1142 assert!(!steps.settling());
1143 }
1144 }
1145
1146 #[test]
1147 fn twenty_requests_in_one_frame_build_only_the_last() {
1148 let mut steps = SkySteps::new(16, None);
1149 let mut calls = Vec::new();
1150 let mut last = 0;
1151 for index in 0..20 {
1152 let source = SkySource::Analytic(model(6.0 + index as f64 * 0.5));
1153 let started = std::time::Instant::now();
1154 last = steps.request(source);
1155 calls.push(started.elapsed().as_secs_f64() * 1000.0);
1156 }
1157 let started = std::time::Instant::now();
1158 steps.dispatch();
1159 let dispatch = started.elapsed().as_secs_f64() * 1000.0;
1160 assert!(steps.settling());
1161 steps.wait();
1162 let result = steps.take().unwrap();
1163 assert_eq!(result.request, last);
1164 assert_eq!(steps.built(), 1);
1165 assert!(steps.take().is_none());
1166 assert!(same_bits(
1167 &stepped(SkySource::Analytic(model(6.0 + 19.0 * 0.5)), 16),
1168 &result.lighting
1169 ));
1170 let mean = calls.iter().sum::<f64>() / calls.len() as f64;
1171 let most = calls.iter().copied().fold(0.0, f64::max);
1172 println!(
1173 "set_sky on the calling thread: mean {mean:.4} ms, max {most:.4} ms over 20 calls; dispatch {dispatch:.4} ms"
1174 );
1175 assert!(mean < 1.0);
1176 assert!(dispatch < 50.0);
1177 }
1178
1179 #[test]
1180 fn a_scrubber_ends_on_its_newest_hour_and_old_results_never_return() {
1181 let mut steps = SkySteps::new(8, None);
1182 let mut newest = 0;
1183 for frame in 0..30 {
1184 newest = steps.request(SkySource::Analytic(model(8.0 + frame as f64 * 0.25)));
1185 steps.dispatch();
1186 if let Some(result) = steps.take() {
1187 assert!(result.request <= newest);
1188 }
1189 }
1190 steps.wait();
1191 let shown = steps.take().map(|result| result.request);
1192 assert!(shown == Some(newest) || steps.shown() == newest);
1193 assert!(steps.built() <= 30);
1194 let settled = steps.settled();
1195 steps.request(SkySource::Analytic(model(13.0)));
1196 steps.dispatch();
1197 steps.settled();
1198 steps.wait();
1199 assert!(steps.take().is_none());
1200 assert!(settled < steps.shown());
1201 }
1202
1203 #[test]
1204 fn wgsl_parses() {
1205 let module = naga::front::wgsl::parse_str(&SKY_WGSL).unwrap();
1206 naga::valid::Validator::new(
1207 naga::valid::ValidationFlags::all(),
1208 naga::valid::Capabilities::all(),
1209 )
1210 .validate(&module)
1211 .unwrap();
1212 }
1213
1214 #[test]
1215 #[ignore = "needs a GPU; run under pgpu"]
1216 fn gpu_sky_matches_cpu_at_morning_noon_and_evening() {
1217 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
1218 let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
1219 label: Some("sky test target"),
1220 size: wgpu::Extent3d {
1221 width: 1,
1222 height: 1,
1223 depth_or_array_layers: 1,
1224 },
1225 mip_level_count: 1,
1226 sample_count: 1,
1227 dimension: wgpu::TextureDimension::D2,
1228 format: wgpu::TextureFormat::Rgba16Float,
1229 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1230 view_formats: &[],
1231 });
1232 let view = texture.create_view(&Default::default());
1233 let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1234 label: Some("sky readback"),
1235 size: 256,
1236 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1237 mapped_at_creation: false,
1238 });
1239 for hour in [8.0, 12.0, 18.0] {
1240 let started = std::time::Instant::now();
1241 let model = model(hour);
1242 let mut pass = SkyPass::new(
1243 &gpu.device,
1244 &gpu.queue,
1245 wgpu::TextureFormat::Rgba16Float,
1246 None,
1247 SkySource::Analytic(model),
1248 2,
1249 );
1250 for direction in [
1251 unit([0.5, 0.7, 0.3]),
1252 [0.0, 1.0, 0.0],
1253 unit([-0.6, 0.4, 0.6]),
1254 ] {
1255 pass.update(&gpu.queue, direction, [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
1256 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1257 {
1258 let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1259 multiview_mask: None,
1260 label: Some("sky test"),
1261 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1262 view: &view,
1263 resolve_target: None,
1264 ops: wgpu::Operations {
1265 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1266 store: wgpu::StoreOp::Store,
1267 },
1268 depth_slice: None,
1269 })],
1270 depth_stencil_attachment: None,
1271 timestamp_writes: None,
1272 occlusion_query_set: None,
1273 });
1274 pass.draw(&mut render);
1275 }
1276 encoder.copy_texture_to_buffer(
1277 texture.as_image_copy(),
1278 wgpu::TexelCopyBufferInfo {
1279 buffer: &readback,
1280 layout: wgpu::TexelCopyBufferLayout {
1281 offset: 0,
1282 bytes_per_row: Some(256),
1283 rows_per_image: Some(1),
1284 },
1285 },
1286 texture.size(),
1287 );
1288 gpu.queue.submit(Some(encoder.finish()));
1289 let (send, receive) = std::sync::mpsc::channel();
1290 let slice = readback.slice(..);
1291 slice.map_async(wgpu::MapMode::Read, move |result| {
1292 let _ = send.send(result);
1293 });
1294 gpu.device
1295 .poll(wgpu::PollType::wait_indefinitely())
1296 .unwrap();
1297 receive.recv().unwrap().unwrap();
1298 let mapped = slice.get_mapped_range().expect("a mapped range");
1299 let actual = [0, 1, 2].map(|i| {
1300 half::f16::from_bits(u16::from_le_bytes([mapped[2 * i], mapped[2 * i + 1]]))
1301 .to_f32()
1302 });
1303 drop(mapped);
1304 readback.unmap();
1305 let expected = model.radiance(direction);
1306 for c in 0..3 {
1307 assert!(
1308 (actual[c] - expected[c]).abs() <= expected[c].max(0.01) * 0.04,
1309 "{hour}: {actual:?} versus {expected:?}"
1310 );
1311 }
1312 println!("sky GPU {hour:02.0}:00 {direction:?}: {actual:?}");
1313 }
1314 println!(
1315 "sky GPU {hour:02.0}:00 headless test wall time: {:.3} ms",
1316 started.elapsed().as_secs_f64() * 1000.0
1317 );
1318 }
1319 }
1320}