USD: Skeleton and blend shape import #110912

Merged
Michael Kowalski merged 38 commits from makowalski/blender:usdskel_import into main 2023-08-17 20:11:58 +02:00
15 changed files with 1498 additions and 24 deletions

View File

@ -429,6 +429,8 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op)
const bool import_meshes = RNA_boolean_get(op->ptr, "import_meshes");
const bool import_volumes = RNA_boolean_get(op->ptr, "import_volumes");
const bool import_shapes = RNA_boolean_get(op->ptr, "import_shapes");
const bool import_skeletons = RNA_boolean_get(op->ptr, "import_skeletons");
const bool import_blendshapes = RNA_boolean_get(op->ptr, "import_blendshapes");
const bool import_subdiv = RNA_boolean_get(op->ptr, "import_subdiv");
@ -492,6 +494,8 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op)
params.import_meshes = import_meshes;
params.import_volumes = import_volumes;
params.import_shapes = import_shapes;
params.import_skeletons = import_skeletons;
params.import_blendshapes = import_blendshapes;
params.prim_path_mask = prim_path_mask;
params.import_subdiv = import_subdiv;
params.import_instance_proxies = import_instance_proxies;
@ -538,6 +542,8 @@ static void wm_usd_import_draw(bContext * /*C*/, wmOperator *op)
uiItemR(col, ptr, "import_meshes", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "import_volumes", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "import_shapes", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "import_skeletons", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "import_blendshapes", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(box, ptr, "prim_path_mask", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(box, ptr, "scale", UI_ITEM_NONE, nullptr, ICON_NONE);
@ -632,6 +638,8 @@ void WM_OT_usd_import(wmOperatorType *ot)
RNA_def_boolean(ot->srna, "import_meshes", true, "Meshes", "");
RNA_def_boolean(ot->srna, "import_volumes", true, "Volumes", "");
RNA_def_boolean(ot->srna, "import_shapes", true, "Shapes", "");
RNA_def_boolean(ot->srna, "import_skeletons", true, "Skeletons", "");
RNA_def_boolean(ot->srna, "import_blendshapes", true, "Blend Shapes", "");
RNA_def_boolean(ot->srna,
"import_subdiv",

View File

@ -110,6 +110,8 @@ set(SRC
intern/usd_reader_stage.cc
intern/usd_reader_volume.cc
intern/usd_reader_xform.cc
intern/usd_reader_skeleton.cc
intern/usd_skel_convert.cc
usd.hh
usd.h
@ -142,6 +144,8 @@ set(SRC
intern/usd_reader_stage.h
intern/usd_reader_volume.h
intern/usd_reader_xform.h
intern/usd_reader_skeleton.h
intern/usd_skel_convert.h
)
if(WITH_HYDRA)

View File

@ -303,6 +303,10 @@ static void import_startjob(void *customdata, bool *stop, bool *do_update, float
}
}
if (data->params.import_skeletons) {
archive->process_armature_modifiers();
}
data->import_ok = !data->was_canceled;
*progress = 1.0f;

View File

@ -7,6 +7,7 @@
#include "usd_reader_mesh.h"
#include "usd_reader_material.h"
#include "usd_skel_convert.h"
#include "BKE_attribute.hh"
#include "BKE_customdata.h"
@ -43,6 +44,7 @@
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdGeom/subset.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <iostream>
@ -268,6 +270,14 @@ void USDMeshReader::read_object_data(Main *bmain, const double motionSampleTime)
}
}
if (import_params_.import_blendshapes) {
import_blendshapes(bmain, object_, prim_);
}
if (import_params_.import_skeletons) {
import_mesh_skel_bindings(bmain, object_, prim_);
}
USDXformReader::read_object_data(bmain, motionSampleTime);
} // namespace blender::io::usd
@ -1099,4 +1109,57 @@ Mesh *USDMeshReader::read_mesh(Mesh *existing_mesh,
return active_mesh;
}
std::string USDMeshReader::get_skeleton_path() const
{
/* Make sure we can apply UsdSkelBindingAPI to the prim.
* Attempting to apply the API to instance proxies generates
* a USD error. */
if (!prim_ || prim_.IsInstanceProxy()) {
return "";
}
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(prim_);
if (!skel_api) {
return "";
}
if (pxr::UsdSkelSkeleton skel = skel_api.GetInheritedSkeleton()) {
return skel.GetPath().GetAsString();
}
return "";
}
std::optional<XformResult> USDMeshReader::get_local_usd_xform(const float time) const
{
if (!import_params_.import_skeletons || prim_.IsInstanceProxy()) {
/* Use the standard transform computation, since we are ignoring
* skinning data. Note that applying the UsdSkelBinding API to an
* instance proxy generates a USD error. */
return USDXformReader::get_local_usd_xform(time);
}
if (pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(prim_)) {
if (skel_api.GetGeomBindTransformAttr().HasAuthoredValue()) {
pxr::GfMatrix4d bind_xf;
if (skel_api.GetGeomBindTransformAttr().Get(&bind_xf)) {
/* The USD bind transform is a matrix of doubles,
* but we cast it to GfMatrix4f because Blender expects
* a matrix of floats. Also, we assume the transform
* is constant over time. */
return XformResult(pxr::GfMatrix4f(bind_xf), true);
}
else {
WM_reportf(RPT_WARNING,
"%s: Couldn't compute geom bind transform for %s",
__func__,
prim_.GetPath().GetAsString().c_str());
}
}
}
return USDXformReader::get_local_usd_xform(time);
}
} // namespace blender::io::usd

