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