Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
3afda12
Add utility to replicate Isaac Lab environments with example
nvlukasz May 2, 2025
6ea6962
Updated Isaac Lab cartpole example
nvlukasz May 2, 2025
b1a42f3
Draft of ArticulationView
nvlukasz May 6, 2025
d28a9ce
Add include_root_joint flag to ArticulationView constructor
nvlukasz May 8, 2025
2db8e25
Added properties for counts and attribute shapes in ArticulationView
nvlukasz May 8, 2025
58fab77
Use correct up-axis in replicate_environment()
nvlukasz May 9, 2025
0c5b452
Use up_axis instead of up_vector
nvlukasz May 9, 2025
4ea4e4d
Added missing USD file
nvlukasz May 9, 2025
616258d
Removed redundant cloner example, renamed carpole example
nvlukasz May 9, 2025
388e5b7
Example tweaks
nvlukasz May 9, 2025
1841df6
Add API for articulation root transforms and velocities
nvlukasz May 12, 2025
93a79d6
Implementation tweaks
nvlukasz May 12, 2025
82cefa9
Code formatting
nvlukasz May 12, 2025
0a298f2
Changed include_root_joint to include_free_joint in ArticulationView
nvlukasz May 12, 2025
12ca4e3
Updated cartpole example to use env_offsets
nvlukasz May 12, 2025
b7c2f4c
Restore loading global data from prototype env
nvlukasz May 12, 2025
f53e8ad
Code formatting
nvlukasz May 12, 2025
b899cce
Periodic ant reset
nvlukasz May 12, 2025
a2f0ea3
Set default env_offsets to zero
nvlukasz May 13, 2025
671a58f
Fixed a bug with computing the end of articulation joint axes
nvlukasz May 13, 2025
17ed45a
Exposed joint limits in ArticulationView
nvlukasz May 13, 2025
687f55e
Cleanup
nvlukasz May 13, 2025
148b795
Demonstrate two ways of setting articulation states
nvlukasz May 13, 2025
5eee5e7
Example tweaks
nvlukasz May 13, 2025
5111130
Updated docstrings
nvlukasz May 13, 2025
61432ad
Use eval_fk() consistently
nvlukasz May 13, 2025
5f9a6a6
Updated usage of eval_fk()
nvlukasz May 14, 2025
5064ded
Allow passing indices to setters for selective updates
nvlukasz May 14, 2025
228e545
Fix silly things
nvlukasz May 14, 2025
53de9cf
Code formatting
nvlukasz May 14, 2025
2eecfb9
Update to latest
nvlukasz May 16, 2025
6b0de17
Use DFS joint ordering
nvlukasz May 16, 2025
a5c828f
Simplify up axis code
nvlukasz May 16, 2025
98dfe8b
Use ant prototype env with ground plane
nvlukasz May 16, 2025
f4fe573
Use ground plane from USD in ant example
nvlukasz May 16, 2025
a952a61
Support for more builtin attributes
nvlukasz May 16, 2025
e33b268
Refactored indexing mode
nvlukasz May 16, 2025
7d8a3c3
Code formatting
nvlukasz May 16, 2025
ecbab9b
Renamed examples
nvlukasz May 16, 2025
355622a
Clean up example assets
nvlukasz May 17, 2025
91a4dba
Switch from joint_target to joint_f for control
nvlukasz May 21, 2025
d85b957
Remove ArticulationView.get_attribute_shape()
nvlukasz May 26, 2025
740dde0
Removed env_offsets from ArticulationView
nvlukasz May 26, 2025
01ea531
Add verbose flag to ArticulationView constructor
nvlukasz May 26, 2025
0d2ba8a
Get rid of copy arg in get_attribute(), use lru_cache for caching str…
nvlukasz May 26, 2025
02f5da8
Use joint_f instead of joint_target in cartpole example
nvlukasz May 26, 2025
e9aeb66
Formatting
nvlukasz May 26, 2025
67a89f6
Added humanoid env
nvlukasz May 23, 2025
37d64d0
Renaming things
nvlukasz May 23, 2025
952dc0c
Ruff ruff
nvlukasz May 26, 2025
f9eaaef
Updated replicate_environment()
nvlukasz May 26, 2025
22c7466
Merge branch 'main' into selection-api
nvlukasz May 27, 2025
85aff6d
Update eval_fk() signature
nvlukasz May 27, 2025
ff7c78d
Merge branch 'main' into selection-api
nvlukasz May 28, 2025
87d56dd
Added anymal
nvlukasz May 28, 2025
855f8ef
Fixed articulation mask device
nvlukasz May 28, 2025
f1ff063
Merge branch 'main' into selection-api
nvlukasz May 30, 2025
8d25c26
Parsing names from keys
nvlukasz May 23, 2025
95563d6
Tweak debug output
nvlukasz May 30, 2025
6d08222
Merge branch 'main' into selection-api
nvlukasz Jun 12, 2025
f57e716
Update to latest
nvlukasz Jun 12, 2025
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
1,552 changes: 1,552 additions & 0 deletions newton/examples/assets/envs/ant_env.usda

