USD: Support armature and shape key export #111931

Merged
Michael Kowalski merged 48 commits from makowalski/blender:usdskel_export_pr into main 2024-01-02 15:51:50 +01:00
20 changed files with 2100 additions and 15 deletions

View File

@ -185,6 +185,10 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
const bool overwrite_textures = RNA_boolean_get(op->ptr, "overwrite_textures");
const bool relative_paths = RNA_boolean_get(op->ptr, "relative_paths");
const bool export_armatures = RNA_boolean_get(op->ptr, "export_armatures");
const bool export_shapekeys = RNA_boolean_get(op->ptr, "export_shapekeys");
const bool only_deform_bones = RNA_boolean_get(op->ptr, "only_deform_bones");
char root_prim_path[FILE_MAX];
RNA_string_get(op->ptr, "root_prim_path", root_prim_path);
process_prim_path(root_prim_path);
@ -196,6 +200,9 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
export_normals,
export_mesh_colors,
export_materials,
export_armatures,
export_shapekeys,
only_deform_bones,
export_subdiv,
selected_objects_only,
visible_objects_only,
@ -234,6 +241,15 @@ static void wm_usd_export_draw(bContext * /*C*/, wmOperator *op)
uiItemR(col, ptr, "export_uvmaps", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "export_normals", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "export_materials", UI_ITEM_NONE, nullptr, ICON_NONE);
col = uiLayoutColumnWithHeading(box, true, IFACE_("Rigging"));
uiItemR(col, ptr, "export_armatures", UI_ITEM_NONE, nullptr, ICON_NONE);
uiLayout *row = uiLayoutRow(col, true);
uiItemR(row, ptr, "only_deform_bones", UI_ITEM_NONE, nullptr, ICON_NONE);
uiLayoutSetActive(row, RNA_boolean_get(ptr, "export_armatures"));
uiItemR(col, ptr, "export_shapekeys", UI_ITEM_NONE, nullptr, ICON_NONE);
col = uiLayoutColumn(box, true);
uiItemR(col, ptr, "export_subdivision", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "root_prim_path", UI_ITEM_NONE, nullptr, ICON_NONE);
@ -246,7 +262,7 @@ static void wm_usd_export_draw(bContext * /*C*/, wmOperator *op)
const bool export_mtl = RNA_boolean_get(ptr, "export_materials");
uiLayoutSetActive(col, export_mtl);
uiLayout *row = uiLayoutRow(col, true);
row = uiLayoutRow(col, true);
uiItemR(row, ptr, "export_textures", UI_ITEM_NONE, nullptr, ICON_NONE);
const bool preview = RNA_boolean_get(ptr, "generate_preview_surface");
uiLayoutSetActive(row, export_mtl && preview);
@ -367,6 +383,22 @@ void WM_OT_usd_export(wmOperatorType *ot)
"Choose how subdivision modifiers will be mapped to the USD subdivision scheme "
"during export");
RNA_def_boolean(ot->srna,
"export_armatures",
true,
"Armatures",
"Export armatures and meshes with armature modifiers as USD skeletons and "
"skinned meshes");
RNA_def_boolean(ot->srna,
"only_deform_bones",
false,
"Only Deform Bones",
"Only export deform bones and their parents");
RNA_def_boolean(
ot->srna, "export_shapekeys", true, "Shape Keys", "Export shape keys as USD blend shapes");
RNA_def_boolean(ot->srna,
"use_instancing",
false,

View File

@ -81,6 +81,8 @@ set(INC_SYS
set(SRC
intern/usd_asset_utils.cc
intern/usd_armature_utils.cc
intern/usd_blend_shape_utils.cc
intern/usd_capi_export.cc
intern/usd_capi_import.cc
intern/usd_hierarchy_iterator.cc
@ -95,6 +97,7 @@ set(SRC
intern/usd_writer_metaball.cc
intern/usd_writer_transform.cc
intern/usd_writer_volume.cc
intern/usd_writer_armature.cc
intern/usd_reader_camera.cc
intern/usd_reader_curve.cc
@ -111,11 +114,14 @@ set(SRC
intern/usd_reader_volume.cc
intern/usd_reader_xform.cc
intern/usd_skel_convert.cc
intern/usd_skel_root_utils.cc
usd.h
usd.hh
intern/usd_asset_utils.h
intern/usd_armature_utils.h
intern/usd_blend_shape_utils.h
intern/usd_exporter_context.h
intern/usd_hash_types.h
intern/usd_hierarchy_iterator.h
@ -130,6 +136,7 @@ set(SRC
intern/usd_writer_metaball.h
intern/usd_writer_transform.h
intern/usd_writer_volume.h
intern/usd_writer_armature.h
intern/usd_reader_camera.h
intern/usd_reader_curve.h
@ -146,6 +153,7 @@ set(SRC
intern/usd_reader_volume.h
intern/usd_reader_xform.h
intern/usd_skel_convert.h
intern/usd_skel_root_utils.h
)
if(WITH_HYDRA)

View File

@ -0,0 +1,192 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_armature_utils.h"
#include "BKE_armature.hh"
#include "BKE_modifier.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_armature_types.h"
#include "ED_armature.hh"
#include "WM_api.hh"
namespace blender::io::usd {
makowalski marked this conversation as resolved Outdated

For clarity, usually FunctionRef is used instead of std::function for a callback like this (where no persistent storage is necessary). It can even be significantly faster (see 25917f0165), though I doubt it matters here.

For clarity, usually `FunctionRef` is used instead of `std::function` for a callback like this (where no persistent storage is necessary). It can even be significantly faster (see 25917f0165b5feea6f04c6df21bb0fd2003b1042), though I doubt it matters here.
/* Recursively invoke the 'visitor' function on the given bone and its children. */
static void visit_bones(const Bone *bone, FunctionRef<void(const Bone *)> visitor)
makowalski marked this conversation as resolved Outdated

blender::FunctionRef -> FunctionRef

`blender::FunctionRef` -> `FunctionRef`
{
if (!(bone && visitor)) {
return;
}
visitor(bone);
makowalski marked this conversation as resolved
Review

Use LISTBASE_FOREACH

Use `LISTBASE_FOREACH`
LISTBASE_FOREACH (const Bone *, child, &bone->childbase) {
visit_bones(child, visitor);
}
}
const ModifierData *get_enabled_modifier(const Object &obj,
makowalski marked this conversation as resolved Outdated

Best if you can return const ArmatureModifierData * to avoid retrieving a mutable modifier from a const object. Same below.

Best if you can return `const ArmatureModifierData *` to avoid retrieving a mutable modifier from a const object. Same below.
ModifierType type,
const Depsgraph *depsgraph)
{
BLI_assert(depsgraph);
Scene *scene = DEG_get_input_scene(depsgraph);
eEvaluationMode mode = DEG_get_mode(depsgraph);
LISTBASE_FOREACH (ModifierData *, md, &obj.modifiers) {
if (!BKE_modifier_is_enabled(scene, md, mode)) {
continue;
}
if (md->type == type) {
return md;
}
}
return nullptr;
}
/* Return the armature modifier on the given object. Return null if no armature modifier
* can be found. */
static const ArmatureModifierData *get_armature_modifier(const Object &obj,
const Depsgraph *depsgraph)
{
const ArmatureModifierData *mod = reinterpret_cast<const ArmatureModifierData *>(
get_enabled_modifier(obj, eModifierType_Armature, depsgraph));
return mod;
makowalski marked this conversation as resolved Outdated

Any particular reason the namespace starts here instead of at the top of the file?

Any particular reason the namespace starts here instead of at the top of the file?
}
void visit_bones(const Object *ob_arm, FunctionRef<void(const Bone *)> visitor)
makowalski marked this conversation as resolved Outdated

Seems like instead of asserting that the object is non-null, the argument could be a reference const Object &

Seems like instead of asserting that the object is non-null, the argument could be a reference `const Object &`
{
if (!(ob_arm && ob_arm->type == OB_ARMATURE && ob_arm->data)) {
return;
}
bArmature *armature = (bArmature *)ob_arm->data;
LISTBASE_FOREACH (const Bone *, bone, &armature->bonebase) {
makowalski marked this conversation as resolved Outdated

Can use the LISTBASE_FOREACH macro, a bit nicer IMO

Can use the `LISTBASE_FOREACH` macro, a bit nicer IMO
visit_bones(bone, visitor);
}
}
makowalski marked this conversation as resolved
Review

Use Vector instead of std::vector

Use `Vector` instead of `std::vector`
void get_armature_bone_names(const Object *ob_arm,
const bool use_deform,
Vector<std::string> &r_names)
makowalski marked this conversation as resolved Outdated

Based on the places the visitor is used, it seems the null check isn't necessary here.

Based on the places the visitor is used, it seems the null check isn't necessary here.
{
Map<StringRef, const Bone *> deform_map;
makowalski marked this conversation as resolved Outdated

I'm fairly sure this Map is hashing and testing equality of the const char * pointers rather than the actual strings. If the strings are owned outside of the map, I'd suggest using Map<StringRef, const Bone *>. That will ensure the hashing and comparison uses the string, and might be faster too, since it won't have to test string length all the time.

I'm fairly sure this Map is hashing and testing equality of the `const char *` pointers rather than the actual strings. If the strings are owned outside of the map, I'd suggest using `Map<StringRef, const Bone *>`. That will ensure the hashing and comparison uses the string, and might be faster too, since it won't have to test string length all the time.
if (use_deform) {
init_deform_bones_map(ob_arm, &deform_map);
}
auto visitor = [&](const Bone *bone) {
if (use_deform && !deform_map.contains(bone->name)) {
return;
}
r_names.append(bone->name);
};
visit_bones(ob_arm, visitor);
}
pxr::TfToken build_usd_joint_path(const Bone *bone)
{
std::string path(pxr::TfMakeValidIdentifier(bone->name));
const Bone *parent = bone->parent;
while (parent) {
path = pxr::TfMakeValidIdentifier(parent->name) + std::string("/") + path;
parent = parent->parent;
}
return pxr::TfToken(path);
}
void create_pose_joints(pxr::UsdSkelAnimation &skel_anim,
const Object &obj,
const Map<StringRef, const Bone *> *deform_map)
{
BLI_assert(obj.pose);
makowalski marked this conversation as resolved Outdated

These null checks seem overly defensive to me. I'd expect object to not be null here, and skel_anim is a reference, so that's more or less checked at compile time. The consensus I've seen is that checking for null at this level is more likely to hide bugs or confuse different abstraction levels.

Good to see you're using references a fair amount in this file :) Doing that a bit more might help clarify things

These null checks seem overly defensive to me. I'd expect object to not be null here, and skel_anim is a reference, so that's more or less checked at compile time. The consensus I've seen is that checking for null at this level is more likely to hide bugs or confuse different abstraction levels. Good to see you're using references a fair amount in this file :) Doing that a bit more might help clarify things
pxr::VtTokenArray joints;
const bPose *pose = obj.pose;
LISTBASE_FOREACH (const bPoseChannel *, pchan, &pose->chanbase) {
if (pchan->bone) {
if (deform_map && !deform_map->contains(pchan->bone->name)) {
/* If deform_map is passed in, assume we're going deform-only.
* Bones not found in the map should be skipped. */
continue;
}
joints.push_back(build_usd_joint_path(pchan->bone));
}
}
skel_anim.GetJointsAttr().Set(joints);
makowalski marked this conversation as resolved Outdated

Same comment here about the null checks

  • const Object *obj -> const Object &obj
  • const char *name -> const StringRefNull name
Same comment here about the null checks - `const Object *obj` -> `const Object &obj` - `const char *name` -> `const StringRefNull name`
}
const Object *get_armature_modifier_obj(const Object &obj, const Depsgraph *depsgraph)
{
const ArmatureModifierData *mod = get_armature_modifier(obj, depsgraph);
return mod ? mod->object : nullptr;
}
bool is_armature_modifier_bone_name(const Object &obj,
const StringRefNull name,
const Depsgraph *depsgraph)
{
const ArmatureModifierData *arm_mod = get_armature_modifier(obj, depsgraph);
if (!arm_mod || !arm_mod->object || !arm_mod->object->data) {
return false;
}
makowalski marked this conversation as resolved Outdated
Type cast style (https://wiki.blender.org/wiki/Style_Guide/C_Cpp#C.2B.2B_Type_Cast)
bArmature *arm = static_cast<bArmature *>(arm_mod->object->data);
return BKE_armature_find_bone_name(arm, name.c_str());
}
makowalski marked this conversation as resolved Outdated

Vector<ModifierData *> -> Vector<const ModifierData *>

`Vector<ModifierData *>` -> `Vector<const ModifierData *>`
bool can_export_skinned_mesh(const Object &obj, const Depsgraph *depsgraph)
{
return get_enabled_modifier(obj, eModifierType_Armature, depsgraph) != nullptr;
}
void init_deform_bones_map(const Object *obj, Map<StringRef, const Bone *> *deform_map)
{
if (!deform_map) {
return;
}
deform_map->clear();
auto deform_visitor = [&](const Bone *bone) {
if (!bone) {
return;
}
const bool deform = !(bone->flag & BONE_NO_DEFORM);
if (deform) {
deform_map->add(bone->name, bone);
}
makowalski marked this conversation as resolved
Review

This && deform_map is not needed, the function already returns early in case it is null.

This `&& deform_map` is not needed, the function already returns early in case it is null.
};
visit_bones(obj, deform_visitor);
/* Get deform parents */
for (const auto &item : deform_map->items()) {
BLI_assert(item.value);
for (const Bone *parent = item.value->parent; parent; parent = parent->parent) {
deform_map->add(parent->name, parent);
makowalski marked this conversation as resolved Outdated

Would rather assert here, think an entry in the mapping without a valid value would be a bug?

Would rather assert here, think an entry in the mapping without a valid value would be a bug?
}
}
makowalski marked this conversation as resolved
Review

Could be a for loop instead...

Could be a `for` loop instead...
}
} // namespace blender::io::usd

View File

@ -0,0 +1,129 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_modifier_types.h"
#include <pxr/base/tf/token.h>
#include <pxr/usd/usdSkel/animation.h>
#include <functional>
struct Bone;
struct Depsgraph;
struct ModifierData;
struct Object;
struct Scene;
struct USDExportParams;
namespace blender::io::usd {
/**
* Recursively invoke the given function on the given armature object's bones.
* This function is a no-op if the object isn't an armature.
*
* \param ob_arm: The armature object
* \param visitor: The function to invoke on each bone
*/
void visit_bones(const Object *ob_arm, FunctionRef<void(const Bone *)> visitor);
/**
* Return in 'r_names' the names of the given armature object's bones.
*
* \param ob_arm: The armature object
* \param use_deform: If true, use only deform bone names, including their parents, to match
* armature export joint indices
* \param r_names: The returned list of bone names
*/
void get_armature_bone_names(const Object *ob_arm, bool use_deform, Vector<std::string> &r_names);
/**
* Return the USD joint path corresponding to the given bone. For example, for the bone
* "Hand", this function might return the full path "Shoulder/Elbow/Hand" of the joint
* in the hierachy.
*
* \param bone: The bone whose path will be queried.
* \return: The path to the joint
*/
pxr::TfToken build_usd_joint_path(const Bone *bone);
/**
* Sets the USD joint paths as an attribute on the given USD animation,
* where the paths correspond to the bones of the given armature.
*
* \param skel_anim: The animation whose joints attribute will be set
* \param ob_arm: The armature object
* \param deform_map: A pointer to a map associating bone names with
* deform bones and their parents. If the pointer
* is not null, assume only deform bones are to be
* exported and bones not found in this map will be
* skipped
*/
void create_pose_joints(pxr::UsdSkelAnimation &skel_anim,
const Object &obj,
const Map<StringRef, const Bone *> *deform_map);
/**
* Return the modifier of the given type enabled for the given dependency graph's
* evaluation mode (viewport or render).
*
* \param obj: Object to query for the modifier
* \param depsgraph: The dependency graph where the object was evaluated
* \return: The modifier
*/
const ModifierData *get_enabled_modifier(const Object &obj,
ModifierType type,
const Depsgraph *depsgraph);
/**
* If the given object has an enabled armature modifier, return the
* armature object bound to the modifier.
*
* \param: Object to check for the modifier
* \param depsgraph: The dependency graph where the object was evaluated
* \return: The armature object
*/
const Object *get_armature_modifier_obj(const Object &obj, const Depsgraph *depsgraph);
/**
* If the given object has an armature modifier, query whether the given
* name matches the name of a bone on the armature referenced by the modifier.
*
* \param obj: Object to query for the modifier
* \param name: Name to check
* \param depsgraph: The dependency graph where the object was evaluated
* \return: True if the name matches a bone name. Return false if no matching
* bone name is found or if the object does not have an armature modifier
*/
bool is_armature_modifier_bone_name(const Object &obj,
const StringRefNull name,
const Depsgraph *depsgraph);
/**
* Query whether exporting a skinned mesh is supported for the given object.
* Currently, the object can be exported as a skinned mesh if it has an enabled
* armature modifier and no other enabled modifiers.
*
* \param obj: Object to query
* \param depsgraph: The dependency graph where the object was evaluated
* \return: True if skinned mesh export is supported, false otherwise
*/
bool can_export_skinned_mesh(const Object &obj, const Depsgraph *depsgraph);
/**
* Initialize the deform bones map:
* - First: grab all bones marked for deforming and store them.
* - Second: loop the deform bones you found and recursively walk up their parent
* hierarchies, marking those bones as deform as well.
* \param obj: Object to query
* \param deform_map: A pointer to the deform_map to fill with deform bones and
* their parents found on the object
*/
void init_deform_bones_map(const Object *obj, Map<StringRef, const Bone *> *deform_map);
} // namespace blender::io::usd

View File

@ -0,0 +1,541 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_blend_shape_utils.h"
#include "usd_skel_convert.h"
#include "usd.h"
#include <pxr/usd/sdf/namespaceEdit.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdSkel/animMapper.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/blendShape.h>
#include <pxr/usd/usdSkel/cache.h>
#include <pxr/usd/usdSkel/skeletonQuery.h>
#include <pxr/usd/usdSkel/utils.h>
#include "DNA_anim_types.h"
#include "DNA_armature_types.h"
#include "DNA_key_types.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_meta_types.h"
#include "DNA_scene_types.h"
#include "BKE_action.h"
#include "BKE_armature.hh"
#include "BKE_deform.h"
#include "BKE_fcurve.h"
#include "BKE_key.h"
#include "BKE_lib_id.h"
#include "BKE_mesh.hh"
#include "BKE_mesh_runtime.hh"
#include "BKE_modifier.hh"
#include "BKE_object.hh"
#include "BKE_object_deform.h"
#include "BLI_math_vector.h"
#include "BLI_set.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
#include "ED_armature.hh"
#include "ED_keyframing.hh"
#include "ED_mesh.hh"
#include <iostream>
#include <string>
#include <vector>
#include "CLG_log.h"
static CLG_LogRef LOG = {"io.usd"};
namespace usdtokens {
static const pxr::TfToken Anim("Anim", pxr::TfToken::Immortal);
static const pxr::TfToken joint1("joint1", pxr::TfToken::Immortal);
static const pxr::TfToken Skel("Skel", pxr::TfToken::Immortal);
} // namespace usdtokens
namespace {
/* Helper struct to facilitate merging blend shape weights time
* samples from multiple meshes to a single skeleton animation. */
struct BlendShapeMergeInfo {
pxr::VtTokenArray src_blend_shapes;
pxr::UsdAttribute src_weights_attr;
/* Remap blend shape weight array from the
* source order to the destination order. */
pxr::UsdSkelAnimMapper anim_map;
void init_anim_map(const pxr::VtTokenArray &dst_blend_shapes)
{
anim_map = pxr::UsdSkelAnimMapper(src_blend_shapes, dst_blend_shapes);
}
};
/* Helper function to avoid name collisions when merging blend shape names from
* multiple meshes to a single skeleton.
*
makowalski marked this conversation as resolved
Review

Use Set instead of std::set

Use `Set` instead of `std::set`
* Attempt to add the given name to the 'names' set as a unique entry, modifying
* the name with a numerical suffix if necessary, and return the unique name that
* was added to the set. */
std::string add_unique_name(blender::Set<std::string> &names, const std::string &name)
{
std::string unique_name = name;
int suffix = 2;
while (names.contains(unique_name)) {
unique_name = name + std::to_string(suffix++);
}
names.add(unique_name);
return unique_name;
}
} // End anonymous namespace.
namespace blender::io::usd {
pxr::TfToken TempBlendShapeWeightsPrimvarName("temp:weights", pxr::TfToken::Immortal);
void ensure_blend_shape_skeleton(pxr::UsdStageRefPtr stage, pxr::UsdPrim &mesh_prim)
{
if (!stage || !mesh_prim) {
return;
}
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
makowalski marked this conversation as resolved
Review

There is no need to explicitly print the function name in the message, the CLOG_ macros do it automatically.

Same for all other cases below.

There is no need to explicitly print the function name in the message, the `CLOG_` macros do it automatically. Same for all other cases below.
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
pxr::UsdSkelSkeleton skel;
if (!skel_api.GetSkeleton(&skel)) {
pxr::SdfPath skel_path = mesh_prim.GetParent().GetPath().AppendChild(usdtokens::Skel);
skel = pxr::UsdSkelSkeleton::Define(stage, skel_path);
if (!skel) {
CLOG_WARN(&LOG,
"Couldn't find or create skeleton bound to mesh prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
skel_api.CreateSkeletonRel().AddTarget(skel.GetPath());
/* Initialize the skeleton. */
pxr::VtMatrix4dArray bind_transforms(1, pxr::GfMatrix4d(1.0));
pxr::VtMatrix4dArray rest_transforms(1, pxr::GfMatrix4d(1.0));
skel.CreateBindTransformsAttr().Set(bind_transforms);
skel.GetRestTransformsAttr().Set(rest_transforms);
/* Some DCCs seem to require joint names to bind the
* skeleton to blendshapes. */
pxr::VtTokenArray joints({usdtokens::joint1});
skel.CreateJointsAttr().Set(joints);
}
pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).GetPrimvar(
TempBlendShapeWeightsPrimvarName);
if (!temp_weights_attr) {
/* No need to create the animation. */
return;
}
pxr::SdfPath anim_path = skel.GetPath().AppendChild(usdtokens::Anim);
pxr::UsdSkelAnimation anim = pxr::UsdSkelAnimation::Define(stage, anim_path);
if (!anim) {
CLOG_WARN(&LOG, "Couldn't define animation at path %s", anim_path.GetAsString().c_str());
return;
}
pxr::VtTokenArray blendshape_names;
skel_api.GetBlendShapesAttr().Get(&blendshape_names);
anim.CreateBlendShapesAttr().Set(blendshape_names);
std::vector<double> times;
temp_weights_attr.GetTimeSamples(&times);
pxr::UsdAttribute anim_weights_attr = anim.CreateBlendShapeWeightsAttr();
pxr::VtFloatArray weights;
for (const double time : times) {
if (temp_weights_attr.Get(&weights, time)) {
anim_weights_attr.Set(weights, time);
}
}
/* Next, set the animation source on the skeleton. */
skel_api = pxr::UsdSkelBindingAPI::Apply(skel.GetPrim());
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to skeleton prim %s",
skel.GetPath().GetAsString().c_str());
return;
}
if (!skel_api.CreateAnimationSourceRel().AddTarget(pxr::SdfPath(usdtokens::Anim))) {
CLOG_WARN(&LOG,
"Couldn't set animation source on skeleton %s",
skel.GetPath().GetAsString().c_str());
}
makowalski marked this conversation as resolved Outdated

Looking at the !obj check here, seems it's a bit overly defensive. This sort of check should probably happen at a higher level.

Looking at the `!obj` check here, seems it's a bit overly defensive. This sort of check should probably happen at a higher level.
pxr::UsdGeomPrimvarsAPI(mesh_prim).RemovePrimvar(TempBlendShapeWeightsPrimvarName);
}
const Key *get_mesh_shape_key(const Object *obj)
{
BLI_assert(obj);
if (!obj->data || obj->type != OB_MESH) {
return nullptr;
}
const Mesh *mesh = static_cast<const Mesh *>(obj->data);
return mesh->key;
}
bool is_mesh_with_shape_keys(const Object *obj)
{
const Key *key = get_mesh_shape_key(obj);
return key && key->totkey > 0 && key->type == KEY_RELATIVE;
}
void create_blend_shapes(pxr::UsdStageRefPtr stage,
const Object *obj,
const pxr::UsdPrim &mesh_prim)
{
const Key *key = get_mesh_shape_key(obj);
if (!(key && mesh_prim)) {
return;
}
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
printf("WARNING: couldn't apply UsdSkelBindingAPI to prim %s\n",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
pxr::VtTokenArray blendshape_names;
std::vector<pxr::SdfPath> blendshape_paths;
/* Get the basis, which we'll use to calculate offsets. */
KeyBlock *basis_key = static_cast<KeyBlock *>(key->block.first);
if (!basis_key) {
return;
}
int basis_totelem = basis_key->totelem;
makowalski marked this conversation as resolved
Review

Comment style

Comment style
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
if (!kb) {
continue;
}
if (kb == basis_key) {
/* Skip the basis. */
continue;
}
pxr::TfToken name(pxr::TfMakeValidIdentifier(kb->name));
blendshape_names.push_back(name);
pxr::SdfPath path = mesh_prim.GetPath().AppendChild(name);
blendshape_paths.push_back(path);
pxr::UsdSkelBlendShape blendshape = pxr::UsdSkelBlendShape::Define(stage, path);
pxr::UsdAttribute offsets_attr = blendshape.CreateOffsetsAttr();
/* Some applications, like Houdini, don't render blend shapes unless the point
* indices are set, so we always create this attribute, even when every index
* is included. */
pxr::UsdAttribute point_indices_attr = blendshape.CreatePointIndicesAttr();
pxr::VtVec3fArray offsets(kb->totelem);
pxr::VtIntArray indices(kb->totelem);
std::iota(indices.begin(), indices.end(), 0);
const float(*fp)[3] = static_cast<float(*)[3]>(kb->data);
const float(*basis_fp)[3] = static_cast<float(*)[3]>(basis_key->data);
makowalski marked this conversation as resolved
Review

The indices can be filled outside of this loop with std::iota(indices.begin(), indices.end(), 0); That means this loop is only doing one thing

The indices can be filled outside of this loop with `std::iota(indices.begin(), indices.end(), 0);` That means this loop is only doing one thing
for (int i = 0; i < kb->totelem; ++i) {
/* Subtract the key positions from the
* basis positions to get the offsets. */
sub_v3_v3v3(offsets[i].data(), fp[i], basis_fp[i]);
}
offsets_attr.Set(offsets);
point_indices_attr.Set(indices);
}
/* Set the blendshape names and targets on the shape. */
pxr::UsdAttribute blendshape_attr = skel_api.CreateBlendShapesAttr();
blendshape_attr.Set(blendshape_names);
skel_api.CreateBlendShapeTargetsRel().SetTargets(blendshape_paths);
/* Some DCCs seem to require joint indices and weights to
* bind the skeleton for blendshapes, so we we create these
* primvars, if needed. */
if (!skel_api.GetJointIndicesAttr().HasAuthoredValue()) {
pxr::VtArray<int> joint_indices(basis_totelem, 0);
skel_api.CreateJointIndicesPrimvar(false, 1).GetAttr().Set(joint_indices);
}
if (!skel_api.GetJointWeightsAttr().HasAuthoredValue()) {
pxr::VtArray<float> joint_weights(basis_totelem, 1.0f);
skel_api.CreateJointWeightsPrimvar(false, 1).GetAttr().Set(joint_weights);
}
}
pxr::VtFloatArray get_blendshape_weights(const Key *key)
{
BLI_assert(key);
pxr::VtFloatArray weights;
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
if (kb == key->block.first) {
/* Skip the first key, which is the basis. */
continue;
}
weights.push_back(kb->curval);
}
return weights;
}
bool has_animated_mesh_shape_key(const Object *obj)
{
const Key *key = get_mesh_shape_key(obj);
return key && key->totkey > 0 && key->adt != nullptr;
}
pxr::VtTokenArray get_blend_shape_names(const Key *key)
{
KeyBlock *basis_key = static_cast<KeyBlock *>(key->block.first);
if (!basis_key) {
return pxr::VtTokenArray();
}
pxr::VtTokenArray blendshape_names;
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
if (kb == basis_key) {
/* Skip the basis. */
continue;
}
pxr::TfToken name(pxr::TfMakeValidIdentifier(kb->name));
blendshape_names.push_back(name);
makowalski marked this conversation as resolved
Review

LISTBASE_FOREACH won't give you a null item-- linked list items are always non-null

`LISTBASE_FOREACH` won't give you a null item-- linked list items are always non-null
}
return blendshape_names;
}
pxr::VtTokenArray get_blend_shapes_attr_value(const pxr::UsdPrim &mesh_prim)
{
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to blend shape prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return pxr::VtTokenArray();
}
pxr::VtTokenArray blend_shape_names;
if (!skel_api.GetBlendShapesAttr().HasValue()) {
return blend_shape_names;
}
if (!skel_api.GetBlendShapesAttr().Get(&blend_shape_names)) {
CLOG_WARN(&LOG,
"Couldn't get blend shapes attribute value for prim %s",
mesh_prim.GetPath().GetAsString().c_str());
}
return blend_shape_names;
}
void remap_blend_shape_anim(pxr::UsdStageRefPtr stage,
const pxr::SdfPath &skel_path,
const pxr::SdfPathSet &mesh_paths)
{
pxr::UsdSkelSkeleton skel = pxr::UsdSkelSkeleton::Get(stage, skel_path);
if (!skel) {
CLOG_WARN(&LOG, "Couldn't get skeleton from path %s", skel_path.GetAsString().c_str());
return;
}
/* Create the animation. */
pxr::SdfPath anim_path = skel_path.AppendChild(usdtokens::Anim);
const pxr::UsdSkelAnimation anim = pxr::UsdSkelAnimation::Define(stage, anim_path);
if (!anim) {
CLOG_WARN(&LOG, "Couldn't define animation at path %s", anim_path.GetAsString().c_str());
return;
}
Vector<BlendShapeMergeInfo> merge_info;
/* We are merging blend shape names and weights from multiple
* meshes to a single animation. In case of name collisions,
makowalski marked this conversation as resolved Outdated

Like mentioned above, blender::Vector should typically be the standard choice for a growable array in Blender code, mainly for the inline buffer, but also for consistency.

Obviously if a library requires a std::vector argument that doesn't apply though.

(Same with Set below, and Map vs std::map elsewhere in this PR).

Like mentioned above, `blender::Vector` should typically be the standard choice for a growable array in Blender code, mainly for the inline buffer, but also for consistency. Obviously if a library requires a `std::vector` argument that doesn't apply though. (Same with `Set` below, and `Map` vs `std::map` elsewhere in this PR).
* we must generate unique blend shape names for the merged
* result. This set keeps track of the unique names that will
* be combined on the animation. */
Set<std::string> merged_names;
/* Iterate over all the meshes, generate unique blend shape names in case of name
makowalski marked this conversation as resolved Outdated

weighs -> weights

`weighs` -> `weights`
* collisions and set up the information we will need to merge the results. */
for (const pxr::SdfPath &mesh_path : mesh_paths) {
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(mesh_path);
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
mesh_path.GetAsString().c_str());
continue;
}
/* Get the blend shape names for this mesh. */
pxr::UsdAttribute blend_shapes_attr = skel_api.GetBlendShapesAttr();
if (!blend_shapes_attr) {
continue;
}
pxr::VtTokenArray names;
if (!skel_api.GetBlendShapesAttr().Get(&names)) {
continue;
}
/* Ensure the names are unique. */
pxr::VtTokenArray unique_names;
for (pxr::TfToken &name : names) {
std::string unique = add_unique_name(merged_names, name.GetString());
unique_names.push_back(pxr::TfToken(unique));
}
/* Set the unique names back on the mesh. */
skel_api.GetBlendShapesAttr().Set(unique_names);
/* Look up the temporary weights time sample we wrote to the mesh. */
pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).GetPrimvar(
TempBlendShapeWeightsPrimvarName);
if (!temp_weights_attr) {
/* No need to create the animation. Shouldn't usually happen. */
return;
}
/* Generate information we will need to merge the weight samples below. */
merge_info.append(BlendShapeMergeInfo());
merge_info.last().src_blend_shapes = unique_names;
merge_info.last().src_weights_attr = temp_weights_attr;
}
if (merged_names.is_empty()) {
/* No blend shape names were collected. Shouldn't usually happen. */
return;
}
/* Copy the list of name strings to a list of tokens, since we need to work with tokens. */
pxr::VtTokenArray skel_blend_shape_names;
for (const std::string &name : merged_names) {
skel_blend_shape_names.push_back(pxr::TfToken(name));
}
/* Initialize the merge info structs with the list of names on the merged animation. */
for (BlendShapeMergeInfo &info : merge_info) {
info.init_anim_map(skel_blend_shape_names);
}
/* Set the names on the animation prim. */
anim.CreateBlendShapesAttr().Set(skel_blend_shape_names);
pxr::UsdAttribute dst_weights_attr = anim.CreateBlendShapeWeightsAttr();
/* Merge the weight time samples. */
std::vector<double> times;
merge_info.first().src_weights_attr.GetTimeSamples(&times);
if (times.empty()) {
/* Times may be empty if there is only a default value for the weights,
* so we read the default. */
times.push_back(pxr::UsdTimeCode::Default().GetValue());
}
pxr::VtFloatArray dst_weights;
for (const double time : times) {
for (const BlendShapeMergeInfo &info : merge_info) {
pxr::VtFloatArray src_weights;
if (info.src_weights_attr.Get(&src_weights, time)) {
if (!info.anim_map.Remap(src_weights, &dst_weights)) {
CLOG_WARN(&LOG, "Failed remapping blend shape weights");
}
}
}
/* Set the merged weights on the animation. */
dst_weights_attr.Set(dst_weights, time);
}
}
Mesh *get_shape_key_basis_mesh(Object *obj)
{
if (!obj || !obj->data || obj->type != OB_MESH) {
return nullptr;
}
/* If we're exporting blend shapes, we export the unmodified mesh with
* the verts in the basis key positions. */
const Mesh *mesh = BKE_object_get_pre_modified_mesh(obj);
makowalski marked this conversation as resolved Outdated

Should be a CLOG_WARN or so

Should be a `CLOG_WARN` or so
if (!mesh || !mesh->key || !mesh->key->block.first) {
return nullptr;
}
KeyBlock *basis = reinterpret_cast<KeyBlock *>(mesh->key->block.first);
if (mesh->verts_num != basis->totelem) {
makowalski marked this conversation as resolved Outdated

Making this mesh const would clarify the code below.

Making this mesh `const` would clarify the code below.
CLOG_WARN(&LOG, "Vertex and shape key element count mismatch for mesh %s", obj->id.name + 2);
return nullptr;
}
/* Make a copy of the mesh so we can update the verts to the basis shape. */
Mesh *temp_mesh = BKE_mesh_copy_for_eval(mesh);
/* Update the verts. */
BKE_keyblock_convert_to_mesh(
basis,
reinterpret_cast<float(*)[3]>(temp_mesh->vert_positions_for_write().data()),
temp_mesh->verts_num);
makowalski marked this conversation as resolved
Review

BKE_mesh_copy_for_eval is a slightly friendlier looking way of doing thing

`BKE_mesh_copy_for_eval` is a slightly friendlier looking way of doing thing
return temp_mesh;
}
} // namespace blender::io::usd

View File

@ -0,0 +1,135 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <pxr/usd/usd/prim.h>
makowalski marked this conversation as resolved
Review

Maybe these should be the corresponding Blender container headers now?

Maybe these should be the corresponding Blender container headers now?
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/tokens.h>
struct Key;
struct Main;
struct Mesh;
struct Object;
struct Scene;
struct USDExportParams;
struct USDImportParams;
namespace blender::io::usd {
/* Name of the temporary USD primvar for storing blend shape
* weight time samples on the mesh before they are copied
* to the bound skeleton. */
extern pxr::TfToken TempBlendShapeWeightsPrimvarName;
struct ImportSettings;
/**
* Return the shape key on the given mesh object.
*
* \param obj: The mesh object
* \return: The shape key on the given object's mesh data, or
* null if the object isn't a mesh.
*/
const Key *get_mesh_shape_key(const Object *obj);
/**
* Query whether the given object is a mesh with relative
* shape keys.
*
* \param obj: The mesh object
* \return: True if the object is a mesh with shape keys, false
* otherwise
*/
bool is_mesh_with_shape_keys(const Object *obj);
/**
* Convert shape keys on the given object to USD blend shapes. The blend shapes
* will be added to the stage as children of the given USD mesh prim. The blendshape
* names and targets will also be set as properites on the prim.
*
* \param stage: The stage
* \param obj: The mesh object whose shape keys will be converted to blend shapes
* \param mesh_prim: The USD mesh that will be assigned the blend shape targets
*/
void create_blend_shapes(pxr::UsdStageRefPtr stage,
const Object *obj,
const pxr::UsdPrim &mesh_prim);
/**
* Return the current weight values of the given key.
*
* \param key: The key whose values will be queried
* \return: The array of key values
*/
pxr::VtFloatArray get_blendshape_weights(const Key *key);
/**
* USD implementations expect that a mesh with blend shape targets
* be bound to a skeleton with an animation that provides the blend
* shape weights. If the given mesh is not already bound to a skeleton
* this function will create a dummy skeleton with a single joint and
* will bind it to the mesh. This is typically required if the source
* Blender mesh has shape keys but not an armature deformer.
*
* This function will also create a skel animation prim as a child of
* the skeleton and will copy the weight time samples from a temporary
* primvar on the mesh to the animation prim.
*
* \param stage: The stage
* \param mesh_prim: The USD mesh to which the skeleton will be bound
*/
void ensure_blend_shape_skeleton(pxr::UsdStageRefPtr stage, pxr::UsdPrim &mesh_prim);
/**
* Query whether the object is a mesh with animated shape keys.
*
* \param obj: The mesh object
* \return: True if the object has animated keys, false otherwise
*/
bool has_animated_mesh_shape_key(const Object *obj);
/**
* Return the block names of the given shape key.
*
* \param key: The key to query
* \return: The list of key block names
*/
pxr::VtTokenArray get_blend_shape_names(const Key *key);
/**
* Return the list of blend shape names given by the mesh
* prim's 'blendShapes' attribute value.
*
* \param mesh_prim: The prim to query
* \return: The list of blend shape names
*/
pxr::VtTokenArray get_blend_shapes_attr_value(const pxr::UsdPrim &mesh_prim);
/**
* When multiple meshes with blend shape animations are bound to one skeleton, USD implementations
* typically expect these animations to be combined in a single animation on the skeleton. This
* function creates an animation prim as a child of the skeleton and merges the blend shape time
* samples from multiple meshes in a single attribute on the animation. Merging the weight samples
* requires handling blend shape name collisions by generating unique names for the combined
* result.
*
* \param stage: The stage
* \param skel_path: Path to the skeleton
* \param mesh_paths: Paths to one or more mesh prims bound to the skeleton
*/
void remap_blend_shape_anim(pxr::UsdStageRefPtr stage,
const pxr::SdfPath &skel_path,
const pxr::SdfPathSet &mesh_paths);
/**
* If the given object is a mesh with shape keys, return a copy of the object's pre-modified mesh
* with its verts in the shape key basis positions. The returned mesh must be freed by the caller.
*
* \param obj: The mesh object with shape keys
* \return: A new mesh corresponding to the shape key basis shape, or null if the object
* isn't a mesh or has no shape keys
*/
Mesh *get_shape_key_basis_mesh(Object *obj);
} // namespace blender::io::usd

View File

@ -297,6 +297,10 @@ pxr::UsdStageRefPtr export_to_stage(const USDExportParams &params,
iter.release_writers();
if (params.export_shapekeys || params.export_armatures) {
iter.process_usd_skel();
}
/* Set the default prim if it doesn't exist */
if (!usd_stage->GetDefaultPrim()) {
/* Use TraverseAll since it's guaranteed to be depth first and will get the first top level

View File

@ -23,4 +23,11 @@ template<> struct DefaultHash<pxr::TfToken> {
return value.Hash();
}
};
template<> struct DefaultHash<pxr::SdfPath> {
uint64_t operator()(const pxr::SdfPath &value) const
{
return (uint64_t)value.GetHash();
}
};
} // namespace blender

View File

@ -3,8 +3,13 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd.h"
#include "usd_armature_utils.h"
#include "usd_blend_shape_utils.h"
#include "usd_hierarchy_iterator.h"
#include "usd_skel_convert.h"
#include "usd_skel_root_utils.h"
#include "usd_writer_abstract.h"
#include "usd_writer_armature.h"
#include "usd_writer_camera.h"
#include "usd_writer_curves.h"
#include "usd_writer_hair.h"
@ -30,6 +35,9 @@
#include "DNA_layer_types.h"
#include "DNA_object_types.h"
#include "DNA_armature_types.h"
#include "DNA_mesh_types.h"
namespace blender::io::usd {
USDHierarchyIterator::USDHierarchyIterator(Main *bmain,
@ -58,6 +66,17 @@ std::string USDHierarchyIterator::make_valid_name(const std::string &name) const
return pxr::TfMakeValidIdentifier(name);
}
void USDHierarchyIterator::process_usd_skel() const
{
skel_export_chaser(stage_,
armature_export_map_,
skinned_mesh_export_map_,
shape_key_mesh_export_map_,
depsgraph_);
create_skel_roots(stage_, params_);
}
void USDHierarchyIterator::set_export_frame(float frame_nr)
{
/* The USD stage is already set up to have FPS time-codes per frame. */
@ -115,13 +134,20 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
case OB_VOLUME:
data_writer = new USDVolumeWriter(usd_export_context);
break;
case OB_ARMATURE:
if (usd_export_context.export_params.export_armatures) {
data_writer = new USDArmatureWriter(usd_export_context);
}
else {
return nullptr;
}
break;
case OB_EMPTY:
case OB_SURF:
case OB_FONT:
case OB_SPEAKER:
case OB_LIGHTPROBE:
case OB_LATTICE:
case OB_ARMATURE:
case OB_GPENCIL_LEGACY:
case OB_GREASE_PENCIL:
case OB_POINTCLOUD:
@ -139,6 +165,10 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
return nullptr;
}
if (data_writer && (params_.export_armatures || params_.export_shapekeys)) {
add_usd_skel_export_mapping(context->object, data_writer->usd_path());
}
return data_writer;
}
@ -156,4 +186,20 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_particle_writer(
return nullptr;
}
void USDHierarchyIterator::add_usd_skel_export_mapping(const Object *obj, const pxr::SdfPath &path)
{
if (params_.export_shapekeys && is_mesh_with_shape_keys(obj)) {
shape_key_mesh_export_map_.add(obj, path);
}
if (params_.export_armatures && obj->type == OB_ARMATURE) {
armature_export_map_.add(obj, path);
}
if (params_.export_armatures && obj->type == OB_MESH &&
can_export_skinned_mesh(*obj, depsgraph_)) {
skinned_mesh_export_map_.add(obj, path);
}
}
} // namespace blender::io::usd

