RAYTRACING TOPICS & TECHNIQUES -
PART 1 – INTRODUCTION
原作者:Jacco Bikker
原文地址:
http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_1_Introduction.shtml
基础
Raytracing 是模拟现实世界的一种方式:你看到的色彩是由太阳(多数情况) 产生的光线(rays of
light) 在自然场景中散射,最终到达你的眼睛。如果我们暂时不去管狭义相对论,那么所有的这
些光线都是笔直的。
考虑下面的插图:
图 1:光线从太阳到观察者
我在这个图中画了一些光线。黄色光线直接从太阳到摄像机。红色的光线在场景中经过反射后
到达摄像机。蓝色的光线则经过了玻璃球体的折射后到达摄像机。
没有到达摄像机的射线,都没有在这张图上画出来。这就是为什么一个光线跟踪器不是由发光
体追踪到观察者,而是由观察者开始。仔细思考上面的图,你就会发现这是一个很好的办法,
因为这样做可以不用考虑光线的方向。
这就意味着我们可以这样做:我们不用等待太阳发出一条光线正好到达摄像机里面还没有被渲
染的那个像素。我们可以从摄像机的每个像素发出一条射线,来追踪他们到达的地方。
代码实现
在这篇文章的底部,你可以找到一个下载文件的连接。这个文件包含一个小的光线跟踪程序 (vc
6.0 工程文件)。它包含了一些我没有在这里提到的基本的东西(winmain 将一些东西显示到屏
幕上,一个类用来控制像素缓冲区和字体的渲染)和光线追踪器。光线追踪器包含在
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 的 Render 方法中:
vector3 o( 0, 0, -5 ); vector3 dir = vector3( m_SX, m_SY, 0 ) - o; NORMALIZE( dir ); Ray r( o, dir );
在这段代码中,射线由起点’o’开始,射向屏幕所在平面的一个像素位置上。发射方向被规范为
单位向量,最后一行代码,创建了这条射线。
注意“屏幕所在的平面”:它是实际是一个放置在虚拟世界中的矩形平面,代表着屏幕。在这
个简单的光线追踪器中,它与原点居中对齐,它有8个世界单位宽和6个世界单位高,正好适合
800x600 分辨率的电脑屏幕。通过这个平面,你可以做很多有意思的事情:如果你把平面远离
摄像机,射线束将会变窄,物体在屏幕上会变大(参见图 2)。如果你转动这个平面(摄像机点
跟随它一起转动) ,你就会看到不同的视角。这是很有意思的,不同的观察角度和事业的东西,
都只是一个逻辑的副产品。
创建场景
接下来,我们需要一个场景来进行光线追踪。场景由物体(Primitive)组成:几何物体(如:球
体和平面)。你也可以使用三角形,然后用三角形来构筑其他所有的物体(Primitive)。
看一下 scene.h 中类的声明。实体“Sphere”和“PlanePrim”继承于”Primitive”。每个物体
(Primitive)都有材质(Material)属性,和一些实现的方法如:Intersect 和 GetNormal.
整个场景在 Scene 类中列出,我们在 InitScene 方法中可以看到它:
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; }
这个方法向场景中添加了一个地面和两个球体,还有一个光源(实际上是两个)。光源就是一个
被标记为“light”的球体。
光线追踪
到目前为止,所有的准备工作都已做完。下面我们首先来看一段描述光线追踪流程的伪代码。
For 每一个像素
{
建立一个由摄像机穿过这个像素点的射线
找到这条射线碰到的第一个物体(Primitive)
测定在射线与物体的交叉点的颜色
将颜色画到像素上
}
我们测试所有的物体来找到与这条射线交叉点最近的物体。这个操作由 raytracer.cpp 中的
Raytrace方法完成。
交叉点计算代码
在经过一些初始化工作后,下面的代码开始执行:
// 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(); } } } }
这段代码计算了一个从交叉点到光源的向量,然后通过计算这个向量的与交叉点物体的法线的
点积来确定这一点强度。这个强度就是这一点面对这个光源的光照度,这个物体上某个面的法
线与光源的角度越大就越暗。”dot > 0”这个判断是为了修正背对光源的面。
结尾
这就是这篇文章的全部了。在下一篇文章中,我们将会加入一些有意思的灯光和阐述如何添加
阴影。
下面是本文中光线追踪器的一些截图。
附件
简单的光线追踪器代码: