计算机图形学入门:3D渲染指南

978-7-115-58391-8
作者: [瑞士]加布里埃尔·甘贝塔(Gabriel Gambetta)
译者: 贾凡
编辑: 郭媛

图书目录:

详情

如今,计算机图形学无处不在,它为视频、游戏等增添了令人瞩目的细节,为大型电影、动画等增添了逼真的特效。本书围绕计算机图形学这一主题展开,是作者讲授计算机图形学课程多年经验的结晶。 本书着重介绍光线追踪渲染器和光栅化渲染器这两大主流渲染器的基本实现过程,以渲染器的需求背景和实现原理作为出发点,辅以必要的简单数学推导过程,从光到阴影与反射,从直线到着色与纹理,逐渐引导出实现渲染器的伪代码,力求使没有丰富编程经验和深厚数学功底的读者也能够完全读懂。 本书是计算机图形学入门的学习教材,特别适合渴望进入计算机图形学世界的“零基础”读者阅读,也适合对计算机图形学开发感兴趣的爱好者以及相关从业人员使用。

图书摘要

版权信息

书名:计算机图形学入门:3D渲染指南

ISBN:978-7-115-58391-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


版  权

著    [瑞士]加布里埃尔·甘贝塔(Gabriel Gambetta)

译    贾 凡

责任编辑 郭 媛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e58391”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

内 容 提 要

如今,计算机图形学无处不在,它为视频、游戏等增添了令人瞩目的细节,为大型电影、动画等增添了逼真的特效。本书围绕计算机图形学这一主题展开,是作者讲授计算机图形学课程多年经验的结晶。

本书着重介绍光线追踪渲染器和光栅化渲染器这两大主流渲染器的基本实现过程,以渲染器的需求背景和实现原理作为出发点,辅以必要的简单数学推导过程,从光到阴影与反射,从直线到着色与纹理,逐渐引导出实现渲染器的伪代码,力求使没有丰富编程经验和深厚数学功底的读者也能够完全读懂。

本书是计算机图形学入门的学习教材,特别适合渴望进入计算机图形学世界的“零基础”读者阅读,也适合对计算机图形学开发感兴趣的爱好者以及相关从业人员使用。

译者序

无意间,我在亚马逊英文官网浏览计算机图形学书籍时看到了本书英文原版,查看内容时,不禁感慨,如果当年我从程序员转行做技术美术(technical artist,TA)的时候,有这样一本计算机图形学入门的教材,将会是多么幸福的一件事情。回想当年学习计算机图形学的时候,迎面而来的各种复杂数学推导过程,逼着我重新拿起了大学的数学课本,除此之外还要面对各种复杂的算法,相信很多从业人员在学习计算机图形学的过程中都会面临这些挑战。本书的作者有着多年计算机图形学教学经验,他将复杂的数学推导和算法实现以浅显易懂的方式讲述给读者。本书是一本绝佳的入门教材。

计算机图形学是一个飞速发展的领域,从前沿的学术研究到具体的行业应用(比如游戏开发、影视特效制作),都对相关人员提出了更大的挑战,相关从业人员只有不断地学习和提高,才能迎接这一挑战。

在这里要感谢我的父母、我的妻子和可爱调皮的儿子,没有你们的付出和欢声笑语,没有你们的包容和理解,本书的翻译工作也不会如此顺利。

感谢人民邮电出版社的老师们,感谢郭媛编辑在本书翻译过程中耐心、细致的指导,没有你们高效、负责的工作,本书无法顺利出版。

感谢网易大话事业部EVE手游开发团队的所有美术同事,感谢大家营造的和谐愉快的工作环境,没有这样的工作环境,本书的翻译工作不会如此顺利。

谨以此书献给所有热爱计算机图形学的每一位读者,祝大家在技术探索道路上,学习愉快!

贾凡

2021年10月7日

前  言

计算机图形学是一个引人入胜的主题。我们是如何将一些算法和几何数据转变成《星球大战》(Star Wars)和《复仇者联盟》(the Avengers)等电影的特效,《玩具总动员》(Toy Story)和《冰雪奇缘》(Frozen)等动画电影的特效,或者《堡垒之夜》(Fortnite)和《使命召唤》(Call of Duty)等流行电子游戏的图像的呢?

计算机图形学也是一个非常广泛的主题:从渲染三维(3-dimension,3D)场景到创建图像滤波器,从数字排版到模拟粒子系统,有许多学科可以视为计算机图形学的一部分。一本书不可能涵盖所有主题,涵盖所有主题应该需要一个图书馆。本书专注于渲染3D场景这一主题。

本书是我为大家提供的浅显易懂地学习计算机图形学的一次尝试。本书的写作目标是让读者(甚至是高中生)能够轻松理解,同时对专业工程师依然保持足够的严谨性。本书涵盖了与完整大学课程相同的主题——事实上这正是基于我在大学里面教授这门课程的多年经验。

这本书是写给谁的

本书适合任何对计算机图形学感兴趣的人阅读,无论你是一名高中生还是一位经验丰富的专业人士。

在书中对思路和算法的选择上,我有意识地选择简单明了的表述方式。虽然书中算法是有行业标准的,但每当有不止一种方法可以实现某个结果时,我都会选择最容易理解的那一种。同时,我也付出了相当大的努力来确保本书没有华而不实的东西或者“耍花招”。我试着记住阿尔伯特·爱因斯坦的建议:“事情应该力求简单,不过不能过于简单。”(Everything should be made as simple as possible,but no simpler.)

阅读本书不需要太多必备知识,也不需要依赖任何软件或硬件。本书中唯一需要使用的基元(primitive)是一个可以用来设置像素颜色的函数,这也与英文书名中的“from Scratch”(从零开始)相吻合[1]。本书使用的算法在概念上很简单,涉及的数学知识也很简单——最多涉及一点点三角函数的知识。我们还要使用一些线性代数的知识。本书包含简短的附录,以非常实用的方式展示了我们阅读本书所需的一切线性代数知识。

