diff --git a/RENDERING_OPTIMIZATION.md b/RENDERING_OPTIMIZATION.md index deea19723..a2f0519f3 100644 --- a/RENDERING_OPTIMIZATION.md +++ b/RENDERING_OPTIMIZATION.md @@ -64,31 +64,6 @@ let candidates = scene.quadtree.query_rect(view_aabb); **Goal:** Reduce geometric complexity when entities are small or far away. -### 3.2 Wire LOD — Curve Segment Reduction - -Arcs, ellipses, and splines tessellate to N segments (currently fixed). Replace with -zoom-adaptive segment count. - -```rust -fn arc_segments(radius_world: f64, px_per_unit: f64) -> u32 { - // target: ~1 segment per 2px of arc length on screen - let arc_px = radius_world * px_per_unit * TWO_PI; - (arc_px / 2.0).clamp(8.0, 256.0) as u32 -} -``` - -Recompute segments when zoom crosses a threshold (epoch-based invalidation already in place). -Store LOD level per entity; retessellate only on LOD level change. - -LOD levels (example): - -| Zoom (px/unit) | Segments per 90° arc | -|----------------|----------------------| -| > 100 px/u | 64 (full detail) | -| 10–100 px/u | 24 | -| 1–10 px/u | 12 | -| < 1 px/u | 6 (or sub-pixel cull)| - ### 3.3 Hatch LOD — Density Reduction At low zoom, hatch line spacing appears smaller than 1px. Options: @@ -163,7 +138,6 @@ Relevant only for perspective (3D) mode with many overlapping solids. ``` Phase 1.4 Scissor for hatch/image low complexity Phase 2.1 Quadtree for 2D medium complexity, scales 2D docs -Phase 3.2 Wire curve LOD medium, affects tessellation cache Phase 3.3 Hatch LOD low complexity Phase 3.4 Mesh LOD high complexity, background thread Phase 2.2 Octree for 3D medium, needed for dense 3D @@ -180,7 +154,6 @@ Phase 4.2 Hi-Z occlusion high complexity, 3D only, last | `src/scene/mesh_model.rs` | Add `Aabb3`, LOD mesh array | | `src/scene/hatch_model.rs` | Add `Aabb2` field | | `src/scene/image_model.rs` | Add `Aabb2` field | -| `src/scene/tessellate.rs` | Adaptive arc segments | | `src/scene/truck_tess.rs` | Multi-resolution mesh tessellation | | `src/scene/mod.rs` | Quadtree/octree; LOD epoch tracking | | `src/scene/pipeline/mod.rs` | Cull hatch/image entities before upload loops | diff --git a/src/scene/mod.rs b/src/scene/mod.rs index af29fa66b..7d2840de5 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1051,6 +1051,21 @@ impl Scene { let blk_ref: &block_cache::BlockCache = &blk_cache; let view_aabb = self.view_world_aabb(); let wpp = self.world_per_pixel(); + // Zoom-adaptive curve sampling for top-level Edge tessellation. Target + // ~0.5 px chord height — far-out arcs that used to emit hundreds of + // segments now collapse to a handful. The guard clears the override + // when this scope exits so off-render tessellation (snap previews, + // hit-test, block_cache rebuild) sees the default. + struct CurveTolGuard; + impl Drop for CurveTolGuard { + fn drop(&mut self) { + crate::scene::truck_tess::set_curve_tol_override(None); + } + } + let _tol_guard = wpp.map(|w| { + crate::scene::truck_tess::set_curve_tol_override(Some((w * 0.5) as f64)); + CurveTolGuard + }); let mut wires: Vec = visible .into_par_iter() .flat_map(|e| { diff --git a/src/scene/truck_tess.rs b/src/scene/truck_tess.rs index 173d0dd5f..18faa2570 100644 --- a/src/scene/truck_tess.rs +++ b/src/scene/truck_tess.rs @@ -20,6 +20,38 @@ const CURVE_TOL: f64 = 0.005; // Triangle mesh tolerance (world units). pub const MESH_TOL: f64 = 0.01; +// ── Zoom-adaptive curve tolerance override ──────────────────────────────── +// +// The Scene sets this once per render frame to a value derived from +// `world_per_pixel` (target ≈ 0.5 px chord height). All Edge tessellations +// inside the frame — including those running on rayon worker threads — read +// from the same atomic, so callers don't need to thread a tolerance +// parameter through every entity-converter signature. Zero means "use +// `CURVE_TOL`"; that is the default, and what BlockCache::build expects. +use std::sync::atomic::{AtomicU64, Ordering}; +static CURVE_TOL_BITS: AtomicU64 = AtomicU64::new(0); + +/// Set the per-frame curve tolerance. `None` (or any non-finite/<=0 value) +/// reverts to the default `CURVE_TOL`. +pub fn set_curve_tol_override(tol: Option) { + let bits = match tol { + Some(t) if t > 0.0 && t.is_finite() => t.to_bits(), + _ => 0, + }; + CURVE_TOL_BITS.store(bits, Ordering::Relaxed); +} + +/// Resolve the active curve tolerance. Clamped to the floor `CURVE_TOL` so +/// extreme zoom-in never under-samples below the existing baseline quality. +fn current_curve_tol() -> f64 { + let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); + if bits == 0 { + CURVE_TOL + } else { + f64::from_bits(bits).max(CURVE_TOL) + } +} + // ── Public result type ──────────────────────────────────────────────────── /// Output of any truck topology tessellation. @@ -65,8 +97,9 @@ pub fn tessellate_edge(e: &Edge, offset: [f64; 3]) -> TruckTessResult { let curve = e.oriented_curve(); let (t0, t1) = curve.range_tuple(); // parameter_division samples adaptively: fewer points on straight segments, - // more on tight curves, all within CURVE_TOL chord-height error. - let (_, pts) = curve.parameter_division((t0, t1), CURVE_TOL); + // more on tight curves, all within the active chord-height tolerance. + // The Scene scales this with zoom so far-out arcs aren't oversampled. + let (_, pts) = curve.parameter_division((t0, t1), current_curve_tol()); let lines = pts .iter() .map(|p| to_local(p.x, p.y, p.z, offset))