View File

@ -56,6 +56,16 @@ class USDMeshReader : public USDGeomReader {
bool topology_changed(const Mesh *existing_mesh, double motionSampleTime) override;
/**
* If the USD mesh prim has a valid UsdSkel schema defined, return the USD path
* string to the bound skeleton, if any. Returns the empty string if no skeleton
* binding is defined.
*
* The returned path is currently used to match armature modifiers with armature
* objects during import.
*/
std::string get_skeleton_path() const;
private:
void process_normals_vertex_varying(Mesh *mesh);
void process_normals_face_varying(Mesh *mesh);
@ -82,6 +92,7 @@ class USDMeshReader : public USDGeomReader {
void read_color_data_primvar(Mesh *mesh,
const pxr::UsdGeomPrimvar &color_primvar,
const double motionSampleTime);
void read_uv_data_primvar(Mesh *mesh,
const pxr::UsdGeomPrimvar &primvar,
const double motionSampleTime);
@ -94,6 +105,12 @@ class USDMeshReader : public USDGeomReader {
const pxr::UsdGeomPrimvar &primvar,
const double motionSampleTime,
MutableSpan<BlenderT> attribute);
/**
* Override transform computation to account for the binding
* transformation for skinned meshes.
*/
std::optional<XformResult> get_local_usd_xform(float time) const override;
makowalski marked this conversation as resolved Outdated

Remove the const from const float time, in the declaration it has no meaning. See the C/C++ style guide for more info.

Remove the `const` from `const float time`, in the declaration it has no meaning. See the C/C++ style guide for more info.
};
} // namespace blender::io::usd

View File

@ -0,0 +1,47 @@
/* SPDX-FileCopyrightText: 2021 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_skeleton.h"
#include "usd_skel_convert.h"
#include "BKE_armature.h"
#include "BKE_idprop.h"
#include "BKE_object.h"
#include "DNA_armature_types.h"
#include "DNA_object_types.h"
#include "MEM_guardedalloc.h"
#include "WM_api.hh"
#include <iostream>
namespace blender::io::usd {
bool USDSkeletonReader::valid() const
{
return skel_ && USDXformReader::valid();
}
void USDSkeletonReader::create_object(Main *bmain, const double /* motionSampleTime */)
{
object_ = BKE_object_add_only_object(bmain, OB_ARMATURE, name_.c_str());
bArmature *arm = BKE_armature_add(bmain, name_.c_str());
object_->data = arm;
}
void USDSkeletonReader::read_object_data(Main *bmain, const double motionSampleTime)
{
if (!object_ || !object_->data || !skel_) {
return;
}
import_skeleton(bmain, object_, skel_);
USDXformReader::read_object_data(bmain, motionSampleTime);
}
} // namespace blender::io::usd

View File

@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.h"
#include "usd_reader_xform.h"
#include <pxr/usd/usdSkel/skeleton.h>
namespace blender::io::usd {
class USDSkeletonReader : public USDXformReader {
private:
pxr::UsdSkelSkeleton skel_;
public:
USDSkeletonReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings), skel_(prim)
{
}
bool valid() const override;
void create_object(Main *bmain, double motionSampleTime) override;
void read_object_data(Main *bmain, double motionSampleTime) override;
};
} // namespace blender::io::usd

View File