[1] 英文书名中的“Computer Graphics from Scratch”也印证了阅读本书不需要太多必备基础知识,适合“从零开始”入门计算机图形学的读者。——译者注

这本书涵盖的内容

本书“从零开始”,构建两个完整的、功能齐全的渲染器:光线追踪渲染器和光栅化渲染器。尽管它们采用截然不同的方法,但是使用它们渲染简单场景时会产生相似的结果。图1显示了两个渲染器的对比效果。

图1 一个简单的场景,分别由本书介绍的光线追踪渲染器(左图)和光栅化渲染器(右图)渲染得到

虽然光线追踪渲染器和光栅化渲染器有相当多的相同特性,但它们并不完全相同,本书将探讨它们的具体优势,我们在图2中可以看到其中一些。

图2 光线追踪渲染器和光栅化渲染器有其独特的功能。左图为光线追踪阴影和递归反射,右图为光栅化纹理

本书所有案例都有通俗易懂的伪代码,读者还可以通过异步社区获取完整的实现代码(它们是用JavaScript编写的,可以直接在任何Web浏览器中运行)。

为什么阅读这本书?

本书提供了编写软件渲染器所需的几乎所有知识。本书没有使用现有的渲染接口(API,应用程序编程接口),如OpenGL、Vulkan、Metal或DirectX。

现代GPU的功能强大且无处不在,很少有人会有充分的理由去编写一个纯软件实现的渲染器。但是,编写一个软件渲染器的经验是很有价值的,原因如下。

着色器就是软件。在20世纪90年代早期,第一个古老的GPU直接在硬件中实现了渲染算法,我们可以使用它但不能修改它(这就是20世纪90年代中期的大多数游戏看起来彼此相似的原因)。现如今,我们可以编写自己的渲染算法,大家称之为着色器(shader),它们在GPU的特殊芯片中运行。

知识就是力量。理解不同渲染技术背后的理论,而不是复制和粘贴一知半解的代码片段或者直接套用某些流行的技术方法,可以让我们编写出更出色的着色器代码以及渲染管线(rendering pipeline)。

计算机图形学很有趣。很少有计算机科学领域能够像计算机图形学这样提供即时的满足感。当我们的SQL语句正确运行时,我们所获得的成就感是无法与我们第一次正确获得光线追踪反射时的感觉相比的。我在大学教了5年计算机图形学课程,我经常想:为什么我喜欢一学期接着一学期地教同样的东西,而且教了这么长时间?当我看到我的学生们的脸被屏幕照亮时,当我看到他们使用自己创建的第一个渲染场景作为他们的桌面背景时,我觉得一切都是值得的。

关于本书

本书总共15章,主要分为两个部分,第一部分(第2章~第5章)介绍光线追踪(raytracing),第二部分(第6章~第15章)介绍光栅化(rasterization),这两部分也分别对应我们将要构建的两个渲染器。

第1章介绍理解这两部分内容所需的一些基础知识。建议读者按照顺序阅读这些章节,但是光线追踪和光栅化这两部分的内容都是完全独立的,读者也可以分开阅读这两部分。

以下是对每一章内容的概述。

第1章 基础入门概念 这一章我们介绍画布(canvas)的定义,它是一个抽象的表面,我们将在其上进行图像绘制。还介绍PutPixel函数,它是我们可以在画布上进行绘制的唯一工具。我们也会带领读者学习颜色的表示方法和运算方法。

第一部分 光线追踪

第2章 基础光线追踪知识 我们开发一个基础的光线追踪算法,能够渲染一些看起来像彩色圆圈的球体。

第3章 光 我们建立一个光与物体相互作用的模型,并对光线追踪渲染器进行扩展,从而可以模拟光。这样在第2章中我们渲染的球体看起来才像真正的球。

第4章 阴影和反射 我们改善球体的渲染效果:它们可以相互投射阴影,并且可以有类似镜面的表面,那样我们就可以看到其他球体的反射效果。

第5章 扩展光线追踪渲染器 我们简单介绍一些可以添加到光线追踪渲染器中的附加功能,但是这些功能的细节超出了本书的范围。

第二部分 光栅化

第6章 直线 我们从一块空白画布开始,开发一种算法来绘制线段。

第7章 填充三角形 我们继续使用第6章的一些核心思想来开发一种算法,用以绘制用单一颜色填充的三角形。

第8章 着色三角形 我们扩展第7章的算法,用平滑的颜色渐变填充三角形。

第9章 透视投影 我们需要从绘制二维(2-dimension,2D)图形中抽身出来学习一些几何和数学知识,我们需要用这些知识来将3D点转换成2D点,这样就可以将它们绘制在画布上了。

第10章 场景的描述和渲染 我们为场景中的物体开发一种表示方法,并探索如何使用透视投影将它们绘制在画布上。

第11章 裁剪 我们开发一种算法来移除相机看不到的那部分场景。现在我们可以安全地从任何相机位置渲染场景。

第12章 移除隐藏表面 我们结合透视投影和着色三角形来渲染实体;为了能够渲染出正确结果,我们需要确保远处的物体不会覆盖近处的物体。

第13章 着色 我们将探索如何将第3章开发的光照方程应用于整个三角形。

第14章 纹理 我们开发一种算法,可以在我们的三角形上“绘制”图像,以此来伪造表面细节。

第15章 扩展光栅化渲染器 我们概述可以添加到光栅化渲染器的功能,但这些功能的细节超出了本书的范围。

附录 线性代数 我们介绍将在本书中使用的一些线性代数的基本概念:点、向量和矩阵。我们介绍可以使用它们完成的操作,并提供一些示例,以此说明我们可以使用它们做什么。

关于作者

我是谷歌的高级软件工程师。我曾经在Improbable公司工作,这是一家真正有机会在现实中创造出“黑客帝国”的公司,至少它彻底改变了多人游戏开发领域。我也曾在Mystery Studio公司工作,这是一家我创立并运营了大约10年的游戏开发公司,它发布了近20款大家可能从未听说过的游戏。

我在大学教了5年的计算机图形学,这是一门为期一学期的大学三年级课程。我要感谢我所有的学生们,他们在无意间充当了我创作素材的实验对象,正是这些素材为本书的创作提供了灵感。

致  谢

很少有书能在极短时间内出版,你将要阅读的这本书已经酝酿了将近20年。正如你可能猜到的那样,很多人以这样或者那样的方式参与到本书的创作中,我要在此感谢他们。按照时间的先后顺序罗列如下。

奥马尔·帕加尼尼、埃内斯托·奥坎波·埃迪和罗伯托·卢布林曼 作为乌拉圭天主教大学工程学院院长和计算机科学系主任,他们给予了我极大的信任,在我还是大学四年级学生的时候,他们就让我接管了计算机图形学课程,并允许我以自认为最好的方式彻底改造这门课程。罗伯托·卢布林曼副教授在我第一年的教学中一直是我的好导师。

我的2003~2008届学生们 除了不知情地接受我不断改进的教学方法之外,他们还接受并尊敬一位仅比他们大一岁(有些时候比他们还小)的教授。当他们创作出第一张光线追踪图像时,他们脸上洋溢的笑容让这一切努力都变得值得。

亚历杭德罗·塞戈维亚·阿扎皮安 从一名学生变成助教,再变成我的朋友,他的投入帮助我不断改进教学素材。在他后来非常成功的职业生涯中,他专注于实时渲染和性能优化,这让我感到自豪。他也是本书的技术审稿人,从修改拼写错误到建议对某些章节进行深层次的结构改进,他的贡献范围很广。

JC范温克尔 他亲自对本书进行审阅编辑,提出了许多有价值的改进建议。

《黑客新闻》的读者们 我的讲义、图表和演示登上了《黑客新闻》的头版,吸引了相当多的关注——包括No Starch出版社的关注。如果这一切没有发生,这本书可能永远不会存在。

比尔·波洛克、亚历克斯·弗里德、凯茜·安德烈亚迪斯以及整个No Starch出版社团队 在我之前看来,我的讲义和图表已经完全达到了可作为一本书出版的水平。他们指导我整理和重塑了我的讲义和图表,使它们变成一本真正的书。他们将原始素材提升到一个全新的水平。我之前并不知道一本书的出版需要如此多的工作和努力,比尔、亚历克斯、凯茜和出版社团队做得非常出色。虽然图书封面上只有我的名字,但请不要忘记,这是一个团队的努力成果。

资源与支持