View File

@ -6,6 +6,7 @@
#include "IO_abstract_hierarchy_iterator.h"
#include "usd.h"
#include "usd_exporter_context.h"
#include "usd_skel_convert.h"
#include <string>
makowalski marked this conversation as resolved Outdated

Different headers here too?

Different headers here too?
@ -28,6 +29,10 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
pxr::UsdTimeCode export_time_;
const USDExportParams &params_;
ObjExportMap armature_export_map_;
ObjExportMap skinned_mesh_export_map_;
ObjExportMap shape_key_mesh_export_map_;
public:
USDHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
@ -38,6 +43,8 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
virtual std::string make_valid_name(const std::string &name) const override;
void process_usd_skel() const;
protected:
virtual bool mark_as_weak_export(const Object *object) const override;
@ -52,6 +59,8 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
private:
USDExporterContext create_usd_export_context(const HierarchyContext *context);
void add_usd_skel_export_mapping(const Object *obj, const pxr::SdfPath &usd_path);
};
} // namespace blender::io::usd

View File

@ -5,7 +5,12 @@
#include "usd_skel_convert.h"
#include "usd.h"
#include "usd_armature_utils.h"
#include "usd_blend_shape_utils.h"
#include "usd_hash_types.h"
#include <pxr/usd/sdf/namespaceEdit.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/blendShape.h>
@ -16,16 +21,18 @@
#include "DNA_anim_types.h"
#include "DNA_armature_types.h"
#include "DNA_key_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_meta_types.h"
#include "DNA_scene_types.h"
#include "BKE_action.h"
#include "BKE_armature.hh"
#include "BKE_attribute.hh"
#include "BKE_deform.h"
#include "BKE_fcurve.h"
#include "BKE_key.h"
#include "BKE_lib_id.h"
#include "BKE_mesh.h"
#include "BKE_mesh.hh"
#include "BKE_mesh_runtime.hh"
#include "BKE_modifier.hh"
#include "BKE_object.hh"
@ -33,6 +40,7 @@
#include "BKE_report.h"
#include "BLI_math_vector.h"
#include "BLI_span.hh"
#include "BLI_string.h"
#include "ED_armature.hh"
@ -42,12 +50,13 @@
#include "ANIM_animdata.hh"
#include "ANIM_fcurve.hh"
#include "WM_api.hh"
#include <iostream>
#include <string>
#include <vector>
#include "CLG_log.h"
static CLG_LogRef LOG = {"io.usd"};
namespace {
/* Utility: return the magnitude of the largest component
@ -334,6 +343,39 @@ void import_skeleton_curves(Main *bmain,
std::for_each(scale_curves.begin(), scale_curves.end(), recalc_handles);
}
/* Set the skeleton path and bind transform on the given mesh. */
void add_skinned_mesh_bindings(const pxr::UsdSkelSkeleton &skel,
pxr::UsdPrim &mesh_prim,
pxr::UsdGeomXformCache &xf_cache)
{
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to skinned mesh prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
/* Specify the path to the skeleton. */
pxr::SdfPath skel_path = skel.GetPath();
skel_api.CreateSkeletonRel().SetTargets(pxr::SdfPathVector({skel_path}));
/* Set the mesh's bind transform. */
if (pxr::UsdAttribute geom_bind_attr = skel_api.CreateGeomBindTransformAttr()) {
/* The bind matrix is the mesh transform relative to the skeleton transform. */
pxr::GfMatrix4d mesh_xf = xf_cache.GetLocalToWorldTransform(mesh_prim);
pxr::GfMatrix4d skel_xf = xf_cache.GetLocalToWorldTransform(skel.GetPrim());
pxr::GfMatrix4d bind_xf = mesh_xf * skel_xf.GetInverse();
geom_bind_attr.Set(bind_xf);
}
else {
CLOG_WARN(&LOG,
"Couldn't create geom bind transform attribute for skinned mesh %s",
mesh_prim.GetPath().GetAsString().c_str());
}
}
} // End anonymous namespace.
namespace blender::io::usd {
@ -1065,4 +1107,220 @@ void import_mesh_skel_bindings(Main *bmain,
}
}
void skel_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &armature_export_map,
const ObjExportMap &skinned_mesh_export_map,
const ObjExportMap &shape_key_mesh_export_map,
const Depsgraph *depsgraph)
{
/* We may need to compute the world transforms of certain prims when
* setting skinning data. Using a shared transform cache can make computing
* the transforms more efficient. */
pxr::UsdGeomXformCache xf_cache(1.0);
skinned_mesh_export_chaser(
stage, armature_export_map, skinned_mesh_export_map, xf_cache, depsgraph);
shape_key_export_chaser(stage, shape_key_mesh_export_map);
}
void skinned_mesh_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &armature_export_map,
const ObjExportMap &skinned_mesh_export_map,
pxr::UsdGeomXformCache &xf_cache,
const Depsgraph *depsgraph)
{
/* Finish creating skinned mesh bindings. */
for (const auto &item : skinned_mesh_export_map.items()) {
const Object *mesh_obj = item.key;
const pxr::SdfPath &mesh_path = item.value;
/* Get the mesh prim from the stage. */
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(mesh_path);
if (!mesh_prim) {
CLOG_WARN(&LOG,
"Invalid export map prim path %s for mesh object %s",
mesh_path.GetAsString().c_str(),
mesh_obj->id.name + 2);
continue;
}
/* Get the armature bound to the mesh's armature modifier. */
const Object *arm_obj = get_armature_modifier_obj(*mesh_obj, depsgraph);
if (!arm_obj) {
CLOG_WARN(&LOG, "Invalid armature modifier for skinned mesh %s", mesh_obj->id.name + 2);
continue;
}
/* Look up the USD skeleton correpsoning to the armature object. */
const pxr::SdfPath *path = armature_export_map.lookup_ptr(arm_obj);
if (!path) {
CLOG_WARN(&LOG, "No export map entry for armature object %s", mesh_obj->id.name + 2);
continue;
}
/* Get the skeleton prim. */
pxr::UsdPrim skel_prim = stage->GetPrimAtPath(*path);
pxr::UsdSkelSkeleton skel(skel_prim);
if (!skel) {
CLOG_WARN(&LOG, "Invalid USD skeleton for armature object %s", arm_obj->id.name + 2);
continue;
}
add_skinned_mesh_bindings(skel, mesh_prim, xf_cache);
}
}
void shape_key_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &shape_key_mesh_export_map)
{
Map<pxr::SdfPath, pxr::SdfPathSet> skel_to_mesh;
/* We will keep track of the mesh prims to clean up the temporary
* weights attribute at the end. */
Vector<pxr::UsdPrim> mesh_prims;
makowalski marked this conversation as resolved Outdated

std::map to Map here too, might as well do this consistently. Let me know if you have questions about the API of Map. Overall it should hopefully be more intuitive than std::map though

`std::map` to `Map` here too, might as well do this consistently. Let me know if you have questions about the API of `Map`. Overall it should hopefully be more intuitive than `std::map` though
/* Finish creating blend shape bindings. */
for (const auto &item : shape_key_mesh_export_map.items()) {
const Object *mesh_obj = item.key;
const pxr::SdfPath &mesh_path = item.value;
/* Get the mesh prim from the stage. */
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(mesh_path);
if (!mesh_prim) {
CLOG_WARN(&LOG,
"Invalid export map prim path %s for mesh object %s",
mesh_path.GetAsString().c_str(),
mesh_obj->id.name + 2);
continue;
}
/* Keep track of all the mesh prims with blend shapes, for cleanup below. */
mesh_prims.append(mesh_prim);
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
pxr::UsdSkelSkeleton skel;
if (skel_api.GetSkeleton(&skel)) {
/* We have a bound skeleton, so we add it to the map. */
pxr::SdfPathSet *mesh_paths = skel_to_mesh.lookup_ptr(skel.GetPath());
if (!mesh_paths) {
skel_to_mesh.add_new(skel.GetPath(), pxr::SdfPathSet());
mesh_paths = skel_to_mesh.lookup_ptr(skel.GetPath());
}
if (mesh_paths) {
mesh_paths->insert(mesh_prim.GetPath());
}
continue;
}
/* The mesh is not bound to a skeleton, so we must create one for it. */
ensure_blend_shape_skeleton(stage, mesh_prim);
}
if (skel_to_mesh.is_empty()) {
return;
}
for (const auto &item : skel_to_mesh.items()) {
remap_blend_shape_anim(stage, item.key, item.value);
}
/* Finally, delete the temp blendshape weights attributes. */
for (pxr::UsdPrim &prim : mesh_prims) {
pxr::UsdGeomPrimvarsAPI(prim).RemovePrimvar(TempBlendShapeWeightsPrimvarName);
}
}
void export_deform_verts(const Mesh *mesh,
const pxr::UsdSkelBindingAPI &skel_api,
const Vector<std::string> &bone_names)
{
BLI_assert(mesh);
BLI_assert(skel_api);
makowalski marked this conversation as resolved Outdated

Same comment here about defensive null checks

Same comment here about defensive null checks
/* Map a deform vertex group index to the
* index of the corresponding joint. I.e.,
* joint_index[n] is the joint index of the
* n-th vertex group. */
Vector<int> joint_index;
/* Build the index mapping. */
LISTBASE_FOREACH (const bDeformGroup *, def, &mesh->vertex_group_names) {
int bone_idx = -1;
/* For now, n-squared search is acceptable. */
for (int i = 0; i < bone_names.size(); ++i) {
makowalski marked this conversation as resolved Outdated

LISTBASE_FOREACH

`LISTBASE_FOREACH`
if (bone_names[i] == def->name) {
bone_idx = i;
break;
}
}
joint_index.append(bone_idx);
}
if (joint_index.is_empty()) {
return;
}
const Span<MDeformVert> dverts = mesh->deform_verts();
int max_totweight = 1;
for (const int i : dverts.index_range()) {
const MDeformVert &vert = dverts[i];
if (vert.totweight > max_totweight) {
makowalski marked this conversation as resolved Outdated

Since this code is already in the blender:: namespace, specifying it here for Span is unnecessary.

Since this code is already in the `blender::` namespace, specifying it here for `Span` is unnecessary.
max_totweight = vert.totweight;
}
}
/* elem_size will specify the number of
* joints that can influence a given point. */
const int element_size = max_totweight;
int num_points = mesh->verts_num;
pxr::VtArray<int> joint_indices(num_points * element_size, 0);
pxr::VtArray<float> joint_weights(num_points * element_size, 0.0f);
/* Current offset into the indices and weights arrays. */
int offset = 0;
for (const int i : dverts.index_range()) {
const MDeformVert &vert = dverts[i];
for (int j = 0; j < element_size; ++j, ++offset) {
if (offset >= joint_indices.size()) {
BLI_assert_unreachable();
return;
}
HooglyBoogly marked this conversation as resolved
Review

This looks like an expensive loop relative to the others. Maybe worth parallelizing with threading::parallel_for?

This looks like an expensive loop relative to the others. Maybe worth parallelizing with `threading::parallel_for`?
Review

This is definitely something to consider. However, I'm thinking that perhaps such an optimization could be done in a new follow-up PR, to simplify debugging and testing initially. Would that be okay?

This is definitely something to consider. However, I'm thinking that perhaps such an optimization could be done in a new follow-up PR, to simplify debugging and testing initially. Would that be okay?
if (j >= vert.totweight) {
continue;
}
int def_nr = static_cast<int>(vert.dw[j].def_nr);
if (def_nr >= joint_index.size()) {
BLI_assert_unreachable();
makowalski marked this conversation as resolved Outdated

Usually an assert (BLI_assert) is a more useful way to check for this sort of thing, since execution will stop in debug builds (if this really is a programmer error).

Usually an assert (`BLI_assert`) is a more useful way to check for this sort of thing, since execution will stop in debug builds (if this really is a programmer error).
continue;
}
if (joint_index[def_nr] == -1) {
continue;
}
joint_indices[offset] = joint_index[def_nr];
joint_weights[offset] = vert.dw[j].weight;
}
}
makowalski marked this conversation as resolved
Review

Object.defbase probably refers to long ago when the names were stored on the object. Nowadays the names are stored on the mesh.

`Object.defbase` probably refers to long ago when the names were stored on the object. Nowadays the names are stored on the mesh.
pxr::UsdSkelNormalizeWeights(joint_weights, element_size);
skel_api.CreateJointIndicesPrimvar(false, element_size).GetAttr().Set(joint_indices);
skel_api.CreateJointWeightsPrimvar(false, element_size).GetAttr().Set(joint_weights);
}
} // namespace blender::io::usd

