Hugo Elias
何咏 译
声明:本文原文由Hugo Elias 撰写,由何咏 翻译。本文仅供学习交流之用。
任何人未经本人同意不得私自转载,任何人不得将本文用于任何商业活动。
简 介:这篇文章是一个经典的辐射度算法的教程,详细的讲述了如何通过辐射度算法为静态场景计算光照贴图。这也是大多数游戏所采用的技术。现在很多的论文和书 籍都讨论了如何在实时渲染中应用光照贴图来产生逼真的光照效果,然而他们主要着重于如何组织光照贴图。而光照贴图究竟是怎样计算出来的,也就是全局照明算 法,却极少有资料进行详细的解释。我在网上搜索到这篇文章,看了之后受益匪浅,于是决定将它翻译出来,让更多的人了解这方面的知识。如果对这篇文章有不理 解的地方,可以联系作者,也可以和我共同讨论( 我的网站:http://program.stedu.net ) 。如果翻译有错漏,也敬请指正和谅解,因为这毕竟是本人第一次翻译文章。如果你的英文水平不错,建议直接看原文: 单击这里
光照和阴影投射算法可以大致地分为两大类:直接照明和全局照明。许多人都会对前者较为熟悉,同时也了解它所带来的问题。这篇文章将首先简要地介绍两种方法,然后将深入地研究一种全局照明算法,这就是辐射度。
直接照明
直接照明是一个被老式渲染引擎( 如3D Studio 、POV 等) 所采用的主要光照方法。一个场景由两种动态物体组成:普通物件和光源。光源在不被其他物件遮挡的情况下向某些物件投射光线,若光源被其他物体遮挡,则会留下阴影。
在这种思想之下有许多方法来产生阴影,如Shadow Volume( 阴影体), Z 缓冲方法,光线追踪等等。但由于它们都采用一个普遍的原则,因此这些方法都有同样的问题,而且都需要捏造一些东西来解决这些问题。
直接照明的优缺点:
|
需 要考虑的最重要的问题是,由于这些方法会产生超越真实的图像,他们只能处理只有点光源的场景,而且场景中的物体都能做到完美地反射和漫反射。现在,除非你 是某种富裕的白痴,你的房子可能并不是装满了完全有光泽的球体和点状的光源。事实上,除非你生活在完全不同的物理背景下的一个宇宙空间,你的房间是不可能 出现任何超级锐利的阴影的。
人们宣称光线追踪器和其他渲染器 能够产生照片级 的真实效果是一件非常自然的事情。但想象如果有人拿一张普通光线追踪(这种渲染方法类似经典OpenGL 光栅和光照渲染方法)的图片给你看,然后声称它是一张照片,你可能会回敬他是一个瞎子或者骗子。
同时也应该注意到,在真实世界里,我们仍然能看到不被直接照亮的物体。阴影永远都不是全黑的。直接照明的渲染器 试图通过加入环境光来解决这样的问题。这样一来所有的物体都接受到一个最小的普遍直接照明值。
全局照明
全局照明方法试图解决由光线追踪所带来的一些问题。一个光线追踪器往往模拟光线在遇到漫反射表面时只折射一次,而全局照明渲染器 模拟光线在场景中的多次反射。在光线追踪算法里,场景中的每个物体都必须被某个光源照亮才可见,而在全局照明中,这个物体可能只是简单的被它周围的物体所照亮。很快就会解释为什么这一点很重要。
由全局照明方法产生的图片看起来真正让人信服。这些方法独自成为一个联盟,让那些老式渲染器 艰苦地渲染一些悲哀的卡通。但是,而且是一个巨大的“ 但是” :但是 它们更加地慢。正像你可能离开你的光线追踪渲染器一 整天,然后回来看着它产生地图像激动地发抖,在这儿也一样。
|
优点 |
缺点 |
辐射度算法: |
- 非常真实的漫反射表面光照 |
- 慢 |
蒙特卡罗法: |
- 非常、非常好的效果 |
- 慢 |
用直接照明照亮一个简单的场景我用3D Studio 对这个简单的场景进行了建模。我想让这个房间看起来就像被被 窗外的太阳照亮一样。 因此,我设置了一个聚光灯照射进来。当我渲染它时,整个房间都几乎是黑色的,除了那一小部分能够被光射到的地方。 打开环境光只是让场景看起来呈现一种统一的灰色,除了地面被照射到的地方呈现统一的红色。 在场景中间加入点光源来展现更多细节,但场景并没有你想象中的被太阳照亮的房间那样的亮斑。 最后,我把背景颜色设为白色,来展现一个明亮的天空。 |
|
|
|
用全局照明照亮这个简单的场景我用我自己的辐射度渲染器 来渲染这个场景。我用Terragen 渲染了一个天空盒来作 为光源,并把它放置与窗户之外。除此之外没有使用任何其他光源。 无需任何其他工作,这个房间看起来被真实 的照亮了。 注意以下几点有趣的地方: ? 整个房间都被照亮并且可见,甚至那些背对者太阳的表面。 ? 软阴影。 ? 墙面上的亮度微妙地过度。 ? 原本灰色地墙面,再也不是原始的灰色,在它们上面有了些温意。天花板甚至可以说是呈现了浅粉红色。 |
清空你脑子里任何你所知道的正常的光照渲染方法。你之前的经验可能会完全地转移你的注意力。
我想询问一个在阴影方面的专家,他会向你解释所有他们所知道的关于这个学科的东西。我的专家是在我面前的一小片墙上的油漆。
Hugo: " 为什么你在阴影当中,而你身边的那一片跟你很相像的油漆却在光亮之中?"
油漆: " 你什么意思?"
Hugo: " 你是怎么知道你什么时候应该在阴影之中,什么时候不在? 你知道哪些阴影投射算法?你只是一些油漆而已啊。"
油漆: " 听着,伙计。我不知道你在说什么。我的任务很简单: 任何击中我的光线,我把它分散开去。"
Hugo: " 任何光线?"
油漆: " 是的。任何光线。我没有任何偏好。"
因此你应该知道了。这就是辐射度算法的基本前提。任何击中一个表面的光都被反射回场景之中。是任何 光线。不仅仅是直接从光源来的光线。任何光线。这就是真实世界中的油漆是怎么想的,这就是辐射度算法的工作机制。
在接下来的文章中,我将详细讲解怎样制作你自己的会说话的油漆。
这样,辐射度渲染器 背 后的基本原则就是移除对物体和光源的划分。现在,你可以认为所有的东西都是一个潜在的光源。任何可见的东西不是辐射光线,就是反射光线。总之,它是一个光 的来源,一个光源。一切周围你能看到的东西都是光源。这样,当我们考虑场景中的某一部分要接受多少光强时,我们必须注意把所有的可见物体发出的光线加起 来。
1: 光源和普通物体之间没有区别。
2: 场景中的一个表面被它周围的所有可见的表面所照亮。
现在你掌握这个总要的思想。我将带你经历一次为场景计算辐射度光照的全过程。
一个简单的场景我们以这个简单的场景开始:一个有三扇窗户的房间。这里有一些柱子和凹槽,可以提供有趣的阴影。 它会被窗外的景物所照亮,我假设窗外的景物只有一个很小、很亮的太阳,除此之外一片漆黑。 |
现在,我们来任意选择一个表面。然后考察它上面的光照。 |
由于一些图形学中难以解决的问题,我们将把它分割成许多小片( 的油漆) ,然后试着从他们的角度来观察这个世界。 从这里开始,我将使用面片 来指代“一小片油漆”。
|
选取他们之中的一个面片。然后想象你就是那个面片。从这个角度,这个世界看起来应该是什么样子呢? |
一个面片的视角将我的眼睛贴紧在这个面片之上,然后看出去,我就能看见这个面片所看见的东西。这个房间非常黑,因为还没有光线进入。但是我把这些边缘画了出来以方便你辨认。 通过将它所看见的所有光强加在一起,我们能够计算出从场景中发出的所有能够击中这个面片的光强。我们把它成为总入射光强 。 这个面片只能看见房间以及窗外漆黑的风景。把所有的入射光强加起来,我们可以看出没有光线射到这里。这个面片应该是一片黑暗。 |
一个较低处的面片的视角选择柱子上的一个稍低一些的面片。这个面片能够看到窗外明亮的太阳。这一次,所有的入射光强相加的结果表明有很多的光线到达这里(尽管太阳很小,但是它很亮)。这个面片被照亮了。
|
墙拄上 的光照为墙拄上 的每个面片重复这个过程,每次都为面片计算 总入射光强之后,我们可以回头看看现在的柱子是什么样子。 在柱子顶部的面片,由于看不见太阳,处在阴影当中。那些能看见太阳的被照得很亮。而那些只能看见太阳的一部分的面片被部分地照亮了。 如此一来,辐射度算法对于场景中的每个其他的面片都用几乎一样的方式重复。正如同你所看到的,阴影逐渐地在那些不能看见光源的地方显现了。 |
整个房间的光照: 第一次遍历为每个面片重复这个过程,给我门带了 这样的场景。除了那些能够从太阳直接接受光线的表面这外,所有的东西都是黑的。 因此,这看起来并不像是被很好地照亮了的场景。忽略那些光照看起来似乎是一块一块 的效果。我们可以通过将场景分割为更多的面片来解决这个问题。更值得注意的是除了被太阳直接照射的地方都是全黑的。在这个时候,辐射度渲染器 并没有体现出它与其他普通渲染器 的不同。然而,我们没有就此而止。既然场景中的某些面片被照得十分明亮,它们自己也变成了光源,并且也能够向场景中的其他部分投射光线。 |
在第一次遍历之后面片的视角那些在上次遍历时不能看见太阳而没有接受到光线的面片,现在可以看到其他面片在发光了。因此在下次遍历之后,这些面片将变得明亮一些。 |
整个房间的光照: 第二次遍历这一次,当你为每个面片计算完 入射光强之后,上次全黑的面片现在 正被照亮。这个房间开始变得有些真实了。 现在所发生的是太阳光照射到表面之后反射一次时,场景的效果。 |
整个房间的光照:第三次遍历第三次遍历产生了光线折射两次的效果。所有的东西看起来大致相同,只是轻微的亮了一些。 下一次遍历也仅仅时 让场景更加明亮,甚至第16 次遍历也并没有带来很大的不同。在那之后已经没有必要做更多的遍历了。 辐射度过程 集中在一个光照解决方案上缓慢地进展。每一次遍历都给场景带来一些轻微地变化,直到产生的变化趋于稳定。根据场景复杂度的不同,以及表面的光照特性,可能需要几次或几千次遍历不等。这取决于你什么时候停止遍历,告诉它已经完成了。 |
辐射光强(Emmision)
尽管我曾说过我们应该认为光源和普通物体是一样的,但场景中显然要有光发出的源头。在真实世界中,一些物体会辐射出光线,但有些不会。并且所有的物体会吸收某些波段的光。我们必须有某种方法区分出场景中那些能够辐射光线的物体。我们在辐射度算法中通过辐射光强来表 述这一点 。我们认为,所有的面片都会辐射出光强,然而大多数面片辐射出的光强为0 。这个面片的属性称为辐射光强(Emmision) 。
反射率(Reflectance)
当光线击中表面时,一些光线被吸收并且转化为热能( 我们可以忽略这一点) ,剩下的则被反射开去。我们称反射出去的光强比例为反射率(Reflectance) 。
入射和出射光强(incident and excident lights)
在每一次遍历的过程中,记录另外两个东西是有必要的: 有多少光强抵达一个面片,有多少光强因反射而离开面片。我们把它们称为入射光强和出射光强。出射光强是面片对外 表现的属性。当我们观看某个面片时,其实是面片的出射光被我们看见了。
incident_light( 入射光强) = sum of all light that a patch can see
excident_light (出射光强) = (incident_light *reflectance ) + emmision
面片的数据结构
既然我们了解了一个面片的所有必要属性,我们就应该定义面 片的数据结构了。稍后,我将解释这四个变量的细节。
structure
PATCH
vec4 emmision
float
reflectance
vec4 incident
vec4 excident
end
structure
我已经讲解了算法的基础,下面将再次使用伪代码的形式加以讲解,让它更加具体。很显然这还是在一个较高的层次上,但我会在后面讲述更多的细节。
load scene
divide each surface into roughly equal sized patches
initialise_patches:
for each Patch
in the scene
if this patch
is a light then
patch.emmision
= some amount of light
else
patch.emmision
= black
end if
patch.excident
= patch.emmision
end Patch
loop
Passes_Loop:
each patch collects light from the scene
for each Patch
in the scene
render the scene from the point of view of this patch
patch.incident
= sum of incident light in rendering
end Patch
loop
calculate excident light from each patch:
for each Patch
in the scene