Skip to content

feat: [dsm-rgb-landcover-tirol] DSM geometry + orthophoto/land-cover albedo for the Tirol demo - #62

Open
JustCreature wants to merge 9 commits into
mainfrom
dsm-rgb-landcover-tirol
Open

feat: [dsm-rgb-landcover-tirol] DSM geometry + orthophoto/land-cover albedo for the Tirol demo#62
JustCreature wants to merge 9 commits into
mainfrom
dsm-rgb-landcover-tirol

Conversation

@JustCreature

@JustCreature JustCreature commented Jun 12, 2026

Copy link
Copy Markdown
Owner

What

The Tirol demo view now renders a photo-real world: trees and buildings as actual geometry (BEV ALS DSM drives the fine tier), the 20 cm BEV orthophoto draped as albedo near/mid range, and the land-cover raster as a material layer (water gets a flat tint + sun glint). Everything is config-driven via config.toml — no UI changes — and every new layer degrades gracefully when its file is missing, so the demo keeps working on machines without the mosaics.

Source File Role
ALS DSM, 1 m, EPSG:3035 (single IFD, LZW 64×64, NoData = +3.4028235e38) big_size/ALS_DSM_CRS3035RES50000mN2650000E4450000.tif fine-tier surface geometry
Orthophoto RGB, 0.2 m, EPSG:31255 (ModernJPEG YCbCr, mask IFDs, 12 overviews) color/2019470_Mosaik_RGB.tif albedo
Land cover, 0.2 m, EPSG:31255 (Gray(4) nibble-packed, 10 overviews) color/2022470_Mosaik_LC.tif material codes (water/veg/buildings)

How

dem_io

  • NoData generalization (bug fix): the DSM's GDAL_NODATA = +3.4028235e38 sailed through the old NaN / < -1000 / == 0 predicate and parsed as a valid 3.4e38 m elevation, poisoning max_terrain_h and the march's sky exit. extract_window, parse_geotiff_auto and the overview-cache builder now share is_nodata_value(), which reads the GDAL_NODATA ASCII tag (42113) and rejects the float-max family.
  • composite_surface_over(canvas, overlay, feather_px): paints valid DSM cells onto the fine-tier DTM with a 6 px BFS feather at the ALS coverage edge. The DTM stays the canvas so fine-tier coverage never shrinks where the DSM window is clipped — deliberately not fill_nodata_from_base with swapped roles, whose 30 px outward blend would sand real canopy down to bare earth along every coverage edge.
  • extract_color_window (color.rs): camera-centred window from the JPEG-YCbCr ortho (the tiff crate's zune path returns raw YCbCr; BT.601→RGB happens on the worker) + land cover nearest-resampled onto the same grid. Output is one RGBA8 buffer: RGB = albedo, A = material code ladder (water 255 / high veg 192 / med veg 128 / building 64 / other 0 — codes ≥ 64 apart so bilinear boundary mixes degrade into the neighbouring band instead of aliasing to an unrelated material).
  • ifd_overview_levels: NewSubfileType-filtered IFD walk. The RGB mosaic interleaves per-dataset transparency-mask IFDs with its overview chain, so a raw scale index (à la ifd_scales) would mis-pair scales with IFDs.
  • The LC mosaic turned out to be Gray(4) — 4-bit nibble-packed chunks, unpacked in extract_u8_window. Class codes are undocumented; they were pinned empirically with the new inspect_color example against single-class spots: Achensee deep centre = 100 % class 5 (water), Hintertux glacier = 100 % class 2 (ground/ice), forest slope = 96 % class 1 (high veg), valley meadow = 88 % class 6 (low veg), Mayrhofen centre = 44 % class 4 (buildings).

render_gpu

  • Two mipmapped Rgba8Unorm ortho windows join the canonical bind group as entries 20–23, each with its own origin/extent/cos_rot/sin_rot uniform block (CameraUniforms 224 → 288 bytes; the std140 layout test pins the new offsets). Deliberately non-sRGB — the whole shader works in display-space 0–255.
  • upload_ortho_fine/close follow the established drop-first reload cycle (placeholder swap → rebuild_bind_grouppoll(Wait) → tracked alloc) so reload peak stays max(old, new); set_ortho_*_inactive eagerly frees. All allocations go through the vram tracker.
  • gen_rgba_mip_bytes box-filters RGB but takes nearest alpha — averaging the discrete material codes would fabricate classes at every mip.
  • Shader: hit-point albedo = procedural ramp → close ortho → fine ortho, with the same 500 m edge smoothstep and per-window rotation as the height tiers, mip level from the existing distance heuristic. Where ortho wins, the brightness floor is lifted (mix(brightness, clamp(brightness, 0.55, 1.0), w·0.7)) — the photo carries the capture day's baked sun shading, and full diffuse on top double-shades north faces. Water (A ≥ ~0.9) blends toward a lake tint and adds a mirror-direction sun glint.

viewer / launcher

  • DemoViewConfig gains surface_tile_paths / ortho_tile_paths / landcover_tile_paths (serde-defaulted to the BEV mosaics, resolved against tiles_dir; old configs parse untouched).
  • The fine worker extracts a DSM window with the same centre/radius as its DTM window and composites it on — normals, DDA self-shadowing and bicubic smoothing then all run on the composite, so trees and buildings get geometry and shadows with zero shader changes for this feature.
  • StreamingTier is now generic over its payload (TierCentre trait), so two new ortho workers (fine ≈ 0.8 m/px, close ≈ 6.4 m/px; radii per the new ortho_radii(VramClass) presets) reuse all drift/trigger bookkeeping. Steady-state ortho cost: ≈ 170 MB (Mid), ≈ 330 MB (High), ≈ 17 MB (Low).
  • Base reloads deactivate + invalidate both ortho windows (their origins are base-relative). The OOM ladder gains step 0: drop both ortho textures before touching any height tier — cosmetic-first degradation.
  • T toggles the ortho drape; R (debug reload) now also invalidates the ortho tiers.

Post-review rendering fixes

  • Buildings & trees no longer look like smooth mountains. The fine tier now carries the DSM, but the Catmull-Rom bicubic normal smoothing (built for bare-earth ridgelines within smooth_radius_m) was rounding every building wall and canopy edge into a hill. The smoothing is now gated by world-slope magnitude — it blends back to the raw per-texel Sobel normal where the gradient is wall-steep (smoothstep(1.0, 2.5) m/m), so man-made faces and canopy edges stay crisp while gentle terrain stays smooth. (Note: true hollow geometry under a tree canopy isn't representable in a single-valued heightfield — this makes trees/buildings read as distinct sharp objects, which was the visible problem.)
  • Black spot where the ortho coverage ends. The BEV mosaic fills areas with no photo coverage with pure black (0,0,0), which draped onto the terrain as a black hole. Each ortho sample is now gated by luminance (smoothstep(2, 16)) so near-black no-data fades out and the procedural terrain colour shows through; real ortho pixels stay well above the threshold, making the coverage edge a clean one-texel fade.

Verification

  • 172 workspace tests + 33 bin tests pass (incl. real-device GPU suites under lavapipe: ortho upload→inactive lifecycle, vram accounting returns to baseline), clippy --all-targets -D warnings clean.
  • Gated tests against the real multi-GB tiles (skip cleanly elsewhere): mask-IFD filtering on the RGB pyramid, DSM float-max NoData mapping, lake-window water-material + YCbCr chroma checks, plus a full offscreen smoke render of the pipeline through the production placement math.
  • Live run (M4 Max, Mid preset): fine ortho 5000² @ 0.8 m/px decodes in 1.3 s on its worker, close window 2978×4306 @ 6.4 m/px in 0.7 s; uploads are steady-state write_texture calls; zero wgpu validation errors.

Coverage

The color/ortho readers and placement math only ran end-to-end against the multi-GB mosaics, which CI lacks, so they showed uncovered and dropped patch coverage below the 60 % gate. Synthetic-fixture tests now exercise the same production paths without the real files: patch coverage 56 % → 81 % (measured under the CI condition, tiles absent).

File Before After
dem_io/src/color.rs 18 % 95 %
dem_io/src/geotiff.rs 40 % 98 %
viewer/tiers.rs 28 % 69 %

viewer/mod.rs stays uncovered by design — it's the winit/GPU event-loop glue the project's testing policy excludes.

Note: the default Hintertux camera sits ~200 m west of the DSM coverage edge (starts ~11.69°E) — fly east into the Tux/Ziller valleys (Mayrhofen is the showcase) to see buildings and forest canopy in relief.

🤖 Generated with Claude Code

JustCreature and others added 6 commits June 12, 2026 20:42
… sentinels in DEM readers

The BEV ALS DSM declares NoData = +3.4028235e38 via the GDAL_NODATA ASCII
tag (42113). The old predicate (NaN / < -1000 / == 0) let those cells
through as valid 3.4e38 m elevations, poisoning max_terrain_h and the
raymarch sky exit. extract_window, parse_geotiff_auto and the overview
cache builder now share is_nodata_value(), which also honours the file's
declared GDAL_NODATA value for finite custom sentinels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to dem_io

composite_surface_over paints the valid cells of a DSM window onto the
fine-tier DTM canvas with a short BFS feather at the ALS coverage and
window boundaries. The DTM stays the canvas so fine-tier coverage never
shrinks where the DSM window is clipped, and real canopy/building heights
are never sanded down the way fill_nodata_from_base's 30 px outward blend
would. Canvas sentinel cells take the overlay at full weight so nothing
ever blends toward -9999.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ow reader to dem_io

extract_color_window reads a camera-centred window from the BEV 20 cm
orthophoto mosaic (ModernJPEG YCbCr chunks decoded via the tiff crate's
zune path, BT.601-converted on the caller's thread) and nearest-resamples
the land-cover raster onto the same grid, packing one RGBA8 buffer:
RGB = albedo, A = material code ladder (water 255 / high veg 192 /
med veg 128 / building 64 / other 0 — codes spaced >= 64 apart so bilinear
boundary mixes degrade gracefully). The LC mosaic is Gray(4): 4-bit
nibble-packed chunks are unpacked in extract_u8_window.

ifd_overview_levels walks the IFD chain filtered by NewSubfileType so the
RGB mosaic's interleaved transparency-mask IFDs don't shift the overview
indices the way raw ifd_scales would.

LC class codes pinned empirically with the new inspect_color example
against single-class reference spots (Achensee = 100% class 5 water,
Hintertux glacier = 100% class 2 ground/ice, forest 96% class 1, meadow
88% class 6, town 44% class 4 buildings). Gated integration tests run
against the local multi-GB tiles and skip cleanly elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd shader draping to render_gpu

Two mipmapped Rgba8Unorm windows (fine + close) join the canonical bind
group as entries 20-23, with their own origin/extent/rotation uniform
blocks (CameraUniforms grows 224 -> 288 bytes; layout test pins the new
offsets). upload_ortho_fine/close follow the established drop-first
reload cycle (placeholder swap -> rebuild_bind_group -> poll(Wait) ->
tracked alloc) and set_ortho_*_inactive eagerly frees the textures.
gen_rgba_mip_bytes box-filters RGB but takes nearest alpha so the
land-cover material codes survive the mip chain. Deliberately
non-sRGB: the whole shader works in display-space 0-255.

Shader: hit-point albedo = procedural ramp -> close ortho -> fine ortho
with the same edge-smoothstep + per-window rotation as the height tiers
and the existing distance-heuristic mip. Where ortho wins, the brightness
floor is lifted (the photo carries baked sun shading; full diffuse on top
double-shades north faces). Water material (alpha >= ~0.9) blends toward
a lake tint and adds a mirror-direction sun glint. ortho_mode is threaded
through dispatch_frame; the drape is a no-op while both extents are 0.

GPU suites gain upload->inactive lifecycle and vram-accounting coverage
for the new texture pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…albedo tiers in the viewer

DemoViewConfig gains surface_tile_paths / ortho_tile_paths /
landcover_tile_paths (serde-defaulted to the BEV Tirol mosaics, resolved
against tiles_dir; missing files degrade gracefully via build_tile_index,
so old configs keep working untouched).

The fine worker extracts a DSM window with the same centre/radius as its
DTM window and composites it on with composite_surface_over — canopy and
buildings become fine-tier geometry with normals, DDA self-shadowing and
bicubic smoothing for free.

StreamingTier becomes generic over its payload (TierCentre trait) so two
new ortho workers (fine ~0.8 m/px, close ~6.4 m/px, radii per the new
ortho_radii(VramClass) presets) reuse all drift/trigger bookkeeping. They
resolve mask-aware overview levels once per file, read RGBA windows via
extract_color_window, pre-pack mips, and the frame loop uploads them like
any other tier. Base reloads deactivate + invalidate both windows (their
origins are base-relative); the OOM ladder gains step 0 = drop ortho
before any height tier; T toggles the drape.

Includes a gated offscreen smoke test that renders the full DSM + ortho
pipeline against the real tiles through the production placement math and
asserts the frame contains ortho-textured terrain
(/tmp/offscreen_dsm_ortho.png for visual inspection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d, add rendering-approaches learning note

CLAUDE.md gains the DSM + ortho/land-cover architecture section (bind
group entries 20-23, material code ladder, empirically pinned LC classes,
OOM step 0), the new dem_io modules, and the updated tiers.rs inventory.
The learnings note compares polygons vs SDF raymarching vs this
renderer's heightfield raymarch and maps the trade-offs onto
shader_texture.wgsl.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JustCreature JustCreature changed the title Dsm rgb landcover tirol feat: [dsm-rgb-landcover-tirol] DSM geometry + orthophoto/land-cover albedo for the Tirol demo Jun 12, 2026
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.16804% with 371 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/viewer/tiers.rs 40.75% 221 Missing ⚠️
src/viewer/mod.rs 0.00% 118 Missing ⚠️
crates/dem_io/src/color.rs 89.63% 23 Missing ⚠️
crates/render_gpu/src/scene/tiers.rs 98.00% 4 Missing ⚠️
crates/dem_io/src/heightmap.rs 97.65% 3 Missing ⚠️
crates/dem_io/src/geotiff.rs 95.23% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

JustCreature and others added 3 commits June 12, 2026 21:03
…t for readable texel sum

cargo clippy --all-targets -D warnings flags the deliberate
(0 + 40 + 80 + 120) / 4 in the mip-average assertion. Keep the four
contributing texels visible instead of collapsing to a bare 60, matching
the existing rationale'd allow on the f16 mip shape test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rtho black coverage

Two rendering fixes for the DSM + ortho draping:

1. Buildings and trees rendered like smooth mountains. The fine tier now
   carries the DSM, but the Catmull-Rom bicubic normal smoothing (meant for
   bare-earth ridgelines within smooth_radius_m) rounded every building wall
   and canopy edge into a hill. Gate the smoothing by world-slope magnitude:
   blend back to the raw per-texel Sobel normal where the gradient is
   wall-steep (smoothstep 1.0->2.5 m/m) so man-made faces and canopy edges
   stay crisp while gentle terrain stays bicubic-smooth.

2. Big black spot where the ortho coverage ends. The BEV mosaic fills areas
   with no photo coverage with pure black (0,0,0), which draped onto the
   terrain. Gate each ortho sample by luminance (smoothstep 2->16) so
   near-black no-data fades out and the procedural terrain colour shows
   through; real ortho pixels stay well above the threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt and IFD enumeration

The color/ortho readers and placement math only ran against the multi-GB
BEV mosaics, which CI never has, so they showed as uncovered and dropped
patch coverage below the 60% gate. Add synthetic-fixture tests that
exercise the same production paths without the real tiles:

- color_synth.rs: tiny in-process RGB + single-band GeoTIFFs drive
  extract_color_window (RGB pass-through, land-cover resample, material
  packing, georef stub) and landcover_histogram (color.rs 18% -> 95%).
- tiers.rs inline tests for cross_crs_world_origin[_and_extent]: same-CRS
  exact placement plus both cross-CRS branches (geographic base, projected
  base) through real proj4 transforms (tiers.rs 28% -> 69%).
- synthetic.rs: ifd_overview_levels over a multi-IFD overview cache
  (geotiff.rs 40% -> 98%).

Patch coverage 56% -> 81% under the CI condition (tiles absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant