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
Each of the three surfaces gets an independently sampled texture, so a single episode can mix
different wall / table / ground appearances. The eval_mode flag switches the
texture pool from the seen split to the unseen split, which is how
you keep evaluation appearances out of the training distribution.
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:
random_background — enable RoboTwin texture randomization for the wall,
table and ground. robot_init_qpos_noise — standard deviation of the Gaussian
noise added to the robot's initial joint configuration at reset.
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=self.robot_init_qpos_noise,
)
self.table_scene.build() # uses random_background from the constructor
# ... load your actors / articulations
Textures are chosen inside build() / create_table_and_wall(),
which runs during scene construction. If your environment is built once and reused, the
background stays fixed for the lifetime of the scene unless the environment is
reconfigured. With reconfiguration_freq set (e.g. 1 for single-env
workflows), each reset rebuilds the scene and re-samples the textures.
The RoboTwin task examples (mani_skill/envs/tasks/tabletop/rm_tasks/) build the
same builder — for example rm_microwave.py constructs
TableSceneBuilder(self, robot_init_qpos_noise=...) and calls
self.table_scene.build(random_background=False) directly. Both call styles are
valid; passing the flag to the constructor keeps the default consistent.
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,
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 as shown below.
Tuning the jitter
The scale IS the randomization strength. Increase the multiplier to spread objects further apart across episodes, or decrease it to keep placements tight:
# Default: [0, 0.02) m per axis (2 cm × 2 cm one-sided range)
xyz[:, :2] += torch.rand((b, 2), device=self.device) * 0.02
# Stronger: [0, 0.1) m per axis
xyz[:, :2] += torch.rand((b, 2), device=self.device) * 0.1
# Even stronger: [0, 0.25) m per axis
xyz[:, :2] += torch.rand((b, 2), device=self.device) * 0.25
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.
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.
Combined example
A task that turns everything up: randomized background, stronger placement jitter, and a slightly noisier robot start:
class TwoRobotMyTaskEnv(BaseEnv):
def __init__(self, *args, robot_uids=("panda_wristcam", "panda_wristcam"),
robot_init_qpos_noise=0.05, placement_noise=0.1, **kwargs):
self.robot_init_qpos_noise = robot_init_qpos_noise
self.placement_noise = placement_noise
super().__init__(*args, robot_uids=robot_uids, **kwargs)
def _load_scene(self, options: dict):
self.table_scene = TableSceneBuilder(
env=self,
random_background=True, # randomized textures
robot_init_qpos_noise=self.robot_init_qpos_noise,
)
self.table_scene.build()
# ... load actors
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)
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.5) * 2 * self.placement_noise
xyz = apply_l1_offset_xy(xyz, offset=(-0.1, 0.1))
self.apple.set_pose(Pose.create_from_pq(
p=xyz, q=torch.tensor([euler2quat(0, 0, np.pi / 6)] * b,
device=self.device, dtype=torch.float32)))
Required assets
Background randomization reads textures from
~/.maniskill/data/robotwin/background_texture/{seen,unseen}/. Install them with
the RoboTwin downloader (see the installation page):
mkdir -p ~/.maniskill/data/robotwin
python -m mani_skill.utils.download_robotwin
cd ~/.maniskill/data/robotwin
unzip objects.zip && rm -rf objects.zip
unzip background_texture.zip && rm -rf background_texture.zip
If random_background=True but the
background_texture/ folder is missing, the builder calls
os.listdir on a non-existent directory and raises. Download the RoboTwin
assets first.
Build your own randomized task from the creating a task guide, or collect demonstrations for it with the data collection pipeline.