Add mujoco/experimental and move USD work there.

PiperOrigin-RevId: 740730698
Change-Id: If9c68798cda502d6d7632906a034437e2557802e
This commit is contained in:
Google DeepMind
2025-03-26 05:35:54 -07:00
committed by Copybara-Service
parent 5c955b8fe2
commit 644b42bee3
14 changed files with 2228 additions and 0 deletions
@@ -0,0 +1,201 @@
// 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/mjcf_file_format.h"
#include <array>
#include <memory>
#include <string>
#include <unordered_set>
#include <mujoco/mujoco.h>
#include "mjcf/mujoco_to_usd.h"
#include <pxr/base/tf/diagnostic.h>
#include <pxr/base/tf/enum.h>
#include <pxr/base/tf/pathUtils.h>
#include <pxr/base/tf/registryManager.h>
#include <pxr/base/tf/staticTokens.h>
#include <pxr/base/tf/type.h>
#include <pxr/base/work/loops.h>
#include <pxr/pxr.h>
#include <pxr/usd/ar/asset.h>
#include <pxr/usd/ar/resolvedPath.h>
#include <pxr/usd/ar/resolver.h>
#include <pxr/usd/sdf/declareHandles.h>
#include <pxr/usd/sdf/fileFormat.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/usd/usdaFileFormat.h>
#include "tinyxml2.h"
PXR_NAMESPACE_OPEN_SCOPE
TF_DEFINE_PUBLIC_TOKENS(UsdMjcfFileFormatTokens, USD_MJCF_FILE_FORMAT_TOKENS);
TF_REGISTRY_FUNCTION(TfType) {
SDF_DEFINE_FILE_FORMAT(UsdMjcfFileFormat, SdfFileFormat);
}
enum ErrorCodes { XmlParsingError };
TF_REGISTRY_FUNCTION(TfEnum) {
TF_ADD_ENUM_NAME(XmlParsingError, "Error when parsing XML.");
};
namespace {
void ResolveMjcfDependencies(const std::string &xml_string,
const std::string &resolved_path);
void AccumulateFilesRecursive(std::unordered_set<std::string> &files,
tinyxml2::XMLElement *elem,
const std::string &resolved_path) {
// get filename
const char *file = elem->Attribute("file");
if (file != nullptr) {
auto identifier = pxr::ArGetResolver().CreateIdentifier(
std::string(file), pxr::ArResolvedPath(resolved_path));
if (!strcasecmp(elem->Value(), "include") ||
!strcasecmp(elem->Value(), "model")) {
auto include_resolved_path = pxr::ArGetResolver().Resolve(identifier);
auto asset = pxr::ArGetResolver().OpenAsset(include_resolved_path);
ResolveMjcfDependencies(asset->GetBuffer().get(), include_resolved_path);
// Neither of these elements should have children.
return;
}
files.insert(identifier);
}
if (!strcasecmp(elem->Value(), "texture")) {
static const char *attributes[] = {"fileright", "fileup", "fileleft",
"filedown", "filefront", "fileback"};
for (const auto &attribute : attributes) {
const char *file = elem->Attribute(attribute);
if (file != nullptr) {
auto identifier = pxr::ArGetResolver().CreateIdentifier(
std::string(file), pxr::ArResolvedPath(resolved_path));
files.insert(identifier);
}
}
}
tinyxml2::XMLElement *child = elem->FirstChildElement();
for (; child; child = child->NextSiblingElement()) {
AccumulateFilesRecursive(files, child, resolved_path);
}
}
void ResolveMjcfDependencies(const std::string &xml_string,
const std::string &resolved_path) {
// load XML file or parse string
tinyxml2::XMLDocument doc;
doc.Parse(xml_string.c_str());
// error checking
if (doc.Error()) {
TF_ERROR(XmlParsingError, "%d:\n%s\n", doc.ErrorID(), doc.ErrorStr());
return;
}
// get top-level element
tinyxml2::XMLElement *root = doc.RootElement();
if (!root) {
TF_ERROR(XmlParsingError, "XML root element not found");
return;
}
// Accumulate file dependencies.
std::unordered_set<std::string> files = {};
AccumulateFilesRecursive(files, root, resolved_path);
auto open_asset = [resolved_path](const std::string &identifier) {
pxr::ArGetResolver().OpenAsset(pxr::ArGetResolver().Resolve(identifier));
};
// Open all assets in parallel.
pxr::WorkParallelForEach(files.begin(), files.end(), open_asset);
}
} // namespace
UsdMjcfFileFormat::UsdMjcfFileFormat()
: SdfFileFormat(
UsdMjcfFileFormatTokens->Id, UsdMjcfFileFormatTokens->Version,
UsdMjcfFileFormatTokens->Target, UsdMjcfFileFormatTokens->Id) {}
UsdMjcfFileFormat::~UsdMjcfFileFormat() {}
bool UsdMjcfFileFormat::CanRead(const std::string &filePath) const {
auto extension = pxr::TfGetExtension(filePath);
if (extension.empty()) {
return false;
}
return extension == this->GetFormatId();
}
bool UsdMjcfFileFormat::ReadImpl(pxr::SdfLayer *layer, mjSpec *spec) const {
auto data = InitData(layer->GetFileFormatArguments());
auto success = mujoco::usd::WriteSpecToData(spec, data);
mj_deleteSpec(spec);
if (!success) {
return false;
}
_SetLayerData(layer, data);
return true;
}
bool UsdMjcfFileFormat::ReadFromString(pxr::SdfLayer *layer,
const std::string &str) const {
std::array<char, 1024> error;
mjSpec *spec =
mj_parseXMLString(str.c_str(), nullptr, error.data(), error.size());
if (spec == nullptr) {
TF_WARN(XmlParsingError, "%s", error.data());
return false;
}
return ReadImpl(layer, spec);
}
bool UsdMjcfFileFormat::Read(pxr::SdfLayer *layer,
const std::string &resolved_path,
bool metadata_only) const {
// Resolved all dependencies so that they are accessible when parsing
// the XML.
std::shared_ptr<pxr::ArAsset> asset =
pxr::ArGetResolver().OpenAsset(pxr::ArResolvedPath(resolved_path));
auto buffer = asset->GetBuffer();
ResolveMjcfDependencies(buffer.get(), resolved_path);
// Parse to USD.
std::array<char, 1024> error;
mjSpec *spec =
mj_parseXML(resolved_path.c_str(), nullptr, error.data(), error.size());
if (spec == nullptr) {
TF_WARN(XmlParsingError, "%s", error.data());
return false;
}
return ReadImpl(layer, spec);
}
bool UsdMjcfFileFormat::WriteToString(const SdfLayer &layer, std::string *str,
const std::string &comment) const {
return SdfFileFormat::FindById(pxr::UsdUsdaFileFormatTokens->Id)
->WriteToString(layer, str, comment);
}
PXR_NAMESPACE_CLOSE_SCOPE
@@ -0,0 +1,85 @@
// 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_PLUGINS_MJCF_MJCF_FILE_FORMAT_H_
#define MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MJCF_FILE_FORMAT_H_
#include <string>
#include <mujoco/mujoco.h>
#include <pxr/base/tf/declarePtrs.h>
#include <pxr/base/tf/staticTokens.h>
#include <pxr/pxr.h>
#include <pxr/usd/sdf/declareHandles.h>
#include <pxr/usd/sdf/fileFormat.h>
#include <pxr/usd/usd/api.h>
PXR_NAMESPACE_OPEN_SCOPE
// The Id should realistically be mjcf, but the id and extension need to match.
// So near term it just assumes the only .xml file we would import is MJCF.
#define USD_MJCF_FILE_FORMAT_TOKENS \
((Id, "xml"))((Version, "1.0"))((Target, "usd"))
TF_DECLARE_PUBLIC_TOKENS(UsdMjcfFileFormatTokens, USD_MJCF_FILE_FORMAT_TOKENS);
TF_DECLARE_WEAK_AND_REF_PTRS(UsdMjcfFileFormat);
class UsdMjcfFileFormat : public SdfFileFormat {
public:
using SdfFileFormat::FileFormatArguments;
// Returns true if 'file' can be read by this format plugin.
USD_API
bool CanRead(const std::string &file) const override;
// Reads scene description from the asset specified by resolved_path into
// 'layer'.
//
// metadataOnly is a flag that asks for only the layer metadata to be read in,
// which can be much faster if that is all that is required but currently we
// ignore it.
//
// Returns true if the asset is successfully read into layer, false otherwise.
USD_API
bool Read(pxr::SdfLayer *layer, const std::string &resolved_path,
bool metadata_only) const override;
// Reads data in the string 'str' into 'layer'.
//
// If the file is successfully read, this method returns true. Otherwise,
// false is returned and errors are posted.
USD_API
bool ReadFromString(SdfLayer *layer, const std::string &str) const override;
// Writes the contents in 'layer' to 'str'. This just forwards to the usda
// implementation.
USD_API
bool WriteToString(const SdfLayer &layer, std::string *str,
const std::string &comment) const override;
protected:
SDF_FILE_FORMAT_FACTORY_ACCESS;
UsdMjcfFileFormat();
virtual ~UsdMjcfFileFormat();
private:
// Function delegated to by Read and ReadFromString.
bool ReadImpl(SdfLayer *layer, mjSpec *spec) const;
};
PXR_NAMESPACE_CLOSE_SCOPE
#endif // MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MJCF_FILE_FORMAT_H_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
// 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_PLUGINS_MJCF_MUJOCO_TO_USD_H_
#define MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MUJOCO_TO_USD_H_
#include <mujoco/mujoco.h>
#include <pxr/usd/sdf/abstractData.h>
namespace mujoco {
namespace usd {
// Given an mjSpec, write it to a SdfAbstractData.
//
// Args:
// spec: mjSpec built programmatically or via parsed XML.
// data: SdfAbstractDataRefPtr that will be written to.
bool WriteSpecToData(mjSpec* spec, pxr::SdfAbstractDataRefPtr& data);
} // namespace usd
} // namespace mujoco
#endif // MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MUJOCO_TO_USD_H_
@@ -0,0 +1,25 @@
{
"Plugins": [
{
"Info": {
"Types": {
"UsdMjcfFileFormat": {
"bases": ["SdfFileFormat"],
"displayName": "MJCF USD File Format",
"extensions": ["xml"],
"formatId": "xml",
"primary": true,
"supportsReading": true,
"supportsWriting": false,
"target": "usd"
}
}
},
"LibraryPath": "",
"Name": "usdMjcf",
"ResourcePath": "",
"Root": ".",
"Type": "library"
}
]
}
+199
View File
@@ -0,0 +1,199 @@
// 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/utils.h"
#include <string>
#include <vector>
#include <pxr/base/tf/token.h>
#include <pxr/usd/sdf/abstractData.h>
#include <pxr/usd/sdf/listOp.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/schema.h>
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/usd/usd/tokens.h>
namespace {
template <typename T>
void AppendChild(pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& specPath,
const pxr::TfToken& childKey, const T& child) {
// Get existing children.
std::vector<T> children;
pxr::SdfAbstractDataTypedValue<std::vector<T>> getter(&children);
data->Has(specPath, childKey, &getter);
children.push_back(child);
data->Set(specPath, childKey,
pxr::SdfAbstractDataConstTypedValue<std::vector<T>>(&children));
}
template <typename T>
void AppendListOp(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& spec_path, const pxr::TfToken& field,
const T& item) {
// Get existing list op.
pxr::SdfListOp<T> list_op;
pxr::SdfAbstractDataTypedValue<pxr::SdfListOp<T>> getter(&list_op);
data->Has(spec_path, field, &getter);
auto items = list_op.GetExplicitItems();
items.push_back(item);
list_op.SetExplicitItems(items);
data->Set(spec_path, field,
pxr::SdfAbstractDataConstTypedValue<pxr::SdfListOp<T>>(&list_op));
}
template <typename T>
void PrependListOp(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& spec_path, const pxr::TfToken& field,
const T& item) {
// Get existing list op.
pxr::SdfListOp<T> listOp;
pxr::SdfAbstractDataTypedValue<pxr::SdfListOp<T>> getter(&listOp);
data->Has(spec_path, field, &getter);
auto prependedItems = listOp.GetPrependedItems();
prependedItems.insert(prependedItems.begin(), item);
listOp.SetPrependedItems(prependedItems);
data->Set(spec_path, field,
pxr::SdfAbstractDataConstTypedValue<pxr::SdfListOp<T>>(&listOp));
}
} // namespace
namespace mujoco {
namespace usd {
pxr::SdfPath CreatePrimSpec(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& parent_path,
const pxr::TfToken& name, const pxr::TfToken& type,
pxr::SdfSpecifier specifier) {
const pxr::SdfPath prim_path = parent_path.AppendChild(name);
data->CreateSpec(prim_path, pxr::SdfSpecTypePrim);
data->Set(prim_path, pxr::SdfFieldKeys->Specifier,
pxr::SdfAbstractDataConstTypedValue<pxr::SdfSpecifier>(&specifier));
if (!type.IsEmpty()) {
data->Set(prim_path, pxr::SdfFieldKeys->TypeName,
pxr::SdfAbstractDataConstTypedValue<const pxr::TfToken>(&type));
}
AppendChild(data, parent_path, pxr::SdfChildrenKeys->PrimChildren, name);
return prim_path;
}
pxr::SdfPath CreateAttributeSpec(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::TfToken& name,
const pxr::SdfValueTypeName& type_name,
pxr::SdfVariability variability) {
const pxr::SdfPath propertyPath = prim_path.AppendProperty(name);
data->CreateSpec(propertyPath, pxr::SdfSpecTypeAttribute);
pxr::TfToken typeNameToken = type_name.GetAsToken();
data->Set(propertyPath, pxr::SdfFieldKeys->TypeName,
pxr::SdfAbstractDataConstTypedValue<pxr::TfToken>(&typeNameToken));
if (variability != pxr::SdfVariabilityVarying) {
data->Set(
propertyPath, pxr::SdfFieldKeys->Variability,
pxr::SdfAbstractDataConstTypedValue<pxr::SdfVariability>(&variability));
}
AppendChild(data, prim_path, pxr::SdfChildrenKeys->PropertyChildren, name);
return propertyPath;
}
pxr::SdfPath CreateRelationshipSpec(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::TfToken& relationship_name,
const pxr::SdfPath& relationship_path,
pxr::SdfVariability variability) {
pxr::SdfPath prop_path = prim_path.AppendProperty(relationship_name);
data->CreateSpec(prop_path, pxr::SdfSpecTypeRelationship);
if (variability != pxr::SdfVariabilityVarying) {
data->Set(
prop_path, pxr::SdfFieldKeys->Variability,
pxr::SdfAbstractDataConstTypedValue<pxr::SdfVariability>(&variability));
}
AppendChild(data, prim_path, pxr::SdfChildrenKeys->PropertyChildren,
relationship_name);
AppendChild(data, prop_path, pxr::SdfChildrenKeys->RelationshipTargetChildren,
relationship_path);
AppendListOp(data, prop_path, pxr::SdfFieldKeys->TargetPaths,
relationship_path);
pxr::SdfPath target_path = prop_path.AppendTarget(relationship_path);
data->CreateSpec(target_path, pxr::SdfSpecTypeRelationshipTarget);
return prop_path;
}
pxr::SdfPath CreateClassSpec(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::TfToken& class_name) {
pxr::SdfPath class_path = prim_path.AppendChild(class_name);
pxr::SdfSpecifier class_specifier = pxr::SdfSpecifier::SdfSpecifierClass;
data->CreateSpec(class_path, pxr::SdfSpecTypePrim);
data->Set(
class_path, pxr::SdfFieldKeys->Specifier,
pxr::SdfAbstractDataConstTypedValue<pxr::SdfSpecifier>(&class_specifier));
AppendChild(data, prim_path, pxr::SdfChildrenKeys->PrimChildren, class_name);
return class_path;
}
void AddAttributeConnection(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& attribute_path,
const pxr::SdfPath& target_attribute_path) {
AppendChild(data, attribute_path, pxr::SdfChildrenKeys->ConnectionChildren,
target_attribute_path);
AppendListOp(data, attribute_path, pxr::SdfFieldKeys->ConnectionPaths,
target_attribute_path);
data->CreateSpec(attribute_path.AppendTarget(target_attribute_path),
pxr::SdfSpecTypeConnection);
}
void AddPrimReference(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::SdfPath& referenced_prim_path) {
PrependListOp(data, prim_path, pxr::SdfFieldKeys->References,
pxr::SdfReference("", referenced_prim_path));
}
void AddPrimInherit(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::SdfPath& class_path) {
PrependListOp(data, prim_path, pxr::SdfFieldKeys->InheritPaths, class_path);
}
void ApplyApiSchema(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::TfToken& schema_name) {
PrependListOp(data, prim_path, pxr::UsdTokens->apiSchemas, schema_name);
}
void SetPrimKind(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path, pxr::TfToken kind) {
SetPrimMetadata(data, prim_path, pxr::TfToken("kind"), kind);
}
} // namespace usd
} // namespace mujoco
+127
View File
@@ -0,0 +1,127 @@
// 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_PLUGINS_MJCF_UTILS_H_
#define MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_UTILS_H_
#include <type_traits>
#include <pxr/base/tf/token.h>
#include <pxr/imaging/hd/primTypeIndex.h>
#include <pxr/usd/sdf/abstractData.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>
namespace mujoco {
namespace usd {
// Create a prim spec and append it as a child of parent_path.
pxr::SdfPath CreatePrimSpec(
pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& parent_path,
const pxr::TfToken& name, const pxr::TfToken& type = pxr::TfToken(),
pxr::SdfSpecifier specifier = pxr::SdfSpecifier::SdfSpecifierDef);
// Create an attribute spec and append it as a child of parent_path.
// By default the attribute will be varying.
pxr::SdfPath CreateAttributeSpec(
pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& prim_path,
const pxr::TfToken& name, const pxr::SdfValueTypeName& type_name,
pxr::SdfVariability variability = pxr::SdfVariabilityVarying);
// Create a relationship spec and append it as a child of prim_path.
pxr::SdfPath CreateRelationshipSpec(
pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& prim_path,
const pxr::TfToken& relationship_name,
const pxr::SdfPath& relationship_path,
pxr::SdfVariability variability = pxr::SdfVariabilityVarying);
pxr::SdfPath CreateClassSpec(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::TfToken& class_name);
void AddAttributeConnection(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& attribute_path,
const pxr::SdfPath& target_attribute_path);
void AddPrimReference(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::SdfPath& referenced_prim_path);
void AddPrimInherit(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::SdfPath& class_path);
void ApplyApiSchema(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path,
const pxr::TfToken& schema_name);
void SetPrimKind(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path, pxr::TfToken kind);
// Set the value specified by key on any field at field_path.
template <typename T>
void SetField(pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& field_path,
const pxr::TfToken key, T&& value) {
using Deduced = typename std::remove_reference_t<T>;
const auto typed_val = pxr::SdfAbstractDataConstTypedValue<Deduced>(&value);
const pxr::SdfAbstractDataConstValue& untyped_val = typed_val;
data->Set(field_path, key, untyped_val);
}
// Set the value specified by key on an attribute spec at attribute_path.
template <typename T>
void SetAttribute(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& attribute_path, const pxr::TfToken key,
T&& value) {
SetField(data, attribute_path, key, value);
}
// Set the value specified by key on a prim spec at prim_path.
template <typename T>
void SetPrimMetadata(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& prim_path, const pxr::TfToken key,
T&& value) {
SetAttribute(data, prim_path, key, value);
}
// Set the value specified by key on an attribute spec at attribute_path.
template <typename T>
void SetAttributeMetadata(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& attribute_path,
const pxr::TfToken key, T&& value) {
SetAttribute(data, attribute_path, key, value);
}
// Set the default value on an attribute spec at attribute_path.
template <typename T>
void SetAttributeDefault(pxr::SdfAbstractDataRefPtr& data,
const pxr::SdfPath& attribute_path,
T&& default_value) {
SetAttribute(data, attribute_path, pxr::SdfFieldKeys->Default, default_value);
}
// Set the value specified by key on the root layer.
template <typename T>
void SetLayerMetadata(pxr::SdfAbstractDataRefPtr& data, const pxr::TfToken& key,
T&& value) {
SetAttribute(data, pxr::SdfPath::AbsoluteRootPath(), key, value);
}
} // namespace usd
} // namespace mujoco
#endif // MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_UTILS_H_
@@ -0,0 +1,64 @@
// 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 "test/experimental/usd/plugins/mjcf/fixture.h"
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <absl/log/check.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/declareHandles.h>
#include <pxr/usd/sdf/fileFormat.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/common.h>
#include <pxr/usd/usd/modelAPI.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
namespace mujoco {
using pxr::SdfPath;
pxr::SdfLayerRefPtr LoadLayer(const std::string& xml) {
auto layer = pxr::SdfLayer::CreateAnonymous(
"test_layer", pxr::SdfFileFormat::FindByExtension("xml"));
layer->ImportFromString(xml);
EXPECT_THAT(layer, testing::NotNull());
return layer;
}
template <>
void ExpectAttributeEqual<pxr::SdfAssetPath>(pxr::UsdStageRefPtr stage,
const char* path,
const pxr::SdfAssetPath& value) {
auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path));
EXPECT_TRUE(attr.IsValid());
pxr::SdfAssetPath attr_value;
attr.Get(&attr_value);
EXPECT_EQ(attr_value.GetAssetPath(), value.GetAssetPath());
}
void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path,
const char* connection_path) {
auto attr = stage->GetAttributeAtPath(SdfPath(path));
EXPECT_TRUE(attr.IsValid());
pxr::SdfPathVector sources;
attr.GetConnections(&sources);
EXPECT_EQ(sources.size(), 1);
EXPECT_EQ(sources[0], SdfPath(connection_path));
}
//
} // namespace mujoco
@@ -0,0 +1,67 @@
// 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_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_
#define MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_
#include <string>
#include <gtest/gtest.h>
#include "test/fixture.h"
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/declareHandles.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/common.h>
#include <pxr/usd/usd/modelAPI.h>
#include <pxr/usd/usd/stage.h>
#define EXPECT_PRIM_VALID(stage, path) \
EXPECT_TRUE((stage)->GetPrimAtPath(SdfPath(path)).IsValid());
#define EXPECT_PRIM_KIND(stage, path, kind) \
{ \
pxr::TfToken prim_kind; \
pxr::UsdModelAPI::Get(stage, SdfPath(path)).GetKind(&prim_kind); \
EXPECT_EQ(kind, prim_kind); \
}
namespace mujoco {
pxr::SdfLayerRefPtr LoadLayer(const std::string& xml);
template <typename T>
void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path,
const T& value) {
auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path));
EXPECT_TRUE(attr.IsValid());
T attr_value;
attr.Get(&attr_value);
EXPECT_EQ(attr_value, value);
}
// Specialization for SdfAssetPath, so that we can compare only the asset path
// and not care about whatever the resolved path is.
// Otherwise the default operator== would fail because it tests for equality of
// the asset path AND the resolved path.
template <>
void ExpectAttributeEqual<pxr::SdfAssetPath>(pxr::UsdStageRefPtr stage,
const char* path,
const pxr::SdfAssetPath& value);
void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path,
const char* connection_path);
using MjcfSdfFileFormatPluginTest = MujocoTest;
} // namespace mujoco
#endif // MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_
@@ -0,0 +1,387 @@
// 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 <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <absl/strings/string_view.h>
#include "test/experimental/usd/plugins/mjcf/fixture.h"
#include "test/fixture.h"
#include <pxr/base/gf/vec2f.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/tf/staticData.h>
#include <pxr/base/tf/staticTokens.h>
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/array.h>
#include <pxr/pxr.h>
#include <pxr/usd/kind/registry.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/declareHandles.h>
#include <pxr/usd/sdf/fileFormat.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/common.h>
#include <pxr/usd/usd/modelAPI.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/primvar.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
PXR_NAMESPACE_OPEN_SCOPE
// clang-format off
TF_DEFINE_PRIVATE_TOKENS(_tokens,
(st)
);
// clang-format on
PXR_NAMESPACE_CLOSE_SCOPE
namespace mujoco {
namespace {
using pxr::SdfPath;
static const char* kMaterialsPath =
"experimental/usd/plugins/mjcf/testdata/materials.xml";
static const char* kMeshObjPath =
"experimental/usd/plugins/mjcf/testdata/mesh_obj.xml";
TEST_F(MjcfSdfFileFormatPluginTest, TestClassAuthored) {
static constexpr char kXml[] = R"(
<mujoco model="test">
<default>
<default class="test">
</default>
</default>
<worldbody>
<body name="test_body" pos="0 0 0">
<geom class="test" type="sphere" size="2 2 2"/>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
EXPECT_PRIM_VALID(stage, "/__class__");
EXPECT_PRIM_VALID(stage, "/__class__/test");
}
TEST_F(MjcfSdfFileFormatPluginTest, TestBasicMeshSources) {
static constexpr char kXml[] = R"(
<mujoco model="mesh test">
<asset>
<mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/>
</asset>
<worldbody>
<body name="test_body">
<geom type="mesh" mesh="tetrahedron"/>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
EXPECT_PRIM_VALID(stage, "/mesh_test");
EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/test_body/tetrahedron");
EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/test_body/tetrahedron/Mesh");
}
TEST_F(MjcfSdfFileFormatPluginTest, TestMaterials) {
const std::string xml_path = GetTestDataFilePath(kMaterialsPath);
auto stage = pxr::UsdStage::Open(xml_path);
EXPECT_THAT(stage, testing::NotNull());
EXPECT_PRIM_VALID(stage, "/mesh_test");
EXPECT_PRIM_VALID(stage, "/mesh_test/Materials");
EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_red");
EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_red/PreviewSurface");
ExpectAttributeEqual(
stage,
"/mesh_test/Materials/material_red/PreviewSurface.inputs:diffuseColor",
pxr::GfVec3f(0.8, 0, 0));
EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_texture");
EXPECT_PRIM_VALID(stage,
"/mesh_test/Materials/material_texture/PreviewSurface");
EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_texture/uvmap");
EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_texture/texture");
ExpectAttributeHasConnection(
stage,
"/mesh_test/Materials/material_texture/"
"PreviewSurface.inputs:diffuseColor",
"/mesh_test/Materials/material_texture/texture.outputs:rgb");
ExpectAttributeEqual(
stage, "/mesh_test/Materials/material_texture/texture.inputs:file",
pxr::SdfAssetPath("textures/cube.png"));
}
TEST_F(MjcfSdfFileFormatPluginTest, TestFaceVaryingMeshSourcesSimpleMjcfMesh) {
static constexpr char kXml[] = R"(
<mujoco model="mesh test">
<asset>
<mesh
name="tetrahedron"
face="0 3 2 0 1 3 0 2 1 1 2 3"
vertex="0 1 0 0 0 0 1 0 1 1 0 -1"
normal="1 0 0 0 1 0 0 0 1 -1 0 0"
texcoord="0.5 0.5 0 0.5 1 1 1 0"/>
</asset>
<worldbody>
<body name="test_body">
<geom type="mesh" mesh="tetrahedron"/>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
auto mesh = pxr::UsdGeomMesh::Get(
stage, SdfPath("/mesh_test/test_body/test_body/tetrahedron/Mesh"));
ASSERT_TRUE(mesh);
pxr::VtArray<int> face_vertex_counts;
mesh.GetFaceVertexCountsAttr().Get(&face_vertex_counts);
EXPECT_EQ(face_vertex_counts.size(), 4);
EXPECT_EQ(face_vertex_counts, pxr::VtArray<int>({3, 3, 3, 3}));
pxr::VtArray<int> face_vertex_indices;
mesh.GetFaceVertexIndicesAttr().Get(&face_vertex_indices);
EXPECT_EQ(face_vertex_indices.size(), 12);
EXPECT_EQ(face_vertex_indices,
pxr::VtArray<int>({0, 3, 2, 0, 1, 3, 0, 2, 1, 1, 2, 3}));
pxr::VtArray<pxr::GfVec3f> normals;
mesh.GetNormalsAttr().Get(&normals);
EXPECT_EQ(normals.size(), face_vertex_indices.size());
// We can't directly check the normals values because they are altered by
// Mujoco's compiling step. So we at least check that normals with the same
// original index are the same.
for (int i = 0; i < face_vertex_indices.size(); ++i) {
for (int j = i + 1; j < face_vertex_indices.size(); ++j) {
if (face_vertex_indices[i] == face_vertex_indices[j]) {
EXPECT_EQ(normals[i], normals[j]);
}
}
}
auto primvars_api = pxr::UsdGeomPrimvarsAPI(mesh.GetPrim());
pxr::VtArray<pxr::GfVec2f> texcoords;
EXPECT_TRUE(primvars_api.HasPrimvar(pxr::_tokens->st));
auto primvar_st = primvars_api.GetPrimvar(pxr::_tokens->st);
primvar_st.Get(&texcoords);
EXPECT_EQ(texcoords.size(), face_vertex_indices.size());
// Check the faceVarying texcoords against the manually indexed source
// texcoords.
pxr::VtArray<pxr::GfVec2f> source_texcoords{
{0.5, 0.5}, {0, 0.5}, {1, 0}, {1, 1}};
for (int i = 0; i < face_vertex_indices.size(); ++i) {
EXPECT_EQ(texcoords[i], source_texcoords[face_vertex_indices[i]]);
}
}
TEST_F(MjcfSdfFileFormatPluginTest,
TestFaceVaryingMeshSourcesObjWithIndexedNormals) {
const std::string xml_path = GetTestDataFilePath(kMeshObjPath);
auto stage = pxr::UsdStage::Open(xml_path);
EXPECT_THAT(stage, testing::NotNull());
auto mesh = pxr::UsdGeomMesh::Get(
stage, SdfPath("/mesh_test/test_body/test_body/mesh/Mesh"));
ASSERT_TRUE(mesh);
pxr::VtArray<int> face_vertex_counts;
mesh.GetFaceVertexCountsAttr().Get(&face_vertex_counts);
EXPECT_EQ(face_vertex_counts.size(), 4);
EXPECT_EQ(face_vertex_counts, pxr::VtArray<int>({3, 3, 3, 3}));
pxr::VtArray<int> face_vertex_indices;
mesh.GetFaceVertexIndicesAttr().Get(&face_vertex_indices);
EXPECT_EQ(face_vertex_indices.size(), 12);
EXPECT_EQ(face_vertex_indices,
pxr::VtArray<int>({0, 3, 2, 0, 1, 3, 0, 2, 1, 1, 2, 3}));
pxr::VtArray<pxr::GfVec3f> normals;
mesh.GetNormalsAttr().Get(&normals);
EXPECT_EQ(normals.size(), face_vertex_indices.size());
// We can't directly check the normals values because they are altered by
// Mujoco's compiling step.
// We also can't access the normals indexing data, and can't use the vertex
// indexing data here because they are separate.
// So we check that the first half of the normals are the same, then the
// second half, as set in the OBJ file.
pxr::GfVec3f first_half_normal = normals[0];
pxr::GfVec3f second_half_normal = normals[face_vertex_indices.size() / 2];
EXPECT_NE(first_half_normal, second_half_normal);
int i = 0;
for (; i < face_vertex_indices.size() / 2; ++i) {
EXPECT_EQ(normals[i], first_half_normal);
}
for (; i < face_vertex_indices.size(); ++i) {
EXPECT_EQ(normals[i], second_half_normal);
}
auto primvars_api = pxr::UsdGeomPrimvarsAPI(mesh.GetPrim());
pxr::VtArray<pxr::GfVec2f> texcoords;
EXPECT_TRUE(primvars_api.HasPrimvar(pxr::_tokens->st));
auto primvar_st = primvars_api.GetPrimvar(pxr::_tokens->st);
primvar_st.Get(&texcoords);
EXPECT_EQ(texcoords.size(), face_vertex_indices.size());
// Check the faceVarying texcoords against the manually indexed source
// texcoords.
// NOTE: For OBJ we must use different indices for the texcoords than for the
// vertices!
std::vector<int> source_face_texcoord_indices{0, 1, 2, 1, 2, 3,
2, 3, 0, 3, 0, 1};
pxr::VtArray<pxr::GfVec2f> source_texcoords{
{0.5, 0.5}, {0, 0.5}, {1, 0}, {1, 1}};
// NOTE: The v component of the texcoords is flipped when Mujoco loads the
// OBJ.
for (auto& uv : source_texcoords) {
uv[1] = 1 - uv[1];
}
for (int i = 0; i < source_face_texcoord_indices.size(); ++i) {
EXPECT_EQ(texcoords[i], source_texcoords[source_face_texcoord_indices[i]]);
}
}
TEST_F(MjcfSdfFileFormatPluginTest, TestBody) {
static constexpr char kXml[] = R"(
<mujoco model="body test">
<asset>
<mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/>
</asset>
<worldbody>
<body name="test_body" pos="0 1 0">
<joint type="free" />
<frame pos="0 0 1">
<frame pos="0 0 1">
<body name="test_body_2" pos="1 0 0">
<geom type="mesh" mesh="tetrahedron"/>
</body>
</frame>
</frame>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
EXPECT_PRIM_VALID(stage, "/body_test");
EXPECT_PRIM_VALID(stage, "/body_test/test_body");
EXPECT_PRIM_VALID(stage, "/body_test/test_body/test_body");
EXPECT_PRIM_VALID(stage, "/body_test/test_body/test_body_2");
}
TEST_F(MjcfSdfFileFormatPluginTest, TestBasicParenting) {
static constexpr char kXml[] = R"(
<mujoco model="test">
<worldbody>
<body name="root" pos="0 1 0">
<body name="root/body_1" pos="1 0 0" />
<body name="root/body_2" pos="1 0 0">
<body name="root/body_3" pos="1 0 0" />
</body>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
EXPECT_PRIM_VALID(stage, "/test/root");
EXPECT_PRIM_VALID(stage, "/test/root/root");
EXPECT_PRIM_VALID(stage, "/test/root/root_body_1");
EXPECT_PRIM_VALID(stage, "/test/root/root_body_2");
EXPECT_PRIM_VALID(stage, "/test/root/root_body_3");
}
TEST_F(MjcfSdfFileFormatPluginTest, TestJointsDoNotAffectParenting) {
static constexpr char kXml[] = R"(
<mujoco model="test">
<asset>
<mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/>
</asset>
<worldbody>
<body name="root" pos="0 1 0">
<joint type="free" />
<geom type="mesh" mesh="tetrahedron"/>
<body name="middle">
<body name="tet">
<joint type="hinge" />
<geom type="mesh" mesh="tetrahedron"/>
</body>
</body>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
EXPECT_PRIM_VALID(stage, "/test/root");
EXPECT_PRIM_VALID(stage, "/test/root/root");
EXPECT_PRIM_VALID(stage, "/test/root/middle");
EXPECT_PRIM_VALID(stage, "/test/root/tet");
}
TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) {
static constexpr char kXml[] = R"(
<mujoco model="test">
<asset>
<mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/>
</asset>
<worldbody>
<body name="root" pos="0 1 0">
<joint type="free" />
<geom type="mesh" mesh="tetrahedron"/>
<body name="middle">
<body name="tet">
<joint type="hinge" />
<geom type="mesh" mesh="tetrahedron"/>
</body>
</body>
</body>
</worldbody>
</mujoco>
)";
pxr::SdfLayerRefPtr layer = LoadLayer(kXml);
auto stage = pxr::UsdStage::Open(layer);
EXPECT_PRIM_KIND(stage, "/test", pxr::KindTokens->group);
EXPECT_PRIM_KIND(stage, "/test/root", pxr::KindTokens->component);
EXPECT_PRIM_KIND(stage, "/test/root/root", pxr::KindTokens->subcomponent);
EXPECT_PRIM_KIND(stage, "/test/root/middle", pxr::KindTokens->subcomponent);
EXPECT_PRIM_KIND(stage, "/test/root/tet", pxr::KindTokens->subcomponent);
}
} // namespace
} // namespace mujoco
@@ -0,0 +1,15 @@
<mujoco model="mesh test">
<asset>
<texture name="texture" file="textures/cube.png" type="2d" />
<material name="material_red" rgba="0.8 0 0 1" />
<material name="material_texture" texture="texture" />
<mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/>
</asset>
<worldbody>
<body name="test_body">
<geom material="material_red" type="mesh" mesh="tetrahedron"/>
<geom material="material_texture" type="mesh" mesh="tetrahedron"/>
</body>
</worldbody>
</mujoco>
@@ -0,0 +1,10 @@
<mujoco model="mesh test">
<asset>
<mesh name="mesh" file="meshes/tetrahedron.obj"/>
</asset>
<worldbody>
<body name="test_body">
<geom type="mesh" mesh="mesh"/>
</body>
</worldbody>
</mujoco>
@@ -0,0 +1,16 @@
v 0 1 0
v 0 0 0
v 1 0 1
v 1 0 -1
vn 1 0 0
vn -1 0 0
vt 0.5 0.5
vt 0 0.5
vt 1 1
vt 1 0
f 1/1/1 4/2/1 3/3/1
f 1/2/1 2/3/1 4/4/1
f 1/3/2 3/4/2 2/1/2
f 2/4/2 3/1/2 4/2/2
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB