perf(scene): hatch LOD — solid-fill substitute + sub-pixel skip (Phase 3.3)

Hatch fills drew the full per-family pattern loop every frame regardless
of zoom. Two LOD tiers, neither of which existed:

  * **Shader-side solid substitute.** When the densest line family's
    spacing × scale projects to less than 2 px, individual lines blur
    into a solid color and the per-fragment family loop is wasted ALU
    (and produces moiré). Pass `world_per_pixel` through Uniforms; the
    hatch fragment shader now returns `h.color` directly in that case.
  * **CPU-side sub-pixel skip.** When the entire hatch AABB projects
    to less than 2 px, drop the draw call. Adds `world_aabb` to
    HatchGpu plus a per-frame `compute_hatch_lod` pass (mirrors the
    existing scissor pass) and `hatch_skip_flags`. The render pass
    skips flagged hatches.

Factored a shared `aabb_below_pixel` helper for future LOD callers.

The Uniforms layout is unchanged in size (96 B) — the trailing
`_pad: vec2<f32>` becomes `world_per_pixel: f32, _pad: f32`, so wgsl
modules that don't read the new field keep working with their old
`_pad: vec2<f32>` declarations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-15 23:18:21 +03:00
commit e7675be0bc
5 changed files with 102 additions and 7 deletions

View file

@ -7,16 +7,27 @@ pub struct Uniforms {
pub view_proj: glam::Mat4,
pub camera_pos: glam::Vec4,
pub viewport_size: [f32; 2],
pub _pad: [f32; 2],
/// World units per screen pixel at the current zoom. Used by the
/// hatch shader to substitute solid fill when pattern line spacing
/// falls below ~2 px (Phase 3.3 LOD).
pub world_per_pixel: f32,
pub _pad: f32,
}
impl Uniforms {
pub fn new(camera: &Camera, bounds: Rectangle) -> Self {
let half_h = camera.ortho_size();
let world_per_pixel = if bounds.height > 0.0 {
(2.0 * half_h) / bounds.height
} else {
0.0
};
Self {
view_proj: camera.view_proj(bounds),
camera_pos: camera.position_vec4(),
viewport_size: [bounds.width, bounds.height],
_pad: [0.0; 2],
world_per_pixel,
_pad: 0.0,
}
}
}