forked from 1b-agent/opencadstudio-upstream
feat(render): double-single vertices for the mesh pipeline
3D solid / surface meshes were the last single-f32 path. truck's mesh tessellation now returns a paired verts_low residual (TruckTessResult::Mesh, polygon_to_result), MeshModel/MeshLodSet carry it, the model-space offset and block-instance transforms reconstruct the f64 position and re-split it after subtracting world_offset, MeshVertex gains position_low (location 3), and mesh.wgsl reconstructs the eye-relative position. Native SAT / ACIS / OBJ mesh paths default to a zero residual (no regression — they were never sub-f32 precise). With this every world-space pipeline (wire, hatch, image, face3d, mesh) is full double-single relative-to-eye; large-coordinate jitter is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0f3d0de5e
commit
ddf6344dbe
10 files changed
+112
-46
No files matched your search
@@ -1274,11 +1274,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_solid(&solid, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
@@ -1352,11 +1354,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_shell(&shell, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
@@ -1437,11 +1441,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_solid(&solid, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
@@ -1454,11 +1460,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_shell(&shell, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
@@ -1486,11 +1494,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_solid(&solid, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
@@ -1503,11 +1513,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_shell(&shell, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
@@ -1593,11 +1605,13 @@ impl OpenCADStudio {
|
||||
match truck_tess::tessellate_shell(&shell, woff) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(crate::scene::model::mesh_model::MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
|
||||
@@ -103,6 +103,7 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
||||
Some(MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low: Vec::new(),
|
||||
normals: norms,
|
||||
indices,
|
||||
color,
|
||||
|
||||
@@ -110,6 +110,7 @@ pub fn tessellate_sat_truck(
|
||||
let mut mesh = MeshModel {
|
||||
name: name.clone(),
|
||||
verts: Vec::new(),
|
||||
verts_low: Vec::new(),
|
||||
normals: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
color,
|
||||
|
||||
@@ -125,6 +125,7 @@ fn tessellate_sat(
|
||||
Some(MeshModel {
|
||||
name,
|
||||
verts,
|
||||
verts_low: Vec::new(),
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
|
||||
@@ -78,9 +78,11 @@ pub enum TruckTessResult {
|
||||
/// returned as parallel double-single (high, low) f32 buffers so the GPU
|
||||
/// keeps sub-unit precision at UTM-scale coordinates.
|
||||
Lines(Vec<[f32; 3]>, Vec<[f32; 3]>),
|
||||
/// A triangle mesh (from Face, Shell, or Solid).
|
||||
/// A triangle mesh (from Face, Shell, or Solid). `verts` is the high half
|
||||
/// of the double-single position; `verts_low` the paired residual.
|
||||
Mesh {
|
||||
verts: Vec<[f32; 3]>,
|
||||
verts_low: Vec<[f32; 3]>,
|
||||
normals: Vec<[f32; 3]>,
|
||||
indices: Vec<u32>,
|
||||
},
|
||||
@@ -177,6 +179,7 @@ pub fn tessellate_shell(s: &Shell, offset: [f64; 3]) -> TruckTessResult {
|
||||
pub fn tessellate_shell(_s: &Shell, _offset: [f64; 3]) -> TruckTessResult {
|
||||
TruckTessResult::Mesh {
|
||||
verts: Vec::new(),
|
||||
verts_low: Vec::new(),
|
||||
normals: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
}
|
||||
@@ -197,6 +200,7 @@ pub fn tessellate_solid(s: &Solid, offset: [f64; 3]) -> TruckTessResult {
|
||||
pub fn tessellate_solid(_s: &Solid, _offset: [f64; 3]) -> TruckTessResult {
|
||||
TruckTessResult::Mesh {
|
||||
verts: Vec::new(),
|
||||
verts_low: Vec::new(),
|
||||
normals: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
}
|
||||
@@ -206,11 +210,14 @@ pub fn tessellate_solid(_s: &Solid, _offset: [f64; 3]) -> TruckTessResult {
|
||||
|
||||
#[cfg(feature = "solid3d")]
|
||||
fn polygon_to_result(mesh: PolygonMesh, offset: [f64; 3]) -> TruckTessResult {
|
||||
let verts: Vec<[f32; 3]> = mesh
|
||||
.positions()
|
||||
.iter()
|
||||
.map(|p| to_local(p.x, p.y, p.z, offset))
|
||||
.collect();
|
||||
let positions = mesh.positions();
|
||||
let mut verts: Vec<[f32; 3]> = Vec::with_capacity(positions.len());
|
||||
let mut verts_low: Vec<[f32; 3]> = Vec::with_capacity(positions.len());
|
||||
for p in positions.iter() {
|
||||
let hi = to_local(p.x, p.y, p.z, offset);
|
||||
verts_low.push(to_local_low(p.x, p.y, p.z, offset, hi));
|
||||
verts.push(hi);
|
||||
}
|
||||
|
||||
// Per-vertex normals: if the mesh has normals, map each triangle vertex's
|
||||
// normal index back to the normals array. Fall back to empty if absent.
|
||||
@@ -262,6 +269,7 @@ fn polygon_to_result(mesh: PolygonMesh, offset: [f64; 3]) -> TruckTessResult {
|
||||
|
||||
TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
}
|
||||
|
||||
+44
-18
@@ -557,20 +557,31 @@ fn overlap_len(a: (f32, f32), b: (f32, f32)) -> f32 {
|
||||
/// uses the same space.
|
||||
fn offset_mesh_lod_set(mut set: MeshLodSet, world_offset: [f64; 3]) -> MeshLodSet {
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let (fx, fy, fz) = (ox as f32, oy as f32, oz as f32);
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
let mut max_y = f32::NEG_INFINITY;
|
||||
for lod in &mut set.lods {
|
||||
for v in &mut lod.verts {
|
||||
v[0] -= fx;
|
||||
v[1] -= fy;
|
||||
v[2] -= fz;
|
||||
if v[0] < min_x { min_x = v[0]; }
|
||||
if v[1] < min_y { min_y = v[1]; }
|
||||
if v[0] > max_x { max_x = v[0]; }
|
||||
if v[1] > max_y { max_y = v[1]; }
|
||||
// Reconstruct the f64 absolute position from the double-single pair,
|
||||
// subtract world_offset in f64, then re-split into (high, low) so the
|
||||
// relative-to-eye shader keeps sub-unit precision at UTM scale.
|
||||
let has_low = lod.verts_low.len() == lod.verts.len();
|
||||
if !has_low {
|
||||
lod.verts_low = vec![[0.0; 3]; lod.verts.len()];
|
||||
}
|
||||
for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) {
|
||||
let ax = v[0] as f64 + vl[0] as f64 - ox;
|
||||
let ay = v[1] as f64 + vl[1] as f64 - oy;
|
||||
let az = v[2] as f64 + vl[2] as f64 - oz;
|
||||
let hx = ax as f32;
|
||||
let hy = ay as f32;
|
||||
let hz = az as f32;
|
||||
*v = [hx, hy, hz];
|
||||
*vl = [(ax - hx as f64) as f32, (ay - hy as f64) as f32, (az - hz as f64) as f32];
|
||||
if hx < min_x { min_x = hx; }
|
||||
if hy < min_y { min_y = hy; }
|
||||
if hx > max_x { max_x = hx; }
|
||||
if hy > max_y { max_y = hy; }
|
||||
}
|
||||
}
|
||||
if min_x.is_finite() {
|
||||
@@ -596,15 +607,30 @@ fn transform_block_mesh_lod_set(
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
let mut max_y = f32::NEG_INFINITY;
|
||||
for lod in &mut out.lods {
|
||||
for v in &mut lod.verts {
|
||||
let w = xform.apply(Vector3::new(v[0] as f64, v[1] as f64, v[2] as f64));
|
||||
v[0] = (w.x - ox) as f32;
|
||||
v[1] = (w.y - oy) as f32;
|
||||
v[2] = (w.z - oz) as f32;
|
||||
if v[0] < min_x { min_x = v[0]; }
|
||||
if v[1] < min_y { min_y = v[1]; }
|
||||
if v[0] > max_x { max_x = v[0]; }
|
||||
if v[1] > max_y { max_y = v[1]; }
|
||||
let has_low = lod.verts_low.len() == lod.verts.len();
|
||||
if !has_low {
|
||||
lod.verts_low = vec![[0.0; 3]; lod.verts.len()];
|
||||
}
|
||||
for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) {
|
||||
// Reconstruct the block-local f64, apply the INSERT transform and
|
||||
// subtract world_offset in f64, then re-split into (high, low).
|
||||
let w = xform.apply(Vector3::new(
|
||||
v[0] as f64 + vl[0] as f64,
|
||||
v[1] as f64 + vl[1] as f64,
|
||||
v[2] as f64 + vl[2] as f64,
|
||||
));
|
||||
let ax = w.x - ox;
|
||||
let ay = w.y - oy;
|
||||
let az = w.z - oz;
|
||||
let hx = ax as f32;
|
||||
let hy = ay as f32;
|
||||
let hz = az as f32;
|
||||
*v = [hx, hy, hz];
|
||||
*vl = [(ax - hx as f64) as f32, (ay - hy as f64) as f32, (az - hz as f64) as f32];
|
||||
if hx < min_x { min_x = hx; }
|
||||
if hy < min_y { min_y = hy; }
|
||||
if hx > max_x { max_x = hx; }
|
||||
if hy > max_y { max_y = hy; }
|
||||
}
|
||||
for n in &mut lod.normals {
|
||||
let d = xform.apply_rotation(Vector3::new(n[0] as f64, n[1] as f64, n[2] as f64));
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
pub struct MeshModel {
|
||||
/// Unique identifier (entity handle value as decimal string).
|
||||
pub name: String,
|
||||
/// World-space vertex positions.
|
||||
/// World-space vertex positions (high half of the double-single pair).
|
||||
pub verts: Vec<[f32; 3]>,
|
||||
/// Low residual paired with `verts` so meshes stay precise at UTM scale.
|
||||
/// Empty = all-zero (legacy / interactive meshes near the origin).
|
||||
pub verts_low: Vec<[f32; 3]>,
|
||||
/// Per-vertex normals (may be empty if not available).
|
||||
pub normals: Vec<[f32; 3]>,
|
||||
/// Triangle indices into `verts` (every 3 values = one triangle).
|
||||
|
||||
@@ -172,12 +172,14 @@ pub fn mesh_from_solid(solid: &Solid, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
match tessellate_solid(solid, [0.0; 3]) {
|
||||
TruckTessResult::Mesh {
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
} if !indices.is_empty() => {
|
||||
let mesh = MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
|
||||
@@ -19,30 +19,37 @@ pub struct MeshVertex {
|
||||
pub position: [f32; 3],
|
||||
pub normal: [f32; 3],
|
||||
pub color: [f32; 4],
|
||||
pub position_low: [f32; 3],
|
||||
}
|
||||
|
||||
impl MeshVertex {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(MeshVertex, position) as u64,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(MeshVertex, normal) as u64,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(MeshVertex, color) as u64,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(MeshVertex, position_low) as u64,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<MeshVertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 12,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 24,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
],
|
||||
attributes: ATTRS,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +140,7 @@ impl MeshGpu {
|
||||
position: pos,
|
||||
normal,
|
||||
color,
|
||||
position_low: mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -30,9 +30,10 @@ struct Uniforms {
|
||||
var<uniform> u: Uniforms;
|
||||
|
||||
struct VertexIn {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) color: vec4<f32>,
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) color: vec4<f32>,
|
||||
@location(3) position_low: vec3<f32>,
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
@@ -45,7 +46,8 @@ struct VertexOut {
|
||||
@vertex
|
||||
fn vs_main(v: VertexIn) -> VertexOut {
|
||||
var out: VertexOut;
|
||||
out.clip_pos = u.view_rot * vec4<f32>((v.position - u.eye_high) - u.eye_low, 1.0);
|
||||
let rel = (v.position - u.eye_high) + (v.position_low - u.eye_low);
|
||||
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
|
||||
out.color = v.color;
|
||||
out.normal = v.normal;
|
||||
out.world_pos = v.position;
|
||||
|
||||
Reference in new issue
Block a user