Unity Shader入门精要(txt+pdf+epub+mobi电子书下载)


发布时间:2020-08-02 13:36:59

点击下载

作者:冯乐乐

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Unity Shader入门精要

Unity Shader入门精要试读:

前言

2004年,有3位年轻人在开发他们的第一款游戏失利后,决定在丹麦首都哥本哈根建立一家游戏引擎公司。最初,他们的想法是要让全世界的开发人员可以使用最少的资源来创建出他们喜欢的游戏。谁也不曾想到,十年以后,这个起初并不起眼的公司已经发展成为游戏引擎公司巨头,而他们的游戏引擎也成为世界上应用最广泛的游戏引擎。没错,这个公司就是Unity Technologies,这3位年轻人分别是公司创始人David Helgason(CEO)、Nicholas Francis(CCO)和Joachim Ante(CTO)。而这3位创始人的初衷也得以实现,截止到2014年,全世界有超过300多万的开发者在使用游戏引擎Unity来开发游戏,更有6亿玩家在玩由Unity引擎制作的游戏。这股“Unity热”一直持续到现在。

虽然Unity引擎上手快,操作界面简单快捷,但许多Unity开发者却发现,当他们需要在Unity中实现一些特殊的画面效果时,往往无从下手。这些画面效果的实现通常和渲染有关,更具体来说,我们通常需要在Unity中编写一些Unity Shader文件来实现它们。一方面,对渲染知识的缺乏和对Shader的不了解导致很多开发者在这条路上举步维艰;另一方面,对游戏画面的提升是越来越多游戏公司的诉求。然而,Unity官方文档中不仅缺少对渲染原理讲解的内容,对Unity Shader本身的一些工作机制(概括来说,Unity Shader是Shader上层的一个抽象)同样缺少相关资料。同时,市面上能适应初学者的Unity Shader书少之又少,基于这些原因,使得我想要编写这样一本书来帮助开发者渡过困境。

本书旨在从基础开始,帮助读者逐渐了解并掌握如何编写Unity Shader。本书不仅仅是要教会读者“如何使用Unity Shader”,更重要的是要帮助读者建立对渲染流程的基本认识,在此基础上,帮助读者学习Unity中的一些渲染机制以及如何使用Unity Shader实现各种自定义的渲染效果。我相信,让读者首先了解原理再进行实践,相比于大量堆砌代码是更好的学习方法。因此,本书在开始实践前,均会为读者讲解大量的原理,让读者在学习时不再一头雾水。

尽管本书专注于学习Unity Shader,但根据我的学习经验来看,在不了解基础的渲染流程和基本的数学知识前,想要深入学习Shader的编写是非常困难的。实际上,Shader仅是整个渲染流程的一个子部分,因此,任何脱离渲染流程的对Shader的讲解可能会让读者更加困惑。而向量运算、矩阵变换等数学知识在Shader的编写中无处不在,因此,这些数学知识往往也是让初学者对Shader望而却步的原因。基于上面的两点观察,本书的安排从易到难,由基础到深入。我们把全书分为了5篇,读者可以在第1章中看到这些章节的具体安排。

随着硬件的发展,Shader的能力也越来越大。如果问你,一个Shader可以做什么?你可能会回答渲染游戏模型、模拟波动的海面、实现各种屏幕特效等。但如果告诉你,上面所示的3张图片完全依靠一个片元着色器来渲染实现,没有借助任何外部模型和纹理,你可能会觉得非常不可思议!读者可以在Shadertoy网站上看到许多这样的例子。例如,上面的小雨伞、五彩的小方块,以及飘动的气球(由于本书是黑白印刷,一些效果无法显现)。一个简简单单的Shader可以做到什么程度的效果,我们已经不可预期。本书的重点不在于教读者如何单纯使用Shader来实现上面的效果,而在于如何让Shader和其他游戏开发元素(例如,模型、纹理、脚本等)相配合,实现游戏中常见的渲染效果,我们在此只想说明Shader可能远比你想象的要强大得多。我们真诚地希望本书可以带领读者走进Shader的世界,让读者理解Shader、掌握Shader,和我们一起享受这样一个奇妙的游戏开发世界!读这本书之前你需要哪些知识

本书面向Unity Shader初学者和程序员,尽量在本书的基础篇中介绍那些必要的基础知识,但仍然希望读者可以具备如下知识。● 有一定(或少量)的编程经验。尽管Unity Shader的编写语言不

同于C++、C#这种高级语言,但相比于完全没有编程经验的读

者来说,学习过这些高级语言的读者更加容易理解Shader的代

码。例如,什么是变量、什么是函数等。对于那些缺少编程经验

但仍对Shader有浓厚兴趣的读者,一个好消息是,在Unity的帮

助下,编写Unity Shader的代码量并不多,因此,这些读者仍然

可以阅读本书。● 对Unity引擎的操作界面比较熟悉。假定读者曾使用过一段时间

的Unity,对其中的一些基本操作已经掌握。例如,如何创建场

景、脚本和游戏对象等。● 保持一定的耐心。我曾听到身边的一些朋友抱怨,为什么自己总

是看不懂、学不会Shader,难道是自己学习能力有问题吗?实

际上,这些朋友大多对Shader的学习缺乏耐心,总是抱着今天

看一下明天就会的心情。但不幸的是,与C++、C#高级语言相

比来说,就算我们成功编写了Shader版的“Hello world”,但对

于为什么要这么写、它们是怎么执行的等一系列基础问题我们仍

然并不理解。这正是我之前提到的,要想彻底理解Shader,就

必须了解整个渲染流水线的工作方式。因此,保持耐心,打好基

础,是每一个想要深入学习Shader的开发者的必经之路。● 有一定的数学基础,包括了解基本的代数运算(如结合律、交换

律等)、三角运算(如正弦、余弦计算等)。除此之外,如果读

者具有大学水平的线性代数、微积分等数学知识,会发现阅读本

书时会更加容易。为了帮助读者学习Shader中常见的数学运

算,我们专门在本书的第4章为读者介绍向量、矩阵、空间变换

等重要的数学内容。

如果你满足上面几点小小的条件,那么恭喜你,现在你可以安心地继续阅读本书了!谁适合读这本书

任何想要了解渲染基础或想要自由地使用Unity Shader编写渲染效果的开发者均可阅读本书。这些开发者不仅限于进行游戏开发的程序员,也包括那些渴望更加自由地在Unity中实现各种画面效果的美工人员、在校学生和爱好者等。为什么你需要这本书

与国内市场已有的介绍相关内容的书籍和资料相比来说,本书有一些独有的特色。● 内容独特。本书填补了Unity Shader和渲染流水线之间的知识鸿

沟,帮助读者打下良好的底层基础。同时,我们也会对Unity中

一些渲染机制的工作原理进行详细剖析,帮助读者解决“是什

么”“为什么”“怎么做”这3个基本问题。除此之外,本书配合

大量实例,让读者在实践中逐渐掌握Unity Shader的编写。● 结构连贯。由于网络上关于Unity Shader的资料非常零散,许多

初学者总是无法系统地进行学习。本书在内容编排上颇费心思,

从基础到进阶再到深入的讲解,解决读者长期以来的学习烦恼。● 充分面向初学者。在本书的编写过程中,我一直在问自己,这么

写到底读者能不能看懂?这使得在本书开头的几个章节中,尤其

是在基础篇和初级篇中的章节中,我们的学习步调放得很慢,这

是因为我非常了解在学习Shader的过程中哪些内容比较难理

解,哪些内容非常容易让人困惑,而这些内容正是挡在初学者面

前的拦路虎!为此,提供了大量的图示并配合文字说明,且在一

些章节最后提供了“答疑解惑”小节来解释那些含糊不清而初学

者又经常疑问的问题。考虑到数学往往是让初学者望而却步的重

要因素,我们在第4章数学一章中特意安排了“农场游戏”这一

背景案例,以这样一个虚拟的场景来帮助读者理解数学在渲染中

是如何发挥作用的。● 包含了Unity 5在渲染方面的新内容。例如,本书多次介绍Unity 5

中的新工具帧调试器(Frame Debugger),并借助该工具的帮助

来理解Unity中的渲染过程;第18章中介绍了Unity 5的基于物理

的渲染(PBR),我们较为详细地剖析了PBR的实现原理,并介

绍了如何在Unity 5中使用它们来实现一些更加真实的渲染效果。

需要注意的是,在本书编写时使用的版本为当时的最新版本

Unity 5.2.1(免费版),但本书出版时Unity可能会发布更新的版

本,这可能会造成一些操作界面与本书内容有所冲突。例如,在

Unity 5.3中,帧调试器的界面更加丰富,包含了材质属性等显示

