第二十章 Skeletal Animation
Skeletal animation(骨骼动画)是指互相连接的变换(骨头)组成的分层集合,以及对应的模型mesh(即骨骼的皮肤)。当这些变换随着时间变化而变化时,模型的mesh就会形成动画效果。本章,我们将会探讨skeketal animation,并开发一些系统用于支持模型动画。
Skinning
把一个skeleton映射到一个mesh的过程称为skinning(换肤)。这项工作主要由一个美术设计师完成,并涉及到与具体bone关联的顶点。一个vertex可以对应于多个bone(通常情况下最多可以有4个),每一个bone使用一个给定的数量值对vertex产生影响(比如,结合每一个bone的影响)。因此,vertex的最终坐标位置由所有对应的bones加权平均计算得到。术语skinning还可以用于表示在运行时vertices如何进行变换。CPU skinning表示使用CPU执行vertices的变换运算,而GPU skinning则表示使用GPU执行变换运算。尽管GPU skinning对可用的bones数量有限制,但是一般情况下都比CPU skinning要快的多。列表20.1列出一个GPU skinning shader代码。
列表20.1 The SkinnedModel.fx Shader
#include "include\\Common.fxh"
#define MaxBones 60
/************* Resources *************/
cbuffer CBufferPerFrame
{
float4 AmbientColor = { 1.0f, 1.0f, 1.0f, 0.0f };
float4 LightColor = { 1.0f, 1.0f, 1.0f, 1.0f };
float3 LightPosition = { 0.0f, 0.0f, 0.0f };
float LightRadius = 10.0f;
float3 CameraPosition;
}
cbuffer CBufferPerObject
{
float4x4 WorldViewProjection : WORLDVIEWPROJECTION;
float4x4 World : WORLD;
float4 SpecularColor : SPECULAR = { 1.0f, 1.0f, 1.0f, 1.0f };
float SpecularPower : SPECULARPOWER = 25.0f;
}
cbuffer CBufferSkinning
{
float4x4 BoneTransforms[MaxBones];
}
Texture2D ColorTexture;
SamplerState ColorSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
/************* Data Structures *************/
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
float3 Normal : NORMAL;
uint4 BoneIndices : BONEINDICES;
float4 BoneWeights : WEIGHTS;
};
struct VS_OUTPUT
{
float4 Position : SV_Position;
float3 Normal : NORMAL;
float2 TextureCoordinate : TEXCOORD0;
float3 WorldPosition : TEXCOORD1;
float Attenuation : TEXCOORD2;
};
/************* Vertex Shader *************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
VS_OUTPUT OUT = (VS_OUTPUT)0;
float4x4 skinTransform = (float4x4)0;
skinTransform += BoneTransforms[IN.BoneIndices.x] * IN.BoneWeights.x;
skinTransform += BoneTransforms[IN.BoneIndices.y] * IN.BoneWeights.y;
skinTransform += BoneTransforms[IN.BoneIndices.z] * IN.BoneWeights.z;
skinTransform += BoneTransforms[IN.BoneIndices.w] * IN.BoneWeights.w;
float4 position = mul(IN.ObjectPosition, skinTransform);
OUT.Position = mul(position, WorldViewProjection);
OUT.WorldPosition = mul(position, World).xyz;
float4 normal = mul(float4(IN.Normal, 0), skinTransform);
OUT.Normal = normalize(mul(normal, World).xyz);
OUT.TextureCoordinate = IN.TextureCoordinate;
float3 lightDirection = LightPosition - OUT.WorldPosition;
OUT.Attenuation = saturate(1.0f - (length(lightDirection) / LightRadius));
return OUT;
}
/************* Pixel Shaders *************/
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 OUT = (float4)0;
float3 lightDirection = LightPosition - IN.WorldPosition;
lightDirection = normalize(lightDirection);
float3 viewDirection = normalize(CameraPosition - IN.WorldPosition);
float3 normal = normalize(IN.Normal);
float n_dot_l = dot(normal, lightDirection);
float3 halfVector = normalize(lightDirection + viewDirection);
float n_dot_h = dot(normal, halfVector);
float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
float4 lightCoefficients = lit(n_dot_l, n_dot_h, SpecularPower);
float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb);
float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb) * IN.Attenuation;
float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w)) * IN.Attenuation;
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
/************* Techniques *************/
technique11 main11
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, pixel_shader()));
}
}
该shader只是在一个point light shader的基础上增加了对skinned models的支持。所有关于skinning的操作都在vertex shader执行,执行过程中主要是使用了新的shader变量BoneTransforms,以及vertex shader输入参数中的BoneIndices和BoneWeights变量。其中,变量BoneTransforms是一个矩阵数组,包含模型骨骼中每一个bone的变换矩阵。VS_INPUT结构体中的BoneIndices变量是一个含有4个无符号整形数的数组(uint4类型),数组中的每一个元素表示访问BoneTransforms数组的索引。此外,BoneWeight变量主要用于vertex对应多个bones时,对所有影响vertex的bones进行加权平均。
在vertex shader中产生使用以下的代码构建一个矩阵skinTransform:
float4x4 skinTransform = (float4x4)0;
skinTransform += BoneTransforms[IN.BoneIndices.x] * IN.BoneWeights.x;
skinTransform += BoneTransforms[IN.BoneIndices.y] * IN.BoneWeights.y;
skinTransform += BoneTransforms[IN.BoneIndices.z] * IN.BoneWeights.z;
skinTransform += BoneTransforms[IN.BoneIndices.w] * IN.BoneWeights.w;
如果某个bone不会对vertex产生影响,那么该bone的权重就为0,因此在计算矩阵skinTransform时就会去掉该bone的Transform值。在CPU端的应用程序中需要保证与vertex关联的所有bones的权重和为1。
然后,把vertex position与矩阵skinTransform相乘(同样,如果vertex中含有surface normal,tangent,以及binormal也需要与该矩阵相乘)。通过矩阵乘法变换之后,bone的矩阵依然位于object的局部坐标系,因此再把position与WorldVeiwProjection矩阵相乘,使得在下一个shader阶段之前把vertex从animated local space变换到homogenous clip space。
Pixel shader的代码与原始的point light pixel shader完全一样。实际上,可以把本书中所有的lighting techniques应用到skinned models中,只需要像vertex shader讨论的那样,增加一些与bone相关的shader变量和输入。