Population and thematic rasters

RasterHeightSurfaceStyle, CategoricalRasterStyle and BivariateRasterStyle are exported from forge3d and forge3d.style. They use the existing native GIS normalization/classification API. Their apply() results include a true-valid mask, display-sRGB RGBA8 cells and legend data. Population results also contain float32 heights; thematic results contain class indices (0 means nodata).

import numpy as np
import forge3d as f3d

population = np.array([[0, 10, 20], [20, 10, 0]], dtype=np.float32)
height_style = f3d.RasterHeightSurfaceStyle(
    height_scale=2, source_units="people/km²", height_units="m", nodata=-9999,
)
terrain = f3d.TerrainSource(
    data=population, style=height_style, crs="EPSG:3857",
    metadata={"source_id": "local-population"},
)
scene = f3d.MapScene(
    terrain=terrain, lighting=f3d.LightingPreset(name="daylight"),
    output=f3d.OutputSpec(width=192, height=192),
)
scene.render("population.png")

Height is source value × height_scale. The scale is in declared height units per source unit; use metres for metric terrain coordinates. Values must be nonnegative outside nodata, and the output must fit finite float32. Units are labels carried into parameters and legends, never inferred or converted. Shade interpolates the low/high colors with gis.normalize_raster minmax values; a constant raster uses the low color. NaN/nonfinite source cells, declared nodata, and false valid_mask cells are excluded. An all-invalid input diagnoses empty_raster. Invalid scales/domains diagnose invalid_argument.

For land cover, supply a CategoricalRasterStyle(palette={class_id: rgba}, labels={class_id: label}, nodata=...) to RasterOverlay(style=..., data=...). Class IDs must be integers and every valid source ID must appear in the palette. Zero is a valid source ID unless explicitly declared nodata. RGBA colors are four integer bytes; class legend rows retain original IDs in ascending order. Native classifier output indices are numbered from 1; 0 is reserved for nodata.

For the starting bivariate slice, supply two increasing finite boundaries per axis and a 3×3 palette to BivariateRasterStyle(x_bins, y_bins, palette, x_label, y_label, ...). Use RasterOverlay(data=x, secondary_data=y, style=...). palette[y_bin][x_bin] has low-to-high y rows and x columns. right=False preserves classifier intervals [-∞, b0), [b0, b1), [b1, ∞); right=True gives (-∞, b0], (b0, b1], (b1, ∞). The two masks intersect. Results expose both axis indices, combined indices 1–9, palette cells and labelled axis legends with units and boundary closure.

MapScene accepts inline arrays or local .npy/GeoTIFF sources (path and secondary_path). GeoTIFF nodata masks are retained. Styled sources must share the terrain grid and CRS; pre-align them using the existing GIS/alignment APIs. The native terrain pass has one thematic color source. Multiple styled overlays diagnose explicitly. A thematic overlay can color a population height surface; it then supplies the color palette while the population style supplies height. Ordinary unstyled raster overlays keep their existing compositor and 3D block.

Nearest-cell native sampling preserves class colors without palette blending. Cells with zero alpha, including nodata, discard terrain fragments. Other alpha values and overlay opacity blend the palette into terrain albedo before native lighting. Lighting and tone mapping can change displayed RGB; apply().rgba is the exact palette reference. Legends are structured data, ready for a consumer; this slice does not compose a legend into a map plate.

Style parameters and inline source cells survive MapScene.save_bundle() / load_bundle() without a schema-version change. Nonfinite source cells encode as null holes and negative zero normalizes to zero. Invalid nonfinite style parameters are rejected. Existing unstyled recipe serialization is unchanged. Styled inline terrain with shape (1, H, W) normalizes to (H, W) at construction, so the data summary and stored values use the same single-band grid. Local file paths remain references; keep those sources available for replay.

Run the complete public example on a physical terrain-capable GPU:

python examples/mapscene_thematic_rasters.py --output artifacts/thematic

It renders all three styles, saves/reloads each bundle, compares PNG bytes and writes render metadata and hashes. The scope excludes spikes, contours, data fetching, alignment redesign and plate composition. No cross-backend or hosted acceptance is established by a single local adapter run.