Scatter3d on scatter2d-ed volume meshes

Dear @jlafleche and @pcallender

I am trying to spawn crates with watertight volumes inside of them. The volumes are not rendered but they are there. After the crates are scattered2d on the surface, I want to scatter3d boxes inside those volumes.

Unfortunately the boxes always appear where the volumes (crates) have been in the previous randomization.

Code:

from box_spawner import BOXES
from base_spawner import BaseSpawner
from scenes.base_scene_config import SceneConfig
from omni.replicator.core.scripts.utils.utils import ReplicatorItem

import omni.replicator.core as rep

CRATE = "file:///home/wojtek/Workspace/Surveily.Omniverse/Library/envs/warehouse_props/crates_inpost.usd"

class CrateSpawner(BaseSpawner):
    def __init__(self):
        super().__init__()
        self.box_pool : list[ReplicatorItem] = []
        self.box_poolGroup : ReplicatorItem
        self.crate_pool : list[ReplicatorItem] = []
        self.crate_poolGroup : ReplicatorItem

    async def load(self, config : SceneConfig):
        while len(self.box_pool) < config.boxCount*config.crateCount:
            for box_url in BOXES:
                if len(self.box_pool) < config.boxCount*config.crateCount:
                    self.box_pool.append(rep.create.from_usd(usd=box_url))

        while len(self.crate_pool) < config.crateCount:
            self.crate_pool.append(rep.create.from_usd(usd=CRATE))

        self.box_poolGroup = rep.create.group(items=self.box_pool)
        self.crate_poolGroup = rep.create.group(items=self.crate_pool)

    def on_trigger(self, seed : int, floor : lambda : ReplicatorItem, config):
        if config.crateCount > 0:
            # Randomize cages
            with self.crate_poolGroup:
                rep.modify.pose(scale=rep.distribution.uniform(0.8,1.2),rotation=rep.distribution.uniform([0, 0, -180],[0, 0, 180]))
                rep.modify.variant(name="fill_type",value="full")
                rep.modify.variant(name="crate_type",value=rep.distribution.choice(choices=["wheeled","pallet"]))
                rep.randomizer.scatter_2d(seed=seed,surface_prims=[floor()],check_for_collisions=True,min_samp=config.scatterRange[0],max_samp=config.scatterRange[1])

            # Randomize boxes in cages
            with self.box_poolGroup:
                rep.modify.pose(scale=rep.distribution.uniform(0.8,1.2),rotation=rep.distribution.uniform([0, 0, -180],[0, 0, 180]))
                rep.randomizer.scatter_3d(volume_prims=rep.get.mesh(path_pattern="\/volume_full",cache_result=True),check_for_collisions=True)

Video:

Started getting random errors:

