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(include_str!(concat!(env!("OUT_DIR"), "/implicit.wgsl")).into()),
174        });
175
176        // Group 1: single uniform buffer containing ImplicitUniformRaw.
177        let implicit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
178            label: Some("implicit_bgl"),
179            entries: &[wgpu::BindGroupLayoutEntry {
180                binding: 0,
181                visibility: wgpu::ShaderStages::FRAGMENT,
182                ty: wgpu::BindingType::Buffer {
183                    ty: wgpu::BufferBindingType::Uniform,
184                    has_dynamic_offset: false,
185                    min_binding_size: None,
186                },
187                count: None,
188            }],
189        });
190
191        // Group 0 reuses camera_bind_group_layout (provides CameraUniform + LightsUniform).
192        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
193            label: Some("implicit_pipeline_layout"),
194            bind_group_layouts: &[&self.camera_bind_group_layout, &implicit_bgl],
195            push_constant_ranges: &[],
196        });
197
198        let make = |fmt: wgpu::TextureFormat| {
199            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
200                label: Some("implicit_pipeline"),
201                layout: Some(&layout),
202                vertex: wgpu::VertexState {
203                    module: &shader,
204                    entry_point: Some("vs_main"),
205                    buffers: &[],
206                    compilation_options: wgpu::PipelineCompilationOptions::default(),
207                },
208                fragment: Some(wgpu::FragmentState {
209                    module: &shader,
210                    entry_point: Some("fs_main"),
211                    targets: &[Some(wgpu::ColorTargetState {
212                        format: fmt,
213                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
214                        write_mask: wgpu::ColorWrites::ALL,
215                    })],
216                    compilation_options: wgpu::PipelineCompilationOptions::default(),
217                }),
218                primitive: wgpu::PrimitiveState {
219                    topology: wgpu::PrimitiveTopology::TriangleList,
220                    cull_mode: None,
221                    ..Default::default()
222                },
223                // Write depth so subsequent screen-image depth-composite items test against it.
224                depth_stencil: Some(wgpu::DepthStencilState {
225                    format: wgpu::TextureFormat::Depth24PlusStencil8,
226                    depth_write_enabled: true,
227                    depth_compare: wgpu::CompareFunction::LessEqual,
228                    stencil: wgpu::StencilState::default(),
229                    bias: wgpu::DepthBiasState::default(),
230                }),
231                multisample: wgpu::MultisampleState {
232                    count: 1,
233                    mask: !0,
234                    alpha_to_coverage_enabled: false,
235                },
236                multiview: None,
237                cache: None,
238            })
239        };
240
241        self.implicit_bgl = Some(implicit_bgl);
242        self.implicit_pipeline = Some(DualPipeline {
243            ldr: make(self.target_format),
244            hdr: make(wgpu::TextureFormat::Rgba16Float),
245        });
246    }
247
248    /// Lazily create the implicit surface outline mask pipeline.
249    ///
250    /// Reuses the same bind group layouts as `ensure_implicit_pipeline`. Must be
251    /// called after `ensure_implicit_pipeline` so that `implicit_bgl` is set.
252    /// No-op if already created.
253    pub(crate) fn ensure_implicit_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
254        if self.implicit_outline_mask_pipeline.is_some() {
255            return;
256        }
257
258        let implicit_bgl = self.implicit_bgl.as_ref().expect(
259            "ensure_implicit_pipeline must be called before ensure_implicit_outline_mask_pipeline",
260        );
261
262        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
263            label: Some("implicit_outline_mask_shader"),
264            source: wgpu::ShaderSource::Wgsl(
265                include_str!(concat!(env!("OUT_DIR"), "/implicit_outline_mask.wgsl")).into(),
266            ),
267        });
268
269        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
270            label: Some("implicit_outline_mask_pipeline_layout"),
271            bind_group_layouts: &[&self.camera_bind_group_layout, implicit_bgl],
272            push_constant_ranges: &[],
273        });
274
275        self.implicit_outline_mask_pipeline = Some(device.create_render_pipeline(
276            &wgpu::RenderPipelineDescriptor {
277                label: Some("implicit_outline_mask_pipeline"),
278                layout: Some(&layout),
279                vertex: wgpu::VertexState {
280                    module: &shader,
281                    entry_point: Some("vs_main"),
282                    buffers: &[],
283                    compilation_options: wgpu::PipelineCompilationOptions::default(),
284                },
285                fragment: Some(wgpu::FragmentState {
286                    module: &shader,
287                    entry_point: Some("fs_main"),
288                    targets: &[Some(wgpu::ColorTargetState {
289                        format: wgpu::TextureFormat::R8Unorm,
290                        blend: None,
291                        write_mask: wgpu::ColorWrites::ALL,
292                    })],
293                    compilation_options: wgpu::PipelineCompilationOptions::default(),
294                }),
295                primitive: wgpu::PrimitiveState {
296                    topology: wgpu::PrimitiveTopology::TriangleList,
297                    cull_mode: None,
298                    ..Default::default()
299                },
300                depth_stencil: Some(wgpu::DepthStencilState {
301                    format: wgpu::TextureFormat::Depth24PlusStencil8,
302                    depth_write_enabled: true,
303                    depth_compare: wgpu::CompareFunction::Less,
304                    stencil: wgpu::StencilState::default(),
305                    bias: wgpu::DepthBiasState::default(),
306                }),
307                multisample: wgpu::MultisampleState {
308                    count: 1,
309                    mask: !0,
310                    alpha_to_coverage_enabled: false,
311                },
312                multiview: None,
313                cache: None,
314            },
315        ));
316    }
317
318    /// Upload one [`GpuImplicitItem`] to GPU, returning the per-draw GPU data.
319    ///
320    /// Panics if called before `ensure_implicit_pipeline`.
321    pub(crate) fn upload_implicit_item(
322        &self,
323        device: &wgpu::Device,
324        item: &GpuImplicitItem,
325    ) -> ImplicitGpuItem {
326        // Build the flat uniform struct.
327        let blend_mode_u32 = match item.blend_mode {
328            ImplicitBlendMode::Union => 0u32,
329            ImplicitBlendMode::SmoothUnion => 1,
330            ImplicitBlendMode::Intersection => 2,
331        };
332
333        let mut raw = ImplicitUniformRaw {
334            num_primitives: item.primitives.len().min(16) as u32,
335            blend_mode: blend_mode_u32,
336            max_steps: item.march_options.max_steps,
337            unlit: item.settings.unlit as u32,
338            step_scale: item.march_options.step_scale,
339            hit_threshold: item.march_options.hit_threshold,
340            max_distance: item.march_options.max_distance,
341            opacity: item.settings.opacity,
342            primitives: [ImplicitPrimitive::zeroed(); 16],
343        };
344
345        for (i, prim) in item.primitives.iter().take(16).enumerate() {
346            raw.primitives[i] = *prim;
347        }
348
349        let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
350            label: Some("implicit_uniform_buf"),
351            contents: bytemuck::bytes_of(&raw),
352            usage: wgpu::BufferUsages::UNIFORM,
353        });
354
355        let bgl = self
356            .implicit_bgl
357            .as_ref()
358            .expect("ensure_implicit_pipeline not called");
359
360        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
361            label: Some("implicit_bind_group"),
362            layout: bgl,
363            entries: &[wgpu::BindGroupEntry {
364                binding: 0,
365                resource: uniform_buf.as_entire_binding(),
366            }],
367        });
368
369        ImplicitGpuItem {
370            _uniform_buf: uniform_buf,
371            bind_group,
372        }
373    }
374}