Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
4e7862c
primary impl
alanray-tech Nov 13, 2025
e5d699f
seems successful
alanray-tech Nov 14, 2025
dd257bd
able to load g1
alanray-tech Nov 19, 2025
97d4c8c
[FEATURE] Add USD stage import functionality and related parsing util…
alanray-tech Nov 20, 2025
b85d994
temp
alanray-tech Nov 25, 2025
0a44794
Merge branch 'Genesis-Embodied-AI:main' into main
alanray-tech Nov 25, 2025
8c88436
Merge remote-tracking branch 'origin/main' into dev
alanray-tech Nov 25, 2025
f652a3a
new structure
alanray-tech Nov 30, 2025
8814e38
add free root joint support
alanray-tech Dec 2, 2025
2e47e34
fix free joint base link init transform
alanray-tech Dec 3, 2025
b0727c8
refactor code to better structure
alanray-tech Dec 3, 2025
4d31b02
Merge branch 'main' into dev
alanray-tech Dec 3, 2025
9fc5db0
add missing usd-parser-related import
alanray-tech Dec 3, 2025
74689d2
support usd optional import
alanray-tech Dec 3, 2025
7a44ac2
add missing morphs.Drone
alanray-tech Dec 4, 2025
303f84f
support assets-download/argparser in usd example, clean up deps
alanray-tech Dec 8, 2025
e51f975
add usd import stage example to unit test
alanray-tech Dec 8, 2025
1f4633e
refactor parser
alanray-tech Dec 8, 2025
cabdd49
fix uv_name missing
alanray-tech Dec 8, 2025
7b0c544
fix rotation scaling extract
alanray-tech Dec 9, 2025
0b23d9a
add usd driver api support
alanray-tech Dec 10, 2025
0f5d298
update doc
alanray-tech Dec 10, 2025
606752d
update workflow, install usd for usd unit test
alanray-tech Dec 10, 2025
83675f1
add a simple animator, add default value init for dofs_frictionloss/d…
alanray-tech Dec 10, 2025
54443f0
add morph option
YilingQiao Dec 10, 2025
82b3533
Merge pull request #1 from YilingQiao/yiling/251210_usd_collision_vis…
alanray-tech Dec 11, 2025
7d64715
Merge remote-tracking branch 'pub/main' into dev
alanray-tech Dec 12, 2025
a3fee62
weird target behaviour, need fix
alanray-tech Dec 12, 2025
dbc08ca
Merge branch 'main' into dev
YilingQiao Dec 14, 2025
bb87147
set target
YilingQiao Dec 16, 2025
c7cfa53
update limit
YilingQiao Dec 17, 2025
8f02db1
add target to dofs info
YilingQiao Dec 23, 2025
886dfe6
Merge pull request #2 from YilingQiao/yiling/251216_change_target
alanray-tech Dec 23, 2025
7820f73
Merge remote-tracking branch 'pub/main' into dev
alanray-tech Dec 28, 2025
f9a037f
update pyproject.toml
alanray-tech Dec 29, 2025
e72dff4
merge origin/dev
alanray-tech Dec 29, 2025
28ba6ce
change damping
YilingQiao Dec 29, 2025
2290640
Merge pull request #3 from YilingQiao/yiling/251229_change_damping
alanray-tech Dec 29, 2025
9e24f49
Merge remote-tracking branch 'pub/main' into dev
alanray-tech Dec 30, 2025
0a8611d
fix rigid_solver_decomp missing entity_idx, which crash the rigid sim…
alanray-tech Dec 30, 2025
eba954a
Fixed the import path of .usda
alanray-tech Dec 30, 2025
a47bae5
fix usd_parser, move Entity type import into the function, so that a …
alanray-tech Dec 31, 2025
6a71764
try skip usd related test on ARM machine
alanray-tech Jan 3, 2026
45c5119
Merge branch 'main' into dev
alanray-tech Jan 4, 2026
fd4dda0
make usd import optional
alanray-tech Jan 4, 2026
e973715
Merge remote-tracking branch 'pub/main' into dev
alanray-tech Jan 5, 2026
e587af0
Merge branch 'main' into dev
alanray-tech Jan 6, 2026
ea45d00
Merge branch 'main' into dev
alanray-tech Jan 6, 2026
10a38a3
improve at api and code style level according to the review
alanray-tech Jan 8, 2026
feb0ebe
refactor parsing logic & clean up codes for better readability and pe…
alanray-tech Jan 9, 2026
3a82ffd
improve docstring
alanray-tech Jan 9, 2026
016909b
try fix CI
alanray-tech Jan 9, 2026
179be0e
fix drone test
alanray-tech Jan 9, 2026
9b0fbe2
unify the usd rigidbody and articulation parser to rigid entity parse…
alanray-tech Jan 10, 2026
c443129
remove compute_joint_axis_scaling_factor, because USD take this value…
alanray-tech Jan 10, 2026
dcea807
Merge branch 'main' into dev
alanray-tech Jan 10, 2026
0b54773
try fix NV EULA agreement input
alanray-tech Jan 11, 2026
e817bda
merge
alanray-tech Jan 11, 2026
5bab2c6
add some unit tests, not fully implemented
alanray-tech Jan 12, 2026
778b92b
add PureRigid/Revoluate/Prismatic/Spherical unit tests
alanray-tech Jan 13, 2026
d470b10
Adding OMNI_KIT_ACCEPT_EULA to the workflows that use USD; Improve ex…
alanray-tech Jan 13, 2026
c84b358
Merge branch 'dev' of https://github.com/alanray-tech/Genesis into dev
alanray-tech Jan 13, 2026
46a025c
use float32 compatible tol in test_usd
alanray-tech Jan 13, 2026
ff159d3
expose more options in USD(Morph), align them with MJCF, test passed
alanray-tech Jan 13, 2026
b111c67
clean up code, add better warning when attribute matching fails
alanray-tech Jan 14, 2026
f7bbd6a
Merge remote-tracking branch 'pub/main' into dev
alanray-tech Jan 14, 2026
86c5def
change warning about attribute missing to debug
alanray-tech Jan 14, 2026
131147c
include OMNI_KIT_ACCEPT_EULA and OMNI_KIT_ALLOW_ROOT in SLURM_ENV_VAR…
alanray-tech Jan 14, 2026
6ce56ec
support batched polar (torch and numpy), add filter function in polar…
alanray-tech Jan 14, 2026
38dd302
update tol
alanray-tech Jan 14, 2026
79a25ad
Merge remote-tracking branch 'pub/main' into dev
alanray-tech Jan 15, 2026
4c5f77c
Final cleanup.
duburcqa Jan 15, 2026
a4d64a7
Merge branch 'main' into dev
duburcqa Jan 15, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
run: |
pip install --upgrade pip setuptools wheel
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -e '.[dev]' pynput
pip install -e '.[dev,usd]' pynput

- name: Get gstaichi version
id: gstaichi_version
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/generic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,18 @@ jobs:
shell: bash
run: |
PYTHON_DEPS="dev"
# Install USD for all platforms except ARM (usd-core doesn't support ARM)
# This is required for test_mesh.py which tests USD parsing functionality
if [[ "${{ matrix.OS }}" != 'ubuntu-24.04-arm' ]] ; then
PYTHON_DEPS="${PYTHON_DEPS},usd"
fi
pip install -e ".[${PYTHON_DEPS}]"

- name: Verify USD installation (for test_mesh.py)
if: matrix.OS != 'ubuntu-24.04-arm'
shell: bash
run: |
python -c "from pxr import Usd; print('USD installed successfully')" || (echo "USD installation failed - required for test_mesh.py" && exit 1)

- name: Get artifact prefix name
id: artifact_prefix
Expand Down
126 changes: 126 additions & 0 deletions examples/usd/import_stage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import argparse
import os
import numpy as np
from huggingface_hub import snapshot_download

import genesis as gs


class AutoJointAnimator:
def __init__(self, scene: gs.Scene):
rigid = scene.sim.rigid_solver

# Get joint limits and handle invalid values (inf/nan -> reasonable defaults)
joint_limits = rigid.dofs_info.limit.to_numpy()
# Clip invalid limits to reasonable range
joint_limits = np.clip(joint_limits, -np.pi, np.pi)

# Get initial joint positions
init_positions = rigid.get_dofs_position().numpy()
if init_positions.ndim > 1:
init_positions = init_positions[0] # Take first environment if batched

# Store joint lower and upper limits
self.joint_lower = joint_limits[:, 0]
self.joint_upper = joint_limits[:, 1]
joint_ranges_original = self.joint_upper - self.joint_lower

# Handle zero or invalid ranges
valid_range_mask = joint_ranges_original > 1e-6

# Normalize initial positions to [-1, 1] range
# This maps: lower → -1, upper → 1
normalized_init = np.zeros_like(init_positions)
for i in range(len(init_positions)):
if valid_range_mask[i]:
normalized_init[i] = 2.0 * (init_positions[i] - self.joint_lower[i]) / joint_ranges_original[i] - 1.0
else:
normalized_init[i] = 0.0 # Center if no valid range

# Clamp to valid range for arcsin
normalized_init = np.clip(normalized_init, -1.0, 1.0)