本书由异步社区出品,社区(https://www.epubit.com)可为您提供相关资源和后续服务。

配套资源

本书提供如下资源:

本书源码;

本书彩图文件。

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载页面,按提示进行操作即可。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e58391”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

提交错误信息

作者、译者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入错误信息后,单击“提交”按钮即可(见右图)。本书的作者、译者和编辑会对您提交的错误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

扫码关注本书

扫描右侧的二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

与我们联系

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发电子邮件给我们,并请在电子邮件标题中注明书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发电子邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发电子邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发电子邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

关于异步社区和异步图书

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近40年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的Logo。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。

       

 异步社区             微信服务号

第1章 基础入门概念

光线追踪渲染器和光栅化渲染器采用不同的方法将3D场景渲染到2D屏幕上。但是,这两种方法有一些共同的基本概念。

在本章中,我们将探索画布(canvas),这是一个抽象表面,我们将在其上渲染图像。我们还将探索坐标系(coordinate system),我们将通过它来“引用”画布上的像素(pixel)。我们将学习如何表示和处理颜色,以及学习如何描述一个3D场景,以便我们的渲染器可以使用它。

1.1 画布

在本书中,我们将在画布上绘制东西:画布是一个矩形像素阵列,我们可以单独设置每一个像素的颜色。我们并不关心画布是显示在屏幕上还是印刷在纸上。我们的目标是在2D画布上呈现一个3D场景,所以我们把精力集中于在这个抽象的矩形像素阵列上进行渲染。

我们将通过一个简单的函数来延伸并构建本书中的所有内容,该函数可为画布上的某一个像素设置颜色,如下面代码所示。

canvas.PutPixel(x, y, color)

这个函数有3个参数:x坐标、y坐标和color(颜色值)。现在让我们先把重点放在坐标上。

坐标系

画布有宽度和高度(均以像素为单位),我们称之为。我们需要一个坐标系来引用画布的像素。对于大多数计算机屏幕,坐标原点位于屏幕左上角,轴向屏幕右侧延伸,轴向屏幕底部延伸,如图1-1所示。

图1-1 大多数计算机屏幕采用的坐标系

考虑到视频内存的组织方式,这个坐标系对于计算机来说是非常自然、合理的,但对于人类来说却不是最自然、合理的使用方式。相反,3D图形程序员倾向于使用另一种坐标系,这也是我们通常在纸上绘制图形时所使用的坐标系:它的原点位于中心,轴“向右增大、向左减小”,而轴“向上增大、向下减小”,如图1-2所示。

图1-2 我们的画布将采用的坐标系

使用这个坐标系的话,轴的范围变成轴的范围变成。同时我们假设,当坐标值超出这一范围时,使用PutPixel函数没有任何作用。

在我们的示例中,画布会被绘制在屏幕上,因此我们需要从一个坐标系转换到另一个坐标系。要做到这一点,我们需要改变坐标系的中心,并反转轴的方向。由此可以得到如下的转换方程。

我们假设PutPixel函数会自动进行这种转换。我们可以认为画布的坐标原点在中心,轴向屏幕右侧延伸,轴向屏幕顶部延伸。

让我们再来看看PutPixel函数剩下的参数:color

1.2 颜色模型

关于颜色如何作用的理论很吸引人,但它超出了本书的范围。以下仅简要介绍与计算机图形学相关的颜色理论方面的相关知识。

当光照射到我们的眼睛时,它会刺激眼睛后面的感光细胞。这些细胞根据入射光的波长产生对应的大脑信号。我们把对这些大脑信号的主观体验称为颜色(color)。

我们通常看不到波长在可见光范围(visible range)以外的光。波长和频率呈反比关系(波的频率越高,波峰之间的距离越小)。红外线(波长大于770nm,对应的频率低于405THz)是无害的,而紫外线(波长小于390nm,对应的频率高于790THz)会灼伤你的皮肤。

每一种你可以想象到的颜色都可以用某些颜色的不同组合来描述。“白色”是所有颜色的总和,而“黑色”可理解为没有任何颜色。用精确的波长来描述颜色是不切实际的。幸运的是,几乎所有的颜色都可以由3种颜色线性组合而成,这3种颜色称为原色(primary color)。

1.2.1 减色法模型

我们在幼儿时会用蜡笔画各种奇奇怪怪的图形,减色法模型(subtractive color model)就是我们用蜡笔所做事情的一个专业的名字。我们拿一张白纸和红、黄、蓝3种颜色的蜡笔。我们先画一个黄色的圆圈,然后画一个蓝色的圆圈与它重叠,这样就得到了绿色!黄色和红色一起就会得到橙色!红色和蓝色一起就会得到紫色!将三者混合在一起,就会得到一种很暗的颜色!幼儿园是不是一个神奇的地方?图1-3显示了减色法模型的原色以及它们混合后得到的颜色。

不同的物体有不同的颜色,因为它们吸收和反射光的方式不同。我们先来分析一下白光,比如阳光(阳光不是很白,但对我们的目的来说已经足够了)。白光包含各种波长的光。当光照射到一个物体的表面时,物体的表面会吸收一部分波长的光,反射其他部分波长的光,这取决于物体的材质。一些反射光会进入我们的眼睛,然后我们的大脑将其转换成颜色。具体是什么颜色呢?就是被物体表面反射的波长总和所形成的颜色。

图1-3 减色法的三原色以及它们的组合

那这些蜡笔是怎么回事?我们从白纸反射白光开始分析。因为是白纸,所以它反射了大部分接收到的光。当我们用“黄色”蜡笔画画时,我们是在纸张上添加了一层材料,这层材料可以吸收一部分波长的光,但会让其他波长的光通过。这部分光经过纸张的反射,再次穿过这一层“黄色”材料,进入我们的眼睛,我们的大脑就把这种特定波长的光组合解释为“黄色”。这一层“黄色”材料的作用就是从原始的白光中减去(subtract)一部分波长的光。

我们可以把每种颜色的圆看作一个滤波器:当我们画一个蓝色圆叠加到黄色圆上面的时候,相当于我们从原来的光中过滤掉了更多波长的光,所以进入我们眼睛的是那些没有被蓝色或黄色的圆过滤掉的波长的光,我们的大脑认为它们是“绿色”的。

总之,我们从包含所有波长的光开始,减去一部分数量的原色,从而创建任何其他颜色。这种颜色模型的名字就来源于我们通过减去其中一部分波长的光来创造颜色这样一个事实。

不过,这种模型并不完全正确。其实减色法模型中的实际原色并不是教给幼儿和美术学生的蓝色、红色和黄色,而是青色(cyan,C)、品红色(magenta,M)和黄色(yellow,Y)。此外,混合这3种原色会产生一种稍暗的颜色,但不是完全的黑色,因此需要添加纯黑色作为第四种“原色”。因为B已经用于表示蓝色,所以黑色(black)用K表示,因此我们得出了CMYK颜色模型(CMYK color model),如图1-4所示。

图1-4 打印机使用的4种减色法原色

我们可以直接在彩色打印机的墨盒上看到这种颜色模型的应用,有时也可以在廉价的传单上看到这种颜色模型的应用,在廉价的传单上,不同的颜色彼此会有略微偏移错位。

1.2.2 加色法模型

减色法模型只是故事的一半。如果你曾经近距离或用放大镜观察屏幕(或者,老实说,不小心对着屏幕打了个喷嚏),你可能会看到一些彩色的小点。它们是红色、绿色和蓝色的。

屏幕与纸张是相反的。纸张不会发光,它只是反射了照射它的光的其中一部分。另一方面,屏幕本身是黑色的,但它们自身会发光。对于纸张,我们从白光开始,减去我们不想要的波长的光;对于屏幕,我们从没有光开始,然后添加(add)我们想要的波长的光。

为此我们需要使用不同的原色。大多数颜色可以通过在黑色表面上添加不同“数量”的红色(red,R)、绿色(green,G)和蓝色(blue,B)来创建,这就是RGB颜色模型(RGB color model),它是一种加色法模型(additive color model),如图1-5所示。

图1-5 加色法的三原色及其部分组合

加色法原色的混合色会比它的各组成颜色更亮(lighter),而减色法原色的混合色会比各组成颜色更暗(darker)。所有加色法原色相加可以得到白色,而所有减色法原色相加得到的却是黑色。

1.2.3 忽略细节

既然大家已经知道了所有细节,那么我们就可以选择性地忘记大部分细节,专注于对我们的工作至关重要的事情。

大多数颜色可以用RGB颜色模型或CMYK颜色模型(或许多其他颜色模型中的任何一种)表示,并且可以从一种颜色空间(color space)转换为另一种颜色空间。由于我们专注于在屏幕上渲染东西,因此我们在本书的其余部分使用RGB颜色模型。

如上所述,物体吸收照射它们的部分光,反射其余的光。一些波长的光被吸收,一些被反射,然后就形成了我们所感知的物体表面的“颜色”。从现在开始,我们将简单地把颜色看作物体表面的一种属性,而忽略光的波长。

1.3 颜色深度和颜色表示法

显示器通过混合不同数量的红色、绿色和蓝色来创建颜色。它们通过给屏幕上微小的颜色点提供不同的电压,以不同的强度点亮这些微小的颜色点,从而创造不同的颜色。

我们可以得到多少种不同的强度呢?虽然电压是连续的,但我们使用计算机来处理颜色,计算机使用的是离散值(有限数量的值)。我们可以表示的红色、绿色和蓝色的色调越多,我们就能产生越多的颜色。

现如今我们看到的大多数图像都是使用8位二进制数来表示一种原色,在这里我们称原色为颜色通道(color channel)。每个通道使用8位二进制数的话,对于一个像素(由3个通道组成)而言就是24位二进制数,总共有种(大约1670万种)不同的颜色。这种格式,称为R8G8B8格式或简称为888格式,是我们将在本书中使用的格式。我们认为这种格式的颜色深度(color depth)为24位。

这绝不是唯一可能的格式。不久前,为了节省存储器,15位和16位格式很受欢迎,在15位的情况下我们为每个通道分配5位,在16位的情况下我们为红色通道分配5位,为绿色通道分配6位,为蓝色通道分配5位(称为R5G6B5格式或565格式)。绿色通道得到了额外的1位,因为我们的眼睛对绿色的变化比对红色或蓝色的变化更敏感。

使用16位二进制数,我们可以获得种(大约65000种)颜色。这意味着24位格式中每256种颜色值对应16位格式中的1种颜色。尽管65000种颜色已经足够了,但对于颜色变化非常缓慢的图像,我们将能够看到非常微妙的“阶梯”现象,而这在1670万种颜色中是看不到这种现象的,其中有足够的二进制位来表示介于两者之间的颜色。对于一些特殊的应用场合,例如电影的色彩分级,最好要能够表现更多的颜色细节,因此每个颜色通道需要使用更多的二进制位。

我们将使用3个字节来表示一种颜色,每个字节保存从0到255的8位颜色通道的值。我们将颜色表示为(R,G,B)。例如,(255,0,0)表示纯红色;(255,255,255)表示白色;(255,0,128)表示红紫色。

1.4 颜色的处理方法

我们将使用一些运算来处理颜色。如果你有一些线性代数知识,你可以把颜色想象成3D颜色空间中的向量。如果没有,请不要担心,我们将介绍我们现在将要使用的基本操作。

我们可以通过将每个颜色通道值乘一个常量来修改颜色的强度,如下所示。

例如,颜色(32,0,128)的强度是颜色(16,0,64)的两倍。

我们可以通过将每个颜色通道值分别相加来将两个颜色相加:

例如,如果我们想要组合红色(255,0,0)和绿色(0,255,0),我们将它们按通道值分别相加,得到(255,255,0),也就是黄色。

这些运算可能产生无效的值,例如,将(192,64,32)的强度增加一倍产生的红色通道值(R)超出了我们的颜色范围。我们将任何超过255的值视为255,任何低于0的值视为0,我们把这称为将值范围限制(clamp)为。这或多或少等同于我们在现实生活中拍摄曝光不足或过度曝光的照片时所发生的情况:我们会得到全黑或全白区域。

这大致就是我们对于颜色的入门知识和PutPixel函数操作的总结。在我们继续第2章之前,让我们花一点儿时间来研究如何表示要渲染的3D物体。

1.5 场景

到目前为止,我们已经介绍了画布,就是我们可以在其上为像素着色的抽象表面。现在我们引入另一个抽象概念——场景(scene),从而将注意力转移到我们感兴趣的要表示的物体上。

场景是在渲染中我们感兴趣的一组物体的集合。它可以代表任何事物,从飘浮在无限空旷空间中的单个球体(我们将从那里开始)到脾气暴躁的怪兽鼻子内部的令人难以置信的详细模型。

我们需要一个坐标系来讨论场景中的物体。我们不能使用与画布相同的坐标系,其中有两个原因。首先,画布是2D的,而场景是3D的。其次,画布和场景使用不同的单位:画布使用像素,场景使用真实单位(如英制或公制)。

图1-6 场景使用的坐标系

场景坐标系中,坐标轴的选择是任意的,因此我们的选择需要对我们的目的有帮助。我们说轴是向上的,轴和轴是水平的,并且3个轴都相互垂直。把平面想象成“地板”,而平面是方形房间里垂直的“墙”。与我们为画布选择的坐标系一致,场景坐标系中轴是向上的,轴是水平的。图1-6展示了场景使用的坐标系。

场景单位的选择取决于我们的场景代表什么。如果我们正在对一个茶杯进行建模,那么测量值“1”可以表示1英寸(1英寸≈2.54cm);如果我们是对太阳系进行建模,那么“1”可以表示1个天文单位。只要我们坚持使用所选择的单位,它们是什么并不重要,所以我们可以从现在开始安心地忽略它们。

1.6 总结

在本章中,我们介绍了画布,这是一种表示矩形表面的抽象概念,我们可以在其上绘制图像。还介绍了一个函数——PutPixel,我们将使用它构建其他所有内容。我们还选择了一个坐标系来“引用”画布上的像素,并描述了一种表示这些像素颜色的方法。最后,我们介绍了场景的概念,并选择了在场景中使用的坐标系。

奠定了这些基础之后,我们可以开始构建光线追踪渲染器和光栅化渲染器了。

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e58391”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

第一部分 光线追踪

第2章 基础光线追踪知识

在本章中,我们将介绍光线追踪(raytracing),这是我们将要介绍的第一个重要算法。首先我们会解释此算法的目的——人们是为了解决什么问题才想到了这样的算法,然后会列出一些算法实现过程的伪代码。之后我们会学习如何在场景中表示光线和物体。最后,我们推导出一种方法,用来计算哪些光线构成了我们场景中每个物体的可见图像,并了解如何在画布上表示它们。

2.1 渲染一幅瑞士风景图

假设我们正在游览一处旅游胜地,并看到了令人惊叹的风景——它是如此令人着迷,我们恰好可以绘制一幅画来捕捉它的美景。图2-1展示了这样一处风景。

假设我们有画布和画笔,但我们并没有太多艺术天赋。难道绘制图画来捕捉美景的愿望就要因此破灭了吗?

不一定。我们可能没有艺术天赋,但我们是有条理、有方法的。所以我们可以顺理成章地想到一件事:去找一张捕虫网。切割出一张长方形的网,把它用木框框起来,然后把它固定在一根木棍上。现在我们可以透过一扇网状的窗户观察风景了。接下来,我们选择最佳视角来欣赏风景,并且再插一根木棍来标记此时我们眼睛所处的确切位置。

图2-1 令人叹为观止的瑞士风景

我们还没有开始画画,但此时我们有了一个固定的视角和一个固定的木框,通过这个木框我们可以观察风景。而且这个固定的木框,被捕虫网分割成了一个个的小方块。然后我们可以开始有条不紊地绘画了。我们在画布上绘制一个网格,网格中格子的数量和我们之前木框上捕虫网的格子数量相同。然后我们观察一下木框上捕虫网左上角的格子,我们透过它可以看到的最主要的颜色是什么?是天蓝色。所以我们把画布上左上角格子的颜色也涂成天蓝色。我们不断重复这个过程,对画布上的每个格子都这样处理,很快画布上就有了一幅“美丽的”风景画,就像我们通过木框看到的那样。最终绘制的图如图2-2所示。

图2-2 对风景的粗略描述

仔细想想,计算机本质上是一台非常有条理的机器,完全缺乏艺术天赋[1]。我们可以像下面这样描述我们的绘画创作过程。

[1] 现在的人工智能(AI)算法,在图像处理方面,已经展现出了一定的艺术性,感兴趣的读者可以搜索机器学习在图像处理方面的应用。——译者注

For each little square on the canvas
    Paint it the right color

非常简单!但是这个过程太抽象了,无法直接在计算机上实现。我们可以描述得更详细一点儿。

Place the eye and the frame as desired
For each square on the canvas
    Determine which square on the grid corresponds to this square on the canvas
    Determine the color seen through that grid square
    Paint the square with that color

这仍然太抽象了,但它开始看起来像一个算法——也许会令大家惊讶,这其实是对一个完整光线追踪算法的较高层面的概述!是的,就是这么简单。

2.2 基本假设

计算机图形学的魅力之一是可以在屏幕上绘制各种东西。为了尽快实现这一点,我们需要做一些经过简化的假设。当然,这些假设对我们可以做的事情施加了一些限制,但我们将在后面的章节中解除这些限制。

首先,我们假定有一个固定的观察位置。观察位置,也就是2.1节我们观察瑞士风景时眼睛所处的位置,我们通常称其为相机位置(camera position),用表示。我们假设相机在空间中占据一个点,它位于坐标系的原点,并且它永远不会从那里移走,所以现在相机的位置是

其次,我们假设有一个固定的相机方位。相机方位决定了相机指向哪里。我们假设相机指向轴正方向(简写为),并且轴正方向()是向上的,轴正方向()是向右的,如图2-3所示。

图2-3 相机的位置和方位

相机位置和方位现已固定。在观察瑞士风景这个示例中,我们仍然缺少的就是用于观察场景的“木框”。我们假设这个木框的尺寸为,并且在相机方位的正前方,也就是垂直于。我们还假设木框到相机的距离是,它的两条边平行于轴和轴,它的中心在轴上。说得有点儿啰唆,但其实质很简单,如图2-4所示。

图2-4所示的矩形作为我们观察世界的窗口,我们称之为视口(viewport)。本质上,我们将在画布上绘制我们通过视口看到的任何内容。请注意,视口的大小和视口到相机的距离决定了从相机可以观察的角度,称为视野(field of view),或简称FOV。人类有将近180°的水平视野(尽管大部分是模糊的周边视觉,没有深度感)。为简单起见,我们设置;这样会使得相机视野大约为53°,从而可以生成合理的而且不会过度扭曲失真的图像。

图2-4 视口的位置和方位

让我们回到前面介绍的“算法”,使用适当的技术术语,并为清单2-1中的步骤编号。

清单2-1 光线追踪算法的简要描述

①Place the camera and the viewport as desired
  For each pixel on the canvas
     ②Determine which square on the viewport corresponds to this pixel
     ③Determine the color seen through that square
     ④Paint the pixel with that color

我们刚刚完成了步骤①(或者更准确地说,暂时解决了这个问题)。步骤④很简单:我们只需使用canvas.PutPixel(x, y, color)。我们需要快速地完成步骤②,然后在接下来的几章中,我们将专注于完成步骤③所涉及的越来越复杂的方法。

2.3 画布空间到视口空间

清单2-1中算法的步骤②要求我们确定视口上的哪个正方形格子对应于画布上的某一像素。我们知道像素的画布坐标——我们称之为。需要注意的是,我们要怎样放置视口,才能便捷地把它的轴与画布的方位相匹配,把它的中心与画布的中心相匹配。因为视口是以世界空间的单位衡量的,而画布是以像素衡量的[2],所以从画布坐标到空间坐标只是比例的改变!

[2] 因为视口是在世界空间,所以是以世界空间的单位衡量的,而画布是与最终的渲染目标(例如屏幕)相关联的,所以以像素作为衡量单位。——译者注

还有一个额外的细节。尽管视口是2D的,但它是嵌在3D空间中的。我们定义它与相机的距离为。根据定义,这个平面,也就是投影平面(projection plane)上的每个点都有。因此,有如下方程。

这样步骤②就完成了。对于画布上的每个像素,我们可以确定其在视口上对应的点

2.4 追踪射线

下一步是弄清楚从相机的位置观察,穿过视口上的某个点的光是什么颜色的。

在现实世界中,光来自光源(如太阳、灯泡等),经过几个物体反射之后,最终到达我们的眼睛。对于场景中的模拟光源,我们可以尝试模拟每个光子离开模拟光源的路径,但这会非常耗时。我们不仅要模拟数量惊人的光子(一个100W的灯泡每秒发出个光子!),而且只有一小部分光子在通过视口后会刚好到达。这种技术称为光子追踪(photon tracing)或光子映射(photon mapping)。不幸的是,这超出了本书的范围。

相反,我们将考虑“反向”的光线:相机发射出射线,我们从这里开始,穿过视口中的某个点,并跟踪射线的路径,直到它“击中”场景中的某个物体。这个物体就是相机通过视口的那个点“看到”的物体。因此,作为第一近似值,我们只需要将该物体的颜色作为“通过该点的光的颜色”,如图2-5所示。

图2-5 视口中的一个小方块,代表了画布上的一个像素,我们用相机通过它看到的物体的颜色绘制该像素

现在我们只需要用一些方程来描述这些几何图形。

2.4.1 射线方程

对我们来说,表示射线最方便的方法是用参数方程。我们知道射线通过点,而且知道它的方向(从点指向点),所以我们可以把射线中的任意点表示为如下方程。

其中是任意实数。通过把从的每个值代入这个方程,我们可得到沿射线的每个点

我们称射线的方向。这样方程变为下面这样。

理解这个方程的一种直观方式是,我们从原点()发出射线,并沿着射线的方向()“前进”某个量(),很容易看出这包括了沿射线的所有点。你可以在本书附录中了解更多关于向量运算的细节。图2-6显示了这些方程的实际应用。

图2-6 射线上不同值的点

图2-6显示了沿射线对应的点。的每个值沿射线产生不同的点。

2.4.2 球体方程

现在我们需要在场景中假设一些物体,这样我们的射线就能击中这些物体。我们可以选择任意的几何图元作为场景的构建块。对于光线追踪,我们使用球体,因为它们很容易用方程来处理。

什么是球体?球体是距离一个固定点有固定距离的点的集合[3]。这个距离叫作球的半径(radius),这个点叫作球的球心(center)。图2-7显示了一个由球心和半径定义的球体。

[3] 在计算机图形学中,除了一些特殊的材质(例如体渲染材质),大部分是表面渲染,因此相应的着色器被称为surface shader。作者没有按照严格的几何定义,因此球体和球面使用了相同的描述函数。本书其余部分,除非特殊说明,一般会沿用这种方式。——译者注

图2-7 一个球体,由它的球心和半径定义

根据上面的定义,如果是球心,是球的半径,那么球体表面上的点必须满足下面的函数。

我们来研究一下这个函数。如果你对这些数学知识不熟悉,可参阅附录。

和点之间的距离就是从点到点的向量的长度,如下所示[4]

[4] 作者使用了简化形式,意为从点指向点的向量。——译者注

一个向量的长度(记为)是它与自身的点积(记为)的平方根,如下所示。

为了消去平方根,我们可以两边同时取平方,得到下面的方程。

上述这些球体方程都是等价的,但最后一个方程在下面的步骤中最便于操作。

2.4.3 射线与球体相交

现在我们有两个方程:一个描述球体上的点,一个描述射线上的点。如下所示。

射线和球体是否相交呢?如果相交,交点在哪里呢?

假设射线和球体确实在点相交。这个点既在射线上又在球体的表面上,所以它必须同时满足两个方程。请注意,这些方程中唯一的未知量就是参数,因为都是已知的,而是我们要找的点。

由于在两个方程中代表相同的点,我们可以用第二个方程中的表达式代替第一个方程中的。这样我们可以得到如下方程。

如果我们能找到满足这个方程的值,我们可以把它代入射线方程从而找到射线与球体的交点。

从目前的方程形式来看,这个方程有点儿难以理解。我们来做一些代数运算看看能得到什么。

首先,让。然后我们可以把方程改写成下面这样。

然后,我们利用分配律将点积展开(同样,可参阅附录),如下所示。

稍微整理一下这些项,我们得到下面的方程。

将参数移出点积并将移到等式的另一侧,我们得到下面的方程。

记住,两个向量的点积是实数,所以前文方程中每个圆括号内的项都是实数。如果我们给它们分别起个名字,我们会得到更熟悉的东西,如下所示。

这刚好就是一个经典的一元二次方程!它的解就是射线与球体相交处参数的值,如下所示。

幸运的是,这在几何上是合理的。你们可能还记得,一元二次方程可以没有解,有一个解,或者有两个不同的解,这取决于判别式的值。这正好分别对应射线不与球体相交、射线与球体相切、射线进入和离开球体3种情况,如图2-8所示。

图2-8 一元二次方程解的几何解释:无解、一个解或两个解

一旦我们求出了的值,我们就可以将其代入射线方程,最终得到与该值对应的交点

2.5 渲染我们的第一组球体

总而言之,对于画布上的每个像素,我们可以计算视口中对应的点。给定相机的位置,我们可以表示从相机出发并穿过视口中某一点的射线方程。给定一个球体,我们可以计算出射线与球体相交的位置。

所以我们需要做的就是计算射线和每个球体的交点,保留距离相机最近的那个交点,然后用适当的颜色在画布上绘制像素。我们已经准备好渲染我们的第一组球体了!

不过,参数的值得特别注意。让我们回到射线方程。

由于射线的原点和方向是固定的,在所有实数范围内改变将产生这条射线中的每个点。我们注意到,对于,我们得到;对于,我们得到的负值产生相反方向的点,即在相机后面。因此,我们可以将参数空间分为3个部分,如表2-1所示。图2-9展示了参数空间的示意。

表2-1 参数空间的细分

t值

参数空间划分

在相机后面

在相机和投影平面/视口之间

在投影平面/视口前面

请注意,相交方程并没有表明球体必须在相机前面;该方程也可以求解出位于相机后面的交点。显然,这不是我们想要的,所以我们应该忽略任何的解。为了进一步避免数学计算上的烦琐,我们将方程的解限制为,即我们将渲染投影平面前面的任何内容。

图2-9 参数空间中的一些点

另一方面,我们不想为的值设置上限,我们希望看到相机前的所有物体,无论它们有多远。但是,因为在以后的阶段我们想要缩短射线段的长度,所以我们现在将引入这种形式,并给一个的上限(对于不能直接表示“无穷”的编程语言,一个非常大的数字就可以了)。

我们现在可以用一些伪代码来表示我们到目前为止所做的一切。一般来说,我们会假设代码可以访问它需要的任何数据,这样我们就不需要费心地显式传递诸如canvas之类的参数,而将重点放在真正必要的参数上。

main函数现在如清单2-2所示。

清单2-2 main函数

main() {
    O = (0,0,0)
    for x = -Cw/2 to Cw/2 {
        for y = -Ch/2 to Ch/2 {
            D = CanvasToViewport(x, y)
            color = TraceRay(O, D, 1, inf)
            canvas.PutPixel(x, y, color)
        }
    }
}

CanvasToViewport函数非常简单,如清单2-3所示。常数d表示相机和投影平面之间的距离。

清单2-3 CanvasToViewport函数

CanvasToViewport(x, y){
    return (x * Vw / Cw, y * Vh / Ch, d)
}

TraceRay函数计算射线与每个球体的交点,如清单2-4所示,并且返回在的取值范围内最近的交点处球体的颜色。

清单2-4 TraceRay函数

TraceRay(O, D, t_min, t_max){
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in [t_min, t_max] and t1 < closest_t {
            closest_t = t1
            closest_sphere = sphere
        }
        if t2 in [t_min, t_max] and t2 < closest_t {
            closest_t = t2
            closest_sphere = sphere
        }
    }
    if closest_sphere == NULL {
        ①return BACKGROUND_COLOR
    }
    return closest_sphere.color
}

