Skip to content

Commit e8d0cec

Browse files
authored
Merge pull request #29 from KraHsu/feat/showcase-and-foundation-docs
M1-M4 follow-up: viewer/batch_render kernel + showcase + docs
2 parents 1042dc5 + 355c645 commit e8d0cec

44 files changed

Lines changed: 2419 additions & 51 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,35 @@ trajectory so breaking changes can land in any minor release until the 1.0 stabi
88

99
### Added
1010

11+
- `examples/genelab_showcase/`: six play-only TaskCfgs that exercise every M1–M4
12+
building block (sensors / ray-cast patterns / contact + air-time / 5 sub-terrains /
13+
`terrain_levels_vel` curriculum / `IdealPDActuator` arm). Each task dumps
14+
per-feature evidence (PNG frames, distance histograms, tracking error, terrain
15+
level histograms) into `logs/showcase/<slug>/` so the visual + numerical behaviour
16+
of every building block can be eyeballed without launching an RL training run.
17+
- `InteractiveScene.viewer_closed` / `ManagerBasedRlEnv.viewer_closed`: viewer-closed
18+
handling moved into the kernel. `InteractiveScene.step` catches Genesis's
19+
`GenesisException("Viewer closed.")`, flips the flag, and becomes a no-op on
20+
subsequent calls. Consumers (RL runner, showcase runner, custom rollouts) only
21+
need to poll `env.viewer_closed` and break; the previous per-consumer
22+
`try/except gs.GenesisException` boilerplate is no longer required.
23+
- `InteractiveSceneCfg.batch_render: bool`: when set, `InteractiveScene._build`
24+
passes `gs.renderers.BatchRenderer(use_rasterizer=False)` to `gs.Scene`. Required
25+
for `CameraSensor` to produce per-env RGB-D tensors. Defaults to `False` for
26+
backwards compatibility.
27+
- `Sensor.pre_build_genesis(gs_scene, entities)`: new no-op hook on the sensor base
28+
class. `InteractiveScene._build` calls it on every sensor cfg right after entity
29+
spawn and before `gs_scene.build`, giving sensors a chance to register Genesis
30+
resources that the renderer snapshots at build time. `CameraSensor` overrides it
31+
to allocate its camera early — required for BatchRender to register the camera in
32+
its build-time snapshot. `InteractiveScene.sensors` now exposes the resulting
33+
instances, and `ManagerBasedRlEnv` binds those instances instead of re-building.
34+
- `docs/concepts/scene.{en,zh}.md`: bilingual concept page covering the M1 stack —
35+
`InteractiveScene` lifecycle, `SimulationCfg`, `InteractiveSceneCfg`, `Articulation`,
36+
`RigidObject`, and the four documented failure modes.
37+
- `docs/examples/showcase.{en,zh}.md`, `docs/examples/unitree-g1.{en,zh}.md`:
38+
bilingual examples pages — the showcase index plus a distilled Unitree G1
39+
walk-through (velocity tracking, motion imitation, long-run profiling).
1140
- `CameraSensor` (with `CameraSensorCfg` and `CameraData`): rigid-mount RGB-D probe
1241
wrapping Genesis's `BatchRenderer` camera. Per-env tensors with shape
1342
`(num_envs, H, W, 3)` for RGB (uint8) and `(num_envs, H, W)` for depth (float meters);