# Map normalized initial position to phase offset using arcsin
# This ensures sin(phase_offset) = normalized_init, so the animation starts at the initial position
self.phase_offsets = np.arcsin(normalized_init)

self.rigid = rigid

def animate(self, scene: gs.Scene):
# Calculate target positions using sin function to interpolate between lower and upper limits
# sin ranges from -1 to 1, we map it to [lower, upper]
# Formula: target = lower + (upper - lower) * (sin(...) + 1) / 2
t = scene.t * scene.dt
theta = np.pi * t + self.phase_offsets
theta = theta % (2 * np.pi)
sin_values = np.sin(theta)

# Map sin from [-1, 1] to [0, 1]
normalized = (sin_values + 1.0) / 2.0

# Interpolate between lower and upper limits
target = self.joint_lower + (self.joint_upper - self.joint_lower) * normalized

# Apply the target positions
self.rigid.control_dofs_position(target)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("-n", "--num_steps", type=int, default=1000)
parser.add_argument("-v", "--vis", action="store_true", default=False)
args = parser.parse_args()

args.num_steps = 1 if "PYTEST_VERSION" in os.environ else args.num_steps
args.vis = False if "PYTEST_VERSION" in os.environ else args.vis

gs.init(backend=gs.cpu)

dt = 0.002
scene = gs.Scene(
viewer_options=gs.options.ViewerOptions(
camera_pos=(3.5, 0.0, 2.5),
camera_lookat=(0.0, 0.0, 0.5),
camera_fov=40,
# enable_interaction=True,
),
rigid_options=gs.options.RigidOptions(
dt=dt,
# constraint_solver=gs.constraint_solver.Newton,
gravity=(0, 0, -9.8),
enable_collision=True,
enable_joint_limit=True,
max_collision_pairs=1000,
),
show_viewer=args.vis,
)

# Download asset from HuggingFace
asset_path = snapshot_download(
repo_type="dataset",
repo_id="Genesis-Intelligence/assets",
revision="main",
allow_patterns="usd/Lightwheel_Kitchen001/Kitchen001/*",
max_workers=1,
)

entities = scene.add_stage(
morph=gs.morphs.USDMorph(
file=f"{asset_path}/usd/Lightwheel_Kitchen001/Kitchen001/Kitchen001.usd",
# convexify=False, # turn it off to use the original mesh and sdf collision detection
),
# vis_mode="collision",
# visualize_contact=True,
)

scene.build()

joint_animator = AutoJointAnimator(scene)

for _ in range(args.num_steps):
joint_animator.animate(scene)
scene.step()


if __name__ == "__main__":
main()
158 changes: 101 additions & 57 deletions genesis/engine/entities/rigid_entity/rigid_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,20 @@ def _load_model(self):
self._joints = gs.List()
self._equalities = gs.List()

if isinstance(self._morph, gs.morphs.Mesh):
self._load_mesh(self._morph, self._surface)
elif isinstance(self._morph, (gs.morphs.MJCF, gs.morphs.URDF, gs.morphs.Drone)):
if isinstance(self._morph, (gs.morphs.Mesh, gs.morphs.USDRigidBody)):
if isinstance(self._morph, gs.morphs.Mesh):
self._load_mesh(self._morph, self._surface)
else:
self._load_usd_rigid_body(self._morph, self._surface)
elif isinstance(
self._morph,
(
gs.morphs.MJCF,
gs.morphs.URDF,
gs.morphs.Drone,
gs.morphs.USDArticulation,
),
):
self._load_scene(self._morph, self._surface)
elif isinstance(self._morph, gs.morphs.Primitive):
self._load_primitive(self._morph, self._surface)
Expand Down Expand Up @@ -313,6 +324,24 @@ def _load_mesh(self, morph, surface):
surface=surface,
)

def _load_usd_rigid_body(self, morph, surface):
"""
Load a USD rigid body, similar to _load_mesh but parsing from USD file.
"""
from genesis.utils.usd import parse_usd_rigid_body

# Parse USD rigid body to get l_info, j_infos, and g_infos
l_info, j_infos, g_infos = parse_usd_rigid_body(morph, surface)

# Create link and joint using _add_by_info
link, (joint,) = self._add_by_info(
l_info=l_info,
j_infos=j_infos,
g_infos=g_infos,
morph=morph,
surface=surface,
)

def _load_terrain(self, morph, surface):
vmesh, mesh, self.terrain_hf = tu.parse_terrain(morph, surface)
self.terrain_scale = np.array((morph.horizontal_scale, morph.vertical_scale), dtype=gs.np_float)
Expand Down Expand Up @@ -363,60 +392,54 @@ def _load_terrain(self, morph, surface):
surface=surface,
)

