Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

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通道纹理看起来是渐变条纹状。

--------------------------未完待续--------------------------------

分析,截图和排版实在太费时间,下一部分怎么着也得等周末了....

 

 

 

posted on 2013-01-09 00:07  clayman  阅读(2439)  评论(1编辑  收藏  举报