diff --git a/source/blender/editors/io/io_usd.cc b/source/blender/editors/io/io_usd.cc index 3882e49c9ea..f5d4e797116 100644 --- a/source/blender/editors/io/io_usd.cc +++ b/source/blender/editors/io/io_usd.cc @@ -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, diff --git a/source/blender/io/usd/CMakeLists.txt b/source/blender/io/usd/CMakeLists.txt index 684ba3a7c7c..5a60906bce5 100644 --- a/source/blender/io/usd/CMakeLists.txt +++ b/source/blender/io/usd/CMakeLists.txt @@ -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) diff --git a/source/blender/io/usd/intern/usd_armature_utils.cc b/source/blender/io/usd/intern/usd_armature_utils.cc new file mode 100644 index 00000000000..e94efda01f7 --- /dev/null +++ b/source/blender/io/usd/intern/usd_armature_utils.cc @@ -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 { + +/* Recursively invoke the 'visitor' function on the given bone and its children. */ +static void visit_bones(const Bone *bone, FunctionRef visitor) +{ + if (!(bone && visitor)) { + return; + } + + visitor(bone); + + LISTBASE_FOREACH (const Bone *, child, &bone->childbase) { + visit_bones(child, visitor); + } +} + +const ModifierData *get_enabled_modifier(const Object &obj, + 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( + get_enabled_modifier(obj, eModifierType_Armature, depsgraph)); + return mod; +} + +void visit_bones(const Object *ob_arm, FunctionRef visitor) +{ + 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) { + visit_bones(bone, visitor); + } +} + +void get_armature_bone_names(const Object *ob_arm, + const bool use_deform, + Vector &r_names) +{ + Map deform_map; + 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 *deform_map) +{ + BLI_assert(obj.pose); + + 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); +} + +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; + } + + bArmature *arm = static_cast(arm_mod->object->data); + + return BKE_armature_find_bone_name(arm, name.c_str()); +} + +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 *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); + } + }; + + 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); + } + } +} + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_armature_utils.h b/source/blender/io/usd/intern/usd_armature_utils.h new file mode 100644 index 00000000000..a7fb1f8f677 --- /dev/null +++ b/source/blender/io/usd/intern/usd_armature_utils.h @@ -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 +#include + +#include + +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 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 &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 *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 *deform_map); + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_blend_shape_utils.cc b/source/blender/io/usd/intern/usd_blend_shape_utils.cc new file mode 100644 index 00000000000..f18c5737f5f --- /dev/null +++ b/source/blender/io/usd/intern/usd_blend_shape_utils.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include + +#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. + * + * 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 &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, + "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 times; + temp_weights_attr.GetTimeSamples(×); + + 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()); + } + + 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(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 blendshape_paths; + + /* Get the basis, which we'll use to calculate offsets. */ + KeyBlock *basis_key = static_cast(key->block.first); + + if (!basis_key) { + return; + } + + int basis_totelem = basis_key->totelem; + + 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(kb->data); + + const float(*basis_fp)[3] = static_cast(basis_key->data); + + 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 joint_indices(basis_totelem, 0); + skel_api.CreateJointIndicesPrimvar(false, 1).GetAttr().Set(joint_indices); + } + + if (!skel_api.GetJointWeightsAttr().HasAuthoredValue()) { + pxr::VtArray 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(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); + } + + 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 merge_info; + + /* We are merging blend shape names and weights from multiple + * meshes to a single animation. In case of name collisions, + * 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 merged_names; + + /* Iterate over all the meshes, generate unique blend shape names in case of name + * 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 times; + merge_info.first().src_weights_attr.GetTimeSamples(×); + + 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); + + if (!mesh || !mesh->key || !mesh->key->block.first) { + return nullptr; + } + + KeyBlock *basis = reinterpret_cast(mesh->key->block.first); + + if (mesh->verts_num != basis->totelem) { + 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(temp_mesh->vert_positions_for_write().data()), + temp_mesh->verts_num); + + return temp_mesh; +} + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_blend_shape_utils.h b/source/blender/io/usd/intern/usd_blend_shape_utils.h new file mode 100644 index 00000000000..85082b0f043 --- /dev/null +++ b/source/blender/io/usd/intern/usd_blend_shape_utils.h @@ -0,0 +1,135 @@ +/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved. + * + * SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include +#include +#include + +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 diff --git a/source/blender/io/usd/intern/usd_capi_export.cc b/source/blender/io/usd/intern/usd_capi_export.cc index a573f4dec21..e1bd405e545 100644 --- a/source/blender/io/usd/intern/usd_capi_export.cc +++ b/source/blender/io/usd/intern/usd_capi_export.cc @@ -297,6 +297,10 @@ pxr::UsdStageRefPtr export_to_stage(const USDExportParams ¶ms, 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 diff --git a/source/blender/io/usd/intern/usd_hash_types.h b/source/blender/io/usd/intern/usd_hash_types.h index 0c6943b5d35..8f3292164e5 100644 --- a/source/blender/io/usd/intern/usd_hash_types.h +++ b/source/blender/io/usd/intern/usd_hash_types.h @@ -23,4 +23,11 @@ template<> struct DefaultHash { return value.Hash(); } }; + +template<> struct DefaultHash { + uint64_t operator()(const pxr::SdfPath &value) const + { + return (uint64_t)value.GetHash(); + } +}; } // namespace blender diff --git a/source/blender/io/usd/intern/usd_hierarchy_iterator.cc b/source/blender/io/usd/intern/usd_hierarchy_iterator.cc index ee4ce8bdda9..ac44414eead 100644 --- a/source/blender/io/usd/intern/usd_hierarchy_iterator.cc +++ b/source/blender/io/usd/intern/usd_hierarchy_iterator.cc @@ -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 diff --git a/source/blender/io/usd/intern/usd_hierarchy_iterator.h b/source/blender/io/usd/intern/usd_hierarchy_iterator.h index dc1e1ca73e4..c98e3d949fb 100644 --- a/source/blender/io/usd/intern/usd_hierarchy_iterator.h +++ b/source/blender/io/usd/intern/usd_hierarchy_iterator.h @@ -6,6 +6,7 @@ #include "IO_abstract_hierarchy_iterator.h" #include "usd.h" #include "usd_exporter_context.h" +#include "usd_skel_convert.h" #include @@ -28,6 +29,10 @@ class USDHierarchyIterator : public AbstractHierarchyIterator { pxr::UsdTimeCode export_time_; const USDExportParams ¶ms_; + 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 diff --git a/source/blender/io/usd/intern/usd_skel_convert.cc b/source/blender/io/usd/intern/usd_skel_convert.cc index fd0af551029..29e3c933b9e 100644 --- a/source/blender/io/usd/intern/usd_skel_convert.cc +++ b/source/blender/io/usd/intern/usd_skel_convert.cc @@ -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 +#include #include #include #include @@ -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 #include #include +#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 skel_to_mesh; + + /* We will keep track of the mesh prims to clean up the temporary + * weights attribute at the end. */ + Vector mesh_prims; + + /* 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 &bone_names) +{ + BLI_assert(mesh); + BLI_assert(skel_api); + + /* 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 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) { + if (bone_names[i] == def->name) { + bone_idx = i; + break; + } + } + + joint_index.append(bone_idx); + } + + if (joint_index.is_empty()) { + return; + } + + const Span 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) { + 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 joint_indices(num_points * element_size, 0); + pxr::VtArray 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; + } + + if (j >= vert.totweight) { + continue; + } + + int def_nr = static_cast(vert.dw[j].def_nr); + + if (def_nr >= joint_index.size()) { + BLI_assert_unreachable(); + continue; + } + + if (joint_index[def_nr] == -1) { + continue; + } + + joint_indices[offset] = joint_index[def_nr]; + joint_weights[offset] = vert.dw[j].weight; + } + } + + 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 diff --git a/source/blender/io/usd/intern/usd_skel_convert.h b/source/blender/io/usd/intern/usd_skel_convert.h index a2fd237a877..31ad3ca8574 100644 --- a/source/blender/io/usd/intern/usd_skel_convert.h +++ b/source/blender/io/usd/intern/usd_skel_convert.h @@ -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 #include +#include +#include #include +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; + +/** + * 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 &bone_names); + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_skel_root_utils.cc b/source/blender/io/usd/intern/usd_skel_root_utils.cc new file mode 100644 index 00000000000..f302ceb3c42 --- /dev/null +++ b/source/blender/io/usd/intern/usd_skel_root_utils.cc @@ -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 +#include +#include +#include + +#include "BKE_report.h" + +#include "WM_types.hh" + +#include + +#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()) { + ancestor = ancestor.GetParent(); + } + + if (ancestor && ancestor.IsA()) { + return pxr::UsdGeomXform(ancestor); + } + + return pxr::UsdGeomXform(); +} + +namespace blender::io::usd { + +void create_skel_roots(pxr::UsdStageRefPtr stage, const USDExportParams ¶ms) +{ + 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() || !prim.HasAPI()) { + 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; + } + 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()) { + 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 diff --git a/source/blender/io/usd/intern/usd_skel_root_utils.h b/source/blender/io/usd/intern/usd_skel_root_utils.h new file mode 100644 index 00000000000..4d450644a51 --- /dev/null +++ b/source/blender/io/usd/intern/usd_skel_root_utils.h @@ -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 + +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 ¶ms); + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_armature.cc b/source/blender/io/usd/intern/usd_writer_armature.cc new file mode 100644 index 00000000000..ed7f5bb4190 --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_armature.cc @@ -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 +#include +#include +#include +#include +#include + +#include +#include + +#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 *deform_bones) +{ + using namespace blender::io::usd; + + pxr::VtTokenArray joints; + pxr::VtArray bind_xforms; + pxr::VtArray 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 *deform_map) +{ + if (!(skel_anim && obj && obj->pose)) { + return; + } + + pxr::VtArray 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 *deform_map = usd_export_context_.export_params.only_deform_bones ? + &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 diff --git a/source/blender/io/usd/intern/usd_writer_armature.h b/source/blender/io/usd/intern/usd_writer_armature.h new file mode 100644 index 00000000000..4fe35b68f95 --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_armature.h @@ -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" + +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 deform_map_; +}; + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_mesh.cc b/source/blender/io/usd/intern/usd_writer_mesh.cc index e6641c0f2e2..727fa50c9a5 100644 --- a/source/blender/io/usd/intern/usd_writer_mesh.cc +++ b/source/blender/io/usd/intern/usd_writer_mesh.cc @@ -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 #include #include #include #include +#include +#include +#include #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 +#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) } } -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 ¶ms = 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; + } + + 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 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 (...) { + 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 diff --git a/source/blender/io/usd/intern/usd_writer_mesh.h b/source/blender/io/usd/intern/usd_writer_mesh.h index ec2f1f57ce5..38b401a226c 100644 --- a/source/blender/io/usd/intern/usd_writer_mesh.h +++ b/source/blender/io/usd/intern/usd_writer_mesh.h @@ -10,6 +10,8 @@ #include +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 diff --git a/source/blender/io/usd/intern/usd_writer_transform.cc b/source/blender/io/usd/intern/usd_writer_transform.cc index 5d3bb9214c0..7be47e85e5a 100644 --- a/source/blender/io/usd/intern/usd_writer_transform.cc +++ b/source/blender/io/usd/intern/usd_writer_transform.cc @@ -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 diff --git a/source/blender/io/usd/usd.h b/source/blender/io/usd/usd.h index a4425566f5d..000ea5aed29 100644 --- a/source/blender/io/usd/usd.h +++ b/source/blender/io/usd/usd.h @@ -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;