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}