不做死就不会死

    尽管花儿很漂亮,但只不过是朵漂亮的花——仅此而已。并无任何非凡之处。而你只是个会编程的人。或许你很优秀,但没有什么与众不同。你与我、或者这个星球上其他所有人都是同类。
    你不仅要吃喝拉撒。当然,还要睡。(最好)在很久以后,你会驾鹤西游,你所创造的一切也将随之消失。甚至是象征不朽的金字塔很久以后也会消失。你知道那些建造金字塔的人的名字么?即便你真的知道,难道你知道了就那么重要么?并非如此。金字塔依然耸立在那里,或是随风而逝。平淡无奇。

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

RAYTRACING TOPICS & TECHNIQUES -
PART 1 – INTRODUCTION
原作者:Jacco Bikker
原文地址:
http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_1_Introduction.shtml

 

基础

 

Raytracing  是模拟现实世界的一种方式:你看到的色彩是由太阳(多数情况) 产生的光线(rays of
light) 在自然场景中散射,最终到达你的眼睛。如果我们暂时不去管狭义相对论,那么所有的这
些光线都是笔直的。


考虑下面的插图:

image

图 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; 
}; 

 

 

一条射线拥有起点和方向。当从摄像机向外发出射线时,所有射线的起点往往是固定的一个点,
射线通过这个点发射出去,穿过屏幕所在的平面,如下图所示。

 

image

图 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”这个判断是为了修正背对光源的面。

 

结尾

这就是这篇文章的全部了。在下一篇文章中,我们将会加入一些有意思的灯光和阐述如何添加
阴影。

 


下面是本文中光线追踪器的一些截图。

 

image

 

image

 

 

附件

简单的光线追踪器代码:

http://115.com/file/dpjyandx#raytracer1.zip

posted on 2012-05-18 13:13  张家瑞  阅读(3697)  评论(0编辑  收藏  举报