在清单2-4中,O表示射线的原点。虽然我们追踪的是相机的射线,它位于原点,但在后面的阶段不一定是这样,所以它必须是一个参数。这同样适用于t_mint_max

请注意,当射线不与任何球体相交时,我们仍然需要返回一些颜色①——我在大多数示例中选择了背景色。

最后,用IntersectRaySphere函数求解这个一元二次方程,如清单2-5所示。

清单2-5 IntersectRaySphere函数

IntersectRaySphere(O, D, sphere){
    r = sphere.radius
    CO = O - sphere.center

    a = dot(D, D)
    b = 2*dot(CO, D)
    c = dot(CO, CO) - r*r

    discriminant = b*b - 4*a*c
    if discriminant < 0 {
        return inf, inf
    }

    t1 = (-b + sqrt(discriminant)) / (2*a)
    t2 = (-b - sqrt(discriminant)) / (2*a)
    return t1, t2
}

为了将这些付诸实践,我们需要定义一个非常简单的场景,如图2-10所示。

图2-10 一个非常简单的场景,分别从上面(左图)和从右面(右图)观察

在伪场景语言中,场景是像下面这样表示的。

viewport_size = 1 x 1
projection_plane_d = 1
sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  #红色
}
sphere {
    center = (2, 0, 4)
    radius = 1
    color = (0, 0, 255)  #蓝色
}
sphere {
    center = (-2, 0, 4)
    radius = 1
    color = (0, 255, 0)  #绿色
}

