[转]辐射度算法(二) - GameRes.com

http://dev.gameres.com/Program/Visual/3D/Radiosity_Translation.htm

 

修正半立方体图像

    这是一个三个同样大小的球体的视图。以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(C, N, Full_View)

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

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

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

    C.direction = N rotated 90
° right
    H.right = RenderView(C, N, 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)被渲染。在渲染半立方体时,它们作为一个明亮的像素出现在渲染出来的图片上,因而向面片投射闪耀的光。它运行得基本正确,但是渲染出来的图像 会出现无法令人接受的假相。右图所示的场景被三个点状聚光灯所照亮,其中的两个光源位于柱子的背后,还有一盏光源位于图片左上角附近,方向指向照相机。场景从这个角度看起来良好,但如果摄影机来回移动,就会出现令人厌恶的假相。

 

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值