Graphenix

 
 

Powered by: 博客园
模板提供:沪江博客
博客园 | 首页 | 发新随笔 | 发新文章 | 联系 | 订阅订阅 | 管理

2008年10月11日

CheckPoint

 到今天为止,我已经实现了一个比较完整的延迟着色(ds)渲染器,包括各种各样的类型的处理和融合opaque,translucent,mirror reflect,cube reflect, terrain,sky,particle,primitive,ui,postprocess(hdr,dof,lightshaft, ssao)等。国内很少有人做基于ds的渲染,导致我找不到人讨论,一直都只好到国外翻。
因此除了总结一下之外,后面有人想做可以留个参考。ds本身实现是很容易的,不过集成一个完整的渲染器却也不是那么容易,之所以有前面那篇nvperfhud分析crysis的文章,最初就是因为我想参考它里面的透明物体的shadow receive做法。

 场景方面就不说了,也太琐碎。只说渲染部分。
 渲染器基于层(layer)来渲染。每个层cache resource之后都会进行以下几个pass:
 
 1,首先是在输出gbuffer之前对于反射之类的rtt的东西的处理。
 2,gbuffer建立。能够写入gbuffer的东西只有terrain(这使得地面也可以接受大量的灯光照射)和opaque物体。对于gbuffer之前我是使用两个pass输出gbuffer,一个输出depth,做earlyz,然后才是真正的gbuffer。
后来合并为一个pass,不再做earlyz,物体不多的时候速度上感觉相差不明显,不过受制的因素较多,不好说哪样一定好。绘制的时候可以标记stencil为后面的光照做优化。
 关于gbuffer的格式我试过两种方式。
 一种是rgba8,rgba8,r16g16f,r32f,分别放diffuse(alpha speculargloss),emissive(alpha specularpower),normalxy,depth。可以看出存放的相当猥琐,并且无法用specularcolor,而且存取时normal和depth都需要encode decode。好处是比较少的显存,和勉强能够在低端机器运行(事实上用了ds基本就告别低端了,硬要做的话只能让大家都痛苦,机器痛苦,人也痛苦)。
 另一种是rgba16f*4,分别放normal(alpha depth),diffuse(alpha diffusewrap),specularcolor(alpha power),emissive。一般情况下f16存放depth精度是足够的。使用这种方法好处是只有depth需要encode decode,并且资源充足,可以存放比较多的材质参数。缺点是显存消耗变成两倍。并且肯定无法在低端卡运行了,不过与其它效果组合之后,这种格式对显存的消耗也不一定比前面一种格式多,比如用hdr的情况下,其实可以跟emissive共用一个buffer,而如果透明物体也要跟opaque物体一样接收shadow和光照,那大多数情况下需要第二个buffer,这时候可以跟diffuse共用。
 3,作为优化,这里将translucent物体用alpha test将不透明部分的深度写入gbuffer的depth。这使得某些象素不需要被计算,而且对某些东西有特殊作用,比如头发等交叉透明的东西。
 4,对gbuffer进行着色,对于castshadow的light,使用depth绘制shadowmask,着色的时候加上。每个灯光一个pass,可以通过设置scissor rect和绘制light的boundingvolume来做优化。并且进行fog等统一类型的判断,使用不同的shader。对于是否有shadow等,可以先构造shader cache。shadow现在用pssm+jitter,相对其它vsm,esm,pcss,psm,tsm之类,这个方法还是比较稳定的,能够适应大多数情况,pssm切分的slice数量和长度可以自由设置,一般情况下4个slice可以比较清晰的覆盖200米以上的范围,这可以满足大多数游戏了。
 5,ao和opaque fog,mirror reflect和cubemap也作为emissive加进去,按理这两个应该属于specular,可是由于统一渲染模型的问题,我将其放在这里。
 6,sky,gbuffer在写入的过程中用stencil做标记,所以sky可以放在这里绘制。
 7,translucent物体。用传统方式,但不处理阴影。由于使用pssm,透明物体的接收shadow变成一个真正的nightmare,不仅只能用foreword,而且要决定用的是哪个slice的shadowmap,而且还不一定完全属于哪个slice,虽然分析了crysis的pass,不过我还不是很确定它怎么做,似乎是用了松散的slice的办法,保证物体总能完全落在某个shadowmap内,因此只用一个shadowmap来计算。在这里获得一个逻辑上基本正确的阴影确实是非常麻烦的事,我仍然在寻找更好的方法。
 8,particle,传统方式渲染,不过利用depth做soft particle。
 9,lightshaft等颜色相关的postprocess。
 10,hdr,我试过好多种公式,包括:

