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:
parent
27896e3401
commit
a994a148fe
10 changed files with 572 additions and 146 deletions
|
|
@ -43,30 +43,55 @@ pub struct MeshLodSet {
|
|||
/// by the per-frame LOD selector to compute the projected pixel
|
||||
/// diagonal.
|
||||
pub world_aabb: [f32; 4],
|
||||
/// World Z extent `[min_z, max_z]`. With `world_aabb` this is the full 3D
|
||||
/// box, which the pick path projects to a screen rect to skip solids whose
|
||||
/// footprint isn't under the cursor (O(solids) instead of ray-testing every
|
||||
/// triangle). `verts` carry only the high half of the double-single
|
||||
/// position, so the bound is f32-precise — fine for a conservative cull.
|
||||
pub z_aabb: [f32; 2],
|
||||
}
|
||||
|
||||
/// 3D bounds of every LOD's vertices: `([min_x, min_y, max_x, max_y], [min_z, max_z])`.
|
||||
pub fn compute_mesh_aabb(lods: &[MeshModel]) -> ([f32; 4], [f32; 2]) {
|
||||
let (mut min_x, mut min_y, mut min_z) = (f32::INFINITY, f32::INFINITY, f32::INFINITY);
|
||||
let (mut max_x, mut max_y, mut max_z) =
|
||||
(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
|
||||
for lod in lods {
|
||||
for &[x, y, z] in &lod.verts {
|
||||
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
|
||||
continue;
|
||||
}
|
||||
min_x = min_x.min(x);
|
||||
min_y = min_y.min(y);
|
||||
min_z = min_z.min(z);
|
||||
max_x = max_x.max(x);
|
||||
max_y = max_y.max(y);
|
||||
max_z = max_z.max(z);
|
||||
}
|
||||
}
|
||||
([min_x, min_y, max_x, max_y], [min_z, max_z])
|
||||
}
|
||||
|
||||
impl MeshLodSet {
|
||||
/// Build a set from its LODs, computing the 3D AABB.
|
||||
pub fn from_lods(lods: Vec<MeshModel>) -> Self {
|
||||
let (world_aabb, z_aabb) = compute_mesh_aabb(&lods);
|
||||
Self { lods, world_aabb, z_aabb }
|
||||
}
|
||||
|
||||
/// Wrap a single MeshModel as a one-LOD set. Used by interactive
|
||||
/// commands that only produce one tessellation (e.g. truck-based
|
||||
/// BOX/CYLINDER creation). The LOD selector will pick slot 0 for
|
||||
/// every zoom level.
|
||||
pub fn from_single(mesh: MeshModel) -> Self {
|
||||
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 &[x, y, _] in &mesh.verts {
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
continue;
|
||||
}
|
||||
if x < min_x { min_x = x; }
|
||||
if y < min_y { min_y = y; }
|
||||
if x > max_x { max_x = x; }
|
||||
if y > max_y { max_y = y; }
|
||||
}
|
||||
Self {
|
||||
lods: vec![mesh],
|
||||
world_aabb: [min_x, min_y, max_x, max_y],
|
||||
}
|
||||
Self::from_lods(vec![mesh])
|
||||
}
|
||||
|
||||
/// Recompute `world_aabb` / `z_aabb` after the LODs' vertices were rewritten
|
||||
/// (relative-to-eye re-split, INSERT transform).
|
||||
pub fn recompute_aabb(&mut self) {
|
||||
let (xy, z) = compute_mesh_aabb(&self.lods);
|
||||
self.world_aabb = xy;
|
||||
self.z_aabb = z;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue