5着色基础(上)
当我们渲染三维对象的时候,模型不止是一个几何的形状,它也应该有所需要的视觉外观。根据应用场景可以要求结果是更写实的或者是更风格化的。
1着色模型
当我们需要决定所渲染物体的外观时,第一步就是选择一个着色模型。通过这一模型可以描述物体的颜色如何根据面的角度,视线和光线方向进行变化。
首先作为一个例子我们先对Gooch shading model进行介绍。这是一种非写实的渲染形式,它用于在技术文档中增加插图细节的易读性。Gooch的核心要点就是比较表面法向量和相对光线的关系。如果面向光线就表现为暖色,反之表现为冷色,之间不同的角度会表现为冷暖颜色的插值。显示效果如下所示,本例子中还补充一个高光效果。
不同的着色模型都会使用若干属性控制显示的效果,这个例子里面用到的属性就是使用的表面颜色。使用不同颜色的结果如下所示。
和本例类似的大多数着色模型的显示颜色都会被表面法向量、视线方向、光线方向所影响,这些方向都会被表达为单位长度的向量。综合以上所有内容就能够定义一个模型的数学公式了。
最终显示的Cshaded颜色会是高光颜色与冷暖颜色的叠加。其中进行插值的系数由以下公式所定义。
t使用内积也就是余弦值表示光线与表面夹角,面向光线时大于1/2背向光线小于1/2。r是反射向量,也就是使用入射光线关于法向量做对称的结果。最后根据rv的夹角可以计算高光所占权重,其中±符号表示一个clamp函数,将结果限制在0和1之间。
通过组合不同的数学公式和着色参数,可以定义极为丰富的写实和风格化着色模型。
2光源
上一节的例子中光源的影响是极为简单的,但真实世界的光是极为复杂的,他们会包含大小颜色形状强度,对于间接光源会包含更多变量,在第九章实现真实感渲染会将这些参数都纳入考虑范畴。
光源对表面产生效果首先要考虑是否被照射的二值状态,这一点可以由多种因素决定,包括与光源的距离、是否在阴影中、是否面向光源,朝向可以使用表面法向量和光源的夹角余弦值的正负进行判断。下一步在这二值之间会有光照强度的一个连续渐变的效果,一般使用一个权值来进行插值实现这一效果。基于以上想法我们定义多光源着色的一般公式如下,unlit表示未被照射物体本身看起来的颜色,Clight表示光线颜色,lit表示权值。
对于同等强度的光线照射在平面上会由于入射角度不同产生不同的照射效果,如下图所示与大的角度产生的照射点间距越大,这代表它的亮度就会越小。而这一强度将于余弦值相关,根据这一原理我们定义公式5.6,加号角标表示负数将取为0。
接下来将介绍几种典型光源,对于给定的表面点位,每个光源都只能从一个方向L照亮这个表面。换句话说所有光源都被认为是一个无穷小的点,虽然现实并不如此,但是大部分光源的大小都远小于相对他们到照明平面的距离,如此这一假设便具有其合理性。
2.1平行光(Directional Lights)
平行光是最简单的光源模型,方向l和clight在整个场景中都是作为常量。平行光没有位置概念,所以是一个抽象的概念,它可以表达光源距离场景无限远的情况。对于实现一些特殊的效果的场景可以要求平行光的clight进行变化,例如可以设置大小两个嵌套的方形,大方之外为纯黑,小方框之内为正常平行光,而二者之间会是一个过渡渐变的效果。
2.2点光源(Punctual Lights)
Punctual这里不能翻译成“准时的”,他表示一个光源拥有位置,但是仍然没有维度(没有形状大小),对于这类光源需要计算它的光照向量l,对于任何表面点p0有公式如下:
Point/Omni Lights
泛光灯随着距离越远,传播光强也会减弱,一种公式称为inverse-square light attenuation,为了能避免除0,最常用的形式如下:
rmin代表着发出光线的物体半径,小于rmin的面表示嵌入到光源体内部,所以这是不可能的。这一公式存在一个性能方面的缺陷,当距离很远的时候有必要直接将光强设置成0从而提高渲染效率,为了避免变为0的边界距离处出现光照的跳变,所使用的公式最好在归0的点导数为0,一种公式称为windowing function:
公式+表示平方前先对负数取为0。在某些应用中,如果对于光照没有关于导数的限制也可以直接使用从0.8rmax到rmax间的线性衰减函数来完成。以上所有与距离相关的公式统称distance falloff functions,在不同场景可用的公式还有很多,例如把窗口函数的4改成2、指数作为分母的函数等等。
场景光(Spotlights)
场景光设置一个方向,偏离这一光照方向越大,传播的光强越小,从而形成一个锥体的光照区域。如下图所示,超过角度u的光强置为零,小于s的光强不做衰减,而在二者之间例如角度p传播光强会使用方向衰减函数directional falloff functions,函数需要使用余弦值做差的商t来进一步计算:
以下三张图片分别对应平行光点光源场景光的照明效果。
其他光源
平行光和点光源是根据他们的光线方向l的计算方法进行区分的。不同类型的光源通过其他方法计算它们的光线方向。例如古墓丽影中设计了一种线段光源,每个着色面计算光线方向会使用该位置距离线段最近的点来计算l。
光源类型的讨论是抽象的,现实世界光源具有大小和形状,如此他们从多个方向照亮一个面点。这样的光源称为区域光源(area lights),这一类型的使用在实时渲染中稳定增加。区域光渲染技术细分为两种类别:计算由于区域光导致的阴影边缘柔和效果;模拟区域光源在表面的着色效果。第二种方法在光滑镜面表面的效果是非常卓越的,可以在反射结果中明显分辨出光源的形状。对光区域进行近似计算一直在发展并且实现变得越发廉价应用也逐步广泛。GPU的能力提升也让我们可以实现比过去更多精细化的技术。
5.3渲染模型实现
之前提到的着色和光照公式当然需要实现为代码才能有所应用。这一节将会介绍实现过程中的设计要点。
计算频率(frequency of evaluation)
当我们设计一套着色过程的实现方案,计算过程需要根据他们的计算频率进行细分。首先判断给定计算的结果是否在整个draw call过程中是作为一个常量。这时计算可以在应用中由CPU执行,而GPU可以后续用于计算开销尤其高昂的计算过程,计算结果会通过图形接口使用uniform变量作为输入传入。或者这一变量可以直接作为常量编译在着色器中。计算的过程也可以脱机执行,在软件安装、应用加载过程执行计算。
另一种情景是变量在程序运行期间处于变化的状态,而且每帧进行渲染都进行更新是没有必要的,例如光线属性在一天的不同时间是不断变化的,这个变化过程可以分摊在多个帧来实现。而当一个变化发生在每一帧中,例如相机投影变换矩阵的变化。这时将uniform变量进行分组可以提高应用执行效率,减少GPU常量更新提升效率。
当一个属性在一次draw call内是不固定的时候,我们可以使用着色器来对其进行处理。实际过程中大部分计算是逐片元在片元着色器上计算完成的。其他的着色器用于执行几何学操作,例如移动和变形。下面给出一个顶点着色器和片元着色器完成相同任务的结果比较,在过去这两种思路分别对应Gouraud shading 和 Phong shading,这两个概念如今不再经常使用了。
上图分别是使用片元着色、顶点着色、和模型顶点信息的展示。结果显示顶点着色器计算得到的高光在越简单的模型中越能明显的看出它所产生的错误。这是因为法向量在面中是线性变换的,直接在顶点着色中计算高光会导致面上的高光无法被计算,只能通过顶点的颜色进行插值得到。理论上我们可以在片元着色中计算镜面高光,而在顶点着色中计算其他着色。但这种方法在实践中不是最优的,线性插值的计算在着色模型中是开销极低的,所以将计算过程按这种方式分割会增加它的开销(例如重复执行了相同的计算或者增加了额外的vary变量)掩盖了它带来的一点优势。
面内的法向量坐标位置都需要使用顶点所设置的属性来进行插值计算,顶点着色输入的法向量我们都会先进性归一化,但是要注意执行插值过程势必会导致向量长度发生变化,如下图左所示。所以在我们执行片元着色器计算前同样需要自行将插值得到的法向量进行一次归一化;那么归一由片元过程负责是否意味着顶点着色其实不需要归一了呢?顶点着色器使用的向量长度同样是重要的,长度不同的顶点法向量进行插值回导致插值结果出现偏移,如下图右所示,左侧长度变短回导致所有法向量插值结果向长的那一边偏移,因此归一化需要在顶点着色和片元着色分别执行。
面内的光线方向并不能使用和法向量一样的插值过程来计算,光线方向需要根据片元所在位置,在片元着色器内进行计算,计算方法就是使用三维坐标的减法快速实现。如果我们确实需要对一些向量属性进行插值,那么需要注意在插值前可能不可以先归一化,如下图所示,先在顶点进行归一化会导致中点位置的光照方向出现错误。
尽管大多着色过程的实现都是基于以上所描述的框架,但对于某些特殊应用可能会有例外。一种平面外观的风格化显示效果在游戏中经常使用,称为平面着色(flat shading)。它的实现一般使用顶点着色器的第一个顶点法向量作为平面的法向量,并且关闭插值效果,从而让面元上的所有片元都使用相同的法向量。
实现范例
书中使用webgl实现了一个着色公式
材质系统
渲染框架基本不会只实现一种shader,专用的系统需要处理应用中需要的各种各样的材质、着色模型和着色器。如之前章节所解释的那样着色器是GPU中的可编程阶段,所以他是API的底层资源无法被作者直接进行交互。但是材质这一概念是个直接面向用户的,它封装了一种表面的视觉效果,材质也可以描述物体的碰撞体积。
材质和着色器不是一一对应的关系,在不同渲染场景,同样的材质可能会使用不同的着色器,同一个着色器也可能被多个材质共享。一个典型的例子称为参数化材质parameterized materials,在这种形式中,我们需要定义材质的模板和材质实例,每个模板描述了带有一类描述数目颜色和纹理值的参数;每个实例对应某个模板以及设置好的所有参数从而产生的实例。
参数可以在运行时更改,这可以通过传入uniform变量到着色程序中;也可以在编译前替换其数值。常见的编译时参数是一个布尔型开关用来控制设计的材质功能是否激活,这一般被设置为一个单选框在用户界面之中。
材质系统的一个最重要的任务之一就是将不同的着色函数分为若干分离的成分并且控制这些成分如何结合。许多场景下这种类型的组合是十分有用的例如:
- 使用几何处理组合曲面着色,例如刚性变换、顶点混合、变形、细分、实例化和剪裁
- 使用合成操作(如像素丢弃和混合)组合曲面着色。
- 将用于计算着色模型参数的操作与着色模型本身的计算组合在一起。
- 相互组合可单独选择的材质特征、选择逻辑和着色器的其余部分。
- 用光照计算构造阴影模型及其参数计算:计算每个光源在阴影点的clight和l值。
如果图形API能把着色代码模块化作为核心功能是非常合适的,但可惜GPU的代码在每个着色阶段都被编译为一个单元,这种分离提供了有限的模块效果。这一点比较接近提到的第一种组合方式,就是几何处理和表面着色分离。但是匹配的并不完美,因为每个着色过程都需要执行一些其他的任务,所以唯一的办法就是在源码层次上实现所有类型的组合,这就主要包括字符串穿拼接替换的操作,一般通过C风格的预处理指令例如#include #if #define。
在选用不同的着色选项时,一种方法时运行时动态执行分支,另一种是使用编译时通过条件预处理。在过去的硬件中动态分支一般是执行非常慢的,而在目前的GPU硬件中处理分支是非常快的,尤其当一次draw call中所有片元使用的分支全部相同的时候是尤其快的。因此大多使用运行时分支处理。但是当变量过多时寄存器不足会导致性能下降,因此编译时变量仍然有它存在的意义,它可以避免将一些永远不可能被执行的代码被包含编译。
材质系统设计师一般使用以下集中策略来实现以上的设计目标: - 源码复用——实现一些函数在共享文件,使用include的方式获取所需要的功能。
- 替换——设计一种着色器,通常被称为ubershader或supershader,它聚合了大量功能,使用编译时预处理条件和动态分支的组合来删除其中未使用的部分。
- 增加——各种各样的功能被定义为具有输入和输出连接器的节点,它们被组合在一起。
- 基于模板——设计了一种模板,只要不同的实现符合该接口,就可以将它们插入其中获得模板实例。
所以说实现着色等式是一个决定哪些部分可以简化的问题,计算各种表达式的频率,以及用户如何修改和控制外观的问题。渲染管道的最终输出是颜色和混合值。下面的小节将会详细介绍如何实现关于抗锯齿、透明度和图像显示,并且如何组合和修改这些值以进行显示。