Skip to main content

polyscope_render/
depth_peel_pass.rs

1//! Depth peeling transparency pass.
2//!
3//! Implements front-to-back depth peeling for correct transparency rendering.
4//! Each peel pass renders the scene, discarding fragments at or in front of the
5//! previous pass's maximum depth. Peeled layers are composited using alpha-under
6//! blending into a final buffer.
7
8/// Resources for the depth peeling transparency system.
9pub struct DepthPeelPass {
10    /// Pipeline for peeling passes (`surface_mesh_peel.wgsl`)
11    peel_pipeline: wgpu::RenderPipeline,
12    /// Pipeline for compositing each peeled layer into the final buffer (under)
13    composite_under_pipeline: wgpu::RenderPipeline,
14    /// Pipeline for compositing the final result onto the scene (over)
15    composite_over_pipeline: wgpu::RenderPipeline,
16    /// Pipeline for updating min-depth (copies peel depth into min-depth via Max blend)
17    depth_update_pipeline: wgpu::RenderPipeline,
18
19    /// Min-depth texture (`Rgba16Float`) — stores the maximum depth peeled so far.
20    /// Uses `Rgba16Float` instead of `R32Float` because `R32Float` is not blendable
21    /// in WebGPU without the float32-blendable feature, and we need Max blend.
22    min_depth_texture: wgpu::Texture,
23    min_depth_view: wgpu::TextureView,
24
25    /// Per-peel-pass color output (`Rgba16Float`, premultiplied alpha)
26    peel_color_texture: wgpu::Texture,
27    peel_color_view: wgpu::TextureView,
28
29    /// Per-peel-pass depth output as color (`R32Float`) — fragment depth written as color
30    peel_depth_color_texture: wgpu::Texture,
31    peel_depth_color_view: wgpu::TextureView,
32
33    /// Actual depth buffer for peel pass (for standard depth testing within each layer)
34    peel_depth_texture: wgpu::Texture,
35    peel_depth_view: wgpu::TextureView,
36
37    /// Final accumulated result (`Rgba16Float`)
38    pub(crate) final_texture: wgpu::Texture,
39    pub(crate) final_view: wgpu::TextureView,
40
41    /// Bind group layout for peel shader's Group 3 (min-depth texture + sampler)
42    pub(crate) peel_bind_group_layout: wgpu::BindGroupLayout,
43    /// Bind group for peel shader (references `min_depth_texture`)
44    peel_bind_group: wgpu::BindGroup,
45
46    /// Bind group layout for composite shader (image + sampler)
47    composite_bind_group_layout: wgpu::BindGroupLayout,
48
49    /// Bind group layout for depth update shader (depth color + sampler)
50    depth_update_bind_group_layout: wgpu::BindGroupLayout,
51
52    sampler: wgpu::Sampler,
53
54    width: u32,
55    height: u32,
56}
57
58impl DepthPeelPass {
59    /// Creates a new depth peel pass with all required resources.
60    #[must_use]
61    pub fn new(
62        device: &wgpu::Device,
63        width: u32,
64        height: u32,
65        mesh_bind_group_layout: &wgpu::BindGroupLayout,
66        slice_plane_bind_group_layout: &wgpu::BindGroupLayout,
67        matcap_bind_group_layout: &wgpu::BindGroupLayout,
68    ) -> Self {
69        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
70            label: Some("depth peel sampler"),
71            mag_filter: wgpu::FilterMode::Nearest,
72            min_filter: wgpu::FilterMode::Nearest,
73            ..Default::default()
74        });
75
76        // Create textures
77        // Min-depth uses Rgba16Float (blendable) instead of R32Float (not blendable).
78        // Only the R channel is used; the Max blend keeps the furthest peeled depth.
79        let (min_depth_texture, min_depth_view) =
80            Self::create_rgba16_texture(device, width, height, "peel min depth");
81        let (peel_color_texture, peel_color_view) =
82            Self::create_rgba16_texture(device, width, height, "peel layer color");
83        let (peel_depth_color_texture, peel_depth_color_view) =
84            Self::create_r32float_texture(device, width, height, "peel layer depth color");
85        let (peel_depth_texture, peel_depth_view) =
86            Self::create_depth_texture(device, width, height);
87        let (final_texture, final_view) =
88            Self::create_rgba16_texture(device, width, height, "peel final");
89
90        // --- Bind group layouts ---
91
92        // Peel shader Group 3: min-depth texture + sampler
93        let peel_bind_group_layout =
94            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
95                label: Some("peel depth bind group layout"),
96                entries: &[
97                    wgpu::BindGroupLayoutEntry {
98                        binding: 0,
99                        visibility: wgpu::ShaderStages::FRAGMENT,
100                        ty: wgpu::BindingType::Texture {
101                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
102                            view_dimension: wgpu::TextureViewDimension::D2,
103                            multisampled: false,
104                        },
105                        count: None,
106                    },
107                    wgpu::BindGroupLayoutEntry {
108                        binding: 1,
109                        visibility: wgpu::ShaderStages::FRAGMENT,
110                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
111                        count: None,
112                    },
113                ],
114            });
115
116        let peel_bind_group = Self::create_peel_bind_group(
117            device,
118            &peel_bind_group_layout,
119            &min_depth_view,
120            &sampler,
121        );
122
123        // Composite shader: image texture + sampler
124        let composite_bind_group_layout =
125            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
126                label: Some("peel composite bind group layout"),
127                entries: &[
128                    wgpu::BindGroupLayoutEntry {
129                        binding: 0,
130                        visibility: wgpu::ShaderStages::FRAGMENT,
131                        ty: wgpu::BindingType::Texture {
132                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
133                            view_dimension: wgpu::TextureViewDimension::D2,
134                            multisampled: false,
135                        },
136                        count: None,
137                    },
138                    wgpu::BindGroupLayoutEntry {
139                        binding: 1,
140                        visibility: wgpu::ShaderStages::FRAGMENT,
141                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
142                        count: None,
143                    },
144                ],
145            });
146
147        // Depth update shader: only needs texture (uses textureLoad, no sampler)
148        let depth_update_bind_group_layout =
149            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
150                label: Some("peel depth update bind group layout"),
151                entries: &[wgpu::BindGroupLayoutEntry {
152                    binding: 0,
153                    visibility: wgpu::ShaderStages::FRAGMENT,
154                    ty: wgpu::BindingType::Texture {
155                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
156                        view_dimension: wgpu::TextureViewDimension::D2,
157                        multisampled: false,
158                    },
159                    count: None,
160                }],
161            });
162
163        // --- Pipelines ---
164
165        let peel_pipeline = Self::create_peel_pipeline(
166            device,
167            mesh_bind_group_layout,
168            slice_plane_bind_group_layout,
169            matcap_bind_group_layout,
170            &peel_bind_group_layout,
171        );
172
173        let under_blend = wgpu::BlendState {
174            color: wgpu::BlendComponent {
175                src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
176                dst_factor: wgpu::BlendFactor::One,
177                operation: wgpu::BlendOperation::Add,
178            },
179            alpha: wgpu::BlendComponent {
180                src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
181                dst_factor: wgpu::BlendFactor::One,
182                operation: wgpu::BlendOperation::Add,
183            },
184        };
185
186        let over_blend = wgpu::BlendState {
187            color: wgpu::BlendComponent {
188                src_factor: wgpu::BlendFactor::One,
189                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
190                operation: wgpu::BlendOperation::Add,
191            },
192            alpha: wgpu::BlendComponent {
193                src_factor: wgpu::BlendFactor::One,
194                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
195                operation: wgpu::BlendOperation::Add,
196            },
197        };
198
199        let composite_under_pipeline = Self::create_composite_pipeline(
200            device,
201            &composite_bind_group_layout,
202            "composite peel (under)",
203            under_blend,
204        );
205
206        let composite_over_pipeline = Self::create_composite_pipeline(
207            device,
208            &composite_bind_group_layout,
209            "composite peel (over)",
210            over_blend,
211        );
212
213        let depth_update_pipeline =
214            Self::create_depth_update_pipeline(device, &depth_update_bind_group_layout);
215
216        Self {
217            peel_pipeline,
218            composite_under_pipeline,
219            composite_over_pipeline,
220            depth_update_pipeline,
221            min_depth_texture,
222            min_depth_view,
223            peel_color_texture,
224            peel_color_view,
225            peel_depth_color_texture,
226            peel_depth_color_view,
227            peel_depth_texture,
228            peel_depth_view,
229            final_texture,
230            final_view,
231            peel_bind_group_layout,
232            peel_bind_group,
233            composite_bind_group_layout,
234            depth_update_bind_group_layout,
235            sampler,
236            width,
237            height,
238        }
239    }
240
241    /// Resizes all textures. Call when window size changes.
242    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
243        if self.width == width && self.height == height {
244            return;
245        }
246        self.width = width;
247        self.height = height;
248
249        let (t, v) = Self::create_rgba16_texture(device, width, height, "peel min depth");
250        self.min_depth_texture = t;
251        self.min_depth_view = v;
252
253        let (t, v) = Self::create_rgba16_texture(device, width, height, "peel layer color");
254        self.peel_color_texture = t;
255        self.peel_color_view = v;
256
257        let (t, v) = Self::create_r32float_texture(device, width, height, "peel layer depth color");
258        self.peel_depth_color_texture = t;
259        self.peel_depth_color_view = v;
260
261        let (t, v) = Self::create_depth_texture(device, width, height);
262        self.peel_depth_texture = t;
263        self.peel_depth_view = v;
264
265        let (t, v) = Self::create_rgba16_texture(device, width, height, "peel final");
266        self.final_texture = t;
267        self.final_view = v;
268
269        // Recreate bind group for min-depth
270        self.peel_bind_group = Self::create_peel_bind_group(
271            device,
272            &self.peel_bind_group_layout,
273            &self.min_depth_view,
274            &self.sampler,
275        );
276    }
277
278    /// Returns the peel pipeline for external use.
279    #[must_use]
280    pub fn peel_pipeline(&self) -> &wgpu::RenderPipeline {
281        &self.peel_pipeline
282    }
283
284    /// Returns the peel bind group (Group 3 for min-depth texture).
285    #[must_use]
286    pub fn peel_bind_group(&self) -> &wgpu::BindGroup {
287        &self.peel_bind_group
288    }
289
290    /// Returns the peel color view (render target for each peel pass).
291    #[must_use]
292    pub fn peel_color_view(&self) -> &wgpu::TextureView {
293        &self.peel_color_view
294    }
295
296    /// Returns the peel depth color view (depth-as-color output).
297    #[must_use]
298    pub fn peel_depth_color_view(&self) -> &wgpu::TextureView {
299        &self.peel_depth_color_view
300    }
301
302    /// Returns the peel depth view (actual depth buffer).
303    #[must_use]
304    pub fn peel_depth_view(&self) -> &wgpu::TextureView {
305        &self.peel_depth_view
306    }
307
308    /// Returns the min-depth view.
309    #[must_use]
310    pub fn min_depth_view(&self) -> &wgpu::TextureView {
311        &self.min_depth_view
312    }
313
314    /// Returns the final accumulated view.
315    #[must_use]
316    pub fn final_view(&self) -> &wgpu::TextureView {
317        &self.final_view
318    }
319
320    /// Composites the current peel layer into the final buffer using alpha-under blending.
321    pub fn composite_layer(&self, encoder: &mut wgpu::CommandEncoder, device: &wgpu::Device) {
322        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
323            label: Some("peel composite bind group"),
324            layout: &self.composite_bind_group_layout,
325            entries: &[
326                wgpu::BindGroupEntry {
327                    binding: 0,
328                    resource: wgpu::BindingResource::TextureView(&self.peel_color_view),
329                },
330                wgpu::BindGroupEntry {
331                    binding: 1,
332                    resource: wgpu::BindingResource::Sampler(&self.sampler),
333                },
334            ],
335        });
336
337        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
338            label: Some("peel composite pass"),
339            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
340                view: &self.final_view,
341                resolve_target: None,
342                ops: wgpu::Operations {
343                    load: wgpu::LoadOp::Load, // Keep previously accumulated layers
344                    store: wgpu::StoreOp::Store,
345                },
346                depth_slice: None,
347            })],
348            depth_stencil_attachment: None,
349            ..Default::default()
350        });
351
352        pass.set_pipeline(&self.composite_under_pipeline);
353        pass.set_bind_group(0, &bind_group, &[]);
354        pass.draw(0..3, 0..1);
355    }
356
357    /// Updates the min-depth texture from the current peel's depth-as-color output.
358    /// Uses Max blend to keep the furthest depth seen so far.
359    pub fn update_min_depth(&self, encoder: &mut wgpu::CommandEncoder, device: &wgpu::Device) {
360        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
361            label: Some("peel depth update bind group"),
362            layout: &self.depth_update_bind_group_layout,
363            entries: &[wgpu::BindGroupEntry {
364                binding: 0,
365                resource: wgpu::BindingResource::TextureView(&self.peel_depth_color_view),
366            }],
367        });
368
369        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
370            label: Some("peel min-depth update pass"),
371            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
372                view: &self.min_depth_view,
373                resolve_target: None,
374                ops: wgpu::Operations {
375                    load: wgpu::LoadOp::Load, // Keep previous min-depth
376                    store: wgpu::StoreOp::Store,
377                },
378                depth_slice: None,
379            })],
380            depth_stencil_attachment: None,
381            ..Default::default()
382        });
383
384        pass.set_pipeline(&self.depth_update_pipeline);
385        pass.set_bind_group(0, &bind_group, &[]);
386        pass.draw(0..3, 0..1);
387    }
388
389    /// Composites the final peeled result onto the HDR scene buffer.
390    pub fn composite_final_to_scene(
391        &self,
392        encoder: &mut wgpu::CommandEncoder,
393        device: &wgpu::Device,
394        hdr_view: &wgpu::TextureView,
395    ) {
396        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
397            label: Some("peel final composite bind group"),
398            layout: &self.composite_bind_group_layout,
399            entries: &[
400                wgpu::BindGroupEntry {
401                    binding: 0,
402                    resource: wgpu::BindingResource::TextureView(&self.final_view),
403                },
404                wgpu::BindGroupEntry {
405                    binding: 1,
406                    resource: wgpu::BindingResource::Sampler(&self.sampler),
407                },
408            ],
409        });
410
411        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
412            label: Some("peel final-to-scene composite pass"),
413            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
414                view: hdr_view,
415                resolve_target: None,
416                ops: wgpu::Operations {
417                    load: wgpu::LoadOp::Load, // Keep opaque scene content
418                    store: wgpu::StoreOp::Store,
419                },
420                depth_slice: None,
421            })],
422            depth_stencil_attachment: None,
423            ..Default::default()
424        });
425
426        pass.set_pipeline(&self.composite_over_pipeline);
427        pass.set_bind_group(0, &bind_group, &[]);
428        pass.draw(0..3, 0..1);
429    }
430
431    // --- Private helpers ---
432
433    fn create_r32float_texture(
434        device: &wgpu::Device,
435        width: u32,
436        height: u32,
437        label: &str,
438    ) -> (wgpu::Texture, wgpu::TextureView) {
439        let texture = device.create_texture(&wgpu::TextureDescriptor {
440            label: Some(label),
441            size: wgpu::Extent3d {
442                width,
443                height,
444                depth_or_array_layers: 1,
445            },
446            mip_level_count: 1,
447            sample_count: 1,
448            dimension: wgpu::TextureDimension::D2,
449            format: wgpu::TextureFormat::R32Float,
450            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
451            view_formats: &[],
452        });
453        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
454        (texture, view)
455    }
456
457    fn create_rgba16_texture(
458        device: &wgpu::Device,
459        width: u32,
460        height: u32,
461        label: &str,
462    ) -> (wgpu::Texture, wgpu::TextureView) {
463        let texture = device.create_texture(&wgpu::TextureDescriptor {
464            label: Some(label),
465            size: wgpu::Extent3d {
466                width,
467                height,
468                depth_or_array_layers: 1,
469            },
470            mip_level_count: 1,
471            sample_count: 1,
472            dimension: wgpu::TextureDimension::D2,
473            format: wgpu::TextureFormat::Rgba16Float,
474            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
475            view_formats: &[],
476        });
477        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
478        (texture, view)
479    }
480
481    fn create_depth_texture(
482        device: &wgpu::Device,
483        width: u32,
484        height: u32,
485    ) -> (wgpu::Texture, wgpu::TextureView) {
486        let texture = device.create_texture(&wgpu::TextureDescriptor {
487            label: Some("peel depth buffer"),
488            size: wgpu::Extent3d {
489                width,
490                height,
491                depth_or_array_layers: 1,
492            },
493            mip_level_count: 1,
494            sample_count: 1,
495            dimension: wgpu::TextureDimension::D2,
496            format: wgpu::TextureFormat::Depth24PlusStencil8,
497            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
498            view_formats: &[],
499        });
500        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
501        (texture, view)
502    }
503
504    fn create_peel_bind_group(
505        device: &wgpu::Device,
506        layout: &wgpu::BindGroupLayout,
507        min_depth_view: &wgpu::TextureView,
508        sampler: &wgpu::Sampler,
509    ) -> wgpu::BindGroup {
510        device.create_bind_group(&wgpu::BindGroupDescriptor {
511            label: Some("peel depth bind group"),
512            layout,
513            entries: &[
514                wgpu::BindGroupEntry {
515                    binding: 0,
516                    resource: wgpu::BindingResource::TextureView(min_depth_view),
517                },
518                wgpu::BindGroupEntry {
519                    binding: 1,
520                    resource: wgpu::BindingResource::Sampler(sampler),
521                },
522            ],
523        })
524    }
525
526    fn create_peel_pipeline(
527        device: &wgpu::Device,
528        mesh_bind_group_layout: &wgpu::BindGroupLayout,
529        slice_plane_bind_group_layout: &wgpu::BindGroupLayout,
530        matcap_bind_group_layout: &wgpu::BindGroupLayout,
531        peel_bind_group_layout: &wgpu::BindGroupLayout,
532    ) -> wgpu::RenderPipeline {
533        let shader_source = include_str!("shaders/surface_mesh_peel.wgsl");
534        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
535            label: Some("surface mesh peel shader"),
536            source: wgpu::ShaderSource::Wgsl(shader_source.into()),
537        });
538
539        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
540            label: Some("mesh peel pipeline layout"),
541            bind_group_layouts: &[
542                mesh_bind_group_layout,        // Group 0
543                slice_plane_bind_group_layout, // Group 1
544                matcap_bind_group_layout,      // Group 2
545                peel_bind_group_layout,        // Group 3
546            ],
547            push_constant_ranges: &[],
548        });
549
550        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
551            label: Some("surface mesh peel pipeline"),
552            layout: Some(&pipeline_layout),
553            vertex: wgpu::VertexState {
554                module: &shader,
555                entry_point: Some("vs_main"),
556                buffers: &[],
557                compilation_options: wgpu::PipelineCompilationOptions::default(),
558            },
559            fragment: Some(wgpu::FragmentState {
560                module: &shader,
561                entry_point: Some("fs_main"),
562                targets: &[
563                    // Color output (Rgba16Float, premultiplied alpha)
564                    Some(wgpu::ColorTargetState {
565                        format: wgpu::TextureFormat::Rgba16Float,
566                        blend: None, // No blending — single layer per pass
567                        write_mask: wgpu::ColorWrites::ALL,
568                    }),
569                    // Depth-as-color output (R32Float) for min-depth tracking
570                    Some(wgpu::ColorTargetState {
571                        format: wgpu::TextureFormat::R32Float,
572                        blend: None,
573                        write_mask: wgpu::ColorWrites::ALL,
574                    }),
575                ],
576                compilation_options: wgpu::PipelineCompilationOptions::default(),
577            }),
578            primitive: wgpu::PrimitiveState {
579                topology: wgpu::PrimitiveTopology::TriangleList,
580                strip_index_format: None,
581                front_face: wgpu::FrontFace::Ccw,
582                cull_mode: None, // Culling handled in shader
583                polygon_mode: wgpu::PolygonMode::Fill,
584                unclipped_depth: false,
585                conservative: false,
586            },
587            depth_stencil: Some(wgpu::DepthStencilState {
588                format: wgpu::TextureFormat::Depth24PlusStencil8,
589                depth_write_enabled: true, // Write depth for this layer
590                depth_compare: wgpu::CompareFunction::Less,
591                stencil: wgpu::StencilState::default(),
592                bias: wgpu::DepthBiasState::default(),
593            }),
594            multisample: wgpu::MultisampleState::default(),
595            multiview: None,
596            cache: None,
597        })
598    }
599
600    fn create_composite_pipeline(
601        device: &wgpu::Device,
602        bind_group_layout: &wgpu::BindGroupLayout,
603        label: &str,
604        blend: wgpu::BlendState,
605    ) -> wgpu::RenderPipeline {
606        let shader_source = include_str!("shaders/composite_peel.wgsl");
607        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
608            label: Some("composite peel shader"),
609            source: wgpu::ShaderSource::Wgsl(shader_source.into()),
610        });
611
612        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
613            label: Some("composite peel pipeline layout"),
614            bind_group_layouts: &[bind_group_layout],
615            push_constant_ranges: &[],
616        });
617
618        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
619            label: Some(label),
620            layout: Some(&pipeline_layout),
621            vertex: wgpu::VertexState {
622                module: &shader,
623                entry_point: Some("vs_main"),
624                buffers: &[],
625                compilation_options: wgpu::PipelineCompilationOptions::default(),
626            },
627            fragment: Some(wgpu::FragmentState {
628                module: &shader,
629                entry_point: Some("fs_main"),
630                targets: &[Some(wgpu::ColorTargetState {
631                    format: wgpu::TextureFormat::Rgba16Float,
632                    blend: Some(blend),
633                    write_mask: wgpu::ColorWrites::ALL,
634                })],
635                compilation_options: wgpu::PipelineCompilationOptions::default(),
636            }),
637            primitive: wgpu::PrimitiveState {
638                topology: wgpu::PrimitiveTopology::TriangleList,
639                ..Default::default()
640            },
641            depth_stencil: None,
642            multisample: wgpu::MultisampleState::default(),
643            multiview: None,
644            cache: None,
645        })
646    }
647
648    fn create_depth_update_pipeline(
649        device: &wgpu::Device,
650        bind_group_layout: &wgpu::BindGroupLayout,
651    ) -> wgpu::RenderPipeline {
652        // Dedicated shader using textureLoad (no sampler needed for R32Float).
653        // Uses Max blend to keep the furthest depth.
654        let shader_source = include_str!("shaders/depth_update_peel.wgsl");
655        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
656            label: Some("depth update shader"),
657            source: wgpu::ShaderSource::Wgsl(shader_source.into()),
658        });
659
660        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
661            label: Some("depth update pipeline layout"),
662            bind_group_layouts: &[bind_group_layout],
663            push_constant_ranges: &[],
664        });
665
666        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
667            label: Some("depth update pipeline"),
668            layout: Some(&pipeline_layout),
669            vertex: wgpu::VertexState {
670                module: &shader,
671                entry_point: Some("vs_main"),
672                buffers: &[],
673                compilation_options: wgpu::PipelineCompilationOptions::default(),
674            },
675            fragment: Some(wgpu::FragmentState {
676                module: &shader,
677                entry_point: Some("fs_main"),
678                targets: &[Some(wgpu::ColorTargetState {
679                    format: wgpu::TextureFormat::Rgba16Float,
680                    // Max blend: keeps the maximum of src and dst.
681                    // Uses Rgba16Float because R32Float is not blendable in WebGPU
682                    // without the float32-blendable feature.
683                    blend: Some(wgpu::BlendState {
684                        color: wgpu::BlendComponent {
685                            src_factor: wgpu::BlendFactor::One,
686                            dst_factor: wgpu::BlendFactor::One,
687                            operation: wgpu::BlendOperation::Max,
688                        },
689                        alpha: wgpu::BlendComponent {
690                            src_factor: wgpu::BlendFactor::One,
691                            dst_factor: wgpu::BlendFactor::One,
692                            operation: wgpu::BlendOperation::Max,
693                        },
694                    }),
695                    write_mask: wgpu::ColorWrites::ALL,
696                })],
697                compilation_options: wgpu::PipelineCompilationOptions::default(),
698            }),
699            primitive: wgpu::PrimitiveState {
700                topology: wgpu::PrimitiveTopology::TriangleList,
701                ..Default::default()
702            },
703            depth_stencil: None,
704            multisample: wgpu::MultisampleState::default(),
705            multiview: None,
706            cache: None,
707        })
708    }
709}