Direct X 12 – Character Animation 角色动画

23.3 顶点融合

上一小节中我们已经展示了动画骨骼。在本小节中我们将学习如何让骨骼的蒙皮也有动画效果哦。而实现这个特性的算法被称为vertex blending(顶点融合)

顶点融合的原理如下。我们已经有了骨头层级结构,但是蒙皮本身是“连续的”网格(例如,我们不会将网格模型分为几个部分,也不会让网格模型与每一根骨头相对应并各自形成动画效果)。此外,一根骨头或者多跟骨头可能会影响同一个顶点;所以顶点的位置最终由对该顶点有影响的所有骨头的final转换矩阵的加权平均值(在制作模型时,美术会设置每一根骨头的权重)。有了这个设置,连接处的顶点才能表现出平滑的融合,或者说,才能让蒙皮看上去是有弹性的“皮肤”;如下图所示。

在Real-Time Rendering Third Edition中提到,一般来说不会存在有超过四根骨头同时影响一个顶点的情况。因此,在代码实现中,我们假设最多只会有4根骨头同时影响一个顶点。角色网格模型的每一个顶点最多包含4个索引,该索引将被用来读取骨头的final转换矩阵数组(每一个索引对应骨骼结构中的一根骨头)。此外,每一个顶点还包含最多4个权重值,分别表示四个会影响该顶点的骨头的权重值。因此,demo中用于顶点融合的顶点结构体如下图所示:

从上图中我们可以看到,右侧的final转换矩阵数组存储了每根骨头的final转换矩阵。结构体中的成员变量BoneIndices表示会影响该顶点的骨头的索引。但我们需要注意的是,顶点不一定会被4根骨头影响;例如,我们只使用两个BoneIndices,也就是说只有两根骨头会影响该顶点。同时我们可以将骨头的权重设为0,这样就免除了骨头对顶点的影响。

如果某个网格模型的顶点格式已经能用于顶点融合,那么我们将其成为蒙皮网格模型(skinned mesh)

顶点v在融合之后变为顶点v’,其位于根节点所在的空间(我们也可以认为顶点v又回到了网格模型的本地空间),其计算公式为:

需要注意的是,w(0)+w(1)+w(2)+w(3)=1。此外,我们在计算融合后的顶点时,分别使用了四根骨头的final转换矩阵将顶点v转换至根节点所在的空间,之后再进行加权平均。

在转换顶点的法线向量与切线向量时,我们也进行类似的操作:

在进行上述转换时,我们假设矩阵F(i)并不包含任何不对称的缩放。否则,我们需要使用该矩阵的逆转置矩阵来转换法线向量(8.2.2小节)。

以下顶点着色器代码展示了如何进行顶点融合,且该顶点最多会被4根骨头影响:

// HLSL
cbuffer cbSkinned : register(b1)
{
    // 每个角色最多有96根骨头
    float4x4 gBoneTransforms[96];
};

struct VertexIn
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 TexC : TEXCOORD;
    float4 TangentL : TANGENT;

#ifdef SKINNED
    float3 BoneWeights : WEIGHTS;
    uint4 BoneIndices : BONEINDICES;
#endif
};

struct VertexOut
{
    float4 PosH : SV_POSITION;
    float4 ShadowPosH : POSITION0;
    float4 SsaoPosH : POSITION1;
    float3 PosW : POSITION2;
    float3 NormalW : NORMAL;
    float3 TangentW : TANGENT;
    float2 TexC : TEXCOORD;
};

VertexOut VS(VertexIn vin)
{
    VertexOut vout = (VertexOut)0.f;

    // 读取材质数据
    MaterialData matData = gMaterialData[gMaterialIndex];

#ifndef SKINNED
    float weights[4] = { 0.f, 0.f, 0.f, 0.f };
    weights[0] = vin.BoneWeights.x;
    weights[1] = vin.BoneWeights.y;
    weights[2] = vin.BoneWeights.z;
    weights[3] = 1.f - weights[0] - weights[1] - weights[2];

    float3 posL = float3(0.f, 0.f, 0.f);
    float3 normalL = float3(0.f, 0.f, 0.f);
    float3 tangentL = float3(0.f, 0.f, 0.f);

    for(int i = 0; i < 4; ++i)
    {
        // 假设我们在进行转换时没有不对称的缩放,
        // 否则需要使用转换矩阵的逆转置矩阵
        posL += weights[i] * mul(float4(vin.PosL, 1.f),
            gBoneTransforms[vin.BoneIndices[i]]).xyz;

        normalL += weights[i] * mul(vin.NormalL,
            (float3x3)gBoneTransforms[vin.BoneIndices[i]));

        tangentL += weights[i] * mul(vin.TangentL.xyz,
            (float3x3)gBoneTransforms[vin.BoneIndices[i]]);
    }

    vin.PosL = posL;
    vin.NormalL = normalL;
    vin.TangentL.xyz = tangentL;
#endif

    // 转换至世界空间
    float4 posW = mul(float4(vin.PosL, 1.f), gWorld);
    vout.PosW = posW.xyz;

    // 假设我们在进行转换时没有不对称的缩放,
    // 否则需要使用转换矩阵的逆转置矩阵
    vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);

    vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);

    // 转换置齐次剪裁空间
    vout.PosH = mul(posW, gViewProj);

    // 生成投影贴图坐标,用以将SSAO贴图映射至场景
    vout.SsaoPosH = mul(posW, gViewProjTex);

    // 输出顶点属性,用以在三角形上进行插值
    float4 texC = mul(float4(vin.TexC, 0.f, 1.f), gTexTransform);
    vout.TexC = mul(texC, matData.MatTransform).xy;

    // 生成投影贴图坐标,用以将阴影贴图映射至场景
    vout.ShadowPosH = mul(posW, gShadowTransform);

    return vout;
}

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据