Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,57 @@ Print and return memory breakdown of the current scene.

---

## LOD Utilities

Level-of-detail helpers for terrain tiles and instanced geometry.

```python
from rtxpy import compute_lod_level, compute_lod_distances, simplify_mesh, build_lod_chain
```

#### `compute_lod_level(distance, lod_distances)`

Map a distance value to a discrete LOD level.

| Parameter | Type | Description |
|-----------|------|-------------|
| `distance` | float | Distance from camera to object/tile center |
| `lod_distances` | list[float] | Ascending thresholds for LOD transitions |

**Returns:** `int` — LOD level (0 = highest detail)

#### `compute_lod_distances(tile_diagonal, factor=3.0, max_lod=3)`

Generate distance thresholds from tile geometry.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `tile_diagonal` | float | | Tile diagonal in world units |
| `factor` | float | `3.0` | Base multiplier for first threshold |
| `max_lod` | int | `3` | Number of LOD transitions |

**Returns:** `list[float]` — distance thresholds

#### `simplify_mesh(vertices, indices, ratio)`

Simplify a triangle mesh via quadric decimation (requires `trimesh`).

| Parameter | Type | Description |
|-----------|------|-------------|
| `vertices` | ndarray | Flat float32 vertex buffer `(N*3,)` |
| `indices` | ndarray | Flat int32 index buffer `(M*3,)` |
| `ratio` | float | Fraction of triangles to keep (0.0-1.0) |

**Returns:** `(vertices, indices)` — simplified mesh buffers

#### `build_lod_chain(vertices, indices, ratios=(1.0, 0.5, 0.25, 0.1))`

Build a list of progressively simplified meshes.

**Returns:** `list[(vertices, indices)]` — one pair per LOD level

---

## RTX Class

Low-level OptiX wrapper. Use this directly for custom ray tracing without the xarray accessor.
Expand Down
2 changes: 2 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ dem.rtx.explore(wind_data=wind) # Shift+W to toggle
| **[** / **]** | Decrease / increase observer height |
| **R** | Decrease terrain resolution (coarser) |
| **Shift+R** | Increase terrain resolution (finer) |
| **Shift+A** | Toggle distance-based terrain LOD |
| **Z** | Decrease vertical exaggeration |
| **Shift+Z** | Increase vertical exaggeration |
| **B** | Toggle mesh type (TIN / voxel) |
Expand Down Expand Up @@ -537,6 +538,7 @@ write_stl('terrain.stl', verts, indices)

## Performance Tips

- **Enable terrain LOD**: Press `Shift+A` in the viewer to activate distance-based LOD. Nearby tiles render at full detail while distant tiles are automatically subsampled, cutting triangle count without visible quality loss
- **Subsample large DEMs**: `dem[::2, ::2]` or `explore(subsample=4)` — 4x subsample is 16x less geometry
- **Lower render_scale**: `explore(render_scale=0.25)` renders at quarter resolution for faster interaction
- **Cache meshes**: Use `mesh_cache` parameter in `place_buildings()`, `place_roads()`, etc. to skip GeoJSON parsing on reload
Expand Down
100 changes: 100 additions & 0 deletions examples/terrain_lod_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Terrain LOD demo — distance-based level of detail for large terrains.

Generates a synthetic 2048x2048 terrain and launches the interactive
viewer. Press Shift+A to toggle terrain LOD, which splits the terrain
into tiles and assigns each tile a resolution based on camera distance.

Controls:
Shift+A Toggle terrain LOD on/off
R / Shift+R Manual resolution down / up (applies globally)
Z / Shift+Z Vertical exaggeration
WASD / arrows Move camera

The LOD system is most useful with large terrains where full-resolution
rendering everywhere would exceed GPU memory or hurt frame rate. Nearby
tiles render at full detail while distant tiles are progressively
subsampled (2x, 4x, 8x).

Usage:
python terrain_lod_demo.py
python terrain_lod_demo.py --size 4096
"""

import argparse

import numpy as np
import xarray as xr

import rtxpy # noqa: F401 — registers .rtx accessor


def make_terrain(size, seed=42):
"""Generate a synthetic island terrain using multi-octave Perlin noise.

