DirectX 12 中 Assimp 的骨骼动画错误

Posted

技术标签:

【中文标题】DirectX 12 中 Assimp 的骨骼动画错误【英文标题】:Skeletal animation bug with Assimp in DirectX 12 【发布时间】:2021-11-07 14:05:07 【问题描述】:

我正在使用 Assimp 将带有动画的 FBX 模型(在 Blender 中创建)加载到我的 DirectX 12 游戏中,但我遇到了游戏应用程序渲染动画的一个非常令人沮丧的错误。

测试模型是一个简单的“旗杆”,包含四个骨骼,如下所示:

Bone0 -> Bone1 -> Bone2​​ -> Bone3

当绕过关键帧动画时,模型在其静止姿势中正确渲染。

当动画仅通过根骨骼 (Bone0) 旋转模型时,模型也会正确渲染和动画。

但是,当导入在第一个关节(即 Bone1)处旋转的模型时,聚集在每个关节周围的顶点似乎“卡”在其原始位置,而“骨骼”周围的顶点似乎会跟随正确的动画。

结果是一个蹩脚的锯齿形拉伸几何图形,如下所示:

相反,模型在其动画姿势结束时应类似于“六角扳手”形状,如 AssimpViewer 实用工具中呈现的同一模型所示:

由于模型在 AssimpViewer 中正确渲染,因此可以合理地假设 Blender 导出的 FBX 文件没有问题。然后我检查并确认“卡在”关节周围的顶点确实确实由游戏加载代码正确分配了它们的顶点权重。 C++模型加载和动画代码基于流行的OGLDev教程:https://ogldev.org/www/tutorial38/tutorial38.html

现在令人恼火的是,由于 AssimpViewer 工具可以正确渲染模型动画,我还从该工具中复制了 SceneAnimator 和 AnimEvaluator 类,以通过该代码分支生成最终的骨骼变换......只是结束在游戏中出现完全相同的曲折错误!

我有理由相信在初始化时找到骨骼层次结构没有任何问题,因此这里是遍历层次结构并在每一帧插入关键帧的关键函数。

VOID Mesh::ReadNodeHeirarchy(FLOAT animationTime, CONST aiNode* pNode, CONST aiAnimation* pAnim, CONST aiMatrix4x4 parentTransform)
        
            std::string nodeName(pNode->mName.data);
        
            // nodeTransform is a relative transform to parent node space
        
            aiMatrix4x4 nodeTransform = pNode->mTransformation;
        
            CONST aiNodeAnim* pNodeAnim = FindNodeAnim(pAnim, nodeName);
            
            if (pNodeAnim)
            
                // Interpolate scaling and generate scaling transformation matrix
            
                aiVector3D scaling(1.f, 1.f, 1.f);
            
                CalcInterpolatedScaling(scaling, animationTime, pNodeAnim);
            
                // Interpolate rotation and generate rotation transformation matrix
            
                aiQuaternion rotationQ (1.f, 0.f, 0.f, 0.f);
            
                CalcInterpolatedRotation(rotationQ, animationTime, pNodeAnim);
            
                // Interpolate translation and generate translation transformation matrix
            
                aiVector3D translat(0.f, 0.f, 0.f);
            
                CalcInterpolatedPosition(translat, animationTime, pNodeAnim);
            
                // build the SRT transform matrix
        
                nodeTransform = aiMatrix4x4(rotationQ.GetMatrix());
                nodeTransform.a1 *= scaling.x; nodeTransform.b1 *= scaling.x; nodeTransform.c1 *= scaling.x;
                nodeTransform.a2 *= scaling.y; nodeTransform.b2 *= scaling.y; nodeTransform.c2 *= scaling.y;
                nodeTransform.a3 *= scaling.z; nodeTransform.b3 *= scaling.z; nodeTransform.c3 *= scaling.z;
                nodeTransform.a4 = translat.x; nodeTransform.b4 = translat.y; nodeTransform.c4 = translat.z;
        
            
            
            aiMatrix4x4 globalTransform = parentTransform * nodeTransform;
        
            if (m_boneMapping.find(nodeName) != m_boneMapping.end())
            
                UINT boneIndex = m_boneMapping[nodeName];
        
                // the global inverse transform returns us to mesh space!!!
        
                m_boneInfo[boneIndex].FinalTransform = m_globalInverseTransform * globalTransform * m_boneInfo[boneIndex].BoneOffset;
                //m_boneInfo[boneIndex].FinalTransform = m_boneInfo[boneIndex].BoneOffset * globalTransform * m_globalInverseTransform;
        
                m_shaderTransforms[boneIndex] = aiMatrixToSimpleMatrix(m_boneInfo[boneIndex].FinalTransform);
            
        
            for (UINT i = 0u; i < pNode->mNumChildren; i++)
            
                ReadNodeHeirarchy(animationTime, pNode->mChildren[i], pAnim, globalTransform);
            
        
        
