到今天为止,我已经实现了一个比较完整的延迟着色(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里。最后再将物体渲染一遍结合灯光的输出结果,也不是很明白。