[DirectX12学习笔记] 模板缓冲

  • 注意!本文是在下几年前入门期间所写(young and naive),其中许多表述可能不正确,为防止误导,请各位读者仔细鉴别。

实现镜子与平面阴影


Depth/Stencil State

每帧渲染开始前我们都应该清除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可以这样求
[DirectX12学习笔记] 模板缓冲_第1张图片注意这样乘完之后w坐标不是1而是n·L,接下来可以不用处理了交给后面硬件完成的齐次除法就可以了,但是有个问题是,n·L一般是负的,所以后面硬件裁切的时候会被裁掉,所以我们这里要用-n·L代替n·L,因为-L和L求得的直线都是同一条,所以交点也不会变。
这个矩阵叫做shadow matrix,点光源的矩阵如下
[DirectX12学习笔记] 模板缓冲_第2张图片注意最后一列和平行光的不一样。
以上两个矩阵可以统一的表示为
[DirectX12学习笔记] 模板缓冲_第3张图片当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;
}

最终效果如图
[DirectX12学习笔记] 模板缓冲_第4张图片

你可能感兴趣的:(DirectX12学习笔记)