Skip to main content

pfx_live/
glass.rs

1use bytemuck::{Pod, Zeroable};
2use pfx_geom::mesh::Mesh;
3use pfx_gpu::GpuProfiler;
4use pfx_materials::Material;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicUsize, Ordering};
7use wgpu::util::DeviceExt;
8
9use crate::frame::{CountedBuffer, MESH_GENERATION_LAST, MESH_SLOT_BITS, MESH_SLOT_MASK};
10
11pub use crate::maps::CausticProjection;
12
13pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
14pub const REACTIVE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
15
16pub type Matrix = [[f32; 4]; 4];
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct MeshHandle(pub u32);
20
21impl MeshHandle {
22    pub fn slot(self) -> usize {
23        (self.0 & MESH_SLOT_MASK) as usize
24    }
25
26    pub fn generation(self) -> u32 {
27        self.0 >> MESH_SLOT_BITS
28    }
29
30    fn pack(slot: usize, generation: u32) -> Self {
31        Self(slot as u32 | generation << MESH_SLOT_BITS)
32    }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct GlassStats {
37    pub live: usize,
38    pub slots: usize,
39    pub buffers: usize,
40}
41
42#[derive(Clone, Copy)]
43pub struct Surface {
44    pub mesh: MeshHandle,
45    pub model: Matrix,
46    pub material: Material,
47    pub liquid: bool,
48    pub fluid_height: f32,
49    pub ripple_height: f32,
50    pub caustic_strength: f32,
51    pub tinted: Option<Material>,
52    pub id: u32,
53    pub casts_shadow: bool,
54    pub shadow_only: bool,
55    pub clip: [[f32; 4]; 2],
56}
57
58#[derive(Clone, Copy)]
59pub struct Camera {
60    pub view: Matrix,
61    pub projection: Matrix,
62}
63
64pub struct Inputs<'a> {
65    pub opaque_hdr_mips: &'a wgpu::TextureView,
66    pub opaque_depth: &'a wgpu::TextureView,
67    pub reflection_cube: &'a wgpu::TextureView,
68    pub caustic: &'a wgpu::TextureView,
69    pub caustic_projection: Option<CausticProjection>,
70    pub fluid_height: &'a wgpu::TextureView,
71    pub ripple_height: &'a wgpu::TextureView,
72    pub mip_count: u32,
73    pub sky_sh: [[f32; 3]; 9],
74}
75
76pub struct Render<'a> {
77    pub camera: Camera,
78    pub surfaces: &'a [Surface],
79    pub inputs: Inputs<'a>,
80    pub output: &'a wgpu::TextureView,
81    pub profiler: Option<&'a mut GpuProfiler>,
82}
83
84#[derive(Clone, Copy)]
85pub struct Refraction {
86    pub uv: [f32; 2],
87    pub front_depth: f32,
88    pub back_depth: f32,
89    pub front_normal: [f32; 3],
90    pub back_normal: [f32; 3],
91    pub projection_scale: [f32; 2],
92    pub ior: f32,
93    pub receiver_depth: f32,
94}
95
96struct MeshGpu {
97    vertices: CountedBuffer,
98    indices: CountedBuffer,
99    vertex_capacity: usize,
100    index_capacity: usize,
101    count: u32,
102    bounds: [[f32; 3]; 2],
103}
104
105struct MeshSlot {
106    generation: u32,
107    mesh: Option<MeshGpu>,
108}
109
110#[derive(Default)]
111pub struct GlassMeshes {
112    slots: Vec<MeshSlot>,
113    free: Vec<usize>,
114    buffers: Arc<AtomicUsize>,
115}
116
117fn check_glass(mesh: &Mesh) -> Result<(), String> {
118    if mesh.positions.len() != mesh.normals.len()
119        || mesh.positions.len() != mesh.uvs.len()
120        || mesh.indices.is_empty()
121    {
122        return Err("glass mesh needs positions, normals, UVs, and indices".into());
123    }
124    Ok(())
125}
126
127fn glass_vertices(mesh: &Mesh) -> Vec<Vertex> {
128    mesh.positions
129        .iter()
130        .zip(&mesh.normals)
131        .zip(&mesh.uvs)
132        .map(|((position, normal), uv)| Vertex {
133            position: [position[0], position[1], position[2], 1.0],
134            normal: [normal[0], normal[1], normal[2], 0.0],
135            uv: [uv[0], uv[1], 0.0, 0.0],
136            look: plain_look(),
137        })
138        .collect()
139}
140
141fn glass_buffer(
142    device: &wgpu::Device,
143    live: &Arc<AtomicUsize>,
144    label: &str,
145    contents: &[u8],
146    usage: wgpu::BufferUsages,
147) -> CountedBuffer {
148    CountedBuffer::new(
149        device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
150            label: Some(label),
151            contents,
152            usage: usage | wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
153        }),
154        live,
155    )
156}
157
158impl GlassMeshes {
159    fn get(&self, handle: MeshHandle) -> Option<&MeshGpu> {
160        let slot = self.slots.get(handle.slot())?;
161        if slot.generation == handle.generation() {
162            slot.mesh.as_ref()
163        } else {
164            None
165        }
166    }
167
168    pub fn stats(&self) -> GlassStats {
169        GlassStats {
170            live: self.slots.iter().filter(|slot| slot.mesh.is_some()).count(),
171            slots: self.slots.len(),
172            buffers: self.buffers.load(Ordering::Relaxed),
173        }
174    }
175
176    fn upload(&mut self, device: &wgpu::Device, mesh: &Mesh) -> Result<MeshHandle, String> {
177        check_glass(mesh)?;
178        let vertices = glass_vertices(mesh);
179        let gpu = MeshGpu {
180            vertices: glass_buffer(
181                device,
182                &self.buffers,
183                "glass vertices",
184                bytemuck::cast_slice(&vertices),
185                wgpu::BufferUsages::VERTEX,
186            ),
187            indices: glass_buffer(
188                device,
189                &self.buffers,
190                "glass indices",
191                bytemuck::cast_slice(&mesh.indices),
192                wgpu::BufferUsages::INDEX,
193            ),
194            vertex_capacity: vertices.len(),
195            index_capacity: mesh.indices.len(),
196            count: mesh.indices.len() as u32,
197            bounds: [mesh.bounds.min, mesh.bounds.max],
198        };
199        if let Some(slot) = self.free.pop() {
200            let entry = &mut self.slots[slot];
201            entry.mesh = Some(gpu);
202            return Ok(MeshHandle::pack(slot, entry.generation));
203        }
204        let slot = self.slots.len();
205        if slot > MESH_SLOT_MASK as usize {
206            return Err("too many glass meshes".into());
207        }
208        self.slots.push(MeshSlot {
209            generation: 0,
210            mesh: Some(gpu),
211        });
212        Ok(MeshHandle::pack(slot, 0))
213    }
214
215    fn release(&mut self, handle: MeshHandle) -> Result<(), String> {
216        if self.get(handle).is_none() {
217            return Err("glass mesh handle is unknown or already released".into());
218        }
219        let slot = handle.slot();
220        let entry = &mut self.slots[slot];
221        entry.mesh = None;
222        entry.generation += 1;
223        if entry.generation < MESH_GENERATION_LAST {
224            self.free.push(slot);
225        }
226        Ok(())
227    }
228
229    fn replace(
230        &mut self,
231        device: &wgpu::Device,
232        queue: &wgpu::Queue,
233        handle: MeshHandle,
234        mesh: &Mesh,
235    ) -> Result<(), String> {
236        check_glass(mesh)?;
237        if self.get(handle).is_none() {
238            return Err("glass mesh handle is unknown or already released".into());
239        }
240        let live = &self.buffers;
241        let Some(gpu) = self.slots[handle.slot()].mesh.as_mut() else {
242            return Err("glass mesh handle is unknown or already released".into());
243        };
244        let vertices = glass_vertices(mesh);
245        if vertices.len() <= gpu.vertex_capacity {
246            queue.write_buffer(&gpu.vertices, 0, bytemuck::cast_slice(&vertices));
247        } else {
248            gpu.vertices = glass_buffer(
249                device,
250                live,
251                "glass vertices",
252                bytemuck::cast_slice(&vertices),
253                wgpu::BufferUsages::VERTEX,
254            );
255            gpu.vertex_capacity = vertices.len();
256        }
257        if mesh.indices.len() <= gpu.index_capacity {
258            queue.write_buffer(&gpu.indices, 0, bytemuck::cast_slice(&mesh.indices));
259        } else {
260            gpu.indices = glass_buffer(
261                device,
262                live,
263                "glass indices",
264                bytemuck::cast_slice(&mesh.indices),
265                wgpu::BufferUsages::INDEX,
266            );
267            gpu.index_capacity = mesh.indices.len();
268        }
269        gpu.count = mesh.indices.len() as u32;
270        gpu.bounds = [mesh.bounds.min, mesh.bounds.max];
271        Ok(())
272    }
273}
274
275#[repr(C)]
276#[derive(Clone, Copy, Pod, Zeroable)]
277struct Vertex {
278    position: [f32; 4],
279    normal: [f32; 4],
280    uv: [f32; 4],
281    look: [f32; 4],
282}
283
284pub const VERTEX_FLOATS: usize = 16;
285pub const LOOK_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
286
287pub fn plain_look() -> [f32; 4] {
288    [0.0, 0.0, 1.0, 0.0]
289}
290
291pub fn subsurface_colour(tint: [f32; 3], sky_sh: &[[f32; 3]; 9], normal: [f32; 3]) -> [f32; 3] {
292    let irradiance = crate::frame::sky_irradiance(sky_sh, normal);
293    std::array::from_fn(|k| tint[k] * irradiance[k] / std::f32::consts::PI)
294}
295
296pub fn uniform_sky_sh(irradiance: [f32; 3]) -> [[f32; 3]; 9] {
297    let mut sh = [[0.0; 3]; 9];
298    sh[0] = irradiance.map(|e| e / 0.282095);
299    sh
300}
301
302#[repr(C)]
303#[derive(Clone, Copy, Pod, Zeroable)]
304struct SceneGpu {
305    projection: Matrix,
306    world_from_view: [[f32; 4]; 3],
307    extent: [f32; 4],
308    caustic: [[f32; 4]; 3],
309    sky: [[f32; 4]; 9],
310}
311
312#[repr(C)]
313#[derive(Clone, Copy, Pod, Zeroable)]
314struct ObjectGpu {
315    model_view: Matrix,
316    mvp: Matrix,
317    normal: [[f32; 4]; 3],
318    base_roughness: [f32; 4],
319    tint_absorption: [f32; 4],
320    optics: [f32; 4],
321    film: [f32; 4],
322    liquid: [f32; 4],
323    tinted_base: [f32; 4],
324    tinted_subsurface: [f32; 4],
325    pick: [u32; 4],
326    clip: [[f32; 4]; 2],
327}
328
329#[repr(C)]
330#[derive(Clone, Copy, Pod, Zeroable)]
331struct CastGpu {
332    model: Matrix,
333    view_proj: Matrix,
334    tint: [f32; 4],
335    tinted: [f32; 4],
336    liquid: [f32; 4],
337    clip: [[f32; 4]; 2],
338}
339
340struct Casts {
341    group: wgpu::BindGroup,
342    draws: Vec<MeshHandle>,
343}
344
345struct Picks {
346    scene: wgpu::BindGroup,
347    ids: wgpu::BindGroup,
348    compose: wgpu::BindGroup,
349    rects: Vec<[u32; 4]>,
350}
351
352pub struct Pass {
353    width: u32,
354    height: u32,
355    viewport: (u32, u32),
356    _front: wgpu::Texture,
357    front_view: wgpu::TextureView,
358    _front_id: wgpu::Texture,
359    front_id_view: wgpu::TextureView,
360    _front_look: wgpu::Texture,
361    front_look_view: wgpu::TextureView,
362    _front_depth: wgpu::Texture,
363    front_depth_view: wgpu::TextureView,
364    _back: wgpu::Texture,
365    back_view: wgpu::TextureView,
366    _back_depth: wgpu::Texture,
367    back_depth_view: wgpu::TextureView,
368    _reactive: wgpu::Texture,
369    reactive_view: wgpu::TextureView,
370    scene: wgpu::Buffer,
371    objects: wgpu::Buffer,
372    capacity: usize,
373    scene_layout: wgpu::BindGroupLayout,
374    id_layout: wgpu::BindGroupLayout,
375    compose_layout: wgpu::BindGroupLayout,
376    sampler: wgpu::Sampler,
377    front_pipe: wgpu::RenderPipeline,
378    back_pipe: wgpu::RenderPipeline,
379    compose_pipe: wgpu::RenderPipeline,
380    pick_pipe: wgpu::RenderPipeline,
381    picks: Option<Picks>,
382    cast_layout: wgpu::BindGroupLayout,
383    cast_pipe: wgpu::RenderPipeline,
384    cast_objects: wgpu::Buffer,
385    cast_capacity: usize,
386    casts: Option<Casts>,
387    meshes: GlassMeshes,
388}
389
390pub fn cast_tint(material: &Material) -> [f32; 3] {
391    let passed = if material.thickness > 0.0 && material.absorption > 0.0 {
392        pfx_materials::beer(material.thickness, material.base, material.absorption)
393    } else {
394        material.base
395    };
396    passed.map(|channel| (channel * material.transmission).clamp(0.0, 1.0))
397}
398
399pub fn casting(surfaces: &[Surface]) -> bool {
400    surfaces.iter().any(|surface| surface.casts_shadow)
401}
402
403pub fn drawn(surfaces: &[Surface]) -> bool {
404    surfaces.iter().any(|surface| !surface.shadow_only)
405}
406
407pub fn clips(clip: &[[f32; 4]; 2], point: [f32; 3]) -> bool {
408    clip.iter().any(|plane| {
409        plane[..3].iter().any(|&value| value != 0.0)
410            && plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] > plane[3]
411    })
412}
413
414pub fn thickness(front_depth: f32, back_depth: f32) -> f32 {
415    (back_depth - front_depth).max(0.0)
416}
417
418pub fn beer_lambert(distance: f32, tint: [f32; 3], absorption: f32) -> [f32; 3] {
419    pfx_materials::beer(distance.max(0.0), tint, absorption.max(0.0))
420}
421
422fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
423    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
424}
425
426fn normalize(v: [f32; 3]) -> [f32; 3] {
427    let inverse = dot(v, v).sqrt().max(1e-8).recip();
428    [v[0] * inverse, v[1] * inverse, v[2] * inverse]
429}
430
431fn refract(incident: [f32; 3], normal: [f32; 3], eta: f32) -> Option<[f32; 3]> {
432    let cos = -dot(incident, normal);
433    let k = 1.0 - eta * eta * (1.0 - cos * cos);
434    (k >= 0.0).then(|| {
435        let v = eta * cos - k.sqrt();
436        [
437            eta * incident[0] + v * normal[0],
438            eta * incident[1] + v * normal[1],
439            eta * incident[2] + v * normal[2],
440        ]
441    })
442}
443
444pub fn refraction_offset(
445    Refraction {
446        uv,
447        front_depth,
448        back_depth,
449        front_normal,
450        back_normal,
451        projection_scale,
452        ior,
453        receiver_depth,
454    }: Refraction,
455) -> [f32; 2] {
456    if front_depth <= 0.0 || back_depth <= front_depth || ior <= 1.0 {
457        return [0.0; 2];
458    }
459    let ndc = [uv[0] * 2.0 - 1.0, 1.0 - uv[1] * 2.0];
460    let entry = [
461        ndc[0] * front_depth / projection_scale[0],
462        ndc[1] * front_depth / projection_scale[1],
463        -front_depth,
464    ];
465    let incident = normalize(entry);
466    let front = normalize(front_normal);
467    let back = normalize(back_normal);
468    let Some(inside) = refract(incident, front, 1.0 / ior) else {
469        return [0.0; 2];
470    };
471    let travel = thickness(front_depth, back_depth) / (-inside[2]).max(0.1);
472    let exit = [
473        entry[0] + inside[0] * travel,
474        entry[1] + inside[1] * travel,
475        -back_depth,
476    ];
477    let outgoing = refract(inside, [-back[0], -back[1], -back[2]], ior).unwrap_or(inside);
478    let reach = (receiver_depth - back_depth).max(0.0) / (-outgoing[2]).max(0.1);
479    let target = [exit[0] + outgoing[0] * reach, exit[1] + outgoing[1] * reach];
480    let target_uv = [
481        0.5 + target[0] * projection_scale[0] / (2.0 * receiver_depth.max(back_depth)),
482        0.5 - target[1] * projection_scale[1] / (2.0 * receiver_depth.max(back_depth)),
483    ];
484    [target_uv[0] - uv[0], target_uv[1] - uv[1]]
485}
486
487fn multiply(a: Matrix, b: Matrix) -> Matrix {
488    let mut out = [[0.0; 4]; 4];
489    for col in 0..4 {
490        for row in 0..4 {
491            out[col][row] = (0..4).map(|k| a[k][row] * b[col][k]).sum();
492        }
493    }
494    out
495}
496
497fn transform(m: Matrix, p: [f32; 4]) -> [f32; 4] {
498    std::array::from_fn(|row| (0..4).map(|col| m[col][row] * p[col]).sum())
499}
500
501fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
502    [
503        a[1] * b[2] - a[2] * b[1],
504        a[2] * b[0] - a[0] * b[2],
505        a[0] * b[1] - a[1] * b[0],
506    ]
507}
508
509fn normal_columns(model_view: Matrix) -> [[f32; 4]; 3] {
510    let a = [model_view[0][0], model_view[0][1], model_view[0][2]];
511    let b = [model_view[1][0], model_view[1][1], model_view[1][2]];
512    let c = [model_view[2][0], model_view[2][1], model_view[2][2]];
513    let columns = [cross(b, c), cross(c, a), cross(a, b)];
514    let determinant = dot(a, columns[0]);
515    columns.map(|v| {
516        [
517            v[0] / determinant,
518            v[1] / determinant,
519            v[2] / determinant,
520            0.0,
521        ]
522    })
523}
524
525fn world_from_view(view: Matrix) -> [[f32; 4]; 3] {
526    let columns = normal_columns(view);
527    let eye = std::array::from_fn::<f32, 3, _>(|i| {
528        -(view[i][0] * view[3][0] + view[i][1] * view[3][1] + view[i][2] * view[3][2])
529    });
530    [
531        [columns[0][0], columns[1][0], columns[2][0], eye[0]],
532        [columns[0][1], columns[1][1], columns[2][1], eye[1]],
533        [columns[0][2], columns[1][2], columns[2][2], eye[2]],
534    ]
535}
536
537fn target(
538    device: &wgpu::Device,
539    size: [u32; 2],
540    format: wgpu::TextureFormat,
541    label: &str,
542) -> (wgpu::Texture, wgpu::TextureView) {
543    let texture = device.create_texture(&wgpu::TextureDescriptor {
544        label: Some(label),
545        size: wgpu::Extent3d {
546            width: size[0],
547            height: size[1],
548            depth_or_array_layers: 1,
549        },
550        mip_level_count: 1,
551        sample_count: 1,
552        dimension: wgpu::TextureDimension::D2,
553        format,
554        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
555            | wgpu::TextureUsages::TEXTURE_BINDING
556            | wgpu::TextureUsages::COPY_SRC,
557        view_formats: &[],
558    });
559    let view = texture.create_view(&Default::default());
560    (texture, view)
561}
562
563fn texture_entry(
564    binding: u32,
565    sample_type: wgpu::TextureSampleType,
566    dimension: wgpu::TextureViewDimension,
567    visibility: wgpu::ShaderStages,
568) -> wgpu::BindGroupLayoutEntry {
569    wgpu::BindGroupLayoutEntry {
570        binding,
571        visibility,
572        ty: wgpu::BindingType::Texture {
573            sample_type,
574            view_dimension: dimension,
575            multisampled: false,
576        },
577        count: None,
578    }
579}
580
581struct PipelineSpec<'a> {
582    layout: &'a wgpu::PipelineLayout,
583    vertex: &'a str,
584    fragment: &'a str,
585    targets: &'a [Option<wgpu::ColorTargetState>],
586    cull: Option<wgpu::Face>,
587    depth: Option<wgpu::DepthStencilState>,
588}
589
590fn pipeline(
591    device: &wgpu::Device,
592    shader: &wgpu::ShaderModule,
593    spec: PipelineSpec<'_>,
594) -> wgpu::RenderPipeline {
595    let PipelineSpec {
596        layout,
597        vertex,
598        fragment,
599        targets,
600        cull,
601        depth,
602    } = spec;
603    let attributes =
604        wgpu::vertex_attr_array![0 => Float32x4, 1 => Float32x4, 2 => Float32x4, 3 => Float32x4];
605    let buffers = [Some(wgpu::VertexBufferLayout {
606        array_stride: std::mem::size_of::<Vertex>() as u64,
607        step_mode: wgpu::VertexStepMode::Vertex,
608        attributes: &attributes,
609    })];
610    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
611        label: Some(fragment),
612        layout: Some(layout),
613        vertex: wgpu::VertexState {
614            module: shader,
615            entry_point: Some(vertex),
616            compilation_options: Default::default(),
617            buffers: if vertex == "surface_vertex" || vertex == "cast_vertex" {
618                &buffers
619            } else {
620                &[]
621            },
622        },
623        fragment: Some(wgpu::FragmentState {
624            module: shader,
625            entry_point: Some(fragment),
626            compilation_options: Default::default(),
627            targets,
628        }),
629        primitive: wgpu::PrimitiveState {
630            cull_mode: cull,
631            ..Default::default()
632        },
633        depth_stencil: depth,
634        multisample: Default::default(),
635        multiview_mask: None,
636        cache: None,
637    })
638}
639
640impl Pass {
641    pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
642        let size = [width.max(1), height.max(1)];
643        let (front, front_view) = target(device, size, HDR_FORMAT, "glass front normals and depth");
644        let (front_id, front_id_view) = target(
645            device,
646            size,
647            wgpu::TextureFormat::R32Uint,
648            "glass front ids",
649        );
650        let (front_look, front_look_view) = target(device, size, LOOK_FORMAT, "glass front look");
651        let (front_depth, front_depth_view) = target(
652            device,
653            size,
654            wgpu::TextureFormat::Depth32Float,
655            "glass front depth",
656        );
657        let (back, back_view) = target(device, size, HDR_FORMAT, "glass back normals and depth");
658        let (back_depth, back_depth_view) = target(
659            device,
660            size,
661            wgpu::TextureFormat::Depth32Float,
662            "glass back depth",
663        );
664        let (reactive, reactive_view) =
665            target(device, size, REACTIVE_FORMAT, "glass reactive mask");
666        let scene = device.create_buffer(&wgpu::BufferDescriptor {
667            label: Some("glass scene"),
668            size: std::mem::size_of::<SceneGpu>() as u64,
669            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
670            mapped_at_creation: false,
671        });
672        let objects = device.create_buffer(&wgpu::BufferDescriptor {
673            label: Some("glass objects"),
674            size: std::mem::size_of::<ObjectGpu>() as u64,
675            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
676            mapped_at_creation: false,
677        });
678        let scene_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
679            label: Some("glass scene layout"),
680            entries: &scene_entries(),
681        });
682        let id_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
683            label: Some("glass id layout"),
684            entries: &id_entries(),
685        });
686        let compose_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
687            label: Some("glass compose layout"),
688            entries: &compose_entries(),
689        });
690        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
691            label: Some("glass WGSL"),
692            source: wgpu::ShaderSource::Wgsl(glass_source().into()),
693        });
694        let front_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
695            label: Some("glass front pipeline layout"),
696            bind_group_layouts: &[Some(&scene_layout)],
697            immediate_size: 0,
698        });
699        let back_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
700            label: Some("glass back pipeline layout"),
701            bind_group_layouts: &[Some(&scene_layout), Some(&id_layout)],
702            immediate_size: 0,
703        });
704        let compose_pipeline_layout =
705            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
706                label: Some("glass compose pipeline layout"),
707                bind_group_layouts: &[Some(&scene_layout), Some(&id_layout), Some(&compose_layout)],
708                immediate_size: 0,
709            });
710        let geometry_targets = [
711            Some(wgpu::ColorTargetState {
712                format: HDR_FORMAT,
713                blend: None,
714                write_mask: wgpu::ColorWrites::ALL,
715            }),
716            Some(wgpu::ColorTargetState {
717                format: wgpu::TextureFormat::R32Uint,
718                blend: None,
719                write_mask: wgpu::ColorWrites::ALL,
720            }),
721            Some(wgpu::ColorTargetState {
722                format: LOOK_FORMAT,
723                blend: None,
724                write_mask: wgpu::ColorWrites::ALL,
725            }),
726        ];
727        let front_pipe = pipeline(
728            device,
729            &shader,
730            PipelineSpec {
731                layout: &front_layout,
732                vertex: "surface_vertex",
733                fragment: "front_fragment",
734                targets: &geometry_targets,
735                cull: Some(wgpu::Face::Back),
736                depth: Some(wgpu::DepthStencilState {
737                    format: wgpu::TextureFormat::Depth32Float,
738                    depth_write_enabled: Some(true),
739                    depth_compare: Some(wgpu::CompareFunction::Less),
740                    stencil: Default::default(),
741                    bias: Default::default(),
742                }),
743            },
744        );
745        let back_pipe = pipeline(
746            device,
747            &shader,
748            PipelineSpec {
749                layout: &back_layout,
750                vertex: "surface_vertex",
751                fragment: "back_fragment",
752                targets: &geometry_targets[..1],
753                cull: Some(wgpu::Face::Front),
754                depth: Some(wgpu::DepthStencilState {
755                    format: wgpu::TextureFormat::Depth32Float,
756                    depth_write_enabled: Some(true),
757                    depth_compare: Some(wgpu::CompareFunction::Greater),
758                    stencil: Default::default(),
759                    bias: Default::default(),
760                }),
761            },
762        );
763        let compose_targets = [
764            Some(wgpu::ColorTargetState {
765                format: HDR_FORMAT,
766                blend: None,
767                write_mask: wgpu::ColorWrites::ALL,
768            }),
769            Some(wgpu::ColorTargetState {
770                format: REACTIVE_FORMAT,
771                blend: None,
772                write_mask: wgpu::ColorWrites::ALL,
773            }),
774        ];
775        let compose_pipe = pipeline(
776            device,
777            &shader,
778            PipelineSpec {
779                layout: &compose_pipeline_layout,
780                vertex: "fullscreen_vertex",
781                fragment: "compose_fragment",
782                targets: &compose_targets,
783                cull: None,
784                depth: None,
785            },
786        );
787        let pick_pipe = pipeline(
788            device,
789            &shader,
790            PipelineSpec {
791                layout: &compose_pipeline_layout,
792                vertex: "fullscreen_vertex",
793                fragment: "pick_fragment",
794                targets: &[Some(wgpu::ColorTargetState {
795                    format: wgpu::TextureFormat::R32Uint,
796                    blend: None,
797                    write_mask: wgpu::ColorWrites::ALL,
798                })],
799                cull: None,
800                depth: None,
801            },
802        );
803        let cast_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
804            label: Some("glass cast layout"),
805            entries: &cast_entries(),
806        });
807        let cast_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
808            label: Some("glass cast WGSL"),
809            source: wgpu::ShaderSource::Wgsl(CAST_WGSL.into()),
810        });
811        let cast_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
812            label: Some("glass cast pipeline layout"),
813            bind_group_layouts: &[Some(&cast_layout)],
814            immediate_size: 0,
815        });
816        let cast_pipe = pipeline(
817            device,
818            &cast_shader,
819            PipelineSpec {
820                layout: &cast_pipeline_layout,
821                vertex: "cast_vertex",
822                fragment: "cast_fragment",
823                targets: &[Some(wgpu::ColorTargetState {
824                    format: crate::shadow::TRANSMISSION_FORMAT,
825                    blend: None,
826                    write_mask: wgpu::ColorWrites::ALL,
827                })],
828                cull: None,
829                depth: Some(wgpu::DepthStencilState {
830                    format: wgpu::TextureFormat::Depth32Float,
831                    depth_write_enabled: Some(true),
832                    depth_compare: Some(wgpu::CompareFunction::Less),
833                    stencil: Default::default(),
834                    bias: Default::default(),
835                }),
836            },
837        );
838        let cast_capacity = crate::shadow::CASCADE_COUNT;
839        let cast_objects = cast_buffer(device, cast_capacity);
840        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
841            label: Some("glass sampler"),
842            address_mode_u: wgpu::AddressMode::ClampToEdge,
843            address_mode_v: wgpu::AddressMode::ClampToEdge,
844            mag_filter: wgpu::FilterMode::Linear,
845            min_filter: wgpu::FilterMode::Linear,
846            mipmap_filter: wgpu::MipmapFilterMode::Linear,
847            ..Default::default()
848        });
849        Self {
850            width: size[0],
851            height: size[1],
852            viewport: (size[0], size[1]),
853            _front: front,
854            front_view,
855            _front_id: front_id,
856            front_id_view,
857            _front_look: front_look,
858            front_look_view,
859            _front_depth: front_depth,
860            front_depth_view,
861            _back: back,
862            back_view,
863            _back_depth: back_depth,
864            back_depth_view,
865            _reactive: reactive,
866            reactive_view,
867            scene,
868            objects,
869            capacity: 1,
870            scene_layout,
871            id_layout,
872            compose_layout,
873            sampler,
874            front_pipe,
875            back_pipe,
876            compose_pipe,
877            pick_pipe,
878            picks: None,
879            cast_layout,
880            cast_pipe,
881            cast_objects,
882            cast_capacity,
883            casts: None,
884            meshes: GlassMeshes::default(),
885        }
886    }
887
888    pub fn prepare_casts(
889        &mut self,
890        device: &wgpu::Device,
891        queue: &wgpu::Queue,
892        layers: &[Matrix],
893        surfaces: &[Surface],
894        fluid_height: &wgpu::TextureView,
895        ripple_height: &wgpu::TextureView,
896    ) -> Result<bool, String> {
897        self.casts = None;
898        let casting: Vec<&Surface> = surfaces
899            .iter()
900            .filter(|surface| surface.casts_shadow)
901            .collect();
902        if casting.is_empty() || layers.is_empty() {
903            return Ok(false);
904        }
905        for surface in &casting {
906            if self.meshes.get(surface.mesh).is_none() {
907                return Err("glass surface refers to a missing or released mesh".into());
908            }
909            if !surface.clip.iter().flatten().all(|value| value.is_finite()) {
910                return Err("glass clip planes must be finite".into());
911            }
912        }
913        let rows: Vec<CastGpu> = layers
914            .iter()
915            .flat_map(|view_proj| {
916                casting.iter().map(move |surface| {
917                    let tint = cast_tint(&surface.material);
918                    let tinted = cast_tint(&surface.tinted.unwrap_or(surface.material));
919                    CastGpu {
920                        model: surface.model,
921                        view_proj: *view_proj,
922                        tint: [tint[0], tint[1], tint[2], 1.0],
923                        tinted: [tinted[0], tinted[1], tinted[2], 1.0],
924                        liquid: [
925                            if surface.liquid { 1.0 } else { 0.0 },
926                            surface.fluid_height,
927                            surface.ripple_height,
928                            0.0,
929                        ],
930                        clip: surface.clip,
931                    }
932                })
933            })
934            .collect();
935        if rows.len() > self.cast_capacity {
936            self.cast_capacity = rows.len().next_power_of_two();
937            self.cast_objects = cast_buffer(device, self.cast_capacity);
938        }
939        queue.write_buffer(&self.cast_objects, 0, bytemuck::cast_slice(&rows));
940        let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
941            label: Some("glass cast group"),
942            layout: &self.cast_layout,
943            entries: &[
944                wgpu::BindGroupEntry {
945                    binding: 0,
946                    resource: self.cast_objects.as_entire_binding(),
947                },
948                wgpu::BindGroupEntry {
949                    binding: 1,
950                    resource: wgpu::BindingResource::TextureView(fluid_height),
951                },
952                wgpu::BindGroupEntry {
953                    binding: 2,
954                    resource: wgpu::BindingResource::TextureView(ripple_height),
955                },
956            ],
957        });
958        self.casts = Some(Casts {
959            group,
960            draws: casting.iter().map(|surface| surface.mesh).collect(),
961        });
962        Ok(true)
963    }
964
965    pub fn draw_casts(&self, pass: &mut wgpu::RenderPass<'_>, layer: usize) {
966        let Some(casts) = &self.casts else {
967            return;
968        };
969        pass.set_pipeline(&self.cast_pipe);
970        pass.set_bind_group(0, &casts.group, &[]);
971        let first = (layer * casts.draws.len()) as u32;
972        for (index, handle) in casts.draws.iter().enumerate() {
973            let Some(mesh) = self.meshes.get(*handle) else {
974                continue;
975            };
976            let row = first + index as u32;
977            pass.set_vertex_buffer(0, mesh.vertices.slice(..));
978            pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
979            pass.draw_indexed(0..mesh.count, 0, row..row + 1);
980        }
981    }
982
983    pub fn reactive(&self) -> &wgpu::TextureView {
984        &self.reactive_view
985    }
986    pub fn front_depth(&self) -> &wgpu::TextureView {
987        &self.front_depth_view
988    }
989    pub fn front_id(&self) -> &wgpu::TextureView {
990        &self.front_id_view
991    }
992    pub fn front_look(&self) -> &wgpu::TextureView {
993        &self.front_look_view
994    }
995    pub fn mesh_buffers(&self, handle: MeshHandle) -> Option<(&wgpu::Buffer, &wgpu::Buffer, u32)> {
996        self.meshes
997            .get(handle)
998            .map(|mesh| (&*mesh.vertices, &*mesh.indices, mesh.count))
999    }
1000
1001    pub fn mesh_stats(&self) -> GlassStats {
1002        self.meshes.stats()
1003    }
1004
1005    pub fn viewport(&self) -> (u32, u32) {
1006        self.viewport
1007    }
1008
1009    pub fn textures(&self) -> Vec<wgpu::Texture> {
1010        vec![
1011            self._front.clone(),
1012            self._front_id.clone(),
1013            self._front_look.clone(),
1014            self._front_depth.clone(),
1015            self._back.clone(),
1016            self._back_depth.clone(),
1017            self._reactive.clone(),
1018        ]
1019    }
1020
1021    pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
1022        crate::viewport::check([width, height], [self.width, self.height])?;
1023        self.viewport = (width, height);
1024        Ok(())
1025    }
1026
1027    pub fn take_meshes(&mut self) -> GlassMeshes {
1028        std::mem::take(&mut self.meshes)
1029    }
1030
1031    pub fn set_meshes(&mut self, meshes: GlassMeshes) {
1032        self.meshes = meshes;
1033    }
1034
1035    pub fn release_mesh(&mut self, handle: MeshHandle) -> Result<(), String> {
1036        self.meshes.release(handle)
1037    }
1038
1039    pub fn replace_mesh(
1040        &mut self,
1041        device: &wgpu::Device,
1042        queue: &wgpu::Queue,
1043        handle: MeshHandle,
1044        mesh: &Mesh,
1045    ) -> Result<(), String> {
1046        self.meshes.replace(device, queue, handle, mesh)
1047    }
1048    pub fn front_normals(&self) -> &wgpu::TextureView {
1049        &self.front_view
1050    }
1051    pub fn back_normals(&self) -> &wgpu::TextureView {
1052        &self.back_view
1053    }
1054    pub fn back_depth(&self) -> &wgpu::TextureView {
1055        &self.back_depth_view
1056    }
1057
1058    pub fn upload_mesh(
1059        &mut self,
1060        device: &wgpu::Device,
1061        mesh: &Mesh,
1062    ) -> Result<MeshHandle, String> {
1063        self.meshes.upload(device, mesh)
1064    }
1065
1066    pub fn liquid_grid(columns: u32, rows: u32, half_extent: [f32; 2], depth: f32) -> Mesh {
1067        let columns = columns.max(1);
1068        let rows = rows.max(1);
1069        let mut positions = Vec::new();
1070        let mut normals = Vec::new();
1071        let mut uvs = Vec::new();
1072        let mut indices = Vec::new();
1073        let count = (columns + 1) * (rows + 1);
1074        for layer in 0..2 {
1075            for y in 0..=rows {
1076                for x in 0..=columns {
1077                    let uv = [x as f32 / columns as f32, y as f32 / rows as f32];
1078                    positions.push([
1079                        (uv[0] * 2.0 - 1.0) * half_extent[0],
1080                        (uv[1] * 2.0 - 1.0) * half_extent[1],
1081                        if layer == 0 { 0.0 } else { -depth.max(0.001) },
1082                    ]);
1083                    normals.push(if layer == 0 {
1084                        [0.0, 0.0, 1.0]
1085                    } else {
1086                        [0.0, 0.0, -1.0]
1087                    });
1088                    uvs.push(uv);
1089                }
1090            }
1091        }
1092        for y in 0..rows {
1093            for x in 0..columns {
1094                let a = y * (columns + 1) + x;
1095                let b = a + 1;
1096                let c = a + columns + 1;
1097                let d = c + 1;
1098                indices.extend([a, b, d, a, d, c]);
1099                indices.extend([
1100                    a + count,
1101                    d + count,
1102                    b + count,
1103                    a + count,
1104                    c + count,
1105                    d + count,
1106                ]);
1107            }
1108        }
1109        let perimeter = |x: u32, y: u32| y * (columns + 1) + x;
1110        for x in 0..columns {
1111            for (a, b) in [
1112                (perimeter(x, 0), perimeter(x + 1, 0)),
1113                (perimeter(x + 1, rows), perimeter(x, rows)),
1114            ] {
1115                indices.extend([a, a + count, b + count, a, b + count, b]);
1116            }
1117        }
1118        for y in 0..rows {
1119            for (a, b) in [
1120                (perimeter(columns, y), perimeter(columns, y + 1)),
1121                (perimeter(0, y + 1), perimeter(0, y)),
1122            ] {
1123                indices.extend([a, a + count, b + count, a, b + count, b]);
1124            }
1125        }
1126        Mesh::new(positions, normals, Vec::new(), uvs, indices)
1127    }
1128
1129    fn ensure_capacity(&mut self, device: &wgpu::Device, count: usize) {
1130        if count <= self.capacity {
1131            return;
1132        }
1133        self.capacity = count.next_power_of_two();
1134        self.objects = device.create_buffer(&wgpu::BufferDescriptor {
1135            label: Some("glass objects"),
1136            size: (self.capacity * std::mem::size_of::<ObjectGpu>()) as u64,
1137            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1138            mapped_at_creation: false,
1139        });
1140    }
1141
1142    pub fn encode(
1143        &mut self,
1144        device: &wgpu::Device,
1145        queue: &wgpu::Queue,
1146        encoder: &mut wgpu::CommandEncoder,
1147        Render {
1148            camera,
1149            surfaces,
1150            inputs,
1151            output,
1152            profiler,
1153        }: Render<'_>,
1154    ) -> Result<(), String> {
1155        let drawn: Vec<Surface> = surfaces
1156            .iter()
1157            .filter(|surface| !surface.shadow_only)
1158            .copied()
1159            .collect();
1160        let surfaces = drawn.as_slice();
1161        if surfaces.len() > 4095 {
1162            return Err("glass pass supports at most 4095 surfaces".into());
1163        }
1164        let view_det = dot(
1165            [camera.view[0][0], camera.view[0][1], camera.view[0][2]],
1166            cross(
1167                [camera.view[1][0], camera.view[1][1], camera.view[1][2]],
1168                [camera.view[2][0], camera.view[2][1], camera.view[2][2]],
1169            ),
1170        );
1171        if !view_det.is_finite() || view_det.abs() < 1e-8 {
1172            return Err("glass camera has a singular view transform".into());
1173        }
1174        for surface in surfaces {
1175            if self.meshes.get(surface.mesh).is_none() {
1176                return Err("glass surface refers to a missing or released mesh".into());
1177            }
1178            let mv = multiply(camera.view, surface.model);
1179            let determinant = dot(
1180                [mv[0][0], mv[0][1], mv[0][2]],
1181                cross(
1182                    [mv[1][0], mv[1][1], mv[1][2]],
1183                    [mv[2][0], mv[2][1], mv[2][2]],
1184                ),
1185            );
1186            if !determinant.is_finite() || determinant.abs() < 1e-8 {
1187                return Err("glass model has a singular transform".into());
1188            }
1189            if !surface.clip.iter().flatten().all(|value| value.is_finite()) {
1190                return Err("glass clip planes must be finite".into());
1191            }
1192        }
1193        self.ensure_capacity(device, surfaces.len());
1194        let meshes: Vec<&MeshGpu> = surfaces
1195            .iter()
1196            .filter_map(|surface| self.meshes.get(surface.mesh))
1197            .collect();
1198        let scene = SceneGpu {
1199            projection: camera.projection,
1200            world_from_view: world_from_view(camera.view),
1201            extent: [
1202                self.viewport.0 as f32,
1203                self.viewport.1 as f32,
1204                inputs.mip_count.max(1) as f32,
1205                0.0,
1206            ],
1207            caustic: CausticProjection::rows(inputs.caustic_projection, 0, 0.0),
1208            sky: inputs.sky_sh.map(|c| [c[0], c[1], c[2], 0.0]),
1209        };
1210        queue.write_buffer(&self.scene, 0, bytemuck::bytes_of(&scene));
1211        let objects: Vec<_> = surfaces
1212            .iter()
1213            .map(|surface| {
1214                let mv = multiply(camera.view, surface.model);
1215                let m = surface.material;
1216                let t = surface.tinted.unwrap_or(m);
1217                ObjectGpu {
1218                    model_view: mv,
1219                    mvp: multiply(camera.projection, mv),
1220                    normal: normal_columns(mv),
1221                    base_roughness: [m.base[0], m.base[1], m.base[2], m.roughness],
1222                    tint_absorption: [
1223                        m.subsurface_tint[0],
1224                        m.subsurface_tint[1],
1225                        m.subsurface_tint[2],
1226                        m.absorption,
1227                    ],
1228                    optics: [m.ior, m.dispersion, m.transmission, m.subsurface],
1229                    film: [
1230                        m.thin_film,
1231                        m.thin_film_ior,
1232                        m.thin_film_amount,
1233                        surface.caustic_strength,
1234                    ],
1235                    liquid: [
1236                        if surface.liquid { 1.0 } else { 0.0 },
1237                        surface.fluid_height,
1238                        surface.ripple_height,
1239                        0.0,
1240                    ],
1241                    tinted_base: [t.base[0], t.base[1], t.base[2], t.thin_film_amount],
1242                    tinted_subsurface: [
1243                        t.subsurface_tint[0],
1244                        t.subsurface_tint[1],
1245                        t.subsurface_tint[2],
1246                        t.subsurface,
1247                    ],
1248                    pick: [surface.id, 0, 0, 0],
1249                    clip: surface.clip,
1250                }
1251            })
1252            .collect();
1253        if !objects.is_empty() {
1254            queue.write_buffer(&self.objects, 0, bytemuck::cast_slice(&objects));
1255        }
1256        let scene_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1257            label: Some("glass scene group"),
1258            layout: &self.scene_layout,
1259            entries: &[
1260                wgpu::BindGroupEntry {
1261                    binding: 0,
1262                    resource: self.scene.as_entire_binding(),
1263                },
1264                wgpu::BindGroupEntry {
1265                    binding: 1,
1266                    resource: self.objects.as_entire_binding(),
1267                },
1268                wgpu::BindGroupEntry {
1269                    binding: 2,
1270                    resource: wgpu::BindingResource::TextureView(inputs.fluid_height),
1271                },
1272                wgpu::BindGroupEntry {
1273                    binding: 3,
1274                    resource: wgpu::BindingResource::TextureView(inputs.ripple_height),
1275                },
1276            ],
1277        });
1278        let id_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1279            label: Some("glass front id group"),
1280            layout: &self.id_layout,
1281            entries: &[wgpu::BindGroupEntry {
1282                binding: 0,
1283                resource: wgpu::BindingResource::TextureView(&self.front_id_view),
1284            }],
1285        });
1286        let compose_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1287            label: Some("glass compose group"),
1288            layout: &self.compose_layout,
1289            entries: &[
1290                wgpu::BindGroupEntry {
1291                    binding: 0,
1292                    resource: wgpu::BindingResource::TextureView(&self.front_view),
1293                },
1294                wgpu::BindGroupEntry {
1295                    binding: 1,
1296                    resource: wgpu::BindingResource::TextureView(&self.back_view),
1297                },
1298                wgpu::BindGroupEntry {
1299                    binding: 2,
1300                    resource: wgpu::BindingResource::TextureView(&self.front_depth_view),
1301                },
1302                wgpu::BindGroupEntry {
1303                    binding: 3,
1304                    resource: wgpu::BindingResource::TextureView(inputs.opaque_hdr_mips),
1305                },
1306                wgpu::BindGroupEntry {
1307                    binding: 4,
1308                    resource: wgpu::BindingResource::TextureView(inputs.reflection_cube),
1309                },
1310                wgpu::BindGroupEntry {
1311                    binding: 5,
1312                    resource: wgpu::BindingResource::TextureView(inputs.caustic),
1313                },
1314                wgpu::BindGroupEntry {
1315                    binding: 6,
1316                    resource: wgpu::BindingResource::TextureView(inputs.opaque_depth),
1317                },
1318                wgpu::BindGroupEntry {
1319                    binding: 7,
1320                    resource: wgpu::BindingResource::Sampler(&self.sampler),
1321                },
1322                wgpu::BindGroupEntry {
1323                    binding: 8,
1324                    resource: wgpu::BindingResource::TextureView(&self.front_look_view),
1325                },
1326            ],
1327        });
1328        let shown: Vec<Option<[u32; 4]>> = surfaces
1329            .iter()
1330            .zip(&objects)
1331            .zip(&meshes)
1332            .map(|((surface, object), mesh)| {
1333                self.bounds(
1334                    &object.mvp,
1335                    &mesh.bounds,
1336                    surface.fluid_height.abs() + surface.ripple_height.abs(),
1337                )
1338            })
1339            .collect();
1340        let rects: Vec<[u32; 4]> = shown.iter().flatten().copied().collect();
1341        let mut profiler = profiler;
1342        let front_time = profiler.as_deref_mut().and_then(|p| p.pass("glass front"));
1343        let back_time = profiler.as_deref_mut().and_then(|p| p.pass("glass back"));
1344        let compose_time = profiler
1345            .as_deref_mut()
1346            .and_then(|p| p.pass("glass compose"));
1347        {
1348            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1349                multiview_mask: None,
1350                label: Some("glass front"),
1351                color_attachments: &[
1352                    Some(wgpu::RenderPassColorAttachment {
1353                        view: &self.front_view,
1354                        resolve_target: None,
1355                        ops: wgpu::Operations {
1356                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1357                            store: wgpu::StoreOp::Store,
1358                        },
1359                        depth_slice: None,
1360                    }),
1361                    Some(wgpu::RenderPassColorAttachment {
1362                        view: &self.front_id_view,
1363                        resolve_target: None,
1364                        ops: wgpu::Operations {
1365                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1366                            store: wgpu::StoreOp::Store,
1367                        },
1368                        depth_slice: None,
1369                    }),
1370                    Some(wgpu::RenderPassColorAttachment {
1371                        view: &self.front_look_view,
1372                        resolve_target: None,
1373                        ops: wgpu::Operations {
1374                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1375                            store: wgpu::StoreOp::Store,
1376                        },
1377                        depth_slice: None,
1378                    }),
1379                ],
1380                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1381                    view: &self.front_depth_view,
1382                    depth_ops: Some(wgpu::Operations {
1383                        load: wgpu::LoadOp::Clear(1.0),
1384                        store: wgpu::StoreOp::Store,
1385                    }),
1386                    stencil_ops: None,
1387                }),
1388                occlusion_query_set: None,
1389                timestamp_writes: profiler
1390                    .as_deref()
1391                    .and_then(|p| p.render_writes(front_time)),
1392            });
1393            crate::viewport::apply(&mut pass, [self.viewport.0, self.viewport.1]);
1394            pass.set_pipeline(&self.front_pipe);
1395            pass.set_bind_group(0, &scene_group, &[]);
1396            for (index, mesh) in meshes.iter().enumerate() {
1397                pass.set_vertex_buffer(0, mesh.vertices.slice(..));
1398                pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
1399                pass.draw_indexed(0..mesh.count, 0, index as u32..index as u32 + 1);
1400            }
1401        }
1402        {
1403            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1404                multiview_mask: None,
1405                label: Some("glass back"),
1406                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1407                    view: &self.back_view,
1408                    resolve_target: None,
1409                    ops: wgpu::Operations {
1410                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1411                        store: wgpu::StoreOp::Store,
1412                    },
1413                    depth_slice: None,
1414                })],
1415                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1416                    view: &self.back_depth_view,
1417                    depth_ops: Some(wgpu::Operations {
1418                        load: wgpu::LoadOp::Clear(0.0),
1419                        store: wgpu::StoreOp::Store,
1420                    }),
1421                    stencil_ops: None,
1422                }),
1423                occlusion_query_set: None,
1424                timestamp_writes: profiler.as_deref().and_then(|p| p.render_writes(back_time)),
1425            });
1426            crate::viewport::apply(&mut pass, [self.viewport.0, self.viewport.1]);
1427            pass.set_pipeline(&self.back_pipe);
1428            pass.set_bind_group(0, &scene_group, &[]);
1429            pass.set_bind_group(1, &id_group, &[]);
1430            for (index, mesh) in meshes.iter().enumerate() {
1431                pass.set_vertex_buffer(0, mesh.vertices.slice(..));
1432                pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
1433                pass.draw_indexed(0..mesh.count, 0, index as u32..index as u32 + 1);
1434            }
1435        }
1436        {
1437            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1438                multiview_mask: None,
1439                label: Some("glass composite"),
1440                color_attachments: &[
1441                    Some(wgpu::RenderPassColorAttachment {
1442                        view: output,
1443                        resolve_target: None,
1444                        ops: wgpu::Operations {
1445                            load: wgpu::LoadOp::Load,
1446                            store: wgpu::StoreOp::Store,
1447                        },
1448                        depth_slice: None,
1449                    }),
1450                    Some(wgpu::RenderPassColorAttachment {
1451                        view: &self.reactive_view,
1452                        resolve_target: None,
1453                        ops: wgpu::Operations {
1454                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1455                            store: wgpu::StoreOp::Store,
1456                        },
1457                        depth_slice: None,
1458                    }),
1459                ],
1460                depth_stencil_attachment: None,
1461                occlusion_query_set: None,
1462                timestamp_writes: profiler
1463                    .as_deref()
1464                    .and_then(|p| p.render_writes(compose_time)),
1465            });
1466            crate::viewport::apply(&mut pass, [self.viewport.0, self.viewport.1]);
1467            pass.set_pipeline(&self.compose_pipe);
1468            pass.set_bind_group(0, &scene_group, &[]);
1469            pass.set_bind_group(1, &id_group, &[]);
1470            pass.set_bind_group(2, &compose_group, &[]);
1471            for rect in &rects {
1472                pass.set_scissor_rect(rect[0], rect[1], rect[2], rect[3]);
1473                pass.draw(0..3, 0..1);
1474            }
1475        }
1476        let picked: Vec<[u32; 4]> = surfaces
1477            .iter()
1478            .zip(&shown)
1479            .filter_map(|(surface, rect)| (surface.id != 0).then_some(*rect).flatten())
1480            .collect();
1481        self.picks = (!picked.is_empty()).then_some(Picks {
1482            scene: scene_group,
1483            ids: id_group,
1484            compose: compose_group,
1485            rects: picked,
1486        });
1487        Ok(())
1488    }
1489
1490    pub fn has_ids(&self) -> bool {
1491        self.picks.is_some()
1492    }
1493
1494    pub fn encode_ids(
1495        &self,
1496        encoder: &mut wgpu::CommandEncoder,
1497        ids: &wgpu::TextureView,
1498        profiler: Option<&mut GpuProfiler>,
1499    ) {
1500        let Some(picks) = &self.picks else {
1501            return;
1502        };
1503        let mut profiler = profiler;
1504        let timing = profiler.as_deref_mut().and_then(|p| p.pass("glass ids"));
1505        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1506            multiview_mask: None,
1507            label: Some("glass ids"),
1508            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1509                view: ids,
1510                resolve_target: None,
1511                ops: wgpu::Operations {
1512                    load: wgpu::LoadOp::Load,
1513                    store: wgpu::StoreOp::Store,
1514                },
1515                depth_slice: None,
1516            })],
1517            depth_stencil_attachment: None,
1518            occlusion_query_set: None,
1519            timestamp_writes: profiler.as_deref().and_then(|p| p.render_writes(timing)),
1520        });
1521        crate::viewport::apply(&mut pass, [self.viewport.0, self.viewport.1]);
1522        pass.set_pipeline(&self.pick_pipe);
1523        pass.set_bind_group(0, &picks.scene, &[]);
1524        pass.set_bind_group(1, &picks.ids, &[]);
1525        pass.set_bind_group(2, &picks.compose, &[]);
1526        for rect in &picks.rects {
1527            pass.set_scissor_rect(rect[0], rect[1], rect[2], rect[3]);
1528            pass.draw(0..3, 0..1);
1529        }
1530    }
1531
1532    fn bounds(&self, mvp: &Matrix, bounds: &[[f32; 3]; 2], padding: f32) -> Option<[u32; 4]> {
1533        let (width, height) = self.viewport;
1534        let mut low = [f32::INFINITY; 2];
1535        let mut high = [f32::NEG_INFINITY; 2];
1536        let mut behind = false;
1537        let mut visible = false;
1538        for z in 0..2 {
1539            for y in 0..2 {
1540                for x in 0..2 {
1541                    let p = [
1542                        bounds[x][0],
1543                        bounds[y][1],
1544                        bounds[z][2] + if z == 1 { padding } else { -padding },
1545                        1.0,
1546                    ];
1547                    let clip = transform(*mvp, p);
1548                    if clip[3] <= 0.0 {
1549                        behind = true;
1550                        continue;
1551                    }
1552                    visible = true;
1553                    let uv = [
1554                        (clip[0] / clip[3] * 0.5 + 0.5) * width as f32,
1555                        (0.5 - clip[1] / clip[3] * 0.5) * height as f32,
1556                    ];
1557                    for i in 0..2 {
1558                        low[i] = low[i].min(uv[i]);
1559                        high[i] = high[i].max(uv[i]);
1560                    }
1561                }
1562            }
1563        }
1564        if !visible {
1565            return None;
1566        }
1567        if behind {
1568            return Some([0, 0, width, height]);
1569        }
1570        let x0 = (low[0].floor() - 2.0).clamp(0.0, width as f32) as u32;
1571        let y0 = (low[1].floor() - 2.0).clamp(0.0, height as f32) as u32;
1572        let x1 = (high[0].ceil() + 2.0).clamp(0.0, width as f32) as u32;
1573        let y1 = (high[1].ceil() + 2.0).clamp(0.0, height as f32) as u32;
1574        (x1 > x0 && y1 > y0).then_some([x0, y0, x1 - x0, y1 - y0])
1575    }
1576}
1577
1578pub fn glass_source() -> String {
1579    format!("{}\n{}", pfx_materials::FILM, GLASS_WGSL)
1580}
1581
1582pub const GLASS_WGSL: &str = r#"
1583struct Scene {
1584    projection: mat4x4f,
1585    world0: vec4f,
1586    world1: vec4f,
1587    world2: vec4f,
1588    extent: vec4f,
1589    caustic_rect: vec4f,
1590    caustic_plane: vec4f,
1591    caustic_info: vec4f,
1592    sky: array<vec4f, 9>,
1593}
1594struct Object {
1595    model_view: mat4x4f,
1596    mvp: mat4x4f,
1597    normal0: vec4f,
1598    normal1: vec4f,
1599    normal2: vec4f,
1600    base_roughness: vec4f,
1601    tint_absorption: vec4f,
1602    optics: vec4f,
1603    film: vec4f,
1604    liquid: vec4f,
1605    tinted_base: vec4f,
1606    tinted_subsurface: vec4f,
1607    pick: vec4u,
1608    clip0: vec4f,
1609    clip1: vec4f,
1610}
1611@group(0) @binding(0) var<uniform> scene: Scene;
1612@group(0) @binding(1) var<storage, read> objects: array<Object>;
1613@group(0) @binding(2) var fluid_tex: texture_2d<f32>;
1614@group(0) @binding(3) var ripple_tex: texture_2d<f32>;
1615@group(1) @binding(0) var front_id_tex: texture_2d<u32>;
1616@group(2) @binding(0) var front_tex: texture_2d<f32>;
1617@group(2) @binding(1) var back_tex: texture_2d<f32>;
1618@group(2) @binding(2) var front_depth_tex: texture_depth_2d;
1619@group(2) @binding(3) var opaque_tex: texture_2d<f32>;
1620@group(2) @binding(4) var reflection_tex: texture_cube<f32>;
1621@group(2) @binding(5) var caustic_tex: texture_2d<f32>;
1622@group(2) @binding(6) var opaque_depth_tex: texture_depth_2d;
1623@group(2) @binding(7) var linear_sampler: sampler;
1624@group(2) @binding(8) var front_look_tex: texture_2d<f32>;
1625
1626struct SurfaceVertex {
1627    @location(0) position: vec4f,
1628    @location(1) normal: vec4f,
1629    @location(2) uv: vec4f,
1630    @location(3) look: vec4f,
1631}
1632struct SurfaceOut {
1633    @builtin(position) position: vec4f,
1634    @location(0) view_normal: vec3f,
1635    @location(1) view_depth: f32,
1636    @location(2) @interpolate(flat) object_id: u32,
1637    @location(3) look: vec4f,
1638    @location(4) world: vec3f,
1639}
1640struct FrontOut {
1641    @location(0) normal_depth: vec4f,
1642    @location(1) id: u32,
1643    @location(2) look: vec4f,
1644}
1645struct ComposeOut {
1646    @location(0) color: vec4f,
1647    @location(1) reactive: f32,
1648}
1649fn height_at(uv: vec2f, object: Object) -> f32 {
1650    let fluid_size = vec2f(textureDimensions(fluid_tex));
1651    let ripple_size = vec2f(textureDimensions(ripple_tex));
1652    let fluid_xy = vec2i(clamp(uv * fluid_size, vec2f(0.0), fluid_size - vec2f(1.0)));
1653    let ripple_xy = vec2i(clamp(uv * ripple_size, vec2f(0.0), ripple_size - vec2f(1.0)));
1654    return textureLoad(fluid_tex, fluid_xy, 0).r * object.liquid.y + textureLoad(ripple_tex, ripple_xy, 0).r * object.liquid.z;
1655}
1656fn to_world(view_point: vec3f) -> vec3f {
1657    return mat3x3f(scene.world0.xyz, scene.world1.xyz, scene.world2.xyz) * view_point + vec3f(scene.world0.w, scene.world1.w, scene.world2.w);
1658}
1659fn clip_plane(plane: vec4f, world: vec3f) -> bool {
1660    return any(plane.xyz != vec3f(0.0)) && dot(plane.xyz, world) > plane.w;
1661}
1662fn clipped(object: Object, world: vec3f) -> bool {
1663    return clip_plane(object.clip0, world) || clip_plane(object.clip1, world);
1664}
1665fn cap(plane: vec4f, world: vec3f, t: f32) -> f32 {
1666    if (!clip_plane(plane, world)) { return t; }
1667    let eye = vec3f(scene.world0.w, scene.world1.w, scene.world2.w);
1668    let along = dot(plane.xyz, world - eye);
1669    if (abs(along) < 1e-8) { return 0.0; }
1670    return min(t, (plane.w - dot(plane.xyz, eye)) / along);
1671}
1672@vertex fn surface_vertex(v: SurfaceVertex, @builtin(instance_index) index: u32) -> SurfaceOut {
1673    let object = objects[index];
1674    var p = v.position;
1675    var n = v.normal.xyz;
1676    if (object.liquid.x > 0.5 && n.z > 0.5) {
1677        let step = 1.0 / vec2f(textureDimensions(fluid_tex));
1678        let dx = height_at(v.uv.xy + vec2f(step.x, 0.0), object) - height_at(v.uv.xy - vec2f(step.x, 0.0), object);
1679        let dy = height_at(v.uv.xy + vec2f(0.0, step.y), object) - height_at(v.uv.xy - vec2f(0.0, step.y), object);
1680        p.z += height_at(v.uv.xy, object);
1681        n = normalize(vec3f(-dx / (2.0 * step.x), -dy / (2.0 * step.y), 1.0));
1682    }
1683    let view = object.model_view * p;
1684    var out: SurfaceOut;
1685    out.position = object.mvp * p;
1686    out.view_normal = normalize(mat3x3f(object.normal0.xyz, object.normal1.xyz, object.normal2.xyz) * n);
1687    out.view_depth = -view.z;
1688    out.object_id = index + 1u;
1689    out.look = v.look;
1690    out.world = to_world(view.xyz);
1691    return out;
1692}
1693@fragment fn front_fragment(input: SurfaceOut) -> FrontOut {
1694    if (input.look.z < 0.0) { discard; }
1695    if (clipped(objects[input.object_id - 1u], input.world)) { discard; }
1696    var out: FrontOut;
1697    out.normal_depth = vec4f(input.view_normal, input.view_depth);
1698    out.id = input.object_id;
1699    out.look = vec4f(input.look.xy, select(0.0, 1.0, input.look.w > 0.5), 0.0);
1700    return out;
1701}
1702@fragment fn back_fragment(input: SurfaceOut) -> @location(0) vec4f {
1703    let pixel = vec2i(input.position.xy);
1704    if (textureLoad(front_id_tex, pixel, 0).r != input.object_id) { discard; }
1705    let object = objects[input.object_id - 1u];
1706    if (!clipped(object, input.world)) {
1707        return vec4f(input.view_normal, input.view_depth);
1708    }
1709    let first = cap(object.clip0, input.world, 1.0);
1710    let t = cap(object.clip1, input.world, first);
1711    if (t <= 0.0) { discard; }
1712    var plane = object.clip1.xyz;
1713    if (t == first) {
1714        plane = object.clip0.xyz;
1715    }
1716    let normal = normalize(transpose(mat3x3f(scene.world0.xyz, scene.world1.xyz, scene.world2.xyz)) * plane);
1717    return vec4f(normal, input.view_depth * t);
1718}
1719@vertex fn fullscreen_vertex(@builtin(vertex_index) vertex: u32) -> @builtin(position) vec4f {
1720    let xy = vec2f(f32((vertex << 1u) & 2u), f32(vertex & 2u));
1721    return vec4f(xy * 2.0 - vec2f(1.0), 0.0, 1.0);
1722}
1723fn refract_ray(incident: vec3f, normal: vec3f, eta: f32) -> vec3f {
1724    let cosine = -dot(incident, normal);
1725    let k = 1.0 - eta * eta * (1.0 - cosine * cosine);
1726    if (k < 0.0) { return reflect(incident, normal); }
1727    return eta * incident + (eta * cosine - sqrt(k)) * normal;
1728}
1729fn landing(uv: vec2f, front_depth: f32, back_depth: f32, front_normal: vec3f, back_normal: vec3f, ior: f32, receiver_depth: f32) -> vec4f {
1730    let ndc = vec2f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
1731    let entry = vec3f(ndc * front_depth / vec2f(scene.projection[0].x, scene.projection[1].y), -front_depth);
1732    let incident = normalize(entry);
1733    let inside = refract_ray(incident, front_normal, 1.0 / ior);
1734    let travel = max(back_depth - front_depth, 0.0) / max(-inside.z, 0.1);
1735    let exit = entry + inside * travel;
1736    let outgoing = refract_ray(inside, -back_normal, ior);
1737    let reach = max(receiver_depth - back_depth, 0.0) / max(-outgoing.z, 0.1);
1738    let hit = exit + outgoing * reach;
1739    return vec4f(hit, max(receiver_depth, back_depth));
1740}
1741fn sample_uv(uv: vec2f, front_depth: f32, back_depth: f32, front_normal: vec3f, back_normal: vec3f, ior: f32, receiver_depth: f32) -> vec2f {
1742    let land = landing(uv, front_depth, back_depth, front_normal, back_normal, ior, receiver_depth);
1743    return vec2f(0.5 + land.x * scene.projection[0].x / (2.0 * land.w), 0.5 - land.y * scene.projection[1].y / (2.0 * land.w));
1744}
1745fn caustic_light(view_point: vec3f, strength: f32) -> vec3f {
1746    if (scene.caustic_info.z < 0.5 || strength == 0.0) {
1747        return vec3f(0.0);
1748    }
1749    let world = to_world(view_point);
1750    let toward = scene.caustic_plane.xyz;
1751    let along = (scene.caustic_plane.w - world.y) / max(toward.y, 1e-4);
1752    let at = (world.xz + toward.xz * along - scene.caustic_rect.xy) / scene.caustic_rect.zw;
1753    if (any(at < vec2f(0.0)) || any(at > vec2f(1.0))) {
1754        return vec3f(0.0);
1755    }
1756    return textureSampleLevel(caustic_tex, linear_sampler, at, 0.0).rgb * strength;
1757}
1758fn sky_irradiance(n: vec3f) -> vec3f {
1759    let d = normalize(n);
1760    let basis0 = vec3f(0.282095, 0.488603 * d.y, 0.488603 * d.z);
1761    let basis1 = vec3f(0.488603 * d.x, 1.092548 * d.x * d.y, 1.092548 * d.y * d.z);
1762    let basis2 = vec3f(0.315392 * (3.0 * d.z * d.z - 1.0), 1.092548 * d.x * d.z, 0.546274 * (d.x * d.x - d.y * d.y));
1763    let sh = scene.sky;
1764    let value = sh[0].xyz * basis0.x + sh[1].xyz * basis0.y + sh[2].xyz * basis0.z
1765        + sh[3].xyz * basis1.x + sh[4].xyz * basis1.y + sh[5].xyz * basis1.z
1766        + sh[6].xyz * basis2.x + sh[7].xyz * basis2.y + sh[8].xyz * basis2.z;
1767    return max(value, vec3f(0.0));
1768}
1769@fragment fn compose_fragment(@builtin(position) position: vec4f) -> ComposeOut {
1770    let pixel = vec2i(position.xy);
1771    let id = textureLoad(front_id_tex, pixel, 0).r;
1772    if (id == 0u) { discard; }
1773    let front_z = textureLoad(front_depth_tex, pixel, 0);
1774    let opaque_z = textureLoad(opaque_depth_tex, pixel, 0);
1775    if (opaque_z < front_z) { discard; }
1776    let front = textureLoad(front_tex, pixel, 0);
1777    let back = textureLoad(back_tex, pixel, 0);
1778    if (back.w <= front.w) { discard; }
1779    let object = objects[id - 1u];
1780    let look = textureLoad(front_look_tex, pixel, 0);
1781    var base = object.base_roughness.xyz;
1782    var scatter_tint = object.tint_absorption.xyz;
1783    var subsurface = object.optics.w;
1784    var film_amount = object.film.z;
1785    if (look.z > 0.5) {
1786        let t = clamp(look.x, 0.0, 1.0);
1787        base = mix(base, object.tinted_base.xyz, t);
1788        scatter_tint = mix(scatter_tint, object.tinted_subsurface.xyz, t);
1789        film_amount = mix(film_amount, object.tinted_base.w, t);
1790        subsurface = mix(subsurface, object.tinted_subsurface.w, t) * max(look.y, 0.0);
1791    }
1792    let uv = position.xy / scene.extent.xy;
1793    let ray = normalize(vec3f(vec2f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0) / vec2f(scene.projection[0].x, scene.projection[1].y), -1.0));
1794    let normal = normalize(front.xyz);
1795    let inner = refract_ray(ray, normal, 1.0 / max(object.optics.x, 1.01));
1796    let distance = (back.w - front.w) / max(-inner.z, 0.1);
1797    var background_depth = back.w + max(back.w - front.w, 0.01) * 2.0;
1798    if (opaque_z < 0.999) {
1799        let linear = scene.projection[3].z / (opaque_z + scene.projection[2].z);
1800        background_depth = max(linear, back.w);
1801    }
1802    let max_lod = max(scene.extent.z - 1.0, 0.0);
1803    let lod = clamp(object.base_roughness.w * object.base_roughness.w * max_lod, 0.0, max_lod);
1804    let ior = vec3f(object.optics.x - 0.016 * object.optics.y, object.optics.x, object.optics.x + 0.022 * object.optics.y);
1805    var refracted = vec3f(0.0);
1806    for (var channel = 0u; channel < 3u; channel++) {
1807        let sample_at = sample_uv(uv, front.w, back.w, normal, normalize(back.xyz), ior[channel], background_depth);
1808        let fit = scene.extent.xy / vec2f(textureDimensions(opaque_tex));
1809        let sample_color = textureSampleLevel(opaque_tex, linear_sampler, clamp(sample_at, vec2f(0.0), vec2f(1.0)) * fit, lod).rgb;
1810        refracted[channel] = sample_color[channel];
1811    }
1812    let absorption = exp(-max(object.tint_absorption.w, 0.0) * distance * (vec3f(1.0) - clamp(scatter_tint, vec3f(0.0), vec3f(1.0))));
1813    refracted *= absorption * base;
1814    var scattered = clamp(subsurface * (1.0 - exp(-distance)), 0.0, 1.0);
1815    if (look.z > 0.5) {
1816        scattered = 1.0 - exp(-subsurface * distance);
1817    }
1818    let world = mat3x3f(scene.world0.xyz, scene.world1.xyz, scene.world2.xyz);
1819    let lit_scatter = scatter_tint * sky_irradiance(world * normal) / 3.14159265;
1820    refracted = mix(refracted, lit_scatter, scattered);
1821    let cosine = clamp(-dot(ray, normal), 0.0, 1.0);
1822    let f0 = pow((object.optics.x - 1.0) / (object.optics.x + 1.0), 2.0);
1823    var fresnel = vec3f(f0 + (1.0 - f0) * pow(1.0 - cosine, 5.0));
1824    if (film_amount > 0.0 && object.film.x > 0.0) {
1825        fresnel = mix(fresnel, film_dielectric(cosine, object.film.x, object.film.y, object.optics.x), clamp(film_amount, 0.0, 1.0));
1826    }
1827    let reflected_world = normalize(world * reflect(ray, normal));
1828    let reflected = textureSampleLevel(reflection_tex, linear_sampler, reflected_world, lod).rgb;
1829    let caustic = caustic_light(landing(uv, front.w, back.w, normal, normalize(back.xyz), ior.y, background_depth).xyz, object.film.w);
1830    let color = mix(reflected, refracted + caustic, (vec3f(1.0) - fresnel) * clamp(object.optics.z, 0.0, 1.0));
1831    var out: ComposeOut;
1832    out.color = vec4f(color, 1.0);
1833    out.reactive = 1.0;
1834    return out;
1835}
1836@fragment fn pick_fragment(@builtin(position) position: vec4f) -> @location(0) u32 {
1837    let pixel = vec2i(position.xy);
1838    let id = textureLoad(front_id_tex, pixel, 0).r;
1839    if (id == 0u) { discard; }
1840    let front_z = textureLoad(front_depth_tex, pixel, 0);
1841    let opaque_z = textureLoad(opaque_depth_tex, pixel, 0);
1842    if (opaque_z < front_z) { discard; }
1843    let front = textureLoad(front_tex, pixel, 0);
1844    let back = textureLoad(back_tex, pixel, 0);
1845    if (back.w <= front.w) { discard; }
1846    let pick = objects[id - 1u].pick.x;
1847    if (pick == 0u) { discard; }
1848    return pick;
1849}
1850"#;
1851
1852pub const CAST_WGSL: &str = r#"
1853struct Cast {
1854    model: mat4x4f,
1855    view_proj: mat4x4f,
1856    tint: vec4f,
1857    tinted: vec4f,
1858    liquid: vec4f,
1859    clip0: vec4f,
1860    clip1: vec4f,
1861}
1862@group(0) @binding(0) var<storage, read> casts: array<Cast>;
1863@group(0) @binding(1) var cast_fluid: texture_2d<f32>;
1864@group(0) @binding(2) var cast_ripple: texture_2d<f32>;
1865struct CastVertex {
1866    @location(0) position: vec4f,
1867    @location(1) normal: vec4f,
1868    @location(2) uv: vec4f,
1869    @location(3) look: vec4f,
1870}
1871struct CastOut {
1872    @builtin(position) position: vec4f,
1873    @location(0) @interpolate(flat) row: u32,
1874    @location(1) look: vec4f,
1875    @location(2) world: vec3f,
1876}
1877fn cast_height(uv: vec2f, caster: Cast) -> f32 {
1878    let fluid_size = vec2f(textureDimensions(cast_fluid));
1879    let ripple_size = vec2f(textureDimensions(cast_ripple));
1880    let fluid_xy = vec2i(clamp(uv * fluid_size, vec2f(0.0), fluid_size - vec2f(1.0)));
1881    let ripple_xy = vec2i(clamp(uv * ripple_size, vec2f(0.0), ripple_size - vec2f(1.0)));
1882    return textureLoad(cast_fluid, fluid_xy, 0).r * caster.liquid.y + textureLoad(cast_ripple, ripple_xy, 0).r * caster.liquid.z;
1883}
1884@vertex fn cast_vertex(v: CastVertex, @builtin(instance_index) row: u32) -> CastOut {
1885    let caster = casts[row];
1886    var p = v.position.xyz;
1887    if (caster.liquid.x > 0.5 && v.normal.z > 0.5) {
1888        p.z += cast_height(v.uv.xy, caster);
1889    }
1890    let world = caster.model * vec4f(p, 1.0);
1891    var out: CastOut;
1892    out.position = caster.view_proj * world;
1893    out.row = row;
1894    out.look = v.look;
1895    out.world = world.xyz;
1896    return out;
1897}
1898fn cast_clip(plane: vec4f, world: vec3f) -> bool {
1899    return any(plane.xyz != vec3f(0.0)) && dot(plane.xyz, world) > plane.w;
1900}
1901@fragment fn cast_fragment(input: CastOut) -> @location(0) vec4u {
1902    if (input.look.z < 0.0) { discard; }
1903    let caster = casts[input.row];
1904    if (cast_clip(caster.clip0, input.world) || cast_clip(caster.clip1, input.world)) { discard; }
1905    var tint = caster.tint.xyz;
1906    if (input.look.w > 0.5) {
1907        tint = mix(tint, caster.tinted.xyz, clamp(input.look.x, 0.0, 1.0));
1908    }
1909    return vec4u(pack4x8unorm(vec4f(tint, 1.0)), bitcast<u32>(input.position.z), 0u, 0u);
1910}
1911"#;
1912
1913pub fn scene_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1914    vec![
1915        wgpu::BindGroupLayoutEntry {
1916            binding: 0,
1917            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1918            ty: wgpu::BindingType::Buffer {
1919                ty: wgpu::BufferBindingType::Uniform,
1920                has_dynamic_offset: false,
1921                min_binding_size: None,
1922            },
1923            count: None,
1924        },
1925        wgpu::BindGroupLayoutEntry {
1926            binding: 1,
1927            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1928            ty: wgpu::BindingType::Buffer {
1929                ty: wgpu::BufferBindingType::Storage { read_only: true },
1930                has_dynamic_offset: false,
1931                min_binding_size: None,
1932            },
1933            count: None,
1934        },
1935        texture_entry(
1936            2,
1937            wgpu::TextureSampleType::Float { filterable: true },
1938            wgpu::TextureViewDimension::D2,
1939            wgpu::ShaderStages::VERTEX,
1940        ),
1941        texture_entry(
1942            3,
1943            wgpu::TextureSampleType::Float { filterable: true },
1944            wgpu::TextureViewDimension::D2,
1945            wgpu::ShaderStages::VERTEX,
1946        ),
1947    ]
1948}
1949
1950fn cast_buffer(device: &wgpu::Device, rows: usize) -> wgpu::Buffer {
1951    device.create_buffer(&wgpu::BufferDescriptor {
1952        label: Some("glass casts"),
1953        size: (rows * std::mem::size_of::<CastGpu>()) as u64,
1954        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1955        mapped_at_creation: false,
1956    })
1957}
1958
1959pub fn cast_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1960    vec![
1961        wgpu::BindGroupLayoutEntry {
1962            binding: 0,
1963            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1964            ty: wgpu::BindingType::Buffer {
1965                ty: wgpu::BufferBindingType::Storage { read_only: true },
1966                has_dynamic_offset: false,
1967                min_binding_size: None,
1968            },
1969            count: None,
1970        },
1971        texture_entry(
1972            1,
1973            wgpu::TextureSampleType::Float { filterable: true },
1974            wgpu::TextureViewDimension::D2,
1975            wgpu::ShaderStages::VERTEX,
1976        ),
1977        texture_entry(
1978            2,
1979            wgpu::TextureSampleType::Float { filterable: true },
1980            wgpu::TextureViewDimension::D2,
1981            wgpu::ShaderStages::VERTEX,
1982        ),
1983    ]
1984}
1985
1986pub fn id_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1987    vec![texture_entry(
1988        0,
1989        wgpu::TextureSampleType::Uint,
1990        wgpu::TextureViewDimension::D2,
1991        wgpu::ShaderStages::FRAGMENT,
1992    )]
1993}
1994
1995pub fn compose_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1996    vec![
1997        texture_entry(
1998            0,
1999            wgpu::TextureSampleType::Float { filterable: true },
2000            wgpu::TextureViewDimension::D2,
2001            wgpu::ShaderStages::FRAGMENT,
2002        ),
2003        texture_entry(
2004            1,
2005            wgpu::TextureSampleType::Float { filterable: true },
2006            wgpu::TextureViewDimension::D2,
2007            wgpu::ShaderStages::FRAGMENT,
2008        ),
2009        texture_entry(
2010            2,
2011            wgpu::TextureSampleType::Depth,
2012            wgpu::TextureViewDimension::D2,
2013            wgpu::ShaderStages::FRAGMENT,
2014        ),
2015        texture_entry(
2016            3,
2017            wgpu::TextureSampleType::Float { filterable: true },
2018            wgpu::TextureViewDimension::D2,
2019            wgpu::ShaderStages::FRAGMENT,
2020        ),
2021        texture_entry(
2022            4,
2023            wgpu::TextureSampleType::Float { filterable: true },
2024            wgpu::TextureViewDimension::Cube,
2025            wgpu::ShaderStages::FRAGMENT,
2026        ),
2027        texture_entry(
2028            5,
2029            wgpu::TextureSampleType::Float { filterable: true },
2030            wgpu::TextureViewDimension::D2,
2031            wgpu::ShaderStages::FRAGMENT,
2032        ),
2033        texture_entry(
2034            6,
2035            wgpu::TextureSampleType::Depth,
2036            wgpu::TextureViewDimension::D2,
2037            wgpu::ShaderStages::FRAGMENT,
2038        ),
2039        wgpu::BindGroupLayoutEntry {
2040            binding: 7,
2041            visibility: wgpu::ShaderStages::FRAGMENT,
2042            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2043            count: None,
2044        },
2045        texture_entry(
2046            8,
2047            wgpu::TextureSampleType::Float { filterable: true },
2048            wgpu::TextureViewDimension::D2,
2049            wgpu::ShaderStages::FRAGMENT,
2050        ),
2051    ]
2052}
2053
2054#[cfg(test)]
2055mod tests {
2056    use super::*;
2057    use pfx_geom::shapes::Shape;
2058    use pfx_gpu::Gpu;
2059
2060    #[test]
2061    fn clip_planes_cut_where_the_point_lies_past_them() {
2062        let none = [[0.0; 4]; 2];
2063        assert!(!clips(&none, [5.0, 5.0, 5.0]));
2064        let strip = [[0.0, 1.0, 0.0, 0.1], [0.0, -1.0, 0.0, 0.0]];
2065        assert!(clips(&strip, [0.0, 0.2, 0.0]));
2066        assert!(clips(&strip, [0.0, -0.1, 0.0]));
2067        assert!(!clips(&strip, [0.0, 0.05, 0.0]));
2068    }
2069
2070    #[test]
2071    fn a_shadow_only_surface_is_not_drawn() {
2072        let surface = Surface {
2073            mesh: MeshHandle(0),
2074            model: [[0.0; 4]; 4],
2075            material: Material::default(),
2076            liquid: false,
2077            fluid_height: 0.0,
2078            ripple_height: 0.0,
2079            caustic_strength: 0.0,
2080            tinted: None,
2081            id: 0,
2082            casts_shadow: true,
2083            shadow_only: true,
2084            clip: [[0.0; 4]; 2],
2085        };
2086        assert!(casting(&[surface]) && !drawn(&[surface]));
2087        let shown = Surface {
2088            shadow_only: false,
2089            ..surface
2090        };
2091        assert!(drawn(&[surface, shown]));
2092        assert!(!drawn(&[]));
2093    }
2094
2095    #[test]
2096    fn shader_validates() {
2097        for source in [glass_source().as_str(), CAST_WGSL] {
2098            let module = naga::front::wgsl::parse_str(source).unwrap();
2099            naga::valid::Validator::new(
2100                naga::valid::ValidationFlags::all(),
2101                naga::valid::Capabilities::all(),
2102            )
2103            .validate(&module)
2104            .unwrap();
2105        }
2106    }
2107
2108    #[test]
2109    fn subsurface_takes_the_sky_at_the_normal() {
2110        let tint = [0.6, 0.5, 0.9];
2111        let uniform = subsurface_colour(
2112            tint,
2113            &uniform_sky_sh([std::f32::consts::PI; 3]),
2114            [0.0, 1.0, 0.0],
2115        );
2116        for k in 0..3 {
2117            assert!((uniform[k] - tint[k]).abs() < 1e-5, "{uniform:?}");
2118        }
2119        let mut sh = uniform_sky_sh([2.0; 3]);
2120        sh[1] = [1.5; 3];
2121        let up = subsurface_colour(tint, &sh, [0.0, 1.0, 0.0]);
2122        let flank = subsurface_colour(tint, &sh, [0.8, 0.6, 0.0]);
2123        let side = crate::frame::sky_irradiance(&sh, [0.8, 0.6, 0.0]);
2124        for k in 0..3 {
2125            assert!(flank[k] < up[k]);
2126            assert!((flank[k] - tint[k] * side[k] / std::f32::consts::PI).abs() < 1e-6);
2127            assert!((up[k] - tint[k] * (2.0 + 1.5 * 0.488603) / std::f32::consts::PI).abs() < 1e-5);
2128        }
2129    }
2130
2131    #[test]
2132    fn front_and_back_give_thickness() {
2133        assert_eq!(thickness(2.0, 2.4), 0.4000001);
2134        assert_eq!(thickness(2.4, 2.0), 0.0);
2135    }
2136
2137    #[test]
2138    fn beer_matches_materials() {
2139        let tint = [0.2, 0.5, 0.9];
2140        let actual = beer_lambert(2.0, tint, 0.7);
2141        let expected = pfx_materials::beer(2.0, tint, 0.7);
2142        for i in 0..3 {
2143            assert!((actual[i] - expected[i]).abs() < 1e-6);
2144        }
2145        assert!(actual[0] < actual[1] && actual[1] < actual[2]);
2146    }
2147
2148    #[test]
2149    fn refraction_matches_slab_reference() {
2150        let uv = [0.7, 0.5];
2151        let offset = refraction_offset(Refraction {
2152            uv,
2153            front_depth: 2.0,
2154            back_depth: 2.3,
2155            front_normal: [0.0, 0.0, 1.0],
2156            back_normal: [0.0, 0.0, -1.0],
2157            projection_scale: [1.0, 1.0],
2158            ior: 1.56,
2159            receiver_depth: 4.0,
2160        });
2161        let incident_x = 0.4 / (1.0_f32 + 0.4 * 0.4).sqrt();
2162        let inside_x = incident_x / 1.56;
2163        let inside_z = -(1.0 - inside_x * inside_x).sqrt();
2164        let exit_x = 0.8 + inside_x * 0.3 / -inside_z;
2165        let outgoing_x = incident_x;
2166        let outgoing_z = -(1.0 - outgoing_x * outgoing_x).sqrt();
2167        let hit_x = exit_x + outgoing_x * 1.7 / -outgoing_z;
2168        let expected = 0.5 + hit_x / 8.0 - uv[0];
2169        assert!(
2170            (offset[0] - expected).abs() < 1e-5,
2171            "{offset:?} vs {expected}"
2172        );
2173        assert!(offset[0] < 0.0);
2174        assert!(offset[1].abs() < 1e-6);
2175    }
2176
2177    fn identity() -> Matrix {
2178        [
2179            [1.0, 0.0, 0.0, 0.0],
2180            [0.0, 1.0, 0.0, 0.0],
2181            [0.0, 0.0, 1.0, 0.0],
2182            [0.0, 0.0, 0.0, 1.0],
2183        ]
2184    }
2185
2186    fn solid_texture(
2187        device: &wgpu::Device,
2188        queue: &wgpu::Queue,
2189        layers: u32,
2190        cube: bool,
2191    ) -> (wgpu::Texture, wgpu::TextureView) {
2192        let texture = device.create_texture(&wgpu::TextureDescriptor {
2193            label: Some("glass test solid texture"),
2194            size: wgpu::Extent3d {
2195                width: 1,
2196                height: 1,
2197                depth_or_array_layers: layers,
2198            },
2199            mip_level_count: 1,
2200            sample_count: 1,
2201            dimension: wgpu::TextureDimension::D2,
2202            format: HDR_FORMAT,
2203            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2204            view_formats: &[],
2205        });
2206        for layer in 0..layers {
2207            queue.write_texture(
2208                wgpu::TexelCopyTextureInfo {
2209                    texture: &texture,
2210                    mip_level: 0,
2211                    origin: wgpu::Origin3d {
2212                        x: 0,
2213                        y: 0,
2214                        z: layer,
2215                    },
2216                    aspect: wgpu::TextureAspect::All,
2217                },
2218                &[0u8; 8],
2219                wgpu::TexelCopyBufferLayout {
2220                    offset: 0,
2221                    bytes_per_row: Some(8),
2222                    rows_per_image: Some(1),
2223                },
2224                wgpu::Extent3d {
2225                    width: 1,
2226                    height: 1,
2227                    depth_or_array_layers: 1,
2228                },
2229            );
2230        }
2231        let view = texture.create_view(&wgpu::TextureViewDescriptor {
2232            dimension: Some(if cube {
2233                wgpu::TextureViewDimension::Cube
2234            } else {
2235                wgpu::TextureViewDimension::D2
2236            }),
2237            ..Default::default()
2238        });
2239        (texture, view)
2240    }
2241
2242    #[test]
2243    #[ignore = "needs a GPU; run under pgpu"]
2244    fn glass_slab_offsets_checker() {
2245        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2246        let width = 256;
2247        let height = 256;
2248        let mut pass = Pass::new(&gpu.device, width, height);
2249        let mesh = Shape::RoundBox {
2250            half: [0.65, 0.65, 0.14],
2251            radius: 0.02,
2252        }
2253        .mesh(0.025);
2254        let handle = pass.upload_mesh(&gpu.device, &mesh).unwrap();
2255        let opaque = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2256        let output = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2257        let mut checker = Vec::with_capacity((width * height * 4) as usize);
2258        for y in 0..height {
2259            for x in 0..width {
2260                let value = if ((x / 8) + (y / 8)) % 2 == 0 {
2261                    0.9
2262                } else {
2263                    0.1
2264                };
2265                checker.extend([
2266                    half::f16::from_f32(value).to_bits(),
2267                    half::f16::from_f32(value).to_bits(),
2268                    half::f16::from_f32(value).to_bits(),
2269                    half::f16::from_f32(1.0).to_bits(),
2270                ]);
2271            }
2272        }
2273        gpu.upload_rgba16(&opaque, &checker).unwrap();
2274        let (_sky, sky_view) = solid_texture(&gpu.device, &gpu.queue, 6, true);
2275        let (_caustic, caustic_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2276        let (_fluid, fluid_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2277        let (_ripple, ripple_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2278        let (_depth, depth_view) = target(
2279            &gpu.device,
2280            [width, height],
2281            wgpu::TextureFormat::Depth32Float,
2282            "glass test opaque depth",
2283        );
2284        let mut model = identity();
2285        let angle = 0.32_f32;
2286        model[0] = [angle.cos(), 0.0, angle.sin(), 0.0];
2287        model[2] = [-angle.sin(), 0.0, angle.cos(), 0.0];
2288        model[3][2] = -2.2;
2289        let surface = Surface {
2290            mesh: handle,
2291            model,
2292            material: pfx_materials::fixtures::glass(),
2293            liquid: false,
2294            fluid_height: 0.0,
2295            ripple_height: 0.0,
2296            caustic_strength: 0.0,
2297            tinted: None,
2298            id: 0,
2299            casts_shadow: false,
2300            shadow_only: false,
2301            clip: [[0.0; 4]; 2],
2302        };
2303        let near = 0.1;
2304        let far = 100.0;
2305        let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
2306        let projection = [
2307            [f, 0.0, 0.0, 0.0],
2308            [0.0, f, 0.0, 0.0],
2309            [0.0, 0.0, far / (near - far), -1.0],
2310            [0.0, 0.0, far * near / (near - far), 0.0],
2311        ];
2312        let mut encoder = gpu
2313            .device
2314            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2315                label: Some("glass slab test"),
2316            });
2317        encoder.copy_texture_to_texture(
2318            wgpu::TexelCopyTextureInfo {
2319                texture: &opaque.texture,
2320                mip_level: 0,
2321                origin: wgpu::Origin3d::ZERO,
2322                aspect: wgpu::TextureAspect::All,
2323            },
2324            wgpu::TexelCopyTextureInfo {
2325                texture: &output.texture,
2326                mip_level: 0,
2327                origin: wgpu::Origin3d::ZERO,
2328                aspect: wgpu::TextureAspect::All,
2329            },
2330            wgpu::Extent3d {
2331                width,
2332                height,
2333                depth_or_array_layers: 1,
2334            },
2335        );
2336        {
2337            let _depth_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2338                multiview_mask: None,
2339                label: Some("glass test depth clear"),
2340                color_attachments: &[],
2341                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2342                    view: &depth_view,
2343                    depth_ops: Some(wgpu::Operations {
2344                        load: wgpu::LoadOp::Clear(1.0),
2345                        store: wgpu::StoreOp::Store,
2346                    }),
2347                    stencil_ops: None,
2348                }),
2349                occlusion_query_set: None,
2350                timestamp_writes: None,
2351            });
2352        }
2353        let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
2354        pass.encode(
2355            &gpu.device,
2356            &gpu.queue,
2357            &mut encoder,
2358            Render {
2359                camera: Camera {
2360                    view: identity(),
2361                    projection,
2362                },
2363                surfaces: &[surface],
2364                inputs: Inputs {
2365                    opaque_hdr_mips: &opaque.view,
2366                    opaque_depth: &depth_view,
2367                    reflection_cube: &sky_view,
2368                    caustic: &caustic_view,
2369                    caustic_projection: None,
2370                    fluid_height: &fluid_view,
2371                    ripple_height: &ripple_view,
2372                    mip_count: 1,
2373                    sky_sh: uniform_sky_sh([std::f32::consts::PI; 3]),
2374                },
2375                output: &output.view,
2376                profiler: Some(&mut profiler),
2377            },
2378        )
2379        .unwrap();
2380        let slot = profiler.finish(&mut encoder);
2381        gpu.queue.submit([encoder.finish()]);
2382        if let Some(slot) = slot {
2383            profiler.submitted(slot);
2384        }
2385        gpu.device
2386            .poll(wgpu::PollType::wait_indefinitely())
2387            .unwrap();
2388        let pixels = gpu.readback_rgba16(&output).unwrap();
2389        let changed = pixels
2390            .chunks_exact(4)
2391            .zip(checker.chunks_exact(4))
2392            .filter(|(out, old)| {
2393                (half::f16::from_bits(out[0]).to_f32() - half::f16::from_bits(old[0]).to_f32())
2394                    .abs()
2395                    > 0.2
2396            })
2397            .count();
2398        assert!(changed > 100, "only {changed} checker pixels moved");
2399        let errors: Vec<_> = (-7..=7)
2400            .map(|shift| {
2401                let mut error = 0.0;
2402                for y in 96..160 {
2403                    for x in 96..160 {
2404                        let index = ((y * width + x) * 4) as usize;
2405                        let source = ((y * width + (x as i32 + shift) as u32) * 4) as usize;
2406                        let actual = half::f16::from_bits(pixels[index]).to_f32();
2407                        let expected = half::f16::from_bits(checker[source]).to_f32();
2408                        error += (actual - expected).abs();
2409                    }
2410                }
2411                error
2412            })
2413            .collect();
2414        let best = errors
2415            .iter()
2416            .enumerate()
2417            .min_by(|a, b| a.1.total_cmp(b.1))
2418            .unwrap();
2419        assert_ne!(best.0, 7, "checker edges stayed in place");
2420        assert!(
2421            best.1 < &(errors[7] * 0.9),
2422            "shifted checker did not fit better: {errors:?}"
2423        );
2424        let timings = profiler.collect(&gpu.device);
2425        assert!(!timings.is_empty(), "GPU timestamps unavailable");
2426        let total: f64 = timings[0].iter().map(|pass| pass.milliseconds).sum();
2427        println!("glass slab 256x256: {total:.3} ms; {changed} shifted pixels");
2428        for timing in &timings[0] {
2429            println!("{}: {:.3} ms", timing.label, timing.milliseconds);
2430        }
2431    }
2432
2433    #[test]
2434    #[ignore = "needs a GPU; run under pgpu"]
2435    fn subsurface_is_lit_by_the_sky_at_the_front_normal() {
2436        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2437        let width = 64;
2438        let height = 64;
2439        let mut pass = Pass::new(&gpu.device, width, height);
2440        let mesh = Shape::RoundBox {
2441            half: [0.65, 0.65, 0.14],
2442            radius: 0.02,
2443        }
2444        .mesh(0.025);
2445        let handle = pass.upload_mesh(&gpu.device, &mesh).unwrap();
2446        let opaque = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2447        let output = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2448        let (_sky, sky_view) = solid_texture(&gpu.device, &gpu.queue, 6, true);
2449        let (_caustic, caustic_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2450        let (_fluid, fluid_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2451        let (_ripple, ripple_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2452        let (_depth, depth_view) = target(
2453            &gpu.device,
2454            [width, height],
2455            wgpu::TextureFormat::Depth32Float,
2456            "glass subsurface opaque depth",
2457        );
2458        let mut model = identity();
2459        model[3][2] = -2.2;
2460        let mut material = pfx_materials::fixtures::glass();
2461        material.subsurface = 1.0e4;
2462        material.subsurface_tint = [0.6, 0.5, 0.9];
2463        material.absorption = 0.0;
2464        material.thin_film_amount = 0.0;
2465        material.transmission = 1.0;
2466        let surface = Surface {
2467            mesh: handle,
2468            model,
2469            material,
2470            liquid: false,
2471            fluid_height: 0.0,
2472            ripple_height: 0.0,
2473            caustic_strength: 0.0,
2474            tinted: None,
2475            id: 0,
2476            casts_shadow: false,
2477            shadow_only: false,
2478            clip: [[0.0; 4]; 2],
2479        };
2480        let mut sky_sh = uniform_sky_sh([2.0, 2.2, 2.5]);
2481        sky_sh[2] = [-1.2, -0.9, -0.5];
2482        sky_sh[3] = [0.4, 0.3, 0.2];
2483        let near = 0.1;
2484        let far = 100.0;
2485        let f = 1.0 / (30.0_f32.to_radians() * 0.5).tan();
2486        let projection = [
2487            [f, 0.0, 0.0, 0.0],
2488            [0.0, f, 0.0, 0.0],
2489            [0.0, 0.0, far / (near - far), -1.0],
2490            [0.0, 0.0, far * near / (near - far), 0.0],
2491        ];
2492        let mut encoder = gpu.device.create_command_encoder(&Default::default());
2493        {
2494            let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2495                multiview_mask: None,
2496                label: Some("glass subsurface clear"),
2497                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2498                    view: &output.view,
2499                    resolve_target: None,
2500                    ops: wgpu::Operations {
2501                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2502                        store: wgpu::StoreOp::Store,
2503                    },
2504                    depth_slice: None,
2505                })],
2506                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2507                    view: &depth_view,
2508                    depth_ops: Some(wgpu::Operations {
2509                        load: wgpu::LoadOp::Clear(1.0),
2510                        store: wgpu::StoreOp::Store,
2511                    }),
2512                    stencil_ops: None,
2513                }),
2514                occlusion_query_set: None,
2515                timestamp_writes: None,
2516            });
2517        }
2518        pass.encode(
2519            &gpu.device,
2520            &gpu.queue,
2521            &mut encoder,
2522            Render {
2523                camera: Camera {
2524                    view: identity(),
2525                    projection,
2526                },
2527                surfaces: &[surface],
2528                inputs: Inputs {
2529                    opaque_hdr_mips: &opaque.view,
2530                    opaque_depth: &depth_view,
2531                    reflection_cube: &sky_view,
2532                    caustic: &caustic_view,
2533                    caustic_projection: None,
2534                    fluid_height: &fluid_view,
2535                    ripple_height: &ripple_view,
2536                    mip_count: 1,
2537                    sky_sh,
2538                },
2539                output: &output.view,
2540                profiler: None,
2541            },
2542        )
2543        .unwrap();
2544        gpu.queue.submit([encoder.finish()]);
2545        let pixels = gpu.readback_rgba16(&output).unwrap();
2546        let centre = ((height / 2 * width + width / 2) * 4) as usize;
2547        let actual: [f32; 3] =
2548            std::array::from_fn(|k| half::f16::from_bits(pixels[centre + k]).to_f32());
2549        let scattered = subsurface_colour(material.subsurface_tint, &sky_sh, [0.0, 0.0, 1.0]);
2550        let ray_cos = 1.0 / (1.0 + 2.0 * (0.5 / width as f32 / f).powi(2)).sqrt();
2551        let f0 = ((material.ior - 1.0) / (material.ior + 1.0)).powi(2);
2552        let fresnel = f0 + (1.0 - f0) * (1.0 - ray_cos).powi(5);
2553        for k in 0..3 {
2554            let expected = scattered[k] * (1.0 - fresnel);
2555            assert!(
2556                (actual[k] - expected).abs() < expected * 0.01 + 2e-3,
2557                "{actual:?} vs {scattered:?} after Fresnel {fresnel}"
2558            );
2559        }
2560        let up = subsurface_colour(material.subsurface_tint, &sky_sh, [0.0, 1.0, 0.0]);
2561        assert!(
2562            (up[0] - scattered[0]).abs() > 0.05,
2563            "the test sky must differ by normal"
2564        );
2565    }
2566
2567    #[test]
2568    #[ignore = "needs a GPU; run under pgpu"]
2569    fn twenty_glass_slabs_at_4k() {
2570        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2571        let width = 3840;
2572        let height = 2160;
2573        let mut pass = Pass::new(&gpu.device, width, height);
2574        let mesh = Shape::RoundBox {
2575            half: [0.22, 0.22, 0.07],
2576            radius: 0.01,
2577        }
2578        .mesh(0.025);
2579        let handle = pass.upload_mesh(&gpu.device, &mesh).unwrap();
2580        let opaque = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2581        let output = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2582        let (_sky, sky_view) = solid_texture(&gpu.device, &gpu.queue, 6, true);
2583        let (_caustic, caustic_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2584        let (_fluid, fluid_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2585        let (_ripple, ripple_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2586        let (_depth, depth_view) = target(
2587            &gpu.device,
2588            [width, height],
2589            wgpu::TextureFormat::Depth32Float,
2590            "glass budget opaque depth",
2591        );
2592        let surfaces: Vec<_> = (0..20)
2593            .map(|index| {
2594                let mut model = identity();
2595                let angle = 0.25_f32;
2596                model[0] = [angle.cos(), 0.0, angle.sin(), 0.0];
2597                model[2] = [-angle.sin(), 0.0, angle.cos(), 0.0];
2598                model[3] = [
2599                    ((index % 5) as f32 - 2.0) * 0.51,
2600                    ((index / 5) as f32 - 1.5) * 0.51,
2601                    -4.0,
2602                    1.0,
2603                ];
2604                Surface {
2605                    mesh: handle,
2606                    model,
2607                    material: pfx_materials::fixtures::glass(),
2608                    liquid: false,
2609                    fluid_height: 0.0,
2610                    ripple_height: 0.0,
2611                    caustic_strength: 0.0,
2612                    tinted: None,
2613                    id: 0,
2614                    casts_shadow: false,
2615                    shadow_only: false,
2616                    clip: [[0.0; 4]; 2],
2617                }
2618            })
2619            .collect();
2620        let near = 0.1;
2621        let far = 100.0;
2622        let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
2623        let projection = [
2624            [f * height as f32 / width as f32, 0.0, 0.0, 0.0],
2625            [0.0, f, 0.0, 0.0],
2626            [0.0, 0.0, far / (near - far), -1.0],
2627            [0.0, 0.0, far * near / (near - far), 0.0],
2628        ];
2629        let camera = Camera {
2630            view: identity(),
2631            projection,
2632        };
2633        let mut clear = gpu
2634            .device
2635            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2636                label: Some("glass budget clear"),
2637            });
2638        {
2639            let _pass = clear.begin_render_pass(&wgpu::RenderPassDescriptor {
2640                multiview_mask: None,
2641                label: Some("glass budget targets"),
2642                color_attachments: &[
2643                    Some(wgpu::RenderPassColorAttachment {
2644                        view: &opaque.view,
2645                        resolve_target: None,
2646                        ops: wgpu::Operations {
2647                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2648                            store: wgpu::StoreOp::Store,
2649                        },
2650                        depth_slice: None,
2651                    }),
2652                    Some(wgpu::RenderPassColorAttachment {
2653                        view: &output.view,
2654                        resolve_target: None,
2655                        ops: wgpu::Operations {
2656                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2657                            store: wgpu::StoreOp::Store,
2658                        },
2659                        depth_slice: None,
2660                    }),
2661                ],
2662                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2663                    view: &depth_view,
2664                    depth_ops: Some(wgpu::Operations {
2665                        load: wgpu::LoadOp::Clear(1.0),
2666                        store: wgpu::StoreOp::Store,
2667                    }),
2668                    stencil_ops: None,
2669                }),
2670                occlusion_query_set: None,
2671                timestamp_writes: None,
2672            });
2673        }
2674        gpu.queue.submit([clear.finish()]);
2675        let inputs = || Inputs {
2676            opaque_hdr_mips: &opaque.view,
2677            opaque_depth: &depth_view,
2678            reflection_cube: &sky_view,
2679            caustic: &caustic_view,
2680            caustic_projection: None,
2681            fluid_height: &fluid_view,
2682            ripple_height: &ripple_view,
2683            mip_count: 1,
2684            sky_sh: uniform_sky_sh([std::f32::consts::PI; 3]),
2685        };
2686        let mut warmup = gpu
2687            .device
2688            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2689                label: Some("glass budget warmup"),
2690            });
2691        pass.encode(
2692            &gpu.device,
2693            &gpu.queue,
2694            &mut warmup,
2695            Render {
2696                camera,
2697                surfaces: &surfaces,
2698                inputs: inputs(),
2699                output: &output.view,
2700                profiler: None,
2701            },
2702        )
2703        .unwrap();
2704        gpu.queue.submit([warmup.finish()]);
2705        let mut encoder = gpu
2706            .device
2707            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2708                label: Some("glass budget timing"),
2709            });
2710        let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
2711        for _ in 0..8 {
2712            pass.encode(
2713                &gpu.device,
2714                &gpu.queue,
2715                &mut encoder,
2716                Render {
2717                    camera,
2718                    surfaces: &surfaces,
2719                    inputs: inputs(),
2720                    output: &output.view,
2721                    profiler: Some(&mut profiler),
2722                },
2723            )
2724            .unwrap();
2725        }
2726        let slot = profiler.finish(&mut encoder).unwrap();
2727        gpu.queue.submit([encoder.finish()]);
2728        profiler.submitted(slot);
2729        gpu.device
2730            .poll(wgpu::PollType::wait_indefinitely())
2731            .unwrap();
2732        let timings = profiler.collect(&gpu.device);
2733        assert_eq!(timings.len(), 1);
2734        let mut samples: Vec<_> = timings[0]
2735            .chunks_exact(3)
2736            .map(|passes| passes.iter().map(|pass| pass.milliseconds).sum::<f64>())
2737            .collect();
2738        assert_eq!(samples.len(), 8);
2739        samples.sort_by(f64::total_cmp);
2740        println!(
2741            "glass 4K 20 objects: median {:.3} ms, p95 {:.3} ms, max {:.3} ms",
2742            samples[4], samples[7], samples[7]
2743        );
2744        assert!(
2745            samples[4] <= 1.0,
2746            "glass median exceeds the 1.0 ms budget: {:.3} ms",
2747            samples[4]
2748        );
2749    }
2750
2751    #[test]
2752    #[ignore = "needs a GPU; run under pgpu"]
2753    fn glass_adds_the_caustic_where_its_light_lands() {
2754        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2755        let (width, height) = (128u32, 128u32);
2756        let mut pass = Pass::new(&gpu.device, width, height);
2757        let mesh = Shape::RoundBox {
2758            half: [0.65, 0.65, 0.14],
2759            radius: 0.02,
2760        }
2761        .mesh(0.025);
2762        let handle = pass.upload_mesh(&gpu.device, &mesh).unwrap();
2763        let opaque = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2764        let grey = half::f16::from_f32(0.3).to_bits();
2765        let one = half::f16::from_f32(1.0).to_bits();
2766        gpu.upload_rgba16(
2767            &opaque,
2768            &[grey, grey, grey, one].repeat((width * height) as usize),
2769        )
2770        .unwrap();
2771        let caustic = gpu.offscreen(1, 1, HDR_FORMAT).unwrap();
2772        let light = [0.5f32, 0.25, 0.0, 1.0].map(|v| half::f16::from_f32(v).to_bits());
2773        gpu.upload_rgba16(&caustic, &light).unwrap();
2774        let (_sky, sky_view) = solid_texture(&gpu.device, &gpu.queue, 6, true);
2775        let (_fluid, fluid_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2776        let (_ripple, ripple_view) = solid_texture(&gpu.device, &gpu.queue, 1, false);
2777        let (_depth, depth_view) = target(
2778            &gpu.device,
2779            [width, height],
2780            wgpu::TextureFormat::Depth32Float,
2781            "glass caustic depth",
2782        );
2783        let mut model = identity();
2784        model[3][2] = -2.2;
2785        let mut view = identity();
2786        view[3] = [-0.4, 0.3, -0.2, 1.0];
2787        let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
2788        let (near, far) = (0.1, 100.0);
2789        let projection = [
2790            [f, 0.0, 0.0, 0.0],
2791            [0.0, f, 0.0, 0.0],
2792            [0.0, 0.0, far / (near - far), -1.0],
2793            [0.0, 0.0, far * near / (near - far), 0.0],
2794        ];
2795        let mut model_world = model;
2796        model_world[3][0] = 0.4;
2797        model_world[3][1] = -0.3;
2798        model_world[3][2] = -2.0;
2799        let mut run = |strength: f32, projection_in: Option<CausticProjection>| {
2800            let output = gpu.offscreen(width, height, HDR_FORMAT).unwrap();
2801            gpu.upload_rgba16(&output, &[0, 0, 0, 0].repeat((width * height) as usize))
2802                .unwrap();
2803            let mut encoder = gpu.device.create_command_encoder(&Default::default());
2804            {
2805                let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2806                    multiview_mask: None,
2807                    label: Some("glass caustic depth clear"),
2808                    color_attachments: &[],
2809                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2810                        view: &depth_view,
2811                        depth_ops: Some(wgpu::Operations {
2812                            load: wgpu::LoadOp::Clear(1.0),
2813                            store: wgpu::StoreOp::Store,
2814                        }),
2815                        stencil_ops: None,
2816                    }),
2817                    occlusion_query_set: None,
2818                    timestamp_writes: None,
2819                });
2820            }
2821            let surface = Surface {
2822                mesh: handle,
2823                model: model_world,
2824                material: pfx_materials::fixtures::glass(),
2825                liquid: false,
2826                fluid_height: 0.0,
2827                ripple_height: 0.0,
2828                caustic_strength: strength,
2829                tinted: None,
2830                id: 0,
2831                casts_shadow: false,
2832                shadow_only: false,
2833                clip: [[0.0; 4]; 2],
2834            };
2835            pass.encode(
2836                &gpu.device,
2837                &gpu.queue,
2838                &mut encoder,
2839                Render {
2840                    camera: Camera { view, projection },
2841                    surfaces: &[surface],
2842                    inputs: Inputs {
2843                        opaque_hdr_mips: &opaque.view,
2844                        opaque_depth: &depth_view,
2845                        reflection_cube: &sky_view,
2846                        caustic: &caustic.view,
2847                        caustic_projection: projection_in,
2848                        fluid_height: &fluid_view,
2849                        ripple_height: &ripple_view,
2850                        mip_count: 1,
2851                        sky_sh: uniform_sky_sh([std::f32::consts::PI; 3]),
2852                    },
2853                    output: &output.view,
2854                    profiler: None,
2855                },
2856            )
2857            .unwrap();
2858            gpu.queue.submit([encoder.finish()]);
2859            let pixels = gpu.readback_rgba16(&output).unwrap();
2860            let index = ((height / 2 * width + width / 2) * 4) as usize;
2861            std::array::from_fn::<f32, 3, _>(|c| half::f16::from_bits(pixels[index + c]).to_f32())
2862        };
2863        let below = CausticProjection {
2864            rect: [0.2, -4.0, 0.8, 4.0],
2865            height: 0.0,
2866            toward_sun: [0.0, 1.0, 0.0],
2867        };
2868        let aside = CausticProjection {
2869            rect: [-0.6, -4.0, 0.5, 4.0],
2870            ..below
2871        };
2872        let dark = run(0.0, Some(below));
2873        let lit = run(1.0, Some(below));
2874        let missed = run(1.0, Some(aside));
2875        let unset = run(1.0, None);
2876        assert!(dark[0] > 0.05, "the glass covers the centre: {dark:?}");
2877        let added: [f32; 3] = std::array::from_fn(|c| lit[c] - dark[c]);
2878        assert!(added[0] > 0.2 && added[0] <= 0.5, "red caustic {added:?}");
2879        assert!(
2880            (added[1] / added[0] - 0.5).abs() < 0.02,
2881            "green caustic {added:?}"
2882        );
2883        assert!(added[2].abs() < 1e-3, "blue caustic {added:?}");
2884        assert_eq!(missed, dark, "a caustic outside its rectangle adds nothing");
2885        assert_eq!(unset, dark, "no projection, no caustic");
2886    }
2887}