1use bytemuck::{Pod, Zeroable};
2use pfx_bake::impostor::{PropArtifact, PropManifest, read_prop_artifact};
3use pfx_gpu::Gpu;
4use std::path::Path;
5
6pub const IMPOSTOR_WGSL: &str = r#"
7struct Params {
8 view_projection: mat4x4f,
9 camera_position: vec4f,
10 camera_right: vec4f,
11 camera_up: vec4f,
12 settings: vec4f,
13 source: vec4f,
14}
15struct Instance {
16 center_radius: vec4f,
17 options: vec4u,
18}
19struct VertexOut {
20 @builtin(position) clip: vec4f,
21 @location(0) local: vec2f,
22 @location(1) @interpolate(flat) instance_id: u32,
23}
24struct FragmentOut {
25 @location(0) color: vec4f,
26 @builtin(frag_depth) depth: f32,
27}
28struct Shade {
29 color: vec4f,
30 depth: f32,
31}
32struct Sample {
33 color: vec4f,
34 position: vec3f,
35 weight: f32,
36}
37@group(0) @binding(0) var<uniform> params: Params;
38@group(0) @binding(1) var<storage, read> instances: array<Instance>;
39@group(0) @binding(2) var radiance: texture_2d_array<f32>;
40@group(0) @binding(3) var geometry: texture_2d<f32>;
41@group(0) @binding(4) var linear_sampler: sampler;
42fn oct_decode(p: vec2f) -> vec3f {
43 let y = 1.0 - abs(p.x) - abs(p.y);
44 if y < 0.0 {
45 return normalize(vec3f((1.0 - abs(p.y)) * select(-1.0, 1.0, p.x >= 0.0), y, (1.0 - abs(p.x)) * select(-1.0, 1.0, p.y >= 0.0)));
46 }
47 return normalize(vec3f(p.x, y, p.y));
48}
49fn oct_encode(d: vec3f) -> vec2f {
50 let p = d.xz / max(abs(d.x) + abs(d.y) + abs(d.z), 1e-20);
51 if d.y < 0.0 {
52 return vec2f((1.0 - abs(p.y)) * select(-1.0, 1.0, p.x >= 0.0), (1.0 - abs(p.x)) * select(-1.0, 1.0, p.y >= 0.0));
53 }
54 return p;
55}
56fn frame_basis(direction: vec3f) -> mat3x3f {
57 let forward = -direction;
58 var nominal_up = vec3f(0.0, 1.0, 0.0);
59 if abs(direction.y) > 0.95 { nominal_up = vec3f(0.0, 0.0, 1.0); }
60 let right = normalize(cross(forward, nominal_up));
61 let up = normalize(cross(right, forward));
62 return mat3x3f(right, up, forward);
63}
64fn hash(pixel: vec2u, object_id: u32) -> f32 {
65 var value = pixel.x * 1664525u + pixel.y * 1013904223u + object_id * 747796405u;
66 value = (value ^ (value >> 16u)) * 2246822519u;
67 value = (value ^ (value >> 13u)) * 3266489917u;
68 return f32(value ^ (value >> 16u)) * (1.0 / 4294967296.0);
69}
70fn geometry_sample(uv: vec2f) -> vec4f {
71 let size = i32(params.settings.x * params.settings.y);
72 return textureLoad(geometry, clamp(vec2i(uv * f32(size)), vec2i(0), vec2i(size - 1)), 0);
73}
74fn frame_sample(cell: vec2i, local: vec2f, inst: Instance, low: i32, high: i32, anchor_mix: f32) -> Sample {
75 let n = i32(params.settings.x);
76 let bounded = clamp(cell, vec2i(0), vec2i(n - 1));
77 let encoded = (vec2f(bounded) + vec2f(0.5)) * (2.0 / f32(n)) - vec2f(1.0);
78 var direction = oct_decode(encoded);
79 if params.settings.w > 0.5 { direction.y = abs(direction.y); }
80 let basis = frame_basis(direction);
81 let radius = inst.center_radius.w;
82 let depth_scale = radius / params.source.x;
83 let eye = inst.center_radius.xyz + direction * radius * 3.0;
84 let scale = 1.0 / 3.0;
85 var uv = vec2f(local.x * 0.5 + 0.5, 0.5 - local.y * 0.5);
86 let cell_size = 1.0 / f32(n);
87 var atlas_uv = (vec2f(bounded) + uv) * cell_size;
88 var feature = geometry_sample(atlas_uv);
89 for (var step = 0; step < 1; step++) {
90 if feature.w <= 0.0 || feature.w * depth_scale > radius * 6.0 { break; }
91 let ray = normalize(basis[2] + basis[0] * ((uv.x * 2.0 - 1.0) * scale) + basis[1] * ((1.0 - uv.y * 2.0) * scale));
92 let point = eye + ray * feature.w * depth_scale;
93 let card_point = inst.center_radius.xyz + params.camera_right.xyz * local.x * radius + params.camera_up.xyz * local.y * radius;
94 let shift = vec2f(dot(point - card_point, params.camera_right.xyz), -dot(point - card_point, params.camera_up.xyz)) / max(radius, 1e-5);
95 uv = clamp(uv - shift * 0.5, vec2f(0.5 / params.settings.y), vec2f(1.0 - 0.5 / params.settings.y));
96 atlas_uv = (vec2f(bounded) + uv) * cell_size;
97 feature = geometry_sample(atlas_uv);
98 }
99 let ray = normalize(basis[2] + basis[0] * ((uv.x * 2.0 - 1.0) * scale) + basis[1] * ((1.0 - uv.y * 2.0) * scale));
100 let position = eye + ray * feature.w * depth_scale;
101 let a = textureSampleLevel(radiance, linear_sampler, atlas_uv, low, 0.0);
102 var color = a;
103 if high != low { color = mix(a, textureSampleLevel(radiance, linear_sampler, atlas_uv, high, 0.0), anchor_mix); }
104 return Sample(color, position, feature.w * depth_scale);
105}
106@vertex fn vertex(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> VertexOut {
107 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));
108 let local = corners[vertex_id];
109 let inst = instances[instance_id];
110 let world = inst.center_radius.xyz + (params.camera_right.xyz * local.x + params.camera_up.xyz * local.y) * inst.center_radius.w;
111 var output: VertexOut;
112 output.clip = params.view_projection * vec4f(world, 1.0);
113 output.local = local;
114 output.instance_id = instance_id;
115 return output;
116}
117fn shade(local: vec2f, instance_id: u32, pixel: vec4f) -> Shade {
118 let inst = instances[instance_id];
119 var toward = params.camera_position.xyz - inst.center_radius.xyz;
120 if params.source.y > 0.5 { toward = params.camera_position.xyz; }
121 toward = normalize(toward);
122 var uv = oct_encode(toward);
123 if params.settings.w > 0.5 { uv = oct_encode(vec3f(toward.x, abs(toward.y), toward.z)); }
124 let grid = (uv * 0.5 + vec2f(0.5)) * params.settings.x - vec2f(0.5);
125 let base = vec2i(floor(grid));
126 let t = fract(grid);
127 var cells: array<vec2i, 3>;
128 var weights: vec3f;
129 if t.x + t.y <= 1.0 {
130 cells = array<vec2i, 3>(base, base + vec2i(1, 0), base + vec2i(0, 1));
131 weights = vec3f(1.0 - t.x - t.y, t.x, t.y);
132 } else {
133 cells = array<vec2i, 3>(base + vec2i(1, 1), base + vec2i(0, 1), base + vec2i(1, 0));
134 weights = vec3f(t.x + t.y - 1.0, 1.0 - t.x, 1.0 - t.y);
135 }
136 let low = i32(params.settings.z);
137 let high = min(low + 1, i32(textureNumLayers(radiance)) - 1);
138 let anchor_mix = fract(params.settings.z);
139 var color = vec4f(0.0);
140 var position = vec3f(0.0);
141 var weight = 0.0;
142 for (var i = 0; i < 3; i++) {
143 let sample = frame_sample(cells[i], local, inst, low, high, anchor_mix);
144 let amount = weights[i] * max(sample.color.a, 0.0);
145 color += sample.color * weights[i];
146 if sample.weight > 0.0 && sample.weight < inst.center_radius.w * 6.0 {
147 position += sample.position * amount;
148 weight += amount;
149 }
150 }
151 if color.a <= 0.5 || hash(vec2u(pixel.xy), inst.options.x) < bitcast<f32>(inst.options.y) || weight <= 1e-5 { discard; }
152 let clip = params.view_projection * vec4f(position / weight, 1.0);
153 var output: Shade;
154 output.color = vec4f(color.rgb / max(color.a, 1e-5), 1.0);
155 output.depth = clamp(clip.z / clip.w, 0.0, 1.0);
156 return output;
157}
158@fragment fn fragment(input: VertexOut) -> FragmentOut {
159 let value = shade(input.local, input.instance_id, input.clip);
160 return FragmentOut(value.color, value.depth);
161}
162@fragment fn shadow_fragment(input: VertexOut) -> @builtin(frag_depth) f32 {
163 return shade(input.local, input.instance_id, input.clip).depth;
164}
165"#;
166
167#[repr(C)]
168#[derive(Clone, Copy, Pod, Zeroable)]
169pub struct ImpostorInstance {
170 pub center_radius: [f32; 4],
171 pub meta: [u32; 4],
172}
173
174impl ImpostorInstance {
175 pub fn new(center: [f32; 3], radius: f32, id: u32, mesh_fraction: f32) -> Result<Self, String> {
176 if center.iter().any(|v| !v.is_finite())
177 || !radius.is_finite()
178 || radius <= 0.0
179 || !mesh_fraction.is_finite()
180 || !(0.0..=1.0).contains(&mesh_fraction)
181 {
182 return Err("invalid impostor instance".into());
183 }
184 Ok(Self {
185 center_radius: [center[0], center[1], center[2], radius],
186 meta: [id, mesh_fraction.to_bits(), 0, 0],
187 })
188 }
189}
190
191#[repr(C)]
192#[derive(Clone, Copy, Pod, Zeroable)]
193pub struct ImpostorParams {
194 pub view_projection: [[f32; 4]; 4],
195 pub camera_position: [f32; 4],
196 pub camera_right: [f32; 4],
197 pub camera_up: [f32; 4],
198 pub settings: [f32; 4],
199 pub source: [f32; 4],
200}
201
202pub type FrameWeights = ([(u32, u32); 3], [f32; 3]);
203
204pub fn frame_weights(direction: [f32; 3], frames_per_side: u32) -> Result<FrameWeights, String> {
205 if frames_per_side < 2
206 || direction.iter().any(|v| !v.is_finite())
207 || direction.iter().all(|v| *v == 0.0)
208 {
209 return Err("invalid frame lookup".into());
210 }
211 let sum = direction.iter().map(|v| v.abs()).sum::<f32>();
212 let p = [direction[0] / sum, direction[2] / sum];
213 let uv = if direction[1] < 0.0 {
214 [
215 (1.0 - p[1].abs()) * if p[0] < 0.0 { -1.0 } else { 1.0 },
216 (1.0 - p[0].abs()) * if p[1] < 0.0 { -1.0 } else { 1.0 },
217 ]
218 } else {
219 p
220 };
221 let n = frames_per_side as f32;
222 let grid = uv.map(|v| (v * 0.5 + 0.5) * n - 0.5);
223 let base = grid.map(f32::floor);
224 let t = [grid[0] - base[0], grid[1] - base[1]];
225 let (cells, weights) = if t[0] + t[1] <= 1.0 {
226 ([(0, 0), (1, 0), (0, 1)], [1.0 - t[0] - t[1], t[0], t[1]])
227 } else {
228 (
229 [(1, 1), (0, 1), (1, 0)],
230 [t[0] + t[1] - 1.0, 1.0 - t[0], 1.0 - t[1]],
231 )
232 };
233 let cell = cells.map(|(x, y)| {
234 (
235 (base[0] as i32 + x).clamp(0, frames_per_side as i32 - 1) as u32,
236 (base[1] as i32 + y).clamp(0, frames_per_side as i32 - 1) as u32,
237 )
238 });
239 Ok((cell, weights))
240}
241
242pub fn anchor_position(hours: &[f32], hour: f32) -> Result<f32, String> {
243 if hours.is_empty()
244 || !hour.is_finite()
245 || hours.iter().any(|v| !v.is_finite())
246 || hours.windows(2).any(|v| v[0] >= v[1])
247 {
248 return Err("invalid impostor anchors".into());
249 }
250 if hour <= hours[0] {
251 return Ok(0.0);
252 }
253 for i in 0..hours.len() - 1 {
254 if hour <= hours[i + 1] {
255 return Ok(i as f32 + (hour - hours[i]) / (hours[i + 1] - hours[i]));
256 }
257 }
258 Ok((hours.len() - 1) as f32)
259}
260
261pub fn mesh_fraction(
262 distance: f32,
263 switch_distance: f32,
264 transition_width: f32,
265) -> Result<f32, String> {
266 if !distance.is_finite()
267 || !switch_distance.is_finite()
268 || !transition_width.is_finite()
269 || switch_distance <= 0.0
270 || transition_width <= 0.0
271 {
272 return Err("invalid impostor transition".into());
273 }
274 let near = switch_distance - transition_width * 0.5;
275 let t = ((distance - near) / transition_width).clamp(0.0, 1.0);
276 Ok(1.0 - t * t * (3.0 - 2.0 * t))
277}
278
279pub struct ImpostorPass {
280 pub manifest: PropManifest,
281 pipeline: wgpu::RenderPipeline,
282 shadow_pipeline: wgpu::RenderPipeline,
283 group: wgpu::BindGroup,
284 uniform: wgpu::Buffer,
285 shadow_groups: [wgpu::BindGroup; crate::shadow::CASCADE_COUNT],
286 shadow_uniforms: [wgpu::Buffer; crate::shadow::CASCADE_COUNT],
287 instances: wgpu::Buffer,
288 capacity: usize,
289}
290
291impl ImpostorPass {
292 pub fn load(
293 gpu: &Gpu,
294 folder: &Path,
295 color_format: wgpu::TextureFormat,
296 depth_format: wgpu::TextureFormat,
297 capacity: usize,
298 ) -> Result<Self, String> {
299 let artifact = read_prop_artifact(folder)?;
300 Self::new(gpu, artifact, color_format, depth_format, capacity)
301 }
302
303 pub fn new(
304 gpu: &Gpu,
305 artifact: PropArtifact,
306 color_format: wgpu::TextureFormat,
307 depth_format: wgpu::TextureFormat,
308 capacity: usize,
309 ) -> Result<Self, String> {
310 if capacity == 0 || artifact.radiance.is_empty() {
311 return Err("impostor pass needs capacity and anchors".into());
312 }
313 let size = artifact.manifest.atlas_size;
314 let layers = artifact.radiance.len() as u32;
315 let pixels = size as usize * size as usize;
316 if size < 16
317 || artifact.manifest.frames_per_side < 2
318 || !artifact.manifest.bounds.radius.is_finite()
319 || artifact.manifest.bounds.radius <= 0.0
320 || !size.is_multiple_of(artifact.manifest.frames_per_side)
321 || size / artifact.manifest.frames_per_side < 8
322 || size > gpu.device.limits().max_texture_dimension_2d
323 || layers > gpu.device.limits().max_texture_array_layers
324 || artifact.radiance.len() != artifact.manifest.anchors.len()
325 || artifact.geometry.len() != pixels * 16
326 || artifact
327 .radiance
328 .iter()
329 .any(|bytes| bytes.len() != pixels * 8)
330 || capacity
331 .checked_mul(std::mem::size_of::<ImpostorInstance>())
332 .is_none()
333 {
334 return Err("impostor artifact exceeds the device or has invalid data".into());
335 }
336 let color = gpu.device.create_texture(&wgpu::TextureDescriptor {
337 label: Some("impostor radiance"),
338 size: wgpu::Extent3d {
339 width: size,
340 height: size,
341 depth_or_array_layers: layers,
342 },
343 mip_level_count: 1,
344 sample_count: 1,
345 dimension: wgpu::TextureDimension::D2,
346 format: wgpu::TextureFormat::Rgba16Float,
347 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
348 view_formats: &[],
349 });
350 for (i, bytes) in artifact.radiance.iter().enumerate() {
351 gpu.queue.write_texture(
352 wgpu::TexelCopyTextureInfo {
353 texture: &color,
354 mip_level: 0,
355 origin: wgpu::Origin3d {
356 x: 0,
357 y: 0,
358 z: i as u32,
359 },
360 aspect: wgpu::TextureAspect::All,
361 },
362 bytes,
363 wgpu::TexelCopyBufferLayout {
364 offset: 0,
365 bytes_per_row: Some(size * 8),
366 rows_per_image: Some(size),
367 },
368 wgpu::Extent3d {
369 width: size,
370 height: size,
371 depth_or_array_layers: 1,
372 },
373 );
374 }
375 let geometry = gpu.device.create_texture(&wgpu::TextureDescriptor {
376 label: Some("impostor geometry"),
377 size: wgpu::Extent3d {
378 width: size,
379 height: size,
380 depth_or_array_layers: 1,
381 },
382 mip_level_count: 1,
383 sample_count: 1,
384 dimension: wgpu::TextureDimension::D2,
385 format: wgpu::TextureFormat::Rgba32Float,
386 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
387 view_formats: &[],
388 });
389 gpu.queue.write_texture(
390 geometry.as_image_copy(),
391 &artifact.geometry,
392 wgpu::TexelCopyBufferLayout {
393 offset: 0,
394 bytes_per_row: Some(size * 16),
395 rows_per_image: Some(size),
396 },
397 geometry.size(),
398 );
399 let color_view = color.create_view(&wgpu::TextureViewDescriptor {
400 dimension: Some(wgpu::TextureViewDimension::D2Array),
401 ..Default::default()
402 });
403 let geometry_view = geometry.create_view(&Default::default());
404 let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
405 mag_filter: wgpu::FilterMode::Linear,
406 min_filter: wgpu::FilterMode::Linear,
407 ..Default::default()
408 });
409 let uniform = gpu.device.create_buffer(&wgpu::BufferDescriptor {
410 label: Some("impostor params"),
411 size: std::mem::size_of::<ImpostorParams>() as u64,
412 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
413 mapped_at_creation: false,
414 });
415 let instances = gpu.device.create_buffer(&wgpu::BufferDescriptor {
416 label: Some("impostor instances"),
417 size: (capacity * std::mem::size_of::<ImpostorInstance>()) as u64,
418 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
419 mapped_at_creation: false,
420 });
421 let layout = gpu
422 .device
423 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
424 label: Some("impostor layout"),
425 entries: &[
426 wgpu::BindGroupLayoutEntry {
427 binding: 0,
428 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
429 ty: wgpu::BindingType::Buffer {
430 ty: wgpu::BufferBindingType::Uniform,
431 has_dynamic_offset: false,
432 min_binding_size: None,
433 },
434 count: None,
435 },
436 wgpu::BindGroupLayoutEntry {
437 binding: 1,
438 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
439 ty: wgpu::BindingType::Buffer {
440 ty: wgpu::BufferBindingType::Storage { read_only: true },
441 has_dynamic_offset: false,
442 min_binding_size: None,
443 },
444 count: None,
445 },
446 wgpu::BindGroupLayoutEntry {
447 binding: 2,
448 visibility: wgpu::ShaderStages::FRAGMENT,
449 ty: wgpu::BindingType::Texture {
450 sample_type: wgpu::TextureSampleType::Float { filterable: true },
451 view_dimension: wgpu::TextureViewDimension::D2Array,
452 multisampled: false,
453 },
454 count: None,
455 },
456 wgpu::BindGroupLayoutEntry {
457 binding: 3,
458 visibility: wgpu::ShaderStages::FRAGMENT,
459 ty: wgpu::BindingType::Texture {
460 sample_type: wgpu::TextureSampleType::Float { filterable: false },
461 view_dimension: wgpu::TextureViewDimension::D2,
462 multisampled: false,
463 },
464 count: None,
465 },
466 wgpu::BindGroupLayoutEntry {
467 binding: 4,
468 visibility: wgpu::ShaderStages::FRAGMENT,
469 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
470 count: None,
471 },
472 ],
473 });
474 let make_group = |params: &wgpu::Buffer| {
475 gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
476 label: Some("impostor group"),
477 layout: &layout,
478 entries: &[
479 wgpu::BindGroupEntry {
480 binding: 0,
481 resource: params.as_entire_binding(),
482 },
483 wgpu::BindGroupEntry {
484 binding: 1,
485 resource: instances.as_entire_binding(),
486 },
487 wgpu::BindGroupEntry {
488 binding: 2,
489 resource: wgpu::BindingResource::TextureView(&color_view),
490 },
491 wgpu::BindGroupEntry {
492 binding: 3,
493 resource: wgpu::BindingResource::TextureView(&geometry_view),
494 },
495 wgpu::BindGroupEntry {
496 binding: 4,
497 resource: wgpu::BindingResource::Sampler(&sampler),
498 },
499 ],
500 })
501 };
502 let group = make_group(&uniform);
503 let shadow_uniforms = std::array::from_fn(|_| {
504 gpu.device.create_buffer(&wgpu::BufferDescriptor {
505 label: Some("impostor cascade params"),
506 size: std::mem::size_of::<ImpostorParams>() as u64,
507 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
508 mapped_at_creation: false,
509 })
510 });
511 let shadow_groups = std::array::from_fn(|index| make_group(&shadow_uniforms[index]));
512 let shader = gpu
513 .device
514 .create_shader_module(wgpu::ShaderModuleDescriptor {
515 label: Some("impostor shader"),
516 source: wgpu::ShaderSource::Wgsl(IMPOSTOR_WGSL.into()),
517 });
518 let pipeline_layout = gpu
519 .device
520 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
521 label: Some("impostor pipeline layout"),
522 bind_group_layouts: &[Some(&layout)],
523 immediate_size: 0,
524 });
525 let pipeline = gpu
526 .device
527 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
528 label: Some("impostor pipeline"),
529 layout: Some(&pipeline_layout),
530 vertex: wgpu::VertexState {
531 module: &shader,
532 entry_point: Some("vertex"),
533 buffers: &[],
534 compilation_options: Default::default(),
535 },
536 primitive: wgpu::PrimitiveState {
537 cull_mode: None,
538 ..Default::default()
539 },
540 depth_stencil: Some(wgpu::DepthStencilState {
541 format: depth_format,
542 depth_write_enabled: Some(true),
543 depth_compare: Some(wgpu::CompareFunction::LessEqual),
544 stencil: Default::default(),
545 bias: Default::default(),
546 }),
547 multisample: Default::default(),
548 fragment: Some(wgpu::FragmentState {
549 module: &shader,
550 entry_point: Some("fragment"),
551 targets: &[Some(wgpu::ColorTargetState {
552 format: color_format,
553 blend: None,
554 write_mask: wgpu::ColorWrites::ALL,
555 })],
556 compilation_options: Default::default(),
557 }),
558 multiview_mask: None,
559 cache: None,
560 });
561 let shadow_pipeline = gpu
562 .device
563 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
564 label: Some("impostor shadow pipeline"),
565 layout: Some(&pipeline_layout),
566 vertex: wgpu::VertexState {
567 module: &shader,
568 entry_point: Some("vertex"),
569 buffers: &[],
570 compilation_options: Default::default(),
571 },
572 primitive: wgpu::PrimitiveState {
573 cull_mode: None,
574 ..Default::default()
575 },
576 depth_stencil: Some(wgpu::DepthStencilState {
577 format: depth_format,
578 depth_write_enabled: Some(true),
579 depth_compare: Some(wgpu::CompareFunction::LessEqual),
580 stencil: Default::default(),
581 bias: Default::default(),
582 }),
583 multisample: Default::default(),
584 fragment: Some(wgpu::FragmentState {
585 module: &shader,
586 entry_point: Some("shadow_fragment"),
587 targets: &[],
588 compilation_options: Default::default(),
589 }),
590 multiview_mask: None,
591 cache: None,
592 });
593 Ok(Self {
594 manifest: artifact.manifest,
595 pipeline,
596 shadow_pipeline,
597 group,
598 uniform,
599 shadow_groups,
600 shadow_uniforms,
601 instances,
602 capacity,
603 })
604 }
605
606 pub fn update(
607 &self,
608 gpu: &Gpu,
609 mut params: ImpostorParams,
610 hour: f32,
611 items: &[ImpostorInstance],
612 ) -> Result<(), String> {
613 if items.len() > self.capacity {
614 return Err("impostor instance capacity exceeded".into());
615 }
616 self.finish_params(&mut params, hour)?;
617 gpu.queue
618 .write_buffer(&self.uniform, 0, bytemuck::bytes_of(¶ms));
619 gpu.queue
620 .write_buffer(&self.instances, 0, bytemuck::cast_slice(items));
621 Ok(())
622 }
623
624 fn finish_params(&self, params: &mut ImpostorParams, hour: f32) -> Result<(), String> {
625 params.settings = [
626 self.manifest.frames_per_side as f32,
627 (self.manifest.atlas_size / self.manifest.frames_per_side) as f32,
628 anchor_position(&self.manifest.anchors, hour)?,
629 if self.manifest.hemisphere { 1.0 } else { 0.0 },
630 ];
631 params.source = [self.manifest.bounds.radius, 0.0, 0.0, 0.0];
632 Ok(())
633 }
634
635 pub fn update_shadows(
636 &self,
637 gpu: &Gpu,
638 fit: &crate::shadow::Fit,
639 hour: f32,
640 items: &[ImpostorInstance],
641 ) -> Result<(), String> {
642 if items.len() > self.capacity {
643 return Err("impostor instance capacity exceeded".into());
644 }
645 let forward = fit.toward_sun.map(|value| -value);
646 let nominal_up = if fit.toward_sun[1].abs() > 0.95 {
647 [0.0, 0.0, 1.0]
648 } else {
649 [0.0, 1.0, 0.0]
650 };
651 let right = cross(forward, nominal_up);
652 let length = right.iter().map(|value| value * value).sum::<f32>().sqrt();
653 let right = right.map(|value| value / length);
654 let up = cross(right, forward);
655 for (index, cascade) in fit.cascades.iter().enumerate() {
656 let mut params = ImpostorParams {
657 view_projection: cascade.view_proj.columns,
658 camera_position: [fit.toward_sun[0], fit.toward_sun[1], fit.toward_sun[2], 0.0],
659 camera_right: [right[0], right[1], right[2], 0.0],
660 camera_up: [up[0], up[1], up[2], 0.0],
661 settings: [0.0; 4],
662 source: [0.0; 4],
663 };
664 self.finish_params(&mut params, hour)?;
665 params.source[1] = 1.0;
666 gpu.queue
667 .write_buffer(&self.shadow_uniforms[index], 0, bytemuck::bytes_of(¶ms));
668 }
669 gpu.queue
670 .write_buffer(&self.instances, 0, bytemuck::cast_slice(items));
671 Ok(())
672 }
673
674 pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>, count: usize) -> Result<(), String> {
675 if count > self.capacity {
676 return Err("impostor instance capacity exceeded".into());
677 }
678 pass.set_pipeline(&self.pipeline);
679 pass.set_bind_group(0, &self.group, &[]);
680 pass.draw(0..6, 0..count as u32);
681 Ok(())
682 }
683
684 pub fn draw_shadow<'a>(
685 &'a self,
686 pass: &mut wgpu::RenderPass<'a>,
687 count: usize,
688 cascade: usize,
689 ) -> Result<(), String> {
690 if count > self.capacity {
691 return Err("impostor instance capacity exceeded".into());
692 }
693 pass.set_pipeline(&self.shadow_pipeline);
694 let group = self
695 .shadow_groups
696 .get(cascade)
697 .ok_or("invalid shadow cascade")?;
698 pass.set_bind_group(0, group, &[]);
699 pass.draw(0..6, 0..count as u32);
700 Ok(())
701 }
702}
703
704fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
705 [
706 a[1] * b[2] - a[2] * b[1],
707 a[2] * b[0] - a[0] * b[2],
708 a[0] * b[1] - a[1] * b[0],
709 ]
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 #[test]
717 #[ignore = "needs a GPU; run under pgpu"]
718 fn prop_shadow_matches_mesh_footprint() {
719 use pfx_bake::impostor::{PropSpec, bake_prop, bounds};
720 use pfx_bake::{Anchor, BakeScene};
721 use pfx_geom::shapes::Shape;
722 use pfx_load::Sky;
723 use pfx_materials::Material;
724 use wgpu::util::DeviceExt;
725
726 let gpu = pollster::block_on(Gpu::headless()).unwrap();
727 let mesh = Shape::RoundBox {
728 half: [0.5, 0.5, 0.5],
729 radius: 0.02,
730 }
731 .mesh(0.02);
732 let triangles = mesh_triangles(&mesh, [0.0; 3], 0);
733 let bounds = bounds(&triangles, &[]).unwrap();
734 let scene = BakeScene {
735 triangles,
736 shapes: Vec::new(),
737 materials: vec![Material::default()],
738 anchors: vec![Anchor {
739 hour: 12.0,
740 sky: Sky {
741 width: 4,
742 height: 2,
743 texels: vec![[0.5; 4]; 8],
744 },
745 sun: pfx_trace::Sun {
746 direction: [0.0, 1.0, 0.0],
747 color: [1.0; 3],
748 intensity: 2.0,
749 },
750 }],
751 };
752 let artifact = bake_prop(
753 &gpu,
754 &scene,
755 &PropSpec {
756 name: "shadow-box".into(),
757 node: Some("box".into()),
758 file: None,
759 frames_per_side: 8,
760 atlas_size: 256,
761 hemisphere: false,
762 anchors: vec![12.0],
763 },
764 bounds,
765 1,
766 4,
767 )
768 .unwrap();
769 let props = ImpostorPass::new(
770 &gpu,
771 artifact,
772 wgpu::TextureFormat::Rgba16Float,
773 wgpu::TextureFormat::Depth32Float,
774 1,
775 )
776 .unwrap();
777 let quality = crate::shadow::Quality {
778 resolution: 128,
779 ..Default::default()
780 };
781 let view = crate::shadow::View {
782 eye: [0.0, 1.0, 4.0],
783 forward: [0.0, -0.25, -1.0],
784 up: [0.0, 1.0, 0.0],
785 fov_y: 1.0,
786 aspect: 1.0,
787 near: 0.1,
788 far: 10.0,
789 };
790 let mut mesh_shadows = crate::shadow::Shadows::new(&gpu.device, quality);
791 let fit = mesh_shadows.fit(&view, [0.0, 1.0, 0.0]);
792 let vertices = gpu
793 .device
794 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
795 label: Some("shadow reference positions"),
796 contents: bytemuck::cast_slice(&mesh.positions),
797 usage: wgpu::BufferUsages::VERTEX,
798 });
799 let indices = gpu
800 .device
801 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
802 label: Some("shadow reference indices"),
803 contents: bytemuck::cast_slice(&mesh.indices),
804 usage: wgpu::BufferUsages::INDEX,
805 });
806 let mut encoder = gpu.device.create_command_encoder(&Default::default());
807 mesh_shadows.render(
808 &gpu.device,
809 &gpu.queue,
810 &mut encoder,
811 &fit,
812 &[crate::shadow::Caster {
813 positions: &vertices,
814 indices: &indices,
815 first_index: 0,
816 index_count: mesh.indices.len() as u32,
817 instances: &[crate::shadow::Mat4::identity()],
818 moving: false,
819 generation: 0,
820 }],
821 );
822 let mut prop_shadows = crate::shadow::Shadows::new(&gpu.device, quality);
823 prop_shadows.render(&gpu.device, &gpu.queue, &mut encoder, &fit, &[]);
824 let item = ImpostorInstance::new(bounds.center, bounds.radius, 1, 0.0).unwrap();
825 props.update_shadows(&gpu, &fit, 12.0, &[item]).unwrap();
826 prop_shadows
827 .render_impostors(&mut encoder, &props, 1)
828 .unwrap();
829 let reference = shadow_pixels(&gpu, &mut encoder, mesh_shadows.atlas());
830 let actual = shadow_pixels(&gpu, &mut encoder, prop_shadows.atlas());
831 gpu.queue.submit(Some(encoder.finish()));
832 let reference = read_shadow_pixels(&gpu, &reference);
833 let actual = read_shadow_pixels(&gpu, &actual);
834 let mut populated = 0;
835 for cascade in 0..crate::shadow::CASCADE_COUNT {
836 let start = cascade * 128 * 128;
837 let end = start + 128 * 128;
838 let mut overlap = 0;
839 let mut union = 0;
840 let mut mesh_count = 0;
841 let mut prop_count = 0;
842 for (mesh, prop) in reference[start..end].iter().zip(&actual[start..end]) {
843 let mesh = *mesh < 0.999;
844 let prop = *prop < 0.999;
845 overlap += usize::from(mesh && prop);
846 union += usize::from(mesh || prop);
847 mesh_count += usize::from(mesh);
848 prop_count += usize::from(prop);
849 }
850 if mesh_count > 0 {
851 populated += 1;
852 assert!(
853 overlap * 10 >= union * 7,
854 "cascade {cascade}: overlap {overlap}/{union}, mesh {mesh_count}, prop {prop_count}"
855 );
856 } else {
857 assert_eq!(prop_count, 0, "unexpected prop shadow in cascade {cascade}");
858 }
859 }
860 assert!(populated >= 2);
861 }
862
863 fn shadow_pixels(
864 gpu: &Gpu,
865 encoder: &mut wgpu::CommandEncoder,
866 atlas: &wgpu::Texture,
867 ) -> wgpu::Buffer {
868 let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
869 label: Some("impostor shadow readback"),
870 size: 512 * 128 * crate::shadow::CASCADE_COUNT as u64,
871 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
872 mapped_at_creation: false,
873 });
874 encoder.copy_texture_to_buffer(
875 wgpu::TexelCopyTextureInfo {
876 texture: atlas,
877 mip_level: 0,
878 origin: wgpu::Origin3d::ZERO,
879 aspect: wgpu::TextureAspect::DepthOnly,
880 },
881 wgpu::TexelCopyBufferInfo {
882 buffer: &buffer,
883 layout: wgpu::TexelCopyBufferLayout {
884 offset: 0,
885 bytes_per_row: Some(512),
886 rows_per_image: Some(128),
887 },
888 },
889 wgpu::Extent3d {
890 width: 128,
891 height: 128,
892 depth_or_array_layers: crate::shadow::CASCADE_COUNT as u32,
893 },
894 );
895 buffer
896 }
897
898 fn read_shadow_pixels(gpu: &Gpu, buffer: &wgpu::Buffer) -> Vec<f32> {
899 let (send, receive) = std::sync::mpsc::channel();
900 buffer
901 .slice(..)
902 .map_async(wgpu::MapMode::Read, move |result| {
903 send.send(result).unwrap();
904 });
905 gpu.device
906 .poll(wgpu::PollType::wait_indefinitely())
907 .unwrap();
908 receive.recv().unwrap().unwrap();
909 let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
910 let pixels = mapped
911 .chunks_exact(4)
912 .map(|bytes| f32::from_le_bytes(bytes.try_into().unwrap()))
913 .collect();
914 drop(mapped);
915 buffer.unmap();
916 pixels
917 }
918
919 fn identity() -> [[f32; 4]; 4] {
920 [
921 [1.0, 0.0, 0.0, 0.0],
922 [0.0, 1.0, 0.0, 0.0],
923 [0.0, 0.0, 1.0, 0.0],
924 [0.0, 0.0, 0.0, 1.0],
925 ]
926 }
927
928 fn perspective(aspect: f32) -> [[f32; 4]; 4] {
929 let near = 0.1;
930 let far = 100.0;
931 let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
932 [
933 [f / aspect, 0.0, 0.0, 0.0],
934 [0.0, f, 0.0, 0.0],
935 [0.0, 0.0, far / (near - far), -1.0],
936 [0.0, 0.0, far * near / (near - far), 0.0],
937 ]
938 }
939
940 fn mesh_triangles(
941 mesh: &pfx_geom::mesh::Mesh,
942 offset: [f32; 3],
943 material: u32,
944 ) -> Vec<pfx_trace::bvh::Triangle> {
945 mesh.indices
946 .chunks_exact(3)
947 .map(|tri| pfx_trace::bvh::Triangle {
948 vertices: [tri[0], tri[1], tri[2]].map(|index| {
949 [0, 1, 2].map(|axis| mesh.positions[index as usize][axis] + offset[axis])
950 }),
951 material,
952 })
953 .collect()
954 }
955
956 fn mesh_handle(
957 frame: &mut crate::frame::Frame,
958 mesh: &pfx_geom::mesh::Mesh,
959 ) -> crate::frame::MeshHandle {
960 frame
961 .upload_mesh(crate::frame::MeshData {
962 positions: &mesh.positions,
963 normals: &mesh.normals,
964 tangents: &mesh.tangents,
965 uvs: &mesh.uvs,
966 uvs1: None,
967 alpha: None,
968 indices: &mesh.indices,
969 })
970 .unwrap()
971 }
972
973 fn png_preview(mesh: &[u16], impostor: &[u16], width: u32, height: u32) {
974 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp");
975 std::fs::create_dir_all(&root).unwrap();
976 let path = root.join("impostor-side-by-side.png");
977 let preview_width = width / 2;
978 let preview_height = height / 2;
979 let mut pixels = vec![0u8; (preview_width * 2 * preview_height * 3) as usize];
980 for y in 0..preview_height {
981 for x in 0..preview_width * 2 {
982 let source = if x < preview_width { mesh } else { impostor };
983 let source_x = (x % preview_width) * 2;
984 let source_y = y * 2;
985 let source_index = ((source_y * width + source_x) * 4) as usize;
986 let destination = ((y * preview_width * 2 + x) * 3) as usize;
987 for channel in 0..3 {
988 let linear = half::f16::from_bits(source[source_index + channel])
989 .to_f32()
990 .max(0.0);
991 let mapped = linear / (1.0 + linear);
992 pixels[destination + channel] =
993 (mapped.powf(1.0 / 2.2) * 255.0).round().clamp(0.0, 255.0) as u8;
994 }
995 }
996 }
997 let file = std::fs::File::create(&path).unwrap();
998 let mut encoder = png::Encoder::new(file, preview_width * 2, preview_height);
999 encoder.set_color(png::ColorType::Rgb);
1000 encoder.set_depth(png::BitDepth::Eight);
1001 encoder
1002 .write_header()
1003 .unwrap()
1004 .write_image_data(&pixels)
1005 .unwrap();
1006 println!("side-by-side: {}", path.display());
1007 }
1008
1009 fn display_value(bits: u16) -> f32 {
1010 let linear = half::f16::from_bits(bits).to_f32().max(0.0);
1011 linear / (1.0 + linear)
1012 }
1013
1014 #[test]
1015 fn frame_weights_sum_to_one() {
1016 for x in -6..=6 {
1017 for y in -6..=6 {
1018 let (_, weights) = frame_weights([x as f32 + 0.1, y as f32 + 0.2, 1.0], 8).unwrap();
1019 assert!(weights.iter().all(|v| *v >= 0.0));
1020 assert!((weights.iter().sum::<f32>() - 1.0).abs() < 1e-6);
1021 }
1022 }
1023 }
1024
1025 #[test]
1026 fn mesh_transition_is_continuous() {
1027 assert_eq!(mesh_fraction(4.0, 10.0, 4.0).unwrap(), 1.0);
1028 assert_eq!(mesh_fraction(10.0, 10.0, 4.0).unwrap(), 0.5);
1029 assert_eq!(mesh_fraction(16.0, 10.0, 4.0).unwrap(), 0.0);
1030 }
1031
1032 #[test]
1033 fn shader_validates() {
1034 let module = naga::front::wgsl::parse_str(IMPOSTOR_WGSL).unwrap();
1035 naga::valid::Validator::new(
1036 naga::valid::ValidationFlags::all(),
1037 naga::valid::Capabilities::all(),
1038 )
1039 .validate(&module)
1040 .unwrap();
1041 }
1042
1043 #[test]
1044 #[ignore = "needs a GPU; run under pgpu"]
1045 fn rounded_box_and_chrome_sphere_at_4k() {
1046 use pfx_bake::impostor::{PropSpec, bake_prop, bounds};
1047 use pfx_bake::{Anchor, BakeScene};
1048 use pfx_geom::shapes::Shape;
1049 use pfx_gpu::GpuProfiler;
1050 use pfx_load::Sky;
1051 use pfx_materials::Material;
1052
1053 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1054 let box_mesh = Shape::RoundBox {
1055 half: [0.8, 0.4, 0.6],
1056 radius: 0.12,
1057 }
1058 .mesh(0.006);
1059 let sphere_mesh = Shape::Ellipsoid {
1060 radius: 0.24,
1061 squash: 1.0,
1062 }
1063 .mesh(0.006);
1064 let sphere_offset = [0.0, 0.62, 0.0];
1065 let mut triangles = mesh_triangles(&box_mesh, [0.0; 3], 0);
1066 triangles.extend(mesh_triangles(&sphere_mesh, sphere_offset, 1));
1067 let materials = vec![
1068 Material {
1069 base: [0.75, 0.16, 0.08],
1070 roughness: 0.7,
1071 ..Default::default()
1072 },
1073 Material {
1074 base: [0.9; 3],
1075 metalness: 1.0,
1076 roughness: 0.22,
1077 ..Default::default()
1078 },
1079 ];
1080 let scene = BakeScene {
1081 triangles,
1082 shapes: Vec::new(),
1083 materials: materials.clone(),
1084 anchors: vec![Anchor {
1085 hour: 12.0,
1086 sky: Sky {
1087 width: 4,
1088 height: 2,
1089 texels: vec![[0.3, 0.4, 0.6, 1.0]; 8],
1090 },
1091 sun: pfx_trace::Sun {
1092 direction: [0.4, 0.8, 0.4],
1093 color: [1.0; 3],
1094 intensity: 2.0,
1095 },
1096 }],
1097 };
1098 let bounds = bounds(&scene.triangles, &scene.shapes).unwrap();
1099 let spec = PropSpec {
1100 name: "rounded-box-chrome".into(),
1101 node: Some("test".into()),
1102 file: None,
1103 frames_per_side: 8,
1104 atlas_size: 512,
1105 hemisphere: false,
1106 anchors: vec![12.0],
1107 };
1108 let artifact = bake_prop(&gpu, &scene, &spec, bounds, 16, 7).unwrap();
1109 let artifact_bytes =
1110 artifact.geometry.len() + artifact.radiance.iter().map(Vec::len).sum::<usize>();
1111 let width = 3840;
1112 let height = 2160;
1113 let mut frame = crate::frame::Frame::new(gpu, width, height).unwrap();
1114 let box_handle = mesh_handle(&mut frame, &box_mesh);
1115 let sphere_handle = mesh_handle(&mut frame, &sphere_mesh);
1116 let mut view = identity();
1117 view[3] = [0.0, -bounds.center[1], -10.0, 1.0];
1118 let projection = perspective(width as f32 / height as f32);
1119 let view_projection = crate::frame::multiply(projection, view);
1120 let camera = crate::frame::Camera {
1121 view,
1122 projection,
1123 previous_view_projection: view_projection,
1124 position: [0.0, bounds.center[1], 10.0],
1125 };
1126 let mut sphere_model = identity();
1127 sphere_model[3] = [0.0, sphere_offset[1], 0.0, 1.0];
1128 let instances = [
1129 crate::frame::Instance::new(box_handle, identity(), 0, 1),
1130 crate::frame::Instance::new(sphere_handle, sphere_model, 1, 2),
1131 ];
1132 frame.set_sky(
1133 crate::sky::SkySource::Hdr(Sky {
1134 width: 4,
1135 height: 2,
1136 texels: vec![[0.3, 0.4, 0.6, 1.0]; 8],
1137 }),
1138 true,
1139 );
1140 let live_scene = crate::frame::Scene {
1141 camera,
1142 time: 0.0,
1143 seed: 7,
1144 sun: crate::frame::Sun {
1145 direction: [0.4, 0.8, 0.4],
1146 colour: [1.0; 3],
1147 intensity: 2.0,
1148 },
1149 instances: &instances,
1150 materials: &materials,
1151 deformers: &[],
1152 wind: Default::default(),
1153 };
1154 frame.render(&live_scene).unwrap();
1155 let mesh_pixels = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
1156 let impostor = ImpostorPass::new(
1157 &frame.gpu,
1158 artifact,
1159 wgpu::TextureFormat::Rgba16Float,
1160 wgpu::TextureFormat::Depth32Float,
1161 200,
1162 )
1163 .unwrap();
1164 let color = frame
1165 .gpu
1166 .offscreen(width, height, wgpu::TextureFormat::Rgba16Float)
1167 .unwrap();
1168 let depth = frame.gpu.device.create_texture(&wgpu::TextureDescriptor {
1169 label: Some("impostor benchmark depth"),
1170 size: wgpu::Extent3d {
1171 width,
1172 height,
1173 depth_or_array_layers: 1,
1174 },
1175 mip_level_count: 1,
1176 sample_count: 1,
1177 dimension: wgpu::TextureDimension::D2,
1178 format: wgpu::TextureFormat::Depth32Float,
1179 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1180 view_formats: &[],
1181 });
1182 let depth_view = depth.create_view(&Default::default());
1183 let params = ImpostorParams {
1184 view_projection,
1185 camera_position: [0.0, bounds.center[1], 10.0, 0.0],
1186 camera_right: [1.0, 0.0, 0.0, 0.0],
1187 camera_up: [0.0, 1.0, 0.0, 0.0],
1188 settings: [0.0; 4],
1189 source: [0.0; 4],
1190 };
1191 let single = [ImpostorInstance::new(bounds.center, bounds.radius, 3, 0.0).unwrap()];
1192 impostor.update(&frame.gpu, params, 12.0, &single).unwrap();
1193 let render = |count: usize, timer: Option<&mut GpuProfiler>, clear: bool| {
1194 let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
1195 let mut timer = timer;
1196 let index = timer.as_mut().and_then(|t| t.pass("200 impostors"));
1197 {
1198 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1199 multiview_mask: None,
1200 label: Some("impostor benchmark"),
1201 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1202 view: &color.view,
1203 resolve_target: None,
1204 depth_slice: None,
1205 ops: wgpu::Operations {
1206 load: if clear {
1207 wgpu::LoadOp::Clear(wgpu::Color::BLACK)
1208 } else {
1209 wgpu::LoadOp::Load
1210 },
1211 store: wgpu::StoreOp::Store,
1212 },
1213 })],
1214 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1215 view: &depth_view,
1216 depth_ops: Some(wgpu::Operations {
1217 load: if clear {
1218 wgpu::LoadOp::Clear(1.0)
1219 } else {
1220 wgpu::LoadOp::Load
1221 },
1222 store: wgpu::StoreOp::Store,
1223 }),
1224 stencil_ops: None,
1225 }),
1226 occlusion_query_set: None,
1227 timestamp_writes: timer.as_ref().and_then(|t| t.render_writes(index)),
1228 });
1229 impostor.draw(&mut pass, count).unwrap();
1230 }
1231 let slot = timer.as_mut().and_then(|t| t.finish(&mut encoder));
1232 frame.gpu.queue.submit(Some(encoder.finish()));
1233 if let (Some(timer), Some(slot)) = (timer, slot) {
1234 timer.submitted(slot);
1235 }
1236 };
1237 render(1, None, true);
1238 let impostor_pixels = frame.gpu.readback_rgba16(&color).unwrap();
1239 png_preview(&mesh_pixels, &impostor_pixels, width, height);
1240 let error = mesh_pixels
1241 .chunks_exact(4)
1242 .zip(impostor_pixels.chunks_exact(4))
1243 .map(|(a, b)| {
1244 (0..3)
1245 .map(|channel| (display_value(a[channel]) - display_value(b[channel])).abs())
1246 .sum::<f32>()
1247 })
1248 .sum::<f32>()
1249 / (width * height * 3) as f32;
1250 let (foreground_error, foreground_pixels) = mesh_pixels
1251 .chunks_exact(4)
1252 .zip(impostor_pixels.chunks_exact(4))
1253 .filter_map(|(a, b)| {
1254 let reference = [0, 1, 2].map(|channel| display_value(a[channel]));
1255 let actual = [0, 1, 2].map(|channel| display_value(b[channel]));
1256 (reference
1257 .iter()
1258 .chain(actual.iter())
1259 .any(|value| *value > 0.01))
1260 .then_some(
1261 (0..3)
1262 .map(|channel| (reference[channel] - actual[channel]).abs())
1263 .sum::<f32>(),
1264 )
1265 })
1266 .fold((0.0, 0usize), |(error, count), value| {
1267 (error + value, count + 1)
1268 });
1269 let foreground_error = foreground_error / (foreground_pixels.max(1) * 3) as f32;
1270 let items = (0..200)
1271 .map(|i| {
1272 let x = (i % 20) as f32 - 9.5;
1273 let y = (i / 20) as f32 - 4.5;
1274 ImpostorInstance::new([x * 0.45, y * 0.42, 0.0], 0.22, i as u32 + 10, 0.0).unwrap()
1275 })
1276 .collect::<Vec<_>>();
1277 impostor.update(&frame.gpu, params, 12.0, &items).unwrap();
1278 let mut profiler = GpuProfiler::new(&frame.gpu.device, &frame.gpu.queue);
1279 for _ in 0..4 {
1280 render(200, None, true);
1281 }
1282 render(200, Some(&mut profiler), false);
1283 frame
1284 .gpu
1285 .device
1286 .poll(wgpu::PollType::wait_indefinitely())
1287 .unwrap();
1288 let timings = profiler.collect(&frame.gpu.device);
1289 let milliseconds = timings
1290 .first()
1291 .and_then(|passes| passes.first())
1292 .map(|pass| pass.milliseconds)
1293 .unwrap_or(f64::NAN);
1294 println!(
1295 "4K impostor mesh MAE={error:.4}, foreground MAE={foreground_error:.4} over {foreground_pixels} pixels, 200 instances={milliseconds:.3} ms, atlas={artifact_bytes} bytes"
1296 );
1297 assert!(error <= 0.02, "4K impostor mesh MAE {error:.4}");
1298 assert!(
1299 milliseconds < 0.5,
1300 "200 impostors took {milliseconds:.3} ms"
1301 );
1302 }
1303}