VOID Mesh::CalcInterpolatedRotation(aiQuaternion& out, FLOAT animationTime, CONST aiNodeAnim* pNodeAnim)
        
            UINT rotationKeys = pNodeAnim->mNumRotationKeys;
        
            // we need at least two values to interpolate...
            if (rotationKeys == 1u)
            
                CONST aiQuaternion& key = pNodeAnim->mRotationKeys[0u].mValue;
                out = key;
                return;
            
        
            UINT rotationIndex = FindRotation(animationTime, pNodeAnim);
            UINT nextRotationIndex = (rotationIndex + 1u) % rotationKeys;
            assert(nextRotationIndex < rotationKeys);
        
            CONST aiQuatKey& key = pNodeAnim->mRotationKeys[rotationIndex];
            CONST aiQuatKey& nextKey = pNodeAnim->mRotationKeys[nextRotationIndex];
        
            FLOAT deltaTime = FLOAT(nextKey.mTime) - FLOAT(key.mTime);
            FLOAT factor = (animationTime - FLOAT(key.mTime)) / deltaTime;
            assert(factor >= 0.f && factor <= 1.f);
            
            aiQuaternion::Interpolate(out, key.mValue, nextKey.mValue, factor);
        

我刚刚在这里包含了旋转插值,因为缩放和平移函数是相同的。对于那些不知道的人,Assimp 的 aiMatrix4x4 类型遵循列向量数学约定,所以我没有弄乱原始矩阵乘法顺序。

关于我的代码与我采用的两个基于 Assimp 的代码分支之间的唯一差异是,需要使用此转换函数将最终转换从 aiMatrix4x4 类型转换为 DirectXTK SimpleMath 矩阵(实际上是 XMMATRIX):

Matrix Mesh::aiMatrixToSimpleMatrix(CONST aiMatrix4x4 m)

    return Matrix
       (m.a1, m.a2, m.a3, m.a4,
        m.b1, m.b2, m.b3, m.b4,
        m.c1, m.c2, m.c3, m.c4,
        m.d1, m.d2, m.d3, m.d4);

由于 aiMatrix4x4 Assimp 矩阵的列向量方向,最终的骨骼变换不会转置以供 HLSL 使用。最终骨骼变换数组被传递到蒙皮顶点着色器常量缓冲区,如下所示。

commandList->SetPipelineState(m_psoForwardSkinned.Get()); // set PSO

// Update vertex shader with current bone transforms

CONST std::vector<Matrix> transforms = m_assimpModel.GetShaderTransforms();
VSBonePassConstants vsBoneConstants;

for (UINT i = 0; i < m_assimpModel.GetNumBones(); i++)

    // We do not transpose bone matrices for HLSL because the original
    // Assimp matrices are column-vector matrices.

    vsBoneConstants.boneTransforms[i] = transforms[i];
    //vsBoneConstants.boneTransforms[i] = transforms[i].Transpose();
    //vsBoneConstants.boneTransforms[i] = Matrix::Identity;

GraphicsResource vsBoneCB = m_graphicsMemory->AllocateConstant(vsBoneConstants);

vsPerObjects.gWorld = m_assimp_world.Transpose(); // vertex shader per object constant
vsPerObjectCB = m_graphicsMemory->AllocateConstant(vsPerObjects);

