Another project
1use wgpu::util::DeviceExt;
2
3use bone_types::{Camera3, Point3, ProjectionKind, UnitVec3, Vec3};
4
5use crate::RenderTargets;
6use crate::ViewportRegion;
7use crate::gpu::{Gpu, PICK_FORMAT};
8use crate::lower_f32;
9use crate::pick::PickId;
10use crate::pipelines::solid::DEPTH_FORMAT;
11use crate::scene::{EdgeScene, GenuineEdge, SilhouetteCandidate};
12
13const DEPTH_BIAS_NDC_PER_PX: f32 = 0.001_5;
14const GENUINE_OFFSET_PX: f32 = 2.0;
15const SILHOUETTE_OFFSET_PX: f32 = 3.0;
16const EDGE_HALF_WIDTH_PX: f32 = 0.75;
17
18const STYLE_DASHED: u32 = 1 << 0;
19const STYLE_HIDDEN: u32 = 1 << 1;
20
21#[derive(Copy, Clone)]
22enum EdgeKind {
23 Genuine,
24 Silhouette,
25 Crease,
26}
27
28impl EdgeKind {
29 const fn style_bits(self) -> u32 {
30 match self {
31 Self::Genuine => 0,
32 Self::Silhouette => 1 << 2,
33 Self::Crease => 2 << 2,
34 }
35 }
36}
37
38#[derive(Copy, Clone)]
39enum EdgeLayer {
40 Visible,
41 Hidden,
42}
43
44impl EdgeLayer {
45 const fn extra_style_bits(self) -> u32 {
46 match self {
47 Self::Visible => 0,
48 Self::Hidden => STYLE_DASHED | STYLE_HIDDEN,
49 }
50 }
51
52 const fn depth_compare(self) -> wgpu::CompareFunction {
53 match self {
54 Self::Visible => wgpu::CompareFunction::LessEqual,
55 Self::Hidden => wgpu::CompareFunction::Greater,
56 }
57 }
58}
59
60#[derive(Copy, Clone)]
61pub(crate) enum HiddenEdges {
62 Omit,
63 Dashed,
64}
65
66#[repr(C)]
67#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
68struct EdgeInstance {
69 a: [f32; 3],
70 b: [f32; 3],
71 half_width_px: f32,
72 depth_offset_px: f32,
73 style_bits: u32,
74 pick_id: u32,
75}
76
77const INSTANCE_STRIDE: u64 = core::mem::size_of::<EdgeInstance>() as u64;
78
79#[repr(C, align(16))]
80#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
81struct EdgeFrame {
82 clip_from_world: [f32; 16],
83 edge_color: [f32; 4],
84 hidden_color: [f32; 4],
85 viewport_px: [f32; 2],
86 dash_period_px: f32,
87 dash_on_ratio: f32,
88 depth_bias_ndc: f32,
89 _pad: [f32; 3],
90}
91
92const UNIFORM_SIZE: u64 = core::mem::size_of::<EdgeFrame>() as u64;
93
94const INSTANCE_ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
95 0 => Float32x3,
96 1 => Float32x3,
97 2 => Float32,
98 3 => Float32,
99 4 => Uint32,
100 5 => Uint32,
101];
102
103#[derive(Copy, Clone)]
104pub(crate) enum EdgeProjection {
105 Orthographic { toward_viewer: Vec3 },
106 Perspective { eye: Point3 },
107}
108
109impl EdgeProjection {
110 #[must_use]
111 pub fn from_camera(camera: Camera3) -> Self {
112 match camera.projection().kind() {
113 ProjectionKind::Orthographic { .. } => Self::Orthographic {
114 toward_viewer: camera.eye() - camera.target(),
115 },
116 ProjectionKind::Perspective { .. } => Self::Perspective { eye: camera.eye() },
117 }
118 }
119
120 fn view_at(self, point: Point3) -> Vec3 {
121 match self {
122 Self::Orthographic { toward_viewer } => toward_viewer,
123 Self::Perspective { eye } => eye - point,
124 }
125 }
126}
127
128#[derive(Copy, Clone)]
129pub(crate) struct EdgeView {
130 pub clip_from_world: [f32; 16],
131 pub projection: EdgeProjection,
132 pub viewport_px: [f32; 2],
133 pub crease_threshold_rad: f64,
134 pub dash_period_px: f32,
135 pub dash_on_ratio: f32,
136 pub edge_color: [f32; 4],
137 pub hidden_color: [f32; 4],
138 pub region: ViewportRegion,
139}
140
141pub(crate) struct Edge3dPipeline {
142 device: wgpu::Device,
143 queue: wgpu::Queue,
144 visible: wgpu::RenderPipeline,
145 hidden: wgpu::RenderPipeline,
146 uniform_buffer: wgpu::Buffer,
147 bind_group: wgpu::BindGroup,
148}
149
150impl Edge3dPipeline {
151 #[must_use]
152 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
153 let device = gpu.device().clone();
154 let queue = gpu.queue().clone();
155 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
156 label: Some("bone-render:edge3d-shader"),
157 source: wgpu::ShaderSource::Wgsl(include_str!("edge_3d.wgsl").into()),
158 });
159 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
160 label: Some("bone-render:edge3d-bgl"),
161 entries: &[wgpu::BindGroupLayoutEntry {
162 binding: 0,
163 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
164 ty: wgpu::BindingType::Buffer {
165 ty: wgpu::BufferBindingType::Uniform,
166 has_dynamic_offset: false,
167 min_binding_size: wgpu::BufferSize::new(UNIFORM_SIZE),
168 },
169 count: None,
170 }],
171 });
172 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
173 label: Some("bone-render:edge3d-layout"),
174 bind_group_layouts: &[Some(&bind_group_layout)],
175 immediate_size: 0,
176 });
177 let visible = build_pipeline(
178 &device,
179 &pipeline_layout,
180 &shader,
181 color_format,
182 EdgeLayer::Visible.depth_compare(),
183 );
184 let hidden = build_pipeline(
185 &device,
186 &pipeline_layout,
187 &shader,
188 color_format,
189 EdgeLayer::Hidden.depth_compare(),
190 );
191 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
192 label: Some("bone-render:edge3d-uniform"),
193 size: UNIFORM_SIZE,
194 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
195 mapped_at_creation: false,
196 });
197 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
198 label: Some("bone-render:edge3d-bg"),
199 layout: &bind_group_layout,
200 entries: &[wgpu::BindGroupEntry {
201 binding: 0,
202 resource: uniform_buffer.as_entire_binding(),
203 }],
204 });
205 Self {
206 device,
207 queue,
208 visible,
209 hidden,
210 uniform_buffer,
211 bind_group,
212 }
213 }
214
215 pub fn draw(
216 &self,
217 encoder: &mut wgpu::CommandEncoder,
218 targets: RenderTargets<'_>,
219 depth_view: &wgpu::TextureView,
220 scene: &EdgeScene,
221 view: EdgeView,
222 hidden: HiddenEdges,
223 ) {
224 let visible = build_instances(scene, view, EdgeLayer::Visible.extra_style_bits());
225 let occluded = match hidden {
226 HiddenEdges::Dashed => {
227 build_instances(scene, view, EdgeLayer::Hidden.extra_style_bits())
228 }
229 HiddenEdges::Omit => Vec::new(),
230 };
231 if visible.is_empty() && occluded.is_empty() {
232 return;
233 }
234 let uniform = EdgeFrame {
235 clip_from_world: view.clip_from_world,
236 edge_color: view.edge_color,
237 hidden_color: view.hidden_color,
238 viewport_px: view.viewport_px,
239 dash_period_px: view.dash_period_px,
240 dash_on_ratio: view.dash_on_ratio,
241 depth_bias_ndc: DEPTH_BIAS_NDC_PER_PX,
242 _pad: [0.0; 3],
243 };
244 self.queue
245 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
246 let visible_buffer = (!visible.is_empty()).then(|| self.instance_buffer(&visible));
247 let occluded_buffer = (!occluded.is_empty()).then(|| self.instance_buffer(&occluded));
248 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
249 label: Some("bone-render:edge3d-pass"),
250 color_attachments: &[
251 Some(wgpu::RenderPassColorAttachment {
252 view: targets.color,
253 resolve_target: None,
254 depth_slice: None,
255 ops: wgpu::Operations {
256 load: wgpu::LoadOp::Load,
257 store: wgpu::StoreOp::Store,
258 },
259 }),
260 Some(wgpu::RenderPassColorAttachment {
261 view: targets.pick,
262 resolve_target: None,
263 depth_slice: None,
264 ops: wgpu::Operations {
265 load: wgpu::LoadOp::Load,
266 store: wgpu::StoreOp::Store,
267 },
268 }),
269 ],
270 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
271 view: depth_view,
272 depth_ops: Some(wgpu::Operations {
273 load: wgpu::LoadOp::Load,
274 store: wgpu::StoreOp::Store,
275 }),
276 stencil_ops: None,
277 }),
278 timestamp_writes: None,
279 occlusion_query_set: None,
280 multiview_mask: None,
281 });
282 crate::apply_viewport_region(&mut pass, view.region);
283 pass.set_bind_group(0, &self.bind_group, &[]);
284 if let Some(buffer) = &visible_buffer {
285 record_layer(&mut pass, &self.visible, buffer, visible.len());
286 }
287 if let Some(buffer) = &occluded_buffer {
288 record_layer(&mut pass, &self.hidden, buffer, occluded.len());
289 }
290 }
291
292 fn instance_buffer(&self, instances: &[EdgeInstance]) -> wgpu::Buffer {
293 self.device
294 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
295 label: Some("bone-render:edge3d-instances"),
296 contents: bytemuck::cast_slice(instances),
297 usage: wgpu::BufferUsages::VERTEX,
298 })
299 }
300}
301
302fn record_layer(
303 pass: &mut wgpu::RenderPass<'_>,
304 pipeline: &wgpu::RenderPipeline,
305 buffer: &wgpu::Buffer,
306 len: usize,
307) {
308 let Ok(count) = u32::try_from(len) else {
309 panic!("edge instance count {len} exceeds u32::MAX");
310 };
311 pass.set_pipeline(pipeline);
312 pass.set_vertex_buffer(0, buffer.slice(..));
313 pass.draw(0..6, 0..count);
314}
315
316impl core::fmt::Debug for Edge3dPipeline {
317 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
318 f.debug_struct("Edge3dPipeline").finish_non_exhaustive()
319 }
320}
321
322fn build_pipeline(
323 device: &wgpu::Device,
324 layout: &wgpu::PipelineLayout,
325 shader: &wgpu::ShaderModule,
326 color_format: wgpu::TextureFormat,
327 depth_compare: wgpu::CompareFunction,
328) -> wgpu::RenderPipeline {
329 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
330 label: Some("bone-render:edge3d-pipeline"),
331 layout: Some(layout),
332 vertex: wgpu::VertexState {
333 module: shader,
334 entry_point: Some("vs"),
335 compilation_options: wgpu::PipelineCompilationOptions::default(),
336 buffers: &[wgpu::VertexBufferLayout {
337 array_stride: INSTANCE_STRIDE,
338 step_mode: wgpu::VertexStepMode::Instance,
339 attributes: &INSTANCE_ATTRS,
340 }],
341 },
342 fragment: Some(wgpu::FragmentState {
343 module: shader,
344 entry_point: Some("fs"),
345 compilation_options: wgpu::PipelineCompilationOptions::default(),
346 targets: &[
347 Some(wgpu::ColorTargetState {
348 format: color_format,
349 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
350 write_mask: wgpu::ColorWrites::ALL,
351 }),
352 Some(wgpu::ColorTargetState {
353 format: PICK_FORMAT,
354 blend: None,
355 write_mask: wgpu::ColorWrites::ALL,
356 }),
357 ],
358 }),
359 primitive: wgpu::PrimitiveState {
360 topology: wgpu::PrimitiveTopology::TriangleList,
361 strip_index_format: None,
362 front_face: wgpu::FrontFace::Ccw,
363 cull_mode: None,
364 polygon_mode: wgpu::PolygonMode::Fill,
365 conservative: false,
366 unclipped_depth: false,
367 },
368 depth_stencil: Some(wgpu::DepthStencilState {
369 format: DEPTH_FORMAT,
370 depth_write_enabled: Some(false),
371 depth_compare: Some(depth_compare),
372 stencil: wgpu::StencilState::default(),
373 bias: wgpu::DepthBiasState::default(),
374 }),
375 multisample: wgpu::MultisampleState::default(),
376 multiview_mask: None,
377 cache: None,
378 })
379}
380
381fn build_instances(scene: &EdgeScene, view: EdgeView, extra_bits: u32) -> Vec<EdgeInstance> {
382 let genuine = scene
383 .genuine()
384 .iter()
385 .copied()
386 .map(|edge| genuine_instance(edge, view.crease_threshold_rad, extra_bits));
387 let silhouette = scene
388 .silhouettes()
389 .iter()
390 .copied()
391 .filter(|candidate| straddles(*candidate, view.projection))
392 .map(|candidate| silhouette_instance(candidate, extra_bits));
393 genuine.chain(silhouette).collect()
394}
395
396fn genuine_instance(edge: GenuineEdge, threshold_rad: f64, extra_bits: u32) -> EdgeInstance {
397 let kind = if edge.crease().radians() >= threshold_rad {
398 EdgeKind::Crease
399 } else {
400 EdgeKind::Genuine
401 };
402 instance(
403 edge.a(),
404 edge.b(),
405 edge.pick(),
406 kind,
407 GENUINE_OFFSET_PX,
408 extra_bits,
409 )
410}
411
412fn silhouette_instance(candidate: SilhouetteCandidate, extra_bits: u32) -> EdgeInstance {
413 instance(
414 candidate.a(),
415 candidate.b(),
416 candidate.pick(),
417 EdgeKind::Silhouette,
418 SILHOUETTE_OFFSET_PX,
419 extra_bits,
420 )
421}
422
423fn instance(
424 a: Point3,
425 b: Point3,
426 pick: PickId,
427 kind: EdgeKind,
428 depth_offset_px: f32,
429 extra_bits: u32,
430) -> EdgeInstance {
431 EdgeInstance {
432 a: lower_point(a),
433 b: lower_point(b),
434 half_width_px: EDGE_HALF_WIDTH_PX,
435 depth_offset_px,
436 style_bits: kind.style_bits() | extra_bits,
437 pick_id: pick.raw(),
438 }
439}
440
441fn lower_point(p: Point3) -> [f32; 3] {
442 let (x, y, z) = p.coords_mm();
443 [lower_f32(x), lower_f32(y), lower_f32(z)]
444}
445
446fn straddles(candidate: SilhouetteCandidate, projection: EdgeProjection) -> bool {
447 let view = projection.view_at(midpoint(candidate.a(), candidate.b()));
448 let front_a = dot_unit_vec(candidate.normal_a(), view);
449 let front_b = dot_unit_vec(candidate.normal_b(), view);
450 front_a * front_b < 0.0
451}
452
453fn midpoint(a: Point3, b: Point3) -> Point3 {
454 let (ax, ay, az) = a.coords_mm();
455 let (bx, by, bz) = b.coords_mm();
456 Point3::from_mm(0.5 * (ax + bx), 0.5 * (ay + by), 0.5 * (az + bz))
457}
458
459fn dot_unit_vec(unit: UnitVec3, vec: Vec3) -> f64 {
460 let (ux, uy, uz) = unit.components();
461 let (vx, vy, vz) = vec.coords_mm();
462 ux * vx + uy * vy + uz * vz
463}