《Unity Shader 入门精要》学习笔记:基本概念
第 2 章 渲染流水线
渲染流水线的工作任务
- 渲染流水线的工作任务在于由一个三维场景出发、生成(或者说渲染)一张二维图形。它的输入是一个虚拟摄像机、一些光源、一些 Shader 以及纹理等,输出是一张人眼可以看到的图像。这个工作通常是由 CPU 和 GPU 共同完成。
渲染流水线流程
应用阶段(Application Stage)
- 由应用主导的,CPU 负责实现,开发者具有对这个阶段的绝对控制权。在这个阶段,开发者有3个主要任务:
- 首先,准备好场景数据,例如摄像机位置、视椎体、场景中包含的模型、使用的光源等;
- 其次,为了提高渲染性能,需要进行一个粗粒度剔除(culling)工作,把那些不可见的物体剔除出去,这样不可见的物体剔除工作就不需要交给几何阶段处理;
- 最后,设置每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。
- 这一阶段最重要的输出是渲染所需要的集合信息,即渲染图元(rendering primitives)。通俗的讲,渲染图元可以是点、线、三角面等。这些图元将被传递给下一个阶段——几何阶段。
几何阶段(Geometry Stage)
- 用于处理所有和我们要绘制的几何相关的事情。比如,决定需要绘制的图元是什么,怎样绘制它们,在哪里绘制它们。这一阶段通常在 GPU 上进行。几何阶段同样可以进一步分成更小的流水线阶段。几何阶段的一个重要任务就是把定点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维定点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。
光栅化阶段(Rasterizer Stage)
- 这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在 GPU 上运行。光栅化的主要任务是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。光栅化阶段同样可以分成更小的流水线阶段。
与 GPU 流水线的区别
- 渲染流水线的3个流水阶段主要是概念流水线,是为了给一个渲染流程进行基本的功能划分而提出的。而 GPU 流水线,则是硬件真正用于实现上述概念的流水线。
GPU 流水线
GPU 的渲染流水线接收定点数据作为输入。这些定点数据是由应用阶段加载到现存中,再由 Draw Call 指定的。这些数据随后被传递给顶点着色器。
几何阶段
顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。
顶点着色器主要完成的工作是:坐标转换和逐定点光照,以及输出后续阶段所需要的数据。
- 坐标转换就是对顶点的坐标(即位置)进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。例如,我们可以通过改变顶点位置来模拟水面、布料等。需要注意的是,无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须完成的一个任务是:把顶点坐标从模拟空间转换到齐次裁剪空间。
- 曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。
- 几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。
裁剪(Clipping)的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。这个阶段是可配置的,我们可以使用自定义的裁剪平面也可以通过指令控制裁剪图元。
裁剪(Clipping)的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。
- 一个图元和摄像机视野的关系有 3 种:完全在视野内、部分在视野内、完全在视野外。
- 完全在视野内的图元就继续传递给下一个流水线阶段;
- 完全在视野外的图元不会继续向下传递,因为他们不需要被渲染;
- 部分在视野内的图元需要进行一个处理,这就是裁剪。
屏幕映射(Screen Mapping)是不可配置且不可编程的,主要负责爸每个图元的坐标转换到屏幕坐标系中。
屏幕映射(Screen Mapping)的任务是把每个图元的 x 和 y 坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
- 屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
光栅化阶段
三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段都是固定函数(Fixed-Function)阶段。
三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。 如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。
片元着色器(Fragment Shader)是可编程的,它用于实现逐片元(Pre-Fragment)的着色操作。
- 片元着色器(Fragment Shader)是另一个非常重要的可编程着色器阶段。在 DirectX 中,片元着色器被称为像素着色器(Pixel Shader),但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。
- 前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段一逐片元操作(Per-Fragment Operations)。
- 片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。
- 这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的 3 个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
- 虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息( gradient,或者说是derivative)。
逐片元操作(Pre-Fragment Operations)阶段负责执行修改颜色、深度缓冲、进行混合等重要操作,该阶段不可编程,但具有很高的可配置性。
逐片元操作(Per-Fragment Operations)是 OpenGL 中的说法,在 DirectX 中,这一阶段被称为输出合并阶段(Output-Merger)。Merger 这个词可能更容易让读者明白这一步骤的目的:合并。而 OpenGL 中的名字可以让读者明白这个阶段的操作单位,即是对每一个片元进行一些操作。
主要任务
- (1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。
- (2) 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
一些容易困惑的概念
什么是 OpenGL/DirectX
- 如果要开发者直接访问 GPU 是一件非常麻烦的事情,我们可能需要和各种寄存器、显存打交道。而图像编程接口在这些硬件的基础上实现了一层抽象。 OpenGL 和 DirectX 就是这些图像应用编程接口,这些接口用于渲染二维或三维图形。可以说,这些接口架起了上层应用程序和底层 GPU 的沟通桥梁。一个应用程序向这些接口发送渲染命令,而这些接口会依次向显卡驱动(GraphicsDriver)发送渲染命令,这些显卡驱动是真正知道如何和 GPU 通信的角色,正是它们把 OpenGL 或者 DirectX 的函数调用翻译成了 GPU 能够听懂的语言,同时它们也负责把纹理等数据转换成 GPU 所支持的格式。一个比喻是,显卡驱动就是显卡的操作系统。
什么是 HLSL、GLSL、CG
在可编程管线出现之前,为了编写着色器代码,开发者们学习汇编语言。为了给开发者们打开更方便的大门,就出现了更高级的着色语言(Shading Language)。 着色语言是专门用于编写着色器的,常见的着色语言有 DirectX 的 HLSL (High Level Shading Language)、OpenGL 的GLSL(OpenGL Shading Language)。以及 NVIDIA 的 CG (C for Graphic)。 HLSL、GLSL、CG 都是“高级(High-Level)” 语言,但这种高级是相对于汇编语言来说的,而不是像 C# 相对于 C 的高级那样。这些语言会被编译成与机器无关的汇编语言,也被称为中间语言(Intermediate Language, IL)。这些中间语言再交给显卡驱动来翻译成真正的机器语言,即 GPU 可以理解的语言。
- GLSL 的优点在于它的跨平台性,它可以在Windows、Linux、 Mac甚至移动平台等多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。也就是说,只要显卡驱动支持对GLSL的编译它就可以运行。这种做法的好处在于,由于供应商完全了解自己的硬件构造,他们知道怎样做可以发挥出最大的作用。换句话说,GLSL是依赖硬件,而非操作系统层级的。但这也意味着GLSL的编译结果将取决于硬件供应商。要知道,世界上有很多硬件供应商——NVIDIA、 ATI等,他们对GLSL的编译实现不尽相同,这可能会造成编译结果不一致的情况,因为这完全取决于供应商的做法。
- 而对于HLSL,是由微软控制着色器的编译,就算使用了不同的硬件,同一个着色器的编译结果也是一样的(前提是版本相同)。但也因此支持HLSL的平台相对比较有限,几乎完全是微软自已的产品,如Windows、Xbox 360、PS3 等。这是因为在其他平台上没有可以编译HLSL的编译器。
- CG则是真正意义上的跨平台。它会根据平台的不同,编译成相应的中间语言。CG语言的跨平台性很大原因取决于与微软的合作,这也导致CG语言的语法和HLSL非常相像,CG语言可以无缝移植成HLSL代码。但缺点是可能无法完全发挥出OpenGL的最新特性。
- 对于 Unity 平台,我们同样可以选择使用哪种语言。在 Unity Shader 中,我们可以选择使用“CG/HLSL”或者“GLSL”。带引号是因为 Unity 里的这些着色语言并不是真正意义上的对应的着色语言,尽管它们的语法几乎一样。以 Unity CG 为例,你有时会发现有些 CG 语法在 Unity Shader 中是不支持的。关于 Unity Shader 和真正的 CG/HLSL、GLSL 之间的关系我们会在后续章节中讲到。
什么是 Draw Call
- 在前面的章节中,我们已经了解了Draw Call的含义。Draw Call本身的含义很简单,就是CPU调用图像编程接口,如OpenGL中的glDrawElements命令或者DirectX中的DrawIndexedPrimitive命令,以命令GPU进行渲染的操作。
- 一个常见的误区是,DrawCall中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,其实不是的,真正“拖后腿”其实的是CPU。
- 在深入理解Draw Call 之前,我们先来看一下CPU和GPU之间的流水线化是怎么实现的,即它们是如何相互独立一起工作的。
什么是 Shader
- GPU流水线上一些可高度编程的阶段,而由Shader(着色器)编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置);
- 有一些特定类型的着色器,如顶点着色器、片元着色器等;
- 依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。
第 3 章 Unity Shader 基础
什么是 Unity Shader
- Unity 提供了一个地方能够让开发者更加轻松地管理着色器代码以及渲染设置(如开启/关闭混合、深度测试、设置渲染顺序等),而不需要像上面的伪代码一样,管理多个文件和函数等。Unity提供的这个“方便的地方”,就是Unity Shader。Unity Shader 本质上就是一个文本文件
材质(Material)和 Unity Shader
首先创建需要的Unity Shader和材质,然后把Unity Shader赋给材质,并在材质面板上调整属性(如使用的纹理、漫反射系数等)。最后,将材质赋给相应的模型来查看最终的渲染效果
Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用哪些纹理等)和指令( 渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应的模型。
Unity 一共提供了 4 种 Unity Shader 模板供我们选择:
- Standard Surface Shader:产生一个包含了标准光照模型的表面着色器模板;
- Unlit Shader:产生一个不包含光照(但包含雾效)的基本的项点/片元着色器;——(本书重点)
- Image Effect Shader:为我们实现各种屏幕后处理效果(详见第12章)提供了一个基本模板;
- Compute Shader:产生一种特殊的Shader文件,这类 Shader 旨在利用 GPU 的并行性来进行一些与常规渲染流水线无关的计算。
什么是 ShaderLab
- Unity Shader为控制渲染过程提供了一层抽象。如果没有使用Unity Shader(左图),开发者需要和很多文件和设置打交道,才能让画面呈现出想要的效果;而在Unity Shader的帮助下(右图),开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作
- 在Unity中,所有的Unity Shader都是使用ShaderLab来编写的。ShaderLab 是Unity提供的编写Unity Shader的一种说明性语言。它使用了一些嵌套在花括号内部的语义(syntax) 来描述一个Unity Shader文件的结构。这些结构包含了许多渲染所需的数据,例如Properties 语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。从设计上来说,ShaderLab 类似于CgFX和Direct3D Effects (.FX)语言,它们都定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。
第 4 章 学习 Shader 的数学基础
坐标系
点和矢量
矩阵
变换
- 变换(transform),指的是我们把一些数据,如点、方向矢量甚至是颜色等,通过某种方式进行转换的过程。在计算机图形学领域,变换非常重要。尽管通过变换我们能够进行的操作是有限的,但这些操作已经足够奠定变换在图形学领域举足轻重的地位了。
平移矩阵
缩放矩阵
旋转矩阵
复合变换
坐标空间
- 渲染流水线中顶点的空间变换过程
- Unity中各个坐标空间的旋向性
法线变换
- 法线(normal),也被称为法矢量(normal vector)。在上面我们已经看到如何使用变换矩阵来变换一个顶点或一个方向矢量,但法线是需要我们特殊处理的一种方向矢量。 在游戏中,模型的一个顶点往往会携带额外的信息,而顶点法线就是其中一种信 息。当我们变换一个模型的时候,不仅需要变换它的顶点,还需要变换顶点法线,以便在后续处理(如片元着色器)中计算光照等。
- 我们先来了解一下另一种方向矢量——切线(tangent),也被称为切矢量(tangent vector)。 与法线类似,切线往往也是模型顶点携带的一种信息。它通常与纹理空间对齐,而且与法线方向垂直
第5章 开始 Unity Shader 学习之旅
使用假彩色对 Unity Shader 进行调试
- 可视化法线
- 可视化切线
- 可视化副切线
- 可视化第一组纹理坐标
- 可视化第二组纹理坐标
- 可视化第一组纹理坐标的小数部分
- 可视化第二组纹理坐标的小数部分
- 可视化顶点颜色
第6章 Unity 中的基础光照
宏观上的渲染包含两大部分:决定一个像素的可见性,决定这个像素上的光照计算。
通常来讲,我们要模拟真实的光照环境来生成一张图像, 需要考虑3种物理现象。
- 首先,光线从光源(lightsource)中被发射出来。
- 然后,光线和场景中的-些物体相交: 一些光线被物体吸收了,而另一些光线被散射到其他方向。
- 最后,摄像机吸收了一些光,产生了一张图像。
光照模型
着色(shading)指的是,根据材质属性( 如漫反射属性等)、光源信息( 如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。我们也把这个等式称为光照模型(Lighting Model)。 不同的光照模型有不同的目的。例如,一些用于描述粗糙的物体表面,一些用于描述金属表面等。
标准光照模型
在1975 年,著名学者裴祥风 (Bui Tuong Phong)提出了标准光照模 型背后的基本理念。标准光照模型只关心直接光照(directlight),也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。
它的基本方法是,把进入到摄像机内的光线分为4个部分,每个部分使用一种方法来计算它的贡献度。这4个部分是。
- 自发光(emissive) 部分,本书使用Cmissise 来表示。这个部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。需要注意的是,如果没有使用全局光照(global illumination)技术,这些自发光的表面并不会真的照亮周围的物体,而是它本身看起来更亮了而已。
- 高光反射(specular)部分,本书使用Cspeceular 来表示。这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
- 漫反射(diffuse) 部分,本书使用Cduifise 来表示。这个部分用于描述,当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。
- 环境光(ambient) 部分,本书使用Cambient 来表示。它用于描述其他所有的间接光照。
光照模型计算方法
- 逐像素光照(per-pixel lighting):在片元着色器中计算光照模型。在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算。这种在面片之间对顶点法线进行插值的技术被称为Phong着色(Phong shading),也被称为Phong插值或法线插值着色技术。这不同于我们之前讲到的 Phong 光照模型。
- 逐顶点光照(per-vertex lighting):在顶点着色器中计算。逐顶点光照也被称为高洛德着色(Gouraud shading)。在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射时)时,逐顶点光照就会出问题。在后面的章节中,我们将会看到这种情况。而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。
漫反射
- diffuse = 光照颜色材质漫反射颜色max( 0, cos( 点到光源方向与法线夹角 ) )
高光反射
Phong 模型
- specular = 光照颜色材质高光反射颜色max( 0, cos(视角方向与反射方向夹角) )^Gloss
Blinn Phong模型
- specular = 光照颜色材质高光反射颜色max( 0, 法线*h向量 )^Gloss
- 这里的h向量 = normarlize( 视角方向 + 光照方向 )
第 7 章 基础纹理
单张纹理
- 使用单张纹理来用为模拟物体的漫反射颜色
凹凸映射(bump mapping)
凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”
高度纹理
- 使用一张高度图来实现凹凸映射
法线纹理
模型空间的法线纹理(左边)
修改后的模型空间中的表面法线存储在一张纹理中
- 实现简单,更加直观
- 可以提供平滑边界
切线空间的法线纹理(右边)
使用模型顶点的切线空间来存储法线
- 自由度很高,同一纹理可应用多个模型
- 可进行 UV 动画
- 可以重用法线纹理
- 可压缩
渐变纹理
- 使用渐变纹理来控制漫反射光照结果
遮罩纹理
- 遮罩纹理可以让我们更加精细地控制光照细节,得到更细腻的效果
第 8 章 透明效果
两种实现透明效果的方法
透明度测试(Alpha Test)
- 要么完全透明,要么完全不透明
透明度混合(Aplha Blending)
- 可以得到真正的半透明效果
第 9 章 更复杂的光照
Unity 的渲染路径
前向渲染路径(Forward Rendering Path)
- 逐顶点处理
- 逐像素处理
- 球谐函数(Spherical Harmonics,SH)处理
延迟渲染路径(Deferred Rendering Path)
顶点照明渲染路径(Vertex Lit Rendering Path)
Unity 的光源类型
- 平行光
- 点光源(point light)
- 聚光灯(spot light)
- 面光源(area light)
Untiy 的光照衰减
- 使用纹理来计算衰减
- 使用数学公式计算衰减
Unity 的阴影
- 不透明物品的阴影
- 透明度物品的阴影
第 10 章 高级纹理
立方体纹理
天空盒子
- 反射
- 折射
- 菲涅尔反射(Fresnel reflection)
渲染纹理
- 镜子效果
- 玻璃效果
程序纹理
- 指的是那些由计算机生成的图像
- 程序材质:专门使用程序纹理的材质
第 11 章 让画面动起来
纹理动画
- 序列帧动画
- 滚动的背景
顶点动画
- 流动的河流
- 广告牌
第 12 章 屏幕后处理效果
建立一个基本的屏幕后处理脚本系统
调整屏幕的亮度、饱和度和对比度
边缘检测
高斯模糊
Bloom 效果
运动模糊
第 13 章 使用深度和法线纹理
深度纹理
- 深度纹理实际就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。由于被存储在一张纹理中,深度纹理里的深度值范围是[0,1],而且通常是非线性分布的。
全局雾效
第 14 章 非真实感渲染
非真实感渲染的目标是使用一些渲染方法使得画面达到和某些特殊的绘画风格相似的效果,例如卡通、水彩风格等。
卡通风格渲染
素描风格渲染
第 15 章 使用噪声
向规则的事物里添加一些“杂乱无章”的效果以达到某种特殊的视觉效果,这种“杂乱无章”的效果来源于噪声
消融效果
箱子的消融效果
- 消融效果使用的噪声纹理
水波效果
包含菲涅耳反射的水面波动效果。在左图中,视角方向和水面法线的夹角越大,反射效果越强。在右图中,视角方向和水面法线的夹角越大,折射效果越强
- 水波效果的立方体纹理
第 16 章 Unity 中的渲染优化技术
移动平台的特点
影响性能的因素
CPU 优化
- 使用批处理技术减少 draw call 数目
GPU 优化
减少需要处理的顶点数目
- 优化几何体
- 使用模型的 LOD(Level of Detail)技术
- 使用遮挡剔除(Occlusion Culling)技术
减少需要处理的片元数目
- 控制绘制顺序
- 警惕透明物体
- 减少实时光照
减少计算复杂度
- 使用 Shader 的 LOD(Level of Detail)技术
- 代码方面的优化
节省内存带宽
- 减少纹理大小
- 利用分辨率缩放
第 17 章 Unity 的表面着色器探秘
表面着色器(Surface Shader)
- 实际上就是在顶点/片元着色器之上又添加了一层抽象
编译指令
- 表面函数
- 光照函数
两个结构体
- 数据来源:Input 结构体
- 表面属性:SurfaceOutput 结构体