View File

@ -3,16 +3,22 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_map.hh"
#include "BLI_vector.hh"
#include "DNA_windowmanager_types.h"
#include <map>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/skeletonQuery.h>
struct Depsgraph;
struct Key;
struct Main;
struct Mesh;
struct Object;
struct Scene;
struct USDExportParams;
struct USDImportParams;
namespace blender::io::usd {
@ -81,4 +87,63 @@ void import_mesh_skel_bindings(Main *bmain,
const pxr::UsdPrim &prim,
ReportList *reports);
/**
* Map an object to its USD prim export path.
*/
using ObjExportMap = Map<const Object *, pxr::SdfPath>;
/**
* This function is called after the USD writers are invoked, to
* complete the UsdSkel export process, for example, to bind skinned
* meshes to skeletons or to set blend shape animation data.
*
* \param stage: The stage
* \param armature_export_map: Map armature objects to USD skeletons
* \param skinned_mesh_export_map: Map mesh objects to USD skinned meshes
* \param shape_key_export_map: Map mesh objects with shape keye to USD meshes
* with blend shape targets
* \param depsgraph: The dependency graph in which objects were evaluated
*/
void skel_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &armature_export_map,
const ObjExportMap &skinned_mesh_export_map,
const ObjExportMap &shape_key_mesh_export_map,
const Depsgraph *depsgraph);
/**
* Complete the export process for skinned meshes.
*
* \param stage: The stage
* \param armature_export_map: Map armature objects to USD skeleton paths
* \param skinned_mesh_export_map: Map mesh objects to USD skinned meshes
* \param xf_cache: Cache to speed up USD prim transform computations
* \param depsgraph: The dependency graph in which objects were evaluated
*/
void skinned_mesh_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &armature_export_map,
const ObjExportMap &skinned_mesh_export_map,
pxr::UsdGeomXformCache &xf_cache,
const Depsgraph *depsgraph);
/**
* Complete the export process for shape keys.
*
* \param stage: The stage
* \param shape_key_export_map: Map mesh objects with shape keye to USD meshes
* with blend shape targets
*/
void shape_key_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &shape_key_mesh_export_map);
/**
* Convert deform groups on the given mesh to USD joint index and weight attributes.
*
* \param stage: The source mesh with deform groups to export
* \param skel_api: API for setting the attributes on the USD prim
* \param bone_names: List of armature bone names corresponding to the deform groups
*/
void export_deform_verts(const Mesh *mesh,
const pxr::UsdSkelBindingAPI &skel_api,
const Vector<std::string> &bone_names);
} // namespace blender::io::usd

