一直以来,电影特效和那些炫目、逼真的三维场景令我惊奇且赞叹不已。在具体接触计算机科学以及C++编程语言之前,我并不知道3DMax或者Maya这类软件的背后究竟发生着什么——在一个可交互的窗口中编辑三维场景,然后进入一个叫做“渲染”的神奇过程,神秘而复杂精妙的事情再不为人知的计算机中发生着,然后就能产生处足以以假乱真的图像。尽管在计算机技术相当发达的今天,通过计算机和软件生成图像并不让人感到惊讶,但是作为一个软件工程的学生,这其中的奥秘依然让我深深着迷。


wKioL1PA64Ozxt34AAEA1pxcEnE827.jpg     

图1    使用本博客所实现的渲染器渲染出的效果



我写这些文字的目的很简单,就是告诉正在阅读这些文字的朋友,如何不依靠任何三维渲染软件,只是用C++编程语言来创造与封面图片一样的真实效果的图像(这充满挑战令人兴奋)。当然,图1并不是光线追踪技术的全部,而且和好莱坞特效比起来也微不足道。但是我相信这是金字塔的地基,有了这些基础,才有可能一步步建造金字塔的上层,并一层一层地向上发展——值得庆幸的是,尽管渲染技术本身极其复杂,但是要达到封面图的效果却并不难,大约5000行C++代码就能生成这样的效果。如果读者是和我一样的计算机专业的大学生,有时候会觉得5000行代码是个天文数字,因为教材上最长的实例代码也不会超过500行(事实上,在我所就读的那所大学,很多计算机专业的学生也许在整个大学生涯中都没有写到5000行代码,大多数时候,他们更宁愿谈谈恋爱或者玩玩游戏)。不过对于那些有实际工作经验的程序员,这是一件相当轻松的事情,在每天少玩2小时的英雄联盟的前提下,也许3~7天就能完成。总而言之,所有的内容都是建立在“不依靠任何三维软件,仅仅使用C++”的假设之上的。

当一件事情完全从零开始,一切的一切都没有任何已有的东西来参考时,事情会变得棘手而且让人手足无措。不过,这也意味着我们会得知整个程序的每一个细节,没有那些高深莫测的API,没有那些复杂的GUI,所有的东西都必须从基础开始,事实上,这有点像自己编写一个链表的类,尽管我们有STL,但是没有什么比编写一个链表(也许你会使用template)更能帮助你理解链表是如何工作的了。

对于如何使用算法生成图像,据我所知,目前仅存在某些基础算法符合实时特征,并可以大致归结为投影算法和图像-空间算法。投影算法将集合图元投影至平面上并负责对象外观的局部着色,这种方法的一种实际实现是“图形流水线”,再进一步地说,就是OpenGL这类的实时图形API,这一类算法支持管线处理机制以及基于显卡的硬件实现方式,因而得到了广泛的应用,特别是在游戏和交互式图像程序中。相对而言,图像-空间算法通过确认像素的光照源来计算该像素的颜色值,比如说,沿着一条光线方向上的逆向光线进行追踪,并且基于一些物理法则来得出这条光线与物体交点的颜色,正由于基于这样的原理,这样的方法被赋予一个优雅的名字:“光线追踪技术”(Ray Tracing)。与光线追踪类似的、用于生成照片级真实地技术还有很多,例如光子映射(Photon Mapping)、辐射度算法(Radiosity)以及一些基于物理和电磁波理论的技术等等。本文讲述的是光线追踪和简单地光子映射技术。

令人遗憾的是,实时图形接口有OpenGL和DirectX这样成熟的API,但是基于物理的渲染技术却没有(我没有仔细考察这一点,似乎有一个叫做OpenRT的开源API,称作是“实时光线追踪的开源程式库”,但是我没有使用过),不过这也正是这些文字存在的原因——这里不涉及到那些高深莫测的编程技术、数据结构或者算法,但是要求您有比较扎实的C++基础(作者本人只是个大学生,相信您的水平完全足够阅读我提供的拙劣的代码),如果您也是计算机专业的大学生,并且也对计算机图形学方面有兴趣,我会非常高兴与您共同探讨相关理论和技术。

最后,欢迎来到计算机渲染的世界!


nocolor

2014年7月于成都





第一章    从#include<iostream>开始


        为什么这篇文章看上去像是在写书?噢,不,老兄,只是因为大学放假了,作者有很多时间而已。

—— nocolor



        尽管我是很想马上开始编写一个渲染框架,不过按照惯例,我们还是得从最基本的部分开始。事实上,之前我就已经写好了一个核心渲染程序,不过我不打算直接把代码贴出来了事,这次几乎是重新开始,试一次完全的新工程。当然,我是肯定没有无聊到先讲解什么是向量或者矩阵,不过可以肯定的是,这些内容在图形学世界里的地位举足轻重。如果您不知道什么事矩阵或者什么是放射变换,那么……呃,我想您一定是大一的新生,没关系,高等数学和线性代数会告诉您所有您感兴趣的东西。本章从一个向量类开始(是的,我们还是要从代码开始),顺带一提的是,本次编程实践使用C++11提供的一些新特新(例如,可变参数的模板),如果您的编译器不支持C++11,没关系,我也提供老版本的代码,这些代码可以运行在所有现行的C++编译器上。

    代码清单1.1

//
//  NC_base_vector.h
//  NCMath
//
//  Created by nocolor on 14-7-10.
//  Copyright (c) 2014年 ___NOCOLOR___. All rights reserved.
//

#ifndef __NCMath__NC_base_vector__
#define __NCMath__NC_base_vector__

#include <iostream>
#include <math.h>

namespace NC
{
    /**
     *  NC_base_vector
     *  
     *  这是一个提供四维向量基础功能的类。
     *
     *  这个类一般并不直接使用,而是作为一些具有向量特征的对象的基类。比如说,点和向量很相似,
     *  它们都用三个或者四个分量,都可以与实数做乘法,但是向量可以加上一个向量,而点与点的加法
     *  却没有意义。与此类似的情况,还有表示RGB颜色的类。
     *  为了将这些区别体现出来,诸如向量、点、RGB颜色的类型可以都从NC_base_vector派生,然后
     *  只需要将NC_base_vector类提供的一些接口声明为非公有,就可以防止进行不符合逻辑的操作。
     */
    template <typename Type>
    class NC_base_vector
    {
    protected:
        //向量的四个分量
        Type x, y, z, w;
        
    public:
        
        //构造函数
        NC_base_vector():x(0), y(0), z(0), w(0){}
        NC_base_vector(const Type& _x, const Type& _y, const Type& _z, const Type& _w):x(_x), y(_y), z(_z), w(_w){}
        NC_base_vector(const NC_base_vector<Type>& vec):x(vec.x), y(vec.y), z(vec.z), w(vec.w){}
        NC_base_vector(const Type& _x, const Type& _y, const Type& _z):x(_x), y(_y), z(_z), w(0){}
        NC_base_vector(const Type& value):x(value), y(value), z(value), w(0){}
        
        //析构函数
        virtual ~NC_base_vector(){}
        
        //设置函数
        virtual const NC_base_vector<Type>& set_x(const Type& _x)
        {
            x = _x;
            return *this;
        }
        
        virtual const NC_base_vector<Type>& set_y(const Type& _y)
        {
            y = _y;
            return *this;
        }
        
        virtual const NC_base_vector<Type>& set_z(const Type& _z)
        {
            z = _z;
            return *this;
        }
        
        virtual const NC_base_vector<Type>& set_w(const Type& _w)
        {
            w = _w;
            return *this;
        }
        
        virtual const NC_base_vector<Type>& set_vector(const Type& _x, const Type& _y, const Type& _z)
        {
            x = _x;
            y = _y;
            z = _z;
            return *this;
        }

        //读取函数
        virtual Type get_x() const {return x;}
        virtual Type get_y() const {return y;}
        virtual Type get_z() const {return z;}
        virtual Type get_w() const {return w;}
        
        //重载[]操作符,可以像数组般使用向量的分量
        virtual Type& operator[] (int i)
        { return *(&this->x + i); }
        
        virtual const Type& operator[] (int i) const
        { return *(&this->x + i); }
        
        //重载向量的常用操作符
        virtual NC_base_vector<Type> operator+ (const NC_base_vector<Type>& rhs) const { return NC_base_vector<Type>(x+rhs.x, y+rhs.y, z+rhs.z); }
        virtual NC_base_vector<Type> operator+ (const Type& rhs) const { return NC_base_vector<Type>(x+rhs, y+rhs, z+rhs); }
        virtual NC_base_vector<Type>& operator+= (const NC_base_vector<Type>& rhs)
        {
            x += rhs.x;
            y += rhs.y;
            z += rhs.z;
            return *this;
        }
        virtual NC_base_vector<Type>& operator+= (const Type& rhs)
        {

            x += rhs;
            y += rhs;
            z += rhs;
            return *this;

        }

        virtual NC_base_vector<Type> operator- (const NC_base_vector<Type>& rhs) const { return NC_base_vector<Type>(x-rhs.x, y-rhs.y, z-rhs.z); }
        virtual NC_base_vector<Type> operator-() const
        {return NC_base_vector<Type>(-x, -y, -z);}
        
        virtual NC_base_vector<Type> operator- (const Type& rhs) const { return NC_base_vector<Type>(x-rhs, y-rhs, z-rhs); }
        virtual NC_base_vector<Type>& operator-= (const NC_base_vector<Type>& rhs)
        {
            x -= rhs.x;
            y -= rhs.y;
            z -= rhs.z;
            return *this;
        }
        virtual NC_base_vector<Type>& operator-= (const Type& rhs)
        {
            
            x -= rhs;
            y -= rhs;
            z -= rhs;
            return *this;
            
        }
        
        virtual NC_base_vector<Type> operator* (const NC_base_vector<Type>& rhs) const { return NC_base_vector<Type>(x*rhs.x, y*rhs.y, z*rhs.z); }
        virtual NC_base_vector<Type> operator* (const Type& rhs){ return NC_base_vector<Type>(x*rhs, y*rhs, z*rhs); }
        friend NC_base_vector<Type> operator* (const Type& lhs, const NC_base_vector<Type>& rhs) {return rhs*lhs;}
        virtual NC_base_vector<Type>& operator*= (const NC_base_vector<Type>& rhs)
        {
            x *= rhs.x;
            y *= rhs.y;
            z *= rhs.z;
            return *this;
        }
        virtual NC_base_vector<Type>& operator*= (const Type& rhs)
        {
            x *= rhs;
            y *= rhs;
            z *= rhs;
            return *this;
            
        }
        
        virtual bool operator== (const NC_base_vector<Type>& rhs) const { return (x == rhs.x && y == rhs.y && z == rhs.z && w == rhs.w); }
        virtual bool operator!= (const NC_base_vector<Type>& rhs) const { return !(*this == rhs); }
        
        virtual NC_base_vector<Type>& operator= (const NC_base_vector<Type>& rhs)
        {
            if(this == &rhs)
                return *this;
            x = rhs.x;
            y = rhs.y;
            z = rhs.z;
            w = rhs.w;
            return *this;
        }
        
        //返回向量的长度
        Type length() const {return sqrt(x*x + y*y + z*z);}
        
        //返回x、y、z中的最大值,之所以不考虑w,是因为在其次坐标中,向量的w一般为0
        Type max_value() const
        {
            Type temp = x > y ? x : y;
            return temp > z ? temp : z;
        }
        
        //返回x、y、z中的最小值,之所以不考虑w,是因为在其次坐标中,向量的w一般为0
        Type min_value() const
        {
            Type temp = x < y ? x : y;
            return temp < z ? temp : z;
        }
        
        //重载<<,只是为了输出方便
        friend std::ostream& operator << (std::ostream& os, const NC_base_vector<Type>& v)
        {
            os << "[";
            os.precision(5);
            os.setf(std::ios_base::showpoint);
            os.width(13);
            os << v.get_x();
            os.width(13);
            os << v.get_y();
            os.width(13);
            os << v.get_z();
            os.width(13);
            os << v.get_w();
            os << "]";
            return os;
        }
        
    };
}

#endif /* defined(__NCMath__NC_base_vector__) */

        嗯……尽管代码清单1.1作为第一组出现的代码,显得有点冗长了,不过好消息是至少您可以直接把它拷贝到文件中就可以编译通过。

       NC_base_vector是一个模板类,因为对于向量来说,不论是float还是double都是合理的数据类型,所以使用template也是合理的选择。和很多常见的向量实现一样,NC_base_vector也包括了各种操作符的重载,例如求两个向量各个分量的和或者差等等——但是值得注意的是,NC_base_vector并不包括求点积、叉积这样的功能,因为它本身并不是数学概念上得向量。不过不必担心,这些基本功能会在另外的类中实现。毕竟NC_base_vector的意义只是“包含四个分量的类”,它本身只是被当做纯粹的数值来使用,当需要逻辑上的数学计算功能时,它的子类(或许也不一定是子类,因为从封装的角度讲,可以使用“has a”的方法来分离接口)会包含相应的功能。

        这样做的好处是,NC_base_vector并不涉及真正的“向量运算”,因此它既可以用来表示点、也可以用来表示RGB颜色,或者别的什么有四个分量组成的类型。当真正需要一个特定的类型的时候——嗯……比如需要用来表示几何点的类型,假设几何点的类叫做NC_point,那么NC_point只需要将operator+()声明为protect或者private,就可以避免对几何点执行加法运算(众所周知,两个点p1和p2的简单相加并不是一个正确地点),这样的灵活性同样适用于RGB颜色。

        另一个不易被察觉的好处是,假设某一天我们需要更高维的向量类,也就是说,如果需要5维或者n维向量的时候,我们无需修改整个工程中所有使用到NC_base_vector的代码(因为我们从来不在真正的计算中使用NC_base_vector,而是使用它的子类,呃,或者那些真正实现了向量功能的类),只需要将NC_base_vector的实现更改为多维的便可以。由于改变实现并不需要改变NC_base_vector提供的接口,因此那些使用这些接口的代码依然可以正常运行,也许我们会在之后的编程中验证这一点(辐射度算法也是生成真实场景的一种技术,但是求解辐射度方程可不是件容易的事情,那时也许会需要n维矩阵来求解……哦!是的!n维矩阵就是n维向量的组合……再加上一点点小小的拓展)。

        需要说明的是,这样的设计并不是花哨的炫耀,作者本身并不是编写代码和软件工程的高手,所以不能证明这样设计代码一定是正确的。至少从简单和性能角度,NC_base_vector或许已经不算合格,不过毕竟本来这些就是写给入门大学生看的,所以如果您有更好地设计,我诚挚地恳求您的指导。

        好的,作为博文,这样的长度已经足够了,本章其余的内容会在以后的文章中完善,欢迎留言。


喝杯咖啡怎么样?或者画会儿画也不错……因为博客不止用来交流技术,也可以用来结交志同道合的朋友,不是吗?

wKioL1PA6hvyrd20AAigsysHF_Q089.jpg耗时4个小时的作品,目前还在继续完善中……