Fix #116615: Store Blender bone lengths on USD Skeletons

USD Skeletons (armatures) are joint-based rather than bone-based in
construction. This means that there's no native bone concept nor is
there any bone lengths. Currently, Blender's USD import uses an
estimation, roughly corresponding to the average distance between
joints in the entire Skeleton, to set the bone length in situations
where it's not obvious that 2 joints are directly connected. This is
imperfect but shouldn't affect actual functioning of the rig.

For armatures coming from external software this is probably the best we
can do. However, for armatures originating from Blender, we can use a
custom primvar to store the bone lengths directly. This allows Blender
armatures to be exported and re-imported in much better fidelity.

This is superior to prior techniques, like those employed by FBX, which
alter the actual Skeleton, inserting extra joints where unnecessary. Not
only is this detrimental when using these files in external software,
but it's still imperfect when importing back into Blender too.

Pull Request: https://projects.blender.org/blender/blender/pulls/126954
This commit is contained in:
Jesse Yurkovich
2024-09-05 20:37:22 +02:00
committed by Jesse Yurkovich
parent d33e708343
commit 832d243249
3 changed files with 87 additions and 53 deletions

View File

@@ -21,6 +21,9 @@ struct Object;
namespace blender::io::usd {
/* Custom Blender Primvar name used for storing armature bone lengths. */
inline const pxr::TfToken BlenderBoneLengths("blender:bone_lengths", pxr::TfToken::Immortal);
/**
* 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.

View File

@@ -848,66 +848,84 @@ void import_skeleton(Main *bmain,
}
}
float avg_len_scale = 0;
for (size_t i = 0; i < num_joints; ++i) {
/* Use our custom bone length data if possible, otherwise fallback to estimated lengths. */
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(skel.GetPrim());
const pxr::UsdGeomPrimvar pv_lengths = pv_api.GetPrimvar(BlenderBoneLengths);
if (pv_lengths.HasValue()) {
pxr::VtArray<float> bone_lengths;
pv_lengths.ComputeFlattened(&bone_lengths);
/* If the bone has any children, scale its length
* by the distance between this bone's head
* and the average head location of its children. */
for (size_t i = 0; i < num_joints; ++i) {
EditBone *bone = edit_bones[i];
pxr::GfVec3f head(bone->head);
pxr::GfVec3f tail(bone->tail);
if (child_bones[i].is_empty()) {
continue;
}
EditBone *parent = edit_bones[i];
if (!parent) {
continue;
}
pxr::GfVec3f avg_child_head(0);
for (int j : child_bones[i]) {
EditBone *child = edit_bones[j];
if (!child) {
continue;
}
pxr::GfVec3f child_head(child->head);
avg_child_head += child_head;
}
avg_child_head /= child_bones[i].size();
pxr::GfVec3f parent_head(parent->head);
pxr::GfVec3f parent_tail(parent->tail);
const float new_len = (avg_child_head - parent_head).GetLength();
/* Check for epsilon relative to the parent head before scaling. */
if (new_len > .00001 * max_mag_component(parent_head)) {
parent_tail = parent_head + (parent_tail - parent_head).GetNormalized() * new_len;
copy_v3_v3(parent->tail, parent_tail.data());
avg_len_scale += new_len;
tail = head + (tail - head).GetNormalized() * bone_lengths[i];
copy_v3_v3(bone->tail, tail.data());
}
}
else {
float avg_len_scale = 0;
for (size_t i = 0; i < num_joints; ++i) {
/* Scale terminal bones by the average length scale. */
avg_len_scale /= num_joints;
/* If the bone has any children, scale its length
* by the distance between this bone's head
* and the average head location of its children. */
for (size_t i = 0; i < num_joints; ++i) {
if (!child_bones[i].is_empty()) {
/* Not a terminal bone. */
continue;
if (child_bones[i].is_empty()) {
continue;
}
EditBone *parent = edit_bones[i];
if (!parent) {
continue;
}
pxr::GfVec3f avg_child_head(0);
for (int j : child_bones[i]) {
EditBone *child = edit_bones[j];
if (!child) {
continue;
}
pxr::GfVec3f child_head(child->head);
avg_child_head += child_head;
}
avg_child_head /= child_bones[i].size();
pxr::GfVec3f parent_head(parent->head);
pxr::GfVec3f parent_tail(parent->tail);
const float new_len = (avg_child_head - parent_head).GetLength();
/* Check for epsilon relative to the parent head before scaling. */
if (new_len > .00001 * max_mag_component(parent_head)) {
parent_tail = parent_head + (parent_tail - parent_head).GetNormalized() * new_len;
copy_v3_v3(parent->tail, parent_tail.data());
avg_len_scale += new_len;
}
}
EditBone *bone = edit_bones[i];
if (!bone) {
continue;
}
pxr::GfVec3f head(bone->head);
/* Check for epsilon relative to the head before scaling. */
if (avg_len_scale > .00001 * max_mag_component(head)) {
pxr::GfVec3f tail(bone->tail);
tail = head + (tail - head).GetNormalized() * avg_len_scale;
copy_v3_v3(bone->tail, tail.data());
/* Scale terminal bones by the average length scale. */
avg_len_scale /= num_joints;
for (size_t i = 0; i < num_joints; ++i) {
if (!child_bones[i].is_empty()) {
/* Not a terminal bone. */
continue;
}
EditBone *bone = edit_bones[i];
if (!bone) {
continue;
}
pxr::GfVec3f head(bone->head);
/* Check for epsilon relative to the head before scaling. */
if (avg_len_scale > .00001 * max_mag_component(head)) {
pxr::GfVec3f tail(bone->tail);
tail = head + (tail - head).GetNormalized() * avg_len_scale;
copy_v3_v3(bone->tail, tail.data());
}
}
}

View File

@@ -11,6 +11,7 @@
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/skeleton.h>
@@ -54,6 +55,7 @@ static void initialize(const Object *obj,
using namespace blender::io::usd;
pxr::VtTokenArray joints;
pxr::VtArray<float> bone_lengths;
pxr::VtArray<pxr::GfMatrix4d> bind_xforms;
pxr::VtArray<pxr::GfMatrix4d> rest_xforms;
@@ -69,6 +71,9 @@ static void initialize(const Object *obj,
return;
}
/* Store Blender bone lengths to facilitate better roundtripping. */
bone_lengths.push_back(bone->length);
joints.push_back(build_usd_joint_path(bone, allow_unicode));
const pxr::GfMatrix4f arm_mat(bone->arm_mat);
bind_xforms.push_back(pxr::GfMatrix4d(arm_mat));
@@ -93,7 +98,15 @@ static void initialize(const Object *obj,
skel.GetBindTransformsAttr().Set(bind_xforms);
skel.GetRestTransformsAttr().Set(rest_xforms);
pxr::UsdSkelBindingAPI usd_skel_api = pxr::UsdSkelBindingAPI::Apply(skel.GetPrim());
const pxr::UsdPrim skel_prim = skel.GetPrim();
/* Store the custom bone lengths as just a regular Primvar attached to the Skeleton. */
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(skel_prim);
pxr::UsdGeomPrimvar pv_lengths = pv_api.CreatePrimvar(
BlenderBoneLengths, pxr::SdfValueTypeNames->FloatArray, pxr::UsdGeomTokens->uniform);
pv_lengths.Set(bone_lengths);
pxr::UsdSkelBindingAPI usd_skel_api = pxr::UsdSkelBindingAPI::Apply(skel_prim);
if (skel_anim) {
usd_skel_api.CreateAnimationSourceRel().SetTargets(