Replicator way of doing usd_scene_construction_utils

Dear Team,

Could you provide an example how to achieve random stacks with Replicator to achieve the results such as the ones described here: GitHub - NVIDIA-Omniverse/usd_scene_construction_utils: Utilities and examples for constructing scenes with the USD Python API

That repo works nicely but it doesn’t operate on ReplicatorItem objects so any randomization becomes quite problematic/impossible.

cc @pcallender @jlafleche

crickets here cc @dennis.lynch

Hello @Turowicz, tere are a few options for better compatibility depending on your setup and what you wan to achieve. What kind of randomization are you trying to do together with the utilities?

Here are some options for integrating the tools with Replicator:

Option A: Full Python workflow + Replicator
It’s possible to mix and match a python workflow with replicator randomization. The friction often comes from needing to get prim paths out of replicator functions. This can be done through:

boxes = rep.get.prims(semantics=[["class", "box"]])
box_prims = .get_output_prims()["prims"]   # where "prims" is the node output attribute that we want prims from

Option B: Extend Replicator with new functionality
If you are creating your own Kit Extension, you can create new OmniGraph nodes that implement novel randomization functionality and use the usd_scene_construction_utils as needed. Otherwise, it’s still possible to use the usd scene construction utilities at prim creation time and to assemble existing Replicator functionality in new ways. Here’s a larger example doing this where we register a new box_stacks randomizer:

import random
import omni.replicator.core as rep
import omni.usd
import os
import sys
from typing import Tuple, Union, List
from pxr import Usd, Sdf, Tf
sys.path.append("/home/jlafleche/Projects/usd_scene_construction_utils/")
from usd_scene_construction_utils import (
    add_usd_ref,
    add_xform,
    compute_bbox,
    compute_bbox_center,
    rotate_x,
    rotate_y,
    rotate_z,
    scale,
    translate,
)

# Create a box stack creation function
@rep.utils.ReplicatorWrapper
def box_stack(
    count: int = 1,
    name: str = None,
    parent: Union[rep.utils.ReplicatorItem, str, Sdf.Path, Usd.Prim] = None,
    semantics: List[Tuple[str, str]] = None,
):
    if count < 1 or not isinstance(count, int):
        raise ValueError("`count` must be a positive integer")

    if isinstance(parent, str) and not Tf.IsValidIdentifier(parent):
        raise ValueError(f"`parent` must be a valid prim path")
    
    stage = omni.usd.get_context().get_stage()
    if parent and not stage.GetPrimAtPath(str(parent)):
        raise ValueError(f"Unable to find `parent` prim: {parent}")

    # Create a random stack of boxes 
    box_asset_url = "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Flat_A/FlatBox_A02_15x21x8cm_PR_NVD_01.usd"

    def add_box_of_size(
            stage,
            path: str, 
            size: Tuple[float, float, float]
        ):
        """Adds a box and re-scales it to match the specified dimensions
        """

        # Add USD box
        prim = add_usd_ref(stage, path, usd_path=box_asset_url)

        rotate_x(prim, random.choice([-90, 0, 90, 180]))
        rotate_y(prim, random.choice([-90, 0, 90, 180]))

        # Scale USD box to fit dimensions
        usd_min, usd_max = compute_bbox(prim)
        
        usd_size = (
            usd_max[0] - usd_min[0],
            usd_max[1] - usd_min[1],
            usd_max[2] - usd_min[2]
        )

        required_scale = (
            size[0] / usd_size[0],
            size[1] / usd_size[1],
            size[2] / usd_size[2]
        )

        scale(prim, required_scale)
        return prim


    def add_random_box_stack(
            stage,
            path: str, 
            count_range=(1, 5),
            size_range=((30, 30, 10), (50, 50, 25)),
            angle_range=(-5, 5),
            jitter_range=(-3,3)
        ):
        container = add_xform(stage, path)
        count = random.randint(*count_range)

        # get sizes and sort
        sizes = [
            (
                random.uniform(size_range[0][0], size_range[1][0]),
                random.uniform(size_range[0][1], size_range[1][1]),
                random.uniform(size_range[0][2], size_range[1][2])
            )
            for i in range(count)
        ]

        sizes = sorted(sizes, key=lambda x: x[0]**2 + x[1]**2, reverse=True)

        boxes = []
        for i in range(count):
            box_i = add_box_of_size(stage, os.path.join(path, f"box_{i}"), sizes[i])
            boxes.append(box_i)

        if count > 0:
            center = compute_bbox_center(boxes[0])
            for i in range(1, count):
                prev_box, cur_box = boxes[i - 1], boxes[i]
                cur_bbox = compute_bbox(cur_box)
                cur_center = compute_bbox_center(cur_box)
                prev_bbox = compute_bbox(prev_box)
                offset = (
                    center[0] - cur_center[0],
                    center[1] - cur_center[1],
                    prev_bbox[1][2] - cur_bbox[0][2]
                )
                translate(cur_box, offset)
            
            # add some noise
            for i in range(count):
                rotate_z(boxes[i], random.uniform(*angle_range))
                translate(boxes[i], (
                    random.uniform(*jitter_range),
                    random.uniform(*jitter_range),
                    0
                ))
        
        if semantics:
            rep.utils._set_semantics(container, semantics)
        return container, boxes

    def add_random_box_stacks(
            stage,
            path: str,
            count_range=(0, 3),
        ):
        container = add_xform(stage, path)
        stacks = []
        count = random.randint(*count_range)
        for i in range(count):
            stack, items = add_random_box_stack(stage, os.path.join(path, f"stack_{i}"))
            stacks.append(stack)

        for i in range(count):
            cur_stack = stacks[i]
            cur_bbox = compute_bbox(cur_stack)
            cur_center = compute_bbox_center(cur_stack)
            translate(cur_stack, (0, -cur_center[1], -cur_bbox[0][2]))
            if i > 0:
                prev_bbox = compute_bbox(stacks[i - 1])
                translate(cur_stack, (prev_bbox[1][0] - cur_bbox[0][0], 0, 0))    
        return container, stacks

    stacks = []
    suffix = ""
    if parent is None:
        parent = rep.create.REPLICATOR_SCOPE
    elif isinstance(parent, Usd.Prim):
        parent = str(parent.GetPath())
    elif isinstance(parent, rep.utils.ReplicatorItem):
        # Use first prim as parent
        parent = parent.get_output("prims")[0]
    if name is None:
        name = "BoxStack"
    for c in range(count):
        box_stacks_path = omni.usd.get_stage_next_free_path(stage, f"{parent}/{name}{suffix}", False)
        add_random_box_stacks(stage, box_stacks_path, count_range=(1,4))
        stacks.append(box_stacks_path)
        suffix = f"_{c}"
    
    return rep.create.group(stacks)

