forked from 1b-agent/opencadstudio-upstream
perf(render): batch solid meshes, fix per-pick freezes on heavy 3D models
Opening / navigating a heavy 3D DWG (e.g. a BIM model with ~10k ACIS solids) was effectively frozen. Several independent costs, all addressed here: - Face3D edge/fill buffers were re-uploaded on every camera move (keyed on camera_generation). They are world-space and selection-independent, so gate them on (geometry_epoch, fill_mode) instead — a pan no longer re-walks every wire. - Solid meshes were drawn one buffer + one draw call per solid (~10k draws/ frame), which strangled the GPU front end. Concatenate every solid's LOD0 into a few large buffers (split only to stay under the 256 MB buffer cap) and draw the whole set in a handful of calls. Built once per geometry epoch, so selection / hover never re-pack it. - Selection / hover highlight is now a small tinted overlay of just the picked solids, drawn last with an Always-depth pipeline so it shows on top even when the solid is occluded — instead of re-tinting and re-uploading all meshes. - Hover / pick hit-testing scanned every triangle of every solid each move (seconds on a heavy model). Add a 3D-AABB screen-rect broad phase (MeshLodSet now carries z_aabb) and reuse the renderer's cached expanded mesh set, so picking is O(solids) cheap projections plus the few solids actually under the cursor. - Block-internal hatch hit-testing exploded every INSERT on every hover. Cache the result per geometry epoch and skip blocks that contain no hatch. - Curved-surface (cone/sphere/torus) tessellation tolerance is now radius- relative (CURVE_REL_TOL) so a cylinder's facet count is size-independent and matches the circle/arc wire tessellation, instead of exploding on large radii. Also adds docs/tessellation.md mapping every EntityType to its tessellation path (truck B-rep vs truck curve topology vs direct). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
27896e3401
commit
a994a148fe
10 files changed
+572
-146
No files matched your search
@@ -28,15 +28,21 @@ use acadrust::entities::acis::{
|
||||
|
||||
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
||||
use crate::scene::convert::solid3d_tess::{
|
||||
apply_body_transform, body_transform, collect_face_loops, cone_axis_span, mesh_aabb,
|
||||
apply_body_transform, body_transform, collect_face_loops, cone_axis_span,
|
||||
};
|
||||
|
||||
/// Slightly over 2π so revolution builders close the loop.
|
||||
const FULL: f64 = std::f64::consts::TAU + 0.2;
|
||||
/// Boundary sampling density for planar faces with curved (circular) edges.
|
||||
const BOUNDARY_SEGS: usize = 64;
|
||||
/// Triangle mesh chord tolerance (world units).
|
||||
/// Triangle mesh chord tolerance (world units) for planar faces, where the
|
||||
/// surface itself adds no curvature.
|
||||
const MESH_TOL: f64 = 0.01;
|
||||
/// Chord tolerance for curved faces, as a fraction of the surface radius, so
|
||||
/// the facet count is radius-independent instead of exploding on large radii.
|
||||
/// `0.1` = 10% chord error (~7 facets per full circle) — a deliberately coarse
|
||||
/// setting that keeps the solid triangle budget low.
|
||||
const CURVE_REL_TOL: f64 = 0.1;
|
||||
|
||||
/// Analytic outward-normal rule for a face, used to orient triangles and
|
||||
/// supply smooth per-vertex normals (truck's face orientation is unreliable
|
||||
@@ -121,14 +127,14 @@ pub fn tessellate_sat_truck(
|
||||
let Some(surf_rec) = sat.resolve(face.surface()) else {
|
||||
continue;
|
||||
};
|
||||
let Some((faces, outward)) = build_face_group(sat, &face, surf_rec) else {
|
||||
let Some((faces, outward, tol)) = build_face_group(sat, &face, surf_rec) else {
|
||||
continue;
|
||||
};
|
||||
if faces.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let shell: Shell = faces.into();
|
||||
let poly = shell.triangulation(MESH_TOL).to_polygon();
|
||||
let poly = shell.triangulation(tol).to_polygon();
|
||||
append_group(&mut mesh, &poly, &outward);
|
||||
}
|
||||
|
||||
@@ -143,11 +149,7 @@ pub fn tessellate_sat_truck(
|
||||
if let Some((m, tr, scale)) = body_transform(sat) {
|
||||
apply_body_transform(&mut mesh, &m, &tr, scale);
|
||||
}
|
||||
let world_aabb = mesh_aabb(&mesh);
|
||||
Some(MeshLodSet {
|
||||
lods: vec![mesh],
|
||||
world_aabb,
|
||||
})
|
||||
Some(MeshLodSet::from_lods(vec![mesh]))
|
||||
}
|
||||
|
||||
/// Build the truck face(s) + outward rule for one analytic ACIS face.
|
||||
@@ -155,7 +157,11 @@ fn build_face_group(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
surf_rec: &acadrust::entities::acis::SatRecord,
|
||||
) -> Option<(Vec<Face>, Outward)> {
|
||||
) -> Option<(Vec<Face>, Outward, f64)> {
|
||||
// Chord tolerance scaled to a curved surface's radius, so the facet count
|
||||
// is radius-independent (matching the circle/arc wire tessellation) rather
|
||||
// than exploding on large radii.
|
||||
let curve_tol = |radius: f64| (radius.abs() * CURVE_REL_TOL).max(1e-6);
|
||||
match surf_rec.entity_type.as_str() {
|
||||
"plane-surface" => {
|
||||
let plane = SatPlaneSurface::from_record(surf_rec)?;
|
||||
@@ -166,28 +172,37 @@ fn build_face_group(
|
||||
} else {
|
||||
[nx, ny, nz]
|
||||
};
|
||||
Some((vec![f], Outward::Const(vnorm(n))))
|
||||
Some((vec![f], Outward::Const(vnorm(n)), MESH_TOL))
|
||||
}
|
||||
"cone-surface" => {
|
||||
let cone = SatConeSurface::from_record(surf_rec)?;
|
||||
let tol = curve_tol(cone.radius());
|
||||
let (faces, out) = cone_faces(sat, &cone)?;
|
||||
Some((faces, out))
|
||||
Some((faces, out, tol))
|
||||
}
|
||||
"sphere-surface" => {
|
||||
let sphere = SatSphereSurface::from_record(surf_rec)?;
|
||||
let (cx, cy, cz) = sphere.center();
|
||||
Some((sphere_faces(&sphere), Outward::Sphere { center: [cx, cy, cz] }))
|
||||
let tol = curve_tol(sphere.radius());
|
||||
Some((
|
||||
sphere_faces(&sphere),
|
||||
Outward::Sphere { center: [cx, cy, cz] },
|
||||
tol,
|
||||
))
|
||||
}
|
||||
"torus-surface" => {
|
||||
let torus = SatTorusSurface::from_record(surf_rec)?;
|
||||
let (cx, cy, cz) = torus.center();
|
||||
let (nx, ny, nz) = torus.normal();
|
||||
// Tube (minor) curvature is the tighter of the two, so sampling it
|
||||
// to the circle tolerance keeps the whole torus at least that smooth.
|
||||
let tol = curve_tol(torus.minor_radius());
|
||||
let out = Outward::Torus {
|
||||
center: [cx, cy, cz],
|
||||
axis: vnorm([nx, ny, nz]),
|
||||
major: torus.major_radius(),
|
||||
};
|
||||
Some((torus_faces(&torus), out))
|
||||
Some((torus_faces(&torus), out, tol))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -181,8 +181,7 @@ fn tessellate_sat_lods(
|
||||
if lods.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let world_aabb = mesh_aabb(&lods[0]);
|
||||
Some(MeshLodSet { lods, world_aabb })
|
||||
Some(MeshLodSet::from_lods(lods))
|
||||
}
|
||||
|
||||
/// Extract the body's placement transform from the SAT document: a row-major
|
||||
@@ -243,6 +242,7 @@ fn scale_lod(base: LodConfig, facet_res: f64) -> LodConfig {
|
||||
|
||||
/// World-XY AABB of the mesh — used by the render-pipeline LOD selector
|
||||
/// to pick a level based on projected pixel diagonal.
|
||||
#[allow(dead_code)] // superseded by mesh_model::compute_mesh_aabb (3D); kept for reference
|
||||
pub(crate) fn mesh_aabb(mesh: &MeshModel) -> [f32; 4] {
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
|
||||
Reference in new issue
Block a user