Basic.fx
基本的Effect文件,包括一个顶点着色器,一个像素着色器和若干Cbuffer。Cbuffer按照更新频率分组有助于提高效率。这里有两组Cbuffer,第一组主要是环境相关参数,一般而言每帧只更新一次。
第二组是和具体绘制对象有关系的参数,包括世界坐标下的对象的矩阵信息,纹理变换矩阵和光照材质等。
cbuffer cbPerFrame
{
DirectionalLight gDirLights[3];
float3 gEyePosW;
float gFogStart;
float gFogRange;
float4 gFogColor;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
float4x4 gTexTransform;
Material gMaterial;
};
另外还包含一个纹理贴图。因为纹理贴图无法包含在cbuffer中所以单独定义。 顶点着色器的输入包括一个本地位置,本地法线和纹理UV,经过顶点着色器后变换为世界坐标和屏幕空间坐标。(世界坐标用于光照计算)
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
};
像素着色器主要作用是纹理采样和光照计算。这里参数中有一个uniform关键字,主要用于将输入参数标记为不变量,从而使编译时能对分支语句等进行优化。
光照的部分不详细叙述,书中介绍得很清楚,主要是使用了兰伯特余弦定理推导光照公式。
Mount&Water
Mount较为简单,生成好纹理网格,获取随机的高度并计算法线和纹理UV,创建顶点和索引坐标即可。绘制时先设置好BasicEffect的参数,然后绘制即可。由于我们有两种顶点坐标,所以每次绘制前都会设置一次输入布局。
Water类似于Mount。关于水波的算法有兴趣可以详细学习《3D游戏与计算机图形学中的数学方法》。由于我们要定期更新水波坐标,所以这里需要一个Dynamic的顶点坐标,然后在顶点更新之后使用Map的方式来更新数据即可。
绘制时,我们的水是半透明的,所以在DrawTransparency时绘制。绘制透明物体需要开启混合之外,还应该关闭深度信息的写入。
pD3dContext->OMSetDepthStencilState(RenderStates::NoDepthDSS, 1);
pD3dContext->OMSetBlendState( RenderStates::TransparentBS , blendFactor, 0xffffffff );
D3D11_DEPTH_STENCIL_DESC noDepthDesc;
noDepthDesc.DepthEnable = true;
noDepthDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
noDepthDesc.DepthFunc = D3D11_COMPARISON_LESS;
noDepthDesc.StencilEnable = false;
noDepthDesc.StencilReadMask = 0xff;
noDepthDesc.StencilWriteMask = 0xff;
noDepthDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDepthDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDepthDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
noDepthDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
noDepthDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDepthDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDepthDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
noDepthDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
HR(device->CreateDepthStencilState(&noDepthDesc, &NoDepthDSS));
为了说明混合/深度测试/深度写入之间的关系,我这里举一个例子,假设我们某个像素位置有6个像素片段ABCDEF,它们的绘制顺序按字母顺序,绘制时状态如下表所示
像素片段 | 透明 | 混合 | 深度值 | 深度测试 | 深度写入 | 模板深度值 | 缓冲区像素 |
A | 否 | 否 | 0.8 | 是 | 是 | 0.8 | A |
B | 否 | 否 | 0.4 | 是 | 是 | 0.4 | B |
C | 否 | 否 | 0.5 | 是 | 是 | 0.4 | B |
D | 是 | 是 | 0.3 | 是 | 否 | 0.4 | B+D |
E | 是 | 是 | 0.45 | 是 | 否 | 0.4 | B+D |
F | 是 | 是 | 0.35 | 是 | 否 | 0.4 | B+D+F |
根据深度信息我们可以判断,从近到远的顺序排列物体应该是DFBECA,由于ABC都是不透明物体,所以ECA都被B遮挡了,而DF是透明的,所以可以透过D和F看见B,实际上可见的物体是三个DFB,这是我们想要的结果。
考虑不关闭深度写入的情况,当绘制到D时,深度值被写为0.3,那么F将无法通过深度测试,所以F会被剔除,最终可见的只剩下DB,显然不正确。
那是否能在透明物体绘制时关闭深度测试呢?关闭的情况下,在B物体后面的E也会被绘制出来,实际绘制的物体变为DFBE,显然也是不正确的。