TV13: Terrain Population LOD Pipeline
Epic TV13 adds automatic mesh simplification, auto-generated LOD chains, and HLOD (Hierarchical Level of Detail) clustering to the terrain scatter system. Users no longer need to author manual LOD meshes for every scatter asset.
What shipped
TV13.1 — Automatic Mesh Simplification
QEM (Quadric Error Metrics) edge-collapse simplification in Rust:
src/geometry/simplify.rsSimplifies any
MeshBuffersto a target fraction of its original triangle countArea-weighted vertex normals are recomputed after collapse
UVs are carried through best-effort (surviving vertex)
Boundary edges are penalized (10x cost) to preserve mesh silhouette
Python wrapper:
forge3d.geometry.simplify_mesh()LOD chain generator:
forge3d.geometry.generate_lod_chain()— simplifies from the original mesh at each level (no cascaded quality loss)
TV13.2 — Auto-Generated LOD Chains for Scatter
forge3d.terrain_scatter.auto_lod_levels()generatesTerrainScatterLevellists from a single high-detail meshConfigurable LOD count, distance thresholds, and simplification ratios
Geometric distance spacing derived from
draw_distancewhen distances are not explicitDrop-in replacement for manually authored LOD level lists
Manual LOD assets still work unchanged —
auto_lod_levels()is a convenience, not a requirement
TV13.3 — HLOD for Dense Distant Populations
Spatial grid clustering merges distant scatter instances into cheaper aggregate representations
Each HLOD cluster is a single non-instanced draw call from its own merged vertex/index buffers
Clusters are built once at batch creation time (not per-frame)
Runtime selection: clusters activate when the entire cluster (including mesh extents at per-instance scale) is beyond
hlod_distance; near instances always render individually through normal LOD selectionNew stats fields:
hlod_cluster_draws,hlod_covered_instances,effective_drawsNew memory fields:
hlod_buffer_bytes,hlod_cluster_countFull plumbing through offscreen renderer, interactive viewer, and IPC paths
HLODPolicydataclass controls all HLOD behavior;hlod=Nonepreserves pre-TV13 baseline exactly
Public API
Mesh Simplification
from forge3d.geometry import simplify_mesh, generate_lod_chain, primitive_mesh
# Simplify a mesh to 25% of its original triangle count
sphere = primitive_mesh("sphere", rings=16, radial_segments=32)
simplified = simplify_mesh(sphere, target_ratio=0.25)
print(f"{sphere.triangle_count} -> {simplified.triangle_count} triangles")
# Generate a 3-level LOD chain from one mesh
chain = generate_lod_chain(sphere, ratios=[1.0, 0.25, 0.07])
for i, mesh in enumerate(chain):
print(f"LOD {i}: {mesh.triangle_count} triangles")
Auto LOD Levels
from forge3d import terrain_scatter as ts
from forge3d.geometry import primitive_mesh
tree = primitive_mesh("cone", radial_segments=24)
# Generate 3 LOD levels with automatic distance spacing
levels = ts.auto_lod_levels(tree, lod_count=3, draw_distance=300.0)
# Result: [TerrainScatterLevel(mesh=tree, max_distance=~33),
# TerrainScatterLevel(mesh=simplified_25%, max_distance=~100),
# TerrainScatterLevel(mesh=simplified_7%, max_distance=None)]
# Use with explicit distances
levels = ts.auto_lod_levels(
tree,
lod_count=3,
lod_distances=[50.0, 150.0, None],
)
# Use with explicit ratios
levels = ts.auto_lod_levels(
tree,
lod_count=3,
ratios=[1.0, 0.3, 0.05],
draw_distance=200.0,
)
HLOD Clustering
from forge3d import terrain_scatter as ts
batch = ts.TerrainScatterBatch(
name="dense_forest",
levels=levels,
transforms=transforms,
max_draw_distance=1000.0,
hlod=ts.HLODPolicy(
hlod_distance=200.0, # beyond this, use HLOD clusters
cluster_radius=80.0, # spatial grid cell size
simplify_ratio=0.1, # aggressive simplification for distant clusters
),
)
Full Scatter Pipeline (Before and After TV13)
Before TV13 — manual LOD meshes required:
tree_hi = primitive_mesh("cone", radial_segments=24)
tree_lo = primitive_mesh("box") # user must author this manually
batch = ts.TerrainScatterBatch(
levels=[
ts.TerrainScatterLevel(mesh=tree_hi, max_distance=100.0),
ts.TerrainScatterLevel(mesh=tree_lo),
],
transforms=transforms,
)
After TV13 — one mesh, automatic LODs and HLOD:
tree = primitive_mesh("cone", radial_segments=24)
batch = ts.TerrainScatterBatch(
levels=ts.auto_lod_levels(tree, lod_count=3, draw_distance=300.0),
transforms=transforms,
max_draw_distance=1000.0,
hlod=ts.HLODPolicy(
hlod_distance=300.0,
cluster_radius=80.0,
),
)
Stats and Memory Reporting
# After rendering
stats = renderer.get_scatter_stats()
memory = renderer.get_scatter_memory_report()
# New TV13 fields in stats:
stats["hlod_cluster_draws"] # HLOD clusters drawn this frame
stats["hlod_covered_instances"] # instances suppressed by active clusters
stats["effective_draws"] # individual LOD draws + HLOD cluster draws
# New TV13 fields in memory report:
memory["hlod_cluster_count"] # number of HLOD clusters
memory["hlod_buffer_bytes"] # GPU memory used by HLOD cluster buffers
memory["total_buffer_bytes"] # now includes HLOD buffer bytes
Parameters Reference
simplify_mesh(mesh, target_ratio)
Parameter |
Type |
Description |
|---|---|---|
|
|
Input mesh to simplify |
|
|
Fraction of original triangles to target, in |
Returns |
|
Simplified mesh with recomputed normals |
generate_lod_chain(mesh, ratios, *, min_triangles=8)
Parameter |
Type |
Description |
|---|---|---|
|
|
Source mesh (LOD 0) |
|
|
Triangle ratios, descending, starting with |
|
|
Floor — levels below this are dropped |
Returns |
|
LOD chain (may be shorter than |
auto_lod_levels(mesh, *, lod_count=3, ...)
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
— |
Highest-detail mesh (LOD 0) |
|
|
|
Number of LOD levels including LOD 0 |
|
|
|
Explicit per-level distances |
|
|
|
Explicit per-level simplification ratios |
|
|
|
Used to derive distances via geometric spacing |
|
|
|
Floor passed to |
Returns |
|
— |
Ready for |
HLODPolicy
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
— |
Distance threshold for HLOD activation |
|
|
— |
Spatial grid cell size for clustering |
|
|
|
Simplification ratio for merged cluster meshes |
Architecture
Rust side
src/geometry/simplify.rs— QEM algorithm: per-vertex quadrics, edge-collapse priority queue, boundary penalization, compact output with recomputed normalssrc/geometry/py_bindings.rs—geometry_simplify_mesh_pyPyO3 bindingsrc/terrain/scatter.rs—HlodConfig,HlodCache,GpuHlodClustertypes; spatial grid clustering inbuild_hlod_cache(); three-passprepare_draws()with cluster activation, instance skip, and cluster draw; extendedTerrainScatterBatchStatsandTerrainScatterMemoryReportsrc/terrain/renderer/scatter.rs— HLOD cluster draw path inrender_scatter_pass();TerrainScatterUploadBatchcarrieshlod_configsrc/terrain/renderer/py_api.rs— Parses"hlod"dict from Python batch configsrc/viewer/terrain/scene/scatter.rs— Viewer-side HLOD draw pathsrc/viewer/ipc/protocol/payloads.rs—hlodfield inIpcTerrainScatterBatchsrc/viewer/ipc/protocol/translate/terrain.rs— Maps IPC HLOD payload to viewer config
Python side
python/forge3d/geometry.py—simplify_mesh(),generate_lod_chain()python/forge3d/terrain_scatter.py—HLODPolicy,auto_lod_levels(), updatedTerrainScatterBatch
Backward Compatibility
All changes are additive. No existing API is removed or renamed.
TerrainScatterBatchwithouthlodparameter behaves identically to pre-TV13Manual
TerrainScatterLevellists still work;auto_lod_levels()is optionalsimplify_mesh()andgenerate_lod_chain()are new geometry functions with no effect on existing codeScatter stats and memory reports gain new fields but existing fields are unchanged
All new HLOD stats fields are
0whenhlod=None
Test Coverage
8 Rust unit tests for QEM simplification (
src/geometry/simplify.rs)17 Rust tests for scatter and HLOD (scatter types, IPC parsing, backward compat)
23 Python tests in
tests/test_terrain_tv13_lod_pipeline.py:TestSimplifyMesh(3 tests): reduce triangles, preserve normals, ratio 1.0TestGenerateLodChain(5 tests): decreasing counts, min_triangles floor, deduplication, validationTestAutoLodLevels(4 tests): defaults, explicit distances, explicit ratios, batch integrationTestHLODPolicy(5 tests): creation, serialization, validation, viewer payloadTestHLODRendering(3 tests): baseline preservation, stats reporting, memory trackingTestEndToEndImageOutput(1 test): auto-LOD scatter renders non-empty image
Example
examples/terrain_tv13_lod_pipeline_demo.py — end-to-end demo using Mt. Fuji DEM:
python examples/terrain_tv13_lod_pipeline_demo.py
Demonstrates:
QEM simplification at multiple ratios (sphere: 1024 -> 511 -> 256 -> 102 triangles)
LOD chain generation from a single cone mesh
auto_lod_levels()producing scatter-ready LOD levelsSide-by-side rendering: baseline (no HLOD) vs HLOD-enabled
Stats comparison showing HLOD cluster draws, covered instances, and effective draws. Note: the HLOD benefit is most visible at high instance counts — the demo uses a moderate scene to keep runtime short, so
effective_drawsmay increase slightly due to HLOD cluster overhead. In production-scale scenes (10k+ instances), HLOD reduces draw-call pressure significantly.PNG output to
examples/out/terrain_tv13_lod_pipeline/