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::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 pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
185 label: Some("implicit_pipeline"),
186 layout: Some(&layout),
187 vertex: wgpu::VertexState {
188 module: &shader,
189 entry_point: Some("vs_main"),
190 buffers: &[],
191 compilation_options: wgpu::PipelineCompilationOptions::default(),
192 },
193 fragment: Some(wgpu::FragmentState {
194 module: &shader,
195 entry_point: Some("fs_main"),
196 targets: &[Some(wgpu::ColorTargetState {
197 format: self.target_format,
198 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
199 write_mask: wgpu::ColorWrites::ALL,
200 })],
201 compilation_options: wgpu::PipelineCompilationOptions::default(),
202 }),
203 primitive: wgpu::PrimitiveState {
204 topology: wgpu::PrimitiveTopology::TriangleList,
205 cull_mode: None,
206 ..Default::default()
207 },
208 // Write depth so subsequent screen-image depth-composite items test against it.
209 depth_stencil: Some(wgpu::DepthStencilState {
210 format: wgpu::TextureFormat::Depth24PlusStencil8,
211 depth_write_enabled: true,
212 depth_compare: wgpu::CompareFunction::LessEqual,
213 stencil: wgpu::StencilState::default(),
214 bias: wgpu::DepthBiasState::default(),
215 }),
216 multisample: wgpu::MultisampleState {
217 count: 1,
218 mask: !0,
219 alpha_to_coverage_enabled: false,
220 },
221 multiview: None,
222 cache: None,
223 });
224
225 self.implicit_bgl = Some(implicit_bgl);
226 self.implicit_pipeline = Some(pipeline);
227 }
228
229 /// Upload one [`GpuImplicitItem`] to GPU, returning the per-draw GPU data.
230 ///
231 /// Panics if called before `ensure_implicit_pipeline`.
232 pub(crate) fn upload_implicit_item(
233 &self,
234 device: &wgpu::Device,
235 item: &GpuImplicitItem,
236 ) -> ImplicitGpuItem {
237 // Build the flat uniform struct.
238 let blend_mode_u32 = match item.blend_mode {
239 ImplicitBlendMode::Union => 0u32,
240 ImplicitBlendMode::SmoothUnion => 1,
241 ImplicitBlendMode::Intersection => 2,
242 };
243
244 let mut raw = ImplicitUniformRaw {
245 num_primitives: item.primitives.len().min(16) as u32,
246 blend_mode: blend_mode_u32,
247 max_steps: item.march_options.max_steps,
248 _pad0: 0,
249 step_scale: item.march_options.step_scale,
250 hit_threshold: item.march_options.hit_threshold,
251 max_distance: item.march_options.max_distance,
252 _pad1: 0.0,
253 primitives: [ImplicitPrimitive::zeroed(); 16],
254 };
255
256 for (i, prim) in item.primitives.iter().take(16).enumerate() {
257 raw.primitives[i] = *prim;
258 }
259
260 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
261 label: Some("implicit_uniform_buf"),
262 contents: bytemuck::bytes_of(&raw),
263 usage: wgpu::BufferUsages::UNIFORM,
264 });
265
266 let bgl = self
267 .implicit_bgl
268 .as_ref()
269 .expect("ensure_implicit_pipeline not called");
270
271 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
272 label: Some("implicit_bind_group"),
273 layout: bgl,
274 entries: &[wgpu::BindGroupEntry {
275 binding: 0,
276 resource: uniform_buf.as_entire_binding(),
277 }],
278 });
279
280 ImplicitGpuItem { _uniform_buf: uniform_buf, bind_group }
281 }
282}