Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions scripts/tutorials/compute_viewpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import argparse
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this script for?

Copy link
Contributor Author

@zhizhe258 zhizhe258 Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To compute the new scene viewpoint. Customer can input the perspective info in Isaacsim GUI and get the relative viewpoint parameters.


import numpy as np
from scipy.spatial.transform import Rotation as R


def compute_lookat(eye, euler_deg, distance=2.0):
"""Compute lookat point from eye position and euler angles.

Args:
eye: Camera eye position [x, y, z]
euler_deg: Camera rotation in degrees [roll, pitch, yaw]
distance: Distance from eye to lookat point (default: 2.0)
"""
eye = np.array(eye)

rot = R.from_euler("xyz", euler_deg, degrees=True)
rot_matrix = rot.as_matrix()

forward = rot_matrix @ np.array([0, 0, -1])

lookat = eye + forward * distance

print(f" self.viewer.eye = ({eye[0]:.5f}, {eye[1]:.5f}, {eye[2]:.5f})")
print(f" self.viewer.lookat = ({lookat[0]:.5f}, {lookat[1]:.5f}, {lookat[2]:.5f})")


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Compute camera lookat point from eye position and euler angles",
formatter_class=argparse.RawDescriptionHelpFormatter,
)

parser.add_argument(
"--eye",
type=float,
nargs=3,
required=True,
metavar=("X", "Y", "Z"),
help="Camera eye position [x, y, z]",
)
parser.add_argument(
"--euler",
type=float,
nargs=3,
required=True,
metavar=("ROLL", "PITCH", "YAW"),
help="Camera rotation in degrees [roll, pitch, yaw]",
)
parser.add_argument(
"--distance",
type=float,
default=2.0,
help="Distance from eye to lookat point (default: 2.0)",
)

args = parser.parse_args()

compute_lookat(args.eye, args.euler, distance=args.distance)
147 changes: 100 additions & 47 deletions scripts/tutorials/marble_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

import argparse
import os
from pathlib import Path

import numpy as np
Expand All @@ -28,6 +29,12 @@
"cube": {
"single": "LeIsaac-SO101-LiftCube-v0",
},
"burger": {
"dual": "LeIsaac-SO101-AssembleHamburger-BiArm-v0",
},
"sausage": {
"dual": "LeIsaac-SO101-SausageCut-BiArm-Direct-v0",
},
}

TASK_CONFIG = {
Expand Down Expand Up @@ -58,7 +65,7 @@
],
"source_scene": "scenes/lightwheel_toyroom/scene.usd",
"assets_subpath": "scenes/lightwheel_toyroom/Assets",
"table_name": "KidRoom_Table01",
"platform_name": "KidRoom_Table01",
},
"orange": {
"objects": ["Orange001", "Orange002", "Orange003", "Plate"],
Expand All @@ -69,13 +76,32 @@
"objects": ["Table038_01", "cloth"],
"source_scene": "scenes/lightwheel_bedroom/scene.usd",
"assets_subpath": "scenes/lightwheel_bedroom",
"table_name": "Table038_01",
"platform_name": "Table038_01",
},
"cube": {
"objects": ["cube"],
"source_scene": "scenes/table_with_cube/scene.usd",
"assets_subpath": "scenes/table_with_cube/cube",
},
"burger": {
"objects": [
"Burger_ChoppingBlock",
"Burger_Plate",
"Burger_Beef_Patties001",
"Burger_Cheese001",
"Burger_Bread002",
],
"source_scene": "scenes/kitchen_with_burger/scene.usd",
"assets_subpath": "scenes/kitchen_with_burger/objects/burger/Assets",
"parent_prim": "Burger",
"platform_name": "Burger_ChoppingBlock",
},
"sausage": {
"objects": ["ChoppingBlock"],
"source_scene": "scenes/kitchen_with_sausage/scene.usd",
"assets_subpath": "scenes/kitchen_with_sausage/objects",
"platform_name": "ChoppingBlock",
},
}


Expand Down Expand Up @@ -135,39 +161,46 @@ def compute_scene_transform(orig_pos, orig_quat, target_pos, target_quat):

def get_object_usd_path(task_type: str, obj_name: str, assets_base: str) -> str:
"""Resolve USD file path for an object"""
config = TASK_CONFIG[task_type]
table_name = config.get("table_name")

