mjlab/scripts/demos/raycast_sensor.py
Upstream Snapshot 32a241c28f
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
Import upstream snapshot c19f713c415a699a79d71cd96aa13c3104a05047
Upstream: https://github.com/michaelgillett/mjlab
Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047
Upstream-Branch: main
2026-08-28 15:42:17 +08:00

253 lines
6.6 KiB
Python

"""Raycast sensor demo.
Run with:
uv run mjpython scripts/demos/raycast_sensor.py [--viewer native|viser] # macOS
uv run python scripts/demos/raycast_sensor.py [--viewer native|viser] # Linux
Examples:
# Grid pattern (default)
uv run python scripts/demos/raycast_sensor.py --pattern grid
# Pinhole camera pattern
uv run python scripts/demos/raycast_sensor.py --pattern pinhole
# With yaw alignment (ignores pitch/roll)
uv run python scripts/demos/raycast_sensor.py --alignment yaw
If using the native viewer, you can launch in interactive mode with:
uv run mjpython scripts/demos/raycast_sensor.py --viewer native --interactive
"""
from __future__ import annotations
import os
from typing import Literal
import mujoco
import numpy as np
import torch
import tyro
import mjlab
import mjlab.terrains as terrain_gen
from mjlab.entity import EntityCfg
from mjlab.envs import ManagerBasedRlEnv, ManagerBasedRlEnvCfg
from mjlab.rl import RslRlVecEnvWrapper
from mjlab.scene import SceneCfg
from mjlab.sensor import (
GridPatternCfg,
ObjRef,
PinholeCameraPatternCfg,
RayCastSensorCfg,
)
from mjlab.terrains.terrain_entity import TerrainEntityCfg
from mjlab.terrains.terrain_generator import TerrainGeneratorCfg
from mjlab.utils.torch import configure_torch_backends
from mjlab.viewer import NativeMujocoViewer, ViserPlayViewer
def create_scanner_spec() -> mujoco.MjSpec:
spec = mujoco.MjSpec()
spec.modelname = "scanner"
mat = spec.add_material()
mat.name = "scanner_mat"
mat.rgba[:] = (1.0, 0.5, 0.0, 0.9)
scanner = spec.worldbody.add_body(mocap=True)
scanner.name = "scanner"
scanner.pos[:] = (0, 0, 2.0)
geom = scanner.add_geom()
geom.name = "scanner_geom"
geom.type = mujoco.mjtGeom.mjGEOM_BOX
geom.size[:] = (0.15, 0.15, 0.05)
geom.mass = 1.0
geom.material = "scanner_mat"
scanner.add_camera(name="scanner", fovy=58.0, resolution=(16, 12))
record_cam = scanner.add_camera(name="record_cam")
record_cam.pos[:] = (2, 0, 2)
record_cam.fovy = 40.0
record_cam.mode = mujoco.mjtCamLight.mjCAMLIGHT_TARGETBODY
record_cam.targetbody = "scanner"
return spec
def create_env_cfg(
pattern: Literal["grid", "pinhole"],
alignment: Literal["base", "yaw", "world"],
) -> ManagerBasedRlEnvCfg:
custom_terrain_cfg = TerrainGeneratorCfg(
size=(4.0, 4.0),
border_width=0.5,
num_rows=1,
num_cols=4,
curriculum=True,
sub_terrains={
"pyramid_stairs_inv": terrain_gen.BoxInvertedPyramidStairsTerrainCfg(
proportion=0.25,
step_height_range=(0.1, 0.25),
step_width=0.3,
platform_width=1.5,
border_width=0.25,
),
"hf_pyramid_slope_inv": terrain_gen.HfPyramidSlopedTerrainCfg(
proportion=0.25,
slope_range=(0.6, 1.5),
platform_width=1.5,
border_width=0.25,
inverted=True,
),
"random_rough": terrain_gen.HfRandomUniformTerrainCfg(
proportion=0.25,
noise_range=(0.05, 0.15),
noise_step=0.02,
border_width=0.25,
),
"wave_terrain": terrain_gen.HfWaveTerrainCfg(
proportion=0.25,
amplitude_range=(0.15, 0.25),
num_waves=3,
border_width=0.25,
),
},
add_lights=True,
)
terrain_cfg = TerrainEntityCfg(
terrain_type="generator",
terrain_generator=custom_terrain_cfg,
num_envs=1,
)
scanner_entity_cfg = EntityCfg(
spec_fn=create_scanner_spec,
init_state=EntityCfg.InitialStateCfg(pos=(0.65, -0.4, 0.5)),
)
if pattern == "grid":
pattern_cfg = GridPatternCfg(
size=(0.6, 0.6),
resolution=0.1,
direction=(0.0, 0.0, -1.0),
)
else:
assert pattern == "pinhole"
pattern_cfg = PinholeCameraPatternCfg.from_mujoco_camera("scanner/scanner")
raycast_cfg = RayCastSensorCfg(
name="terrain_scan",
frame=ObjRef(type="body", name="scanner", entity="scanner"),
pattern=pattern_cfg,
ray_alignment=alignment,
max_distance=5.0,
exclude_parent_body=True,
debug_vis=True,
viz=RayCastSensorCfg.VizCfg(
hit_color=(0.0, 1.0, 0.0, 0.9),
miss_color=(1.0, 0.0, 0.0, 0.5),
show_rays=False,
show_normals=True,
),
)
cfg = ManagerBasedRlEnvCfg(
decimation=10,
scene=SceneCfg(
num_envs=1,
env_spacing=0.0,
extent=2.0,
terrain=terrain_cfg,
entities={"scanner": scanner_entity_cfg},
sensors=(raycast_cfg,),
),
)
cfg.viewer.body_name = "scanner"
cfg.viewer.distance = 12.0
cfg.viewer.elevation = -25.0
cfg.viewer.azimuth = 135.0
return cfg
def main(
viewer: str = "auto",
interactive: bool = False,
pattern: Literal["grid", "pinhole"] = "grid",
alignment: Literal["base", "yaw", "world"] = "base",
) -> None:
configure_torch_backends()
device = "cuda:0" if torch.cuda.is_available() else "cpu"
print("=" * 60)
print("Raycast Sensor Demo - 4 Terrain Types")
print(f" Pattern: {pattern}")
print(f" Alignment: {alignment}")
print("=" * 60)
print()
env_cfg = create_env_cfg(pattern, alignment)
env = ManagerBasedRlEnv(cfg=env_cfg, device=device)
env = RslRlVecEnvWrapper(env)
if viewer == "auto":
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
resolved_viewer = "native" if has_display else "viser"
else:
resolved_viewer = viewer
use_auto_scan = (resolved_viewer == "viser") or (not interactive)
if use_auto_scan:
class AutoScanPolicy:
def __init__(self):
self.step_count = 0
def __call__(self, obs) -> torch.Tensor:
del obs
t = self.step_count * 0.005
y_period = 1000
y_normalized = (self.step_count % y_period) / y_period
y = -8.0 + 16.0 * y_normalized
x = 1.5 * np.sin(2 * np.pi * t * 0.3)
z = 1.0
env.unwrapped.sim.data.mocap_pos[0, 0, :] = torch.tensor(
[x, y, z], device=device, dtype=torch.float32
)
env.unwrapped.sim.data.mocap_quat[0, 0, :] = torch.tensor(
[1, 0, 0, 0], device=device, dtype=torch.float32
)
self.step_count += 1
return torch.zeros(env.unwrapped.action_space.shape, device=device)
policy = AutoScanPolicy()
else:
class PolicyZero:
def __call__(self, obs) -> torch.Tensor:
del obs
return torch.zeros(env.unwrapped.action_space.shape, device=device)
policy = PolicyZero()
if resolved_viewer == "native":
print("Launching native viewer...")
NativeMujocoViewer(env, policy).run()
elif resolved_viewer == "viser":
print("Launching viser viewer...")
ViserPlayViewer(env, policy).run()
else:
raise ValueError(f"Unknown viewer: {viewer}")
env.close()
if __name__ == "__main__":
tyro.cli(main, config=mjlab.TYRO_FLAGS)