Large diffs are not rendered by default.

Binary file added newton/examples/assets/envs/anymal.usd
Binary file not shown.
884 changes: 884 additions & 0 deletions newton/examples/assets/envs/cartpole_env.usda

Large diffs are not rendered by default.

Binary file added newton/examples/assets/envs/humanoid_env.usd
Binary file not shown.
224 changes: 224 additions & 0 deletions newton/examples/example_selection_ant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import math

import torch
import warp as wp

import newton
import newton.examples
import newton.utils
from newton.utils.isaaclab import replicate_environment
from newton.utils.selection import ArticulationView


class Example:
def __init__(self, stage_path=None, num_envs=8):
self.num_envs = num_envs

builder, stage_info = replicate_environment(
newton.examples.get_asset("envs/ant_env.usda"),
"/World/envs/env_0",
"/World/envs/env_{}",
num_envs,
(5.0, 5.0, 0.0),
# USD importer args
collapse_fixed_joints=True,
joint_ordering="dfs",
)

up_axis = stage_info.get("up_axis") or newton.Axis.Z

# finalize model
self.model = builder.finalize()

self.solver = newton.solvers.MuJoCoSolver(self.model)

self.renderer = None
if stage_path:
self.renderer = newton.utils.SimRendererOpenGL(
path=stage_path,
model=self.model,
scaling=2.0,
up_axis=str(up_axis),
screen_width=1280,
screen_height=720,
camera_pos=(0, 4, 30),
)

self.state_0 = self.model.state()
self.state_1 = self.model.state()
self.control = self.model.control()

self.sim_time = 0.0
fps = 60
self.frame_dt = 1.0 / fps

self.sim_substeps = 10
self.sim_dt = self.frame_dt / self.sim_substeps

self.next_reset = 0.0

# ===========================================================
# create articulation view
# ===========================================================
self.ants = ArticulationView(self.model, "/World/envs/*/Robot/torso", include_free_joint=True)

print(f"articulation count: {self.ants.count}")
print(f"link_count: {self.ants.link_count}")
print(f"joint_count: {self.ants.joint_count}")
print(f"joint_axis_count: {self.ants.joint_axis_count}")

print(f"joint_q shape: {self.ants.get_attribute('joint_q', self.model).shape}")
print(f"joint_qd shape: {self.ants.get_attribute('joint_qd', self.model).shape}")
print(f"joint_f shape: {self.ants.get_attribute('joint_f', self.model).shape}")
print(f"joint_target shape: {self.ants.get_attribute('joint_target', self.model).shape}")
print(f"body_q shape: {self.ants.get_attribute('body_q', self.model).shape}")
print(f"body_qd shape: {self.ants.get_attribute('body_qd', self.model).shape}")

# set all dofs to the middle of their range by default
dof_limit_lower = wp.to_torch(self.ants.get_attribute("joint_limit_lower", self.model))
dof_limit_upper = wp.to_torch(self.ants.get_attribute("joint_limit_upper", self.model))
default_dof_transforms = 0.5 * (dof_limit_lower + dof_limit_upper)