‎docs/concepts/scene.en.md‎

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Scene and entities
2+
3+
`genelab.scene.InteractiveScene` is the composition root that owns a Genesis
4+
`gs.Scene` together with the articulated robot, rigid props, optional terrain, and
5+
the mouse-interaction plugin. `ManagerBasedRlEnv` defers all sim-level orchestration
6+
to the scene; the env layer only manages the seven MDP terms.
7+
8+
## Composing a Genesis scene
9+
10+
A scene is built from two cfg objects passed to the env. `SimulationCfg` carries the
11+
Genesis runtime knobs (timestep, parallel-env count, viewer toggle); `InteractiveSceneCfg`
12+
carries the composition (entities, terrain, sensors, mouse plugin, BatchRenderer
13+
toggle). The scene parses the cfg, allocates wrapper objects, then defers the actual
14+
Genesis allocation to the `build()` call.
15+
16+
```python
17+
from genelab.asset_zoo import CartpoleCfg
18+
from genelab.configs import InteractiveSceneCfg, SimulationCfg
19+
from genelab.envs.manager_based_rl_env import ManagerBasedRlEnvCfg
20+
21+
env_cfg = ManagerBasedRlEnvCfg(
22+
simulation=SimulationCfg(num_envs=4096, dt=0.005, substeps=1, vis=False),
23+
scene=InteractiveSceneCfg(env_spacing=(2.5, 2.5)),
24+
robot=CartpoleCfg(),
25+
)
26+
```
27+
28+
## Lifecycle
29+
30+
`InteractiveScene` follows a strict three-step lifecycle, enforced at runtime:
31+
32+
1. **Construct**: `InteractiveScene(sim_cfg, scene_cfg, device_hint=...)` parses the
33+
cfgs and allocates wrapper objects for every entity declared in
34+
`scene_cfg.entities`. No Genesis import yet.
35+
2. **Add entities**: `add_entity(name, cfg)` registers extra entities — the env uses
36+
this to inject `ManagerBasedRlEnvCfg.robot` under the name `"robot"`. Calling
37+
`add_entity` after `build()` raises `RuntimeError`.
38+
3. **Build**: `build()` imports Genesis, initialises the backend, creates the
39+
`gs.Scene`, spawns ground / terrain / entities / cameras, attaches the mouse
40+
plugin when configured, then calls `gs_scene.build(n_envs, env_spacing)`.
41+
42+
`ManagerBasedRlEnv.__init__` runs all three steps in order and post-build binds every
43+
articulation so the per-joint and per-link tensors land.
44+
45+
## SimulationCfg
46+
47+
| Field | Default | Meaning |
48+
|---|---|---|
49+
| `vis` | `False` | Open the Genesis viewer. Forces `num_envs=1` in practice; the viewer can only render env 0. |
50+
| `gpu` | `False` | Use the CUDA backend (`gs.gpu`). Required for `BatchRenderer`. |
51+
| `steps` | `240` | Default number of steps the CLI `play` runner takes when no override is given. |
52+
| `dt` | `0.01` | Physics timestep in seconds. |
53+
| `substeps` | `4` | Genesis substeps per physics step. |
54+
| `num_envs` | `1` | Parallel-env count. Most managers tensorise across this dimension. |
55+
56+
## InteractiveSceneCfg
57+
58+
| Field | Default | Meaning |
59+
|---|---|---|
60+
| `env_spacing` | `(2.0, 2.0)` | Per-env XY offset Genesis applies when `num_envs > 1`. |
61+
| `sensors` | `()` | Tuple of `SensorCfg` instances built and bound after the scene is constructed. |
62+
| `mouse_interaction` | `False` | Attach the GeneLab mouse-drag plugin to the viewer (only effective with `vis=True`). |
63+
| `entities` | `{}` | Additional `ArticulationCfg` / `RigidObjectCfg` keyed by name. The env injects the robot as `"robot"`. |
64+
| `terrain` | `None` | A `TerrainGeneratorCfg`. Default `None` adds a flat `gs.morphs.Plane`. |
65+
| `batch_render` | `False` | Pass `gs.renderers.BatchRenderer(use_rasterizer=False)` to `gs.Scene`. Required for `CameraSensor` to emit per-env RGB-D tensors. Linux x86-64 + CUDA only. |
66+
67+
## Articulation
68+
69+
`Articulation` wraps an articulated robot. Construct from `ArticulationCfg` and add
70+
to the scene via `scene.add_entity("robot", cfg)` (or set
71+
`ManagerBasedRlEnvCfg.robot` and let the env wire it).
72+
73+
| Field | Type | Meaning |
74+
|---|---|---|
75+
| `mjcf_path` | `str` | Absolute path to a MuJoCo XML file. |
76+
| `init_pos` | `tuple[float, float, float]` | World-frame spawn offset in metres. |
77+
| `init_quat` | `tuple[float, float, float, float]` | World-frame spawn orientation (wxyz). |
78+
| `default_joint_pos` | `dict[str, float]` | Regex-keyed default joint position; last match wins. |
79+
| `actuators` | `dict[str, ActuatorBaseCfg]` | Joint groups. Every actuated joint must be covered by exactly one group. |
80+
| `foot_link_names` | `tuple[str, ...]` | Optional metadata for downstream MDP terms (e.g. contact sensors). |
81+
82+
The pre-M3 `joint_kp`, `joint_kv`, and dict-shaped `action_scale` knobs are
83+
removed — configure equivalent groups through `actuators`. Passive joints take an
84+
explicit zero-gain `ImplicitPDActuatorCfg` so the topology stays visible.
85+
86+
```python
87+
from genelab.actuator import ImplicitPDActuatorCfg
88+
from genelab.entity import ArticulationCfg
89+
90+
cartpole_cfg = ArticulationCfg(
91+
mjcf_path="/path/to/cartpole.xml",
92+
init_pos=(0.0, 0.0, 1.5),
93+
default_joint_pos={"cart_slide": 0.0, "pole_hinge": 0.0},
94+
actuators={
95+
"cart": ImplicitPDActuatorCfg(
96+
target_names_expr=("cart_slide",), stiffness=80.0, damping=8.0,
97+
),
98+
"pole": ImplicitPDActuatorCfg(
99+
target_names_expr=("pole_hinge",), stiffness=0.0, damping=0.0,
100+
),
101+
},
102+
)
103+
```
104+
105+
## RigidObject
106+
107+
`RigidObject` represents a non-articulated body. No actuators, no per-step state — it
108+
is geometry that participates in collisions and contacts but is never driven. Spawn
109+
via `scene.add_entity("name", RigidObjectCfg(...))` before `build()`.
110+
111+
| Field | Type | Meaning |
112+
|---|---|---|
113+
| `morph` | `Literal["plane", "box", "sphere", "mesh", "mjcf"]` | Genesis morph selector. |
114+
| `file` | `str \| None` | Path to mesh / MJCF; required for `"mesh"` / `"mjcf"`. |
115+
| `size` | `tuple[float, ...]` | `(x, y, z)` for box, `(radius,)` for sphere. |
116+
| `init_pos`, `init_quat` | tuple | World-frame spawn pose (wxyz quat). |
117+
| `fixed` | `bool` | When `True`, the body is welded to the world frame. |
118+
119+
Typical uses are static obstacles, target markers, and passive payloads. For
120+
articulated joints — even one — use `Articulation` instead.
121+
122+
## Failure modes worth knowing
123+
124+
!!! warning "`add_entity` after `build`"
125+
`InteractiveScene.add_entity` raises `RuntimeError` once `build()` has run.
126+
Construct the env, then add every extra entity, then let the env call `build()`.
127+
128+
!!! warning "Empty `actuators` dict"
129+
`Articulation.bind` rejects an empty `actuators` dict. Every actuated joint must
130+
be covered by exactly one group; unmatched or doubly-matched joints raise
131+
`ValueError` with the conflicting joint names listed.
132+
133+
!!! warning "Genesis backend mismatch"
134+
`SimulationCfg.gpu=True` requires a working CUDA install. On macOS or CPU-only
135+
Linux, leave `gpu=False`; the CPU backend handles single-env play loops fine.
136+
`batch_render=True` further requires Linux x86-64 + CUDA + Madrona — there is no
137+
CPU fallback.
138+
139+
!!! note "Viewer and parallel envs"
140+
`SimulationCfg.vis=True` is only meaningful with `num_envs=1`. The viewer
141+
renders env 0 regardless, so leave `num_envs` high for training and switch to a
142+
dedicated `play_env` with `num_envs=1` for visual inspection.
143+
144+
!!! tip "Viewer closed mid-rollout"
145+
When the user closes the Genesis viewer, the kernel catches
146+
`GenesisException("Viewer closed.")` inside `InteractiveScene.step` and flips
147+
`env.viewer_closed = True`. Subsequent `env.step` calls become no-ops. Loop
148+
drivers should poll the flag instead of writing their own try / except:
149+
```python
150+
for step in range(max_steps):
151+
obs, *_ = env.step(action)
152+
if env.viewer_closed:
153+
break
154+
```
155+
156+
## See also
157+
158+
- [Configs](configs.md)
159+
- [Actuators](actuators.md)
160+
- [Asset zoo](asset_zoo.md)

‎docs/concepts/scene.zh.md‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# 场景与实体
2+
3+
`genelab.scene.InteractiveScene` 是组合根 (composition root),持有一个 Genesis `gs.Scene`
4+
以及关节机器人、刚体道具、可选地形、鼠标交互插件。`ManagerBasedRlEnv` 把所有 sim 层
5+
编排都交给 scene;env 层只管 7 个 MDP term。
6+
7+
## 组合 Genesis 场景
8+
9+
scene 从两个传给 env 的 cfg 对象构造。`SimulationCfg` 携带 Genesis 运行时旋钮 (时间步、
10+
并行 env 数、viewer 开关);`InteractiveSceneCfg` 携带组合内容 (entities、terrain、sensors、
11+
mouse 插件、BatchRenderer 开关)。scene 解析 cfg、分配 wrapper 对象,真正的 Genesis 分配
12+
延后到 `build()` 调用。
13+
14+
```python
15+
from genelab.asset_zoo import CartpoleCfg
16+
from genelab.configs import InteractiveSceneCfg, SimulationCfg
17+
from genelab.envs.manager_based_rl_env import ManagerBasedRlEnvCfg
18+
19+
env_cfg = ManagerBasedRlEnvCfg(
20+
simulation=SimulationCfg(num_envs=4096, dt=0.005, substeps=1, vis=False),
21+
scene=InteractiveSceneCfg(env_spacing=(2.5, 2.5)),
22+
robot=CartpoleCfg(),
23+
)
24+
```
25+
26+
## 生命周期
27+
28+
`InteractiveScene` 遵循严格的三步生命周期,运行时强校验:
29+
30+
1. **构造**:`InteractiveScene(sim_cfg, scene_cfg, device_hint=...)` 解析 cfg 并为
31+
`scene_cfg.entities` 里每个实体分配 wrapper。此刻还没 import Genesis。
32+
2. **追加实体**:`add_entity(name, cfg)` 注册额外实体——env 用它把
33+
`ManagerBasedRlEnvCfg.robot` 以 `"robot"` 名注入。`build()` 之后再调
34+
`add_entity` 会抛 `RuntimeError`。
35+
3. **构建**:`build()` 导入 Genesis、初始化 backend、创建 `gs.Scene`、生成地面 / 地形 /
36+
实体 / 相机,按需挂上鼠标插件,最后 `gs_scene.build(n_envs, env_spacing)`。
37+
38+
`ManagerBasedRlEnv.__init__` 顺序跑完三步,build 之后再 bind 每个 articulation,让
39+
per-joint / per-link tensor 落位。
40+
41+
## SimulationCfg
42+
43+
| 字段 | 默认 | 含义 |
44+
|---|---|---|
45+
| `vis` | `False` | 打开 Genesis viewer。实际只配合 `num_envs=1`;viewer 只能渲染 env 0。 |
46+
| `gpu` | `False` | 使用 CUDA backend (`gs.gpu`)。`BatchRenderer` 需要它。 |
47+
| `steps` | `240` | CLI `play` 在没指定 override 时默认跑的步数。 |
48+
| `dt` | `0.01` | 物理时间步 (秒)。 |
49+
| `substeps` | `4` | 每个物理步内的 Genesis 子步数。 |
50+
| `num_envs` | `1` | 并行 env 数。大多数 manager 在这个维度上向量化。 |
51+
52+
## InteractiveSceneCfg
53+
54+
| 字段 | 默认 | 含义 |
55+
|---|---|---|
56+
| `env_spacing` | `(2.0, 2.0)` | `num_envs > 1` 时 Genesis 在 XY 上为每个 env 留的间距。 |
57+
| `sensors` | `()` | scene 构造完后批量 build + bind 的 `SensorCfg` 元组。 |
58+
| `mouse_interaction` | `False` | 给 viewer 挂上 GeneLab 的鼠标拖拽插件 (仅在 `vis=True` 时有效)。 |
59+
| `entities` | `{}` | 按名字索引的额外 `ArticulationCfg` / `RigidObjectCfg`。env 会以 `"robot"` 名注入主机器人。 |
60+
| `terrain` | `None` | `TerrainGeneratorCfg`;默认 `None` 时铺一张 `gs.morphs.Plane` 平面。 |
61+
| `batch_render` | `False` | 把 `gs.renderers.BatchRenderer(use_rasterizer=False)` 传给 `gs.Scene`。`CameraSensor` 输出 per-env RGB-D 张量必需。仅支持 Linux x86-64 + CUDA。 |
62+
63+
## Articulation
64+
65+
`Articulation` 包装一个关节机器人。从 `ArticulationCfg` 构造,再通过
66+
`scene.add_entity("robot", cfg)` 加入 scene (或者直接设 `ManagerBasedRlEnvCfg.robot`,让
67+
env 自动接管)。
68+
69+
| 字段 | 类型 | 含义 |
70+
|---|---|---|
71+
| `mjcf_path` | `str` | MuJoCo XML 文件的绝对路径。 |
72+
| `init_pos` | `tuple[float, float, float]` | 世界系初始位移 (米)。 |
73+
| `init_quat` | `tuple[float, float, float, float]` | 世界系初始姿态 (wxyz 四元数)。 |
74+
| `default_joint_pos` | `dict[str, float]` | 正则键控的默认关节位置;按 last-match-wins。 |
75+
| `actuators` | `dict[str, ActuatorBaseCfg]` | 关节分组。每个被驱动关节必须恰被一个分组覆盖。 |
76+
| `foot_link_names` | `tuple[str, ...]` | 给下游 MDP term (例如 contact sensor) 的可选元数据。 |
77+
78+
M3 之前的 `joint_kp` / `joint_kv` / dict 形 `action_scale` 旋钮已经移除——同等行为请
79+
通过 `actuators` 配。被动关节用一个零增益的 `ImplicitPDActuatorCfg` 显式覆盖,保持
80+
拓扑可见。
81+
82+
```python
83+
from genelab.actuator import ImplicitPDActuatorCfg
84+
from genelab.entity import ArticulationCfg
85+
86+
cartpole_cfg = ArticulationCfg(
87+
mjcf_path="/path/to/cartpole.xml",
88+
init_pos=(0.0, 0.0, 1.5),
89+
default_joint_pos={"cart_slide": 0.0, "pole_hinge": 0.0},
90+
actuators={
91+
"cart": ImplicitPDActuatorCfg(
92+
target_names_expr=("cart_slide",), stiffness=80.0, damping=8.0,
93+
),
94+
"pole": ImplicitPDActuatorCfg(
95+
target_names_expr=("pole_hinge",), stiffness=0.0, damping=0.0,
96+
),
97+
},
98+
)
99+
```
100+
101+
## RigidObject
102+
103+
`RigidObject` 表示一个非关节刚体。无 actuator、无 per-step 状态——它就是几何,参与碰撞
104+
与接触,但永远不被驱动。`build()` 之前通过 `scene.add_entity("name", RigidObjectCfg(...))`
105+
加入。
106+
107+
| 字段 | 类型 | 含义 |
108+
|---|---|---|
109+
| `morph` | `Literal["plane", "box", "sphere", "mesh", "mjcf"]` | Genesis morph 选择。 |
110+
| `file` | `str \| None` | mesh / MJCF 路径;`"mesh"` / `"mjcf"` 必填。 |
111+
| `size` | `tuple[float, ...]` | box 是 `(x, y, z)`;sphere 是 `(radius,)`。 |
112+
| `init_pos`、`init_quat` | tuple | 世界系初始位姿 (wxyz quat)。 |
113+
| `fixed` | `bool` | `True` 时把刚体焊死在世界系。 |
114+
115+
典型用法是静态障碍、目标 marker、被动负载。只要有一个关节就用 `Articulation`,不要用
116+
`RigidObject`。
117+
118+
## 已知失效模式
119+
120+
!!! warning "`build` 之后追加实体"
121+
`build()` 之后再调 `InteractiveScene.add_entity` 会抛 `RuntimeError`。先构造 env,把
122+
所有额外实体加完,再让 env 调用 `build()`。
123+
124+
!!! warning "空 `actuators` 字典"
125+
`Articulation.bind` 拒绝空 `actuators`。每个被驱动关节必须被恰一个分组覆盖;未匹配
126+
或被双重匹配的关节会带着冲突关节名抛 `ValueError`。
127+
128+
!!! warning "Genesis backend 不匹配"
129+
`SimulationCfg.gpu=True` 需要可用的 CUDA 安装。macOS / 纯 CPU Linux 上保持
130+
`gpu=False`;CPU backend 跑单 env play 循环没问题。`batch_render=True` 额外要求
131+
Linux x86-64 + CUDA + Madrona——没有 CPU 回退。
132+
133+
!!! note "Viewer 与并行 env"
134+
`SimulationCfg.vis=True` 仅在 `num_envs=1` 时有意义。viewer 只渲染 env 0,所以训练
135+
时把 `num_envs` 设高,再单独配一份 `num_envs=1` 的 `play_env` 用于人眼检查。
136+
137+
!!! tip "Rollout 中途关闭 viewer"
138+
用户关掉 Genesis viewer 后,内核会在 `InteractiveScene.step` 里捕获
139+
`GenesisException("Viewer closed.")` 并把 `env.viewer_closed` 置 `True`,后续
140+
`env.step` 自动 no-op。上层循环只需轮询这个 flag,不必自己写 try / except:
141+
```python
142+
for step in range(max_steps):
143+
obs, *_ = env.step(action)
144+
if env.viewer_closed:
145+
break
146+
```
147+
148+
## See also
149+
150+
- [Configs](configs.md)
151+
- [Actuators](actuators.md)
152+
- [Asset zoo](asset_zoo.md)

