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 wgpu::util::DeviceExt as _;
7use crate::resources::{DualPipeline, ViewportGpuResources};
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 color: [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.color  = [1.0, 0.5, 0.2, 1.0];
93///
94/// let item = GpuImplicitItem {
95///     primitives:    vec![prim],
96///     blend_mode:    ImplicitBlendMode::SmoothUnion,
97///     march_options: GpuImplicitOptions::default(),
98/// };
99/// ```
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}
108
109impl ImplicitPrimitive {
110    /// Return a zeroed primitive with all fields set to zero.
111    pub fn zeroed() -> Self {
112        bytemuck::Zeroable::zeroed()
113    }
114}
115
116// ---------------------------------------------------------------------------
117// GPU-internal types (not exported from the crate root)
118// ---------------------------------------------------------------------------
119
120/// Flat uniform buffer layout matching the WGSL `ImplicitUniform` struct.
121///
122/// Total size: 32 header bytes + 16 * 64 primitive bytes = 1056 bytes.
123#[repr(C)]
124#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
125pub(crate) struct ImplicitUniformRaw {
126    pub num_primitives: u32,
127    pub blend_mode: u32,
128    pub max_steps: u32,
129    pub _pad0: u32,
130    pub step_scale: f32,
131    pub hit_threshold: f32,
132    pub max_distance: f32,
133    pub _pad1: f32,
134    pub primitives: [ImplicitPrimitive; 16],
135}
136
137/// Per-draw GPU data for one [`GpuImplicitItem`].
138pub(crate) struct ImplicitGpuItem {
139    pub _uniform_buf: wgpu::Buffer,
140    pub bind_group: wgpu::BindGroup,
141}
142
143// ---------------------------------------------------------------------------
144// Pipeline init and upload (impl ViewportGpuResources)
145// ---------------------------------------------------------------------------
146
147impl ViewportGpuResources {
148    /// Lazily create the GPU implicit surface render pipeline.
149    ///
150    /// No-op if already created.  Called from `prepare()` when any
151    /// `GpuImplicitItem` is submitted via `SceneFrame::gpu_implicit`.
152    pub(crate) fn ensure_implicit_pipeline(&mut self, device: &wgpu::Device) {
153        if self.implicit_pipeline.is_some() {
154            return;
155        }
156
157        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
158            label: Some("implicit_shader"),
159            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/implicit.wgsl").into()),
160        });
161
162        // Group 1: single uniform buffer containing ImplicitUniformRaw.
163        let implicit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
164            label: Some("implicit_bgl"),
165            entries: &[wgpu::BindGroupLayoutEntry {
166                binding: 0,
167                visibility: wgpu::ShaderStages::FRAGMENT,
168                ty: wgpu::BindingType::Buffer {
169                    ty: wgpu::BufferBindingType::Uniform,
170                    has_dynamic_offset: false,
171                    min_binding_size: None,
172                },
173                count: None,
174            }],
175        });
176
177        // Group 0 reuses camera_bind_group_layout (provides CameraUniform + LightsUniform).
178        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
179            label: Some("implicit_pipeline_layout"),
180            bind_group_layouts: &[&self.camera_bind_group_layout, &implicit_bgl],
181            push_constant_ranges: &[],
182        });
183
184        let make = |fmt: wgpu::TextureFormat| {
185            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
186                label: Some("implicit_pipeline"),
187                layout: Some(&layout),
188                vertex: wgpu::VertexState {
189                    module: &shader,
190                    entry_point: Some("vs_main"),
191                    buffers: &[],
192                    compilation_options: wgpu::PipelineCompilationOptions::default(),
193                },
194                fragment: Some(wgpu::FragmentState {
195                    module: &shader,
196                    entry_point: Some("fs_main"),
197                    targets: &[Some(wgpu::ColorTargetState {
198                        format: fmt,
199                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
200                        write_mask: wgpu::ColorWrites::ALL,
201                    })],
202                    compilation_options: wgpu::PipelineCompilationOptions::default(),
203                }),
204                primitive: wgpu::PrimitiveState {
205                    topology: wgpu::PrimitiveTopology::TriangleList,
206                    cull_mode: None,
207                    ..Default::default()
208                },
209                // Write depth so subsequent screen-image depth-composite items test against it.
210                depth_stencil: Some(wgpu::DepthStencilState {
211                    format: wgpu::TextureFormat::Depth24PlusStencil8,
212                    depth_write_enabled: true,
213                    depth_compare: wgpu::CompareFunction::LessEqual,
214                    stencil: wgpu::StencilState::default(),
215                    bias: wgpu::DepthBiasState::default(),
216                }),
217                multisample: wgpu::MultisampleState {
218                    count: 1,
219                    mask: !0,
220                    alpha_to_coverage_enabled: false,
221                },
222                multiview: None,
223                cache: None,
224            })
225        };
226
227        self.implicit_bgl = Some(implicit_bgl);
228        self.implicit_pipeline = Some(DualPipeline {
229            ldr: make(self.target_format),
230            hdr: make(wgpu::TextureFormat::Rgba16Float),
231        });
232    }
233
234    /// Upload one [`GpuImplicitItem`] to GPU, returning the per-draw GPU data.
235    ///
236    /// Panics if called before `ensure_implicit_pipeline`.
237    pub(crate) fn upload_implicit_item(
238        &self,
239        device: &wgpu::Device,
240        item: &GpuImplicitItem,
241    ) -> ImplicitGpuItem {
242        // Build the flat uniform struct.
243        let blend_mode_u32 = match item.blend_mode {
244            ImplicitBlendMode::Union => 0u32,
245            ImplicitBlendMode::SmoothUnion => 1,
246            ImplicitBlendMode::Intersection => 2,
247        };
248
249        let mut raw = ImplicitUniformRaw {
250            num_primitives: item.primitives.len().min(16) as u32,
251            blend_mode: blend_mode_u32,
252            max_steps: item.march_options.max_steps,
253            _pad0: 0,
254            step_scale: item.march_options.step_scale,
255            hit_threshold: item.march_options.hit_threshold,
256            max_distance: item.march_options.max_distance,
257            _pad1: 0.0,
258            primitives: [ImplicitPrimitive::zeroed(); 16],
259        };
260
261        for (i, prim) in item.primitives.iter().take(16).enumerate() {
262            raw.primitives[i] = *prim;
263        }
264
265        let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
266            label: Some("implicit_uniform_buf"),
267            contents: bytemuck::bytes_of(&raw),
268            usage: wgpu::BufferUsages::UNIFORM,
269        });
270
271        let bgl = self
272            .implicit_bgl
273            .as_ref()
274            .expect("ensure_implicit_pipeline not called");
275
276        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
277            label: Some("implicit_bind_group"),
278            layout: bgl,
279            entries: &[wgpu::BindGroupEntry {
280                binding: 0,
281                resource: uniform_buf.as_entire_binding(),
282            }],
283        });
284
285        ImplicitGpuItem { _uniform_buf: uniform_buf, bind_group }
286    }
287}