Render Flow of Divinity II (part 2 shadow map)
作者:clayman 仅供个人学习使用,转载请保留作者以及原文链接,勿用于任何商业用途。
上一篇文章介绍了Divinity2渲染流程的pre pass阶段,最后稍稍漏了一部分,这里补上。Pre-pass中ps 76行以后的代码是alpha test实现,这里asm代码逻辑看起来稍微有些奇怪,逻辑其实并不复杂,通过改变c0变量(AlphaTestFunction)的值,可以选择裁剪alpha值大于,小于或者等于AlphaTestRef的像素,比普通alpha test稍微高级一点点的版本:) 好了,接下来让我们看shadow map生成。
Shadow map生成在pre-pass之后从EID 14924开始,点击0x226D8C80处的纹理,可以看到shadow map如下图所示:
这是一张包含了4张shadow map的纹理,2048*2048,R16G16F格式,显然这是cascaded/parallel-split shadow maps。但如果你仔细看贴图中的内容,这并不是标准的ppsm,而是以视点为中心与观察方向无关的csm,类似于real-time 3rd中介绍Hellgate:London所用的方法: generate a fixed set of shadow map at differernt resolutions , nested around the viewer.
这里可以找到该算法的详细信息以及demo。由于与视线无关,这种方法的好处是不必每帧都重新渲染所有层次的shadow map,可以把计算分散到n帧进行,比如每帧总是渲染最靠近观察者的左上角L0级shadow map,n%3==0帧刷新L1级,n%3==1帧刷新L2,n%3==2刷新L3级shadow map,牺牲一定的质量换取性能。实际上游戏设置就有每帧刷新所有shadow map还是分步刷新的选项。Pix文件抓数据时用了最高设置,所以后面可以看到4张shadow map都刷新了。注意,因为是渲染到texture atlases,需要为不同的shadow map设置不同的viewport。下面是生成shadow map所用的vertex shader:
1 // Generated by Microsoft (R) D3DX9 Shader Compiler 2 // Parameters: 3 // float4x4 kLightViewProj; 4 // float4x4 kWorld; 5 // Registers: 6 // Name Reg Size 7 // -------------- ----- ---- 8 // kWorld c0 4 9 // kLightViewProj c4 4 10 // 11 // Default values: 12 // kWorld 13 // c0 = { 0, 0, 0, 0 }; 14 // c1 = { 0, 0, 0, 0 }; 15 // c2 = { 0, 0, 0, 0 }; 16 // c3 = { 0, 0, 0, 0 }; 17 // 18 // kLightViewProj 19 // c4 = { 0, 0, 0, 0 }; 20 // c5 = { 0, 0, 0, 0 }; 21 // c6 = { 0, 0, 0, 0 }; 22 // c7 = { 0, 0, 0, 0 }; 23 // 24 25 vs_1_1 26 def c8, 0, 0, 0, 0 27 dcl_position v0 28 29 dp4 r0.x, v0, c0 //worldPos = mul(inputPos,kWorld) 30 dp4 r0.y, v0, c1 31 dp4 r0.z, v0, c2 32 dp4 r0.w, v0, c3 33 34 dp4 oPos.x, r0, c4 //lightViewProjPos = mul(worldPos,lightViewProj); 35 dp4 oPos.y, r0, c5 //outPos.xy = lightViewProjPos.xy; 36 dp4 r1.x, r0, c6 37 dp4 r0.y, r0, c7 38 39 slt r0.z, r1.x, c8.x //sign = (lightViewProjPos.z < 0)? 1 : 0; 40 mad r0.x, r0.z, -r1.x, r1.x // temp = sign * (-lightViewProjPos.z) + lightViewProjPos.z; 41 42 mov oPos.zw, r0.xyxy //outPos.zw = float2(temp ,lightViewProjPos.w); 43 mov oT0.xy, r0 //outTexcoord.xy = xxxx
代码非常简单,唯一需要注意的是39,40行对z值为负数的情况作了处理(当物体位于camera之后)。另外,slt/mad也是最常见的用branchless代码实现branch的方法,翻译为普通代码相当于:
if(lightViewProjPos.z < 0)
lightViewProjPos.z = 0;
接下来是pixel shader代码:
1 ps_2_0 2 def c0, 0, 0, 0, 0 3 dcl t0.xy 4 5 rcp r0.w, t0.y r0.w = 1 / lightViewProjPos.w 6 mul r0.x, r0.w, t0.x r0.x = r0.w * t0.x; 7 mul r0.y, r0.x, r0.x r0.y = r0.x * r0.x; 8 mov r0.zw, c0.x r0.zw = 00; 9 mov oC0, r0
PS输出post-perspective距离,特别注意G通道中输出了距离的平方,所以这还是一张variance shadow map!EID24883完成了4张shadow map的渲染。 Divinity2只有静态物体产生阴影:(, 唯一比较特别的是树叶,树叶总是在每张shadow map最后渲染,如果采用跨帧渲染渲染,树叶阴影总是静态的,如果是全刷选项,树叶则是动态阴影。感兴趣可以查看24883处DP所用的shader,这里不再仔细分析。接下来EID24884 – 24928对shadow map做了blur,vs代码非常简单,只看第一个blur pass的ps代码:
1 // Generated by Microsoft (R) D3DX9 Shader Compiler 2 // Parameters: 3 // float g_SunSMTexelSize; 4 // Registers: 5 // Name Reg Size 6 // ---------------- ----- ---- 7 // g_SunSMTexelSize c0 1 8 9 // Default values: 10 //g_SunSMTexelSize 11 //c0 = { 0.000488281, 0, 0, 0 }; 12 13 preshader 14 mul c0.x, c0.x, (-2) 15 add c1.x, c0.x, c0.x 16 17 // approximately 2 instructions used 18 // Generated by Microsoft (R) D3DX9 Shader Compiler 19 // Parameters: 20 // float g_SunSMTexelSize; 21 // sampler2D shadowMapSampler; 22 23 24 // Registers: 25 // Name Reg Size 26 // ---------------- ----- ---- 27 // g_SunSMTexelSize c2 1 28 // shadowMapSampler s0 1 29 // Default values: 30 // g_SunSMTexelSize 31 c2 = { 0.000488281, 0, 0, 0 }; 32 33 ps_2_0 34 def c3, 0, 0.0625, 0, 0 35 def c4, 0.0625, 0.25, 0.375, 0.25 36 def c5, 0, 1, 0, 0 37 dcl t0.xy 38 dcl_2d s0 39 40 mov r0.x, c3.x r0.x = c3.x 41 mov r0.y, c0.x r0.y = g_SunSMTexelSize * -2 42 add r0.xy, r0, t0 r0.xy += texcoord 43 44 mov r1.x, c3.x 45 mov r1.y, -c2.x 46 add r1.xy, r1, t0 r1.xy = texcoord + float2(0,g_SunSMTexelSize *2); 47 48 mov r2.x, c3.x 49 mov r2.y, c2.x 50 add r2.xy, r2, t0 r2.xy = texcoord + float2(0,g_SunSMTexelSize); 51 52 mov r3.x, c3.x 53 mov r3.y, c1.x 54 add r3.xy, r3, t0 r3.xy = texcoord + float2(0,g_SunSMTexelSize * 4); 55 56 texld r0, r0, s0 57 texld r1, r1, s0 58 texld r2, r2, s0 59 texld r4, t0, s0 60 texld r3, r3, s0 61 62 mov r0.y, r1.x 63 mov r0.w, r2.x 64 mov r0.z, r4.x r0 = float4(sample0.x,sample1.x,sample2.x,sample4.x); 65 66 67 mul r1, r0, r0 r1 = mul(r0,r0); //r0^2 68 dp4 r0.w, r0, c4 weightSum = dot(r0,c4); 69 dp4 r1.w, r1, c4 weightSum2 = dot(r1,c4); 70 mul r2.w, r3.x, r3.x r2.w = r3 * r3 71 mad r0.x, r3.x, c3.y, r0.w r0.x = r3.x * 0.0626 + weightSum 72 mad r0.y, r2.w, c3.y, r1.w r0.y = r2.w * 0.0625 + weightSum2 73 mov r0.z, c5.x 74 mov r0.w, c5.y 75 mov oC0, r0 return float4(r0.x,r0.y,0,1); 76 77 // approximately 29 instruction slots used (5 texture, 24 arithmetic)
标准的水平模糊,做了5个采样点 -2,0,1,2,4。有意思的是采样只用了普通的深度,然后做了一次平方,而没有直接读取shadow map中之前输出的深度平方值。这样的话,其实shadow map生成还有优化余地,直接使用f16的纹理,或者f32的纹理也行。最后一段代码看起来稍微有些复杂,其实只是加上了第5个参数的权重而已。
从EID24948开始,又专为主角渲染了一张shadow map,纹理大小1024*1024,R32F格式,代码和之前的类似,只不过vs多了骨骼动画的计算.这里不详细讨论。
下一次,我将介绍最复杂也是最有意思的light pass阶段。最后,foward渲染阶段时,再着重分析各种不同物体(terrain,model,sky...)的渲染。
--------------------------本人才学疏漏,如有错误欢迎指证,未完待续--------------------------------