‎docs/concepts/sensors.en.md‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,15 @@ finite-difference at the control rate is **not** filtered internally; layer
9797

9898
### CameraSensor
9999

100-
Rigid-mount RGB-D camera bolted to a named link via a fixed 4×4 transform. `bind()`
101-
resolves the link, allocates one Genesis camera handle through `scene.gs_scene.add_camera`,
102-
builds the offset matrix from `offset_pos` / `offset_quat`, and calls `cam.attach`.
103-
Each `data` access invokes `cam.move_to_attach()` then `cam.render(...)` once per
104-
control step; the cached :class:`CameraData` returns `rgb` (uint8) and / or `depth`
105-
(float meters), shaped `(num_envs, H, W, 3)` / `(num_envs, H, W)`.
100+
Rigid-mount RGB-D camera bolted to a named link via a fixed 4×4 transform.
101+
`pre_build_genesis()` runs before `gs_scene.build` so the camera is registered in
102+
time for `BatchRenderer` to snapshot it; it allocates the Genesis camera handle
103+
through `gs_scene.add_camera`, builds the offset matrix from `offset_pos` /
104+
`offset_quat`, and calls `cam.attach`. `bind()` then validates the link name
105+
against `env.link_names`. Each `data` access invokes `cam.move_to_attach()` and
106+
`cam.render(...)` once per control step; the cached `CameraData` returns `rgb`
107+
(uint8) and / or `depth` (float meters), shaped `(num_envs, H, W, 3)` /
108+
`(num_envs, H, W)`.
106109

