序
一直以来,电影特效和那些炫目、逼真的三维场景令我惊奇且赞叹不已。在具体接触计算机科学以及C++编程语言之前,我并不知道3DMax或者Maya这类软件的背后究竟发生着什么——在一个可交互的窗口中编辑三维场景,然后进入一个叫做“渲染”的神奇过程,神秘而复杂精妙的事情再不为人知的计算机中发生着,然后就能产生处足以以假乱真的图像。尽管在计算机技术相当发达的今天,通过计算机和软件生成图像并不让人感到惊讶,但是作为一个软件工程的学生,这其中的奥秘依然让我深深着迷。
图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或许已经不算合格,不过毕竟本来这些就是写给入门大学生看的,所以如果您有更好地设计,我诚挚地恳求您的指导。
好的,作为博文,这样的长度已经足够了,本章其余的内容会在以后的文章中完善,欢迎留言。
喝杯咖啡怎么样?或者画会儿画也不错……因为博客不止用来交流技术,也可以用来结交志同道合的朋友,不是吗?
耗时4个小时的作品,目前还在继续完善中……
渲染入门,使用C++编写基本的渲染器