View File

@ -0,0 +1,143 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_skel_root_utils.h"
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/root.h>
#include "BKE_report.h"
#include "WM_types.hh"
#include <iostream>
#include "CLG_log.h"
static CLG_LogRef LOG = {"io.usd"};
/* Utility: return the common Xform ancestor of the given prims. Is no such ancestor can
* be found, return an in valid Xform. */
static pxr::UsdGeomXform get_xform_ancestor(const pxr::UsdPrim &prim1, const pxr::UsdPrim &prim2)
{
if (!prim1 || !prim2) {
return pxr::UsdGeomXform();
}
pxr::SdfPath prefix = prim1.GetPath().GetCommonPrefix(prim2.GetPath());
if (prefix.IsEmpty()) {
return pxr::UsdGeomXform();
}
pxr::UsdPrim ancestor = prim1.GetStage()->GetPrimAtPath(prefix);
if (!ancestor) {
return pxr::UsdGeomXform();
}
while (ancestor && !ancestor.IsA<pxr::UsdGeomXform>()) {
ancestor = ancestor.GetParent();
}
if (ancestor && ancestor.IsA<pxr::UsdGeomXform>()) {
return pxr::UsdGeomXform(ancestor);
}
return pxr::UsdGeomXform();
}
namespace blender::io::usd {
void create_skel_roots(pxr::UsdStageRefPtr stage, const USDExportParams &params)
{
if (!stage || !(params.export_armatures || params.export_shapekeys)) {
return;
}
ReportList *reports = params.worker_status ? params.worker_status->reports : nullptr;
/* Whether we converted any prims to UsdSkel. */
bool converted_to_usdskel = false;
pxr::UsdPrimRange it = stage->Traverse();
for (pxr::UsdPrim prim : it) {
if (!prim) {
continue;
}
if (prim.IsA<pxr::UsdSkelSkeleton>() || !prim.HasAPI<pxr::UsdSkelBindingAPI>()) {
continue;
}
pxr::UsdSkelBindingAPI skel_bind_api(prim);
if (!skel_bind_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to prim %s",
prim.GetPath().GetAsString().c_str());
continue;
}
/* If we got here, then this prim has the skel binding API. */
/* Get this prim's bound skeleton. */
pxr::UsdSkelSkeleton skel;
if (!skel_bind_api.GetSkeleton(&skel)) {
continue;
}
if (!skel.GetPrim().IsValid()) {
CLOG_WARN(&LOG, "Invalid skeleton for prim %s", prim.GetPath().GetAsString().c_str());
continue;
}
/* Try to find a commmon ancestor of the skinned prim and its bound skeleton. */
pxr::UsdSkelRoot prim_skel_root = pxr::UsdSkelRoot::Find(prim);
pxr::UsdSkelRoot skel_skel_root = pxr::UsdSkelRoot::Find(skel.GetPrim());
if (prim_skel_root && skel_skel_root && prim_skel_root.GetPath() == skel_skel_root.GetPath()) {
continue;
}
if (pxr::UsdGeomXform xf = get_xform_ancestor(prim, skel.GetPrim())) {
/* We found a common Xform ancestor, so we set its type to UsdSkelRoot. */
CLOG_INFO(
&LOG, 4, "Converting Xform prim %s to a SkelRoot", prim.GetPath().GetAsString().c_str());
pxr::UsdSkelRoot::Define(stage, xf.GetPath());
converted_to_usdskel = true;
makowalski marked this conversation as resolved Outdated

Why BKE_report? This looks like it should be a CLOG_INFO instead?

Why `BKE_report`? This looks like it should be a `CLOG_INFO` instead?
}
else {
BKE_reportf(reports,
RPT_WARNING,
"%s: Couldn't find a common Xform ancestor for skinned prim %s "
"and skeleton %s to convert to a USD SkelRoot. "
"This can be addressed by setting a root primitive in the export options.\n",
__func__,
prim.GetPath().GetAsString().c_str(),
skel.GetPath().GetAsString().c_str());
}
}
if (!converted_to_usdskel) {
return;
}
/* Check for nested SkelRoots, i.e., SkelRoots beneath other SkelRoots, which we want to avoid.
*/
it = stage->Traverse();
for (pxr::UsdPrim prim : it) {
if (prim.IsA<pxr::UsdSkelRoot>()) {
if (pxr::UsdSkelRoot root = pxr::UsdSkelRoot::Find(prim.GetParent())) {
/* This is a nested SkelRoot, so convert it to an Xform. */
pxr::UsdGeomXform::Define(stage, prim.GetPath());
}
}
}
}
} // namespace blender::io::usd

