第三章 光线追踪器的基本要素

3.1 光线追踪计算的工作原理

简化版的光线追踪器应能够处理下列操作:

定义多个对象;
确定各个对象的材质;
定义相应的光照;
定义像素的窗口;
for(各个像素)
    从像素中心向对象投射一束光线;
    计算光线与对象之间的最近碰撞点(如果存在);
    if(光线与某一对象发生碰撞)
        通过对象的材质以及光照计算像素的颜色值;
    else
        将像素的颜色值设定为黑色;

在光线追踪计算中,光线可以穿透对象,即使该对象是不透明的。

位于视平面上的像素将垂直于光线,称之为视平面像素。

3.2 场景世界

场景世界包含多个几何对象、光源、相机、视平面以及背景颜色。全部场景元素的位置和方向将使用代表世界坐标或仅采用(x,y,z)。

3.3 光线

一条光线可看作是定义于某一源点o、方向为单位向量d的无限延伸的直线。另外,可通过参数t实现光线的参数化,且t=0处代表了光线的源点。因而,光线上的任一点p可表示为:p=o+td。

光源以及方向在执行光线-对象间的相交计算前,通常定义于世界坐标系统中。

在光线追踪计算中,一般采用下列光线类型

  • 主光线,始于各像素中心位置,并位于透视投影中的相机处;
  • 次级光线,一般为反射光线,且始于对象表面处;
  • 阴影光线,主要用于着色且源于对象表面某处;
  • 光照光线,来自于相应的光源并用于模仿全局光照。

在本章中主要讨论主光线。

Ray.h代码如下:

#ifndef __RAY__
#define __RAY__

#include "Point3D.h"
#include "Vector3D.h"

class Ray
{
public:
    Point3D o;
    Vector3D d;
    Ray(void);
    Ray(const Point3D& origin, const Vector3D& dir);
    Ray(const Ray& ray);

    Ray& operator = (const Ray& rhs);
    ~Ray(void);
};

3.4 光线-对象相交测试

3.4.1 概述

将在区间[x, INF]内计算最小t值。其中x为一个较小的证书,例如t=10^-6^以保证在后续章节中计算结果的正确性。

3.4.2 光线和隐式表面

隐式表面定义:f(x,y,z)=0

采用f(o+td)=0来计算碰撞点。

3.4.3 几何对象

全部几何对象均继承自基类GrametricObject。在本章中奖使用某些简化的数据结构,包括GrametricObject类、Plane类以及Sphere类。

GrametricObject类中的部分声明如下,其中包含了RGBColor,之后会用材质指针来代替RGBColor字段。

class GrametricObject
{
public:
    ...
    virtual bool hit(const Ray& ray, double& tmin, ShadeRec& sr) const = 0;
protected:
    RGBColor color;
};

hit()函数参数列表中的ShadeRec对象充当一个工具类,用以储存光线追踪器所需的全部信息,并对光线-对象间的碰撞点进行着色。着色将会计算反射光线的颜色值。ShadeRec类代码如下:

class ShadeRec
{
public:
    bool hit_an_object;             //did the ray hit an object?
    Point3D local_hit_point;        //world coordinates of hit point
    Normal normal;                  //normal at hit point
    RGBColor color;                 //used in Chapter 3 only
    World& d;                       //worle reference for shading

    ShadeRec(World& wr);            //constructor
    ShadeRec(const ShadeRec& sr)    //copy constructor
    ~ShadeRec(void);                //destructor

    ShadeRec& operator = (const ShadeRec& rhs);
                                    //assignment operator
};

ShadeRec::ShadeRec(World& wr) : hit_an_object(false),
                                local_hit_point(),
                                normal(),
                                color(black),
                                w(wr)
{}

3.4.4 平面

平面的方程为:(p-a)n=0,n为法向量。

将(o+td)代入得,。计算得到t值并可得到碰撞点坐标(更加高效)。

Plane类储存了顶点和发现数据。代码如下:

class Plane : public GeomerricObject
{
public:
    Plane(void);
    Plane(const Point3D p, const Normal& n);
    ...
    virtual bool hit(const Ray& ray, double& t, ShadeRec& s) const;
private:
    Point3D point;                    //point through which plane passes
    Normal normal;                    //normal to the plane
    static const double kEpsilon;     //see Chapter 16
}

bool Plane::hit(const Ray& ray, double& t, ShadeRec& s) const
{
    double t = (point - ray.o) * normal / (ray.d * normal);
    if (t > kEpsilon)
    {
        tmin = t;
        sr.normal = normal;
        sr.local_hit_point = ray.o + t * ray.d;
        return true;
    }
    else
    {
        return false;
    }
}