@ -11,6 +11,7 @@
#include "usd_reader_nurbs.h"
#include "usd_reader_prim.h"
#include "usd_reader_shape.h"
#include "usd_reader_skeleton.h"
#include "usd_reader_volume.h"
#include "usd_reader_xform.h"
@ -42,9 +43,12 @@
#include "BLI_string.h"
#include "BKE_lib_id.h"
#include "BKE_modifier.h"
#include "DNA_material_types.h"
#include "WM_api.hh"
namespace blender::io::usd {
USDStageReader::USDStageReader(pxr::UsdStageRefPtr stage,
@ -100,6 +104,9 @@ USDPrimReader *USDStageReader::create_reader_if_allowed(const pxr::UsdPrim &prim
if (params_.import_volumes && prim.IsA<pxr::UsdVolVolume>()) {
return new USDVolumeReader(prim, params_, settings_);
}
if (params_.import_skeletons && prim.IsA<pxr::UsdSkelSkeleton>()) {
return new USDSkeletonReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomImageable>()) {
return new USDXformReader(prim, params_, settings_);
}
@ -134,6 +141,9 @@ USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim)
if (prim.IsA<pxr::UsdVolVolume>()) {
return new USDVolumeReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdSkelSkeleton>()) {
return new USDSkeletonReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomImageable>()) {
return new USDXformReader(prim, params_, settings_);
}
@ -167,6 +177,11 @@ bool USDStageReader::include_by_visibility(const pxr::UsdGeomImageable &imageabl
bool USDStageReader::include_by_purpose(const pxr::UsdGeomImageable &imageable) const
{
if (params_.import_skeletons && imageable.GetPrim().IsA<pxr::UsdSkelSkeleton>()) {
/* Always include skeletons, if requested by the user, regardless of purpose. */
return true;
}
if (params_.import_guide && params_.import_proxy && params_.import_render) {
/* The options allow any purpose, so we trivially include the prim. */
return true;
@ -318,6 +333,48 @@ void USDStageReader::collect_readers(Main *bmain)
collect_readers(bmain, root);
}
void USDStageReader::process_armature_modifiers() const
{
/* Iteratate over the skeleton readers to create the
* armature object map, which maps a USD skeleton prim
* path to the corresponding armature object. */
std::map<std::string, Object *> usd_path_to_armature;
for (const USDPrimReader *reader : readers_) {
if (dynamic_cast<const USDSkeletonReader *>(reader) && reader->object()) {
usd_path_to_armature.insert(std::make_pair(reader->prim_path(), reader->object()));
}
}
/* Iterate over the mesh readers and set armature objects on armature modifiers. */
for (const USDPrimReader *reader : readers_) {
if (!reader->object()) {
continue;
}
const USDMeshReader *mesh_reader = dynamic_cast<const USDMeshReader *>(reader);
makowalski marked this conversation as resolved Outdated

Swap the condition and continue here as well. That wayall the precondition checks use the same logic, and the main path of the for-body can be followed vertically.

Swap the condition and `continue` here as well. That wayall the precondition checks use the same logic, and the main path of the for-body can be followed vertically.
if (!mesh_reader) {
continue;
}
/* Check if the mesh object has an armature modifier. */
ModifierData *md = BKE_modifiers_findby_type(reader->object(), eModifierType_Armature);
if (!md) {
continue;
}
ArmatureModifierData *amd = reinterpret_cast<ArmatureModifierData *>(md);
/* Assign the armature based on the bound USD skeleton path of the skinned mesh. */
std::string skel_path = mesh_reader->get_skeleton_path();
std::map<std::string, Object *>::const_iterator it = usd_path_to_armature.find(skel_path);
if (it == usd_path_to_armature.end()) {
WM_reportf(RPT_WARNING,
"%s: Couldn't find armature object corresponding to USD skeleton %s",
__func__,
skel_path.c_str());
}
amd->object = it->second;
}
}
void USDStageReader::import_all_materials(Main *bmain)
{
BLI_assert(valid());

View File

@ -45,6 +45,13 @@ class USDStageReader {
void collect_readers(struct Main *bmain);
/**
makowalski marked this conversation as resolved Outdated

Docstrings should be Doxygen style, so start with /**

Docstrings should be Doxygen style, so start with `/**`
* Complete setting up the armature modifiers that
* were created for skinned meshes by setting the
* modifier object on the corresponding modifier.
*/
void process_armature_modifiers() const;
/* Convert every material prim on the stage to a Blender
* material, including materials not used by any geometry.
* Note that collect_readers() must be called before calling

View File

@ -67,37 +67,20 @@ void USDXformReader::read_matrix(float r_mat[4][4] /* local matrix */,
const float scale,
bool *r_is_constant)
{
if (r_is_constant) {
*r_is_constant = true;
}
BLI_assert(r_mat);
BLI_assert(r_is_constant);
*r_is_constant = true;
unit_m4(r_mat);
pxr::UsdGeomXformable xformable;
std::optional<XformResult> xf_result = get_local_usd_xform(time);
if (use_parent_xform_) {
xformable = pxr::UsdGeomXformable(prim_.GetParent());
}
else {
xformable = pxr::UsdGeomXformable(prim_);
}
if (!xformable) {
/* This might happen if the prim is a Scope. */
if (!xf_result) {
return;
}
if (r_is_constant) {
*r_is_constant = !xformable.TransformMightBeTimeVarying();
}
pxr::GfMatrix4d usd_local_xf;
bool reset_xform_stack;
xformable.GetLocalTransformation(&usd_local_xf, &reset_xform_stack, time);
/* Convert the result to a float matrix. */
pxr::GfMatrix4f mat4f = pxr::GfMatrix4f(usd_local_xf);
mat4f.Get(r_mat);
std::get<0>(*xf_result).Get(r_mat);
*r_is_constant = std::get<1>(*xf_result);
/* Apply global scaling and rotation only to root objects, parenting
* will propagate it. */
@ -168,4 +151,28 @@ bool USDXformReader::is_root_xform_prim() const
return false;
}
std::optional<XformResult> USDXformReader::get_local_usd_xform(const float time) const
{
pxr::UsdGeomXformable xformable = use_parent_xform_ ? pxr::UsdGeomXformable(prim_.GetParent()) :
pxr::UsdGeomXformable(prim_);
makowalski marked this conversation as resolved

r_xform is not documented as being optional, and this function has no meaning when it is nullptr. This means that having r_xform == nullptr is a programming error, rather than an expected result of some input. Use BLI_assert() to check such errors, instead of silently ignoring them.

It might even be better to circumvent the whole situation, and make this class of errors impossible. This can be done by changing the return type to std::optional<pxr::GfMatrix4d>. If r_is_constant is optional, document it as such. Otherwise it might be useful to just return an optional tuple (matrix, is_constant).

`r_xform` is not documented as being optional, and this function has no meaning when it is `nullptr`. This means that having `r_xform == nullptr` is a programming error, rather than an expected result of some input. Use `BLI_assert()` to check such errors, instead of silently ignoring them. It might even be better to circumvent the whole situation, and make this class of errors impossible. This can be done by changing the return type to `std::optional<pxr::GfMatrix4d>`. If `r_is_constant` is optional, document it as such. Otherwise it might be useful to just return an optional tuple `(matrix, is_constant)`.
Review

I refactored the function to return an optional tuple, as you recommended. I think this is an elegant solution. Thanks for the suggestion!

I refactored the function to return an optional tuple, as you recommended. I think this is an elegant solution. Thanks for the suggestion!
if (!xformable) {
/* This might happen if the prim is a Scope. */
return std::nullopt;
}
bool is_constant = !xformable.TransformMightBeTimeVarying();
bool reset_xform_stack;
pxr::GfMatrix4d xform;
if (!xformable.GetLocalTransformation(&xform, &reset_xform_stack, time)) {
return std::nullopt;
}
/* The USD bind transform is a matrix of doubles,
makowalski marked this conversation as resolved Outdated

As xform is already a pxr::GfMatrix4f, and that's the expected type for a XformResult, why is this cast necessary? Might be worth a comment.

As `xform` is already a `pxr::GfMatrix4f`, and that's the expected type for a `XformResult`, why is this cast necessary? Might be worth a comment.

That's a good point, and I've added a comment to explain this. xform is a matrix for doubles (GfMatrix4d), but Blender expects a matrix of floats, so I explicitly convert to a GfMatrix4f for the return value.

That's a good point, and I've added a comment to explain this. `xform` is a matrix for doubles (`GfMatrix4d`), but Blender expects a matrix of floats, so I explicitly convert to a `GfMatrix4f` for the return value.
* but we cast it to GfMatrix4f because Blender expects
* a matrix of floats. */
return XformResult(pxr::GfMatrix4f(xform), is_constant);
}
} // namespace blender::io::usd

View File

@ -12,6 +12,10 @@
namespace blender::io::usd {
/** A transformation matrix and a boolean indicating
makowalski marked this conversation as resolved Outdated

Document what the boolean means.

Document what the boolean means.
* whether the matrix is constant over time. */
using XformResult = std::tuple<pxr::GfMatrix4f, bool>;
class USDXformReader : public USDPrimReader {
private:
bool use_parent_xform_;
@ -50,6 +54,18 @@ class USDXformReader : public USDPrimReader {
protected:
/* Returns true if the contained USD prim is the root of a transform hierarchy. */
bool is_root_xform_prim() const;
/**
* Return the USD prim's local transformation.
*
* \param time: Time code for evaluating the transform.
makowalski marked this conversation as resolved Outdated

An explanation fo the type should be done at the type declaration itself, not at one specific use of that type.

An explanation fo the type should be done at the type declaration itself, not at one specific use of that type.
*
makowalski marked this conversation as resolved Outdated

This should document what the returned boolean means.

This should document what the returned boolean means.
* \return: Optional tuple with the following elements:
* - The transform matrix.
* - A boolean flag indicating whether the matrix
* is constant over time.
*/
virtual std::optional<XformResult> get_local_usd_xform(float time) const;
};
} // namespace blender::io::usd

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <map>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdSkel/skeletonQuery.h>
struct Main;
struct Object;
struct Scene;
struct USDExportParams;
struct USDImportParams;
namespace blender::io::usd {
struct ImportSettings;
/**
* This file contains utilities for converting between UsdSkel data and
* Blender armatures and shape keys. The following is a reference on the
* UsdSkel API:
*
* https://openusd.org/23.05/api/usd_skel_page_front.html
*/
/**
* Import USD blend shapes from a USD primitive as shape keys on a mesh
* object. Optionally, if the blend shapes have animating weights, the
* time-sampled weights will be imported as shape key animation curves.
* If the USD primitive does not have blend shape targets defined, this
* function is a no-op.
*
* \param bmain: Main pointer
* \param mesh_obj: Mesh object to which imported shape keys will be added
* \param prim: The USD primitive from which blendshapes will be imported
* \param import_anim: Whether to import time-sampled weights as shape key
* animation curves
*/
void import_blendshapes(Main *bmain,
Object *mesh_obj,
const pxr::UsdPrim &prim,
bool import_anim = true);
/**
* Import the given USD skeleton as an armature object. Optionally, if the
makowalski marked this conversation as resolved Outdated

Typo: sekeleton > skeleton

Typo: sekeleton > skeleton
* skeleton has an animation defined, the time sampled joint transforms will be
* imported as bone animation curves.
*
* \param bmain: Main pointer
* \param arm_obj: Armature object to which the bone hierachy will be added
* \param skel: The USD skeleton from which bones and animation will be imported
* \param import_anim: Whether to import time-sampled joint transforms as bone
* animation curves
*/
void import_skeleton(Main *bmain,
Object *arm_obj,
const pxr::UsdSkelSkeleton &skel,
bool import_anim = true);
/**
* Import skinning data from a source USD prim as deform groups and an armature
nathanvegdahl marked this conversation as resolved
Review

I might be misunderstanding the comment, but I think by deform groups you mean vertex groups? That's the Blender terminology.

I might be misunderstanding the comment, but I think by deform groups you mean vertex groups? That's the Blender terminology.
Review

@mont29 informed me that in fact "deform group" is also used in some areas of Blender's code. So never mind! (Might be good to unify the terminology in the code base at some point, but that's obviously not for this PR!)

@mont29 informed me that in fact "deform group" is also used in some areas of Blender's code. So never mind! (Might be good to unify the terminology in the code base at some point, but that's obviously not for this PR!)
* modifier on the given mesh object. If the USD prim does not have a skeleton
* binding defined, this function is a no-op.
*
* \param bmain: Main pointer
* \param obj: Mesh object to which an armature modifier will be added
* \param prim: The USD primitive from which skinning data will be imported
*/
void import_mesh_skel_bindings(Main *bmain, Object *mesh_obj, const pxr::UsdPrim &prim);
} // namespace blender::io::usd

View File

@ -72,6 +72,8 @@ struct USDImportParams {
bool import_meshes;
bool import_volumes;
bool import_shapes;
bool import_skeletons;
bool import_blendshapes;
char *prim_path_mask;
bool import_subdiv;
bool import_instance_proxies;

View File

@ -13,6 +13,8 @@ from pxr import Sdf
import bpy
from mathutils import Matrix, Vector, Quaternion, Euler
args = None
@ -302,6 +304,106 @@ class USDImportTest(AbstractUSDTest):
self.assertEqual(4, num_uvmaps_found, "One or more test materials failed to import")
def test_import_usd_blend_shapes(self):
"""Test importing USD blend shapes with animated weights."""
infile = str(self.testdir / "usd_blend_shape_test.usda")
res = bpy.ops.wm.usd_import(filepath=infile)
self.assertEqual({'FINISHED'}, res)
obj = bpy.data.objects["Plane"]
obj.active_shape_key_index = 1
key = obj.active_shape_key
self.assertEqual(key.name, "Key_1", "Unexpected shape key name")
# Verify the number of shape key points.
self.assertEqual(len(key.data), 4, "Unexpected number of shape key point")
# Verify shape key point coordinates
# Reference point values.
refs = ((-2.51, -1.92, 0.20), (0.86, -1.46, -0.1),
(-1.33, 1.29, .84), (1.32, 2.20, -0.42))
for i in range(4):
co = key.data[i].co
ref = refs[i]
# Compare coordinates.
for j in range(3):
self.assertAlmostEqual(co[j], ref[j], 2)
# Verify the shape key values.
bpy.context.scene.frame_set(1)
self.assertAlmostEqual(key.value, .002, 1)
bpy.context.scene.frame_set(30)
self.assertAlmostEqual(key.value, .900, 3)
bpy.context.scene.frame_set(60)
self.assertAlmostEqual(key.value, .100, 3)
def test_import_usd_skel_joints(self):
"""Test importing USD animated skeleton joints."""
infile = str(self.testdir / "arm.usda")
res = bpy.ops.wm.usd_import(filepath=infile)
self.assertEqual({'FINISHED'}, res)
# Verify armature was imported.
arm_obj = bpy.data.objects["Skel"]
self.assertEqual(arm_obj.type, "ARMATURE", "'Skel' object is not an armature")
arm = arm_obj.data
bones = arm.bones
# Verify bone parenting.
self.assertIsNone(bones['Shoulder'].parent, "Shoulder bone should not be parented")
self.assertEqual(bones['Shoulder'], bones['Elbow'].parent, "Elbow bone should be child of Shoulder bone")
self.assertEqual(bones['Elbow'], bones['Hand'].parent, "Hand bone should be child of Elbow bone")
# Verify armature modifier was created on the mesh.
mesh_obj = bpy.data.objects['Arm']
# Get all the armature modifiers on the mesh.
arm_mods = [m for m in mesh_obj.modifiers if m.type == "ARMATURE"]
self.assertEqual(len(arm_mods), 1, "Didn't get expected armatrue modifier")
self.assertEqual(arm_mods[0].object, arm_obj, "Armature modifier does not reference the imported armature")
# Verify expected deform groups.
# There are 4 points in each group.
for i in range(4):
self.assertAlmostEqual(mesh_obj.vertex_groups['Hand'].weight(i), 1.0, 2, "Unexpected weight for Hand deform vert")
self.assertAlmostEqual(mesh_obj.vertex_groups['Shoulder'].weight(4 + i), 1.0, 2, "Unexpected weight for Shoulder deform vert")
self.assertAlmostEqual(mesh_obj.vertex_groups['Elbow'].weight(8 + i), 1.0, 2, "Unexpected weight for Elbow deform vert")
action = bpy.data.actions['SkelAction']
# Verify the Elbow joint rotation animation.
curve_path = 'pose.bones["Elbow"].rotation_quaternion'
# Quat W
f = action.fcurves.find(curve_path, index=0)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion W curve")
self.assertAlmostEqual(f.evaluate(0), 1.0, 2, "Unexpected value for rotation quaternion W curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.707, 2, "Unexpected value for rotation quaternion W curve at frame 10")
# Quat X
f = action.fcurves.find(curve_path, index=1)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion X curve")
self.assertAlmostEqual(f.evaluate(0), 0.0, 2, "Unexpected value for rotation quaternion X curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.707, 2, "Unexpected value for rotation quaternion X curve at frame 10")
# Quat Y
f = action.fcurves.find(curve_path, index=2)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion Y curve")
self.assertAlmostEqual(f.evaluate(0), 0.0, 2, "Unexpected value for rotation quaternion Y curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.0, 2, "Unexpected value for rotation quaternion Y curve at frame 10")
# Quat Z
f = action.fcurves.find(curve_path, index=3)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion Z curve")
self.assertAlmostEqual(f.evaluate(0), 0.0, 2, "Unexpected value for rotation quaternion Z curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.0, 2, "Unexpected value for rotation quaternion Z curve at frame 10")
def main():
global args