1use bytemuck::{Pod, Zeroable};
2use pfx_gpu::wgpu;
3use pfx_physics::{Boid, Emitter, ParticleKind, Speck};
4use wgpu::util::DeviceExt;
5
6pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
7pub const SCENE_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
8
9#[repr(C)]
10#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
11pub struct Plume {
12 pub source: [f32; 3],
13 pub radius: f32,
14 pub spread: [f32; 2],
15 pub drift: [[f32; 2]; 2],
16 pub sway: [[f32; 5]; 2],
17 pub fade: [f32; 3],
18 pub depth_per_width: f32,
19 pub grain: [f32; 2],
20 pub lift: f32,
21 pub warp: [f32; 2],
22 pub churn: f32,
23 pub threshold: [f32; 2],
24}
25
26impl Default for Plume {
27 fn default() -> Self {
28 Self {
29 source: [0.0; 3],
30 radius: 0.02,
31 spread: [1.0, 2.0],
32 drift: [[0.0; 2]; 2],
33 sway: [[10.0, 1.0, 0.005, 0.5, 2.0]; 2],
34 fade: [0.01, 0.05, 0.15],
35 depth_per_width: 6.0,
36 grain: [60.0, 40.0],
37 lift: 0.05,
38 warp: [2.0, 0.2],
39 churn: 0.6,
40 threshold: [0.4, 0.85],
41 }
42 }
43}
44
45impl Plume {
46 pub fn at(source: [f32; 3]) -> Self {
47 Self {
48 source,
49 ..Self::default()
50 }
51 }
52
53 fn look(&self) -> [f32; 28] {
54 let [sx, sz] = self.sway;
55 [
56 self.spread[0],
57 self.spread[1],
58 self.drift[0][0],
59 self.drift[0][1],
60 self.drift[1][0],
61 self.drift[1][1],
62 sx[0],
63 sx[1],
64 sx[2],
65 sx[3],
66 sx[4],
67 sz[0],
68 sz[1],
69 sz[2],
70 sz[3],
71 sz[4],
72 self.fade[0],
73 self.fade[1],
74 self.fade[2],
75 self.depth_per_width,
76 self.grain[0],
77 self.grain[1],
78 self.lift,
79 self.warp[0],
80 self.warp[1],
81 self.churn,
82 self.threshold[0],
83 self.threshold[1],
84 ]
85 }
86
87 fn key(&self) -> [u32; 28] {
88 self.look().map(f32::to_bits)
89 }
90
91 fn constants(&self) -> Vec<(&'static str, f64)> {
92 PLUME_CONSTANTS
93 .iter()
94 .zip(self.look())
95 .map(|(name, value)| (*name, f64::from(value)))
96 .collect()
97 }
98}
99
100const MAX_VOLUME_PIPELINES: usize = 16;
101
102fn volume_constants(cuts: VolumeCuts, plume: &Plume) -> Vec<(&'static str, f64)> {
103 let mut constants = cuts.constants().to_vec();
104 constants.extend(plume.constants());
105 constants
106}
107
108const PLUME_CONSTANTS: [&str; 28] = [
109 "SPREAD_BASE",
110 "SPREAD_GROWTH",
111 "DRIFT_X_QUAD",
112 "DRIFT_X_LIN",
113 "DRIFT_Z_QUAD",
114 "DRIFT_Z_LIN",
115 "SWAY_X_WAVE",
116 "SWAY_X_RATE",
117 "SWAY_X_AMP",
118 "SWAY_X_BASE",
119 "SWAY_X_GROWTH",
120 "SWAY_Z_WAVE",
121 "SWAY_Z_RATE",
122 "SWAY_Z_AMP",
123 "SWAY_Z_BASE",
124 "SWAY_Z_GROWTH",
125 "FADE_IN",
126 "FADE_OUT_START",
127 "FADE_OUT_END",
128 "DEPTH_PER_WIDTH",
129 "GRAIN_H",
130 "GRAIN_V",
131 "LIFT_RATE",
132 "WARP_AMP",
133 "WARP_RATE",
134 "CHURN",
135 "THRESHOLD_LOW",
136 "THRESHOLD_HIGH",
137];
138
139#[derive(Clone, Copy, Debug, PartialEq)]
140pub enum VolumeKind {
141 Plume(Plume),
142 Smoke,
143}
144
145pub const HAZE_AMBIENT_GREY: f32 = 0.03;
146
147#[derive(Clone, Copy, Debug, PartialEq)]
148pub struct RoomHaze {
149 pub fog: f32,
150 pub smoke: f32,
151 pub mist: f32,
152 pub floor: f32,
153 pub phase: f32,
154 pub back: f32,
155 pub gold: [f32; 3],
156 pub ambient: [f32; 3],
157 pub reach: f32,
158 pub unmapped: f32,
159 pub lo: [f32; 3],
160 pub hi: [f32; 3],
161 pub seed: u32,
162}
163
164impl RoomHaze {
165 pub fn new(lo: [f32; 3], hi: [f32; 3]) -> Self {
166 Self {
167 fog: 0.0,
168 smoke: 0.0,
169 mist: 0.0,
170 floor: 0.0,
171 phase: 0.3,
172 back: 0.5,
173 gold: [1.0; 3],
174 ambient: [HAZE_AMBIENT_GREY; 3],
175 reach: 3.0,
176 unmapped: 0.0,
177 lo,
178 hi,
179 seed: 0,
180 }
181 }
182
183 pub fn from_amount(lo: [f32; 3], hi: [f32; 3], amount: f32) -> Self {
184 let amount = if amount.is_nan() {
185 0.0
186 } else {
187 amount.clamp(0.0, 1.0)
188 };
189 Self {
190 fog: amount / 6.0,
191 smoke: amount * 20.0,
192 mist: amount * 0.2,
193 floor: amount * 2.0,
194 ..Self::new(lo, hi)
195 }
196 }
197}
198
199#[derive(Clone, Copy, Debug, PartialEq)]
200pub struct RoomMotes {
201 pub lo: [f32; 3],
202 pub hi: [f32; 3],
203 pub cell: f32,
204 pub share: f32,
205 pub brightness: f32,
206 pub gold: [f32; 3],
207}
208
209impl RoomMotes {
210 pub fn new(
211 lo: [f32; 3],
212 hi: [f32; 3],
213 cell: f32,
214 share: f32,
215 brightness: f32,
216 gold: [f32; 3],
217 ) -> Self {
218 Self {
219 lo,
220 hi,
221 cell,
222 share,
223 brightness,
224 gold,
225 }
226 }
227}
228
229#[repr(C)]
230#[derive(Clone, Copy, Debug, Pod, Zeroable)]
231pub struct EffectFrame {
232 pub view_proj: [[f32; 4]; 4],
233 pub eye: [f32; 4],
234 pub right: [f32; 4],
235 pub up: [f32; 4],
236 pub forward: [f32; 4],
237 pub lens: [f32; 4],
238 pub sun: [f32; 4],
239 pub sun_color: [f32; 4],
240 pub extent: [f32; 4],
241 pub volume_lo: [f32; 4],
242 pub volume_hi: [f32; 4],
243 pub volume_color: [f32; 4],
244 pub volume_control: [f32; 4],
245 pub room_light: [f32; 4],
246 pub room_shape: [f32; 4],
247 pub room_lo: [f32; 4],
248 pub room_hi: [f32; 4],
249 pub plume: Plume,
250}
251
252impl EffectFrame {
253 pub fn new(width: u32, height: u32, time: f32, seed: u32) -> Self {
254 Self {
255 view_proj: [
256 [1.0, 0.0, 0.0, 0.0],
257 [0.0, 1.0, 0.0, 0.0],
258 [0.0, 0.0, 1.0, 0.0],
259 [0.0, 0.0, 0.0, 1.0],
260 ],
261 eye: [0.0, 0.0, 3.0, 0.0],
262 right: [1.0, 0.0, 0.0, 0.0],
263 up: [0.0, 1.0, 0.0, 0.0],
264 forward: [0.0, 0.0, -1.0, 0.0],
265 lens: [1.0, 1.0, 0.0, 0.0],
266 sun: [0.4, 0.8, 0.4, 0.0],
267 sun_color: [1.0, 0.9, 0.7, 1.0],
268 extent: [width as f32, height as f32, time, f32::from_bits(seed)],
269 volume_lo: [-0.25, -0.4, -0.5, 0.0],
270 volume_hi: [0.25, 0.7, 0.5, 0.0],
271 volume_color: [0.75, 0.8, 0.85, 0.0],
272 volume_control: [2.0, 0.35, 1.0, 0.0],
273 room_light: [0.0; 4],
274 room_shape: [0.0; 4],
275 room_lo: [0.0; 4],
276 room_hi: [1.0; 4],
277 plume: Plume::default(),
278 }
279 }
280
281 pub fn set_volume(
282 &mut self,
283 kind: VolumeKind,
284 lo: [f32; 3],
285 hi: [f32; 3],
286 color: [f32; 3],
287 density: f32,
288 anisotropy: f32,
289 ) {
290 self.volume_lo = [lo[0], lo[1], lo[2], 0.0];
291 self.volume_hi = [hi[0], hi[1], hi[2], 0.0];
292 self.volume_color = [color[0], color[1], color[2], 0.0];
293 self.volume_control = [
294 density.max(0.0),
295 anisotropy.clamp(-0.9, 0.9),
296 1.0,
297 if kind == VolumeKind::Smoke { 1.0 } else { 0.0 },
298 ];
299 if let VolumeKind::Plume(plume) = kind {
300 self.plume = plume;
301 }
302 }
303
304 pub fn scale_time(&mut self, scale: f32) {
305 self.extent[2] *= scale.max(0.0);
306 }
307
308 pub fn set_room_haze(&mut self, haze: RoomHaze) {
309 self.volume_lo = [-1.0e4, -1.0e4, -1.0e4, haze.floor.max(0.0)];
310 self.volume_hi = [1.0e4, 1.0e4, 1.0e4, haze.smoke.max(0.0)];
311 self.volume_color = [
312 haze.ambient[0].max(0.0),
313 haze.ambient[1].max(0.0),
314 haze.ambient[2].max(0.0),
315 haze.mist.max(0.0),
316 ];
317 self.volume_control = [haze.fog.max(0.0), haze.phase.clamp(-0.9, 0.9), 1.0, 2.0];
318 self.room_light = [
319 haze.gold[0].max(0.0),
320 haze.gold[1].max(0.0),
321 haze.gold[2].max(0.0),
322 haze.back.clamp(0.0, 1.0),
323 ];
324 self.room_shape = [haze.reach.max(0.0), haze.unmapped.clamp(0.0, 1.0), 0.0, 0.0];
325 self.room_lo = [haze.lo[0], haze.lo[1], haze.lo[2], 0.0];
326 self.room_hi = [haze.hi[0], haze.hi[1], haze.hi[2], 0.0];
327 }
328}
329
330pub fn fog_transmittance(fog: f32, distance: f32, reach: f32) -> f32 {
331 (-fog.max(0.0) * distance.max(0.0).min(reach.max(0.0))).exp()
332}
333
334pub fn fog_in_scatter(density: f32, fog: f32, light: f32, distance: f32) -> f32 {
335 let distance = distance.max(0.0);
336 if fog <= 0.0 {
337 return density * light * distance;
338 }
339 density * light * (1.0 - (-fog * distance).exp()) / fog
340}
341
342pub fn henyey_greenstein(cosine: f32, g: f32) -> f32 {
343 let d = 1.0 + g * g - 2.0 * g * cosine;
344 (1.0 - g * g) / (4.0 * std::f32::consts::PI * d * d.sqrt())
345}
346
347fn cell_hash(cell: [i32; 3], seed: u32) -> f32 {
348 let z = hash((cell[2] as u32).wrapping_mul(83_492_791) ^ seed);
349 let y = hash((cell[1] as u32).wrapping_mul(19_349_663) ^ z);
350 let h = hash((cell[0] as u32).wrapping_mul(73_856_093) ^ y);
351 (h >> 8) as f32 / 16_777_216.0
352}
353
354pub fn room_motes(seed: u32, time: f32, motes: RoomMotes) -> Vec<ParticleInstance> {
355 let cell = motes.cell.max(1.0e-3);
356 let flow = [0.0021 * time, 0.0012 * time, -0.0009 * time];
357 let range = |axis: usize| {
358 let first = ((motes.lo[axis] - flow[axis]) / cell).floor() as i32;
359 let last = ((motes.hi[axis] - flow[axis]) / cell).floor() as i32;
360 first..=last
361 };
362 let add =
363 |c: [i32; 3], offset: [i32; 3]| [c[0] + offset[0], c[1] + offset[1], c[2] + offset[2]];
364 let mut out = Vec::new();
365 for x in range(0) {
366 for y in range(1) {
367 for z in range(2) {
368 let c = [x, y, z];
369 let h = cell_hash(add(c, [17, 5, 91]), seed);
370 if h >= motes.share {
371 continue;
372 }
373 let j = [
374 cell_hash(add(c, [3, 0, 0]), seed),
375 cell_hash(add(c, [0, 7, 0]), seed),
376 cell_hash(add(c, [0, 0, 11]), seed),
377 ];
378 let wander = [
379 (time * 0.31 + h * 40.0).sin() * 0.12,
380 (time * 0.23 + j[0] * 30.0).sin() * 0.12,
381 (time * 0.27 + j[1] * 20.0).cos() * 0.12,
382 ];
383 let centre: [f32; 3] = std::array::from_fn(|axis| {
384 (c[axis] as f32 + 0.5 + (j[axis] - 0.5) * 0.5 + wander[axis]) * cell
385 + flow[axis]
386 });
387 let radius = 0.00025 + 0.0005 * j[2] * j[2];
388 let twinkle = 0.55 + 0.45 * (time * (0.7 + 1.3 * j[0]) + h * 60.0).sin();
389 let glow = motes.brightness * twinkle;
390 out.push(ParticleInstance {
391 position_size: [centre[0], centre[1], centre[2], radius],
392 velocity_alpha: [0.0, 0.0, 0.0, 1.0],
393 color_rotation: [
394 motes.gold[0] * glow,
395 motes.gold[1] * glow,
396 motes.gold[2] * glow,
397 0.0,
398 ],
399 detail: [1.0, 0.0, h, 1.0],
400 });
401 }
402 }
403 }
404 out
405}
406
407pub fn plume_optical_depth(
408 density: f32,
409 path_length: f32,
410 box_width: f32,
411 depth_per_width: f32,
412) -> f32 {
413 density.max(0.0) * path_length.max(0.0) * depth_per_width / box_width.max(1.0e-6)
414}
415
416#[repr(C)]
417#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
418pub struct ParticleInstance {
419 pub position_size: [f32; 4],
420 pub velocity_alpha: [f32; 4],
421 pub color_rotation: [f32; 4],
422 pub detail: [f32; 4],
423}
424
425fn hash(seed: u32) -> u32 {
426 let state = seed.wrapping_mul(747_796_405).wrapping_add(2_891_336_453);
427 let mixed = ((state >> ((state >> 28) + 4)) ^ state).wrapping_mul(277_803_737);
428 (mixed >> 22) ^ mixed
429}
430
431pub fn pack_particles(
432 specks: &[Speck],
433 kind: ParticleKind,
434 time: f32,
435 color: [f32; 3],
436) -> Vec<ParticleInstance> {
437 specks
438 .iter()
439 .filter(|speck| speck.life > 0.0 && speck.age < speck.life)
440 .map(|speck| {
441 let random = hash(speck.seed) as f32 / u32::MAX as f32;
442 let life = (1.0 - speck.age / speck.life).clamp(0.0, 1.0);
443 let (size, alpha, kind_id) = match kind {
444 ParticleKind::Petal => (0.035 + 0.02 * random, 0.8, 0.0),
445 ParticleKind::Dust => (0.003 + 0.004 * random, 0.5, 1.0),
446 ParticleKind::Steam => (0.08 + 0.04 * random, 0.2, 2.0),
447 };
448 let speed = (speck.vel[0] * speck.vel[0]
449 + speck.vel[1] * speck.vel[1]
450 + speck.vel[2] * speck.vel[2])
451 .sqrt();
452 let angle = random * std::f32::consts::TAU
453 + time
454 * (1.0 + random * 2.0)
455 * if kind == ParticleKind::Petal {
456 1.0
457 } else {
458 0.1
459 };
460 ParticleInstance {
461 position_size: [speck.pos[0], speck.pos[1], speck.pos[2], size],
462 velocity_alpha: [
463 speck.vel[0],
464 speck.vel[1],
465 speck.vel[2],
466 alpha * life.min(0.2 + speck.age * 4.0),
467 ],
468 color_rotation: [color[0], color[1], color[2], angle],
469 detail: [kind_id, speed, random, 0.0],
470 }
471 })
472 .collect()
473}
474
475pub fn pack_emitter(
476 emitter: &Emitter,
477 kind: ParticleKind,
478 time: f32,
479 color: [f32; 3],
480) -> Vec<ParticleInstance> {
481 pack_particles(emitter.specks(), kind, time, color)
482}
483
484#[derive(Clone, Copy, Debug, PartialEq, Eq)]
485pub enum CreatureKind {
486 Bird,
487 Fish,
488}
489
490#[repr(C)]
491#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
492pub struct CreatureInstance {
493 pub position_scale: [f32; 4],
494 pub right: [f32; 4],
495 pub up: [f32; 4],
496 pub forward: [f32; 4],
497 pub motion: [f32; 4],
498}
499
500fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
501 [
502 a[1] * b[2] - a[2] * b[1],
503 a[2] * b[0] - a[0] * b[2],
504 a[0] * b[1] - a[1] * b[0],
505 ]
506}
507
508fn unit(a: [f32; 3]) -> [f32; 3] {
509 let length = (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt();
510 if length < 1e-6 {
511 [0.0, 0.0, -1.0]
512 } else {
513 a.map(|v| v / length)
514 }
515}
516
517fn distance_squared(position: [f32; 4], eye: [f32; 4]) -> f32 {
518 (0..3)
519 .map(|axis| (position[axis] - eye[axis]).powi(2))
520 .sum()
521}
522
523pub fn pack_creatures(
524 boids: &[Boid],
525 kind: CreatureKind,
526 size: f32,
527 seed: u32,
528) -> Vec<CreatureInstance> {
529 boids
530 .iter()
531 .enumerate()
532 .map(|(index, boid)| {
533 let forward = unit(boid.vel);
534 let reference = if forward[1].abs() > 0.95 {
535 [0.0, 0.0, 1.0]
536 } else {
537 [0.0, 1.0, 0.0]
538 };
539 let right = unit(cross(reference, forward));
540 let up = cross(forward, right);
541 let speed =
542 (boid.vel[0] * boid.vel[0] + boid.vel[1] * boid.vel[1] + boid.vel[2] * boid.vel[2])
543 .sqrt();
544 let phase = hash(seed ^ index as u32) as f32 / u32::MAX as f32 * std::f32::consts::TAU;
545 CreatureInstance {
546 position_scale: [boid.pos[0], boid.pos[1], boid.pos[2], size],
547 right: [right[0], right[1], right[2], 0.0],
548 up: [up[0], up[1], up[2], 0.0],
549 forward: [forward[0], forward[1], forward[2], 0.0],
550 motion: [
551 phase,
552 speed,
553 if kind == CreatureKind::Bird { 0.0 } else { 1.0 },
554 0.0,
555 ],
556 }
557 })
558 .collect()
559}
560
561#[repr(C)]
562#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
563pub struct CreatureVertex {
564 pub position: [f32; 4],
565}
566
567pub fn upsample_weights(
568 full_depth: f32,
569 depths: [f32; 4],
570 fractions: [f32; 2],
571 depth_scale: f32,
572) -> [f32; 4] {
573 let x = fractions[0].clamp(0.0, 1.0);
574 let y = fractions[1].clamp(0.0, 1.0);
575 let spatial = [(1.0 - x) * (1.0 - y), x * (1.0 - y), (1.0 - x) * y, x * y];
576 let mut weights = [0.0; 4];
577 let mut total = 0.0;
578 for i in 0..4 {
579 let difference = (full_depth - depths[i]).abs();
580 weights[i] = spatial[i] * (-difference * depth_scale.max(0.0)).exp();
581 total += weights[i];
582 }
583 if total > 1e-6 {
584 weights.map(|weight| weight / total)
585 } else {
586 let nearest = depths
587 .iter()
588 .enumerate()
589 .min_by(|left, right| {
590 (full_depth - *left.1)
591 .abs()
592 .total_cmp(&(full_depth - *right.1).abs())
593 })
594 .map(|(index, _)| index)
595 .unwrap_or(0);
596 let mut fallback = [0.0; 4];
597 fallback[nearest] = 1.0;
598 fallback
599 }
600}
601
602fn volume_bounds(frame: &EffectFrame, width: u32, height: u32) -> Option<(u32, u32, u32, u32)> {
603 let mut min = [f32::INFINITY; 2];
604 let mut max = [f32::NEG_INFINITY; 2];
605 for x in [frame.volume_lo[0], frame.volume_hi[0]] {
606 for y in [frame.volume_lo[1], frame.volume_hi[1]] {
607 for z in [frame.volume_lo[2], frame.volume_hi[2]] {
608 let point = [x, y, z, 1.0];
609 let mut clip = [0.0; 4];
610 for (row, value) in clip.iter_mut().enumerate() {
611 *value = (0..4)
612 .map(|column| frame.view_proj[column][row] * point[column])
613 .sum();
614 }
615 if clip[3] <= 0.0 || !clip.iter().all(|value| value.is_finite()) {
616 return Some((0, 0, width, height));
617 }
618 let screen = [
619 (clip[0] / clip[3] * 0.5 + 0.5) * width as f32,
620 (0.5 - clip[1] / clip[3] * 0.5) * height as f32,
621 ];
622 for axis in 0..2 {
623 min[axis] = min[axis].min(screen[axis]);
624 max[axis] = max[axis].max(screen[axis]);
625 }
626 }
627 }
628 }
629 let left = (min[0] - 8.0).floor().clamp(0.0, width as f32) as u32;
630 let top = (min[1] - 8.0).floor().clamp(0.0, height as f32) as u32;
631 let right = (max[0] + 8.0).ceil().clamp(0.0, width as f32) as u32;
632 let bottom = (max[1] + 8.0).ceil().clamp(0.0, height as f32) as u32;
633 if right <= left || bottom <= top {
634 None
635 } else {
636 Some((left, top, right - left, bottom - top))
637 }
638}
639
640pub const PARTICLE_WGSL: &str = r#"
641struct Frame {
642 view_proj: mat4x4f, eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
643 lens: vec4f, sun: vec4f, sun_color: vec4f, extent: vec4f,
644 volume_lo: vec4f, volume_hi: vec4f, volume_color: vec4f, volume_control: vec4f,
645}
646struct Shadow {
647 splits: vec4f, filter_params: vec4f, sun: vec4f, texels: vec4f,
648 view_proj_0: mat4x4f, view_proj_1: mat4x4f, view_proj_2: mat4x4f,
649}
650struct Particle {
651 position_size: vec4f, velocity_alpha: vec4f, color_rotation: vec4f, detail: vec4f,
652}
653@group(0) @binding(0) var<uniform> frame: Frame;
654@group(0) @binding(1) var scene_depth: texture_2d<f32>;
655@group(0) @binding(2) var<storage, read> particles: array<Particle>;
656@group(0) @binding(3) var shadow_atlas: texture_depth_2d_array;
657@group(0) @binding(4) var<uniform> shadow: Shadow;
658struct Out { @builtin(position) clip: vec4f, @location(0) uv: vec2f, @location(1) color: vec4f, @location(2) distance: f32, @location(3) kind: f32, @location(4) world: vec3f, @location(5) lit: f32, }
659@vertex fn vs_main(@builtin(vertex_index) vertex: u32, @builtin(instance_index) index: u32) -> Out {
660 let p = particles[index];
661 let corners = array<vec2f, 6>(vec2f(-1.0,-1.0),vec2f(1.0,-1.0),vec2f(-1.0,1.0),vec2f(-1.0,1.0),vec2f(1.0,-1.0),vec2f(1.0,1.0));
662 let corner = corners[vertex];
663 let spin = p.color_rotation.w;
664 var axis_x = frame.right.xyz * cos(spin) + frame.up.xyz * sin(spin);
665 var axis_y = frame.up.xyz * cos(spin) - frame.right.xyz * sin(spin);
666 if (p.detail.x > 0.5 && p.detail.x < 1.5 && p.detail.y > 0.02) {
667 let v = normalize(p.velocity_alpha.xyz - frame.forward.xyz * dot(p.velocity_alpha.xyz, frame.forward.xyz));
668 axis_y = v;
669 axis_x = cross(axis_y, frame.forward.xyz);
670 }
671 let tumble = select(1.0, 0.45 + 0.55 * abs(sin(spin * 0.67)), p.detail.x < 0.5);
672 let world = p.position_size.xyz + (axis_x * corner.x * tumble + axis_y * corner.y) * p.position_size.w;
673 var out: Out;
674 out.clip = frame.view_proj * vec4f(world, 1.0);
675 out.uv = corner;
676 out.color = vec4f(p.color_rotation.xyz, p.velocity_alpha.w);
677 out.distance = length(world - frame.eye.xyz);
678 out.kind = p.detail.x;
679 out.world = world;
680 out.lit = p.detail.w;
681 return out;
682}
683fn sunlight(world: vec3f, distance: f32) -> f32 {
684 if (shadow.splits.w <= 0.0) { return 1.0; }
685 var layer = 0;
686 var matrix = shadow.view_proj_0;
687 if (distance > shadow.splits.y) { layer = 1; matrix = shadow.view_proj_1; }
688 if (distance > shadow.splits.z) { layer = 2; matrix = shadow.view_proj_2; }
689 let clip = matrix * vec4f(world, 1.0);
690 let point = clip.xyz / clip.w;
691 let uv = point.xy * vec2f(0.5, -0.5) + vec2f(0.5);
692 if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0)) || point.z < 0.0 || point.z > 1.0) { return 1.0; }
693 let size = textureDimensions(shadow_atlas);
694 let at = clamp(vec2i(uv * vec2f(size)), vec2i(0), vec2i(size) - vec2i(1));
695 let depth = textureLoad(shadow_atlas, at, layer, 0);
696 return select(0.0, 1.0, point.z - shadow.sun.w <= depth);
697}
698@fragment fn fs_main(in: Out) -> @location(0) vec4f {
699 let pixel = vec2i(in.clip.xy);
700 let depth = textureLoad(scene_depth, pixel, 0).x;
701 let soft = clamp((depth - in.distance) * 4.0, 0.0, 1.0);
702 let r = length(in.uv);
703 let petal = smoothstep(1.0, 0.7, length(vec2f(in.uv.x, in.uv.y * 1.35)));
704 let dust = exp(-r * r * 5.0) * (0.7 + 0.3 * pow(max(0.0, 1.0 - r), 16.0));
705 let steam = exp(-r * r * 3.0);
706 let shape = select(select(petal, dust, in.kind > 0.5), steam, in.kind > 1.5);
707 if (in.lit > 0.5) {
708 let glint = max(1.0 - r, 0.0) * select(0.0, 1.0, depth > dot(in.world - frame.eye.xyz, frame.forward.xyz));
709 return vec4f(in.color.rgb * frame.sun_color.rgb * frame.sun_color.w * sunlight(in.world, in.distance) * glint, 0.0);
710 }
711 let alpha = in.color.a * shape * soft;
712 return vec4f(in.color.rgb * alpha, alpha);
713}
714"#;
715
716pub const CREATURE_WGSL: &str = r#"
717struct Frame {
718 view_proj: mat4x4f, eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
719 lens: vec4f, sun: vec4f, sun_color: vec4f, extent: vec4f,
720 volume_lo: vec4f, volume_hi: vec4f, volume_color: vec4f, volume_control: vec4f,
721}
722struct Creature { position_scale: vec4f, right: vec4f, up: vec4f, forward: vec4f, motion: vec4f, }
723@group(0) @binding(0) var<uniform> frame: Frame;
724@group(0) @binding(1) var<storage, read> creatures: array<Creature>;
725@group(0) @binding(2) var<storage, read> vertices: array<vec4f>;
726@group(0) @binding(3) var scene_depth: texture_2d<f32>;
727struct Out { @builtin(position) clip: vec4f, @location(0) color: vec3f, @location(1) distance: f32, }
728@vertex fn vs_main(@builtin(vertex_index) vertex: u32, @builtin(instance_index) index: u32) -> Out {
729 let instance = creatures[index];
730 var local = vertices[vertex].xyz;
731 let phase = frame.extent.z * (4.0 + instance.motion.y * 1.3) + instance.motion.x;
732 if (instance.motion.z < 0.5) {
733 local.y += sin(phase) * abs(local.x) * 0.5;
734 } else {
735 local.x += sin(phase + local.z * 4.0) * max(-local.z, 0.0) * 0.2;
736 }
737 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);
738 var out: Out;
739 out.clip = frame.view_proj * vec4f(world, 1.0);
740 out.distance = length(world - frame.eye.xyz);
741 let light = 0.45 + 0.55 * max(0.0, dot(normalize(instance.up.xyz + vec3f(0.0, 0.3, 0.0)), normalize(frame.sun.xyz)));
742 out.color = select(vec3f(0.72, 0.58, 0.43), vec3f(0.28, 0.54, 0.63), instance.motion.z > 0.5) * light * frame.sun_color.rgb;
743 return out;
744}
745@fragment fn fs_main(in: Out) -> @location(0) vec4f {
746 if (in.distance > textureLoad(scene_depth, vec2i(in.clip.xy), 0).x) { discard; }
747 return vec4f(in.color, 1.0);
748}
749"#;
750
751pub const VOLUME_WGSL: &str = r#"
752struct Frame {
753 view_proj: mat4x4f, eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
754 lens: vec4f, sun: vec4f, sun_color: vec4f, extent: vec4f,
755 volume_lo: vec4f, volume_hi: vec4f, volume_color: vec4f, volume_control: vec4f,
756 room_light: vec4f, room_shape: vec4f, room_lo: vec4f, room_hi: vec4f,
757 plume: array<vec4f, 8>,
758}
759struct Shadow {
760 splits: vec4f, filter_params: vec4f, sun: vec4f, texels: vec4f,
761 view_proj_0: mat4x4f, view_proj_1: mat4x4f, view_proj_2: mat4x4f,
762}
763override STEPS: u32 = 20u;
764override WARP_OCTAVES: i32 = 4;
765override OCTAVES: i32 = 4;
766override EARLY_OUT: bool = false;
767override SPREAD_BASE: f32 = 1.0;
768override SPREAD_GROWTH: f32 = 2.0;
769override DRIFT_X_QUAD: f32 = 0.0;
770override DRIFT_X_LIN: f32 = 0.0;
771override DRIFT_Z_QUAD: f32 = 0.0;
772override DRIFT_Z_LIN: f32 = 0.0;
773override SWAY_X_WAVE: f32 = 10.0;
774override SWAY_X_RATE: f32 = 1.0;
775override SWAY_X_AMP: f32 = 0.005;
776override SWAY_X_BASE: f32 = 0.5;
777override SWAY_X_GROWTH: f32 = 2.0;
778override SWAY_Z_WAVE: f32 = 10.0;
779override SWAY_Z_RATE: f32 = 1.0;
780override SWAY_Z_AMP: f32 = 0.005;
781override SWAY_Z_BASE: f32 = 0.5;
782override SWAY_Z_GROWTH: f32 = 2.0;
783override FADE_IN: f32 = 0.01;
784override FADE_OUT_START: f32 = 0.05;
785override FADE_OUT_END: f32 = 0.15;
786override DEPTH_PER_WIDTH: f32 = 6.0;
787override GRAIN_H: f32 = 60.0;
788override GRAIN_V: f32 = 40.0;
789override LIFT_RATE: f32 = 0.05;
790override WARP_AMP: f32 = 2.0;
791override WARP_RATE: f32 = 0.2;
792override CHURN: f32 = 0.6;
793override THRESHOLD_LOW: f32 = 0.4;
794override THRESHOLD_HIGH: f32 = 0.85;
795@group(0) @binding(0) var<uniform> frame: Frame;
796@group(0) @binding(1) var scene_depth: texture_2d<f32>;
797@group(0) @binding(2) var shadow_atlas: texture_depth_2d_array;
798@group(0) @binding(3) var<uniform> shadow: Shadow;
799@group(0) @binding(4) var room_field: texture_3d<f32>;
800@group(0) @binding(5) var room_sampler: sampler;
801struct Out { @builtin(position) clip: vec4f, @location(0) uv: vec2f, }
802@vertex fn vs_main(@builtin(vertex_index) vertex: u32) -> Out {
803 let xy = array<vec2f, 3>(vec2f(-1.0,-1.0),vec2f(3.0,-1.0),vec2f(-1.0,3.0));
804 var out: Out;
805 out.clip = vec4f(xy[vertex], 0.0, 1.0);
806 out.uv = xy[vertex] * vec2f(0.5,-0.5) + vec2f(0.5);
807 return out;
808}
809fn pcg(value: u32) -> u32 {
810 let state = value * 747796405u + 2891336453u;
811 let mixed = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
812 return (mixed >> 22u) ^ mixed;
813}
814fn hash3(p: vec3i) -> f32 {
815 let seed = select(0u, bitcast<u32>(frame.extent.w), frame.volume_control.w > 0.5);
816 let h = pcg(u32(p.x) * 73856093u ^ u32(p.y) * 19349663u ^ u32(p.z) * 83492791u ^ seed);
817 return f32(h >> 8u) / 16777216.0;
818}
819fn noise3(p: vec3f) -> f32 {
820 let i = vec3i(floor(p));
821 let f = fract(p);
822 let t = f * f * (3.0 - 2.0 * f);
823 let a = mix(mix(hash3(i), hash3(i + vec3i(1,0,0)), t.x), mix(hash3(i + vec3i(0,1,0)), hash3(i + vec3i(1,1,0)), t.x), t.y);
824 let b = mix(mix(hash3(i + vec3i(0,0,1)), hash3(i + vec3i(1,0,1)), t.x), mix(hash3(i + vec3i(0,1,1)), hash3(i + vec3i(1,1,1)), t.x), t.y);
825 return mix(a, b, t.z);
826}
827fn fbm(p: vec3f, octaves: i32) -> f32 {
828 var sum = 0.0;
829 var amplitude = 0.55;
830 var q = p;
831 for (var i = 0; i < octaves; i++) {
832 sum += noise3(q) * amplitude;
833 q = q * 2.07 + vec3f(1.7, 9.2, 3.1);
834 amplitude *= 0.5;
835 }
836 return sum * (1.03125 / (1.1 * (1.0 - pow(0.5, f32(octaves)))));
837}
838fn density(p: vec3f) -> f32 {
839 let rise = p.y - frame.volume_lo.y;
840 let lift = p.y - frame.plume[0].y;
841 let smoke = frame.volume_control.w > 0.5;
842 let width = select(max(frame.volume_hi.z - frame.volume_lo.z, 0.000001), max(frame.volume_hi.x - frame.volume_lo.x, 0.000001), smoke);
843 let origin = frame.plume[0].xyz;
844 let drift = vec3f(DRIFT_X_QUAD * lift * lift + DRIFT_X_LIN * lift, 0.0, DRIFT_Z_QUAD * lift * lift + DRIFT_Z_LIN * lift);
845 let sway = vec3f(sin(lift * SWAY_X_WAVE - frame.extent.z * SWAY_X_RATE) * SWAY_X_AMP * (SWAY_X_BASE + lift * SWAY_X_GROWTH), 0.0, cos(lift * SWAY_Z_WAVE - frame.extent.z * SWAY_Z_RATE) * SWAY_Z_AMP * (SWAY_Z_BASE + lift * SWAY_Z_GROWTH));
846 let steam_centre = origin + vec3f(0.0, lift, 0.0) + drift + sway;
847 let smoke_rise = clamp(rise / max(frame.volume_hi.y - frame.volume_lo.y, 0.001), 0.0, 1.0);
848 let smoke_centre = (frame.volume_lo.xyz + frame.volume_hi.xyz) * 0.5 + vec3f(sin(smoke_rise * 9.0 - frame.extent.z * 1.3) * width * 0.29 * smoke_rise, 0.0, cos(smoke_rise * 7.0 - frame.extent.z) * width * 0.21 * smoke_rise);
849 let centre = select(steam_centre, smoke_centre, smoke);
850 let radius = select(frame.plume[0].w * (SPREAD_BASE + lift * SPREAD_GROWTH), width * (0.4 + smoke_rise * 0.25), smoke);
851 let core = exp(-dot((p - centre).xz, (p - centre).xz) / max(radius * radius, 0.00000001));
852 let steam_fade = smoothstep(0.0, FADE_IN, lift) * (1.0 - smoothstep(FADE_OUT_START, FADE_OUT_END, lift));
853 if (EARLY_OUT && !smoke && core * steam_fade < 0.002) { return 0.0; }
854 let q = vec3f(p.x * GRAIN_H, (p.y - frame.extent.z * LIFT_RATE) * GRAIN_V, p.z * GRAIN_H);
855 let warp = vec3f(fbm(q * 0.5 + vec3f(frame.extent.z * WARP_RATE, 0.0, 0.0), WARP_OCTAVES), 0.0, fbm(q * 0.5 + vec3f(3.1, 0.0, frame.extent.z * WARP_RATE), WARP_OCTAVES)) * WARP_AMP;
856 let steam_turbulence = smoothstep(THRESHOLD_LOW, THRESHOLD_HIGH, fbm(q + warp + vec3f(0.0, -frame.extent.z * CHURN, 0.0), OCTAVES));
857 let smoke_q = p * 18.0 + vec3f(0.0, -frame.extent.z * 0.7, 0.0);
858 let smoke_turbulence = smoothstep(0.3, 0.72, noise3(smoke_q) * 0.65 + noise3(smoke_q * 2.07 + vec3f(1.7,9.2,3.1)) * 0.35);
859 let turbulence = select(steam_turbulence, 0.25 + 0.75 * smoke_turbulence, smoke);
860 let smoke_fade = smoothstep(0.0, 0.1, smoke_rise) * (1.0 - smoothstep(0.85, 1.0, smoke_rise));
861 let fade = select(steam_fade, smoke_fade, smoke);
862 if (!smoke && core * fade < 0.002) { return 0.0; }
863 return core * turbulence * fade * frame.volume_control.x;
864}
865fn phase(cosine: f32, g: f32) -> f32 {
866 let g2 = g * g;
867 return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * cosine, 0.001), 1.5);
868}
869struct VolumeOut { @location(0) radiance: vec4f, @location(1) distance: f32, }
870fn room_density(p: vec3f) -> f32 {
871 let drift = vec3f(0.011, 0.0035, -0.004) * frame.extent.z * 1.6;
872 let uvw = (p + drift - frame.room_lo.xyz) / max(frame.room_hi.xyz - frame.room_lo.xyz, vec3f(0.0001));
873 var field = vec4f(0.4375, 0.0, 0.46875, 0.4375);
874 if (all(uvw >= vec3f(0.0)) && all(uvw <= vec3f(1.0))) {
875 field = textureSampleLevel(room_field, room_sampler, uvw, 0.0);
876 }
877 let behind = smoothstep(0.45, 0.02, p.z);
878 let top = smoothstep(0.2, 0.46, p.y) * (0.35 + 0.9 * field.z);
879 let low = exp(-max(p.y, 0.0) / 0.035) * (0.3 + 0.9 * field.w);
880 return frame.volume_control.x * (0.5 + field.x) + behind * (frame.volume_hi.w * field.y + frame.volume_color.w * top) + frame.volume_lo.w * low;
881}
882fn room_sunlight(world: vec3f, distance: f32) -> f32 {
883 if (shadow.splits.w <= 0.0) { return 1.0; }
884 var layer = 0;
885 var matrix = shadow.view_proj_0;
886 if (distance > shadow.splits.y) { layer = 1; matrix = shadow.view_proj_1; }
887 if (distance > shadow.splits.z) { layer = 2; matrix = shadow.view_proj_2; }
888 let clip = matrix * vec4f(world, 1.0);
889 let point = clip.xyz / clip.w;
890 let uv = point.xy * vec2f(0.5, -0.5) + vec2f(0.5);
891 if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0)) || point.z < 0.0 || point.z > 1.0) { return frame.room_shape.y; }
892 let size = textureDimensions(shadow_atlas);
893 let at = clamp(vec2i(uv * vec2f(size)), vec2i(0), vec2i(size) - vec2i(1));
894 return select(0.0, 1.0, point.z - shadow.sun.w <= textureLoad(shadow_atlas, at, layer, 0));
895}
896fn room_phase(cosine: f32, g: f32) -> f32 {
897 let d = 1.0 + g * g - 2.0 * g * cosine;
898 return (1.0 - g * g) / (12.566371 * d * sqrt(d));
899}
900fn room(low: vec2f, limit: f32, ray: vec3f) -> VolumeOut {
901 var out: VolumeOut;
902 out.distance = limit;
903 let along = max(dot(ray, normalize(frame.forward.xyz)), 0.001);
904 let span = min(limit / along, frame.room_shape.x);
905 if (span <= 0.0) {
906 out.radiance = vec4f(0.0);
907 return out;
908 }
909 let steps = 6u;
910 let step_length = span / f32(steps);
911 let turn = f32(pcg(bitcast<u32>(frame.extent.z) ^ bitcast<u32>(frame.extent.w)) >> 8u) / 16777216.0;
912 let jitter = fract(fract(52.9829189 * fract(dot(floor(low), vec2f(0.06711056, 0.00583715)))) + turn);
913 let sun = normalize(frame.sun.xyz);
914 let cosine = dot(ray, sun);
915 let phase = mix(room_phase(cosine, frame.volume_control.y), room_phase(cosine, -0.55), frame.room_light.w);
916 let direct = frame.sun_color.rgb * frame.sun_color.w * frame.room_light.rgb * phase;
917 let fog = frame.volume_control.x;
918 var light = vec3f(0.0);
919 for (var i = 0u; i < steps; i++) {
920 let distance = (f32(i) + jitter) * step_length;
921 let p = frame.eye.xyz + ray * distance;
922 let density = room_density(p);
923 if (density <= 0.0) { continue; }
924 light += density * exp(-fog * distance) * (direct * room_sunlight(p, distance * along) + frame.volume_color.rgb) * step_length;
925 }
926 out.radiance = vec4f(light, 1.0 - exp(-fog * span));
927 return out;
928}
929@fragment fn fs_main(in: Out) -> VolumeOut {
930 let screen = vec2i(clamp(in.uv * frame.extent.xy, vec2f(0.0), frame.extent.xy - vec2f(1.0)));
931 let scene_limit = textureLoad(scene_depth, screen, 0).x;
932 let limit = select(1e30, scene_limit, frame.volume_control.w > 0.5);
933 let centre_clip = frame.view_proj * vec4f(frame.eye.xyz + frame.forward.xyz, 1.0);
934 let centre = centre_clip.xy / centre_clip.w;
935 let ray = normalize(frame.forward.xyz + frame.right.xyz * ((in.uv.x * 2.0 - 1.0 - centre.x) * frame.lens.x) + frame.up.xyz * ((1.0 - in.uv.y * 2.0 - centre.y) * frame.lens.y));
936 if (frame.volume_control.w > 1.5) { return room(in.clip.xy, scene_limit, ray); }
937 let safe = select(ray, select(vec3f(-0.00001), vec3f(0.00001), ray >= vec3f(0.0)), abs(ray) < vec3f(0.00001));
938 let t0 = (frame.volume_lo.xyz - frame.eye.xyz) / safe;
939 let t1 = (frame.volume_hi.xyz - frame.eye.xyz) / safe;
940 let enter = max(max(min(t0.x,t1.x),min(t0.y,t1.y)),max(min(t0.z,t1.z),0.0));
941 let leave = min(min(max(t0.x,t1.x),max(t0.y,t1.y)),min(max(t0.z,t1.z),limit));
942 var out: VolumeOut;
943 out.radiance = vec4f(0.0);
944 out.distance = scene_limit;
945 if (leave <= enter) { return out; }
946 let steps = STEPS;
947 let step_length = (leave - enter) / f32(steps);
948 let sun = normalize(frame.sun.xyz);
949 let scatter = frame.volume_color.rgb * (vec3f(0.32) + frame.sun_color.rgb * frame.sun_color.w * phase(dot(ray,sun), frame.volume_control.y)) * 0.85;
950 var transmittance = 1.0;
951 var light = vec3f(0.0);
952 for (var i = 0u; i < steps; i++) {
953 let distance = enter + (f32(i) + 0.5) * step_length;
954 let p = frame.eye.xyz + ray * distance;
955 let width = select(max(frame.volume_hi.z - frame.volume_lo.z, 0.000001), max(frame.volume_hi.x - frame.volume_lo.x, 0.000001), frame.volume_control.w > 0.5);
956 let scale = select(DEPTH_PER_WIDTH, 3.0, frame.volume_control.w > 0.5);
957 let absorbed = 1.0 - exp(-density(p) * step_length * scale / width);
958 let weight = transmittance * absorbed;
959 light += weight * scatter;
960 transmittance *= 1.0 - absorbed;
961 }
962 out.radiance = vec4f(light, 1.0 - transmittance);
963 return out;
964}
965"#;
966
967pub const ROOM_FIELD_WGSL: &str = r#"
968struct Field { lo: vec4f, hi: vec4f, size: vec4u, }
969@group(0) @binding(0) var<uniform> field: Field;
970@group(0) @binding(1) var target_field: texture_storage_3d<rgba8unorm, write>;
971fn pcg(value: u32) -> u32 {
972 let state = value * 747796405u + 2891336453u;
973 let mixed = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
974 return (mixed >> 22u) ^ mixed;
975}
976fn hash3(p: vec3i) -> f32 {
977 let h = pcg(bitcast<u32>(p.x) * 73856093u ^ pcg(bitcast<u32>(p.y) * 19349663u ^ pcg(bitcast<u32>(p.z) * 83492791u ^ field.size.w)));
978 return f32(h >> 8u) / 16777216.0;
979}
980fn vnoise(p: vec3f) -> f32 {
981 let i = vec3i(floor(p));
982 let f = fract(p);
983 let u = f * f * (3.0 - 2.0 * f);
984 let a = mix(hash3(i), hash3(i + vec3i(1, 0, 0)), u.x);
985 let b = mix(hash3(i + vec3i(0, 1, 0)), hash3(i + vec3i(1, 1, 0)), u.x);
986 let c = mix(hash3(i + vec3i(0, 0, 1)), hash3(i + vec3i(1, 0, 1)), u.x);
987 let d = mix(hash3(i + vec3i(0, 1, 1)), hash3(i + vec3i(1, 1, 1)), u.x);
988 return mix(mix(a, b, u.y), mix(c, d, u.y), u.z);
989}
990fn fbm(p: vec3f, octaves: i32) -> f32 {
991 var sum = 0.0;
992 var amp = 0.5;
993 var q = p;
994 for (var i = 0; i < octaves; i++) {
995 sum += amp * vnoise(q);
996 q = q * 2.03 + vec3f(1.7, 9.2, 3.1);
997 amp *= 0.5;
998 }
999 return sum;
1000}
1001@compute @workgroup_size(4, 4, 4)
1002fn main(@builtin(global_invocation_id) id: vec3u) {
1003 if (any(id >= field.size.xyz)) { return; }
1004 let p = field.lo.xyz + (vec3f(id) + vec3f(0.5)) / vec3f(field.size.xyz) * (field.hi.xyz - field.lo.xyz);
1005 let w = p * 2.4;
1006 let warp = vec3f(fbm(w + vec3f(1.7, 0.0, 0.0), 3), fbm(w + vec3f(0.0, 9.2, 0.0), 3), fbm(w + vec3f(0.0, 0.0, 4.4), 3)) - vec3f(0.5);
1007 let q = p + warp * 0.18;
1008 let body = fbm(q * vec3f(2.6, 4.0, 2.6), 3);
1009 let ridge = 1.0 - abs(fbm(q * vec3f(7.0, 16.0, 7.0) + vec3f(11.0), 4) * 2.0 - 1.0);
1010 let curl = pow(ridge, 6.0) * smoothstep(0.42, 0.7, body);
1011 let top = fbm(q * vec3f(3.0, 6.0, 3.0) + vec3f(5.0), 4);
1012 let low = fbm(q * vec3f(5.0, 12.0, 5.0) + vec3f(3.0), 3);
1013 textureStore(target_field, vec3i(id), vec4f(body, curl, top, low));
1014}
1015"#;
1016
1017pub const ROOM_FIELD_SPACING: f32 = 0.012;
1018
1019pub const ROOM_BLUR_WGSL: &str = r#"
1020struct Frame {
1021 view_proj: mat4x4f, eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
1022 lens: vec4f, sun: vec4f, sun_color: vec4f, extent: vec4f,
1023 volume_lo: vec4f, volume_hi: vec4f, volume_color: vec4f, volume_control: vec4f,
1024}
1025@group(0) @binding(0) var<uniform> frame: Frame;
1026@group(0) @binding(2) var low_color: texture_2d<f32>;
1027@group(0) @binding(3) var low_depth: texture_2d<f32>;
1028@vertex fn vs_main(@builtin(vertex_index) vertex: u32) -> @builtin(position) vec4f {
1029 let xy = array<vec2f, 3>(vec2f(-1.0,-1.0),vec2f(3.0,-1.0),vec2f(-1.0,3.0));
1030 return vec4f(xy[vertex], 0.0, 1.0);
1031}
1032override low_scale: f32 = 4.0;
1033@fragment fn fs_main(@builtin(position) position: vec4f) -> @location(0) vec4f {
1034 let pixel = vec2i(position.xy);
1035 let last = vec2i(min(vec2f(textureDimensions(low_color)), ceil(frame.extent.xy / low_scale))) - vec2i(1);
1036 let centre = textureLoad(low_depth, pixel, 0).x;
1037 let edge = 0.01 + 0.05 * centre;
1038 var total = vec4f(0.0);
1039 var weight = 0.0;
1040 for (var y = -1; y <= 1; y++) {
1041 for (var x = -1; x <= 1; x++) {
1042 let at = clamp(pixel + vec2i(x, y), vec2i(0), last);
1043 let depth = textureLoad(low_depth, at, 0).x;
1044 let w = exp(-f32(x * x + y * y) * 0.5) * select(0.0, 1.0, abs(depth - centre) < edge);
1045 total += textureLoad(low_color, at, 0) * w;
1046 weight += w;
1047 }
1048 }
1049 return total / max(weight, 0.000001);
1050}
1051"#;
1052
1053pub const UPSAMPLE_WGSL: &str = r#"
1054struct Frame {
1055 view_proj: mat4x4f, eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
1056 lens: vec4f, sun: vec4f, sun_color: vec4f, extent: vec4f,
1057 volume_lo: vec4f, volume_hi: vec4f, volume_color: vec4f, volume_control: vec4f,
1058}
1059@group(0) @binding(0) var<uniform> frame: Frame;
1060@group(0) @binding(1) var scene_depth: texture_2d<f32>;
1061@group(0) @binding(2) var low_color: texture_2d<f32>;
1062@group(0) @binding(3) var low_depth: texture_2d<f32>;
1063@vertex fn vs_main(@builtin(vertex_index) vertex: u32) -> @builtin(position) vec4f {
1064 let xy = array<vec2f, 3>(vec2f(-1.0,-1.0),vec2f(3.0,-1.0),vec2f(-1.0,3.0));
1065 return vec4f(xy[vertex], 0.0, 1.0);
1066}
1067override low_scale: f32 = 4.0;
1068@fragment fn fs_main(@builtin(position) position: vec4f) -> @location(0) vec4f {
1069 let size = min(vec2f(textureDimensions(low_color)), ceil(frame.extent.xy / low_scale));
1070 let point = position.xy / frame.extent.xy * size - vec2f(0.5);
1071 let base = vec2i(floor(point));
1072 let fraction = fract(point);
1073 let max_pixel = vec2i(size) - vec2i(1);
1074 let full = textureLoad(scene_depth, vec2i(position.xy), 0).x;
1075 var total = 0.0;
1076 var result = vec4f(0.0);
1077 var nearest = 1e30;
1078 var nearest_color = vec4f(0.0);
1079 for (var y = 0; y < 2; y++) {
1080 for (var x = 0; x < 2; x++) {
1081 let pixel = clamp(base + vec2i(x,y), vec2i(0), max_pixel);
1082 let depth = textureLoad(low_depth, pixel, 0).x;
1083 let difference = abs(full - depth);
1084 let color = textureLoad(low_color, pixel, 0);
1085 if (difference < nearest) {
1086 nearest = difference;
1087 nearest_color = color;
1088 }
1089 let spatial = select(1.0 - fraction.x, fraction.x, x == 1) * select(1.0 - fraction.y, fraction.y, y == 1);
1090 let weight = spatial * exp(-difference * frame.volume_control.z);
1091 result += color * weight;
1092 total += weight;
1093 }
1094 }
1095 return select(nearest_color, result / max(total, 0.000001), total > 0.000001);
1096}
1097"#;
1098
1099fn uniform(device: &wgpu::Device, frame: &EffectFrame) -> wgpu::Buffer {
1100 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1101 label: Some("effects frame"),
1102 contents: bytemuck::bytes_of(frame),
1103 usage: wgpu::BufferUsages::UNIFORM,
1104 })
1105}
1106
1107fn storage<T: Pod>(device: &wgpu::Device, label: &'static str, values: &[T]) -> wgpu::Buffer {
1108 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1109 label: Some(label),
1110 contents: bytemuck::cast_slice(values),
1111 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1112 })
1113}
1114
1115fn buffer_entry(
1116 binding: u32,
1117 visibility: wgpu::ShaderStages,
1118 ty: wgpu::BufferBindingType,
1119) -> wgpu::BindGroupLayoutEntry {
1120 wgpu::BindGroupLayoutEntry {
1121 binding,
1122 visibility,
1123 ty: wgpu::BindingType::Buffer {
1124 ty,
1125 has_dynamic_offset: false,
1126 min_binding_size: None,
1127 },
1128 count: None,
1129 }
1130}
1131
1132fn texture_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
1133 wgpu::BindGroupLayoutEntry {
1134 binding,
1135 visibility: wgpu::ShaderStages::FRAGMENT,
1136 ty: wgpu::BindingType::Texture {
1137 sample_type: wgpu::TextureSampleType::Float { filterable: false },
1138 view_dimension: wgpu::TextureViewDimension::D2,
1139 multisampled: false,
1140 },
1141 count: None,
1142 }
1143}
1144
1145fn shadow_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
1146 wgpu::BindGroupLayoutEntry {
1147 binding,
1148 visibility: wgpu::ShaderStages::FRAGMENT,
1149 ty: wgpu::BindingType::Texture {
1150 sample_type: wgpu::TextureSampleType::Depth,
1151 view_dimension: wgpu::TextureViewDimension::D2Array,
1152 multisampled: false,
1153 },
1154 count: None,
1155 }
1156}
1157
1158fn pipeline(
1159 device: &wgpu::Device,
1160 label: &'static str,
1161 source: &str,
1162 layout: &wgpu::BindGroupLayout,
1163 targets: &[Option<wgpu::ColorTargetState>],
1164) -> wgpu::RenderPipeline {
1165 pipeline_with(device, label, source, layout, targets, &[])
1166}
1167
1168fn pipeline_with(
1169 device: &wgpu::Device,
1170 label: &'static str,
1171 source: &str,
1172 layout: &wgpu::BindGroupLayout,
1173 targets: &[Option<wgpu::ColorTargetState>],
1174 constants: &[(&str, f64)],
1175) -> wgpu::RenderPipeline {
1176 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1177 label: Some(label),
1178 source: wgpu::ShaderSource::Wgsl(source.into()),
1179 });
1180 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1181 label: Some(label),
1182 bind_group_layouts: &[Some(layout)],
1183 immediate_size: 0,
1184 });
1185 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1186 label: Some(label),
1187 layout: Some(&pipeline_layout),
1188 vertex: wgpu::VertexState {
1189 module: &shader,
1190 entry_point: Some("vs_main"),
1191 compilation_options: wgpu::PipelineCompilationOptions::default(),
1192 buffers: &[],
1193 },
1194 primitive: wgpu::PrimitiveState {
1195 topology: wgpu::PrimitiveTopology::TriangleList,
1196 cull_mode: None,
1197 ..Default::default()
1198 },
1199 depth_stencil: None,
1200 multisample: wgpu::MultisampleState::default(),
1201 fragment: Some(wgpu::FragmentState {
1202 module: &shader,
1203 entry_point: Some("fs_main"),
1204 compilation_options: wgpu::PipelineCompilationOptions {
1205 constants,
1206 ..Default::default()
1207 },
1208 targets,
1209 }),
1210 multiview_mask: None,
1211 cache: None,
1212 })
1213}
1214
1215fn premultiplied_target() -> Option<wgpu::ColorTargetState> {
1216 Some(wgpu::ColorTargetState {
1217 format: HDR_FORMAT,
1218 blend: Some(wgpu::BlendState {
1219 color: wgpu::BlendComponent {
1220 src_factor: wgpu::BlendFactor::One,
1221 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1222 operation: wgpu::BlendOperation::Add,
1223 },
1224 alpha: wgpu::BlendComponent {
1225 src_factor: wgpu::BlendFactor::One,
1226 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1227 operation: wgpu::BlendOperation::Add,
1228 },
1229 }),
1230 write_mask: wgpu::ColorWrites::ALL,
1231 })
1232}
1233
1234fn color_attachment(view: &wgpu::TextureView, clear: bool) -> wgpu::RenderPassColorAttachment<'_> {
1235 wgpu::RenderPassColorAttachment {
1236 view,
1237 resolve_target: None,
1238 ops: wgpu::Operations {
1239 load: if clear {
1240 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
1241 } else {
1242 wgpu::LoadOp::Load
1243 },
1244 store: wgpu::StoreOp::Store,
1245 },
1246 depth_slice: None,
1247 }
1248}
1249
1250pub struct CreatureMesh {
1251 vertices: wgpu::Buffer,
1252 indices: wgpu::Buffer,
1253 index_count: u32,
1254}
1255
1256pub struct EffectCommands<'a> {
1257 pub device: &'a wgpu::Device,
1258 pub encoder: &'a mut wgpu::CommandEncoder,
1259}
1260
1261#[derive(Clone, Copy)]
1262pub struct EffectTargets<'a> {
1263 pub hdr: &'a wgpu::TextureView,
1264 pub depth: &'a wgpu::TextureView,
1265 pub shadow: Option<(&'a wgpu::TextureView, &'a wgpu::Buffer)>,
1266}
1267
1268pub struct VolumeTimestamps<'a> {
1269 pub march: Option<wgpu::RenderPassTimestampWrites<'a>>,
1270 pub upsample: Option<wgpu::RenderPassTimestampWrites<'a>>,
1271}
1272
1273impl CreatureMesh {
1274 pub fn new(device: &wgpu::Device, vertices: &[CreatureVertex], indices: &[u32]) -> Self {
1275 let index_count = indices.len() as u32;
1276 let vertices = storage(device, "creature vertices", vertices);
1277 let indices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1278 label: Some("creature indices"),
1279 contents: bytemuck::cast_slice(indices),
1280 usage: wgpu::BufferUsages::INDEX,
1281 });
1282 Self {
1283 vertices,
1284 indices,
1285 index_count,
1286 }
1287 }
1288
1289 pub fn shadow_buffers(&self) -> (&wgpu::Buffer, &wgpu::Buffer, u32) {
1290 (&self.vertices, &self.indices, self.index_count)
1291 }
1292}
1293
1294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1295pub struct VolumeCuts {
1296 pub steps: u32,
1297 pub warp_octaves: u32,
1298 pub octaves: u32,
1299 pub early_out: bool,
1300}
1301
1302impl VolumeCuts {
1303 pub const FULL: Self = Self {
1304 steps: 20,
1305 warp_octaves: 4,
1306 octaves: 4,
1307 early_out: false,
1308 };
1309
1310 pub const DEFAULT: Self = Self {
1311 early_out: true,
1312 ..Self::FULL
1313 };
1314
1315 fn constants(&self) -> [(&'static str, f64); 4] {
1316 [
1317 ("STEPS", f64::from(self.steps.max(1))),
1318 ("WARP_OCTAVES", f64::from(self.warp_octaves.clamp(1, 4))),
1319 ("OCTAVES", f64::from(self.octaves.clamp(1, 4))),
1320 ("EARLY_OUT", f64::from(u8::from(self.early_out))),
1321 ]
1322 }
1323}
1324
1325impl Default for VolumeCuts {
1326 fn default() -> Self {
1327 Self::DEFAULT
1328 }
1329}
1330
1331fn volume_targets() -> [Option<wgpu::ColorTargetState>; 2] {
1332 [
1333 Some(wgpu::ColorTargetState {
1334 format: HDR_FORMAT,
1335 blend: None,
1336 write_mask: wgpu::ColorWrites::ALL,
1337 }),
1338 Some(wgpu::ColorTargetState {
1339 format: SCENE_DEPTH_FORMAT,
1340 blend: None,
1341 write_mask: wgpu::ColorWrites::ALL,
1342 }),
1343 ]
1344}
1345
1346pub struct Effects {
1347 cuts: VolumeCuts,
1348 look: Plume,
1349 cached_volumes: Vec<(VolumeCuts, [u32; 28], wgpu::RenderPipeline)>,
1350 width: u32,
1351 height: u32,
1352 viewport: (u32, u32),
1353 scale: u32,
1354 _low_color: wgpu::Texture,
1355 low_color_view: wgpu::TextureView,
1356 _low_depth: wgpu::Texture,
1357 low_depth_view: wgpu::TextureView,
1358 _low_blur: wgpu::Texture,
1359 low_blur_view: wgpu::TextureView,
1360 _empty_shadow: wgpu::Texture,
1361 empty_shadow_view: wgpu::TextureView,
1362 empty_shadow_data: wgpu::Buffer,
1363 particle_layout: wgpu::BindGroupLayout,
1364 creature_layout: wgpu::BindGroupLayout,
1365 volume_layout: wgpu::BindGroupLayout,
1366 upsample_layout: wgpu::BindGroupLayout,
1367 particle_pipeline: wgpu::RenderPipeline,
1368 creature_pipeline: wgpu::RenderPipeline,
1369 upsample_pipeline: wgpu::RenderPipeline,
1370 room_blur_pipeline: wgpu::RenderPipeline,
1371 room_field_layout: wgpu::BindGroupLayout,
1372 room_field_pipeline: wgpu::ComputePipeline,
1373 room_sampler: wgpu::Sampler,
1374 _empty_field: wgpu::Texture,
1375 empty_field_view: wgpu::TextureView,
1376 room_field: Option<RoomField>,
1377}
1378
1379struct RoomField {
1380 key: ([u32; 3], [u32; 3], u32),
1381 _texture: wgpu::Texture,
1382 view: wgpu::TextureView,
1383}
1384
1385fn field_key(haze: &RoomHaze) -> ([u32; 3], [u32; 3], u32) {
1386 (
1387 haze.lo.map(f32::to_bits),
1388 haze.hi.map(f32::to_bits),
1389 haze.seed,
1390 )
1391}
1392
1393pub fn room_field_size(haze: &RoomHaze) -> [u32; 3] {
1394 std::array::from_fn(|axis| {
1395 (((haze.hi[axis] - haze.lo[axis]) / ROOM_FIELD_SPACING).ceil() as u32).clamp(1, 256)
1396 })
1397}
1398
1399fn field_texture(
1400 device: &wgpu::Device,
1401 size: [u32; 3],
1402 usage: wgpu::TextureUsages,
1403) -> wgpu::Texture {
1404 device.create_texture(&wgpu::TextureDescriptor {
1405 label: Some("effects room haze field"),
1406 size: wgpu::Extent3d {
1407 width: size[0],
1408 height: size[1],
1409 depth_or_array_layers: size[2],
1410 },
1411 mip_level_count: 1,
1412 sample_count: 1,
1413 dimension: wgpu::TextureDimension::D3,
1414 format: wgpu::TextureFormat::Rgba8Unorm,
1415 usage,
1416 view_formats: &[],
1417 })
1418}
1419
1420impl Effects {
1421 pub fn new(device: &wgpu::Device, width: u32, height: u32, scale: u32) -> Result<Self, String> {
1422 if width == 0 || height == 0 || !matches!(scale, 2 | 4) {
1423 return Err("effects need nonzero dimensions and half or quarter resolution".into());
1424 }
1425 let low_width = width.div_ceil(scale);
1426 let low_height = height.div_ceil(scale);
1427 let texture = |label, format| {
1428 device.create_texture(&wgpu::TextureDescriptor {
1429 label: Some(label),
1430 size: wgpu::Extent3d {
1431 width: low_width,
1432 height: low_height,
1433 depth_or_array_layers: 1,
1434 },
1435 mip_level_count: 1,
1436 sample_count: 1,
1437 dimension: wgpu::TextureDimension::D2,
1438 format,
1439 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1440 | wgpu::TextureUsages::TEXTURE_BINDING,
1441 view_formats: &[],
1442 })
1443 };
1444 let low_color = texture("effects low color", HDR_FORMAT);
1445 let low_color_view = low_color.create_view(&Default::default());
1446 let low_depth = texture("effects low depth", SCENE_DEPTH_FORMAT);
1447 let low_depth_view = low_depth.create_view(&Default::default());
1448 let low_blur = texture("effects low blur", HDR_FORMAT);
1449 let low_blur_view = low_blur.create_view(&Default::default());
1450 let empty_shadow = device.create_texture(&wgpu::TextureDescriptor {
1451 label: Some("effects empty shadow"),
1452 size: wgpu::Extent3d {
1453 width: 1,
1454 height: 1,
1455 depth_or_array_layers: 3,
1456 },
1457 mip_level_count: 1,
1458 sample_count: 1,
1459 dimension: wgpu::TextureDimension::D2,
1460 format: wgpu::TextureFormat::Depth32Float,
1461 usage: wgpu::TextureUsages::TEXTURE_BINDING,
1462 view_formats: &[],
1463 });
1464 let empty_shadow_view = empty_shadow.create_view(&wgpu::TextureViewDescriptor {
1465 dimension: Some(wgpu::TextureViewDimension::D2Array),
1466 ..Default::default()
1467 });
1468 let empty_shadow_data = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1469 label: Some("effects empty shadow data"),
1470 contents: &[0; 256],
1471 usage: wgpu::BufferUsages::UNIFORM,
1472 });
1473 let particle_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1474 label: Some("particle bindings"),
1475 entries: &[
1476 buffer_entry(
1477 0,
1478 wgpu::ShaderStages::VERTEX_FRAGMENT,
1479 wgpu::BufferBindingType::Uniform,
1480 ),
1481 texture_entry(1),
1482 buffer_entry(
1483 2,
1484 wgpu::ShaderStages::VERTEX,
1485 wgpu::BufferBindingType::Storage { read_only: true },
1486 ),
1487 shadow_entry(3),
1488 buffer_entry(
1489 4,
1490 wgpu::ShaderStages::FRAGMENT,
1491 wgpu::BufferBindingType::Uniform,
1492 ),
1493 ],
1494 });
1495 let creature_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1496 label: Some("creature bindings"),
1497 entries: &[
1498 buffer_entry(
1499 0,
1500 wgpu::ShaderStages::VERTEX,
1501 wgpu::BufferBindingType::Uniform,
1502 ),
1503 buffer_entry(
1504 1,
1505 wgpu::ShaderStages::VERTEX,
1506 wgpu::BufferBindingType::Storage { read_only: true },
1507 ),
1508 buffer_entry(
1509 2,
1510 wgpu::ShaderStages::VERTEX,
1511 wgpu::BufferBindingType::Storage { read_only: true },
1512 ),
1513 texture_entry(3),
1514 ],
1515 });
1516 let volume_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1517 label: Some("volume bindings"),
1518 entries: &[
1519 buffer_entry(
1520 0,
1521 wgpu::ShaderStages::FRAGMENT,
1522 wgpu::BufferBindingType::Uniform,
1523 ),
1524 texture_entry(1),
1525 shadow_entry(2),
1526 buffer_entry(
1527 3,
1528 wgpu::ShaderStages::FRAGMENT,
1529 wgpu::BufferBindingType::Uniform,
1530 ),
1531 wgpu::BindGroupLayoutEntry {
1532 binding: 4,
1533 visibility: wgpu::ShaderStages::FRAGMENT,
1534 ty: wgpu::BindingType::Texture {
1535 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1536 view_dimension: wgpu::TextureViewDimension::D3,
1537 multisampled: false,
1538 },
1539 count: None,
1540 },
1541 wgpu::BindGroupLayoutEntry {
1542 binding: 5,
1543 visibility: wgpu::ShaderStages::FRAGMENT,
1544 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1545 count: None,
1546 },
1547 ],
1548 });
1549 let upsample_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1550 label: Some("upsample bindings"),
1551 entries: &[
1552 buffer_entry(
1553 0,
1554 wgpu::ShaderStages::FRAGMENT,
1555 wgpu::BufferBindingType::Uniform,
1556 ),
1557 texture_entry(1),
1558 texture_entry(2),
1559 texture_entry(3),
1560 ],
1561 });
1562 let particle_pipeline = pipeline(
1563 device,
1564 "effects particles",
1565 PARTICLE_WGSL,
1566 &particle_layout,
1567 &[premultiplied_target()],
1568 );
1569 let creature_pipeline = pipeline(
1570 device,
1571 "effects creatures",
1572 CREATURE_WGSL,
1573 &creature_layout,
1574 &[Some(wgpu::ColorTargetState {
1575 format: HDR_FORMAT,
1576 blend: None,
1577 write_mask: wgpu::ColorWrites::ALL,
1578 })],
1579 );
1580 let cuts = VolumeCuts::default();
1581 let look = Plume::default();
1582 let cached_volumes = vec![(
1583 cuts,
1584 look.key(),
1585 pipeline_with(
1586 device,
1587 "effects volume",
1588 VOLUME_WGSL,
1589 &volume_layout,
1590 &volume_targets(),
1591 &volume_constants(cuts, &look),
1592 ),
1593 )];
1594 let low_scale = [("low_scale", f64::from(scale))];
1595 let upsample_pipeline = pipeline_with(
1596 device,
1597 "effects upsample",
1598 UPSAMPLE_WGSL,
1599 &upsample_layout,
1600 &[premultiplied_target()],
1601 &low_scale,
1602 );
1603 let room_blur_pipeline = pipeline_with(
1604 device,
1605 "effects room haze blur",
1606 ROOM_BLUR_WGSL,
1607 &upsample_layout,
1608 &[Some(wgpu::ColorTargetState {
1609 format: HDR_FORMAT,
1610 blend: None,
1611 write_mask: wgpu::ColorWrites::ALL,
1612 })],
1613 &low_scale,
1614 );
1615 let room_field_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1616 label: Some("room haze field bindings"),
1617 entries: &[
1618 buffer_entry(
1619 0,
1620 wgpu::ShaderStages::COMPUTE,
1621 wgpu::BufferBindingType::Uniform,
1622 ),
1623 wgpu::BindGroupLayoutEntry {
1624 binding: 1,
1625 visibility: wgpu::ShaderStages::COMPUTE,
1626 ty: wgpu::BindingType::StorageTexture {
1627 access: wgpu::StorageTextureAccess::WriteOnly,
1628 format: wgpu::TextureFormat::Rgba8Unorm,
1629 view_dimension: wgpu::TextureViewDimension::D3,
1630 },
1631 count: None,
1632 },
1633 ],
1634 });
1635 let room_field_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1636 label: Some("effects room haze field"),
1637 source: wgpu::ShaderSource::Wgsl(ROOM_FIELD_WGSL.into()),
1638 });
1639 let room_field_pipeline_layout =
1640 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1641 label: Some("effects room haze field"),
1642 bind_group_layouts: &[Some(&room_field_layout)],
1643 immediate_size: 0,
1644 });
1645 let room_field_pipeline =
1646 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1647 label: Some("effects room haze field"),
1648 layout: Some(&room_field_pipeline_layout),
1649 module: &room_field_module,
1650 entry_point: Some("main"),
1651 compilation_options: Default::default(),
1652 cache: None,
1653 });
1654 let room_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1655 label: Some("effects room haze field"),
1656 mag_filter: wgpu::FilterMode::Linear,
1657 min_filter: wgpu::FilterMode::Linear,
1658 address_mode_u: wgpu::AddressMode::ClampToEdge,
1659 address_mode_v: wgpu::AddressMode::ClampToEdge,
1660 address_mode_w: wgpu::AddressMode::ClampToEdge,
1661 ..Default::default()
1662 });
1663 let empty_field = field_texture(device, [1, 1, 1], wgpu::TextureUsages::TEXTURE_BINDING);
1664 let empty_field_view = empty_field.create_view(&Default::default());
1665 Ok(Self {
1666 cuts,
1667 look,
1668 cached_volumes,
1669 width,
1670 height,
1671 viewport: (width, height),
1672 scale,
1673 _low_color: low_color,
1674 low_color_view,
1675 _low_depth: low_depth,
1676 low_depth_view,
1677 _low_blur: low_blur,
1678 low_blur_view,
1679 _empty_shadow: empty_shadow,
1680 empty_shadow_view,
1681 empty_shadow_data,
1682 particle_layout,
1683 creature_layout,
1684 volume_layout,
1685 upsample_layout,
1686 particle_pipeline,
1687 creature_pipeline,
1688 upsample_pipeline,
1689 room_blur_pipeline,
1690 room_field_layout,
1691 room_field_pipeline,
1692 room_sampler,
1693 _empty_field: empty_field,
1694 empty_field_view,
1695 room_field: None,
1696 })
1697 }
1698
1699 pub fn dimensions(&self) -> (u32, u32, u32) {
1700 (self.width, self.height, self.scale)
1701 }
1702
1703 pub fn viewport(&self) -> (u32, u32) {
1704 self.viewport
1705 }
1706
1707 pub fn textures(&self) -> Vec<wgpu::Texture> {
1708 vec![
1709 self._low_color.clone(),
1710 self._low_depth.clone(),
1711 self._low_blur.clone(),
1712 ]
1713 }
1714
1715 pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
1716 crate::viewport::check([width, height], [self.width, self.height])?;
1717 self.viewport = (width, height);
1718 Ok(())
1719 }
1720
1721 fn low_viewport(&self) -> [u32; 2] {
1722 [
1723 self.viewport.0.div_ceil(self.scale),
1724 self.viewport.1.div_ceil(self.scale),
1725 ]
1726 }
1727
1728 pub fn volume_cuts(&self) -> VolumeCuts {
1729 self.cuts
1730 }
1731
1732 pub fn set_volume_cuts(&mut self, device: &wgpu::Device, cuts: VolumeCuts) {
1733 self.cuts = cuts;
1734 let look = self.look;
1735 self.ensure_volume(device, &look);
1736 }
1737
1738 pub fn set_plume(&mut self, device: &wgpu::Device, plume: &Plume) {
1739 self.ensure_volume(device, plume);
1740 }
1741
1742 fn ensure_volume(&mut self, device: &wgpu::Device, plume: &Plume) {
1743 let key = plume.key();
1744 self.look = *plume;
1745 if self
1746 .cached_volumes
1747 .iter()
1748 .any(|(known, look, _)| *known == self.cuts && *look == key)
1749 {
1750 return;
1751 }
1752 if self.cached_volumes.len() >= MAX_VOLUME_PIPELINES {
1753 self.cached_volumes.remove(0);
1754 }
1755 let built = pipeline_with(
1756 device,
1757 "effects volume",
1758 VOLUME_WGSL,
1759 &self.volume_layout,
1760 &volume_targets(),
1761 &volume_constants(self.cuts, plume),
1762 );
1763 self.cached_volumes.push((self.cuts, key, built));
1764 }
1765
1766 pub fn prepare_room(&mut self, commands: &mut EffectCommands<'_>, haze: &RoomHaze) {
1767 let key = field_key(haze);
1768 if self
1769 .room_field
1770 .as_ref()
1771 .is_some_and(|field| field.key == key)
1772 {
1773 return;
1774 }
1775 let size = room_field_size(haze);
1776 let texture = field_texture(
1777 commands.device,
1778 size,
1779 wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING,
1780 );
1781 let view = texture.create_view(&Default::default());
1782 let mut values = [0u32; 12];
1783 for axis in 0..3 {
1784 values[axis] = haze.lo[axis].to_bits();
1785 values[4 + axis] = haze.hi[axis].to_bits();
1786 values[8 + axis] = size[axis];
1787 }
1788 values[11] = haze.seed;
1789 let uniform = commands
1790 .device
1791 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1792 label: Some("room haze field box"),
1793 contents: bytemuck::cast_slice(&values),
1794 usage: wgpu::BufferUsages::UNIFORM,
1795 });
1796 let bind = commands
1797 .device
1798 .create_bind_group(&wgpu::BindGroupDescriptor {
1799 label: Some("room haze field bindings"),
1800 layout: &self.room_field_layout,
1801 entries: &[
1802 wgpu::BindGroupEntry {
1803 binding: 0,
1804 resource: uniform.as_entire_binding(),
1805 },
1806 wgpu::BindGroupEntry {
1807 binding: 1,
1808 resource: wgpu::BindingResource::TextureView(&view),
1809 },
1810 ],
1811 });
1812 {
1813 let mut pass = commands
1814 .encoder
1815 .begin_compute_pass(&wgpu::ComputePassDescriptor {
1816 label: Some("effects room haze field"),
1817 timestamp_writes: None,
1818 });
1819 pass.set_pipeline(&self.room_field_pipeline);
1820 pass.set_bind_group(0, &bind, &[]);
1821 pass.dispatch_workgroups(
1822 size[0].div_ceil(4),
1823 size[1].div_ceil(4),
1824 size[2].div_ceil(4),
1825 );
1826 }
1827 self.room_field = Some(RoomField {
1828 key,
1829 _texture: texture,
1830 view,
1831 });
1832 }
1833
1834 pub fn render_volume(
1835 &mut self,
1836 commands: &mut EffectCommands<'_>,
1837 frame: &EffectFrame,
1838 targets: EffectTargets<'_>,
1839 timestamps: VolumeTimestamps<'_>,
1840 ) {
1841 self.ensure_volume(commands.device, &frame.plume);
1842 let look_key = frame.plume.key();
1843 let (full_width, full_height) = self.viewport;
1844 let (left, top, width, height) = volume_bounds(frame, full_width, full_height).unwrap_or((
1845 0,
1846 0,
1847 full_width,
1848 full_height,
1849 ));
1850 let low_left = left / self.scale;
1851 let low_top = top / self.scale;
1852 let low_right = (left + width).div_ceil(self.scale);
1853 let low_bottom = (top + height).div_ceil(self.scale);
1854 let uniform = uniform(commands.device, frame);
1855 let volume_bind = commands
1856 .device
1857 .create_bind_group(&wgpu::BindGroupDescriptor {
1858 label: Some("volume bindings"),
1859 layout: &self.volume_layout,
1860 entries: &[
1861 wgpu::BindGroupEntry {
1862 binding: 0,
1863 resource: uniform.as_entire_binding(),
1864 },
1865 wgpu::BindGroupEntry {
1866 binding: 1,
1867 resource: wgpu::BindingResource::TextureView(targets.depth),
1868 },
1869 wgpu::BindGroupEntry {
1870 binding: 2,
1871 resource: wgpu::BindingResource::TextureView(
1872 targets
1873 .shadow
1874 .map_or(&self.empty_shadow_view, |pair| pair.0),
1875 ),
1876 },
1877 wgpu::BindGroupEntry {
1878 binding: 3,
1879 resource: targets
1880 .shadow
1881 .map_or(&self.empty_shadow_data, |pair| pair.1)
1882 .as_entire_binding(),
1883 },
1884 wgpu::BindGroupEntry {
1885 binding: 4,
1886 resource: wgpu::BindingResource::TextureView(
1887 self.room_field
1888 .as_ref()
1889 .map_or(&self.empty_field_view, |field| &field.view),
1890 ),
1891 },
1892 wgpu::BindGroupEntry {
1893 binding: 5,
1894 resource: wgpu::BindingResource::Sampler(&self.room_sampler),
1895 },
1896 ],
1897 });
1898 {
1899 let mut pass = commands
1900 .encoder
1901 .begin_render_pass(&wgpu::RenderPassDescriptor {
1902 multiview_mask: None,
1903 label: Some("effects volume quarter resolution"),
1904 color_attachments: &[
1905 Some(color_attachment(&self.low_color_view, true)),
1906 Some(wgpu::RenderPassColorAttachment {
1907 view: &self.low_depth_view,
1908 resolve_target: None,
1909 ops: wgpu::Operations {
1910 load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
1911 store: wgpu::StoreOp::Store,
1912 },
1913 depth_slice: None,
1914 }),
1915 ],
1916 depth_stencil_attachment: None,
1917 timestamp_writes: timestamps.march,
1918 occlusion_query_set: None,
1919 });
1920 let volume_pipeline = self
1921 .cached_volumes
1922 .iter()
1923 .find(|(known, look, _)| *known == self.cuts && *look == look_key)
1924 .map(|(_, _, built)| built)
1925 .expect("the active volume cuts and plume have a pipeline");
1926 crate::viewport::apply(&mut pass, self.low_viewport());
1927 pass.set_pipeline(volume_pipeline);
1928 pass.set_bind_group(0, &volume_bind, &[]);
1929 pass.set_scissor_rect(
1930 low_left,
1931 low_top,
1932 low_right - low_left,
1933 low_bottom - low_top,
1934 );
1935 pass.draw(0..3, 0..1);
1936 }
1937 let room = frame.volume_control[3] > 1.5;
1938 let (blur_writes, upsample_writes) = match (room, timestamps.upsample) {
1939 (true, Some(writes)) => (
1940 Some(wgpu::RenderPassTimestampWrites {
1941 query_set: writes.query_set,
1942 beginning_of_pass_write_index: writes.beginning_of_pass_write_index,
1943 end_of_pass_write_index: None,
1944 }),
1945 Some(wgpu::RenderPassTimestampWrites {
1946 query_set: writes.query_set,
1947 beginning_of_pass_write_index: None,
1948 end_of_pass_write_index: writes.end_of_pass_write_index,
1949 }),
1950 ),
1951 (_, writes) => (None, writes),
1952 };
1953 if room {
1954 let blur_bind = commands
1955 .device
1956 .create_bind_group(&wgpu::BindGroupDescriptor {
1957 label: Some("room haze blur bindings"),
1958 layout: &self.upsample_layout,
1959 entries: &[
1960 wgpu::BindGroupEntry {
1961 binding: 0,
1962 resource: uniform.as_entire_binding(),
1963 },
1964 wgpu::BindGroupEntry {
1965 binding: 1,
1966 resource: wgpu::BindingResource::TextureView(&self.low_depth_view),
1967 },
1968 wgpu::BindGroupEntry {
1969 binding: 2,
1970 resource: wgpu::BindingResource::TextureView(&self.low_color_view),
1971 },
1972 wgpu::BindGroupEntry {
1973 binding: 3,
1974 resource: wgpu::BindingResource::TextureView(&self.low_depth_view),
1975 },
1976 ],
1977 });
1978 let mut pass = commands
1979 .encoder
1980 .begin_render_pass(&wgpu::RenderPassDescriptor {
1981 multiview_mask: None,
1982 label: Some("effects room haze blur"),
1983 color_attachments: &[Some(color_attachment(&self.low_blur_view, true))],
1984 depth_stencil_attachment: None,
1985 timestamp_writes: blur_writes,
1986 occlusion_query_set: None,
1987 });
1988 crate::viewport::apply(&mut pass, self.low_viewport());
1989 pass.set_pipeline(&self.room_blur_pipeline);
1990 pass.set_bind_group(0, &blur_bind, &[]);
1991 pass.set_scissor_rect(
1992 low_left,
1993 low_top,
1994 low_right - low_left,
1995 low_bottom - low_top,
1996 );
1997 pass.draw(0..3, 0..1);
1998 }
1999 let low_color = if room {
2000 &self.low_blur_view
2001 } else {
2002 &self.low_color_view
2003 };
2004 let upsample_bind = commands
2005 .device
2006 .create_bind_group(&wgpu::BindGroupDescriptor {
2007 label: Some("volume upsample bindings"),
2008 layout: &self.upsample_layout,
2009 entries: &[
2010 wgpu::BindGroupEntry {
2011 binding: 0,
2012 resource: uniform.as_entire_binding(),
2013 },
2014 wgpu::BindGroupEntry {
2015 binding: 1,
2016 resource: wgpu::BindingResource::TextureView(targets.depth),
2017 },
2018 wgpu::BindGroupEntry {
2019 binding: 2,
2020 resource: wgpu::BindingResource::TextureView(low_color),
2021 },
2022 wgpu::BindGroupEntry {
2023 binding: 3,
2024 resource: wgpu::BindingResource::TextureView(&self.low_depth_view),
2025 },
2026 ],
2027 });
2028 let mut pass = commands
2029 .encoder
2030 .begin_render_pass(&wgpu::RenderPassDescriptor {
2031 multiview_mask: None,
2032 label: Some("effects depth aware upsample"),
2033 color_attachments: &[Some(color_attachment(targets.hdr, false))],
2034 depth_stencil_attachment: None,
2035 timestamp_writes: upsample_writes,
2036 occlusion_query_set: None,
2037 });
2038 crate::viewport::apply(&mut pass, [full_width, full_height]);
2039 pass.set_pipeline(&self.upsample_pipeline);
2040 pass.set_bind_group(0, &upsample_bind, &[]);
2041 pass.set_scissor_rect(left, top, width, height);
2042 pass.draw(0..3, 0..1);
2043 }
2044
2045 pub fn render_particles(
2046 &self,
2047 commands: &mut EffectCommands<'_>,
2048 frame: &EffectFrame,
2049 targets: EffectTargets<'_>,
2050 particles: &[ParticleInstance],
2051 timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
2052 ) {
2053 if particles.is_empty() {
2054 return;
2055 }
2056 let uniform = uniform(commands.device, frame);
2057 let mut ordered = particles.to_vec();
2058 ordered.sort_by(|left, right| {
2059 distance_squared(right.position_size, frame.eye)
2060 .total_cmp(&distance_squared(left.position_size, frame.eye))
2061 });
2062 let instances = storage(commands.device, "particle instances", &ordered);
2063 let bind = commands
2064 .device
2065 .create_bind_group(&wgpu::BindGroupDescriptor {
2066 label: Some("particle bindings"),
2067 layout: &self.particle_layout,
2068 entries: &[
2069 wgpu::BindGroupEntry {
2070 binding: 0,
2071 resource: uniform.as_entire_binding(),
2072 },
2073 wgpu::BindGroupEntry {
2074 binding: 1,
2075 resource: wgpu::BindingResource::TextureView(targets.depth),
2076 },
2077 wgpu::BindGroupEntry {
2078 binding: 2,
2079 resource: instances.as_entire_binding(),
2080 },
2081 wgpu::BindGroupEntry {
2082 binding: 3,
2083 resource: wgpu::BindingResource::TextureView(
2084 targets
2085 .shadow
2086 .map_or(&self.empty_shadow_view, |pair| pair.0),
2087 ),
2088 },
2089 wgpu::BindGroupEntry {
2090 binding: 4,
2091 resource: targets
2092 .shadow
2093 .map_or(&self.empty_shadow_data, |pair| pair.1)
2094 .as_entire_binding(),
2095 },
2096 ],
2097 });
2098 let mut pass = commands
2099 .encoder
2100 .begin_render_pass(&wgpu::RenderPassDescriptor {
2101 multiview_mask: None,
2102 label: Some("effects particles"),
2103 color_attachments: &[Some(color_attachment(targets.hdr, false))],
2104 depth_stencil_attachment: None,
2105 timestamp_writes: timestamps,
2106 occlusion_query_set: None,
2107 });
2108 crate::viewport::apply(&mut pass, [self.viewport.0, self.viewport.1]);
2109 pass.set_pipeline(&self.particle_pipeline);
2110 pass.set_bind_group(0, &bind, &[]);
2111 pass.draw(0..6, 0..particles.len() as u32);
2112 }
2113
2114 pub fn render_creatures(
2115 &self,
2116 commands: &mut EffectCommands<'_>,
2117 frame: &EffectFrame,
2118 targets: EffectTargets<'_>,
2119 mesh: &CreatureMesh,
2120 creatures: &[CreatureInstance],
2121 timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
2122 ) {
2123 if creatures.is_empty() || mesh.index_count == 0 {
2124 return;
2125 }
2126 let uniform = uniform(commands.device, frame);
2127 let mut ordered = creatures.to_vec();
2128 ordered.sort_by(|left, right| {
2129 distance_squared(right.position_scale, frame.eye)
2130 .total_cmp(&distance_squared(left.position_scale, frame.eye))
2131 });
2132 let instances = storage(commands.device, "creature instances", &ordered);
2133 let bind = commands
2134 .device
2135 .create_bind_group(&wgpu::BindGroupDescriptor {
2136 label: Some("creature bindings"),
2137 layout: &self.creature_layout,
2138 entries: &[
2139 wgpu::BindGroupEntry {
2140 binding: 0,
2141 resource: uniform.as_entire_binding(),
2142 },
2143 wgpu::BindGroupEntry {
2144 binding: 1,
2145 resource: instances.as_entire_binding(),
2146 },
2147 wgpu::BindGroupEntry {
2148 binding: 2,
2149 resource: mesh.vertices.as_entire_binding(),
2150 },
2151 wgpu::BindGroupEntry {
2152 binding: 3,
2153 resource: wgpu::BindingResource::TextureView(targets.depth),
2154 },
2155 ],
2156 });
2157 let mut pass = commands
2158 .encoder
2159 .begin_render_pass(&wgpu::RenderPassDescriptor {
2160 multiview_mask: None,
2161 label: Some("effects creatures"),
2162 color_attachments: &[Some(color_attachment(targets.hdr, false))],
2163 depth_stencil_attachment: None,
2164 timestamp_writes: timestamps,
2165 occlusion_query_set: None,
2166 });
2167 crate::viewport::apply(&mut pass, [self.viewport.0, self.viewport.1]);
2168 pass.set_pipeline(&self.creature_pipeline);
2169 pass.set_bind_group(0, &bind, &[]);
2170 pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
2171 pass.draw_indexed(0..mesh.index_count, 0, 0..creatures.len() as u32);
2172 }
2173}
2174
2175#[cfg(test)]
2176mod tests {
2177 use super::*;
2178 use pfx_gpu::{Gpu, GpuProfiler};
2179
2180 const LO: [f32; 3] = [-1.0, 0.0, -1.0];
2181 const HI: [f32; 3] = [1.0, 1.0, 1.0];
2182
2183 #[test]
2184 fn haze_from_zero_amount_has_no_density() {
2185 let haze = RoomHaze::from_amount(LO, HI, 0.0);
2186 assert_eq!([haze.fog, haze.smoke, haze.mist, haze.floor], [0.0; 4]);
2187 let mut frame = EffectFrame::new(8, 8, 0.0, 0);
2188 frame.set_room_haze(haze);
2189 assert_eq!(frame.volume_control[0], 0.0);
2190 assert_eq!(frame.volume_lo[3], 0.0);
2191 assert_eq!(frame.volume_hi[3], 0.0);
2192 assert_eq!(frame.volume_color[3], 0.0);
2193 assert_eq!(RoomHaze::from_amount(LO, HI, -1.0), haze);
2194 assert_eq!(RoomHaze::from_amount(LO, HI, f32::NAN), haze);
2195 }
2196
2197 #[test]
2198 fn haze_from_amount_takes_the_bounds_and_neutral_defaults() {
2199 let haze = RoomHaze::from_amount(LO, HI, 0.3);
2200 let neutral = RoomHaze::new(LO, HI);
2201 assert!((haze.fog - 0.05).abs() < 1.0e-6);
2202 assert!((haze.smoke - 6.0).abs() < 1.0e-5);
2203 assert!((haze.floor - 0.6).abs() < 1.0e-6);
2204 assert!(haze.mist > 0.0 && haze.mist < 0.1);
2205 assert_eq!(
2206 (haze.phase, haze.back, haze.reach, haze.lo, haze.hi),
2207 (
2208 neutral.phase,
2209 neutral.back,
2210 neutral.reach,
2211 neutral.lo,
2212 neutral.hi
2213 )
2214 );
2215 assert_eq!(haze.gold, [1.0; 3]);
2216 assert_eq!(haze.ambient, [HAZE_AMBIENT_GREY; 3]);
2217 }
2218
2219 #[test]
2220 fn haze_from_amount_grows_and_clamps_at_one() {
2221 let low = RoomHaze::from_amount(LO, HI, 0.3);
2222 let full = RoomHaze::from_amount(LO, HI, 1.0);
2223 assert!(full.fog > low.fog && full.smoke > low.smoke);
2224 assert!(full.mist > low.mist && full.floor > low.floor);
2225 assert_eq!(RoomHaze::from_amount(LO, HI, 7.0), full);
2226 }
2227
2228 #[test]
2229 fn plume_depth_follows_box_scale() {
2230 let small = plume_optical_depth(1.8, 0.1, 0.1, 9.0);
2231 let large = plume_optical_depth(1.8, 1.0, 1.0, 9.0);
2232 assert!((small - large).abs() < 1.0e-5);
2233 assert!(1.0 - (-small).exp() > 0.9);
2234 }
2235
2236 #[test]
2237 fn room_fog_follows_beer_lambert() {
2238 let through = (0..64).fold(1.0_f32, |through, _| {
2239 through * (-0.05_f32 * 2.0 / 64.0).exp()
2240 });
2241 assert!((fog_transmittance(0.05, 2.0, 3.0) - through).abs() < 1e-6);
2242 assert!((fog_transmittance(0.05, 9.0, 3.0) - (-0.15_f32).exp()).abs() < 1e-6);
2243 assert_eq!(fog_transmittance(0.0, 3.0, 3.0), 1.0);
2244 assert_eq!(fog_transmittance(0.05, 0.0, 3.0), 1.0);
2245 }
2246
2247 #[test]
2248 fn room_in_scatter_integrates_attenuated_light() {
2249 let steps = 4096;
2250 let step = 1.3 / steps as f32;
2251 let marched: f32 = (0..steps)
2252 .map(|i| 0.07 * (-0.05 * (i as f32 + 0.5) * step).exp() * 2.9 * step)
2253 .sum();
2254 assert!((fog_in_scatter(0.07, 0.05, 2.9, 1.3) - marched).abs() < 1e-5);
2255 assert!((fog_in_scatter(0.07, 0.0, 2.9, 1.3) - 0.07 * 2.9 * 1.3).abs() < 1e-6);
2256 }
2257
2258 #[test]
2259 fn henyey_greenstein_integrates_to_one() {
2260 let steps = 20_000;
2261 for g in [-0.55, 0.0, 0.35] {
2262 let total: f32 = (0..steps)
2263 .map(|i| {
2264 let cosine = -1.0 + 2.0 * (i as f32 + 0.5) / steps as f32;
2265 henyey_greenstein(cosine, g) * 2.0 * std::f32::consts::PI * 2.0 / steps as f32
2266 })
2267 .sum();
2268 assert!((total - 1.0).abs() < 1e-3, "g {g}: {total}");
2269 }
2270 }
2271
2272 fn fixture_motes() -> RoomMotes {
2273 RoomMotes::new(
2274 [-0.6, 0.0, -0.4],
2275 [0.7, 0.5, 0.5],
2276 0.04,
2277 0.2,
2278 0.7,
2279 [1.0, 0.9, 0.7],
2280 )
2281 }
2282
2283 #[test]
2284 fn motes_repeat_for_seed_and_time() {
2285 let motes = fixture_motes();
2286 let first = room_motes(7, 2.5, motes);
2287 assert!(!first.is_empty());
2288 assert_eq!(first, room_motes(7, 2.5, motes));
2289 assert_ne!(first, room_motes(8, 2.5, motes));
2290 assert_ne!(first, room_motes(7, 3.5, motes));
2291 }
2292
2293 #[test]
2294 fn motes_fill_their_share_of_the_cells() {
2295 let motes = fixture_motes();
2296 let cells = (0..3)
2297 .map(|axis| ((motes.hi[axis] - motes.lo[axis]) / motes.cell).ceil())
2298 .product::<f32>();
2299 let share = room_motes(0, 0.0, motes).len() as f32 / cells;
2300 assert!((share - 0.2).abs() < 0.02, "{share}");
2301 for mote in room_motes(0, 0.0, motes) {
2302 assert!((0.00025..=0.00075).contains(&mote.position_size[3]));
2303 }
2304 }
2305
2306 #[test]
2307 fn plume_footprint_overlaps_its_analytic_shape() {
2308 let plume = Plume {
2309 drift: [[-0.8, -0.1], [0.0, 0.0]],
2310 sway: [[15.0, 1.0, 0.01, 0.4, 5.0], [11.0, 0.8, 0.01, 0.4, 4.0]],
2311 ..Plume::default()
2312 };
2313 let radius = plume.radius;
2314 let weight = plume.depth_per_width / (radius * 6.0);
2315 let mut reference = 0;
2316 let mut overlap = 0;
2317 let mut displaced = 0;
2318 for yi in 0..201 {
2319 let rise = yi as f32 * 0.001;
2320 let edge = |a: f32, b: f32, x: f32| {
2321 let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
2322 t * t * (3.0 - 2.0 * t)
2323 };
2324 let fade =
2325 edge(0.0, plume.fade[0], rise) * (1.0 - edge(plume.fade[1], plume.fade[2], rise));
2326 let drift = plume.drift[0];
2327 let sway = plume.sway[0];
2328 let axis = drift[0] * rise * rise
2329 + drift[1] * rise
2330 + (rise * sway[0]).sin() * sway[2] * (sway[3] + rise * sway[4]);
2331 let width = radius * (plume.spread[0] + rise * plume.spread[1]);
2332 for xi in 0..161 {
2333 let x = (xi as f32 - 80.0) * 0.001;
2334 let core = (-((x - axis) / width).powi(2)).exp();
2335 let analytic = 1.0 - (-1.8 * core * fade * weight * 0.012).exp();
2336 let engine = 1.0
2337 - (-plume_optical_depth(
2338 1.8 * core * fade,
2339 0.012,
2340 radius * 6.0,
2341 plume.depth_per_width,
2342 ))
2343 .exp();
2344 let shifted = 1.0
2345 - (-plume_optical_depth(
2346 1.8 * (-((x - axis - 0.03) / width).powi(2)).exp() * fade,
2347 0.012,
2348 radius * 6.0,
2349 plume.depth_per_width,
2350 ))
2351 .exp();
2352 if analytic > 0.05 {
2353 reference += 1;
2354 overlap += usize::from(engine > 0.05);
2355 displaced += usize::from(shifted > 0.05);
2356 }
2357 }
2358 }
2359 assert!(reference > 400, "{reference}");
2360 assert!(overlap as f32 / reference as f32 >= 0.8);
2361 assert!(displaced as f32 / (reference as f32) < 0.8);
2362 }
2363
2364 #[test]
2365 fn particle_and_creature_packing() {
2366 let specks = [Speck {
2367 pos: [1.0, 2.0, 3.0],
2368 vel: [0.0, 1.0, 0.0],
2369 age: 0.5,
2370 life: 2.0,
2371 seed: 42,
2372 }];
2373 let petals = pack_particles(&specks, ParticleKind::Petal, 1.0, [1.0; 3]);
2374 assert_eq!(
2375 petals,
2376 pack_particles(&specks, ParticleKind::Petal, 1.0, [1.0; 3])
2377 );
2378 assert_eq!(petals[0].position_size[..3], specks[0].pos);
2379 assert_ne!(
2380 petals[0].color_rotation[3],
2381 pack_particles(&specks, ParticleKind::Petal, 2.0, [1.0; 3])[0].color_rotation[3]
2382 );
2383 assert_eq!(std::mem::size_of::<ParticleInstance>(), 64);
2384 let boids = [Boid {
2385 pos: [2.0, 3.0, 4.0],
2386 vel: [0.0, 0.0, -2.0],
2387 }];
2388 let birds = pack_creatures(&boids, CreatureKind::Bird, 0.3, 4);
2389 assert_eq!(birds, pack_creatures(&boids, CreatureKind::Bird, 0.3, 4));
2390 assert_eq!(birds[0].position_scale, [2.0, 3.0, 4.0, 0.3]);
2391 assert_eq!(birds[0].forward[..3], [0.0, 0.0, -1.0]);
2392 assert_eq!(std::mem::size_of::<CreatureInstance>(), 80);
2393 }
2394
2395 #[test]
2396 fn shaders_parse() {
2397 for source in [
2398 PARTICLE_WGSL,
2399 CREATURE_WGSL,
2400 VOLUME_WGSL,
2401 UPSAMPLE_WGSL,
2402 ROOM_BLUR_WGSL,
2403 ROOM_FIELD_WGSL,
2404 ] {
2405 naga::front::wgsl::parse_str(source).unwrap();
2406 }
2407 }
2408
2409 #[test]
2410 fn bilateral_weights_respect_depth() {
2411 let weights = upsample_weights(2.0, [2.0, 100.0, 2.0, 100.0], [0.5, 0.5], 1.0);
2412 assert!(weights[0] > 0.49 && weights[2] > 0.49);
2413 assert!(weights[1] < 1e-6 && weights[3] < 1e-6);
2414 assert!((weights.iter().sum::<f32>() - 1.0).abs() < 1e-6);
2415 let flat = upsample_weights(2.0, [2.0; 4], [0.25, 0.75], 1.0);
2416 assert!((flat[0] - 0.1875).abs() < 1e-6);
2417 }
2418
2419 #[test]
2420 #[ignore = "needs a GPU; run under pgpu"]
2421 fn room_haze_transmittance_matches_analytic_fog() {
2422 let gpu = pollster::block_on(Gpu::headless()).unwrap();
2423 let (width, height) = (256, 128);
2424 let mut effects = Effects::new(&gpu.device, width, height, 4).unwrap();
2425 for (depth, ambient) in [(2.0_f32, 0.3_f32), (5.0, 0.3), (1.2, 0.0)] {
2426 let hdr = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2427 let scene_depth = gpu.device.create_texture(&wgpu::TextureDescriptor {
2428 label: Some("room haze test depth"),
2429 size: wgpu::Extent3d {
2430 width,
2431 height,
2432 depth_or_array_layers: 1,
2433 },
2434 mip_level_count: 1,
2435 sample_count: 1,
2436 dimension: wgpu::TextureDimension::D2,
2437 format: SCENE_DEPTH_FORMAT,
2438 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2439 | wgpu::TextureUsages::TEXTURE_BINDING,
2440 view_formats: &[],
2441 });
2442 let depth_view = scene_depth.create_view(&Default::default());
2443 let mut encoder = gpu.device.create_command_encoder(&Default::default());
2444 {
2445 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2446 multiview_mask: None,
2447 label: Some("room haze test clear"),
2448 color_attachments: &[
2449 Some(color_attachment(&hdr.view, true)),
2450 Some(wgpu::RenderPassColorAttachment {
2451 view: &depth_view,
2452 resolve_target: None,
2453 ops: wgpu::Operations {
2454 load: wgpu::LoadOp::Clear(wgpu::Color {
2455 r: f64::from(depth),
2456 g: 0.0,
2457 b: 0.0,
2458 a: 0.0,
2459 }),
2460 store: wgpu::StoreOp::Store,
2461 },
2462 depth_slice: None,
2463 }),
2464 ],
2465 depth_stencil_attachment: None,
2466 timestamp_writes: None,
2467 occlusion_query_set: None,
2468 });
2469 }
2470 let mut frame = EffectFrame::new(width, height, 1.0, 17);
2471 frame.lens = [0.001, 0.001, 0.0, 0.0];
2472 frame.sun_color = [1.0, 1.0, 1.0, 0.0];
2473 let haze = RoomHaze {
2474 fog: 0.05,
2475 smoke: 0.0,
2476 mist: 0.0,
2477 floor: 0.0,
2478 phase: 0.35,
2479 back: 0.7,
2480 gold: [1.0; 3],
2481 ambient: [ambient; 3],
2482 reach: 3.0,
2483 unmapped: 1.0,
2484 lo: [-1.0, -1.0, -1.0],
2485 hi: [1.0, 1.0, 1.0],
2486 seed: 0,
2487 };
2488 frame.set_room_haze(haze);
2489 effects.prepare_room(
2490 &mut EffectCommands {
2491 device: &gpu.device,
2492 encoder: &mut encoder,
2493 },
2494 &haze,
2495 );
2496 frame.eye = [0.0, 1.0, 3.0, 0.0];
2497 effects.render_volume(
2498 &mut EffectCommands {
2499 device: &gpu.device,
2500 encoder: &mut encoder,
2501 },
2502 &frame,
2503 EffectTargets {
2504 hdr: &hdr.view,
2505 depth: &depth_view,
2506 shadow: None,
2507 },
2508 VolumeTimestamps {
2509 march: None,
2510 upsample: None,
2511 },
2512 );
2513 gpu.queue.submit(Some(encoder.finish()));
2514 let pixels = gpu.readback_rgba16(&hdr).unwrap();
2515 let at = ((height / 2 * width + width / 2) * 4) as usize;
2516 let value = |channel: usize| half::f16::from_bits(pixels[at + channel]).to_f32();
2517 let span = depth.min(3.0);
2518 let alpha = 1.0 - fog_transmittance(0.05, span, 3.0);
2519 assert!(
2520 (value(3) - alpha).abs() < 2e-3,
2521 "depth {depth}: alpha {} against {alpha}",
2522 value(3)
2523 );
2524 let flat = fog_in_scatter(0.05, 0.05, ambient, span);
2525 assert!(
2526 value(0) >= 0.5 * flat - 1e-3 && value(0) <= 1.5 * flat + 1e-3,
2527 "depth {depth}: {} against {flat}",
2528 value(0)
2529 );
2530 }
2531 }
2532
2533 #[test]
2534 #[ignore = "needs a GPU; run under pgpu"]
2535 fn four_k_effects() {
2536 let gpu = pollster::block_on(Gpu::headless()).unwrap();
2537 let width = 3840;
2538 let height = 2160;
2539 let hdr = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2540 let depth = gpu.device.create_texture(&wgpu::TextureDescriptor {
2541 label: Some("effects test depth"),
2542 size: wgpu::Extent3d {
2543 width,
2544 height,
2545 depth_or_array_layers: 1,
2546 },
2547 mip_level_count: 1,
2548 sample_count: 1,
2549 dimension: wgpu::TextureDimension::D2,
2550 format: SCENE_DEPTH_FORMAT,
2551 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2552 view_formats: &[],
2553 });
2554 let depth_view = depth.create_view(&Default::default());
2555 let mut clear = gpu.device.create_command_encoder(&Default::default());
2556 {
2557 let _pass = clear.begin_render_pass(&wgpu::RenderPassDescriptor {
2558 multiview_mask: None,
2559 label: Some("effects test clear"),
2560 color_attachments: &[
2561 Some(color_attachment(&hdr.view, true)),
2562 Some(wgpu::RenderPassColorAttachment {
2563 view: &depth_view,
2564 resolve_target: None,
2565 ops: wgpu::Operations {
2566 load: wgpu::LoadOp::Clear(wgpu::Color {
2567 r: 100.0,
2568 g: 0.0,
2569 b: 0.0,
2570 a: 0.0,
2571 }),
2572 store: wgpu::StoreOp::Store,
2573 },
2574 depth_slice: None,
2575 }),
2576 ],
2577 depth_stencil_attachment: None,
2578 timestamp_writes: None,
2579 occlusion_query_set: None,
2580 });
2581 }
2582 gpu.queue.submit(Some(clear.finish()));
2583 let mut effects = Effects::new(&gpu.device, width, height, 4).unwrap();
2584 let mut frame = EffectFrame::new(width, height, 1.0, 17);
2585 frame.volume_lo = [-0.5, -0.05, -0.5, 0.0];
2586 frame.volume_hi = [0.5, 0.15, 0.5, 0.0];
2587 let a = -100.0 / 99.9;
2588 let b = -10.0 / 99.9;
2589 frame.view_proj = [
2590 [1.0, 0.0, 0.0, 0.0],
2591 [0.0, 1.0, 0.0, 0.0],
2592 [0.0, 0.0, a, -1.0],
2593 [0.0, 0.0, -3.0 * a + b, 3.0],
2594 ];
2595 let petals: Vec<_> = (0..10_000)
2596 .map(|index| {
2597 let x = 0.35 + (index % 100) as f32 * 0.002;
2598 let y = -0.2 + (index / 100) as f32 * 0.004;
2599 Speck {
2600 pos: [x, y, 0.0],
2601 vel: [0.0, -0.2, 0.0],
2602 age: 0.5,
2603 life: 2.0,
2604 seed: index,
2605 }
2606 })
2607 .collect();
2608 let petals = pack_particles(&petals, ParticleKind::Petal, 1.0, [1.0; 3]);
2609 let boids: Vec<_> = (0..200)
2610 .map(|index| Boid {
2611 pos: [
2612 -0.6 + (index % 20) as f32 * 0.01,
2613 (index / 20) as f32 * 0.01,
2614 0.0,
2615 ],
2616 vel: [0.0, 0.0, -1.0],
2617 })
2618 .collect();
2619 let birds = pack_creatures(&boids, CreatureKind::Bird, 0.1, 9);
2620 let mesh = CreatureMesh::new(
2621 &gpu.device,
2622 &[
2623 CreatureVertex {
2624 position: [-1.0, 0.0, 0.0, 1.0],
2625 },
2626 CreatureVertex {
2627 position: [0.0, 0.0, -0.3, 1.0],
2628 },
2629 CreatureVertex {
2630 position: [1.0, 0.0, 0.0, 1.0],
2631 },
2632 CreatureVertex {
2633 position: [0.0, 0.0, 0.3, 1.0],
2634 },
2635 ],
2636 &[0, 1, 2, 0, 2, 3],
2637 );
2638 let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
2639 let mut encoder = gpu.device.create_command_encoder(&Default::default());
2640 let march = profiler.pass("volume march");
2641 let upsample = profiler.pass("volume upsample");
2642 let creatures = profiler.pass("creatures");
2643 let particles = profiler.pass("particles");
2644 let targets = EffectTargets {
2645 hdr: &hdr.view,
2646 depth: &depth_view,
2647 shadow: None,
2648 };
2649 let mut commands = EffectCommands {
2650 device: &gpu.device,
2651 encoder: &mut encoder,
2652 };
2653 effects.render_volume(
2654 &mut commands,
2655 &frame,
2656 targets,
2657 VolumeTimestamps {
2658 march: profiler.render_writes(march),
2659 upsample: profiler.render_writes(upsample),
2660 },
2661 );
2662 effects.render_creatures(
2663 &mut commands,
2664 &frame,
2665 targets,
2666 &mesh,
2667 &birds,
2668 profiler.render_writes(creatures),
2669 );
2670 effects.render_particles(
2671 &mut commands,
2672 &frame,
2673 targets,
2674 &petals,
2675 profiler.render_writes(particles),
2676 );
2677 let slot = profiler.finish(&mut encoder);
2678 gpu.queue.submit(Some(encoder.finish()));
2679 if let Some(slot) = slot {
2680 profiler.submitted(slot);
2681 gpu.device
2682 .poll(wgpu::PollType::wait_indefinitely())
2683 .unwrap();
2684 for timing in profiler.collect(&gpu.device).into_iter().flatten() {
2685 eprintln!("{}: {:.3} ms", timing.label, timing.milliseconds);
2686 }
2687 }
2688 let pixels = gpu.readback_rgba16(&hdr).unwrap();
2689 let lit = |x: usize, y: usize| -> bool {
2690 let index = (y * width as usize + x) * 4;
2691 pixels[index] != 0 || pixels[index + 1] != 0 || pixels[index + 2] != 0
2692 };
2693 assert!(lit(1920, 1080));
2694 assert!(lit(2200, 1080));
2695 assert!(lit(1600, 1050));
2696 assert!(!lit(20, 20));
2697 }
2698
2699 #[test]
2700 fn steam_clock_scales_only_time() {
2701 let base = EffectFrame::new(8, 8, 3.0, 5);
2702 let mut same = base;
2703 same.scale_time(1.0);
2704 assert_eq!(bytemuck::bytes_of(&same), bytemuck::bytes_of(&base));
2705 let mut half = base;
2706 half.scale_time(0.5);
2707 assert_eq!(half.extent, [8.0, 8.0, 1.5, f32::from_bits(5)]);
2708 let mut frozen = base;
2709 frozen.scale_time(-2.0);
2710 assert_eq!(frozen.extent[2], 0.0);
2711 }
2712
2713 fn steam_pixels(gpu: &Gpu, effects: &mut Effects, frame: &EffectFrame) -> Vec<u16> {
2714 let (width, height) = (256, 128);
2715 let hdr = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2716 let depth = gpu.device.create_texture(&wgpu::TextureDescriptor {
2717 label: Some("steam clock test depth"),
2718 size: wgpu::Extent3d {
2719 width,
2720 height,
2721 depth_or_array_layers: 1,
2722 },
2723 mip_level_count: 1,
2724 sample_count: 1,
2725 dimension: wgpu::TextureDimension::D2,
2726 format: SCENE_DEPTH_FORMAT,
2727 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2728 view_formats: &[],
2729 });
2730 let depth_view = depth.create_view(&Default::default());
2731 let mut encoder = gpu.device.create_command_encoder(&Default::default());
2732 {
2733 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2734 multiview_mask: None,
2735 label: Some("steam clock test clear"),
2736 color_attachments: &[
2737 Some(color_attachment(&hdr.view, true)),
2738 Some(wgpu::RenderPassColorAttachment {
2739 view: &depth_view,
2740 resolve_target: None,
2741 ops: wgpu::Operations {
2742 load: wgpu::LoadOp::Clear(wgpu::Color {
2743 r: 100.0,
2744 g: 0.0,
2745 b: 0.0,
2746 a: 0.0,
2747 }),
2748 store: wgpu::StoreOp::Store,
2749 },
2750 depth_slice: None,
2751 }),
2752 ],
2753 depth_stencil_attachment: None,
2754 timestamp_writes: None,
2755 occlusion_query_set: None,
2756 });
2757 }
2758 effects.render_volume(
2759 &mut EffectCommands {
2760 device: &gpu.device,
2761 encoder: &mut encoder,
2762 },
2763 frame,
2764 EffectTargets {
2765 hdr: &hdr.view,
2766 depth: &depth_view,
2767 shadow: None,
2768 },
2769 VolumeTimestamps {
2770 march: None,
2771 upsample: None,
2772 },
2773 );
2774 gpu.queue.submit(Some(encoder.finish()));
2775 gpu.readback_rgba16(&hdr).unwrap()
2776 }
2777
2778 fn test_plume() -> Plume {
2779 Plume {
2780 radius: 0.15,
2781 ..Plume::at([0.0, -0.05, 0.0])
2782 }
2783 }
2784
2785 fn steam_frame(time: f32) -> EffectFrame {
2786 let mut frame = EffectFrame::new(256, 128, time, 17);
2787 frame.set_volume(
2788 VolumeKind::Plume(test_plume()),
2789 [-0.5, -0.05, -0.5],
2790 [0.5, 0.15, 0.5],
2791 [0.82, 0.85, 0.88],
2792 1.6,
2793 0.25,
2794 );
2795 let a = -100.0 / 99.9;
2796 let b = -10.0 / 99.9;
2797 frame.view_proj = [
2798 [1.0, 0.0, 0.0, 0.0],
2799 [0.0, 1.0, 0.0, 0.0],
2800 [0.0, 0.0, a, -1.0],
2801 [0.0, 0.0, -3.0 * a + b, 3.0],
2802 ];
2803 frame
2804 }
2805
2806 #[test]
2807 #[ignore = "needs a GPU; run under pgpu"]
2808 fn the_steam_at_a_given_time_is_the_same_pixels_under_every_cut() {
2809 let gpu = pollster::block_on(Gpu::headless()).unwrap();
2810 let mut effects = Effects::new(&gpu.device, 256, 128, 4).unwrap();
2811 let cuts = [
2812 VolumeCuts::FULL,
2813 VolumeCuts::default(),
2814 VolumeCuts {
2815 steps: 6,
2816 warp_octaves: 1,
2817 octaves: 1,
2818 early_out: true,
2819 },
2820 ];
2821 for cut in cuts {
2822 effects.set_volume_cuts(&gpu.device, cut);
2823 let first = steam_pixels(&gpu, &mut effects, &steam_frame(2.0));
2824 let again = steam_pixels(&gpu, &mut effects, &steam_frame(2.0));
2825 let later = steam_pixels(&gpu, &mut effects, &steam_frame(2.5));
2826 assert!(first.iter().any(|v| *v != 0), "{cut:?} draws nothing");
2827 assert_eq!(first, again, "{cut:?}");
2828 assert_ne!(first, later, "{cut:?}");
2829 }
2830 effects.set_volume_cuts(&gpu.device, VolumeCuts::FULL);
2831 let reference = steam_pixels(&gpu, &mut effects, &steam_frame(2.0));
2832 let mut fresh = Effects::new(&gpu.device, 256, 128, 4).unwrap();
2833 assert_eq!(
2834 reference,
2835 steam_pixels(&gpu, &mut fresh, &steam_frame(2.0)).as_slice()
2836 );
2837 }
2838
2839 #[test]
2840 #[ignore = "needs a GPU; run under pgpu"]
2841 fn the_early_out_leaves_the_steam_byte_identical() {
2842 let gpu = pollster::block_on(Gpu::headless()).unwrap();
2843 let mut effects = Effects::new(&gpu.device, 256, 128, 4).unwrap();
2844 effects.set_volume_cuts(&gpu.device, VolumeCuts::FULL);
2845 let today = steam_pixels(&gpu, &mut effects, &steam_frame(2.0));
2846 effects.set_volume_cuts(
2847 &gpu.device,
2848 VolumeCuts {
2849 early_out: true,
2850 ..VolumeCuts::FULL
2851 },
2852 );
2853 assert_eq!(today, steam_pixels(&gpu, &mut effects, &steam_frame(2.0)));
2854 }
2855
2856 #[test]
2857 #[ignore = "needs a GPU; run under pgpu"]
2858 fn steam_at_half_scale_equals_steam_at_half_the_time() {
2859 let gpu = pollster::block_on(Gpu::headless()).unwrap();
2860 let (width, height) = (256, 128);
2861 let mut effects = Effects::new(&gpu.device, width, height, 4).unwrap();
2862 let frame_at = |time: f32, scale: f32| {
2863 let mut frame = EffectFrame::new(width, height, time, 17);
2864 frame.set_volume(
2865 VolumeKind::Plume(test_plume()),
2866 [-0.5, -0.05, -0.5],
2867 [0.5, 0.15, 0.5],
2868 [0.82, 0.85, 0.88],
2869 1.6,
2870 0.25,
2871 );
2872 let a = -100.0 / 99.9;
2873 let b = -10.0 / 99.9;
2874 frame.view_proj = [
2875 [1.0, 0.0, 0.0, 0.0],
2876 [0.0, 1.0, 0.0, 0.0],
2877 [0.0, 0.0, a, -1.0],
2878 [0.0, 0.0, -3.0 * a + b, 3.0],
2879 ];
2880 frame.scale_time(scale);
2881 frame
2882 };
2883 let halved = steam_pixels(&gpu, &mut effects, &frame_at(2.0, 0.5));
2884 let earlier = steam_pixels(&gpu, &mut effects, &frame_at(1.0, 1.0));
2885 let later = steam_pixels(&gpu, &mut effects, &frame_at(2.0, 1.0));
2886 assert!(halved.iter().any(|v| *v != 0), "the steam draws nothing");
2887 assert_eq!(halved, earlier);
2888 assert_ne!(halved, later);
2889 }
2890}