107110
| Field | Type | Meaning |
108111
|-------|------|---------|
@@ -115,10 +118,11 @@ control step; the cached :class:`CameraData` returns `rgb` (uint8) and / or `dep
115118
| `render_rgb`, `render_depth` | `bool` | Independently toggle each channel; the disabled channel is `None` in the output dataclass. |
116119

117120
!!! warning "BatchRenderer is the only parallel-env backend"
118-
Multi-env RGB-D requires `gs.init(backend=gs.cuda)` plus
119-
`gs.Scene(renderer=gs.renderers.BatchRenderer(use_rasterizer=False), ...)` —
120-
Linux x86-64 + CUDA only. The module file can still be imported on macOS / CPU,
121-
but `bind()` will raise as soon as Genesis allocates the camera against an
121+
Multi-env RGB-D requires `gs.init(backend=gs.cuda)` and
122+
`InteractiveSceneCfg.batch_render=True` — the scene then passes
123+
`gs.renderers.BatchRenderer(use_rasterizer=False)` to `gs.Scene`. Linux x86-64 +
124+
CUDA only. The module file can still be imported on macOS / CPU, but
125+
`gs_scene.build` will raise once it inspects the camera list against an
122126
incompatible renderer.
123127

124128
### ContactSensor

0 commit comments

Comments
 (0)