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
306 lines
7.4 KiB
Python
306 lines
7.4 KiB
Python
"""Render images of each sub-terrain type for documentation.
|
|
|
|
Generates one PNG per terrain type at a fixed difficulty level.
|
|
Images are saved to docs/source/_static/terrains/.
|
|
|
|
Run with:
|
|
uv run python scripts/tools/render_terrain_gallery.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import mujoco
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
import mjlab.terrains as terrain_gen
|
|
from mjlab.terrains import TerrainEntity, TerrainEntityCfg
|
|
from mjlab.terrains.terrain_generator import TerrainGeneratorCfg
|
|
from mjlab.utils import spec_config as spec_cfg
|
|
|
|
OUTPUT_DIR = Path("docs/source/_static/terrains")
|
|
WIDTH = 1080
|
|
HEIGHT = 1080
|
|
DIFFICULTY = 0.85
|
|
PATCH_SIZE = (8.0, 8.0)
|
|
|
|
|
|
# Each entry: (filename, SubTerrainCfg instance).
|
|
TERRAIN_TYPES: list[tuple[str, terrain_gen.SubTerrainCfg]] = [
|
|
# Primitive terrains.
|
|
(
|
|
"box_flat",
|
|
terrain_gen.BoxFlatTerrainCfg(proportion=1.0),
|
|
),
|
|
(
|
|
"box_pyramid_stairs",
|
|
terrain_gen.BoxPyramidStairsTerrainCfg(
|
|
proportion=1.0,
|
|
step_height_range=(0.0, 0.2),
|
|
step_width=0.3,
|
|
platform_width=3.0,
|
|
border_width=1.0,
|
|
),
|
|
),
|
|
(
|
|
"box_inverted_pyramid_stairs",
|
|
terrain_gen.BoxInvertedPyramidStairsTerrainCfg(
|
|
proportion=1.0,
|
|
step_height_range=(0.0, 0.2),
|
|
step_width=0.3,
|
|
platform_width=3.0,
|
|
border_width=1.0,
|
|
),
|
|
),
|
|
(
|
|
"box_random_stairs",
|
|
terrain_gen.BoxRandomStairsTerrainCfg(
|
|
proportion=1.0,
|
|
step_width=0.8,
|
|
step_height_range=(0.1, 0.3),
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"box_open_stairs",
|
|
terrain_gen.BoxOpenStairsTerrainCfg(
|
|
proportion=1.0,
|
|
step_height_range=(0.1, 0.2),
|
|
step_width_range=(0.4, 0.8),
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"box_random_grid",
|
|
terrain_gen.BoxRandomGridTerrainCfg(
|
|
proportion=1.0,
|
|
grid_width=0.4,
|
|
grid_height_range=(0.0, 0.3),
|
|
platform_width=1.0,
|
|
),
|
|
),
|
|
(
|
|
"box_random_spread",
|
|
terrain_gen.BoxRandomSpreadTerrainCfg(
|
|
proportion=1.0,
|
|
num_boxes=80,
|
|
box_width_range=(0.1, 1.0),
|
|
box_length_range=(0.1, 2.0),
|
|
box_height_range=(0.05, 0.3),
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"box_stepping_stones",
|
|
terrain_gen.BoxSteppingStonesTerrainCfg(
|
|
proportion=1.0,
|
|
stone_size_range=(0.4, 0.8),
|
|
stone_distance_range=(0.2, 0.5),
|
|
stone_height=0.2,
|
|
stone_height_variation=0.1,
|
|
stone_size_variation=0.2,
|
|
displacement_range=0.1,
|
|
floor_depth=2.0,
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"box_narrow_beams",
|
|
terrain_gen.BoxNarrowBeamsTerrainCfg(
|
|
proportion=1.0,
|
|
num_beams=12,
|
|
beam_width_range=(0.2, 0.8),
|
|
beam_height=0.2,
|
|
spacing=0.8,
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
floor_depth=2.0,
|
|
),
|
|
),
|
|
(
|
|
"box_tilted_grid",
|
|
terrain_gen.BoxTiltedGridTerrainCfg(
|
|
proportion=1.0,
|
|
grid_width=1.0,
|
|
tilt_range_deg=20.0,
|
|
height_range=0.3,
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
floor_depth=2.0,
|
|
),
|
|
),
|
|
(
|
|
"box_nested_rings",
|
|
terrain_gen.BoxNestedRingsTerrainCfg(
|
|
proportion=1.0,
|
|
num_rings=8,
|
|
ring_width_range=(0.3, 0.6),
|
|
gap_range=(0.1, 0.4),
|
|
height_range=(0.1, 0.4),
|
|
platform_width=1.0,
|
|
border_width=0.25,
|
|
floor_depth=2.0,
|
|
),
|
|
),
|
|
# Heightfield terrains.
|
|
(
|
|
"hf_pyramid_slope",
|
|
terrain_gen.HfPyramidSlopedTerrainCfg(
|
|
proportion=1.0,
|
|
slope_range=(0.0, 0.7),
|
|
platform_width=2.0,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"hf_random_uniform",
|
|
terrain_gen.HfRandomUniformTerrainCfg(
|
|
proportion=1.0,
|
|
noise_range=(0.02, 0.10),
|
|
noise_step=0.02,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"hf_wave",
|
|
terrain_gen.HfWaveTerrainCfg(
|
|
proportion=1.0,
|
|
amplitude_range=(0.1, 0.5),
|
|
num_waves=6,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"hf_discrete_obstacles",
|
|
terrain_gen.HfDiscreteObstaclesTerrainCfg(
|
|
proportion=1.0,
|
|
obstacle_width_range=(0.3, 1.0),
|
|
obstacle_height_range=(0.05, 0.3),
|
|
num_obstacles=40,
|
|
border_width=0.25,
|
|
),
|
|
),
|
|
(
|
|
"hf_perlin_noise",
|
|
terrain_gen.HfPerlinNoiseTerrainCfg(
|
|
proportion=1.0,
|
|
height_range=(0.0, 1.0),
|
|
octaves=4,
|
|
persistence=0.3,
|
|
lacunarity=2.0,
|
|
scale=10.0,
|
|
horizontal_scale=0.1,
|
|
border_width=0.50,
|
|
),
|
|
),
|
|
]
|
|
|
|
|
|
CAMERA_DISTANCE = 8.0
|
|
CAMERA_ELEVATION_DEG = 50.0 # Degrees from horizontal (90 = top-down).
|
|
CAMERA_AZIMUTH_DEG = 135.0 # Rotation around the vertical axis.
|
|
FOV_PADDING = 1.1
|
|
|
|
|
|
def render_terrain(
|
|
name: str,
|
|
sub_terrain_cfg: terrain_gen.SubTerrainCfg,
|
|
) -> np.ndarray:
|
|
"""Generate and render a single terrain type, return RGB array."""
|
|
terrain_cfg = TerrainEntityCfg(
|
|
terrain_type="generator",
|
|
terrain_generator=TerrainGeneratorCfg(
|
|
seed=42,
|
|
size=PATCH_SIZE,
|
|
num_rows=1,
|
|
num_cols=1,
|
|
border_width=0.0,
|
|
curriculum=False,
|
|
difficulty_range=(DIFFICULTY, DIFFICULTY),
|
|
color_scheme="height",
|
|
sub_terrains={name: sub_terrain_cfg},
|
|
),
|
|
lights=(
|
|
spec_cfg.LightCfg(
|
|
name="sun",
|
|
type="directional",
|
|
dir=(-0.15, -0.15, -0.97),
|
|
castshadow=True,
|
|
),
|
|
),
|
|
)
|
|
terrain = TerrainEntity(terrain_cfg, device="cpu")
|
|
|
|
# Place camera at an isometric-ish angle to reveal 3D structure.
|
|
elev = np.deg2rad(CAMERA_ELEVATION_DEG)
|
|
azim = np.deg2rad(CAMERA_AZIMUTH_DEG)
|
|
cam_pos = CAMERA_DISTANCE * np.array(
|
|
[
|
|
np.cos(elev) * np.cos(azim),
|
|
np.cos(elev) * np.sin(azim),
|
|
np.sin(elev),
|
|
]
|
|
)
|
|
|
|
# Look at a point slightly below the origin so the terrain centers
|
|
# in the frame instead of sitting in the bottom half.
|
|
lookat = np.array([0.0, 0.0, -2.0])
|
|
forward = lookat - cam_pos
|
|
forward /= np.linalg.norm(forward)
|
|
# MuJoCo camera convention: -z is forward, y is up.
|
|
up = np.array([0.0, 0.0, 1.0])
|
|
right = np.cross(forward, up)
|
|
right /= np.linalg.norm(right)
|
|
cam_up = np.cross(right, forward)
|
|
|
|
# Build rotation matrix (columns: right, cam_up, -forward).
|
|
rot = np.column_stack([right, cam_up, -forward])
|
|
# Convert to quaternion (wxyz).
|
|
from scipy.spatial.transform import Rotation
|
|
|
|
quat_xyzw = Rotation.from_matrix(rot).as_quat()
|
|
quat_wxyz = [quat_xyzw[3], quat_xyzw[0], quat_xyzw[1], quat_xyzw[2]]
|
|
|
|
# FOV sized to fit the patch diagonal from this viewing angle.
|
|
apparent_size = max(PATCH_SIZE) * FOV_PADDING
|
|
fovy_rad = 2 * np.arctan2(apparent_size / 2, CAMERA_DISTANCE)
|
|
|
|
terrain.spec.worldbody.add_camera(
|
|
name="gallery",
|
|
pos=cam_pos.tolist(),
|
|
quat=quat_wxyz,
|
|
fovy=np.rad2deg(fovy_rad),
|
|
)
|
|
|
|
model = terrain.spec.compile()
|
|
model.vis.global_.offheight = HEIGHT
|
|
model.vis.global_.offwidth = WIDTH
|
|
data = mujoco.MjData(model)
|
|
mujoco.mj_forward(model, data)
|
|
|
|
with mujoco.Renderer(model, height=HEIGHT, width=WIDTH) as renderer:
|
|
renderer.update_scene(data, camera="gallery")
|
|
return renderer.render()
|
|
|
|
|
|
def main() -> None:
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
for filename, sub_cfg in TERRAIN_TYPES:
|
|
print(f"Rendering {filename}...")
|
|
img = render_terrain(filename, sub_cfg)
|
|
Image.fromarray(img).save(OUTPUT_DIR / f"{filename}.png")
|
|
print(f" Saved {OUTPUT_DIR / filename}.png")
|
|
|
|
print(f"\nDone. {len(TERRAIN_TYPES)} images saved to {OUTPUT_DIR}/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|