Another project
0

Configure Feed

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

feat(render): solid fill pipeline

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

author
Lewis
date (Jun 5, 2026, 4:01 PM +0300) commit 35cf22df parent 410a3226 change-id lmmottwt
+417 -4
+91 -2
crates/bone-render/src/lib.rs
··· 1 1 pub mod camera; 2 + pub mod camera3; 2 3 pub mod diff; 3 4 pub mod gpu; 4 5 pub mod pick; ··· 9 10 pub mod surface; 10 11 11 12 pub use camera::{Camera2, GridSpacing, PixelsPerMm, ViewportExtent, ViewportPx}; 13 + pub use camera3::{ 14 + ViewportPoint, clip_from_world, frame_isometric, orbit_about_pixel, orbit_about_point, 15 + pan_pixels, world_from_clip, world_on_focal_plane, world_ray, zoom_about_pixel, 16 + }; 12 17 pub use diff::{PixelDiff, PixelDiffError, PixelDiffReport, PixelDiffThreshold, PixelMismatch}; 13 18 pub use gpu::{BackendTag, Capabilities, Gpu, OffscreenContext}; 14 19 pub use pick::{ ··· 16 21 }; 17 22 pub use pipelines::{ 18 23 ArcPipeline, ChromeInstance, ChromePipeline, ChromeTextPipeline, GlyphPipeline, GridPipeline, 19 - LinesPipeline, SdfGlyphInstance, TextPipeline, 24 + LinesPipeline, SdfGlyphInstance, SolidPipeline, TextPipeline, 20 25 }; 21 26 pub use preview::{PreviewArc, PreviewCircle, SketchPreview}; 22 27 pub use scene::{ 23 28 RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine, ScenePoint, 24 - SceneRelationGlyph, SketchScene, 29 + SceneRelationGlyph, SketchScene, SolidScene, 25 30 }; 26 31 pub use snapshot::{ 27 32 ClearColor, GlyphStyle, GridStyle, SnapshotFrame, StrokeStyle, Style, TextStyle, decode_png, ··· 29 34 }; 30 35 pub use surface::{SurfaceContext, SurfaceError}; 31 36 37 + #[allow( 38 + clippy::cast_possible_truncation, 39 + clippy::cast_precision_loss, 40 + reason = "GPU upload narrows f64 geometry to f32 through this single funnel" 41 + )] 42 + pub(crate) fn lower_f32(value: f64) -> f32 { 43 + value as f32 44 + } 45 + 32 46 #[derive(Copy, Clone)] 33 47 pub struct RenderTargets<'a> { 34 48 pub color: &'a wgpu::TextureView, ··· 72 86 query: PickQuery, 73 87 extent: ViewportExtent, 74 88 }, 89 + #[error("camera lowering failed: {0}")] 90 + Camera(#[from] bone_types::TypesError), 75 91 } 76 92 77 93 pub type Result<T, E = RenderError> = core::result::Result<T, E>; ··· 161 177 }) 162 178 } 163 179 } 180 + 181 + #[derive(Debug)] 182 + pub struct SolidRenderer { 183 + solid: SolidPipeline, 184 + depth: Option<DepthTarget>, 185 + } 186 + 187 + #[derive(Debug)] 188 + struct DepthTarget { 189 + extent: ViewportExtent, 190 + texture: wgpu::Texture, 191 + } 192 + 193 + impl SolidRenderer { 194 + #[must_use] 195 + pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 196 + Self { 197 + solid: SolidPipeline::new(gpu, color_format), 198 + depth: None, 199 + } 200 + } 201 + 202 + pub fn render( 203 + &mut self, 204 + ctx: &OffscreenContext, 205 + scene: &SolidScene, 206 + camera: bone_types::Camera3, 207 + style: &Style, 208 + ) -> Result<SnapshotFrame> { 209 + let extent = ctx.extent(); 210 + let clip_from_world = camera3::clip_from_world(camera, extent)?; 211 + if !matches!(&self.depth, Some(target) if target.extent == extent) { 212 + self.depth = Some(DepthTarget { 213 + extent, 214 + texture: depth_texture(ctx.gpu().device(), extent), 215 + }); 216 + } 217 + let Some(target) = &self.depth else { 218 + unreachable!("depth target populated above"); 219 + }; 220 + let depth_view = target 221 + .texture 222 + .create_view(&wgpu::TextureViewDescriptor::default()); 223 + let solid = &self.solid; 224 + ctx.render(|encoder, color_view, _pick_view| { 225 + solid.draw( 226 + encoder, 227 + color_view, 228 + &depth_view, 229 + scene, 230 + clip_from_world, 231 + style, 232 + ); 233 + }) 234 + } 235 + } 236 + 237 + fn depth_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture { 238 + device.create_texture(&wgpu::TextureDescriptor { 239 + label: Some("bone-render:solid-depth"), 240 + size: wgpu::Extent3d { 241 + width: extent.width().value(), 242 + height: extent.height().value(), 243 + depth_or_array_layers: 1, 244 + }, 245 + mip_level_count: 1, 246 + sample_count: 1, 247 + dimension: wgpu::TextureDimension::D2, 248 + format: pipelines::solid::DEPTH_FORMAT, 249 + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 250 + view_formats: &[], 251 + }) 252 + }
+2
crates/bone-render/src/pipelines/mod.rs
··· 4 4 pub mod glyph; 5 5 pub mod grid; 6 6 pub mod lines; 7 + pub mod solid; 7 8 pub mod text; 8 9 mod text_common; 9 10 ··· 13 14 pub use glyph::GlyphPipeline; 14 15 pub use grid::GridPipeline; 15 16 pub use lines::LinesPipeline; 17 + pub use solid::SolidPipeline; 16 18 pub use text::TextPipeline; 17 19 18 20 use crate::camera::Camera2;
+240
crates/bone-render/src/pipelines/solid.rs
··· 1 + use wgpu::util::DeviceExt; 2 + 3 + use crate::gpu::Gpu; 4 + use crate::lower_f32; 5 + use crate::scene::SolidScene; 6 + use crate::snapshot::Style; 7 + 8 + pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; 9 + 10 + const LIGHT_DIR: [f32; 4] = [0.302, 0.503, 0.809, 0.0]; 11 + const BASE_COLOR: [f32; 4] = [0.72, 0.74, 0.78, 1.0]; 12 + const AMBIENT: f32 = 0.28; 13 + 14 + #[repr(C)] 15 + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 16 + struct SolidVertex { 17 + position: [f32; 3], 18 + normal: [f32; 3], 19 + } 20 + 21 + const VERTEX_STRIDE: u64 = core::mem::size_of::<SolidVertex>() as u64; 22 + 23 + #[repr(C, align(16))] 24 + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 25 + struct SolidUniform { 26 + clip_from_world: [f32; 16], 27 + light_dir: [f32; 4], 28 + base_color: [f32; 4], 29 + ambient: f32, 30 + pad: [f32; 3], 31 + } 32 + 33 + const UNIFORM_SIZE: u64 = core::mem::size_of::<SolidUniform>() as u64; 34 + 35 + const VERTEX_ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![ 36 + 0 => Float32x3, 37 + 1 => Float32x3, 38 + ]; 39 + 40 + pub struct SolidPipeline { 41 + device: wgpu::Device, 42 + queue: wgpu::Queue, 43 + pipeline: wgpu::RenderPipeline, 44 + uniform_buffer: wgpu::Buffer, 45 + bind_group: wgpu::BindGroup, 46 + } 47 + 48 + impl SolidPipeline { 49 + #[must_use] 50 + pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 51 + let device = gpu.device().clone(); 52 + let queue = gpu.queue().clone(); 53 + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 54 + label: Some("bone-render:solid-shader"), 55 + source: wgpu::ShaderSource::Wgsl(include_str!("solid.wgsl").into()), 56 + }); 57 + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 58 + label: Some("bone-render:solid-bgl"), 59 + entries: &[wgpu::BindGroupLayoutEntry { 60 + binding: 0, 61 + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 62 + ty: wgpu::BindingType::Buffer { 63 + ty: wgpu::BufferBindingType::Uniform, 64 + has_dynamic_offset: false, 65 + min_binding_size: wgpu::BufferSize::new(UNIFORM_SIZE), 66 + }, 67 + count: None, 68 + }], 69 + }); 70 + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 71 + label: Some("bone-render:solid-layout"), 72 + bind_group_layouts: &[Some(&bind_group_layout)], 73 + immediate_size: 0, 74 + }); 75 + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 76 + label: Some("bone-render:solid-pipeline"), 77 + layout: Some(&pipeline_layout), 78 + vertex: wgpu::VertexState { 79 + module: &shader, 80 + entry_point: Some("vs"), 81 + compilation_options: wgpu::PipelineCompilationOptions::default(), 82 + buffers: &[wgpu::VertexBufferLayout { 83 + array_stride: VERTEX_STRIDE, 84 + step_mode: wgpu::VertexStepMode::Vertex, 85 + attributes: &VERTEX_ATTRS, 86 + }], 87 + }, 88 + fragment: Some(wgpu::FragmentState { 89 + module: &shader, 90 + entry_point: Some("fs"), 91 + compilation_options: wgpu::PipelineCompilationOptions::default(), 92 + targets: &[Some(wgpu::ColorTargetState { 93 + format: color_format, 94 + blend: None, 95 + write_mask: wgpu::ColorWrites::ALL, 96 + })], 97 + }), 98 + primitive: wgpu::PrimitiveState { 99 + topology: wgpu::PrimitiveTopology::TriangleList, 100 + strip_index_format: None, 101 + front_face: wgpu::FrontFace::Ccw, 102 + cull_mode: Some(wgpu::Face::Back), 103 + polygon_mode: wgpu::PolygonMode::Fill, 104 + conservative: false, 105 + unclipped_depth: false, 106 + }, 107 + depth_stencil: Some(wgpu::DepthStencilState { 108 + format: DEPTH_FORMAT, 109 + depth_write_enabled: Some(true), 110 + depth_compare: Some(wgpu::CompareFunction::Less), 111 + stencil: wgpu::StencilState::default(), 112 + bias: wgpu::DepthBiasState::default(), 113 + }), 114 + multisample: wgpu::MultisampleState::default(), 115 + multiview_mask: None, 116 + cache: None, 117 + }); 118 + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { 119 + label: Some("bone-render:solid-uniform"), 120 + size: UNIFORM_SIZE, 121 + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 122 + mapped_at_creation: false, 123 + }); 124 + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 125 + label: Some("bone-render:solid-bg"), 126 + layout: &bind_group_layout, 127 + entries: &[wgpu::BindGroupEntry { 128 + binding: 0, 129 + resource: uniform_buffer.as_entire_binding(), 130 + }], 131 + }); 132 + Self { 133 + device, 134 + queue, 135 + pipeline, 136 + uniform_buffer, 137 + bind_group, 138 + } 139 + } 140 + 141 + pub fn draw( 142 + &self, 143 + encoder: &mut wgpu::CommandEncoder, 144 + color_view: &wgpu::TextureView, 145 + depth_view: &wgpu::TextureView, 146 + scene: &SolidScene, 147 + clip_from_world: [f32; 16], 148 + style: &Style, 149 + ) { 150 + let uniform = SolidUniform { 151 + clip_from_world, 152 + light_dir: LIGHT_DIR, 153 + base_color: BASE_COLOR, 154 + ambient: AMBIENT, 155 + pad: [0.0; 3], 156 + }; 157 + self.queue 158 + .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform)); 159 + let vertices = build_vertices(scene); 160 + let indices = build_indices(scene); 161 + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 162 + label: Some("bone-render:solid-pass"), 163 + color_attachments: &[Some(wgpu::RenderPassColorAttachment { 164 + view: color_view, 165 + resolve_target: None, 166 + depth_slice: None, 167 + ops: wgpu::Operations { 168 + load: wgpu::LoadOp::Clear(style.background().into()), 169 + store: wgpu::StoreOp::Store, 170 + }, 171 + })], 172 + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 173 + view: depth_view, 174 + depth_ops: Some(wgpu::Operations { 175 + load: wgpu::LoadOp::Clear(1.0), 176 + store: wgpu::StoreOp::Store, 177 + }), 178 + stencil_ops: None, 179 + }), 180 + timestamp_writes: None, 181 + occlusion_query_set: None, 182 + multiview_mask: None, 183 + }); 184 + let Ok(count) = u32::try_from(indices.len()) else { 185 + panic!("solid index count {} exceeds u32::MAX", indices.len()); 186 + }; 187 + if count == 0 { 188 + return; 189 + } 190 + let vertex_buffer = self 191 + .device 192 + .create_buffer_init(&wgpu::util::BufferInitDescriptor { 193 + label: Some("bone-render:solid-vertices"), 194 + contents: bytemuck::cast_slice(&vertices), 195 + usage: wgpu::BufferUsages::VERTEX, 196 + }); 197 + let index_buffer = self 198 + .device 199 + .create_buffer_init(&wgpu::util::BufferInitDescriptor { 200 + label: Some("bone-render:solid-indices"), 201 + contents: bytemuck::cast_slice(&indices), 202 + usage: wgpu::BufferUsages::INDEX, 203 + }); 204 + pass.set_pipeline(&self.pipeline); 205 + pass.set_bind_group(0, &self.bind_group, &[]); 206 + pass.set_vertex_buffer(0, vertex_buffer.slice(..)); 207 + pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32); 208 + pass.draw_indexed(0..count, 0, 0..1); 209 + } 210 + } 211 + 212 + impl core::fmt::Debug for SolidPipeline { 213 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 214 + f.debug_struct("SolidPipeline").finish_non_exhaustive() 215 + } 216 + } 217 + 218 + fn build_vertices(scene: &SolidScene) -> Vec<SolidVertex> { 219 + scene 220 + .positions() 221 + .iter() 222 + .zip(scene.normals()) 223 + .map(|(point, normal)| { 224 + let (px, py, pz) = point.coords_mm(); 225 + let (nx, ny, nz) = normal.components(); 226 + SolidVertex { 227 + position: [lower_f32(px), lower_f32(py), lower_f32(pz)], 228 + normal: [lower_f32(nx), lower_f32(ny), lower_f32(nz)], 229 + } 230 + }) 231 + .collect() 232 + } 233 + 234 + fn build_indices(scene: &SolidScene) -> Vec<u32> { 235 + scene 236 + .triangles() 237 + .iter() 238 + .flat_map(|tri| tri.iter().copied()) 239 + .collect() 240 + }
+33
crates/bone-render/src/pipelines/solid.wgsl
··· 1 + struct Uniform { 2 + clip_from_world: mat4x4<f32>, 3 + light_dir: vec4<f32>, 4 + base_color: vec4<f32>, 5 + ambient: f32, 6 + pad0: f32, 7 + pad1: f32, 8 + pad2: f32, 9 + }; 10 + 11 + @group(0) @binding(0) var<uniform> u: Uniform; 12 + 13 + struct VsOut { 14 + @builtin(position) clip: vec4<f32>, 15 + @location(0) world_normal: vec3<f32>, 16 + }; 17 + 18 + @vertex 19 + fn vs(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>) -> VsOut { 20 + var out: VsOut; 21 + out.clip = u.clip_from_world * vec4<f32>(position, 1.0); 22 + out.world_normal = normal; 23 + return out; 24 + } 25 + 26 + @fragment 27 + fn fs(in: VsOut) -> @location(0) vec4<f32> { 28 + let n = normalize(in.world_normal); 29 + let l = normalize(u.light_dir.xyz); 30 + let lambert = max(dot(n, l), 0.0); 31 + let shade = u.ambient + (1.0 - u.ambient) * lambert; 32 + return vec4<f32>(u.base_color.rgb * shade, u.base_color.a); 33 + }
+51 -2
crates/bone-render/src/scene.rs
··· 2 2 ArcData, CircleData, DimensionValue, LineData, Sketch, SketchDimension, SketchEntity, 3 3 SketchRelation, 4 4 }; 5 - use bone_kernel::{Aabb2, Arc2, arc_bounding_box}; 5 + use bone_kernel::{Aabb2, Arc2, SolidMesh, arc_bounding_box}; 6 6 use bone_types::{ 7 - Angle, Length, Point2, SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, Vec2, 7 + Angle, Length, Point2, Point3, SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, 8 + UnitVec3, Vec2, 8 9 }; 9 10 use core::f64::consts::FRAC_1_SQRT_2; 10 11 use uom::si::angle::degree; ··· 617 618 pick, 618 619 for_construction: data.for_construction(), 619 620 }) 621 + } 622 + 623 + #[derive(Clone, Debug, PartialEq)] 624 + pub struct SolidScene { 625 + positions: Vec<Point3>, 626 + normals: Vec<UnitVec3>, 627 + triangles: Vec<[u32; 3]>, 628 + } 629 + 630 + impl SolidScene { 631 + #[must_use] 632 + pub fn empty() -> Self { 633 + Self { 634 + positions: Vec::new(), 635 + normals: Vec::new(), 636 + triangles: Vec::new(), 637 + } 638 + } 639 + 640 + #[must_use] 641 + pub fn from_mesh(mesh: &SolidMesh) -> Self { 642 + mesh.faces().iter().fold(Self::empty(), |mut scene, face| { 643 + let Ok(base) = u32::try_from(scene.positions.len()) else { 644 + panic!("solid mesh vertex count fits a u32 index"); 645 + }; 646 + scene.positions.extend_from_slice(face.positions()); 647 + scene.normals.extend_from_slice(face.normals()); 648 + scene 649 + .triangles 650 + .extend(face.triangles().iter().map(|tri| tri.map(|i| i + base))); 651 + scene 652 + }) 653 + } 654 + 655 + #[must_use] 656 + pub fn positions(&self) -> &[Point3] { 657 + &self.positions 658 + } 659 + 660 + #[must_use] 661 + pub fn normals(&self) -> &[UnitVec3] { 662 + &self.normals 663 + } 664 + 665 + #[must_use] 666 + pub fn triangles(&self) -> &[[u32; 3]] { 667 + &self.triangles 668 + } 620 669 } 621 670 622 671 #[cfg(test)]