每帧渲染开始前我们都应该清除depth和stencil buffer到我们指定的值,例如
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
第二个参数是我们要清除的buffer,第三个参数是我们depth buffer清除到的值,第四个则是stencil buffer要清除到的值,第五个参数是pRects数组的长度,第六个参数pRects数组是一系列矩形区域,传入这些矩形区域可以指定要清空的区域,如果传入nullptr则是清空整个depth/stencil buffer。
if( StencilRef & StencilReadMask ⊴ Value &StencilReadMask)
accept pixel
else
reject pixel
Value是现在存储的值,StencilRef是我们要传入的值,这两个值读取的时候都要通过一个StencilReadMask,然后中间的运算符号也有我们规定,有以下取值
typedef enum D3D11_COMPARISON_FUNC { D3D11_COMPARISON_NEVER = 1,
D3D11_COMPARISON_LESS = 2,
D3D11_COMPARISON_EQUAL = 3,
D3D11_COMPARISON_LESS_EQUAL = 4,
D3D11_COMPARISON_GREATER = 5,
D3D11_COMPARISON_NOT_EQUAL = 6,
D3D11_COMPARISON_GREATER_EQUAL = 7,
D3D11_COMPARISON_ALWAYS = 8
} D3D11_COMPARISON_FUNC;
在创建PSO的时候会要填一个D3D12_DEPTH_STENCIL_DESC,这里面包含的内容如下
typedef struct D3D12_DEPTH_STENCIL_DESC {
BOOL DepthEnable;
D3D12_DEPTH_WRITE_MASK DepthWriteMask;
D3D12_COMPARISON_FUNC DepthFunc;
BOOL StencilEnable;
UINT8 StencilReadMask;
UINT8 StencilWriteMask;
D3D12_DEPTH_STENCILOP_DESC FrontFace;
D3D12_DEPTH_STENCILOP_DESC BackFace;
} D3D12_DEPTH_STENCIL_DESC;
第一个参数是启用深度缓冲,第二个是写入掩码,第三个是深度比较的函数,第四个是是否启用模板缓冲,第五个和第六个是读取和写入的掩码,第七个和第八个是正面朝向的面和反面朝向的面的Stencil Op Desc,可能的取值如下
typedef enum D3D12_STENCIL_OP {
D3D12_STENCIL_OP_KEEP,
D3D12_STENCIL_OP_ZERO,
D3D12_STENCIL_OP_REPLACE,
D3D12_STENCIL_OP_INCR_SAT,
D3D12_STENCIL_OP_DECR_SAT,
D3D12_STENCIL_OP_INVERT,
D3D12_STENCIL_OP_INCR,
D3D12_STENCIL_OP_DECR
} ;
如果要传入StencilRef的值的话,可以这样
mCommandList->OMSetStencilRef(1);
虚幻里的镜子是专门给镜子创建一个摄像机来实现,这里利用stencil buffer来实现另一种镜子,就是把镜子里要反射的东西全部根据镜子对称一次(包括光源),第一次渲染只写入到stencil buffer,先清零stencil buffer,把屏幕上没被挡住的区域(即通过深度测试的部分)的stencil buffer值设置为1,第二次渲染的时候把要反射的物体正常渲染,不过要打开stencil test,把stencil buffer里为1的部分渲染出来即可,在这之后,再把镜子以透明度方式渲染出来,盖在反射出来的图像上,就看起来像一面镜子了,具体代码和阴影部分一并写在下方。
这是一种最简单的阴影算法,给定一个平面和光的位置,让阴影只投射在这个平面上,首先考虑平行光,给定平行光方向和一个点的位置,可以确定一条线,给定一个平面的位置,就可以求出线和平面的交点,假如要被投影的点是p,入射方向是L,面的法线是n,面的偏移常量是d,(n,d)可以表示一个面,然后交点s可以这样求
注意这样乘完之后w坐标不是1而是n·L,接下来可以不用处理了交给后面硬件完成的齐次除法就可以了,但是有个问题是,n·L一般是负的,所以后面硬件裁切的时候会被裁掉,所以我们这里要用-n·L代替n·L,因为-L和L求得的直线都是同一条,所以交点也不会变。
这个矩阵叫做shadow matrix,点光源的矩阵如下
注意最后一列和平行光的不一样。
以上两个矩阵可以统一的表示为
当Lw为0时表示平行光,Lw为1时表示点光源。
获取Shadow Matrix的方法如下
inline XMMATRIX XM_CALLCONV XMMatrixShadow(
FXMVECTOR ShadowPlane,
FXMVECTOR LightPosition);
直接利用shadow matrix来变换算出阴影顶点位置的话,会有个问题,那就是如果一个曲面是闭合的,那么这个曲面一定会有多个三角面被投影到平面上的同一个位置,这样就会出现两倍的阴影,会变得很黑,称为阴影的double blending,要正常渲染平面阴影,可以使用stencil buffer,首先清零stencil buffer,然后设置stencil buffer成只接受0,然后如果pass了stencil test,就设置这一位为1,这样的话如果有一个像素算了两次阴影,第二次不会被渲染出来。
这里只列出关键部分的代码。
首先阴影是要一个材质的,可以设置为全黑然后透明度0.5。
auto shadowMat = std::make_unique<Material>();
shadowMat->Name = "shadowMat";
shadowMat->MatCBIndex = 4;
shadowMat->DiffuseSrvHeapIndex = 3;
shadowMat->DiffuseAlbedo = XMFLOAT4(0.0f, 0.0f, 0.0f, 0.5f);
shadowMat->FresnelR0 = XMFLOAT3(0.001f, 0.001f, 0.001f);
shadowMat->Roughness = 0.0f;
然后场景中的骷髅头要三个render item,一个是本身,一个是镜子里的,另一个是阴影(材质不一样、cb不一样、render layer也不一样)。
auto skullRitem = std::make_unique<RenderItem>();
skullRitem->World = MathHelper::Identity4x4();
skullRitem->TexTransform = MathHelper::Identity4x4();
skullRitem->ObjCBIndex = 2;
skullRitem->Mat = mMaterials["skullMat"].get();
skullRitem->Geo = mGeometries["skullGeo"].get();
skullRitem->PrimitiveType = D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
skullRitem->IndexCount = skullRitem->Geo->DrawArgs["skull"].IndexCount;
skullRitem->StartIndexLocation = skullRitem->Geo->DrawArgs["skull"].StartIndexLocation;
skullRitem->BaseVertexLocation = skullRitem->Geo->DrawArgs["skull"].BaseVertexLocation;
mSkullRitem = skullRitem.get();
mRitemLayer[(int)RenderLayer::Opaque].push_back(skullRitem.get());
// Reflected skull will have different world matrix, so it needs to be its own render item.
auto reflectedSkullRitem = std::make_unique<RenderItem>();
*reflectedSkullRitem = *skullRitem;
reflectedSkullRitem->ObjCBIndex = 3;
mReflectedSkullRitem = reflectedSkullRitem.get();
mRitemLayer[(int)RenderLayer::Reflected].push_back(reflectedSkullRitem.get());
// Shadowed skull will have different world matrix, so it needs to be its own render item.
auto shadowedSkullRitem = std::make_unique<RenderItem>();
*shadowedSkullRitem = *skullRitem;
shadowedSkullRitem->ObjCBIndex = 4;
shadowedSkullRitem->Mat = mMaterials["shadowMat"].get();
mShadowedSkullRitem = shadowedSkullRitem.get();
mRitemLayer[(int)RenderLayer::Shadow].push_back(shadowedSkullRitem.get());
然后是创建PSO,除了opaque和transparent以外,还要了另外三个PSO,一个是设置Stencil来标记镜子中可以渲染的部分的PSO,一个是渲染镜子里的物体时,用来做stencil test的PSO,还有一个是阴影的PSO。
//
// PSO for marking stencil mirrors.
//
CD3DX12_BLEND_DESC mirrorBlendState(D3D12_DEFAULT);
mirrorBlendState.RenderTarget[0].RenderTargetWriteMask = 0;
D3D12_DEPTH_STENCIL_DESC mirrorDSS;
mirrorDSS.DepthEnable = true;
mirrorDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
mirrorDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
mirrorDSS.StencilEnable = true;
mirrorDSS.StencilReadMask = 0xff;
mirrorDSS.StencilWriteMask = 0xff;
mirrorDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
mirrorDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
// We are not rendering backfacing polygons, so these settings do not matter.
mirrorDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
mirrorDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
D3D12_GRAPHICS_PIPELINE_STATE_DESC markMirrorsPsoDesc = opaquePsoDesc;
markMirrorsPsoDesc.BlendState = mirrorBlendState;
markMirrorsPsoDesc.DepthStencilState = mirrorDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&markMirrorsPsoDesc, IID_PPV_ARGS(&mPSOs["markStencilMirrors"])));
//
// PSO for stencil reflections.
//
D3D12_DEPTH_STENCIL_DESC reflectionsDSS;
reflectionsDSS.DepthEnable = true;
reflectionsDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
reflectionsDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
reflectionsDSS.StencilEnable = true;
reflectionsDSS.StencilReadMask = 0xff;
reflectionsDSS.StencilWriteMask = 0xff;
reflectionsDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
reflectionsDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
D3D12_GRAPHICS_PIPELINE_STATE_DESC drawReflectionsPsoDesc = opaquePsoDesc;
drawReflectionsPsoDesc.DepthStencilState = reflectionsDSS;
drawReflectionsPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&drawReflectionsPsoDesc, IID_PPV_ARGS(&mPSOs["drawStencilReflections"])));
//
// PSO for shadow objects
//
// We are going to draw shadows with transparency, so base it off the transparency description.
D3D12_DEPTH_STENCIL_DESC shadowDSS;
shadowDSS.DepthEnable = true;
shadowDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
shadowDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
shadowDSS.StencilEnable = true;
shadowDSS.StencilReadMask = 0xff;
shadowDSS.StencilWriteMask = 0xff;
shadowDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
shadowDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
shadowDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
shadowDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
D3D12_GRAPHICS_PIPELINE_STATE_DESC shadowPsoDesc = transparentPsoDesc;
shadowPsoDesc.DepthStencilState = shadowDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&shadowPsoDesc, IID_PPV_ARGS(&mPSOs["shadow"])));
然后Update传入矩阵的时候,镜子里的物体接受的光照也不一样,所以要反过来
void StencilApp::UpdateReflectedPassCB(const GameTimer& gt)
{
mReflectedPassCB = mMainPassCB;
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
// Reflect the lighting.
for(int i = 0; i < 3; ++i)
{
XMVECTOR lightDir = XMLoadFloat3(&mMainPassCB.Lights[i].Direction);
XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
XMStoreFloat3(&mReflectedPassCB.Lights[i].Direction, reflectedLightDir);
}
// Reflected pass stored in index 1
auto currPassCB = mCurrFrameResource->PassCB.get();
currPassCB->CopyData(1, mReflectedPassCB);
}
最后是draw,注意每个draw call之前的参数改变,比如设置stencil ref值为1或者改变pass cb(把光线反过来)等等。
可以看到是先渲染不透明物体,再是用stencil标记没被挡住的镜子,再渲染镜子里的物体,渲染完后把cb和ref参数恢复成main pass正常的参数,再渲染透明的镜子和骷髅头的影子。
// Draw opaque items--floors, walls, skull.
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
// Mark the visible mirror pixels in the stencil buffer with the value 1
mCommandList->OMSetStencilRef(1);
mCommandList->SetPipelineState(mPSOs["markStencilMirrors"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
// Draw the reflection into the mirror only (only for pixels where the stencil buffer is 1).
// Note that we must supply a different per-pass constant buffer--one with the lights reflected.
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
mCommandList->SetPipelineState(mPSOs["drawStencilReflections"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
// Restore main pass constants and stencil ref.
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
mCommandList->OMSetStencilRef(0);
// Draw mirror with transparency so reflection blends through.
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
// Draw shadows
mCommandList->SetPipelineState(mPSOs["shadow"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Shadow]);
然后把头骨的世界矩阵更新写在OnKeyboardInput里面了,可以用WASD控制头骨的位置
void StencilApp::OnKeyboardInput(const GameTimer& gt)
{
//
// Allow user to move skull.
//
const float dt = gt.DeltaTime();
if(GetAsyncKeyState('A') & 0x8000)
mSkullTranslation.x -= 1.0f*dt;
if(GetAsyncKeyState('D') & 0x8000)
mSkullTranslation.x += 1.0f*dt;
if(GetAsyncKeyState('W') & 0x8000)
mSkullTranslation.y += 1.0f*dt;
if(GetAsyncKeyState('S') & 0x8000)
mSkullTranslation.y -= 1.0f*dt;
// Don't let user move below ground plane.
mSkullTranslation.y = MathHelper::Max(mSkullTranslation.y, 0.0f);
// Update the new world matrix.
XMMATRIX skullRotate = XMMatrixRotationY(0.5f*MathHelper::Pi);
XMMATRIX skullScale = XMMatrixScaling(0.45f, 0.45f, 0.45f);
XMMATRIX skullOffset = XMMatrixTranslation(mSkullTranslation.x, mSkullTranslation.y, mSkullTranslation.z);
XMMATRIX skullWorld = skullRotate*skullScale*skullOffset;
XMStoreFloat4x4(&mSkullRitem->World, skullWorld);
// Update reflection world matrix.
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
XMStoreFloat4x4(&mReflectedSkullRitem->World, skullWorld * R);
// Update shadow world matrix.
XMVECTOR shadowPlane = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // xz plane
XMVECTOR toMainLight = -XMLoadFloat3(&mMainPassCB.Lights[0].Direction);
XMMATRIX S = XMMatrixShadow(shadowPlane, toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f, 0.001f, 0.0f);
XMStoreFloat4x4(&mShadowedSkullRitem->World, skullWorld * S * shadowOffsetY);
mSkullRitem->NumFramesDirty = gNumFrameResources;
mReflectedSkullRitem->NumFramesDirty = gNumFrameResources;
mShadowedSkullRitem->NumFramesDirty = gNumFrameResources;
}