1use bytemuck::{Pod, Zeroable};
2use pfx_geom::tree::{Card, CardLod, Tree, tree_sway};
3use pfx_gpu::wgpu;
4use pfx_physics::{Emitter, ParticleKind};
5use wgpu::util::DeviceExt;
6
7pub const DEFAULT_RESOLUTION: u32 = 1024;
8pub const DEFAULT_PROCEDURAL_STRENGTH: f32 = 1.0;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum CanopyMode {
12 #[default]
13 Cookie,
14 Procedural,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct Gobo {
19 pub gust: [f32; 5],
20 pub sway: [[f32; 4]; 2],
21 pub branch: [f32; 4],
22 pub clusters: [f32; 5],
23 pub leaves: [f32; 6],
24 pub weight: f32,
25}
26
27impl Gobo {
28 pub fn validate(&self) -> Result<(), String> {
29 let all = self
30 .gust
31 .iter()
32 .chain(self.sway.iter().flatten())
33 .chain(&self.branch)
34 .chain(&self.clusters)
35 .chain(&self.leaves)
36 .chain(std::iter::once(&self.weight));
37 if all.clone().any(|value| !value.is_finite()) {
38 return Err("canopy gobo values must be finite".into());
39 }
40 if self.branch[0] == self.branch[1]
41 || self.clusters[3] == self.clusters[4]
42 || self.leaves[1] == self.leaves[2]
43 {
44 return Err("canopy gobo smoothstep edges must differ".into());
45 }
46 if self.weight <= 0.0 {
47 return Err("canopy gobo weight must be positive".into());
48 }
49 Ok(())
50 }
51}
52
53impl Default for Gobo {
54 fn default() -> Self {
55 Self {
56 gust: [0.2, 0.6, 0.35, 1.0, 0.4],
57 sway: [[1.0, 8.0, 0.005, 0.01], [0.8, 6.0, 0.004, 0.004]],
58 branch: [-0.5, 0.2, 0.8, 0.6],
59 clusters: [3.0, 4.0, 2.0, 0.3, 0.6],
60 leaves: [16.0, 0.35, 0.55, 1.2, 1.0, 0.08],
61 weight: 1.0,
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct CardColors {
68 pub tint_g: [f32; 3],
69 pub tint_b: [f32; 3],
70 pub glow: [f32; 3],
71}
72
73impl Default for CardColors {
74 fn default() -> Self {
75 Self {
76 tint_g: [0.9, 0.9, 0.85],
77 tint_b: [0.8, 0.85, 0.7],
78 glow: [0.8, 0.8, 0.6],
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug)]
84pub struct CanopyFrame {
85 pub origin: [f32; 3],
86 pub time: f32,
87 pub strength: f32,
88 pub direction: [f32; 3],
89 pub sun: [f32; 3],
90 pub receiver_y: f32,
91 pub angular_radius: f32,
92}
93
94pub const CANOPY_LIGHT_HEAD: &str = r#"
95struct CanopyParams {
96 center_extent: vec4f,
97 sun_receiver: vec4f,
98 wind: vec4f,
99 direction: vec4f,
100 projected_extent: vec4f,
101 mode_sharpness: vec4f,
102}
103@group(3) @binding(9) var canopy_cookie: texture_2d<f32>;
104@group(3) @binding(10) var canopy_sampler: sampler;
105@group(3) @binding(11) var<uniform> canopy_params: CanopyParams;
106fn canopy_hash(p: vec2f) -> f32 {
107 let q = fract(p * vec2f(123.34, 456.21));
108 return fract((q.x + 45.32) * (q.y + 45.32) * 34.23 + q.x * 13.7);
109}
110fn canopy_noise(p: vec2f) -> f32 {
111 let i = floor(p);
112 let f = fract(p);
113 let u = f * f * (3.0 - 2.0 * f);
114 return mix(mix(canopy_hash(i), canopy_hash(i + vec2f(1.0, 0.0)), u.x), mix(canopy_hash(i + vec2f(0.0, 1.0)), canopy_hash(i + vec2f(1.0, 1.0)), u.x), u.y);
115}
116fn canopy_fbm(p: vec2f) -> f32 {
117 return canopy_noise(p) * 0.55 + canopy_noise(p * 2.13 + 3.1) * 0.28 + canopy_noise(p * 4.37 + 7.7) * 0.17;
118}
119"#;
120
121pub const CANOPY_LIGHT_TAIL: &str = r#"fn canopy_light(world_position: vec3f) -> f32 {
122 let sun_input = canopy_params.sun_receiver.xyz;
123 let sun = select(vec3f(0.0, 1.0, 0.0), sun_input / max(length(sun_input), 1e-6), length(sun_input) > 1e-6);
124 if (canopy_params.mode_sharpness.x > 0.5) {
125 let origin = vec3f(canopy_params.center_extent.x, canopy_params.wind.z, canopy_params.center_extent.y);
126 let horizontal = vec3f(-sun.z, 0.0, sun.x);
127 let gobo_u = horizontal / max(length(horizontal), 1e-6);
128 let gobo_v = cross(gobo_u, sun);
129 let q = vec2f(dot(world_position - origin, gobo_u), dot(world_position - origin, gobo_v));
130 let light = procedural_canopy(q, canopy_params.wind.x);
131 let scale = canopy_params.mode_sharpness.z;
132 if (scale == 1.0) { return light; }
133 return 1.0 - (1.0 - light) * scale;
134 }
135 let reference = select(vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(sun.y) > 0.99);
136 let axis_u = normalize(cross(sun, reference));
137 let axis_v = cross(axis_u, sun);
138 let projected = vec2f(dot(world_position, axis_u), dot(world_position, axis_v));
139 let uv = (projected - canopy_params.projected_extent.xy) / canopy_params.projected_extent.zw + vec2f(0.5);
140 if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) { return 1.0; }
141 let texel = 1.0 / vec2f(textureDimensions(canopy_cookie));
142 let crown = vec3f(canopy_params.center_extent.x, canopy_params.wind.w, canopy_params.center_extent.y);
143 let distance = dot(crown - world_position, sun);
144 if (distance <= 0.0) { return 1.0; }
145 let radius = max(canopy_params.direction.w * distance * canopy_params.mode_sharpness.y / canopy_params.projected_extent.z, texel.x);
146 var shade = 0.0;
147 for (var y = -2; y <= 2; y++) {
148 for (var x = -2; x <= 2; x++) {
149 if (x * x + y * y <= 4) {
150 let offset = vec2f(f32(x), f32(y)) * radius * 0.5;
151 shade += textureSampleLevel(canopy_cookie, canopy_sampler, uv + offset, 0.0).r;
152 }
153 }
154 }
155 return 1.0 - shade / 13.0;
156}
157"#;
158
159fn literal(value: f32) -> String {
160 format!("{value:?}")
161}
162
163pub fn canopy_light_wgsl(gobo: &Gobo) -> String {
164 let [g0, g1, g2, g3, g4] = gobo.gust.map(literal);
165 let [x0, x1, x2, x3] = gobo.sway[0].map(literal);
166 let [z0, z1, z2, z3] = gobo.sway[1].map(literal);
167 let [b0, b1, b2, b3] = gobo.branch.map(literal);
168 let [c0, c1, c2, c3, c4] = gobo.clusters.map(literal);
169 let [l0, l1, l2, l3, l4, l5] = gobo.leaves.map(literal);
170 let weight = literal(gobo.weight);
171 let procedural = format!(
172 "fn procedural_canopy(q: vec2f, t: f32) -> f32 {{
173 let gust = sin(t * {g0}) * {g1} + sin(t * {g2} + {g3}) * {g4};
174 let sway = vec2f(sin(t * {x0} + q.y * {x1}) * {x2} + gust * {x3}, cos(t * {z0} + q.x * {z1}) * {z2} + gust * {z3});
175 let p = q + sway;
176 let branch = smoothstep({b0}, {b1}, p.x * {b2} + p.y * {b3});
177 let clusters = smoothstep({c3}, {c4}, canopy_fbm(p * {c0} + vec2f({c1}, {c2})));
178 let leaves = smoothstep({l1}, {l2}, canopy_fbm(p * {l0} + vec2f(sin(t * {l3}) * {l5}, cos(t * {l4}) * {l5})));
179 return 1.0 - leaves * clusters * branch * {weight};
180}}
181"
182 );
183 [CANOPY_LIGHT_HEAD, &procedural, CANOPY_LIGHT_TAIL].concat()
184}
185
186pub const CANOPY_COOKIE_WGSL: &str = r#"
187struct CanopyParams {
188 center_extent: vec4f,
189 sun_receiver: vec4f,
190 wind: vec4f,
191 direction: vec4f,
192 projected_extent: vec4f,
193 mode_sharpness: vec4f,
194}
195struct Card {
196 position_size: vec4f,
197 right_size: vec4f,
198 up: vec4f,
199 pivot_level: vec4f,
200 motion_color: vec4f,
201 atlas: vec4f,
202 lod: vec4f,
203}
204@group(0) @binding(0) var<uniform> params: CanopyParams;
205@group(0) @binding(1) var<storage, read> cards: array<Card>;
206@group(0) @binding(2) var alpha_atlas: texture_2d<f32>;
207@group(0) @binding(3) var alpha_sampler: sampler;
208fn tree_sway(rest: vec3f, pivot: vec3f, level: f32, stiffness: f32, wind_time: f32, strength: f32, direction: vec3f) -> vec3f {
209 let d = normalize(direction.xz + vec2f(0.00001, 0.0));
210 let arm = length(rest - pivot);
211 let phase = wind_time * (0.7 + level * 0.41) + pivot.x * 0.83 + pivot.z * 0.67;
212 let bend = sin(phase) * strength * (1.0 - stiffness) * arm * (0.012 + 0.009 * level);
213 return rest + vec3f(d.x * bend, -abs(bend) * 0.12, d.y * bend);
214}
215struct Varying { @builtin(position) clip: vec4f, @location(0) uv: vec2f, @location(1) @interpolate(flat) atlas: vec4f, @location(2) @interpolate(flat) lod: vec4f, }
216@vertex fn vertex(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> Varying {
217 let card = cards[instance];
218 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));
219 let corner = corners[vertex];
220 let rest = card.position_size.xyz + card.right_size.xyz * card.position_size.w * corner.x + card.up.xyz * card.right_size.w * corner.y;
221 let moved = tree_sway(rest, card.pivot_level.xyz, card.pivot_level.w, card.motion_color.x, params.wind.x, params.wind.y, params.direction.xyz) + vec3f(params.center_extent.x, params.wind.z, params.center_extent.y);
222 let sun_input = params.sun_receiver.xyz;
223 let sun = select(vec3f(0.0, 1.0, 0.0), sun_input / max(length(sun_input), 1e-6), length(sun_input) > 1e-6);
224 let reference = select(vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(sun.y) > 0.99);
225 let axis_u = normalize(cross(sun, reference));
226 let axis_v = cross(axis_u, sun);
227 let projected = vec2f(dot(moved, axis_u), dot(moved, axis_v));
228 let uv = (projected - params.projected_extent.xy) / params.projected_extent.zw + vec2f(0.5);
229 var out: Varying;
230 out.clip = vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0);
231 out.uv = (corner + vec2f(1.0)) * 0.5;
232 out.atlas = card.atlas;
233 out.lod = card.lod;
234 return out;
235}
236@fragment fn fragment(input: Varying) -> @location(0) f32 {
237 let uv = input.atlas.xy + input.uv * input.atlas.zw;
238 let weight = select(1.0 - input.lod.x, input.lod.x, input.lod.z > 0.5);
239 return textureSample(alpha_atlas, alpha_sampler, uv).r * weight;
240}
241"#;
242
243const CANOPY_VISIBLE_TEMPLATE: &str = r#"
244struct CanopyParams { center_extent: vec4f, sun_receiver: vec4f, wind: vec4f, direction: vec4f, projected_extent: vec4f, mode_sharpness: vec4f, }
245struct Card { position_size: vec4f, right_size: vec4f, up: vec4f, pivot_level: vec4f, motion_color: vec4f, atlas: vec4f, lod: vec4f, }
246@group(0) @binding(0) var<uniform> params: CanopyParams;
247@group(0) @binding(1) var<storage, read> cards: array<Card>;
248@group(0) @binding(2) var alpha_atlas: texture_2d<f32>;
249@group(0) @binding(3) var alpha_sampler: sampler;
250@group(1) @binding(0) var<uniform> view_proj: mat4x4f;
251fn tree_sway(rest: vec3f, pivot: vec3f, level: f32, stiffness: f32, wind_time: f32, strength: f32, direction: vec3f) -> vec3f {
252 let d = normalize(direction.xz + vec2f(0.00001, 0.0));
253 let arm = length(rest - pivot);
254 let phase = wind_time * (0.7 + level * 0.41) + pivot.x * 0.83 + pivot.z * 0.67;
255 let bend = sin(phase) * strength * (1.0 - stiffness) * arm * (0.012 + 0.009 * level);
256 return rest + vec3f(d.x * bend, -abs(bend) * 0.12, d.y * bend);
257}
258struct Varying { @builtin(position) clip: vec4f, @location(0) uv: vec2f, @location(1) @interpolate(flat) atlas: vec4f, @location(2) @interpolate(flat) color: vec3f, @location(3) @interpolate(flat) normal: vec3f, }
259@vertex fn vertex(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> Varying {
260 let card = cards[instance];
261 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));
262 let corner = corners[vertex];
263 let rest = card.position_size.xyz + card.right_size.xyz * card.position_size.w * corner.x + card.up.xyz * card.right_size.w * corner.y;
264 let moved = tree_sway(rest, card.pivot_level.xyz, card.pivot_level.w, card.motion_color.x, params.wind.x, params.wind.y, params.direction.xyz) + vec3f(params.center_extent.x, params.wind.z, params.center_extent.y);
265 var out: Varying;
266 out.clip = view_proj * vec4f(moved, 1.0);
267 out.uv = (corner + vec2f(1.0)) * 0.5;
268 out.atlas = card.atlas;
269 out.color = card.motion_color.yzw;
270 out.normal = normalize(cross(card.right_size.xyz, card.up.xyz));
271 return out;
272}
273@fragment fn fragment(input: Varying, @builtin(front_facing) front: bool) -> @location(0) vec4f {
274 let uv = input.atlas.xy + input.uv * input.atlas.zw;
275 let texel = textureSample(alpha_atlas, alpha_sampler, uv);
276 let normal = select(-input.normal, input.normal, front);
277 let sun = normalize(params.sun_receiver.xyz);
278 let facing = dot(normal, sun);
279 let diffuse = max(facing, 0.0);
280 let transmission = max(-facing, 0.0) * 0.48;
281 let pigment = mix(input.color, @TINT_G@, texel.g * 0.6);
282 let petal = mix(pigment, @TINT_B@, texel.b * 0.65);
283 let lit = petal * (0.22 + diffuse * 0.78) + @GLOW@ * transmission * 0.52;
284 return vec4f(lit, texel.r);
285}
286"#;
287
288pub fn canopy_visible_wgsl(colors: &CardColors) -> String {
289 let colour = |c: [f32; 3]| {
290 format!(
291 "vec3f({}, {}, {})",
292 literal(c[0]),
293 literal(c[1]),
294 literal(c[2])
295 )
296 };
297 CANOPY_VISIBLE_TEMPLATE
298 .replace("@TINT_G@", &colour(colors.tint_g))
299 .replace("@TINT_B@", &colour(colors.tint_b))
300 .replace("@GLOW@", &colour(colors.glow))
301}
302
303#[repr(C)]
304#[derive(Clone, Copy, Pod, Zeroable)]
305struct Params {
306 center_extent: [f32; 4],
307 sun_receiver: [f32; 4],
308 wind: [f32; 4],
309 direction: [f32; 4],
310 projected_extent: [f32; 4],
311 mode_sharpness: [f32; 4],
312}
313
314#[repr(C)]
315#[derive(Clone, Copy, Pod, Zeroable)]
316struct GpuCard {
317 position_size: [f32; 4],
318 right_size: [f32; 4],
319 up: [f32; 4],
320 pivot_level: [f32; 4],
321 motion_color: [f32; 4],
322 atlas: [f32; 4],
323 lod: [f32; 4],
324}
325
326fn gpu_card(card: &Card, coverage: f32, id: u32, near: bool) -> GpuCard {
327 GpuCard {
328 position_size: [
329 card.position[0],
330 card.position[1],
331 card.position[2],
332 card.size[0],
333 ],
334 right_size: [card.right[0], card.right[1], card.right[2], card.size[1]],
335 up: [card.up[0], card.up[1], card.up[2], 0.0],
336 pivot_level: [
337 card.sway.pivot[0],
338 card.sway.pivot[1],
339 card.sway.pivot[2],
340 card.sway.level,
341 ],
342 motion_color: [
343 card.sway.stiffness,
344 card.color[0],
345 card.color[1],
346 card.color[2],
347 ],
348 atlas: card.atlas,
349 lod: [coverage, id as f32, if near { 1.0 } else { 0.0 }, 0.0],
350 }
351}
352
353fn near_coverage(diameter: f32, projection_y: f32, height: u32, depth: f32) -> f32 {
354 let pixels = diameter * projection_y.abs() * height as f32 / (2.0 * depth.abs().max(0.05));
355 ((pixels - 9.0) / 3.0).clamp(0.0, 1.0)
356}
357
358struct CardGroup {
359 start: usize,
360 end: usize,
361 cluster: bool,
362}
363
364pub struct Canopy {
365 pub texture: wgpu::Texture,
366 pub view: wgpu::TextureView,
367 pub sampler: wgpu::Sampler,
368 pub resolution: u32,
369 pub center: [f32; 2],
370 pub extent: [f32; 2],
371 pub card_count: u32,
372 source: Vec<GpuCard>,
373 groups: Vec<CardGroup>,
374 visible: Vec<GpuCard>,
375 card_buffer: wgpu::Buffer,
376 params: wgpu::Buffer,
377 bind: wgpu::BindGroup,
378 pipeline: wgpu::RenderPipeline,
379 cookie_layout: wgpu::BindGroupLayout,
380 sample_layout: wgpu::BindGroupLayout,
381 current: Params,
382 canopy_height: f32,
383 weight: f32,
384}
385
386impl Canopy {
387 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, tree: &Tree, resolution: u32) -> Self {
388 let resolution = resolution.max(16);
389 let cards: Vec<GpuCard> = tree
390 .cards
391 .iter()
392 .enumerate()
393 .map(|(index, card)| gpu_card(card, 1.0, index as u32, true))
394 .collect();
395 let mut groups = Vec::new();
396 let mut index = 0;
397 while index < tree.cards.len() {
398 let start = index;
399 let cluster = tree.cards[index].lod == CardLod::Cluster;
400 index += 1;
401 if cluster {
402 while index < tree.cards.len() && tree.cards[index].lod == CardLod::Individual {
403 index += 1;
404 }
405 }
406 groups.push(CardGroup {
407 start,
408 end: index,
409 cluster,
410 });
411 }
412 let card_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
413 label: Some("canopy cards"),
414 contents: bytemuck::cast_slice(&cards),
415 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
416 });
417 let extent = [tree.spec.crown_width * 2.8, tree.spec.crown_width * 2.8];
418 let current = Params {
419 center_extent: [0.0, 0.0, extent[0], extent[1]],
420 sun_receiver: [0.0, 1.0, 0.0, 0.0],
421 wind: [0.0, 0.0, 0.0, tree.spec.height * 0.8],
422 direction: [1.0, 0.0, 0.0, 0.00465],
423 projected_extent: [0.0, 0.0, extent[0], extent[1]],
424 mode_sharpness: [0.0, 1.0, 0.0, DEFAULT_PROCEDURAL_STRENGTH],
425 };
426 let params = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
427 label: Some("canopy params"),
428 contents: bytemuck::bytes_of(¤t),
429 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
430 });
431 let texture = device.create_texture(&wgpu::TextureDescriptor {
432 label: Some("canopy cookie"),
433 size: wgpu::Extent3d {
434 width: resolution,
435 height: resolution,
436 depth_or_array_layers: 1,
437 },
438 mip_level_count: 1,
439 sample_count: 1,
440 dimension: wgpu::TextureDimension::D2,
441 format: wgpu::TextureFormat::R8Unorm,
442 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
443 | wgpu::TextureUsages::TEXTURE_BINDING
444 | wgpu::TextureUsages::COPY_SRC,
445 view_formats: &[],
446 });
447 let view = texture.create_view(&Default::default());
448 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
449 mag_filter: wgpu::FilterMode::Linear,
450 min_filter: wgpu::FilterMode::Linear,
451 ..Default::default()
452 });
453 let atlas = device.create_texture(&wgpu::TextureDescriptor {
454 label: Some("tree alpha atlas"),
455 size: wgpu::Extent3d {
456 width: tree.atlas.width,
457 height: tree.atlas.height,
458 depth_or_array_layers: 1,
459 },
460 mip_level_count: 1,
461 sample_count: 1,
462 dimension: wgpu::TextureDimension::D2,
463 format: wgpu::TextureFormat::Rgba8Unorm,
464 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
465 view_formats: &[],
466 });
467 let atlas_view = atlas.create_view(&Default::default());
468 queue.write_texture(
469 wgpu::TexelCopyTextureInfo {
470 texture: &atlas,
471 mip_level: 0,
472 origin: wgpu::Origin3d::ZERO,
473 aspect: wgpu::TextureAspect::All,
474 },
475 &tree.atlas.pixels,
476 wgpu::TexelCopyBufferLayout {
477 offset: 0,
478 bytes_per_row: Some(tree.atlas.width * 4),
479 rows_per_image: Some(tree.atlas.height),
480 },
481 wgpu::Extent3d {
482 width: tree.atlas.width,
483 height: tree.atlas.height,
484 depth_or_array_layers: 1,
485 },
486 );
487 let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
488 mag_filter: wgpu::FilterMode::Linear,
489 min_filter: wgpu::FilterMode::Linear,
490 address_mode_u: wgpu::AddressMode::ClampToEdge,
491 address_mode_v: wgpu::AddressMode::ClampToEdge,
492 ..Default::default()
493 });
494 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
495 label: Some("canopy cookie layout"),
496 entries: &cookie_entries(),
497 });
498 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
499 label: Some("canopy cookie bind"),
500 layout: &layout,
501 entries: &[
502 wgpu::BindGroupEntry {
503 binding: 0,
504 resource: params.as_entire_binding(),
505 },
506 wgpu::BindGroupEntry {
507 binding: 1,
508 resource: card_buffer.as_entire_binding(),
509 },
510 wgpu::BindGroupEntry {
511 binding: 2,
512 resource: wgpu::BindingResource::TextureView(&atlas_view),
513 },
514 wgpu::BindGroupEntry {
515 binding: 3,
516 resource: wgpu::BindingResource::Sampler(&atlas_sampler),
517 },
518 ],
519 });
520 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
521 label: Some("canopy cookie shader"),
522 source: wgpu::ShaderSource::Wgsl(CANOPY_COOKIE_WGSL.into()),
523 });
524 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
525 label: Some("canopy cookie pipeline layout"),
526 bind_group_layouts: &[Some(&layout)],
527 immediate_size: 0,
528 });
529 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
530 label: Some("canopy cookie pipeline"),
531 layout: Some(&pipeline_layout),
532 vertex: wgpu::VertexState {
533 module: &module,
534 entry_point: Some("vertex"),
535 compilation_options: Default::default(),
536 buffers: &[],
537 },
538 fragment: Some(wgpu::FragmentState {
539 module: &module,
540 entry_point: Some("fragment"),
541 compilation_options: Default::default(),
542 targets: &[Some(wgpu::ColorTargetState {
543 format: wgpu::TextureFormat::R8Unorm,
544 blend: Some(wgpu::BlendState {
545 color: wgpu::BlendComponent {
546 src_factor: wgpu::BlendFactor::One,
547 dst_factor: wgpu::BlendFactor::OneMinusSrc,
548 operation: wgpu::BlendOperation::Add,
549 },
550 alpha: wgpu::BlendComponent::REPLACE,
551 }),
552 write_mask: wgpu::ColorWrites::ALL,
553 })],
554 }),
555 primitive: wgpu::PrimitiveState {
556 cull_mode: None,
557 ..Default::default()
558 },
559 depth_stencil: None,
560 multisample: Default::default(),
561 multiview_mask: None,
562 cache: None,
563 });
564 let sample_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
565 label: Some("canopy sample layout"),
566 entries: &[
567 wgpu::BindGroupLayoutEntry {
568 binding: 9,
569 visibility: wgpu::ShaderStages::FRAGMENT,
570 ty: wgpu::BindingType::Texture {
571 sample_type: wgpu::TextureSampleType::Float { filterable: true },
572 view_dimension: wgpu::TextureViewDimension::D2,
573 multisampled: false,
574 },
575 count: None,
576 },
577 wgpu::BindGroupLayoutEntry {
578 binding: 10,
579 visibility: wgpu::ShaderStages::FRAGMENT,
580 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
581 count: None,
582 },
583 wgpu::BindGroupLayoutEntry {
584 binding: 11,
585 visibility: wgpu::ShaderStages::FRAGMENT,
586 ty: wgpu::BindingType::Buffer {
587 ty: wgpu::BufferBindingType::Uniform,
588 has_dynamic_offset: false,
589 min_binding_size: None,
590 },
591 count: None,
592 },
593 ],
594 });
595 Self {
596 texture,
597 view,
598 sampler,
599 resolution,
600 center: [0.0, 0.0],
601 extent,
602 card_count: cards.len() as u32,
603 source: cards,
604 groups,
605 visible: Vec::with_capacity(tree.cards.len()),
606 card_buffer,
607 params,
608 bind,
609 pipeline,
610 cookie_layout: layout,
611 sample_layout,
612 current,
613 canopy_height: tree.spec.height * 0.8,
614 weight: 1.0,
615 }
616 }
617
618 pub fn sample_layout(&self) -> &wgpu::BindGroupLayout {
619 &self.sample_layout
620 }
621
622 pub fn select_lod(
623 &mut self,
624 queue: &wgpu::Queue,
625 camera: crate::frame::Camera,
626 height: u32,
627 origin: [f32; 3],
628 ) {
629 self.visible.clear();
630 for group in &self.groups {
631 let index = group.start;
632 let card = self.source[index];
633 if group.cluster {
634 let center = [
635 card.position_size[0] + origin[0],
636 card.position_size[1] + origin[1],
637 card.position_size[2] + origin[2],
638 ];
639 let view =
640 crate::frame::transform(camera.view, [center[0], center[1], center[2], 1.0]);
641 let near = near_coverage(0.032, camera.projection[1][1], height, view[2]);
642 let id = index as u32;
643 if near < 1.0 {
644 let mut far = card;
645 far.lod = [near, id as f32, 0.0, 0.0];
646 self.visible.push(far);
647 }
648 if near > 0.0 {
649 for flower in &self.source[index + 1..group.end] {
650 let mut flower = *flower;
651 flower.lod = [near, id as f32, 1.0, 0.0];
652 self.visible.push(flower);
653 }
654 }
655 } else {
656 self.visible.push(card);
657 }
658 }
659 self.card_count = self.visible.len() as u32;
660 if !self.visible.is_empty() {
661 queue.write_buffer(&self.card_buffer, 0, bytemuck::cast_slice(&self.visible));
662 }
663 }
664 pub fn cookie_layout(&self) -> &wgpu::BindGroupLayout {
665 &self.cookie_layout
666 }
667 pub fn cookie_bind(&self) -> &wgpu::BindGroup {
668 &self.bind
669 }
670 pub fn params(&self) -> &wgpu::Buffer {
671 &self.params
672 }
673 pub fn set_mode(&mut self, mode: CanopyMode) {
674 self.current.mode_sharpness[0] = match mode {
675 CanopyMode::Cookie => 0.0,
676 CanopyMode::Procedural => 1.0,
677 };
678 }
679 pub fn set_sharpness(&mut self, sharpness: f32) -> Result<(), String> {
680 if !sharpness.is_finite() || sharpness <= 0.0 {
681 return Err("canopy sharpness must be finite and positive".into());
682 }
683 self.current.mode_sharpness[1] = sharpness;
684 Ok(())
685 }
686 pub fn set_strength(&mut self, strength: f32) -> Result<(), String> {
687 if !strength.is_finite() || !(0.0..=1.0).contains(&strength) {
688 return Err("canopy strength must be finite and between 0 and 1".into());
689 }
690 self.current.mode_sharpness[2] = strength / self.weight;
691 self.current.mode_sharpness[3] = strength;
692 Ok(())
693 }
694 pub fn set_weight(&mut self, weight: f32) {
695 self.weight = weight;
696 self.current.mode_sharpness[2] = self.current.mode_sharpness[3] / weight;
697 }
698 pub fn strength(&self) -> f32 {
699 self.current.mode_sharpness[3]
700 }
701 pub fn sample_bind(&self, device: &wgpu::Device) -> wgpu::BindGroup {
702 device.create_bind_group(&wgpu::BindGroupDescriptor {
703 label: Some("canopy sample bind"),
704 layout: &self.sample_layout,
705 entries: &[
706 wgpu::BindGroupEntry {
707 binding: 9,
708 resource: wgpu::BindingResource::TextureView(&self.view),
709 },
710 wgpu::BindGroupEntry {
711 binding: 10,
712 resource: wgpu::BindingResource::Sampler(&self.sampler),
713 },
714 wgpu::BindGroupEntry {
715 binding: 11,
716 resource: self.params.as_entire_binding(),
717 },
718 ],
719 })
720 }
721 pub fn update(
722 &mut self,
723 queue: &wgpu::Queue,
724 encoder: &mut wgpu::CommandEncoder,
725 frame: CanopyFrame,
726 timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
727 ) {
728 let (center, extent) = projected_bounds(
729 frame.origin,
730 self.canopy_height,
731 self.current.center_extent[2],
732 frame.sun,
733 frame.receiver_y,
734 );
735 self.center = center;
736 self.extent = extent;
737 self.current.center_extent[0..2].copy_from_slice(&[frame.origin[0], frame.origin[2]]);
738 self.current.projected_extent = [center[0], center[1], extent[0], extent[1]];
739 self.current.sun_receiver = [frame.sun[0], frame.sun[1], frame.sun[2], frame.receiver_y];
740 self.current.wind = [
741 frame.time,
742 frame.strength,
743 frame.origin[1],
744 self.canopy_height + frame.origin[1],
745 ];
746 self.current.direction = [
747 frame.direction[0],
748 frame.direction[1],
749 frame.direction[2],
750 frame.angular_radius,
751 ];
752 queue.write_buffer(&self.params, 0, bytemuck::bytes_of(&self.current));
753 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
754 multiview_mask: None,
755 label: Some("canopy cookie"),
756 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
757 view: &self.view,
758 resolve_target: None,
759 ops: wgpu::Operations {
760 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
761 store: wgpu::StoreOp::Store,
762 },
763 depth_slice: None,
764 })],
765 depth_stencil_attachment: None,
766 timestamp_writes: timestamps,
767 occlusion_query_set: None,
768 });
769 pass.set_pipeline(&self.pipeline);
770 pass.set_bind_group(0, &self.bind, &[]);
771 pass.draw(0..6, 0..self.card_count);
772 }
773}
774
775fn projected_bounds(
776 origin: [f32; 3],
777 canopy_height: f32,
778 base_extent: f32,
779 sun: [f32; 3],
780 _receiver_y: f32,
781) -> ([f32; 2], [f32; 2]) {
782 let [u, v] = projection_axes(sun);
783 let crown = [origin[0], origin[1] + canopy_height, origin[2]];
784 let center = [dot3(crown, u), dot3(crown, v)];
785 let extent = [base_extent + canopy_height, base_extent + canopy_height];
786 (center, extent)
787}
788
789fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
790 a.into_iter().zip(b).map(|(x, y)| x * y).sum()
791}
792
793fn projection_axes(sun: [f32; 3]) -> [[f32; 3]; 2] {
794 let length = dot3(sun, sun).sqrt();
795 let s = if length > 1.0e-6 {
796 sun.map(|component| component / length)
797 } else {
798 [0.0, 1.0, 0.0]
799 };
800 let reference = if s[1].abs() > 0.99 {
801 [0.0, 0.0, 1.0]
802 } else {
803 [0.0, 1.0, 0.0]
804 };
805 let u = [
806 s[1] * reference[2] - s[2] * reference[1],
807 s[2] * reference[0] - s[0] * reference[2],
808 s[0] * reference[1] - s[1] * reference[0],
809 ];
810 let length = dot3(u, u).sqrt().max(1.0e-6);
811 let u = u.map(|component| component / length);
812 let v = [
813 u[1] * s[2] - u[2] * s[1],
814 u[2] * s[0] - u[0] * s[2],
815 u[0] * s[1] - u[1] * s[0],
816 ];
817 [u, v]
818}
819
820pub fn projected_coverage(
821 tree: &Tree,
822 time: f32,
823 strength: f32,
824 direction: [f32; 3],
825 sun: [f32; 3],
826 _receiver_y: f32,
827) -> f32 {
828 let [axis_u, axis_v] = projection_axes(sun);
829 let tile_width = tree.atlas.width as usize / 6;
830 let mut opacity = [0.0_f32; 6];
831 for row in tree
832 .atlas
833 .pixels
834 .chunks_exact(tree.atlas.width as usize * 4)
835 {
836 for (tile, sum) in opacity.iter_mut().enumerate() {
837 *sum += (tile * tile_width..(tile + 1) * tile_width)
838 .map(|x| row[x * 4] as f32 / 255.0)
839 .sum::<f32>();
840 }
841 }
842 let divisor = (tile_width * tree.atlas.height as usize).max(1) as f32;
843 for value in &mut opacity {
844 *value /= divisor;
845 }
846 let mut area = 0.0;
847 for c in &tree.cards {
848 let corners = [[-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0]];
849 let mut p = [[0.0; 2]; 3];
850 for (i, corner) in corners.iter().enumerate() {
851 let rest = [
852 c.position[0]
853 + c.right[0] * c.size[0] * corner[0]
854 + c.up[0] * c.size[1] * corner[1],
855 c.position[1]
856 + c.right[1] * c.size[0] * corner[0]
857 + c.up[1] * c.size[1] * corner[1],
858 c.position[2]
859 + c.right[2] * c.size[0] * corner[0]
860 + c.up[2] * c.size[1] * corner[1],
861 ];
862 let m = tree_sway(
863 rest,
864 c.sway.pivot,
865 c.sway.level,
866 c.sway.stiffness,
867 time,
868 strength,
869 direction,
870 );
871 p[i] = [dot3(m, axis_u), dot3(m, axis_v)];
872 }
873 let ax = p[1][0] - p[0][0];
874 let ay = p[1][1] - p[0][1];
875 let bx = p[2][0] - p[0][0];
876 let by = p[2][1] - p[0][1];
877 area += (ax * by - ay * bx).abs() * opacity[(c.atlas[0] * 6.0).round() as usize];
878 }
879 area
880}
881
882pub fn petal_emitters(
883 tree: &Tree,
884 origin: [f32; 3],
885 time: f32,
886 strength: f32,
887 direction: [f32; 3],
888 max_total_rate: f32,
889) -> Vec<Emitter> {
890 if !max_total_rate.is_finite() || max_total_rate <= 0.0 || tree.petal_sources.is_empty() {
891 return Vec::new();
892 }
893 let native_rate = tree.petal_sources[0].rate.max(0.001);
894 let count = ((max_total_rate / native_rate).ceil() as usize).clamp(1, tree.petal_sources.len());
895 let per_source = max_total_rate / count as f32;
896 (0..count)
897 .map(|i| {
898 let index = ((i as f32 + 0.5) * tree.petal_sources.len() as f32 / count as f32).floor()
899 as usize;
900 let source = &tree.petal_sources[index.min(tree.petal_sources.len() - 1)];
901 let position = tree_sway(
902 source.position,
903 source.sway.pivot,
904 source.sway.level,
905 source.sway.stiffness,
906 time,
907 strength,
908 direction,
909 );
910 let position = [
911 position[0] + origin[0],
912 position[1] + origin[1],
913 position[2] + origin[2],
914 ];
915 let jitter = 0.7
916 + 0.3
917 * ((source.seed.wrapping_mul(0x9e3779b97f4a7c15) >> 40) as f32 / 16_777_216.0);
918 Emitter::new(
919 ParticleKind::Petal,
920 position,
921 source.rate.min(per_source) * jitter,
922 source.seed,
923 )
924 })
925 .collect()
926}
927
928pub fn cookie_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
929 vec![
930 wgpu::BindGroupLayoutEntry {
931 binding: 0,
932 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
933 ty: wgpu::BindingType::Buffer {
934 ty: wgpu::BufferBindingType::Uniform,
935 has_dynamic_offset: false,
936 min_binding_size: None,
937 },
938 count: None,
939 },
940 wgpu::BindGroupLayoutEntry {
941 binding: 1,
942 visibility: wgpu::ShaderStages::VERTEX,
943 ty: wgpu::BindingType::Buffer {
944 ty: wgpu::BufferBindingType::Storage { read_only: true },
945 has_dynamic_offset: false,
946 min_binding_size: None,
947 },
948 count: None,
949 },
950 wgpu::BindGroupLayoutEntry {
951 binding: 2,
952 visibility: wgpu::ShaderStages::FRAGMENT,
953 ty: wgpu::BindingType::Texture {
954 sample_type: wgpu::TextureSampleType::Float { filterable: true },
955 view_dimension: wgpu::TextureViewDimension::D2,
956 multisampled: false,
957 },
958 count: None,
959 },
960 wgpu::BindGroupLayoutEntry {
961 binding: 3,
962 visibility: wgpu::ShaderStages::FRAGMENT,
963 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
964 count: None,
965 },
966 ]
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 #[test]
973 fn cluster_lod_changes_with_distance_without_coverage_jump() {
974 let mut previous = 1.0;
975 for depth in [4.0, 6.0, 8.0, 10.0, 14.0, 20.0, 30.0] {
976 let near = near_coverage(0.032, 2.7, 2160, depth);
977 let far = 1.0 - near;
978 assert!(near <= previous);
979 assert!((near + far - 1.0).abs() < 1e-6);
980 previous = near;
981 }
982 assert_eq!(near_coverage(0.032, 2.7, 2160, 4.0), 1.0);
983 assert_eq!(near_coverage(0.032, 2.7, 2160, 30.0), 0.0);
984 for pixels in [9.0_f32, 12.0] {
985 let depth = 0.032 * 2.7 * 2160.0 / (2.0 * pixels);
986 let before = near_coverage(0.032, 2.7, 2160, depth - 0.001);
987 let after = near_coverage(0.032, 2.7, 2160, depth + 0.001);
988 assert!((before - after).abs() < 0.001);
989 }
990 }
991 #[test]
992 fn shaders_and_area() {
993 for source in [
994 CANOPY_COOKIE_WGSL.to_string(),
995 canopy_visible_wgsl(&CardColors::default()),
996 ] {
997 let module = naga::front::wgsl::parse_str(&source).unwrap();
998 naga::valid::Validator::new(
999 naga::valid::ValidationFlags::all(),
1000 naga::valid::Capabilities::all(),
1001 )
1002 .validate(&module)
1003 .unwrap();
1004 }
1005 naga::front::wgsl::parse_str(&canopy_light_wgsl(&Gobo::default())).unwrap();
1006 let tree = Tree::new(9, pfx_geom::tree::TreeSpec::plum(), 0.02);
1007 let overhead = projected_coverage(&tree, 0.0, 0.0, [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.0);
1008 let tile_width = tree.atlas.width as usize / 6;
1009 let alpha = |tile: usize| -> f32 {
1010 tree.atlas
1011 .pixels
1012 .chunks_exact(4)
1013 .enumerate()
1014 .filter(|(index, _)| index % tree.atlas.width as usize / tile_width == tile)
1015 .map(|(_, rgba)| rgba[0] as f32 / 255.0)
1016 .sum::<f32>()
1017 / (tile_width * tree.atlas.height as usize) as f32
1018 };
1019 let expected = tree
1020 .cards
1021 .iter()
1022 .map(|c| {
1023 4.0 * c.size[0]
1024 * c.size[1]
1025 * (c.right[0] * c.up[2] - c.right[2] * c.up[0]).abs()
1026 * alpha((c.atlas[0] * 6.0).round() as usize)
1027 })
1028 .sum::<f32>();
1029 assert!((overhead - expected).abs() < expected * 1e-5);
1030 let angled = projected_coverage(&tree, 0.0, 0.0, [1.0, 0.0, 0.0], [0.4, 1.0, 0.2], 0.0);
1031 assert!((overhead - angled).abs() > overhead * 0.001);
1032 }
1033
1034 #[test]
1035 #[ignore = "needs a GPU; run under pgpu"]
1036 fn cookie_angular_radius_softens_an_edge() {
1037 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
1038 let tree = Tree::new(9, pfx_geom::tree::TreeSpec::plum(), 0.02);
1039 let mut canopy = Canopy::new(&gpu.device, &gpu.queue, &tree, 256);
1040 canopy.current.wind[3] = 1.0;
1041 canopy.current.projected_extent = [0.0, 0.0, 1.0, 1.0];
1042 let pixels: Vec<u8> = (0..256 * 256)
1043 .map(|index| if index % 256 >= 128 { 255 } else { 0 })
1044 .collect();
1045 let cookie = gpu.device.create_texture(&wgpu::TextureDescriptor {
1046 label: Some("filter edge"),
1047 size: wgpu::Extent3d {
1048 width: 256,
1049 height: 256,
1050 depth_or_array_layers: 1,
1051 },
1052 mip_level_count: 1,
1053 sample_count: 1,
1054 dimension: wgpu::TextureDimension::D2,
1055 format: wgpu::TextureFormat::R8Unorm,
1056 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1057 view_formats: &[],
1058 });
1059 gpu.queue.write_texture(
1060 wgpu::TexelCopyTextureInfo {
1061 texture: &cookie,
1062 mip_level: 0,
1063 origin: wgpu::Origin3d::ZERO,
1064 aspect: wgpu::TextureAspect::All,
1065 },
1066 &pixels,
1067 wgpu::TexelCopyBufferLayout {
1068 offset: 0,
1069 bytes_per_row: Some(256),
1070 rows_per_image: Some(256),
1071 },
1072 wgpu::Extent3d {
1073 width: 256,
1074 height: 256,
1075 depth_or_array_layers: 1,
1076 },
1077 );
1078 let cookie_bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1079 label: Some("filter edge bind"),
1080 layout: canopy.sample_layout(),
1081 entries: &[
1082 wgpu::BindGroupEntry {
1083 binding: 9,
1084 resource: wgpu::BindingResource::TextureView(
1085 &cookie.create_view(&Default::default()),
1086 ),
1087 },
1088 wgpu::BindGroupEntry {
1089 binding: 10,
1090 resource: wgpu::BindingResource::Sampler(&canopy.sampler),
1091 },
1092 wgpu::BindGroupEntry {
1093 binding: 11,
1094 resource: canopy.params.as_entire_binding(),
1095 },
1096 ],
1097 });
1098 let empty = gpu
1099 .device
1100 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1101 label: Some("filter empty layout"),
1102 entries: &[],
1103 });
1104 let empty_bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1105 label: Some("filter empty bind"),
1106 layout: &empty,
1107 entries: &[],
1108 });
1109 let layout = gpu
1110 .device
1111 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1112 label: Some("filter layout"),
1113 bind_group_layouts: &[
1114 Some(&empty),
1115 Some(&empty),
1116 Some(&empty),
1117 Some(canopy.sample_layout()),
1118 ],
1119 immediate_size: 0,
1120 });
1121 let shader = gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
1122 label: Some("filter shader"),
1123 source: wgpu::ShaderSource::Wgsl(
1124 format!(
1125 "{}\n@vertex fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {{ let corners = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); return vec4f(corners[index], 0.0, 1.0); }}\n@fragment fn fragment() -> @location(0) vec4f {{ let light = canopy_light(vec3f(0.03, 0.0, 0.0)); return vec4f(light, light, light, 1.0); }}",
1126 canopy_light_wgsl(&Gobo::default())
1127 )
1128 .into(),
1129 ),
1130 });
1131 let pipeline = gpu
1132 .device
1133 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1134 label: Some("filter pipeline"),
1135 layout: Some(&layout),
1136 vertex: wgpu::VertexState {
1137 module: &shader,
1138 entry_point: Some("vertex"),
1139 compilation_options: Default::default(),
1140 buffers: &[],
1141 },
1142 fragment: Some(wgpu::FragmentState {
1143 module: &shader,
1144 entry_point: Some("fragment"),
1145 compilation_options: Default::default(),
1146 targets: &[Some(wgpu::ColorTargetState {
1147 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1148 blend: None,
1149 write_mask: wgpu::ColorWrites::ALL,
1150 })],
1151 }),
1152 primitive: Default::default(),
1153 depth_stencil: None,
1154 multisample: Default::default(),
1155 multiview_mask: None,
1156 cache: None,
1157 });
1158 let target = gpu
1159 .offscreen(1, 1, wgpu::TextureFormat::Rgba8UnormSrgb)
1160 .unwrap();
1161 let mut sample = |angular_radius, sharpness| {
1162 canopy.current.direction[3] = angular_radius;
1163 canopy.set_sharpness(sharpness).unwrap();
1164 gpu.queue
1165 .write_buffer(&canopy.params, 0, bytemuck::bytes_of(&canopy.current));
1166 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1167 {
1168 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1169 multiview_mask: None,
1170 label: Some("filter sample"),
1171 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1172 view: &target.view,
1173 resolve_target: None,
1174 ops: wgpu::Operations {
1175 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1176 store: wgpu::StoreOp::Store,
1177 },
1178 depth_slice: None,
1179 })],
1180 depth_stencil_attachment: None,
1181 timestamp_writes: None,
1182 occlusion_query_set: None,
1183 });
1184 pass.set_pipeline(&pipeline);
1185 for group in 0..3 {
1186 pass.set_bind_group(group, &empty_bind, &[]);
1187 }
1188 pass.set_bind_group(3, &cookie_bind, &[]);
1189 pass.draw(0..3, 0..1);
1190 }
1191 gpu.queue.submit([encoder.finish()]);
1192 gpu.readback_rgba8(&target).unwrap()[0]
1193 };
1194 let sharp = sample(0.0001, 1.0);
1195 let soft = sample(0.08, 1.0);
1196 let sharper = sample(0.08, 0.3);
1197 assert!(sharp < 5, "sharp={sharp}");
1198 assert!(soft > 100 && soft < 200, "soft={soft}");
1199 assert!(sharper < soft);
1200 }
1201 #[test]
1202 fn morning_and_evening_shade_tracks_projected_crown() {
1203 let tree = Tree::new(9, pfx_geom::tree::TreeSpec::plum(), 0.02);
1204 let origin = [2.0, 0.0, -3.0];
1205 let height = tree.spec.height * 0.8;
1206 let base = tree.spec.crown_width * 2.8;
1207 for hour in [7.0, 18.0] {
1208 let sun = pfx_core::daylight::Daylight {
1209 hour,
1210 day: pfx_core::daylight::Daylight::DAY,
1211 latitude: pfx_core::daylight::Daylight::LATITUDE,
1212 heading: pfx_core::daylight::Daylight::HEADING,
1213 }
1214 .sun()
1215 .y_up
1216 .map(|value| value as f32);
1217 let (center, extent) = projected_bounds(origin, height, base, sun, 0.0);
1218 let [u, v] = projection_axes(sun);
1219 let crown = [origin[0], origin[1] + height, origin[2]];
1220 let expected = [dot3(crown, u), dot3(crown, v)];
1221 assert!((center[0] - expected[0]).abs() < 1e-5);
1222 assert!((center[1] - expected[1]).abs() < 1e-5);
1223 assert!(extent[0] > base);
1224 let mut sum = [0.0; 2];
1225 let mut weight = 0.0;
1226 for card in &tree.cards {
1227 let world = [
1228 origin[0] + card.position[0],
1229 origin[1] + card.position[1],
1230 origin[2] + card.position[2],
1231 ];
1232 let projected = [dot3(world, u), dot3(world, v)];
1233 let area = card.size[0] * card.size[1];
1234 sum[0] += projected[0] * area;
1235 sum[1] += projected[1] * area;
1236 weight += area;
1237 }
1238 let centroid = [sum[0] / weight, sum[1] / weight];
1239 assert!((centroid[0] - center[0]).abs() < extent[0] * 0.2);
1240 assert!((centroid[1] - center[1]).abs() < extent[1] * 0.2);
1241 }
1242 }
1243 #[test]
1244 fn vertical_receiver_uses_sun_perpendicular_coordinates() {
1245 let sun = [0.4, 0.7, 0.3];
1246 let [u, v] = projection_axes(sun);
1247 let receiver = [0.1, -0.2, 0.0];
1248 let along_sun = [
1249 receiver[0] + sun[0] * 3.0,
1250 receiver[1] + sun[1] * 3.0,
1251 receiver[2] + sun[2] * 3.0,
1252 ];
1253 assert!((dot3(receiver, u) - dot3(along_sun, u)).abs() < 1.0e-6);
1254 assert!((dot3(receiver, v) - dot3(along_sun, v)).abs() < 1.0e-6);
1255 assert!(dot3(u, v).abs() < 1.0e-6);
1256 }
1257 #[test]
1258 fn petals_are_seeded_and_rate_limited() {
1259 let tree = Tree::new(41, pfx_geom::tree::TreeSpec::cherry(), 0.02);
1260 let mut first = petal_emitters(&tree, [0.0; 3], 1.0, 1.0, [1.0, 0.0, 0.0], 5.0);
1261 let mut second = petal_emitters(&tree, [0.0; 3], 1.0, 1.0, [1.0, 0.0, 0.0], 5.0);
1262 assert_eq!(first.len(), second.len());
1263 assert!(first.len() < tree.petal_sources.len());
1264 let wind = pfx_physics::Wind::new(4, [1.0, 0.0, 0.0], 0.5);
1265 for frame in 0..1200 {
1266 let time = frame as f32 / 60.0;
1267 for emitter in &mut first {
1268 emitter.step(1.0 / 60.0, time, &wind);
1269 }
1270 for emitter in &mut second {
1271 emitter.step(1.0 / 60.0, time, &wind);
1272 }
1273 }
1274 let total = first.iter().map(|e| e.specks().len()).sum::<usize>();
1275 assert!(total > 0 && total < 40, "{total} live petals");
1276 for (a, b) in first.iter().zip(&second) {
1277 assert_eq!(a.specks().len(), b.specks().len());
1278 for (left, right) in a.specks().iter().zip(b.specks()) {
1279 assert_eq!(left.pos, right.pos);
1280 }
1281 }
1282 }
1283}
1284
1285#[cfg(test)]
1286mod gpu_tests {
1287 use super::*;
1288 use pfx_geom::tree::TreeSpec;
1289 use pfx_gpu::{Gpu, GpuProfiler};
1290
1291 #[test]
1292 #[ignore = "needs a GPU; run under pgpu"]
1293 fn low_sun_cookie_centroid() {
1294 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1295 let tree = Tree::new(9, TreeSpec::plum(), 0.02);
1296 let mut canopy = Canopy::new(&gpu.device, &gpu.queue, &tree, 256);
1297 let origin = [2.0, 0.0, -3.0];
1298 for hour in [7.0, 18.0] {
1299 let sun = pfx_core::daylight::Daylight {
1300 hour,
1301 day: pfx_core::daylight::Daylight::DAY,
1302 latitude: pfx_core::daylight::Daylight::LATITUDE,
1303 heading: pfx_core::daylight::Daylight::HEADING,
1304 }
1305 .sun()
1306 .y_up
1307 .map(|value| value as f32);
1308 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1309 canopy.update(
1310 &gpu.queue,
1311 &mut encoder,
1312 CanopyFrame {
1313 origin,
1314 time: 0.0,
1315 strength: 0.0,
1316 direction: [1.0, 0.0, 0.0],
1317 sun,
1318 receiver_y: 0.0,
1319 angular_radius: 0.00465,
1320 },
1321 None,
1322 );
1323 let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1324 label: Some("low sun cookie readback"),
1325 size: 256 * 256,
1326 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1327 mapped_at_creation: false,
1328 });
1329 encoder.copy_texture_to_buffer(
1330 canopy.texture.as_image_copy(),
1331 wgpu::TexelCopyBufferInfo {
1332 buffer: &readback,
1333 layout: wgpu::TexelCopyBufferLayout {
1334 offset: 0,
1335 bytes_per_row: Some(256),
1336 rows_per_image: Some(256),
1337 },
1338 },
1339 wgpu::Extent3d {
1340 width: 256,
1341 height: 256,
1342 depth_or_array_layers: 1,
1343 },
1344 );
1345 gpu.queue.submit(Some(encoder.finish()));
1346 let (send, receive) = std::sync::mpsc::channel();
1347 readback
1348 .slice(..)
1349 .map_async(wgpu::MapMode::Read, move |result| {
1350 send.send(result).unwrap();
1351 });
1352 gpu.device
1353 .poll(wgpu::PollType::wait_indefinitely())
1354 .unwrap();
1355 receive.recv().unwrap().unwrap();
1356 let mapped = readback
1357 .slice(..)
1358 .get_mapped_range()
1359 .expect("a mapped range");
1360 let mut sum = [0.0_f32; 2];
1361 let mut weight = 0.0_f32;
1362 for (index, alpha) in mapped.iter().enumerate() {
1363 let x = index % 256;
1364 let y = index / 256;
1365 let uv = [(x as f32 + 0.5) / 256.0, (y as f32 + 0.5) / 256.0];
1366 let world = [
1367 canopy.center[0] + (uv[0] - 0.5) * canopy.extent[0],
1368 canopy.center[1] + (uv[1] - 0.5) * canopy.extent[1],
1369 ];
1370 let alpha = f32::from(*alpha) / 255.0;
1371 sum[0] += world[0] * alpha;
1372 sum[1] += world[1] * alpha;
1373 weight += alpha;
1374 }
1375 assert!(weight > 1.0);
1376 let centroid = [sum[0] / weight, sum[1] / weight];
1377 let [u, v] = projection_axes(sun);
1378 let crown = [origin[0], origin[1] + tree.spec.height * 0.8, origin[2]];
1379 let expected = [dot3(crown, u), dot3(crown, v)];
1380 assert!((centroid[0] - expected[0]).abs() < canopy.extent[0] * 0.2);
1381 assert!((centroid[1] - expected[1]).abs() < canopy.extent[1] * 0.2);
1382 drop(mapped);
1383 readback.unmap();
1384 }
1385 }
1386
1387 #[repr(C)]
1388 #[derive(Clone, Copy, Pod, Zeroable)]
1389 struct WoodVertex {
1390 position: [f32; 3],
1391 pivot: [f32; 3],
1392 level: f32,
1393 stiffness: f32,
1394 }
1395
1396 const WOOD_WGSL: &str = r#"
1397struct CanopyParams {
1398 center_extent: vec4f,
1399 sun_receiver: vec4f,
1400 wind: vec4f,
1401 direction: vec4f,
1402 projected_extent: vec4f,
1403}
1404@group(0) @binding(0) var<uniform> params: CanopyParams;
1405fn tree_sway(rest: vec3f, pivot: vec3f, level: f32, stiffness: f32, wind_time: f32, strength: f32, direction: vec3f) -> vec3f {
1406 let d = normalize(direction.xz + vec2f(0.00001, 0.0));
1407 let arm = length(rest - pivot);
1408 let phase = wind_time * (0.7 + level * 0.41) + pivot.x * 0.83 + pivot.z * 0.67;
1409 let bend = sin(phase) * strength * (1.0 - stiffness) * arm * (0.012 + 0.009 * level);
1410 return rest + vec3f(d.x * bend, -abs(bend) * 0.12, d.y * bend);
1411}
1412struct Wood { @location(0) position: vec3f, @location(1) pivot: vec3f, @location(2) level: f32, @location(3) stiffness: f32, }
1413@vertex fn vertex(input: Wood) -> @builtin(position) vec4f {
1414 let p = tree_sway(input.position, input.pivot, input.level, input.stiffness, params.wind.x, params.wind.y, params.direction.xyz);
1415 return vec4f(p.x / 8.0, (p.y - p.z) / 14.0, 0.0, 1.0);
1416}
1417@fragment fn fragment() -> @location(0) vec4f { return vec4f(0.29, 0.16, 0.11, 1.0); }
1418"#;
1419
1420 const GROUND_WGSL: &str = r#"
1421@vertex fn vertex(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
1422 let p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
1423 return vec4f(p[i], 0.0, 1.0);
1424}
1425@fragment fn fragment(@builtin(position) pos: vec4f) -> @location(0) vec4f {
1426 let uv = pos.xy / vec2f(3840.0, 2160.0);
1427 let world = vec3f((uv.x * 2.0 - 1.0) * 8.0, 0.0, (uv.y * 2.0 - 1.0) * 14.0);
1428 let light = canopy_light(world);
1429 return vec4f(vec3f(0.65, 0.74, 0.52) * (0.3 + 0.7 * light), 1.0);
1430}
1431"#;
1432
1433 #[test]
1434 fn preview_shaders_validate() {
1435 for source in [
1436 WOOD_WGSL.to_string(),
1437 format!("{}\n{GROUND_WGSL}", canopy_light_wgsl(&Gobo::default())),
1438 ] {
1439 let module = naga::front::wgsl::parse_str(&source).unwrap();
1440 naga::valid::Validator::new(
1441 naga::valid::ValidationFlags::all(),
1442 naga::valid::Capabilities::all(),
1443 )
1444 .validate(&module)
1445 .unwrap();
1446 }
1447 }
1448
1449 fn pipeline(
1450 device: &wgpu::Device,
1451 shader: &str,
1452 layout: &wgpu::PipelineLayout,
1453 buffers: &[Option<wgpu::VertexBufferLayout<'_>>],
1454 alpha: bool,
1455 ) -> wgpu::RenderPipeline {
1456 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1457 label: Some("tree test shader"),
1458 source: wgpu::ShaderSource::Wgsl(shader.into()),
1459 });
1460 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1461 label: Some("tree test pipeline"),
1462 layout: Some(layout),
1463 vertex: wgpu::VertexState {
1464 module: &module,
1465 entry_point: Some("vertex"),
1466 compilation_options: Default::default(),
1467 buffers,
1468 },
1469 fragment: Some(wgpu::FragmentState {
1470 module: &module,
1471 entry_point: Some("fragment"),
1472 compilation_options: Default::default(),
1473 targets: &[Some(wgpu::ColorTargetState {
1474 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1475 blend: alpha.then_some(wgpu::BlendState::ALPHA_BLENDING),
1476 write_mask: wgpu::ColorWrites::ALL,
1477 })],
1478 }),
1479 primitive: wgpu::PrimitiveState {
1480 cull_mode: None,
1481 ..Default::default()
1482 },
1483 depth_stencil: None,
1484 multisample: Default::default(),
1485 multiview_mask: None,
1486 cache: None,
1487 })
1488 }
1489
1490 #[test]
1491 #[ignore = "needs a GPU; run under pgpu"]
1492 fn cherry_over_ground_at_two_wind_times() {
1493 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1494 let tree = Tree::new(49, TreeSpec::cherry(), 0.004);
1495 let mut canopy = Canopy::new(&gpu.device, &gpu.queue, &tree, DEFAULT_RESOLUTION);
1496 let vertices: Vec<WoodVertex> = tree
1497 .wood
1498 .positions
1499 .iter()
1500 .zip(&tree.sway)
1501 .map(|(position, sway)| WoodVertex {
1502 position: *position,
1503 pivot: sway.pivot,
1504 level: sway.level,
1505 stiffness: sway.stiffness,
1506 })
1507 .collect();
1508 let vertex_buffer = gpu
1509 .device
1510 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1511 label: Some("cherry wood vertices"),
1512 contents: bytemuck::cast_slice(&vertices),
1513 usage: wgpu::BufferUsages::VERTEX,
1514 });
1515 let index_buffer = gpu
1516 .device
1517 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1518 label: Some("cherry wood indices"),
1519 contents: bytemuck::cast_slice(&tree.wood.indices),
1520 usage: wgpu::BufferUsages::INDEX,
1521 });
1522 let empty = gpu
1523 .device
1524 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1525 label: Some("unused ground group"),
1526 entries: &[],
1527 });
1528 let empty_bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1529 label: Some("unused ground bind"),
1530 layout: &empty,
1531 entries: &[],
1532 });
1533 let ground_layout = gpu
1534 .device
1535 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1536 label: Some("ground layout"),
1537 bind_group_layouts: &[
1538 Some(&empty),
1539 Some(&empty),
1540 Some(&empty),
1541 Some(canopy.sample_layout()),
1542 ],
1543 immediate_size: 0,
1544 });
1545 let wood_bind_layout =
1546 gpu.device
1547 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1548 label: Some("wood wind layout"),
1549 entries: &[wgpu::BindGroupLayoutEntry {
1550 binding: 0,
1551 visibility: wgpu::ShaderStages::VERTEX,
1552 ty: wgpu::BindingType::Buffer {
1553 ty: wgpu::BufferBindingType::Uniform,
1554 has_dynamic_offset: false,
1555 min_binding_size: None,
1556 },
1557 count: None,
1558 }],
1559 });
1560 let wood_bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1561 label: Some("wood wind bind"),
1562 layout: &wood_bind_layout,
1563 entries: &[wgpu::BindGroupEntry {
1564 binding: 0,
1565 resource: canopy.params.as_entire_binding(),
1566 }],
1567 });
1568 let wood_layout = gpu
1569 .device
1570 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1571 label: Some("wood layout"),
1572 bind_group_layouts: &[Some(&wood_bind_layout)],
1573 immediate_size: 0,
1574 });
1575 let camera_layout = gpu
1576 .device
1577 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1578 label: Some("cherry camera layout"),
1579 entries: &[wgpu::BindGroupLayoutEntry {
1580 binding: 0,
1581 visibility: wgpu::ShaderStages::VERTEX,
1582 ty: wgpu::BindingType::Buffer {
1583 ty: wgpu::BufferBindingType::Uniform,
1584 has_dynamic_offset: false,
1585 min_binding_size: None,
1586 },
1587 count: None,
1588 }],
1589 });
1590 let camera: [f32; 16] = [
1591 1.0 / 8.0,
1592 0.0,
1593 0.0,
1594 0.0,
1595 0.0,
1596 1.0 / 14.0,
1597 0.0,
1598 0.0,
1599 0.0,
1600 -1.0 / 14.0,
1601 0.0,
1602 0.0,
1603 0.0,
1604 0.0,
1605 0.0,
1606 1.0,
1607 ];
1608 let camera_buffer = gpu
1609 .device
1610 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1611 label: Some("cherry camera"),
1612 contents: bytemuck::cast_slice(&camera),
1613 usage: wgpu::BufferUsages::UNIFORM,
1614 });
1615 let camera_bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1616 label: Some("cherry camera bind"),
1617 layout: &camera_layout,
1618 entries: &[wgpu::BindGroupEntry {
1619 binding: 0,
1620 resource: camera_buffer.as_entire_binding(),
1621 }],
1622 });
1623 let visible_layout = gpu
1624 .device
1625 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1626 label: Some("cherry cards layout"),
1627 bind_group_layouts: &[Some(canopy.cookie_layout()), Some(&camera_layout)],
1628 immediate_size: 0,
1629 });
1630 let ground = pipeline(
1631 &gpu.device,
1632 &format!("{}\n{GROUND_WGSL}", canopy_light_wgsl(&Gobo::default())),
1633 &ground_layout,
1634 &[],
1635 false,
1636 );
1637 let wood = pipeline(
1638 &gpu.device,
1639 WOOD_WGSL,
1640 &wood_layout,
1641 &[Some(wgpu::VertexBufferLayout {
1642 array_stride: 32,
1643 step_mode: wgpu::VertexStepMode::Vertex,
1644 attributes: &[
1645 wgpu::VertexAttribute {
1646 format: wgpu::VertexFormat::Float32x3,
1647 offset: 0,
1648 shader_location: 0,
1649 },
1650 wgpu::VertexAttribute {
1651 format: wgpu::VertexFormat::Float32x3,
1652 offset: 12,
1653 shader_location: 1,
1654 },
1655 wgpu::VertexAttribute {
1656 format: wgpu::VertexFormat::Float32,
1657 offset: 24,
1658 shader_location: 2,
1659 },
1660 wgpu::VertexAttribute {
1661 format: wgpu::VertexFormat::Float32,
1662 offset: 28,
1663 shader_location: 3,
1664 },
1665 ],
1666 })],
1667 false,
1668 );
1669 let visible = pipeline(
1670 &gpu.device,
1671 &canopy_visible_wgsl(&CardColors::default()),
1672 &visible_layout,
1673 &[],
1674 true,
1675 );
1676 let ground_bind = canopy.sample_bind(&gpu.device);
1677 let target = gpu
1678 .offscreen(3840, 2160, wgpu::TextureFormat::Rgba8UnormSrgb)
1679 .unwrap();
1680 let mut images = Vec::new();
1681 for (index, time) in [0.0_f32, 2.3].into_iter().enumerate() {
1682 let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
1683 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1684 let cookie_time = profiler.pass("canopy cookie");
1685 let wood_time = profiler.pass("cherry draw 4K");
1686 canopy.update(
1687 &gpu.queue,
1688 &mut encoder,
1689 CanopyFrame {
1690 origin: [0.0; 3],
1691 time,
1692 strength: 2.0,
1693 direction: [1.0, 0.0, 0.35],
1694 sun: [0.25, 0.95, 0.15],
1695 receiver_y: 0.0,
1696 angular_radius: 0.00465,
1697 },
1698 profiler.render_writes(cookie_time),
1699 );
1700 {
1701 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1702 multiview_mask: None,
1703 label: Some("ground and cherry"),
1704 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1705 view: &target.view,
1706 resolve_target: None,
1707 ops: wgpu::Operations {
1708 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1709 store: wgpu::StoreOp::Store,
1710 },
1711 depth_slice: None,
1712 })],
1713 depth_stencil_attachment: None,
1714 timestamp_writes: None,
1715 occlusion_query_set: None,
1716 });
1717 pass.set_pipeline(&ground);
1718 pass.set_bind_group(0, &empty_bind, &[]);
1719 pass.set_bind_group(1, &empty_bind, &[]);
1720 pass.set_bind_group(2, &empty_bind, &[]);
1721 pass.set_bind_group(3, &ground_bind, &[]);
1722 pass.draw(0..3, 0..1);
1723 }
1724 {
1725 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1726 multiview_mask: None,
1727 label: Some("cherry draw"),
1728 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1729 view: &target.view,
1730 resolve_target: None,
1731 ops: wgpu::Operations {
1732 load: wgpu::LoadOp::Load,
1733 store: wgpu::StoreOp::Store,
1734 },
1735 depth_slice: None,
1736 })],
1737 depth_stencil_attachment: None,
1738 timestamp_writes: profiler.render_writes(wood_time),
1739 occlusion_query_set: None,
1740 });
1741 pass.set_pipeline(&wood);
1742 pass.set_bind_group(0, &wood_bind, &[]);
1743 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1744 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1745 pass.draw_indexed(0..tree.wood.indices.len() as u32, 0, 0..1);
1746 pass.set_pipeline(&visible);
1747 pass.set_bind_group(0, canopy.cookie_bind(), &[]);
1748 pass.set_bind_group(1, &camera_bind, &[]);
1749 pass.draw(0..6, 0..canopy.card_count);
1750 }
1751 let slot = profiler.finish(&mut encoder);
1752 gpu.queue.submit(Some(encoder.finish()));
1753 if let Some(slot) = slot {
1754 profiler.submitted(slot);
1755 gpu.device
1756 .poll(wgpu::PollType::wait_indefinitely())
1757 .unwrap();
1758 for t in profiler.collect(&gpu.device).into_iter().flatten() {
1759 eprintln!("{} at {}: {:.3} ms", t.label, time, t.milliseconds);
1760 }
1761 }
1762 let pixels = gpu.readback_rgba8(&target).unwrap();
1763 let output = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp");
1764 std::fs::create_dir_all(&output).unwrap();
1765 let file =
1766 std::fs::File::create(output.join(format!("trees-wind-{index}.png"))).unwrap();
1767 let mut png = png::Encoder::new(file, 3840, 2160);
1768 png.set_color(png::ColorType::Rgba);
1769 png.set_depth(png::BitDepth::Eight);
1770 png.write_header()
1771 .unwrap()
1772 .write_image_data(&pixels)
1773 .unwrap();
1774 images.push(pixels);
1775 }
1776 let mut single = tree.clone();
1777 let card = tree
1778 .cards
1779 .iter()
1780 .max_by(|a, b| {
1781 let projected = |c: &pfx_geom::tree::Card| {
1782 (c.right[0] * c.up[2] - c.right[2] * c.up[0]).abs() * c.size[0] * c.size[1]
1783 };
1784 projected(a).total_cmp(&projected(b))
1785 })
1786 .copied()
1787 .unwrap();
1788 single.cards = vec![card];
1789 let mut one = Canopy::new(&gpu.device, &gpu.queue, &single, DEFAULT_RESOLUTION);
1790 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1791 one.update(
1792 &gpu.queue,
1793 &mut encoder,
1794 CanopyFrame {
1795 origin: [0.0; 3],
1796 time: 0.0,
1797 strength: 0.0,
1798 direction: [1.0, 0.0, 0.0],
1799 sun: [0.0, 1.0, 0.0],
1800 receiver_y: 0.0,
1801 angular_radius: 0.00465,
1802 },
1803 None,
1804 );
1805 let row = DEFAULT_RESOLUTION;
1806 let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1807 label: Some("one card cookie readback"),
1808 size: u64::from(row) * u64::from(row),
1809 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1810 mapped_at_creation: false,
1811 });
1812 encoder.copy_texture_to_buffer(
1813 one.texture.as_image_copy(),
1814 wgpu::TexelCopyBufferInfo {
1815 buffer: &buffer,
1816 layout: wgpu::TexelCopyBufferLayout {
1817 offset: 0,
1818 bytes_per_row: Some(row),
1819 rows_per_image: Some(row),
1820 },
1821 },
1822 wgpu::Extent3d {
1823 width: row,
1824 height: row,
1825 depth_or_array_layers: 1,
1826 },
1827 );
1828 gpu.queue.submit(Some(encoder.finish()));
1829 let (send, recv) = std::sync::mpsc::channel();
1830 buffer
1831 .slice(..)
1832 .map_async(wgpu::MapMode::Read, move |result| {
1833 send.send(result).unwrap();
1834 });
1835 gpu.device
1836 .poll(wgpu::PollType::wait_indefinitely())
1837 .unwrap();
1838 recv.recv().unwrap().unwrap();
1839 let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
1840 let covered =
1841 mapped.iter().map(|v| *v as f32 / 255.0).sum::<f32>() * one.extent[0] * one.extent[1]
1842 / (row * row) as f32;
1843 let expected = projected_coverage(&single, 0.0, 0.0, [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.0);
1844 assert!(
1845 (covered - expected).abs() < expected * 0.35,
1846 "cookie area {covered} versus projected {expected}"
1847 );
1848 drop(mapped);
1849 buffer.unmap();
1850 let sway_shader = format!(
1851 "{}\n@group(0) @binding(0) var<storage, read_write> result: array<vec4f, 1>; @compute @workgroup_size(1) fn main() {{ result[0] = vec4f(tree_sway(vec3f(1.0, 6.0, 1.0), vec3f(0.0, 5.0, 0.0), 2.0, 0.18, 1.0, 2.0, vec3f(1.0, 0.0, 0.3)), 1.0); }}",
1852 pfx_geom::tree::TREE_SWAY_WGSL
1853 );
1854 let module = gpu
1855 .device
1856 .create_shader_module(wgpu::ShaderModuleDescriptor {
1857 label: Some("tree sway reference"),
1858 source: wgpu::ShaderSource::Wgsl(sway_shader.into()),
1859 });
1860 let layout = gpu
1861 .device
1862 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1863 label: Some("tree sway reference layout"),
1864 entries: &[wgpu::BindGroupLayoutEntry {
1865 binding: 0,
1866 visibility: wgpu::ShaderStages::COMPUTE,
1867 ty: wgpu::BindingType::Buffer {
1868 ty: wgpu::BufferBindingType::Storage { read_only: false },
1869 has_dynamic_offset: false,
1870 min_binding_size: None,
1871 },
1872 count: None,
1873 }],
1874 });
1875 let pipeline_layout = gpu
1876 .device
1877 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1878 label: Some("tree sway reference pipeline layout"),
1879 bind_group_layouts: &[Some(&layout)],
1880 immediate_size: 0,
1881 });
1882 let pipeline = gpu
1883 .device
1884 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1885 label: Some("tree sway reference pipeline"),
1886 layout: Some(&pipeline_layout),
1887 module: &module,
1888 entry_point: Some("main"),
1889 compilation_options: Default::default(),
1890 cache: None,
1891 });
1892 let output = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1893 label: Some("tree sway result"),
1894 size: 16,
1895 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
1896 mapped_at_creation: false,
1897 });
1898 let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1899 label: Some("tree sway readback"),
1900 size: 16,
1901 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1902 mapped_at_creation: false,
1903 });
1904 let bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1905 label: Some("tree sway reference bind"),
1906 layout: &layout,
1907 entries: &[wgpu::BindGroupEntry {
1908 binding: 0,
1909 resource: output.as_entire_binding(),
1910 }],
1911 });
1912 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1913 {
1914 let mut pass = encoder.begin_compute_pass(&Default::default());
1915 pass.set_pipeline(&pipeline);
1916 pass.set_bind_group(0, &bind, &[]);
1917 pass.dispatch_workgroups(1, 1, 1);
1918 }
1919 encoder.copy_buffer_to_buffer(&output, 0, &readback, 0, 16);
1920 gpu.queue.submit(Some(encoder.finish()));
1921 let (send, recv) = std::sync::mpsc::channel();
1922 readback
1923 .slice(..)
1924 .map_async(wgpu::MapMode::Read, move |result| {
1925 send.send(result).unwrap();
1926 });
1927 gpu.device
1928 .poll(wgpu::PollType::wait_indefinitely())
1929 .unwrap();
1930 recv.recv().unwrap().unwrap();
1931 let mapped = readback
1932 .slice(..)
1933 .get_mapped_range()
1934 .expect("a mapped range");
1935 let expected = tree_sway(
1936 [1.0, 6.0, 1.0],
1937 [0.0, 5.0, 0.0],
1938 2.0,
1939 0.18,
1940 1.0,
1941 2.0,
1942 [1.0, 0.0, 0.3],
1943 );
1944 for (i, value) in expected.into_iter().enumerate() {
1945 let actual = f32::from_le_bytes(mapped[i * 4..i * 4 + 4].try_into().unwrap());
1946 assert!(
1947 (actual - value).abs() < 1e-4,
1948 "sway component {i}: {actual} versus {value}"
1949 );
1950 }
1951 drop(mapped);
1952 readback.unmap();
1953 let blossoms = images[0]
1954 .chunks_exact(4)
1955 .filter(|pixel| pixel[0] > pixel[1].saturating_add(10) && pixel[2] > pixel[1])
1956 .count();
1957 assert!(blossoms > 1000, "only {blossoms} blossom pixels");
1958 let changed = images[0]
1959 .chunks_exact(4)
1960 .zip(images[1].chunks_exact(4))
1961 .filter(|(a, b)| a != b)
1962 .count();
1963 assert!(changed > 1000, "only {changed} pixels changed");
1964 }
1965}