首先声明,本人是自学DX12,有很多的理解也许不到位,不过都是自己的理解。在很长一段时间里边,我从迷茫到有一天开始能看懂,现在是第三次开始刷DX12了,于是在此表明写作的初衷:
1.有一些DX12的学习心得,希望发出来,有大佬如果愿意指教,万分感谢;
2.如果对于才入门的人来说,这可能是我的白话教程,也许会对你有所帮助,但不可尽信,因为我也不确定我对不对;
3.DX12的概念很多,也是想把这作为自己的学习笔记来做,希望对自己也有帮助,如果有一天我发现哪里错了会及时回来更正。
那么话不多说,现在开始!!!
一、什么叫模板
这里我先暂时不进入正题,先对模板建立一个概念我觉得比直接了解正题更加重要,这会帮助你建立一个可类比的感性认识。
模板就好像一张面具,戴在脸上,你可以说想让别人看你面具,或者说看不被面具修饰的部分,总而言之就是将一个可观察的面加以划分,分为哪些可见,哪些不可见,用D3D12的原话表述:使用模板来组织特定的像素片段渲染到后台缓冲。
模板缓冲是一个大小与RTV,和深度缓冲一致的资源,因此可以将像素一一对应起来。我们不妨这样想:RTV相当于我们的脸,而模板缓冲相当于一张面膜,面膜完美的覆盖了表面的肌肤,但是还是留有呼吸的口子。如果此时,你打翻了一盒墨水,那么不被面膜覆盖的地方将立马变色(因为面膜覆盖在表面深度小于脸部,所以模板与深度缓冲是配合使用的,所以我们一般也称之为DSV),如此一来,你可以说面膜阻止了墨水对你脸部的染色,或者说你允许墨水滋润你部分肌肤。不同角度的说法,但是总之,他就是只对一个主体的部门进行阻碍或者通行。
二、模板的使用
1.每帧重置
因为我们在初始化阶段就创建了DSV了,并且在每帧的开始重置DSV:
mCommandList->ClearDepthStencilView(DepthStencilView(),//DSV
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, //与深度一起的清理重置
1.0f,//depth
0,//模板初始值
0,//Rect区域数目
nullptr);//区域
每帧重置,属于是新的一样了。
2.PSO配置
然后,我们就要在PSO的模板属性中进行配置,以让流水线按照我们设置的状态渲染场景Item,这都是硬件自己做的,我们要做的事就是理清绘制逻辑,填写参数,如下:
绘制逻辑:
1).先确定哪个几何体模型需要镜像,然后先绘制本体;
2).用镜子范围几何体作为渲染项,禁用RTV(不真实渲染镜子)和深度缓冲的写入(为以后绘制在镜子后边的物体做准备),允许深度测试(剔除挡在镜子前边的几何体),仅修改模板缓冲,设置此时进行模板测试为从初始值0替换为标记值(mCommandList->OMSetStencilRef(1)),对于通过模板测试的部分设置为系统设置值进行标记(此时原始全为0的模板缓冲就标记了部分系统设置值,给出了一个我们可见的镜子范围);
3).将需要镜像的几何体使用坐标变换到镜子内侧(镜像),进行绘制,由于我们之前禁用了深度写入,所以这个地方不会被视为被镜子挡住的,并设置此时的模板测试为等于标记值1,那么也就是只有镜子范围内可见区域的像素才会被后续像素处理,否则抛弃对该像素的处理。所谓的像素处理就是进入像素着色器生成镜像;
4).最后我们绘制镜子,将镜子作为透明体来绘制,则可以把镜子后边的dst混合到镜子表面上,并且深度缓冲开启情况下自动丢弃了原始对镜像几何体的绘制。
原理如上,来看看代码和解析吧:
//绘制不透明物,待镜像的几何体
ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["opaque"].Get()));
.....
//清理DSV
mCommandList->ClearDepthStencilView(DepthStencilView(),//DSV
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f,0,0,nullptr);
......
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
//标记模板
mCommandList->SetPipelineState(mPSOs["markStencilMirrors"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
对于mPSOs["markStencilMirrors"]设置如下:
{
//获得我们可见的镜子像素范围
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;
//使用设置的模板参考值进行替换,相当于从整个DSV初值中用0,1标记镜子范围
mirrorDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
//每次都成功,保证完全记录镜子的模板像素范围,让所有通过测试的都变为设置模板值
mirrorDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
// 背面,一般剔除背面,所以不关心
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"])));
}
//绘制镜子后边的镜像
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
mCommandList->SetPipelineState(mPSOs["drawStencilReflections"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
对于mPSOs["drawStencilReflections"]设置如下:
{
//渲染实物的镜像
D3D12_DEPTH_STENCIL_DESC reflectionsDSS;
reflectionsDSS.DepthEnable = true;
//开启深度写入,不禁用写入RTV,渲染像素
reflectionsDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
reflectionsDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
reflectionsDSS.StencilEnable = true;
reflectionsDSS.StencilReadMask = 0xff;
reflectionsDSS.StencilWriteMask = 0xff;
//为0失败的继续保持
reflectionsDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
//设置为相等,此时系统设置就是1,这就与模板测试通过替换标记的1对上了,因此只会在镜子的可见范围上进行绘制
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"])));
}
要注意一下FrontCounterClockwise ,因为顶点按照索引组织的顺序没改变,所以法线方向不变,但是镜像实际关于镜子对称了,所以进行设置。
//绘制镜子
就是一个Blend操作,注意要换回此时的常量缓冲,因为这不是绘制镜像了
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
// Draw mirror with transparency so reflection blends through.
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
对于mPSOs["transparent"]设置如下:
{
D3D12_GRAPHICS_PIPELINE_STATE_DESC transparentPsoDesc = opaquePsoDesc;
//镜子的混合,按道理是放在镜像效果的最后的
D3D12_RENDER_TARGET_BLEND_DESC transparencyBlendDesc;
transparencyBlendDesc.BlendEnable = true;
transparencyBlendDesc.LogicOpEnable = false;
//根据Alpha绘制
transparencyBlendDesc.SrcBlend = D3D12_BLEND_SRC_ALPHA;
transparencyBlendDesc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
transparencyBlendDesc.BlendOp = D3D12_BLEND_OP_ADD;
transparencyBlendDesc.SrcBlendAlpha = D3D12_BLEND_ONE;
transparencyBlendDesc.DestBlendAlpha = D3D12_BLEND_ZERO;
transparencyBlendDesc.BlendOpAlpha = D3D12_BLEND_OP_ADD;
transparencyBlendDesc.LogicOp = D3D12_LOGIC_OP_NOOP;
transparencyBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
transparentPsoDesc.BlendState.RenderTarget[0] = transparencyBlendDesc;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&transparentPsoDesc, IID_PPV_ARGS(&mPSOs["transparent"])));
}
以上各步骤就已经实现了镜子效果的绘制,这里边我们主要关注模板的使用,两个阶段:
1.用模板标记镜子的可见范围;
2.根据与可见范围模板值相等来填充像素着色结果。
说实话,阴影效果和模板实际上来说是没啥太大关系的,实现阴影的途径是多样的:
1.将阴影看做是一种几何体,只不过它没有厚度,然后就给一种很暗的材质即可绘制;
2.使用Shadow Map这样的阴影映射纹理,即以光源作为视点渲染一张深度图,并以摄像机视点渲染深度图,两张深度图对同一像素点深度值进行对比,如果能够被摄像机看到,但是不能被光源看到(摄像机深度值更深),那就是阴影(局部光照模型);
3.使用Ray Trace,观察点与光源之间有遮挡则视为阴影。
是的,以上都没有涉及模板。从我们实现上来说,后两种应该都不太好上手,毕竟需要操作深度图或者进行光追,不太符合初学者能力,而对于第一条,当做几何体来做,那就容易多了,毕竟绘制并渲染几何体使用我们对于PSO的设置即可,即便是考虑阴影不是全黑,要和背景混合也只需要Blend就能实现。然而问题也就出在这里了。什么问题呢?
试想一下,如果我们计算出某个像素是多个几何顶点的投影点,那么就会进行多次混合(不是全黑效果),就会导致这个像素越来越暗,这叫做“双重混合”,我觉得叫多重混合比较好,毕竟可能不止一次。那么要防止发生双重混合,就要保证只渲染一次,或者说多余一次的渲染被我们抛弃。OK提到抛弃,那就应该轮到模板上线,基本原理如下:
1.清空模板缓冲为0(如果本身就是0的可以不做了),并设置系统值为0:mCommandList->OMSetStencilRef(0);
2.设置PSO的模板检测为相等时通过,通过后增加模板值(0++);并渲染该像素;
3.当如果再次操作到该像素,此时为1了,不能与模板值0相等,因此失败,抛弃像素,否则进行第2步。
对应的PSO中模板设置如下:
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"])));
通过这种占位操作就实现阴影的单次绘制。这种绘制阴影的方式理论上可行,实际上存在很多问题:
1.如果得到阴影几何体?这种计算本身就是逐顶点进行的,是消耗性能的,如果实在平面还好说,如果是曲面还要逐个曲面相交计算;
2.要解决Z-fight问题,所以会有适当地偏移,这是不准确的;
3.阴影边界太过明显,做出的是硬阴影。
OK,as you can see,我们认识了一下模板这个面具,你可以理解为他试图遮蔽什么,又或者是试图体现什么,重要的主要是分析首先逻辑,然后才是参数设置,剩下的就交给硬件自己实现了。