OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev / compare_engine_vs_lookdev.py

87 lines · python ref:
#!/usr/bin/env python3
"""
Put the ENGINE's terrain next to the LOOKDEV's, same ground, same colours.

Every check in this session has produced a number, and a number has not once
told anybody what they were looking at. This writes one PNG: lookdev on the
left, engine on the right, difference on the far right, hypsometric tint shared
so the two are directly comparable by eye.

The engine half comes from map_dc_diffusion writing /tmp/engine_gpu_surface.f32
on load -- a 64^3 debug_gpu_density dispatch, so it is the GPU's own field, not
the CPU proxy that misled this session repeatedly.

    python3 compare_engine_vs_lookdev.py
"""
import os, subprocess, sys
import numpy as np

DC   = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "dc-terrain"))
HBLK = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "hyper-tps",
                                    "assets", "terrain", "diffusion_gully_30km.hblk"))
ENG  = "/tmp/engine_gpu_surface.f32"
SPAN, N = 7680.0, 64
CX, CZ = 53760.0, 38400.0
OX, OZ = CX - SPAN / 2.0, CZ - SPAN / 2.0
STEP = SPAN / float(N - 1)


def tint(h, lo, hi):
    t = np.clip((h - lo) / max(hi - lo, 1e-6), 0, 1)
    stops = np.array([[0.05,0.12,0.28],[0.20,0.35,0.45],[0.30,0.42,0.22],
                      [0.55,0.50,0.30],[0.60,0.55,0.52],[0.92,0.92,0.94]])
    idx = t * (len(stops) - 1)
    i0 = np.floor(idx).astype(int); i1 = np.minimum(i0 + 1, len(stops) - 1)
    f = (idx - i0)[..., None]
    rgb = stops[i0] * (1 - f) + stops[i1] * f
    # hillshade so shape reads, not just altitude
    gy, gx = np.gradient(h, STEP)
    n = 1.0 / np.sqrt(gx*gx + gy*gy + 1.0)
    shade = np.clip(0.45 + 0.75 * ((-gx - gy) * n * 0.5 + 0.5), 0, 1.35)
    return np.clip(rgb * shade[..., None], 0, 1)


def main():
    if not os.path.exists(ENG):
        print(f"missing {ENG} — load map_dc_diffusion once; it writes this on load")
        return 2
    eng = np.fromfile(ENG, dtype="<f4", count=N*N).astype(np.float64).reshape(N, N)
    miss = eng < -99998.0
    print(f"engine: {N}x{N}, {(~miss).sum()} columns with a surface, "
          f"{eng[~miss].min():.0f}..{eng[~miss].max():.0f} m")

    out = "/tmp/lookdev_surface.f32"
    r = subprocess.run(["/tmp/erosion_render4", "--heightmap", HBLK,
                        "--ox", str(OX), "--oz", str(OZ), "--n", str(N),
                        "--mpp", str(STEP), "--out", out], cwd=DC,
                       capture_output=True, text=True)
    if r.returncode != 0:
        print(r.stdout, r.stderr); return 1
    look = np.fromfile(out, dtype="<f4", count=N*N).astype(np.float64).reshape(N, N)
    print(f"lookdev: {look.min():.0f}..{look.max():.0f} m")

    valid = ~miss
    d = np.abs(eng - look)
    print(f"\n  max |engine - lookdev| = {d[valid].max():8.1f} m")
    print(f"  rms                    = {np.sqrt((d[valid]**2).mean()):8.1f} m")

    lo = min(eng[valid].min(), look.min()); hi = max(eng[valid].max(), look.max())
    a = tint(look, lo, hi); b = tint(np.where(miss, lo, eng), lo, hi)
    dm = np.clip(d / max(d[valid].max(), 1e-6), 0, 1)
    c = np.stack([dm, dm*0.3, 1.0-dm], -1)
    gap = np.ones((N, 4, 3))
    img = (np.concatenate([a, gap, b, gap, c], axis=1) * 255).astype(np.uint8)
    try:
        from PIL import Image
        Image.fromarray(img).resize((img.shape[1]*6, img.shape[0]*6), Image.NEAREST)\
             .save("/tmp/engine_vs_lookdev.png")
        print("\n  wrote /tmp/engine_vs_lookdev.png  (lookdev | engine | difference)")
    except ImportError:
        img.tofile("/tmp/engine_vs_lookdev.raw")
        print("\n  no PIL; wrote raw RGB to /tmp/engine_vs_lookdev.raw")
    return 0


if __name__ == "__main__":
    sys.exit(main())