Render Flow of Divinity II (part 1)
作者:clayman
仅供个人学习使用,转载请保留作者以及原文链接,勿用于任何商业用途。
当爱好变成职业后,爱好就被毁了,这几年一直没有太多时间玩游戏,却迷恋上了把新游戏都 pix一遍的习惯... 和各种源码党相比,一直觉得通过 pix/perfHUD/PerfStudio来了解一个游戏/引擎更快,更实际,特别仅就渲染而言,有经验的程序员看到 pix数据,大概就知道应该怎么来写了。
早就想写一系列关于各种游戏的 pix分析文章,今天终于忙里偷闲开个头,来分析一下 << Divinity II: Ego Draconis神界 2>> 的渲染流程。之所以分析这个游戏并没有特殊的原因,只是因为手头刚好有这游戏的 pix文件,如果时间允许,会有更多这系列的分析文章:) 猛击>>这里<<可以下载到文章所用的pix文件。本文假设你已经对pix有所了解,熟悉基本用法。
Divinity II是 09 年的一个单机魔幻 rpg游戏,貌似是 Larian Studios自己开发的引擎,基于 DX9, light-pre pass 构架。Shader 管理应该是在 D3DEffect上做了一个轻量级的wrapper,可以看到 Effect函数和SetShaderConstant 混用的代码。基于 Effect框架的缺点就是太多冗余 render state设置。
每帧渲染的开始,首先是生成一些动态贴图,可以看到 pix eid 3848之前的代码先渲染了一张树叶纹理。
从EID 3876 -- 14896进入 pre-pass阶段,渲染normal 和depth buffer。 Render Target使用了A16B16G16R16 格式,后面分析代码可以看到 blue和alpha 通道保存了 view space的法线,R/G 通道是深度信息。后面通过分析shader,再来解释为什么图片看起来是这样子。
来仔细分析 pre-pass中一个简单静态物体的渲染: EID4174处的路灯。首先在EID 4097和 4147处可以看到渲染使用了2张贴图,一张普通纹理,一张 normal map,注意, normal map使用了G/A 通道保存数据,使用 DTX5格式时常见的提高精度的技巧。
以下为顶点格式,这里使用了short4n保存法线,其实顶点还有进一步压缩的空间,纹理坐标也可以用short2n来保存,或者分别放到norml和binormal的w通道,这样每个顶点可以节约8byte。
struct { Float3 position Float2 texcoord Short4n normal Short4n binormal }
下面是vertex shader,我已经为每段asm代码做了详细注释:
1 // float4x4 g_Proj; 2 // float4x4 g_View; 3 // float4x4 g_World; 4 // float4x4 g_WorldView; 5 6 // Registers: 7 // Name Reg Size 8 // ------------ ----- ---- 9 // g_World c0 4 10 // g_WorldView c4 3 11 // g_View c8 4 12 // g_Proj c12 4 13 14 vs_3_0 15 16 def c7, 1, 0, 3.05185094e-005, 0 //note 1/32767 = 3.05185094e-005 17 //input data format 18 dcl_position v0 19 dcl_normal v1 20 dcl_texcoord v2 21 dcl_binormal v3 22 23 //vertex shader output 24 dcl_position o0 25 dcl_texcoord o1 26 dcl_texcoord1 o2.xyz 27 dcl_texcoord2 o3.xyz 28 dcl_texcoord3 o4.xyz 29 dcl_texcoord4 o5.xy 30 31 //start vs.. 32 mul r0.xyz, c7.z, v1 //normal = vsIn.normal * (1 / 32767); 33 dp3 r1.x, r0, c4 //viewSpaceNormal = mul(normal,xyz, g_worldView) 34 dp3 r1.y, r0, c5 35 dp3 r1.z, r0, c6 36 37 mul r0.xyz, c7.z, v3 //binormal = vsIn.binormal * (1 / 32767)
38 dp3 r2.x, r0, c4 //viewSpaceBinormal = mul(binormal.xyz * g_worldView 39 dp3 r2.y, r0, c5 40 dp3 r2.z, r0, c6 41 42 mul r0.xyz, r1.zxyw, r2.yzxw //tangent = cross(normal,binormal 43 mad r0.xyz, r1.yzxw, r2.zxyw, -r0 44 45 mov o2.xyz, r1 //vsOut.normal = viewSpaceNormal 46 mov o3.xyz, r2 //vsOut.binormal = viewSpaceNormal 47 mov o4.xyz, -r0 //vsOut.tangent = -tangent 48 49 mad r0, v0.xyzx, c7.xxxy, c7.yyyx //position.xyz = position.xyz * 1 + 0; 50 51 dp4 r1.x, r0, c0 //worldPos = mul(position,g_world); 52 dp4 r1.y, r0, c1 53 dp4 r1.z, r0, c2 54 dp4 r1.w, r0, c3 55 56 dp4 r0.x, r1, c8 //viewPos = mul(position,g_view); 57 dp4 r0.y, r1, c9 58 dp4 r0.z, r1, c10 59 dp4 r0.w, r1, c11 60 61 dp4 r1.x, r0, c12 //worldViewProjPos = mul(viewPos,g_proj) 62 dp4 r1.y, r0, c13 63 dp4 r1.z, r0, c14 64 dp4 r1.w, r0, c15 65 66 mov o0, r1 //vsOut.projPos = worldViewProjPos 67 mov o1, r1 //vsOut.screenPos = worldViewProjPos 68 mov o5.xy, v2 //vsOut.texcoord = vsIn.texcoord
对很多初次看shader asm的人来说,其实并不难,有很多规律可以寻找,比如1,连续的2~4个dpx指令,一定是矢量和matrix相乘;2. mul和mad连续出现,并且变量以42,43行的模式出现,必然是cross。有了这些基础知识,上面的代码就非常容易看懂了,都是简单的坐标变换而已。不过有2个地方比较奇怪,第一,32和37行,short4n的格式在输入vs时会自动除以32767,这里又除了32768似乎有些多余;第二,49行是缩放和位移坐标的公式,但在这里出现似乎也是多余的。最后注意,vs分别输出了position,normal,binormal,tangent和texcoord到ps中,接下来看ps代码:
1 // sampler2D Base; 2 // sampler2D Normal; 3 // float3 g_AlphaTestFunction; 4 // float g_AlphaTestRef; 5 // float g_fDiffFarNearClip; 6 // float g_fNearClip; 7 8 // Registers: 9 // Name Reg Size 10 // ------------------- ----- ---- 11 // g_AlphaTestFunction c0 1 12 // g_AlphaTestRef c1 1 13 // g_fNearClip c2 1 14 // g_fDiffFarNearClip c3 1 15 // Base s0 1 16 // Normal s1 1 17 ps_3_0 18 def c4, 2, -1, 1, 0 19 def c5, 0.5, 3, 65535, 0 20 dcl_texcoord v0.z position.z 21 dcl_texcoord1 v1.xyz normal 22 dcl_texcoord2 v2.xyz binormal 23 dcl_texcoord3 v3.xyz tangent 24 dcl_texcoord4 v4.xy texcoord 25 dcl_2d s0 26 dcl_2d s1 27 28 29 30 //sample and unpack normal map 31 texld r0, v4, s1 //normalColor = tex2D(normalMap,vsOut.texcoord); 32 mad r0.xy, r0.ywzw, c4.x, c4.y //normalMap.xy = normalColor.yw * 2 - 1; 33 34 //tansform normal 35 nrm r1.xyz, v2 //binormal = normalize(vsOut.binormal); 36 mul r1.xyz, r0.x, r1 //r1.xyz = normalMap.x * binormal; 37 38 nrm r2.xyz, v3 //tangent = normalize(vsOut.tangent); 39 mad r1.xyz, r2, r0.y, r1 //r1.xyz = normalMap.y * normalMap.y + r1.xyz; 40 41 // z = sqrt( 1 – x*x – y*y) 42 mad r0.y, r0.y, -r0.y, c4.z //r0.y = normalMap.y * (-normalMap.y) + 1; 43 mad r0.x, r0.x, -r0.x, r0.y //ro.x = normalMap.x * (-normalMap.x) + normalMap.y; 44 rsq r0.x, r0.x //z = sqrt(r0.x); 45 rcp r0.x, r0.x 46 47 nrm r2.xyz, v1 //normal = normalize(vsOut.normal); 48 mad r0.xyz, r2, r0.x, r1 //r0.xyz = normal * z + r1.xyz; 49 nrm r1.xyz, r0 //finalNormal = normalize(r0.xyz); 50 51 // r0 = 1 / normalLength 52 dp3 r0.x, r1, r1 //r0.x = dot(finalNormal,finalNormal); 53 rsq r0.x, r0.x //r0.x = 1/sqrt(r0.x); 54 55 mad r0.yz, r1.xxyw, r0.x, c4.z //r0.yz = finalNormal.xy * r0.x + 1 56 mul r0.x, r1.z, r0.x //finalNormalZ = finalNormal.z * r0.x 57 58 cmp r0.x, r0.x, c4.z, c4.w //if(r0.x >=0) 1; else 0 59 mul_sat r0.yz, r0, c5.x //r0.yz = sat(r0.yz * 0.5f); 60 mad oC0.z, r0.x, c5.y, r0.y //out.z = r0.x * 3 + r0.y; 61 mov oC0.w, r0.z //out.w = r0.z; 62 63 add r0.x, c2.x, v0.z //r0.x = g_fNearClip + position.z; 64 rcp r0.y, c3.x //r0.y = 1 / g_fDiffFarNearClip; 65 mul r0.x, r0.x, r0.y //r0.x = r0.x * r0.y; 66 mul r0.x, r0.x, c5.z //r0.x = r0.x * 65535; 67 68 //clamp(r0.x,0,65535); 69 max r1.x, r0.x, c4.w //r1.x = max(r0.x,0); 70 min r0.x, r1.x, c5.z //r0.x = min(r1.x,65536); 71 72 frc r0.y, r0.x //r0.y = frc(r0.x); 73 add oC0.x, r0.x, -r0.y //out.x = r0.x - r0.y; 74 mov oC0.y, r0.y //out.y = r0.y; 75 76 //alpha test 77 texld r0, v4, s0 //baseColor = tex2d(baseTex,vsOut.texcoord); 78 add r0.x, r0.w, -c1.x //deltaAlpha = baseColor.a - g_AlphaTestRef; 79 80 cmp r0.y, -r0_abs.x, c4.z, c4.w //if(-abs(deltaAlpha) >= 0)r0.y =-1 else r0.y =0;
81 if_ne r0.y, -r0.y if(r0.y != 0) 82 mov r0.y, c4.y c0.y = -1 83 else 84 mov r0.y, c4.w 85 endif 86 87 88 mul r1, -r0.x, c0.x r1.x = -deltaAlpha * 0; 89 texkill r1 texkill(r1); 90 mul r1, r0.x, c0.y r1 = deltaAlpha * 1; 91 texkill r1 texkill(r1); 92 mul r0, r0.y, c0.z r0 = r0.y * 1; 93 texkill r0 texkill(r0);
与vs相比ps就复杂很多了. 首先,这段代码里也有几个常见的asm规律rsq,rcp连续出现就等于sqrt函数,dp3,rsq,mul连续出现就等于normalize。其实上面代码的逻辑并不复杂,只是在编译器编译以后,代码顺序有些混乱。ps主要干了几件事情,首先,读取normal map,应为normal map只记录了2个分量的值,41~45行的代码计算了第三个分量,此外,56行以上的代码还把normal从tangent space变换到了view space。面代码中52,53,56中的代码都有些冗余,49行已经对normal进行过归一化,这几行代码相当于又计算了一次。58~61行把viewspace的法线x,y值从[-1,1]压缩到[0,1]范围,并且输出到z,w通道中。这里要特别注意60行的代码,当normal的z值为正,也就是法线指向屏幕里面时(这里用的右手系,背对观察者的面,确保后面的pass正确处理back face),会把法线x值设置为一个大于1的值,这里是程序3,再以后的shader中可以看到程序用这个值的大小来判断法线方向。
63~74行的代码实现了顶点深度信息的输出,这里g_fNearClip是near plane的距离,大约是0.3,g_fDiffFarNearClip是near/far plane之间的距离,大约是2999.7, 代码对深度进行了量化,并分别把整数和小数部分保存到x,y通道中,因为x是整数部分大部分值大于1,所以最上面的图中r通道几乎为白色,y通道由于是小数部分,对于连续的深度来说总是从0~1重复出现,因此g通道纹理看起来是渐变条纹状。
--------------------------未完待续--------------------------------
分析,截图和排版实在太费时间,下一部分怎么着也得等周末了....