commandList->SetGraphicsRootConstantBufferView(RootParameterIndex::VSBoneConstantBuffer, vsBoneCB.GpuAddress());
commandList->SetGraphicsRootConstantBufferView(RootParameterIndex::VSPerObjConstBuffer, vsPerObjectCB.GpuAddress());
//commandList->SetGraphicsRootDescriptorTable(RootParameterIndex::ObjectSRV, m_shaderTextureHeap->GetGpuHandle(ShaderTexDescriptors::SuzanneDiffuse));
commandList->SetGraphicsRootDescriptorTable(RootParameterIndex::ObjectSRV, m_shaderTextureHeap->GetGpuHandle(ShaderTexDescriptors::DefaultDiffuse));

for (UINT i = 0; i < m_assimpModel.GetMeshSize(); i++)

    commandList->IASetVertexBuffers(0u, 1u, &m_assimpModel.meshEntries[i].GetVertexBufferView());
    commandList->IASetIndexBuffer(&m_assimpModel.meshEntries[i].GetIndexBufferView());
    commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    commandList->DrawIndexedInstanced(m_assimpModel.meshEntries[i].GetIndexCount(), 1u, 0u, 0u, 0u);

请注意,我正在使用上述代码中 DirectXTK12 库中的图形资源内存管理帮助器对象。最后,这是我正在使用的蒙皮顶点着色器。

// Luna (2016) lighting model adapted from Moller
#define MAX_BONES 4

    // vertex shader constant data that varies per object
    cbuffer cbVSPerObject : register(b3)
    
        float4x4 gWorld;
        //float4x4 gTexTransform;
    
    
    // vertex shader constant data that varies per frame
    cbuffer cbVSPerFrame : register(b5)
    
        float4x4 gViewProj;
        float4x4 gShadowTransform;
    
    
    // bone matrix constant data that varies per object
    cbuffer cbVSBonesPerObject : register(b9)
    
        float4x4 gBoneTransforms[MAX_BONES];
    
    
    struct VertexIn
    
        float3 posL : SV_POSITION;
        float3 normalL : NORMAL;
        float2 texCoord : TEXCOORD0;
        float3 tangentU  : TANGENT;
        float4 boneWeights : BONEWEIGHT;
        uint4 boneIndices  : BONEINDEX;
    ;
    
    struct VertexOut
    
        float4 posH : SV_POSITION;
        //float3 posW : POSITION;
        float4 shadowPosH : POSITION0;
        float3 posW : POSITION1;
        float3 normalW : NORMAL;
        float2 texCoord : TEXCOORD0;
        float3 tangentW : TANGENT;
    ;
    
    VertexOut VS_main(VertexIn vin)
    
        VertexOut vout = (VertexOut)0.f;
    
        // Perform vertex skinning.
        // Ignore BoneWeights.w and instead calculate the last weight value
        // to ensure all bone weights sum to unity.
    
        float4 weights = vin.boneWeights;
        //weights.w = 1.f - dot(weights.xyz, float3(1.f, 1.f, 1.f));
    
        //float4 weights =  0.f, 0.f, 0.f, 0.f ;
        //weights.x = vin.boneWeights.x;
        //weights.y = vin.boneWeights.y;
        //weights.z = vin.boneWeights.z;
    
        weights.w = 1.f - (weights.x + weights.y + weights.z);
    
        float4 localPos = float4(vin.posL, 1.f);
        float3 localNrm = vin.normalL;
        float3 localTan = vin.tangentU;
    
        float3 objPos = mul(localPos, (float4x3)gBoneTransforms[vin.boneIndices.x]).xyz * weights.x;
        objPos += mul(localPos, (float4x3)gBoneTransforms[vin.boneIndices.y]).xyz * weights.y;
        objPos += mul(localPos, (float4x3)gBoneTransforms[vin.boneIndices.z]).xyz * weights.z;
        objPos += mul(localPos, (float4x3)gBoneTransforms[vin.boneIndices.w]).xyz * weights.w;
    
        float3 objNrm = mul(localNrm, (float3x3)gBoneTransforms[vin.boneIndices.x]) * weights.x;
        objNrm += mul(localNrm, (float3x3)gBoneTransforms[vin.boneIndices.y]) * weights.y;
        objNrm += mul(localNrm, (float3x3)gBoneTransforms[vin.boneIndices.z]) * weights.z;
        objNrm += mul(localNrm, (float3x3)gBoneTransforms[vin.boneIndices.w]) * weights.w;
    
        float3 objTan = mul(localTan, (float3x3)gBoneTransforms[vin.boneIndices.x]) * weights.x;
        objTan += mul(localTan, (float3x3)gBoneTransforms[vin.boneIndices.y]) * weights.y;
        objTan += mul(localTan, (float3x3)gBoneTransforms[vin.boneIndices.z]) * weights.z;
        objTan += mul(localTan, (float3x3)gBoneTransforms[vin.boneIndices.w]) * weights.w;
    
        vin.posL = objPos;
        vin.normalL = objNrm;
        vin.tangentU.xyz = objTan;
        //vin.posL = posL;
        //vin.normalL = normalL;
        //vin.tangentU.xyz = tangentL;
    
        // End vertex skinning
    
        // transform to world space
        float4 posW = mul(float4(vin.posL, 1.f), gWorld);
        vout.posW = posW.xyz;
    
        // assumes nonuniform scaling, otherwise needs inverse-transpose of world matrix
        vout.normalW = mul(vin.normalL, (float3x3)gWorld);
        vout.tangentW = mul(vin.tangentU, (float3x3)gWorld);
    
        // transform to homogenous clip space
        vout.posH = mul(posW, gViewProj);
    
        // pass texcoords to pixel shader
        vout.texCoord = vin.texCoord;
    
        //float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
        //vout.TexC = mul(texC, gMatTransform).xy;
    
        // generate projective tex-coords to project shadow map onto scene
        vout.shadowPosH = mul(posW, gShadowTransform);
    
        return vout;
    

