2

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

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

骨骼0 -> 骨骼1 -> 骨骼2 -> 骨骼3

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

当动画仅通过根骨骼 (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 中验证)。

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

4

2 回答 2

1

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

于 2021-09-14T10:26:38.123 回答
0

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

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

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

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

于 2021-09-23T11:05:11.427 回答