光线追踪技术 - 第二章 – Phone光照模型、镜像和阴影
Raytracing Topics & Techniques - Part 2 - Phong, Mirrors and Shadows
原作者:Jacco Bikker
原文地址:
http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_2_Phong_Mirrors_and_Shadows.shtml
翻译日期:2012年5月24日
在第一章中,我介绍了光线追踪的基础知识:从摄像机发射一系列穿过屏幕到达场景的射线,找出每条射线最近的交点,然后用交点的法线点乘指向光源的向量得出一个简单的漫反射阴影。
在第二章中,我将介绍Phone先生,他的卫生间的镜子和他的背光面:)
考虑下面的这张图:
图一:主射线
这张图展示了第一章中的简单光线跟踪器的射线发射到场景中的情形。一条射线可以与光源或一个物体碰撞,或者什么都没有碰到。这些射线没有反射和折射,它们被称为“主射线”。
除主射线外,你可以使用“二级射线”。下面图片展示了此种情况:
图2:各种各样的二级射线
图中蓝色的线是反射光线。对于反射来说,它简单的从一个平面弹回来。稍后会介绍如何计算它。
绿色的线是折射光线。它比反射光线要难计算一些,但也是可以计算的。计算它需要知道折射率和一个由Snell先生(Willebrord Snellius)制定的公式。
红色的线用来探测光源。简单来说,当计算漫反射光照时,如果光源对交点可见,则将点积乘以1,如果被阻挡则乘以0,如果有一般光源可见,则乘以0.5。(译者注:这段话是要解决光源不是一个点的情况。当光源具有一定体积时,就可能出现光源被遮挡不完全的状况。此时,用第一章中讲的点乘的方式计算出来的这一点的光照强度,需要根据遮挡的情况做调整,需要将计算结果乘以光源没有被遮挡的部分所占的百分比。)
如果你从摄像机开始,沿着一条黄色线出发,你会注意到每条射线都产生了一整套二级射线:一条反射射线、一条折射射线和每个光源的阴影射线。当生成以后,每条二级射线(除了阴影射线)都被看作是一条普通射线。这意味着一条反射射线可以被再次反射、第三次反射、第四次反生......这种技术被称为“递归光线跟踪”。每一条新的射线都会讲它的颜色汇集到它上一级的射线中去,这样,每条射线都会影响到主射线穿过的这个像素点(屏幕上的一个像素)的最终颜色。
为使递归循环可以结束和避免花费过多渲染时间,通常我们会设置一个值来限制递归的深度。
通过法线计算反射射线,可以使用下面的公式:
R = V - 2 * DOT( V, N ) * N;
(R是反射射线,V是入射射线,N是平面法线)
下面的代码可以添加到光线追踪器中为每个光源计算漫反射光照的循环中去。
// calculate reflection float refl = prim->GetMaterial()->GetReflection(); if (refl > 0.0f) { vector3 N = prim->GetNormal( pi ); vector3 R = a_Ray.GetDirection() - 2.0f * DOT( a_Ray.GetDirection(), N ) * N; if (a_Depth < TRACEDEPTH) { Color rcol( 0, 0, 0 ); float dist; Raytrace( Ray( pi + R * EPSILON, R ), rcol, a_Depth + 1, a_RIndex, dist ); a_Acc += refl * rcol * prim->GetMaterial()->GetColor(); } }
如果你没有更改光线追踪器的场景,你会看到下面的图像:
这是一个很大的进步。球体间互相映现,此外,球体也映现出地面。
创建一个“完美的”光照模型是非常复杂的,所以我们取真实光照的近似值。到目前为止,我们使用的漫反射光照模型很好的表现了看起来比较柔和的物体,但是对于有光泽的物体的表现还不够。换言之,现在的光照模型除了光强之外,我们什么都控制不了。
图3:漫反射与镜面反射对比
左侧图片展示了我们目前使用的光照模型:光源向量与法线的点积。它从黑到白是线性变换的。
右侧图片展示的也是同样的点积,但是这次将幂指数提高到50。这次,在两个向量夹角很小的时候,会出现亮斑。随着角度增大,亮度快速下降到零。
组合这些改进是很重要的:我们得到了想当的灵活性。一个材质可以具有漫反射阴影和镜面反射阴影;我们可以通过调整幂指数来改变高光区域的大小。
这还不是很正确。
漫反射阴影是正确的:漫反射材质在所有方向上散射光线,所以最亮的区域应该是在面对光源的地方。可以通过法线与光源方向的向量做点乘来计算这个结果。
镜面反射则有一些不同:简单来说,镜面反射是对光源的反射。你可以在真实世界中检查这个现象:找一个有光泽的物体,把它放到在一个灯光下的桌子上,然后移动你的头。你会注意到当你移动的时候高光区域并不是停留在物体的同一个区域:高光区域就是对光源的反射,它随着视角的变化而变化。Phone建议下面的光照模型,这个模型将反射向量包括在其中:
intensity = diffuse * (L.N) + specular * (V.R)n
(L是从交点到光源的向量,N是交点的法线,V是观察的方向向量, R是L在交点上的反射向量)(译者注:上式中最后的n为n次幂,n是幂指数)
注意这个公式包含了漫反射和镜面反射光照模型。
下面是代码实现:
vector3 V = a_Ray.GetDirection(); vector3 R = L - 2.0f * DOT( L, N ) * N; float dot = DOT( V, R ); if (dot > 0) { float spec = powf( dot, 20 ) * prim->GetMaterial()->GetSpecular() * shade; // add specular component to ray color a_Acc += spec * light->GetMaterial()->GetColor(); }
将这些代码加入光照计算中后,光线渲染器会渲染出如下效果的图片:
画面又是一个很大的进步。
最后一种二级射线是阴影射线。这跟其它的射线有一些不同:它们不直接向产生他们的射线提供颜色信息;它们是用来检测一个光源是否可以“看到”这个交点。这个测试的结果被用在漫反射和镜面反射的计算中。
下面的代码为每个光源创建一条阴影射线,然后把这些射线与场景中的其他物体做相交测试。
// handle point light source float shade = 1.0f; if (light->GetType() == Primitive::SPHERE) { vector3 L = ((Sphere*)light)->GetCentre() - pi; float tdist = LENGTH( L ); L *= (1.0f / tdist); Ray r = Ray( pi + L * EPSILON, L ); for ( int s = 0; s < m_Scene->GetNrPrimitives(); s++ ) { Primitive* pr = m_Scene->GetPrimitive( s ); if ((pr != light) && (pr->Intersect( r, tdist ))) { shade = 0; break; } } }
大部分的代码应该很熟悉了。测试的结果存储在一个浮点型的变量“shade”中:值为1表示一个可见的光源,0表示不可见。用一个浮点型来表示看起来很奇怪;我们以后将会加入区域光源,它们通常只有一部分是可见的。在那种情况下,“shade”就会在0-1之间取值了。
另外,上面的代码没有检测在阴影射线上与其交点最近的物体。因为不需要这样做:只要检测到比光源近的任意一个交点就可以。这是一个很重要的优化,这样做可以尽快结束交点检测部分的循环。
图片:
这是最终效果。两个球体,镜像,漫反射和镜面反射,两个光源引起的阴影。平面上的光照小时在远方,是因为远处的漫反射点积越来越小,球体的光照很好。所有这些在一秒钟就可以渲染完成。
注意观察平面上的阴影的表示,阴影是怎样使平面的谋一部分变为全黑的。还要注意观察球体的颜色是怎样影响被镜像到球体表面的平面的颜色。
光线追踪的一个有意思的地方是,你可以向其中插入新东西,其他的东西照常运行。例如,添加阴影和Phong光照模型的同时加入了镜像和高光。这跟光线追踪的平行性质有关:每个射线之间是没有关联的,这使得递归光线追踪非常适合在多处理器上渲染,同时也非常适合组合多种多样的算法。
另外,在目前的光线追踪器中有一个错误,我会在第三版本中修正:阴影射线测试的结果被同时使用在漫反射和镜面反射上。这显然是错误的。:)
这就是第二章的全部了。下一章:折射、贝尔定律和动态超级采样。
第二版本的光线渲染器可以通过以下链接下载: