Randomization Snippets#
Examples of randomization using USD and Isaac Sim APIs. These examples demonstrate how to randomize scenes for synthetic data generation (SDG) in scenarios where default replicator randomizers are not sufficient or applicable.
The snippets are designed to align with the structure and function names used in the replicator example snippets. In comparison they also have the option to write the data to disk by stetting write_data=True
.
Prerequisites:
Familiarity with USD.
Ability to execute code from the Script Editor.
Understanding basic replicator concepts, such as subframes.
Randomizing Light Sources#
This snippet sets up a new environment containing a cube and a sphere; it then spawns a given number of lights and randomizes selected attributes for these lights over a specified number of frames.
Randomizing Light Sources
1import asyncio
2import os
3
4import numpy as np
5import omni.kit.commands
6import omni.replicator.core as rep
7import omni.usd
8from omni.isaac.core.utils.semantics import add_update_semantics
9from pxr import Gf, Sdf, UsdGeom
10
11omni.usd.get_context().new_stage()
12stage = omni.usd.get_context().get_stage()
13
14sphere = stage.DefinePrim("/World/Sphere", "Sphere")
15UsdGeom.Xformable(sphere).AddTranslateOp().Set((0.0, 1.0, 1.0))
16add_update_semantics(sphere, "sphere", "class")
17
18cube = stage.DefinePrim("/World/Cube", "Cube")
19UsdGeom.Xformable(cube).AddTranslateOp().Set((0.0, -2.0, 2.0))
20add_update_semantics(cube, "cube", "class")
21
22plane_path = "/World/Plane"
23omni.kit.commands.execute("CreateMeshPrimWithDefaultXform", prim_path=plane_path, prim_type="Plane")
24plane_prim = stage.GetPrimAtPath(plane_path)
25plane_prim.CreateAttribute("xformOp:scale", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(10, 10, 1))
26
27
28def sphere_lights(num):
29 lights = []
30 for i in range(num):
31 # "CylinderLight", "DiskLight", "DistantLight", "DomeLight", "RectLight", "SphereLight"
32 prim_type = "SphereLight"
33 next_free_path = omni.usd.get_stage_next_free_path(stage, f"/World/{prim_type}", False)
34 light_prim = stage.DefinePrim(next_free_path, prim_type)
35 UsdGeom.Xformable(light_prim).AddTranslateOp().Set((0.0, 0.0, 0.0))
36 UsdGeom.Xformable(light_prim).AddRotateXYZOp().Set((0.0, 0.0, 0.0))
37 UsdGeom.Xformable(light_prim).AddScaleOp().Set((1.0, 1.0, 1.0))
38 light_prim.CreateAttribute("inputs:enableColorTemperature", Sdf.ValueTypeNames.Bool).Set(True)
39 light_prim.CreateAttribute("inputs:colorTemperature", Sdf.ValueTypeNames.Float).Set(6500.0)
40 light_prim.CreateAttribute("inputs:radius", Sdf.ValueTypeNames.Float).Set(0.5)
41 light_prim.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(30000.0)
42 light_prim.CreateAttribute("inputs:color", Sdf.ValueTypeNames.Color3f).Set((1.0, 1.0, 1.0))
43 light_prim.CreateAttribute("inputs:exposure", Sdf.ValueTypeNames.Float).Set(0.0)
44 light_prim.CreateAttribute("inputs:diffuse", Sdf.ValueTypeNames.Float).Set(1.0)
45 light_prim.CreateAttribute("inputs:specular", Sdf.ValueTypeNames.Float).Set(1.0)
46 lights.append(light_prim)
47 return lights
48
49
50async def run_randomizations_async(num_frames, lights, write_data=True, delay=0):
51 if write_data:
52 writer = rep.WriterRegistry.get("BasicWriter")
53 out_dir = os.getcwd() + "/_out_rand_lights"
54 print(f"Writing data to {out_dir}..")
55 writer.initialize(output_dir=out_dir, rgb=True)
56 rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512))
57 writer.attach(rp)
58
59 for _ in range(num_frames):
60 for light in lights:
61 light.GetAttribute("xformOp:translate").Set(
62 (np.random.uniform(-5, 5), np.random.uniform(-5, 5), np.random.uniform(4, 6))
63 )
64 scale_rand = np.random.uniform(0.5, 1.5)
65 light.GetAttribute("xformOp:scale").Set((scale_rand, scale_rand, scale_rand))
66 light.GetAttribute("inputs:colorTemperature").Set(np.random.normal(4500, 1500))
67 light.GetAttribute("inputs:intensity").Set(np.random.normal(25000, 5000))
68 light.GetAttribute("inputs:color").Set(
69 (np.random.uniform(0.1, 0.9), np.random.uniform(0.1, 0.9), np.random.uniform(0.1, 0.9))
70 )
71
72 if write_data:
73 await rep.orchestrator.step_async(rt_subframes=16)
74 else:
75 await omni.kit.app.get_app().next_update_async()
76 if delay > 0:
77 await asyncio.sleep(delay)
78
79
80num_frames = 10
81lights = sphere_lights(10)
82asyncio.ensure_future(run_randomizations_async(num_frames=num_frames, lights=lights, delay=0.2))
Randomizing Textures#
The snippet sets up an environment, spawns a given number of cubes and spheres, and randomizes their textures for the given number of frames. After the randomizations their original materials are reassigned. The snippet also showcases how to create a new material and assign it to a prim.
Randomizing Textures
1import asyncio
2import os
3
4import numpy as np
5import omni.replicator.core as rep
6import omni.usd
7from omni.isaac.nucleus import get_assets_root_path
8from omni.isaac.core.utils.semantics import add_update_semantics, get_semantics
9from pxr import Gf, Sdf, UsdGeom, UsdShade
10
11omni.usd.get_context().new_stage()
12stage = omni.usd.get_context().get_stage()
13dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight")
14dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(1000.0)
15
16sphere = stage.DefinePrim("/World/Sphere", "Sphere")
17UsdGeom.Xformable(sphere).AddTranslateOp().Set((0.0, 0.0, 1.0))
18add_update_semantics(sphere, "sphere", "class")
19
20num_cubes = 10
21for _ in range(num_cubes):
22 prim_type = "Cube"
23 next_free_path = omni.usd.get_stage_next_free_path(stage, f"/World/{prim_type}", False)
24 cube = stage.DefinePrim(next_free_path, prim_type)
25 UsdGeom.Xformable(cube).AddTranslateOp().Set((np.random.uniform(-3.5, 3.5), np.random.uniform(-3.5, 3.5), 1))
26 scale_rand = np.random.uniform(0.25, 0.5)
27 UsdGeom.Xformable(cube).AddScaleOp().Set((scale_rand, scale_rand, scale_rand))
28 add_update_semantics(cube, "cube", "class")
29
30plane_path = "/World/Plane"
31omni.kit.commands.execute("CreateMeshPrimWithDefaultXform", prim_path=plane_path, prim_type="Plane")
32plane_prim = stage.GetPrimAtPath(plane_path)
33plane_prim.CreateAttribute("xformOp:scale", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(10, 10, 1))
34
35
36def get_shapes():
37 stage = omni.usd.get_context().get_stage()
38 shapes = []
39 for prim in stage.Traverse():
40 sem_dict = get_semantics(prim)
41 sem_values = sem_dict.values()
42 if ("class", "cube") in sem_values or ("class", "sphere") in sem_values:
43 shapes.append(prim)
44 return shapes
45
46
47shapes = get_shapes()
48
49
50def create_omnipbr_material(mtl_url, mtl_name, mtl_path):
51 stage = omni.usd.get_context().get_stage()
52 omni.kit.commands.execute("CreateMdlMaterialPrim", mtl_url=mtl_url, mtl_name=mtl_name, mtl_path=mtl_path)
53 material_prim = stage.GetPrimAtPath(mtl_path)
54 shader = UsdShade.Shader(omni.usd.get_shader_from_material(material_prim, get_prim=True))
55
56 # Add value inputs
57 shader.CreateInput("diffuse_color_constant", Sdf.ValueTypeNames.Color3f)
58 shader.CreateInput("reflection_roughness_constant", Sdf.ValueTypeNames.Float)
59 shader.CreateInput("metallic_constant", Sdf.ValueTypeNames.Float)
60
61 # Add texture inputs
62 shader.CreateInput("diffuse_texture", Sdf.ValueTypeNames.Asset)
63 shader.CreateInput("reflectionroughness_texture", Sdf.ValueTypeNames.Asset)
64 shader.CreateInput("metallic_texture", Sdf.ValueTypeNames.Asset)
65
66 # Add other attributes
67 shader.CreateInput("project_uvw", Sdf.ValueTypeNames.Bool)
68
69 # Add texture scale and rotate
70 shader.CreateInput("texture_scale", Sdf.ValueTypeNames.Float2)
71 shader.CreateInput("texture_rotate", Sdf.ValueTypeNames.Float)
72
73 material = UsdShade.Material(material_prim)
74 return material
75
76
77def create_materials(num):
78 MDL = "OmniPBR.mdl"
79 mtl_name, _ = os.path.splitext(MDL)
80 MAT_PATH = "/World/Looks"
81 materials = []
82 for _ in range(num):
83 prim_path = omni.usd.get_stage_next_free_path(stage, f"{MAT_PATH}/{mtl_name}", False)
84 mat = create_omnipbr_material(mtl_url=MDL, mtl_name=mtl_name, mtl_path=prim_path)
85 materials.append(mat)
86 return materials
87
88
89materials = create_materials(len(shapes))
90
91
92async def run_randomizations_async(num_frames, materials, textures, write_data=True, delay=0):
93 if write_data:
94 writer = rep.WriterRegistry.get("BasicWriter")
95 out_dir = os.getcwd() + "/_out_rand_textures"
96 print(f"Writing data to {out_dir}..")
97 writer.initialize(output_dir=out_dir, rgb=True)
98 rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512))
99 writer.attach(rp)
100
101 # Apply the new materials and store the initial ones to reassign later
102 initial_materials = {}
103 for i, shape in enumerate(shapes):
104 cur_mat, _ = UsdShade.MaterialBindingAPI(shape).ComputeBoundMaterial()
105 initial_materials[shape] = cur_mat
106 UsdShade.MaterialBindingAPI(shape).Bind(materials[i], UsdShade.Tokens.strongerThanDescendants)
107
108 for _ in range(num_frames):
109 for mat in materials:
110 shader = UsdShade.Shader(omni.usd.get_shader_from_material(mat, get_prim=True))
111 diffuse_texture = np.random.choice(textures)
112 shader.GetInput("diffuse_texture").Set(diffuse_texture)
113 project_uvw = np.random.choice([True, False], p=[0.9, 0.1])
114 shader.GetInput("project_uvw").Set(bool(project_uvw))
115 texture_scale = np.random.uniform(0.1, 1)
116 shader.GetInput("texture_scale").Set((texture_scale, texture_scale))
117 texture_rotate = np.random.uniform(0, 45)
118 shader.GetInput("texture_rotate").Set(texture_rotate)
119
120 if write_data:
121 await rep.orchestrator.step_async(rt_subframes=4)
122 else:
123 await omni.kit.app.get_app().next_update_async()
124 if delay > 0:
125 await asyncio.sleep(delay)
126
127 # Reassign the initial materials
128 for shape, mat in initial_materials.items():
129 if mat:
130 UsdShade.MaterialBindingAPI(shape).Bind(mat, UsdShade.Tokens.strongerThanDescendants)
131 else:
132 UsdShade.MaterialBindingAPI(shape).UnbindAllBindings()
133
134
135assets_root_path = get_assets_root_path()
136textures = [
137 assets_root_path + "/NVIDIA/Materials/vMaterials_2/Ground/textures/aggregate_exposed_diff.jpg",
138 assets_root_path + "/NVIDIA/Materials/vMaterials_2/Ground/textures/gravel_track_ballast_diff.jpg",
139 assets_root_path + "/NVIDIA/Materials/vMaterials_2/Ground/textures/gravel_track_ballast_multi_R_rough_G_ao.jpg",
140 assets_root_path + "/NVIDIA/Materials/vMaterials_2/Ground/textures/rough_gravel_rough.jpg",
141]
142
143num_frames = 10
144asyncio.ensure_future(run_randomizations_async(num_frames, materials, textures, delay=0.2))
Sequential Randomizations#
The snippet provides an example of more complex randomizations, where the results of the first randomization are used to determine the next randomization. It uses a custom sampler function to set the location of the camera by iterating over (almost) equidistant points on a sphere. The snippet starts by setting up the environment, a forklift, a pallet, a bin, and a dome light. For every randomization frame, it cycles through the dome light textures, moves the pallet to a random location, and then moves the bin so that it is fully on top of the pallet. Finally, it moves the camera to a new location on the sphere, ensuring it faces the bin.
Sequential Randomizations
1import asyncio
2import itertools
3import os
4
5import numpy as np
6import omni.replicator.core as rep
7import omni.usd
8from omni.isaac.nucleus import get_assets_root_path
9from pxr import Gf, Usd, UsdGeom, UsdLux
10
11
12# https://stackoverflow.com/questions/9600801/evenly-distributing-n-points-on-a-sphere
13# https://arxiv.org/pdf/0912.4540.pdf
14def next_point_on_sphere(idx, num_points, radius=1, origin=(0, 0, 0)):
15 offset = 2.0 / num_points
16 inc = np.pi * (3.0 - np.sqrt(5.0))
17 z = ((idx * offset) - 1) + (offset / 2)
18 phi = ((idx + 1) % num_points) * inc
19 r = np.sqrt(1 - pow(z, 2))
20 y = np.cos(phi) * r
21 x = np.sin(phi) * r
22 return [(x * radius) + origin[0], (y * radius) + origin[1], (z * radius) + origin[2]]
23
24
25assets_root_path = get_assets_root_path()
26FORKLIFT_PATH = assets_root_path + "/Isaac/Props/Forklift/forklift.usd"
27PALLET_PATH = assets_root_path + "/Isaac/Props/Pallet/pallet.usd"
28BIN_PATH = assets_root_path + "/Isaac/Props/KLT_Bin/small_KLT_visual.usd"
29
30omni.usd.get_context().new_stage()
31stage = omni.usd.get_context().get_stage()
32
33dome_light = UsdLux.DomeLight.Define(stage, "/World/Lights/DomeLight")
34dome_light.GetIntensityAttr().Set(1000)
35
36forklift_prim = stage.DefinePrim("/World/Forklift", "Xform")
37forklift_prim.GetReferences().AddReference(FORKLIFT_PATH)
38if not forklift_prim.GetAttribute("xformOp:translate"):
39 UsdGeom.Xformable(forklift_prim).AddTranslateOp()
40forklift_prim.GetAttribute("xformOp:translate").Set((-4.5, -4.5, 0))
41
42pallet_prim = stage.DefinePrim("/World/Pallet", "Xform")
43pallet_prim.GetReferences().AddReference(PALLET_PATH)
44if not pallet_prim.GetAttribute("xformOp:translate"):
45 UsdGeom.Xformable(pallet_prim).AddTranslateOp()
46if not pallet_prim.GetAttribute("xformOp:rotateXYZ"):
47 UsdGeom.Xformable(pallet_prim).AddRotateXYZOp()
48
49bin_prim = stage.DefinePrim("/World/Bin", "Xform")
50bin_prim.GetReferences().AddReference(BIN_PATH)
51if not bin_prim.GetAttribute("xformOp:translate"):
52 UsdGeom.Xformable(bin_prim).AddTranslateOp()
53if not bin_prim.GetAttribute("xformOp:rotateXYZ"):
54 UsdGeom.Xformable(bin_prim).AddRotateXYZOp()
55
56cam = stage.DefinePrim("/World/Camera", "Camera")
57if not cam.GetAttribute("xformOp:translate"):
58 UsdGeom.Xformable(cam).AddTranslateOp()
59if not cam.GetAttribute("xformOp:orient"):
60 UsdGeom.Xformable(cam).AddOrientOp()
61
62
63async def run_randomizations_async(
64 num_frames, dome_light, dome_textures, pallet_prim, bin_prim, write_data=True, delay=0
65):
66 if write_data:
67 writer = rep.WriterRegistry.get("BasicWriter")
68 out_dir = os.getcwd() + "/_out_rand_sphere_scan"
69 print(f"Writing data to {out_dir}..")
70 writer.initialize(output_dir=out_dir, rgb=True)
71 rp_persp = rep.create.render_product("/OmniverseKit_Persp", (512, 512), name="PerspView")
72 rp_cam = rep.create.render_product(str(cam.GetPath()), (512, 512), name="SphereView")
73 writer.attach([rp_cam, rp_persp])
74
75 textures_cycle = itertools.cycle(dome_textures)
76
77 bb_cache = UsdGeom.BBoxCache(time=Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
78 pallet_size = bb_cache.ComputeWorldBound(pallet_prim).GetRange().GetSize()
79 pallet_length = pallet_size.GetLength()
80 bin_size = bb_cache.ComputeWorldBound(bin_prim).GetRange().GetSize()
81
82 for i in range(num_frames):
83 # Set next background texture every nth frame and run an app update
84 if i % 5 == 0:
85 dome_light.GetTextureFileAttr().Set(next(textures_cycle))
86 await omni.kit.app.get_app().next_update_async()
87
88 # Randomize pallet pose
89 pallet_prim.GetAttribute("xformOp:translate").Set(
90 Gf.Vec3d(np.random.uniform(-1.5, 1.5), np.random.uniform(-1.5, 1.5), 0)
91 )
92 rand_z_rot = np.random.uniform(-90, 90)
93 pallet_prim.GetAttribute("xformOp:rotateXYZ").Set(Gf.Vec3d(0, 0, rand_z_rot))
94 pallet_tf_mat = omni.usd.get_world_transform_matrix(pallet_prim)
95 pallet_rot = pallet_tf_mat.ExtractRotation()
96 pallet_pos = pallet_tf_mat.ExtractTranslation()
97
98 # Randomize bin position on top of the rotated pallet area making sure the bin is fully on the pallet
99 rand_transl_x = np.random.uniform(-pallet_size[0] / 2 + bin_size[0] / 2, pallet_size[0] / 2 - bin_size[0] / 2)
100 rand_transl_y = np.random.uniform(-pallet_size[1] / 2 + bin_size[1] / 2, pallet_size[1] / 2 - bin_size[1] / 2)
101
102 # Adjust bin position to account for the random rotation of the pallet
103 rand_z_rot_rad = np.deg2rad(rand_z_rot)
104 rot_adjusted_transl_x = rand_transl_x * np.cos(rand_z_rot_rad) - rand_transl_y * np.sin(rand_z_rot_rad)
105 rot_adjusted_transl_y = rand_transl_x * np.sin(rand_z_rot_rad) + rand_transl_y * np.cos(rand_z_rot_rad)
106 bin_prim.GetAttribute("xformOp:translate").Set(
107 Gf.Vec3d(
108 pallet_pos[0] + rot_adjusted_transl_x,
109 pallet_pos[1] + rot_adjusted_transl_y,
110 pallet_pos[2] + pallet_size[2] + bin_size[2] / 2,
111 )
112 )
113 # Keep bin rotation aligned with pallet
114 bin_prim.GetAttribute("xformOp:rotateXYZ").Set(pallet_rot.GetAxis() * pallet_rot.GetAngle())
115
116 # Get next camera position on a sphere looking at the bin with a randomized distance
117 rand_radius = np.random.normal(3, 0.5) * pallet_length
118 bin_pos = omni.usd.get_world_transform_matrix(bin_prim).ExtractTranslation()
119 cam_pos = next_point_on_sphere(i, num_points=num_frames, radius=rand_radius, origin=bin_pos)
120 cam.GetAttribute("xformOp:translate").Set(Gf.Vec3d(*cam_pos))
121
122 eye = Gf.Vec3d(*cam_pos)
123 target = Gf.Vec3d(*bin_pos)
124 up_axis = Gf.Vec3d(0, 0, 1)
125 look_at_quatd = Gf.Matrix4d().SetLookAt(eye, target, up_axis).GetInverse().ExtractRotation().GetQuat()
126 cam.GetAttribute("xformOp:orient").Set(Gf.Quatf(look_at_quatd))
127
128 if write_data:
129 await rep.orchestrator.step_async(rt_subframes=4)
130 else:
131 await omni.kit.app.get_app().next_update_async()
132 if delay > 0:
133 await asyncio.sleep(delay)
134
135
136num_frames = 90
137dome_textures = [
138 assets_root_path + "/NVIDIA/Assets/Skies/Cloudy/champagne_castle_1_4k.hdr",
139 assets_root_path + "/NVIDIA/Assets/Skies/Clear/evening_road_01_4k.hdr",
140 assets_root_path + "/NVIDIA/Assets/Skies/Clear/mealie_road_4k.hdr",
141 assets_root_path + "/NVIDIA/Assets/Skies/Clear/qwantani_4k.hdr",
142]
143asyncio.ensure_future(run_randomizations_async(num_frames, dome_light, dome_textures, pallet_prim, bin_prim, delay=0.2))
Physics-based Randomized Volume Filling#
The snippet randomizes the stacking of objects on multiple surfaces. It randomly spawns a given number of pallets in the selected areas and then spawns physically simulated boxes on top of them. A temporary collision box area is created around the pallets to prevent the boxes from falling off. Once all the boxes have been dropped, they are moved in various directions and finally pulled towards the center of the pallet for more stable stacking. Finally, the collision area is removed, after which the boxes can also fall to the ground. To allow easier sliding of the boxes into more stable positions, their friction is temporarily reduced during the simulation.
Physics-based Randomized Volume Filling
1import asyncio
2import random
3from itertools import chain
4
5import carb
6import omni.kit.app
7import omni.usd
8from omni.isaac.core.utils.bounds import compute_aabb, compute_obb, create_bbox_cache
9from omni.isaac.nucleus import get_assets_root_path
10from omni.physx import get_physx_simulation_interface
11from pxr import (
12 Gf,
13 PhysicsSchemaTools,
14 PhysxSchema,
15 Sdf,
16 Usd,
17 UsdGeom,
18 UsdPhysics,
19 UsdShade,
20 UsdUtils,
21)
22
23
24# Add transformation properties to the prim (if not already present)
25def set_transform_attributes(prim, location=None, orientation=None, rotation=None, scale=None):
26 if location is not None:
27 if not prim.HasAttribute("xformOp:translate"):
28 UsdGeom.Xformable(prim).AddTranslateOp()
29 prim.GetAttribute("xformOp:translate").Set(location)
30 if orientation is not None:
31 if not prim.HasAttribute("xformOp:orient"):
32 UsdGeom.Xformable(prim).AddOrientOp()
33 prim.GetAttribute("xformOp:orient").Set(orientation)
34 if rotation is not None:
35 if not prim.HasAttribute("xformOp:rotateXYZ"):
36 UsdGeom.Xformable(prim).AddRotateXYZOp()
37 prim.GetAttribute("xformOp:rotateXYZ").Set(rotation)
38 if scale is not None:
39 if not prim.HasAttribute("xformOp:scale"):
40 UsdGeom.Xformable(prim).AddScaleOp()
41 prim.GetAttribute("xformOp:scale").Set(scale)
42
43
44# Enables collisions with the asset (without rigid body dynamics the asset will be static)
45def add_colliders(prim):
46 # Iterate descendant prims (including root) and add colliders to mesh or primitive types
47 for desc_prim in Usd.PrimRange(prim):
48 if desc_prim.IsA(UsdGeom.Mesh) or desc_prim.IsA(UsdGeom.Gprim):
49 # Physics
50 if not desc_prim.HasAPI(UsdPhysics.CollisionAPI):
51 collision_api = UsdPhysics.CollisionAPI.Apply(desc_prim)
52 else:
53 collision_api = UsdPhysics.CollisionAPI(desc_prim)
54 collision_api.CreateCollisionEnabledAttr(True)
55
56 # Add mesh specific collision properties only to mesh types
57 if desc_prim.IsA(UsdGeom.Mesh):
58 if not desc_prim.HasAPI(UsdPhysics.MeshCollisionAPI):
59 mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(desc_prim)
60 else:
61 mesh_collision_api = UsdPhysics.MeshCollisionAPI(desc_prim)
62 mesh_collision_api.CreateApproximationAttr().Set("convexHull")
63
64
65# Enables rigid body dynamics (physics simulation) on the prim (having valid colliders is recommended)
66def add_rigid_body_dynamics(prim, disable_gravity=False, angular_damping=None):
67 # Physics
68 if not prim.HasAPI(UsdPhysics.RigidBodyAPI):
69 rigid_body_api = UsdPhysics.RigidBodyAPI.Apply(prim)
70 else:
71 rigid_body_api = UsdPhysics.RigidBodyAPI(prim)
72 rigid_body_api.CreateRigidBodyEnabledAttr(True)
73 # PhysX
74 if not prim.HasAPI(PhysxSchema.PhysxRigidBodyAPI):
75 physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI.Apply(prim)
76 else:
77 physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI(prim)
78 physx_rigid_body_api.GetDisableGravityAttr().Set(disable_gravity)
79 if angular_damping is not None:
80 physx_rigid_body_api.CreateAngularDampingAttr().Set(angular_damping)
81
82
83# Create a new prim with the provided asset URL and transform properties
84def create_asset(stage, asset_url, path, location=None, rotation=None, orientation=None, scale=None):
85 prim_path = omni.usd.get_stage_next_free_path(stage, path, False)
86 reference_url = asset_url if asset_url.startswith("omniverse://") else get_assets_root_path() + asset_url
87 prim = stage.DefinePrim(prim_path, "Xform")
88 prim.GetReferences().AddReference(reference_url)
89 set_transform_attributes(prim, location=location, rotation=rotation, orientation=orientation, scale=scale)
90 return prim
91
92
93# Create a new prim with the provided asset URL and transform properties including colliders
94def create_asset_with_colliders(stage, asset_url, path, location=None, rotation=None, orientation=None, scale=None):
95 prim = create_asset(stage, asset_url, path, location, rotation, orientation, scale)
96 add_colliders(prim)
97 return prim
98
99
100# Create collision walls around the top surface of the prim with the given height and thickness
101def create_collision_walls(stage, prim, bbox_cache=None, height=2, thickness=0.3, material=None, visible=False):
102 # Use the untransformed axis-aligned bounding box to calculate the prim surface size and center
103 if bbox_cache is None:
104 bbox_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
105 local_range = bbox_cache.ComputeWorldBound(prim).GetRange()
106 width, depth, local_height = local_range.GetSize()
107 # Raise the midpoint height to the prim's surface
108 mid = local_range.GetMidpoint() + Gf.Vec3d(0, 0, local_height / 2)
109
110 # Define the walls (name, location, size) with the specified thickness added externally to the surface and height
111 walls = [
112 ("floor", (mid[0], mid[1], mid[2] - thickness / 2), (width, depth, thickness)),
113 ("ceiling", (mid[0], mid[1], mid[2] + height + thickness / 2), (width, depth, thickness)),
114 ("left_wall", (mid[0] - (width + thickness) / 2, mid[1], mid[2] + height / 2), (thickness, depth, height)),
115 ("right_wall", (mid[0] + (width + thickness) / 2, mid[1], mid[2] + height / 2), (thickness, depth, height)),
116 ("front_wall", (mid[0], mid[1] + (depth + thickness) / 2, mid[2] + height / 2), (width, thickness, height)),
117 ("back_wall", (mid[0], mid[1] - (depth + thickness) / 2, mid[2] + height / 2), (width, thickness, height)),
118 ]
119
120 # Use the parent prim path to create the walls as children (use local coordinates)
121 prim_path = prim.GetPath()
122 collision_walls = []
123 for name, location, size in walls:
124 prim = stage.DefinePrim(f"{prim_path}/{name}", "Cube")
125 scale = (size[0] / 2.0, size[1] / 2.0, size[2] / 2.0)
126 set_transform_attributes(prim, location=location, scale=scale)
127 add_colliders(prim)
128 if not visible:
129 UsdGeom.Imageable(prim).MakeInvisible()
130 if material is not None:
131 mat_binding_api = UsdShade.MaterialBindingAPI.Apply(prim)
132 mat_binding_api.Bind(material, UsdShade.Tokens.weakerThanDescendants, "physics")
133 collision_walls.append(prim)
134 return collision_walls
135
136
137# Slide the assets independently in perpendicular directions and then pull them all together towards the given center
138async def apply_forces_async(stage, boxes, pallet, strength=550, strength_center_multiplier=2):
139 timeline = omni.timeline.get_timeline_interface()
140 timeline.play()
141 # Get the pallet center and forward vector to apply forces in the perpendicular directions and towards the center
142 pallet_tf: Gf.Matrix4d = UsdGeom.Xformable(pallet).ComputeLocalToWorldTransform(Usd.TimeCode.Default())
143 pallet_center = pallet_tf.ExtractTranslation()
144 pallet_rot: Gf.Rotation = pallet_tf.ExtractRotation()
145 force_forward = Gf.Vec3d(pallet_rot.TransformDir(Gf.Vec3d(1, 0, 0))) * strength
146 force_right = Gf.Vec3d(pallet_rot.TransformDir(Gf.Vec3d(0, 1, 0))) * strength
147
148 physx_api = get_physx_simulation_interface()
149 stage_id = UsdUtils.StageCache.Get().GetId(stage).ToLongInt()
150 for box_prim in boxes:
151 body_path = PhysicsSchemaTools.sdfPathToInt(box_prim.GetPath())
152 forces = [force_forward, force_right, -force_forward, -force_right]
153 for force in chain(forces, forces):
154 box_tf: Gf.Matrix4d = UsdGeom.Xformable(box_prim).ComputeLocalToWorldTransform(Usd.TimeCode.Default())
155 box_position = carb.Float3(*box_tf.ExtractTranslation())
156 physx_api.apply_force_at_pos(stage_id, body_path, carb.Float3(force), box_position, "Force")
157 for _ in range(10):
158 await omni.kit.app.get_app().next_update_async()
159
160 # Pull all box at once to the pallet center
161 for box_prim in boxes:
162 body_path = PhysicsSchemaTools.sdfPathToInt(box_prim.GetPath())
163 box_tf: Gf.Matrix4d = UsdGeom.Xformable(box_prim).ComputeLocalToWorldTransform(Usd.TimeCode.Default())
164 box_location = box_tf.ExtractTranslation()
165 force_to_center = (pallet_center - box_location) * strength * strength_center_multiplier
166 physx_api.apply_force_at_pos(stage_id, body_path, carb.Float3(*force_to_center), carb.Float3(*box_location))
167 for _ in range(20):
168 await omni.kit.app.get_app().next_update_async()
169 timeline.pause()
170
171
172# Create a new stage and and run the example scenario
173async def stack_boxes_on_pallet_async(pallet_prim, boxes_urls_and_weights, num_boxes, drop_height=1.5, drop_margin=0.2):
174 pallet_path = pallet_prim.GetPath()
175 print(f"[BoxStacking] Running scenario for pallet {pallet_path} with {num_boxes} boxes..")
176 stage = omni.usd.get_context().get_stage()
177 bbox_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
178
179 # Create a custom physics material to allow the boxes to easily slide into stacking positions
180 material_path = f"{pallet_path}/Looks/PhysicsMaterial"
181 default_material = UsdShade.Material.Define(stage, material_path)
182 physics_material = UsdPhysics.MaterialAPI.Apply(default_material.GetPrim())
183 physics_material.CreateRestitutionAttr().Set(0.0) # Inelastic collision (no bouncing)
184 physics_material.CreateStaticFrictionAttr().Set(0.01) # Small friction to allow sliding of stationary boxes
185 physics_material.CreateDynamicFrictionAttr().Set(0.01) # Small friction to allow sliding of moving boxes
186
187 # Apply the physics material to the pallet
188 mat_binding_api = UsdShade.MaterialBindingAPI.Apply(pallet_prim)
189 mat_binding_api.Bind(default_material, UsdShade.Tokens.weakerThanDescendants, "physics")
190
191 # Create collision walls around the top of the pallet and apply the physics material to them
192 collision_walls = create_collision_walls(
193 stage, pallet_prim, bbox_cache, height=drop_height + drop_margin, material=default_material
194 )
195
196 # Create the random boxes (without physics) with the specified weights and sort them by size (volume)
197 box_urls, box_weights = zip(*boxes_urls_and_weights)
198 rand_boxes_urls = random.choices(box_urls, weights=box_weights, k=num_boxes)
199 boxes = [create_asset(stage, box_url, f"{pallet_path}_Boxes/Box_{i}") for i, box_url in enumerate(rand_boxes_urls)]
200 boxes.sort(key=lambda box: bbox_cache.ComputeLocalBound(box).GetVolume(), reverse=True)
201
202 # Calculate the drop area above the pallet taking into account the pallet surface, drop height and the margin
203 # Note: The boxes can be spawned colliding with the surrounding collision walls as they will be pushed inwards
204 pallet_range = bbox_cache.ComputeWorldBound(pallet_prim).GetRange()
205 pallet_width, pallet_depth, pallet_heigth = pallet_range.GetSize()
206 # Move the spawn center at the given height above the pallet surface
207 spawn_center = pallet_range.GetMidpoint() + Gf.Vec3d(0, 0, pallet_heigth / 2 + drop_height)
208 spawn_width, spawn_depth = pallet_width / 2 - drop_margin, pallet_depth / 2 - drop_margin
209
210 # Use the pallet local-to-world transform to apply the local random offsets relative to the pallet
211 pallet_tf: Gf.Matrix4d = UsdGeom.Xformable(pallet_prim).ComputeLocalToWorldTransform(Usd.TimeCode.Default())
212 pallet_rot: Gf.Rotation = pallet_tf.ExtractRotation()
213
214 # Simulate dropping the boxes from random poses on the pallet
215 timeline = omni.timeline.get_timeline_interface()
216 for box_prim in boxes:
217 # Create a random location and orientation for the box within the drop area in local frame
218 local_loc = spawn_center + Gf.Vec3d(
219 random.uniform(-spawn_width, spawn_width), random.uniform(-spawn_depth, spawn_depth), 0
220 )
221 axes = [Gf.Vec3d(1, 0, 0), Gf.Vec3d(0, 1, 0), Gf.Vec3d(0, 0, 1)]
222 angles = [random.choice([180, 90, 0, -90, -180]) + random.uniform(-3, 3) for _ in axes]
223 local_rot = Gf.Rotation()
224 for axis, angle in zip(axes, angles):
225 local_rot *= Gf.Rotation(axis, angle)
226
227 # Transform the local pose to the pallet's world coordinate system
228 world_loc = pallet_tf.Transform(local_loc)
229 world_quat = Gf.Quatf((pallet_rot * local_rot).GetQuat())
230
231 # Set the spawn pose and enable collisions and rigid body dynamics with dampened angular movements
232 set_transform_attributes(box_prim, location=world_loc, orientation=world_quat)
233 add_colliders(box_prim)
234 add_rigid_body_dynamics(box_prim, angular_damping=0.9)
235
236 # Bind the physics material to the box (allow frictionless sliding)
237 mat_binding_api = UsdShade.MaterialBindingAPI.Apply(box_prim)
238 mat_binding_api.Bind(default_material, UsdShade.Tokens.weakerThanDescendants, "physics")
239 # Wait for an app update to load the new attributes
240 await omni.kit.app.get_app().next_update_async()
241
242 # Play simulation for a few frames for each box
243 timeline.play()
244 for _ in range(20):
245 await omni.kit.app.get_app().next_update_async()
246 timeline.pause()
247
248 # Iteratively apply forces to the boxes to move them around then pull them all together towards the pallet center
249 await apply_forces_async(stage, boxes, pallet_prim)
250
251 # Remove rigid body dynamics of the boxes until all other scenarios are completed
252 for box in boxes:
253 UsdPhysics.RigidBodyAPI(box).GetRigidBodyEnabledAttr().Set(False)
254
255 # Increase the friction to prevent sliding of the boxes on the pallet before removing the collision walls
256 physics_material.CreateStaticFrictionAttr().Set(0.9)
257 physics_material.CreateDynamicFrictionAttr().Set(0.9)
258
259 # Remove collision walls
260 for wall in collision_walls:
261 stage.RemovePrim(wall.GetPath())
262 return boxes
263
264
265# Run the example scenario
266async def run_box_stacking_scenarios_async(num_pallets=1, env_url=None):
267 # List of pallets and boxes to randomly choose from with their respective weights
268 pallets_urls_and_weights = [
269 ("/Isaac/Environments/Simple_Warehouse/Props/SM_PaletteA_01.usd", 0.25),
270 ("/Isaac/Environments/Simple_Warehouse/Props/SM_PaletteA_02.usd", 0.75),
271 ]
272 boxes_urls_and_weights = [
273 ("/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxA_01.usd", 0.02),
274 ("/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxB_01.usd", 0.06),
275 ("/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxC_01.usd", 0.12),
276 ("/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxD_01.usd", 0.80),
277 ]
278
279 # Load a predefined or create a new stage
280 if env_url is not None:
281 env_path = env_url if env_url.startswith("omniverse://") else get_assets_root_path() + env_url
282 omni.usd.get_context().open_stage(env_path)
283 stage = omni.usd.get_context().get_stage()
284 else:
285 omni.usd.get_context().new_stage()
286 stage = omni.usd.get_context().get_stage()
287 distant_light = stage.DefinePrim("/World/Lights/DistantLight", "DistantLight")
288 distant_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(400.0)
289 if not distant_light.HasAttribute("xformOp:rotateXYZ"):
290 UsdGeom.Xformable(distant_light).AddRotateXYZOp()
291 distant_light.GetAttribute("xformOp:rotateXYZ").Set((0, 60, 0))
292 dome_light = stage.DefinePrim("/World/Lights/DomeLight", "DomeLight")
293 dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(500.0)
294
295 # Spawn the pallets
296 pallets = []
297 pallets_urls, pallets_weights = zip(*pallets_urls_and_weights)
298 rand_pallet_urls = random.choices(pallets_urls, weights=pallets_weights, k=num_pallets)
299 # Custom pallet poses for the evnironment
300 custom_pallet_locations = [
301 (-9.3, 5.3, 1.3),
302 (-9.3, 7.3, 1.3),
303 (-9.3, -0.6, 1.3),
304 ]
305 random.shuffle(custom_pallet_locations)
306 for i, pallet_url in enumerate(rand_pallet_urls):
307 # Use a custom location for every other pallet
308 if env_url is not None:
309 if i % 2 == 0 and custom_pallet_locations:
310 rand_loc = Gf.Vec3d(*custom_pallet_locations.pop())
311 else:
312 rand_loc = Gf.Vec3d(-6.5, i * 1.75, 0) + Gf.Vec3d(random.uniform(-0.2, 0.2), random.uniform(0, 0.2), 0)
313 else:
314 rand_loc = Gf.Vec3d(i * 1.5, 0, 0) + Gf.Vec3d(random.uniform(0, 0.2), random.uniform(-0.2, 0.2), 0)
315 rand_rot = (0, 0, random.choice([180, 90, 0, -90, -180]) + random.uniform(-15, 15))
316 pallet_prim = create_asset_with_colliders(
317 stage, pallet_url, f"/World/Pallet_{i}", location=rand_loc, rotation=rand_rot
318 )
319 pallets.append(pallet_prim)
320
321 # Stack the boxes on the pallets
322 total_boxes = []
323 for pallet in pallets:
324 if env_url is not None:
325 rand_num_boxes = random.randint(8, 15)
326 stacked_boxes = await stack_boxes_on_pallet_async(
327 pallet, boxes_urls_and_weights, num_boxes=rand_num_boxes, drop_height=1.0
328 )
329 else:
330 rand_num_boxes = random.randint(12, 20)
331 stacked_boxes = await stack_boxes_on_pallet_async(pallet, boxes_urls_and_weights, num_boxes=rand_num_boxes)
332 total_boxes.extend(stacked_boxes)
333
334 # Re-enable rigid body dynamics of the boxes and run the simulation for a while
335 for box in total_boxes:
336 UsdPhysics.RigidBodyAPI(box).GetRigidBodyEnabledAttr().Set(True)
337 timeline = omni.timeline.get_timeline_interface()
338 timeline.play()
339 for _ in range(200):
340 await omni.kit.app.get_app().next_update_async()
341 timeline.pause()
342
343
344async def run_scenarios_async():
345 await run_box_stacking_scenarios_async(num_pallets=6)
346 await run_box_stacking_scenarios_async(num_pallets=6, env_url="/Isaac/Environments/Simple_Warehouse/warehouse.usd")
347
348
349asyncio.ensure_future(run_scenarios_async())