Files
Mujoco_WASM/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc
T
Matija Kecman 8eca9926dc Fix various MuJoCo Studio build issues on Windows, MacOS and Linux
PiperOrigin-RevId: 833313973
Change-Id: I7ec181374e0bb49c32420d025044c31f6fc3c657
2025-11-17 06:35:43 -08:00

2374 lines
101 KiB
C++

// 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 "mjcf/mujoco_to_usd.h"
#include <algorithm>
#include <cstddef>
#include <numbers>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <mujoco/experimental/usd/mjcPhysics/tokens.h>
#include <mujoco/experimental/usd/utils.h>
#include <mujoco/mjspec.h>
#include <mujoco/mujoco.h>
#include "mjcf/utils.h"
#include <pxr/base/arch/attributes.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/quatf.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/vec2f.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/tf/callContext.h>
#include <pxr/base/tf/diagnostic.h>
#include <pxr/base/tf/diagnosticHelper.h>
#include <pxr/base/tf/enum.h>
#include <pxr/base/tf/registryManager.h>
#include <pxr/base/tf/staticData.h>
#include <pxr/base/tf/staticTokens.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/vt/types.h>
#include <pxr/base/vt/value.h>
#include <pxr/usd/kind/registry.h>
#include <pxr/usd/sdf/abstractData.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/declareHandles.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/schema.h>
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdLux/tokens.h>
#include <pxr/usd/usdPhysics/fixedJoint.h>
#include <pxr/usd/usdPhysics/joint.h>
#include <pxr/usd/usdPhysics/prismaticJoint.h>
#include <pxr/usd/usdPhysics/revoluteJoint.h>
#include <pxr/usd/usdPhysics/sphericalJoint.h>
#include <pxr/usd/usdPhysics/tokens.h>
#include <pxr/usd/usdShade/tokens.h>
#include <pxr/usdImaging/usdImaging/tokens.h>
namespace {
// The ID of the World in mjModel and mjData.
static constexpr int kWorldIndex = 0;
// Using to satisfy TF_DEFINE_PRIVATE_TOKENS macro below and avoid operating in
// PXR_NS.
using pxr::TfToken;
template <typename T>
using TfStaticData = pxr::TfStaticData<T>;
// clang-format off
TF_DEFINE_PRIVATE_TOKENS(kTokens,
((body, "Body"))
((body_name, "mujoco:body_name"))
((geom, "Geom"))
((light, "Light"))
((meshScope, "MeshSources"))
((materialsScope, "Materials"))
((physicsMaterialsScope, "PhysicsMaterials"))
((previewSurface, "PreviewSurface"))
((keyframesScope, "Keyframes"))
((actuatorsScope, "Actuators"))
((keyframe, "Keyframe"))
((surface, "PreviewSurface"))
((world, "World"))
((xformOpTransform, "xformOp:transform"))
((xformOpScale, "xformOp:scale"))
(st)
((primvarsSt, "primvars:st"))
((outputsSt, "outputs:st"))
((inputsSt, "inputs:st"))
((inputsVarname, "inputs:varname"))
((inputsFile, "inputs:file"))
((inputsWrapS, "inputs:wrapS"))
((inputsWrapT, "inputs:wrapT"))
((inputsDiffuseColor, "inputs:diffuseColor"))
((inputsEmissiveColor, "inputs:emissiveColor"))
((outputsRgb, "outputs:rgb"))
((outputsR, "outputs:r"))
((outputsG, "outputs:g"))
((outputsB, "outputs:b"))
((inputsMetallic, "inputs:metallic"))
((inputsOcclusion, "inputs:occlusion"))
((inputsRoughness, "inputs:roughness"))
(repeat)
((sourceMesh, pxr::UsdGeomTokens->Mesh))
((inputsNormal, "inputs:normal"))
((joint, "Joint"))
);
// Using to satisfy TF_REGISTRY_FUNCTION macro below and avoid operating in PXR_NS.
using pxr::TfEnum;
using pxr::Tf_RegistryStaticInit;
using pxr::Tf_RegistryInit;
using pxr::TfEnum;
template <typename T>
using Arch_PerLibInit = pxr::Arch_PerLibInit<T>;
#if defined(ARCH_OS_DARWIN)
using Arch_ConstructorEntry = pxr::Arch_ConstructorEntry;
#endif
enum ErrorCodes { UnsupportedActuatorTypeError, UnsupportedGeomTypeError, MujocoCompilationError };
TF_REGISTRY_FUNCTION(pxr::TfEnum) {
TF_ADD_ENUM_NAME(UnsupportedGeomTypeError, "UsdGeom type is unsupported.")
TF_ADD_ENUM_NAME(MujocoCompilationError, "Mujoco spec failed to compile.")
}
// Usings to satisfy TF_ERROR macro.
using pxr::TfCallContext;
using pxr::Tf_PostErrorHelper;
// clang-format on
using pxr::MjcPhysicsTokens;
using mujoco::usd::AddAttributeConnection;
using mujoco::usd::AddPrimInherit;
using mujoco::usd::AddPrimReference;
using mujoco::usd::ApplyApiSchema;
using mujoco::usd::CreateAttributeSpec;
using mujoco::usd::CreateClassSpec;
using mujoco::usd::CreatePrimSpec;
using mujoco::usd::CreateRelationshipSpec;
using mujoco::usd::SetAttributeDefault;
using mujoco::usd::SetAttributeTimeSample;
using mujoco::usd::SetLayerMetadata;
using mujoco::usd::SetPrimKind;
using mujoco::usd::SetPrimPurpose;
pxr::GfMatrix4d MujocoPosQuatToTransform(mjtNum *pos, mjtNum *quat) {
pxr::GfQuatd quaternion = pxr::GfQuatd::GetIdentity();
quaternion.SetReal(quat[0]);
quaternion.SetImaginary(quat[1], quat[2], quat[3]);
pxr::GfRotation rotation(quaternion);
pxr::GfVec3d translation(0.0, 0.0, 0.0);
translation.Set(pos[0], pos[1], pos[2]);
pxr::GfMatrix4d transform;
transform.SetTransform(rotation, translation);
return transform;
}
} // namespace
class ModelWriter {
public:
ModelWriter(mjSpec* spec, mjModel* model, pxr::SdfLayerRefPtr layer,
bool skip_elems_from_usd)
: spec_(spec),
model_(model),
layer_(layer),
skip_elems_from_usd_(skip_elems_from_usd),
class_path_("/Bad_Path") {
body_paths_ = std::vector<pxr::SdfPath>(model->nbody);
site_paths_ = std::vector<pxr::SdfPath>(model->nsite);
joint_paths_ = std::vector<pxr::SdfPath>(model->njnt);
}
~ModelWriter() { mj_deleteModel(model_); }
bool ShouldWrite(mjsElement* element) {
// If we've been told to skip over spec elements that originated from USD,
// check the user value and if it has a primpath then it came from decoding
// a USD stage into an MjSpec.
return !skip_elems_from_usd_ || mujoco::usd::GetUsdPrimPathUserValue(element).IsEmpty();
}
void Write() {
// Create top level class holder.
class_path_ = CreateClassSpec(layer_, pxr::SdfPath::AbsoluteRootPath(),
pxr::TfToken("__class__"))->GetPath();
// Create the world body.
body_paths_[kWorldIndex] = WriteWorldBody(kWorldIndex)->GetPath();
layer_->SetDocumentation("Generated by mujoco model writer.");
// Set the world body to be the default prim for referencing/payloads.
layer_->SetDefaultPrim(body_paths_[kWorldIndex].GetNameToken());
// Mujoco is Z up by default.
SetLayerMetadata(layer_, pxr::UsdGeomTokens->upAxis, pxr::UsdGeomTokens->z);
// Mujoco is authored in meters by default.
SetLayerMetadata(layer_, pxr::UsdGeomTokens->metersPerUnit,
pxr::UsdGeomLinearUnits::meters);
WritePhysicsScene();
// Author mesh scope + mesh prims to be referenced.
WriteMeshes();
WriteMaterials();
WriteBodies();
WriteActuators();
WriteKeyframes();
}
private:
mjSpec *spec_;
mjModel *model_;
pxr::SdfLayerRefPtr layer_;
bool skip_elems_from_usd_ = false;
// Path to top level class spec that all classes should be children of.
pxr::SdfPath class_path_;
// Mapping from Mujoco body id to SdfPath.
std::vector<pxr::SdfPath> body_paths_;
// Mapping from Mujoco site id to SdfPath.
std::vector<pxr::SdfPath> site_paths_;
// Mapping from Mujoco joint id to SdfPath.
std::vector<pxr::SdfPath> joint_paths_;
// Mapping from mesh names to Mesh prim path.
std::unordered_map<std::string, pxr::SdfPath> mesh_paths_;
// Set of body ids that have had the articulation root API applied.
std::unordered_set<int> articulation_roots_;
// Given a name index and a parent prim path this returns a
// token such that appending it to the parent prim path does not
// identify an existing prim spec.
//
// This is necessary since mujoco does not require names for elements
// so we must differentiate between elements of the same type.
//
// For example:
// <mujoco model="model with two planes">
// <worldbody>
// <geom type="plane"/>
// <geom type="plane"/>
// </worldbody>
// </mujoco>
//
// We expect that the occurrence of this happens little enough that linear
// searching is plenty efficient.
pxr::TfToken GetAvailablePrimName(const std::string base_name,
const pxr::TfToken fallback_name,
const pxr::SdfPath &parent_path) {
const auto valid_base_name = pxr::TfMakeValidIdentifier(
base_name.empty() ? fallback_name : base_name);
std::string name = valid_base_name;
pxr::SdfPath test_path = parent_path.AppendChild(pxr::TfToken(name));
int count = 1;
while (layer_->HasSpec(test_path) &&
layer_->GetSpecType(test_path) == pxr::SdfSpecType::SdfSpecTypePrim) {
name = pxr::TfStringPrintf("%s_%d", valid_base_name.c_str(), count++);
test_path = parent_path.AppendChild(pxr::TfToken(name));
}
return pxr::TfToken(name);
}
// This function, conversely to GetAvailablePrimName will not handle
// collisions. This is useful when looking up a prim path that might exist or
// a path that you know must be unique.
pxr::TfToken GetValidPrimName(const std::string name) {
return pxr::TfToken(pxr::TfMakeValidIdentifier(name));
}
struct BodyPathComponents {
pxr::SdfPath parent_path;
pxr::TfToken body_name;
};
void WriteScaleXformOp(const pxr::SdfPrimSpecHandle prim_spec,
const pxr::GfVec3f &scale) {
auto scale_attr_spec =
CreateAttributeSpec(layer_, prim_spec, kTokens->xformOpScale,
pxr::SdfValueTypeNames->Float3);
SetAttributeDefault(layer_, scale_attr_spec, scale);
}
void WriteTransformXformOp(const pxr::SdfPrimSpecHandle prim_spec,
const pxr::GfMatrix4d &transform) {
auto transform_op_spec =
CreateAttributeSpec(layer_, prim_spec, kTokens->xformOpTransform,
pxr::SdfValueTypeNames->Matrix4d);
SetAttributeDefault(layer_, transform_op_spec, transform);
}
void WriteXformOpOrder(const pxr::SdfPrimSpecHandle prim_spec,
const pxr::VtArray<pxr::TfToken> &order) {
auto xform_op_order_spec =
CreateAttributeSpec(layer_, prim_spec, pxr::UsdGeomTokens->xformOpOrder,
pxr::SdfValueTypeNames->TokenArray);
SetAttributeDefault(layer_, xform_op_order_spec, order);
}
template <typename T>
void WriteUniformAttribute(const pxr::SdfPrimSpecHandle prim_spec,
const pxr::SdfValueTypeName &value_type_name,
const pxr::TfToken &token, const T &value) {
auto attr_spec = CreateAttributeSpec(
layer_, prim_spec, token, value_type_name, pxr::SdfVariabilityUniform);
SetAttributeDefault(layer_, attr_spec, value);
}
template <typename T>
void WriteColorAndOpacityAttributes(const pxr::SdfPrimSpecHandle prim_spec,
const T &element) {
// If rgba is not the default (0.5, 0.5, 0.5, 1), then set the
// displayColor attribute.
// No effort is made to properly handle the interaction between rgba
// and the material if both are specified.
if (element->rgba[0] != 0.5f || element->rgba[1] != 0.5f ||
element->rgba[2] != 0.5f || element->rgba[3] != 1.0f) {
// Set the displayColor attribute.
auto display_color_attr = CreateAttributeSpec(
layer_, prim_spec, pxr::UsdGeomTokens->primvarsDisplayColor,
pxr::SdfValueTypeNames->Color3fArray);
SetAttributeDefault(
layer_, display_color_attr,
pxr::VtArray<pxr::GfVec3f>{
{element->rgba[0], element->rgba[1], element->rgba[2]}});
// Set the displayOpacity attribute, only if the opacity is not 1.
if (element->rgba[3] != 1.0f) {
auto display_opacity_attr = CreateAttributeSpec(
layer_, prim_spec, pxr::UsdGeomTokens->primvarsDisplayOpacity,
pxr::SdfValueTypeNames->FloatArray);
SetAttributeDefault(layer_, display_opacity_attr,
pxr::VtArray<float>{element->rgba[3]});
}
}
}
void PrependToXformOpOrder(const pxr::SdfPrimSpecHandle prim_spec,
const pxr::VtArray<pxr::TfToken> &order) {
auto xform_op_order_path =
prim_spec->GetPath().AppendProperty(pxr::UsdGeomTokens->xformOpOrder);
if (!prim_spec->HasField(pxr::UsdGeomTokens->xformOpOrder)) {
WriteXformOpOrder(prim_spec, order);
return;
}
auto existing_order =
prim_spec->GetField(pxr::UsdGeomTokens->xformOpOrder).Get<pxr::VtArray<pxr::TfToken>>();
pxr::VtArray<pxr::TfToken> new_order(order.size() + existing_order.size());
std::copy(order.begin(), order.end(), new_order.begin());
std::copy(existing_order.begin(), existing_order.end(),
new_order.begin() + order.size());
SetAttributeDefault(layer_, layer_->GetAttributeAtPath(xform_op_order_path), new_order);
}
void WriteMesh(const mjsMesh *mesh, const pxr::SdfPath &parent_path) {
auto name = GetAvailablePrimName(*mjs_getName(mesh->element),
pxr::UsdGeomTokens->Mesh, parent_path);
auto subcomponent_spec =
CreatePrimSpec(layer_, parent_path, name, pxr::UsdGeomTokens->Xform);
auto mesh_spec =
CreatePrimSpec(layer_, subcomponent_spec->GetPath(), kTokens->sourceMesh,
pxr::UsdGeomTokens->Mesh);
mesh_paths_[*mjs_getName(mesh->element)] = subcomponent_spec->GetPath();
ApplyApiSchema(layer_, mesh_spec, MjcPhysicsTokens->MjcMeshCollisionAPI);
pxr::TfToken inertia = MjcPhysicsTokens->legacy;
if (mesh->inertia == mjtMeshInertia::mjMESH_INERTIA_EXACT) {
inertia = MjcPhysicsTokens->exact;
} else if (mesh->inertia == mjtMeshInertia::mjMESH_INERTIA_CONVEX) {
inertia = MjcPhysicsTokens->convex;
} else if (mesh->inertia == mjtMeshInertia::mjMESH_INERTIA_SHELL) {
inertia = MjcPhysicsTokens->shell;
}
WriteUniformAttribute(mesh_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcInertia, inertia);
WriteUniformAttribute(mesh_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcMaxhullvert, mesh->maxhullvert);
// NOTE: The geometry data taken from the spec is the post-compilation
// data after it has been mjCMesh::Compile'd. So don't be surprised if
// things like user defined vertices have moved due to re-centering to
// CoM and other modifications (see mjCMesh::Process for other xforms).
int mesh_id = mjs_getId(mesh->element);
int vert_start_offset = model_->mesh_vertadr[mesh_id] * 3;
int nvert = model_->mesh_vertnum[mesh_id];
pxr::VtArray<pxr::GfVec3f> points;
points.reserve(nvert);
for (int i = vert_start_offset; i < vert_start_offset + nvert * 3; i += 3) {
points.emplace_back(&model_->mesh_vert[i]);
}
auto points_attr =
CreateAttributeSpec(layer_, mesh_spec, pxr::UsdGeomTokens->points,
pxr::SdfValueTypeNames->Vector3fArray);
SetAttributeDefault(layer_, points_attr, points);
// NOTE: nface is never 0.
int nface = model_->mesh_facenum[mesh_id];
pxr::VtArray<int> faces;
faces.reserve(nface * 3);
int face_start_offset = model_->mesh_faceadr[mesh_id] * 3;
for (int i = face_start_offset; i < face_start_offset + nface * 3; i += 3) {
faces.push_back(model_->mesh_face[i]);
faces.push_back(model_->mesh_face[i + 1]);
faces.push_back(model_->mesh_face[i + 2]);
}
auto face_vertex_idx_attr= CreateAttributeSpec(
layer_, mesh_spec, pxr::UsdGeomTokens->faceVertexIndices,
pxr::SdfValueTypeNames->IntArray);
SetAttributeDefault(layer_, face_vertex_idx_attr, faces);
pxr::VtArray<int> vertex_counts;
for (int i = 0; i < nface; ++i) {
// Mujoco is always triangles.
vertex_counts.push_back(3);
}
auto face_vertex_counts_attr= CreateAttributeSpec(
layer_, mesh_spec, pxr::UsdGeomTokens->faceVertexCounts,
pxr::SdfValueTypeNames->IntArray);
SetAttributeDefault(layer_, face_vertex_counts_attr, vertex_counts);
if (model_->mesh_normalnum[mesh_id]) {
// We have to convert from Mujoco's indexed normals to USD's faceVarying
// normals.
pxr::VtArray<pxr::GfVec3f> normals;
normals.reserve(nface * 3);
int normal_start_adr = model_->mesh_normaladr[mesh_id];
int face_start_offset = model_->mesh_faceadr[mesh_id] * 3;
for (int i = face_start_offset; i < face_start_offset + nface * 3; ++i) {
int normal_adr = normal_start_adr + model_->mesh_facenormal[i];
normals.emplace_back(&model_->mesh_normal[normal_adr * 3]);
}
auto normals_attr=
CreateAttributeSpec(layer_, mesh_spec, pxr::UsdGeomTokens->normals,
pxr::SdfValueTypeNames->Vector3fArray);
SetAttributeDefault(layer_, normals_attr, normals);
normals_attr->SetField(pxr::UsdGeomTokens->interpolation,
pxr::UsdGeomTokens->faceVarying);
}
if (model_->mesh_texcoordnum[mesh_id]) {
// We have to convert from Mujoco's indexed texcoords to USD's faceVarying
// texcoords.
pxr::VtArray<pxr::GfVec2f> texcoords;
texcoords.reserve(nface * 3);
int texcoord_start_adr = model_->mesh_texcoordadr[mesh_id];
int face_start_offset = model_->mesh_faceadr[mesh_id] * 3;
for (int i = face_start_offset; i < face_start_offset + nface * 3; ++i) {
int texcoord_adr = texcoord_start_adr + model_->mesh_facetexcoord[i];
// Invert the V coordinate, Mujoco assumes OpenGL 0,0 is top left.
// But USD UVs use image bottom left 0,0 convention.
pxr::GfVec2f uv(&model_->mesh_texcoord[texcoord_adr * 2]);
uv[1] = 1.0f - uv[1];
texcoords.push_back(uv);
}
auto texcoords_attr=
CreateAttributeSpec(layer_, mesh_spec, kTokens->primvarsSt,
pxr::SdfValueTypeNames->TexCoord2fArray);
SetAttributeDefault(layer_, texcoords_attr, texcoords);
texcoords_attr->SetField(pxr::UsdGeomTokens->interpolation,
pxr::UsdGeomTokens->faceVarying);
}
// Default subdivision scheme is catmull clark so explicitly set it
// to none here.
auto subdivision_scheme= CreateAttributeSpec(
layer_, mesh_spec, pxr::UsdGeomTokens->subdivisionScheme,
pxr::SdfValueTypeNames->Token);
SetAttributeDefault(layer_, subdivision_scheme,
pxr::UsdGeomTokens->none);
}
void WritePhysicsScene() {
auto physics_scene_spec = CreatePrimSpec(
layer_, body_paths_[kWorldIndex], pxr::UsdPhysicsTokens->PhysicsScene,
pxr::UsdPhysicsTokens->PhysicsScene);
ApplyApiSchema(layer_, physics_scene_spec, MjcPhysicsTokens->MjcSceneAPI);
const std::vector<std::pair<pxr::TfToken, double>>
option_double_attributes = {
{MjcPhysicsTokens->mjcOptionTimestep, spec_->option.timestep},
{MjcPhysicsTokens->mjcOptionTolerance, spec_->option.tolerance},
{MjcPhysicsTokens->mjcOptionLs_tolerance,
spec_->option.ls_tolerance},
{MjcPhysicsTokens->mjcOptionNoslip_tolerance,
spec_->option.noslip_tolerance},
{MjcPhysicsTokens->mjcOptionCcd_tolerance,
spec_->option.ccd_tolerance},
{MjcPhysicsTokens->mjcOptionImpratio, spec_->option.impratio},
{MjcPhysicsTokens->mjcOptionDensity, spec_->option.density},
{MjcPhysicsTokens->mjcOptionViscosity, spec_->option.viscosity},
{MjcPhysicsTokens->mjcOptionO_margin, spec_->option.o_margin},
};
for (const auto &[token, value] : option_double_attributes) {
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Double,
token, value);
}
const std::vector<std::pair<pxr::TfToken, int>> option_int_attributes = {
{MjcPhysicsTokens->mjcOptionIterations, spec_->option.iterations},
{MjcPhysicsTokens->mjcOptionLs_iterations, spec_->option.ls_iterations},
{MjcPhysicsTokens->mjcOptionNoslip_iterations,
spec_->option.noslip_iterations},
{MjcPhysicsTokens->mjcOptionCcd_iterations,
spec_->option.ccd_iterations},
{MjcPhysicsTokens->mjcOptionSdf_iterations,
spec_->option.sdf_iterations},
{MjcPhysicsTokens->mjcOptionSdf_initpoints,
spec_->option.sdf_initpoints},
};
for (const auto &[token, value] : option_int_attributes) {
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Int,
token, value);
}
auto cone_attr = CreateAttributeSpec(
layer_, physics_scene_spec, MjcPhysicsTokens->mjcOptionCone,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
switch (spec_->option.cone) {
case mjCONE_PYRAMIDAL:
SetAttributeDefault(layer_, cone_attr, MjcPhysicsTokens->pyramidal);
break;
case mjCONE_ELLIPTIC:
SetAttributeDefault(layer_, cone_attr, MjcPhysicsTokens->elliptic);
break;
default:
break;
}
auto jacobian_attr = CreateAttributeSpec(
layer_, physics_scene_spec, MjcPhysicsTokens->mjcOptionJacobian,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
switch (spec_->option.jacobian) {
case mjJAC_AUTO:
SetAttributeDefault(layer_, jacobian_attr, MjcPhysicsTokens->auto_);
break;
case mjJAC_DENSE:
SetAttributeDefault(layer_, jacobian_attr, MjcPhysicsTokens->dense);
break;
case mjJAC_SPARSE:
SetAttributeDefault(layer_, jacobian_attr, MjcPhysicsTokens->sparse);
break;
default:
break;
}
auto solver_attr = CreateAttributeSpec(
layer_, physics_scene_spec, MjcPhysicsTokens->mjcOptionSolver,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
switch (spec_->option.solver) {
case mjSOL_NEWTON:
SetAttributeDefault(layer_, solver_attr, MjcPhysicsTokens->newton);
break;
case mjSOL_PGS:
SetAttributeDefault(layer_, solver_attr, MjcPhysicsTokens->pgs);
break;
case mjSOL_CG:
SetAttributeDefault(layer_, solver_attr, MjcPhysicsTokens->cg);
break;
default:
break;
}
pxr::GfVec3f gravity(spec_->option.gravity[0], spec_->option.gravity[1],
spec_->option.gravity[2]);
// Normalize will normalize gravity in place and return the magnitude before
// normalization.
float gravity_magnitude = gravity.Normalize();
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Float,
pxr::UsdPhysicsTokens->physicsGravityMagnitude,
gravity_magnitude);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Vector3f,
pxr::UsdPhysicsTokens->physicsGravityDirection,
gravity);
pxr::GfVec3d wind(spec_->option.wind[0], spec_->option.wind[1],
spec_->option.wind[2]);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Double3,
MjcPhysicsTokens->mjcOptionWind, wind);
pxr::GfVec3d magnetic(spec_->option.magnetic[0], spec_->option.magnetic[1],
spec_->option.magnetic[2]);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Double3,
MjcPhysicsTokens->mjcOptionMagnetic, magnetic);
pxr::VtArray<double> o_solref(spec_->option.o_solref,
spec_->option.o_solref + mjNREF);
WriteUniformAttribute(physics_scene_spec,
pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcOptionO_solref, o_solref);
pxr::VtArray<double> o_solimp(spec_->option.o_solimp,
spec_->option.o_solimp + mjNIMP);
WriteUniformAttribute(physics_scene_spec,
pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcOptionO_solimp, o_solimp);
pxr::VtArray<double> o_friction(spec_->option.o_friction,
spec_->option.o_friction + 5);
WriteUniformAttribute(physics_scene_spec,
pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcOptionO_friction, o_friction);
auto integrator_attr = CreateAttributeSpec(
layer_, physics_scene_spec, MjcPhysicsTokens->mjcOptionIntegrator,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
switch (spec_->option.integrator) {
case mjINT_EULER:
SetAttributeDefault(layer_, integrator_attr, MjcPhysicsTokens->euler);
break;
case mjINT_RK4:
SetAttributeDefault(layer_, integrator_attr, MjcPhysicsTokens->rk4);
break;
default:
break;
}
auto create_flag_attr = [&](pxr::TfToken token, int flag, bool enable) {
int flags =
enable ? spec_->option.enableflags : spec_->option.disableflags;
bool value = enable ? (flags & flag) : !(flags & flag);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
token, value);
};
const std::vector<std::pair<pxr::TfToken, int>> enable_flags = {
{MjcPhysicsTokens->mjcFlagMulticcd, mjENBL_MULTICCD},
{MjcPhysicsTokens->mjcFlagFwdinv, mjENBL_FWDINV},
{MjcPhysicsTokens->mjcFlagEnergy, mjENBL_ENERGY},
{MjcPhysicsTokens->mjcFlagOverride, mjENBL_OVERRIDE},
{MjcPhysicsTokens->mjcFlagInvdiscrete, mjENBL_INVDISCRETE}};
for (const auto &[token, flag] : enable_flags) {
create_flag_attr(token, flag, true);
}
const std::vector<std::pair<pxr::TfToken, int>> disable_flags = {
{MjcPhysicsTokens->mjcFlagConstraint, mjDSBL_CONSTRAINT},
{MjcPhysicsTokens->mjcFlagEquality, mjDSBL_EQUALITY},
{MjcPhysicsTokens->mjcFlagFrictionloss, mjDSBL_FRICTIONLOSS},
{MjcPhysicsTokens->mjcFlagLimit, mjDSBL_LIMIT},
{MjcPhysicsTokens->mjcFlagContact, mjDSBL_CONTACT},
{MjcPhysicsTokens->mjcFlagSpring, mjDSBL_SPRING},
{MjcPhysicsTokens->mjcFlagDamper, mjDSBL_DAMPER},
{MjcPhysicsTokens->mjcFlagGravity, mjDSBL_GRAVITY},
{MjcPhysicsTokens->mjcFlagClampctrl, mjDSBL_CLAMPCTRL},
{MjcPhysicsTokens->mjcFlagWarmstart, mjDSBL_WARMSTART},
{MjcPhysicsTokens->mjcFlagFilterparent, mjDSBL_FILTERPARENT},
{MjcPhysicsTokens->mjcFlagActuation, mjDSBL_ACTUATION},
{MjcPhysicsTokens->mjcFlagRefsafe, mjDSBL_REFSAFE},
{MjcPhysicsTokens->mjcFlagSensor, mjDSBL_SENSOR},
{MjcPhysicsTokens->mjcFlagMidphase, mjDSBL_MIDPHASE},
{MjcPhysicsTokens->mjcFlagEulerdamp, mjDSBL_EULERDAMP},
{MjcPhysicsTokens->mjcFlagAutoreset, mjDSBL_AUTORESET},
{MjcPhysicsTokens->mjcFlagNativeccd, mjDSBL_NATIVECCD},
{MjcPhysicsTokens->mjcFlagIsland, mjDSBL_ISLAND}};
for (const auto &[token, flag] : disable_flags) {
create_flag_attr(token, flag, false);
}
// Compiler attributes
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerAutoLimits,
(bool)spec_->compiler.autolimits);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcCompilerBoundMass,
spec_->compiler.boundmass);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcCompilerBoundInertia,
spec_->compiler.boundinertia);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcCompilerSetTotalMass,
spec_->compiler.settotalmass);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerUseThread,
(bool)spec_->compiler.usethread);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerBalanceInertia,
(bool)spec_->compiler.balanceinertia);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcCompilerAngle,
spec_->compiler.degree ? MjcPhysicsTokens->degree
: MjcPhysicsTokens->radian);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerFitAABB,
(bool)spec_->compiler.fitaabb);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerFuseStatic,
(bool)spec_->compiler.fusestatic);
pxr::TfToken inertiafromgeom_token = MjcPhysicsTokens->auto_;
if (spec_->compiler.inertiafromgeom ==
mjINERTIAFROMGEOM_TRUE) { // mjINERTIA_TRUE
inertiafromgeom_token = MjcPhysicsTokens->true_;
} else if (spec_->compiler.inertiafromgeom ==
mjINERTIAFROMGEOM_FALSE) { // mjINERTIA_FALSE
inertiafromgeom_token = MjcPhysicsTokens->false_;
}
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcCompilerInertiaFromGeom,
inertiafromgeom_token);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerAlignFree,
(bool)spec_->compiler.alignfree);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcCompilerInertiaGroupRangeMin,
spec_->compiler.inertiagrouprange[0]);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcCompilerInertiaGroupRangeMax,
spec_->compiler.inertiagrouprange[1]);
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcCompilerSaveInertial,
(bool)spec_->compiler.saveinertial);
}
void WriteMeshes() {
// Create a scope for the meshes to keep things organized
auto scope =
CreatePrimSpec(layer_, body_paths_[kWorldIndex], kTokens->meshScope,
pxr::UsdGeomTokens->Scope);
// Make the mesh scope invisible since they will be referenced by the bits
// that should be visible.
scope->SetField(pxr::SdfFieldKeys->Active, false);
mjsMesh *mesh = mjs_asMesh(mjs_firstElement(spec_, mjOBJ_MESH));
while (mesh) {
if (ShouldWrite(mesh->element)) {
WriteMesh(mesh, scope->GetPath());
}
mesh = mjs_asMesh(mjs_nextElement(spec_, mesh->element));
}
}
pxr::SdfAttributeSpecHandle AddUVTextureShader(
const pxr::SdfPrimSpecHandle material_spec, const pxr::TfToken &name) {
auto uvmap_shader_spec =
CreatePrimSpec(layer_, material_spec->GetPath(), name, pxr::UsdShadeTokens->Shader);
auto uvmap_info_id_attr = CreateAttributeSpec(
layer_, uvmap_shader_spec, pxr::UsdShadeTokens->infoId,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
SetAttributeDefault(layer_, uvmap_info_id_attr,
pxr::UsdImagingTokens->UsdPrimvarReader_float2);
auto uvmap_varname_attr =
CreateAttributeSpec(layer_, uvmap_shader_spec, kTokens->inputsVarname,
pxr::SdfValueTypeNames->Token);
SetAttributeDefault(layer_, uvmap_varname_attr, kTokens->st);
auto uvmap_st_output_attr =
CreateAttributeSpec(layer_, uvmap_shader_spec, kTokens->outputsSt,
pxr::SdfValueTypeNames->Float2);
return uvmap_st_output_attr;
}
std::vector<pxr::SdfAttributeSpecHandle> AddTextureShader(
const pxr::SdfPrimSpecHandle material_spec, const char *texture_file,
const pxr::TfToken &name,
const pxr::SdfAttributeSpecHandle uvmap_st_output_attr,
const std::vector<pxr::TfToken> &output_channels) {
auto texture_shader_spec =
CreatePrimSpec(layer_, material_spec->GetPath(), name, pxr::UsdShadeTokens->Shader);
auto texture_info_id_attr = CreateAttributeSpec(
layer_, texture_shader_spec, pxr::UsdShadeTokens->infoId,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
SetAttributeDefault(layer_, texture_info_id_attr,
pxr::UsdImagingTokens->UsdUVTexture);
auto texture_file_attr =
CreateAttributeSpec(layer_, texture_shader_spec, kTokens->inputsFile,
pxr::SdfValueTypeNames->Asset);
SetAttributeDefault(layer_, texture_file_attr,
pxr::SdfAssetPath(texture_file));
auto texture_st_input_attr =
CreateAttributeSpec(layer_, texture_shader_spec, kTokens->inputsSt,
pxr::SdfValueTypeNames->Float2);
AddAttributeConnection(layer_, texture_st_input_attr, uvmap_st_output_attr);
auto texture_wrap_s_attr =
CreateAttributeSpec(layer_, texture_shader_spec, kTokens->inputsWrapS,
pxr::SdfValueTypeNames->Token);
SetAttributeDefault(layer_, texture_wrap_s_attr, kTokens->repeat);
auto texture_wrap_t_attr =
CreateAttributeSpec(layer_, texture_shader_spec, kTokens->inputsWrapT,
pxr::SdfValueTypeNames->Token);
SetAttributeDefault(layer_, texture_wrap_t_attr, kTokens->repeat);
std::vector<pxr::SdfAttributeSpecHandle> texture_output_attrs;
for (const auto &output_channel : output_channels) {
pxr::SdfValueTypeName value_type;
if (output_channel == kTokens->outputsRgb) {
value_type = pxr::SdfValueTypeNames->Float3;
} else {
// Assume the other specified channels are outputR, outputG, outputB.
value_type = pxr::SdfValueTypeNames->Float;
}
texture_output_attrs.push_back(CreateAttributeSpec(
layer_, texture_shader_spec, output_channel, value_type));
}
return texture_output_attrs;
}
pxr::SdfPrimSpecHandle WritePhysicsMaterial(mjsGeom *geom) {
pxr::SdfPath scope_path =
body_paths_[kWorldIndex].AppendChild(kTokens->physicsMaterialsScope);
pxr::SdfPrimSpecHandle scope_spec = layer_->GetPrimAtPath(scope_path);
if (!scope_spec) {
scope_spec = CreatePrimSpec(layer_, body_paths_[kWorldIndex],
kTokens->physicsMaterialsScope,
pxr::UsdGeomTokens->Scope);
}
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdShadeTokens->Material, scope_path);
auto material_spec =
CreatePrimSpec(layer_, scope_spec->GetPath(), name, pxr::UsdShadeTokens->Material);
ApplyApiSchema(layer_, material_spec,
pxr::UsdPhysicsTokens->PhysicsMaterialAPI);
ApplyApiSchema(layer_, material_spec, MjcPhysicsTokens->MjcMaterialAPI);
mjsGeom *geom_default = mjs_getDefault(geom->element)->geom;
if (geom->friction[0] != geom_default->friction[0]) {
// Since MuJoCo has no concept of static friction, only write dynamic
// friction to remain truthful to how MuJoCo perceives the data.
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Float,
pxr::UsdPhysicsTokens->physicsDynamicFriction,
(float)geom->friction[0]);
}
if (geom->friction[1] != geom_default->friction[1]) {
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcTorsionalfriction,
geom->friction[1]);
}
if (geom->friction[2] != geom_default->friction[2]) {
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcRollingfriction,
geom->friction[2]);
}
return material_spec;
}
void WriteMaterial(mjsMaterial *material, const pxr::SdfPath &parent_path) {
// Create a Material prim.
auto name =
GetAvailablePrimName(*mjs_getName(material->element),
pxr::UsdShadeTokens->Material, parent_path);
auto material_spec =
CreatePrimSpec(layer_, parent_path, name, pxr::UsdShadeTokens->Material);
// Create a Shader prim "PreviewSurface" under the Material prim.
auto preview_surface_shader_spec =
CreatePrimSpec(layer_, material_spec->GetPath(), kTokens->previewSurface,
pxr::UsdShadeTokens->Shader);
// Set the Shader'sinfoId attribute to UsdPreviewSurface, a standard surface
// shader.
auto info_id_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, pxr::UsdShadeTokens->infoId,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
SetAttributeDefault(layer_, info_id_attr,
pxr::UsdImagingTokens->UsdPreviewSurface);
// Connect material's surface output to the preview surface's surface
// output.
auto surface_output_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, pxr::UsdShadeTokens->outputsSurface,
pxr::SdfValueTypeNames->Token);
auto material_surface_output_attr = CreateAttributeSpec(
layer_, material_spec, pxr::UsdShadeTokens->outputsSurface,
pxr::SdfValueTypeNames->Token);
AddAttributeConnection(layer_, material_surface_output_attr,
surface_output_attr);
// Connect material's displacement output to the preview surface's
// displacement output.
auto displacement_output_attr =
CreateAttributeSpec(layer_, preview_surface_shader_spec,
pxr::UsdShadeTokens->outputsDisplacement,
pxr::SdfValueTypeNames->Token);
auto material_displacement_output_attr = CreateAttributeSpec(
layer_, material_spec, pxr::UsdShadeTokens->outputsDisplacement,
pxr::SdfValueTypeNames->Token);
AddAttributeConnection(layer_, material_displacement_output_attr,
displacement_output_attr);
// Add an st (uv) Shader, a prim var reader for the UV coordinates.
const pxr::SdfAttributeSpecHandle &uvmap_st_output_attr =
AddUVTextureShader(material_spec, pxr::TfToken("uvmap"));
const mjStringVec &textures = *(material->textures);
// Set the values of metallic, roughness and occlusion. These can come from
// an ORM packed texture, as individual textures, or as a values defined in
// mjsMaterial_ (with the exception of occlusion).
auto metallic_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsMetallic,
pxr::SdfValueTypeNames->Float);
auto roughness_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsRoughness,
pxr::SdfValueTypeNames->Float);
// Find the occlusion, roughness, and metallic textures.
if (mjTEXROLE_ORM < textures.size()) {
std::string orm_texture_name = textures[mjTEXROLE_ORM];
mjsTexture *orm_texture = mjs_asTexture(
mjs_findElement(spec_, mjOBJ_TEXTURE, orm_texture_name.c_str()));
std::string occlusion_texture_name = textures[mjTEXROLE_OCCLUSION];
mjsTexture *occlusion_texture = mjs_asTexture(mjs_findElement(
spec_, mjOBJ_TEXTURE, occlusion_texture_name.c_str()));
std::string roughness_texture_name = textures[mjTEXROLE_ROUGHNESS];
mjsTexture *roughness_texture = mjs_asTexture(mjs_findElement(
spec_, mjOBJ_TEXTURE, roughness_texture_name.c_str()));
std::string metallic_texture_name = textures[mjTEXROLE_METALLIC];
mjsTexture *metallic_texture = mjs_asTexture(
mjs_findElement(spec_, mjOBJ_TEXTURE, metallic_texture_name.c_str()));
if (orm_texture) {
// Create the ORM shader and connect its output to the preview
// surface ORM attrs.
const std::vector<pxr::SdfAttributeSpecHandle> orm_output_attrs =
AddTextureShader(
material_spec, orm_texture->file->c_str(),
pxr::TfToken("orm_packed"), uvmap_st_output_attr,
{kTokens->outputsR, kTokens->outputsG, kTokens->outputsB});
if (orm_output_attrs.size() == 3) {
auto occlusion_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsOcclusion,
pxr::SdfValueTypeNames->Float);
AddAttributeConnection(layer_, occlusion_attr, orm_output_attrs[0]);
AddAttributeConnection(layer_, roughness_attr, orm_output_attrs[1]);
AddAttributeConnection(layer_, metallic_attr, orm_output_attrs[2]);
}
} else {
if (metallic_texture) {
const std::vector<pxr::SdfAttributeSpecHandle> metallic_output_attrs =
AddTextureShader(material_spec, metallic_texture->file->c_str(),
pxr::TfToken("metallic"), uvmap_st_output_attr,
{kTokens->outputsRgb});
if (metallic_output_attrs.size() == 1) {
AddAttributeConnection(layer_, metallic_attr,
metallic_output_attrs[0]);
}
} else {
SetAttributeDefault(layer_, metallic_attr, material->metallic);
}
if (roughness_texture) {
const std::vector<pxr::SdfAttributeSpecHandle> roughness_output_attrs =
AddTextureShader(material_spec, roughness_texture->file->c_str(),
pxr::TfToken("roughness"), uvmap_st_output_attr,
{kTokens->outputsRgb});
if (roughness_output_attrs.size() == 1) {
AddAttributeConnection(layer_, roughness_attr,
roughness_output_attrs[0]);
}
} else {
SetAttributeDefault(layer_, roughness_attr, material->roughness);
}
if (occlusion_texture) {
auto occlusion_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsOcclusion,
pxr::SdfValueTypeNames->Float);
const std::vector<pxr::SdfAttributeSpecHandle> occlusion_output_attrs =
AddTextureShader(material_spec, occlusion_texture->file->c_str(),
pxr::TfToken("occlusion"), uvmap_st_output_attr,
{kTokens->outputsRgb});
if (occlusion_output_attrs.size() == 1) {
AddAttributeConnection(layer_, occlusion_attr,
occlusion_output_attrs[0]);
}
}
}
}
// Find the normal texture if specified.
if (mjTEXROLE_NORMAL < textures.size()) {
std::string normal_texture_name = textures[mjTEXROLE_NORMAL];
mjsTexture *normal_texture = mjs_asTexture(
mjs_findElement(spec_, mjOBJ_TEXTURE, normal_texture_name.c_str()));
if (normal_texture) {
auto normal_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsNormal,
pxr::SdfValueTypeNames->Normal3f);
// Create the normal map shader and connect its output to the preview
// surface normal attr.
const std::vector<pxr::SdfAttributeSpecHandle> normal_map_output_attrs =
AddTextureShader(material_spec, normal_texture->file->c_str(),
pxr::TfToken("normal"), uvmap_st_output_attr,
{kTokens->outputsRgb});
if (normal_map_output_attrs.size() == 1) {
AddAttributeConnection(layer_, normal_attr,
normal_map_output_attrs[0]);
}
}
}
// Connect an emissive texture if specified.
if (mjTEXROLE_EMISSIVE < textures.size()) {
std::string emissive_texture_name = textures[mjTEXROLE_EMISSIVE];
mjsTexture *emissive_texture = mjs_asTexture(
mjs_findElement(spec_, mjOBJ_TEXTURE, emissive_texture_name.c_str()));
if (emissive_texture) {
auto emissive_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsEmissiveColor,
pxr::SdfValueTypeNames->Color3f);
const std::vector<pxr::SdfAttributeSpecHandle> emissive_map_output_attrs =
AddTextureShader(material_spec, emissive_texture->file->c_str(),
pxr::TfToken("emissive"), uvmap_st_output_attr,
{kTokens->outputsRgb});
if (emissive_map_output_attrs.size() == 1) {
AddAttributeConnection(layer_, emissive_attr,
emissive_map_output_attrs[0]);
}
}
}
// Set the value of diffuse color. This can come from a diffuse texture
// or as a value defined in mjsMaterial_.
auto diffuse_color_attr = CreateAttributeSpec(
layer_, preview_surface_shader_spec, kTokens->inputsDiffuseColor,
pxr::SdfValueTypeNames->Color3f);
// Find the main texture if specified.
std::string main_texture_name = textures[mjTEXROLE_RGB];
mjsTexture *main_texture = mjs_asTexture(
mjs_findElement(spec_, mjOBJ_TEXTURE, main_texture_name.c_str()));
if (main_texture) {
// Create the texture shader and connect it to the diffuse color
// attribute.
const std::vector<pxr::SdfAttributeSpecHandle>
texture_diffuse_output_attrs =
AddTextureShader(material_spec, main_texture->file->c_str(),
pxr::TfToken("diffuse"), uvmap_st_output_attr,
{kTokens->outputsRgb});
if (texture_diffuse_output_attrs.size() == 1) {
AddAttributeConnection(layer_, diffuse_color_attr,
texture_diffuse_output_attrs[0]);
}
} else {
// If no texture is specified, use the rgba diffuse color.
SetAttributeDefault(layer_, diffuse_color_attr,
pxr::GfVec3f(material->rgba[0], material->rgba[1],
material->rgba[2]));
}
}
void WriteMaterials() {
// Create a scope for the meshes to keep things organized
auto scope =
CreatePrimSpec(layer_, body_paths_[kWorldIndex], kTokens->materialsScope,
pxr::UsdGeomTokens->Scope);
mjsMaterial *material =
mjs_asMaterial(mjs_firstElement(spec_, mjOBJ_MATERIAL));
while (material) {
WriteMaterial(material, scope->GetPath());
material = mjs_asMaterial(mjs_nextElement(spec_, material->element));
}
}
void WriteKeyframesWithName(const std::string &name,
const std::vector<mjsKey *> &keyframes,
const pxr::SdfPath &parent_path) {
if (keyframes.empty()) {
return;
}
const auto keyframe_name = pxr::TfToken(pxr::TfMakeValidIdentifier(
name.empty() ? MjcPhysicsTokens->MjcKeyframe : name));
pxr::SdfPath keyframe_path = parent_path.AppendChild(keyframe_name);
auto keyframe_spec =
layer_->HasSpec(keyframe_path)
? layer_->GetPrimAtPath(keyframe_path)
: CreatePrimSpec(layer_, parent_path, keyframe_name,
pxr::MjcPhysicsTokens->MjcKeyframe);
auto set_attribute_data = [&](const pxr::SdfAttributeSpecHandle &attr_spec,
const pxr::VtDoubleArray &value,
mjsKey *keyframe) {
// If the keyframe time is the default, and there are no other keyframes
// set the attribute at the default time code.
if (keyframe->time == 0 && keyframes.size() == 1) {
SetAttributeDefault(layer_, attr_spec, value);
} else {
SetAttributeTimeSample(layer_, attr_spec, keyframe->time, value);
}
};
for (auto *keyframe : keyframes) {
auto qpos_attr =
CreateAttributeSpec(layer_, keyframe_spec, MjcPhysicsTokens->mjcQpos,
pxr::SdfValueTypeNames->DoubleArray);
set_attribute_data(
qpos_attr,
pxr::VtDoubleArray(keyframe->qpos->begin(), keyframe->qpos->end()),
keyframe);
auto qvel_attr =
CreateAttributeSpec(layer_, keyframe_spec, MjcPhysicsTokens->mjcQvel,
pxr::SdfValueTypeNames->DoubleArray);
set_attribute_data(
qvel_attr,
pxr::VtDoubleArray(keyframe->qvel->begin(), keyframe->qvel->end()),
keyframe);
auto act_attr =
CreateAttributeSpec(layer_, keyframe_spec, MjcPhysicsTokens->mjcAct,
pxr::SdfValueTypeNames->DoubleArray);
set_attribute_data(
act_attr,
pxr::VtDoubleArray(keyframe->act->begin(), keyframe->act->end()),
keyframe);
auto ctrl_attr =
CreateAttributeSpec(layer_, keyframe_spec, MjcPhysicsTokens->mjcCtrl,
pxr::SdfValueTypeNames->DoubleArray);
set_attribute_data(
ctrl_attr,
pxr::VtDoubleArray(keyframe->ctrl->begin(), keyframe->ctrl->end()),
keyframe);
auto mpos_attr =
CreateAttributeSpec(layer_, keyframe_spec, MjcPhysicsTokens->mjcMpos,
pxr::SdfValueTypeNames->DoubleArray);
set_attribute_data(
mpos_attr,
pxr::VtDoubleArray(keyframe->mpos->begin(), keyframe->mpos->end()),
keyframe);
auto mquat_attr_path =
CreateAttributeSpec(layer_, keyframe_spec, MjcPhysicsTokens->mjcMquat,
pxr::SdfValueTypeNames->DoubleArray);
set_attribute_data(
mquat_attr_path,
pxr::VtDoubleArray(keyframe->mquat->begin(), keyframe->mquat->end()),
keyframe);
}
}
void WriteKeyframes() {
std::unordered_map<std::string, std::vector<mjsKey *>> keyframes_map;
mjsKey *keyframe = mjs_asKey(mjs_firstElement(spec_, mjOBJ_KEY));
while (keyframe) {
if (ShouldWrite(keyframe->element)) {
std::string keyframe_name = mjs_getName(keyframe->element)->empty()
? kTokens->keyframe
: *mjs_getName(keyframe->element);
keyframes_map[keyframe_name].push_back(keyframe);
}
keyframe = mjs_asKey(mjs_nextElement(spec_, keyframe->element));
}
auto scope =
CreatePrimSpec(layer_, body_paths_[kWorldIndex], kTokens->keyframesScope,
pxr::UsdGeomTokens->Scope);
for (const auto &[keyframe_name, keyframes] : keyframes_map) {
WriteKeyframesWithName(keyframe_name, keyframes, scope->GetPath());
}
}
void WriteActuator(mjsActuator *actuator, const pxr::SdfPath &parent_path) {
const pxr::TfToken valid_name = GetValidPrimName(*mjs_getName(actuator->element));
const pxr::SdfPath actuator_path = parent_path.AppendChild(valid_name);
pxr::SdfPrimSpecHandle actuator_spec;
if (layer_->HasSpec(actuator_path)) {
actuator_spec = layer_->GetPrimAtPath(actuator_path);
} else {
actuator_spec = CreatePrimSpec(layer_, parent_path, valid_name,
pxr::MjcPhysicsTokens->MjcActuator);
}
pxr::SdfPath target_path;
if (actuator->trntype == mjtTrn::mjTRN_BODY) {
int body_id = mj_name2id(model_, mjOBJ_BODY, actuator->target->c_str());
target_path = body_paths_[body_id];
} else if (actuator->trntype == mjtTrn::mjTRN_SITE ||
actuator->trntype == mjtTrn::mjTRN_SLIDERCRANK) {
int site_id = mj_name2id(model_, mjOBJ_SITE, actuator->target->c_str());
target_path = site_paths_[site_id];
} else if (actuator->trntype == mjtTrn::mjTRN_JOINT) {
int joint_id = mj_name2id(model_, mjOBJ_JOINT, actuator->target->c_str());
target_path = joint_paths_[joint_id];
} else {
TF_WARN(UnsupportedActuatorTypeError,
"Unsupported transmission type for actuator %d",
mjs_getId(actuator->element));
return;
}
CreateRelationshipSpec(layer_, actuator_spec, MjcPhysicsTokens->mjcTarget,
target_path, pxr::SdfVariabilityUniform);
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcGroup, actuator->group);
if (!actuator->refsite->empty()) {
int refsite_id =
mj_name2id(model_, mjOBJ_SITE, actuator->refsite->c_str());
pxr::SdfPath refsite_path = site_paths_[refsite_id];
CreateRelationshipSpec(layer_, actuator_spec, MjcPhysicsTokens->mjcRefSite,
refsite_path, pxr::SdfVariabilityUniform);
}
if (!actuator->slidersite->empty()) {
int slidersite_id =
mj_name2id(model_, mjOBJ_SITE, actuator->slidersite->c_str());
pxr::SdfPath slidersite_path = site_paths_[slidersite_id];
CreateRelationshipSpec(layer_, actuator_spec,
MjcPhysicsTokens->mjcSliderSite, slidersite_path,
pxr::SdfVariabilityUniform);
}
const std::vector<std::pair<pxr::TfToken, int>> limited_attributes = {
{MjcPhysicsTokens->mjcCtrlLimited, actuator->ctrllimited},
{MjcPhysicsTokens->mjcForceLimited, actuator->forcelimited},
{MjcPhysicsTokens->mjcActLimited, actuator->actlimited},
};
for (const auto &[token, value] : limited_attributes) {
pxr::TfToken limited_token = pxr::MjcPhysicsTokens->auto_;
if (value == mjLIMITED_TRUE) {
limited_token = pxr::MjcPhysicsTokens->true_;
} else if (value == mjLIMITED_FALSE) {
limited_token = pxr::MjcPhysicsTokens->false_;
}
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Token, token,
limited_token);
}
const std::vector<std::pair<pxr::TfToken, double>>
actuator_double_attributes = {
{MjcPhysicsTokens->mjcCtrlRangeMin, actuator->ctrlrange[0]},
{MjcPhysicsTokens->mjcCtrlRangeMax, actuator->ctrlrange[1]},
{MjcPhysicsTokens->mjcForceRangeMin, actuator->forcerange[0]},
{MjcPhysicsTokens->mjcForceRangeMax, actuator->forcerange[1]},
{MjcPhysicsTokens->mjcActRangeMin, actuator->actrange[0]},
{MjcPhysicsTokens->mjcActRangeMax, actuator->actrange[1]},
{MjcPhysicsTokens->mjcLengthRangeMin, actuator->lengthrange[0]},
{MjcPhysicsTokens->mjcLengthRangeMax, actuator->lengthrange[1]},
{MjcPhysicsTokens->mjcCrankLength, actuator->cranklength},
};
for (const auto &[token, value] : actuator_double_attributes) {
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Double,
token, value);
}
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcActDim, actuator->actdim);
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcActEarly,
(bool)actuator->actearly);
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcInheritRange,
actuator->inheritrange);
WriteUniformAttribute(
actuator_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcGear,
pxr::VtDoubleArray(actuator->gear, actuator->gear + 6));
pxr::TfToken dyn_type;
if (actuator->dyntype == mjtDyn::mjDYN_NONE) {
dyn_type = MjcPhysicsTokens->none;
} else if (actuator->dyntype == mjtDyn::mjDYN_INTEGRATOR) {
dyn_type = MjcPhysicsTokens->integrator;
} else if (actuator->dyntype == mjtDyn::mjDYN_FILTER) {
dyn_type = MjcPhysicsTokens->filter;
} else if (actuator->dyntype == mjtDyn::mjDYN_FILTEREXACT) {
dyn_type = MjcPhysicsTokens->filterexact;
} else if (actuator->dyntype == mjtDyn::mjDYN_MUSCLE) {
dyn_type = MjcPhysicsTokens->muscle;
} else if (actuator->dyntype == mjtDyn::mjDYN_USER) {
dyn_type = MjcPhysicsTokens->user;
}
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcDynType, dyn_type);
WriteUniformAttribute(
actuator_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcDynPrm,
pxr::VtDoubleArray(actuator->dynprm, actuator->dynprm + 10));
pxr::TfToken gain_type;
if (actuator->gaintype == mjtGain::mjGAIN_FIXED) {
gain_type = MjcPhysicsTokens->fixed;
} else if (actuator->gaintype == mjtGain::mjGAIN_AFFINE) {
gain_type = MjcPhysicsTokens->affine;
} else if (actuator->gaintype == mjtGain::mjGAIN_MUSCLE) {
gain_type = MjcPhysicsTokens->muscle;
} else if (actuator->gaintype == mjtGain::mjGAIN_USER) {
gain_type = MjcPhysicsTokens->user;
}
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcGainType, gain_type);
WriteUniformAttribute(
actuator_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcGainPrm,
pxr::VtDoubleArray(actuator->gainprm, actuator->gainprm + 10));
pxr::TfToken bias_type;
if (actuator->biastype == mjtBias::mjBIAS_NONE) {
bias_type = MjcPhysicsTokens->fixed;
} else if (actuator->biastype == mjtBias::mjBIAS_AFFINE) {
bias_type = MjcPhysicsTokens->affine;
} else if (actuator->biastype == mjtBias::mjBIAS_MUSCLE) {
bias_type = MjcPhysicsTokens->muscle;
} else if (actuator->biastype == mjtBias::mjBIAS_USER) {
bias_type = MjcPhysicsTokens->user;
}
WriteUniformAttribute(actuator_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcBiasType, bias_type);
WriteUniformAttribute(
actuator_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcBiasPrm,
pxr::VtDoubleArray(actuator->biasprm, actuator->biasprm + 10));
}
void WriteActuators() {
auto scope =
CreatePrimSpec(layer_, body_paths_[kWorldIndex], kTokens->actuatorsScope,
pxr::UsdGeomTokens->Scope);
mjsActuator *actuator =
mjs_asActuator(mjs_firstElement(spec_, mjOBJ_ACTUATOR));
while (actuator) {
if (ShouldWrite(actuator->element)) {
WriteActuator(actuator, scope->GetPath());
}
actuator = mjs_asActuator(mjs_nextElement(spec_, actuator->element));
}
}
pxr::SdfPrimSpecHandle WriteMeshGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
std::string mj_name = mjs_getName(geom->element)->empty()
? *geom->meshname
: *mjs_getName(geom->element);
auto name =
GetAvailablePrimName(mj_name, pxr::UsdGeomTokens->Mesh, body_path);
auto subcomponent_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Xform);
// Reference the mesh asset written in WriteMeshes.
AddPrimReference(layer_, subcomponent_spec, mesh_paths_[*geom->meshname]);
// We want to use instancing with meshes, and it requires creating a parent
// scope to be referenced, with the Mesh prim as a child.
// To be able to actually manipulate the Mesh prim, we need to create and
// return the corresponding `over` prim as a child of the referencing prim.
auto over_mesh_spec =
CreatePrimSpec(layer_, subcomponent_spec->GetPath(), kTokens->sourceMesh,
pxr::UsdGeomTokens->Mesh, pxr::SdfSpecifierOver);
return over_mesh_spec;
}
pxr::SdfPrimSpecHandle WriteSiteGeom(const mjsSite *site,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(site->element),
pxr::UsdGeomTokens->Cube, body_path);
int site_idx = mjs_getId(site->element);
const mjtNum *size = &model_->site_size[site_idx * 3];
pxr::SdfPrimSpecHandle site_spec;
switch (site->type) {
case mjGEOM_BOX:
site_spec = WriteBox(name, size, body_path);
break;
case mjGEOM_SPHERE:
site_spec = WriteSphere(name, size, body_path);
break;
case mjGEOM_CAPSULE:
site_spec = WriteCapsule(name, size, body_path);
break;
case mjGEOM_CYLINDER:
site_spec = WriteCylinder(name, size, body_path);
break;
case mjGEOM_ELLIPSOID:
site_spec = WriteEllipsoid(name, size, body_path);
break;
default:
break;
}
return site_spec;
}
pxr::SdfPrimSpecHandle WriteBox(const pxr::TfToken &name, const mjtNum *size,
const pxr::SdfPath &body_path) {
pxr::SdfPrimSpecHandle box_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Cube);
// MuJoCo uses half sizes. Always set size to 2 (and correspondingly extent
// from -1 to 1), and let scale determine the actual size.
auto size_attr_spec =
CreateAttributeSpec(layer_, box_spec, pxr::UsdGeomTokens->size,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, size_attr_spec, 2.0);
auto extent_attr_spec =
CreateAttributeSpec(layer_, box_spec, pxr::UsdGeomTokens->extent,
pxr::SdfValueTypeNames->Float3Array);
SetAttributeDefault(layer_, extent_attr_spec,
pxr::VtArray<pxr::GfVec3f>(
{pxr::GfVec3f(-1, -1, -1), pxr::GfVec3f(1, 1, 1)}));
pxr::GfVec3f scale(static_cast<float>(size[0]), static_cast<float>(size[1]),
static_cast<float>(size[2]));
WriteScaleXformOp(box_spec, scale);
WriteXformOpOrder(box_spec,
pxr::VtArray<pxr::TfToken>{kTokens->xformOpScale});
return box_spec;
}
pxr::SdfPrimSpecHandle WriteBoxGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdGeomTokens->Cube, body_path);
int geom_idx = mjs_getId(geom->element);
mjtNum *geom_size = &model_->geom_size[geom_idx * 3];
return WriteBox(name, geom_size, body_path);
}
pxr::SdfPrimSpecHandle WriteCapsule(const pxr::TfToken name, const mjtNum *size,
const pxr::SdfPath &body_path) {
auto capsule_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Capsule);
auto radius_attr_spec =
CreateAttributeSpec(layer_, capsule_spec, pxr::UsdGeomTokens->radius,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, radius_attr_spec, (double)size[0]);
auto height_attr_spec =
CreateAttributeSpec(layer_, capsule_spec, pxr::UsdGeomTokens->height,
pxr::SdfValueTypeNames->Double);
// MuJoCo uses half sizes.
SetAttributeDefault(layer_, height_attr_spec, (double)(size[1] * 2));
return capsule_spec;
}
pxr::SdfPrimSpecHandle WriteCapsuleGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdGeomTokens->Capsule, body_path);
int geom_idx = mjs_getId(geom->element);
mjtNum *geom_size = &model_->geom_size[geom_idx * 3];
return WriteCapsule(name, geom_size, body_path);
}
pxr::SdfPrimSpecHandle WriteCylinder(const pxr::TfToken name, const mjtNum *size,
const pxr::SdfPath &body_path) {
auto cylinder_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Cylinder);
auto radius_attr_spec =
CreateAttributeSpec(layer_, cylinder_spec, pxr::UsdGeomTokens->radius,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, radius_attr_spec, (double)size[0]);
auto height_attr_spec =
CreateAttributeSpec(layer_, cylinder_spec, pxr::UsdGeomTokens->height,
pxr::SdfValueTypeNames->Double);
// MuJoCo uses half sizes.
SetAttributeDefault(layer_, height_attr_spec, (double)(size[1] * 2));
return cylinder_spec;
}
pxr::SdfPrimSpecHandle WriteCylinderGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdGeomTokens->Cylinder, body_path);
int geom_idx = mjs_getId(geom->element);
mjtNum *geom_size = &model_->geom_size[geom_idx * 3];
return WriteCylinder(name, geom_size, body_path);
}
pxr::SdfPrimSpecHandle WriteEllipsoid(const pxr::TfToken name, const mjtNum *size,
const pxr::SdfPath &body_path) {
auto ellipsoid_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Sphere);
pxr::GfVec3f scale = {static_cast<float>(size[0]),
static_cast<float>(size[1]),
static_cast<float>(size[2])};
auto radius_attr_spec =
CreateAttributeSpec(layer_, ellipsoid_spec, pxr::UsdGeomTokens->radius,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, radius_attr_spec, 1.0);
WriteScaleXformOp(ellipsoid_spec, scale);
WriteXformOpOrder(ellipsoid_spec,
pxr::VtArray<pxr::TfToken>{kTokens->xformOpScale});
return ellipsoid_spec;
}
pxr::SdfPrimSpecHandle WriteEllipsoidGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdGeomTokens->Sphere, body_path);
int geom_idx = mjs_getId(geom->element);
mjtNum *geom_size = &model_->geom_size[geom_idx * 3];
return WriteEllipsoid(name, geom_size, body_path);
}
pxr::SdfPrimSpecHandle WriteSphere(const pxr::TfToken name, const mjtNum *size,
const pxr::SdfPath &body_path) {
auto sphere_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Sphere);
auto radius_attr_spec =
CreateAttributeSpec(layer_, sphere_spec, pxr::UsdGeomTokens->radius,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, radius_attr_spec, (double)size[0]);
return sphere_spec;
}
pxr::SdfPrimSpecHandle WriteSphereGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdGeomTokens->Sphere, body_path);
int geom_idx = mjs_getId(geom->element);
mjtNum *geom_size = &model_->geom_size[geom_idx * 3];
return WriteSphere(name, geom_size, body_path);
}
pxr::SdfPrimSpecHandle WritePlane(const pxr::TfToken &name, const mjtNum *size,
const pxr::SdfPath &body_path) {
auto plane_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Plane);
// MuJoCo uses half sizes.
// Note that UsdGeomPlane is infinite for simulation purposes but can have
// width/length for visualization, same as MuJoCo.
double width = size[0] * 2.0;
double length = size[1] * 2.0;
auto width_attr_spec =
CreateAttributeSpec(layer_, plane_spec, pxr::UsdGeomTokens->width,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, width_attr_spec, width);
auto length_attr_spec =
CreateAttributeSpec(layer_, plane_spec, pxr::UsdGeomTokens->length,
pxr::SdfValueTypeNames->Double);
SetAttributeDefault(layer_, length_attr_spec, length);
// MuJoCo plane is always a XY plane with +Z up.
// UsdGeomPlane is also a XY plane if axis is 'Z', which is default.
// So no need to set axis attribute explicitly.
return plane_spec;
}
pxr::SdfPrimSpecHandle WritePlaneGeom(const mjsGeom *geom,
const pxr::SdfPath &body_path) {
auto name = GetAvailablePrimName(*mjs_getName(geom->element),
pxr::UsdGeomTokens->Plane, body_path);
int geom_idx = mjs_getId(geom->element);
mjtNum *geom_size = &model_->geom_size[geom_idx * 3];
return WritePlane(name, geom_size, body_path);
}
void WriteSite(mjsSite *site, const mjsBody *body) {
const int body_id = mjs_getId(body->element);
const auto &body_path = body_paths_[body_id];
auto name = GetAvailablePrimName(*mjs_getName(site->element),
pxr::UsdGeomTokens->Xform, body_path);
// Create a geom primitive and set its purpose to guide so it won't be
// rendered.
auto site_spec = WriteSiteGeom(site, body_path);
SetPrimPurpose(layer_, site_spec, pxr::UsdGeomTokens->guide);
ApplyApiSchema(layer_, site_spec, MjcPhysicsTokens->MjcSiteAPI);
WriteUniformAttribute(site_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcGroup, site->group);
WriteColorAndOpacityAttributes(site_spec, site);
int site_id = mjs_getId(site->element);
auto transform = MujocoPosQuatToTransform(&model_->site_pos[3 * site_id],
&model_->site_quat[4 * site_id]);
WriteTransformXformOp(site_spec, transform);
PrependToXformOpOrder(
site_spec, pxr::VtArray<pxr::TfToken>{kTokens->xformOpTransform});
site_paths_[site_id] = site_spec->GetPath();
}
void WriteGeom(mjsGeom *geom, const mjsBody *body) {
const int body_id = mjs_getId(body->element);
const auto &body_path = body_paths_[body_id];
pxr::SdfPrimSpecHandle geom_spec;
int geom_id = mjs_getId(geom->element);
switch (geom->type) {
case mjGEOM_PLANE:
geom_spec = WritePlaneGeom(geom, body_path);
break;
case mjGEOM_MESH:
geom_spec = WriteMeshGeom(geom, body_path);
break;
case mjGEOM_BOX:
geom_spec = WriteBoxGeom(geom, body_path);
break;
case mjGEOM_CAPSULE:
geom_spec = WriteCapsuleGeom(geom, body_path);
break;
case mjGEOM_CYLINDER:
geom_spec = WriteCylinderGeom(geom, body_path);
break;
case mjGEOM_ELLIPSOID:
geom_spec = WriteEllipsoidGeom(geom, body_path);
break;
case mjGEOM_SPHERE:
geom_spec = WriteSphereGeom(geom, body_path);
break;
default:
TF_WARN(UnsupportedGeomTypeError, "Unsupported geom type for geom %d",
geom_id);
return;
}
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcGroup, geom->group);
if (model_->geom_contype[geom_id] == 0 &&
model_->geom_conaffinity[geom_id] == 0) {
// If the geom is purely visual, apply the imageable API.
ApplyApiSchema(layer_, geom_spec, MjcPhysicsTokens->MjcImageableAPI);
}
// Apply the physics schemas if we are writing physics and the
// geom participates in collisions.
if (model_->geom_contype[geom_id] != 0 ||
model_->geom_conaffinity[geom_id] != 0) {
ApplyApiSchema(layer_, geom_spec,
pxr::UsdPhysicsTokens->PhysicsCollisionAPI);
ApplyApiSchema(layer_, geom_spec, MjcPhysicsTokens->MjcCollisionAPI);
WriteUniformAttribute(
geom_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcShellinertia,
geom->typeinertia == mjtGeomInertia::mjINERTIA_SHELL);
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcPriority, geom->priority);
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcCondim, geom->condim);
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcSolmix, geom->solmix);
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcSolmix, geom->solmix);
WriteUniformAttribute(
geom_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSolref,
pxr::VtArray<double>(geom->solref, geom->solref + mjNREF));
WriteUniformAttribute(
geom_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSolimp,
pxr::VtArray<double>(geom->solimp, geom->solimp + mjNIMP));
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcMargin, geom->margin);
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcGap, geom->gap);
if (geom->mass >= mjMINVAL || geom->density >= mjMINVAL) {
ApplyApiSchema(layer_, geom_spec, pxr::UsdPhysicsTokens->PhysicsMassAPI);
}
if (geom->mass >= mjMINVAL) {
auto mass_attr_spec = CreateAttributeSpec(
layer_, geom_spec, pxr::UsdPhysicsTokens->physicsMass,
pxr::SdfValueTypeNames->Float, pxr::SdfVariabilityUniform);
// Make sure to cast to float here since mjtNum might be a double.
SetAttributeDefault(layer_, mass_attr_spec, (float)geom->mass);
}
// Even though density is not used for mass computation when mass exists
// we want to retain the information anyways.
if (geom->density >= mjMINVAL) {
auto density_attr_spec = CreateAttributeSpec(
layer_, geom_spec, pxr::UsdPhysicsTokens->physicsDensity,
pxr::SdfValueTypeNames->Float, pxr::SdfVariabilityUniform);
// Make sure to cast to float here since mjtNum might be a double.
SetAttributeDefault(layer_, density_attr_spec, (float)geom->density);
}
mjsDefault *geom_default = mjs_getDefault(geom->element);
if (geom->friction[0] != geom_default->geom->friction[0] ||
geom->friction[1] != geom_default->geom->friction[1] ||
geom->friction[2] != geom_default->geom->friction[2]) {
auto physics_material_spec = WritePhysicsMaterial(geom);
ApplyApiSchema(layer_, geom_spec, pxr::UsdShadeTokens->MaterialBindingAPI);
// Bind the material to this geom.
CreateRelationshipSpec(
layer_, geom_spec,
pxr::UsdShadeTokens->materialBinding, physics_material_spec->GetPath(),
pxr::SdfVariabilityUniform);
}
// For meshes, also apply PhysicsMeshCollisionAPI and set the
// approximation attribute.
if (geom->type == mjGEOM_MESH) {
ApplyApiSchema(layer_, geom_spec,
pxr::UsdPhysicsTokens->PhysicsMeshCollisionAPI);
// Note: MuJoCo documentation states that for collision purposes, meshes
// are always replaced with their convex hulls. Therefore, we set the
// approximation attribute to convexHull explicitly.
auto approximation_attr_spec = CreateAttributeSpec(
layer_, geom_spec, pxr::UsdPhysicsTokens->physicsApproximation,
pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform);
SetAttributeDefault(layer_, approximation_attr_spec,
pxr::UsdPhysicsTokens->convexHull);
}
} else {
// Currently imageable only has a group API. But since it's the same
// naming in MjcCollisionsAPI we've already set it earlier in this
// function.
ApplyApiSchema(layer_, geom_spec, MjcPhysicsTokens->MjcImageableAPI);
}
mjsDefault *spec_default = mjs_getDefault(geom->element);
pxr::TfToken valid_class_name =
GetValidPrimName(*mjs_getName(spec_default->element));
pxr::SdfPath geom_class_path = class_path_.AppendChild(valid_class_name);
if (!layer_->HasSpec(geom_class_path)) {
auto class_spec =
CreateClassSpec(layer_, class_path_, valid_class_name);
auto visibility_attr_spec =
CreateAttributeSpec(layer_, class_spec, pxr::UsdGeomTokens->visibility,
pxr::SdfValueTypeNames->Token);
SetAttributeDefault(layer_, visibility_attr_spec,
pxr::UsdGeomTokens->inherited);
}
// Bind material if it exists.
if (!geom->material->empty()) {
pxr::SdfPath material_path =
body_paths_[kWorldIndex]
.AppendChild(kTokens->materialsScope)
.AppendChild(GetValidPrimName(*geom->material));
if (layer_->HasSpec(material_path)) {
ApplyApiSchema(layer_, geom_spec,
pxr::UsdShadeTokens->MaterialBindingAPI);
// Bind the material to this geom.
CreateRelationshipSpec(layer_, geom_spec,
pxr::UsdShadeTokens->materialBinding,
material_path, pxr::SdfVariabilityUniform);
}
}
WriteColorAndOpacityAttributes(geom_spec, geom);
if (body_id == kWorldIndex) {
SetPrimKind(layer_, geom_spec, pxr::KindTokens->component);
}
// Inherit from class.
AddPrimInherit(layer_, geom_spec, geom_class_path);
auto transform = MujocoPosQuatToTransform(&model_->geom_pos[3 * geom_id],
&model_->geom_quat[4 * geom_id]);
WriteTransformXformOp(geom_spec, transform);
PrependToXformOpOrder(
geom_spec, pxr::VtArray<pxr::TfToken>{kTokens->xformOpTransform});
}
void WriteSites(mjsBody *body) {
mjsSite *site = mjs_asSite(mjs_firstChild(body, mjOBJ_SITE, false));
while (site) {
if (ShouldWrite(site->element)) {
WriteSite(site, body);
}
site = mjs_asSite(mjs_nextChild(body, site->element, false));
}
}
void WriteGeoms(mjsBody *body) {
mjsGeom *geom = mjs_asGeom(mjs_firstChild(body, mjOBJ_GEOM, false));
while (geom) {
if (ShouldWrite(geom->element)) {
WriteGeom(geom, body);
}
geom = mjs_asGeom(mjs_nextChild(body, geom->element, false));
}
}
void WriteJoints(mjsBody *body) {
int body_id = mjs_getId(body->element);
if (body_id == kWorldIndex) return;
mjsJoint *joint = mjs_asJoint(mjs_firstChild(body, mjOBJ_JOINT, false));
if (!joint) {
// If no joint is found, then we pass nullptr to create a FixedJoint.
// WriteJoint properly handles the case where the parent is the worldbody.
WriteJoint(nullptr, body);
} else {
if (ShouldWrite(joint->element)) {
WriteJoint(joint, body);
}
if (mjs_asJoint(mjs_nextChild(body, joint->element, false))) {
TF_WARN(
"Multiple joints found for body %d. Only writing the first one.",
body_id);
}
}
}
// Write the joint. If null, then a FixedJoint is created.
void WriteJoint(mjsJoint *joint, const mjsBody *parent_mj_body) {
// Default to fixed joint if joint is null.
pxr::TfToken joint_prim_type = pxr::UsdPhysicsTokens->PhysicsFixedJoint;
int joint_id = -1;
if (joint) {
joint_id = mjs_getId(joint->element);
mjtJoint type = (mjtJoint)model_->jnt_type[joint_id];
switch (type) {
case mjJNT_FREE:
// Free joints are guaranteed to only ever be on the top-level body so
// we just write no joint. As a top-level body with no joint it will
// be considered a floating-base body.
return;
case mjJNT_HINGE:
joint_prim_type = pxr::UsdPhysicsTokens->PhysicsRevoluteJoint;
break;
case mjJNT_SLIDE:
joint_prim_type = pxr::UsdPhysicsTokens->PhysicsPrismaticJoint;
break;
default:
TF_WARN("Unsupported joint type '%d' for joint '%s'. Skipping.",
(int)type, mjs_getName(joint->element)->c_str());
return;
}
}
int body_id = mjs_getId(parent_mj_body->element);
// the joint connects the current body as body1, to its parent body as
// body0.
int body1_id_usd = body_id;
int body0_id_usd = model_->body_parentid[body_id];
const pxr::SdfPath &body1_path_usd = body_paths_[body1_id_usd];
auto joint_name = joint ? *mjs_getName(joint->element) : "FixedJoint";
pxr::TfToken joint_name_token =
GetAvailablePrimName(joint_name, kTokens->joint, body1_path_usd);
auto joint_spec = CreatePrimSpec(layer_, body1_path_usd,
joint_name_token, joint_prim_type);
// Set body0 and body1 relationships
// For the initial joints that connect to the world, we signal this by
// keeping the body0 relationship empty.
if (body0_id_usd != kWorldIndex) {
const pxr::SdfPath &body0_path_usd = body_paths_[body0_id_usd];
CreateRelationshipSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsBody0,
body0_path_usd, pxr::SdfVariabilityUniform);
}
CreateRelationshipSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsBody1, body1_path_usd,
pxr::SdfVariabilityUniform);
// Joint frame in MuJoCo is defined by jnt_pos and jnt_axis in body1's frame
// For FixedJoint, these are both unity.
pxr::GfVec3d mj_jnt_pos = pxr::GfVec3d(0.0);
pxr::GfVec3d mj_jnt_axis = pxr::GfVec3d(0.0, 0.0, 1.0);
if (joint) {
mj_jnt_pos = pxr::GfVec3d(&model_->jnt_pos[joint_id * 3]);
mj_jnt_axis = pxr::GfVec3d(&model_->jnt_axis[joint_id * 3]);
}
// Local joint frame for body1
pxr::GfVec3f local_pos1(mj_jnt_pos);
pxr::GfQuatf local_rot1(
pxr::GfRotation(pxr::GfVec3f::ZAxis(), mj_jnt_axis).GetQuat());
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLocalPos1,
pxr::SdfValueTypeNames->Float3),
local_pos1);
if (joint_prim_type == pxr::UsdPhysicsTokens->PhysicsRevoluteJoint ||
joint_prim_type == pxr::UsdPhysicsTokens->PhysicsPrismaticJoint) {
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLocalRot1,
pxr::SdfValueTypeNames->Quatf),
local_rot1);
} else {
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLocalRot1,
pxr::SdfValueTypeNames->Quatf),
pxr::GfQuatf::GetIdentity());
}
// Calculate local joint frame for body0
pxr::GfMatrix4d body1_transform_local =
MujocoPosQuatToTransform(&model_->body_pos[body1_id_usd * 3],
&model_->body_quat[body1_id_usd * 4]);
pxr::GfVec3d jnt_pos_parent_local =
body1_transform_local.Transform(mj_jnt_pos);
pxr::GfVec3d jnt_axis_parent_local =
body1_transform_local.TransformDir(mj_jnt_axis);
pxr::GfVec3f local_pos0(jnt_pos_parent_local);
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLocalPos0,
pxr::SdfValueTypeNames->Float3),
local_pos0);
if (joint_prim_type == pxr::UsdPhysicsTokens->PhysicsRevoluteJoint ||
joint_prim_type == pxr::UsdPhysicsTokens->PhysicsPrismaticJoint) {
pxr::GfQuatf other_rot0(
pxr::GfRotation(pxr::GfVec3f::ZAxis(), jnt_axis_parent_local)
.GetQuat());
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLocalRot0,
pxr::SdfValueTypeNames->Quatf),
other_rot0);
} else {
// Fixed joints have no frame and no axis per se. We simply need the
// rotation quaternion of the body its on.
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLocalRot0,
pxr::SdfValueTypeNames->Quatf),
body1_transform_local.ExtractRotationQuat());
}
if (joint) {
mjtJoint type = (mjtJoint)model_->jnt_type[joint_id];
// Joint-specific attributes
if (type == mjJNT_HINGE || type == mjJNT_SLIDE) {
// The joint motion occurs around/along the Z-axis of the joint frame
// established by localRot0/1.
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsAxis,
pxr::SdfValueTypeNames->Token),
pxr::UsdPhysicsTokens->z); // "Z" axis
}
if (model_->jnt_limited[joint_id]) {
float lower_limit = model_->jnt_range[joint_id * 2];
float upper_limit = model_->jnt_range[joint_id * 2 + 1];
if (type == mjJNT_HINGE) {
// Convert radians to degrees for USD
// As per the XML Reference, "mjModel always uses radians"
lower_limit *= (180.0 / mjPI);
upper_limit *= (180.0 / mjPI);
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLowerLimit,
pxr::SdfValueTypeNames->Float),
lower_limit);
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsUpperLimit,
pxr::SdfValueTypeNames->Float),
upper_limit);
} else if (type == mjJNT_SLIDE) {
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsLowerLimit,
pxr::SdfValueTypeNames->Float),
lower_limit);
SetAttributeDefault(
layer_,
CreateAttributeSpec(layer_, joint_spec,
pxr::UsdPhysicsTokens->physicsUpperLimit,
pxr::SdfValueTypeNames->Float),
upper_limit);
}
}
// Finally write the mjcPhysicsJointAPI attributes.
ApplyApiSchema(layer_, joint_spec, MjcPhysicsTokens->MjcJointAPI);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Int,
MjcPhysicsTokens->mjcGroup, joint->group);
WriteUniformAttribute(
joint_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSpringdamper,
pxr::VtArray<double>(joint->springdamper, joint->springdamper + 2));
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSolreflimit,
pxr::VtArray<double>(joint->solref_limit,
joint->solref_limit + mjNREF));
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSolimplimit,
pxr::VtArray<double>(joint->solimp_limit,
joint->solimp_limit + mjNIMP));
WriteUniformAttribute(
joint_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSolreffriction,
pxr::VtArray<double>(joint->solref_friction,
joint->solref_friction + mjNREF));
WriteUniformAttribute(
joint_spec, pxr::SdfValueTypeNames->DoubleArray,
MjcPhysicsTokens->mjcSolimpfriction,
pxr::VtArray<double>(joint->solimp_friction,
joint->solimp_friction + mjNIMP));
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcStiffness, joint->stiffness);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcActuatorfrcrangeMin,
joint->actfrcrange[0]);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcActuatorfrcrangeMax,
joint->actfrcrange[1]);
pxr::TfToken actuatorfrclimited_token = MjcPhysicsTokens->auto_;
if (joint->actfrclimited == mjLIMITED_TRUE) {
actuatorfrclimited_token = MjcPhysicsTokens->true_;
} else if (joint->actfrclimited == mjLIMITED_FALSE) {
actuatorfrclimited_token = MjcPhysicsTokens->false_;
}
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Token,
MjcPhysicsTokens->mjcActuatorfrclimited,
actuatorfrclimited_token);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Bool,
MjcPhysicsTokens->mjcActuatorgravcomp,
static_cast<bool>(joint->actgravcomp));
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcMargin, joint->margin);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcRef, joint->ref);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcSpringref, joint->springref);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcArmature, joint->armature);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcDamping, joint->damping);
WriteUniformAttribute(joint_spec, pxr::SdfValueTypeNames->Double,
MjcPhysicsTokens->mjcFrictionloss,
joint->frictionloss);
}
if (joint_id >= 0) {
joint_paths_[joint_id] = joint_spec->GetPath();
}
}
void WriteCamera(mjsCamera *spec_cam, const mjsBody *body) {
const auto &body_path = body_paths_[mjs_getId(body->element)];
auto name = GetAvailablePrimName(*mjs_getName(spec_cam->element),
pxr::UsdGeomTokens->Camera, body_path);
// Create a root Xform for the world body with the model name if it exists
// otherwise called 'World'.
auto camera_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdGeomTokens->Camera);
int cam_id = mjs_getId(spec_cam->element);
auto transform = MujocoPosQuatToTransform(&model_->cam_pos[3 * cam_id],
&model_->cam_quat[4 * cam_id]);
WriteTransformXformOp(camera_spec, transform);
WriteXformOpOrder(camera_spec,
pxr::VtArray<pxr::TfToken>{kTokens->xformOpTransform});
// If the camera intrinsics are specified, then it is important that we
// reproduce the code in mujoco/src/engine/engine_vis_visualize.c
const float *cam_sensorsize = &model_->cam_sensorsize[cam_id * 2];
bool use_intrinsic = cam_sensorsize[1] > 0.0f;
float znear = spec_->visual.map.znear * model_->stat.extent * 100;
float zfar = spec_->visual.map.zfar * model_->stat.extent * 100;
mjtNum fovy = model_->cam_fovy[cam_id];
const float *cam_intrinsic = &model_->cam_intrinsic[cam_id * 4];
const float aspect_ratio =
use_intrinsic ? cam_sensorsize[0] / cam_sensorsize[1] : 4.0f / 3;
float vertical_apperture =
2 * znear *
(use_intrinsic ? 1.0f / cam_intrinsic[1] *
(cam_sensorsize[1] / 2.f - cam_intrinsic[3])
: mju_tan((fovy / 2) * (std::numbers::pi / 180.0)));
float horizontal_aperture =
use_intrinsic ? 2 * znear / cam_intrinsic[0] *
(cam_sensorsize[0] / 2.f - cam_intrinsic[2])
: vertical_apperture * aspect_ratio;
WriteUniformAttribute(camera_spec, pxr::SdfValueTypeNames->Float2,
pxr::UsdGeomTokens->clippingRange,
pxr::GfVec2f(znear, zfar));
WriteUniformAttribute(camera_spec, pxr::SdfValueTypeNames->Float,
pxr::UsdGeomTokens->focalLength, znear);
WriteUniformAttribute(camera_spec, pxr::SdfValueTypeNames->Float,
pxr::UsdGeomTokens->verticalAperture,
vertical_apperture);
WriteUniformAttribute(camera_spec, pxr::SdfValueTypeNames->Float,
pxr::UsdGeomTokens->horizontalAperture,
horizontal_aperture);
}
void WriteCameras(mjsBody *body) {
mjsCamera *cam = mjs_asCamera(mjs_firstChild(body, mjOBJ_CAMERA, false));
while (cam) {
if (ShouldWrite(cam->element)) {
WriteCamera(cam, body);
}
cam = mjs_asCamera(mjs_nextChild(body, cam->element, false));
}
}
void WriteLight(mjsLight *light, const mjsBody *body) {
const auto &body_path = body_paths_[mjs_getId(body->element)];
auto name = GetAvailablePrimName(*mjs_getName(light->element),
kTokens->light, body_path);
// Create a root Xform for the world body with the model name if it exists
// otherwise called 'World'.
auto light_spec =
CreatePrimSpec(layer_, body_path, name, pxr::UsdLuxTokens->SphereLight);
int light_id = mjs_getId(light->element);
auto transform = MujocoPosQuatToTransform(&model_->light_pos[3 * light_id],
&model_->light_dir[4 * light_id]);
WriteTransformXformOp(light_spec, transform);
WriteXformOpOrder(light_spec,
pxr::VtArray<pxr::TfToken>{kTokens->xformOpTransform});
}
void WriteLights(mjsBody *body) {
mjsLight *light = mjs_asLight(mjs_firstChild(body, mjOBJ_LIGHT, false));
while (light) {
if (ShouldWrite(light->element)) {
WriteLight(light, body);
}
light = mjs_asLight(mjs_nextChild(body, light->element, false));
}
}
void WriteBody(mjsBody *body) {
int body_id = mjs_getId(body->element);
// This should be safe as we process parent bodies before children.
mjsBody *parent = mjs_getParent(body->element);
int parent_id = mjs_getId(parent->element);
pxr::SdfPath parent_path = body_paths_[parent_id];
pxr::TfToken body_name = GetValidPrimName(*mjs_getName(body->element));
// Create Xform prim for body.
auto body_spec = CreatePrimSpec(layer_, parent_path, body_name,
pxr::UsdGeomTokens->Xform);
// The parent_path will be a component which makes the actual articulated
// bodies subcomponents.
auto kind = parent_id == kWorldIndex ? pxr::KindTokens->component
: pxr::KindTokens->subcomponent;
SetPrimKind(layer_, body_spec, kind);
// If the parent is not the world body, but is child of the world body
// then we need to apply the articulation root API.
if (parent_id != kWorldIndex) {
int parent_parent_id = mjs_getId(mjs_getParent(parent->element)->element);
// We guard against applying the API more than once, which can happen when
// there are multiple children.
if (parent_parent_id == kWorldIndex &&
articulation_roots_.find(parent_id) == articulation_roots_.end()) {
ApplyApiSchema(layer_, layer_->GetPrimAtPath(parent_path),
pxr::UsdPhysicsTokens->PhysicsArticulationRootAPI);
articulation_roots_.insert(parent_id);
}
}
// If the body had a mass specified then it must have either inertia or
// fullinertia specified per inertia element XML documentation.
// Therefore it is sufficient to check if the mass is non-zero to see if
// we should set inertial attributes on the body.
//
// Note that if the user has NOT specified any inertial properties then
// we don't want to pull values from the compiled model since coming back
// into Mujoco would take those values instead of computing them
// automatically from the subtree.
if (body->mass > 0) {
// User might have specified the inertia via fullinertia and the
// compiler has extracted all values properly. So leverage those
// instead of doing the computation ourselves here.
ApplyApiSchema(layer_, body_spec, pxr::UsdPhysicsTokens->PhysicsMassAPI);
WriteUniformAttribute(body_spec, pxr::SdfValueTypeNames->Float,
pxr::UsdPhysicsTokens->physicsMass,
(float)model_->body_mass[body_id]);
mjtNum *body_ipos = &model_->body_ipos[body_id * 3];
pxr::GfVec3f inertial_pos(body_ipos[0], body_ipos[1], body_ipos[2]);
WriteUniformAttribute(body_spec, pxr::SdfValueTypeNames->Point3f,
pxr::UsdPhysicsTokens->physicsCenterOfMass,
inertial_pos);
mjtNum *body_iquat = &model_->body_iquat[body_id * 4];
pxr::GfQuatf inertial_frame(body_iquat[0], body_iquat[1], body_iquat[2],
body_iquat[3]);
WriteUniformAttribute(body_spec, pxr::SdfValueTypeNames->Quatf,
pxr::UsdPhysicsTokens->physicsPrincipalAxes,
inertial_frame);
mjtNum *inertia = &model_->body_inertia[body_id * 3];
pxr::GfVec3f diag_inertia(inertia[0], inertia[1], inertia[2]);
WriteUniformAttribute(body_spec, pxr::SdfValueTypeNames->Float3,
pxr::UsdPhysicsTokens->physicsDiagonalInertia,
diag_inertia);
}
ApplyApiSchema(layer_, body_spec,
pxr::UsdPhysicsTokens->PhysicsRigidBodyAPI);
// Create classes if necessary
mjsDefault *spec_default = mjs_getDefault(body->element);
pxr::TfToken body_class_name =
GetValidPrimName(*mjs_getName(spec_default->element));
pxr::SdfPath body_class_path = class_path_.AppendChild(body_class_name);
if (!layer_->HasSpec(body_class_path)) {
CreateClassSpec(layer_, class_path_, body_class_name);
}
// Create XformOp attribute for body transform.
auto xform_op_spec =
CreateAttributeSpec(layer_, body_spec, kTokens->xformOpTransform,
pxr::SdfValueTypeNames->Matrix4d);
// mjModel will have all frames already accounted for so no need to worry
// about them here.
auto body_xform = MujocoPosQuatToTransform(&model_->body_pos[body_id * 3],
&model_->body_quat[body_id * 4]);
SetAttributeDefault(layer_, xform_op_spec, body_xform);
// Create XformOpOrder attribute for body transform order.
// For us this is simply the transform we authored above.
WriteXformOpOrder(body_spec,
pxr::VtArray<pxr::TfToken>{kTokens->xformOpTransform});
pxr::VtDictionary customData;
customData[kTokens->body_name] = *mjs_getName(body->element);
body_spec->SetField(pxr::SdfFieldKeys->CustomData, customData);
body_paths_[body_id] = body_spec->GetPath();
mujoco::usd::SetUsdPrimPathUserValue(body->element, body_spec->GetPath());
}
void WriteBodies() {
mjsBody *body = mjs_asBody(mjs_firstElement(spec_, mjOBJ_BODY));
while (body) {
// If this body originally came from USD, don't try and write it
// again.
if (ShouldWrite(body->element)) {
// Only write a rigidbody if we are not the world body.
// We fall through since the world body might have static
// geom children.
if (mjs_getId(body->element) != kWorldIndex) {
WriteBody(body);
}
WriteSites(body);
WriteGeoms(body);
WriteJoints(body);
WriteCameras(body);
WriteLights(body);
}
body = mjs_asBody(mjs_nextElement(spec_, body->element));
}
}
pxr::SdfPrimSpecHandle WriteWorldBody(const size_t body_index) {
// Create a root Xform for the world body with the model name if it exists
// otherwise called 'World'.
auto name = GetAvailablePrimName(*spec_->modelname, kTokens->world,
pxr::SdfPath::AbsoluteRootPath());
auto world_group_spec =
CreatePrimSpec(layer_, pxr::SdfPath::AbsoluteRootPath(), name,
pxr::UsdGeomTokens->Xform);
SetPrimKind(layer_, world_group_spec, pxr::KindTokens->group);
return world_group_spec;
}
};
namespace mujoco {
namespace usd {
bool WriteSpecToData(mjSpec *spec, pxr::SdfLayerRefPtr layer, bool skip_elems_from_usd) {
mjModel *model = mj_compile(spec, nullptr);
if (model == nullptr) {
TF_ERROR(MujocoCompilationError, "%s", mjs_getError(spec));
return false;
}
ModelWriter(spec, model, layer, skip_elems_from_usd).Write();
return true;
}
} // namespace usd
} // namespace mujoco