Domain randomization
Two knobs control the visual and spatial diversity of every benchmark task: background
texture randomization via TableSceneBuilder(..., random_background=True), and
per-episode object-placement jitter via the xyz[:, :2] += rand * scale pattern
in _initialize_episode.
Overview
Simulation makes it cheap to chop and change aspects of the scene that would be expensive in
the real world. The Imitator Game task template exposes two ready-made domain-randomization
hooks, both implemented in the shared
mani_skill/utils/scene_builder/table/scene_builder.py and the task template at
mani_skill/envs/tasks/_template/template_task.py:
Background textures
Randomized wall, table and ground textures sampled from the RoboTwin background-texture
dataset, toggled with random_background=True.
Object placement
A uniform XY offset added to every object's spawn pose in
_initialize_episode; the magnitude is the scale in
xyz[:, :2] += torch.rand(...) * SCALE.
Robot init pose
Gaussian noise on the robot's initial joint configuration, controlled by
robot_init_qpos_noise on TableSceneBuilder.
Background randomization
The background randomization is implemented by
TableSceneBuilder.create_table_and_wall(...). When enabled, it samples random
texture files for the wall, the table and the
ground from the RoboTwin background-texture directory:
def create_table_and_wall(
self,
table_xy_bias=[0, 0],
table_height=0.9,
random_background=False,
eval_mode=False
):
...
if random_background:
texture_type = "seen" if not eval_mode else "unseen"
directory_path = str(Path.home() / ".maniskill" / "data" / "robotwin" / f"background_texture/{texture_type}")
file_count = len([name for name in os.listdir(directory_path) ...])
wall_texture = np.random.randint(0, file_count)
table_texture = np.random.randint(0, file_count)
ground_texture = np.random.randint(0, file_count)
...
else:
wall_texture, table_texture, ground_texture = None, None, None
Robot init qpos noise
The robot's initial joint configuration is also perturbed at every reset. In
TableSceneBuilder.initialize() a Gaussian offset with standard deviation
robot_init_qpos_noise is added to the rest qpos for the supported robot types:
qpos = np.array([
0.0, np.pi / 8, 0, -np.pi * 5 / 8,
0, np.pi * 3 / 4, np.pi / 4, 0.04, 0.04,
])
qpos = (self.env._episode_rng.normal(
0, self.robot_init_qpos_noise, (b, len(qpos))) + qpos)
qpos[:, -2:] = 0.04 # gripper fingers always start open at 0.04
self.env.agent.reset(qpos)
The fingers (qpos[:, -2:]) are always reset to a fixed open position; the
noise applies to the arm joints. This prevents the policy from memorizing a single initial
arm configuration.
How to enable it
The TableSceneBuilder is constructed in your task's _load_scene.
Set random_background=True in the constructor to turn the randomization on:
from mani_skill.utils.scene_builder.table import TableSceneBuilder
class TwoRobotMyTaskEnv(BaseEnv):
def _load_scene(self, options: dict):
self.table_scene = TableSceneBuilder(
env=self,
random_background=True, # ← enables texture randomization
robot_init_qpos_noise=0.02, # ← enables robot initial joint pose randomization
)
self.table_scene.build() # uses random_background from the constructor
# ... load your actors / articulations
Object placement randomization
In addition to the background, every object's spawn pose is randomized per episode. The
template does this in _initialize_episode by adding a uniform XY jitter to the
base pose before calling set_pose:
def _initialize_episode(self, env_idx: torch.Tensor, options: dict):
with torch.device(self.device):
b = len(env_idx)
self.table_scene.initialize(env_idx)
# Randomize apple pose (with optional L1 xy offset).
xyz = torch.zeros((b, 3), device=self.device)
xyz[:, 0] = -0.1
xyz[:, 1] = -0.15
xyz[:, 2] = self.apple_z
xyz[:, :2] += torch.rand((b, 2), device=self.device) * 0.02 # ← placement jitter
xyz = apply_l1_offset_xy(xyz, offset=(-0.1, 0.1))
qs = torch.tensor([euler2quat(0, 0, np.pi / 6)] * b, # ← rotation jitter
device=self.device, dtype=torch.float32)
self.apple.set_pose(Pose.create_from_pq(p=xyz, q=qs))
# Randomize basket pose (shared offset so items stay inside).
basket_xyz = torch.zeros((b, 3), device=self.device)
basket_xyz[:, 0] = 0.0
basket_xyz[:, 1] = 0.1
basket_xyz[:, 2] = self.basket_z
basket_xyz[:, :2] += torch.rand((b, 2), device=self.device) * 0.02
basket_xyz = apply_l1_offset_xy(basket_xyz, offset=(-0.1, 0.1))
self.breadbasket.set_pose(...)
torch.rand((b, 2)) draws b uniform samples in
[0, 1) for the X and Y axes. Multiplied by the scale, each object receives a
one-sided offset in [0, scale) on each axis — so the default
0.02 meters gives a 2 cm × 2 cm offset range from the anchor point. For a
symmetric range around the anchor, subtract 0.5 before scaling.
Symmetric noise — subtract half the scale so the nominal position stays
at the center: xyz[:, :2] += (torch.rand((b, 2), ...) - 0.5) * 0.1.
Shared offset for coupled objects — draw one offset and reuse it for
every object that must stay together (the template does this for the apple and basket),
so randomized placements don't break relative geometry.
Keep the scale small enough that objects stay within the dual-Panda reachable workspace
and don't collide with the wall or fall off the table (the table is 2.0 m × 1.2 m).
Start from 0.02 and increase gradually while watching
env.reset(seed=...) behavior.
Build your own randomized task from the creating a task guide, or collect demonstrations for it with the data collection pipeline.