Using BBoxCache to make an auto Guide Cube

Hey all,

I’m in the process of trying to create an Auto Guide button for our asset team.
I have the geo(character,prop whatever) in the model.usd. I compute the bbox for that geometry and would then like to “apply” that to a cube as its new extents.
Is this possible or am I barking up the wrong tree here?

Thanks Sue

I’d say this note in the documentation about Extents/Bounds is related.

And if you were to compute the extents manually afterwards make sure to do so for every timecode that the object changes in a way that influences its size as described here

The way I understand what you’re trying to do is:

from pxr import UsdGeom

prims = stage.Traverse()
for prim in prims:
    if not prim.IsA(UsdGeom.Boundable):
    boundable = UsdGeom.Boundable(prim)
    extent_attr = boundable.GetExtentAttr()
    has_value = False
    if extent_attr.IsValid() and extent_attr.IsAuthored():
        # TODO: Technically this should figure out whether all time samples had their extents computed
        # A value was already set
        print(f"{prim} has extent already set")
    if not extent_attr.IsValid():
        extent_attr = boundable.CreateExtentAttr()
    # TODO: This should actually compute for all timecodes
    time = stage.GetStartTimeCode()
    extent = boundable.ComputeExtent(time)
    print(f"Setting {prim} extent {extent} at time {time}")

Note the todos in there as I’ve marked a lot of it psuedocode. A reference that shows how to potentially use the BBoxCache is in this example

Is this roughly your goal?

This does not use the BBoxCache at all since as far as I know it’s mostly intended to be used for bbox queries against xforms (parents of leaf shapes) to compute their global size at a point in time which as the UsdGeomBoundable documentation mentions is not intended to be written into the usd files:

Therefore, we limit to precomputing (generally) leaf-prim extent, which avoids the need to read in large point arrays to compute bounds, and provides UsdGeomBBoxCache the means to efficiently compute and (session-only) cache intermediate bounds. You are free to compute and author intermediate bounds into your scenes, of course, which may work well if you have sufficient locks on your pipeline to guarantee that once authored, the geometry and transforms upon which they are based will remain unchanged, or if accuracy of the bounds is not an ironclad requisite.

This is the code I have so far:

from pxr import Gf,  Sdf, Usd, UsdGeom, Vt

stage = Usd.Stage.Open(guide_path)
# Setup scene
root_prim_path = Sdf.Path("/root")
root_prim = stage.DefinePrim(root_prim_path, "Xform")
cone_prim_path = Sdf.Path("/root/cone")
cone_prim = stage.DefinePrim(cone_prim_path, "Cone")
guide = stage.DefinePrim("/guide", "Cube")

root_xformable = UsdGeom.Xformable(root_prim)
root_translate_op = root_xformable.AddTranslateOp()
root_translate_op.Set(Gf.Vec3h([50, 30, 10]))
root_rotate_op = root_xformable.AddRotateZOp()
cone_xformable = UsdGeom.Xformable(cone_prim)
cone_translate_op = cone_xformable.AddTranslateOp()
cone_rotate_op = cone_xformable.AddRotateXYZOp()

time_code = Usd.TimeCode.Default() # Determine frame to lookup
bbox_cache = UsdGeom.BBoxCache(time_code, [UsdGeom.Tokens.default_, UsdGeom.Tokens.render],
                               useExtentsHint=False, ignoreVisibility=False)

# Useful for intersection testing:
bbox = bbox_cache.ComputeWorldBound(cone_prim)
root_geom_model_API = UsdGeom.ModelAPI.Apply(guide)
aligned_range = bbox.ComputeAlignedRange()
extentsHint = Vt.Vec3hArray([Gf.Vec3h(list(aligned_range.GetMin())), Gf.Vec3h(list(aligned_range.GetMax()))])
root_geom_model_API.SetExtentsHint(extentsHint, time_code)

So I get extents onto the cube but it doesnt move to where my cone is…
So this is why im asking if im missunderstanding how to re-apply the bbox info back onto the cube

Could it be that your cube has extents explicitly defined that override the extents hint and that what you’re seeing as BBox info is the extents which you don’t appear to be setting?

If I’m not mistaken these store into different attributes extent and extentsHint.

What the usd looks like inside. I will change extentsHints to extents see what happens

what I’m seeing in Houdini:

I would have expected the cube to be at the same place as the cone? I’m i understanding this correctly

A few things, @Suekieza

  1. Setting extent or extentsHint is never going to change an objects rendered or evaluated position - that should always be computed from the Xformable hierarchy directly, plus the geometry. THe only possible exception would be if you are using GeomModelAPI Draw Mode BBox.
  2. extentsHint is a Vec3fArray, not a Vec3hArray
  3. extentsHint is always optional, and intended to purely be a speed optimization that clients can opt into, so setting it to something other than would be computed from first-principles based on extent attrs and xforms is likely to lead to inconsistent behavior.

So, you could author the extent of individual Boundable prims to whatever you want, but it may (or may not!) affect rendering, physics, etc in surprising ways.

