Multi-Environment SimulationΒΆ
Create the world with the desired environment count, register the same object id in each environment, and retrieve a batched view.
world = ke.sim.KangSimWorld(num_envs=16, sim_dt=1.0 / 120.0)
for env_id in range(16):
world.add_rigid(
box_data,
env_id=env_id,
obj_id=0,
name=f"box_{env_id}",
pos=positions[env_id],
)
boxes = world.get_rigid_batch(obj_id=0)
State and reset values are batched along the first dimension:
positions = torch.zeros((16, 3), dtype=torch.float32)
rotations = torch.zeros((16, 4), dtype=torch.float32)
rotations[:, 3] = 1.0
boxes.set_root_state(None, positions, rotations)
root_pos = boxes.get_root_pos() # shape: [16, 3]
Run:
python ./python/examples/sim_world_multi_env.py --num-envs 16
Complete source: sim_world_multi_env.py
1"""Batched KangSimWorld example with multiple independent environments.
2
3This is the small ramp example: create many envs, spawn one rigid body per env,
4step them together, and read batched state tensors from ``world.state``.
5
6The example also demonstrates runtime PhysX material changes on a sloped static
7terrain. All boxes start with a low-friction material; after a short delay,
8half of the envs are switched to a high-friction material while the other half
9remains slippery. The UI shows the average downhill speed of both groups so
10friction changes can be checked while the simulation is running.
11"""
12
13from __future__ import annotations
14
15import argparse
16import math
17from pathlib import Path
18
19import torch
20
21import kangengine as ke
22from kangengine import imgui, keys
23
24
25def package_asset_path(*parts: str) -> str:
26 return str(Path(ke.__file__).resolve().parent / "assets" / Path(*parts))
27
28
29def grid_position(index: int, columns: int, spacing: float, height: float):
30 row = index // columns
31 col = index % columns
32 x = (col - (columns - 1) * 0.5) * spacing
33 y = row * spacing
34 return [x, y, height]
35
36
37# TDOO : make easy Cloner API
38def grid_positions(num_envs: int, columns: int, spacing: float, height: float):
39 env_ids = torch.arange(num_envs, dtype=torch.float32)
40 rows = torch.div(env_ids, columns, rounding_mode="floor")
41 cols = torch.remainder(env_ids, columns)
42 x = (cols - (columns - 1) * 0.5) * spacing
43 y = rows * spacing
44 z = torch.full_like(x, float(height))
45 return torch.stack((x, y, z), dim=1)
46
47
48def y_axis_quat_xyzw(angle_rad: float):
49 half = angle_rad * 0.5
50 return [0.0, math.sin(half), 0.0, math.cos(half)]
51
52
53def friction_group_color(env_id: int):
54 # Even envs stay low-friction/slippery. Odd envs switch to high friction at
55 # runtime. Keep colors grouped instead of using a gradient so the test reads
56 # visually at a glance.
57 if env_id % 2 == 0:
58 return [0.15, 0.45, 1.0, 1.0] # low friction: blue
59 return [1.0, 0.42, 0.12, 1.0] # runtime high friction: orange
60
61
62class MultiEnvSimWorldApp(ke.App):
63 def __init__(self, num_envs: int, friction_switch_time: float):
64 super().__init__()
65 self.num_envs = int(num_envs)
66 self.friction_switch_time = float(friction_switch_time)
67
68 def setup(self):
69 self.timing = self.configure_timing(
70 ke.SimulationTimingConfig(
71 render_hz=0.0,
72 physics_hz=120.0,
73 fixed_update_hz=60.0,
74 )
75 )
76 self.set_simulation_hotkeys_enabled(True)
77 self.sim_time = 0.0
78 self.material_update_count = 0
79 self.runtime_material_applied = False
80 self.spacing = 0.8
81 self.ramp_angle_deg = 18.0
82 self.ramp_angle = math.radians(self.ramp_angle_deg)
83 self.ramp_half_extents = [4.7, 4.0, 0.05]
84 self.ramp_pos = [0.0, 0.0, 0.0]
85 self.ramp_rot_xyzw = y_axis_quat_xyzw(self.ramp_angle)
86 self.box_half_z = 0.12
87 self.spawn_x = -2.4
88 self.columns = max(1, int(math.ceil(math.sqrt(self.num_envs))))
89 self.low_friction_envs = tuple(range(0, self.num_envs, 2))
90 self.high_friction_envs = tuple(range(1, self.num_envs, 2))
91 if not self.high_friction_envs and self.num_envs > 0:
92 self.high_friction_envs = (0,)
93 self.low_friction_envs = ()
94 self.low_friction_material = ke.physics.PhysicsMaterialDesc([0.05, 0.05, 0.0])
95 self.high_friction_material = ke.physics.PhysicsMaterialDesc([3.0, 2.5, 0.0])
96
97 self.standard_materials = self.create_standard_materials()
98 self.set_camera_view([3.6, -5.0, 2.8], [0.0, 0.0, 0.6])
99
100 self.world = ke.sim.KangSimWorld(
101 num_envs=self.num_envs,
102 sim_dt=self.timing.physics_dt,
103 add_ground=False,
104 )
105 self.world.physics.add_static_box(
106 self.ramp_half_extents,
107 self.ramp_pos,
108 self.ramp_rot_xyzw,
109 register_as_ground=True,
110 )
111 self.visual = ke.visual.sim.SimWorldVisualizer(self, self.world)
112 self._add_ramp_visual()
113 self.box_xml = package_asset_path("objects", "box.xml")
114 box_data = self.world.load_mjcf(self.box_xml)
115 for env_id in range(self.num_envs):
116 pos = self._ramp_spawn_position(env_id)
117 self.world.add_rigid(
118 box_data,
119 env_id=env_id,
120 obj_id=0,
121 name=f"box_{env_id}",
122 pos=pos,
123 density=60.0,
124 )
125
126 self.box = self.world.get_rigid_batch(obj_id=0)
127 # Runtime API smoke: start every rigid instance with a slippery material.
128 # Later, a subset is switched to high friction while the sim is running.
129 self.material_update_count = self.box.set_collision_material(
130 None, self.low_friction_material
131 )
132 self.box_visual_batch = self.visual.add(
133 self.box,
134 self.box_xml,
135 path="/group/box",
136 material=self.standard_materials.common,
137 color=[friction_group_color(env_id) for env_id in range(self.num_envs)],
138 )
139
140 self._reset()
141 print(f"Multi-env KangSimWorld example: num_envs={self.num_envs}")
142 print(f"root_pos tensor shape: {tuple(self.box.get_root_pos().shape)}")
143 print(
144 "Runtime friction demo: "
145 f"initial low-friction shapes updated={self.material_update_count}; "
146 f"switch high-friction envs={self.high_friction_envs} "
147 f"at t={self.friction_switch_time:.2f}s; "
148 f"ramp={self.ramp_angle_deg:.1f}deg"
149 )
150 self.check_error()
151
152 def _reset(self):
153 self.sim_time = 0.0
154 self.runtime_material_applied = False
155 self.material_update_count = self.box.set_collision_material(
156 None, self.low_friction_material
157 )
158 positions = torch.tensor(
159 [self._ramp_spawn_position(env_id) for env_id in range(self.num_envs)],
160 dtype=torch.float32,
161 )
162 rotations = torch.zeros((self.num_envs, 4), dtype=torch.float32)
163 rotations[:, 3] = 1.0 # quat xyzw
164
165 velocities = torch.zeros((self.num_envs, 3), dtype=torch.float32)
166 # +X is downhill for the ramp quaternion above.
167 velocities[:, 0] = 1.0
168 velocities[:, 1] = 0.0
169 velocities[:, 2] = 0.0
170
171 self.box.set_root_state(
172 None,
173 positions,
174 rotations,
175 linear_velocity=velocities,
176 angular_velocity=[0.0, 0.0, 0.0],
177 )
178 self.world.step(substeps=0, apply_commands=False)
179 self.visual.sync()
180
181 def pre_update(self):
182 if self.was_key_pressed(keys.R):
183 self._reset()
184
185 def fixed_update(self, fixed_dt):
186 self.world.advance(fixed_dt)
187 self.sim_time += fixed_dt
188 if (
189 not self.runtime_material_applied
190 and self.sim_time >= self.friction_switch_time
191 ):
192 self.material_update_count += self.box.set_collision_material(
193 self.high_friction_envs, self.high_friction_material
194 )
195 self.runtime_material_applied = True
196 print(
197 "Runtime friction switched: "
198 f"envs={self.high_friction_envs}, "
199 f"total_updated_shapes={self.material_update_count}"
200 )
201
202 def pre_render(self):
203 self.visual.sync()
204 self.check_error()
205
206 def render(self):
207 root_pos = self.box.get_root_pos()
208 root_vel = self.box.get_root_vel()
209 mean_height = float(root_pos[:, 2].mean().item())
210 min_height = float(root_pos[:, 2].min().item())
211 max_height = float(root_pos[:, 2].max().item())
212 downhill_speed = root_vel[:, 0]
213 low_speed = self._mean_for_envs(downhill_speed, self.low_friction_envs)
214 high_speed = self._mean_for_envs(downhill_speed, self.high_friction_envs)
215
216 imgui.begin("Multi-Env Sim World")
217 imgui.text("KangSimWorld batched state example")
218 imgui.text("Enter: play/pause Space: pause/step R: reset")
219 imgui.separator()
220 state = "paused" if self.is_simulation_paused() else "running"
221 imgui.text(f"State: {state}")
222 imgui.text(f"Envs: {self.num_envs}")
223 imgui.text(f"root_pos shape: {tuple(root_pos.shape)}")
224 imgui.text("height min/mean/max:")
225 imgui.same_line()
226 imgui.text(f"{min_height: .2f} / {mean_height: .2f} / {max_height: .2f}")
227 imgui.separator()
228 imgui.text("Runtime friction material test")
229 imgui.text(
230 f"switch: {'done' if self.runtime_material_applied else 'pending'} at t={self.friction_switch_time:.2f}s"
231 )
232 imgui.text(f"ramp angle: {self.ramp_angle_deg:.1f} deg")
233 imgui.text(f"updated shapes: {self.material_update_count}")
234 imgui.text(f"low friction envs: {self.low_friction_envs}")
235 imgui.text(f"high friction envs: {self.high_friction_envs}")
236 imgui.text(f"downhill velocity low/high: {low_speed: .3f} / {high_speed: .3f}")
237 imgui.end()
238
239 def _ramp_spawn_position(self, env_id: int):
240 row = env_id // self.columns
241 col = env_id % self.columns
242 y = (col - (self.columns - 1) * 0.5) * self.spacing
243 x = self.spawn_x - row * 0.45
244 z = self._ramp_top_z(x) + self.box_half_z + 0.04
245 return [x, y, z]
246
247 def _ramp_top_z(self, x: float) -> float:
248 half_t = self.ramp_half_extents[2]
249 return -math.tan(self.ramp_angle) * float(x) + half_t / math.cos(
250 self.ramp_angle
251 )
252
253 def _add_ramp_visual(self):
254 mesh_data = ke.geometry.create_box_data(
255 self.ramp_half_extents[0] * 2.0,
256 self.ramp_half_extents[1] * 2.0,
257 self.ramp_half_extents[2] * 2.0,
258 )
259 view = self.add_mesh(
260 "/terrain/inclined_box",
261 mesh_data,
262 self.standard_materials.common,
263 color=[0.45, 0.45, 0.5, 1.0],
264 )
265 view.prim.set_local_translation(ke.Vec3(self.ramp_pos))
266 # ke.Quat constructor is (w, x, y, z), while PhysX binding args above
267 # use xyzw lists.
268 view.prim.set_local_rotation(
269 ke.Quat(
270 self.ramp_rot_xyzw[3],
271 self.ramp_rot_xyzw[0],
272 self.ramp_rot_xyzw[1],
273 self.ramp_rot_xyzw[2],
274 )
275 )
276 return view
277
278 @staticmethod
279 def _mean_for_envs(values: torch.Tensor, env_ids: tuple[int, ...]) -> float:
280 if not env_ids:
281 return float("nan")
282 index = torch.tensor(env_ids, dtype=torch.long, device=values.device)
283 return float(values.index_select(0, index).mean().item())
284
285 def cleanup(self):
286 if hasattr(self, "world"):
287 self.world.release()
288
289
290def parse_args():
291 parser = argparse.ArgumentParser(description=__doc__)
292 parser.add_argument("--num-envs", type=int, default=16)
293 parser.add_argument("--width", type=int, default=1280)
294 parser.add_argument("--height", type=int, default=720)
295 parser.add_argument(
296 "--friction-switch-time",
297 type=float,
298 default=1.0,
299 help="Seconds before switching odd envs to high friction at runtime.",
300 )
301 return parser.parse_args()
302
303
304def main():
305 args = parse_args()
306 app = MultiEnvSimWorldApp(args.num_envs, args.friction_switch_time)
307 app.initialize(args.width, args.height, False, ke.UpAxis.Z)
308 app.start()
309
310
311if __name__ == "__main__":
312 main()
The example compares low- and high-friction groups on a ramp and displays batched state statistics.
