Skip to main content

pfx_live/
impostor.rs

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(&params));
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(&params));
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}