Skip to main content

viewport_lib/resources/
implicit.rs

1//! GPU implicit surface types and pipeline.
2//!
3//! Public API: [`GpuImplicitItem`], [`ImplicitPrimitive`], [`ImplicitBlendMode`],
4//! [`GpuImplicitOptions`].
5
6use crate::resources::{DualPipeline, ViewportGpuResources};
7use crate::scene::material::ItemSettings;
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
81/// One GPU implicit surface draw item submitted via [`SceneFrame::gpu_implicit`].
82///
83/// Up to 16 [`ImplicitPrimitive`] entries are supported per item.
84///
85/// # Example
86/// ```no_run
87/// use viewport_lib::{GpuImplicitItem, GpuImplicitOptions, ImplicitBlendMode, ImplicitPrimitive};
88///
89/// let mut prim = ImplicitPrimitive::zeroed();
90/// prim.kind   = 1;  // sphere
91/// prim.blend  = 0.9;
92/// prim.params = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];  // center=origin, radius=1
93/// prim.colour  = [1.0, 0.5, 0.2, 1.0];
94///
95/// let mut item = GpuImplicitItem::default();
96/// item.primitives    = vec![prim];
97/// item.blend_mode    = ImplicitBlendMode::SmoothUnion;
98/// item.march_options = GpuImplicitOptions::default();
99/// ```
100#[non_exhaustive]
101pub struct GpuImplicitItem {
102    /// Primitive descriptors (max 16 entries; excess entries are ignored).
103    pub primitives: Vec<ImplicitPrimitive>,
104    /// How the primitives are combined.
105    pub blend_mode: ImplicitBlendMode,
106    /// Ray-march quality settings.
107    pub march_options: GpuImplicitOptions,
108    /// Per-item render settings (visibility, appearance, pick identity, selection state).
109    pub settings: ItemSettings,
110}
111
112impl Default for GpuImplicitItem {
113    fn default() -> Self {
114        Self {
115            primitives: Vec::new(),
116            blend_mode: crate::resources::ImplicitBlendMode::Union,
117            march_options: GpuImplicitOptions::default(),
118            settings: ItemSettings::default(),
119        }
120    }
121}
122
123impl ImplicitPrimitive {
124    /// Return a zeroed primitive with all fields set to zero.
125    pub fn zeroed() -> Self {
126        bytemuck::Zeroable::zeroed()
127    }
128}
129
130// ---------------------------------------------------------------------------
131// GPU-internal types (not exported from the crate root)
132// ---------------------------------------------------------------------------
133
134/// Flat uniform buffer layout matching the WGSL `ImplicitUniform` struct.
135///
136/// Total size: 32 header bytes + 16 * 64 primitive bytes = 1056 bytes.
137#[repr(C)]
138#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
139pub(crate) struct ImplicitUniformRaw {
140    pub num_primitives: u32,
141    pub blend_mode: u32,
142    pub max_steps: u32,
143    pub unlit: u32,
144    pub step_scale: f32,
145    pub hit_threshold: f32,
146    pub max_distance: f32,
147    pub opacity: f32,
148    pub primitives: [ImplicitPrimitive; 16],
149}
150
151/// Per-draw GPU data for one [`GpuImplicitItem`].
152pub(crate) struct ImplicitGpuItem {
153    pub _uniform_buf: wgpu::Buffer,
154    pub bind_group: wgpu::BindGroup,
155}
156
157// ---------------------------------------------------------------------------
158// Pipeline init and upload (impl ViewportGpuResources)
159// ---------------------------------------------------------------------------
160
161impl ViewportGpuResources {
162    /// Lazily create the GPU implicit surface render pipeline.
163    ///
164    /// No-op if already created.  Called from `prepare()` when any
165    /// `GpuImplicitItem` is submitted via `SceneFrame::gpu_implicit`.
166    pub(crate) fn ensure_implicit_pipeline(&mut self, device: &wgpu::Device) {
167        if self.implicit_pipeline.is_some() {
168            return;
169        }
170
171        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
172            label: Some("implicit_shader"),
173            source: wgpu::ShaderSource::Wgsl(
174                include_str!(concat!(env!("OUT_DIR"), "/implicit.wgsl")).into(),
175            ),
176        });
177
178        // Group 1: single uniform buffer containing ImplicitUniformRaw.
179        let implicit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
180            label: Some("implicit_bgl"),
181            entries: &[wgpu::BindGroupLayoutEntry {
182                binding: 0,
183                visibility: wgpu::ShaderStages::FRAGMENT,
184                ty: wgpu::BindingType::Buffer {
185                    ty: wgpu::BufferBindingType::Uniform,
186                    has_dynamic_offset: false,
187                    min_binding_size: None,
188                },
189                count: None,
190            }],
191        });
192
193        // Group 0 reuses camera_bind_group_layout (provides CameraUniform + LightsUniform).
194        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
195            label: Some("implicit_pipeline_layout"),
196            bind_group_layouts: &[&self.camera_bind_group_layout, &implicit_bgl],
197            push_constant_ranges: &[],
198        });
199
200        let make = |fmt: wgpu::TextureFormat| {
201            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
202                label: Some("implicit_pipeline"),
203                layout: Some(&layout),
204                vertex: wgpu::VertexState {
205                    module: &shader,
206                    entry_point: Some("vs_main"),
207                    buffers: &[],
208                    compilation_options: wgpu::PipelineCompilationOptions::default(),
209                },
210                fragment: Some(wgpu::FragmentState {
211                    module: &shader,
212                    entry_point: Some("fs_main"),
213                    targets: &[Some(wgpu::ColorTargetState {
214                        format: fmt,
215                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
216                        write_mask: wgpu::ColorWrites::ALL,
217                    })],
218                    compilation_options: wgpu::PipelineCompilationOptions::default(),
219                }),
220                primitive: wgpu::PrimitiveState {
221                    topology: wgpu::PrimitiveTopology::TriangleList,
222                    cull_mode: None,
223                    ..Default::default()
224                },
225                // Write depth so subsequent screen-image depth-composite items test against it.
226                depth_stencil: Some(wgpu::DepthStencilState {
227                    format: wgpu::TextureFormat::Depth24PlusStencil8,
228                    depth_write_enabled: true,
229                    depth_compare: wgpu::CompareFunction::LessEqual,
230                    stencil: wgpu::StencilState::default(),
231                    bias: wgpu::DepthBiasState::default(),
232                }),
233                multisample: wgpu::MultisampleState {
234                    count: 1,
235                    mask: !0,
236                    alpha_to_coverage_enabled: false,
237                },
238                multiview: None,
239                cache: None,
240            })
241        };
242
243        self.implicit_bgl = Some(implicit_bgl);
244        self.implicit_pipeline = Some(DualPipeline {
245            ldr: make(self.target_format),
246            hdr: make(wgpu::TextureFormat::Rgba16Float),
247        });
248    }
249
250    /// Lazily create the implicit surface outline mask pipeline.
251    ///
252    /// Reuses the same bind group layouts as `ensure_implicit_pipeline`. Must be
253    /// called after `ensure_implicit_pipeline` so that `implicit_bgl` is set.
254    /// No-op if already created.
255    pub(crate) fn ensure_implicit_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
256        if self.implicit_outline_mask_pipeline.is_some() {
257            return;
258        }
259
260        let implicit_bgl = self.implicit_bgl.as_ref().expect(
261            "ensure_implicit_pipeline must be called before ensure_implicit_outline_mask_pipeline",
262        );
263
264        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
265            label: Some("implicit_outline_mask_shader"),
266            source: wgpu::ShaderSource::Wgsl(
267                include_str!(concat!(env!("OUT_DIR"), "/implicit_outline_mask.wgsl")).into(),
268            ),
269        });
270
271        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
272            label: Some("implicit_outline_mask_pipeline_layout"),
273            bind_group_layouts: &[&self.camera_bind_group_layout, implicit_bgl],
274            push_constant_ranges: &[],
275        });
276
277        self.implicit_outline_mask_pipeline = Some(device.create_render_pipeline(
278            &wgpu::RenderPipelineDescriptor {
279                label: Some("implicit_outline_mask_pipeline"),
280                layout: Some(&layout),
281                vertex: wgpu::VertexState {
282                    module: &shader,
283                    entry_point: Some("vs_main"),
284                    buffers: &[],
285                    compilation_options: wgpu::PipelineCompilationOptions::default(),
286                },
287                fragment: Some(wgpu::FragmentState {
288                    module: &shader,
289                    entry_point: Some("fs_main"),
290                    targets: &[Some(wgpu::ColorTargetState {
291                        format: wgpu::TextureFormat::R8Unorm,
292                        blend: None,
293                        write_mask: wgpu::ColorWrites::ALL,
294                    })],
295                    compilation_options: wgpu::PipelineCompilationOptions::default(),
296                }),
297                primitive: wgpu::PrimitiveState {
298                    topology: wgpu::PrimitiveTopology::TriangleList,
299                    cull_mode: None,
300                    ..Default::default()
301                },
302                depth_stencil: Some(wgpu::DepthStencilState {
303                    format: wgpu::TextureFormat::Depth24PlusStencil8,
304                    depth_write_enabled: true,
305                    depth_compare: wgpu::CompareFunction::Less,
306                    stencil: wgpu::StencilState::default(),
307                    bias: wgpu::DepthBiasState::default(),
308                }),
309                multisample: wgpu::MultisampleState {
310                    count: 1,
311                    mask: !0,
312                    alpha_to_coverage_enabled: false,
313                },
314                multiview: None,
315                cache: None,
316            },
317        ));
318    }
319
320    /// Upload one [`GpuImplicitItem`] to GPU, returning the per-draw GPU data.
321    ///
322    /// Panics if called before `ensure_implicit_pipeline`.
323    pub(crate) fn upload_implicit_item(
324        &self,
325        device: &wgpu::Device,
326        item: &GpuImplicitItem,
327    ) -> ImplicitGpuItem {
328        // Build the flat uniform struct.
329        let blend_mode_u32 = match item.blend_mode {
330            ImplicitBlendMode::Union => 0u32,
331            ImplicitBlendMode::SmoothUnion => 1,
332            ImplicitBlendMode::Intersection => 2,
333        };
334
335        let mut raw = ImplicitUniformRaw {
336            num_primitives: item.primitives.len().min(16) as u32,
337            blend_mode: blend_mode_u32,
338            max_steps: item.march_options.max_steps,
339            unlit: item.settings.unlit as u32,
340            step_scale: item.march_options.step_scale,
341            hit_threshold: item.march_options.hit_threshold,
342            max_distance: item.march_options.max_distance,
343            opacity: item.settings.opacity,
344            primitives: [ImplicitPrimitive::zeroed(); 16],
345        };
346
347        for (i, prim) in item.primitives.iter().take(16).enumerate() {
348            raw.primitives[i] = *prim;
349        }
350
351        let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
352            label: Some("implicit_uniform_buf"),
353            contents: bytemuck::bytes_of(&raw),
354            usage: wgpu::BufferUsages::UNIFORM,
355        });
356
357        let bgl = self
358            .implicit_bgl
359            .as_ref()
360            .expect("ensure_implicit_pipeline not called");
361
362        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
363            label: Some("implicit_bind_group"),
364            layout: bgl,
365            entries: &[wgpu::BindGroupEntry {
366                binding: 0,
367                resource: uniform_buf.as_entire_binding(),
368            }],
369        });
370
371        ImplicitGpuItem {
372            _uniform_buf: uniform_buf,
373            bind_group,
374        }
375    }
376}