1)Reinhard。d3dhdrlighting和nv的一些sample,缺点是太暗,而且灰度低。

2)Reinhard modify。ue3和一些游戏用,比较难控制,很难适应任意亮度。

3)exp。crysis用,比较平滑,不过仍然无法适应任意亮度。

4)pow。nv的某个openglsample,能够适应大多数亮度,不过不太符合人的颜色感觉。

5)log。这是我参照crysis的exp自己凑的公式,目前也用这一个。
 11,颜色无关的postprocess,比如dof。多说两句,dof看似简单,其实要处理的比较正确还是很麻烦的。我参考过很多文章,包括gpugems3的cod的方法,ati advance dof,starscraft2,nv sample,u3,crysis(u3和crysis用的是ati的方法)等等。。。。其中只有cod的方法能够比较正确的处理。其它方法都是有问题的,我现在的做法是结合cod和ati的方法。
 12,ui。
 
 上面几步看似简单,实际上我试验了不少时间。其中的细节还有很多麻烦事,耗费时间不计其数。
 层数量可以自由控制和叠加,渲染到某个target上。
 
 ds的好处网上很多地方都说了,最主要的是能够处理大量光源,并且不需要耗费cpu去寻找每个物体的影响光源。
感觉上ds某些思想跟光线追踪类似,也许它的出现预示着光线追踪的普及应该不太远了。。。

 虽然感受到ds的好处,但同时也被它的不足折磨的够呛。下面说说不足方面,我没有看过shaderx的那篇ds drawbacks的文章。有些东西需要彻底实现过后才能有深刻的印象,下面这些都是我自己碰到的。
 1,首先是透明物体,很多文章说ds最大的问题是透明物体的处理,单独从渲染上来说这实际也不是太大的问题,毕竟只是切换到传统方式来渲染。只是相对ds的方式来说不太和谐。。。主要是破坏了ds的好处,仍然需要去寻找影响光源,而且跟pssm结合之后,这几乎成了我最大的问题。
 2,统一渲染方式。这几个字眼在别的地方也许是一件好事,但在当前硬件下面却不见得。我感觉这才是ds最大的问题。一旦写入gbuffer,那么所有的象素就只能用一个方式来着色。这造成了很多东西需要额外处理,比如sky,某些terrain,水面,镜子(我写入emissive,但我认为并不完美)等等。。。好多东西要额外处理,某些情况下导致能够写入gbuffer的东西变的很少了,ds的好处被压缩了。

 3,室内处理。ds对室内处理薄弱,在不做特殊处理的情况下,很容易发生一个灯从一个房间穿过墙壁照到另一个房间,如果用传统方式,我们可以直接对房间忽略这个灯光。gpugems里面那个韩国游戏用了一种boxlight来处理,我也实现了一个,但效果不太好,因为gbuffer和其它translucent物体都需要进行处理,提高了复杂度。
 4,怪异的材质系统,由于使用了统一渲染模型,因此对于ds来说颜色输出部分已经不需要编辑了,也不可能拿来编辑。我们要做的是渲染公式的输入部分,即输入到gbuffer的材质参数。比如使用phong模型的话,diffuse,specular color,power,emissive,等等。而且由于2的原因,额外处理的东西很多,材质系统变得很难写,也很难编辑,时刻要想到同时满足不同的shader,我到现在也才设计了一种脚本,没有做成编辑器。
 5,显存问题。这个东西也许以后不是问题,但现在还是问题。gbuffer加上其它hdr,ssao,dof,shadow等所用图,显存很容易就超过100m,特别是分辨率大了之后。
 6,Multisample Antialiasing的问题就不说了。
 
 其实还有其它暂时想不起的问题,那些问题都很细小,但让你在做下去的时候感觉就是不太舒服,甚至有些时候想用回传统方式的渲染。
 
 后来我看到了一个称为light pre pass的渲染方式,作者写的模模糊糊,我也看的迷迷糊糊,感觉上逻辑好像不通,或者作者故意掩盖了一些东西没有表达。其做法是类似ds,但反过来,只输出depth和normal,然后就计算灯光,写入一个buffer里。最后再将物体渲染一遍结合灯光的输出结果,也不是很明白。