当我们在这个场景上运行我们的算法时,我们最终得到了一个非常棒的光线追踪渲染场景,如图2-11所示。

你可以在本书附件的/cgfs/basic-rays-demo目录下面找到该算法的实时实现,双击Basic raytracing demo.html文件或者用浏览器打开该文件即可。

图2-11 一个非常棒的光线追踪渲染场景

我知道,这一效果有点儿令人失望,不是吗?反射、阴影和高光效果去哪里啦?别担心,我们会实现这些的。这是个很好的开端。这些球体看起来像圆形,这总比它们看起来像猫要好。它们看起来不太像球体的原因是,缺少了人类如何决定物体形状的一个关键组成部分:物体与光的相互作用方式。我们将在第3章讨论这个问题。

2.6 总结

在本章中,我们已经奠定了光线追踪渲染器的基础。我们选择了一套固定的设置(相机和视口的位置和方位,以及视口的大小);我们选择了球体和射线的表示方法;我们探索了必要的数学知识来弄清楚球体和射线如何相交;我们把这些都放在一起,从而可以用纯色在画布上绘制球体。

接下来的章节将在此基础上,对光与场景中物体的交互方式进行更详细的建模。

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e58391”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

相关图书

Creo Parametric 8 中文版从入门到精通
Creo Parametric 8 中文版从入门到精通
Origin科技绘图与数据分析实战
Origin科技绘图与数据分析实战
UG NX中文版三维电气布线设计
UG NX中文版三维电气布线设计
趣学3D One——青少年三维创意与设计(第2版)
趣学3D One——青少年三维创意与设计(第2版)
AutoCAD 2020中文版三维造型设计从入门到精通
AutoCAD 2020中文版三维造型设计从入门到精通
Cinema 4D实战案例教材
Cinema 4D实战案例教材

相关文章

相关课程