十二、D3D12学习笔记——模板缓冲

首先声明,本人是自学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的模板缓冲就标记了部分系统设置值,给出了一个我们可见的镜子范围);

十二、D3D12学习笔记——模板缓冲_第1张图片

 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,我们认识了一下模板这个面具,你可以理解为他试图遮蔽什么,又或者是试图体现什么,重要的主要是分析首先逻辑,然后才是参数设置,剩下的就交给硬件自己实现了。

你可能感兴趣的:(DirextX,12学习,3d,c++,图形渲染)