def _load_scene(self, morph, surface):
# Mujoco's unified MJCF+URDF parser is not good enough for now to be used for loading both MJCF and URDF files.
# First, it would happen when loading visual meshes having supported format (i.e. Collada files '.dae').
# Second, it does not take into account URDF 'mimic' joint constraints. However, it does a better job at
# initialized undetermined physics parameters.
if isinstance(morph, gs.morphs.MJCF):
# Mujoco's unified MJCF+URDF parser systematically for MJCF files
l_infos, links_j_infos, links_g_infos, eqs_info = mju.parse_xml(morph, surface)
else:
# Custom "legacy" URDF parser for loading geometries (visual and collision) and equality constraints.
# This is necessary because Mujoco cannot parse visual geometries (meshes) reliably for URDF.
l_infos, links_j_infos, links_g_infos, eqs_info = uu.parse_urdf(morph, surface)

# Mujoco's unified MJCF+URDF parser for only link, joints, and collision geometries properties.
morph_ = copy(morph)
morph_.visualization = False
try:
# Mujoco's unified MJCF+URDF parser for URDF files.
# Note that Mujoco URDF parser completely ignores equality constraints.
l_infos, links_j_infos_mj, links_g_infos_mj, _ = mju.parse_xml(morph_, surface)

# Mujoco is not parsing actuators properties
for j_info_gs in chain.from_iterable(links_j_infos):
for j_info_mj in chain.from_iterable(links_j_infos_mj):
if j_info_mj["name"] == j_info_gs["name"]:
for name in ("dofs_force_range", "dofs_armature", "dofs_kp", "dofs_kv"):
j_info_mj[name] = j_info_gs[name]
links_j_infos = links_j_infos_mj

# Take into account 'world' body if it was added automatically for our legacy URDF parser
if len(links_g_infos_mj) == len(links_g_infos) + 1:
assert not links_g_infos_mj[0]
links_g_infos.insert(0, [])
assert len(links_g_infos_mj) == len(links_g_infos)

# Update collision geometries, ignoring fake" visual geometries returned by Mujoco, (which is using
# collision as visual to avoid loading mesh files), and keeping the true visual geometries provided
# by our custom legacy URDF parser.
# Note that the Kinematic tree ordering is stable between Mujoco and Genesis (Hopefully!).
for link_g_infos, link_g_infos_mj in zip(links_g_infos, links_g_infos_mj):
# Remove collision geometries from our legacy URDF parser
for i_g, g_info in tuple(enumerate(link_g_infos))[::-1]:
is_col = g_info["contype"] or g_info["conaffinity"]
if is_col:
del link_g_infos[i_g]

# Add visual geometries from Mujoco's unified MJCF+URDF parser
for g_info in link_g_infos_mj:
is_col = g_info["contype"] or g_info["conaffinity"]
if is_col:
link_g_infos.append(g_info)
except (ValueError, AssertionError):
gs.logger.info("Falling back to legacy URDF parser. Default values of physics properties may be off.")

def _collect_urdf_articulation_info(self, morph, surface):
# Custom "legacy" URDF parser for loading geometries (visual and collision) and equality constraints.
# This is necessary because Mujoco cannot parse visual geometries (meshes) reliably for URDF.
l_infos, links_j_infos, links_g_infos, eqs_info = uu.parse_urdf(morph, surface)
# Mujoco's unified MJCF+URDF parser for only link, joints, and collision geometries properties.
morph_ = copy(morph)
morph_.visualization = False
try:
# Mujoco's unified MJCF+URDF parser for URDF files.
# Note that Mujoco URDF parser completely ignores equality constraints.
l_infos, links_j_infos_mj, links_g_infos_mj, _ = mju.parse_xml(morph_, surface)

# Mujoco is not parsing actuators properties
for j_info_gs in chain.from_iterable(links_j_infos):
for j_info_mj in chain.from_iterable(links_j_infos_mj):
if j_info_mj["name"] == j_info_gs["name"]:
for name in ("dofs_force_range", "dofs_armature", "dofs_kp", "dofs_kv"):
j_info_mj[name] = j_info_gs[name]
links_j_infos = links_j_infos_mj

# Take into account 'world' body if it was added automatically for our legacy URDF parser
if len(links_g_infos_mj) == len(links_g_infos) + 1:
assert not links_g_infos_mj[0]
links_g_infos.insert(0, [])
assert len(links_g_infos_mj) == len(links_g_infos)