posted @ 2008-10-11 02:16 linyizsh 阅读(697) 评论(2) 编辑
 
修改crysis支持nvperfhud
    其实对熟悉反汇编的人来说并不难,本来用hook也可以,不过crysis是通过CryRenderD3D9.dll显式加载d3d9.dll,IAT表中没有相关地址,hook比较麻烦。所以我还是修改反汇编来做。

    首先我们需要确定d3d9.dll中Direct3DCreate9的地址,d3d9是系统模块,地址固定,很多工具可以查,没有工具也可以,LoadLibrary即可得到d3d9.dll的地址,然后GetProcAddress即可得到Direct3DCreate9的地址。可知地址是:0x4B66AED0。

    然后找一个反汇编调试工具,我用ollyice,是ollydbg的修改版,用oll启动crysis之后,先直接运行,直到d3d9.dll出现在memory map中:

然后我们转到d3d9.dll模块中,定位到4B66AED0这个地址,设置一个断点:

然后重新运行调试Crysis,一直运行到刚才设置的这个断点,返回堆栈,我们发现Direct3DCreate9这个函数是在CrySystem.dll中调用的,而不是CryRenderD3D9.dll中(实际上后面CryRenderD3D9.dll中还会再调一次,不过我们已经不需要了):

执行到函数返回,IDirect3D9指针就存在eax内存中,从eax内存可以看到IDirect3D9指针指针地址是0x4B641A98:

实际上这也是一个固定地址。转到d3d9.dll模块的这个地址我们看到以下这个表:

这就是IDirect3D9类的函数表,而CreateDevice的偏移是0x40,也就是最后一个函数,可以看到地址是0x4B6C1660,转到这个地址,也就是CreateDevice函数的起始位置,设置一个断点:

然后继续运行,直到断点断下来,可以返回堆栈看看:

其实这里的CreateDevice通过CryRenderD3D9.dll调入,并不是真的创建设备,可能只是做一下检测之类,从返回的代码上看,它创建之后马上就删掉了,我们继续运行,CreateDevice函数又断了下来,这次是真正的创建Device操作,返回调用的函数:

可以看到,其中call ecx就是调用CreateDevice函数,上面7个push是传入参数(最后一个是direct3D指针本身),倒数第二个和第三个就是adapter和devicetype,也就是我们要修改的参数,nvperfhud的要求是这两个参数一个用nv的adapter,通常是1,跟显卡和显示器数量有关,另一个用ref(数值2),如果不修改,调试到这里会发现adapter是0,devicetype是1,如何修改呢,直接改为push 2和push 1是不行的,因为我们看到这两个数来自,dword ptr [esi+8]和dword ptr [esi+4],不管这是哪里的内存,直接push的话会跟它不对应,导致在后面游戏直接死掉,可能后面的代码有其它的匹配判断,所以我们需要连同这两个内存一个修改,容易想到的是在
3816A5FA    8B46 08         mov     eax, dword ptr [esi+8]
和
3816A5FE    8B46 04         mov     eax, dword ptr [esi+4]
之前分别增加两条指令
mov     dword ptr [esi+8], 2
mov     dword ptr [esi+4], 1
但这样一来
指令长度就超过地址范围了,导致后面的指令被覆盖。不过还好我们看到call后面的两条指令
cmp eax, 88760868
jnz short 3816A658
这两条指令的意思是判断createdevice的返回值,如果为0(创建成功),就跳转到下面的地址,既然到了这一步,我们就假设createdevice永远为成功,不要检测了,所以我们直接覆盖掉这些指令,但要在call之后,加一条jmp跳到那个成功的地址,否则将不正确,如下图:

大功告成,先把原来的CryRenderD3D9.dll备份一下,把修改后的模块存为CryRenderD3D9.dll。将crysis拖入nvperfhud,就能看到有分析了。

posted @ 2008-10-11 00:05 linyizsh 阅读(1050) 评论(3) 编辑