Raytracing: Theory & Implementation Part 1, Introduction
编译自原文:http://www.devmaster.net/articles/raytracing_series/part1.php
代码下载:
http://www.devmaster.net/articles/raytracing_series/raytracer1.zip
介绍
在这一系列文章中,我将带你领略光线跟踪的魅力,首先我们会从一些简单元素出发(球体、平面、反射),让你来熟悉基础概念。
然后我们会加入折射、面光源和反走样,进一步改善绘制效果。而质量提高的代价是绘制速度会大大减慢,因此我们还需要添加简单的空间划分来进行加速。
最后,我将会介绍关于全局光照明的内容。
基础
光线跟踪的出发点来源于模仿自然现象,眼睛通常观察到的颜色,是由太阳发出,经过各种反射,最后进入人眼。如下图所示:
|
图 1: 从太阳到人眼的光路图 |
我在图中绘制了一些光线,黄色的光线直接从光源出发达到相机。红色的光线在进入相机之前经过了球迷的反弹。而蓝色的光线是穿过了球体,并引起了传播方向的变化。
图中没有画出由光源发出但是没有到达观察者的光线,因为这些光线对观察者没有影响。因此光线跟踪使用了这样一种方法,让光线从观察者出发,看看它们都击中了什么物体。
代码结构
在文章底部,有一个源代码的链接(VC6),其中包含了一些基础的代码,以及核心代码raytracer,包含在raytracer.cpp/.h和scene.cpp/.h中。向量计算,pi值,屏幕分辨率等定义在文件common.h中。
产生射线
在 raytracer.h中, 可以看到光线的类有如下定义:
class
Ray
{
public
:
Ray() : m_Origin( vector3( 0, 0, 0 ) ), m_Direction( vector3( 0, 0, 0 ) ) {};
Ray( vector3& a_Origin, vector3& a_Dir );
…
private
:
vector3 m_Origin;
vector3 m_Direction;
};
|
每个射线包含一个原点和一个方向,由于射线都是从相机发出,所以原点通常都是一个固定点,从原点出发设想屏幕所在的每个像素点。在二维上看就是下图的效果:
|
图 2: 从相机出发生成射线 |
raytracer.cpp中生成射线的代码如下:
vector3 o( 0, 0, -5 );
vector3 dir = vector3( m_SX, m_SY, 0 ) - o;
NORMALIZE( dir );
Ray r( o, dir );
|
在这段代码中,一条射线从原点o出发,指向屏幕平面的一个点,方向向量被单位化,这条射线就生成了。
这里的屏幕平面是指虚拟空间中的一个矩形,用来代表屏幕。它的宽度为8个单位,长为6个单位,正好适合于800*600的分辨率。当然你也可以在空间中移动、旋转这个矩形。
创建场景
接下来,我们需要创建一个绘制所需要的场景。通常会包括一些几何体像球体、平板,你也可以选择使用三角形,并用三角形来构造其他所有几何题。
看一下scene.h中的类定义, 'Sphere' 和 'PlanePrim' 都继承自 'Primitive',都包含一个'Material'属性,并实现了对应的求法线和求交算法。
场景本身被保存在一个'Scene'的类中,场景的初始化代码如下:
void
Scene::InitScene()
{
m_Primitive = new
Primitive*[100];
// ground plane
m_Primitive[0] = new
PlanePrim( vector3( 0, 1, 0 ), 4.4f );
m_Primitive[0]->SetName( "plane" );
m_Primitive[0]->GetMaterial()->SetReflection( 0 );
m_Primitive[0]->GetMaterial()->SetDiffuse( 1.0f );
m_Primitive[0]->GetMaterial()->SetColor( Color( 0.4f, 0.3f, 0.3f ) );
// big sphere
m_Primitive[1] = new
Sphere( vector3( 1, -0.8f, 3 ), 2.5f );
m_Primitive[1]->SetName( "big sphere" );
m_Primitive[1]->GetMaterial()->SetReflection( 0.6f );
m_Primitive[1]->GetMaterial()->SetColor( Color( 0.7f, 0.7f, 0.7f ) );
// small sphere
m_Primitive[2] = new
Sphere( vector3( -5.5f, -0.5, 7 ), 2 );
m_Primitive[2]->SetName( "small sphere" );
m_Primitive[2]->GetMaterial()->SetReflection( 1.0f );
m_Primitive[2]->GetMaterial()->SetDiffuse( 0.1f );
m_Primitive[2]->GetMaterial()->SetColor( Color( 0.7f, 0.7f, 1.0f ) );
// light source 1
m_Primitive[3] = new
Sphere( vector3( 0, 5, 5 ), 0.1f );
m_Primitive[3]->Light( true
);
m_Primitive[3]->GetMaterial()->SetColor( Color( 0.6f, 0.6f, 0.6f ) );
// light source 2
m_Primitive[4] = new
Sphere( vector3( 2, 5, 1 ), 0.1f );
m_Primitive[4]->Light( true
);
m_Primitive[4]->GetMaterial()->SetColor( Color( 0.7f, 0.7f, 0.9f ) );
// set number of primitives
m_Primitives = 5;
}
|
这段代码给场景中增加了一组平板、两个球体,以及两个光源。这里的光源只是被标注为“光源”的球体。
光线跟踪
现在光线跟踪前的准备工作已经就绪,我们可以来看一下这个过程的伪代码:
For each pixel
{
Construct ray from camera through pixel
Find first primitive hit by ray
Determine color at intersection point
Draw color
}
|
为了确定和光线相交的最近的几何体,需要对场景中的所有物体进行测试,这个是在raytracer.cpp中实现的。
求交代码
初始化以后,下面的代码将被执行:
// 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
}
}
|
这个循环对场景中的每个物体进行操作,调用每个的求交方法。函数Intersect的参数是一条射线,返回一个整数说明是否相交,已经相交点到射线起点的距离。这个循环中保存着当前找到的最近的交点。
颜色
当我们可以确定哪个物体被光线射中以后,光线的颜色就可以被计算出来。简单实用物体的材质颜色实在太无趣。在例子中,使用两种光的漫射进行了计算,通过下面这个循环来实现。
// 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;
// calculate diffuse shading
vector3 L = ((Sphere*)light)->GetCentre() - pi;
NORMALIZE( L );
vector3 N = prim->GetNormal( pi );
if
(prim->GetMaterial()->GetDiffuse() > 0)
{
float
dot = DOT( N, L );
if
(dot > 0)
{
float
diff = dot * prim->GetMaterial()->GetDiffuse();
// add diffuse component to ray color
a_Acc += diff * prim->GetMaterial()->GetColor() * light->GetMaterial()->GetColor();
}
}
}
}
|
这段代码计算了交点到光源的向量,用这个向量和物体表面的法线点积来表示亮度。这样的结果就是物体上正对光源的一定被照的很亮,而周围的点则会暗一些。在这里点积大于0,表示面向光源可以被照射到。
后记
下一篇文章将讲述如何增加更多的关系和阴影,下面是一些效果图。