if task_type == "toys":
if obj_name == table_name:
platform_name = TASK_CONFIG[task_type].get("platform_name")
if obj_name == platform_name:
return f"{assets_base}/{obj_name}/{obj_name}.usd"
name = obj_name[:-3] if obj_name.endswith("_01") else obj_name
return f"{assets_base}/Kit1/{name}.usd"

if task_type == "orange":
return f"{assets_base}/{obj_name}/{obj_name}.usd"

if task_type == "cloth":
if obj_name == table_name:
platform_name = TASK_CONFIG[task_type].get("platform_name")
if obj_name == platform_name:
folder = obj_name[:-3] if obj_name.endswith("_01") else obj_name
return f"{assets_base}/LW_Loft/Loft/{folder}/{folder}.usd"
return f"{assets_base}/cloth/cloth.usd"

if task_type == "cube":
return f"{assets_base}/cube.usd"

raise ValueError(f"Unknown object: {obj_name}")
# Common pattern: orange, burger, sausage
if task_type in ("orange", "burger", "sausage"):
return f"{assets_base}/{obj_name}/{obj_name}.usd"

raise ValueError(f"Unknown task type: {task_type}")


def read_layout_from_usd(usd_path: str, object_names: list[str]) -> dict[str, dict]:
def read_layout_from_usd(
usd_path: str, object_names: list[str], parent_prim_name: str | None = None
) -> dict[str, dict]:
"""Read object poses from USD (first-level children of root prim)"""
from pxr import Usd, UsdGeom

stage = Usd.Stage.Open(usd_path)
if not stage:
raise RuntimeError(f"Cannot open: {usd_path}")

root = stage.GetDefaultPrim() or stage.GetPrimAtPath("/")
if parent_prim_name:
root = stage.GetPrimAtPath(f"/World/{parent_prim_name}")
if not root.IsValid():
raise RuntimeError(f"Parent prim '{parent_prim_name}' not found in {usd_path}")
else:
root = stage.GetDefaultPrim() or stage.GetPrimAtPath("/")
object_set = set(object_names)
layout = {}

