译注
个人先对第一篇的流程做个总结,从一个固定的光源点向一定范围发射出一些主要的光线,想象光线前方有一个巨大的虚拟平面,那么我们要做的就是决定这个虚拟平面上每个像素点的颜色是什么。如何决定呢?很简单,我们跟踪光线前进,看光线会最先与前面场景中哪个几何体发生相交,那么就根据相交点来决定虚拟平面上对应的点的颜色值(比如上一篇中就利用了相交点处几何体的材质颜色与灯光颜色,还加上了散射因子来决定对应的像素点的颜色)。
Introduction
在第一篇中介绍了光线跟踪的基础知识:从一个照相机发射出光线,让这些光线穿过一个屏幕平面进入到场景中,跟踪光线从而寻找出其与几何体最近的相交点,并简单地使用一个点积值来计算散射度,最终完成对像素点颜色值的计算。
下图所示的是第一篇中那个简单的光线跟踪发射到场景中的光线,它们可以击中光源,或者一个几何体,也可以啥也没打中。这里没有反射,也没有折射。我们称之为” primary rays”.
当然与之对应的光线称之为”secondary rays”,如下图所示:
图中的蓝线是反射光线,绿线是折射光线。后者比前者要难计算,但也是可以做的。它的计算主要涉及到折射因子和折射定律。
红线是用来探测光源的。一般来说,如果你想计算散射光,那么若对于相交点来说光源是可见的,就对点积乘以1,否则就乘以0,将其排除出去。当然若光源是半可见就乘以0.5.
如果你跟踪从照相机中发出的一条黄线的话,你会发现每条黄线都可以产生出一系列secondary rays:一条反射光线,一条折射光线,并为每个光源产生一条阴影线。这些光线产生后(除了阴影线外)都可以被视为普通的光线。这意味着一条反射光线可以再被反射和折射,这种方法叫做“递归光线跟踪”。每条新产生的光线都增加了它先前光线聚集的地方的颜色,最终每条光线都对最开始由primary ray穿过的像素点的颜色做出了自己的贡献。
为了防止无穷的循环,一般会对递归的层次有一个限制。
Reflections
为了对于一个已知的平面法向量时对一条光线进行反射,我们使用下面这个公式:
这里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();//颜色值中加入反射光的贡献值
}
}
加入了反射后看起来效果要比上一篇里好些了。注意两个球彼此之间是会互相反射的,而且球也会反射地表平面。
Phong光照模型
到目前为止我们使用的散射光着色对于非刚体来说是不错的,但对于比较有光泽的物体来说就不够好了。此外,我们无法插手去控制它,更别说想去控制光照的强度了。
先来看看下面这幅图:
左图表示的是我们目前使用的光照模型:法向量与光源向量的点积,可以看出在黑白色之间有一个线性的过渡。
右图 使用和左图一样的点积,但权值提高到50。这样一来,当两个向量很接近时就会出现一个很亮的斑点,然后迅速降低到0.
结合进这些改进的话,我们就获得了一些灵活性。一个物体可以有散射,有镜面反射,而我们可以通过调整其权值来设置高亮区的大小。
但这仍是不够的。。。
散射光是可以的:一个散射物体会向各个方向散射光线,所以它最亮的地方恰好就是物体面向光源那儿。用法向量和到光源的向量之间的点积就是这个结果。
镜面光就有点不同了,一般来说,镜面高亮区是对光源的散射性反射。你可以观察下生活中这样的情况:拿一个闪闪发亮的物体,把它放到桌子上并置于灯下,然后移动的视线。你会注意到当你移动眼睛时物体上的发光点并不是始终保持在同一个位置处。这是因为反射作用,当你的视点变化时,它的位置就自然发生改变。Phong于是提出了如下的光照模型,它最大的特点就是将反射向量考虑进来了。
(这里L是从相交点到光源的向量,N是平面法向量,V是视线方向,R是L在表面上的反射向量)。注意这个公式包含了散射和镜面反射光。
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();
}
增加了Phong光照模型的计算后,产生的结果如下图:Shadows
最后一种类型的secondary ray是阴影线。这种光线和其他的不同:它对于产生它的光线的颜色没有贡献;相反,它们经常用来判段一个光源是否可以“看见”一个相交点。
// 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则是一个要排除的光源。这里使用一个浮点数是有点奇怪,但到后面我们会增加面光源,而面光源一般是部分可见的。在那种情况下我们会使用值在0到1之间的’shade’。另外不得不提的是,上面这段代码并不一定可以找到阴影线与场景中的几何体之间最近的相交点。这点并不影响:只要能找到比光源近的几何体就可以了。这样做是出于优化的考虑,因为只要我们尽快找到一个相交点就马上终止循环了。
上图就是我们这篇文章最后得到的效果了,两个光线跟踪的球体,带反射,散射,和镜面光照,还有来自两个光源产生的阴影。
附带一句,这个光线跟踪器有一个bug:阴影测试的结果不仅用于计算散射因子,也用来计算镜面反射因子。严格来说,这是不对的,作者在后面的文章中会提出一个解决方案的。
光线跟踪
Primitive* Engine::Raytrace( Ray& a_Ray, Color& a_Acc, int a_Depth, float a_RIndex, float& a_Dist ) { if (a_Depth > TRACEDEPTH) return 0; // trace primary ray a_Dist = 1000000.0f; vector3 pi; Primitive* prim = 0; int result; // find the nearest intersection for ( int s = 0; s < m_Scene->GetNrPrimitives(); s++ ) { Primitive* pr = m_Scene->GetPrimitive( s ); int res; if (res = pr->Intersect( a_Ray, a_Dist )) { prim = pr; result = res; // 0 = miss, 1 = hit, -1 = hit from inside primitive } } // no hit, terminate ray if (!prim) return 0; // handle intersection if (prim->IsLight()) { // we hit a light, stop tracing a_Acc = Color( 1, 1, 1 ); } else { // determine color at point of intersection pi = a_Ray.GetOrigin() + a_Ray.GetDirection() * a_Dist; // trace lights for ( int l = 0; l < m_Scene->GetNrPrimitives(); l++ ) { Primitive* p = m_Scene->GetPrimitive( l ); if (p->IsLight()) { Primitive* light = p; // 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; } } } // calculate diffuse shading vector3 L = ((Sphere*)light)->GetCentre() - pi; NORMALIZE( L ); vector3 N = prim->GetNormal( pi ); if (prim->GetMaterial()->GetDiffuse() > 0) { float dot = DOT( L, N ); if (dot > 0) { float diff = dot * prim->GetMaterial()->GetDiffuse() * shade; // add diffuse component to ray color a_Acc += diff * light->GetMaterial()->GetColor() * prim->GetMaterial()->GetColor(); } } // determine specular component if (prim->GetMaterial()->GetSpecular() > 0) { // point light source: sample once for specular highlight 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(); } } } } // calculate reflection float refl = prim->GetMaterial()->GetReflection(); if (refl > 0.0f) {//几何体材质的反射度大于0 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();//颜色值中加入反射光的贡献值 } } } // return pointer to primitive hit by primary ray return prim; }
原文链接: http://www.devmaster.net/articles/raytracing_series/part2.php
出处:http://phinecos.cnblogs.com/