Some checks failed
nightly / Test against latest dependencies (py3.10) (push) Has been cancelled
nightly / Test against latest dependencies (py3.13) (push) Has been cancelled
tests / tests (3.13, locked) (push) Has been cancelled
tests / tests (3.13, unlocked) (push) Has been cancelled
tests / pyright (3.10) (push) Has been cancelled
tests / lint-format (push) Has been cancelled
tests / tests (3.10, locked) (push) Has been cancelled
tests / tests (3.11, locked) (push) Has been cancelled
tests / tests (3.12, locked) (push) Has been cancelled
tests / pyright (3.11) (push) Has been cancelled
tests / pyright (3.12) (push) Has been cancelled
tests / pyright (3.13) (push) Has been cancelled
tests / ty-check (3.10) (push) Has been cancelled
tests / ty-check (3.11) (push) Has been cancelled
tests / ty-check (3.12) (push) Has been cancelled
tests / ty-check (3.13) (push) Has been cancelled
tests / stubs (push) Has been cancelled
tests / smoke-test (push) Has been cancelled
Docker / check_paths (push) Has been cancelled
docs / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Upstream: https://github.com/michaelgillett/mjlab Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047 Upstream-Branch: main
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
"""Interactive single-patch terrain explorer (Viser + MuJoCo MjSpec).
|
|
|
|
Run with:
|
|
uv run python scripts/tools/terrain_explorer.py
|
|
uv run python scripts/tools/terrain_explorer.py --port 8081
|
|
|
|
Then open the printed URL (default http://localhost:8080).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import time
|
|
|
|
import mujoco
|
|
import numpy as np
|
|
import viser
|
|
from mjviser.conversions import merge_geoms
|
|
|
|
from mjlab.terrains.config import ALL_TERRAIN_PRESETS
|
|
from mjlab.terrains.terrain_generator import TerrainGenerator, TerrainGeneratorCfg
|
|
|
|
PATCH_SIZE = (8.0, 8.0)
|
|
|
|
|
|
# Per-preset overrides applied when building in the explorer (e.g. to surface
|
|
# difficulty-driven behavior that is off by default).
|
|
_PRESET_OVERRIDES: dict[str, dict] = {
|
|
"random_rough": {"scale_with_difficulty": True},
|
|
}
|
|
|
|
|
|
def _build_terrain_mesh(preset_name: str, difficulty: float, seed: int):
|
|
"""Generate a single terrain patch and return a merged trimesh (or raise)."""
|
|
preset_fn = ALL_TERRAIN_PRESETS[preset_name]
|
|
overrides = _PRESET_OVERRIDES.get(preset_name, {})
|
|
generator_cfg = TerrainGeneratorCfg(
|
|
seed=seed,
|
|
size=PATCH_SIZE,
|
|
num_rows=1,
|
|
num_cols=1,
|
|
border_width=0.0,
|
|
curriculum=False,
|
|
# A degenerate range pins the single patch to exactly this difficulty.
|
|
difficulty_range=(difficulty, difficulty),
|
|
color_scheme="height",
|
|
sub_terrains={preset_name: preset_fn(proportion=1.0, **overrides)},
|
|
)
|
|
generator = TerrainGenerator(generator_cfg)
|
|
spec = mujoco.MjSpec()
|
|
generator.compile(spec)
|
|
model = spec.compile()
|
|
|
|
terrain_body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "terrain")
|
|
geom_ids = [i for i in range(model.ngeom) if model.geom_bodyid[i] == terrain_body_id]
|
|
return merge_geoms(model, geom_ids)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--port", type=int, default=8080, help="Port for the viser server."
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
server = viser.ViserServer(port=args.port)
|
|
preset_names = sorted(ALL_TERRAIN_PRESETS)
|
|
|
|
terrain_dropdown = server.gui.add_dropdown(
|
|
"Terrain", options=preset_names, initial_value=preset_names[0]
|
|
)
|
|
difficulty_slider = server.gui.add_slider(
|
|
"Difficulty", min=0.0, max=1.0, step=0.01, initial_value=0.0
|
|
)
|
|
seed_input = server.gui.add_number("Seed", initial_value=42, step=1)
|
|
status = server.gui.add_markdown("**Status:** ready")
|
|
|
|
handle: viser.SceneNodeHandle | None = None
|
|
|
|
def update() -> None:
|
|
nonlocal handle
|
|
name = terrain_dropdown.value
|
|
difficulty = float(difficulty_slider.value)
|
|
seed = int(seed_input.value)
|
|
status.content = f"**Status:** building `{name}` at difficulty {difficulty:.2f}..."
|
|
try:
|
|
mesh = _build_terrain_mesh(name, difficulty, seed)
|
|
except Exception as e: # noqa: BLE001 - surface any generation failure in the UI.
|
|
status.content = f"**Error:** {type(e).__name__}: {e}"
|
|
print(f"Failed to build {name} at difficulty {difficulty}: {e}")
|
|
return
|
|
if handle is not None:
|
|
handle.remove()
|
|
handle = server.scene.add_mesh_trimesh("/terrain", mesh)
|
|
status.content = (
|
|
f"**Loaded** `{name}` at difficulty {difficulty:.2f} ({len(mesh.faces):,} faces)"
|
|
)
|
|
|
|
terrain_dropdown.on_update(lambda _: update())
|
|
difficulty_slider.on_update(lambda _: update())
|
|
seed_input.on_update(lambda _: update())
|
|
|
|
# Top-down-ish initial camera.
|
|
@server.on_client_connect
|
|
def _(client: viser.ClientHandle) -> None:
|
|
client.camera.position = np.array([10.0, 10.0, 8.0])
|
|
client.camera.look_at = np.array([0.0, 0.0, 0.0])
|
|
|
|
update()
|
|
while True:
|
|
time.sleep(1.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|