我在发布之前尝试的最后一些测试:

我使用从 Blender 导出的 Collada (DAE) 模型测试了代码,只是在 Win32 桌面应用程序中观察到同样扭曲的锯齿形。

我还确认加载模型的 aiScene 对象返回全局根变换的单位矩阵(也在 AssimpViewer 中验证)。

我已经盯着这段代码大约一个星期了,我快疯了!真的希望有人能发现我错过的东西。如果您需要更多代码或信息,请询问!

【问题讨论】:

首先:非常感谢您分享您的观察!我真的很欣赏这一点。 嗨@KimKulling,我知道你是Assimp 的主要作者,我也很感激这个问题得到你的关注。我仍然不知道这个错误是否在我的代码库中,或者它是否是 Assimp 的 FBX 导入器的基本问题。从我的帖子开始,我发现如果我像这样设置节点全局变换: globalTranform = parentTransform * nodeTransform(其中 nodeTransform = pNode->mTransformation)然后我得到一个完美的绑定(休息)姿势,没有失真。当我用本地 TRS(平移/旋转/缩放)矩阵替换 nodeTransform 时,如下所示: globalTranform = parentTransform * localTransform 然后包含动画插值并产生失真动画。因此,错误可能出在缩放/旋转/平移关键帧插值代码中,但我用于插值的代码与 AssimpViewer AnimEvaluator 类中的代码相同。 您可以尝试在您的代码中打开/关闭缩放/旋转和变换插值吗?也许这有助于我们找出问题所在。 我可以通过简单地不将 SRT 矩阵连接到全局变换来避免插值代码。或者,我可以通过注释掉对“CalcInterpolatedX()”函数的调用来关闭插值。你是这个意思吗?无论哪种方式,在没有动画的情况下,我得到了我正在使用的“旗杆”四骨模型的正确(不失真)休息姿势。 【参考方案1】:

这似乎是教程/文档中已发布代码的错误。如果您能在此处打开问题报告,那就太好了:Assimp-Projectpage on GitHub。

【讨论】:

谢谢@KimKulling,会的。【参考方案2】:

又经历了将近两周的痛苦,但我终于找到了错误。它在我自己的代码中,而且是我自己造成的。在我展示解决方案之前,我应该解释一下我为到达那里所做的进一步故障排除。

在对 Assimp 失去信心后(即使 AssimpViewer 工具正确地为我的模型设置动画),我转向了 FBX SDK。作为 SDK 的一部分提供的 FBX ViewScene 命令行实用工具也可以正确显示和动画我的模型,所以我有希望...

