延时光照

2021年9月19日 9点热度 0条评论 来源: pizi0475

去的几个月里,我一直在思考如何解决机库内部和舰船外壳的光照问题。此前,我只使用过单向光源:太阳。以前的绝大多数游戏都是将预先计算的光照嵌入材质(称为“光照贴图”),但这明显不适应“过程生成的游戏”,因为内容是随游戏动态生成的。此外,就算能行,想想一艘几公里长的战列舰表面需要多少材质内存来存储所有的光照信息吧!

  幸好,有办法解决问题……来看看延时光照的神奇宇宙吧!

延时光照

  传统上,通过直向光照就可以不经过预先计算而实现动态光照。算法极为简单:第一次渲染时,运用恒定的环境光颜色给场景渲染提供深度值缓冲和颜色缓冲。此后,每一束光都用附加混合渲染受这束光线影响的几何体。这种光渲染可以包含多种效果,比如法线贴图/逐像素光照,阴影映射,等等……

  这种技术运用在诸如Doom 3之类的游戏上,效果良好,但它受几何尺寸的影响很大。比如说一个由5000个三角形组成的物体,部分受4个光源的影响。这表示,要照亮这个物体,我们需要在总共5次渲染内渲染25000个三角形(1次环境渲染 + 4次光照渲染,每次5,000)。显然,我们可以优化:假定一束光和一个物体,只渲染物体上受该束光线影响的三角形,但这需要事先运算,而《无限星辰》这样的动态和过程生成的游戏却做不到。

  我们不妨设想一下这种状况:一条战列舰由十几组5,000-10,000个三角形的物体组成,而我们计划在战舰外壳上安装100盏灯。问题:如果使用直向光照,要达到渲染效果需要多少个三角形?答案:很多、超多以及太多。

  在现代游戏中越来越多的运用的是延时光照。此技术在显卡着色模式3.0以前不太现实,因为它也需要对几何体进行反复渲染。但使用多重目标渲染,就可以在一次渲染中完成,就要一次!而不必考虑环境光源的数量。不论1个光源还是100个光源:不必按照光源反复渲染受影响的物体。听起来神奇吧?

  延时光照的概念是这样的:在一次正推渲染中,几何信息被渲染成一组缓冲,通常被称为“几何缓冲”(简称:G缓冲)。这组信息通常包括漫反射颜色(漫反射率)、表面法线、像素和镜头之间的深度或直线距离、高光强度、自发光,等等……请注意,在这个阶段,不计算光照。

  这个工作完成后,就运用附加混合渲染每个光源(点光源可视为12个三角形的空间)的边界容积。在像素着色时,再利用当前的光线和深度读取G缓冲以重构像素位置,然后运用该位置计算光线颜色和衰减、确定法线贴图或阴影映射,等等……

落实

G缓冲

  《无限星辰》中有一些技巧和特别用法。我们简单看一下。首先是几何缓冲(G缓冲)。

  我使用了4个RGBAF16缓冲。它们存储下列数据:

- R G B A
缓冲 1 FL(直向光照) FL FL 深度值
缓冲 2 漫反射 漫反射 漫反射 自发光
缓冲 3 法线 法线 法线 高光
缓冲 4 速度 速度 消光 材质ID

  'FL' = 直向光照(Forward lighting)。这在《无限星辰》中比较特殊。我仍旧为阳光和环境光照(包括全部前像素光照、法线贴图和阴影映射)运行一次直向光照扫描,并将结果存储在第一缓冲的RGB通道中。我还可以延缓它,但这样会在在大气散射方面会有问题。在像素层级上,散射方程式非常简单:它只调制一个消光颜色(Fex),并增加一个内散射颜色(Lin):

  Final = Color * Fex + Lin

  Fex和Lin按各几何顶点运算,运算量很大。按各像素计算的话会严重降低帧数。

  如果不进行直向光照扫描,我就必须把散射值存入G缓冲。这将需要6条通道(3条 Fex 的和3条 Lin 的)。 这里我只能用4条,而给灰度等级‘消光’提供一条,用于调制光线(同时阳光必须有一个RGB颜色消光)。

  ‘速度’指视觉空间的速度矢量,用于动态模糊(通过本帧和前帧上像素的位置差计算)。

  ‘法线’存在三个通道中。我有计划只存储到两个通道里,并在着色时再计算第三个着色。不过,这要求把符号位编码到其中一个通道中,因此我尚未落实。法线(以及通常的光照)在视觉空间中计算。

  ‘材质ID’是可以用于光照着色的编号,以便根据物体的材质进行计算。

  由此可见,很难不使用4个G缓冲。

  至于格式,我采取了F16。它便于存储深度,也便于在HDR中进行数值的编码。

