perf(pipeline): per-frame mesh frustum cull (Phase 2.2)
Roadmap calls for an Aabb3 octree, but H7CAD's mesh entities only carry XY world_aabb (Phase 3.4 stored XY for the LOD selector and never extended to Z) and the camera is orthographic top-down for every realistic workflow. So Phase 2.2 here becomes a draw-side cull: `compute_mesh_lod` now also populates a `mesh_visible: Vec<bool>` flag using the same 4-corner projection it already does for LOD picking, and the mesh render pass skips draws whose projected AABB sits entirely outside the viewport rect. Mesh quadtree-indexing is still in place from Phase 2.1 (Solid3D / Region / Body are not in `is_unindexable_entity`, so they're inserted into `entity_index`). When mesh entity counts grow large enough that the upload-time scan dominates over per-frame draws, Phase 2.3 can plug `query_rect` into `upload_meshes` too. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5b8cdd940e
commit
fcf4d4fe28
1 changed files with 55 additions and 0 deletions
|
|
@ -75,6 +75,11 @@ pub struct Pipeline {
|
|||
/// the projected pixel diagonal. Mirrors `hatch_pixel_scissors` —
|
||||
/// recomputed in `compute_mesh_lod`.
|
||||
mesh_lod_levels: Vec<usize>,
|
||||
/// Per-mesh frustum-visible flag (Phase 2.2). `false` when the
|
||||
/// mesh's projected AABB falls entirely outside the viewport rect
|
||||
/// — the draw loop skips it. Mirrors `mesh_lod_levels` length /
|
||||
/// index space; recomputed alongside it.
|
||||
mesh_visible: Vec<bool>,
|
||||
/// Batched 3DFACE fill (all faces in one buffer) and edges (merged wire).
|
||||
gpu_face3d_fill: Option<Face3DGpu>,
|
||||
gpu_face3d_edges: Vec<WireGpu>,
|
||||
|
|
@ -622,6 +627,7 @@ impl Pipeline {
|
|||
image_pixel_scissors: vec![],
|
||||
gpu_meshes: vec![],
|
||||
mesh_lod_levels: vec![],
|
||||
mesh_visible: vec![],
|
||||
gpu_face3d_fill: None,
|
||||
gpu_face3d_edges: vec![],
|
||||
viewcube,
|
||||
|
|
@ -730,6 +736,14 @@ impl Pipeline {
|
|||
.iter()
|
||||
.map(|m| pick_mesh_lod(m, view_proj, clip_w, clip_h))
|
||||
.collect();
|
||||
// Phase 2.2 — frustum-visibility flag per mesh. Cheap: same
|
||||
// 4-corner projection used for LOD selection, just answering a
|
||||
// different question (any corner inside the viewport rect?).
|
||||
self.mesh_visible = self
|
||||
.gpu_meshes
|
||||
.iter()
|
||||
.map(|m| !aabb_offscreen(m.world_aabb, view_proj, clip_w, clip_h))
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn upload_hatches(&mut self, device: &wgpu::Device, hatches: &[HatchModel]) {
|
||||
|
|
@ -915,6 +929,9 @@ impl Pipeline {
|
|||
pass.set_pipeline(&self.mesh_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for (i, set) in self.gpu_meshes.iter().enumerate() {
|
||||
if !self.mesh_visible.get(i).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let level = self
|
||||
.mesh_lod_levels
|
||||
.get(i)
|
||||
|
|
@ -1285,6 +1302,44 @@ fn aabb_diagonal_pixels(
|
|||
(dx * dx + dy * dy).sqrt()
|
||||
}
|
||||
|
||||
/// `true` when the world-XY AABB projects entirely outside the
|
||||
/// viewport rect. Phase 2.2 mesh-frustum cull. Equivalent to a 2D
|
||||
/// bounding-box rejection test in NDC; same 4-corner projection used
|
||||
/// for LOD picking, so the extra cost is negligible.
|
||||
fn aabb_offscreen(
|
||||
aabb: [f32; 4],
|
||||
view_proj: glam::Mat4,
|
||||
clip_w: u32,
|
||||
clip_h: u32,
|
||||
) -> bool {
|
||||
let [x0, y0, x1, y1] = aabb;
|
||||
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
|
||||
return false;
|
||||
}
|
||||
let w = clip_w as f32;
|
||||
let h = clip_h as f32;
|
||||
let corners = [
|
||||
view_proj.project_point3(glam::Vec3::new(x0, y0, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x1, y0, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x0, y1, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x1, y1, 0.0)),
|
||||
];
|
||||
let mut min_px = f32::INFINITY;
|
||||
let mut max_px = f32::NEG_INFINITY;
|
||||
let mut min_py = f32::INFINITY;
|
||||
let mut max_py = f32::NEG_INFINITY;
|
||||
for c in &corners {
|
||||
let px = (c.x + 1.0) * 0.5 * w;
|
||||
let py = (1.0 - c.y) * 0.5 * h;
|
||||
if px < min_px { min_px = px; }
|
||||
if px > max_px { max_px = px; }
|
||||
if py < min_py { min_py = py; }
|
||||
if py > max_py { max_py = py; }
|
||||
}
|
||||
// Reject if projected AABB sits fully to one side of the viewport.
|
||||
max_px < 0.0 || min_px > w || max_py < 0.0 || min_py > h
|
||||
}
|
||||
|
||||
/// Return `true` when the world-XY AABB's screen-space size is below the
|
||||
/// given pixel threshold. Used by LOD passes (hatch skip, etc.) to drop
|
||||
/// draw calls that wouldn't contribute a visible pixel.
|
||||
|
|
|
|||
Loading…
Reference in a new issue