if self.ants.include_free_joint:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would've been great if we didn't have to put this burden on the user side and could resolve this if/else internally;
Does this also mean that these if/else statements would be propagated all the way to IL to support various solvers

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These if-statements are in the examples only to show two different ways of doing things. Normally, as a user, you would pick one and stick to it - no need to provide both code paths and if-statements.

Some background info...

There are two ways of dealing with articulation root transforms:

  1. Include the free joint when getting/setting joint_q and joint_qd. This allows you to set the root and dof states in a single call. I think this is the preferred way of doing it in Newton, but it deviates from the legacy tensor API.
  2. Set the root states separately from the internal dof states. That requires two calls, but it mimics the legacy tensor API.

I'd be happy to collapse this into a single option, but I think that would mean option 1 (include free joint).

Another thing you might find weird is that the joint_f array used to set joint forces/torques includes the free joint in Newton. So it can be used to apply joint forces and forces on the articulation root in one call, but that again deviates from the legacy tensor API.

So the include_free_joint option is there to ease porting environments from the legacy tensor API to Newton. I'm open to revisiting this. For example, we could always include the free joint. It's the right thing to do for Newton, but it would mean a more complicated rewrite for Isaac Lab.

Or maybe we can keep the include_free_joint flag but make it more granular, so that you can set it on every attribute getter/setter. E.g.,

joint_q = view.get_attribute("joint_q", state, include_free_joint=True)
joint_f = view.get_attribute("joint_f", control, include_free_joint=False)

