10/06/2026

Remapping my world from above with ground data

Using open data from IGN (french institute for geographical maps), I asked new Claude 5.0 to build a threejs powered landscape viewer. I downloaded data files from IGN (`.shp`/`.dbf` format) and let a custom processor convert to binary format as explained by Claude who did the job :

The format is deliberately minimal — a raw dump of a Float32Array, with all the metadata moved out to a JSON sidecar. Two files in web/data/:

heightmap.bin — 15.4 MB of nothing but altitudes. It’s a 1883 × 2049 grid (W × H) of IEEE-754 32-bit floats, little-endian, in meters above sea level. There is no header, no magic number, no compression: byte 0 is the start of the first float, and the file is exactly W × H × 4 bytes. The values are laid out row-major (whole first row, then the second…), and row 0 is the northernmost row, matching screen/texture convention so the viewer never has to flip anything. So the altitude of grid cell (col x, row y from the north) lives at byte offset (y * 1883 + x) * 4, and in JS it’s just:

const grid = new Float32Array(await (await fetch('data/heightmap.bin')).arrayBuffer());
const alt = grid[y * width + x];   // mètres

That’s the whole “parser” — which is the point. A typed array view over the fetched buffer costs zero decoding time, unlike the source .shp/.dbf you have selected, which need real parsing (record headers, ring offsets, DBF field descriptors…).

meta.json — everything needed to interpret those numbers geographically:

  • widthheight — grid dimensions (1883 × 2049);
  • cellSize — ground distance between adjacent samples, ~75.2 m;
  • xmin/ymin/xmax/ymax — the bounding box in Lambert-93 meters (EPSG:2154), so cell (x, y) sits at easting xmin + x*cellSize, northing ymax − y*cellSize (minus, because row 0 is north);
  • zmax (375 m) — used by the viewer to scale the color ramp;
  • crs and source — provenance.

Semantics worth knowing:

  • Sea is exactly 0 — the flood-fill clamp wrote literal zeros, which is why the viewer can treat z ≤ 0.5 as water.
  • Between contour lines the values are interpolated (the Laplace solve), so off-contour cells are smooth estimates, not measurements; on-contour cells average the contour altitudes that crossed them.
  • No NoData value exists. Beyond the department boundary the grid holds extrapolated values rather than a sentinel — a known limitation noted in the README.

Trade-offs of the choice: Float32 is 4 bytes/cell where Uint16 decimeters would be 2 (≈ 7.7 MB), and PNG-16 would compress further — but both need decode steps and quantization, while this loads in one fetch + one constructor call and round-trips the solver’s output exactly. If load size ever matters (e.g. serving over slow networks), the easy win is gzip on the wire: smooth float rasters compress to roughly half

related projects : online mapping with threejs

To top