性能

  一开始,我对G缓冲的单项和总体性能都不太满意。毕竟有4组F16格式的缓冲:需要很高的带宽。在ATI X1950 XT显卡上,光建立和清除恒定颜色的G缓冲就会导致1280x1024分辨率下帧数为130fps。这还没有导入任何三角形。可以想象,改变分辨率将严重影响帧数,而且,我发现其总体效果和屏幕分辨率成线性关系。

  我还发现“BUG不断的ATI OpenGL驱动”的一个新毛病。清除Z-缓冲的性能只和颜色附着的数量相关。清除附着了4色颜色缓冲的Z-缓冲(即便关闭了颜色写入)比清除只附着了1种颜色的颜色缓冲的Z-缓冲慢4倍。作为“修正”,我需要清除Z-缓冲时,只有先摘除颜色缓冲。

光源扫描(Light pass)

  一旦直向光照扫描完成,其数据都存于G缓冲之后,我就用CPU进行可视平截头体的裁剪(frustum culling),以找到镜头平截头体(frustum)内的全部可见光源。此刻,这些光源按类型排序:点光源、局部光源、指向性光源和环境点光源(最后一种后详)。

  直向光照('FL')颜色被复制到累计缓冲中。所有的光源都在这里累计。在直向光照扫描中采用的深度值缓冲同样适用于延时光照扫描。

  对每个光源,都要“扫描”,并使用下列状态:

  • 深度值测试开启(因此直向光照的深度值缓冲再次附着)
  • 深度值写入关闭
  • 剔除开启
  • 附加混合开启
  • 如果镜头位于体积光中,深度值测试功能设定为较强(GREATER),否则为较弱(LESS)

  使用临界值判断镜头是否在体积光内。临界值的具体数值被选定为至少等于镜头的靠近z值。甚至可以用更高的值,以减少无效渲染(overdraw)。比如一个点光源,就使用边界盒,测试便类似下列情形:

const SBox3DD& bbox = pointLight->getBBoxWorld();
SBox3DD bbox2 = bbox;
bbox2.m_min -= SVec3DD(m_camera->getZNear() * 2.0f);
bbox2.m_max += SVec3DD(m_camera->getZNear() * 2.0f);
bbox2.m_min -= SVec3DD(pointLight->getRadius());
bbox2.m_max += SVec3DD(pointLight->getRadius());
TBool isInBox = bbox2.isIn(m_camera->getPositionWorld());
m_renderer->setDepthTesting(true, isInBox ? C_COMP_GREATER : C_COMP_LESS);

  随着镜头进入体积光中,将深度值测试反转到较强(GREATER),以便在背景/天际盒中非常迅速地舍弃像素。

  我也试验过用边界球描述点光源,但发现减少的无效渲染被更多的多边形所抵消了(100个多边形,而不是边界盒的12个三角形)。

  我尚未着手局部光源,不过或许会用金字塔或圆锥形作为边界体。

  作为优化,所有同类的光源都使用相同的着色和材质进行渲染。这就减少了状态变化,因为我不必在两个光源之间改变着色或材质。

光照着色

  对于每个光源,Z值范围由CPU确定。对于点光源,只要考虑镜头和光源中心的距离,加减光照半径。当在着色中对深度值取样时,超过Z值范围的像素便被舍弃。这是着色过程的第一步。以下为片段:

vec4 ColDist = texture2DRect(ColDistTex, gl_FragCoord.xy);
if (ColDist.w < LightRange.x || ColDist.w > LightRange.y)
  discard;

  着色的其余环节没有太多可说的。一束光由镜头的原点/右方/上方矢量和像素的当前位置被生成。此光束乘以深度值,该值在视觉空间里有位置。光源位置被作为视觉空间的常数上载到着色过程;已经存储于视觉空间的法线也从G缓冲中取样。此后,很容易得出光照公式。别忘记衰减(颜色在光照半径处应该变黑), 否则光照中将有裂缝。

抗锯齿

  在最终扫描时,着色用于光照累计缓冲的抗锯齿。这里没什么创新:我用了Tabula Rasa上面GPU Gems 3的技术。用边缘过滤器从G缓冲的深度值和法线中寻找边缘,已经边缘上“模糊”的像素。参数必须略为调整,但总体上我用不到一个小时就成功了。其效果不如真正的抗锯齿(在延时光照引擎上无法通过硬件实现)好,但还可以接受,而且性能出色(我测量有5-10%的性能损失)。此为经过抗锯齿处理的边缘:

即时光能传递(Instant radiosity)

  延时光照正常运转后,我才惊奇地发现它刻划多个光源时多么出色。实际上,重要的是像素无效渲染,考虑到延时光照的本质,这很自然并在预料之中,但我吃惊地发现,只要无效渲染是恒定的,我可以支持100个光源,而损失不到10%的帧数。

  这让我想到用延时光照的力量,通过即时光能传递(Instant radiosity),增加间接光照。

  算法相对简单:建立各个光源,向随机方向发出N条光子射线。在射线和场景交汇的每个截面上,生成一个光子并存于列表中。该射线此后被干掉(俄国轮盘赌),或被递归地反射到新的随机方向上。每一访问的光子的初始颜色在每次递归反射时都得到表面颜色的累计。我以漫反射材质外加当前访问的重心坐标对表面颜色取样。

  在试验中,我取N = 2048,于是在最终的列表中得到数千个光子。本过程耗时大约 150 毫秒。我得出在一个中等复杂的场景(100,000个三角形)中,我能制造大概每秒20,000个光子,而且尚未进行CPU核心优化。

  第二步,建立规律的坐标格,使用相同单元的光子被合并(颜色只需均匀混合)。于是每个单元的环境点光源就生成了,内含至少一个光子。基于N的值与单元的尺度,可组合成几十至几千种环境点光源。这一过程很快:大概1毫秒处理1000个光子。

  下列截图就有间接光照。请注意那面红墙如何把光透到地板和天花板上。小绿盒子也一样。还请注意主光源没有阴影(在房间正中,靠近天花板),因此有些光泄露到左墙和地板上。最后请注意,环境闭合不是伪造的:没进行SSAO(屏幕空间环境光遮蔽)或是预运算!图片中一共有一个直接点光源和大约500个环境点光源。在NVidia 8800 GTX上,分辨率1280x1024,开抗锯齿的模式,大概44 fps。

结论

  我在Wargrim的机库采用了延时光照和即时光能传递。我用SpAce的材质包,花一个小时完成了该机库的材质。我将部分漫反射材质的侧墙涂成黄色,截图上可以看到:请注意光线是如何反射,并在周围形成泛黄的环境光照的。

  机库内有25个直接点光源。即时光照使用了不同的设置,而且随着环境点光源数量的增加,它们的实际半径在下降。此处是不同栅格尺寸在8800 GTX用1280x1024分辨率的效果:

单元大小 #环境点光源数量 帧数
0.2 69 91
0.1 195 87
0.05 1496 46
0.03 5144 30
0.02 10605 17
0.01 24159 8

  我认为这张表在显示延时光照的威力是特别有用。五千个光源还有30 fps 的帧数!而且都是动态的(虽然在这个例子中都是环境光照光照,是否动态并未意义):不论删除或是移动那盏都不影响帧数!

  在下面的截图中,使用了数百个环境点光源(抱歉,我不记得具体设定了)。在有些图片中能看到绿点/球:那正好显示出环境光源的位置。


全光照:直接光照 + 环境光照


仅有直接光照


仅有环境(间接)光照

文章翻译:Infinity汉化组:江左
原文链接:http://www.infinity-universe.com ... d=105&Itemid=26

    原文作者:pizi0475
    原文地址: https://blog.csdn.net/pizi0475/article/details/8036186
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。