#!/usr/bin/env python3
"""
GATE: is the baked heightfield BAND-LIMITED per LOD?
The property the octree clipmap needs is not "the coarse surface is close to the
fine one" -- it is "the coarse surface is a SMOOTHED version of the fine one".
Every other layer in this generator gets that from nyquist_octaves(..., voxel_scale,
..), which drops octaves as the voxel grows. A sampled heightfield has no such
property unless somebody gives it one.
Without it, LOD k samples a 30 m field at 2^k * 30 m spacing. At LOD 7 that is
points 3840 m apart in a field whose sd is 1167 m, so neighbouring voxels differ by
about 1.6 km and the coarse mesh is noise rather than terrain. geometric_error then
never falls (dc_octree_clipmap.cpp:328-351 uses it for both the content-aware split
boost and the screen-space test), so the clipmap splits forever and never converges.
THE CHECK: render the surface at voxel V on a V-spaced grid, and compare against
the FINE surface box-averaged over V. A band-limited field matches; a point-sampled
one does not.
ALIGNMENT, which this gate got wrong first time round and reported as a code bug.
Level-k texel j is the average of level-0 texels [j*2^k, (j+1)*2^k), so its CENTRE
sits at ox + (j*2^k + (2^k-1)/2)*mpp -- half a coarse texel to the right of
ox + j*V. Sampling the renderer at ox + j*V and comparing against box j compares
two different places, and reads as 57 m of error at LOD 4 where there is 13. The
coarse render is therefore offset onto the box centres.
TOLERANCE IS RELATIVE TO THE VOXEL. A difference far below the voxel size cannot
be represented by that LOD's mesh and is not a defect; what matters is whether the
coarse surface is the fine one smoothed, to within what the LOD can express.
THE CONTROL is V = 30, the field's own pitch, where the two are the same thing by
construction. If the control ever fails the comparison itself is wrong.
python gate_heightmap_lod.py assets/terrain/diffusion_gully_30km.hblk
"""
import argparse, os, subprocess, sys
import numpy as np
DC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "dc-terrain"))
MPP = 30.0
def render(binpath, hblk, ox, oz, n, mpp, voxel, out):
cmd = [binpath, "--heightmap", hblk, "--ox", str(ox), "--oz", str(oz),
"--n", str(n), "--mpp", str(mpp), "--out", out,
"--set", "voxel_size", str(voxel)]
r = subprocess.run(cmd, cwd=DC, capture_output=True, text=True)
if r.returncode != 0:
print(r.stdout, r.stderr); raise SystemExit("renderer failed")
return np.fromfile(out, dtype="<f4", count=n * n).astype(np.float64).reshape(n, n)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("hblk"); ap.add_argument("--bin", default="/tmp/erosion_render2")
ap.add_argument("--ox", type=float, default=45000.0)
ap.add_argument("--oz", type=float, default=30000.0)
ap.add_argument("--fine-n", type=int, default=256)
ap.add_argument("--tol-frac", type=float, default=0.10,
help="rms tolerance as a fraction of the voxel size")
a = ap.parse_args()
hb = os.path.abspath(a.hblk)
span = a.fine_n * MPP
print(f"window {span/1000:.2f} km at ({a.ox:.0f}, {a.oz:.0f}), "
f"fine {a.fine_n}x{a.fine_n} @ {MPP:g} m\n")
fine = render(a.bin, hb, a.ox, a.oz, a.fine_n, MPP, 1.0, "/tmp/lod_fine.f32")
print(f"{'voxel m':>8} {'LOD':>4} {'rms vs box-avg':>15} {'tol':>8} verdict")
fails = 0
for V in [30.0, 120.0, 480.0, 1920.0]:
s = int(round(V / MPP))
n = a.fine_n // s
if n < 4:
continue
# onto the box centres — see ALIGNMENT above
shift = (s - 1) * MPP * 0.5
coarse = render(a.bin, hb, a.ox + shift, a.oz + shift, n, V, V,
f"/tmp/lod_{int(V)}.f32")
m = n * s
box = fine[:m, :m].reshape(n, s, n, s).mean(axis=(1, 3))
rms = float(np.sqrt(((coarse - box) ** 2).mean()))
tol = max(a.tol_frac * V, 5.0)
ok = rms <= tol
tag = "PASS" if ok else "FAIL"
if V == MPP:
tag = ("CONTROL ok" if ok else "CONTROL BROKEN — the comparison is wrong")
elif not ok:
fails += 1
print(f"{V:>8.0f} {int(np.log2(V/MPP)):>4} {rms:>15.1f} {tol:>8.1f} {tag}")
print()
if fails == 0:
print("VERDICT: the field is band-limited — every LOD is a smoothed version")
print(" of the one below it, which is what the clipmap needs to settle.")
return 0
print("VERDICT: the field is NOT band-limited. Coarse LODs point-sample a 30 m")
print(" field, so they are noise rather than smoothed terrain, and the")
print(" clipmap has nothing to converge to.")
return 1
if __name__ == "__main__":
sys.exit(main())