Skip to main content

viewport_lib/resources/volume/
implicit.rs

1//! GPU implicit surface types and pipeline.
2//!
3//! Public API: [`GpuImplicitItem`], [`ImplicitPrimitive`], [`ImplicitBlendMode`],
4//! [`GpuImplicitOptions`].
5
6use crate::renderer::GpuImplicitItem;
7use crate::resources::DeviceResources;
8use wgpu::util::DeviceExt as _;
9
10// ---------------------------------------------------------------------------
11// Public API types
12// ---------------------------------------------------------------------------
13
14/// Primitive descriptor for the GPU implicit SDF.
15///
16/// The shader evaluates each primitive independently and combines them
17/// according to the item's [`ImplicitBlendMode`].
18///
19/// # Primitive kinds and `params` layout
20///
21/// | `kind` | Primitive | `params[0..4]`   | `params[4..8]`    |
22/// |--------|-----------|-----------------|-------------------|
23/// | 1      | Sphere    | cx,cy,cz,radius | unused            |
24/// | 2      | Box       | cx,cy,cz,_      | hx,hy,hz,_ (half-extents) |
25/// | 3      | Plane     | nx,ny,nz,d      | unused (normal + offset)  |
26/// | 4      | Capsule   | ax,ay,az,radius | bx,by,bz,_ (endpoints)   |
27#[repr(C)]
28#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
29pub struct ImplicitPrimitive {
30    /// Primitive type discriminant (1=sphere, 2=box, 3=plane, 4=capsule).
31    pub kind: u32,
32    /// Smooth-min blend radius used when the item's blend mode is `SmoothUnion`.
33    /// Zero produces a hard union.
34    pub blend: f32,
35    #[doc(hidden)]
36    pub _pad: [f32; 2],
37    /// Kind-specific parameters, first four floats.
38    pub params: [f32; 8],
39    /// Linear RGBA colour for this primitive.
40    /// Colours are blended by proximity weight at the hit point.
41    pub colour: [f32; 4],
42}
43
44/// How multiple primitives are combined into a single SDF.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub enum ImplicitBlendMode {
47    /// Hard min() union (sharp junctions between primitives)
48    #[default]
49    Union,
50    /// Smooth-min union (primitives fuse organically; uses per-primitive `blend` radius)
51    SmoothUnion,
52    /// Max() intersection (only the region inside all primitives is visible)
53    Intersection,
54}
55
56/// March configuration for a [`GpuImplicitItem`].
57#[derive(Clone, Copy, Debug)]
58pub struct GpuImplicitOptions {
59    /// Maximum ray-march steps before the ray is considered a miss. Default: 128.
60    pub max_steps: u32,
61    /// Step-scale applied to the SDF distance each iteration (< 1 improves thin-feature quality).
62    /// Default: 0.85.
63    pub step_scale: f32,
64    /// Distance threshold for a ray-surface hit. Default: 5e-4.
65    pub hit_threshold: f32,
66    /// Maximum ray length before miss. Default: 40.0.
67    pub max_distance: f32,
68}
69
70impl Default for GpuImplicitOptions {
71    fn default() -> Self {
72        Self {
73            max_steps: 128,
74            step_scale: 0.85,
75            hit_threshold: 5e-4,
76            max_distance: 40.0,
77        }
78    }
79}
80
81impl ImplicitPrimitive {
82    /// Return a zeroed primitive with all fields set to zero.
83    pub fn zeroed() -> Self {
84        bytemuck::Zeroable::zeroed()
85    }
86}
87
88// ---------------------------------------------------------------------------
89// GPU-internal types (not exported from the crate root)
90// ---------------------------------------------------------------------------
91
92/// Flat uniform buffer layout matching the WGSL `ImplicitUniform` struct.
93///
94/// Total size: 32 header bytes + 16 * 64 primitive bytes = 1056 bytes.
95#[repr(C)]
96#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
97pub(crate) struct ImplicitUniformRaw {
98    pub num_primitives: u32,
99    pub blend_mode: u32,
100    pub max_steps: u32,
101    pub unlit: u32,
102    pub step_scale: f32,
103    pub hit_threshold: f32,
104    pub max_distance: f32,
105    pub opacity: f32,
106    pub primitives: [ImplicitPrimitive; 16],
107}
108
109/// Per-draw GPU data for one [`GpuImplicitItem`].
110pub(crate) struct ImplicitGpuItem {
111    pub _uniform_buf: wgpu::Buffer,
112    pub bind_group: wgpu::BindGroup,
113}
114
115// ---------------------------------------------------------------------------
116// Pipeline init and upload (impl DeviceResources)
117// ---------------------------------------------------------------------------
118
119impl DeviceResources {
120    /// Lazily create the GPU implicit surface render pipeline.
121    ///
122    /// No-op if already created.  Called from `prepare()` when any
123    /// `GpuImplicitItem` is submitted via `SceneFrame::gpu_implicit`.
124    pub(crate) fn ensure_implicit_pipeline(&mut self, device: &wgpu::Device) {
125        if self.implicit.pipeline.is_some() {
126            return;
127        }
128
129        let shader = crate::resources::builders::wgsl_module(
130            device,
131            "implicit_shader",
132            crate::resources::builders::wgsl_source!("implicit"),
133        );
134
135        // Group 1: single uniform buffer containing ImplicitUniformRaw.
136        let implicit_bgl = crate::resources::builders::uniform_bgl(
137            device,
138            "implicit_bgl",
139            wgpu::ShaderStages::FRAGMENT,
140        );
141
142        // Group 0 reuses camera_bind_group_layout (provides CameraUniform + LightsUniform).
143        let layout = crate::resources::builders::standard_scene_layout(
144            device,
145            "implicit_pipeline_layout",
146            &self.camera_bind_group_layout,
147            &implicit_bgl,
148        );
149
150        self.implicit.bgl = Some(implicit_bgl);
151        // depth_write is on so subsequent screen-image depth-composite items test against it.
152        self.implicit.pipeline = Some(crate::resources::builders::build_dual_pipeline(
153            device,
154            &crate::resources::builders::DualPipelineDesc {
155                label: "implicit_pipeline",
156                layout: &layout,
157                shader: &shader,
158                vertex_entry: "vs_main",
159                fragment_entry: "fs_main",
160                vertex_buffers: &[],
161                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
162                topology: wgpu::PrimitiveTopology::TriangleList,
163                cull_mode: None,
164                depth_write: true,
165                depth_compare: wgpu::CompareFunction::LessEqual,
166                sample_count: 1,
167                ldr_format: self.target_format,
168            },
169        ));
170    }
171
172    /// Lazily create the implicit surface outline mask pipeline.
173    ///
174    /// Reuses the same bind group layouts as `ensure_implicit_pipeline`. Must be
175    /// called after `ensure_implicit_pipeline` so that `implicit_bgl` is set.
176    /// No-op if already created.
177    pub(crate) fn ensure_implicit_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
178        if self.implicit.outline_mask_pipeline.is_some() {
179            return;
180        }
181
182        let implicit_bgl = self.implicit.bgl.as_ref().expect(
183            "ensure_implicit_pipeline must be called before ensure_implicit_outline_mask_pipeline",
184        );
185
186        let shader = crate::resources::builders::wgsl_module(
187            device,
188            "implicit_outline_mask_shader",
189            crate::resources::builders::wgsl_source!("implicit_outline_mask"),
190        );
191
192        let layout = crate::resources::builders::standard_scene_layout(
193            device,
194            "implicit_outline_mask_pipeline_layout",
195            &self.camera_bind_group_layout,
196            implicit_bgl,
197        );
198
199        self.implicit.outline_mask_pipeline =
200            Some(crate::resources::builders::build_outline_mask_pipeline(
201                device,
202                "implicit_outline_mask_pipeline",
203                &layout,
204                &shader,
205                wgpu::TextureFormat::R8Unorm,
206                &[],
207                None,
208                true,
209                wgpu::CompareFunction::Less,
210            ));
211    }
212
213    /// Upload one [`GpuImplicitItem`] to GPU, returning the per-draw GPU data.
214    ///
215    /// Panics if called before `ensure_implicit_pipeline`.
216    pub(crate) fn upload_implicit_item(
217        &self,
218        device: &wgpu::Device,
219        item: &GpuImplicitItem,
220    ) -> ImplicitGpuItem {
221        // Build the flat uniform struct.
222        let blend_mode_u32 = match item.blend_mode {
223            ImplicitBlendMode::Union => 0u32,
224            ImplicitBlendMode::SmoothUnion => 1,
225            ImplicitBlendMode::Intersection => 2,
226        };
227
228        let mut raw = ImplicitUniformRaw {
229            num_primitives: item.primitives.len().min(16) as u32,
230            blend_mode: blend_mode_u32,
231            max_steps: item.march_options.max_steps,
232            unlit: item.settings.unlit as u32,
233            step_scale: item.march_options.step_scale,
234            hit_threshold: item.march_options.hit_threshold,
235            max_distance: item.march_options.max_distance,
236            opacity: item.settings.opacity,
237            primitives: [ImplicitPrimitive::zeroed(); 16],
238        };
239
240        for (i, prim) in item.primitives.iter().take(16).enumerate() {
241            raw.primitives[i] = *prim;
242        }
243
244        let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
245            label: Some("implicit_uniform_buf"),
246            contents: bytemuck::bytes_of(&raw),
247            usage: wgpu::BufferUsages::UNIFORM,
248        });
249
250        let bgl = self
251            .implicit
252            .bgl
253            .as_ref()
254            .expect("ensure_implicit_pipeline not called");
255
256        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
257            label: Some("implicit_bind_group"),
258            layout: bgl,
259            entries: &[wgpu::BindGroupEntry {
260                binding: 0,
261                resource: uniform_buf.as_entire_binding(),
262            }],
263        });
264
265        ImplicitGpuItem {
266            _uniform_buf: uniform_buf,
267            bind_group,
268        }
269    }
270}