这篇文章对应的是龙书的最后一章,里面用到的模型和动画文件格式都不是fbx,所以具体怎么读取我们不用去关心,知道怎么用就行了,格式变一下原理还是一样的。
首先骨骼是个树状的存储结构,父骨骼动的时候每个子骨骼都会跟着动,然后每个骨骼有自己的坐标空间,要把点从骨骼空间变换到坐标空间,必须从当前骨骼开始一路向上乘上每个父骨骼的变换矩阵最后才变换到世界空间
但是这样算的话,我们每个骨骼都去乘父骨骼的变换矩阵,就重复算了很多次,如果我们自上而下地计算变换矩阵,就可以省下重复的计算。我用们i表示第i根骨骼,p表示父骨骼,toRoot表示变换到根的变换矩阵,toParent表示到父骨骼空间的变换矩阵,那么有
这样我们就可以自顶向下地计算出每根骨骼到root的变换矩阵。
然后如果要把模型局部坐标系下(也就是绑定空间下)的点变换到已经播放了动画的root空间下,我们要先把点从绑定空间变换到骨骼的空间,再从空间空间变换到root空间,每根骨骼都有自己的空间,我们把将点从绑定空间变换到骨骼的空间的矩阵叫做offset矩阵,从骨骼空间变换到root空间的矩阵则是上面说的toRoot矩阵,如下图所示。
可以看出动画的信息其实是包含在toRoot矩阵里的,offset矩阵则是固定的,也就是根据骨骼相对于绑定空间的位置算出来的,toRoot则是我们在播放动画的时候每帧都要更新的。我们把这两个矩阵的乘积叫做Final矩阵,简记作Fi,如下
然后如果我们要从上至下地计算Final矩阵,我们必须要保证遍历一次骨骼列表的时候父骨骼一定在子骨骼前面,也就是要拓扑排序好的骨骼列表才能这样计算,下面是一个可行的例子
可以看到这种情况下我们是可以直接迭代计算final矩阵的,因为所有父骨骼都比子骨骼更早出现。
现在假如我们有了所有骨骼的Final矩阵,我们要怎么计算顶点位置呢?下面的demo里因为情况简单所以最多只允许一个顶点被四根骨骼影响,如果最多4根骨骼,每根骨骼的权重为wi,权重的和为1,那么顶点当前位置该这样计算
然后法线和切线应该这样变换
这里我们认为只有各向等比例的缩放,实际上如果有不等比例的缩放,我们这里应该乘的是Final矩阵的逆转置。
这里我们用一个m3d文件来实现一个动画demo,这个m3d文件里包含了蒙皮好的模型、骨骼信息、动画,可以用M3DLoader来读取。
接下来给出关键部分的代码。
首先封装一些动画相关的类
struct Keyframe
{
Keyframe();
~Keyframe();
float TimePos;
DirectX::XMFLOAT3 Translation;
DirectX::XMFLOAT3 Scale;
DirectX::XMFLOAT4 RotationQuat;
};
struct BoneAnimation
{
float GetStartTime()const;
float GetEndTime()const;
void Interpolate(float t, DirectX::XMFLOAT4X4& M)const;
std::vector<Keyframe> Keyframes;
};
struct AnimationClip
{
float GetClipStartTime()const;
float GetClipEndTime()const;
void Interpolate(float t, std::vector<DirectX::XMFLOAT4X4>& boneTransforms)const;
std::vector<BoneAnimation> BoneAnimations;
};
class SkinnedData
{
public:
UINT BoneCount()const;
float GetClipStartTime(const std::string& clipName)const;
float GetClipEndTime(const std::string& clipName)const;
void Set(
std::vector<int>& boneHierarchy,
std::vector<DirectX::XMFLOAT4X4>& boneOffsets,
std::unordered_map<std::string, AnimationClip>& animations);
// In a real project, you'd want to cache the result if there was a chance
// that you were calling this several times with the same clipName at
// the same timePos.
void GetFinalTransforms(const std::string& clipName, float timePos,
std::vector<DirectX::XMFLOAT4X4>& finalTransforms)const;
private:
// Gives parentIndex of ith bone.
std::vector<int> mBoneHierarchy;
std::vector<DirectX::XMFLOAT4X4> mBoneOffsets;
std::unordered_map<std::string, AnimationClip> mAnimations;
};
一个BoneAnimation对应的是一个骨骼上的一串动画,一个AnimationClip则是由一组BoneAnimation组成,对应骨骼模型上每根骨骼的动画。而一个SkinnedData则包含了很多个AnimationClip,并且每个有一个字符串名字,存在一个unordered map里,此外,SkinnedData还包含了一个整数数组来存对应父骨骼的标号,-1表示这根骨骼是root骨骼,然后SkinnedData里还存了每根骨骼的Offset矩阵,这个矩阵就是骨骼对应在绑定空间下的变换矩阵。
然后BoneAnimiation插值的实现和上一章一样,而AnimationClip的插值则是循环对这个AnimationClip里的每个BoneAnimation进行插值。
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);
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);
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 = XMQuaternionSlerp(q0, q1, lerpPercent);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
break;
}
}
}
}
void AnimationClip::Interpolate(float t, std::vector<XMFLOAT4X4>& boneTransforms)const
{
for(UINT i = 0; i < BoneAnimations.size(); ++i)
{
BoneAnimations[i].Interpolate(t, boneTransforms[i]);
}
}
然后计算Final矩阵的方式则是从上至下地计算offset*toRoot矩阵。
void SkinnedData::GetFinalTransforms(const std::string& clipName, float timePos, std::vector<XMFLOAT4X4>& finalTransforms)const
{
UINT numBones = mBoneOffsets.size();
std::vector<XMFLOAT4X4> toParentTransforms(numBones);
// Interpolate all the bones of this clip at the given time instance.
auto clip = mAnimations.find(clipName);
clip->second.Interpolate(timePos, toParentTransforms);
//
// Traverse the hierarchy and transform all the bones to the root space.
//
std::vector<XMFLOAT4X4> toRootTransforms(numBones);
// The root bone has index 0. The root bone has no parent, so its toRootTransform
// is just its local bone transform.
toRootTransforms[0] = toParentTransforms[0];
// Now find the toRootTransform of the children.
for(UINT i = 1; i < numBones; ++i)
{
XMMATRIX toParent = XMLoadFloat4x4(&toParentTransforms[i]);
int parentIndex = mBoneHierarchy[i];
XMMATRIX parentToRoot = XMLoadFloat4x4(&toRootTransforms[parentIndex]);
XMMATRIX toRoot = XMMatrixMultiply(toParent, parentToRoot);
XMStoreFloat4x4(&toRootTransforms[i], toRoot);
}
// Premultiply by the bone offset transform to get the final transform.
for(UINT i = 0; i < numBones; ++i)
{
XMMATRIX offset = XMLoadFloat4x4(&mBoneOffsets[i]);
XMMATRIX toRoot = XMLoadFloat4x4(&toRootTransforms[i]);
XMMATRIX finalTransform = XMMatrixMultiply(offset, toRoot);
XMStoreFloat4x4(&finalTransforms[i], XMMatrixTranspose(finalTransform));
}
}
然后再定义一个骨骼模型SkinnedModelInstance,其中包含了一个Skinned Data,还有一个用来存final矩阵的数组,以及更新动画的函数。
struct SkinnedModelInstance
{
SkinnedData* SkinnedInfo = nullptr;
std::vector<DirectX::XMFLOAT4X4> FinalTransforms;
std::string ClipName;
float TimePos = 0.0f;
// Called every frame and increments the time position, interpolates the
// animations for each bone based on the current animation clip, and
// generates the final transforms which are ultimately set to the effect
// for processing in the vertex shader.
void UpdateSkinnedAnimation(float dt)
{
TimePos += dt;
// Loop animation
if(TimePos > SkinnedInfo->GetClipEndTime(ClipName))
TimePos = 0.0f;
// Compute the final transforms for this time position.
SkinnedInfo->GetFinalTransforms(ClipName, TimePos, FinalTransforms);
}
};
接下来看主程序,先初始化,初始化的时候我们用M3DLoader载入骨骼模型。
void SkinnedMeshApp::LoadSkinnedModel()
{
std::vector<M3DLoader::SkinnedVertex> vertices;
std::vector<std::uint16_t> indices;
M3DLoader m3dLoader;
m3dLoader.LoadM3d(mSkinnedModelFilename, vertices, indices,
mSkinnedSubsets, mSkinnedMats, mSkinnedInfo);
mSkinnedModelInst = std::make_unique<SkinnedModelInstance>();
mSkinnedModelInst->SkinnedInfo = &mSkinnedInfo;
mSkinnedModelInst->FinalTransforms.resize(mSkinnedInfo.BoneCount());
mSkinnedModelInst->ClipName = "Take1";
mSkinnedModelInst->TimePos = 0.0f;
const UINT vbByteSize = (UINT)vertices.size() * sizeof(SkinnedVertex);
const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);
auto geo = std::make_unique<MeshGeometry>();
geo->Name = mSkinnedModelFilename;
ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->VertexBufferCPU));
CopyMemory(geo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize);
ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->IndexBufferCPU));
CopyMemory(geo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);
geo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(), vbByteSize, geo->VertexBufferUploader);
geo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(), ibByteSize, geo->IndexBufferUploader);
geo->VertexByteStride = sizeof(SkinnedVertex);
geo->VertexBufferByteSize = vbByteSize;
geo->IndexFormat = DXGI_FORMAT_R16_UINT;
geo->IndexBufferByteSize = ibByteSize;
for(UINT i = 0; i < (UINT)mSkinnedSubsets.size(); ++i)
{
SubmeshGeometry submesh;
std::string name = "sm_" + std::to_string(i);
submesh.IndexCount = (UINT)mSkinnedSubsets[i].FaceCount * 3;
submesh.StartIndexLocation = mSkinnedSubsets[i].FaceStart * 3;
submesh.BaseVertexLocation = 0;
geo->DrawArgs[name] = submesh;
}
mGeometries[geo->Name] = std::move(geo);
}
编译shader的时候,骨骼模型渲染也用的是Default.hlsl,但是要多定义一个SKINNED,具体内容后面shader部分会提到。
void SkinnedMeshApp::BuildShadersAndInputLayout()
{
const D3D_SHADER_MACRO alphaTestDefines[] =
{
"ALPHA_TEST", "1",
NULL, NULL
};
const D3D_SHADER_MACRO skinnedDefines[] =
{
"SKINNED", "1",
NULL, NULL
};
mShaders["standardVS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", nullptr, "VS", "vs_5_1");
mShaders["skinnedVS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", skinnedDefines, "VS", "vs_5_1");
mShaders["opaquePS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", nullptr, "PS", "ps_5_1");
mShaders["shadowVS"] = d3dUtil::CompileShader(L"Shaders\\Shadows.hlsl", nullptr, "VS", "vs_5_1");
mShaders["skinnedShadowVS"] = d3dUtil::CompileShader(L"Shaders\\Shadows.hlsl", skinnedDefines, "VS", "vs_5_1");
mShaders["shadowOpaquePS"] = d3dUtil::CompileShader(L"Shaders\\Shadows.hlsl", nullptr, "PS", "ps_5_1");
mShaders["shadowAlphaTestedPS"] = d3dUtil::CompileShader(L"Shaders\\Shadows.hlsl", alphaTestDefines, "PS", "ps_5_1");
mShaders["debugVS"] = d3dUtil::CompileShader(L"Shaders\\ShadowDebug.hlsl", nullptr, "VS", "vs_5_1");
mShaders["debugPS"] = d3dUtil::CompileShader(L"Shaders\\ShadowDebug.hlsl", nullptr, "PS", "ps_5_1");
mShaders["drawNormalsVS"] = d3dUtil::CompileShader(L"Shaders\\DrawNormals.hlsl", nullptr, "VS", "vs_5_1");
mShaders["skinnedDrawNormalsVS"] = d3dUtil::CompileShader(L"Shaders\\DrawNormals.hlsl", skinnedDefines, "VS", "vs_5_1");
mShaders["drawNormalsPS"] = d3dUtil::CompileShader(L"Shaders\\DrawNormals.hlsl", nullptr, "PS", "ps_5_1");
mShaders["ssaoVS"] = d3dUtil::CompileShader(L"Shaders\\Ssao.hlsl", nullptr, "VS", "vs_5_1");
mShaders["ssaoPS"] = d3dUtil::CompileShader(L"Shaders\\Ssao.hlsl", nullptr, "PS", "ps_5_1");
mShaders["ssaoBlurVS"] = d3dUtil::CompileShader(L"Shaders\\SsaoBlur.hlsl", nullptr, "VS", "vs_5_1");
mShaders["ssaoBlurPS"] = d3dUtil::CompileShader(L"Shaders\\SsaoBlur.hlsl", nullptr, "PS", "ps_5_1");
mShaders["skyVS"] = d3dUtil::CompileShader(L"Shaders\\Sky.hlsl", nullptr, "VS", "vs_5_1");
mShaders["skyPS"] = d3dUtil::CompileShader(L"Shaders\\Sky.hlsl", nullptr, "PS", "ps_5_1");
mInputLayout =
{
{
"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
mSkinnedInputLayout =
{
{
"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"WEIGHTS", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 44, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{
"BONEINDICES", 0, DXGI_FORMAT_R8G8B8A8_UINT, 0, 56, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
}
然后创建RenderItem的时候,骨骼模型上每个材质对应一个submesh,要单独创建一个render item,也就是说一个骨骼模型由很多个render item构成。
for(UINT i = 0; i < mSkinnedMats.size(); ++i)
{
std::string submeshName = "sm_" + std::to_string(i);
auto ritem = std::make_unique<RenderItem>();
// Reflect to change coordinate system from the RHS the data was exported out as.
XMMATRIX modelScale = XMMatrixScaling(0.05f, 0.05f, -0.05f);
XMMATRIX modelRot = XMMatrixRotationY(MathHelper::Pi);
XMMATRIX modelOffset = XMMatrixTranslation(0.0f, 0.0f, -5.0f);
XMStoreFloat4x4(&ritem->World, modelScale*modelRot*modelOffset);
ritem->TexTransform = MathHelper::Identity4x4();
ritem->ObjCBIndex = objCBIndex++;
ritem->Mat = mMaterials[mSkinnedMats[i].Name].get();
ritem->Geo = mGeometries[mSkinnedModelFilename].get();
ritem->PrimitiveType = D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
ritem->IndexCount = ritem->Geo->DrawArgs[submeshName].IndexCount;
ritem->StartIndexLocation = ritem->Geo->DrawArgs[submeshName].StartIndexLocation;
ritem->BaseVertexLocation = ritem->Geo->DrawArgs[submeshName].BaseVertexLocation;
// All render items for this solider.m3d instance share
// the same skinned model instance.
ritem->SkinnedCBIndex = 0;
ritem->SkinnedModelInst = mSkinnedModelInst.get();
mRitemLayer[(int)RenderLayer::SkinnedOpaque].push_back(ritem.get());
mAllRitems.push_back(std::move(ritem));
}
然后骨骼模型单独用了一个层,即SkinnedOpaque层,因为我们要换PSO,所以不能和opaque一起渲染。
然后是创建渲染骨骼模型的PSO。
//
// PSO for skinned pass.
//
D3D12_GRAPHICS_PIPELINE_STATE_DESC skinnedOpaquePsoDesc = opaquePsoDesc;
skinnedOpaquePsoDesc.InputLayout = {
mSkinnedInputLayout.data(), (UINT)mSkinnedInputLayout.size() };
skinnedOpaquePsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["skinnedVS"]->GetBufferPointer()),
mShaders["skinnedVS"]->GetBufferSize()
};
skinnedOpaquePsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["opaquePS"]->GetBufferPointer()),
mShaders["opaquePS"]->GetBufferSize()
};
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&skinnedOpaquePsoDesc, IID_PPV_ARGS(&mPSOs["skinnedOpaque"])));
然后在Update里面更新SkinnedPass的cb,即计算好Final矩阵然后写进skinned cb里。
void SkinnedMeshApp::UpdateSkinnedCBs(const GameTimer& gt)
{
auto currSkinnedCB = mCurrFrameResource->SkinnedCB.get();
// We only have one skinned model being animated.
mSkinnedModelInst->UpdateSkinnedAnimation(gt.DeltaTime());
SkinnedConstants skinnedConstants;
std::copy(
std::begin(mSkinnedModelInst->FinalTransforms),
std::end(mSkinnedModelInst->FinalTransforms),
&skinnedConstants.BoneTransforms[0]);
currSkinnedCB->CopyData(0, skinnedConstants);
}
然后draw部分,用skinnedOpaque的PSO渲染SkinnedOpaque层。
mCommandList->SetPipelineState(mPSOs["skinnedOpaque"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::SkinnedOpaque]);
最后是完整的shader代码。
// Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif
// Include common HLSL code.
#include "Common.hlsl"
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
float3 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.0f;
// Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex];
#ifdef SKINNED
float weights[4] = {
0.0f, 0.0f, 0.0f, 0.0f };
weights[0] = vin.BoneWeights.x;
weights[1] = vin.BoneWeights.y;
weights[2] = vin.BoneWeights.z;
weights[3] = 1.0f - weights[0] - weights[1] - weights[2];
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)
{
// Assume no nonuniform scaling when transforming normals, so
// that we do not have to use the inverse-transpose.
posL += weights[i] * mul(float4(vin.PosL, 1.0f), 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
// Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosW = posW.xyz;
// Assumes nonuniform scaling; otherwise, need to use inverse-transpose of world matrix.
vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);
// Transform to homogeneous clip space.
vout.PosH = mul(posW, gViewProj);
// Generate projective tex-coords to project SSAO map onto scene.
vout.SsaoPosH = mul(posW, gViewProjTex);
// Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, matData.MatTransform).xy;
// Generate projective tex-coords to project shadow map onto scene.
vout.ShadowPosH = mul(posW, gShadowTransform);
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
// Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex];
float4 diffuseAlbedo = matData.DiffuseAlbedo;
float3 fresnelR0 = matData.FresnelR0;
float roughness = matData.Roughness;
uint diffuseMapIndex = matData.DiffuseMapIndex;
uint normalMapIndex = matData.NormalMapIndex;
// Dynamically look up the texture in the array.
diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
#ifdef ALPHA_TEST
// Discard pixel if texture alpha < 0.1. We do this test as soon
// as possible in the shader so that we can potentially exit the
// shader early, thereby skipping the rest of the shader code.
clip(diffuseAlbedo.a - 0.1f);
#endif
// Interpolating normal can unnormalize it, so renormalize it.
pin.NormalW = normalize(pin.NormalW);
float4 normalMapSample = gTextureMaps[normalMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample.rgb, pin.NormalW, pin.TangentW);
// Uncomment to turn off normal mapping.
//bumpedNormalW = pin.NormalW;
// Vector from point being lit to eye.
float3 toEyeW = normalize(gEyePosW - pin.PosW);
// Finish texture projection and sample SSAO map.
pin.SsaoPosH /= pin.SsaoPosH.w;
float ambientAccess = gSsaoMap.Sample(gsamLinearClamp, pin.SsaoPosH.xy, 0.0f).r;
// Light terms.
float4 ambient = ambientAccess*gAmbientLight*diffuseAlbedo;
// Only the first light casts a shadow.
float3 shadowFactor = float3(1.0f, 1.0f, 1.0f);
shadowFactor[0] = CalcShadowFactor(pin.ShadowPosH);
const float shininess = (1.0f - roughness) * normalMapSample.a;
Material mat = {
diffuseAlbedo, fresnelR0, shininess };
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
bumpedNormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight;
// Add in specular reflections.
float3 r = reflect(-toEyeW, bumpedNormalW);
float4 reflectionColor = gCubeMap.Sample(gsamLinearWrap, r);
float3 fresnelFactor = SchlickFresnel(fresnelR0, bumpedNormalW, r);
litColor.rgb += shininess * fresnelFactor * reflectionColor.rgb;
// Common convention to take alpha from diffuse albedo.
litColor.a = diffuseAlbedo.a;
return litColor;
}
注意在VS里,如果定义了SKINNED,就会先根据权重和对应骨骼的final矩阵来把顶点变换到当前动画的位置(局部空间),然后再和普通的opaque物体一样正常渲染。
程序运行效果如图(这模型真不是一般的丑):