前面两小节分别说明了骨骼动画的数学原理和一种骨骼动画格式M3D的读取,这一节讲解渲染的思路。
(1)渲染的思路
其实,进行骨骼动画的渲染,整体思路是:
第一,求出某个动画片段下某一个时间点下所有骨骼的FinalMatrix(由骨骼在某一帧的Pos,Scale,Quat求出)
第二,由于“第一”中渲染骨骼动画的时间点可能不在整数帧数上,比如我假设某个动画片段存在50帧画面,开始时间为0s,结束时间为2.0s,而第48帧画面的开始时间为1.8s,第49帧的画面的开始时间为1.9s,而假设你想渲染的动画时间点为1.85s,所以你想求出所有骨骼的FinalMatrix,就需要进行相应的插值,进行平滑过度。
//得到某个骨头在时间t的变换矩阵
void BoneAnimation::Interpolate(float t, XMFLOAT4X4& M)const
{
if (t < keyFrames.front().TimePos)
{
XMVECTOR S = XMLoadFloat3(&keyFrames.front().Scale); //缩放矩阵
XMVECTOR P = XMLoadFloat3(&keyFrames.front().Translation); //平移矩阵
XMVECTOR Q = XMLoadFloat4(&keyFrames.front().RotationQuat); //旋转四元素
XMVECTOR zero = XMVectorSet(0.0f,0.0f,0.0f,1.0f);
// XMMatrixAffineTransformation的第二个参数为旋转中心
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
else if (t >= keyFrames.back().TimePos)
{
XMVECTOR S = XMLoadFloat3(&keyFrames.back().Scale); //缩放矩阵
XMVECTOR P = XMLoadFloat3(&keyFrames.back().Translation); //平移矩阵
XMVECTOR Q = XMLoadFloat4(&keyFrames.back().RotationQuat); //旋转四元素
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
// XMMatrixAffineTransformation的第二个参数为旋转中心
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
else
{
for (UINT i = 0; i < keyFrames.size() - 1; ++i)
{
if (t > keyFrames[i].TimePos&&t < keyFrames[i + 1].TimePos)
{
float lerpPercent = (t - keyFrames[i].TimePos) / (keyFrames[i+1].TimePos - keyFrames[i].TimePos);
XMVECTOR S0 = XMLoadFloat3(&keyFrames[i].Scale);
XMVECTOR S1 = XMLoadFloat3(&keyFrames[i+1].Scale);
XMVECTOR P0 = XMLoadFloat3(&keyFrames[i].Translation);
XMVECTOR P1 = XMLoadFloat3(&keyFrames[i + 1].Translation);
XMVECTOR Q0 = XMLoadFloat4(&keyFrames[i].RotationQuat);
XMVECTOR Q1 = XMLoadFloat4(&keyFrames[i + 1].RotationQuat);
//对相邻两个帧数的动画的相应变化量进行插值
//缩放量进行插值
XMVECTOR S = XMVectorLerp(S0,S1,lerpPercent);
//移动量进行插值
XMVECTOR P = XMVectorLerp(P0, P1, lerpPercent);
//对旋转量进行插值
XMVECTOR Q = XMVectorLerp(Q0, Q1, lerpPercent);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
}
}
}
//得到所有骨头在时间t的变换矩阵数组,boneTransform[i]代表第i根骨头在时间t的变换矩阵
void AnimationClip::Interpolate(float t, vector<XMFLOAT4X4>& boneTransform)const
{
for (UINT i = 0; i < BoneAnimations.size(); ++i)
{
BoneAnimations[i].Interpolate(t, boneTransform[i]);
}
}
(2)渲染的代码
bool GraphicsClass::RenderSkeletalCharacter(float DeltaTime)
{
// 变换矩阵
XMMATRIX WorldMatrix,ViewMatrix, ProjMatrix;
//限定DeltaTime不能过大,也就是骨骼动画状态不能突变
if (DeltaTime > 1.0f)
{
DeltaTime = 0.0f;
}
//更新第一个人物的骨骼动画时间
mCharacterInstance1.Update(DeltaTime);
/*---------------绘制第一个人物实例(CharacterInstance1)--------------------*/
//第一,(更新)获取ViewMatrix(根据CameraClass的mPostion和mRotation来生成的)
mFirstCameraClass->UpdateViewMatrix();
//第二,获取三个变换矩阵(WorldMatrix和ProjMatrixViewMatrix来自CameraClass)
WorldMatrix =XMLoadFloat4x4(&mCharacterInstance1.World);
ViewMatrix = mFirstCameraClass->GetViewMatrix();
ProjMatrix = mD3D->GetProjMatrix();
//第三,进行渲染(人物顶点数据,索引数据,各种常量缓存,纹理资源等)
for (UINT subset = 0; subset < mCharacterInstance1.Model->SubsetCount; ++subset)
{
/**************绘制人物3DMesh的每个子部分******************/
//设置各种常量缓存资源,纹理资源
mShaderManageClass->RenderSkeletalCharacterShader(mD3D->GetDeviceContext(), mCharacterInstance1.FinalTransforms.size(), WorldMatrix, ViewMatrix, ProjMatrix,
mCharacterInstance1.Model->DiffuseMapSRV[subset], mCharacterInstance1.Model->NormalMapSRV[subset], mLightClass->GetAmbientColor(), mLightClass->GetDiffuseColor(), mLightClass->GetLightDirection(), mFirstCameraClass->GetPositionXM(), &mCharacterInstance1.FinalTransforms[0], mCharacterInstance1.Model->Mat[subset]);
//DrawCall调用
mCharacterInstance1.Model->ModelMesh.Draw(mD3D->GetDeviceContext(), subset);
}
return true;
}
Texture2D DiffuseMap:register(t0); //漫反射纹理资源
Texture2D NormalMap:register(t1); //法线纹理资源
SamplerState SampleType:register(s0); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
};
cbuffer CBLight:register(b1)
{
float4 AmbientLight;
float4 DiffuseLight;
float3 LightDirection;
float pad;
}
cbuffer CBBoneTranform:register(b2)
{
matrix BoneTranforms[80];
}
cbuffer Material : register(b3)
{
float4 Ambient;
float4 Diffuse;
float4 Specular; // w = SpecPower
}
cbuffer Camera : register(b4)
{
float3 CameraPos;
float pad1;
}
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
float4 TangentL : TANGENT;
float3 Weights : WEIGHTS;
uint4 BoneIndices : BONEINDICES;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
float3 W_Normal:NORMAL; //世界空间的法线
float3 W_Pos:POSITION;
float4 W_Tangent:TANGENT; //世界空间的切线
};
float3 NormalSampleToWorldSpace(float3 normalMapSample, float3 unitNormalW, float4 tangentW)
{
//将法线贴图顶点像素颜色大小转为单位向量值
float3 normalT = 2.0f*normalMapSample - 1.0f;
//求出T矩阵
float3 N = unitNormalW;
float3 T = normalize(tangentW.xyz - dot(tangentW.xyz, N)*N);
float3 B = tangentW.w*cross(N, T);
//TBN世界变换矩阵(从切线空间到局部空间再到世界空间)
float3x3 TBN = float3x3(T, B, N);
//位于切线空间的法向量乘以TBN矩阵直接从切线空间变到世界空间
float3 bumpedNormalW = normalize(mul(normalT, TBN));
return bumpedNormalW;
}
VertexOut VS(VertexIn ina)
{
VertexOut outa;
//初始化顶点的骨头权重
float weights[4] = { 0.0f,0.0f,0.0f,0.0f };
weights[0] = ina.Weights.x;
weights[1] = ina.Weights.y;
weights[2] = ina.Weights.z;
weights[3] = 1- ina.Weights.x- ina.Weights.y- ina.Weights.z;
float3 PosL = float3(0.0f, 0.0f, 0.0f);
float3 NormalL= float3(0.0f, 0.0f, 0.0f);
float3 TangentL = float3(0.0f, 0.0f, 0.0f);
//将顶点按骨骼权重比进行相应的骨骼变换
for (int i = 0; i < 4; ++i)
{
PosL += weights[i] * mul(float4(ina.PosL, 1.0f), BoneTranforms[ina.BoneIndices[i]]).xyz;
NormalL+= weights[i] * mul(ina.NormalL, (float3x3)BoneTranforms[ina.BoneIndices[i]]);
TangentL+= weights[i] * mul(ina.TangentL.xyz, (float3x3)BoneTranforms[ina.BoneIndices[i]]);
}
//变换到齐次裁剪空间
//outa.Pos = mul(ina.PosL, World); float3*float4X4 这个并不报错,忽略了分值w为1.0f
outa.Pos = mul(float4(PosL, 1.0f), World);
outa.W_Pos = outa.Pos.xyz;
outa.Pos = mul(outa.Pos, View);
outa.Pos = mul(outa.Pos, Proj);
//法线变换到世界空间
outa.W_Normal = mul(NormalL, (float3x3)WorldInvTranspose); //此事世界逆转置矩阵的第四行本来就没啥用
outa.W_Normal = normalize(outa.W_Normal);
outa.Tex= ina.Tex;
//切线变换到世界空间
outa.W_Tangent = float4(normalize(mul(TangentL, (float3x3)World)), ina.TangentL.w);
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
float DiffuseFactor;
float SpecularFactor;
float4 DiffuseMapColor;
float4 DiffuseColor;
float4 AmbientColor;
float4 SpecualrColor;
float4 color;
float3 NormalMapColor;
DiffuseMapColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
//第一,获取采样颜色
DiffuseMapColor = DiffuseMap.Sample(SampleType, outa.Tex);
//对法线贴图进采样
NormalMapColor=NormalMap.Sample(SampleType, outa.Tex).rgb;
//利用世界空间的法线和切线求出处于法线贴图上的在世界空间的法向量
float3 bumpNormalW = NormalSampleToWorldSpace(NormalMapColor, outa.W_Normal, outa.W_Tangent);
//规格化灯光方向
float3 NormalLightDir = normalize(LightDirection);
//第二,求出DiffuseFactor
float3 InvLightDir = -NormalLightDir;
DiffuseFactor = saturate(dot(InvLightDir, bumpNormalW));
//第三,求出SpecularFactor
float3 ReflectLightDir = reflect(LightDirection, bumpNormalW);
//规格化反射光反向
ReflectLightDir = normalize(ReflectLightDir);
//求出像素到相机的方向
float3 PixelToCameraDir = CameraPos - outa.W_Pos;
//规格化PixelToCameraDir
PixelToCameraDir = normalize(PixelToCameraDir);
SpecularFactor = saturate(pow(dot(PixelToCameraDir, ReflectLightDir), Specular.w));
//第四,求出color
color = saturate(AmbientLight*Ambient + DiffuseFactor*DiffuseLight*Diffuse + SpecularFactor*DiffuseLight*Specular);
color.a = 1.0f;
//第五,用灯光颜色调节纹理颜色
color = color*DiffuseMapColor;
return color;
}
程序运行GIF画面:
(3)碰到的问题
1.人物网格,乘以缩放矩阵时,注意对Z坐标进行翻转(可能是M3D文件格式的数据问题)
//第二十二,实例化SkinModelClass
mCharacterInstance1.Model = mCharacterModel;
mCharacterInstance2.Model = mCharacterModel;
mCharacterInstance1.TimePos = 0.0f;
mCharacterInstance2.TimePos = 0.0f;
mCharacterInstance1.ClipName = "Take1";
mCharacterInstance2.ClipName = "Take1";
mCharacterInstance1.FinalTransforms.resize(mCharacterModel->SkinnedData.BoneCount());
mCharacterInstance2.FinalTransforms.resize(mCharacterModel->SkinnedData.BoneCount());
XMMATRIX modelScale = XMMatrixScaling(0.15f, 0.15f, -0.15f); //
XMMATRIX modelRot = XMMatrixRotationY(0);
XMMATRIX modelOffset = XMMatrixTranslation(55.0f, 10.0f, 36.0f);
XMStoreFloat4x4(&mCharacterInstance1.World, modelScale*modelRot*modelOffset);
XMStoreFloat4x4(&mCharacterInstance2.World, modelScale*modelRot*modelOffset);
若不将坐标进行Z翻转(乘以负数),人物的渲染结果将出现问题,如下所示:
2.访问内存错误的问题
在写骨骼动画渲染的时候,碰上了内存访问错误问题,如下图所示:
3.float3Xfloat4X4的错误情况
在写这个demo的时候。碰上一个到目前为止都没碰上的问题,就是使用float3的PosL乘以float4X4,导致图形完全无法显示,如下所示:outa.Pos = mul(ina.PosL, World)
其中outa.Pos为float4类型, float3*float4X4按道理编译Shader会报错,但实际上并没有报错,导致了outa.Pos.w默认值为0,而让图形无法显示,这种错误有时候真的难以察觉,因为图形无法显示,连Debug VertexShader和PixelShader的机会都没有。
(4)源代码链接: