【译】光线跟踪:理论与实现(三) 折射与Lambert-Beer 定律

Introduction<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

      作者在这一篇中将解释如何去跟踪折射光线。这将涉及到在相交点处产生新的光线,并且计算新光线的方向。此外,作者还将运用Lambert-Beer 定律来解释光线在物体内部的吸收情况。最后作者将展示如何加入反锯齿的效果,并且如何对光线跟踪器进行加速优化。

Refractions

折射的情况如图1所示,注意观察光线在几何体表面是如何改变方向的,又是如何穿过几何体后面那个点的。在这个点后面的物体会看起来是倒转的镜像。

      几何体表面处光线的弯曲程度取决于两种物质的折射率:光线进入几何体前所在的物质的折射率,构成几何体的物质的折射率。例如:空气和真空的折射率为1.020摄氏度的水的折射率为1.33
   2008041302.jpg

   下面这段代码应该让你感到很熟悉了吧。它就是构造折射光线,递归地跟踪它,并将结果颜色值加入到产生折射光的光线中去。有一点值得注意:法向量乘以了一个值’result’。这个值是对每个几何体的相交测试时获得的,值是10,分别表示是否命中。当然,还有第3个选择:-1,这表示是从几何体内部命中的。这点也是很重要的:如果一个光线命是从外部命中一个几何体的,那其实是没有命中这个几何体,而是它附件的物质。因此,其法向量应该取反。

None.gif      //  calculate refraction
None.gif
         float  refr  =  prim -> GetMaterial() -> GetRefraction();
None.gif        
if  ((refr  >   0 &&  (a_Depth  <  TRACEDEPTH))
ExpandedBlockStart.gifContractedBlock.gif        
dot.gif {
InBlock.gif            
float rindex = prim->GetMaterial()->GetRefrIndex();
InBlock.gif            
float n = a_RIndex / rindex;
InBlock.gif            vector3 N 
= prim->GetNormal( pi ) * (float)result;
InBlock.gif            
float cosI = -DOT( N, a_Ray.GetDirection() );
InBlock.gif            
float cosT2 = 1.0f - n * n * (1.0f - cosI * cosI);
InBlock.gif            
if (cosT2 > 0.0f)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                vector3 T 
= (n * a_Ray.GetDirection()) + (n * cosI - sqrtf( cosT2 )) * N;
InBlock.gif                Color rcol( 
000 );
InBlock.gif                
float dist;
InBlock.gif                Raytrace( Ray( pi 
+ T * EPSILON, T ), rcol, a_Depth + 1, rindex, dist );
InBlock.gif                a_Acc 
+= rcol * transparency;
ExpandedSubBlockEnd.gif            }

ExpandedBlockEnd.gif        }

None.gif
None.gif

下图就是加入折射的效果图:
   2008041303.jpg

毫无疑问,你会注意到现在光线跟踪器很慢的,这是因为每条光线要与每个几何体进行相交测试来找出最近的相交点。当然,有很多方法可以对其进行改进,作者在后面的文章中会使用一种空间细分的方法来对相交测试的数量进行限制的。

Lambert-Beer定律

      上面的图中,你会发现球是蓝色的,因此折射图的颜色也是稍微偏蓝色的。这是因为折射光返回的颜色值会与几何体颜色相乘。这在反射,散射以及镜面光的计算中都有这种情况。

      现在让我们来想象有这么一个水池,里面充满了带颜色的物质(比如说水里面混合着蓝墨水)。池子的浅处大概<?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />10cm深,深处呢有1深。如果你从上往底下看,很明显可以看到在较深的那端的底部受到颜色的影响会比浅处的要大。这种效应就叫Beer定律。

      Beer定律可以用下列公式表示:

None.gif light_out  =  light_in  *  e(e  *  c  *  d)

这个公式主要是用来计算溶解在水中的物质的光吸收度的。’e’是一个常量,表明溶剂的吸收度(准确来说,是单位为L/mol*cm的摩尔吸光率)。’c’是溶质的数量,单位mol/L.’d’是光线的路径长度。一般我们可以简化公式如下:

