Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(render): 3d edge draw pipeline

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jun 6, 2026, 1:33 AM +0300) commit 195f1178 parent f48fd745 change-id nsxlunlv
+519 -1
+371
crates/bone-render/src/pipelines/edge_3d.rs
··· 1 + use wgpu::util::DeviceExt; 2 + 3 + use bone_types::{Camera3, Point3, ProjectionKind, UnitVec3, Vec3}; 4 + 5 + use crate::RenderTargets; 6 + use crate::gpu::{Gpu, PICK_FORMAT}; 7 + use crate::lower_f32; 8 + use crate::pick::PickId; 9 + use crate::pipelines::solid::DEPTH_FORMAT; 10 + use crate::scene::{EdgeScene, GenuineEdge, SilhouetteCandidate}; 11 + 12 + const EDGE_COLOR: [f32; 4] = [0.90, 0.90, 0.93, 1.0]; 13 + const HIDDEN_COLOR: [f32; 4] = [0.45, 0.45, 0.50, 1.0]; 14 + const DEPTH_BIAS_NDC_PER_PX: f32 = 0.001_5; 15 + const GENUINE_OFFSET_PX: f32 = 2.0; 16 + const SILHOUETTE_OFFSET_PX: f32 = 3.0; 17 + const EDGE_HALF_WIDTH_PX: f32 = 0.75; 18 + 19 + #[derive(Copy, Clone)] 20 + enum EdgeKind { 21 + Genuine, 22 + Silhouette, 23 + Crease, 24 + } 25 + 26 + impl EdgeKind { 27 + const fn style_bits(self) -> u32 { 28 + match self { 29 + Self::Genuine => 0, 30 + Self::Silhouette => 1 << 2, 31 + Self::Crease => 2 << 2, 32 + } 33 + } 34 + } 35 + 36 + #[repr(C)] 37 + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 38 + struct EdgeInstance { 39 + a: [f32; 3], 40 + b: [f32; 3], 41 + half_width_px: f32, 42 + depth_offset_px: f32, 43 + style_bits: u32, 44 + pick_id: u32, 45 + } 46 + 47 + const INSTANCE_STRIDE: u64 = core::mem::size_of::<EdgeInstance>() as u64; 48 + 49 + #[repr(C, align(16))] 50 + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 51 + struct EdgeFrame { 52 + clip_from_world: [f32; 16], 53 + edge_color: [f32; 4], 54 + hidden_color: [f32; 4], 55 + viewport_px: [f32; 2], 56 + dash_period_px: f32, 57 + dash_on_ratio: f32, 58 + depth_bias_ndc: f32, 59 + _pad: [f32; 3], 60 + } 61 + 62 + const UNIFORM_SIZE: u64 = core::mem::size_of::<EdgeFrame>() as u64; 63 + 64 + const INSTANCE_ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![ 65 + 0 => Float32x3, 66 + 1 => Float32x3, 67 + 2 => Float32, 68 + 3 => Float32, 69 + 4 => Uint32, 70 + 5 => Uint32, 71 + ]; 72 + 73 + #[derive(Copy, Clone)] 74 + pub enum EdgeProjection { 75 + Orthographic { toward_viewer: Vec3 }, 76 + Perspective { eye: Point3 }, 77 + } 78 + 79 + impl EdgeProjection { 80 + #[must_use] 81 + pub fn from_camera(camera: Camera3) -> Self { 82 + match camera.projection().kind() { 83 + ProjectionKind::Orthographic { .. } => Self::Orthographic { 84 + toward_viewer: camera.eye() - camera.target(), 85 + }, 86 + ProjectionKind::Perspective { .. } => Self::Perspective { eye: camera.eye() }, 87 + } 88 + } 89 + 90 + fn view_at(self, point: Point3) -> Vec3 { 91 + match self { 92 + Self::Orthographic { toward_viewer } => toward_viewer, 93 + Self::Perspective { eye } => eye - point, 94 + } 95 + } 96 + } 97 + 98 + #[derive(Copy, Clone)] 99 + pub struct EdgeView { 100 + pub clip_from_world: [f32; 16], 101 + pub projection: EdgeProjection, 102 + pub viewport_px: [f32; 2], 103 + pub crease_threshold_rad: f64, 104 + pub dash_period_px: f32, 105 + pub dash_on_ratio: f32, 106 + } 107 + 108 + pub struct Edge3dPipeline { 109 + device: wgpu::Device, 110 + queue: wgpu::Queue, 111 + pipeline: wgpu::RenderPipeline, 112 + uniform_buffer: wgpu::Buffer, 113 + bind_group: wgpu::BindGroup, 114 + } 115 + 116 + impl Edge3dPipeline { 117 + #[must_use] 118 + pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 119 + let device = gpu.device().clone(); 120 + let queue = gpu.queue().clone(); 121 + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 122 + label: Some("bone-render:edge3d-shader"), 123 + source: wgpu::ShaderSource::Wgsl(include_str!("edge_3d.wgsl").into()), 124 + }); 125 + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 126 + label: Some("bone-render:edge3d-bgl"), 127 + entries: &[wgpu::BindGroupLayoutEntry { 128 + binding: 0, 129 + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 130 + ty: wgpu::BindingType::Buffer { 131 + ty: wgpu::BufferBindingType::Uniform, 132 + has_dynamic_offset: false, 133 + min_binding_size: wgpu::BufferSize::new(UNIFORM_SIZE), 134 + }, 135 + count: None, 136 + }], 137 + }); 138 + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 139 + label: Some("bone-render:edge3d-layout"), 140 + bind_group_layouts: &[Some(&bind_group_layout)], 141 + immediate_size: 0, 142 + }); 143 + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 144 + label: Some("bone-render:edge3d-pipeline"), 145 + layout: Some(&pipeline_layout), 146 + vertex: wgpu::VertexState { 147 + module: &shader, 148 + entry_point: Some("vs"), 149 + compilation_options: wgpu::PipelineCompilationOptions::default(), 150 + buffers: &[wgpu::VertexBufferLayout { 151 + array_stride: INSTANCE_STRIDE, 152 + step_mode: wgpu::VertexStepMode::Instance, 153 + attributes: &INSTANCE_ATTRS, 154 + }], 155 + }, 156 + fragment: Some(wgpu::FragmentState { 157 + module: &shader, 158 + entry_point: Some("fs"), 159 + compilation_options: wgpu::PipelineCompilationOptions::default(), 160 + targets: &[ 161 + Some(wgpu::ColorTargetState { 162 + format: color_format, 163 + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 164 + write_mask: wgpu::ColorWrites::ALL, 165 + }), 166 + Some(wgpu::ColorTargetState { 167 + format: PICK_FORMAT, 168 + blend: None, 169 + write_mask: wgpu::ColorWrites::ALL, 170 + }), 171 + ], 172 + }), 173 + primitive: wgpu::PrimitiveState { 174 + topology: wgpu::PrimitiveTopology::TriangleList, 175 + strip_index_format: None, 176 + front_face: wgpu::FrontFace::Ccw, 177 + cull_mode: None, 178 + polygon_mode: wgpu::PolygonMode::Fill, 179 + conservative: false, 180 + unclipped_depth: false, 181 + }, 182 + depth_stencil: Some(wgpu::DepthStencilState { 183 + format: DEPTH_FORMAT, 184 + depth_write_enabled: Some(false), 185 + depth_compare: Some(wgpu::CompareFunction::LessEqual), 186 + stencil: wgpu::StencilState::default(), 187 + bias: wgpu::DepthBiasState::default(), 188 + }), 189 + multisample: wgpu::MultisampleState::default(), 190 + multiview_mask: None, 191 + cache: None, 192 + }); 193 + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { 194 + label: Some("bone-render:edge3d-uniform"), 195 + size: UNIFORM_SIZE, 196 + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 197 + mapped_at_creation: false, 198 + }); 199 + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 200 + label: Some("bone-render:edge3d-bg"), 201 + layout: &bind_group_layout, 202 + entries: &[wgpu::BindGroupEntry { 203 + binding: 0, 204 + resource: uniform_buffer.as_entire_binding(), 205 + }], 206 + }); 207 + Self { 208 + device, 209 + queue, 210 + pipeline, 211 + uniform_buffer, 212 + bind_group, 213 + } 214 + } 215 + 216 + pub fn draw( 217 + &self, 218 + encoder: &mut wgpu::CommandEncoder, 219 + targets: RenderTargets<'_>, 220 + depth_view: &wgpu::TextureView, 221 + scene: &EdgeScene, 222 + view: EdgeView, 223 + ) { 224 + let instances = build_instances(scene, view); 225 + if instances.is_empty() { 226 + return; 227 + } 228 + let uniform = EdgeFrame { 229 + clip_from_world: view.clip_from_world, 230 + edge_color: EDGE_COLOR, 231 + hidden_color: HIDDEN_COLOR, 232 + viewport_px: view.viewport_px, 233 + dash_period_px: view.dash_period_px, 234 + dash_on_ratio: view.dash_on_ratio, 235 + depth_bias_ndc: DEPTH_BIAS_NDC_PER_PX, 236 + _pad: [0.0; 3], 237 + }; 238 + self.queue 239 + .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform)); 240 + let instance_buffer = self 241 + .device 242 + .create_buffer_init(&wgpu::util::BufferInitDescriptor { 243 + label: Some("bone-render:edge3d-instances"), 244 + contents: bytemuck::cast_slice(&instances), 245 + usage: wgpu::BufferUsages::VERTEX, 246 + }); 247 + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 248 + label: Some("bone-render:edge3d-pass"), 249 + color_attachments: &[ 250 + Some(wgpu::RenderPassColorAttachment { 251 + view: targets.color, 252 + resolve_target: None, 253 + depth_slice: None, 254 + ops: wgpu::Operations { 255 + load: wgpu::LoadOp::Load, 256 + store: wgpu::StoreOp::Store, 257 + }, 258 + }), 259 + Some(wgpu::RenderPassColorAttachment { 260 + view: targets.pick, 261 + resolve_target: None, 262 + depth_slice: None, 263 + ops: wgpu::Operations { 264 + load: wgpu::LoadOp::Load, 265 + store: wgpu::StoreOp::Store, 266 + }, 267 + }), 268 + ], 269 + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 270 + view: depth_view, 271 + depth_ops: Some(wgpu::Operations { 272 + load: wgpu::LoadOp::Load, 273 + store: wgpu::StoreOp::Store, 274 + }), 275 + stencil_ops: None, 276 + }), 277 + timestamp_writes: None, 278 + occlusion_query_set: None, 279 + multiview_mask: None, 280 + }); 281 + let len = instances.len(); 282 + let Ok(count) = u32::try_from(len) else { 283 + panic!("edge instance count {len} exceeds u32::MAX"); 284 + }; 285 + pass.set_pipeline(&self.pipeline); 286 + pass.set_bind_group(0, &self.bind_group, &[]); 287 + pass.set_vertex_buffer(0, instance_buffer.slice(..)); 288 + pass.draw(0..6, 0..count); 289 + } 290 + } 291 + 292 + impl core::fmt::Debug for Edge3dPipeline { 293 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 294 + f.debug_struct("Edge3dPipeline").finish_non_exhaustive() 295 + } 296 + } 297 + 298 + fn build_instances(scene: &EdgeScene, view: EdgeView) -> Vec<EdgeInstance> { 299 + let genuine = scene 300 + .genuine() 301 + .iter() 302 + .copied() 303 + .map(|edge| genuine_instance(edge, view.crease_threshold_rad)); 304 + let silhouette = scene 305 + .silhouettes() 306 + .iter() 307 + .copied() 308 + .filter(|candidate| straddles(*candidate, view.projection)) 309 + .map(silhouette_instance); 310 + genuine.chain(silhouette).collect() 311 + } 312 + 313 + fn genuine_instance(edge: GenuineEdge, threshold_rad: f64) -> EdgeInstance { 314 + let kind = if edge.crease().radians() >= threshold_rad { 315 + EdgeKind::Crease 316 + } else { 317 + EdgeKind::Genuine 318 + }; 319 + instance(edge.a(), edge.b(), edge.pick(), kind, GENUINE_OFFSET_PX) 320 + } 321 + 322 + fn silhouette_instance(candidate: SilhouetteCandidate) -> EdgeInstance { 323 + instance( 324 + candidate.a(), 325 + candidate.b(), 326 + candidate.pick(), 327 + EdgeKind::Silhouette, 328 + SILHOUETTE_OFFSET_PX, 329 + ) 330 + } 331 + 332 + fn instance( 333 + a: Point3, 334 + b: Point3, 335 + pick: PickId, 336 + kind: EdgeKind, 337 + depth_offset_px: f32, 338 + ) -> EdgeInstance { 339 + EdgeInstance { 340 + a: lower_point(a), 341 + b: lower_point(b), 342 + half_width_px: EDGE_HALF_WIDTH_PX, 343 + depth_offset_px, 344 + style_bits: kind.style_bits(), 345 + pick_id: pick.raw(), 346 + } 347 + } 348 + 349 + fn lower_point(p: Point3) -> [f32; 3] { 350 + let (x, y, z) = p.coords_mm(); 351 + [lower_f32(x), lower_f32(y), lower_f32(z)] 352 + } 353 + 354 + fn straddles(candidate: SilhouetteCandidate, projection: EdgeProjection) -> bool { 355 + let view = projection.view_at(midpoint(candidate.a(), candidate.b())); 356 + let front_a = dot_unit_vec(candidate.normal_a(), view); 357 + let front_b = dot_unit_vec(candidate.normal_b(), view); 358 + front_a * front_b < 0.0 359 + } 360 + 361 + fn midpoint(a: Point3, b: Point3) -> Point3 { 362 + let (ax, ay, az) = a.coords_mm(); 363 + let (bx, by, bz) = b.coords_mm(); 364 + Point3::from_mm(0.5 * (ax + bx), 0.5 * (ay + by), 0.5 * (az + bz)) 365 + } 366 + 367 + fn dot_unit_vec(unit: UnitVec3, vec: Vec3) -> f64 { 368 + let (ux, uy, uz) = unit.components(); 369 + let (vx, vy, vz) = vec.coords_mm(); 370 + ux * vx + uy * vy + uz * vz 371 + }
+145
crates/bone-render/src/pipelines/edge_3d.wgsl
··· 1 + struct Frame { 2 + clip_from_world: mat4x4<f32>, 3 + edge_color: vec4<f32>, 4 + hidden_color: vec4<f32>, 5 + viewport_px: vec2<f32>, 6 + dash_period_px: f32, 7 + dash_on_ratio: f32, 8 + depth_bias_ndc: f32, 9 + _pad0: f32, 10 + _pad1: f32, 11 + _pad2: f32, 12 + }; 13 + 14 + struct Instance { 15 + @location(0) a: vec3<f32>, 16 + @location(1) b: vec3<f32>, 17 + @location(2) half_width_px: f32, 18 + @location(3) depth_offset_px: f32, 19 + @location(4) style_bits: u32, 20 + @location(5) pick_id: u32, 21 + }; 22 + 23 + struct VsOut { 24 + @builtin(position) clip: vec4<f32>, 25 + @location(0) fb_a: vec2<f32>, 26 + @location(1) fb_b: vec2<f32>, 27 + @location(2) half_width_px: f32, 28 + @location(3) @interpolate(flat) style_bits: u32, 29 + @location(4) @interpolate(flat) pick_id: u32, 30 + }; 31 + 32 + struct FsOut { 33 + @location(0) color: vec4<f32>, 34 + @location(1) pick_id: u32, 35 + }; 36 + 37 + @group(0) @binding(0) var<uniform> u: Frame; 38 + 39 + const CORNERS: array<vec2<f32>, 6> = array<vec2<f32>, 6>( 40 + vec2<f32>(0.0, 0.0), 41 + vec2<f32>(1.0, 0.0), 42 + vec2<f32>(0.0, 1.0), 43 + vec2<f32>(0.0, 1.0), 44 + vec2<f32>(1.0, 0.0), 45 + vec2<f32>(1.0, 1.0), 46 + ); 47 + 48 + const STYLE_DASHED: u32 = 1u; 49 + const STYLE_HIDDEN: u32 = 2u; 50 + 51 + fn to_fb(clip: vec4<f32>) -> vec2<f32> { 52 + let ndc = clip.xy / clip.w; 53 + return vec2<f32>((ndc.x * 0.5 + 0.5) * u.viewport_px.x, (0.5 - ndc.y * 0.5) * u.viewport_px.y); 54 + } 55 + 56 + @vertex 57 + fn vs(@builtin(vertex_index) vid: u32, inst: Instance) -> VsOut { 58 + var corners = CORNERS; 59 + let corner = corners[vid]; 60 + 61 + let world_ca = u.clip_from_world * vec4<f32>(inst.a, 1.0); 62 + let world_cb = u.clip_from_world * vec4<f32>(inst.b, 1.0); 63 + 64 + var out: VsOut; 65 + let near_w = 1.0e-4; 66 + if (world_ca.w <= near_w && world_cb.w <= near_w) { 67 + out.clip = vec4<f32>(0.0, 0.0, 2.0, 1.0); 68 + out.fb_a = vec2<f32>(0.0, 0.0); 69 + out.fb_b = vec2<f32>(0.0, 0.0); 70 + out.half_width_px = inst.half_width_px; 71 + out.style_bits = inst.style_bits; 72 + out.pick_id = inst.pick_id; 73 + return out; 74 + } 75 + let w_span = world_cb.w - world_ca.w; 76 + let t_near = select(0.0, (near_w - world_ca.w) / w_span, abs(w_span) > 1.0e-20); 77 + let near_point = mix(world_ca, world_cb, t_near); 78 + let ca = select(world_ca, near_point, world_ca.w <= near_w); 79 + let cb = select(world_cb, near_point, world_cb.w <= near_w); 80 + 81 + let fb_a = to_fb(ca); 82 + let fb_b = to_fb(cb); 83 + let d = fb_b - fb_a; 84 + let len_px = max(length(d), 1.0e-4); 85 + let t_hat = d / len_px; 86 + let n_hat = vec2<f32>(-t_hat.y, t_hat.x); 87 + let expand = inst.half_width_px + 1.0; 88 + 89 + let pick_b = corner.x > 0.5; 90 + let base_clip = select(ca, cb, pick_b); 91 + let base_fb = select(fb_a, fb_b, pick_b); 92 + let cap = corner.x * 2.0 - 1.0; 93 + let side = corner.y * 2.0 - 1.0; 94 + let vert_fb = base_fb + t_hat * (cap * expand) + n_hat * (side * expand); 95 + 96 + let ndc_x = vert_fb.x / u.viewport_px.x * 2.0 - 1.0; 97 + let ndc_y = 1.0 - vert_fb.y / u.viewport_px.y * 2.0; 98 + let zbias = inst.depth_offset_px * u.depth_bias_ndc; 99 + 100 + out.clip = vec4<f32>( 101 + vec2<f32>(ndc_x, ndc_y) * base_clip.w, 102 + base_clip.z - zbias * base_clip.w, 103 + base_clip.w, 104 + ); 105 + out.fb_a = fb_a; 106 + out.fb_b = fb_b; 107 + out.half_width_px = inst.half_width_px; 108 + out.style_bits = inst.style_bits; 109 + out.pick_id = inst.pick_id; 110 + return out; 111 + } 112 + 113 + @fragment 114 + fn fs(in: VsOut) -> FsOut { 115 + let p = in.clip.xy; 116 + let ab = in.fb_b - in.fb_a; 117 + let ab2 = max(dot(ab, ab), 1.0e-6); 118 + let h = clamp(dot(p - in.fb_a, ab) / ab2, 0.0, 1.0); 119 + let proj = in.fb_a + ab * h; 120 + let dist = length(p - proj); 121 + 122 + let aa = 0.7; 123 + let coverage = 1.0 - smoothstep(in.half_width_px - aa, in.half_width_px + aa, dist); 124 + if (coverage <= 0.0) { 125 + discard; 126 + } 127 + 128 + let dashed = (in.style_bits & STYLE_DASHED) != 0u; 129 + let along_px = h * length(ab); 130 + let dash_visible = !dashed 131 + || u.dash_period_px <= 0.0 132 + || fract(along_px / u.dash_period_px) <= u.dash_on_ratio; 133 + if (!dash_visible) { 134 + discard; 135 + } 136 + 137 + let hidden = (in.style_bits & STYLE_HIDDEN) != 0u; 138 + let base = select(u.edge_color, u.hidden_color, hidden); 139 + let a = base.a * coverage; 140 + 141 + var out: FsOut; 142 + out.color = vec4<f32>(base.rgb * a, a); 143 + out.pick_id = in.pick_id; 144 + return out; 145 + }
+3 -1
crates/bone-render/src/pipelines/mod.rs
··· 1 1 pub mod arc; 2 2 pub mod chrome; 3 3 pub mod chrome_text; 4 + pub mod edge_3d; 4 5 pub mod glyph; 5 6 pub mod grid; 6 7 pub mod lines; ··· 11 12 pub use arc::ArcPipeline; 12 13 pub use chrome::{ChromeInstance, ChromePipeline}; 13 14 pub use chrome_text::{ChromeTextPipeline, SdfGlyphInstance}; 15 + pub use edge_3d::{Edge3dPipeline, EdgeProjection, EdgeView}; 14 16 pub use glyph::GlyphPipeline; 15 17 pub use grid::GridPipeline; 16 18 pub use lines::LinesPipeline; 17 - pub use solid::{SolidPipeline, SolidView}; 19 + pub use solid::{FaceFill, SolidPipeline, SolidView}; 18 20 pub use text::TextPipeline; 19 21 20 22 use crate::camera::Camera2;