The flexibility is nice, but it adds complexity.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The more I think about it, the more I want to get rid of this include_free_joint logic. And I would also like to get rid of the special methods for dealing with articulation roots (get/set_root_transforms() and get/set_root_velocities().

I think the selection API should always include the root joint. That is the Newton Way.

This would make it slightly more difficult to port envs from the legacy tensor API, but hear me out. I don't think it'll be that bad.

If the root joint is a free joint:

  • To get/set root transforms, use the joint_q attribute slice [:, :7].
  • To get/set the internal joint transforms, use the joint_q attribute slice [:, 7:].
  • To get/set root velocities, use the joint_qd attribute slice [:, :6].
  • To get/set the internal joint velocities, use the joint_qd attribute slice [:, 6:].
  • To apply internal joint forces, use the joint_f attribute slice [:, 6:].

So it's essentially up to the user (or Isaac Lab) to read/write the correct slice of the joint arrays. This would simplify the selection API and get rid of the sketchy include_free_joint flag that could be misunderstood or misused. That flag is only there to emulate compatibility with how we did things in PhysX.

@Milad-Rakhsha-NV @mmacklin what do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This should also make it easier to accommodate root joints that are not free joints. We'd just need to tweak the slice offsets based on how many coords/dofs/axes the root joint has.

@nvlukasz nvlukasz May 26, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As an aside, fixed root joints are a bit of a conundrum right now, because they are stripped by the builder before the selection API sees them. So we might need some placeholder joint, something that tells us that there was a fixed joint. Especially if we want to allow getting/setting the transform of the root joint.

Cartpole is a good existing example. According to the joints in the Model, the cart joint is the root joint and the cart is the root link. But that's obviously not correct. We can't change the root transform of the rail, because its shapes are part of the world geometry and there's no easy way to make the descendant links depend on the transform of those static shapes (as far as I can tell).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@nvlukasz how about we have a 2D mask for environments and joints? Anyway we wanted a way to set a subset of joints based on joint indices. If we can have the mask when getting and setting the attributes. Wouldn’t that satisfy both use cases and avoid duplication?

@nvlukasz nvlukasz May 27, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmmm, maybe. I don't recall discussing a mask for subsets of joints, just remapping indices based on some orderings.

I think we can do masks, yes, but that doesn't really solve all root transform/velocity issues. For example, if the root joint is a free joint, you can get the root link transform from joint_q. But if it's a fixed joint, you can't get it from joint_q, but have to use joint_X_p. So the masks and attributes you use will need to change depending on the root joint type, which I think will be pretty annoying. That's why I'd rather provide special methods for that, like get_root_transforms() and get_root_velocities().

We can still use masks for joints, but I don't think it fixes everything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I want to clarify a few things about root transforms / free joints / maximal coordinates since there's a lot of issues flying around this atm:

@gyeomannvidia

  • Both MuJoCo + Featherstone require a FREE joint for floating base articulations
  • XPBD and other maximal coordinate solvers support FREE joints but do not require them to be present, in which case the corresponding root DOFs won't appear in the joint_q array
  • Currently when a free joint is present its joint_q DOFs are initialized to zero instead of inheriting the body xform, which can be unintuitive Bug in setting start pose of a rigid body. #93
  • body_q is a function of joint_q, and eval_fk() is the way that we sync between them, and must be called before simulation, also related to issue Bug in setting start pose of a rigid body. #93
  • Non floating base rigid bodies do not have the root transform stored in the joint_q array (e.g.: revolute base) but are stored in the body_q state and computed using the parent_xform of the joint.

My suggestion is that for Newton we should always add FREE joints for floating base articulations and rigid bodies. This is necessary to allow users to automatically switch between different solvers. (#43). TBD where to do this, finalize() maybe a bit late, so I would suggest importers should handle this for now and that we document clearly this requirement for users building models by hand.

In addition, we should initialize joint_q for FREE joints to the child body xform when calling add_joint_free() so that the previously created body xform is respected.

@nvlukasz

    get_root_transforms() and set_root_transforms()
    get_root_velocities() and set_root_velocities()

Assuming that the ArticulationView knows what the root body is, then I think these root quantities can be retrieved directly from body_q[root_body], body_qd[root_body], in a unified way that doesn't depend on variations in joint layout (e.g.: revolute versus free joint at the root).

@nvlukasz nvlukasz May 28, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Assuming that the ArticulationView knows what the root body is, then I think these root quantities can be retrieved directly from body_q[root_body], body_qd[root_body]

That's what I do currently. I use body_q and body_qd for getting the root link states, but I set them through joint_q and joint_qd if there's a free joint (I had assumed that body_q and body_qd are essentially read-only attributes for articulations).

But I think that using body_q and body_qd is actually wrong. We wanted this API to support reading and writing directly to the Newton buffers without copying. The getters return arrays that alias the Newton buffers with appropriate offsets and strides. Example:

my_joint_q = view.get_attribute("joint_q", state)
wp.launch(magic, ..., outputs=[my_joint_q])

The kernel writes directly to State.joint_q using the strided alias my_joint_q. There are no copies or staging buffers, which is what we wanted. Technically there's no need for a setter, because the data are already in the right place, but there's a plot twist coming and I'll come back to it shortly (*).

Now. What should get_root_transforms() and get_joint_transforms() do? I think they should do something equivalent. Example:

my_roots = view.get_root_transforms(state)
my_joints = view.get_joint_transforms(state)
wp.launch(more_magic, ..., outputs=[my_roots, my_joints])

The kernel writes directly to State.joint_q, this time using two strided aliases, one for the roots and one for the rest of the joints.

  • get_root_transforms() == get_attribute("joint_q")[:, :7]
  • get_joint_transforms() == get_attribute("joint_q")[:, 7:]

I think this is nice and it fulfills what Isaac Lab is asking for.

If get_root_transforms() returned an alias of the body_q array, I don't think the zero-copy approach will work. I assume that we would still need to copy the root transforms to joint_q, otherwise they'll get overwritten on the next simulation step or eval_fk() and the changes will be lost. Please correct me if I'm wrong.

Things get interesting when the root joint is not a free joint. I would still like to support a zero-copy approach to setting all the transforms. For example, if the root joint is fixed:

  • get_root_transforms() == get_attribute("joint_X_p")[:, 0] # root joint frame in parent
  • get_joint_transforms() == get_attribute("joint_q") # all internal joint coords

Is this correct? I'm basing it on some sample code from @eric-heiden that used joint_q for free joints and joint_X_p for fixed joints.

I would like to keep this zero-copy mode as much as possible, though it may not be possible in all scenarios.

Zero-copy may not be possible if we want to re-order the joints or use masks like @oahmednv requested. We may need staging buffers to swizzle the joint data around.

my_joint_q = view.get_attribute("joint_q", state)

If the user requested a different joint order when constructing the ArticulationView, then the ordering of joints in my_joint_q is different than in State.joint_q. We have a couple of possibilities here.

  1. Create a contiguous staging buffer for this and copy reordered joint data from joint_q into it.
  2. Return a wp.indexedarray that remaps the joint axes as needed.

Creating a staging buffer is a con, but the pro is that it's compatible with PyTorch and other array-based frameworks. That's basically what we did in the previous tensor APIs. Creating a wp.indexedarray over State.joint_q avoids a staging buffer. The indexed array can be used in zero-copy mode from Warp, but for PyTorch we still need to create a contiguous copy. And even with Warp, introducing wp.indexedarray into the mix can be a curve ball. If in some cases we return a strided wp.array (no reordering) and in others we return a wp.indexedarray, then the user must be prepared for it. That means writing kernels that support both, which brings new caveats.

(*) Getting back to setters. With zero-copy mode, setters are not needed, because we just write to the Newton arrays directly. But as described above, zero-copy is not always possible, so a setter is needed to put the data in the right place. The API guidance I recommend is that the setter should always be called. Under the hood, we can early-out if the values are already in the right place, so it will be essentially free. But for API correctness, a setter should be called. Furthermore, if we ever need to notify the solver that something has changed, the setter is the place to do it.

So yeah, designing this API is quite challenging with all the different objectives that sometimes contradict each other.

  • Support zero-copy mode, especially with Warp, but also with PyTorch et al if possible.
  • Support joint reordering or masks.
  • Stay true to the Newtonic ways - the "raw" attribute API accomplishes that I think.
  • Stay compatible with previous tensor APIs - that's what methods like get_root_transforms() should accomplish.

So we offer two API flavours, one for working with the raw attributes and one that provides an interface that resembles the previous tensor APIs. With raw attributes, you can:

# get root and all joints together
transforms = view.get_attribute("joint_q", state)
velocities = view.get_attribute("joint_qd", state)
...
# set root and all joints together
view.set_attribute("joint_q", state, transforms)
view.set_attribute("joint_qd", state, velocities)

And equivalently, with the other methods:

# get root and internal joints separately
root_transforms = view.get_root_transforms(state)
root_velocities = view.get_root_velocities(state)
joint_transforms = view.get_joint_transforms(state)
joint_velocities = view.get_joint_velocities(state)
...
# set root and internal joints separately
view.set_root_transforms(state, root_transforms)
view.set_root_velocities(state, root_velocities)
view.set_joint_transforms(state, joint_transforms)
view.set_joint_velocities(state, joint_velocities)

With the notable difference that if we change the root free joint to a fixed joint, the code that uses raw attributes will need to change, but the code that uses the other methods can stay the same. So the raw attributes are a more "low-level" way to work with Newton, and the other methods create a light abstraction layer.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Alright, I think I found good solutions. There are some breaking changes, so I created a separate PR for review: #186

# combined root and dof transforms
self.default_transforms = wp.to_torch(self.ants.get_attribute("joint_q", self.model)).clone()
self.default_transforms[:, 2] = 0.8 # z-coordinate of articulation root
self.default_transforms[:, 7:] = default_dof_transforms
# combined root and dof velocities
self.default_velocities = wp.to_torch(self.ants.get_attribute("joint_qd", self.model)).clone()
self.default_velocities[:, 2] = 0.5 * math.pi # rotate about z-axis
self.default_velocities[:, 5] = 5.0 # move up z-axis
else:
# root transforms
self.default_root_transforms = wp.to_torch(self.ants.get_root_transforms(self.model)).clone()
self.default_root_transforms[:, 2] = 0.8
# dof transforms
self.default_dof_transforms = default_dof_transforms
# root velocities
self.default_root_velocities = wp.to_torch(self.ants.get_root_velocities(self.model)).clone()
self.default_root_velocities[:, 2] = 0.5 * math.pi # rotate about z-axis
self.default_root_velocities[:, 5] = 5.0 # move up z-axis
# dof velocities
self.default_dof_velocities = wp.to_torch(self.ants.get_attribute("joint_qd", self.model)).clone()

# create disjoint index groups to alternate between
all_indices = torch.arange(num_envs, dtype=torch.int32)
self.indices_0 = all_indices[::2]
self.indices_1 = all_indices[1::2]

# reset all
self.reset()
self.next_reset = self.sim_time + 2.0

self.use_cuda_graph = wp.get_device().is_cuda
if self.use_cuda_graph:
with wp.ScopedCapture() as capture:
self.simulate()
self.graph = capture.graph

def simulate(self):
for _ in range(self.sim_substeps):
self.state_0.clear_forces()

# explicit collisions needed without MuJoCo solver
if not isinstance(self.solver, newton.solvers.MuJoCoSolver):
newton.collision.collide(self.model, self.state_0)

self.solver.step(self.model, self.state_0, self.state_1, self.control, None, self.sim_dt)
self.state_0, self.state_1 = self.state_1, self.state_0

def step(self):
if self.sim_time >= self.next_reset:
self.reset(self.indices_0)
self.next_reset = self.sim_time + 2.0
self.indices_0, self.indices_1 = self.indices_1, self.indices_0

# =========================
# apply random controls
# =========================
joint_forces = 300.0 - 600.0 * torch.rand((self.num_envs, 8))
if self.ants.include_free_joint:
# include the leading root joint (pad with zeros)
joint_forces = torch.cat([torch.zeros((self.num_envs, 6)), joint_forces], axis=1)
self.ants.set_attribute("joint_f", self.control, joint_forces)

with wp.ScopedTimer("step", active=False):
if self.use_cuda_graph:
wp.capture_launch(self.graph)
else:
self.simulate()
self.sim_time += self.frame_dt

def reset(self, indices=None):
# ==============================
# set transforms and velocities
# ==============================
if self.ants.include_free_joint:
# set root and dof transforms together
self.ants.set_attribute("joint_q", self.state_0, self.default_transforms, indices=indices)
# set root and dof velocities together
self.ants.set_attribute("joint_qd", self.state_0, self.default_velocities, indices=indices)
else:
# set root and dof transforms separately
self.ants.set_root_transforms(self.state_0, self.default_root_transforms, indices=indices)
self.ants.set_attribute("joint_q", self.state_0, self.default_dof_transforms, indices=indices)
# set root and dof velocities separately
self.ants.set_root_velocities(self.state_0, self.default_root_velocities, indices=indices)
self.ants.set_attribute("joint_qd", self.state_0, self.default_dof_velocities, indices=indices)

if not isinstance(self.solver, newton.solvers.MuJoCoSolver):
self.ants.eval_fk(self.state_0, indices=indices)

def render(self):
if self.renderer is None:
return

with wp.ScopedTimer("render", active=False):
self.renderer.begin_frame(self.sim_time)
self.renderer.render(self.state_0)
self.renderer.end_frame()


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--device", type=str, default=None, help="Override the default Warp device.")
parser.add_argument(
"--stage_path",
type=lambda x: None if x == "None" else str(x),
default="example_selection_ant.usd",
help="Path to the output USD file.",
)
parser.add_argument("--num_frames", type=int, default=1200, help="Total number of frames.")
parser.add_argument("--num_envs", type=int, default=16, help="Total number of simulated environments.")

args = parser.parse_known_args()[0]

with wp.ScopedDevice(args.device):
example = Example(stage_path=args.stage_path, num_envs=args.num_envs)

for _ in range(args.num_frames):
example.step()
example.render()

# import time
# time.sleep(0.2)

if example.renderer:
example.renderer.save()
Loading