# Update collision geometries, ignoring fake" visual geometries returned by Mujoco, (which is using
# collision as visual to avoid loading mesh files), and keeping the true visual geometries provided
# by our custom legacy URDF parser.
# Note that the Kinematic tree ordering is stable between Mujoco and Genesis (Hopefully!).
for link_g_infos, link_g_infos_mj in zip(links_g_infos, links_g_infos_mj):
# Remove collision geometries from our legacy URDF parser
for i_g, g_info in tuple(enumerate(link_g_infos))[::-1]:
is_col = g_info["contype"] or g_info["conaffinity"]
if is_col:
del link_g_infos[i_g]

# Add visual geometries from Mujoco's unified MJCF+URDF parser
for g_info in link_g_infos_mj:
is_col = g_info["contype"] or g_info["conaffinity"]
if is_col:
link_g_infos.append(g_info)
except (ValueError, AssertionError):
gs.logger.info("Falling back to legacy URDF parser. Default values of physics properties may be off.")

return l_infos, links_j_infos, links_g_infos, eqs_info

def _build_up_articulation(self, l_infos, links_j_infos, links_g_infos, eqs_info, morph, surface):
# Add free floating joint at root if necessary
if (
(isinstance(morph, gs.morphs.Drone) or (isinstance(morph, gs.morphs.URDF) and not morph.fixed))
Expand Down Expand Up @@ -582,6 +605,26 @@ def _load_scene(self, morph, surface):
sol_params=eq_info["sol_params"],
)

def _load_scene(self, morph, surface):
# Mujoco's unified MJCF+URDF parser is not good enough for now to be used for loading both MJCF and URDF files.
# First, it would happen when loading visual meshes having supported format (i.e. Collada files '.dae').
# Second, it does not take into account URDF 'mimic' joint constraints. However, it does a better job at
# initialized undetermined physics parameters.
if isinstance(morph, gs.morphs.MJCF):
# Mujoco's unified MJCF+URDF parser systematically for MJCF files
l_infos, links_j_infos, links_g_infos, eqs_info = mju.parse_xml(morph, surface)
elif isinstance(morph, (gs.morphs.URDF, gs.morphs.Drone)):
l_infos, links_j_infos, links_g_infos, eqs_info = self._collect_urdf_articulation_info(morph, surface)
elif isinstance(morph, gs.morphs.USDArticulation):
from genesis.utils.usd import parse_usd_articulation

l_infos, links_j_infos, links_g_infos, eqs_info = parse_usd_articulation(morph, surface)
else:
gs.raise_exception(f"Unsupported morph type: {type(morph)}")

if len(l_infos) > 0:
self._build_up_articulation(l_infos, links_j_infos, links_g_infos, eqs_info, morph, surface)

def _build(self):
for link in self._links:
link._build()
Expand Down Expand Up @@ -740,6 +783,7 @@ def _add_by_info(self, l_info, j_infos, g_infos, morph, surface):
dofs_kp=j_info.get("dofs_kp", np.zeros(n_dofs)),
dofs_kv=j_info.get("dofs_kv", np.zeros(n_dofs)),
dofs_force_range=j_info.get("dofs_force_range", np.tile([[-np.inf, np.inf]], [n_dofs, 1])),
dofs_target=j_info.get("dofs_target", np.zeros(n_dofs)),
)
joints.append(joint)

Expand Down
9 changes: 9 additions & 0 deletions genesis/engine/entities/rigid_entity/rigid_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(
dofs_kp,
dofs_kv,
dofs_force_range,
dofs_target,
):
self._name = name
self._entity = entity
Expand Down Expand Up @@ -68,6 +69,7 @@ def __init__(
self._dofs_kp = dofs_kp
self._dofs_kv = dofs_kv
self._dofs_force_range = dofs_force_range
self._dofs_target = dofs_target

def __getattr__(self, name):
# Must be implemented to throw deprecation warnings when accessing old properties, ignoring introspection
Expand Down Expand Up @@ -427,6 +429,13 @@ def dofs_force_range(self):
"""
return self._dofs_force_range

@property
def dofs_target(self):
"""
Returns the target positions of the dofs of the joint.
"""
return self._dofs_target

@property
def is_built(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion genesis/engine/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def from_morph_surface(cls, morph, surface=None):
" and rotate glb mesh by default later and gradually enforce this option."
)
elif morph.is_format(gs.options.morphs.USD_FORMATS):
import genesis.utils.usda as usda_utils
import genesis.utils.usd.usda as usda_utils

meshes = usda_utils.parse_mesh_usd(morph.file, morph.group_by_material, morph.scale, surface)
elif isinstance(morph, gs.options.morphs.MeshSet):
Expand Down
Loading
Loading