因此,在查看了 FBX SDK 教程几天后,又花了一周时间为我的 Windows 桌面游戏编写 FBX 导入器,我加载了我的模型,然后……看到与加载的版本完全相同的锯齿形动画异常通过 Assimp!

这个令人沮丧的结果意味着我至少可以消除 Assimp 和 FBX SDK 作为问题的根源,并再次关注顶点着色器。我用于顶点蒙皮的着色器取自 Frank Luna 文本的“角色动画”一章。它在各个方面都是相同的,这导致我重新检查了在应用程序端声明的 C++ 顶点结构......

这是蒙皮顶点的 C++ 顶点声明:

struct Vertex

    // added constructors
    Vertex() = default;

    Vertex(FLOAT x, FLOAT y, FLOAT z,
        FLOAT nx, FLOAT ny, FLOAT nz,
        FLOAT u, FLOAT v,
        FLOAT tx, FLOAT ty, FLOAT tz) :
        Pos(x, y, z),
        Normal(nx, ny, nz),
        TexC(u, v),
        Tangent(tx, ty, tz) 

    Vertex(DirectX::SimpleMath::Vector3 pos,
        DirectX::SimpleMath::Vector3 normal,
        DirectX::SimpleMath::Vector2 texC,
        DirectX::SimpleMath::Vector3 tangent) :
        Pos(pos), Normal(normal), TexC(texC), Tangent(tangent) 

    DirectX::SimpleMath::Vector3 Pos;
    DirectX::SimpleMath::Vector3 Normal;
    DirectX::SimpleMath::Vector2 TexC;
    DirectX::SimpleMath::Vector3 Tangent;
    FLOAT BoneWeights[4];
    BYTE BoneIndices[4];
    //UINT BoneIndices[4]; <--- YOU HAVE CAUSED ME A MONTH OF PAIN
;

很早以前,由于 Luna 使用 BYTE 存储骨骼索引数组感到困惑,我将此结构元素更改为 UINT,认为这仍然与此处显示的输入声明匹配:

static CONST D3D12_INPUT_ELEMENT_DESC inputElementDescSkinned[] =

     "SV_POSITION", 0u, DXGI_FORMAT_R32G32B32_FLOAT, 0u, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0u ,
     "NORMAL", 0u, DXGI_FORMAT_R32G32B32_FLOAT, 0u, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0u ,
     "TEXCOORD", 0u, DXGI_FORMAT_R32G32_FLOAT, 0u, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0u ,
     "TANGENT", 0u, DXGI_FORMAT_R32G32B32_FLOAT, 0u, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0u ,
    // "BINORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 ,
     "BONEWEIGHT", 0u, DXGI_FORMAT_R32G32B32A32_FLOAT, 0u, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0u ,
     "BONEINDEX", 0u, DXGI_FORMAT_R8G8B8A8_UINT, 0u, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0u ,
;

这是错误。通过在骨骼索引的顶点结构中声明UINT,分配了四个字节来存储每个骨骼索引。但是在顶点输入声明中,为“BONEINDEX”指定的DXGI_FORMAT_R8G8B8A8_UINT 格式为每个索引分配一个字节。我怀疑这种数据类型和格式大小不匹配导致只有一个有效的骨骼索引能够适合 BONEINDEX 元素,因此每帧只有一个索引值传递给顶点着色器,而不是四个索引的整个数组正确的骨骼变换查找。

所以现在我已经学会了……艰难的方法……为什么 Luna 为原始 C++ 顶点结构中的骨骼索引声明了一个 BYTE 数组。

我希望这种经验对其他人有价值,并且始终小心更改原始学习资源中的代码。

【讨论】:

以上是关于DirectX 12 中 Assimp 的骨骼动画错误的主要内容,如果未能解决你的问题,请参考以下文章

Skeleton with Assimp 骨骼动画解析

在 Assimp 中获取骨骼动画的变换矩阵

Assimp 动画骨骼变换

CSharpGL(50)使用Assimp加载骨骼动画

一步步学OpenGL(38) -《Assimp库实现骨骼蒙皮动画》

一步步学OpenGL(38) -《Assimp库实现骨骼蒙皮动画》