Skip to main content

viewport_lib/resources/
implicit.rs

1//! GPU implicit surface types and pipeline for Phase 16.
2//!
3//! Public API: [`GpuImplicitItem`], [`ImplicitPrimitive`], [`ImplicitBlendMode`],
4//! [`GpuImplicitOptions`].
5
6use crate::resources::{DualPipeline, ViewportGpuResources};
7use wgpu::util::DeviceExt as _;
8
9// ---------------------------------------------------------------------------
10// Public API types
11// ---------------------------------------------------------------------------
12
13/// Primitive descriptor for the GPU implicit SDF.
14///
15/// The shader evaluates each primitive independently and combines them
16/// according to the item's [`ImplicitBlendMode`].
17///
18/// # Primitive kinds and `params` layout
19///
20/// | `kind` | Primitive | `params[0..4]`   | `params[4..8]`    |
21/// |--------|-----------|-----------------|-------------------|
22/// | 1      | Sphere    | cx,cy,cz,radius | unused            |
23/// | 2      | Box       | cx,cy,cz,_      | hx,hy,hz,_ (half-extents) |
24/// | 3      | Plane     | nx,ny,nz,d      | unused (normal + offset)  |
25/// | 4      | Capsule   | ax,ay,az,radius | bx,by,bz,_ (endpoints)   |
26#[repr(C)]
27#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
28pub struct ImplicitPrimitive {
29    /// Primitive type discriminant (1=sphere, 2=box, 3=plane, 4=capsule).
30    pub kind: u32,
31    /// Smooth-min blend radius used when the item's blend mode is `SmoothUnion`.
32    /// Zero produces a hard union.
33    pub blend: f32,
34    #[doc(hidden)]
35    pub _pad: [f32; 2],
36    /// Kind-specific parameters, first four floats.
37    pub params: [f32; 8],
38    /// Linear RGBA colour for this primitive.
39    /// Colours are blended by proximity weight at the hit point.
40    pub colour: [f32; 4],
41}
42
43/// How multiple primitives are combined into a single SDF.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum ImplicitBlendMode {
46    /// Hard min() union — sharp junctions between primitives.
47    #[default]
48    Union,
49    /// Smooth-min union — primitives fuse organically; uses per-primitive `blend` radius.
50    SmoothUnion,
51    /// Max() intersection — only the region inside all primitives is visible.
52    Intersection,
53}
54
55/// March configuration for a [`GpuImplicitItem`].
56#[derive(Clone, Copy, Debug)]
57pub struct GpuImplicitOptions {
58    /// Maximum ray-march steps before the ray is considered a miss. Default: 128.
59    pub max_steps: u32,
60    /// Step-scale applied to the SDF distance each iteration (< 1 improves thin-feature quality).
61    /// Default: 0.85.
62    pub step_scale: f32,
63    /// Distance threshold for a ray-surface hit. Default: 5e-4.
64    pub hit_threshold: f32,
65    /// Maximum ray length before miss. Default: 40.0.
66    pub max_distance: f32,
67}
68
69impl Default for GpuImplicitOptions {
70    fn default() -> Self {
71        Self {
72            max_steps: 128,
73            step_scale: 0.85,
74            hit_threshold: 5e-4,
75            max_distance: 40.0,
76        }
77    }
78}
79
80/// One GPU implicit surface draw item submitted via [`SceneFrame::gpu_implicit`].
81///
82/// Up to 16 [`ImplicitPrimitive`] entries are supported per item.
83///
84/// # Example
85/// ```no_run
86/// use viewport_lib::{GpuImplicitItem, GpuImplicitOptions, ImplicitBlendMode, ImplicitPrimitive};
87///
88/// let mut prim = ImplicitPrimitive::zeroed();
89/// prim.kind   = 1;  // sphere
90/// prim.blend  = 0.9;
91/// prim.params = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];  // center=origin, radius=1
92/// prim.colour  = [1.0, 0.5, 0.2, 1.0];
93///
94/// let mut item = GpuImplicitItem::default();
95/// item.primitives    = vec![prim];
96/// item.blend_mode    = ImplicitBlendMode::SmoothUnion;
97/// item.march_options = GpuImplicitOptions::default();
98/// ```
99#[non_exhaustive]
100pub struct GpuImplicitItem {
101    /// Primitive descriptors (max 16 entries; excess entries are ignored).
102    pub primitives: Vec<ImplicitPrimitive>,
103    /// How the primitives are combined.
104    pub blend_mode: ImplicitBlendMode,
105    /// Ray-march quality settings.
106    pub march_options: GpuImplicitOptions,
107    /// Pick ID for unified selection API. `0` = not selectable.
108    pub id: u64,
109    /// Per-item appearance overrides (hidden, unlit, opacity, wireframe).
110    pub appearance: crate::scene::material::AppearanceSettings,
111    /// If `true`, draws an outline ring around the implicit surface.
112    pub selected: bool,
113}
114
115impl Default for GpuImplicitItem {
116    fn default() -> Self {
117        Self {
118            primitives: Vec::new(),
119            blend_mode: crate::resources::ImplicitBlendMode::Union,
120            march_options: GpuImplicitOptions::default(),
121            id: 0,
122            appearance: crate::scene::material::AppearanceSettings::default(),
123            selected: false,
124        }
125    }
126}
127
128impl ImplicitPrimitive {
129    /// Return a zeroed primitive with all fields set to zero.
130    pub fn zeroed() -> Self {
131        bytemuck::Zeroable::zeroed()
132    }
133}
134
135// ---------------------------------------------------------------------------
136// GPU-internal types (not exported from the crate root)
137// ---------------------------------------------------------------------------
138
139/// Flat uniform buffer layout matching the WGSL `ImplicitUniform` struct.
140///
141/// Total size: 32 header bytes + 16 * 64 primitive bytes = 1056 bytes.
142#[repr(C)]
143#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
144pub(crate) struct ImplicitUniformRaw {
145    pub num_primitives: u32,
146    pub blend_mode: u32,
147    pub max_steps: u32,
148    pub unlit: u32,
149    pub step_scale: f32,
150    pub hit_threshold: f32,
151    pub max_distance: f32,
152    pub opacity: f32,
153    pub primitives: [ImplicitPrimitive; 16],
154}
155
156/// Per-draw GPU data for one [`GpuImplicitItem`].
157pub(crate) struct ImplicitGpuItem {
158    pub _uniform_buf: wgpu::Buffer,
159    pub bind_group: wgpu::BindGroup,
160}
161
162// ---------------------------------------------------------------------------
163// Pipeline init and upload (impl ViewportGpuResources)
164// ---------------------------------------------------------------------------
165
166impl ViewportGpuResources {
167    /// Lazily create the GPU implicit surface render pipeline.
168    ///
169    /// No-op if already created.  Called from `prepare()` when any
170    /// `GpuImplicitItem` is submitted via `SceneFrame::gpu_implicit`.
171    pub(crate) fn ensure_implicit_pipeline(&mut self, device: &wgpu::Device) {
172        if self.implicit_pipeline.is_some() {
173            return;
174        }
175
176        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
177            label: Some("implicit_shader"),
178            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/implicit.wgsl").into()),
179        });
180
181        // Group 1: single uniform buffer containing ImplicitUniformRaw.
182        let implicit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
183            label: Some("implicit_bgl"),
184            entries: &[wgpu::BindGroupLayoutEntry {
185                binding: 0,
186                visibility: wgpu::ShaderStages::FRAGMENT,
187                ty: wgpu::BindingType::Buffer {
188                    ty: wgpu::BufferBindingType::Uniform,
189                    has_dynamic_offset: false,
190                    min_binding_size: None,
191                },
192                count: None,
193            }],
194        });
195
196        // Group 0 reuses camera_bind_group_layout (provides CameraUniform + LightsUniform).
197        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
198            label: Some("implicit_pipeline_layout"),
199            bind_group_layouts: &[&self.camera_bind_group_layout, &implicit_bgl],
200            push_constant_ranges: &[],
201        });
202
203        let make = |fmt: wgpu::TextureFormat| {
204            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
205                label: Some("implicit_pipeline"),
206                layout: Some(&layout),
207                vertex: wgpu::VertexState {
208                    module: &shader,
209                    entry_point: Some("vs_main"),
210                    buffers: &[],
211                    compilation_options: wgpu::PipelineCompilationOptions::default(),
212                },
213                fragment: Some(wgpu::FragmentState {
214                    module: &shader,
215                    entry_point: Some("fs_main"),
216                    targets: &[Some(wgpu::ColorTargetState {
217                        format: fmt,
218                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
219                        write_mask: wgpu::ColorWrites::ALL,
220                    })],
221                    compilation_options: wgpu::PipelineCompilationOptions::default(),
222                }),
223                primitive: wgpu::PrimitiveState {
224                    topology: wgpu::PrimitiveTopology::TriangleList,
225                    cull_mode: None,
226                    ..Default::default()
227                },
228                // Write depth so subsequent screen-image depth-composite items test against it.
229                depth_stencil: Some(wgpu::DepthStencilState {
230                    format: wgpu::TextureFormat::Depth24PlusStencil8,
231                    depth_write_enabled: true,
232                    depth_compare: wgpu::CompareFunction::LessEqual,
233                    stencil: wgpu::StencilState::default(),
234                    bias: wgpu::DepthBiasState::default(),
235                }),
236                multisample: wgpu::MultisampleState {
237                    count: 1,
238                    mask: !0,
239                    alpha_to_coverage_enabled: false,
240                },
241                multiview: None,
242                cache: None,
243            })
244        };
245
246        self.implicit_bgl = Some(implicit_bgl);
247        self.implicit_pipeline = Some(DualPipeline {
248            ldr: make(self.target_format),
249            hdr: make(wgpu::TextureFormat::Rgba16Float),
250        });
251    }
252
253    /// Lazily create the implicit surface outline mask pipeline.
254    ///
255    /// Reuses the same bind group layouts as `ensure_implicit_pipeline`. Must be
256    /// called after `ensure_implicit_pipeline` so that `implicit_bgl` is set.
257    /// No-op if already created.
258    pub(crate) fn ensure_implicit_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
259        if self.implicit_outline_mask_pipeline.is_some() {
260            return;
261        }
262
263        let implicit_bgl = self.implicit_bgl.as_ref().expect(
264            "ensure_implicit_pipeline must be called before ensure_implicit_outline_mask_pipeline",
265        );
266
267        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
268            label: Some("implicit_outline_mask_shader"),
269            source: wgpu::ShaderSource::Wgsl(
270                include_str!("../shaders/implicit_outline_mask.wgsl").into(),
271            ),
272        });
273
274        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
275            label: Some("implicit_outline_mask_pipeline_layout"),
276            bind_group_layouts: &[&self.camera_bind_group_layout, implicit_bgl],
277            push_constant_ranges: &[],
278        });
279
280        self.implicit_outline_mask_pipeline = Some(device.create_render_pipeline(
281            &wgpu::RenderPipelineDescriptor {
282                label: Some("implicit_outline_mask_pipeline"),
283                layout: Some(&layout),
284                vertex: wgpu::VertexState {
285                    module: &shader,
286                    entry_point: Some("vs_main"),
287                    buffers: &[],
288                    compilation_options: wgpu::PipelineCompilationOptions::default(),
289                },
290                fragment: Some(wgpu::FragmentState {
291                    module: &shader,
292                    entry_point: Some("fs_main"),
293                    targets: &[Some(wgpu::ColorTargetState {
294                        format: wgpu::TextureFormat::R8Unorm,
295                        blend: None,
296                        write_mask: wgpu::ColorWrites::ALL,
297                    })],
298                    compilation_options: wgpu::PipelineCompilationOptions::default(),
299                }),
300                primitive: wgpu::PrimitiveState {
301                    topology: wgpu::PrimitiveTopology::TriangleList,
302                    cull_mode: None,
303                    ..Default::default()
304                },
305                depth_stencil: Some(wgpu::DepthStencilState {
306                    format: wgpu::TextureFormat::Depth24PlusStencil8,
307                    depth_write_enabled: true,
308                    depth_compare: wgpu::CompareFunction::Less,
309                    stencil: wgpu::StencilState::default(),
310                    bias: wgpu::DepthBiasState::default(),
311                }),
312                multisample: wgpu::MultisampleState {
313                    count: 1,
314                    mask: !0,
315                    alpha_to_coverage_enabled: false,
316                },
317                multiview: None,
318                cache: None,
319            },
320        ));
321    }
322
323    /// Upload one [`GpuImplicitItem`] to GPU, returning the per-draw GPU data.
324    ///
325    /// Panics if called before `ensure_implicit_pipeline`.
326    pub(crate) fn upload_implicit_item(
327        &self,
328        device: &wgpu::Device,
329        item: &GpuImplicitItem,
330    ) -> ImplicitGpuItem {
331        // Build the flat uniform struct.
332        let blend_mode_u32 = match item.blend_mode {
333            ImplicitBlendMode::Union => 0u32,
334            ImplicitBlendMode::SmoothUnion => 1,
335            ImplicitBlendMode::Intersection => 2,
336        };
337
338        let mut raw = ImplicitUniformRaw {
339            num_primitives: item.primitives.len().min(16) as u32,
340            blend_mode: blend_mode_u32,
341            max_steps: item.march_options.max_steps,
342            unlit: item.appearance.unlit as u32,
343            step_scale: item.march_options.step_scale,
344            hit_threshold: item.march_options.hit_threshold,
345            max_distance: item.march_options.max_distance,
346            opacity: item.appearance.opacity,
347            primitives: [ImplicitPrimitive::zeroed(); 16],
348        };
349
350        for (i, prim) in item.primitives.iter().take(16).enumerate() {
351            raw.primitives[i] = *prim;
352        }
353
354        let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
355            label: Some("implicit_uniform_buf"),
356            contents: bytemuck::bytes_of(&raw),
357            usage: wgpu::BufferUsages::UNIFORM,
358        });
359
360        let bgl = self
361            .implicit_bgl
362            .as_ref()
363            .expect("ensure_implicit_pipeline not called");
364
365        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
366            label: Some("implicit_bind_group"),
367            layout: bgl,
368            entries: &[wgpu::BindGroupEntry {
369                binding: 0,
370                resource: uniform_buf.as_entire_binding(),
371            }],
372        });
373
374        ImplicitGpuItem {
375            _uniform_buf: uniform_buf,
376            bind_group,
377        }
378    }
379}