该碰撞函数仅为一个简单的测试版本,碰撞测试一般都较为复杂。

3.4.5 球体

球体方程可以改写为

代入得

相当于解一个一元二次方程。

代码如下,并没有对d=0时的相切状态加以测试。

bool Sphere::hit(const Ray& ray, double& tmin, ShadeRec& sr) const
{
    double t;
    Vector3D temp = ray.o - center;
    double a = ray.d * ray.d;
    double b = 2.0 * temp * ray.d;
    double c = temp * temp - radius * radius;
    double disc = b * b - 4.0 * a * c;
    if (disc < 0.0)
    {
        return false;
    }
    else
    {
        double e = sqrt(disc);
        double denom = 2.0 * a;
        t = (-b - c) / denom;
        if (t > kEpsilon)
        {
            tmin = t;
            sr.normal = (temp + t * ray.d) / radius;
            sr.local_hit_point = ray.o + t * ray.d;
            return true;
        }
        t = (-b + c) / denom;
        if (t > kEpsilon)
        {
            tmin = t;
            sr.normal = (temp + t * ray.d) / radius;
            sr.local_hit_point = ray.o + t * ray.d;
            return true;
        }
        return false;
    }
}

3.5 颜色值



a和p为两个浮点数。相关计算包括:

操作 定义 返回类型
c1 + c2 (r1 + r2, g1 + g2, b1 + b2) RBG color
ac (ar, ag, ab) RBG color
ca (ar, ag, ab) RBG color
c / a (r / a, g / a, b / a) RBG color
c1 = c2 (r1 = r2, g1 = g2, b1 = b2) RBG color reference
c1*c2 (r1r2, g1g2, b1b2) RBG color
c^p (r^p,g^p,b^p) RBG color
c1 += c2 (r1 += r2, g1 += g2, b1 += b2) RBG color reference

3.6 基本的光线追踪器

3.6.1 工作类

需要12个工作类,如下所示:

几何对象 跟踪器 工具类 场景
GeometricObject Tracer Normal ViewPlane
Sphere SingleSphere Point3D World
- - Ray -
- - RGBColor -
- - ShadeRec -
- - Vector3D -

在目前阶段,World类、ViewPlane类、ShadeRec类以及GeometricObject类仅采用简化版本。

World类内容见P52。

3.6.3 主函数

int main(void)
{
    World w;
    w.build();
    w.render_scene();
    return 0;
}

3.6.3 视平面

ViewPlane类储存了水平、垂直方向上的全部像素以及像素尺寸。

class ViewPlane
{
public:
    int hres;                    //horizontal image resolution
    int vres;                    //vertical image resolution
    float s;                     //pixel size
    float gamma;                 //monitor gamma factor
    float inv_gamma;             //one over gamma
    ...
}

光线均始于各像素点的中心。z~w~坐标用于确定黄线的源点且各光线源点的z~w~均相同。

3.6.4 像素和图像

在一般的观察条件下,通过渲染窗口获得的可见场景常称为视域。

视域取决于水平方向和垂直方向上的像素数量及其尺寸。同时,它们还定义了光线源点以及场景采样点的位置。

3.6.5 build()函数

build()函数如下:

void World::build(void)
{
    vp.set_hers(200);
    vp.set_vres(200);
    vp.set_pixel_size(1.0);
    vp.set_gamma(1.0);
    background_color = black;
    tracer_ptr = new SingleSphere(this);
    sphere.set_center(0.0);
    sphere.set_radius(85.0);
}

3.6.6 渲染场景

函数World::render_scene()负责渲染场景。代码见P57。

3.7 跟踪器

跟踪函数如下:

RGBColor SingleSphere::trace_ray(const Ray& ray) const
{
    ShadeRec sr(*world_ptr);
    double t;
    if (world_ptr->sphere.hit(ray, t, sr))
    {
        return red;
    }
    else
    {
        return black;
    }
}

3.8 颜色显示

display_pixel()函数将各个像素的颜色值转化为显示器可以支持的颜色,这个过程设计到3个步骤:

  • 色调映射
  • gamma值修正
  • 整数映射

显示器的亮度值通常与工作电压呈线性关系,因而gamma值修正是必要的。一般有。其中$v$表示工作电压,$\gamma$表示当前显示器的gamma值。修正之后的表达式为:

3.9 对多个物体实施光线追踪

此时要在World类中添加多个物体,并返回最小值的颜色值进行绘制。