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}