2025-02-20 16:13:19 [1,029,771ms] [Error] [omni.graph.core.plugin] /Replicator/SDGPipeline/OgnScatter3D: [/Replicator/SDGPipeline] Assertion raised in compute - operands could not be broadcast together with shapes (3,7,30) (3,6,31) (3,7,30) 
  File "/home/wojtek/Documents/Applications/isaac-sim-standalone@4.5.0-rc.36+release.19112.f59b3005.gl.linux-x86_64.release/extscache/omni.replicator.core-1.11.35+106.5.0.lx64.r.cp310/omni/replicator/core/ogn/python/_impl/nodes/OgnScatter3D.py", line 208, in compute
    voxel_vol = mvu.VoxelUtils._rm_overlap_direct(
  File "/home/wojtek/Documents/Applications/isaac-sim-standalone@4.5.0-rc.36+release.19112.f59b3005.gl.linux-x86_64.release/extscache/omni.replicator.core-1.11.35+106.5.0.lx64.r.cp310/omni/replicator/core/scripts/utils/mesh_voxel_utils.py", line 678, in _rm_overlap_direct
    voxel_vol[

I’ve made an isolated repro and it seems to work OK:

import asyncio
import omni.replicator.core as rep

async def main():
    plane = rep.create.plane(scale=10)
    volume = rep.create.cylinder(scale=2,visible=False)

    with rep.trigger.on_frame(max_execs=1000):
        with volume:
            rep.randomizer.scatter_2d(surface_prims=[plane], check_for_collisions=True)
        
        with rep.create.cone(scale=0.2,
                            rotation=rep.distribution.uniform((-90,-90,-90),(90,90,90)),
                            count=50):
            rep.randomizer.color(colors=rep.distribution.uniform((0, 0, 0), (1, 1, 1)))
            rep.randomizer.scatter_3d(volume_prims=[volume], check_for_collisions=True)

asyncio.run(main())

But this whole thing is seriously unstable, look at what heppens with the cones when I click the close window button (X). I guess ANYTHING can break the scattering even as unrelated as a close window dialog.

OK I’ve found the repro. The scattering3d breaks when we spawn box assets instead of cone primitives and add rotation to the metal crate. That is the killer combination. If we remove rotation it works.

Use the code below with the crate asset I sent you in DMs @pcallender @jlafleche:

REPRO:

import asyncio
import omni.replicator.core as rep

CRATE = "crates_inpost.usd"
BOXES = [
            # Remote
            "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",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Flat_A/FlatBox_A06_28x30x8cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Cube_A/CubeBox_A01_10cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Cube_A/CubeBox_A03_21cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Cube_A/CubeBox_A06_42cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Long_A/LongBox_A01_8x16x8cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Long_A/LongBox_A04_11x51x10cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/White_A/WhiteCorrugatedBox_A03_15x15x15cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/White_A/WhiteCorrugatedBox_A22_30x41x30cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/White_A/WhiteCorrugatedBox_A31_25x30x15cm_PR_NVD_01.usd",
]

async def main():
    plane = rep.create.plane(scale=1000)
    crate = rep.create.from_usd(usd=CRATE)

    with rep.trigger.on_frame(max_execs=1000):
        with crate:
            rep.modify.pose(rotation=rep.distribution.uniform([0, 0, -180],[0, 0, 180]))
            rep.randomizer.scatter_2d(surface_prims=[plane], check_for_collisions=True)
        
        with rep.randomizer.instantiate(paths=BOXES,size=3,mode="scene_instance"):
            rep.randomizer.scatter_3d(volume_prims=rep.get.mesh(path_pattern="\/volume_full",cache_result=True), check_for_collisions=True)


asyncio.run(main())

Thanks for the repro. I brought this up to the engineers today.

Thanks for all the info @Turowicz! I believe we understand the issue. The scattering of the crates relies on ensuring that the volume_prim modifications were done first. To ensure scheduling, the system relies on checking to see if any of the volume prims were previously used in any of the nodes already and adjust the execution graph accordingly. The problem you’re encountering is that because the prim used as a volume isn’t directly moved (its parent is moved), that check comes up empty.

If you use the crate prim directly as the volume, the problem goes away:
rep.randomizer.scatter_3d(volume_prims=crate, check_for_collisions=True)

Of course, this is not ideall as you want to be able to constrain scattering to a specific sub-mesh. We’ll do this by specifically setting the cube as a graph dependency with a new function:

def depend_on(replicator_item: rep.utils.ReplicatorItem):
    if not isinstance(replicator_item, rep.utils.ReplicatorItem):
        raise ValueError(f"Expected object of type 'ReplicatorItem', got {type(replicator_item)}.")
    # Get last execution in the subgraph starting with `replicator_item`
    last_exec = rep.utils._get_last_exec_attr(replicator_item.node)
    # Add last exec's node to the top of the exec stack
    replicator_item._exec_stack.append(last_exec.get_node())

The full code:

import asyncio
import omni.replicator.core as rep

CRATE = "/home/jlafleche/Downloads/warehouse_props/crates_inpost.usd"
BOXES = [
            # Remote
            "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",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Flat_A/FlatBox_A06_28x30x8cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Cube_A/CubeBox_A01_10cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Cube_A/CubeBox_A03_21cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Cube_A/CubeBox_A06_42cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Long_A/LongBox_A01_8x16x8cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/Long_A/LongBox_A04_11x51x10cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/White_A/WhiteCorrugatedBox_A03_15x15x15cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/White_A/WhiteCorrugatedBox_A22_30x41x30cm_PR_NVD_01.usd",
            "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/DigitalTwin/Assets/Warehouse/Shipping/Cardboard_Boxes/White_A/WhiteCorrugatedBox_A31_25x30x15cm_PR_NVD_01.usd",
]

def depend_on(replicator_item: rep.utils.ReplicatorItem):
    if not isinstance(replicator_item, rep.utils.ReplicatorItem):
        raise ValueError(f"Expected object of type 'ReplicatorItem', got '{type(replicator_item)}'.")

    last_exec = rep.utils._get_last_exec_attr(replicator_item.node)
    replicator_item._exec_stack.append(last_exec.get_node())

async def main():
    plane = rep.create.plane(scale=1000)
    crate = rep.create.from_usd(usd=CRATE)

    with rep.trigger.on_frame(max_execs=1000):
        with crate:
            rep.modify.pose(rotation=rep.distribution.uniform([0, 0, -180],[0, 0, 180]))
            rep.randomizer.scatter_2d(surface_prims=[plane], check_for_collisions=True)

        with rep.randomizer.instantiate(paths=BOXES,size=3,mode="scene_instance"):
            depend_on(crate)
            rep.randomizer.scatter_3d(volume_prims=rep.get.mesh(path_pattern="\/volume_full",cache_result=True), check_for_collisions=True)

asyncio.run(main())

Thank you so much for reporting this @Turowicz, we’ll be looking at making improvements to make this type of situation much easier to handle in the future.

1 Like