Rework USD scene traversal, ensure DFS order of bodies is respected, remove implicit welding of non-jointed nested bodies

PiperOrigin-RevId: 779121640
Change-Id: Ic3281ac10ce51975cd681e9342b2fffd5f24376f
This commit is contained in:
Robin Alazard
2025-07-04 03:24:35 -07:00
committed by Copybara-Service
parent 2937efd5f2
commit 85ad1eec91
3 changed files with 356 additions and 393 deletions
+182
View File
@@ -0,0 +1,182 @@
// Copyright 2025 DeepMind Technologies Limited
//
// 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.
#include "experimental/usd/kinematic_tree.h"
#include <algorithm>
#include <deque>
#include <map>
#include <memory>
#include <set>
#include <utility>
#include <vector>
#include <mujoco/mujoco.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usdPhysics/joint.h>
namespace mujoco {
namespace usd {
bool GetJointBodies(const pxr::UsdPhysicsJoint& joint,
const pxr::SdfPath& default_prim_path, pxr::SdfPath* from,
pxr::SdfPath* to) {
pxr::SdfPathVector body1_paths;
joint.GetBody1Rel().GetTargets(&body1_paths);
if (body1_paths.empty()) {
mju_warning("Joint %s does not have body1 rel. Skipping.",
joint.GetPath().GetAsString().c_str());
return false;
} else if (body1_paths.size() > 1) {
mju_warning("Joint %s has multiple body1 rels. Skipping.",
joint.GetPath().GetAsString().c_str());
return false;
}
*to = body1_paths[0];
pxr::SdfPathVector body0_paths;
joint.GetBody0Rel().GetTargets(&body0_paths);
if (body0_paths.size() > 1) {
mju_warning("Joint %s has multiple body0 rels. Skipping.",
joint.GetPath().GetAsString().c_str());
return false;
}
// Empty body0, or body0 pointing to the default prim means we'll attach
// to the worldbody.
if (body0_paths.empty() || body0_paths[0] == default_prim_path) {
*from = pxr::SdfPath();
} else {
*from = body0_paths[0];
}
return true;
}
std::unique_ptr<KinematicNode> BuildKinematicTree(
const std::vector<pxr::UsdPhysicsJoint>& joints,
const std::vector<pxr::SdfPath>& all_body_paths,
const pxr::SdfPath& default_prim_path) {
std::map<pxr::SdfPath, std::vector<pxr::SdfPath>> children_map;
std::map<pxr::SdfPath, pxr::SdfPath> parent_map;
std::map<std::pair<pxr::SdfPath, pxr::SdfPath>, pxr::SdfPath>
edge_to_joint_map;
std::set<pxr::SdfPath> all_nodes(all_body_paths.begin(),
all_body_paths.end());
for (const auto& joint : joints) {
pxr::SdfPath from, to;
if (!GetJointBodies(joint, default_prim_path, &from, &to)) {
continue;
}
auto edge_key = std::make_pair(from, to);
auto it = edge_to_joint_map.find(edge_key);
if (it == edge_to_joint_map.end()) {
edge_to_joint_map[edge_key] = joint.GetPath();
} else {
mju_warning(
"Multiple explicit joints defined between body %s and body %s. "
"Joint1: %s, Joint2: %s. Keeping the first one found: %s",
(from.IsEmpty() ? "<worldbody>" : from.GetString()).c_str(),
to.GetString().c_str(), it->second.GetString().c_str(),
joint.GetPath().GetString().c_str(), it->second.GetString().c_str());
continue;
}
if (from == to) {
mju_error("Self-loop detected at node %s", to.GetString().c_str());
return nullptr;
}
if (parent_map.count(to)) {
mju_error("Node %s has multiple parents ('%s' and '%s').",
to.GetString().c_str(), parent_map.at(to).GetString().c_str(),
from.GetString().c_str());
return nullptr;
}
children_map[from].push_back(to);
parent_map[to] = from;
all_nodes.insert(from);
all_nodes.insert(to);
}
// Sort children in children_map to respect the DFS order from the stage.
for (auto& [_, children] : children_map) {
std::sort(
children.begin(), children.end(),
[&v = all_body_paths](const auto& a, const auto& b) {
return std::distance(v.begin(), std::find(v.begin(), v.end(), a)) <
std::distance(v.begin(), std::find(v.begin(), v.end(), b));
});
}
// The world body is represented by an empty SdfPath.
auto world_root = std::make_unique<KinematicNode>();
std::map<pxr::SdfPath, KinematicNode*> node_map;
node_map[pxr::SdfPath()] = world_root.get();
// Use a deque for traversal. We will add roots to the back and children
// to the front to perform a DFS on each root's tree.
std::deque<pxr::SdfPath> q;
// Add roots (floating-base bodies and children of the world) to the queue,
// preserving the DFS order from the USD stage.
for (const auto& body_path : all_body_paths) {
if (!body_path.IsEmpty()) {
const auto it = parent_map.find(body_path);
// A root is a body that has no parent, or its parent is the world.
if (it == parent_map.end() || it->second.IsEmpty()) {
q.push_back(body_path);
}
}
}
while (!q.empty()) {
pxr::SdfPath current_path = q.front();
q.pop_front();
pxr::SdfPath parent_path = parent_map.count(current_path)
? parent_map.at(current_path)
: pxr::SdfPath();
KinematicNode* parent_node = node_map.at(parent_path);
auto new_node = std::make_unique<KinematicNode>();
new_node->body_path = current_path;
if (edge_to_joint_map.count({parent_path, current_path})) {
new_node->joint_path = edge_to_joint_map.at({parent_path, current_path});
}
node_map[current_path] = new_node.get();
parent_node->children.push_back(std::move(new_node));
if (children_map.count(current_path)) {
const auto& children = children_map.at(current_path);
// Add children to the front of the queue in reverse order to ensure
// they are processed in the correct order by the DFS.
for (auto it = children.rbegin(); it != children.rend(); ++it) {
q.push_front(*it);
}
}
}
// After traversal, check for unvisited nodes.
// Unvisited nodes at this point imply a cycle.
for (const auto& node : all_nodes) {
if (!node.IsEmpty() && !node_map.count(node)) {
mju_error("Cycle detected involving node %s.", node.GetString().c_str());
return nullptr;
}
}
return world_root;
}
} // namespace usd
} // namespace mujoco
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2025 DeepMind Technologies Limited
//
// 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.
#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_KINEMATIC_TREE_H_
#define MUJOCO_SRC_EXPERIMENTAL_USD_KINEMATIC_TREE_H_
#include <memory>
#include <vector>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usdPhysics/joint.h>
namespace mujoco {
namespace usd {
// A struct to represent a node in the kinematic tree.
// Using a struct with a vector of children preserves the order of bodies,
// which is important for things like keyframes and policy compatibility.
struct KinematicNode {
pxr::SdfPath body_path;
pxr::SdfPath joint_path; // Joint connecting this node to its parent.
std::vector<std::unique_ptr<KinematicNode>> children;
};
// Builds a single kinematic tree from a list of joints.
// The DFS order of bodies in the tree is determined by the order of bodies in
// `all_body_paths`.
// All bodies, including static and floating-base bodies, are organized under a
// single world root. An empty 'from' path in an edge represents the world body.
// Returns the root of the kinematic tree, or `nullptr` for invalid structures.
std::unique_ptr<KinematicNode> BuildKinematicTree(
const std::vector<pxr::UsdPhysicsJoint>& joints,
const std::vector<pxr::SdfPath>& all_body_paths,
const pxr::SdfPath& default_prim_path);
} // namespace usd
} // namespace mujoco
#endif // MUJOCO_SRC_EXPERIMENTAL_USD_KINEMATIC_TREE_H_
+124 -393
View File
@@ -12,14 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <deque>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
@@ -35,6 +32,7 @@
#include <mujoco/experimental/usd/usd.h>
#include <mujoco/experimental/usd/utils.h>
#include <mujoco/mujoco.h>
#include "experimental/usd/kinematic_tree.h"
#include <pxr/base/gf/declare.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/vec3d.h>
@@ -887,7 +885,7 @@ void ParseMjcPhysicsJointAPI(mjsJoint* mj_joint,
void ParseUsdPhysicsCollider(mjSpec* spec,
const pxr::UsdPhysicsCollisionAPI& collision_api,
const pxr::UsdPrim& parent_prim, mjsBody* parent,
const pxr::UsdPrim& body_prim, mjsBody* parent,
pxr::UsdGeomXformCache& xform_cache) {
pxr::UsdPrim prim = collision_api.GetPrim();
// UsdPhysicsCollisionAPI can only be applied to gprim primitives.
@@ -938,7 +936,7 @@ void ParseUsdPhysicsCollider(mjSpec* spec,
}
}
SetLocalPoseFromPrim(prim, parent_prim, geom, xform_cache);
SetLocalPoseFromPrim(prim, body_prim, geom, xform_cache);
if (!MaybeParseGeomPrimitive(prim, geom, xform_cache)) {
if (prim.IsA<pxr::UsdGeomMesh>()) {
@@ -1139,7 +1137,8 @@ void ParseMjcPhysicsSite(mjSpec* spec, const pxr::MjcPhysicsSiteAPI& site_api,
pxr::UsdGeomXformCache& xform_cache) {
auto prim = site_api.GetPrim();
mjsSite* site = mjs_addSite(parent, 0);
mjs_setName(site->element, site_api.GetPrim().GetPath().GetAsString().c_str());
mjs_setName(site->element,
site_api.GetPrim().GetPath().GetAsString().c_str());
SetLocalPoseFromPrim(site_api.GetPrim(), parent_prim, site, xform_cache);
// Convert USD type to MuJoCo type.
@@ -1170,7 +1169,8 @@ void ParseMjcPhysicsKeyframe(mjSpec* spec,
auto mquat_attr = keyframe.GetMjcMquatAttr();
auto setKeyframeData = [](mjsKey* key, const pxr::UsdAttribute& attr,
std::vector<double>** key_data, double* time = nullptr) {
std::vector<double>** key_data,
double* time = nullptr) {
if (attr.HasAuthoredValue()) {
pxr::VtDoubleArray data;
if (time == nullptr) {
@@ -1257,291 +1257,78 @@ bool IsObjectInPhysicsScene(const T& object,
return false;
};
// A struct to hold the forest representation.
// The forest is a map from a root path to its tree.
// A tree is an adjacency list, mapping a parent path to its children paths.
using MjUsdForest =
std::map<pxr::SdfPath, std::map<pxr::SdfPath, std::vector<pxr::SdfPath>>>;
// Helper type to store all the prims that belong to a body.
using BodyPrimMap = std::map<pxr::SdfPath, std::vector<pxr::SdfPath>>;
// A directed edge.
using Edge = std::pair<pxr::SdfPath, pxr::SdfPath>;
// Recursively traverses the kinematic tree, creating bodies, joints, and geoms
// in the mjSpec.
void PopulateSpecFromTree(pxr::UsdStageRefPtr stage, mjSpec* spec,
mjsBody* parent_mj_body,
const mujoco::usd::KinematicNode* parent_node,
const mujoco::usd::KinematicNode& current_node,
pxr::UsdGeomXformCache& xform_cache,
const BodyPrimMap& body_to_prims) {
mjsBody* current_mj_body;
// A map from a directed edge to the path of the joint representing that edge.
using EdgesMap = std::map<Edge, pxr::SdfPath>;
// Constructs and validates a forest (a collection of disjoint trees)
// from a list of directed edges. It also considers all rigid bodies in the
// scene, treating those not involved in any edge as isolated, free-floating
// bodies.
//
// An empty 'from' path represents the world body. Returns `std::nullopt` for
// invalid forest structures (e.g. cycles, multiple parents).
std::optional<MjUsdForest> BuildForestFromEdges(
const EdgesMap& edges, const std::vector<pxr::SdfPath>& all_body_paths) {
if (edges.empty() && all_body_paths.empty()) {
return MjUsdForest{};
}
std::map<pxr::SdfPath, std::vector<pxr::SdfPath>> children_map;
std::map<pxr::SdfPath, pxr::SdfPath> parent_map;
std::set<pxr::SdfPath> all_nodes(all_body_paths.begin(),
all_body_paths.end());
// 1. Initial Pass: Build maps and perform local validation
for (const auto& edge_pair : edges) {
const auto& [from, to] = edge_pair.first;
if (from == to) {
mju_error("Self-loop detected at node %s", to.GetString().c_str());
return std::nullopt;
}
if (parent_map.count(to)) {
mju_error("Node %s has multiple parents ('%s' and '%s').",
to.GetString().c_str(), parent_map.at(to).GetString().c_str(),
from.GetString().c_str());
return std::nullopt;
}
children_map[from].push_back(to);
parent_map[to] = from;
all_nodes.insert(from);
all_nodes.insert(to);
}
// 2. Find all root nodes
// A root is a node that is not a child of any other node. This includes
// roots of kinematic trees and isolated rigid bodies.
std::set<pxr::SdfPath> roots = all_nodes;
for (const auto& pair : parent_map) {
roots.erase(pair.first); // `pair.first` is a child node.
}
if (roots.empty() && !all_nodes.empty()) {
mju_error("No root nodes found, but edges exist. A cycle is present.");
return std::nullopt;
}
// 3. Build the forest, claiming nodes for each tree
MjUsdForest forest;
std::set<pxr::SdfPath> claimed_nodes;
for (const auto& root : roots) {
// Perform a traversal (BFS) to find all nodes in this tree
std::deque<pxr::SdfPath> q;
q.push_back(root);
std::set<pxr::SdfPath> nodes_in_this_tree;
while (!q.empty()) {
pxr::SdfPath current_node = q.front();
q.pop_front();
if (claimed_nodes.count(current_node)) {
mju_error("Node %s is shared between multiple trees.",
current_node.GetString().c_str());
return std::nullopt;
}
nodes_in_this_tree.insert(current_node);
claimed_nodes.insert(current_node);
if (children_map.count(current_node)) {
for (const auto& child : children_map.at(current_node)) {
q.push_back(child);
}
}
}
// Construct the adjacency list for this specific tree
std::map<pxr::SdfPath, std::vector<pxr::SdfPath>> tree_adj_list;
for (const auto& node : nodes_in_this_tree) {
if (children_map.count(node)) {
tree_adj_list[node] = children_map.at(node);
}
}
forest[root] = tree_adj_list;
}
// 4. Final check for cycles (unclaimed nodes)
std::set<pxr::SdfPath> unclaimed_nodes; // all_nodes - claimed_nodes
std::set_difference(all_nodes.begin(), all_nodes.end(), claimed_nodes.begin(),
claimed_nodes.end(),
std::inserter(unclaimed_nodes, unclaimed_nodes.begin()));
if (!unclaimed_nodes.empty()) {
std::string unclaimed_str;
for (const auto& node : unclaimed_nodes) {
unclaimed_str += "'" + node.GetString() + "' ";
}
mju_error(
"Cycle detected. The following nodes are part of a cycle "
"and not reachable from any root: %s",
unclaimed_str.c_str());
return std::nullopt;
}
return forest;
}
void TraverseAndBuildTree(
pxr::UsdStageRefPtr stage, mjSpec* spec, mjsBody* parent_mj_body,
const pxr::SdfPath& parent_body_path, const pxr::SdfPath& current_body_path,
const std::map<pxr::SdfPath, std::vector<pxr::SdfPath>>& tree,
const EdgesMap& edges, pxr::UsdGeomXformCache& xform_cache);
// Traverses the prim and all its descendants in the USD hierarchy and parses
// supported entities like colliders and sites, attaching them to the given
// mjBody. The traversal for a given branch stops when:
// - a descendant with a RigidBodyAPI is found, as that will be handled by
// TraverseAndBuildTree.
// - a descendant with a ResetXformStack is found, as that will be handled by
// the top level traversal of independent prims.
void ParseCurrentAndDescendants(mjSpec* spec, const pxr::UsdPrim& prim,
const pxr::UsdPrim& parent_prim, mjsBody* body,
pxr::UsdGeomXformCache& xform_cache) {
if (prim.HasAPI<pxr::UsdPhysicsCollisionAPI>()) {
ParseUsdPhysicsCollider(spec, pxr::UsdPhysicsCollisionAPI(prim),
parent_prim, body, xform_cache);
}
if (prim.HasAPI<pxr::MjcPhysicsSiteAPI>()) {
ParseMjcPhysicsSite(spec, pxr::MjcPhysicsSiteAPI(prim), parent_prim, body,
xform_cache);
}
// Make sure we traverse into instance proxies to ensure we support
// instanceable references.
// See https://openusd.org/dev/api/_usd__page__scenegraph_instancing.html
for (const auto& child :
prim.GetFilteredChildren(pxr::UsdTraverseInstanceProxies())) {
if (child.HasAPI<pxr::UsdPhysicsRigidBodyAPI>()) {
continue;
}
if (xform_cache.GetResetXformStack(child)) {
continue;
}
ParseCurrentAndDescendants(spec, child, prim, body, xform_cache);
}
}
// Recursively traverses a kinematic tree, creating bodies and joints in the
// mjSpec.
void TraverseAndBuildTree(
pxr::UsdStageRefPtr stage, mjSpec* spec, mjsBody* parent_mj_body,
const pxr::SdfPath& parent_body_path, const pxr::SdfPath& current_body_path,
const std::map<pxr::SdfPath, std::vector<pxr::SdfPath>>& tree,
const EdgesMap& edges, pxr::UsdGeomXformCache& xform_cache) {
pxr::UsdPrim current_body_prim = stage->GetPrimAtPath(current_body_path);
pxr::UsdPrim parent_prim_for_xform =
parent_body_path.IsEmpty() ? stage->GetPseudoRoot()
: stage->GetPrimAtPath(parent_body_path);
mjsBody* current_mj_body = ParseUsdPhysicsRigidbody(
spec, pxr::UsdPhysicsRigidBodyAPI(current_body_prim),
parent_prim_for_xform, parent_mj_body, xform_cache);
auto edge_key = std::make_pair(parent_body_path, current_body_path);
auto it_edge = edges.find(edge_key);
if (it_edge != edges.end()) {
// An edge exists, indicating a connection to the parent. This body is
// either world-attached or part of a larger articulation.
const pxr::SdfPath& joint_path = it_edge->second;
if (!joint_path.IsEmpty()) {
// An explicit joint prim exists, so we parse it.
pxr::UsdPrim joint_prim = stage->GetPrimAtPath(joint_path);
ParseUsdPhysicsJoint(spec, joint_prim, current_mj_body, xform_cache);
}
// If joint_path is empty, it's an implicit fixed joint. No joint is created
// in the mjSpec, effectively welding the body to its parent.
if (current_node.body_path.IsEmpty()) {
// This is the world root node.
current_mj_body = mjs_findBody(spec, "world");
} else {
// No edge found. This condition is met for the root of a floating-base
// tree, which has no defined joint connecting it to the world.
if (parent_mj_body == mjs_findBody(spec, "world")) {
// We explicitly create a free joint to make it a floating-base body.
// This is a regular body.
pxr::UsdPrim current_body_prim =
stage->GetPrimAtPath(current_node.body_path);
pxr::SdfPath parent_body_path =
parent_node ? parent_node->body_path : pxr::SdfPath();
pxr::UsdPrim parent_prim_for_xform =
parent_body_path.IsEmpty() ? stage->GetPseudoRoot()
: stage->GetPrimAtPath(parent_body_path);
current_mj_body = ParseUsdPhysicsRigidbody(
spec, pxr::UsdPhysicsRigidBodyAPI(current_body_prim),
parent_prim_for_xform, parent_mj_body, xform_cache);
if (!current_node.joint_path.IsEmpty()) {
pxr::UsdPrim joint_prim = stage->GetPrimAtPath(current_node.joint_path);
ParseUsdPhysicsJoint(spec, joint_prim, current_mj_body, xform_cache);
} else if (parent_mj_body == mjs_findBody(spec, "world")) {
// No joint to parent, and parent is world: this is a floating body.
mjsJoint* free_joint = mjs_addJoint(current_mj_body, nullptr);
free_joint->type = mjJNT_FREE;
}
}
// Parse all geoms/sites that are found on this body in the USD hierarchy.
ParseCurrentAndDescendants(spec, current_body_prim, parent_prim_for_xform,
current_mj_body, xform_cache);
// Recurse through the kinematic tree.
auto it_tree = tree.find(current_body_path);
if (it_tree != tree.end()) {
const auto& children_paths = it_tree->second;
for (const auto& child_path : children_paths) {
TraverseAndBuildTree(stage, spec, current_mj_body, current_body_path,
child_path, tree, edges, xform_cache);
}
}
}
// Adds a new edge to the edges map, dealing with duplicates:
// - explicit joints always replace implicit joints
// - more than one explicit joint is unsupported so we print a warning and
// keep the first one found
void AddEdge(EdgesMap& edges, const pxr::SdfPath& from, const pxr::SdfPath& to,
const pxr::SdfPath& joint) {
auto edge_key = std::make_pair(from, to);
auto it = edges.find(edge_key);
if (it == edges.end()) {
// No existing edge, add the new one.
edges[edge_key] = joint;
} else {
// Edge already exists.
pxr::SdfPath& existing_joint = it->second;
bool new_is_explicit = !joint.IsEmpty();
bool existing_is_explicit = !existing_joint.IsEmpty();
if (new_is_explicit) {
if (existing_is_explicit) {
// Both are explicit: this is an error condition.
mju_warning(
"Multiple explicit joints defined between body %s and body %s. "
"Joint1: %s, Joint2: %s. Keeping the first one found: %s",
(from.IsEmpty() ? "<worldbody>" : from.GetString()).c_str(),
to.GetString().c_str(), existing_joint.GetString().c_str(),
joint.GetString().c_str(), existing_joint.GetString().c_str());
} else {
// New is explicit, existing is implicit: replace.
existing_joint = joint;
// Add geoms/sites/etc. belonging to the current body.
auto it_prims = body_to_prims.find(current_node.body_path);
if (it_prims != body_to_prims.end()) {
pxr::UsdPrim body_prim_for_xform =
current_node.body_path.IsEmpty()
? stage->GetPseudoRoot()
: stage->GetPrimAtPath(current_node.body_path);
for (const auto& gprim_path : it_prims->second) {
pxr::UsdPrim prim = stage->GetPrimAtPath(gprim_path);
if (prim.HasAPI<pxr::UsdPhysicsCollisionAPI>()) {
ParseUsdPhysicsCollider(spec, pxr::UsdPhysicsCollisionAPI(prim),
body_prim_for_xform, current_mj_body,
xform_cache);
}
if (prim.HasAPI<pxr::MjcPhysicsSiteAPI>()) {
ParseMjcPhysicsSite(spec, pxr::MjcPhysicsSiteAPI(prim),
body_prim_for_xform, current_mj_body, xform_cache);
}
}
// If new is implicit, and an edge already exists (either explicit or
// implicit), we keep the existing one. No action needed.
}
}
// Returns the nesting body prim (or an invalid prim if there's
// any resets_xform_stack, or we've reached the end).
pxr::UsdPrim GetNestingBodyPrim(const pxr::UsdPrim& prim,
pxr::UsdGeomXformCache& xform_cache) {
if (xform_cache.GetResetXformStack(prim)) {
return pxr::UsdPrim();
// Recurse through children.
for (const auto& child_node : current_node.children) {
PopulateSpecFromTree(stage, spec, current_mj_body, &current_node,
*child_node, xform_cache, body_to_prims);
}
pxr::UsdPrim previous_prim = prim.GetParent();
while (previous_prim.IsValid()) {
// If we find a rigid body, this is our answer. The prim is nested.
if (previous_prim.HasAPI<pxr::UsdPhysicsRigidBodyAPI>()) {
return previous_prim;
}
// If we encounter a prim that resets the transform stack *before* finding
// a rigid body, the chain is broken. The prim is not nested.
if (xform_cache.GetResetXformStack(previous_prim)) {
return pxr::UsdPrim();
}
previous_prim = previous_prim.GetParent();
}
return pxr::UsdPrim();
}
} // namespace
mjSpec* mj_parseUSDStage(const pxr::UsdStageRefPtr stage) {
mjSpec* spec = mj_makeSpec();
mjsBody* world = mjs_findBody(spec, "world");
std::vector<pxr::UsdPhysicsScene> physics_scenes;
// Xform cache to use for all queries when parsing.
@@ -1573,135 +1360,79 @@ mjSpec* mj_parseUSDStage(const pxr::UsdStageRefPtr stage) {
default_prim_path = stage->GetDefaultPrim().GetPath();
}
std::vector<pxr::SdfPath> body_paths;
EdgesMap edges;
// TODO(robinalazard): Re-introduce properly adding objects to their
// respective physics scene, or default.
// Data Structures
std::vector<pxr::UsdPhysicsJoint> all_joints;
std::vector<pxr::SdfPath> all_body_paths_vec;
BodyPrimMap body_to_prims;
// Traverse all prims under the pseudo-root.
// We ensure to traverse into instance proxies to ensure we support
// instanceable references.
// See https://openusd.org/dev/api/_usd__page__scenegraph_instancing.html
for (auto prim : stage->Traverse(pxr::UsdTraverseInstanceProxies())) {
// When traversing the whole scene, if we encounter a rigidbody or a joint,
// then we populate the edges map and the list of body paths, which will be
// processed later to build the articulation trees.
//
// If we encounter _anything else_:
// - if we find that it's an independent prim (e.g. a static collider or
// site) not belonging to any rigidbody, then we add it directly to the
// world.
// - otherwise, they will be handled when building the articulation trees.
// =========================================================================
// PASS 1: Collect Bodies, Joints, and Geoms/Sites/etc.
// =========================================================================
// A single DFS pass to find all bodies, joints, and determine
// which body owns each geom/site/etc. prim.
std::vector<pxr::SdfPath> owner_stack;
owner_stack.push_back(pxr::SdfPath()); // Start with the world as owner.
const auto range = pxr::UsdPrimRange::PreAndPostVisit(
stage->GetPseudoRoot(), pxr::UsdTraverseInstanceProxies());
for (auto it = range.begin(); it != range.end(); ++it) {
pxr::UsdPrim prim = *it;
bool is_body = prim.HasAPI<pxr::UsdPhysicsRigidBodyAPI>();
bool resets = xform_cache.GetResetXformStack(prim);
// Only update (push/pop) the owner stack for bodies (becomes new owner) and
// resetXformStack (reset owner to world).
bool is_pushed_to_stack = is_body || resets;
if (it.IsPostVisit()) {
if (is_pushed_to_stack) {
owner_stack.pop_back();
}
continue;
}
pxr::SdfPath prim_path = prim.GetPath();
pxr::SdfPath prim_owner = owner_stack.back();
if (is_body) {
all_body_paths_vec.push_back(prim_path);
prim_owner = prim_path;
} else if (resets) {
prim_owner = pxr::SdfPath(); // Reset owner to world.
}
if (is_pushed_to_stack) {
owner_stack.push_back(prim_owner);
}
if (prim.HasAPI<pxr::UsdPhysicsCollisionAPI>() ||
prim.HasAPI<pxr::MjcPhysicsSiteAPI>()) {
body_to_prims[prim_owner].push_back(prim_path);
}
if (prim.IsA<pxr::UsdPhysicsJoint>()) {
pxr::SdfPath joint_path = prim.GetPath();
pxr::UsdPhysicsJoint joint(prim);
all_joints.push_back(pxr::UsdPhysicsJoint(prim));
pxr::SdfPath body1_path;
pxr::SdfPathVector body1_paths;
joint.GetBody1Rel().GetTargets(&body1_paths);
if (body1_paths.empty()) {
mju_warning("Joint %s does not have body1 rel. Skipping.",
prim.GetPath().GetAsString().c_str());
continue;
} else if (body1_paths.size() > 1) {
mju_warning("Joint %s has multiple body1 rels. Skipping.",
prim.GetPath().GetAsString().c_str());
continue;
}
body1_path = body1_paths[0];
pxr::SdfPath body0_path;
pxr::SdfPathVector body0_paths;
joint.GetBody0Rel().GetTargets(&body0_paths);
if (body0_paths.size() > 1) {
mju_warning("Joint %s has multiple body0 rels. Skipping.",
prim.GetPath().GetAsString().c_str());
continue;
}
// Empty body0, or body0 pointing to the default prim means we'll attach
// to the worldbody.
if (body0_paths.empty() || body0_paths[0] == default_prim_path) {
body0_path = pxr::SdfPath();
} else {
body0_path = body0_paths[0];
}
AddEdge(edges, body0_path, body1_path, joint_path);
it.PruneChildren();
} else if (prim.IsA<pxr::MjcPhysicsKeyframe>()) {
ParseMjcPhysicsKeyframe(spec, pxr::MjcPhysicsKeyframe(prim));
continue;
} else if (prim.HasAPI<pxr::UsdPhysicsRigidBodyAPI>()) {
pxr::SdfPath body_path = prim.GetPath();
body_paths.push_back(body_path);
// Find whether we are nested under a parent body.
// Note: if any xform in between (including the current prim) resets the
// xform stack then we are not nested.
pxr::UsdPrim nesting_body_prim = GetNestingBodyPrim(prim, xform_cache);
if (nesting_body_prim.IsValid()) {
AddEdge(edges, nesting_body_prim.GetPath(), body_path, pxr::SdfPath());
}
} else {
// TODO(robinalazard): the way we handle independent prims right now means
// their relative transforms will be ignored be always pass
// stage->GetPseudoRoot() as the parent. It works for most scene
// realistically. But we should fix it.
if (prim.HasAPI<pxr::UsdPhysicsCollisionAPI>()) {
// Find whether the collider belongs to a body. If yes, it will be we
// handled later when building the articulation trees. Otherwise, it's a
// static collider and we add it directly to the world.
pxr::UsdPrim nesting_body_prim = GetNestingBodyPrim(prim, xform_cache);
if (!nesting_body_prim.IsValid()) {
ParseUsdPhysicsCollider(spec, pxr::UsdPhysicsCollisionAPI(prim),
stage->GetPseudoRoot(), world, xform_cache);
}
}
if (prim.HasAPI<pxr::MjcPhysicsSiteAPI>()) {
// Find whether the site belongs to a body. If yes, it will be we
// handled later when building the articulation trees. Otherwise, it's a
// static site and we add it directly to the world.
pxr::UsdPrim nesting_body_prim = GetNestingBodyPrim(prim, xform_cache);
if (!nesting_body_prim.IsValid()) {
ParseMjcPhysicsSite(spec, pxr::MjcPhysicsSiteAPI(prim),
stage->GetPseudoRoot(), world, xform_cache);
}
}
it.PruneChildren();
}
}
std::optional<MjUsdForest> forest = BuildForestFromEdges(edges, body_paths);
if (forest.has_value()) {
// Now that we have the forest, we can walk through it and add the bodies
// and joints to the spec. From each body we can also visit and parse all
// the children to also add the corresponding colliders to the body in the
// spec.
for (const auto& [root_path, tree] : *forest) {
if (root_path.IsEmpty()) {
// This root is the world. This case handles all kinematic trees that
// are attached to the world (fixed-base articulations).
// We iterate through its direct children which are the root bodies of
// each world-attached tree. We iterate through these children and begin
// the recursive build from there.
const auto& children_of_world = tree.at(pxr::SdfPath());
for (const auto& child_path : children_of_world) {
TraverseAndBuildTree(stage, spec, world, root_path, child_path, tree,
edges, xform_cache);
}
} else {
// Conversely, this case handles all the remaining top-level root bodies
// which are their own roots and are not attached to the world
// (floating-base articulations). This includes isolated bodies.
// We directly begin the recursive build from the toplevel root.
//
// Note: the absence of an edge in the `edges` map connecting the world
// to this root is what signals to `TraverseAndBuildTree` that this is a
// floating base, prompting the creation of a free joint.
TraverseAndBuildTree(stage, spec, world, pxr::SdfPath(), root_path,
tree, edges, xform_cache);
}
}
// =========================================================================
// PASS 2: Build the kinematic tree and populate the mjSpec.
// =========================================================================
std::unique_ptr<mujoco::usd::KinematicNode> kinematic_tree =
mujoco::usd::BuildKinematicTree(all_joints, all_body_paths_vec,
default_prim_path);
if (kinematic_tree) {
PopulateSpecFromTree(stage, spec, /*parent_mj_body=*/nullptr,
/*parent_node=*/nullptr, *kinematic_tree, xform_cache,
body_to_prims);
}
return spec;