1 Like

Hey hey, Thanks Spiff and Roy for all the help :smiley: I got something working will share tomorrow when its not just a jumbled mess of things

Hey hey, so here is the code(simplified) that I ended up with to make my guide cube ;D

from pxr import Gf, Sdf, Usd, UsdGeom

# Path to the USD file that will contain the guide mesh
guide_path = "./guide.usda"

def get_extent(stage, prim):
    Calculates & sets the extentsHint of a prim based on its World Bounds

        stage (Usd.Stage): The USD stage containing the primitive.
        prim (str or Usd.Prim): The address to the primitive such as '/root/cube_model'
                                or the prim object itself.

        extents_hint (Vec3fArray): A two-by-three array containing the endpoints of the
                                   axis-aligned, object-space extent.
    # Convert prim_path string to prim
    if isinstance(prim, str):
        prim_path = prim
        prim = stage.GetPrimAtPath(prim_path)

    # Create a BBoxCache to compute the bounding box
    bbox_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(),

    # Compute the world bounds of the primitive
    bbox_bounds = bbox_cache.ComputeWorldBound(prim)
    bound_align = bbox_bounds.ComputeAlignedBox()
    bbox_min = bound_align.GetMin()
    bbox_max = bound_align.GetMax()

    def convert_to_float(number):
        # Corrects floating-point formatting issues
        if "e" in str(number):
            string_min = str(number)
            correction = string_min.split("e")
            return float(correction[0])
        return number

    # Correct the bounding box values
    corrected_min = [convert_to_float(number) for number in bbox_min]
    corrected_max = [convert_to_float(number) for number in bbox_max]

    # Compute extents
    extents = [

    # Define vertex points for the cube mesh
    vertex_points = [
        Gf.Vec3d(corrected_max[0], corrected_min[1], corrected_max[2]),
        Gf.Vec3d(corrected_min[0], corrected_min[1], corrected_max[2]),
        Gf.Vec3d(corrected_max[0], corrected_max[1], corrected_max[2]),
        Gf.Vec3d(corrected_min[0], corrected_max[1], corrected_max[2]),
        Gf.Vec3d(corrected_min[0], corrected_min[1], corrected_min[2]),
        Gf.Vec3d(corrected_max[0], corrected_min[1], corrected_min[2]),
        Gf.Vec3d(corrected_min[0], corrected_max[1], corrected_min[2]),
        Gf.Vec3d(corrected_max[0], corrected_max[1], corrected_min[2]),
    return extents, vertex_points

def create_cube_mesh(stage, bbox_extent, vertex_points, default_prim):
    Creates a cube mesh in the USD stage.

        stage (Usd.Stage): The USD stage where the cube mesh will be created.
        bbox_extent (Vec3fArray): Extents of the bounding box.
        vertex_points (list of Gf.Vec3d): List of vertex points for the cube.
        default_prim (Usd.Prim): The default primitive where the cube will be added.

        Usd.Prim: The created cube mesh primitive.
    # Create hierarchy for the cube mesh
    asset_prim = stage.DefinePrim(f"{default_prim.GetPath()}", "Xform")
    model_prim = stage.DefinePrim(f"{default_prim.GetPath()}/model", "Xform")
    guide_prim = stage.DefinePrim(f"{default_prim.GetPath()}/model/guide", "Xform")
    cube_mesh = stage.DefinePrim(f"{default_prim.GetPath()}/model/guide/box", "Mesh")

    # Define attributes for the cube mesh
    face_vertex_counts_attribute = cube_mesh.CreateAttribute("faceVertexCounts", Sdf.ValueTypeNames.Int)
    face_vertex_counts_attribute.Set([4, 4, 4, 4, 4, 4])

    points_attribute = cube_mesh.CreateAttribute("points", Sdf.ValueTypeNames.Point3f)

    extents_cube = cube_mesh.CreateAttribute("extent", Sdf.ValueTypeNames.Vector3fArray)

    face_vertex_indices_attribute = cube_mesh.CreateAttribute("faceVertexIndices", Sdf.ValueTypeNames.Int)
    face_vertex_indices_attribute.Set([0, 1, 3, 2, 4, 5, 7, 6, 6, 7, 2, 3, 5, 4, 1, 0, 5, 0, 2, 7, 1, 4, 6, 3])

    return cube_mesh

# Path to the USD file containing the model
model_path = "./geo.usda"

# Open the USD stage for the model
model_stage = Usd.Stage.Open(model_path)

# Get the default prim of the model
asset_prim = model_stage.GetDefaultPrim()
print(f"Auto Guide will be calculated for: {asset_prim.GetPath()}")

# Calculate bounding box extent and vertex points
bbox_extent, vertex_points = get_extent(model_stage, asset_prim)

# Create a new USD stage for the guide and create the cube mesh
stage = Usd.Stage.CreateNew(guide_path)
cube_mesh = create_cube_mesh(stage, bbox_extent, vertex_points, asset_prim)

# Save the new USD stage with the cube mesh