首先我们先简单的优化下代码。
在我们现在的项目中,shader运行总共需要4个常量缓冲区: 槽位0:传入渲染目标特有的ObjectConstants结构,里面有模型转世界坐标矩阵、纹理转换矩阵、纹理偏移矩阵 槽位1:传入通用的投影矩阵、光照等等 PassConstants 槽位2:传入渲染目标特有纹理参数 MaterialConstants 槽位3:传入渲染目标特有的纹理视图 renderItem.srv 0、2、3都是渲染目标特有的。这里添加一个函数setShaderParam稍微封装下我们目前有两个PSO。m_PSO和m_PSOEX。采用一个无序map封装下,方便管理优化之后的github: https://github.com/mversace/DirectX12-MiniEngine-Dragon/tree/12ee9996066d9205a95c4b45a1bc5d6c16dcb5ea
这个纹理透明,很容易做的。只需要在ps shader的main函数中加一行代码就可以了。 这样,alpha通道低于0.1的像素就不再渲染了
clip(diffuseAlbedo.a - 0.1f);而箱子正面透过去后,会发现箱子的背面看不到。这是因为我们设置的PSO会自动进行背面剔除。这个剔除的操作先于ps shader。 我们可以创建一个新的不进行背面剔除的PSO,来对箱子进行渲染
GraphicsPSO alphaTestPSO = defaultPSO; raster = Graphics::RasterizerDefault; raster.CullMode = D3D12_CULL_MODE_NONE; // 不剔除背面 alphaTestPSO.SetRasterizerState(raster); alphaTestPSO.Finalize(); m_mapPSO[E_EPT_ALPHATEST] = alphaTestPSO;稍微修改下代码,使得程序运行直接跑水体这个部分
bool m_bRenderShapes = false;然后运行发现水的波浪持续升高,基本快把山淹没了。 仔细调试后发现是update函数的入参,在第二帧时会传入一个极大值,导致水的波浪计算错误。
可以搜索Core中的‘s_FrameStartTick’变量,发现第一次update时,这个值是0,经过一次present后,取得当前的时间戳,直接变成了一个极大值。导致第二次的update入参过大。后续会恢复正常。
void Graphics::Present(void) { ... if (s_EnableVSync) { // With VSync enabled, the time step between frames becomes a multiple of 16.666 ms. We need // to add logic to vary between 1 and 2 (or 3 fields). This delta time also determines how // long the previous frame should be displayed (i.e. the present interval.) s_FrameTime = (s_LimitTo30Hz ? 2.0f : 1.0f) / 60.0f; if (s_DropRandomFrames) { if (std::rand() % 50 == 0) s_FrameTime += (1.0f / 60.0f); } } else { // 非垂直同步情况下。 // 第一次update时s_FrameTime=0,之后执行以下代码。因为s_FrameStartTick初始化为0,导致s_FrameTime计算出一个极大值 // 在第二次update时,入参就会很大,导致一些问题。 // 修复方式:在Graphics::Initialize中将s_FrameStartTick初始化为SystemTime::GetCurrentTick() s_FrameTime = (float)SystemTime::TimeBetweenTicks(s_FrameStartTick, CurrentTick); } s_FrameStartTick = CurrentTick; ...那么我们只需要把这个变量做一个初始化即可 s_FrameStartTick = SystemTime::GetCurrentTick();
修复后的执行效果如下:
雾效果来说,是很简单的。只需要给ps shader传入一些雾的参数进去,然后稍微做下处理就可以了
cbuffer cbPass : register(b1) { ... // Allow application to change fog parameters once per frame. // For example, we may only use fog for certain times of day. float4 gFogColor; // 雾的颜色 float gFogStart; // 雾开始的范围 float gFogRange; // 雾的总体范围 float2 pad2; ... } float4 main(VertexOut pin) : SV_Target0 { ... float4 litColor = ambient + directLight; // 这里判断一下雾的alpha值,如果为0则不再处理,这样就可以在C++中通过传入的参数不同来控制是否渲染雾效果了 if (gFogColor.a > 0) { // 获取雾的系数。取观察点到雾边缘的距离除以雾的总范围得到一个[0,1]的系数 float fogAmount = saturate((distToEye - gFogStart) / gFogRange); // 系数fogAmount把当前像素的颜色与雾的颜色做混合处理。 // lerp(x, y, s) = x + s(y - x); 可见如果s=1.0,则只保留雾的颜色 litColor = lerp(litColor, gFogColor, fogAmount); } // 颜色的透明通道采用diffuseAlbedo中的值 litColor.a = diffuseAlbedo.a; }修改PassConstants结构体添加上对应的字段,并写入默认值即可。
同时对于某屏幕坐标位置没有对应的顶点时,也需要画上雾的颜色,所以初始化时,设置渲染目标缓冲区的默认颜色为雾色
Graphics::g_SceneColorBuffer.SetClearColor({ 0.7f, 0.7f, 0.7f });首先学习一下官方文档: https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn481541(v=win.10) https://docs.microsoft.com/zh-cn/windows/desktop/direct3dhlsl/dx-graphics-hlsl-packing-rules
我们注意到,这里添加了一个XMFLOAT2 pad;这个是用来辅助对齐的。 对于shader入参,需要16字节对齐。我们来仔细分析下PassConstants结构体 以16字节编号[0, n]来分析
__declspec(align(16)) struct PassConstants { Matrix4 viewProj = Matrix4(kIdentity); // 0-3 Vector3 eyePosW = { 0.0f, 0.0f, 0.0f }; // 4 Vector3本身就占用16个字节,这点要注意,这是MiniEngine特意处理过的 Vector4 ambientLight = { 0.0f, 0.0f, 0.0f, 1.0f }; // 5 Vector4 FogColor = { 0.7f, 0.7f, 0.7f, 0.3f }; // 6 float gFogStart = 40.0f; // 7 float gFogRange = 150.0f; // 7 XMFLOAT2 pad; // 7 Light Lights[MaxLights]; // 直接拆分如下: XMFLOAT3 Strength = { 0.0f, 0.0f, 0.05f }; // 8 这结构占12个字节 float FalloffStart = 0.0f; // 8 .... };如果这里不加 XMFLOAT2 pad,会导致如下结果:
__declspec(align(16)) struct PassConstants { ... float gFogStart = 40.0f; // 7 float gFogRange = 150.0f; // 7 Light Lights[MaxLights]; // 直接拆分如下: XMFLOAT3 Strength = { 0.0f, 0.0f, 0.05f }; // ? 前俩float占据7号,后俩占据8号,因为XMFLOAT3的本质就是连续的3个float float FalloffStart = 0.0f; // 8 .... };我们当然可以修改Lights结构体中的XMFLOAT3改成Vector3,强行自己占领固定的16个字节。但由此导致的就是shader中要添加一些占位float来修复Light结构体的对齐问题。
龙书中使用的都是XMFLOAT这类dx提供的结构体,采用添加占位符的方式是很方便的(毕竟16字节对齐,这部分总是要填东西的,总体的常量缓冲区占用不会变多)。 而我们这里混合使用了MiniEngine中math库定义的结构体。可以省掉一些占位符,总体来说见仁见智吧。 只要能达到C++中的结构体与shader入参对应起来就可以。
来看下雾的效果图:
水体的透明效果,实际上才是龙书本章所讲的混合模式。 我们在渲染时,会不断的执行ps shader,执行一次,就会把渲染目标缓冲区对应像素覆盖一次。
混合就是对于本次ps shader输出和目标渲染缓冲区的颜色值,按照一定的规则混合成新的颜色填入缓冲区。
而这个混合操作是在PSO中定义的。对于这种混合来说,渲染目标的顺序很关键。如果先渲染需要混合的目标,在渲染其他的,会覆盖掉混合效果。 而对于多个混合效果来说,他们之间的顺序很多时候不是很关键(因为很多混合操作支持交换律)。
当然严格意义上来讲,先绘制没有混合效果的物体,然后依据离摄像机由远及近的距离依次绘制混合效果物体时最标准的。 注意:修改目标顺序并不影响他们之间的深度/模板测试添加一个透明混合的PSO
GraphicsPSO transparentPSO = defaultPSO; auto blend = Graphics::BlendTraditional; blend.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO; transparentPSO.SetBlendState(blend); transparentPSO.Finalize(); m_mapPSO[E_EPT_TRANSPARENT] = transparentPSO;修改水体的透明度,这里直接修改水体目标的材质即可。
m_renderItemWaves.diffuseAlbedo = XMFLOAT4(1.0f, 1.0f, 1.0f, 0.5f); // 最后一位即是透明度调整渲染顺序,把水体渲染改到最后。
github: https://github.com/mversace/DirectX12-MiniEngine-Dragon/tree/6aadeb230a162dae1ff00cc201ec65e33b375cc3
看下结果:
添加一行代码就可以了。
GraphicsPSO transparentPSO = defaultPSO; auto blend = Graphics::BlendTraditional; blend.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO; // 只允许写入蓝和alpha通道 blend.RenderTarget[0].RenderTargetWriteMask = (D3D12_COLOR_WRITE_ENABLE_BLUE | D3D12_COLOR_WRITE_ENABLE_ALPHA); transparentPSO.SetBlendState(blend); transparentPSO.Finalize(); m_mapPSO[E_EPT_TRANSPARENT] = transparentPSO;github: https://github.com/mversace/DirectX12-MiniEngine-Dragon/tree/039f1b787a58bf138b8ce3371e9cf10b5ea53ee1
对于混合模式来说,使用起来是很简单的。比较麻烦的是清楚系统提供的这些混合模式都是什么效果。 当要做一个具体实现时,需要知道采用哪种混合模式,怎么修改shader能实现。这才是最重要的。
当然我也不太懂,这一块还是需要多看多学多实践。