Skip to main content

pfx_live/
shadow.rs

1use std::cell::Cell;
2use std::hash::{Hash, Hasher};
3use std::num::NonZeroU64;
4
5use crate::effects::{CreatureInstance, CreatureMesh};
6use pfx_gpu::{Gpu, GpuProfiler, wgpu};
7use wgpu::util::DeviceExt;
8
9pub const CASCADE_COUNT: usize = 3;
10pub const DEFAULT_RESOLUTION: u32 = 4096;
11pub const SAMPLE_UNIFORM_BYTES: usize = 288;
12pub const SOFT_TAPS: u32 = 16;
13pub const BLOCKER_TAPS: u32 = 8;
14pub const FILTER_TAPS: u32 = 12;
15pub const SOFT_TAP_LIMIT: u32 = 64;
16pub const BLOCKER_GATHERS: u32 = 0;
17pub const SOFT_REACH_TEXELS: f32 = 48.0;
18pub const SOFT_SLOPE_LIMIT: f32 = 4.0;
19pub const OUTER_SCALE: f32 = 3.0;
20pub const SOFT_OFFSET: f32 = 0.0;
21
22pub const SHADOW_WGSL: &str = r#"
23override use_soft_search: bool = true;
24override use_soft_filter: bool = true;
25override use_cascade_blend: bool = true;
26struct ShadowSample {
27    splits: vec4f,
28    filter_params: vec4f,
29    sun: vec4f,
30    texels: vec4f,
31    view_proj_0: mat4x4f,
32    view_proj_1: mat4x4f,
33    view_proj_2: mat4x4f,
34    soft: vec4f,
35    soft_taps: vec4f,
36}
37
38fn shadow_select(distance: f32, splits: vec4f, blend: f32) -> vec3f {
39    var cascade = 0.0;
40    if (distance > splits.y) {
41        cascade = 1.0;
42    }
43    if (distance > splits.z) {
44        cascade = 2.0;
45    }
46    var next = cascade;
47    var t = 0.0;
48    if (cascade < 2.0) {
49        let start = select(splits.y, splits.x, cascade < 0.5);
50        let end = select(splits.z, splits.y, cascade < 0.5);
51        let band = (end - start) * blend;
52        if (band > 0.0 && distance > end - band) {
53            next = cascade + 1.0;
54            t = clamp((distance - (end - band)) / band, 0.0, 1.0);
55        }
56    }
57    return vec3f(cascade, next, t);
58}
59
60fn shadow_blend(closer: f32, farther: f32, t: f32) -> f32 {
61    return mix(closer, farther, t);
62}
63
64fn shadow_project(view_proj: mat4x4f, world: vec3f) -> vec3f {
65    let clip = view_proj * vec4f(world, 1.0);
66    let ndc = clip.xyz / clip.w;
67    let uv = vec2f(ndc.x, -ndc.y) * 0.5 + vec2f(0.5, 0.5);
68    return vec3f(uv, ndc.z);
69}
70
71fn shadow_pcf(
72    map: texture_depth_2d_array,
73    samp: sampler_comparison,
74    uv: vec2f,
75    layer: i32,
76    depth: f32,
77    radius: f32,
78) -> f32 {
79    let size = textureDimensions(map);
80    let texel = vec2f(1.0 / f32(size.x), 1.0 / f32(size.y));
81    var lit = 0.0;
82    for (var y = -1; y <= 1; y++) {
83        for (var x = -1; x <= 1; x++) {
84            let at = uv + vec2f(f32(x), f32(y)) * texel * radius;
85            lit += textureSampleCompareLevel(map, samp, at, layer, depth);
86        }
87    }
88    return lit / 9.0;
89}
90
91fn shadow_disc(index: u32, count: u32, turn: f32) -> vec2f {
92    let radius = sqrt((f32(index) + 0.5) / f32(count));
93    let angle = f32(index) * 2.39996323 + turn;
94    return vec2f(cos(angle), sin(angle)) * radius;
95}
96
97fn shadow_turn(world: vec3f) -> f32 {
98    let p = fract(world * vec3f(443.897, 441.423, 437.195));
99    let q = p + dot(p, p.yzx + 19.19);
100    return fract((q.x + q.y) * q.z) * 6.2831853;
101}
102
103fn shadow_slope(view_proj: mat4x4f, normal: vec3f, size: vec2f, scales: vec2f, limit: f32) -> vec2f {
104    let a0 = view_proj[0].xyz;
105    let a1 = view_proj[1].xyz;
106    let a2 = view_proj[2].xyz;
107    let len = length(normal);
108    if (len < 1e-6) {
109        return vec2f(0.0);
110    }
111    let n = normal / len;
112    let m = n.x * cross(a1, a2) + n.y * cross(a2, a0) + n.z * cross(a0, a1);
113    let denom = select(-1e-6, m.z, abs(m.z) >= 1e-6);
114    let g = -m.xy / denom;
115    var per_texel = vec2f(g.x * 2.0 / size.x, -g.y * 2.0 / size.y);
116    let tangent = length(per_texel) * scales.x * scales.y;
117    if (tangent > limit) {
118        per_texel = per_texel * (limit / tangent);
119    }
120    return per_texel;
121}
122
123fn shadow_soft(
124    map: texture_depth_2d_array,
125    samp: sampler_comparison,
126    gather: sampler,
127    view_proj: mat4x4f,
128    projected: vec3f,
129    layer: i32,
130    depth: f32,
131    radius: f32,
132    soft: vec4f,
133    taps: vec4f,
134    world: vec3f,
135    normal: vec3f,
136) -> f32 {
137    let size = vec2f(textureDimensions(map));
138    let per_depth = 1.0 / max(length(vec3f(view_proj[0].z, view_proj[1].z, view_proj[2].z)), 1e-8);
139    let per_world = 0.5 * length(vec3f(view_proj[0].x, view_proj[1].x, view_proj[2].x)) * size.x;
140    let slope = shadow_slope(view_proj, normal, size, vec2f(per_depth, per_world), soft.w);
141    let turn = shadow_turn(world);
142    let reach = min(depth * per_depth * soft.x * per_world, soft.y);
143    let count = u32(soft.z);
144    let filter_count = u32(taps.y);
145    let search = max(reach, radius);
146    if (!use_soft_search) {
147        return shadow_soft_filter(map, samp, projected, layer, depth, search, filter_count, turn, slope);
148    }
149    let centre = projected.xy * size;
150    let gathers = u32(taps.x);
151    var blockers = 0.0;
152    var total = 0.0;
153    var near = vec3f(-1e30, 0.0, 0.0);
154    var far = false;
155    var inner = false;
156    var tested = count;
157    if (gathers > 0u) {
158        tested = gathers * 4u;
159        for (var i = 0u; i < gathers; i++) {
160            let at = round(centre + shadow_disc(i, gathers, turn) * search);
161            let stored = textureGather(map, gather, at / size, layer);
162            let corner = at - vec2f(0.5) - centre;
163            let offsets = array<vec2f, 4>(corner + vec2f(0.0, 1.0), corner + vec2f(1.0, 1.0), corner + vec2f(1.0, 0.0), corner);
164            for (var k = 0u; k < 4u; k++) {
165                let shift = dot(slope, offsets[k]);
166                if (stored[k] < depth + shift) {
167                    let level = stored[k] - shift;
168                    far = far || stored[k] <= 0.0;
169                    inner = inner || stored[k] > 0.0;
170                    blockers += 1.0;
171                    total += level;
172                    near = shadow_near(near, level, depth);
173                }
174            }
175        }
176    } else {
177        for (var i = 0u; i < count; i++) {
178            let at = centre + shadow_disc(i, count, turn) * search;
179            let texel = clamp(vec2i(floor(at)), vec2i(0), vec2i(size) - vec2i(1));
180            let stored = textureLoad(map, texel, layer, 0);
181            let here = depth + dot(slope, vec2f(texel) + vec2f(0.5) - centre);
182            if (stored < here) {
183                let level = stored - dot(slope, vec2f(texel) + vec2f(0.5) - centre);
184                far = far || stored <= 0.0;
185                inner = inner || stored > 0.0;
186                blockers += 1.0;
187                total += level;
188                near = shadow_near(near, level, depth);
189            }
190        }
191    }
192    let flags = u32(taps.z);
193    if (blockers < 0.5 && (flags & 8u) == 0u) {
194        return 1.0;
195    }
196    if (!use_soft_filter) {
197        return 1.0 - blockers / f32(tested);
198    }
199    if ((flags & 2u) != 0u && blockers > f32(tested) - 0.5) {
200        return 0.0;
201    }
202    let clustered = (flags & 1u) != 0u && near.z * 2.0 >= blockers;
203    let blocker = select(select(total / blockers, near.y / near.z, clustered), depth, blockers < 0.5);
204    let width = clamp((depth - blocker) * per_depth * soft.x * per_world, radius, soft.y);
205    var lit = 1.0;
206    if ((flags & 16u) == 0u || clustered || near.z < 0.5) {
207        lit = shadow_soft_filter(map, samp, projected, layer, depth, width, filter_count, turn, slope);
208    } else {
209        let above = textureLoad(map, clamp(vec2i(floor(centre)), vec2i(0), vec2i(size) - vec2i(1)), layer, 0);
210        let seen = above < depth && above > 0.0;
211        let top = select(near.x, above, seen);
212        let level = select(near.y / near.z, above, seen);
213        let close = clamp((depth - level) * per_depth * soft.x * per_world, radius, soft.y);
214        if (close >= width * 0.5) {
215            lit = shadow_soft_filter(map, samp, projected, layer, depth, width, filter_count, turn, slope);
216        } else {
217            let band = top - 0.5 * max(depth - top, 0.0);
218            lit = shadow_layered_filter(map, samp, projected, layer, depth, band, width, close, filter_count, turn, slope);
219        }
220    }
221    if ((flags & 64u) == 0u || !far || lit <= 0.0) {
222        return lit;
223    }
224    let hidden = clamp(select(0.0, (depth - near.y / near.z) * per_depth * soft.x * per_world, inner), radius, soft.y);
225    return min(lit, shadow_soft_filter(map, samp, projected, 1, depth, hidden, filter_count, turn, slope));
226}
227
228fn shadow_layered_filter(
229    map: texture_depth_2d_array,
230    samp: sampler_comparison,
231    projected: vec3f,
232    layer: i32,
233    depth: f32,
234    band: f32,
235    width: f32,
236    close: f32,
237    count: u32,
238    turn: f32,
239    slope: vec2f,
240) -> f32 {
241    let size = vec2f(textureDimensions(map));
242    var lit = 0.0;
243    for (var i = 0u; i < count; i++) {
244        let disc = shadow_disc(i, count, turn + 1.7);
245        let far = disc * width;
246        let far_lit = textureSampleCompareLevel(map, samp, projected.xy + far / size, layer, band + dot(slope, far));
247        let near = disc * close;
248        let at = projected.xy + near / size;
249        let shift = dot(slope, near);
250        let near_blocked = textureSampleCompareLevel(map, samp, at, layer, band + shift) - textureSampleCompareLevel(map, samp, at, layer, depth + shift);
251        lit += far_lit * (1.0 - clamp(near_blocked, 0.0, 1.0));
252    }
253    return lit / f32(count);
254}
255
256fn shadow_near(near: vec3f, level: f32, depth: f32) -> vec3f {
257    if (level > near.x + 0.5 * max(depth - level, 0.0)) {
258        return vec3f(level, level, 1.0);
259    }
260    if (level >= near.x - 0.5 * max(depth - near.x, 0.0)) {
261        return vec3f(max(near.x, level), near.y + level, near.z + 1.0);
262    }
263    return near;
264}
265
266fn shadow_soft_filter(
267    map: texture_depth_2d_array,
268    samp: sampler_comparison,
269    projected: vec3f,
270    layer: i32,
271    depth: f32,
272    width: f32,
273    count: u32,
274    turn: f32,
275    slope: vec2f,
276) -> f32 {
277    let size = vec2f(textureDimensions(map));
278    var lit = 0.0;
279    for (var i = 0u; i < count; i++) {
280        let step = shadow_disc(i, count, turn + 1.7) * width;
281        let at = projected.xy + step / size;
282        lit += textureSampleCompareLevel(map, samp, at, layer, depth + dot(slope, step));
283    }
284    return lit / f32(count);
285}
286
287fn shadow_offset(world: vec3f, normal: vec3f, sun: vec3f, filter_params: vec4f, texel: f32, soft: f32) -> vec3f {
288    let len = length(normal);
289    let n = select(vec3f(0.0, 1.0, 0.0), normal / max(len, 1e-8), len > 1e-6);
290    let slen = length(sun);
291    let toward = select(vec3f(0.0, 1.0, 0.0), sun / max(slen, 1e-8), slen > 1e-6);
292    let ndotl = clamp(dot(n, toward), 0.0, 1.0);
293    let slope = sqrt(max(1.0 - ndotl * ndotl, 0.0));
294    let scale = select(1.0, soft, soft >= 0.0 && ndotl * ndotl * 17.0 >= 1.0);
295    return world + n * texel * (filter_params.y + filter_params.z * slope) * scale;
296}
297
298fn shadow_one(
299    map: texture_depth_2d_array,
300    samp: sampler_comparison,
301    gather: sampler,
302    data: ShadowSample,
303    world: vec3f,
304    normal: vec3f,
305    cascade: i32,
306) -> f32 {
307    var view_proj = data.view_proj_0;
308    var texel = data.texels.x;
309    if (cascade == 1) {
310        view_proj = data.view_proj_1;
311        texel = data.texels.y;
312    } else if (cascade == 2) {
313        view_proj = data.view_proj_2;
314        texel = data.texels.z;
315    }
316    var layer = cascade;
317    let soft = select(-1.0, data.soft_taps.w, (use_soft_search || use_soft_filter) && data.soft.x > 0.0);
318    var projected = shadow_project(view_proj, shadow_offset(world, normal, data.sun.xyz, data.filter_params, texel, soft));
319    if ((any(projected < vec3f(0.0)) || any(projected > vec3f(1.0))) && (u32(data.soft_taps.z) & 32u) != 0u && cascade != 2) {
320        view_proj = data.view_proj_2;
321        texel = data.texels.z;
322        layer = 2;
323        projected = shadow_project(view_proj, shadow_offset(world, normal, data.sun.xyz, data.filter_params, texel, soft));
324    }
325    if (any(projected < vec3f(0.0)) || any(projected > vec3f(1.0))) {
326        return 1.0;
327    }
328    if ((use_soft_search || use_soft_filter) && data.soft.x > 0.0) {
329        return shadow_soft(
330            map,
331            samp,
332            gather,
333            view_proj,
334            projected,
335            layer,
336            projected.z - data.sun.w,
337            data.filter_params.x,
338            data.soft,
339            data.soft_taps,
340            world,
341            normal,
342        );
343    }
344    return shadow_pcf(
345        map,
346        samp,
347        projected.xy,
348        layer,
349        projected.z - data.sun.w,
350        data.filter_params.x,
351    );
352}
353
354fn shadow_sample(
355    map: texture_depth_2d_array,
356    samp: sampler_comparison,
357    gather: sampler,
358    data: ShadowSample,
359    world: vec3f,
360    normal: vec3f,
361    view_distance: f32,
362) -> f32 {
363    if ((u32(data.soft_taps.z) & 32u) != 0u) {
364        return shadow_one(map, samp, gather, data, world, normal, 0);
365    }
366    let choice = shadow_select(view_distance, data.splits, data.filter_params.w);
367    if (use_cascade_blend && choice.z > 0.0 && (u32(data.soft_taps.z) & 4u) != 0u) {
368        let dither = fract(shadow_turn(world.zxy) * 0.15915494);
369        return shadow_one(map, samp, gather, data, world, normal, i32(select(choice.x, choice.y, dither < choice.z)));
370    }
371    let lit = shadow_one(map, samp, gather, data, world, normal, i32(choice.x));
372    if (!use_cascade_blend || choice.z <= 0.0) {
373        return lit;
374    }
375    let next = shadow_one(map, samp, gather, data, world, normal, i32(choice.y));
376    return shadow_blend(lit, next, choice.z);
377}
378"#;
379
380const DEPTH_WGSL: &str = r#"
381struct Light {
382    view_proj: mat4x4f,
383}
384
385@group(0) @binding(0) var<uniform> light: Light;
386
387struct VertexIn {
388    @location(0) position: vec3f,
389    @location(1) model0: vec4f,
390    @location(2) model1: vec4f,
391    @location(3) model2: vec4f,
392    @location(4) model3: vec4f,
393}
394
395@vertex
396fn vs_main(in: VertexIn) -> @builtin(position) vec4f {
397    let model = mat4x4f(in.model0, in.model1, in.model2, in.model3);
398    return light.view_proj * model * vec4f(in.position, 1.0);
399}
400"#;
401
402const CREATURE_DEPTH_WGSL: &str = r#"
403struct Light { view_proj: mat4x4f, }
404struct Creature { position_scale: vec4f, right: vec4f, up: vec4f, forward: vec4f, motion: vec4f, }
405@group(0) @binding(0) var<uniform> light: Light;
406@group(1) @binding(0) var<storage, read> vertices: array<vec4f>;
407@group(1) @binding(1) var<storage, read> creatures: array<Creature>;
408@group(1) @binding(2) var<uniform> time: vec4f;
409@vertex fn vs_main(@builtin(vertex_index) vertex: u32, @builtin(instance_index) index: u32) -> @builtin(position) vec4f {
410    let instance = creatures[index];
411    var local = vertices[vertex].xyz;
412    let phase = time.x * (4.0 + instance.motion.y * 1.3) + instance.motion.x;
413    if (instance.motion.z < 0.5) {
414        local.y += sin(phase) * abs(local.x) * 0.5;
415    } else {
416        local.x += sin(phase + local.z * 4.0) * max(-local.z, 0.0) * 0.2;
417    }
418    let world = instance.position_scale.xyz + instance.position_scale.w * (instance.right.xyz * local.x + instance.up.xyz * local.y + instance.forward.xyz * local.z);
419    return light.view_proj * vec4f(world, 1.0);
420}
421"#;
422
423const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
424pub const TRANSMISSION_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Uint;
425
426#[derive(Clone, Copy, Debug, PartialEq)]
427pub struct Mat4 {
428    pub columns: [[f32; 4]; 4],
429}
430
431impl Mat4 {
432    pub const fn identity() -> Self {
433        Self {
434            columns: [
435                [1.0, 0.0, 0.0, 0.0],
436                [0.0, 1.0, 0.0, 0.0],
437                [0.0, 0.0, 1.0, 0.0],
438                [0.0, 0.0, 0.0, 1.0],
439            ],
440        }
441    }
442
443    pub const fn from_columns(columns: [[f32; 4]; 4]) -> Self {
444        Self { columns }
445    }
446
447    pub fn multiply(self, right: Self) -> Self {
448        let mut columns = [[0.0; 4]; 4];
449        for (col, column) in columns.iter_mut().enumerate() {
450            for (row, value) in column.iter_mut().enumerate() {
451                *value = self.columns[0][row] * right.columns[col][0]
452                    + self.columns[1][row] * right.columns[col][1]
453                    + self.columns[2][row] * right.columns[col][2]
454                    + self.columns[3][row] * right.columns[col][3];
455            }
456        }
457        Self { columns }
458    }
459
460    pub fn transform_point(self, point: [f32; 3]) -> [f32; 3] {
461        let mut clip = [0.0; 4];
462        let source = [point[0], point[1], point[2], 1.0];
463        for (row, value) in clip.iter_mut().enumerate() {
464            *value = self.columns[0][row] * source[0]
465                + self.columns[1][row] * source[1]
466                + self.columns[2][row] * source[2]
467                + self.columns[3][row] * source[3];
468        }
469        let scale = if clip[3].abs() < 1.0e-12 {
470            1.0
471        } else {
472            clip[3]
473        };
474        [clip[0] / scale, clip[1] / scale, clip[2] / scale]
475    }
476
477    fn to_bytes(self) -> [u8; 64] {
478        let mut bytes = [0; 64];
479        for (col, column) in self.columns.iter().enumerate() {
480            for (row, value) in column.iter().enumerate() {
481                let start = (col * 4 + row) * 4;
482                bytes[start..start + 4].copy_from_slice(&value.to_le_bytes());
483            }
484        }
485        bytes
486    }
487}
488
489#[derive(Clone, Copy, Debug, PartialEq)]
490pub struct View {
491    pub eye: [f32; 3],
492    pub forward: [f32; 3],
493    pub up: [f32; 3],
494    pub fov_y: f32,
495    pub aspect: f32,
496    pub near: f32,
497    pub far: f32,
498}
499
500#[derive(Clone, Copy, Debug, PartialEq)]
501pub struct Quality {
502    pub resolution: u32,
503    pub lambda: f32,
504    pub blend: f32,
505    pub pcf_radius: f32,
506    pub normal_bias: f32,
507    pub slope_bias: f32,
508    pub depth_bias: f32,
509    pub caster_margin: f32,
510    pub receiver: Option<ReceiverBox>,
511    pub sun_radius_deg: f32,
512    pub blocker_taps: u32,
513    pub filter_taps: u32,
514    pub blocker_gathers: u32,
515    pub nearest_blocker: bool,
516    pub umbra_skip: bool,
517    pub lit_skip: bool,
518    pub blend_dither: bool,
519    pub layered: bool,
520    pub outer_scale: f32,
521    pub near_map: bool,
522    pub soft_offset: f32,
523}
524
525#[derive(Clone, Copy, Debug, PartialEq)]
526pub struct ReceiverBox {
527    pub min: [f32; 3],
528    pub max: [f32; 3],
529}
530
531impl Default for Quality {
532    fn default() -> Self {
533        Self {
534            resolution: DEFAULT_RESOLUTION,
535            lambda: 0.5,
536            blend: 0.1,
537            pcf_radius: 1.0,
538            normal_bias: 1.0,
539            slope_bias: 2.0,
540            depth_bias: 0.0008,
541            caster_margin: 0.0,
542            receiver: None,
543            sun_radius_deg: 0.0,
544            blocker_taps: BLOCKER_TAPS,
545            filter_taps: FILTER_TAPS,
546            blocker_gathers: BLOCKER_GATHERS,
547            nearest_blocker: true,
548            umbra_skip: true,
549            lit_skip: true,
550            blend_dither: true,
551            layered: false,
552            outer_scale: OUTER_SCALE,
553            near_map: true,
554            soft_offset: SOFT_OFFSET,
555        }
556    }
557}
558
559impl Quality {
560    pub fn splits_near_map(&self) -> bool {
561        self.near_map && self.receiver.is_some() && outer(self)
562    }
563}
564
565#[derive(Clone, Copy, Debug, PartialEq)]
566pub struct Choice {
567    pub cascade: usize,
568    pub next: usize,
569    pub blend: f32,
570}
571
572#[derive(Clone, Copy, Debug, PartialEq)]
573pub struct Cascade {
574    pub near: f32,
575    pub far: f32,
576    pub view_proj: Mat4,
577    pub texel: f32,
578    pub light_center: [f32; 3],
579    pub snapped_center: [f32; 3],
580}
581
582#[derive(Clone, Copy, Debug, PartialEq)]
583pub struct Fit {
584    pub splits: [f32; 4],
585    pub cascades: [Cascade; CASCADE_COUNT],
586    pub toward_sun: [f32; 3],
587    pub resolution: u32,
588}
589
590pub struct Caster<'a> {
591    pub positions: &'a wgpu::Buffer,
592    pub indices: &'a wgpu::Buffer,
593    pub first_index: u32,
594    pub index_count: u32,
595    pub instances: &'a [Mat4],
596    pub moving: bool,
597    pub generation: u64,
598}
599
600struct Draw<'a> {
601    positions: &'a wgpu::Buffer,
602    indices: &'a wgpu::Buffer,
603    first_index: u32,
604    index_count: u32,
605    instance_offset: u64,
606    instance_count: u32,
607}
608
609#[derive(PartialEq)]
610struct StaticDraw {
611    positions: u64,
612    indices: u64,
613    first_index: u32,
614    index_count: u32,
615    generation: u64,
616    instances: Vec<u32>,
617}
618
619#[derive(PartialEq)]
620struct StaticKey {
621    sun: [u32; 3],
622    matrices: [[u32; 16]; CASCADE_COUNT],
623    draws: Vec<StaticDraw>,
624    content: u64,
625    near_map: bool,
626}
627
628pub type LayerDraw<'d> = dyn FnMut(&mut wgpu::RenderPass<'_>, usize, bool) + 'd;
629
630struct Transmission {
631    size: u32,
632    tiles: FaceTiles,
633    map: wgpu::Texture,
634    _depth: wgpu::Texture,
635    view: wgpu::TextureView,
636    layers: Vec<wgpu::TextureView>,
637    depth_layers: Vec<wgpu::TextureView>,
638    key: Option<StaticKey>,
639}
640
641#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
642pub struct FaceTiles {
643    pub faces: u32,
644    pub tile: u32,
645    pub columns: u32,
646}
647
648impl FaceTiles {
649    pub fn new(faces: usize, size: u32, face_resolution: u32) -> Self {
650        if faces == 0 || size == 0 {
651            return Self::default();
652        }
653        let tile = transmission_size(face_resolution).min(size).max(1);
654        Self {
655            faces: faces as u32,
656            tile,
657            columns: (size / tile).max(1),
658        }
659    }
660
661    pub fn per_layer(&self) -> u32 {
662        self.columns * self.columns
663    }
664
665    pub fn layers(&self) -> u32 {
666        if self.faces == 0 {
667            0
668        } else {
669            self.faces.div_ceil(self.per_layer())
670        }
671    }
672
673    pub fn place(&self, face: u32) -> (u32, [u32; 2]) {
674        let per = self.per_layer().max(1);
675        let slot = face % per;
676        (
677            CASCADE_COUNT as u32 + face / per,
678            [
679                slot % self.columns.max(1) * self.tile,
680                slot / self.columns.max(1) * self.tile,
681            ],
682        )
683    }
684}
685
686pub struct Shadows {
687    quality: Cell<Quality>,
688    resolution: u32,
689    atlas: wgpu::Texture,
690    atlas_view: wgpu::TextureView,
691    layers: [wgpu::TextureView; CASCADE_COUNT],
692    backup: wgpu::Texture,
693    pipeline: wgpu::RenderPipeline,
694    creature_pipeline: wgpu::RenderPipeline,
695    creature_layout: wgpu::BindGroupLayout,
696    compare: wgpu::Sampler,
697    bind: wgpu::BindGroup,
698    uniform: wgpu::Buffer,
699    align: u32,
700    instances: wgpu::Buffer,
701    instance_capacity: u64,
702    cache: Option<StaticKey>,
703    atlas_clean: bool,
704    reused: bool,
705    transmission: Option<Transmission>,
706}
707
708fn outer(quality: &Quality) -> bool {
709    finite(quality.outer_scale) > 1.0
710}
711
712pub fn transmission_size(resolution: u32) -> u32 {
713    (resolution / 4).max(256).min(resolution)
714}
715
716pub fn splits(near: f32, far: f32, lambda: f32) -> [f32; 4] {
717    let near = finite(near).max(1.0e-4);
718    let far = finite(far).max(near + 1.0e-3);
719    let lambda = finite(lambda).clamp(0.0, 1.0);
720    let ratio = far / near;
721    let mut out = [near, 0.0, 0.0, far];
722    for (index, slot) in out.iter_mut().enumerate().skip(1).take(CASCADE_COUNT - 1) {
723        let portion = index as f32 / CASCADE_COUNT as f32;
724        let logarithmic = near * ratio.powf(portion);
725        let uniform = near + (far - near) * portion;
726        *slot = lambda * logarithmic + (1.0 - lambda) * uniform;
727    }
728    out
729}
730
731pub fn select_cascade(distance: f32, splits: [f32; 4], blend: f32) -> Choice {
732    let blend = finite(blend).clamp(0.0, 1.0);
733    let mut cascade = 0;
734    if distance > splits[1] {
735        cascade = 1;
736    }
737    if distance > splits[2] {
738        cascade = 2;
739    }
740    if cascade < CASCADE_COUNT - 1 {
741        let start = splits[cascade];
742        let end = splits[cascade + 1];
743        let band = (end - start) * blend;
744        if band > 0.0 && distance > end - band {
745            let t = ((distance - (end - band)) / band).clamp(0.0, 1.0);
746            return Choice {
747                cascade,
748                next: cascade + 1,
749                blend: t,
750            };
751        }
752    }
753    Choice {
754        cascade,
755        next: cascade,
756        blend: 0.0,
757    }
758}
759
760pub fn light_basis(toward_sun: [f32; 3]) -> [[f32; 3]; 3] {
761    let forward = if length(toward_sun) < 1.0e-8 {
762        [0.0, -1.0, 0.0]
763    } else {
764        normalize(scale(toward_sun, -1.0))
765    };
766    let helper = least_aligned(forward);
767    let right = normalize(cross(forward, helper));
768    let up = cross(right, forward);
769    [right, up, forward]
770}
771
772pub fn normal_offset(
773    world: [f32; 3],
774    normal: [f32; 3],
775    toward_sun: [f32; 3],
776    texel: f32,
777    normal_bias: f32,
778    slope_bias: f32,
779) -> [f32; 3] {
780    let normal = if length(normal) > 1.0e-6 {
781        normalize(normal)
782    } else {
783        [0.0, 1.0, 0.0]
784    };
785    let toward = if length(toward_sun) > 1.0e-6 {
786        normalize(toward_sun)
787    } else {
788        [0.0, 1.0, 0.0]
789    };
790    let ndotl = dot(normal, toward).clamp(0.0, 1.0);
791    let slope = (1.0 - ndotl * ndotl).max(0.0).sqrt();
792    add(
793        world,
794        scale(normal, texel * (normal_bias + slope_bias * slope)),
795    )
796}
797
798pub fn frustum_corners(view: &View, near: f32, far: f32) -> [[f32; 3]; 8] {
799    let [right, up, forward] = camera_axes(view);
800    let (fov_y, aspect, _, _) = camera_limits(view);
801    let tan_y = (fov_y * 0.5).tan();
802    let tan_x = tan_y * aspect;
803    let mut corners = [[0.0; 3]; 8];
804    for (plane, distance) in [near, far].into_iter().enumerate() {
805        let center = add(view.eye, scale(forward, distance));
806        let hx = tan_x * distance;
807        let hy = tan_y * distance;
808        for (corner, (sx, sy)) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)]
809            .into_iter()
810            .enumerate()
811        {
812            corners[plane * 4 + corner] =
813                add(center, add(scale(right, sx * hx), scale(up, sy * hy)));
814        }
815    }
816    corners
817}
818
819pub fn camera_view_proj(view: &View) -> Mat4 {
820    let [right, up, forward] = camera_axes(view);
821    let (fov_y, aspect, near, far) = camera_limits(view);
822    perspective(fov_y, aspect, near, far).multiply(view_matrix(right, up, forward, view.eye))
823}
824
825pub fn fit(view: &View, toward_sun: [f32; 3], quality: &Quality) -> Fit {
826    let (fov_y, aspect, near, far) = camera_limits(view);
827    let distances = splits(near, far, quality.lambda);
828    let [_, _, forward] = camera_axes(view);
829    let [light_right, light_up, light_forward] = light_basis(toward_sun);
830    let resolution = quality.resolution.max(2);
831    let light_view = view_matrix(light_right, light_up, light_forward, [0.0, 0.0, 0.0]);
832    let mut cascades = [Cascade {
833        near: distances[0],
834        far: distances[1],
835        view_proj: Mat4::identity(),
836        texel: 1.0,
837        light_center: [0.0; 3],
838        snapped_center: [0.0; 3],
839    }; CASCADE_COUNT];
840    for (index, cascade) in cascades.iter_mut().enumerate() {
841        let slice_near = distances[index];
842        let slice_far = distances[index + 1];
843        let fit_near = if index == 0 {
844            slice_near
845        } else {
846            let previous = distances[index - 1];
847            slice_near - (slice_near - previous) * finite(quality.blend).clamp(0.0, 1.0)
848        };
849        if let Some(receiver) = quality.receiver {
850            let grow = if index == CASCADE_COUNT - 1 && outer(quality) {
851                finite(quality.outer_scale)
852            } else {
853                1.0
854            };
855            let mut lo = [f32::INFINITY; 3];
856            let mut hi = [f32::NEG_INFINITY; 3];
857            for x in [receiver.min[0], receiver.max[0]] {
858                for y in [receiver.min[1], receiver.max[1]] {
859                    for z in [receiver.min[2], receiver.max[2]] {
860                        let point = [x, y, z];
861                        let light = [
862                            dot(light_right, point),
863                            dot(light_up, point),
864                            -dot(light_forward, point),
865                        ];
866                        for axis in 0..3 {
867                            lo[axis] = lo[axis].min(light[axis]);
868                            hi[axis] = hi[axis].max(light[axis]);
869                        }
870                    }
871                }
872            }
873            let center = [
874                (lo[0] + hi[0]) * 0.5,
875                (lo[1] + hi[1]) * 0.5,
876                (lo[2] + hi[2]) * 0.5,
877            ];
878            if grow != 1.0 {
879                for axis in 0..3 {
880                    lo[axis] = center[axis] + (lo[axis] - center[axis]) * grow;
881                    hi[axis] = center[axis] + (hi[axis] - center[axis]) * grow;
882                }
883            }
884            let span_x = (hi[0] - lo[0]).max(1.0e-4);
885            let span_y = (hi[1] - lo[1]).max(1.0e-4);
886            let texel = span_x.max(span_y) / (resolution as f32 - 3.0).max(1.0);
887            let snapped = [snap(center[0], texel), snap(center[1], texel), center[2]];
888            let half_x = span_x * 0.5 + texel * 1.5;
889            let half_y = span_y * 0.5 + texel * 1.5;
890            let z_lo = lo[2] - texel;
891            let z_hi = hi[2] + texel + quality.caster_margin.max(0.0);
892            let projection = ortho(
893                snapped[0] - half_x,
894                snapped[0] + half_x,
895                snapped[1] - half_y,
896                snapped[1] + half_y,
897                -z_hi,
898                -z_lo,
899            );
900            *cascade = Cascade {
901                near: slice_near,
902                far: slice_far,
903                view_proj: projection.multiply(light_view),
904                texel,
905                light_center: center,
906                snapped_center: snapped,
907            };
908            continue;
909        }
910        let (center_distance, radius) = slice_sphere(fit_near, slice_far, fov_y, aspect);
911        let center = add(view.eye, scale(forward, center_distance));
912        let light_center = [
913            dot(light_right, center),
914            dot(light_up, center),
915            -dot(light_forward, center),
916        ];
917        let texel = (2.0 * radius) / (resolution as f32 - 1.0).max(1.0);
918        let half = radius + texel * 0.5;
919        let snapped = [
920            snap(light_center[0], texel),
921            snap(light_center[1], texel),
922            snap(light_center[2], texel),
923        ];
924        let margin = half + quality.caster_margin.max(0.0);
925        let z_lo = snapped[2] - half;
926        let z_hi = snapped[2] + margin;
927        let projection = ortho(
928            snapped[0] - half,
929            snapped[0] + half,
930            snapped[1] - half,
931            snapped[1] + half,
932            -z_hi,
933            -z_lo,
934        );
935        *cascade = Cascade {
936            near: slice_near,
937            far: slice_far,
938            view_proj: projection.multiply(light_view),
939            texel,
940            light_center,
941            snapped_center: snapped,
942        };
943    }
944    Fit {
945        splits: distances,
946        cascades,
947        toward_sun: normalize(toward_sun),
948        resolution,
949    }
950}
951
952pub fn sample_uniform(fit: &Fit, quality: &Quality) -> [u8; SAMPLE_UNIFORM_BYTES] {
953    let mut bytes = [0; SAMPLE_UNIFORM_BYTES];
954    let radius = finite(quality.sun_radius_deg).clamp(0.0, 10.0);
955    write_vec4(&mut bytes, 0, fit.splits);
956    write_vec4(
957        &mut bytes,
958        16,
959        [
960            quality.pcf_radius.max(0.0),
961            quality.normal_bias.max(0.0),
962            quality.slope_bias.max(0.0),
963            quality.blend.clamp(0.0, 1.0),
964        ],
965    );
966    write_vec4(
967        &mut bytes,
968        32,
969        [
970            fit.toward_sun[0],
971            fit.toward_sun[1],
972            fit.toward_sun[2],
973            quality.depth_bias.max(0.0),
974        ],
975    );
976    write_vec4(
977        &mut bytes,
978        48,
979        [
980            fit.cascades[0].texel,
981            fit.cascades[1].texel,
982            fit.cascades[2].texel,
983            0.0,
984        ],
985    );
986    for (index, cascade) in fit.cascades.iter().enumerate() {
987        let start = 64 + index * 64;
988        bytes[start..start + 64].copy_from_slice(&cascade.view_proj.to_bytes());
989    }
990    let flags = u8::from(quality.nearest_blocker)
991        | u8::from(quality.umbra_skip) << 1
992        | u8::from(quality.blend_dither) << 2
993        | u8::from(!quality.lit_skip) << 3
994        | u8::from(quality.layered) << 4
995        | u8::from(quality.receiver.is_some() && outer(quality)) << 5
996        | u8::from(quality.splits_near_map()) << 6;
997    write_vec4(&mut bytes, 272, [0.0, 0.0, f32::from(flags), 0.0]);
998    if radius > 0.0 {
999        write_vec4(
1000            &mut bytes,
1001            256,
1002            [
1003                radius.to_radians().tan(),
1004                SOFT_REACH_TEXELS,
1005                quality.blocker_taps.clamp(1, SOFT_TAP_LIMIT) as f32,
1006                SOFT_SLOPE_LIMIT,
1007            ],
1008        );
1009        write_vec4(
1010            &mut bytes,
1011            272,
1012            [
1013                quality.blocker_gathers.min(SOFT_TAP_LIMIT) as f32,
1014                quality.filter_taps.clamp(1, SOFT_TAP_LIMIT) as f32,
1015                f32::from(flags),
1016                finite(quality.soft_offset).max(0.0),
1017            ],
1018        );
1019    }
1020    bytes
1021}
1022
1023impl Shadows {
1024    pub fn new(device: &wgpu::Device, quality: Quality) -> Self {
1025        let resolution = quality
1026            .resolution
1027            .clamp(2, device.limits().max_texture_dimension_2d);
1028        let quality = Quality {
1029            resolution,
1030            ..quality
1031        };
1032        let size = wgpu::Extent3d {
1033            width: resolution,
1034            height: resolution,
1035            depth_or_array_layers: CASCADE_COUNT as u32,
1036        };
1037        let atlas = depth_texture(device, size, "shadow atlas");
1038        let backup = depth_texture(device, size, "shadow static");
1039        let atlas_view = array_view(&atlas);
1040        let layers = [0, 1, 2].map(|layer| layer_view(&atlas, layer));
1041        let align = device.limits().min_uniform_buffer_offset_alignment.max(256);
1042        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
1043            label: Some("shadow light"),
1044            size: u64::from(align) * CASCADE_COUNT as u64,
1045            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1046            mapped_at_creation: false,
1047        });
1048        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1049            label: Some("shadow light"),
1050            entries: &light_entries(),
1051        });
1052        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1053            label: Some("shadow light"),
1054            layout: &bind_layout,
1055            entries: &[wgpu::BindGroupEntry {
1056                binding: 0,
1057                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1058                    buffer: &uniform,
1059                    offset: 0,
1060                    size: NonZeroU64::new(64),
1061                }),
1062            }],
1063        });
1064        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1065            label: Some("shadow"),
1066            bind_group_layouts: &[Some(&bind_layout)],
1067            immediate_size: 0,
1068        });
1069        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1070            label: Some("shadow depth"),
1071            source: wgpu::ShaderSource::Wgsl(DEPTH_WGSL.into()),
1072        });
1073        let compare = device.create_sampler(&wgpu::SamplerDescriptor {
1074            label: Some("shadow compare"),
1075            address_mode_u: wgpu::AddressMode::ClampToEdge,
1076            address_mode_v: wgpu::AddressMode::ClampToEdge,
1077            address_mode_w: wgpu::AddressMode::ClampToEdge,
1078            mag_filter: wgpu::FilterMode::Nearest,
1079            min_filter: wgpu::FilterMode::Nearest,
1080            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
1081            compare: Some(wgpu::CompareFunction::LessEqual),
1082            ..Default::default()
1083        });
1084        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1085            label: Some("shadow depth"),
1086            layout: Some(&pipeline_layout),
1087            vertex: wgpu::VertexState {
1088                module: &shader,
1089                entry_point: Some("vs_main"),
1090                compilation_options: wgpu::PipelineCompilationOptions::default(),
1091                buffers: &[
1092                    Some(wgpu::VertexBufferLayout {
1093                        array_stride: 12,
1094                        step_mode: wgpu::VertexStepMode::Vertex,
1095                        attributes: &[wgpu::VertexAttribute {
1096                            format: wgpu::VertexFormat::Float32x3,
1097                            offset: 0,
1098                            shader_location: 0,
1099                        }],
1100                    }),
1101                    Some(wgpu::VertexBufferLayout {
1102                        array_stride: 64,
1103                        step_mode: wgpu::VertexStepMode::Instance,
1104                        attributes: &[
1105                            wgpu::VertexAttribute {
1106                                format: wgpu::VertexFormat::Float32x4,
1107                                offset: 0,
1108                                shader_location: 1,
1109                            },
1110                            wgpu::VertexAttribute {
1111                                format: wgpu::VertexFormat::Float32x4,
1112                                offset: 16,
1113                                shader_location: 2,
1114                            },
1115                            wgpu::VertexAttribute {
1116                                format: wgpu::VertexFormat::Float32x4,
1117                                offset: 32,
1118                                shader_location: 3,
1119                            },
1120                            wgpu::VertexAttribute {
1121                                format: wgpu::VertexFormat::Float32x4,
1122                                offset: 48,
1123                                shader_location: 4,
1124                            },
1125                        ],
1126                    }),
1127                ],
1128            },
1129            primitive: wgpu::PrimitiveState {
1130                topology: wgpu::PrimitiveTopology::TriangleList,
1131                cull_mode: None,
1132                ..Default::default()
1133            },
1134            depth_stencil: Some(wgpu::DepthStencilState {
1135                format: DEPTH_FORMAT,
1136                depth_write_enabled: Some(true),
1137                depth_compare: Some(wgpu::CompareFunction::Less),
1138                stencil: wgpu::StencilState::default(),
1139                bias: wgpu::DepthBiasState::default(),
1140            }),
1141            multisample: wgpu::MultisampleState::default(),
1142            fragment: None,
1143            multiview_mask: None,
1144            cache: None,
1145        });
1146        let creature_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1147            label: Some("creature shadow data"),
1148            entries: &creature_entries(),
1149        });
1150        let creature_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1151            label: Some("creature shadow depth"),
1152            source: wgpu::ShaderSource::Wgsl(CREATURE_DEPTH_WGSL.into()),
1153        });
1154        let creature_pipeline_layout =
1155            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1156                label: Some("creature shadow depth"),
1157                bind_group_layouts: &[Some(&bind_layout), Some(&creature_layout)],
1158                immediate_size: 0,
1159            });
1160        let creature_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1161            label: Some("creature shadow depth"),
1162            layout: Some(&creature_pipeline_layout),
1163            vertex: wgpu::VertexState {
1164                module: &creature_shader,
1165                entry_point: Some("vs_main"),
1166                compilation_options: Default::default(),
1167                buffers: &[],
1168            },
1169            primitive: wgpu::PrimitiveState {
1170                topology: wgpu::PrimitiveTopology::TriangleList,
1171                cull_mode: None,
1172                ..Default::default()
1173            },
1174            depth_stencil: Some(wgpu::DepthStencilState {
1175                format: DEPTH_FORMAT,
1176                depth_write_enabled: Some(true),
1177                depth_compare: Some(wgpu::CompareFunction::Less),
1178                stencil: wgpu::StencilState::default(),
1179                bias: wgpu::DepthBiasState::default(),
1180            }),
1181            multisample: Default::default(),
1182            fragment: None,
1183            multiview_mask: None,
1184            cache: None,
1185        });
1186        let instance_capacity = 256 * 64;
1187        let instances = device.create_buffer(&wgpu::BufferDescriptor {
1188            label: Some("shadow instances"),
1189            size: instance_capacity,
1190            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1191            mapped_at_creation: false,
1192        });
1193        Self {
1194            quality: Cell::new(quality),
1195            resolution,
1196            atlas,
1197            atlas_view,
1198            layers,
1199            backup,
1200            pipeline,
1201            creature_pipeline,
1202            creature_layout,
1203            compare,
1204            bind,
1205            uniform,
1206            align,
1207            instances,
1208            instance_capacity,
1209            cache: None,
1210            atlas_clean: false,
1211            reused: false,
1212            transmission: None,
1213        }
1214    }
1215
1216    pub fn fit(&self, view: &View, toward_sun: [f32; 3]) -> Fit {
1217        fit(view, toward_sun, &self.quality.get())
1218    }
1219
1220    pub fn resolution(&self) -> u32 {
1221        self.resolution
1222    }
1223
1224    pub fn quality(&self) -> Quality {
1225        self.quality.get()
1226    }
1227
1228    pub fn set_quality(&self, quality: Quality) -> Result<(), String> {
1229        if quality.resolution != self.resolution {
1230            return Err("shadow resolution needs a new atlas".into());
1231        }
1232        self.quality.set(quality);
1233        Ok(())
1234    }
1235
1236    pub fn update_quality(&mut self, device: &wgpu::Device, quality: Quality) {
1237        let resolution = quality
1238            .resolution
1239            .clamp(2, device.limits().max_texture_dimension_2d);
1240        if resolution != self.resolution {
1241            let size = wgpu::Extent3d {
1242                width: resolution,
1243                height: resolution,
1244                depth_or_array_layers: CASCADE_COUNT as u32,
1245            };
1246            self.atlas = depth_texture(device, size, "shadow atlas");
1247            self.backup = depth_texture(device, size, "shadow static");
1248            self.atlas_view = array_view(&self.atlas);
1249            self.layers = [0, 1, 2].map(|layer| layer_view(&self.atlas, layer));
1250            self.resolution = resolution;
1251            self.cache = None;
1252            self.atlas_clean = false;
1253            self.reused = false;
1254            self.transmission = None;
1255        }
1256        self.quality.set(Quality {
1257            resolution,
1258            ..quality
1259        });
1260    }
1261
1262    pub fn atlas(&self) -> &wgpu::Texture {
1263        &self.atlas
1264    }
1265
1266    pub fn atlas_view(&self) -> &wgpu::TextureView {
1267        &self.atlas_view
1268    }
1269
1270    pub fn sampler(&self) -> &wgpu::Sampler {
1271        &self.compare
1272    }
1273
1274    pub fn sample_uniform(&self, fit: &Fit) -> [u8; SAMPLE_UNIFORM_BYTES] {
1275        sample_uniform(fit, &self.quality.get())
1276    }
1277
1278    pub fn reused_static(&self) -> bool {
1279        self.reused
1280    }
1281
1282    pub fn render(
1283        &mut self,
1284        device: &wgpu::Device,
1285        queue: &wgpu::Queue,
1286        encoder: &mut wgpu::CommandEncoder,
1287        fit: &Fit,
1288        casters: &[Caster<'_>],
1289    ) {
1290        self.render_timed(device, queue, encoder, fit, casters, None);
1291    }
1292
1293    pub fn render_creatures(
1294        &mut self,
1295        gpu: &Gpu,
1296        encoder: &mut wgpu::CommandEncoder,
1297        fit: &Fit,
1298        mesh: &CreatureMesh,
1299        creatures: &[CreatureInstance],
1300        time: f32,
1301    ) {
1302        let (vertices, indices, index_count) = mesh.shadow_buffers();
1303        if creatures.is_empty() || index_count == 0 {
1304            return;
1305        }
1306        for (index, cascade) in fit.cascades.iter().enumerate() {
1307            gpu.queue.write_buffer(
1308                &self.uniform,
1309                u64::from(self.align) * index as u64,
1310                &cascade.view_proj.to_bytes(),
1311            );
1312        }
1313        let instances = gpu
1314            .device
1315            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1316                label: Some("creature shadow instances"),
1317                contents: bytemuck::cast_slice(creatures),
1318                usage: wgpu::BufferUsages::STORAGE,
1319            });
1320        let time = gpu
1321            .device
1322            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1323                label: Some("creature shadow time"),
1324                contents: bytemuck::cast_slice(&[time, 0.0_f32, 0.0, 0.0]),
1325                usage: wgpu::BufferUsages::UNIFORM,
1326            });
1327        let bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1328            label: Some("creature shadow data"),
1329            layout: &self.creature_layout,
1330            entries: &[
1331                wgpu::BindGroupEntry {
1332                    binding: 0,
1333                    resource: vertices.as_entire_binding(),
1334                },
1335                wgpu::BindGroupEntry {
1336                    binding: 1,
1337                    resource: instances.as_entire_binding(),
1338                },
1339                wgpu::BindGroupEntry {
1340                    binding: 2,
1341                    resource: time.as_entire_binding(),
1342                },
1343            ],
1344        });
1345        for (index, layer) in self.layers.iter().enumerate() {
1346            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1347                multiview_mask: None,
1348                label: Some("creature shadow cascade"),
1349                color_attachments: &[],
1350                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1351                    view: layer,
1352                    depth_ops: Some(wgpu::Operations {
1353                        load: wgpu::LoadOp::Load,
1354                        store: wgpu::StoreOp::Store,
1355                    }),
1356                    stencil_ops: None,
1357                }),
1358                timestamp_writes: None,
1359                occlusion_query_set: None,
1360            });
1361            pass.set_pipeline(&self.creature_pipeline);
1362            pass.set_bind_group(0, &self.bind, &[self.align * index as u32]);
1363            pass.set_bind_group(1, &bind, &[]);
1364            pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
1365            pass.draw_indexed(0..index_count, 0, 0..creatures.len() as u32);
1366        }
1367        self.atlas_clean = false;
1368    }
1369
1370    pub fn render_timed(
1371        &mut self,
1372        device: &wgpu::Device,
1373        queue: &wgpu::Queue,
1374        encoder: &mut wgpu::CommandEncoder,
1375        fit: &Fit,
1376        casters: &[Caster<'_>],
1377        mut profiler: Option<&mut GpuProfiler>,
1378    ) {
1379        let key = static_key(fit, casters);
1380        let hit = self.cache.as_ref() == Some(&key);
1381        if hit && self.atlas_clean && !casters.iter().any(is_moving) {
1382            self.reused = true;
1383            return;
1384        }
1385        let mut bytes = Vec::new();
1386        let mut static_draws = Vec::new();
1387        let mut moving_draws = Vec::new();
1388        for caster in casters {
1389            if caster.index_count == 0 || caster.instances.is_empty() {
1390                continue;
1391            }
1392            if hit && !caster.moving {
1393                continue;
1394            }
1395            let offset = bytes.len() as u64;
1396            for instance in caster.instances {
1397                bytes.extend_from_slice(&instance.to_bytes());
1398            }
1399            let draw = Draw {
1400                positions: caster.positions,
1401                indices: caster.indices,
1402                first_index: caster.first_index,
1403                index_count: caster.index_count,
1404                instance_offset: offset,
1405                instance_count: u32::try_from(caster.instances.len()).unwrap_or(u32::MAX),
1406            };
1407            if caster.moving {
1408                moving_draws.push(draw);
1409            } else {
1410                static_draws.push(draw);
1411            }
1412        }
1413        self.ensure_instances(device, bytes.len() as u64);
1414        if !bytes.is_empty() {
1415            queue.write_buffer(&self.instances, 0, &bytes);
1416        }
1417        for (index, cascade) in fit.cascades.iter().enumerate() {
1418            queue.write_buffer(
1419                &self.uniform,
1420                u64::from(self.align) * index as u64,
1421                &cascade.view_proj.to_bytes(),
1422            );
1423        }
1424        self.reused = hit;
1425        if hit {
1426            if !self.atlas_clean {
1427                copy_depth(encoder, &self.backup, &self.atlas, self.resolution);
1428            }
1429            self.encode(
1430                encoder,
1431                &moving_draws,
1432                wgpu::LoadOp::Load,
1433                profiler.as_deref_mut(),
1434            );
1435            self.atlas_clean = moving_draws.is_empty();
1436            return;
1437        }
1438        self.encode(
1439            encoder,
1440            &static_draws,
1441            wgpu::LoadOp::Clear(1.0),
1442            profiler.as_deref_mut(),
1443        );
1444        copy_depth(encoder, &self.atlas, &self.backup, self.resolution);
1445        self.encode(encoder, &moving_draws, wgpu::LoadOp::Load, profiler);
1446        self.atlas_clean = moving_draws.is_empty();
1447        self.cache = Some(key);
1448    }
1449
1450    pub fn render_with(
1451        &mut self,
1452        encoder: &mut wgpu::CommandEncoder,
1453        fit: &Fit,
1454        content: u64,
1455        moving: bool,
1456        draw: &mut LayerDraw<'_>,
1457        mut profiler: Option<&mut GpuProfiler>,
1458    ) {
1459        let mut key = static_key(fit, &[]);
1460        key.content = content;
1461        key.near_map = self.quality.get().splits_near_map();
1462        let hit = self.cache.as_ref() == Some(&key);
1463        if hit && self.atlas_clean && !moving {
1464            self.reused = true;
1465            return;
1466        }
1467        self.reused = hit;
1468        if hit {
1469            if !self.atlas_clean {
1470                copy_depth(encoder, &self.backup, &self.atlas, self.resolution);
1471            }
1472        } else {
1473            self.encode_layers(
1474                encoder,
1475                wgpu::LoadOp::Clear(1.0),
1476                true,
1477                draw,
1478                profiler.as_deref_mut(),
1479            );
1480            copy_depth(encoder, &self.atlas, &self.backup, self.resolution);
1481            self.cache = Some(key);
1482        }
1483        if moving {
1484            self.encode_layers(encoder, wgpu::LoadOp::Load, false, draw, profiler);
1485        }
1486        self.atlas_clean = !moving;
1487    }
1488
1489    pub fn transmission_view(&self) -> Option<&wgpu::TextureView> {
1490        self.transmission
1491            .as_ref()
1492            .map(|transmission| &transmission.view)
1493    }
1494
1495    pub fn transmission_texture(&self) -> Option<&wgpu::Texture> {
1496        self.transmission
1497            .as_ref()
1498            .map(|transmission| &transmission.map)
1499    }
1500
1501    pub fn face_tiles(&self) -> FaceTiles {
1502        self.transmission
1503            .as_ref()
1504            .map_or_else(FaceTiles::default, |transmission| transmission.tiles)
1505    }
1506
1507    #[allow(clippy::too_many_arguments)]
1508    pub fn render_transmission(
1509        &mut self,
1510        device: &wgpu::Device,
1511        encoder: &mut wgpu::CommandEncoder,
1512        fit: &Fit,
1513        content: u64,
1514        moving: bool,
1515        faces: (usize, u32),
1516        draw: &mut LayerDraw<'_>,
1517        mut profiler: Option<&mut GpuProfiler>,
1518    ) {
1519        let size = transmission_size(self.resolution);
1520        let tiles = FaceTiles::new(faces.0, size, faces.1);
1521        let mut key = static_key(fit, &[]);
1522        let mut hasher = std::collections::hash_map::DefaultHasher::new();
1523        content.hash(&mut hasher);
1524        tiles.hash(&mut hasher);
1525        key.content = hasher.finish();
1526        if self
1527            .transmission
1528            .as_ref()
1529            .is_none_or(|transmission| transmission.size != size || transmission.tiles != tiles)
1530        {
1531            self.transmission = Some(transmission_maps(device, size, tiles));
1532        }
1533        let Some(transmission) = self.transmission.as_mut() else {
1534            return;
1535        };
1536        if !moving && transmission.key.as_ref() == Some(&key) {
1537            return;
1538        }
1539        let timing = profiler
1540            .as_deref_mut()
1541            .and_then(|timer| timer.pass("shadow transmission"));
1542        for (index, (layer, depth)) in transmission
1543            .layers
1544            .iter()
1545            .zip(&transmission.depth_layers)
1546            .take(CASCADE_COUNT)
1547            .enumerate()
1548        {
1549            let writes = profiler
1550                .as_deref()
1551                .and_then(|timer| timer.render_writes(timing))
1552                .map(|writes| wgpu::RenderPassTimestampWrites {
1553                    query_set: writes.query_set,
1554                    beginning_of_pass_write_index: writes
1555                        .beginning_of_pass_write_index
1556                        .filter(|_| index == 0),
1557                    end_of_pass_write_index: writes
1558                        .end_of_pass_write_index
1559                        .filter(|_| index + 1 == CASCADE_COUNT),
1560                })
1561                .filter(|writes| {
1562                    writes.beginning_of_pass_write_index.is_some()
1563                        || writes.end_of_pass_write_index.is_some()
1564                });
1565            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1566                multiview_mask: None,
1567                label: Some("shadow transmission"),
1568                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1569                    view: layer,
1570                    resolve_target: None,
1571                    depth_slice: None,
1572                    ops: wgpu::Operations {
1573                        load: wgpu::LoadOp::Clear(wgpu::Color {
1574                            r: f64::from(u32::MAX),
1575                            g: f64::from(1.0f32.to_bits()),
1576                            b: 0.0,
1577                            a: 0.0,
1578                        }),
1579                        store: wgpu::StoreOp::Store,
1580                    },
1581                })],
1582                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1583                    view: depth,
1584                    depth_ops: Some(wgpu::Operations {
1585                        load: wgpu::LoadOp::Clear(1.0),
1586                        store: wgpu::StoreOp::Discard,
1587                    }),
1588                    stencil_ops: None,
1589                }),
1590                timestamp_writes: writes,
1591                occlusion_query_set: None,
1592            });
1593            draw(&mut pass, index, true);
1594            draw(&mut pass, index, false);
1595        }
1596        let light_timing = (tiles.faces > 0)
1597            .then(|| {
1598                profiler
1599                    .as_deref_mut()
1600                    .and_then(|timer| timer.pass("light transmission"))
1601            })
1602            .flatten();
1603        let extra = tiles.layers() as usize;
1604        for (index, (layer, depth)) in transmission
1605            .layers
1606            .iter()
1607            .zip(&transmission.depth_layers)
1608            .skip(CASCADE_COUNT)
1609            .enumerate()
1610        {
1611            let writes = profiler
1612                .as_deref()
1613                .and_then(|timer| timer.render_writes(light_timing))
1614                .map(|writes| wgpu::RenderPassTimestampWrites {
1615                    query_set: writes.query_set,
1616                    beginning_of_pass_write_index: writes
1617                        .beginning_of_pass_write_index
1618                        .filter(|_| index == 0),
1619                    end_of_pass_write_index: writes
1620                        .end_of_pass_write_index
1621                        .filter(|_| index + 1 == extra),
1622                })
1623                .filter(|writes| {
1624                    writes.beginning_of_pass_write_index.is_some()
1625                        || writes.end_of_pass_write_index.is_some()
1626                });
1627            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1628                multiview_mask: None,
1629                label: Some("light transmission"),
1630                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1631                    view: layer,
1632                    resolve_target: None,
1633                    depth_slice: None,
1634                    ops: wgpu::Operations {
1635                        load: wgpu::LoadOp::Clear(wgpu::Color {
1636                            r: f64::from(u32::MAX),
1637                            g: f64::from(1.0f32.to_bits()),
1638                            b: 0.0,
1639                            a: 0.0,
1640                        }),
1641                        store: wgpu::StoreOp::Store,
1642                    },
1643                })],
1644                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1645                    view: depth,
1646                    depth_ops: Some(wgpu::Operations {
1647                        load: wgpu::LoadOp::Clear(1.0),
1648                        store: wgpu::StoreOp::Discard,
1649                    }),
1650                    stencil_ops: None,
1651                }),
1652                timestamp_writes: writes,
1653                occlusion_query_set: None,
1654            });
1655            let first = index as u32 * tiles.per_layer();
1656            for face in first..(first + tiles.per_layer()).min(tiles.faces) {
1657                let (_, origin) = tiles.place(face);
1658                pass.set_viewport(
1659                    origin[0] as f32,
1660                    origin[1] as f32,
1661                    tiles.tile as f32,
1662                    tiles.tile as f32,
1663                    0.0,
1664                    1.0,
1665                );
1666                pass.set_scissor_rect(origin[0], origin[1], tiles.tile, tiles.tile);
1667                draw(&mut pass, CASCADE_COUNT + face as usize, false);
1668            }
1669        }
1670        transmission.key = Some(key);
1671    }
1672
1673    pub fn render_impostors(
1674        &mut self,
1675        encoder: &mut wgpu::CommandEncoder,
1676        props: &crate::impostor::ImpostorPass,
1677        count: usize,
1678    ) -> Result<(), String> {
1679        if count == 0 {
1680            return Ok(());
1681        }
1682        for (index, layer) in self.layers.iter().enumerate() {
1683            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1684                multiview_mask: None,
1685                label: Some("impostor shadow cascade"),
1686                color_attachments: &[],
1687                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1688                    view: layer,
1689                    depth_ops: Some(wgpu::Operations {
1690                        load: wgpu::LoadOp::Load,
1691                        store: wgpu::StoreOp::Store,
1692                    }),
1693                    stencil_ops: None,
1694                }),
1695                timestamp_writes: None,
1696                occlusion_query_set: None,
1697            });
1698            props.draw_shadow(&mut pass, count, index)?;
1699        }
1700        self.atlas_clean = false;
1701        Ok(())
1702    }
1703
1704    fn encode_layers(
1705        &self,
1706        encoder: &mut wgpu::CommandEncoder,
1707        load: wgpu::LoadOp<f32>,
1708        statics: bool,
1709        draw: &mut LayerDraw<'_>,
1710        mut profiler: Option<&mut GpuProfiler>,
1711    ) {
1712        for (index, layer) in self.layers.iter().enumerate() {
1713            let timing = profiler
1714                .as_deref_mut()
1715                .and_then(|timer| timer.pass(format!("shadow cascade {}", index + 1)));
1716            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1717                multiview_mask: None,
1718                label: Some("shadow cascade"),
1719                color_attachments: &[],
1720                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1721                    view: layer,
1722                    depth_ops: Some(wgpu::Operations {
1723                        load,
1724                        store: wgpu::StoreOp::Store,
1725                    }),
1726                    stencil_ops: None,
1727                }),
1728                timestamp_writes: profiler
1729                    .as_deref()
1730                    .and_then(|timer| timer.render_writes(timing)),
1731                occlusion_query_set: None,
1732            });
1733            draw(&mut pass, index, statics);
1734        }
1735    }
1736
1737    fn ensure_instances(&mut self, device: &wgpu::Device, bytes: u64) {
1738        if bytes <= self.instance_capacity {
1739            return;
1740        }
1741        let size = bytes
1742            .max(self.instance_capacity.saturating_mul(2))
1743            .max(4096);
1744        self.instances = device.create_buffer(&wgpu::BufferDescriptor {
1745            label: Some("shadow instances"),
1746            size,
1747            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1748            mapped_at_creation: false,
1749        });
1750        self.instance_capacity = size;
1751    }
1752
1753    fn encode(
1754        &self,
1755        encoder: &mut wgpu::CommandEncoder,
1756        draws: &[Draw<'_>],
1757        load: wgpu::LoadOp<f32>,
1758        mut profiler: Option<&mut GpuProfiler>,
1759    ) {
1760        if matches!(load, wgpu::LoadOp::Load) && draws.is_empty() {
1761            return;
1762        }
1763        for (index, layer) in self.layers.iter().enumerate() {
1764            let timing = profiler
1765                .as_deref_mut()
1766                .and_then(|timer| timer.pass(format!("shadow cascade {}", index + 1)));
1767            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1768                multiview_mask: None,
1769                label: Some("shadow cascade"),
1770                color_attachments: &[],
1771                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1772                    view: layer,
1773                    depth_ops: Some(wgpu::Operations {
1774                        load,
1775                        store: wgpu::StoreOp::Store,
1776                    }),
1777                    stencil_ops: None,
1778                }),
1779                timestamp_writes: profiler
1780                    .as_deref()
1781                    .and_then(|timer| timer.render_writes(timing)),
1782                occlusion_query_set: None,
1783            });
1784            if draws.is_empty() {
1785                continue;
1786            }
1787            pass.set_pipeline(&self.pipeline);
1788            pass.set_bind_group(0, &self.bind, &[self.align * index as u32]);
1789            for draw in draws {
1790                let end = draw.instance_offset + u64::from(draw.instance_count) * 64;
1791                pass.set_vertex_buffer(0, draw.positions.slice(..));
1792                pass.set_vertex_buffer(1, self.instances.slice(draw.instance_offset..end));
1793                pass.set_index_buffer(draw.indices.slice(..), wgpu::IndexFormat::Uint32);
1794                let first = draw.first_index;
1795                pass.draw_indexed(first..first + draw.index_count, 0, 0..draw.instance_count);
1796            }
1797        }
1798    }
1799}
1800
1801fn is_moving(caster: &Caster<'_>) -> bool {
1802    caster.moving && caster.index_count > 0 && !caster.instances.is_empty()
1803}
1804
1805fn static_key(fit: &Fit, casters: &[Caster<'_>]) -> StaticKey {
1806    use std::collections::hash_map::DefaultHasher;
1807    let mut matrices = [[0; 16]; CASCADE_COUNT];
1808    for (index, cascade) in fit.cascades.iter().enumerate() {
1809        for (element, value) in cascade.view_proj.columns.iter().flatten().enumerate() {
1810            matrices[index][element] = value.to_bits();
1811        }
1812    }
1813    let mut draws = Vec::new();
1814    for caster in casters {
1815        if caster.moving || caster.index_count == 0 || caster.instances.is_empty() {
1816            continue;
1817        }
1818        let mut positions = DefaultHasher::new();
1819        caster.positions.hash(&mut positions);
1820        let mut indices = DefaultHasher::new();
1821        caster.indices.hash(&mut indices);
1822        let mut instance_bits = Vec::with_capacity(caster.instances.len() * 16);
1823        for instance in caster.instances {
1824            instance_bits.extend(
1825                instance
1826                    .columns
1827                    .iter()
1828                    .flatten()
1829                    .map(|value| value.to_bits()),
1830            );
1831        }
1832        draws.push(StaticDraw {
1833            positions: positions.finish(),
1834            indices: indices.finish(),
1835            first_index: caster.first_index,
1836            index_count: caster.index_count,
1837            generation: caster.generation,
1838            instances: instance_bits,
1839        });
1840    }
1841    StaticKey {
1842        sun: [
1843            fit.toward_sun[0].to_bits(),
1844            fit.toward_sun[1].to_bits(),
1845            fit.toward_sun[2].to_bits(),
1846        ],
1847        matrices,
1848        draws,
1849        content: 0,
1850        near_map: false,
1851    }
1852}
1853
1854fn transmission_maps(device: &wgpu::Device, size: u32, tiles: FaceTiles) -> Transmission {
1855    let count = CASCADE_COUNT as u32 + tiles.layers();
1856    let extent = wgpu::Extent3d {
1857        width: size,
1858        height: size,
1859        depth_or_array_layers: count,
1860    };
1861    let map = device.create_texture(&wgpu::TextureDescriptor {
1862        label: Some("shadow transmission"),
1863        size: extent,
1864        mip_level_count: 1,
1865        sample_count: 1,
1866        dimension: wgpu::TextureDimension::D2,
1867        format: TRANSMISSION_FORMAT,
1868        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1869            | wgpu::TextureUsages::TEXTURE_BINDING
1870            | wgpu::TextureUsages::COPY_SRC,
1871        view_formats: &[],
1872    });
1873    let depth = device.create_texture(&wgpu::TextureDescriptor {
1874        label: Some("shadow transmission depth"),
1875        size: extent,
1876        mip_level_count: 1,
1877        sample_count: 1,
1878        dimension: wgpu::TextureDimension::D2,
1879        format: DEPTH_FORMAT,
1880        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1881        view_formats: &[],
1882    });
1883    let colour_view = |dimension, base_array_layer, count| {
1884        map.create_view(&wgpu::TextureViewDescriptor {
1885            label: Some("shadow transmission"),
1886            dimension: Some(dimension),
1887            base_array_layer,
1888            array_layer_count: Some(count),
1889            ..Default::default()
1890        })
1891    };
1892    Transmission {
1893        size,
1894        tiles,
1895        view: colour_view(wgpu::TextureViewDimension::D2Array, 0, count),
1896        layers: (0..count)
1897            .map(|layer| colour_view(wgpu::TextureViewDimension::D2, layer, 1))
1898            .collect(),
1899        depth_layers: (0..count).map(|layer| layer_view(&depth, layer)).collect(),
1900        map,
1901        _depth: depth,
1902        key: None,
1903    }
1904}
1905
1906fn depth_texture(device: &wgpu::Device, size: wgpu::Extent3d, label: &str) -> wgpu::Texture {
1907    device.create_texture(&wgpu::TextureDescriptor {
1908        label: Some(label),
1909        size,
1910        mip_level_count: 1,
1911        sample_count: 1,
1912        dimension: wgpu::TextureDimension::D2,
1913        format: DEPTH_FORMAT,
1914        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1915            | wgpu::TextureUsages::TEXTURE_BINDING
1916            | wgpu::TextureUsages::COPY_SRC
1917            | wgpu::TextureUsages::COPY_DST,
1918        view_formats: &[],
1919    })
1920}
1921
1922fn array_view(texture: &wgpu::Texture) -> wgpu::TextureView {
1923    texture.create_view(&wgpu::TextureViewDescriptor {
1924        label: Some("shadow array"),
1925        format: Some(DEPTH_FORMAT),
1926        dimension: Some(wgpu::TextureViewDimension::D2Array),
1927        usage: None,
1928        aspect: wgpu::TextureAspect::DepthOnly,
1929        base_mip_level: 0,
1930        mip_level_count: Some(1),
1931        base_array_layer: 0,
1932        array_layer_count: Some(CASCADE_COUNT as u32),
1933    })
1934}
1935
1936fn layer_view(texture: &wgpu::Texture, layer: u32) -> wgpu::TextureView {
1937    texture.create_view(&wgpu::TextureViewDescriptor {
1938        label: Some("shadow layer"),
1939        format: Some(DEPTH_FORMAT),
1940        dimension: Some(wgpu::TextureViewDimension::D2),
1941        usage: None,
1942        aspect: wgpu::TextureAspect::DepthOnly,
1943        base_mip_level: 0,
1944        mip_level_count: Some(1),
1945        base_array_layer: layer,
1946        array_layer_count: Some(1),
1947    })
1948}
1949
1950fn copy_depth(
1951    encoder: &mut wgpu::CommandEncoder,
1952    source: &wgpu::Texture,
1953    destination: &wgpu::Texture,
1954    resolution: u32,
1955) {
1956    encoder.copy_texture_to_texture(
1957        source.as_image_copy(),
1958        destination.as_image_copy(),
1959        wgpu::Extent3d {
1960            width: resolution,
1961            height: resolution,
1962            depth_or_array_layers: CASCADE_COUNT as u32,
1963        },
1964    );
1965}
1966
1967fn write_vec4(bytes: &mut [u8], offset: usize, value: [f32; 4]) {
1968    for (index, component) in value.iter().enumerate() {
1969        let start = offset + index * 4;
1970        bytes[start..start + 4].copy_from_slice(&component.to_le_bytes());
1971    }
1972}
1973
1974fn camera_limits(view: &View) -> (f32, f32, f32, f32) {
1975    let near = finite(view.near).max(1.0e-4);
1976    let far = finite(view.far).max(near + 1.0e-3);
1977    let fov_y = finite(view.fov_y).clamp(1.0e-3, std::f32::consts::PI - 1.0e-3);
1978    let aspect = finite(view.aspect).max(1.0e-4);
1979    (fov_y, aspect, near, far)
1980}
1981
1982fn camera_axes(view: &View) -> [[f32; 3]; 3] {
1983    let forward = normalize(view.forward);
1984    let mut right = cross(forward, view.up);
1985    if length(right) < 1.0e-6 {
1986        right = cross(forward, least_aligned(forward));
1987    }
1988    let right = normalize(right);
1989    let up = cross(right, forward);
1990    [right, up, forward]
1991}
1992
1993fn slice_sphere(near: f32, far: f32, fov_y: f32, aspect: f32) -> (f32, f32) {
1994    let tan_y = (fov_y * 0.5).tan();
1995    let tan_x = tan_y * aspect;
1996    let tan_corner = (tan_x * tan_x + tan_y * tan_y).sqrt();
1997    let factor = 1.0 + tan_corner * tan_corner;
1998    let center = (far + near) * 0.5 * factor;
1999    let far_radius = far * tan_corner;
2000    let radius = ((center - far) * (center - far) + far_radius * far_radius).sqrt();
2001    (center, radius.max(1.0e-4))
2002}
2003
2004fn perspective(fov_y: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
2005    let y = 1.0 / (fov_y * 0.5).tan();
2006    let span = near - far;
2007    Mat4::from_columns([
2008        [y / aspect, 0.0, 0.0, 0.0],
2009        [0.0, y, 0.0, 0.0],
2010        [0.0, 0.0, far / span, -1.0],
2011        [0.0, 0.0, near * far / span, 0.0],
2012    ])
2013}
2014
2015fn ortho(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Mat4 {
2016    let width = right - left;
2017    let height = top - bottom;
2018    let depth = far - near;
2019    Mat4::from_columns([
2020        [2.0 / width, 0.0, 0.0, 0.0],
2021        [0.0, 2.0 / height, 0.0, 0.0],
2022        [0.0, 0.0, -1.0 / depth, 0.0],
2023        [
2024            -(right + left) / width,
2025            -(top + bottom) / height,
2026            -near / depth,
2027            1.0,
2028        ],
2029    ])
2030}
2031
2032fn view_matrix(right: [f32; 3], up: [f32; 3], forward: [f32; 3], eye: [f32; 3]) -> Mat4 {
2033    Mat4::from_columns([
2034        [right[0], up[0], -forward[0], 0.0],
2035        [right[1], up[1], -forward[1], 0.0],
2036        [right[2], up[2], -forward[2], 0.0],
2037        [-dot(right, eye), -dot(up, eye), dot(forward, eye), 1.0],
2038    ])
2039}
2040
2041fn snap(value: f32, texel: f32) -> f32 {
2042    (value / texel).round() * texel
2043}
2044
2045fn least_aligned(direction: [f32; 3]) -> [f32; 3] {
2046    let ax = direction[0].abs();
2047    let ay = direction[1].abs();
2048    let az = direction[2].abs();
2049    if ax <= ay && ax <= az {
2050        [1.0, 0.0, 0.0]
2051    } else if ay <= az {
2052        [0.0, 1.0, 0.0]
2053    } else {
2054        [0.0, 0.0, 1.0]
2055    }
2056}
2057
2058fn finite(value: f32) -> f32 {
2059    if value.is_finite() { value } else { 0.0 }
2060}
2061
2062fn length(value: [f32; 3]) -> f32 {
2063    dot(value, value).sqrt()
2064}
2065
2066fn normalize(value: [f32; 3]) -> [f32; 3] {
2067    let len = length(value);
2068    if len < 1.0e-8 {
2069        [0.0, 1.0, 0.0]
2070    } else {
2071        scale(value, 1.0 / len)
2072    }
2073}
2074
2075fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
2076    left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
2077}
2078
2079fn cross(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
2080    [
2081        left[1] * right[2] - left[2] * right[1],
2082        left[2] * right[0] - left[0] * right[2],
2083        left[0] * right[1] - left[1] * right[0],
2084    ]
2085}
2086
2087fn add(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
2088    [left[0] + right[0], left[1] + right[1], left[2] + right[2]]
2089}
2090
2091fn scale(value: [f32; 3], factor: f32) -> [f32; 3] {
2092    [value[0] * factor, value[1] * factor, value[2] * factor]
2093}
2094
2095pub fn light_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
2096    vec![wgpu::BindGroupLayoutEntry {
2097        binding: 0,
2098        visibility: wgpu::ShaderStages::VERTEX,
2099        ty: wgpu::BindingType::Buffer {
2100            ty: wgpu::BufferBindingType::Uniform,
2101            has_dynamic_offset: true,
2102            min_binding_size: NonZeroU64::new(64),
2103        },
2104        count: None,
2105    }]
2106}
2107
2108pub fn creature_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
2109    vec![
2110        wgpu::BindGroupLayoutEntry {
2111            binding: 0,
2112            visibility: wgpu::ShaderStages::VERTEX,
2113            ty: wgpu::BindingType::Buffer {
2114                ty: wgpu::BufferBindingType::Storage { read_only: true },
2115                has_dynamic_offset: false,
2116                min_binding_size: None,
2117            },
2118            count: None,
2119        },
2120        wgpu::BindGroupLayoutEntry {
2121            binding: 1,
2122            visibility: wgpu::ShaderStages::VERTEX,
2123            ty: wgpu::BindingType::Buffer {
2124                ty: wgpu::BufferBindingType::Storage { read_only: true },
2125                has_dynamic_offset: false,
2126                min_binding_size: None,
2127            },
2128            count: None,
2129        },
2130        wgpu::BindGroupLayoutEntry {
2131            binding: 2,
2132            visibility: wgpu::ShaderStages::VERTEX,
2133            ty: wgpu::BindingType::Buffer {
2134                ty: wgpu::BufferBindingType::Uniform,
2135                has_dynamic_offset: false,
2136                min_binding_size: None,
2137            },
2138            count: None,
2139        },
2140    ]
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145    use super::*;
2146    use crate::effects::CreatureVertex;
2147
2148    #[test]
2149    fn light_face_tiles_pack_into_layers_after_the_cascades() {
2150        let tiles = FaceTiles::new(12, 1024, 1024);
2151        assert_eq!((tiles.tile, tiles.columns, tiles.layers()), (256, 4, 1));
2152        assert_eq!(tiles.place(0), (CASCADE_COUNT as u32, [0, 0]));
2153        assert_eq!(tiles.place(5), (CASCADE_COUNT as u32, [256, 256]));
2154        assert_eq!(tiles.place(11), (CASCADE_COUNT as u32, [768, 512]));
2155        let large = FaceTiles::new(24, 512, 4096);
2156        assert_eq!((large.tile, large.columns, large.layers()), (512, 1, 24));
2157        assert_eq!(large.place(23), (CASCADE_COUNT as u32 + 23, [0, 0]));
2158        let spill = FaceTiles::new(24, 512, 1024);
2159        assert_eq!((spill.tile, spill.columns, spill.layers()), (256, 2, 6));
2160        assert_eq!(spill.place(7), (CASCADE_COUNT as u32 + 1, [256, 256]));
2161        assert_eq!(FaceTiles::new(0, 1024, 1024).layers(), 0);
2162        assert_eq!(FaceTiles::new(0, 1024, 1024), FaceTiles::default());
2163    }
2164
2165    #[test]
2166    fn receiver_box_increases_shadow_texel_density() {
2167        let view = View {
2168            eye: [0.0, 0.2, 1.6],
2169            forward: [0.0, -0.1, -1.0],
2170            up: [0.0, 1.0, 0.0],
2171            fov_y: 0.5,
2172            aspect: 16.0 / 9.0,
2173            near: 0.4,
2174            far: 6.0,
2175        };
2176        let sun = [0.3, 0.8, 0.5];
2177        let default = fit(&view, sun, &Quality::default());
2178        let quality = Quality {
2179            resolution: 4096,
2180            receiver: Some(ReceiverBox {
2181                min: [-0.4, -0.25, -0.2],
2182                max: [0.4, 0.25, 0.35],
2183            }),
2184            caster_margin: 3.0,
2185            ..Quality::default()
2186        };
2187        let fitted = fit(&view, sun, &quality);
2188        let box_bound = quality.receiver.unwrap();
2189        let outer = fitted.cascades[CASCADE_COUNT - 1].texel / fitted.cascades[0].texel;
2190        assert!((outer - OUTER_SCALE).abs() < 1.0e-3, "{outer}");
2191        for (index, cascade) in fitted.cascades.iter().enumerate() {
2192            let texel = if index == CASCADE_COUNT - 1 {
2193                cascade.texel / OUTER_SCALE
2194            } else {
2195                cascade.texel
2196            };
2197            assert!(texel < default.cascades[2].texel * 0.5);
2198            assert!(texel < 0.0003);
2199            for x in [box_bound.min[0], box_bound.max[0]] {
2200                for y in [box_bound.min[1], box_bound.max[1]] {
2201                    for z in [box_bound.min[2], box_bound.max[2]] {
2202                        let point = cascade.view_proj.transform_point([x, y, z]);
2203                        assert!(point[0].abs() <= 1.0 && point[1].abs() <= 1.0);
2204                    }
2205                }
2206            }
2207        }
2208    }
2209
2210    #[test]
2211    #[ignore = "needs a GPU; run under pgpu"]
2212    fn quality_change_reallocates_only_for_resolution() {
2213        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
2214        let mut shadows = Shadows::new(
2215            &gpu.device,
2216            Quality {
2217                resolution: 64,
2218                ..Quality::default()
2219            },
2220        );
2221        let atlas = shadows.atlas.clone();
2222        let backup = shadows.backup.clone();
2223        shadows.update_quality(
2224            &gpu.device,
2225            Quality {
2226                resolution: 64,
2227                lambda: 0.8,
2228                ..Quality::default()
2229            },
2230        );
2231        assert_eq!(shadows.atlas, atlas);
2232        assert_eq!(shadows.backup, backup);
2233        assert_eq!(shadows.quality().lambda, 0.8);
2234        shadows.update_quality(
2235            &gpu.device,
2236            Quality {
2237                resolution: 128,
2238                ..Quality::default()
2239            },
2240        );
2241        assert_ne!(shadows.atlas, atlas);
2242        assert_ne!(shadows.backup, backup);
2243        assert_eq!(shadows.resolution(), 128);
2244        assert_eq!(shadows.atlas.size().width, 128);
2245        assert_eq!(shadows.backup.size().width, 128);
2246    }
2247
2248    #[test]
2249    #[ignore = "needs a GPU; run under pgpu"]
2250    fn bird_depth_appears_in_shadow_atlas() {
2251        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
2252        let quality = Quality {
2253            resolution: 64,
2254            receiver: Some(ReceiverBox {
2255                min: [-1.0, -0.1, -1.0],
2256                max: [1.0, 0.1, 1.0],
2257            }),
2258            caster_margin: 2.0,
2259            ..Quality::default()
2260        };
2261        let mut shadows = Shadows::new(
2262            &gpu.device,
2263            Quality {
2264                resolution: 64,
2265                ..Quality::default()
2266            },
2267        );
2268        shadows.set_quality(quality).unwrap();
2269        let view = View {
2270            eye: [0.0, 2.0, 3.0],
2271            forward: [0.0, -0.5, -1.0],
2272            up: [0.0, 1.0, 0.0],
2273            fov_y: 1.0,
2274            aspect: 1.0,
2275            near: 0.1,
2276            far: 6.0,
2277        };
2278        let fit = shadows.fit(&view, [0.0, 1.0, 0.0]);
2279        let mesh = CreatureMesh::new(
2280            &gpu.device,
2281            &[
2282                CreatureVertex {
2283                    position: [-0.4, 0.0, -0.4, 0.0],
2284                },
2285                CreatureVertex {
2286                    position: [0.4, 0.0, -0.4, 0.0],
2287                },
2288                CreatureVertex {
2289                    position: [0.0, 0.0, 0.4, 0.0],
2290                },
2291            ],
2292            &[0, 1, 2],
2293        );
2294        let bird = CreatureInstance {
2295            position_scale: [0.0, 1.0, 0.0, 1.0],
2296            right: [1.0, 0.0, 0.0, 0.0],
2297            up: [0.0, 1.0, 0.0, 0.0],
2298            forward: [0.0, 0.0, 1.0, 0.0],
2299            motion: [0.0; 4],
2300        };
2301        let mut encoder = gpu.device.create_command_encoder(&Default::default());
2302        shadows.render(&gpu.device, &gpu.queue, &mut encoder, &fit, &[]);
2303        shadows.render_creatures(&gpu, &mut encoder, &fit, &mesh, &[bird], 0.0);
2304        let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
2305            label: Some("bird shadow readback"),
2306            size: 64 * 256,
2307            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2308            mapped_at_creation: false,
2309        });
2310        encoder.copy_texture_to_buffer(
2311            wgpu::TexelCopyTextureInfo {
2312                texture: shadows.atlas(),
2313                mip_level: 0,
2314                origin: wgpu::Origin3d::ZERO,
2315                aspect: wgpu::TextureAspect::DepthOnly,
2316            },
2317            wgpu::TexelCopyBufferInfo {
2318                buffer: &readback,
2319                layout: wgpu::TexelCopyBufferLayout {
2320                    offset: 0,
2321                    bytes_per_row: Some(256),
2322                    rows_per_image: Some(64),
2323                },
2324            },
2325            wgpu::Extent3d {
2326                width: 64,
2327                height: 64,
2328                depth_or_array_layers: 1,
2329            },
2330        );
2331        gpu.queue.submit([encoder.finish()]);
2332        readback.slice(..).map_async(wgpu::MapMode::Read, |_| {});
2333        gpu.device
2334            .poll(wgpu::PollType::wait_indefinitely())
2335            .unwrap();
2336        let bytes = readback
2337            .slice(..)
2338            .get_mapped_range()
2339            .expect("a mapped range");
2340        let at = (32 * 256 + 32 * 4) as usize;
2341        let depth = f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
2342        assert!(depth < 1.0, "{depth}");
2343    }
2344
2345    fn desk() -> View {
2346        View {
2347            eye: [0.0, 1.6, 6.0],
2348            forward: [0.0, -0.15, -1.0],
2349            up: [0.0, 1.0, 0.0],
2350            fov_y: 50.0_f32.to_radians(),
2351            aspect: 16.0 / 9.0,
2352            near: 0.2,
2353            far: 40.0,
2354        }
2355    }
2356
2357    fn overhead() -> View {
2358        View {
2359            eye: [0.0, 8.0, 0.2],
2360            forward: [0.0, -1.0, 0.0],
2361            up: [0.0, 0.0, 1.0],
2362            fov_y: 45.0_f32.to_radians(),
2363            aspect: 1.0,
2364            near: 0.5,
2365            far: 30.0,
2366        }
2367    }
2368
2369    fn close(left: f32, right: f32) -> bool {
2370        (left - right).abs() <= 1.0e-4 * left.abs().max(right.abs()).max(1.0)
2371    }
2372
2373    #[test]
2374    fn splits_cover_near_to_far_without_gaps() {
2375        for (near, far, lambda) in [(0.1, 100.0, 0.5), (0.05, 0.2, 0.0), (0.2, 10_000.0, 1.0)] {
2376            let distances = splits(near, far, lambda);
2377            assert!(close(distances[0], near));
2378            assert!(close(distances[3], far));
2379            for pair in distances.windows(2) {
2380                assert!(pair[0] < pair[1], "{distances:?}");
2381            }
2382            let uniform = splits(near, far, 0.0);
2383            let logarithmic = splits(near, far, 1.0);
2384            let practical = splits(near, far, 0.5);
2385            for (index, ((uniform_value, log_value), practical_value)) in uniform
2386                .iter()
2387                .zip(logarithmic.iter())
2388                .zip(practical.iter())
2389                .enumerate()
2390            {
2391                let portion = index as f32 / CASCADE_COUNT as f32;
2392                let expected_uniform = near + (far - near) * portion;
2393                let expected_log = near * (far / near).powf(portion);
2394                assert!(close(*uniform_value, expected_uniform), "{uniform:?}");
2395                assert!(
2396                    close(*log_value, expected_log),
2397                    "{logarithmic:?} {expected_log}"
2398                );
2399                assert!(close(
2400                    *practical_value,
2401                    0.5 * expected_log + 0.5 * expected_uniform
2402                ));
2403            }
2404            let mut previous = 0;
2405            for step in 0..=2000 {
2406                let t = step as f32 / 2000.0;
2407                let distance = distances[0] + (distances[3] - distances[0]) * t;
2408                let choice = select_cascade(distance, distances, 0.1);
2409                assert!(choice.cascade < CASCADE_COUNT);
2410                assert!(choice.cascade == previous || choice.cascade == previous + 1);
2411                assert!(choice.next == choice.cascade || choice.next == choice.cascade + 1);
2412                let start = distances[choice.cascade];
2413                let end = distances[choice.cascade + 1];
2414                assert!(
2415                    distance + 1.0e-3 >= start && distance <= end + 1.0e-3,
2416                    "{distance} {choice:?} {distances:?}"
2417                );
2418                previous = choice.cascade;
2419            }
2420        }
2421        let distances = splits(1.0, 90.0, 0.5);
2422        assert_eq!(select_cascade(distances[1], distances, 0.0).cascade, 0);
2423        assert_eq!(
2424            select_cascade(distances[1].next_up(), distances, 0.0).cascade,
2425            1
2426        );
2427        assert_eq!(select_cascade(distances[3], distances, 0.0).cascade, 2);
2428        let end = distances[1];
2429        let band = (end - distances[0]) * 0.1;
2430        let at_end = select_cascade(end, distances, 0.1);
2431        assert_eq!(at_end.cascade, 0);
2432        assert_eq!(at_end.next, 1);
2433        assert!(close(at_end.blend, 1.0));
2434        let mid = (distances[0] + end - band) * 0.5;
2435        assert_eq!(select_cascade(mid, distances, 0.1).blend, 0.0);
2436    }
2437
2438    #[test]
2439    fn light_matrices_contain_each_cascades_corners() {
2440        let quality = Quality {
2441            resolution: 256,
2442            ..Quality::default()
2443        };
2444        let views = [desk(), overhead()];
2445        let suns = [[0.0, 1.0, 0.0], [0.35, 0.85, 0.2], [1.0, 0.18, -0.2]];
2446        for view in views {
2447            let projection = camera_view_proj(&view);
2448            for corner in frustum_corners(&view, view.near, view.far) {
2449                let ndc = projection.transform_point(corner);
2450                assert!(
2451                    ndc[0].abs() <= 1.0 + 1.0e-3
2452                        && ndc[1].abs() <= 1.0 + 1.0e-3
2453                        && (-1.0e-3..=1.0 + 1.0e-3).contains(&ndc[2]),
2454                    "{ndc:?}"
2455                );
2456            }
2457            for sun in suns {
2458                let fitted = fit(&view, sun, &quality);
2459                assert_eq!(fitted.cascades.len(), CASCADE_COUNT);
2460                for (index, cascade) in fitted.cascades.iter().enumerate() {
2461                    assert!(close(cascade.near, fitted.splits[index]));
2462                    assert!(close(cascade.far, fitted.splits[index + 1]));
2463                    let fit_near = if index == 0 {
2464                        cascade.near
2465                    } else {
2466                        cascade.near - (cascade.near - fitted.splits[index - 1]) * quality.blend
2467                    };
2468                    for corner in frustum_corners(&view, fit_near, cascade.far) {
2469                        let ndc = cascade.view_proj.transform_point(corner);
2470                        assert!(
2471                            ndc[0].abs() <= 1.0 + 2.0e-3
2472                                && ndc[1].abs() <= 1.0 + 2.0e-3
2473                                && (-2.0e-3..=1.0 + 2.0e-3).contains(&ndc[2]),
2474                            "cascade {index} {ndc:?} sun {sun:?}"
2475                        );
2476                    }
2477                    let [right, up, forward] = light_basis(sun);
2478                    let snapped = cascade.snapped_center;
2479                    let world = add(
2480                        add(scale(right, snapped[0]), scale(up, snapped[1])),
2481                        scale(forward, -snapped[2]),
2482                    );
2483                    let origin = cascade.view_proj.transform_point(world);
2484                    let stepped = cascade
2485                        .view_proj
2486                        .transform_point(add(world, scale(right, cascade.texel)));
2487                    assert!(origin[0].abs() < 1.0e-3, "{origin:?}");
2488                    assert!(origin[1].abs() < 1.0e-3, "{origin:?}");
2489                    let expected = 2.0 / fitted.resolution as f32;
2490                    assert!(
2491                        (stepped[0] - origin[0] - expected).abs() < 2.0e-3,
2492                        "{} {expected}",
2493                        stepped[0] - origin[0]
2494                    );
2495                }
2496            }
2497        }
2498    }
2499
2500    #[test]
2501    fn texel_snap_holds_under_a_small_camera_move() {
2502        let quality = Quality {
2503            resolution: 128,
2504            ..Quality::default()
2505        };
2506        let sun = [0.25, 0.9, 0.15];
2507        let view = desk();
2508        let before = fit(&view, sun, &quality);
2509        let [right, _, _] = light_basis(sun);
2510        let mut slack = f32::MAX;
2511        for cascade in &before.cascades {
2512            let error = (cascade.light_center[0] - cascade.snapped_center[0]).abs();
2513            assert!(error <= cascade.texel * 0.5 + 1.0e-4, "{error}");
2514            slack = slack.min(cascade.texel * 0.5 - error);
2515        }
2516        assert!(slack > 0.0, "{slack}");
2517        let mut moved = view;
2518        moved.eye = add(moved.eye, scale(right, slack * 0.5));
2519        let after = fit(&moved, sun, &quality);
2520        for (first, second) in before.cascades.iter().zip(after.cascades.iter()) {
2521            assert_eq!(first.view_proj, second.view_proj);
2522            assert_eq!(first.snapped_center, second.snapped_center);
2523        }
2524        let mut jumped = view;
2525        jumped.eye = add(jumped.eye, scale(right, before.cascades[0].texel * 3.0));
2526        let changed = fit(&jumped, sun, &quality);
2527        assert_ne!(
2528            before.cascades[0].snapped_center[0],
2529            changed.cascades[0].snapped_center[0]
2530        );
2531    }
2532
2533    #[test]
2534    fn sampling_shader_names_its_entry_points() {
2535        for name in [
2536            "shadow_select",
2537            "shadow_pcf",
2538            "shadow_blend",
2539            "shadow_sample",
2540        ] {
2541            assert!(
2542                SHADOW_WGSL.contains(&format!("fn {name}(")),
2543                "{name} missing"
2544            );
2545        }
2546        assert!(SHADOW_WGSL.contains("textureSampleCompareLevel"));
2547        assert!(SHADOW_WGSL.contains("shadow_offset"));
2548        assert_eq!(Quality::default().resolution, DEFAULT_RESOLUTION);
2549        assert_eq!(DEFAULT_RESOLUTION, 4096);
2550    }
2551
2552    #[test]
2553    fn sample_uniform_packs_splits_matrices_and_bias() {
2554        let quality = Quality {
2555            resolution: 64,
2556            pcf_radius: 1.5,
2557            normal_bias: 0.75,
2558            slope_bias: 2.5,
2559            depth_bias: 0.002,
2560            blend: 0.2,
2561            ..Quality::default()
2562        };
2563        let fitted = fit(&desk(), [0.2, 1.0, 0.1], &quality);
2564        let bytes = sample_uniform(&fitted, &quality);
2565        for (index, split) in fitted.splits.iter().enumerate() {
2566            let start = index * 4;
2567            let value = f32::from_le_bytes(bytes[start..start + 4].try_into().unwrap());
2568            assert_eq!(value.to_bits(), split.to_bits());
2569        }
2570        let radius = f32::from_le_bytes(bytes[16..20].try_into().unwrap());
2571        let normal = f32::from_le_bytes(bytes[20..24].try_into().unwrap());
2572        let slope = f32::from_le_bytes(bytes[24..28].try_into().unwrap());
2573        let blend = f32::from_le_bytes(bytes[28..32].try_into().unwrap());
2574        assert_eq!(radius, 1.5);
2575        assert_eq!(normal, 0.75);
2576        assert_eq!(slope, 2.5);
2577        assert_eq!(blend, 0.2);
2578        for (index, cascade) in fitted.cascades.iter().enumerate() {
2579            let start = 64 + index * 64;
2580            assert_eq!(&bytes[start..start + 64], &cascade.view_proj.to_bytes());
2581        }
2582        let flat = normal_offset(
2583            [1.0, 2.0, 3.0],
2584            [0.0, 1.0, 0.0],
2585            [0.0, 1.0, 0.0],
2586            0.2,
2587            1.0,
2588            2.0,
2589        );
2590        assert!(close(flat[0], 1.0) && close(flat[1], 2.2) && close(flat[2], 3.0));
2591        let grazing = normal_offset(
2592            [1.0, 2.0, 3.0],
2593            [1.0, 0.0, 0.0],
2594            [0.0, 1.0, 0.0],
2595            0.2,
2596            1.0,
2597            2.0,
2598        );
2599        assert!(close(grazing[0], 1.6) && close(grazing[1], 2.0));
2600    }
2601
2602    #[test]
2603    fn sample_uniform_packs_the_soft_sun_taps() {
2604        let read =
2605            |bytes: &[u8], at: usize| f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
2606        let hard = Quality::default();
2607        let fitted = fit(&desk(), [0.2, 1.0, 0.1], &hard);
2608        let bytes = sample_uniform(&fitted, &hard);
2609        assert!(bytes[256..280].iter().all(|byte| *byte == 0));
2610        assert_eq!(read(&bytes, 280), 7.0);
2611        let soft = Quality {
2612            sun_radius_deg: 0.8,
2613            blocker_taps: 12,
2614            filter_taps: 200,
2615            blocker_gathers: 6,
2616            nearest_blocker: false,
2617            blend_dither: false,
2618            ..Quality::default()
2619        };
2620        let bytes = sample_uniform(&fitted, &soft);
2621        assert!(close(read(&bytes, 256), 0.8_f32.to_radians().tan()));
2622        assert_eq!(read(&bytes, 264), 12.0);
2623        assert_eq!(read(&bytes, 272), 6.0);
2624        assert_eq!(read(&bytes, 276), SOFT_TAP_LIMIT as f32);
2625        assert_eq!(read(&bytes, 280), 2.0);
2626        let shipped = sample_uniform(
2627            &fitted,
2628            &Quality {
2629                sun_radius_deg: 0.8,
2630                ..Quality::default()
2631            },
2632        );
2633        assert_eq!(read(&shipped, 264), BLOCKER_TAPS as f32);
2634        assert_eq!(read(&shipped, 276), FILTER_TAPS as f32);
2635        assert_eq!(read(&shipped, 272), BLOCKER_GATHERS as f32);
2636        assert_eq!(read(&shipped, 280), 7.0);
2637    }
2638
2639    #[test]
2640    fn the_outer_cascade_grows_the_receiver_box() {
2641        let read =
2642            |bytes: &[u8], at: usize| f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
2643        let boxed = Quality {
2644            resolution: 1024,
2645            receiver: Some(ReceiverBox {
2646                min: [-0.5, -0.1, -0.3],
2647                max: [0.5, 0.4, 0.3],
2648            }),
2649            ..Quality::default()
2650        };
2651        let sun = [0.45, 0.62, 0.64];
2652        let grown = fit(&desk(), sun, &boxed);
2653        assert_eq!(grown.cascades[0].view_proj, grown.cascades[1].view_proj);
2654        let ratio = grown.cascades[2].texel / grown.cascades[0].texel;
2655        assert!((ratio - OUTER_SCALE).abs() < 1.0e-3, "{ratio}");
2656        assert_eq!(
2657            grown.cascades[2].snapped_center[2],
2658            grown.cascades[0].snapped_center[2]
2659        );
2660        let [right, _, _] = light_basis(sun);
2661        let receiver = boxed.receiver.unwrap();
2662        let centre: [f32; 3] = std::array::from_fn(|k| (receiver.min[k] + receiver.max[k]) * 0.5);
2663        let mut half = 0.0f32;
2664        for x in [receiver.min[0], receiver.max[0]] {
2665            for y in [receiver.min[1], receiver.max[1]] {
2666                for z in [receiver.min[2], receiver.max[2]] {
2667                    half = half.max(dot(right, add([x, y, z], scale(centre, -1.0))).abs());
2668                }
2669            }
2670        }
2671        let outside = add(centre, scale(right, half * OUTER_SCALE * 0.8));
2672        let inner = grown.cascades[0].view_proj.transform_point(outside);
2673        let outer = grown.cascades[2].view_proj.transform_point(outside);
2674        assert!(inner[0].abs() > 1.0, "{inner:?}");
2675        assert!(
2676            outer[0].abs() <= 1.0 && (0.0..=1.0).contains(&outer[2]),
2677            "{outer:?}"
2678        );
2679        assert_eq!(read(&sample_uniform(&grown, &boxed), 280), 103.0);
2680        let off = Quality {
2681            outer_scale: 0.0,
2682            ..boxed
2683        };
2684        let same = fit(&desk(), sun, &off);
2685        assert_eq!(same.cascades[0].view_proj, same.cascades[2].view_proj);
2686        assert_eq!(same.cascades[0], grown.cascades[0]);
2687        assert_eq!(read(&sample_uniform(&same, &off), 280), 7.0);
2688        let free = fit(&desk(), sun, &Quality::default());
2689        assert_eq!(read(&sample_uniform(&free, &Quality::default()), 280), 7.0);
2690    }
2691
2692    #[test]
2693    #[ignore = "needs a GPU; run under pgpu"]
2694    fn cube_shadows_a_plane() {
2695        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
2696        let quality = Quality {
2697            resolution: 256,
2698            depth_bias: 0.0004,
2699            normal_bias: 0.5,
2700            slope_bias: 1.0,
2701            ..Quality::default()
2702        };
2703        let mut shadows = Shadows::new(&gpu.device, quality);
2704        let eye_y = 6.0_f32;
2705        let view = View {
2706            eye: [0.0, eye_y, 0.0],
2707            forward: [0.0, -1.0, 0.0],
2708            up: [0.0, 0.0, 1.0],
2709            fov_y: 2.0 * (1.0 / eye_y).atan(),
2710            aspect: 1.0,
2711            near: 1.0,
2712            far: 12.0,
2713        };
2714        let sun = [0.0, 1.0, 0.0];
2715        let fitted = shadows.fit(&view, sun);
2716        let plane = select_cascade(eye_y, fitted.splits, quality.blend);
2717        assert_eq!(plane.cascade, 1, "{:?}", fitted.splits);
2718        assert_eq!(plane.blend, 0.0);
2719        let static_model = Mat4::from_columns([
2720            [0.4, 0.0, 0.0, 0.0],
2721            [0.0, 1.0, 0.0, 0.0],
2722            [0.0, 0.0, 0.3, 0.0],
2723            [0.35, 0.7, 0.25, 1.0],
2724        ]);
2725        let moving_a = Mat4::from_columns([
2726            [0.3, 0.0, 0.0, 0.0],
2727            [0.0, 1.0, 0.0, 0.0],
2728            [0.0, 0.0, 0.3, 0.0],
2729            [-0.4, 0.7, -0.25, 1.0],
2730        ]);
2731        let moving_b = Mat4::from_columns([
2732            [0.3, 0.0, 0.0, 0.0],
2733            [0.0, 1.0, 0.0, 0.0],
2734            [0.0, 0.0, 0.3, 0.0],
2735            [-0.4, 0.7, 0.35, 1.0],
2736        ]);
2737        for model in [static_model, moving_a, moving_b] {
2738            for sx in [-0.5_f32, 0.5] {
2739                for sy in [-0.5_f32, 0.5] {
2740                    for sz in [-0.5_f32, 0.5] {
2741                        let world = model.transform_point([sx, sy, sz]);
2742                        let ndc = fitted.cascades[1].view_proj.transform_point(world);
2743                        assert!(
2744                            ndc[0].abs() <= 1.0
2745                                && ndc[1].abs() <= 1.0
2746                                && (0.0..=1.0).contains(&ndc[2]),
2747                            "{world:?} {ndc:?}"
2748                        );
2749                    }
2750                }
2751            }
2752        }
2753        let (positions, indices) = unit_cube();
2754        let position_buffer = upload(
2755            &gpu.device,
2756            &gpu.queue,
2757            &positions,
2758            wgpu::BufferUsages::VERTEX,
2759        );
2760        let index_buffer = upload(&gpu.device, &gpu.queue, &indices, wgpu::BufferUsages::INDEX);
2761        let static_instances = [static_model];
2762        let moving_instances_a = [moving_a];
2763        let moving_instances_b = [moving_b];
2764        let count = render_receiver(
2765            &gpu,
2766            &mut shadows,
2767            &fitted,
2768            &position_buffer,
2769            &index_buffer,
2770            &static_instances,
2771            &[],
2772        );
2773        assert!(!shadows.reused_static());
2774        assert_shadow(&count, static_bounds(), None, "static");
2775        let again = render_receiver(
2776            &gpu,
2777            &mut shadows,
2778            &fitted,
2779            &position_buffer,
2780            &index_buffer,
2781            &static_instances,
2782            &[],
2783        );
2784        assert!(shadows.reused_static());
2785        assert_eq!(again.shadowed, count.shadowed);
2786        let with_moving = render_receiver(
2787            &gpu,
2788            &mut shadows,
2789            &fitted,
2790            &position_buffer,
2791            &index_buffer,
2792            &static_instances,
2793            &moving_instances_a,
2794        );
2795        assert!(shadows.reused_static());
2796        assert_shadow(
2797            &with_moving,
2798            static_bounds(),
2799            Some(bounds_of(&moving_a)),
2800            "moving a",
2801        );
2802        let moved = render_receiver(
2803            &gpu,
2804            &mut shadows,
2805            &fitted,
2806            &position_buffer,
2807            &index_buffer,
2808            &static_instances,
2809            &moving_instances_b,
2810        );
2811        assert!(shadows.reused_static());
2812        assert_shadow(
2813            &moved,
2814            static_bounds(),
2815            Some(bounds_of(&moving_b)),
2816            "moving b",
2817        );
2818        let old = bounds_of(&moving_a);
2819        assert!(
2820            !point_shadowed(&moved, (old.0 + old.1) * 0.5, (old.2 + old.3) * 0.5),
2821            "stale moving shadow\n{}",
2822            ascii(&moved.pixels)
2823        );
2824        println!(
2825            "shadowed texels: static {} with moving {} after move {}",
2826            count.shadowed, with_moving.shadowed, moved.shadowed
2827        );
2828    }
2829
2830    struct Shot {
2831        pixels: Vec<u8>,
2832        shadowed: u32,
2833    }
2834
2835    fn render_receiver(
2836        gpu: &pfx_gpu::Gpu,
2837        shadows: &mut Shadows,
2838        fitted: &Fit,
2839        positions: &wgpu::Buffer,
2840        indices: &wgpu::Buffer,
2841        static_instances: &[Mat4],
2842        moving_instances: &[Mat4],
2843    ) -> Shot {
2844        let mut encoder = gpu.device.create_command_encoder(&Default::default());
2845        let static_caster = Caster {
2846            positions,
2847            indices,
2848            first_index: 0,
2849            index_count: 36,
2850            instances: static_instances,
2851            moving: false,
2852            generation: 0,
2853        };
2854        let moving_caster = Caster {
2855            positions,
2856            indices,
2857            first_index: 0,
2858            index_count: 36,
2859            instances: moving_instances,
2860            moving: true,
2861            generation: 0,
2862        };
2863        let casters = if moving_instances.is_empty() {
2864            vec![static_caster]
2865        } else {
2866            vec![static_caster, moving_caster]
2867        };
2868        shadows.render(&gpu.device, &gpu.queue, &mut encoder, fitted, &casters);
2869        let uniform_bytes = shadows.sample_uniform(fitted);
2870        let uniform = gpu.device.create_buffer(&wgpu::BufferDescriptor {
2871            label: Some("shadow sample"),
2872            size: SAMPLE_UNIFORM_BYTES as u64,
2873            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2874            mapped_at_creation: false,
2875        });
2876        gpu.queue.write_buffer(&uniform, 0, &uniform_bytes);
2877        let sampler = shadows.sampler();
2878        let bind_layout = gpu
2879            .device
2880            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2881                label: Some("shadow receiver"),
2882                entries: &[
2883                    wgpu::BindGroupLayoutEntry {
2884                        binding: 0,
2885                        visibility: wgpu::ShaderStages::FRAGMENT,
2886                        ty: wgpu::BindingType::Texture {
2887                            sample_type: wgpu::TextureSampleType::Depth,
2888                            view_dimension: wgpu::TextureViewDimension::D2Array,
2889                            multisampled: false,
2890                        },
2891                        count: None,
2892                    },
2893                    wgpu::BindGroupLayoutEntry {
2894                        binding: 1,
2895                        visibility: wgpu::ShaderStages::FRAGMENT,
2896                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
2897                        count: None,
2898                    },
2899                    wgpu::BindGroupLayoutEntry {
2900                        binding: 2,
2901                        visibility: wgpu::ShaderStages::FRAGMENT,
2902                        ty: wgpu::BindingType::Buffer {
2903                            ty: wgpu::BufferBindingType::Uniform,
2904                            has_dynamic_offset: false,
2905                            min_binding_size: NonZeroU64::new(SAMPLE_UNIFORM_BYTES as u64),
2906                        },
2907                        count: None,
2908                    },
2909                    wgpu::BindGroupLayoutEntry {
2910                        binding: 3,
2911                        visibility: wgpu::ShaderStages::FRAGMENT,
2912                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2913                        count: None,
2914                    },
2915                ],
2916            });
2917        let gather = gpu
2918            .device
2919            .create_sampler(&wgpu::SamplerDescriptor::default());
2920        let bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
2921            label: Some("shadow receiver"),
2922            layout: &bind_layout,
2923            entries: &[
2924                wgpu::BindGroupEntry {
2925                    binding: 3,
2926                    resource: wgpu::BindingResource::Sampler(&gather),
2927                },
2928                wgpu::BindGroupEntry {
2929                    binding: 0,
2930                    resource: wgpu::BindingResource::TextureView(shadows.atlas_view()),
2931                },
2932                wgpu::BindGroupEntry {
2933                    binding: 1,
2934                    resource: wgpu::BindingResource::Sampler(sampler),
2935                },
2936                wgpu::BindGroupEntry {
2937                    binding: 2,
2938                    resource: uniform.as_entire_binding(),
2939                },
2940            ],
2941        });
2942        let source = format!(
2943            "{SHADOW_WGSL}\n\
2944@group(0) @binding(0) var map: texture_depth_2d_array;\n\
2945@group(0) @binding(1) var samp: sampler_comparison;\n\
2946@group(0) @binding(2) var<uniform> data: ShadowSample;\n\
2947@group(0) @binding(3) var gather: sampler;\n\
2948@vertex\n\
2949fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {{\n\
2950    var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n\
2951    return vec4f(positions[index], 0.0, 1.0);\n\
2952}}\n\
2953@fragment\n\
2954fn fs(@builtin(position) pixel: vec4f) -> @location(0) vec4f {{\n\
2955    let uv = pixel.xy / vec2f(64.0, 64.0);\n\
2956    let world = vec3f(-1.0 + uv.x * 2.0, 0.0, -1.0 + uv.y * 2.0);\n\
2957    let distance = dot(world - vec3f(0.0, 6.0, 0.0), vec3f(0.0, -1.0, 0.0));\n\
2958    let lit = shadow_sample(map, samp, gather, data, world, vec3f(0.0, 1.0, 0.0), distance);\n\
2959    return vec4f(1.0 - lit, 0.0, 0.0, 1.0);\n\
2960}}\n"
2961        );
2962        let shader = gpu
2963            .device
2964            .create_shader_module(wgpu::ShaderModuleDescriptor {
2965                label: Some("shadow receiver"),
2966                source: wgpu::ShaderSource::Wgsl(source.into()),
2967            });
2968        let layout = gpu
2969            .device
2970            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2971                label: Some("shadow receiver"),
2972                bind_group_layouts: &[Some(&bind_layout)],
2973                immediate_size: 0,
2974            });
2975        let pipeline = gpu
2976            .device
2977            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2978                label: Some("shadow receiver"),
2979                layout: Some(&layout),
2980                vertex: wgpu::VertexState {
2981                    module: &shader,
2982                    entry_point: Some("vs"),
2983                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2984                    buffers: &[],
2985                },
2986                primitive: wgpu::PrimitiveState {
2987                    topology: wgpu::PrimitiveTopology::TriangleList,
2988                    ..Default::default()
2989                },
2990                depth_stencil: None,
2991                multisample: wgpu::MultisampleState::default(),
2992                fragment: Some(wgpu::FragmentState {
2993                    module: &shader,
2994                    entry_point: Some("fs"),
2995                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2996                    targets: &[Some(wgpu::ColorTargetState {
2997                        format: wgpu::TextureFormat::Rgba8Unorm,
2998                        blend: None,
2999                        write_mask: wgpu::ColorWrites::ALL,
3000                    })],
3001                }),
3002                multiview_mask: None,
3003                cache: None,
3004            });
3005        let color = gpu.device.create_texture(&wgpu::TextureDescriptor {
3006            label: Some("shadow receiver"),
3007            size: wgpu::Extent3d {
3008                width: 64,
3009                height: 64,
3010                depth_or_array_layers: 1,
3011            },
3012            mip_level_count: 1,
3013            sample_count: 1,
3014            dimension: wgpu::TextureDimension::D2,
3015            format: wgpu::TextureFormat::Rgba8Unorm,
3016            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3017            view_formats: &[],
3018        });
3019        let view = color.create_view(&Default::default());
3020        {
3021            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3022                multiview_mask: None,
3023                label: Some("shadow receiver"),
3024                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3025                    view: &view,
3026                    resolve_target: None,
3027                    ops: wgpu::Operations {
3028                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3029                        store: wgpu::StoreOp::Store,
3030                    },
3031                    depth_slice: None,
3032                })],
3033                depth_stencil_attachment: None,
3034                timestamp_writes: None,
3035                occlusion_query_set: None,
3036            });
3037            pass.set_pipeline(&pipeline);
3038            pass.set_bind_group(0, &bind, &[]);
3039            pass.draw(0..3, 0..1);
3040        }
3041        let row = 256_u32;
3042        let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
3043            label: Some("shadow readback"),
3044            size: u64::from(row) * 64,
3045            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3046            mapped_at_creation: false,
3047        });
3048        encoder.copy_texture_to_buffer(
3049            color.as_image_copy(),
3050            wgpu::TexelCopyBufferInfo {
3051                buffer: &buffer,
3052                layout: wgpu::TexelCopyBufferLayout {
3053                    offset: 0,
3054                    bytes_per_row: Some(row),
3055                    rows_per_image: Some(64),
3056                },
3057            },
3058            color.size(),
3059        );
3060        gpu.queue.submit(Some(encoder.finish()));
3061        let (send, receive) = std::sync::mpsc::channel();
3062        let slice = buffer.slice(..);
3063        slice.map_async(wgpu::MapMode::Read, move |result| {
3064            let _ = send.send(result);
3065        });
3066        gpu.device
3067            .poll(wgpu::PollType::wait_indefinitely())
3068            .unwrap();
3069        receive.recv().unwrap().unwrap();
3070        let mapped = slice.get_mapped_range().expect("a mapped range");
3071        let mut pixels = Vec::with_capacity(64 * 64 * 4);
3072        for padded in mapped.chunks_exact(row as usize) {
3073            pixels.extend_from_slice(&padded[..256]);
3074        }
3075        drop(mapped);
3076        buffer.unmap();
3077        let shadowed = pixels
3078            .chunks_exact(4)
3079            .filter(|pixel| pixel[0] > 127)
3080            .count() as u32;
3081        Shot { pixels, shadowed }
3082    }
3083
3084    fn upload(
3085        device: &wgpu::Device,
3086        queue: &wgpu::Queue,
3087        bytes: &[u8],
3088        usage: wgpu::BufferUsages,
3089    ) -> wgpu::Buffer {
3090        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
3091            label: Some("shadow mesh"),
3092            size: bytes.len() as u64,
3093            usage: usage | wgpu::BufferUsages::COPY_DST,
3094            mapped_at_creation: false,
3095        });
3096        queue.write_buffer(&buffer, 0, bytes);
3097        buffer
3098    }
3099
3100    fn unit_cube() -> (Vec<u8>, Vec<u8>) {
3101        let vertices = [
3102            [-0.5_f32, -0.5, -0.5],
3103            [0.5, -0.5, -0.5],
3104            [0.5, 0.5, -0.5],
3105            [-0.5, 0.5, -0.5],
3106            [-0.5, -0.5, 0.5],
3107            [0.5, -0.5, 0.5],
3108            [0.5, 0.5, 0.5],
3109            [-0.5, 0.5, 0.5],
3110        ];
3111        let indices: [u32; 36] = [
3112            0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 5, 1, 0, 4, 5, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7,
3113            4, 1, 5, 6, 1, 6, 2,
3114        ];
3115        let mut positions = Vec::new();
3116        for vertex in vertices {
3117            for value in vertex {
3118                positions.extend_from_slice(&value.to_le_bytes());
3119            }
3120        }
3121        let mut packed = Vec::new();
3122        for index in indices {
3123            packed.extend_from_slice(&index.to_le_bytes());
3124        }
3125        (positions, packed)
3126    }
3127
3128    fn bounds_of(model: &Mat4) -> (f32, f32, f32, f32) {
3129        let min = model.transform_point([-0.5, 0.0, -0.5]);
3130        let max = model.transform_point([0.5, 0.0, 0.5]);
3131        (min[0], max[0], min[2], max[2])
3132    }
3133
3134    fn static_bounds() -> (f32, f32, f32, f32) {
3135        bounds_of(&Mat4::from_columns([
3136            [0.4, 0.0, 0.0, 0.0],
3137            [0.0, 1.0, 0.0, 0.0],
3138            [0.0, 0.0, 0.3, 0.0],
3139            [0.35, 0.7, 0.25, 1.0],
3140        ]))
3141    }
3142
3143    fn world_at(i: u32, j: u32) -> (f32, f32) {
3144        (
3145            -1.0 + (i as f32 + 0.5) / 32.0,
3146            -1.0 + (j as f32 + 0.5) / 32.0,
3147        )
3148    }
3149
3150    fn point_shadowed(shot: &Shot, x: f32, z: f32) -> bool {
3151        let i = ((x + 1.0) * 32.0 - 0.5).round().clamp(0.0, 63.0) as u32;
3152        let j = ((z + 1.0) * 32.0 - 0.5).round().clamp(0.0, 63.0) as u32;
3153        shot.pixels[((j * 64 + i) * 4) as usize] > 127
3154    }
3155
3156    fn assert_shadow(
3157        shot: &Shot,
3158        stays: (f32, f32, f32, f32),
3159        extra: Option<(f32, f32, f32, f32)>,
3160        label: &str,
3161    ) {
3162        let margin = 0.08;
3163        let mut inside = 0_u32;
3164        let mut inside_shadow = 0_u32;
3165        let mut outside_shadow = 0_u32;
3166        for j in 0..64 {
3167            for i in 0..64 {
3168                let (x, z) = world_at(i, j);
3169                let in_static = x > stays.0 + margin
3170                    && x < stays.1 - margin
3171                    && z > stays.2 + margin
3172                    && z < stays.3 - margin;
3173                let in_extra = extra.is_some_and(|bounds| {
3174                    x > bounds.0 + margin
3175                        && x < bounds.1 - margin
3176                        && z > bounds.2 + margin
3177                        && z < bounds.3 - margin
3178                });
3179                let required = in_static || in_extra;
3180                let clear_of_static = x < stays.0 - margin
3181                    || x > stays.1 + margin
3182                    || z < stays.2 - margin
3183                    || z > stays.3 + margin;
3184                let clear_of_extra = extra.is_none_or(|bounds| {
3185                    x < bounds.0 - margin
3186                        || x > bounds.1 + margin
3187                        || z < bounds.2 - margin
3188                        || z > bounds.3 + margin
3189                });
3190                let shadowed = shot.pixels[((j * 64 + i) * 4) as usize] > 127;
3191                if required {
3192                    inside += 1;
3193                    if shadowed {
3194                        inside_shadow += 1;
3195                    }
3196                }
3197                if clear_of_static && clear_of_extra && shadowed {
3198                    outside_shadow += 1;
3199                }
3200            }
3201        }
3202        assert!(
3203            inside > 20 && inside_shadow * 10 >= inside * 9 && outside_shadow < 30,
3204            "{label}: inside {inside_shadow}/{inside}, outside {outside_shadow}, total {}\n{}",
3205            shot.shadowed,
3206            ascii(&shot.pixels)
3207        );
3208        assert!(
3209            (80..900).contains(&shot.shadowed),
3210            "{label}: {} shadowed\n{}",
3211            shot.shadowed,
3212            ascii(&shot.pixels)
3213        );
3214    }
3215
3216    fn ascii(pixels: &[u8]) -> String {
3217        let mut out = String::new();
3218        for j in (0..64).step_by(2) {
3219            for i in (0..64).step_by(2) {
3220                out.push(if pixels[((j * 64 + i) * 4) as usize] > 127 {
3221                    '#'
3222                } else {
3223                    '.'
3224                });
3225            }
3226            out.push('\n');
3227        }
3228        out
3229    }
3230}