辐射度算法

来源:互联网 发布:打击网络彩票 编辑:程序博客网 时间:2024/04/28 17:53

简介:这篇文章是一个经典的辐射度算法的教程,详细的讲述了如何通过辐射度算法为静态场景计算光照贴图。这也是大多数游戏所采用的技术。现在很多的论文和书籍都讨论了如何在实时渲染中应用光照贴图来产生逼真的光照效果,然而他们主要着重于如何组织光照贴图。而光照贴图究竟是怎样计算出来的,也就是全局照明算法,却极少有资料进行详细的解释。我在网上搜索到这篇文章,看了之后受益匪浅,于是决定将它翻译出来,让更多的人了解这方面的知识。如果对这篇文章有不理解的地方,可以联系作者,也可以和我共同讨论(我的网站:http://program.stedu.net)。如果翻译有错漏,也敬请指正和谅解,因为这毕竟是本人第一次翻译文章。如果你的英文水平不错,建议直接看原文:单击这里


    光照和阴影投射算法可以大致地分为两大类:直接照明和全局照明。许多人都会对前者较为熟悉,同时也了解它所带来的问题。这篇文章将首先简要地介绍两种方法,然后将深入地研究一种全局照明算法,这就是辐射度。

直接照明

    直接照明是一个被老式渲染引擎(如3D Studio、POV等)所采用的主要光照方法。一个场景由两种动态物体组成:普通物件和光源。光源在不被其他物件遮挡的情况下向某些物件投射光线,若光源被其他物体遮挡,则会留下阴影。

    在这种思想之下有许多方法来产生阴影,如Shadow Volume(阴影体), Z缓冲方法,光线追踪等等。但由于它们都采用一个普遍的原则,因此这些方法都有同样的问题,而且都需要捏造一些东西来解决这些问题。

    直接照明的优缺点:

 

优点

缺点

光线追踪:

- 能够同时渲染由参数或多边形描述的物体
- 允许你实现一些很酷的体效果(volumetric effects)

- 慢速
- 非常锐利的阴影和反射

阴影体:

- 可以加以修改修改以渲染软阴影 (非常有技巧性)

- 实现起来需要技巧
- 非常锐利的阴影 
- 物体只能用多边形描述

Z缓冲
(Shadow Mapping):

- 容易实现
- 快速 (能做到实时)

- 锐利的阴影,锯齿问题。

      

    需要考虑的最重要的问题是,由于这些方法会产生超越真实的图像,他们只能处理只有点光源的场景,而且场景中的物体都能做到完美地反射和漫反射。现在,除非你是某种富裕的白痴,你的房子可能并不是装满了完全有光泽的球体和点状的光源。事实上,除非你生活在完全不同的物理背景下的一个宇宙空间,你的房间是不可能出现任何超级锐利的阴影的。

    人们宣称光线追踪器和其他渲染器能够产生照片级的真实效果是一件非常自然的事情。但想象如果有人拿一张普通光线追踪(这种渲染方法类似经典OpenGL光栅和光照渲染方法)的图片给你看,然后声称它是一张照片,你可能会回敬他是一个瞎子或者骗子。

    同时也应该注意到,在真实世界里,我们仍然能看到不被直接照亮的物体。阴影永远都不是全黑的。直接照明的渲染器试图通过加入环境光来解决这样的问题。这样一来所有的物体都接受到一个最小的普遍直接照明值。

全局照明

    全局照明方法试图解决由光线追踪所带来的一些问题。一个光线追踪器往往模拟光线在遇到漫反射表面时只折射一次,而全局照明渲染器模拟光线在场景中的多次反射。在光线追踪算法里,场景中的每个物体都必须被某个光源照亮才可见,而在全局照明中,这个物体可能只是简单的被它周围的物体所照亮。很快就会解释为什么这一点很重要。

全局照明的优缺点

    由全局照明方法产生的图片看起来真正让人信服。这些方法独自成为一个联盟,让那些老式渲染器艰苦地渲染一些悲哀的卡通。但是,而且是一个巨大的“但是”:但是它们更加地慢。正像你可能离开你的光线追踪渲染器一整天,然后回来看着它产生地图像激动地发抖,在这儿也一样。

 

优点

缺点

辐射度算法:

- 非常真实的漫反射表面光照
- 概念简单,容易实现
- 能够容易地使用3D硬件加速计算

- 慢
- 不能很好地处理点光源
- 也不能处理有光泽的表面
- 总是过于复杂而且很少在书本中解释

蒙特卡罗法:

- 非常、非常好的效果
- 能够很好地模拟各种光学效果

- 慢 
- 轻度困难
- 需要聪明才智来优化
- 总是过于复杂而且很少在书本中解释

 

用直接照明照亮一个简单的场景

    我用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

    我已经讲解了算法的基础,下面将再次使用伪代码的形式加以讲解,让它更加具体。很显然这还是在一个较高的层次上,但我会在后面讲述更多的细节。

   

辐射度算法 伪代码: 级别 1

  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
    I = patch.incident
    R = patch.reflectance
    E = patch.emmision
    patch.excident = (I*R) + E
  end Patch loop
 
  Have we done enough passes?
    if not then goto Passes_Loop
 

代码解释

initialize patches:(初始化面片)
  一开始,所有的面片都是黑的,除了那些能自身辐射出光线的面片。因此,那些能辐射光线的面片的出射光强的初始值应被初始化伪它的辐射光强。其他面片的辐射光强都应为0。

Passes Loop(遍历循环):
   代码多次重复这个循环直到场景有了可接受的光照效果。每次循环之后,也就多模拟了一次光在场景中的反射。

each patch collects light from the scene(每个面片从场景中收集光强)
    如果我之前在文章中解释的那样,每个面片都被它能够看见的其他面片照亮。要达到这个目的,可以简单地把把场景渲染到面片的视角,然后把它所看的光强相加。我将在下一小节更详细地解释这一步。

calculate excident light from each patch(为每个面片计算出射光强):
    计算出有多少光强抵达面片之后,我们现在可以计算出有多少光强离开面片(被反射)。


   这个过程必须被循环多次以达到一个好的效果。如果渲染器还需要一个循环,我们就调转到标记"Passes Loop"。

实现辐射度: 半立方体(Hemi Cubes)

    在实现辐射度算法的过程中,我们首先要解决的问题是,从面片的角度渲染世界。到目前为止我用了鱼眼视角来表示面片看到的场景,但这并不容易,也很难实现。有个更好的方法,半立方体!

 

半球体
    想象把一个鱼眼视图贴到一个半球体表面上。把半球体放在一个面片的前面,那么从那个面片的角度来看,被扭曲场景贴到一个半球体表面上,又变回原来的样子,这和 从那个面片正常渲染的场景是一样的,没有任何区别。

   将一个摄影机放置在半球体的中心,你可以看到视图看起来和普通的渲染是一样的。(右图)

    如果你能找到一个办法能够很轻松地渲染到一个鱼眼视角,那么你可以把每个象素地亮度加起来以确定这个面片地入射光强。然而渲染一个鱼眼视角并不容易,所以我么你必须另寻方法。


从半球体的中心渲染

 

半立方体
    让人惊奇的是(或者让人很不惊奇,取决于你的数学水平) 一个半立方体看起来和一个半球体完全一致。


从半立方体的中心渲染

 

展开半立方体

    想象着将一个半立方体展开. 你得到了什么? 一个正方形图片和四个长方形图片。中间的正方形的图片是从面片的位置,朝向面片的法向量直接渲染得到的。其他的四个部分是与法线呈90°的上、下、左、右方向的渲染结果。

    因此,你可以很容易地得到每一个图片,你只需把摄像机放置在面片上,然后朝着前、上、下、左、右方向各渲染一副图像。四个边上的图片,当然应该分割成一半,因此只需要渲染一半的图像。

 

修正半立方体图像

    这是一个三个同样大小的球体的视图。以90°的透视视角渲染,三个球距摄像机的距离相同,但由于透视变换的属性,在视图两边的物体被拉伸而占据了比中间的物体更大的屏幕面积。

    如果这是半立方体正中间的图像,且三个球体都是光源,那么在图像边缘的物体投射到面片上的光线就会偏多。这会导致不精确性,因此我们必须修正这个问题。

    如果你想用半立方体来计算总共的入射光强,并且仅将半立方体中的像素值都加起来,那些处在图像边缘的物体就会得到一个不公平的权重。这会向面片投射更多的光线。

    为了弥补这一点,将图片边缘的像素变暗是有必要的。这样才能让所有的物体均匀地向面片投射光线。不管它们位于图像的那些位置,我不想完整地解释为什么,只想 告诉你这是怎样做的。

    半立方体表面的像素应乘以摄影机方向和光线入射方向之间的夹角的余弦值。

     左边的贴图用来弥补这个失真。

兰伯特的余弦定律

    任何初学计算机图形学的人都应该知道兰伯特的余弦定律:表面的亮度正比于表面法线和光源方向的夹角的余弦值。因此,我们在这里也应该应用这个定律。这只是简单地将半立方体图像与相关系数相乘。

    左边是一张应用了余弦定律的贴图。白色代表1.0,黑色代表0.0。

两者叠加:乘法贴图

现在注意了,这一点非常重要

    将两个贴图相乘得到了这个贴图。这个贴图对于产生精确的辐射度解决方案是必要的。它用来调节透视投影带来的失真,也包括了兰伯特的余弦定律。

    创建了这个贴图之后,正中间的值应该是1.0,四周角落的值应该是0.0。在它可以使用之前,这个贴图必须被单位化。

    也就是说,贴图中所有的像素值之和应为1.0。 方法如下:

·         对乘法贴图中所有的像素求和

·         将每个像素的值除以这个和.

    现在,贴图中心的像素值应远小于1.0。

计算入射光强

    这个过程在场景中选取一个点(通常是一个面片),以及改点所在表面的法向量,然后计算所有到达该点的光强。

    首先,算法使用RenderView函数渲染半立方体的5个面。这个过程的参数包括一个点,描述了摄影机应放在哪里,以及一个向量,描述了摄影机正前方向,还有一个参数告诉这个过程要渲染半立方体的哪个面。这5张图片存储在hemicube的结构里,记为H(下图的左列)。

    一旦半立方体H被渲染完毕,它就与乘法贴图M相乘(下图中间列)。结果存储在半立方体R中(下图右列)。

    之后,R中的所有像素值相加后除以半立方体的像素总数,这就得到了该点的入射光强。

procedure Calc_Incident_Light(point: P, vector: N)  

    light TotalLight
    hemicube H, R, M
    H
 = empty
    M = Multiplier Hemicube
    R = empty

    div = sum of pixels in M

    camera C
    C.lens
 = P

    C.direction
 = N
    H.front
 = RenderView(CN, Full_View)

    C.direction = N rotated 90° down
    H.down = RenderView(CN, Top_Half)

    C.direction = N rotated 90° up
    H.up = RenderView(CN, Bottom_Half)

    C.direction = N rotated 90° left
    H.left = RenderView(CN, Right_Half)

    C.direction = N rotated 90° right
    H.right = RenderView(CN, Left_Half)

    multiply all pixels in H by corresponding
    pixels in M, storing the results in R

    TotalLight
 = black

    loop p through each pixel in R
      add p to TotalLight 
    end loop
    
    divide TotalLight by div

    return TotalLight
  end procedure

对伪代码中的变量类型的说明

light: 用于存储光照强度,如:

  structure light
    float Red
    float Green
    float Blue
  end structure

hemicube: 用于存储从某一点所观察到的场景。一个半立方体应包含5个图片,如之前所说明的那样,每个像素的类型都是light。对于乘法半立方体来说,所存储的并不是一个光照强度值,而是一些小于1.0乘法因子。之前已经说明。

  structure hemicube
    image front
    image up
    image down
    image left
    image right
  end structure

camera: 如:

  structure camera
    point  lens
    vector direction
  end structure

增加解决方案的精确度

    你可能会自己想到,这种鬼东西似乎要很多的渲染过程。做这些东西使得处理器处在高强度状态。你当然是正确的。基本上你不得不渲染几千次带有纹理的场景。

    所幸的是,这是一个自从黎明破晓的时候人们就在研究的问题了。自从光栅显示器诞生的那一刻起,自从那个时候就有了关于如何快速渲染带有纹理的场景的许多工作。我不会在这一方面走得太深,我确实不是一个最具资格的人来讨论如何优化渲染过程。我自己的渲染器是如此的慢以致于你会用诅咒的语言来描述它。算法本身很适合用3D硬件来加速,可是你必须做 一些额外的前期准备工作来让硬件渲染32位的纹理。

    我即将讨论的速度优化方法不会关心具体的加速半立方体的渲染方法,但是会讨论如何减少半立方体的渲染次数。你会,也理应会注意到光照贴图看起来呈现一种低分辨率的块状,但不要怕,它们的分辨率可以根据你的需要进行调节。

    看一些左边用红线标出的表面。光照效果基本上十分简单,有一个较亮的区域,还有一个不太亮的区域,两者之间有一条相当锐利的界线。要减少边缘的锐利程度,你一般情况下需要一个更高分辨率的光照贴图,因此必须渲染更多的半立方体。但是似乎并不 值得为那些较黑或较亮的区域计算过多的半立方体,处在这两个区域之中的面片的颜色几乎是一致的。但是在锐利的边缘附近多渲染一些半立方体会更加有价值,而对那些处在亮或暗区域之中的面片则不需要过于细分。

    这是十分简单的。我即将讲述的算法将渲染少量的半立方体均匀地覆盖在表面上,然后在靠近边缘的区域渲染更多的半立方体,对于剩下的光照贴图纹素,仅用线性插值来填充。

 

 

算法:在左下角你可以看见正在被创建的光照贴图。在它旁边,你能看到有些像素通过计算半立方体来确定,而有些通过线性插值来决定。

 

1:使用半立方体为每4个像素确定一个值. (左图红色的点)

这些像素在右图用表示.

2: 遍历1:检查相邻两个之间的值的差。如果这个差大于某个阈值,则为像素(左图绿色区域)单独渲染半立方体。否则像素的值由插值决定。

3: 遍历2:检查位于四个像素中心的像素 。如果相邻的两个像素差别太大,为这个像素单独渲染半立方体,否则使用线性插值决定像素的颜色值。

4: 遍历 1:如同第二步,只是空间缩小一半。

5: 遍历 2:如同第三步,只是空间缩小一半。

    你应该能够看到,在左边的图中,大多数光照贴图像素都是通过线性插值决定的。事实上,对于一个由1769个象素的光照贴图来说,仅有563个像素是通过渲染半立方体来决定的。而另外1206个像素是通过线性插值决定的。现在,由于渲染一个 半立方体需要非常长的时间,比起几乎不花费时间的线性插值,这个方法是速度提升了大约60%!

    至此,这个方法还不是完美的。它偶尔会错过光照贴图上一些细节。但在大多数情况下它的结果是非常好的。有个简单的方法来捕获微小的细节,但我把它留给你自己去思考。

    以下是伪代码,注释就不翻译了。

####  CODE EDITING IN PROGRESS - BIT MESSY STILL ####
 
 float ratio2(float a, float b)
 {
     if ((a==0) && (b==0))    return 1.0;
     if ((a==0) || (b==0))    return 0.0;
 
     if (a>b)    return b/a;
     else        return a/b;
 }
 
 float ratio4(float a, float b, float c, float d) 
 {
     float q1 = ratio2(a,b);
     float q2 = ratio2(c,d);
 
     if (q1<q2)    return q1;
     else          return q2;
 }
 
 
 procedure CalcLightMap()
 
 vector  normal = LightMap.Surface_Normal
 float   Xres   = LightMap.X_resolution
 float   Yres   = LightMap.Y_resolution
 point3D SamplePoint
 light   I1, I2, I3, I4
 
 Accuracy = Some value greater than 0.0, and less than 1.0.  
            Higher values give a better quality Light Map (and a slower render).
            0.5 is ok for the first passes of the renderer.
            0.98 is good for the final pass.
 
 Spacing = 4     Higher values of Spacing give a slightly faster render, but
                 will be more likely to miss fine details. I find that 4 is
                 a pretty reasonable compromise. 
 
 
 // 1: Initially, calculate an even grid of pixels across the Light Map.
 // For each pixel calculate the 3D coordinates of the centre of the patch that
 // corresponds to this pixel. Render a hemicube at that point, and add up
 // the incident light. Write that value into the Light Map.
 // The spacing in this grid is fixed. The code only comes here once per Light
 // Map, per render pass. 
 
 for (y=0; y<Yres; y+=Spacing)
     for (x=0; x<Xres; x+=Spacing)
     {
         SamplePoint = Calculate coordinates of centre of patch
         incidentLight = Calc_Incident_Light(SamplePoint, normal)
         LightMap[x, y] = incidentLight
     }
 
 // return here when another pass is required
 Passes_Loop:
     threshold = pow(Accuracy, Spacing)
 
 
     // 2: Part 1.
     HalfSpacing = Spacing/2;
     for (y=HalfSpacing; y<=Yres+HalfSpacing; y+=Spacing)
     {
         for (x=HalfSpacing; x<=Xres+HalfSpacing; x+=Spacing)
         {
             // Calculate the inbetween pixels, whose neighbours 
               are above and below this pixel
             if (x<Xres)    // Don't go off the edge of the Light Map now
             {
                 x1 = x
                 y1 = y-HalfSpacing
 
                 // Read the 2 (left and right) neighbours from the Light Map
                 I1 = LightMap[x1+HalfSpacing, y1]
                 I2 = LightMap[x1-HalfSpacing, y1]
 
                 // If the neighbours are very similar, then just interpolate.
                 if ( (ratio2(I1.R,I2.R) > threshold) &&
                      (ratio2(I1.G,I2.G) > threshold) &&
                      (ratio2(I1.B,I2.B) > threshold) )
                 {
                     incidentLight.R = (I1.R+I2.R) * 0.5
                     incidentLight.G = (I1.G+I2.G) * 0.5
                     incidentLight.B = (I1.B+I2.B) * 0.5
                     LightMap[x1, y1] = incidentLight
                 }
                 // Otherwise go to the effort of rendering a hemicube, 
                       and adding it all up.
                 else
                 {
                     SamplePoint = Calculate coordinates of centre of patch
                     incidentLight = Calc_Incident_Light(SamplePoint, normal)
                     LightMap[x1, y1] = incidentLight
                 }
             }
             
 
             // Calculate the inbetween pixels, whose neighbours are left and 
               right of this pixel
             if (y<Yres)    // Don't go off the edge of the Light Map now
             {
                 x1 = x-HalfSpacing
                 y1 = y
              
                 // Read the 2 (up and down) neighbours from the Light Map
                 I1 = LightMap[x1,y1-HalfSpacing];
                 I2 = LightMap[x1,y1+HalfSpacing];
 
                 // If the neighbours are very similar, then just interpolate.
                 if ( (ratio2(I1.R,I2.R) > threshold) &&
                      (ratio2(I1.G,I2.G) > threshold) &&
                      (ratio2(I1.B,I2.B) > threshold) )
                 {
                     incidentLight.R = (I1.R+I2.R) * 0.5
                     incidentLight.G = (I1.G+I2.G) * 0.5
                     incidentLight.B = (I1.B+I2.B) * 0.5
                     LightMap[x1,y1] = incidentLight
                 }
                 // Otherwise go to the effort of rendering a hemicube, 
                       and adding it all up.
                 else
                 {
                     SamplePoint = Calculate coordinates of centre of patch
                     incidentLight = Calc_Incident_Light(SamplePoint, normal)
                     LightMap[x1, y1] = incidentLight
                 }
 
             }//end if
 
         }//end x loop
     }//end y loop
 
 
 
     // 3: Part 2
     // Calculate the pixels, whose neighbours are on all 4 sides of this pixel
    
     for (y=HalfSpacing; y<=(Yres-HalfSpacing); y+=Spacing)
     {
         for (x=HalfSpacing; x<=(Xres-HalfSpacing); x+=Spacing)
         {
             I1 = LightMap[x, y-HalfSpacing]
             I2 = LightMap[x, y+HalfSpacing]
             I3 = LightMap[x-HalfSpacing, y]
             I4 = LightMap[x+HalfSpacing, y]
 
             if ( (ratio4(I1.R,I2.R,I3.R,I4.R) > threshold) &&
                  (ratio4(I1.G,I2.G,I3.G,I4.G) > threshold) &&
                  (ratio4(I1.B,I2.B,I3.B,I4.B) > threshold) )
             {
                 incidentLight.R = (I1.R + I2.R + I3.R + I4.R) * 0.25
                 incidentLight.G = (I1.G + I2.G + I3.G + I4.G) * 0.25
                 incidentLight.B = (I1.B + I2.B + I3.B + I4.B) * 0.25
                 LightMap[x,y] = incidentLight
             }
             else
             {
                 SamplePoint = Calculate coordinates of centre of patch
                 incidentLight = Calc_Incident_Light(SamplePoint, normal)
                 LightMap[x, y] = incidentLight;
             }
         }
     }
 
 
     Spacing = Spacing / 2
     Stop if Spacing = 1, otherwise go to Passes_Loop

点光源

     人们普遍认为辐射度算法不能很好地处理点光源。从某种程度上讲确实如此,但在真实场景中出现点光源几乎是不可能的。

    我试过向场景增加点状物体作为光源,使它作为粒子像素(Wu-Pixel)被渲染。在渲染半立方体时,它们作为一个明亮的像素出现在渲染出来的图片上,因而向面片投射闪耀的光。它运行得基本正确,但是渲染出来的图像 会出现无法令人接受的假相。右图所示的场景被三个点状聚光灯所照亮,其中的两个光源位于柱子的背后,还有一盏光源位于图片左上角附近,方向指向照相机。场景从这个角度看起来良好,但如果摄影机来回移动,就会出现令人厌恶的假相。

 

    你可以看到,在下方的图片里,出现了三条暗线。这看起来似乎是因为点光源在靠近半立方体边缘的地方就消失了。可能如果我的数学好些的话就不会这么糟糕,但我想就算那样也还是会有引人注意的赝像。

    因此,与其将点光源渲染到半立方体中,你不如使用光线追踪,在面片和点光源之间投射光线。


使用3D渲染硬件加速

    辐射度算法的一个好的方面是它能够十分容易地通过使用3D渲染硬件来优化。只要你能让它做直接的纹理映射,并且关闭着色器、反锯齿以及多贴图(Mip-maps)等。

    具体的优化方法可能并不是你所想象的那样,但它工作得很好,可以让CPU和渲染硬件平行工作。硬件负责处理纹理映射以及隐面消除(Z缓冲),而CPU负责剩下的工作。

    就我所知,目前还没有渲染硬件能够处理浮点型的光照数据,甚至不能处理超过255的光照强度。因此我们无法让它直接渲染带有光照信息的场景,然而,只需一点小技巧,我们可以让硬件来做纹理映射和隐面消除,只要你通过一个快速的循环回读光照信息。

    如果3D硬件可以向屏幕写入33位像素,那么它就可以用来写入任何32位的值。3D硬件并不能真正地向屏幕写入浮点型的RGB值,但它可以写入32位的指针,指向那儿应当出现的面片。一旦有了这个渲染结果,你只需简单地读出每个像素,让后使用它的32位值作为指针定位到那儿应当被渲染的面片。

这是从上面的场景中取出的一张面片贴图。每个像素都由3个浮点数构成,它们分别代表R、G、B通道的值。因此3D硬件不能直接处理这样的贴图。

 

这里是另外一张贴图。它看起来十分的怪异,但是现在不要理会它看起来的样子。在这个贴图中每个像素都是一个32位的值,代表了左图中相应面片的指针。

为什么这个贴图的颜色看起来会有这种效果是因为最低位的三个字节被解释成了颜色。

 

    一旦你有了一整套这样的指针纹理(每个表面都有一个),你可以把它传给3D硬件让其渲染。渲染出来的场景看起来像右图这样。

    场景看起来十分奇怪,但你可以辨认出这些表面都覆盖了像上图那样的纹理。这些像素不应该被理解为颜色,而应该是指针。如果你的显卡使用32位的纹理,那么它会以ARGB的形式出现,且A、R、G、B各 占8位。不要理会这样的结构,把每个像素都当作一个32位的值,把它们作为内存指针来重新创建被面片覆盖的场景。

重要:你必须确保场景是被纯纹理映射的。这就是说:无线性插值,无动感模糊,无着色器/光照,无多贴图,无雾效、无伽马校正。如果你不禁用这些,产生出来的地址就不会指向正确的地方,你的程序肯定会崩溃。

    如何优化辐射度计算过程应该解释得比较清楚了。如果还有不明白的方,告诉我,我会试着加上更多的解释。

误会和迷惑

(如何使用渲染出来的光照贴图)

    辐射度渲染器的输出是一幅每个像素都是三个浮点数的图片。图片中亮度的范围可能十分的广泛。如同我之前所说的那样,天空的亮度比室内表面的亮度要亮的多。而且太阳还要比它亮几千倍。你怎样处理这样的图片呢?

    你普通的显示器最多只能产生很暗的光线,并不比室内的表面所发出的光亮多少。如果要显示这样的图片就需要一个能发出和太阳光一样强烈的光线的显示器。而且显卡对于每个通道都要有一个浮点数。这些东西在技术上并不存在,更别说安全因素。那么你应该怎么做呢?

    很多人对照片对真实世界的记录感到满意,并且认可它是对真实世界忠实的记录。他们错了。照片重现现实中的光亮的本领并不比显示器强。照片不能发出像太阳一样强烈的光,但人们却从不怀疑它的真实性,这就是让人迷惑的地方。

人的视觉

    我们的视觉正是我们最重要的感观。我们每天的生活都信任它,但到目前为止对它的信任还没让我们死亡。它常常拯救了我们的生命。对于我们的祖先来说,这也是一个重要的感观,对于最早的后来发展为人类的鱼来说也是如此。我们的眼球经历了一段漫长的进化过程,对我们的生存有着至关重要的作用,因此它们确实十分的优秀。它们能够感受到非常低的光照度,最低能低到5个光子,同样还能感受到非常明亮的天空。眼球并不是视觉系统的唯一部分,可能更重要的是它们之后的大脑。一个令人难以相信的神经网络,由多层处理过程组成,然后把眼球的输出转化为我们对眼前事物的感知。大脑必须能够辨认出同样的物体,无论它是被照亮还是非常的暗。而且还能做一些让令人惊奇的光线补偿。我们不曾注意到当我们从非常亮的室外走到一个被暗淡的黄色光源照亮的室内时,眼前光亮程度的变化。当你在这两种情况下拍照时,你可能需要改变不同型号的胶卷来防止照出来的图片很黄或很黑。

    试着这样做:在一个阴天出门,站在一个白色物体前,然后看着天上的云,你会觉得这个物体变成灰色了,但你重新看回这个物体时,它又恢复了白色。这说明什么问题?白色的东西被灰色的云所照亮,因此不会存在比云更亮的东西了。但是我们 仍然能够区分出它是白色的。如果你不相信我,以天上的云为背景给白色的物体照一张相。你会看见白色的东西比云暗得多。

别相信你的眼睛: 它们比你聪明得多。

    那么你要怎么做呢?既然人们信服照片带来的真实性,我们可以把渲染器的输出作为场景中的光照模型,然后仿照照相机的原理做一个粗糙的近似。我已经写了一篇关于这部分的文章:Exposure, 因此我就不再多费口舌了。

参考文献

The Solid Map: Methods for Generating a 2D Texture Map for Solid Texturing: http://graphics.cs.uiuc.edu/~jch/papers/pst.pdf
    This paper will be very useful if you are going to try to implement your own radiosity renderer. How do you apply a texture map evenly, and without distortion across some arbitary polygonal object? A radiosity renderer will need to do this.

Helios32: http://www.helios32.com/
    Offers a platform-independent solution for developers looking for radiosity rendering capabilities.

Radiosity In English: http://www.flipcode.com/tutorials/tut_rad.shtml
    As the title suggests this is an article about Radiosity, written using English words. I didn't understand it.

Real Time Radiosity: http://www.gamedev.net/reference/programming/features/rtradiosity2/
    That sounds a little more exciting. There doesn't seem to be a demo though.

An Empirical Comparison of Radiosity Algorithms: http://www.cs.cmu.edu/~radiosity/emprad-tr.html
    A good technical article comparing matrix, progressive, and wavelet radiosity algorithms. Written by a couple of the masters.

A Rapid Hierarchical Radiosity Algorithm: http://graphics.stanford.edu/papers/rad/
    A paper that presents a rapid hierarchical radiosity algorithm for illuminating scenes containing large polygonal patches.

KODI's Radiosity Page : http://ls7-www.informatik.uni-dortmund.de/~kohnhors/radiosity.html
    A whole lot of good radiosity links.

Graphic Links: http://web.tiscalinet.it/GiulianoCornacchiola/Eng/GraphicLinks6.htm
    Even more good links.

Rover: Radiosity for Virtual Reality Systems: http://www.scs.leeds.ac.uk/cuddles/rover/
    *Very Good* A thesis on Radiosity. Contains a large selection of good articles on radiosity, and very many abstracts of papers on the subject.

Daylighting Design: http://www.arce.ukans.edu/book/daylight/daylight.htm
    A very indepth article about daylight.


    译者注:写这篇文章的时候,HDR还没有成为一个主流的技术。而现在,你可以把辐射度渲染器输出的光照贴图作为HDR的来源,产生逼真的动态亮度变化和光晕效果。

0 0
原创粉丝点击