Expand Down Expand Up @@ -210,13 +243,11 @@ def load_robot_pose(task_type: str, use_dual_arm: bool = False) -> dict[str, lis
if use_dual_arm:
env_id = task_env.get("dual")
if not env_id:
print(f"[WARN] Dual-arm not supported for '{task_type}', using single-arm")
env_id = task_env["single"]
use_dual_arm = False
else:
print(f"[INFO] Using dual-arm config: {env_id}")
raise ValueError(f"Dual-arm not supported for '{task_type}'")
else:
env_id = task_env["single"]
env_id = task_env.get("single")
if not env_id:
raise ValueError(f"Single-arm not supported for '{task_type}'")

env_cfg = parse_env_cfg(env_id, device="cpu", num_envs=1)

Expand All @@ -240,7 +271,7 @@ def compose_scene(
assets_base: str,
target_pos: list[float],
target_quat: list[float],
include_table: bool = False,
include_platform: bool = False,
use_dual_arm: bool = False,
) -> str:
"""
Expand All @@ -249,32 +280,32 @@ def compose_scene(
2. Place objects at their original positions

Args:
include_table: Include table in output (default: False)
include_platform: Include platform (table/board) in output (default: False)
use_dual_arm: Use dual-arm configuration (toys/cloth only, uses left_arm as reference)
"""
from pxr import Gf, Usd, UsdGeom

config = TASK_CONFIG[task_type]
table_name = config.get("table_name")
platform_name = config.get("platform_name")

# Warn if table not supported
if include_table and not table_name:
print(f"[WARN] Table not supported for '{task_type}'")
include_table = False
# Warn if platform not supported
if include_platform and not platform_name:
print(f"[WARN] Platform not supported for '{task_type}'")
include_platform = False

# Build source USD path and read layout
source_usd = f"{assets_base}/{config['source_scene']}"
print(f"[INFO] Reading layout from: {source_usd}")
layout = read_layout_from_usd(source_usd, config["objects"])
layout = read_layout_from_usd(source_usd, config["objects"], parent_prim_name=config.get("parent_prim"))

# Compute scene transform based on reference point
if include_table and table_name:
# Use table as reference point
table_pose = layout.get(table_name)
if not table_pose:
raise RuntimeError(f"Table '{table_name}' not found in source USD")
orig_pos, orig_quat = table_pose["pos"], table_pose["rot"]
print(f"[INFO] Using table '{table_name}' as reference: pos={orig_pos}")
if include_platform and platform_name:
# Use platform as reference point
platform_pose = layout.get(platform_name)
if not platform_pose:
raise RuntimeError(f"Platform '{platform_name}' not found in source USD")
orig_pos, orig_quat = platform_pose["pos"], platform_pose["rot"]
print(f"[INFO] Using platform '{platform_name}' as reference: pos={orig_pos}")
else:
# Use robot as reference point (dual-arm uses left_arm)
robot = load_robot_pose(task_type, use_dual_arm=use_dual_arm)
Expand All @@ -283,38 +314,62 @@ def compose_scene(

scene_pos, scene_quat = compute_scene_transform(orig_pos, orig_quat, target_pos, target_quat)

# Filter out table if not included
if not include_table and table_name:
layout = {k: v for k, v in layout.items() if k != table_name}
# Filter out platform if not included
if not include_platform and platform_name:
layout = {k: v for k, v in layout.items() if k != platform_name}

# Create output stage
stage = Usd.Stage.CreateNew(output_usd)
world = UsdGeom.Xform.Define(stage, "/World")
stage.SetDefaultPrim(world.GetPrim())

# Add background with transform
# Helper to compute relative path from output USD to referenced USD
output_dir = Path(output_usd).resolve().parent

def to_relative(abs_path: str) -> str:
try:
return os.path.relpath(abs_path, output_dir)
except ValueError:
# On Windows, relpath fails across drives - use absolute path
return abs_path

# Add background with transform (use relative path)
bg_prim = stage.DefinePrim("/World/Scene")
bg_prim.GetReferences().AddReference(background_usd)
bg_relative = to_relative(background_usd)
bg_prim.GetReferences().AddReference(bg_relative)
print(f"[INFO] Background reference: {bg_relative}")
bg_xform = UsdGeom.Xformable(bg_prim)
bg_xform.ClearXformOpOrder()
bg_xform.AddTranslateOp().Set(Gf.Vec3d(*scene_pos))
bg_xform.AddOrientOp().Set(Gf.Quatf(*scene_quat))

# Add objects at original positions
assets_path = f"{assets_base}/{config['assets_subpath']}"

# Determine parent path for objects (burger uses /World/Burger, others use /World)
parent_prim_name = config.get("parent_prim")
if parent_prim_name:
objects_parent = f"/World/{parent_prim_name}"
# Create parent Xform
UsdGeom.Xform.Define(stage, objects_parent)
print(f"[INFO] Created parent group: {objects_parent}")
else:
objects_parent = "/World"

for name, pose in layout.items():
usd_path = get_object_usd_path(task_type, name, assets_path)
if not Path(usd_path).exists():
print(f"[WARN] {usd_path} not found, skipping")
continue

prim = stage.DefinePrim(f"/World/{name}")
prim.GetReferences().AddReference(usd_path)
prim = stage.DefinePrim(f"{objects_parent}/{name}")
usd_relative = to_relative(usd_path)
prim.GetReferences().AddReference(usd_relative)
xform = UsdGeom.Xformable(prim)
xform.ClearXformOpOrder()
xform.AddTranslateOp().Set(Gf.Vec3d(*pose["pos"]))
xform.AddOrientOp().Set(Gf.Quatf(*pose["rot"]))
print(f"[OK] Added {name}")
print(f"[OK] Added {name} -> {usd_relative}")

stage.Save()
print(f"[OK] Saved: {output_usd}")
Expand Down Expand Up @@ -342,18 +397,16 @@ def compose_scene(
metavar=("W", "X", "Y", "Z"),
help="Target robot quaternion",
)
parser.add_argument("--include-table", action="store_true", help="Include table in output")
parser.add_argument("--include-platform", action="store_true", help="Include working platform in output")
parser.add_argument(
"--dual-arm",
action="store_true",
help="Use dual-arm configuration (toys/cloth only, uses left_arm as reference)",
help="Use dual-arm configuration (uses left_arm as reference)",
)

args = parser.parse_args()

# Set LEISAAC_ASSETS_ROOT to the provided assets base path
import os

os.environ["LEISAAC_ASSETS_ROOT"] = args.assets_base
print(f"[INFO] Set LEISAAC_ASSETS_ROOT={args.assets_base}")

Expand All @@ -374,7 +427,7 @@ def compose_scene(
assets_base=args.assets_base,
target_pos=args.target_pos,
target_quat=args.target_quat,
include_table=args.include_table,
include_platform=args.include_platform,
use_dual_arm=args.dual_arm,
)

Expand Down
Loading