View File

@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.h"
#include <pxr/usd/usd/stage.h>
namespace blender::io::usd {
/**
* We must structure the scene graph to encapsulate skinned prim under a UsdSkelRoot
* prim. Per the USD documentation, a SkelRoot is a
*
* "Boundable prim type used to identify a scope beneath which skeletally-posed primitives are
* defined. A SkelRoot must be defined at or above a skinned primitive for any skinning behaviors
* in UsdSkel."
*
* See: https://openusd.org/23.05/api/class_usd_skel_root.html#details
*
* This function attempts to ensure that skinned prims and skeletons are encapsulated
* under SkelRoots, converting existing Xform primitives to SkelRoots to achieve this,
* if possible. In the case where no common ancestor which can be converted to a SkelRoot
* is found, this function issues a warning. One way to address such a case is by setting a
* root prim in the export options, so that this root prim can be converted to a SkelRoot
* for the entire scene.
*
* \param stage: The stage
* \param params: The export parameters
*/
void create_skel_roots(pxr::UsdStageRefPtr stage, const USDExportParams &params);
} // namespace blender::io::usd

View File

@ -0,0 +1,201 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_armature.h"
#include "usd_armature_utils.h"
#include "usd_hierarchy_iterator.h"
#include "usd_writer_transform.h"
#include "BKE_action.h"
#include "BKE_armature.hh"
#include "DNA_armature_types.h"
#include "ED_armature.hh"
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/skeleton.h>
#include <pxr/usd/usdSkel/tokens.h>
#include <functional>
#include <iostream>
#include "CLG_log.h"
static CLG_LogRef LOG = {"io.usd"};
namespace usdtokens {
static const pxr::TfToken Anim("Anim", pxr::TfToken::Immortal);
} // namespace usdtokens
/* Get the pose matrix for the given channe. The matrix is computed
* relative to its parent, if a parent exists. The returned matrix
* corresponds to the USD joint-local transform. */
static pxr::GfMatrix4d parent_relative_pose_mat(const bPoseChannel *pchan)
{
/* Note that the float matrix will be returned as GfMatrix4d, because
* USD requires doubles. */
const pxr::GfMatrix4f pose_mat(pchan->pose_mat);
if (pchan->parent) {
const pxr::GfMatrix4f parent_pose_mat(pchan->parent->pose_mat);
const pxr::GfMatrix4f xf = pose_mat * parent_pose_mat.GetInverse();
return pxr::GfMatrix4d(xf);
}
/* No parent, so return the pose matrix directly. */
return pxr::GfMatrix4d(pose_mat);
}
/* Initialize the given skeleton and animation from
* the given armature object. */
static void initialize(const Object *obj,
pxr::UsdSkelSkeleton &skel,
pxr::UsdSkelAnimation &skel_anim,
const blender::Map<blender::StringRef, const Bone *> *deform_bones)
{
using namespace blender::io::usd;
pxr::VtTokenArray joints;
pxr::VtArray<pxr::GfMatrix4d> bind_xforms;
pxr::VtArray<pxr::GfMatrix4d> rest_xforms;
/* Function to collect the bind and rest transforms from each bone. */
auto visitor = [&](const Bone *bone) {
if (!bone) {
return;
}
if (deform_bones && !deform_bones->contains(bone->name)) {
/* If deform_map is passed in, assume we're going deform-only.
* Bones not found in the map should be skipped. */
return;
}
joints.push_back(build_usd_joint_path(bone));
const pxr::GfMatrix4f arm_mat(bone->arm_mat);
bind_xforms.push_back(pxr::GfMatrix4d(arm_mat));
/* Set the rest transform to the parent-relative pose matrix, or the parent-relative
* armature matrix, if no pose channel exists. */
if (const bPoseChannel *pchan = BKE_pose_channel_find_name(obj->pose, bone->name)) {
rest_xforms.push_back(parent_relative_pose_mat(pchan));
}
else if (bone->parent) {
pxr::GfMatrix4f parent_arm_mat(bone->parent->arm_mat);
const pxr::GfMatrix4f rest_mat = arm_mat * parent_arm_mat.GetInverse();
rest_xforms.push_back(pxr::GfMatrix4d(rest_mat));
}
else {
rest_xforms.push_back(pxr::GfMatrix4d(arm_mat));
}
};
visit_bones(obj, visitor);
skel.GetJointsAttr().Set(joints);
skel.GetBindTransformsAttr().Set(bind_xforms);
skel.GetRestTransformsAttr().Set(rest_xforms);
pxr::UsdSkelBindingAPI usd_skel_api = pxr::UsdSkelBindingAPI::Apply(skel.GetPrim());
if (skel_anim) {
usd_skel_api.CreateAnimationSourceRel().SetTargets(
pxr::SdfPathVector({pxr::SdfPath(usdtokens::Anim)}));
create_pose_joints(skel_anim, *obj, deform_bones);
}
}
/* Add skeleton transform samples from the armature pose channels. */
static void add_anim_sample(pxr::UsdSkelAnimation &skel_anim,
const Object *obj,
const pxr::UsdTimeCode time,
const blender::Map<blender::StringRef, const Bone *> *deform_map)
{
makowalski marked this conversation as resolved
Review

Should use CLOG_WARN instead.

Actually, have you ever run into such a case? I think this could be an assert instead.

Should use `CLOG_WARN` instead. Actually, have you ever run into such a case? I think this could be an assert instead.
if (!(skel_anim && obj && obj->pose)) {
return;
}
pxr::VtArray<pxr::GfMatrix4d> xforms;
const bPose *pose = obj->pose;
LISTBASE_FOREACH (const bPoseChannel *, pchan, &pose->chanbase) {
BLI_assert(pchan->bone);
if (deform_map && !deform_map->contains(pchan->bone->name)) {
/* If deform_map is passed in, assume we're going deform-only.
* Bones not found in the map should be skipped. */
continue;
}
xforms.push_back(parent_relative_pose_mat(pchan));
}
skel_anim.SetTransforms(xforms, time);
}
namespace blender::io::usd {
USDArmatureWriter::USDArmatureWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
void USDArmatureWriter::do_write(HierarchyContext &context)
{
if (!(context.object && context.object->type == OB_ARMATURE && context.object->data)) {
BLI_assert_unreachable();
return;
}
/* Create the skeleton. */
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdSkelSkeleton skel = pxr::UsdSkelSkeleton::Define(stage, usd_export_context_.usd_path);
if (!skel) {
CLOG_WARN(&LOG,
"Couldn't define UsdSkelSkeleton %s",
usd_export_context_.usd_path.GetString().c_str());
return;
}
pxr::UsdSkelAnimation skel_anim;
if (usd_export_context_.export_params.export_animation) {
/* Create the skeleton animation primitive as a child of the skeleton. */
pxr::SdfPath anim_path = usd_export_context_.usd_path.AppendChild(usdtokens::Anim);
skel_anim = pxr::UsdSkelAnimation::Define(stage, anim_path);
if (!skel_anim) {
CLOG_WARN(&LOG, "Couldn't define UsdSkelAnimation %s", anim_path.GetString().c_str());
return;
}
}
Map<StringRef, const Bone *> *deform_map = usd_export_context_.export_params.only_deform_bones ?
makowalski marked this conversation as resolved Outdated

Why naming the mapping use_deform, instead of deform_map? This is fairly confusing imho...

Why naming the mapping `use_deform`, instead of `deform_map`? This is fairly confusing imho...
&deform_map_ :
nullptr;
if (!this->frame_has_been_written_) {
init_deform_bones_map(context.object, deform_map);
initialize(context.object, skel, skel_anim, deform_map);
}
if (usd_export_context_.export_params.export_animation) {
add_anim_sample(skel_anim, context.object, get_export_time_code(), deform_map);
}
}
bool USDArmatureWriter::check_is_animated(const HierarchyContext &context) const
{
const Object *obj = context.object;
if (!(obj && obj->type == OB_ARMATURE)) {
return false;
}
return obj->adt != nullptr;
}
} // namespace blender::io::usd

View File

@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.h"
#include "BLI_map.hh"
makowalski marked this conversation as resolved Outdated

Unnecessary includes here?

Unnecessary includes here?
struct Bone;
struct Object;
namespace blender::io::usd {
class USDArmatureWriter : public USDAbstractWriter {
public:
USDArmatureWriter(const USDExporterContext &ctx);
protected:
virtual void do_write(HierarchyContext &context) override;
virtual bool check_is_animated(const HierarchyContext &context) const override;
private:
Map<StringRef, const Bone *> deform_map_;
};
} // namespace blender::io::usd

View File

@ -2,13 +2,21 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_mesh.h"
#include "usd_armature_utils.h"
#include "usd_blend_shape_utils.h"
#include "usd_hierarchy_iterator.h"
#include "usd_skel_convert.h"
#include "usd_writer_armature.h"
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/blendShape.h>
#include "BLI_array_utils.hh"
#include "BLI_assert.h"
@ -17,11 +25,15 @@
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BKE_armature.hh"
#include "BKE_attribute.hh"
#include "BKE_customdata.hh"
#include "BKE_deform.h"
#include "BKE_key.h"
#include "BKE_lib_id.h"
#include "BKE_material.h"
#include "BKE_mesh.hh"
#include "BKE_mesh_runtime.hh"
#include "BKE_mesh_wrapper.hh"
#include "BKE_modifier.hh"
#include "BKE_object.hh"
@ -29,6 +41,8 @@
#include "DEG_depsgraph.hh"
#include "DNA_armature_types.h"
#include "DNA_key_types.h"
#include "DNA_layer_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_fluidsim_types.h"
@ -38,6 +52,13 @@
#include <iostream>
#include "CLG_log.h"
static CLG_LogRef LOG = {"io.usd"};
namespace usdtokens {
static const pxr::TfToken Anim("Anim", pxr::TfToken::Immortal);
} // namespace usdtokens
namespace blender::io::usd {
const pxr::UsdTimeCode defaultTime = pxr::UsdTimeCode::Default();
@ -112,7 +133,9 @@ void USDGenericMeshWriter::do_write(HierarchyContext &context)
}
}
makowalski marked this conversation as resolved Outdated