# Register box stack function so it's now part of replicator
rep.create.register(box_stack)

# Create box stack randomizer by combining replicator nodes
@rep.utils.ReplicatorWrapper
def box_stacks(surface_prims: rep.utils.ReplicatorItem, count: int = 1):
    stacks = rep.create.box_stack(count=count, semantics=[("class", "box_stack")])
    with stacks:
        rep.randomizer.scatter_2d(surface_prims=surface_prims)
        rep.modify.pose(rotation=rep.distribution.uniform((0, 0, -180), (0, 0, 180)))

# Register randomizer
rep.randomizer.register(box_stacks)

# Set stage defaults
rep.settings.set_stage_up_axis("Z")
rep.settings.set_stage_meters_per_unit(1.0)

# Get main scene
warehouse = rep.create.from_usd("omniverse://localhost/NVIDIA/Assets/Isaac/4.2/Isaac/Environments/Simple_Warehouse/full_warehouse.usd")
with warehouse:
    rep.modify.pose(scale=100)

# Use Replicator to create and randomize stack placement on the floor
floor = rep.get.prims(semantics=[("class", "floor")])
with rep.trigger.on_frame():
    rep.randomizer.box_stacks(surface_prims=floor, count=100)

Thank you for the extensive explanation. I will have a go at this early next week. I am trying to generate stacks of boxes and pallets, then scatter them around different scenes together with other objects.

Running your example works, thanks! It saved me A LOT of time!

Dear @jlafleche

Thank you for help, it is working.

I have noticed that when I scatter the stacks they are not regenerated on each orchestrator step. Meaning, we always have the same stacks being scattered around. Would it be possible to modify your example in order to force the replicator to generate completely new stacks on each step?

I have modified your heavily code to include other object types, but I seem to be unable to achieve the above on my own.

This is what I mean. Stacks are all the same, with just the colour randomization as I am modifying the material tints.



This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.