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: 用于存储光照强度,如:
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)被渲染。在渲染半立方体时,它们作为一个明亮的像素出现在渲染出来的图片上,因而向面片投射闪耀的光。它运行得基本正确,但是渲染出来的图像 会出现无法令人接受的假相。右图所示的场景被三个点状聚光灯所照亮,其中的两个光源位于柱子的背后,还有一盏光源位于图片左上角附近,方向指向照相机。场景从这个角度看起来良好,但如果摄影机来回移动,就会出现令人厌恶的假相。
|
|
你可以看到,在下方的图片里,出现了三条暗线。这看起来似乎是因为点光源在靠近半立方体边缘的地方就消失了。可能如果我的数学好些的话就不会这么糟糕,但我想就算那样也还是会有引人注意的赝像。 因此,与其将点光源渲染到半立方体中,你不如使用光线追踪,在面片和点光源之间投射光线。 |
|