信息,但这并不影响阅读,我们在本书的勘误网址上会更新(https://github.com/ candycat1992/Unity_Shaders_Book)。● 补充了大量延伸阅读资料。渲染领域的博大精深绝不是一本书可

以涵盖的,因此,在本书一些章节的最后,提供了“扩展阅读”

小节,让那些希望更加深入学习的读者可以在提供的资料中找到

更多的学习内容。

总而言之,我希望你可以从这本书中学到许多有价值的内容,并能够享受这个过程。相信我,这些内容很有趣。本书源代码

读者可以在开源网站github(https://github.com/ candycat1992/Unity_Shaders_Book)上下载本书的源代码。在编写本书时,我们使用的是当时Unity的最新版Unity 5.2.1(免费版),并在Mac 10.9.5平台和Windows 8平台下验证了代码的正确性。本书源代码的组织方式大多按资源类型和章节进行划分,主要包含了以下关键文件夹。文 件 夹说 明包含了各章对应的场景,每个章节对应一个子文件夹,例如第7章所有场景所在的子文件夹为Assets/Scenes/Chapter7。每个场景的命名方式为Scene_章Assets/号_小节号_次小节号,例如7.2.3节对应的场景名为ScenesScene_7_2_3。如果同一个小节包含了多个场景,那么会使用英文字母作为后缀依次表示,例如7.1.2节包含了两个场景Scene_7_1_2_a和Scene_7_1_2_b包含了各章实现的Unity Shader文件,每个章节对应一个子文件夹,例如第7章实现的所有Unity Shader所Assets/在的子文件夹为Assets/Shaders/Chapter7。每个ShadersUnity Shader的命名方式为ChapterX-功能,例如第7章使用渐变纹理的Unity Shader名为Chapter7-RampTexture包含了各章对应的材质,每个章节对应一个子文件夹,例如第7章所有材质所在的子文件夹为Assets/ Assets/Scenes/Chapter7。每个材质的命名方式与它使用的MaterialsUnity Shader名称相匹配,并以Mat作为后缀,例如使用名为Chapter7-RampTexture的Unity Shader的材质名称是RampTextureMat包含了各章对应的C#脚本,每个章节对应一个子文Assets/件夹,例如第5章所有脚本所在的子文件夹为Assets/ScriptsScripts/Chapter5包含了各章使用的纹理贴图,每个章节对应一个子文Assets/件夹,例如第7章使用的所有纹理所在的子文件夹为TexturesAssets/Textures/Chapter7

除了上述文件夹外,源代码中还包含了一些辅助文件夹。例如,Assets/Editor文件夹中包含了一些需要在编辑器状态下运行的脚本,Assets/Prefabs文件夹下包含了各章使用的预设模型和其他常用预设模型等。读者反馈

尽管我们在本书的编写过程中多次检查内容的正确性,但书中难免仍然会出现一些错误,欢迎读者批评指正。读者可以将问题反映到本书源代码所在的github讨论页(https:// github.com/candycat1992/Unity_Shaders_Book/issues),此网址也是本书源代码下载地址,该地址中也包括本书示例彩色图文档。也可以发邮件(lelefeng1992@gmail.com)联系作者,本书答疑QQ群为438103099。

编辑联系邮箱为zhangtao@ptpress.com.cn。致谢

首先,我要感谢《Unity 3D ShaderLab开发实战详解》一书的作者郭浩瑜老师,是他向出版社的推荐才导致了本书的编写和出版,并给了我许多在书籍编写过程中的建议和帮助。

感谢卢鹏先生,在本书编写过程中,我们进行了很多关于优化、效果实现等方面的讨论,这些讨论让本书的内容更加丰富。卢先生的乐于分享和好学的精神让我十分敬佩。

我也要感谢我的家人,我的父母和姐姐,是你们在背后的默默支持让我走到了今天,永远爱你们。还要感谢我的男朋友之之,在我遇到瓶颈时,永远是你的鼓励和支持让我走出困境。也是你的帮助,让本书现在的封面得以呈现在读者面前。

除此之外,从开始编写本书到完成之时,很多网友给了我莫大的鼓励和可贵的建议,我从未想到有这么多素未谋面的朋友在关注着本书的进展,感谢你们,是你们让我更加有动力写完本书。

感谢宣雨松和罗盛誉老师在百忙之中为本书写推荐序,谢谢你们的鼓励和支持。

最后,我要感谢人民邮电出版社的编辑张涛,是您的热情鼓励让我对本书的未来满怀希望。谢谢您对本书在内容编排、封面设计等方面的意见和建议,让这本书变得更好。感谢对本书进行修改和排版的出版社工作人员,是你们让这本书更完美地呈现在读者面前。作者第1篇基础篇

这是很重要的一篇,尽管在本篇中我们没有进行真正的代码编写,但本篇会为初学者普及基本的理论知识以及必要的数学基础,为读者顺利步入Unity Shader学习打下很好的基础。

第1章 欢迎来到Shader的世界

欢迎来到Shader的世界!我们曾不断听到周围有人提出类似的问题:“Shader是什么”“我应该看哪些书才能学好Shader”“学习Unity Shader,我应该从哪里着手”。我希望这本书可以告诉你这些问题的答案。让你离制作心目中优秀游戏的心愿更近一步。

第2章 渲染流水线

这一章讲解了现代GPU是如何实现整个渲染流水线的,这些内容对于理解Shader的工作原理有着非常重要的作用。

第3章 Unity Shader基础

这一章将讲解Unity Shader的实现原理和基本语法,同时也将为读者解答一些常见的困惑点。

第4章 学习Shader所需的数学基础

数学向来是初学者面对的一大学习障碍。然而,在初级阶段的渲染学习中,我们需要掌握的数学理论实际上并不复杂。这一章将为读者讲解渲染过程中常见的数学知识。这章内容可以帮助读者理解Shader中的数学运算,我们在讲解过程中以一个具体的例子来阐述“一头奶牛的鼻子是如何一步步被绘制到屏幕上的”。第1章欢迎来到Shader的世界

欢迎来到Shader的世界!我们曾不断听到周围有人提出类似的问题:“Shader是什么”“我应该看哪些书才能学好Shader”“学习Unity Shader,我应该从哪里着手”。我们希望这本书可以告诉你这些问题的答案。如果本书是你学习Shader的第一本书,我们希望这本书可以为你打开一扇新的大门,让你离制作心目中的优秀游戏的心愿更近一步;如果不是,我们同样希望这本书可以让你更深入地理解Shader的方方面面,在学习Shader的过程中更上一层楼。1.1 程序员的三大浪漫

有人说,程序员的三大浪漫是编译原理、操作系统和图形学(是的,我已经听到很多人在反驳这句话了,不要当真啦)。不管你是否认同这句话,我们只是想借此说明图形学在程序员心目中的地位。正在看此书的你,想必多多少少都对图形学或者渲染有一定兴趣,也许你想要通过此书来学习如何实现游戏中的各种特效,也许你仅仅是好奇那些绚丽的画面是如何产生的。我们是程序员中的“外貌协会”,期待着用代码编写出一个绚丽多姿的世界。这就是我们的浪漫。

我想,读者大概都经历过这样的场景:当你在游戏里看到那些出色的画面时,你很好奇这样的游戏是如何制作出来的,更具体的是,这样的渲染效果是如何得到的。于是你搜索后发现,这个游戏是Unity引擎开发的,更巧的是,Unity也是你熟知的引擎!于是你继续搜索,想要知道如何在Unity里实现这样的效果,最后,你往往会得到“要编写自己的Shader”这样的答案。总算有了一些头绪,你继续在网络上搜索如何学习编写Shader。于是你看到了很多文章,这些文章告诉你Unity Shader有哪些语法,一个普通的漫反射或者边缘高光的效果的代码是什么样子的。然后,你把这些代码粘贴到Unity中,保存后运行,效果出现了!一切看起来好像都很顺利,可是,当你仔细阅读这些代码时,却往往没有头绪。你不知道为什么要有一个名为vert和frag的函数,它们是什么时候调用的,为什么vert函数里要进行一些矩阵运算,这些矩阵是用来做什么的,为什么当你按照C#里面的一些语法编写时Shader却报错了。这些疑问大大影响了你学习Shader的信心,你开始觉得这是一个比学习C#难许多倍的事情,怀疑自己是不是还不具备学习如何编写Shader的基础。

如果上面的情景和你的经历有些类似,那么相信我,有很多人和你有一样的烦恼。事实上,我们之所以会觉得学习Shader比学习C#这样的编程语言更加困难,一个原因是因为Shader需要牵扯到整个渲染流程。当学习C++、C#这样的高级语言时,我们可以在不了解计算机架构的情况下仍然编写出实现各种功能的代码,这样的高级语言更符合人类的思维方式。然而,Shader并不是这样的。我们之所以要学习Shader,是想要学习如何把物体按照自己的意愿渲染到屏幕上,但是,Shader只是整个渲染流程中的一个子部分。虽然它很关键,但想要学习它,我们就需要了解整个渲染流程是如何进行的。和C++这样的高级语言不同,尽管Shader的编写语言已经达到了我们可以理解的程度,但Shader更多地是面向GPU的工作方式,所以它的一些语法对我们来说并不那么直观。因此,任何一篇只讲语法、不讲渲染框架的文章都无法解决读者的困惑。

我们希望通过本书可以帮助读者建立一个渲染流程的整体体系,这些基础是跨越Shader学习中层层障碍的重要因素。我们也相信,在学习完本书后,读者可以自行回答本章开头提出的那些问题。1.2 本书结构

我们在编写本书时尽量考虑到没有渲染基础的读者们。因此,我们把整书分成了五大篇。

● 基础篇

这是很重要的一篇,尽管在本篇中我们没有进行真正的代码编写,但基础篇会为初学者普及基本的理论知识以及必要的数学基础。基础篇包括了以下3个章节。

第2章 渲染流水线 这一章讲解了现代GPU是如何实现整个渲染流水线的,这些内容对于理解Shader的工作原理有着非常重要的作用。

第3章 Unity Shader基础 Unity在原有的渲染流程上进行了封装,并提供给开发者新的图像编程接口—Unity Shader。这一章将讲解Unity Shader的实现原理和基本语法,同时也将为读者解答一些常见的困惑点。

第4章 学习Shader所需的数学基础 数学向来是初学者面对的一大学习障碍。然而,在初级阶段的渲染学习中,我们需要掌握的数学理论实际并不复杂。本章将为读者讲解渲染过程中常见的数学知识,如矢量、矩阵运算、坐标空间等。本章内容可以大大帮助读者理解Shader中的数学运算。为了帮助读者加深理解,我们在讲解过程中以一个具体的例子来阐述“一头奶牛的鼻子是如何一步步被绘制到屏幕上的”。

● 初级篇

在学习完基础篇后,我们就正式开始了Unity Shader的学习之旅。初级篇将会从最简单的Shader开始,讲解Shader中基础的光照模型、纹理和透明效果等初级渲染效果。需要注意的是,我们在初级篇中实现的Unity Shader大多不能直接用于真实项目中,因为它们缺少了完整的光照计算,例如阴影、光照衰减等,仅仅是为了阐述一些实现原理。在第9章最后,我们会给出包括了完整光照计算的Unity Shader。初级篇包含了以下4个章节。

第5章 开始Unity Shader学习之旅 本章将实现一个简单的顶点/片元着色器,并详细解释其中每个步骤的原理,这需要读者对之前基础篇的内容有所理解。本章还会给出关于Unity Shader的一些常用的辅助技巧,例如如何调试、查看内置代码以及编写规范等。

第6章 Unity中的基础光照 本章将学习如何在Shader中实现基本的光照模型,如漫反射、高光反射等。我们首先解释如何从无到有实现一个光照模型,最后给出使用Unity提供的内置函数来实现的版本。

第7章 基础纹理 纹理的使用给渲染的世界带来了更多的变化。这一章将会讲述如何在Unity Shader中使用法线纹理、遮罩纹理等基础纹理。

第8章 透明效果 透明是游戏中常用的渲染效果。这一章首先介绍了渲染的实现原理,并给出了和Unity的渲染顺序相关的重要内容。在了解了这些内容的基础上,我们将学习如何实现透明度测试和透明度混合等透明效果。

● 中级篇

中级篇是本书的进阶篇章,主要讲解Unity中的渲染路径、如何计算光照衰减和阴影、如何使用高级纹理和动画等一系列进阶内容。中级篇包含了以下3个章节。

第9章 更复杂的光照 我们在初级篇中实现的光照模型没有考虑一些重要的光照计算,如阴影和光照衰减。本章首先讲解Unity中的3种渲染路径和3种重要的光源类型,再解释如何在前向渲染路径中实现包含了光照衰减、阴影等效果的完整的光照计算。在本章最后,我们会给出基于之前学习内容实现的包含了完整光照计算的Unity Shader。

第10章 高级纹理 这一章将会讲解如何在Unity Shader中使用立方体纹理、渲染纹理和程序纹理等高级纹理。

第11章 让画面动起来 静态的画面往往是无趣的。这一章将帮助读者学习如何在Shader中使用时间变量来实现纹理动画、顶点动画等动态效果。

● 高级篇

高级篇涵盖了一些Shader的高级用法,例如如何实现屏幕特效、利用法线和深度缓冲以及非真实感渲染等,同时,我们还会介绍一些针对移动平台的优化技巧。高级篇的结构如下。

第12章 屏幕后处理效果 屏幕特效是游戏中常用的渲染手法之一。这一章将介绍如何在Unity中实现一个基本的屏幕后处理脚本系统,并给出一些基本的屏幕特效的实现原理,如高斯模糊、边缘检测等。

第13章 使用深度和法线纹理 使用深度和法线纹理可以帮助我们实现很多屏幕特效。本章将介绍如何在Unity中获取这些特殊的纹理来实现屏幕特效。

第14章 非真实感渲染 很多游戏使用了非真实感渲染的方法来渲染游戏画面。这一章将会给出常见的非真实感渲染的算法,如卡通渲染、素描风格的渲染等。本章的扩展阅读部分可以帮助读者找到更多其他类型的非真实感渲染的实现方法。

第15章 使用噪声 很多时候噪声是我们的救星。本章给出了噪声在游戏渲染中的一些应用。

第16章 Unity中的渲染优化技术 优化往往是游戏渲染中的重点。这一章介绍了Unity中针对移动平台使用的常见的优化技巧。

● 扩展篇

扩展篇旨在进一步扩展读者的视野。本篇将会介绍Unity的表面着色器的实现机制,并介绍基于物理的渲染的相关内容。最后,我们给出了更多的关于学习渲染的资料。扩展篇包含了以下4个章节。

第17章 Unity的表面着色器探秘 Unity提出了一种新颖的Shader形式—表面着色器。本章将会介绍这些表面着色器是如何实现的,以及如何使用这些表面着色器来实现渲染。

第18章 基于物理的渲染 Unity 5终于引入了基于物理的渲染,这给Unity引擎带来了更强的渲染能力。这一章将介绍基于物理渲染的理论基础,并解释Unity是如何实现基于物理的渲染的。我们还会在本章实现一个基本的场景来进一步阐述如何在Unity 5中利用基于物理的渲染。

第19章 Unity 5更新了什么 相较于Unity 4.x,Unity 5在Shader方面有很多重要的更新。本章将给出Unity 5中一些重要的更新,以帮助读者解决在升级Unity 5时所面对的各种问题。

第20章 还有更多内容吗 图形学的丰富多彩远远超乎我们的想象,我们相信一本书也远远无法满足一些读者强烈的求知欲。在最后一章中,我们将给出许多非常有价值的学习资料,以帮助读者进行更深入的学习。

那么,你准备好了吗?和我们一起进入Shader的世界吧!第2章渲染流水线

在开始一切学习之前,我们有必要了解什么是Shader,即着色器。与之关系非常紧密的就是渲染流水线。可以说,如果你没有了解过渲染流水线的工作流程,就永远无法说自己对Shader已经入门。

渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏幕上看到的所有效果。它的输入是一个虚拟摄像机、一些光源、一些Shader以及纹理等。

本章将会给出渲染流水线的概览,同时会尽量避免数学上的计算,而仅仅提供一些全局上的描述。本书给出的流水线不仅适用于Unity平台,如果读者想要深入了解并学习着色器的话,会发现下面的内容同样是非常重要和有价值的。2.1 综述

要学会怎么使用Shader,我们首先要了解Shader是怎么工作的。实际上,Shader仅仅是渲染流水线中的一个环节,想要让我们的Shader发挥出它的作用,我们就需要知道它在渲染流水线中扮演了怎样的角色。而本节会给出简化后的渲染流水线的工作流程。2.1.1 什么是流水线

我们先来看一下真实生活中的流水线是什么。在工业上,流水线被广泛应用在装配线上。

我们来举一个例子。假设,老王有一个生产洋娃娃的工厂,一个洋娃娃的生产流程可以分为4个步骤:第1步,制作洋娃娃的躯干;第2步,缝上眼睛和嘴巴;第3步,添加头发;第4步,给洋娃娃进行最后的产品包装。

在流水线出现之前,只有在每个洋娃娃完成了所有这4个工序后才能开始制作下一个洋娃娃。如果说每个步骤需要的时间是1小时的话,那么每4个小时才能生产一个洋娃娃。

但后来人们发现了一个更加有效的方法,即使用流水线。老王把流水线引入工厂之后,工厂发生了很大的变化。虽然制作一个洋娃娃仍然需要4个步骤,但不需要从头到尾完成全部步骤,而是每个步骤由专人来完成,所有步骤并行进行。也就是说,当工序1完成了制作躯干的任务并把其交给工序2时,工序1又开始进行下一个洋娃娃的制作了。

使用流水线的好处在于可以提高单位时间的生产量。在洋娃娃的例子中,使用了流水线技术后每1个小时就可以生产一个洋娃娃。图2.1显示了使用流水线前后生产效率的变化。

可以发现,流水线系统中决定最后生产速度的是最慢的工序所需的时间。例如,如果生产洋娃娃的第二道工序需要的是两个小时,其他工序仍然需要1个小时的话,那么平均每两个小时才能生产出一个洋娃娃。即工序2是性能的瓶颈(bottleneck)。

理想情况下,如果把一个非流水线系统分成n个流水线阶段,且每个阶段耗费时间相同的话,会使整个系统得到n倍的速度提升。▲图2.1 真实生活中的流水线2.1.2 什么是渲染流水线

上面的关于流水线的概念同样适用于计算机的图像渲染中。渲染流水线的工作任务在于由一个三维场景出发、生成(或者说渲染)一张二维图像。换句话说,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。而这个工作通常是由CPU和GPU共同完成的。[1]《Render-Time Rendering, Third Edition》一书中将一个渲染流程分成3个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。

注意,这里仅仅是概念性阶段,每个阶段本身通常也是一个流水线系统,即包含了子流水线阶段。图2.2显示了3个概念阶段之间的联系。▲图2.2 渲染流水线中的3个概念阶段

● 应用阶段

从名字我们可以看出,这个阶段是由我们的应用主导的,因此通常由CPU负责实现。换句话说,我们这些开发者具有这个阶段的绝对控制权。

在这一阶段中,开发者有3个主要任务:首先,我们需要准备好场景数据,例如摄像机的位置、视锥体、场景中包含了哪些模型、使用了哪些光源等等;其次,为了提高渲染性能,我们往往需要做一个粗粒度剔除(culling)工作,以把那些不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理;最后,我们需要设置好每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。这一阶段最重要的输出是渲染所需的几何信息,即渲染图元(rendering primitives)。通俗来讲,渲染图元可以是点、线、三角面等。这些渲染图元将会被传递给下一个阶段——几何阶段。

由于是由开发者主导这一阶段,因此应用阶段的流水线化是由开发者决定的。这不在本书的范畴内,有兴趣的读者可以参考本章的扩展阅读部分。

● 几何阶段

几何阶段用于处理所有和我们要绘制的几何相关的事情。例如,决定需要绘制的图元是什么,怎样绘制它们,在哪里绘制它们。这一阶段通常在GPU上进行。

几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。这个阶段可以进一步分成更小的流水线阶段,这在下一章中会讲到。几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。

● 光栅化阶段

这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行。光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。

和上一个阶段类似,光栅化阶段也可以分成更小的流水线阶段。

提示读者需要把上面的3个流水线阶段和我们将要讲到的

GPU流水线阶段区分开来。这里的流水线均是概念流水

线,是我们为了给一个渲染流程进行基本的功能划分而提出

来的。下面要介绍的GPU流水线,则是硬件真正用于实现

上述概念的流水线。2.2 CPU和GPU之间的通信

渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面3个阶段:(1)把数据加载到显存中。(2)设置渲染状态。(3)调用Draw Call(在本章的最后我们还会继续讨论它)。2.2.1 把数据加载到显存中

所有渲染所需的数据都需要从硬盘(Hard Disk Drive,HDD)中加载到系统内存(Random Access Memory,RAM)中。然后,网格和纹理等数据又被加载到显卡上的存储空间——显存(Video Random Access Memory,VRAM)中。这是因为,显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。图2.3所示给出了这样一个例子。▲图2.3 渲染所需的数据(两张纹理以及3个网格)从硬盘最终加载到显存中。 在渲染时,GPU可以快速访问这些数据

需要注意的是,真实渲染中需要加载到显存中的数据往往比图1.3所示复杂许多。例如,顶点的位置信息、法线方向、顶点颜色、纹理坐标等。

当把数据加载到显存中后,RAM中的数据就可以移除了。但对于一些数据来说,CPU仍然需要访问它们(例如,我们希望CPU可以访问网格数据来进行碰撞检测),那么我们可能就不希望这些数据被移除,因为从硬盘加载到RAM的过程是十分耗时的。

在这之后,开发者还需要通过CPU来设置渲染状态,从而“指导”GPU如何进行渲染工作。2.2.2 设置渲染状态

什么是渲染状态呢?一个通俗的解释就是,这些状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器(Vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等。如果我们没有更改渲染状态,那么所有的网格都将使用同一种渲染状态。图2.4显示了当使用同一种渲染状态时,渲染3个不同网格的结果。▲图2.4 在同一状态下渲染3个网格。由于没有更改渲染状态,因此3个网格的外观看起来像是同一种材质的物体

在准备好上述所有工作后,CPU就需要调用一个渲染命令来告诉GPU:“嘿!老兄,我都帮你把数据准备好啦,你可以按照我的设置来开始渲染啦!”而这个渲染命令就是Draw Call。2.2.3 调用Draw Call

相信接触过渲染优化的读者应该都听说过Draw Call。实际上,Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息——这是因为我们已经在上一个阶段中完成了!图2.5形象化地阐释了这个过程。

当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。而这个计算过程,就是我们下一节要讲的GPU流水线。▲图2.5 CPU通过调用Draw Call来告诉GPU开始进行一个渲染过程。 一个Draw Call会指向本次调用需要渲染的图元列表2.3 GPU流水线

当GPU从CPU那里得到渲染命令后,就会进行一系列流水线操作,最终把图元渲染到屏幕上。2.3.1 概述

在上一节中,我们解释了在应用阶段,CPU是如何和GPU通信,并通过调用Draw Call来命令GPU进行渲染。GPU渲染的过程就是GPU流水线。

对于概念阶段的后两个阶段,即几何阶段和光栅化阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU。GPU通过实现流水线化,大大加快了渲染速度。虽然我们无法完全控制这两个阶段的实现细节,但GPU向开发者开放了很多控制权。在这一节中,我们将具体了解GPU是如何实现这两个概念阶段的。

几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或可编程性。图2.6中展示了不同的流水线阶段以及它们的可配置性或可编程性。▲图2.6 GPU的渲染流水线实现。颜色表示了不同阶段的可配置性或可编程性:绿色表示该流水线阶段是完全可编程控制的,黄色表示该流水线阶段可以配置但不是可编程的,蓝色表示该流水线阶段是由GPU固定实现的,开发者没有任何控制权。实线表示该Shader必须由开发者编程实现,虚线表示该Shader是可选的

从图中可以看出,GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器。

顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。下一个流水线阶段是裁剪(Clipping),这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。这个阶段是可配置的。例如,我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。几何概念阶段的最后一个流水线阶段是屏幕映射(Screen Mapping)。这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。

光栅化概念阶段中的三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也都是固定函数(Fixed-Function)的阶段。接下来的片元着色器(Fragment Shader),则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。最后,逐片元操作(Per-Fragment Operations)阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。

接下来,我们会对其中主要的流水线阶段进行更加详细的解释。2.3.2 顶点着色器

顶点着色器(Vertex Shader)是流水线的第一个阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。例如,我们无法得知两个顶点是否属于同一个三角网格。但正是因为这样的相互独立性,GPU可以利用本身的特性并行化处理每一个顶点,这意味着这一阶段的处理速度会很快。

顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照。当然,除了这两个主要任务外,顶点着色器还可以输出后续阶段所需的数据。图2.7展示了在顶点着色器中对顶点位置进行坐标变换并计算顶点颜色的过程。▲图2.7 GPU在每个输入的网格顶点上都会调用顶点着色器。顶点着色器必须进行顶点的坐标变换,需要时还可以计算和输出顶点的颜色。例如,我们可能需要进行逐顶点的光照

● 坐标变换。顾名思义,就是对顶点的坐标(即位置)进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。例如,我们可以通过改变顶点位置来模拟水面、布料等。但需要注意的是,无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间。想想看,我们在顶点着色器中是不是会看到类似下面的代码:o.pos = mul(UNITY_MVP, v.position);

类似上面这句代码的功能,就是把顶点坐标转换到齐次裁剪坐标系下,接着通常再由硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates ,NDC)。具体数学上的实现细节我们会在第4章中讲到。图2.8展示了这样的一个转换过程。▲图2.8 顶点着色器会将模型顶点的位置变换到齐次裁剪坐标空间下, 进行输出后再由硬件做透视除法得到NDC下的坐标

需要注意的是,图2.8给出的坐标范围是OpenGL同时也是Unity使用的NDC,它的z分量范围在[−1, 1]之间,而在DirectX中,NDC的z分量范围是[0, 1]。顶点着色器可以有不同的输出方式。最常见的输出路径是经光栅化后交给片元着色器进行处理。而在现代的Shader Model中,它还可以把数据发送给曲面细分着色器或几何着色器,感兴趣的读者可以自行了解。2.3.3 裁剪

由于我们的场景可能会很大,而摄像机的视野范围很有可能不会覆盖所有的场景物体,一个很自然的想法就是,那些不在摄像机视野范围的物体不需要被处理。而裁剪(Clipping)就是为了完成这个目的而被提出来的。

一个图元和摄像机视野的关系有3种:完全在视野内、部分在视野内、完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要进行一个处理,这就是裁剪。例如,一条线段的一个顶点在视野内,而另一个顶点不在视野内,那么在视野外部的顶点应该使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点处。

由于我们已知在NDC下的顶点位置,即顶点位置在一个立方体内,因此裁剪就变得很简单:只需要将图元裁剪到单位立方体内。图2.9展示了这样的一个过程。▲图2.9 只有在单位立方体的图元才需要被继续处理。因此,完全在单位立方体外部的图元(红色三角形)被舍弃,完全在单位立方体内部的图元(绿色三角形)将被保留。和单位立方体相交的图元(黄色三角形)会被裁剪,新的顶点会被生成,原来在外部的顶点会被舍弃

和顶点着色器不同,这一步是不可编程的,即我们无法通过编程来控制裁剪的过程,而是硬件上的固定操作,但我们可以自定义一个裁剪操作来对这一步进行配置。2.3.4 屏幕映射

这一步输入的坐标仍然是三维坐标系下的坐标(范围在单位立方体内)。屏幕映射(Screen Mapping)的任务是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。

假设,我们需要把场景渲染到一个窗口上,窗口的范围是从最小的窗口坐标(x ,y )到最大的窗口坐标(x ,y ),其中x < x 且y < y 。11221212由于我们输入的坐标范围在−1到1,因此可以想象到,这个过程实际是一个缩放的过程,如图2.10所示。你可能会问,那么输入的z坐标会怎么样呢?屏幕映射不会对输入的z坐标做任何处理。实际上,屏幕坐标系和z坐标一起构成了一个坐标系,叫做窗口坐标系(Window Coordinates)。这些值会一起被传递到光栅化阶段。▲图2.10 屏幕映射将x、y坐标从(-1, 1)范围转换到屏幕坐标系中

屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。

有一个需要引起注意的地方是,屏幕坐标系在OpenGL和DirectX之间的差异问题。OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义了屏幕的左上角为最小的窗口坐标值。图2.11显示了这样的差异。▲图2.11 OpenGL和DirectX的屏幕坐标系差异。对于一张512*512大小的图像,在OpenGL中其(0, 0)点在左下角,而在DirectX中其(0, 0)点在左上角

产生这种差异的原因是,微软的窗口都使用了这样的坐标系统,因为这和我们的阅读方式是一致的:从左到右、从上到下,并且很多图像文件也是按照这样的格式进行存储的。

不管原因如何,差异就这么造成了。留给我们开发者的就是,要时刻小心这样的差异,如果你发现得到的图像是倒转的,那么很有可能就是这个原因造成的。2.3.5 三角形设置

由这一步开始就进入了光栅化阶段。从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。

光栅化的第一个流水线阶段是三角形设置(Triangle Setup)。这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。2.3.6  三角形遍历

三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。

三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。图2.12展示了三角形遍历阶段的简化计算过程。▲图2.12 三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对3个顶点的信息进行插值得到的。例如,对图2.12中3个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0

这一步的输出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。2.3.7 片元着色器

片元着色器(Fragment Shader)是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器(Pixel Shader),但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。

前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。我们随后就会讲到。

片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。图2.13显示了这样一个过程。

这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。▲图2.13 根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色

虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息(gradient,或者说是derivative)。有兴趣的读者可以参考本章的扩展阅读部分。2.3.8 逐片元操作

终于到了渲染流水线的最后一步。逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。Merger这个词可能更容易让读者明白这一步骤的目的:合并。而OpenGL中的名字可以让读者明白这个阶段的操作单位,即是对每一个片元进行一些操作。那么问题来了,要合并哪些数据?又要进行哪些操作呢?

这一阶段有几个主要任务。(1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。(2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。

需要指明的是,逐片元操作阶段是高度可配置性的,即我们可以设置每一步的操作细节。这在后面会讲到。

这个阶段首先需要解决每个片元的可见性问题。这需要进行一系列测试。这就好比考试,一个片元只有通过了所有的考试,才能最终获得和GPU谈判的资格,这个资格指的是它可以和颜色缓冲区进行合并。如果它没有通过其中的某一个测试,那么对不起,之前为了产生这个片元所做的所有工作都是白费的,因为这个片元会被舍弃掉。Poor fragment!图2.14给出了简化后的逐片元操作所做的操作。▲图2.14 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中

测试的过程实际上是个比较复杂的过程,而且不同的图形接口(例如OpenGL和DirectX)的实现细节也不尽相同。这里给出两个最基本的测试——深度测试和模板测试的实现过程。能否理解这些测试过程将关乎读者是否可以理解本书后面章节中提到的渲染队列,尤其是处理透明效果时出现的问题。图2.15给出了深度测试和模板测试的简化流程图。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载