Jelajahi Sumber

USD Keyframe Animations (#5856)

USD Keyframe Animations

Signed-off-by: AMZN-Gene <[email protected]>
Signed-off-by: Gene Walters <[email protected]>
Co-authored-by: Kazuki Y <[email protected]>
Co-authored-by: kazu0617 <[email protected]>
Co-authored-by: Kim Kulling <[email protected]>
Co-authored-by: Kim Kulling <[email protected]>
Gene Walters 9 bulan lalu
induk
melakukan
2493deff37

+ 123 - 0
code/AssetLib/USD/USDLoaderImplTinyusdz.cpp

@@ -213,6 +213,7 @@ void USDImporterImplTinyusdz::InternReadFile(
     }
 
     // sanityCheckNodesRecursive(pScene->mRootNode);
+    animations(render_scene, pScene);
     meshes(render_scene, pScene, nameWExt);
     materials(render_scene, pScene, nameWExt);
     textures(render_scene, pScene, nameWExt);
@@ -223,6 +224,128 @@ void USDImporterImplTinyusdz::InternReadFile(
 
     setupBlendShapes(render_scene, pScene, nameWExt);
 }
+void USDImporterImplTinyusdz::animations(
+    const tinyusdz::tydra::RenderScene& render_scene,
+    aiScene* pScene) {
+    if (render_scene.animations.empty()) {
+        return;
+    }
+
+    pScene->mNumAnimations = render_scene.animations.size();
+    pScene->mAnimations = new aiAnimation *[pScene->mNumAnimations];
+
+    for (int animationIndex = 0; animationIndex < pScene->mNumAnimations; ++animationIndex) {
+
+        const auto &animation = render_scene.animations[animationIndex];
+
+        auto newAiAnimation = new aiAnimation();
+        pScene->mAnimations[animationIndex] = newAiAnimation;
+
+        newAiAnimation->mName = animation.abs_path;
+
+        if (animation.channels_map.empty()) {
+            newAiAnimation->mNumChannels = 0;
+            continue;
+        }
+
+        // each channel affects a node (joint)
+        newAiAnimation->mNumChannels = animation.channels_map.size();
+        newAiAnimation->mChannels = new aiNodeAnim *[newAiAnimation->mNumChannels];
+        int channelIndex = 0;
+        for (const auto &[jointName, animationChannelMap] : animation.channels_map) {
+            auto newAiNodeAnim = new aiNodeAnim();
+            newAiAnimation->mChannels[channelIndex] = newAiNodeAnim;
+            newAiNodeAnim->mNodeName = jointName;
+            newAiAnimation->mDuration = 0;
+
+            std::vector<aiVectorKey> positionKeys;
+            std::vector<aiQuatKey> rotationKeys;
+            std::vector<aiVectorKey> scalingKeys;
+
+            for (const auto &[channelType, animChannel] : animationChannelMap) {
+                switch (channelType) {
+                case tinyusdz::tydra::AnimationChannel::ChannelType::Rotation:
+                    if (animChannel.rotations.static_value.has_value()) {
+                        rotationKeys.emplace_back(0, tinyUsdzQuatToAiQuat(animChannel.rotations.static_value.value()));
+                    }
+                    for (const auto &rotationAnimSampler : animChannel.rotations.samples) {
+                        if (rotationAnimSampler.t > newAiAnimation->mDuration) {
+                            newAiAnimation->mDuration = rotationAnimSampler.t;
+                        }
+
+                        rotationKeys.emplace_back(rotationAnimSampler.t, tinyUsdzQuatToAiQuat(rotationAnimSampler.value));
+                    }
+                    break;
+                case tinyusdz::tydra::AnimationChannel::ChannelType::Scale:
+                    if (animChannel.scales.static_value.has_value()) {
+                        scalingKeys.emplace_back(0, tinyUsdzScaleOrPosToAssimp(animChannel.scales.static_value.value()));
+                    }
+                    for (const auto &scaleAnimSampler : animChannel.scales.samples) {
+                        if (scaleAnimSampler.t > newAiAnimation->mDuration) {
+                            newAiAnimation->mDuration = scaleAnimSampler.t;
+                        }
+                        scalingKeys.emplace_back(scaleAnimSampler.t, tinyUsdzScaleOrPosToAssimp(scaleAnimSampler.value));
+                    }
+                    break;
+                case tinyusdz::tydra::AnimationChannel::ChannelType::Transform:
+                    if (animChannel.transforms.static_value.has_value()) {
+                        aiVector3D position;
+                        aiVector3D scale;
+                        aiQuaternion rotation;
+                        tinyUsdzMat4ToAiMat4(animChannel.transforms.static_value.value().m).Decompose(scale, rotation, position);
+
+                        positionKeys.emplace_back(0, position);
+                        scalingKeys.emplace_back(0, scale);
+                        rotationKeys.emplace_back(0, rotation);
+                    }
+                    for (const auto &transformAnimSampler : animChannel.transforms.samples) {
+                        if (transformAnimSampler.t > newAiAnimation->mDuration) {
+                            newAiAnimation->mDuration = transformAnimSampler.t;
+                        }
+
+                        aiVector3D position;
+                        aiVector3D scale;
+                        aiQuaternion rotation;
+                        tinyUsdzMat4ToAiMat4(transformAnimSampler.value.m).Decompose(scale, rotation, position);
+
+                        positionKeys.emplace_back(transformAnimSampler.t, position);
+                        scalingKeys.emplace_back(transformAnimSampler.t, scale);
+                        rotationKeys.emplace_back(transformAnimSampler.t, rotation);
+                    }
+                    break;
+                case tinyusdz::tydra::AnimationChannel::ChannelType::Translation:
+                    if (animChannel.translations.static_value.has_value()) {
+                        positionKeys.emplace_back(0, tinyUsdzScaleOrPosToAssimp(animChannel.translations.static_value.value()));
+                    }
+                    for (const auto &translationAnimSampler : animChannel.translations.samples) {
+                        if (translationAnimSampler.t > newAiAnimation->mDuration) {
+                            newAiAnimation->mDuration = translationAnimSampler.t;
+                        }
+
+                        positionKeys.emplace_back(translationAnimSampler.t, tinyUsdzScaleOrPosToAssimp(translationAnimSampler.value));
+                    }
+                    break;
+                default:
+                    TINYUSDZLOGW(TAG, "Unsupported animation channel type (%s). Please update the USD importer to support this animation channel.", tinyusdzAnimChannelTypeFor(channelType).c_str());
+                }
+            }
+
+            newAiNodeAnim->mNumPositionKeys = positionKeys.size();
+            newAiNodeAnim->mPositionKeys = new aiVectorKey[newAiNodeAnim->mNumPositionKeys];
+            std::move(positionKeys.begin(), positionKeys.end(), newAiNodeAnim->mPositionKeys);
+
+            newAiNodeAnim->mNumRotationKeys = rotationKeys.size();
+            newAiNodeAnim->mRotationKeys = new aiQuatKey[newAiNodeAnim->mNumRotationKeys];
+            std::move(rotationKeys.begin(), rotationKeys.end(), newAiNodeAnim->mRotationKeys);
+
+            newAiNodeAnim->mNumScalingKeys = scalingKeys.size();
+            newAiNodeAnim->mScalingKeys = new aiVectorKey[newAiNodeAnim->mNumScalingKeys];
+            std::move(scalingKeys.begin(), scalingKeys.end(), newAiNodeAnim->mScalingKeys);
+
+            ++channelIndex;
+        }
+    }
+}
 
 void USDImporterImplTinyusdz::meshes(
         const tinyusdz::tydra::RenderScene &render_scene,

+ 4 - 0
code/AssetLib/USD/USDLoaderImplTinyusdz.h

@@ -65,6 +65,10 @@ public:
             aiScene *pScene,
             IOSystem *pIOHandler);
 
+    void animations(
+            const tinyusdz::tydra::RenderScene &render_scene,
+            aiScene *pScene);
+
     void meshes(
             const tinyusdz::tydra::RenderScene &render_scene,
             aiScene *pScene,

+ 0 - 21
code/AssetLib/USD/USDLoaderImplTinyusdzHelper.cpp

@@ -100,27 +100,6 @@ std::string Assimp::tinyusdzNodeTypeFor(NodeType type) {
     }
 }
 
-aiMatrix4x4 Assimp::tinyUsdzMat4ToAiMat4(const double matIn[4][4]) {
-    aiMatrix4x4 matOut;
-    matOut.a1 = matIn[0][0];
-    matOut.a2 = matIn[1][0];
-    matOut.a3 = matIn[2][0];
-    matOut.a4 = matIn[3][0];
-    matOut.b1 = matIn[0][1];
-    matOut.b2 = matIn[1][1];
-    matOut.b3 = matIn[2][1];
-    matOut.b4 = matIn[3][1];
-    matOut.c1 = matIn[0][2];
-    matOut.c2 = matIn[1][2];
-    matOut.c3 = matIn[2][2];
-    matOut.c4 = matIn[3][2];
-    matOut.d1 = matIn[0][3];
-    matOut.d2 = matIn[1][3];
-    matOut.d3 = matIn[2][3];
-    matOut.d4 = matIn[3][3];
-    return matOut;
-}
-
 aiVector3D Assimp::tinyUsdzScaleOrPosToAssimp(const std::array<float, 3> &scaleOrPosIn) {
     return aiVector3D(scaleOrPosIn[0], scaleOrPosIn[1], scaleOrPosIn[2]);
 }

+ 23 - 1
code/AssetLib/USD/USDLoaderImplTinyusdzHelper.h

@@ -48,14 +48,36 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <assimp/types.h>
 #include "tinyusdz.hh"
 #include "tydra/render-data.hh"
+#include <type_traits>
 
 namespace Assimp {
 
 std::string tinyusdzAnimChannelTypeFor(
         tinyusdz::tydra::AnimationChannel::ChannelType animChannel);
 std::string tinyusdzNodeTypeFor(tinyusdz::tydra::NodeType type);
-aiMatrix4x4 tinyUsdzMat4ToAiMat4(const double matIn[4][4]);
 
+template <typename T>
+aiMatrix4x4 tinyUsdzMat4ToAiMat4(const T matIn[4][4]) {
+    static_assert(std::is_floating_point_v<T>, "Only floating-point types are allowed.");
+    aiMatrix4x4 matOut;
+    matOut.a1 = matIn[0][0];
+    matOut.a2 = matIn[1][0];
+    matOut.a3 = matIn[2][0];
+    matOut.a4 = matIn[3][0];
+    matOut.b1 = matIn[0][1];
+    matOut.b2 = matIn[1][1];
+    matOut.b3 = matIn[2][1];
+    matOut.b4 = matIn[3][1];
+    matOut.c1 = matIn[0][2];
+    matOut.c2 = matIn[1][2];
+    matOut.c3 = matIn[2][2];
+    matOut.c4 = matIn[3][2];
+    matOut.d1 = matIn[0][3];
+    matOut.d2 = matIn[1][3];
+    matOut.d3 = matIn[2][3];
+    matOut.d4 = matIn[3][3];
+    return matOut;
+}
 aiVector3D tinyUsdzScaleOrPosToAssimp(const std::array<float, 3> &scaleOrPosIn);
 
 /**

+ 1 - 1
code/PostProcessing/ValidateDataStructure.cpp

@@ -447,7 +447,7 @@ void ValidateDSProcess::Validate(const aiMesh *pMesh, const aiBone *pBone, float
         if (pBone->mWeights[i].mVertexId >= pMesh->mNumVertices) {
             ReportError("aiBone::mWeights[%i].mVertexId is out of range", i);
         } else if (!pBone->mWeights[i].mWeight || pBone->mWeights[i].mWeight > 1.0f) {
-            ReportWarning("aiBone::mWeights[%i].mWeight has an invalid value", i);
+                ReportWarning("aiBone::mWeights[%i].mWeight has an invalid value %i. Value must be greater than zero and less than 1.", i, pBone->mWeights[i].mWeight);
         }
         afSum[pBone->mWeights[i].mVertexId] += pBone->mWeights[i].mWeight;
     }

+ 1 - 0
test/models-nonbsd/USD/usda/README.md

@@ -2,3 +2,4 @@
 [texturedcube.usda](texturedcube.usda) copied from tinyusdz/models (No attribution/license cited in that project)
 [translated-cube.usda](translated-cube.usda) copied from tinyusdz/models (No attribution/license cited in that project)
 [simple-skin-test.usda](simple-skin-test.usda) copied from tinyusdz/models (No attribution/license cited in that project)
+[simple-skin-animation-test.usda](simple-skin-animation-test.usda) modified tinyusdz/models (No attribution/license cited in that project)

+ 237 - 0
test/models-nonbsd/USD/usda/simple-skin-animation-test.usda

@@ -0,0 +1,237 @@
+#usda 1.0
+(
+    defaultPrim = "root"
+    doc = "Blender v4.2.3 LTS"
+    endTimeCode = 40
+    metersPerUnit = 1
+    startTimeCode = 0
+    timeCodesPerSecond = 24
+    upAxis = "Z"
+)
+
+def Xform "root" (
+    customData = {
+        dictionary Blender = {
+            bool generated = 1
+        }
+    }
+)
+{
+    def SkelRoot "Armature_001"
+    {
+        custom string userProperties:blender:object_name = "Armature.001"
+        float3 xformOp:rotateXYZ = (-89.99999, 0, 0)
+        float3 xformOp:scale = (1, 1, 1)
+        double3 xformOp:translate = (0, -1.7017418146133423, 0)
+        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
+
+        def Xform "Grid"
+        {
+            custom string userProperties:blender:object_name = "Grid"
+            float3 xformOp:rotateXYZ = (89.99999, -0, 0)
+            float3 xformOp:scale = (1, 1, 1)
+            double3 xformOp:translate = (0, -7.438549687321938e-8, 1.7017418146133423)
+            uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
+
+            def Mesh "Grid" (
+                active = true
+                prepend apiSchemas = ["SkelBindingAPI"]
+            )
+            {
+                float3[] extent = [(-1, -1, 0), (1, 1, 0)]
+                int[] faceVertexCounts = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
+                int[] faceVertexIndices = [0, 1, 6, 5, 1, 2, 7, 6, 2, 3, 8, 7, 3, 4, 9, 8, 5, 6, 11, 10, 6, 7, 12, 11, 7, 8, 13, 12, 8, 9, 14, 13, 10, 11, 16, 15, 11, 12, 17, 16, 12, 13, 18, 17, 13, 14, 19, 18, 15, 16, 21, 20, 16, 17, 22, 21, 17, 18, 23, 22, 18, 19, 24, 23]
+                normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
+                    interpolation = "faceVarying"
+                )
+                point3f[] points = [(-1, -1, 0), (-0.5, -1, 0), (0, -1, 0), (0.5, -1, 0), (1, -1, 0), (-1, -0.5, 0), (-0.5, -0.5, 0), (0, -0.5, 0), (0.5, -0.5, 0), (1, -0.5, 0), (-1, 0, 0), (-0.5, 0, 0), (0, 0, 0), (0.5, 0, 0), (1, 0, 0), (-1, 0.5, 0), (-0.5, 0.5, 0), (0, 0.5, 0), (0.5, 0.5, 0), (1, 0.5, 0), (-1, 1, 0), (-0.5, 1, 0), (0, 1, 0), (0.5, 1, 0), (1, 1, 0)]
+                bool[] primvars:sharp_face = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] (
+                    interpolation = "uniform"
+                )
+                matrix4d primvars:skel:geomBindTransform = ( (1, 0, 0, 0), (0, 1.331580543606492e-7, 0.9999999999999911, 0), (0, -0.9999999999999911, 1.331580543606492e-7, 0), (0, -7.438549687321943e-8, 1.701741814613342, 1) )
+                int[] primvars:skel:jointIndices = [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1] (
+                    elementSize = 2
+                    interpolation = "vertex"
+                )
+                float[] primvars:skel:jointWeights = [0.43767902, 0.56232095, 0.605441, 0.39455906, 1, 0, 0.605441, 0.39455906, 0.43767902, 0.562321, 0.23470172, 0.7652983, 0.18096954, 0.81903046, 1, 0, 0.18096954, 0.81903046, 0.23470171, 0.7652983, 0.114853, 0.88514704, 0.06506716, 0.9349329, 1, 0, 0.06506715, 0.9349329, 0.11485299, 0.88514704, 0.059175473, 0.94082457, 0.009479873, 0.9905201, 1, 0, 0.009479865, 0.9905201, 0.059175465, 0.94082457, 0.031181445, 0.96881855, 1, 0, 1, 0, 1, 0, 0.031181442, 0.9688186] (
+                    elementSize = 2
+                    interpolation = "vertex"
+                )
+                texCoord2f[] primvars:st = [(0, 0), (0.25, 0), (0.25, 0.25), (0, 0.25), (0.25, 0), (0.5, 0), (0.5, 0.25), (0.25, 0.25), (0.5, 0), (0.75, 0), (0.75, 0.25), (0.5, 0.25), (0.75, 0), (1, 0), (1, 0.25), (0.75, 0.25), (0, 0.25), (0.25, 0.25), (0.25, 0.5), (0, 0.5), (0.25, 0.25), (0.5, 0.25), (0.5, 0.5), (0.25, 0.5), (0.5, 0.25), (0.75, 0.25), (0.75, 0.5), (0.5, 0.5), (0.75, 0.25), (1, 0.25), (1, 0.5), (0.75, 0.5), (0, 0.5), (0.25, 0.5), (0.25, 0.75), (0, 0.75), (0.25, 0.5), (0.5, 0.5), (0.5, 0.75), (0.25, 0.75), (0.5, 0.5), (0.75, 0.5), (0.75, 0.75), (0.5, 0.75), (0.75, 0.5), (1, 0.5), (1, 0.75), (0.75, 0.75), (0, 0.75), (0.25, 0.75), (0.25, 1), (0, 1), (0.25, 0.75), (0.5, 0.75), (0.5, 1), (0.25, 1), (0.5, 0.75), (0.75, 0.75), (0.75, 1), (0.5, 1), (0.75, 0.75), (1, 0.75), (1, 1), (0.75, 1)] (
+                    interpolation = "faceVarying"
+                )
+                rel skel:skeleton = </root/Armature_001/Armature/Armature>
+                uniform token subdivisionScheme = "none"
+                custom string userProperties:blender:data_name = "Grid"
+            }
+        }
+
+        def Xform "Armature"
+        {
+            custom string userProperties:blender:object_name = "Armature"
+            float3 xformOp:rotateXYZ.timeSamples = {
+                0: (0, -0, 0),
+            }
+            float3 xformOp:scale.timeSamples = {
+                0: (1, 1, 1),
+            }
+            double3 xformOp:translate.timeSamples = {
+                0: (0, 0, 0),
+            }
+            uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
+
+            def Skeleton "Armature" (
+                prepend apiSchemas = ["SkelBindingAPI"]
+            )
+            {
+                uniform matrix4d[] bindTransforms = [( (1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0), (0, 0, 0, 1) ), ( (1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0), (0, 0, 1, 1) )]
+                uniform token[] joints = ["Bone", "Bone/Bone_001"]
+                uniform matrix4d[] restTransforms = [( (1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0), (0, 0, 0, 1) ), ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 1, 0, 1) )]
+                rel skel:animationSource = </root/Armature_001/Armature/Armature/Anim>
+
+                def SkelAnimation "Anim"
+                {
+                    uniform token[] joints = ["Bone", "Bone/Bone_001"]
+                    quatf[] rotations.timeSamples = {
+                        0: [(0.70710677, 0.70710677, 0, 0), (1, 0, 0, 0)],
+                        1: [(0.70710677, 0.70710677, 0, 0), (0.9996082, 0, 0, 0.027989032)],
+                        2: [(0.70710677, 0.70710677, 0, 0), (0.99463546, 0, 0, 0.10344209)],
+                        3: [(0.70710677, 0.70710677, 0, 0), (0.9774578, 0, 0, 0.21113087)],
+                        4: [(0.70710677, 0.70710677, 0, 0), (0.9432686, 0, 0, 0.3320306)],
+                        5: [(0.70710677, 0.70710677, 0, 0), (0.89442724, 0, 0, 0.4472135)],
+                        6: [(0.70710677, 0.70710677, 0, 0), (0.83920974, 0, 0, 0.5438079)],
+                        7: [(0.70710677, 0.70710677, 0, 0), (0.7869733, 0, 0, 0.61698705)],
+                        8: [(0.70710677, 0.70710677, 0, 0), (0.7447736, 0, 0, 0.6673172)],
+                        9: [(0.70710677, 0.70710677, 0, 0), (0.7170745, 0, 0, 0.6969965)],
+                        10: [(0.70710677, 0.70710677, 0, 0), (0.70710677, 0, 0, 0.70710677)],
+                        11: [(0.70710677, 0.70710677, 0, 0), (0.7122517, 0, 0, 0.70192415)],
+                        12: [(0.70710677, 0.70710677, 0, 0), (0.7271744, 0, 0, 0.68645275)],
+                        13: [(0.70710677, 0.70710677, 0, 0), (0.75127214, 0, 0, 0.6599926)],
+                        14: [(0.70710677, 0.70710677, 0, 0), (0.7839187, 0, 0, 0.62086356)],
+                        15: [(0.70710677, 0.70710677, 0, 0), (0.82404196, 0, 0, 0.5665288)],
+                        16: [(0.70710677, 0.70710677, 0, 0), (0.8695245, 0, 0, 0.49388987)],
+                        17: [(0.70710677, 0.70710677, 0, 0), (0.91649354, 0, 0, 0.4000495)],
+                        18: [(0.70710677, 0.70710677, 0, 0), (0.9588755, 0, 0, 0.28382713)],
+                        19: [(0.70710677, 0.70710677, 0, 0), (0.9890088, 0, 0, 0.14785683)],
+                        20: [(0.70710677, 0.70710677, 0, 0), (1, 0, 0, 0)],
+                        21: [(0.70710677, 0.70710677, 0, 0), (0.9890088, 0, 0, -0.14785682)],
+                        22: [(0.70710677, 0.70710677, 0, 0), (0.9588755, 0, 0, -0.28382716)],
+                        23: [(0.70710677, 0.70710677, 0, 0), (0.91649354, 0, 0, -0.40004945)],
+                        24: [(0.70710677, 0.70710677, 0, 0), (0.86952454, 0, 0, -0.49388978)],
+                        25: [(0.70710677, 0.70710677, 0, 0), (0.82404196, 0, 0, -0.5665288)],
+                        26: [(0.70710677, 0.70710677, 0, 0), (0.7839186, 0, 0, -0.6208636)],
+                        27: [(0.70710677, 0.70710677, 0, 0), (0.7512721, 0, 0, -0.65999264)],
+                        28: [(0.70710677, 0.70710677, 0, 0), (0.7271744, 0, 0, -0.68645275)],
+                        29: [(0.70710677, 0.70710677, 0, 0), (0.7122517, 0, 0, -0.70192415)],
+                        30: [(0.70710677, 0.70710677, 0, 0), (0.70710677, 0, 0, -0.70710677)],
+                        31: [(0.70710677, 0.70710677, 0, 0), (0.7170746, 0, 0, -0.69699645)],
+                        32: [(0.70710677, 0.70710677, 0, 0), (0.7447736, 0, 0, -0.6673172)],
+                        33: [(0.70710677, 0.70710677, 0, 0), (0.7869733, 0, 0, -0.61698705)],
+                        34: [(0.70710677, 0.70710677, 0, 0), (0.83920974, 0, 0, -0.5438079)],
+                        35: [(0.70710677, 0.70710677, 0, 0), (0.8944272, 0, 0, -0.44721356)],
+                        36: [(0.70710677, 0.70710677, 0, 0), (0.9432686, 0, 0, -0.3320306)],
+                        37: [(0.70710677, 0.70710677, 0, 0), (0.97745776, 0, 0, -0.21113095)],
+                        38: [(0.70710677, 0.70710677, 0, 0), (0.99463546, 0, 0, -0.10344207)],
+                        39: [(0.70710677, 0.70710677, 0, 0), (0.9996082, 0, 0, -0.027989028)],
+                        40: [(0.70710677, 0.70710677, 0, 0), (1, 0, 0, 0)],
+                    }
+                    half3[] scales.timeSamples = {
+                        0: [(1, 1, 1), (1, 1, 1)],
+                        1: [(1, 1, 1), (1, 1, 1)],
+                        2: [(1, 1, 1), (1, 1, 1)],
+                        3: [(1, 1, 1), (1, 1, 1)],
+                        4: [(1, 1, 1), (1, 1, 1)],
+                        5: [(1, 1, 1), (1, 1, 1)],
+                        6: [(1, 1, 1), (1, 1, 1)],
+                        7: [(1, 1, 1), (1, 1, 1)],
+                        8: [(1, 1, 1), (1, 1, 1)],
+                        9: [(1, 1, 1), (1, 1, 1)],
+                        10: [(1, 1, 1), (1, 1, 1)],
+                        11: [(1, 1, 1), (1, 1, 1)],
+                        12: [(1, 1, 1), (1, 1, 1)],
+                        13: [(1, 1, 1), (1, 1, 1)],
+                        14: [(1, 1, 1), (1, 1, 1)],
+                        15: [(1, 1, 1), (1, 1, 1)],
+                        16: [(1, 1, 1), (1, 1, 1)],
+                        17: [(1, 1, 1), (1, 1, 1)],
+                        18: [(1, 1, 1), (1, 1, 1)],
+                        19: [(1, 1, 1), (1, 1, 1)],
+                        20: [(1, 1, 1), (1, 1, 1)],
+                        21: [(1, 1, 1), (1, 1, 1)],
+                        22: [(1, 1, 1), (1, 1, 1)],
+                        23: [(1, 1, 1), (1, 1, 1)],
+                        24: [(1, 1, 1), (1, 1, 1)],
+                        25: [(1, 1, 1), (1, 1, 1)],
+                        26: [(1, 1, 1), (1, 1, 1)],
+                        27: [(1, 1, 1), (1, 1, 1)],
+                        28: [(1, 1, 1), (1, 1, 1)],
+                        29: [(1, 1, 1), (1, 1, 1)],
+                        30: [(1, 1, 1), (1, 1, 1)],
+                        31: [(1, 1, 1), (1, 1, 1)],
+                        32: [(1, 1, 1), (1, 1, 1)],
+                        33: [(1, 1, 1), (1, 1, 1)],
+                        34: [(1, 1, 1), (1, 1, 1)],
+                        35: [(1, 1, 1), (1, 1, 1)],
+                        36: [(1, 1, 1), (1, 1, 1)],
+                        37: [(1, 1, 1), (1, 1, 1)],
+                        38: [(1, 1, 1), (1, 1, 1)],
+                        39: [(1, 1, 1), (1, 1, 1)],
+                        40: [(1, 1, 1), (1, 1, 1)],
+                    }
+                    float3[] translations.timeSamples = {
+                        0: [(0, 0, 0), (0, 1, 0)],
+                        1: [(0, 0, 0), (0, 1, 0)],
+                        2: [(0, 0, 0), (0, 1, 0)],
+                        3: [(0, 0, 0), (0, 1, 0)],
+                        4: [(0, 0, 0), (0, 1, 0)],
+                        5: [(0, 0, 0), (0, 1, 0)],
+                        6: [(0, 0, 0), (0, 1, 0)],
+                        7: [(0, 0, 0), (0, 1, 0)],
+                        8: [(0, 0, 0), (0, 1, 0)],
+                        9: [(0, 0, 0), (0, 1, 0)],
+                        10: [(0, 0, 0), (0, 1, 0)],
+                        11: [(0, 0, 0), (0, 1, 0)],
+                        12: [(0, 0, 0), (0, 1, 0)],
+                        13: [(0, 0, 0), (0, 1, 0)],
+                        14: [(0, 0, 0), (0, 1, 0)],
+                        15: [(0, 0, 0), (0, 1, 0)],
+                        16: [(0, 0, 0), (0, 1, 0)],
+                        17: [(0, 0, 0), (0, 1, 0)],
+                        18: [(0, 0, 0), (0, 1, 0)],
+                        19: [(0, 0, 0), (0, 1, 0)],
+                        20: [(0, 0, 0), (0, 1, 0)],
+                        21: [(0, 0, 0), (0, 1, 0)],
+                        22: [(0, 0, 0), (0, 1, 0)],
+                        23: [(0, 0, 0), (0, 1, 0)],
+                        24: [(0, 0, 0), (0, 1, 0)],
+                        25: [(0, 0, 0), (0, 1, 0)],
+                        26: [(0, 0, 0), (0, 1, 0)],
+                        27: [(0, 0, 0), (0, 1, 0)],
+                        28: [(0, 0, 0), (0, 1, 0)],
+                        29: [(0, 0, 0), (0, 1, 0)],
+                        30: [(0, 0, 0), (0, 1, 0)],
+                        31: [(0, 0, 0), (0, 1, 0)],
+                        32: [(0, 0, 0), (0, 1, 0)],
+                        33: [(0, 0, 0), (0, 1, 0)],
+                        34: [(0, 0, 0), (0, 1, 0)],
+                        35: [(0, 0, 0), (0, 1, 0)],
+                        36: [(0, 0, 0), (0, 1, 0)],
+                        37: [(0, 0, 0), (0, 1, 0)],
+                        38: [(0, 0, 0), (0, 1, 0)],
+                        39: [(0, 0, 0), (0, 1, 0)],
+                        40: [(0, 0, 0), (0, 1, 0)],
+                    }
+                }
+            }
+        }
+    }
+
+    def DomeLight "env_light"
+    {
+        float inputs:intensity = 1
+        asset inputs:texture:file = @.\textures\color_121212.hdr@
+        float3 xformOp:rotateXYZ = (90, 1.2722219e-14, 90)
+        uniform token[] xformOpOrder = ["xformOp:rotateXYZ"]
+    }
+}
+

+ 11 - 0
test/unit/utUSDImport.cpp

@@ -77,3 +77,14 @@ TEST_F(utUSDImport, skinnedMeshTest) {
     EXPECT_NE(nullptr, scene->mRootNode->FindNode("Bone"));
     EXPECT_NE(nullptr, scene->mRootNode->FindNode("Bone/Bone_001"));
 }
+
+TEST_F(utUSDImport, singleAnimationTest) {
+    Assimp::Importer importer;
+    const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/../models-nonbsd/USD/usda/simple-skin-animation-test.usda", aiProcess_ValidateDataStructure);
+    EXPECT_NE(nullptr, scene);
+    EXPECT_TRUE(scene->HasAnimations());
+    EXPECT_EQ(2, scene->mAnimations[0]->mNumChannels);  // 2 bones. 1 channel for each bone
+}
+
+// Note: Add multi-animation test once supported by USD
+// See https://github.com/lighttransport/tinyusdz/issues/122 for details.