主要包括了三个方面的内容,几何着色器,纹理数组和clip。
绘制树木的顶点只包含树木的位置和尺寸信息,我们需要将这部分信息传送到几何着色器中并转化为我们需要的屏幕坐标系顶点信息。
几何着色器中主要是通过视线的方向确定树的right方向,因为这里的树是一个类似billboard的纹理贴图,所以只需要用
float3 right = cross(up, look);
即可算出right方向,然后通过中心的世界坐标与树的尺寸,结合right方向(up始终是(0,1,0))即可求出树的四个顶点位置,它们的纹理坐标都是固定的,所以这里直接用一个数组来索引四个顶点的纹理坐标。
像素着色器中,主要是通过图元ID在纹理数组中进行纹理采样。语义SV_PrimitiveID表示图元ID,在输入汇编阶段会为每个图元生成一个唯一的图元ID。这里必须使用纹理数组对象Texture2DArray而不能用Texture2D TexArray[4],因为采样的纹理的索引必须是常量,就是说,我们可以用TexArray[1].Sample但是不能用TexArray[pin.PrimID%4],而纹理数组对象的采样没有这个限制。
clip函数能够在像素着色器中舍弃像素片段,这里我们使用这个函数来舍弃树木纹理中的alph值过小的部分。用clip函数舍弃的像素片段将不会再执行之后的管线阶段,所以能略微提高性能。
这两个Object主要是模板的使用,其中关于镜面反射和阴影的计算部分不详细叙述,因为使用了现成的数学库,所以较为简单,可以结合代码学习。文章主要介绍模板测试在这部分的工作。
这里实现的镜面反射实际上是在反射的位置重新绘制一遍骷髅,问题在于我们需要知道屏幕中哪些部分是镜子,因为只应该在镜子中看到倒影,所以这里应该用模板来限制反射骷髅的绘制区域。
首先第一步,我们先绘制一次镜子,由于只绘制到模板缓冲区中,所以这里需要把绘制到render target上的功能关闭,通过设置BlendState可以达到目的。
D3D11_BLEND_DESC noRenderTargetWritesDesc = {0};
noRenderTargetWritesDesc.AlphaToCoverageEnable = false;
noRenderTargetWritesDesc.IndependentBlendEnable = false;
noRenderTargetWritesDesc.RenderTarget[0].BlendEnable = false;
noRenderTargetWritesDesc.RenderTarget[0].RenderTargetWriteMask = 0;
HR(device->CreateBlendState(&noRenderTargetWritesDesc, &NoRenderTargetWritesBS));
这里把RenderTargetWriteMask设置为0,就是说任何颜色都不会绘制到render target中。
另外还需要开启模板写入的功能,
// MarkMirrorDSS
D3D11_DEPTH_STENCIL_DESC mirrorDesc;
mirrorDesc.DepthEnable = false;//Disable the depth clip.
mirrorDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
mirrorDesc.DepthFunc = D3D11_COMPARISON_LESS;
mirrorDesc.StencilEnable = true;
mirrorDesc.StencilReadMask = 0xff;
mirrorDesc.StencilWriteMask = 0xff;
mirrorDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
// We are not rendering backfacing polygons, so these settings do not matter.
mirrorDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
HR(device->CreateDepthStencilState(&mirrorDesc, &MarkMirrorDSS));
这里除了开启模板写入之外还关闭了深度测试,确保模板总是会被更新的。因为我们实际绘制的图象会做深度测试,而模板绘制过程中不绘制图象,所以这里关闭深度测试不会有问题。
pD3dContext->OMSetDepthStencilState(RenderStates::MarkMirrorDSS, 1);
通过设置模板状态,绘制玻璃的时候回将模板缓冲区中对应的区域置为1。然后我们绘制倒影,设置模板状态为
pD3dContext->OMSetDepthStencilState(RenderStates::DrawReflectionDSS, 1);
这个状态会检测模板缓冲区的值,并和输入的参照值做对比,如果相等则通过,否则不通过并舍弃像素片段。
另外,玻璃只有一面有倒影,因为我们在绘制模板的时候开启了背面剔除,所以背面并没有模板值被更新。如果关闭背面剔除,那么就会看到另一个骷髅了。
绘制骷髅的影子要复杂一些,因为一方面我们要保证影子的每个像素只被绘制一次(因为影子是半透明的,如果同一个点有多个像素被绘制那么影子会叠加,不符合实际),另外我只希望影子绘制在地面上,所以还需要用模板来限制影子的绘制区域。
绘制地面模板的过程和镜子相似,不过这里要注意和书本中的设置不同,我关闭了深度测试,如果不关闭,模板绘制过程中由于实际地面的存在,模板地面可能不能通过深度测试而无法更新模板缓冲区,所以我关闭了深度测试。
这里我们将模板缓冲区设为1<<1
pD3dContext->OMSetDepthStencilState(RenderStates::MarkMirrorDSS, 1<<1);
然后绘制影子的模板状态
D3D11_DEPTH_STENCIL_DESC noDoubleBlendDesc;
noDoubleBlendDesc.DepthEnable = true;
noDoubleBlendDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
noDoubleBlendDesc.DepthFunc = D3D11_COMPARISON_LESS;
noDoubleBlendDesc.StencilEnable = true;
noDoubleBlendDesc.StencilReadMask = 0xff;
noDoubleBlendDesc.StencilWriteMask = 0xff;
noDoubleBlendDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_ZERO;
noDoubleBlendDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
noDoubleBlendDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_ZERO;
noDoubleBlendDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
HR(device->CreateDepthStencilState(&noDoubleBlendDesc, &StencilNoDoubleBlendDSS));
判断的方式是相等,即缓冲区中的模板值和参考值相等时通过,一旦通过就把模板值清空,从而确保任何一点只能有一个像素片段通过模板测试,并且能限制在地面区域中(因为只有地面区域的模板值是1<<1)。
我们的场景实际上对地面和玻璃都进行了两次绘制,之所以这样做是因为我们任何一次的模板操作都会完全改变像素的模板缓冲区的全部bits。我们可以通过设置StencilReadMask和StencilWriteMask来严格限制模板缓冲区的读写,从而让模板缓冲区的操作不会影响到其余的模板缓冲区操作。