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}