spelling "ot" -> "or"

spelling "ot" -> "or"
void USDGenericMeshWriter::write_custom_data(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh)
void USDGenericMeshWriter::write_custom_data(const Object *obj,
const Mesh *mesh,
pxr::UsdGeomMesh usd_mesh)
{
const bke::AttributeAccessor attributes = mesh->attributes();
@ -136,6 +159,28 @@ void USDGenericMeshWriter::write_custom_data(const Mesh *mesh, pxr::UsdGeomMesh
return true;
}
if ((usd_export_context_.export_params.export_armatures ||
usd_export_context_.export_params.export_shapekeys) &&
attribute_id.name().rfind("skel:") == 0)
{
/* If we're exporting armatures or shape keys to UsdSkel, we skip any
* attributes that have names with the "skel:" namespace, to avoid possible
* conflicts. Such attribute might have been previously imported into Blender
* from USD, but can no longer be considered valid. */
return true;
}
if (usd_export_context_.export_params.export_armatures &&
is_armature_modifier_bone_name(
*obj, attribute_id.name().data(), usd_export_context_.depsgraph))
{
/* This attribute is likely a vertex group for the armature modifier,
* and it may conflict with skinning data that will be written to
* the USD mesh, so we skip it. Such vertex groups will instead be
* handled in #export_deform_verts(). */
return true;
}
/* UV Data. */
if (meta_data.domain == bke::AttrDomain::Corner && meta_data.data_type == CD_PROP_FLOAT2) {
if (usd_export_context_.export_params.export_uvmaps) {
@ -497,7 +542,7 @@ void USDGenericMeshWriter::write_mesh(HierarchyContext &context,
attr_corner_sharpnesses, pxr::VtValue(usd_mesh_data.crease_sharpnesses), timecode);
}
write_custom_data(mesh, usd_mesh);
write_custom_data(context.object, mesh, usd_mesh);
write_surface_velocity(mesh, usd_mesh);
const pxr::TfToken subdiv_scheme = get_subdiv_scheme(subsurfData);
@ -823,11 +868,196 @@ void USDGenericMeshWriter::write_surface_velocity(const Mesh *mesh, pxr::UsdGeom
usd_mesh.CreateVelocitiesAttr().Set(usd_velocities, timecode);
}
USDMeshWriter::USDMeshWriter(const USDExporterContext &ctx) : USDGenericMeshWriter(ctx) {}
Mesh *USDMeshWriter::get_export_mesh(Object *object_eval, bool & /*r_needsfree*/)
USDMeshWriter::USDMeshWriter(const USDExporterContext &ctx)
: USDGenericMeshWriter(ctx), write_skinned_mesh_(false), write_blend_shapes_(false)
{
}
void USDMeshWriter::set_skel_export_flags(const HierarchyContext &context)
{
write_skinned_mesh_ = false;
write_blend_shapes_ = false;
const USDExportParams &params = usd_export_context_.export_params;
/* We can write a skinned mesh if exporting armatures is enabled and the object has an armature
* modifier. */
write_skinned_mesh_ = params.export_armatures &&
can_export_skinned_mesh(*context.object, usd_export_context_.depsgraph);
/* We can write blend shapes if exporting shape keys is enabled and the object has shape keys. */
write_blend_shapes_ = params.export_shapekeys && is_mesh_with_shape_keys(context.object);
}
void USDMeshWriter::init_skinned_mesh(const HierarchyContext &context)
{
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
if (!mesh_prim.IsValid()) {
CLOG_WARN(&LOG,
"%s: couldn't get valid mesh prim for mesh %s",
__func__,
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
deadpin marked this conversation as resolved Outdated

Generally, what happens if there's both shape keys and armatures on the object?

Generally, what happens if there's both shape keys and armatures on the object?

If there are both shape keys and an armature deformer on the object, the mesh is saved in the base shape key pose, which also becomes the rest pose for the skeletal binding.

If there are both shape keys and an armature deformer on the object, the mesh is saved in the base shape key pose, which also becomes the rest pose for the skeletal binding.
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
const Object *arm_obj = get_armature_modifier_obj(*context.object,
usd_export_context_.depsgraph);
if (!arm_obj) {
CLOG_WARN(&LOG,
"Couldn't get armature modifier object for skinned mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
Vector<std::string> bone_names;
get_armature_bone_names(
arm_obj, usd_export_context_.export_params.only_deform_bones, bone_names);
if (bone_names.is_empty()) {
CLOG_WARN(&LOG,
"No armature bones for skinned mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
bool needsfree = false;
Mesh *mesh = get_export_mesh(context.object, needsfree);
if (mesh == nullptr) {
return;
}
try {
export_deform_verts(mesh, skel_api, bone_names);
if (needsfree) {
free_export_mesh(mesh);
}
}
catch (...) {
makowalski marked this conversation as resolved
Review

Where are exceptions coming from in this path?

Where are exceptions coming from in this path?
Review

These could possibly come from the USD calls in export_deform_verts(). In general, this is following the pattern in USDGenericMeshWriter::do_write() to ensure the temporary mesh is freed.

These could possibly come from the USD calls in `export_deform_verts()`. In general, this is following the pattern in `USDGenericMeshWriter::do_write()` to ensure the temporary mesh is freed.
if (needsfree) {
free_export_mesh(mesh);
}
throw;
}
}
void USDMeshWriter::init_blend_shapes(const HierarchyContext &context)
{
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
if (!mesh_prim.IsValid()) {
CLOG_WARN(&LOG,
"Couldn't get valid mesh prim for mesh %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
create_blend_shapes(this->usd_export_context_.stage, context.object, mesh_prim);
}
void USDMeshWriter::do_write(HierarchyContext &context)
{
set_skel_export_flags(context);
if (frame_has_been_written_ && (write_skinned_mesh_ || write_blend_shapes_)) {
/* When writing skinned meshes or blend shapes, we only write the rest mesh once,
* so we return early after the first frame has been written. However, we still
* update blend shape weights if needed. */
if (write_blend_shapes_) {
add_shape_key_weights_sample(context.object);
}
return;
}
USDGenericMeshWriter::do_write(context);
if (write_skinned_mesh_) {
init_skinned_mesh(context);
}
if (write_blend_shapes_) {
init_blend_shapes(context);
add_shape_key_weights_sample(context.object);
}
}
Mesh *USDMeshWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
{
if (write_blend_shapes_) {
r_needsfree = true;
/* We return the pre-modified mesh with the verts in the shape key
* basis positions. */
return get_shape_key_basis_mesh(object_eval);
}
if (write_skinned_mesh_) {
r_needsfree = false;
/* We must export the skinned mesh in its rest pose. We therefore
* return the pre-modified mesh, so that the armature modifier isn't
* applied. */
return BKE_object_get_pre_modified_mesh(object_eval);
}
/* Return the fully evaluated mesh. */
r_needsfree = false;
return BKE_object_get_evaluated_mesh(object_eval);
}
void USDMeshWriter::add_shape_key_weights_sample(const Object *obj)
{
if (!obj) {
return;
}
const Key *key = get_mesh_shape_key(obj);
if (!key) {
return;
}
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
if (!mesh_prim.IsValid()) {
CLOG_WARN(&LOG,
"Couldn't get valid mesh prim for mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
pxr::VtFloatArray weights = get_blendshape_weights(key);
pxr::UsdTimeCode timecode = get_export_time_code();
/* Save the weights samples to a temporary privar which will be copied to
* a skeleton animation later. */
pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).CreatePrimvar(
TempBlendShapeWeightsPrimvarName, pxr::SdfValueTypeNames->FloatArray);
if (!temp_weights_attr) {
CLOG_WARN(&LOG,
"Couldn't create primvar %s on prim %s",
TempBlendShapeWeightsPrimvarName.GetText(),
mesh_prim.GetPath().GetAsString().c_str());
return;
}
temp_weights_attr.Set(weights, timecode);
}
} // namespace blender::io::usd

View File

@ -10,6 +10,8 @@
#include <pxr/usd/usdGeom/mesh.h>
struct Key;
namespace blender::bke {
class AttributeIDRef;
struct AttributeMetaData;
@ -47,7 +49,7 @@ class USDGenericMeshWriter : public USDAbstractWriter {
void write_normals(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh);
void write_surface_velocity(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh);
void write_custom_data(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh);
void write_custom_data(const Object *obj, const Mesh *mesh, pxr::UsdGeomMesh usd_mesh);
void write_generic_data(const Mesh *mesh,
pxr::UsdGeomMesh usd_mesh,
const bke::AttributeIDRef &attribute_id,
@ -68,11 +70,27 @@ class USDGenericMeshWriter : public USDAbstractWriter {
};
class USDMeshWriter : public USDGenericMeshWriter {
bool write_skinned_mesh_;
bool write_blend_shapes_;
public:
USDMeshWriter(const USDExporterContext &ctx);
protected:
virtual void do_write(HierarchyContext &context) override;
virtual Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
/**
* Determine whether we should write skinned mesh or blend shape data
* based on the export parameters and the modifiers enabled on the object.
*/
void set_skel_export_flags(const HierarchyContext &context);
void init_skinned_mesh(const HierarchyContext &context);
void init_blend_shapes(const HierarchyContext &context);
void add_shape_key_weights_sample(const Object *obj);
};
} // namespace blender::io::usd

View File

@ -28,7 +28,9 @@ void USDTransformWriter::do_write(HierarchyContext &context)
if (!xformOp_) {
xformOp_ = xform.AddTransformOp();
}
xformOp_.Set(pxr::GfMatrix4d(parent_relative_matrix), get_export_time_code());
pxr::GfMatrix4d mat_val(parent_relative_matrix);
usd_value_writer_.SetAttribute(xformOp_.GetAttr(), mat_val, get_export_time_code());
}
bool USDTransformWriter::check_is_animated(const HierarchyContext &context) const

View File

@ -65,6 +65,9 @@ struct USDExportParams {
bool export_normals = true;
bool export_mesh_colors = true;
bool export_materials = true;
bool export_armatures = true;
bool export_shapekeys = true;
bool only_deform_bones = false;
eSubdivExportMode export_subdiv = USD_SUBDIV_BEST_MATCH;
bool selected_objects_only = false;
bool visible_objects_only = true;