Falls back to simple sine-based terrain if xarray-spatial is not
installed.
"""
try:
import cupy as cp
from xrspatial import generate_terrain

template = xr.DataArray(
cp.zeros((size, size), dtype=cp.float32), dims=['y', 'x'],
)
terrain = generate_terrain(
template,
x_range=(-5000, 5000),
y_range=(-5000, 5000),
seed=seed,
zfactor=3000,
full_extent=(-5000, -5000, 5000, 5000),
noise_mode='ridged',
warp_strength=0.3,
octaves=5,
)
# Make it an island
sea_level = float(cp.nanmedian(terrain.data)) * 0.8
terrain.data[:] = cp.maximum(terrain.data - sea_level, 0)
return terrain
except ImportError:
pass

# Fallback: sin/cos-based terrain (no xarray-spatial needed)
rng = np.random.default_rng(seed)
y = np.linspace(0, 4 * np.pi, size, dtype=np.float32)
x = np.linspace(0, 4 * np.pi, size, dtype=np.float32)
yy, xx = np.meshgrid(y, x, indexing='ij')
z = (np.sin(xx) * np.cos(yy * 0.7) * 500
+ np.sin(xx * 2.3 + 1) * np.cos(yy * 1.7 + 0.5) * 200
+ rng.normal(0, 10, (size, size)).astype(np.float32))
z = np.maximum(z, 0)
return xr.DataArray(z, dims=['y', 'x'])


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Terrain LOD demo — press Shift+A in viewer to toggle",
)
parser.add_argument("--size", type=int, default=2048,
help="Grid size in pixels (default: 2048)")
parser.add_argument("--seed", type=int, default=42,
help="Random seed (default: 42)")
args = parser.parse_args()

print(f"Generating {args.size}x{args.size} terrain...")
terrain = make_terrain(args.size, seed=args.seed)

elev = terrain.data
if hasattr(elev, 'get'):
elev = elev.get()
print(f"Elevation range: {np.nanmin(elev):.0f} - {np.nanmax(elev):.0f} m")
print(f"\nPress Shift+A to toggle terrain LOD")
print(f"Press H for help overlay\n")

terrain.rtx.explore(
width=1920,
height=1080,
render_scale=0.5,
subsample=1,
title="Terrain LOD Demo",
)
6 changes: 6 additions & 0 deletions rtxpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from .rtx import (
RTX,

Check failure on line 2 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:2:5: F401 `.rtx.RTX` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `RTX as RTX`
has_cupy,

Check failure on line 3 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:3:5: F401 `.rtx.has_cupy` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `has_cupy as has_cupy`
get_device_count,

Check failure on line 4 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:4:5: F401 `.rtx.get_device_count` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `get_device_count as get_device_count`
get_device_properties,

Check failure on line 5 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:5:5: F401 `.rtx.get_device_properties` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `get_device_properties as get_device_properties`
list_devices,

Check failure on line 6 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:6:5: F401 `.rtx.list_devices` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `list_devices as list_devices`
get_current_device,

Check failure on line 7 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:7:5: F401 `.rtx.get_current_device` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `get_current_device as get_current_device`
get_capabilities,

Check failure on line 8 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:8:5: F401 `.rtx.get_capabilities` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `get_capabilities as get_capabilities`
)
from .mesh import (
triangulate_terrain,

Check failure on line 11 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:11:5: F401 `.mesh.triangulate_terrain` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `triangulate_terrain as triangulate_terrain`
voxelate_terrain,

Check failure on line 12 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (F401)

rtxpy/__init__.py:12:5: F401 `.mesh.voxelate_terrain` imported but unused; consider removing, adding to `__all__`, or using a redundant alias help: Use an explicit re-export: `voxelate_terrain as voxelate_terrain`
add_terrain_skirt,
build_terrain_skirt,
write_stl,
Expand All @@ -23,6 +23,12 @@
load_meshes_from_zarr,
chunks_for_pixel_window,
)
from .lod import (
compute_lod_level,
compute_lod_distances,
simplify_mesh,
build_lod_chain,
)
from .analysis import viewshed, hillshade, render, flyover, view
from .engine import explore

Check failure on line 33 in rtxpy/__init__.py

View workflow job for this annotation

GitHub Actions / Lint & Import Check

ruff (I001)

rtxpy/__init__.py:1:1: I001 Import block is un-sorted or un-formatted help: Organize imports

Expand Down
Loading