None.gif light_out  =  light_in  *  e(d  *  C)

这个d是路径长度,C是一个常量,表示物质的密度,减小它的值会使得光线在穿越物体时的时间增大。

吸收及透明度的计算需要以颜色因子来单位进行逐个计算。

None.gif                  //  apply Beer's law
None.gif
                Color absorbance  =  prim -> GetMaterial() -> GetColor()  *   0.15f   *   - dist;
None.gif                Color transparency 
=  Color( expf( absorbance.r ), expf( absorbance.g ), expf( absorbance.b ) );
None.gif                a_Acc 
+=  rcol  *  transparency;

下面是效果图:
2008041301.jpg
   对于目前程序中使用的场景来说,应用这个定律对于画面的质量没有多大影响。不过一旦你开始使用复杂的场景,情况就大不一样了。

很多光线跟踪器都使用下述简单的方法:每个物体都有一个反射变量,它会与折射光返回的颜色值相乘,同时还有一个折射变量,会与折射光返回的颜色值相乘。然而对于折射光来说,这并不正确:每条折射光线在进入和退出几何体时会被计算两次。而且,从一个薄的物体穿过与穿过一个厚的物体有相同的衰减度。最大的问题更在于我们直观上会感觉不对:光线密度在几何体表面没有减小,而只会在几何体内部减小。

SuperSampling

假设我们使用下列代码来替换产生光线处的代码:

None.gif for  (  int  tx  =   0 ; tx  <   4 ; tx ++  )  for  (  int  ty  =   0 ; ty  <   4 ; ty ++  )
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    vector3 dir 
= vector3( m_SX + m_DX * tx / 4.0f, m_SY + m_DY * ty / 4.0f0 ) - o;
InBlock.gif    NORMALIZE( dir );
InBlock.gif    Ray r( o, dir );
InBlock.gif    
float dist;
InBlock.gif    Primitive
* prim = Raytrace( r, acc, 11.0f, dist );
ExpandedBlockEnd.gif}

None.gif
None.gif
int  red  =  ( int )(acc.r  *   16 );
None.gif
int  green  =  ( int )(acc.g  *   16 );
None.gif
int  blue  =  ( int )(acc.b  *   16 ); 
None.gif

这段代码作用是为每个像素点发射16条光线,因此结果图像具有抗锯齿效果,但缺点就是耗时太多。

也许你注意到了光线跟踪器返回的是指向几何体的指针,它指向被primary ray命中的几何体。我们做如下修改,代码的性能就大幅提升:

None.gif //  fire primary rays
None.gif
Color acc(  0 0 0  );
None.gifvector3 dir 
=  vector3( m_SX, m_SY,  0  )  -  o;
None.gifNORMALIZE( dir );
None.gifRay r( o, dir );
None.gif
float  dist;
None.gifPrimitive
*  prim  =  Raytrace( r, acc,  1 1.0f , dist );
None.gif
int  red, green, blue;
None.gif
if  (prim  !=  lastprim)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif        lastprim 
= prim;
InBlock.gif    Color acc( 
000 );
InBlock.gif    
for ( int tx = -1; tx < 2; tx++ ) for ( int ty = -1; ty < 2; ty++ )
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        vector3 dir 
= vector3( m_SX + m_DX * tx / 2.0f, m_SY + m_DY * ty / 2.0f0 ) - o;
InBlock.gif        NORMALIZE( dir );
InBlock.gif        Ray r( o, dir );
InBlock.gif        
float dist;
InBlock.gif        Primitive
* prim = Raytrace( r, acc, 11.0f, dist );
ExpandedSubBlockEnd.gif    }

InBlock.gif    red 
= (int)(acc.r * (256 / 9));
InBlock.gif    green 
= (int)(acc.g * (256 / 9));
InBlock.gif    blue 
= (int)(acc.b * (256 / 9));
ExpandedBlockEnd.gif}

None.gif
None.gif
else
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    red 
= (int)(acc.r * 256);
InBlock.gif    green 
= (int)(acc.g * 256);
InBlock.gif    blue 
= (int)(acc.b * 256);
ExpandedBlockEnd.gif}

None.gif
if  (red  >   255 ) red  =   255 ;
None.gif
if  (green  >   255 ) green  =   255 ;
None.gif
None.gif
if  (blue  >   255 ) blue  =   255 ;
None.gif

ok,it’s over.在下一篇中作者将介绍“空间分割”思想的运用。

原文链接:http://www.devmaster.net/articles/raytracing_series/part3.php

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内容介绍在成像领域,我们有很多手段。比如你可以通过照相机的光学元件,也可以通过像电脑游戏中的那样,通过GPU的一套渲染管线来实现成像。当然除此之外是不是就没有其他的成像方式了呢?当然答案是否定的。 在我们不去使用计算机图形学那套去成像的时候,最土鳖和最容易理解的成像方式就是光线追踪了。这里同学们应该理解到的有一个点,第一光线追踪不是唯一的成像方式,第二它与传统的GPU成像或者说 计算机图形学里说的那些光栅化之类的从思路上就有区别,第光线追踪是最简单的成像方式之一,大概你学完高中数学就可以实现光线追踪,写完两个C++类足以做成非常优质的画面。所以同学们要对光线追踪有一个 清晰的认识,不要认为你学完这一套就无敌了,其实你学完了才会发现,这比OpenGL那些一套一套的规则简单多了。 大部分情况下,由于光线追踪不是按照图形学那边的那些管线来做的,所以它不讲究效率,而是遵循物理意义上的画质最佳。所以基本上你学会光线追踪,且不从事电影行业或者不学习引擎内核去研发高端引擎,那么这块知识估计你会带进坟墓。适合人群光线追踪适合于那些探究画质的同学,你可以轻松的把你的思维应用到你的算法中,但大概率无法转化成为实时算法,也就是无法转化成传统渲染管线这边的一套一套的东西。因为仿真从算法出发点上就是不考虑效率的。 你可以用光线追踪去渲染一些精致的画面,如果你是学习了游戏引擎了的话,你可以尝试自己写一个光线追踪的渲染器,来执行烘焙场景的操作。大部分情况下,通用引擎会使用AutoDesk的Beast SDK,比如Unity3D 里面就有beast.exe。如果你是游戏引擎的内核程序员,那么你有可能将你光线追踪和离线渲染学来的知识通过烘焙场景的方式来应用到你的实际工作中。光线追踪的地位在实时渲染领域中使用光线追踪的算法的探索当然也有人在做,这其中最厉害的当然就是Unreal,值得我们学习。如果你在你的引擎内核里使用了像vulkan这样的高级别渲染器,兼容性会差一点,但是你此时 就可以学习Unreal做光线追踪的思路,在实时渲染中,去或多或少加一点光线追踪。我们可以来思考这样的一个问题,实时渲染追求的是速度与性能,离线渲染追求的是极限画质。于是乎那些大神,或许未来你 就是这些大神中的某一个,你们做的操作莫过于把离线渲染算法中的某一部分比较烧性能的环节,比如通过IBL的方式事先通过离线渲染把所有渲染数据存储到一张图像里去,然后在实时渲染的时候把这张图片 中的数据取出来直接运算,就可以得到比实时渲染好,但是比离线渲染差那么一点点画质。这里之所以无法让实时渲染和离线渲染的画质完全一致是因为我们的3D世界就如同我们的眼球一样精度是很高的。如果你的 图片的分辨率不够大,离线渲染的时候存储的数据都是比较粗糙的采样数据,无法描绘出一个精致的世界。课程安排在我们的课程中,我们来通过最简单的方式,依然是最简单的方式来理解光线追踪是怎么玩出来的。画面或许很好看,但都是简单的高中几何数学,即便我们认为你没写过程序都能看懂意思。我们课程里面不涉及 物理渲染,我们使用的依然是经典的lambert这样的光照模型。物理渲染的方式既可以在实时渲染里实现,也可以在离线渲染里实现。大体的框架不会变,只是计算光的时候算法会变,那部分估计也不是美术可以听懂的了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值