Unity 3D 内建着色器源码剖析

978-7-115-50704-4
作者: 熊新科
译者:
编辑: 张涛

图书目录:

详情

本书不是一本3D图形编程入门书,也不是一本Unity 3D着色器编程入门书。本书的定位是“Unity 3D着色器代码分析教程+Unity 3D着色器编程参考手册”。因此,本书的目标读者是:有一定Unity 3D开发经验;学习过基础的Direct 3D/OpenGL图形编程,比如能知道“顶点”“纹理”和“着色器”是什么的概念和作用;了解“矩阵乘法”“四元数”和“坐标系变换”等数学等概念。

图书摘要

版权信息

书名:Unity 3D 内建着色器源码剖析

ISBN:978-7-115-50704-4

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

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

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

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

著    熊新科

责任编辑 张 涛

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

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

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

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书既是一本Unity 3D着色器代码分析教程,也是一本Unity 3D着色器编程参考手册。全书共12章,主要内容包括:实时3D渲染流水线,辐射度、光度和色度学基本理论,Unity 3D着色器系统,着色器工具函数,Unity 3D引擎的多例化技术,前向渲染和延迟渲染,Unity 3D的全局光照和阴影,UnityShadow Library.cginc文件分析,AutoLight.cginc文件分析,基于物理的光照模型,Unity 3D标准着色器和Standard. shader文件分析,片元着色器实时绘制图像实战案例。

本书适合Unity Shader的游戏开发者、程序员阅读,也可供相关专业人士参考。


1999年,我刚读大一,当我用Turbo C写下第一句绘图代码时,就对使用计算机绘制五彩斑斓的图形产生了浓厚的兴趣。计算机游戏正是把计算机图形学理论实践到极致的典范。当时,每次看到同学们玩《星际争霸》《帝国时代》等游戏,我就总是在思索:这些游戏中如此漂亮的画面是怎么绘制出来的?带着兴趣和疑问,我陆续学习了C++语言、Windows编程和DirectDraw图形绘制技术。经过漫长的学习和积累之后,在2001年,我终于用DirectDraw做出了一个小作品。这个小作品很简单,就是一个小人在一个可卷动的场景中走来走去。完成这个小作品之后,我就已经很清楚自己大学毕业后的工作目标了。2003年毕业前夕,我获得了去珠海市金山软件股份有限公司西山居实习的机会。在西山居我结识了很多业界朋友,也了解到我当时所熟悉的基于DirectDraw的2D图形编程技术已经过时。未来的计算机游戏,无论是2D游戏还是3D游戏,在图形领域都使用统一的3D编程接口,充分利用硬件加速性能已是大势所趋。同样是在西山居,我首次了解到了DirectX 8.0的第一代可编程着色器语言,开启了我长达十余年的图形着色器学习的历程。

从2004年年初到2010年年底,我先后在深圳市、厦门市参与了若干款PC网络游戏的开发,先后接触了LithTech 6.7和Gamebryo 2.3这两款3D引擎。由于购买了这两款引擎的源代码,在工作之余,我也尝试深入分析引擎的架构实现,从中学习了很多知识。随着手机游戏的爆发,Unity 3D以其易用性和良好的多平台支持性得到广大用户的青睐,成为国内开发手机游戏厂商的首选。我也于2013年年底加入了学习Unity 3D的大军。

在学习Unity 3D的过程中,除了学习基本的逻辑开发外,我还深入地学习了着色器的实现。作为一个商业闭源引擎,它由C++语言实现的引擎底层是一般用户无法接触到的。但幸运的是,Unity 3D开放了着色器层的源代码实现。基本上,官方所开发的各种效果的着色器源代码都可以免费获取。我在学习Unity 3D着色器开发时,除了查阅官方教程外,更多的是研究内置着色器的源代码以了解其实现原理。在长达4年的学习过程中,我积累了很多关于Unity 3D着色器编程的心得和经验,同时做了一些笔记。囿于工作繁重,无法花太多精力去组织文字,这些心得经验都比较零散且不成篇幅,而写一本技术专著则是我多年来的夙愿。直至2017年,我离职在家专心写作,把这些年的积累写成了本书。

作为一名用C++语言开发过若干游戏的程序员,我通读了著名华人技术作家侯捷的《STL源码剖析》《深入浅出MFC》等深入剖析C++源码框架的书籍。我被侯捷剖析大系统和大架构的能力深深折服而心向往之;对侯捷的“通过剖析经典作品的源码吸取养分,从而提升自己能力”的观点也深以为然。因此,本书也效法侯捷的《STL源码剖析》,以深入剖析Unity 3D内置着色器的功能实现为主,希望能给读者在开发工作中提供最大的帮助。

在写作本书的过程中,我得到了各方的大力支持,没有这些支持,我很难完成技术写作这项繁重的工作。首先要感谢本书的策划编辑张涛,他以独到的眼光和无限的信任促成了本书的诞生。同时,要感谢我的父母。作为一名就读于邮电院校的学生,虽然最后没有进入电信行业,可能和父母对我的期盼有所偏离,但他们对我选择游戏开发行业这条路是理解和支持的。其次要感谢我的妻子,没有她对我离职在家写书的支持和理解,我就不能在无后顾之忧的条件下专心写作。最后要诚挚地感谢每一位购买了本书的读者,你们对本书的喜爱和肯定是我继续钻研Unity 3D着色器编程技术并写出好的作品回报各位图形编程爱好者的最大动力。

熊新科


本书不是一本3D图形编程入门书,也不是一本Unity 3D着色器编程入门书。本书的定位是“Unity 3D着色器代码分析教程+Unity 3D着色器编程参考手册”。因此,本书的目标读者应具有一定的图形编程知识:有一定的Unity 3D开发经验;学习过基础的Direct 3D/OpenGL图形编程,如知道顶点、纹理和着色器的概念与作用;了解矩阵乘法、四元数和坐标系变换等数学概念。

本书是为下面3类读者撰写的。你在阅读本书之前,阅读过Unity 3D自带的或者第三方编写的着色器代码,依样画葫芦地修改过,对大部分代码知道其实现的功能,但又有很多细节不明白;或者你已经能写一些Unity 3D ShaderLab代码了,但还不清楚Unity 3D提供的内置着色器库可以提供多少现有的工具代码;抑或你因工作需要,要把Unity 3D提供的着色器进行精简改造以适应自己的项目,对千头万绪的细节感到困难。本书有助于解决这些读者的痛点。

本书所剖析的Unity 3D内置着色器代码版本是2017.2.0f3,读者可以从Unity 3D官网下载这些着色器代码。这些代码以名为builtin_shaders-2017.2.0f3.zip的压缩包的形式提供,解压缩后,内有4个目录和1个license.txt文件,介绍如下。

目录CGIncludes存放了37个扩展名为cginc的文件,两个扩展名为glslinc的文件。这些文件就是Unity 3D提供的内置着色器的头文件。本书将重点剖析表0-1中头文件的实现。

表0-1 头文件

文件名

功能描述

AutoLight.cginc

提供了一系列用来计算阴影的宏和函数

HLSLSupport.cginc

对HLSL着色器语言用宏进行封装

Lighting.cginc

对lambert、Phong、Blinn-Phong等光照模型进行封装

UnityCG.cginc

提供了大量着色器在开发时会用到的工具函数和宏

UnityGBuffer.cginc

提供了用于延迟渲染的与G-Buffer操作相关的函数和宏

UnityGlobalIllumination.cginc

提供了用于进行全局光照计算的工具函数

UnityImageBasedLighting.cginc

提供了“基于图形照明”的相关操作的工具函数

UnityInstancing.cginc

提供了使用GPU(图形处理器)多例化技术时用到的宏

UnityLightingCommon.cginc

提供了全局光照计算所需要的结构体的定义

UnityMetaPass.cginc

提供了与元渲染通路信息相关的函数和着色器变量

UnityPBSLighting.cginc

提供了基于物理着色所需要的光照计算函数,可用于外观着色器

UnityShaderUtilities.cginc

提供了把物体从模型空间变换到裁剪函数的工具函数UnityObjectToClipPos

UnityShaderVariables.cginc

提供了进行着色器开发时由引擎底层传递给着色器程序的着色器变量

UnityShadowLibrary.cginc

提供了计算阴影时用到的宏和工具函数

UnityStandardBRDF.cginc

提供了标准着色器中用到的和BRDF(bidirectional reflectance distribution function,双向反射分布函数)计算相关的工具函数

UnityStandardConfig.cginc

标准着色器用到的一些开关配置信息

UnityStandardCore.cginc

标准版的标准着色器的顶点和片元着色器的实现文件

UnityStandardCoreForward.cginc

提供了标准版的标准着色器的顶点/片元入口函数

UnityStandardCoreForwardSimple.cginc

提供了简化版的标准着色器的顶点/片元入口函数

UnityStandardInput.cginc

标准着色器用到的一些顶点输入结构信息和输入计算函数

UnityStandardMeta.cginc

标准着色器中元渲染通路的实现

UnityStandardShadow.cginc

标准着色器中阴影投射渲染通路的实现

UnityStandardUtils.cginc

标准着色器中一些辅助用工具函数的实现

目录DefaultResources存放了Unity 3D引擎内置的简单着色器。

目录DefaultResourcesExtra提供了大量渲染效果的着色器实现,Mobile子目录下的shader文件就是移动平台下的漫反射效果、粒子系统、法线贴图和光照图效果的实现。本书将详细剖析该目录下的Standard.shader文件,即标准着色器的实现。

目录Editor中唯一的文件是StandardShaderUI.cs。该段代码是当材质文件使用了标准着色器时,材质对应的inspector界面的实现。

文件license.txt用于说明Unity 3D开发公司对这些着色器代码的版权。

既然本书的定位是“Unity 3D着色器代码分析教程+Unity 3D着色器编程参考手册”,那么读者需要按照一定的阅读顺序才能达到最佳的阅读效果。对于初中级读者,可先从第1章开始精读。第1章对当前主流的渲染流水线进行阐述,讲述顶点处理阶段、光栅化阶段、片元处理与输出合并阶段这三大处理阶段的实现。这三大处理阶段是主流渲染流水线都必须实现的阶段。

1.1节概述了渲染流水线,讲述了主流渲染流水线的各个阶段,以及各个阶段的操作。1.2节介绍顶点处理阶段。首先详细讲述顶点的组织方式、坐标系的确定方式,然后对把顶点从模型空间变换至世界空间、从世界空间变换至观察空间、从观察空间变换到裁剪空间所用到的各个变换矩阵进行详细说明,最后分析Unity 3D中这些矩阵的封装代码,让读者在懂得使用这些矩阵的同时能知其然且知其所以然。1.3节介绍光栅化阶段,对其中的各个子阶段进行详细的数学推导说明。光栅化阶段是由硬件实现不可编程的,对它进行详细的数学说明也是为了让读者深入了解其原理,在开发工作中能够从底层去理解渲染流水线的机制。今后读者如果工作中需要用到其他的3D引擎,能融会贯通且更快地上手。1.4节介绍片元处理与输出合并阶段。其中,片元处理子阶段就是片元着色器的内容;输出合并子阶段也是由硬件实现不可编程的,但流水线提供了若干功能函数以对它进行控制。该节重点讲述输出合并中的深度值操作和Alpha值操作,以及Unity 3D为这两个操作所提供的控制函数。

精读完第1章后,可接着通读第2章。第2章主要从物理学的角度阐述图形渲染中本质的问题,即光的能量传递与分布问题。其中,2.1节和2.2节是学习基于物理渲染的前置知识,里面所阐述的各个物理量和它们的数学关系是阅读第10章与第11章的基础;2.3节讲述计算机如何对颜色进行数学建模,而该节则是理解2.4节中颜色空间的基础;2.4节重点讲述计算机图形学中关于“伽马校正”的内容,阅读完该节后,相信读者在工作中碰到“画面颜色总是不对且偏暗”的问题时,能理解它产生的缘由并能解决之。

第3章对Unity 3D特有的外观着色器进行分析,阐述外观着色器和传统的顶点/片元着色器之间的关系。Unity 3D的内置着色器代码中大量使用了着色器多样体,因此同一套着色器能够被编译到各个不同的硬件平台,了解着色器多样体的原理是剖析Unity 3D内置着色器代码所必需的。因此,3.4节详细分析这些着色器多样体的原理和使用方法。如果读者已经熟悉该章内容,可以跳过它。

因为Unity 3D是一个跨平台引擎,所以在Unity 3D内置着色器代码中要时刻考虑通用性问题。尤其是在开发手机游戏时,开发环境通常是Windows/Mac平台,而运行环境多是Android/iOS平台。因此,一套着色器代码起码要支持开发和运行两种不同的环境。不同的平台下使用的着色器语言也有所不同。虽然Unity 3D着色器推荐以Cg语言作为前端的开发语言,但是Unity 3D会在后台将Cg语言代码编译为目标平台的最佳运行语言的字节码。例如,在Windows平台上最佳运行语言是HLSL,而Android/iOS平台上则是OpenGL ES。Unity 3D着色器语言提供了一系列消除平台和开发语言差异性的机制。3.5节和3.6节会阐述这些机制。

Unity 3D提供了大量的通用工具函数和一些由引擎底层在运行期赋值的着色器变量。无论是Unity 3D内置着色器或者第三方编写的着色器,都大量使用了这些预定义的通用工具函数和着色器变量。这些通用工具函数集中在UnityCG.cginc文件中,着色器变量则定义在UnityShader Variables.cginc文件中。第4章重点剖析这两个文件的实现,尤其讲述UnityCG.cginc文件中工具函数的实现原理。开发者在编写自己的着色器时,如果碰到一些要实现的功能,不妨先查阅该章,看看Unity 3D引擎是否已经提供了已有的实现。同时,因为Unity 3D内置着色器自身也大量使用了这两个文件中的内容,所以本书其他章节中也大量交叉引用了该章内容,读者在阅读剖析着色器代码的章节时,也应经常查看该章内容。

Unity 3D内置着色器大量使用了GPU多例化技术。Unity 3D在UnityInstancing.cginc文件中提供了使用GPU多例化技术要用到的宏。第5章讲述GPU多例化技术的实现原理,并剖析UnityInstancing.cginc文件中Unity 3D引擎对它的封装实现。

Unity 3D有两种渲染方式:一种是前向渲染,另一种是延迟渲染。Unity 3D提供的标准着色器文件Standard.shader中有这两种渲染方式的实现。第6章讲述前向渲染和延迟渲染的基本原理,以及Unity 3D对延迟渲染的一些实现细节。

图形渲染的两大主题是光照和阴影的计算。Unity 3D引擎除了支持光源对物体的照明计算(即直接照明)之外,还支持物体之间的光照效果,即间接照明。两者统称为全局照明。第7~9章讲述Unity 3D的全局光照和阴影计算原理。其中,7.7节从数学原理出发,重点阐述球谐光照原理和Unity 3D对它的封装实现。Unity 3D提供了大量完成光照计算和阴影计算的工具函数与宏,无论是第三方着色器还是引擎内置着色器都会大量使用到它们。这些函数与宏分别在UnityShadow Library.cginc文件和AutoLight.cginc文件中定义。第8章和第9章详细剖析这两个文件的实现。如果开发者在自己编写的着色器中需要实现某功能,或者在阅读第三方着色器代码时碰到这些文件中定义的函数和宏,可以查阅这两章。

近年来,能够产生更为逼真效果的基于物理的光照模型开始广泛应用在各大3D引擎中。第10章分析若干简单的光照模型,并从数学和物理原理上分析基于Cook-Torrance模型的光照模型的实现。Standard.shader文件则是基于物理光照模型的着色器的实现。第11章详细分析Standard.shader文件的实现,以及实现Standard.shader时要用到的分布在UnityStandardInput.cginc、UnityStandard Utils.cginc等文件中的工具宏和函数。

第12章是着色器编程实战案例。该章将使用Unity 3D着色器,在不使用任何纹理贴图的方式下,利用带符号距离场技术,通过片元着色器绘制一个名为“星夜之海”的动态场景。

代码段的格式如下。

//文本块最左侧有一条竖线,表明这是一个代码块
//代码中的行状注释用“//”符号开头
//块状注释用“/**/”符号包含。代码中的Cg语言关键字加粗显示
float3 a = float3(1.0,1.0,1.0);
float3 b = float3(2.0,1.0,1.0);
float3 c = dot(a,b);

原始代码中,原本是在一行中定义的,但由于纸面篇幅所限,会做一些换行处理。为了保持代码的严谨性,原始代码中的一些宏定义原本是没有转行声明符“\”的,在本书中会加上,例如:

#if defined(UNITY_COMPILER_HLSLCC) && !defined(SHADER_API_GLCORE) 
#define UNITY_DECLARE_TEX3D_FLOAT(tex) Texture3D_float tex;\
 SamplerState sampler##tex

上面的代码中,第二行末尾的“\”符号在原始代码中是不存在的。为了排版需要,把原来在一行的语句段分写成两行。在Cg语言中定义一个宏时,换行时要加上“\”符号。

本书引用的原始代码中,出于排版和剖析说明的原因,在保证不改变代码逻辑的前提下,会对原始代码做版面上的调整。例如,在原始文件中原本是书写成一行的代码,可能会变成多行书写;原来代码没有注释的地方,可能会在书中加上注释;原本有英文注释的地方,可能会换上中文注释。但为了便于读者对照着原始文件阅读本书,本书中引用的原始代码段所在原始文件中的名字、所在目录,以及在原始文件中的起始行和结束行都会在前面加上注释说明,如下所示。

// 所在文件:UnityGlobalIllumination.cginc
// 所在目录:CGIncludes
// 从原文件第44行开始,至第49行结束
inline void ResetUnityLight(out UnityLight outLight)
{
    outLight.color = half3(0, 0, 0);
    outLight.dir = half3(0, 1, 0); //任意设置一个光线输出方向,不为空即可 
    outLight.ndotl = 0;            //数据项未使用
}

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

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

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

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

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们,邮箱为zhangtao@ptpress.com.cn。

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

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

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

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

异步社区

微信服务号


在计算机体系结构中,管线(pipeline)可以理解为处理数据的各个阶段和步骤。3D渲染流水线(render pipeline[1])接收描述三维场景的数据内容,经过若干阶段的处理,将其以二维图像的形式输出。渲染流水线有多种,本章讨论的是基于光栅器插值(rasterizer interpolation)的实时3D渲染流水线。

目前主流的实时3D渲染流水线有Direct 3D和OpenGL。这两种流水线在具体的实作上大体相同,但细节上的差异也很明显。因为Unity 3D是一个跨平台引擎,所以本章讨论的渲染流水线也不局限于某一家具体的实作,而是讨论各个实作共同的阶段和机制。在深入探讨时,也会分析Direct3D与OpenGL的差异。

渲染流水线一般可以分为如下阶段:顶点处理(vertex processing)、光栅化(rasterization)、片元处理(fragment processing)和输出合并(output merging)。顶点处理阶段对存储在顶点缓冲区(vertex buffer)中的各顶点执行各种操作,如坐标系变换等。光栅化阶段对由顶点构成并变换到裁剪空间(clip space)的多边形进行扫描插值,将这些多边形转换成一系列的片元集合。片元(fragment)是指一组数据值,这些数据最终用于对颜色缓冲区[2](color buffer)中的像素[3](pixel)颜色值、透明值,以及深度缓冲区(depth buffer)中的深度值进行更新。片元处理阶段对各个片元进行操作,确定每个片元的最终颜色值和透明值。输出合并阶段则是对片元与颜色缓冲区中的像素进行比较或合并操作,然后更新像素的颜色值和透明值。

实时3D渲染流水线发展至今已经很成熟,而且其组成阶段比上述4个阶段要多一些。尽管如此,上述4个阶段仍然是每一家实时3D渲染流水线实作的主要内容。其中,顶点处理与片元处理两个阶段是可编程的(programmable)。针对这些阶段的、由GPU执行的相关程序称为顶点着色器(vertex shader)和片元着色器(fragment shader)。顶点着色器可针对顶点执行任何转换操作;片元着色器可针对片元用各种方式决定其最终颜色值和透明值。图1-1所示为渲染流水线的基本阶段和流程,其中顶点处理阶段和片元处理阶段是可编程控制的;而光栅化阶段和输出合并阶段则由硬件以固定不可编程的方式实现。

▲图1-1 渲染流水线的基本阶段和流程

3D渲染流水线的第一个阶段便是顶点处理阶段。本阶段将会读取描述三维场景内容的顶点信息并进行处理。在计算机图形学中,可以使用各种建模方案提供描述三维场景的顶点信息,常用和高效的方式是使用多边形网格(polygon mesh)去组织顶点。

不同3D渲染流水线的实作,所支持的组成网格的多边形种类有所不同,但都需要使用凸多边形[4](convex polygon)。在实践中大都使用三角形网格,如图1-2所示。当使用三角形网格对表面进行近似模拟时,可以考虑使用多种细分(tessellation)处理方法。当三角形的细分程度越高时,网格就越接近原始表面,处理时间也会随之增加。因此,在实践中要根据当前硬件条件和开发需求选择一个折中的方案。

▲图1-2 利用三角形网格组织顶点

描述三角形网格的常见方法有多种,其中一个是列举顶点,即顺序读取3个顶点构成一个三角形。存储顶点的内存区域即1.1节介绍的顶点缓冲区。如图1-3所示,缓冲区内的顶点可以定义3个三角形,这种表达方式称为三角形列表(triangles list)。三角形列表的方式比较直观,但显然缓冲区中的顶点信息存在冗余。非索引方式的三角形列表,一个网格如果包含n个三角形,则顶点缓冲区中有3n个顶点。在图1-3中,3个三角形同时拥有(1,1,0)处的顶点,且该顶点在缓冲区中重复出现了3次。

▲图1-3 三角形列表

在三角形网格中,一个顶点经常被多个三角形共享。因此,可以给每个顶点都分配一个整数索引值,记录三角形的方式可以从直接记录顶点本身变成记录顶点索引,以减少数据冗余,让每一个顶点在顶点缓冲区中只需要存储一份。顶点索引则存储在索引缓冲区(index buffer)中,如图1-4所示。

▲图1-4 索引方式的三角形列表,索引缓冲区负责存储三角形用到的顶点编号信息

图1-4中的顶点只包含了位置信息,乍一看,顶点缓冲区中节省出来的空间也得要用在索引缓冲区上。在实际应用中,除了位置信息外,顶点缓冲区中存储的信息还包含法线(normal)、纹理映射坐标(texture mapping coordinate);如果是用作动画模型的顶点,那么还有骨骼权重(bone weight)等。因此,当顶点数量很大时,如果使用索引方式,那么顶点缓冲区冗余数据的减少量是非常可观的。

描述空间方位的坐标系有多种,如极坐标系、球面坐标系和笛卡儿坐标系等。在3D渲染流水线中,使用最广泛的是笛卡儿坐标系。笛卡儿坐标系可以分为左手坐标系和右手坐标系。当左手大拇指或右手大拇指指向坐标系z轴正方向时,其余四指指尖的环绕方向,就是坐标系x轴绕向y轴的方向,满足这一规则的笛卡儿坐标系即称为左手坐标系或右手坐标系,如图1-5所示。

▲图1-5 笛卡儿坐标系

在光照计算中需要使用法线。法线既可以是一个顶点的法线,也可以是一个多边形的法线。首先探讨一个三角形的法线。在右手坐标系中定义一个三角形的法线朝向,使用右手法则定义,即右手四指围拢,按照组成三角形3个顶点在缓冲区中先后排列顺序围拢四指,此时右手拇指的朝向就是三角形的法线方向;在左手坐标系中,则使用左手法则定义,方法和右手法则相同,左手拇指的朝向就是三角形的法线方向。

如图1-6所示,三角形处于右手坐标系下,其3个顶点在顶点缓冲区中以的顺序排列,那么采用右手.法则,使右手四指按顶点先后排列顺序的走向弯曲,得到图1-6所示的三角形法线朝向。

▲图1-6 确定三角形法线朝向

在给定了顶点排列顺序之后,用向量叉积运算可以计算出法向量的值。依然以图1-6为例,假设连接p1p2形成边向量v12,连接p1p3形成边向量v13。利用两个边向量的叉积,可以得到垂直于两个边向量的向量。用向量除以它自己的长度便得到单位化(normalized,又称为规格化)的法向量。

(1-1)

必须注意的是,向量的叉积运算是不满足交换律的。式(1-1)中v12v13如果交换叉积运算顺序,就相当于图1-6的顶点在缓冲区中的排列顺序变为,此时法向量的计算公式应该为

(1-2)

依据向量叉乘的性质,式(1-1)和式(1-2)得出的向量的方向是相反的。由此可知,给定三角形的法向量主要依赖于该三角形顶点的排列顺序。的排列顺序称为逆时针方向(counter clockwise,CCW),的排列顺序称为顺时针方向(clockwise,CW)。另外,在右手坐标系中以某种顶点排列顺序形成的三角形,不改变顶点排列顺序地放在左手坐标系中时,其法线也和在右手坐标系时相反。如果把法线朝向方定义为三角形外表面,法线朝向方相反方向定义为内表面,则当把顶点数据从左(右)手导入右(左)手坐标系时,会产生内外表面相反的情况。如图1-7所示,图1-7(a)是右手坐标系,三角形的顶点按的顺序排列,此时三角形的法线朝外;图1-7(b)是左手坐标系,如果保持按的顺序排列,此时三角形的法线则朝里。

因此,要解决图1-7揭示的三角形的内外表面反转的问题,需要重新调整顶点在缓冲区中的排列顺序。图1-7(a)中右手坐标系的顶点,在图1-7(b)的左手坐标系下排列顺序改为即可解决问题。

▲图1-7 右手坐标系和左手坐标系

在实际开发中,顶点的法线就更为重要一些。大部分建模软件在编辑模型时就可以直接指定顶点法线。与三角形的法线就是该三角形所在平面垂直的向量不同,理论上一个顶点的法线可以是过该点的任意一条射线。一般情况下,某顶点的法线通常通过共享该顶点的三角形法线进行计算,如图1-8所示。法线n的计算公式如下。

▲图1-8 顶点的法线n共享该顶点的三角形法线

除了后面将要介绍的观察空间使用右手坐标系外,Unity 3D中的其他空间均使用左手坐标系。

1.2.2节提到的顶点数据的创建方式,用于创建包含顶点数据的多边形网格的坐标系称为模型坐标系(model coordinate),坐标系所对应的空间称为模型空间(model space[5])。在顶点处理阶段,顶点数据将会贯穿多个空间,直至到达裁剪空间(clip space),如图1-9所示。

▲图1-9 顶点处理的变换操作和对应的空间

1.仿射变换和齐次坐标

图1-9中的世界变换和观察变换由缩放变换(scale transform)、旋转变换(rotation transform)和平移变换(translation transform)这3种变换组合而成。其中,缩放变换和旋转变换称为线性变换(linear transform),线性变换和平移变换统称为仿射变换(affine transform)。投影变换所用到的变换则称为射影变换。

三维的缩放变换可以用一个3×3矩阵Mscale描述:

(1-3)

式中,scalex、scaley和scalez表示沿着xyz轴方向上的缩放系数。如果全部缩放系数都相等,那么该缩放操作称为均匀缩放操作;否则,称为非均匀缩放操作。

如果是均匀缩放操作,那么Unity 3D会定义一个名为UNITY_ASSUME_UNIFORM_SCALING的着色器多样体(shader variant,3.4节会详述此概念,目前可以将其视为一个“宏”)。着色器代码将会根据此多样体是否定义了执行不同的操作。如果UNITY_ASSUME_UNIFORM_ SCALING未被启用,当把顶点从模型空间变换到世界空间中,或者从世界空间变换到观察空间中时,需要对顶点的法线做一个操作,使得它能正确地变换。4.2.4节会详述此问题。

Unity 3D中使用列向量和列矩阵描述顶点信息,所以可以把顶点的坐标值右乘缩放矩阵实现缩放操作,如下:

(1-4)

式(1-4)表示把位置点(由于排版的原因,本书将会在正文中用“行向量的转置”的方法描述一个列向量)进行缩放操作,变换得到新坐标值

要定义一个三维旋转操作,需要定义对应的旋转轴。当某向量分别绕坐标系的xyz轴旋转角度时,分别有以下旋转矩阵MrxMryMrz

(1-5)

如果要对一个位置点进行平移操作,可以让位置点加上一个描述沿着每个坐标值移动多少距离的向量 ,如下所示。

(1-6)

与缩放变换和旋转变换不同,上述的平移变换是一个加法操作。事实上,用右乘一个三阶矩阵的方法去对一个三维向量进行平移变换是不可能实现的,因为平移变换不是一个线性操作。要解决这个问题,需要使用齐次坐标,把三维向量扩展成一个四维齐次坐标,然后把平移向量扩展成以下4阶矩阵的形式。

(1-7)

旋转矩阵和缩放矩阵也可以通过增加第4列与第4行的方式把矩阵四阶化,这样也可以对齐次坐标进行变换,式(1-4)可以改写为

(1-8)

如果变换矩阵是仿射矩阵,则矩阵的第4行是[0 0 0 1];如果是射影变换,如后面将要介绍的投影变换,则矩阵的第4行不是[0 0 0 1]。对于顶点的齐次向量,第4项w依据不同的使用场合,有各种不同的取值。齐次向量时对应于笛卡儿坐标,且该齐次向量表示一个位置点;如果w=0,则表示方向。如果对一个方向向量进行平移,实际上是不会产生任何作用的,如在式(1-7)中,向量代入w=0,乘以平移矩阵后得到的结果仍然是

2.世界矩阵及其推导过程

当包含顶点数据的模型建模完成后,所有顶点隶属于模型空间且固定不动。某一模型的模型空间和其他模型的模型空间没有任何的关联关系。在渲染流水线中,首先是要把分属在不同模型中的所有顶点整合到单一空间中。该单一空间就是世界空间。

假设有一个球体模型和一个圆柱模型,把它们从自身的模型空间中变换到世界空间,如图1-10所示。

▲图1-10 将球体模型和圆柱模型从自身的模型空间中变换到世界空间

针对球体模型的世界变换仅为缩放操作,假设缩放系数为2,则球体模型的世界变换矩阵Msphere

(1-9)

在图1-10中,球体模型的北极点在其模型空间中的坐标是[0 1 0]T,将其齐次化为[0 1 0 1]T,利用世界变换矩阵Msphere将其变换到世界空间中的[0 2 0 1]T处,如下所示。

(1-10)

圆柱模型的世界变换操作包含绕x轴旋转-90°的旋转操作,以及旋转后沿着x轴平移5个单位的平移操作。

定义旋转角度的正负规则:让旋转轴朝向观察者,如果此时的旋转方向为逆时针方向,则表示旋转的角度为负值;若为顺时针方向,则为正值。

根据式(1-5)和式(1-7)可得旋转矩阵Mrx和平移矩阵Mt,如下所示。

(1-11)

经过旋转操作后,基于模型空间的圆柱模型顶面圆心的齐次化坐标[0 2 0 1]T变换到了[0 0 −2 1]T,如下所示。

(1-12)

旋转后再做一个平移操作,可以得到最后的圆柱模型顶面圆心的齐次化坐标为[5 0 −2 1]T,如下所示。

(1-13)

可见,如果把顶点的三维坐标齐次化成四维齐次坐标,那么针对此顶点所有的平移、旋转、缩放变换(仿射变换)可以通过矩阵连乘的方式变换。由于Unity 3D的顶点坐标是采用列向量的方式描述,因此对应的矩阵连乘方式是右乘,即坐标列向量写在公式的最右边,各变换矩阵按变换的先后顺序依次从右往左写。

3.表面法线的变换

上文提到了缩放矩阵有非均匀缩放操作和均匀缩放操作两种。如果某三角形网格的变换矩阵为M,即网格上的所有顶点也将使用M进行变换。当M是旋转变换矩阵、平移变换矩阵和均匀缩放矩阵中的一种或者它们的组合时,顶点的法线也可以直接通过乘以M从模型空间变换到世界空间;当M为旋转变换矩阵、平移变换矩阵和非均匀缩放矩阵中的一种或者它们的组合时,则要把顶点的法线从模型空间变换到世界空间,该变换矩阵就必须为M的逆转置矩阵,即(M−1)T

4.Unity 3D中的模型空间坐标系和世界空间坐标系

Unity 3D在各平台上,顶点的模型坐标系统一使用左手坐标系。因此,从3ds Max、Maya等建模工具导出顶点数据时,无论原始坐标系是什么,都要准确地将其转换到左手坐标系。在顶点缓冲区中,把顶点坐标定义为w分量为1的四维齐次坐标。因为w分量为1,所以等同于三维的笛卡儿坐标。令在模型空间的顶点坐标vInModelSpace为(x, y, z, 1),变换到世界空间中的坐标vInWorldSpace为(wx, wy, wz, 1)。通过使用unity_ObjectToWorld内置变量(参见4.1.1节),可以把顶点从模型空间变换到世界空间,代码如下:

float4 vInWorld = mul(unity_ObjectToWorld,vInModel);

对所有的顶点进行世界变换操作完毕后,可以在世界空间内定义摄像机(camera)。当给定摄像机的状态后,则观察空间[6](view space)也得以确立,且世界空间中的顶点也随之变换到观察空间中。

1.观察空间

通常摄像机需要通过3个参数定义,即Eye、LookAt和Up。Eye指摄像机在世界空间中位置的坐标;LookAt指世界坐标中摄像机所观察位置的坐标;Up则指在世界空间中,近似于(注意,并不是等于)摄像机朝上的方向向量,通常定义为世界坐标系的y轴。构造观察空间的方法和步骤如图1-11所示。给定Eye、LookAt和Up后,即可定义观察空间。观察空间的原点位于Eye处,由3个向量{u,v,n}(对应于x、y、z坐标轴)构成。在观察空间中,摄像机位于原点处且指向−n,即摄像机的观察方向(也称朝前方向,forward)为−n

▲图1-11 构造观察空间的方法和步骤

2.观察矩阵及其推导过程

从图1-11可以看出,在定义u向量时,采用右手法则确定Upn的叉乘,即u方向。最终向量uvn是相互垂直的。如果把这3个向量分别视为观察空间xyz轴,则观察空间的坐标系是一个右手坐标系,Unity 3D中定义的观察空间也是右手坐标系。假设图1-11中的LookAt点和Eye点在世界空间的坐标值是(0,2,10)与(0,3,20),那么在观察空间中,点LookAt则位于轴上,在轴、轴上的值为0,且LookAt点到Eye点的距离为。因此,在观察坐标系下,点LookAt的坐标值为

世界空间中的所有顶点,如果按点LookAt的方式重新定义在观察空间中,则该重定义操作便可称为观察变换(view transform)。考察观察变换用到的变换矩阵,可以把变换操作分解为平移和旋转两步。从理论上来说,把模型从世界空间变换到观察空间,应是在给定观察坐标系的前提下,保持观察坐标系不动;然后计算出模型的顶点相对于观察坐标系3个坐标轴平移了多少位移,旋转了多少度;最后套用式(1-5)和式(1-7)得出平移矩阵和旋转矩阵,最终组合成观察变换矩阵。但这样计算平移量比较简便,计算旋转度就会麻烦。因此,可以换一个思路,让模型和坐标系“锚定”,然后通过平移旋转观察坐标系,使之最终与世界坐标系重合。如图1-12所示,当观察坐标系平移旋转时,与之“锚定”的模型顶点也随之平移旋转。最终世界空间与观察空间重合时,模型顶点变换后得到的世界坐标值,实质上就是它在观察坐标系下的值。

▲图1-12 观察变换矩阵的推导和分解步骤

以圆柱顶面上的点LookAt为例,首先把点LookAt和Eye用一个刚性的连线“锚定”,然后把观察坐标系从Eye处移到世界坐标系原点O处。平移向量为O-Eye。写成平移矩阵Mt,如下所示。

(1-14)

如图1-12中的平移变换步骤,移动观察坐标系时,与观察坐标系“锚定”的点LookAt也随之平移。代入前面给定点Eye和LookAt在世界坐标系下的值,得到平移后的点值LookAttranslation为(0,− 1,−10,1),如下所示。

(1-15)

完成平移后,需要在保持观察坐标系的3条坐标轴uvn始终相互垂直的前提下,旋转它们并使其朝向和世界坐标系的3条坐标轴xyz完全重合。世界坐标系的3条坐标轴的方向向量分别为[1 0 0 0 ]T、[0 1 0 0 ]T和[0 0 1 0 ]T。也就是说,要构造一个矩阵,使得坐标轴uvn的方向向量值右乘矩阵Mr时,分别等于xyz的方向向量值。矩阵Mr

(1-16)

式(1-16)各列中的分量即为uvn轴的方向向量值。代入u轴的方向向量值[ux uy uz 0]T,计算可得

(1-17)

式(1-17)中的结果向量[uxuz+uyuy+uzuz vxux+vyuy+vzuz nzux+nyuy+nzuz]T的3个分量值实质上就是向量u分别与向量unv的点积值,因为unv三向量相互垂直且为单位向量。向量点积公式为

(1-18)

式中,θab的夹角。

如图1-12中的旋转变换步骤,可得结果向量实质上就是[1 0 0 0]T。同理可得,MrvMrn的值分别为[0 1 0 0]T和[0 0 1 0]T。因此,到了这一步,变换矩阵MtempView

(1-19)

注意,到这一步变换还没有结束,因为采用的世界坐标系和观察坐标系都是按照Unity 3D的实现,分别是左手坐标系和右手坐标系,并且这两种坐标系的x轴和y轴是重合的,z轴则相反。因此,还必须让MtempView右乘一个矩阵Mz并对z轴取反,才能得到最终变换矩阵Mview,如下所示。

(1-20)

观察变换的矩阵推导和分解步骤,如图1-12所示。

顶点在世界坐标系下的位置坐标值右乘Mview,便可以变换到观察空间中。

3.Unity 3D中的观察空间坐标系

在各平台下,Unity 3D的观察坐标系统一使用右手坐标系。令在世界空间中顶点的坐标为vInWorldSpace=(wxwywz,1),观察空间中的顶点坐标vInViewSpace=(vxvyvz,1)。通过使用unity_MatrixV内置变量(参见4.1.1节),可以把顶点从基于左手坐标系的世界空间变换到基于右手坐标的观察空间,代码如下。

float4 vInViewSpace = mul(unity_MatrixV,vInWorldSpace);

通过观察变换可以将模型顶点从世界空间变换到观察空间。1.2.4节使用uvn表示观察坐标系的3条坐标轴。因为变换之后所有数据已经不需要在世界空间中进行考察,所以接下来将依照习惯使用xyz表示观察坐标系的3条坐标轴。

1.视截体

通常摄像机的取景范围(或者称为视野范围)是有限的。在渲染流水线中,通常使用视截体(view frustum[7])去框定这一取景范围。视截体是一个正棱台(regular prismoid),其两个底面平行且宽高比例相等。使用4个参数加以定义,即fovY、aspect、nf,如图1-13所示。fovY定义了沿垂直方向的视野区域(field of view,FOV)。aspect表示视截体底面的宽度与高度。如果把视截体正棱台的4个侧边向较小底面一端延伸,正棱台将延展成为正棱锥(regular pyramid)。正棱锥的顶点就是4个侧边的汇聚点,摄像机就位于此点。n表示近截面,即靠近摄像机位置点的视截体底面,显然在观察坐标系下近截面所处的平面是z=−nf表示远截面,即离摄像机位置点较远的视截体底面,在观察坐标系下远截面所处的平面是z=−f。在图1-13中,圆柱体和立方体都处于视截体之外,因而不可见。必须指出的是,视截体定义的近截面和远截面是不符合人类视觉原理的,就好比“近在眼前”的物体,虽然比近截面离摄像机的距离还要小,但摄像机不可能拍摄不到。之所以如此定义,主要是为了提高计算效率。

▲图1-13 视截体的定义

图1-13中的球体和圆柱体,这一类在视截体外的模型不会对最终渲染出来的图像效果产生任何贡献。因此,如果在把顶点数据投递给渲染流水线之前,把这些无贡献的模型顶点丢弃,则会大幅地提升性能。该操作称为视截体剔除操作(view frustum culling),通常由软件完成,成熟的 3D 渲染引擎都有实现。典型的视截体剔除操作的流程:在运行之前的预处理阶段,计算好多边形网格的包围体(bounding volumn),可以是包围盒(bounding box)或者包围球(bounding sphere),随后CPU(central processing unit,中央处理器)执行多边形网格的包围体与视截体的相交测试。如果多边形网格的包围体完全在视截体之外,则丢弃;完全或部分在视截体的,则投递进流水线。

图1-13中仅有立方体通过了视截体剔除操作被投递进流水线。但立方体并不是完全在视截体内,有一部分与远截面相交并且在其之后。因此,多边形应根据视截体的边界面进行裁剪处理,仅显示视截体之内的那一部分多边形。但必须要注意的是,裁剪操作并不是在观察空间,而是在光栅化阶段中的裁剪空间中,由硬件完成。因此,在顶点处理阶段,投影变换可视为最后一步操作。

即使不进行视截体剔除操作,把所有的模型顶点都投递到渲染流水线中,在光栅化处理阶段也还是会把不该显示的模型给裁剪掉。但因为光栅化阶段是在顶点处理阶段之后,如果不剔除,对最终渲染图像无贡献的模型也会经过顶点着色器执行处理,这种“劳而无功”的操作是很低效的方法。

2.投影矩阵及其推导过程

通过投影变换(projection transform)可以将正棱台状的视截体转换为一个轴对齐的(axis-aligned)立方体。该轴对齐立方体所框定的空间就是裁剪空间。更为准确地说,这种投影变换称为透视投影(perspective projection)。如图1-14所示,立方体的xy的取值范围都是[−1,1],z的取值范围是[−1,0][8]。“投影”一词容易让读者联想起投影机投射到银幕上的图像,但在渲染流水线中,透视投影变换并不生成二维图像,其只使场景中的三维物体发生变形。

▲图1-14 投影变换

图1-14(a)把一个视截体变为正方形,视截体内的物体也随之变形。图1-14(b)为视截体的横截面,视截体可以视为投影线的相交结果。投影线相交于摄像机原点处,通常把该原点称为投影中心点(center of projection)。假设投影平面位于视截体和投影中心点之间,投影线将构成投影平面内的场景图像。

在图1-14(b)中,在左边视截体中定义了两个线段l1l2。在三维空间中l1长于l2,但在投影平面内,这两线段的投影长度是相等的,这表现了透视投影的“近大远小”特性;在右边的轴对齐立方体中,投影变换使得投影线变为相互平行,这种相互平行的投影线称为通用投影线。从图1-14可见,线段l1l2经投影变换后,各自对应的线段l1'l2' 的长度是相等的。

令视截体中有一个顶点p,其坐标是(xyz),p经投影变换转换为(x'y'z')的p'。因为投影变换限定了x'y' 的取值范围都是[−1,1],z' 的取值范围是[−1,0],所以可计算得到x' y'z' 的值。

首先计算y' 。图1-15显示了视截体的横切面。pp' 分别表示(yz)和(y'z'),图1-15定义了一个投影平面,此平面的定义公式为

y' 的取值限制在[−1,1]中,可以通过相似三角形计算获得,如图1-15所示。

▲图1-15 计算投影矩阵1

在图1-15中,△pAO和△p' A' O' 都是直角三角形且相似。根据相似三角形的性质公式:

(1-21)

可得

(1-22)

如图1-16所示,D是坐标系原点到投影平面的距离,即z'。同理计算x'x' 的取值范围是[−1,1],令fovX为水平方向的视野角,有:

▲图1-16 计算投影矩阵2

(1-23)

在式(1-23)中,fovX未知,但可以根据fovY和aspect得到fovX,进而推导出x',aspect为视截体的截面的宽高比。因为视截体是一个正棱台,所以也等于图1-16中的投影平面的WH的比值。同时,根据三角函数,有以下等式:

(1-24)

整理式(1-24),可得

(1-25)

结合式(1-23)和式(1-25),可以得到x'

(1-26)

到了这一步,代入x'y' 的值,点p' 的坐标值(x'y'z' )齐次化之后可以写成

(1-27)

式(1-27)中的z' 依然未知。根据齐次坐标的性质,如果w≠0,则(xyz,1)等价于(wxwywzw)。现在把p' 的坐标值乘以−z,得

(1-28)

观察式(1-28)中的坐标值,实质上p' 的坐标向量值可以由p的坐标向量值[x y z 1]T右乘一个矩阵Mprojection得到,即

(1-29)

如果把矩阵第3行的4个数找到,即可找到投影矩阵,即要求解以下方程式中系数m1m2m3m4的值。

(1-30)

要解得这4个系数值,首先看图1-17。在视截体中,任意一个平行于投影平面的平面,其上面的任意一个顶点投影到投影平面上,其z都是相等的,即待投影点的投影z和其x、y无关。

前面提到视截体是正棱台,且投影平面平行于棱台的两底面,即远截面和近截面,那么从图1-17中可以看出,在视截体中任意选取一个平行于底面的平面,该平面上任意一点在投影平面上的投影点的z,与该点在平面上的xy是无关的。因此,式(1-30)中的m1m2应该为0才能使xy的具体取值不影响投影点的z' ,即

m3z+m4=−zz'

(1-31)

又由于投影后z' 的取值范围是[−1,0],显然远截面的z(即−f)投影后的z' 为−1,近截面的z(即−n)投影后z' 为0,因此代入式(1-31)可得方程组。

▲图1-17 视截体中平面的投影

(1-32)

解式(1-32),即可得到。代入矩阵MtempProjection的第3行,可得

(1-33)

图1-14中所定义的裁剪空间(轴对齐立方体)是基于右手坐标系的。在顶点处理阶段,投影变换可以视为最后一步操作,随后的顶点将进入硬件光栅化阶段。在光栅化阶段,裁剪空间采用左手坐标系,Unity 3D也遵循该规则,因此需要把右手坐标系的裁剪空间变换到左手坐标系。左手坐标系的裁剪空间的x、y轴的朝向与右手坐标系相同,即z轴相反。因此,要把MtempProjection右乘一个倒转z轴的矩阵才能得到最终的投影矩阵Mprojection,如下:

(1-34)

再回头看式(1-31)。前面假定了轴对齐立方体的z的取值范围是[−1,0]。现在把该轴对齐立方体拉大,使其取值范围变成[−1,1]。这时远截面的z(即−f )投影后的z' 为−1,近截面的z(即−n)投影后z' 为1。因此,代入式(1-31)可得新方程组:

(1-35)

解式(1-35),可得

代入两值到矩阵MtempProjection的第3行后,再根据式(1-34),可得到最终的投影矩阵MProjection

(1-36)

实质上,式(1-34)和式(1-36)的Mprojection分别是Unity 3D在Direct3D平台和OpenGL平台上的投影矩阵值。根据式(1-31)中z' 的不同取值范围,投影矩阵的第3行在不同平台上有不同的值。在Direct3D平台,z' 的取值范围是[−1,0],而OpenGL的是[1,−1]。当把坐标系从右手坐标系变换到左手坐标系时,在Direct3D平台,z' 的取值范围是[0,1],而OpenGL的是[−1,1]。

3.裁剪空间中未做透视除法的顶点坐标的z分量

必须注意的是,用顶点坐标右乘投影矩阵,从观察空间变换到裁剪空间时,得到的齐次坐标值的w分量不为1。又根据式(1-28),把右手坐标系转成左手坐标系时有p' =zz' 可以得知:在Direct3D平台上,p' =zz' 中的z' 的取值范围是[0,1],p' =zz' 中的z的取值范围是[nf],所得到未除以w分量的齐次坐标值z分量的取值范围是[0,1]。在OpenGL平台上,z' 的取值范围是[−1,1],p' =zz' 中的z' 的取值范围是[nf],得到未除以w分量的齐次坐标值z分量的取值范围是[−nf]。把w分量不为1的裁剪空间坐标值除以w,使得w分量等于1,四维齐次坐标降维成三维笛卡儿坐标的操作,称为透视除法。透视除法是在光栅化阶段进行的,参见1.3.2节。

4.Unity 3D的裁剪空间坐标系

在各平台下,Unity 3D的裁剪空间坐标系统一使用左手坐标系,并且在未经透视除法之前,是一个不等价于三维笛卡儿坐标的四维齐次坐标系。令在基于右手坐标系的观察空间中顶点坐标为vInViewSpace=(vxvyvz,1),经投影变换后,变换到基于左手坐标系的裁剪空间的顶点坐标为vInClipSpace=(cxcyczcw)。由于投影变换不是仿射变换,因此顶点在裁剪空间中的齐次坐标vInClipSpace的w分量不为1。调用UnityViewToClipPos函数(参见4.2.4节),可以把顶点从观察空间变换到裁剪空间,代码如下:

float4 vInClipSpace = UnityViewToClipPos(
float3 (vInViewPos.x,vInViewPos.y,vInViewPos.z));

至此,顶点的变换处理过程已经完成,在现代渲染流水线实作中,这些变换操作通常在顶点着色器中完成。因此,Unity 3D引擎在其内置着色器中预先定义了很多变换用的矩阵。这些矩阵在运行时由引擎填充好,并通过CPU传递给GPU着色器,在第4章中会详细介绍这些矩阵。在阅读第4章时,若对代码背后蕴含的数学原理感到困惑,也可以回过头来查阅本章。

经顶点处理阶段完成后的顶点,将进入由硬件执行的光栅化阶段。在该阶段,硬件首先依据传递进来的描述顶点拓扑信息的顶点输入流(vertices input stream),把顶点组装为图元(primitive)。图元的种类有多种,如线段和三角形等。本节只以三角形为例讨论。同时,各图元还将被进一步处理,确定其在二维屏幕上的绘制形式,最终光栅化(rasterized)为一系列片元的集合。传递过来的顶点数据,如位置、颜色、法线等,都将进行插值计算,并且赋给光栅化得到的片元。该处理阶段统称为图元组装(primitives assembly)和光栅化操作,有些渲染流水线的实现则简称此阶段为光栅器。

光栅化阶段包括以下几个子过程:裁剪操作、透视除法(perspective division)、背面剔除(back face culling)[9]操作、视口变换和扫描转换(scan coversion)。目前的主流渲染流水线中,光栅化阶段在硬件电路中实现,不支持可编程操作。

裁剪操作是在裁剪空间中,对图元(本节以三角形为例)进行剪切操作。裁剪操作用到的算法在硬件中实现,用户无法改变它。和视截体剔除操作类似:如果裁剪空间中的三角形完全在裁剪空间中,则传递给下一步骤;如果在裁剪空间外,则丢弃之;如果和裁剪空间相交,则进行裁剪操作。

图1-18表示通过使用式(1-34)中的投影矩阵Mprojection,把视截体变换为表示裁剪空间的立方体视见体。和仿射变换的矩阵不同,Mprojection矩阵的第4行是,而不是。因此,一个w分量为1的四维齐次坐标经投影矩阵变换后,得到的四维齐次坐标的w分量为−z,而且−z肯定为正数。

为说明投影变换操作,令、aspect=1、n=1、f=2。把图1-18中P1、P2Q1、Q2坐标齐次化,得到P1=(0,1,−1,1)、P2=(0,1,−2,1)、Q1=(0,0,−1,1)、Q2=(0,0,−2,1)。

▲图1-18 投影变换将生成裁剪空间内的顶点,各顶点除以自身坐标中的w 分量后将齐次坐标转换到笛卡儿坐标

另外,根据式(1-34),把fovY、aspect、n、f代入投影矩阵Mprojection,得

(1-37)

P1、P2Q1、Q2分别右乘Mprojection后,得到P1' =(0,1,0,1)、P2' =(0,1,2,2)、Q1' =(0,0,0,1)、Q2' =(0,0,2,2)。仔细一一对应观察P1P1'P2P2'Q1Q1'Q2Q2'。可知经过投影变换后,在两个空间中对应的顶点,其x、y没有变化,图中原来等长的l1l2还是等长,没有呈现出“近大远小”的透视效果;z有变化,但变换后的z超出了[0,1]范围。为了把该齐次坐标变成笛卡儿坐标,需要把P1'、P2'Q1'、Q2' 各自除以它们的w分量,这一步就是透视除法。经过透视除法操作之后得到P1' =(0,1,0,1)、P2' =(0,0.5,1,1)、Q1' =(0,0,0,1)、Q2' =(0,0,1,1)。这时xy发生了变化,图中原来等长的l1l2变成了,产生了“近大远小”的效果,并且z也限定在了[0,1]范围内。在Direct3D平台上,经过透视除法后,把裁剪空间中齐次坐标值z分量的取值范围,从原来在[0, far]限制在[0,1]范围内;在OpenGL平台上,把裁剪空间中齐次坐标值z分量的取值范围,从原来的[−n, f ]限制在[−1,1]范围内。两种平台下裁剪空间中齐次坐标值xy则限制在[−1,1]内。这些经过透视除法的坐标称为标准化设备坐标[10](normalized device coordinates,NDC)。

在渲染流水线中,剔除操作在不同的环节中有着不同的具体实现。总之,就是把摄像机不可见的内容排除掉。前面中提到的视截体剔除操作消除了部分在视截体之外的模型。在裁剪操作时把一些在裁剪空间之间的多边形给剪掉。除此之外,还有背面剔除操作,即把背向于摄像机观察方向的多边形消除掉。背向于摄像机观察方向的多边形称为背面(back face),正对摄像机观察方向的多边形称为正面(front face)。

图1-19为在观察空间中做背面剔除操作的原理。图1-19中,T2表示背面三角形,T1表示正面三角形。判断一个三角形T是正面还是背面,可以通过计算三角形法线向量n与摄像机位置到当前三角形法线连线向量c之间的点积,然后根据点积值与0的大小关系加以判断。点积计算式是,其中定义为向量nc之间的夹角。如果nc之间的夹角为锐角,则为正值,表示当前三角形为背面;如果nc之间的夹角为钝角,则为负值,表示当前三角形为正面;如果恰好为0,表示nc相互垂直,当前三角形为侧向面。

▲图1-19 在观察空间中做背面剔除操作的原理

该算法肯定可以准确地判断三角形是正面还是背面。然而在大部分渲染流水线的实现中,背面剔除操作并不是在观察空间中完成的。因为该算法里,要判断每一个三角形的正面背面,必须计算出摄像机到该三角形法线的连线向量。每次渲染有成千上万个三角形,这个计算操作是非常耗时和低效的。因此,优化手段就是不计算这些连线向量,而是想办法把连线向量常数化。通过投影矩阵把顶点变换到裁剪空间之后,1.2.5节提到的通用投影线便可以当做连线向量使用,如图1-14所示。但必须注意的是,图1-14中的视见立方体的z轴朝向和真正的裁剪空间的z轴朝向是相反的。因此,裁剪空间中的连线向量应该是[0 0 1]。

前面提到,要定义三角形的法线朝向,需根据它的顶点排列顺序,用左(右)手法则决定。假设图1-19中的三角形T1投影到裁剪空间的xy平面z=0上,得到三角形t1。顶点p1p2p3依次对应投影顶点,则图1-19中T1的顶点按的顺序排列。因此,t1的顶点也对应地按的顺序排列。注意,原本在观察空间中的三角形T1的顶点排列顺序是逆时针顺序,此时在裁剪空间中变成了顺时针顺序。如图1-20所示,可见观察空间中的三角形T1对应于裁剪空间中的三角形t1,两者的正面和背面是刚好相反的。

▲图1-20 在裁剪空间中做背面剔除操作

令方向向量,按照左手法则,计算得到的向量ab的叉积就是该三角形的法线向量,如下:

(1-38)

代入到式(1-38)中,可以得到

(1-39)

和连线向量c=(0,0,1)做点积操作,根据点积值的正负关系就可以得到三角形t1是正面还是背面。的值应为

(1-40)

代入图1-20中的坐标数字值。可解得的值为−15。因此,法线与连线向量的夹角为钝角。也就是说,三角形t1的正面对着摄像机,对应的三角形T1则背面对着摄像机。因此,三角形T1应该被剔除,符合图1-19。

Unity 3D ShaderLab语言提供了控制背面剔除操作的指令,代码如下:

//这些代码要放在一个pass块内
Cull Back   //表示不渲染背向摄像机的多边形,这也是默认设置
Cull Front  //表示不渲染正向摄像机的多边形
Cull Off    //表示正向和背向摄像机的多边形都予以渲染

计算机屏幕窗口与其自身的屏幕空间(screen space)相关联,视口(viewport)可以视为当前场景所投影的矩形区域。视口定义于当前屏幕空间中,并且不一定非得为全部屏幕,可以为当前进程窗口的部分区域。

在渲染流水线中,屏幕空间和视口都以3D模式定义。如图1-21所示,屏幕空间的原点位于窗口左上角处,x轴朝右,y轴朝下,z轴指向屏幕内侧,通过给定minX、minY、Width、Heigth及深度范围[minX,maxZ]定义屏幕空间。由图1-21可知,屏幕空间采用右手坐标系,视口的深度范围值[minZ, maxZ]定义了投影场景的z的范围。z将会应用于深度缓冲区中。视口的宽高比为Width/Height,并且此值应该要和视截体的宽高比相等。

▲图1-21 定义屏幕空间

通过视口变换,可以把表示裁剪空间的视见立方体转换为三维视口。裁剪空间采用左手坐标系,屏幕空间则采用右手坐标系,且两者xz轴同向,y轴相反。因此,从裁剪空间中变换到屏幕空间中首先需要进行逆置y轴操作,逆置y轴的矩阵MinvertY

(1-41)

逆置y轴之后,要把原来在裁剪空间中xy轴方向上大小范围是[−1,1],z轴方向上大小范围是[−1,1]或[0,1]的顶点,缩放到x轴方向上宽度是Width,y轴方向上高度是Height,z轴方向上深度是maxZ−minZ,该缩放操作的矩阵Mscale

(1-42)

完成缩放操作后,再做一次平移操作,使坐标系的原点从中间移到屏幕左上角。该平移矩阵Mtranslation

(1-43)

连乘上述的3个矩阵便可得到最终的视口变换矩阵Mviewport,如下:

经过视口变换之后,组成图元的顶点即完全变换到二维屏幕上。接下来进行扫描转换,把顶点插值成片元。

视口转换将各个图元都转换到屏幕空间中,接下来进行扫描转换。扫描转换过程是光栅化阶段的最后一步,在该过程中定义了图元覆盖的屏幕空间像素位置,对各顶点属性进行插值计算,进而定义了各个像素点对应的片元属性。顶点属性随着应用的实际需求,除了位置坐标点值之外,可能还会包括法线、切线、纹理映射坐标、顶点颜色和深度值等。扫描转换都会对这些信息进行插值。在实时三维渲染流水线中,图形硬件供应商针对扫描转换采取各种优化算法,加快运算速度,并且这些算法都是用硬件电路实现的。对于用户而言,扫描转换这一步也是不可编程的,因此本节不作详细叙述。

当全部片元属性都完成扫描转换之后,即完成光栅化阶段,渲染流水线的下一个步骤便是片元处理阶段。在片元处理阶段中,每一次的处理过程只处理一个片元,但在现代的显卡中,可以多个处理过程并行进行。

与顶点类似,经过光栅化阶段生成的片元通常也包含深度值、法线向量、RGB颜色值及一组纹理映射坐标。片元着色器代码将使用这些信息确定各个片元的颜色值(注意,不是最终的像素颜色值)。片元处理阶段对于最终的图像渲染效果起着关键作用,尤其随着现代GPU的性能提升,原来一些在顶点着色器进行光照计算的操作都逐渐在片元着色器中实现,以获得更高的渲染质量。

在各种各样的纹理中,二维图像纹理是最直观、最简单的一种,即通过粘贴或者环绕方式把图像覆盖在待渲染物体的表面。如图1-22所示,把一个方格图片粘贴到棱锥的一个表面上。

▲图1-22 图像纹理

1.纹理映射坐标

纹理中最小的一个单元通常称为纹素(texel),此概念用来有效区分颜色缓冲区中的像素图素(image)。纹理通常可以描述为一个二维的纹素阵列。如图1-23所示,每一个纹素均包含了一个唯一地址,即二维纹素阵列的横纵索引。

▲图1-23 采用标准化纹理坐标(u,v)访问不同纹理并且获取到不同的纹素

纹理操作需要确定纹理和待渲染模型表面之间的对应关系。也就是说,对于模型表面的顶点,应该在纹理空间(texture space)中获取位置,然后将此处的纹素应用于顶点上。这些映射关系通常在多边形建模阶段就可以利用专门软件构建完毕。

在建模阶段给多边形网格顶点赋予纹素地址时,会产生某一特定网格和某一特定纹理相耦合的问题,即一张纹理的规格只能用在一个网格上。为了解决该问题,通常会把纹理坐标标准化,即把整数纹素索引规格化到[0, 1]范围内。这些在[0, 1]范围内的坐标通常用(u, v)表示,即纹理映射坐标。如图1-23所示,纹理坐标映射为纹理1中的纹素索引(2, 2),映射为纹理2中的纹素索引(3, 3)。

不同平台上的纹理映射坐标采用不同的纹理空间坐标系。它们的坐标系是存在差异的。如图1-24所示,在OpenGL平台上,纹理坐标系的原点在纹理图的左下角,u轴水平向右,v轴垂直向上;而在Direct3D平台上,纹理坐标系的原点在纹理图的左上角,u轴水平向右,v轴垂直向下。如果纹理多边形网格从Direct3D平台切换到OpenGL平台,则各个顶点的纹理坐标应从(u, v)调整为(u, 1−v)。

▲图1-24 Direct3D和OpenGL的纹理坐标系的差异

2.纹理映射坐标与纹素阵列索引

在光栅化阶段,由硬件对屏幕空间内的顶点纹理映射坐标进行插值计算,该阶段对用户是透明而且是不可控的。本节对纹理映射坐标进行插值计算并得到对应的纹素阵列索引算法。以图1-25为例,此图演示了Direct3D 9平台下的顶点纹理映射坐标的插值效果。三角形顶点包含以下纹理坐标(0, 0)和(1, 0),利用和扫描转换阶段中插值其他顶点属性的相同算法,完毕后各个片元将被赋值为经过插值的坐标。

▲图1-25 在光栅化阶段插值生成的片元纹理映射坐标

当给定了片元的纹理映射坐标后,对应的纹理阵列索引值将通过图形API(application programming interface,应用程序编程接口)在运行期中自动计算得到。Direct3D 9平台下,根据纹理映射坐标(u, v)和纹理的高宽(sizeX, sizeY)计算纹理阵列索引值(tx, ty),如下:

(1-44)

注意,在式(1-44)中,计算纹理阵列索引(tx, ty)时需要减去0.5,这是为了解决Direct3D 9等平台中存在的半像素偏移(half pixel offset)问题[11]。Unity 3D引擎在5.5.0f3版本之前,定义了一个UNITY_HALF_TEXEL_OFFSET宏去对应解决在Direct3D 9、XBox360、PSP2平台上的这个问题,如在5.5.0f3版本的内置着色器函数ComputeScreenPos函数和UnityPixelSnap函数中就使用了该宏。在5.5.0f3版本之后,该半像素偏移操作已经由引擎在后台处理了[12],因此,不需要在着色器代码中去费神考虑这个问题。

片元着色器在执行完毕之后,返回的片元携带了颜色值透明值的RGBA信息,以及其深度值信息。其中RGBA中的A即指Alpha值,即透明值。当流水线启用了Alpha测试(Alpha Test)或者Alpha混合(Alpha Blend),以及启用了对深度缓冲区进行深度测试(Z Test)操作时,渲染流水线将会对这个返回的片元的颜色值透明值和深度值与当前颜色缓冲区中的像素进行比较或者整合操作。该阶段就是输出合并阶段。目前主流的渲染流水线中,不支持对输出合并阶段的可编程操作,但是会提供一系列的Alpha测试指令、Alpha混合指令和深度测试指令来对片元透明值和深度值进行比较整合操作。1.4.4节会介绍Unity 3D对这一系列的指令的使用方式。

图1-26显示了视口中的两个三角形,视口中深浅色的两个三角形竞争同一个像素点p的颜色,最终因为深色三角形靠近摄像机,点p颜色为深色。类似的决策机制可以通过与深度缓冲区(也称为Z缓冲区)中的深度值进行比较来确定。基于深度缓冲区的相关算法称为深度缓冲区机制(或者称为Z缓冲区机制)。

▲图1-26 点p的颜色

深度缓冲区和颜色缓冲区的分辨率是相同的,并且记录了存储于当前缓冲区中的深度值。当位于(x, y)处的片元从片元程序中返回时,其深度值将与位于深度缓冲区的(x, y)处的深度值进行比较。如果该片元的深度值较小,则它的颜色值、透明值和深度值将分别更新到位于(x, y)处的颜色缓冲区和深度缓冲区中;否则,该片元将视为处于当前可见像素的后方且不可见,因而被渲染流水线丢弃。

图1-27显示了按照先绘制图1-26中的浅色三角形,后绘制深色三角形的顺序,深度缓冲区和颜色缓冲区的按这种顺序的更新方式。在图1-27中,假设深度缓冲区中的深度值的取值范围是[0.0,1.0],深度初始化为最大值即1.0。颜色缓冲区中的颜色值初始化为白色。

▲图1-27 先绘制浅色三角形后绘制深色三角形的过程

为了简化描述,假设两个三角形平行于屏幕空间的xy平面,并且深色三角形和浅色三角形分别位于z轴的0.6和0.7处。图1-27(b)显示了浅色三角形的处理结果,图1-27(c)显示了深色三角形的处理结果。位于(37,57)、(38,56)和(39,56)处的颜色缓冲区的颜色则由浅色变为深色,深度缓冲区中对应的相同坐标处的深度值也随之更新。

图1-28为先绘制深色三角形后绘制浅色三角形的过程。通过观察可知,当渲染完毕之后,深度缓冲区和颜色缓冲区包含与图1-27相同的信息。从理论上来说,只要启用了深度测试机制,图元就可以以任意的先后顺序绘制而最终仍能得到正确的结果,这也是深度测试算法广为流行的原因。但在实际开发时,尤其是要处理半透明物体的绘制时,需要对图元按距离当前摄像机远近进行排序。

▲图1-28 为先绘制深色三角形后绘制浅色三角形的过程

前面所讲的全部待绘制物体的表面都假设是不透明的。因此,当两个表面片元针对某一像素位置进行比较时,一个片元可以完全遮挡住另一个片元。而某些物体的表面可以呈现半透明状态,假设当前片元和颜色缓冲区对应位置的像素相比有着较小的深度值,且该像素和当前片元之间呈现半透明效果。这种情况可以通过在片元颜色值和像素颜色值之间执行颜色混合操作得以实现。该处理过程大多数是采用了片元的Alpha值和像素的Alpha值进行操作,因此名为Alpha混合,但实质上,称其为颜色混合可能更为贴切。

一般情况下,Alpha通道与颜色各分量通道应包含相同的位数。例如,如果红色通道包含8位数据,则Alpha通道也应包含8位数据。因此,颜色值将包含32位的RGBA数据。当采用8位数据时,Alpha通道可表示256种不同级别的透明度值。通常,在编程实践中使用单位化的浮点数取值范围[0.0,1.0],而不是整数型的[0, 255]。其中,最小值0表示完全透明状态,最大值表示完全不透明状态。

执行Alpha混合操作所用到Alpha混合指令有多种,较为常见的指令如下。

(1-45)

式中,Cf为当前片元的颜色;Cp为颜色缓冲区中片元对应的像素点颜色;Af为片元的Alpha值;Ap为像素点的Alpha值;color为混合后的最终颜色;alpha为混合后的最终Alpha值。

从式(1-45)可见,当为0时,则当前片元完全透明不可见,因此最终混合颜色就是像素点颜色;当为1时,则当前片元完全不透明,最终混合颜色就是当前片元颜色。

如图1-29所示,令深色三角形的全部顶点的R、G、B颜色值与透明值为(0,0, 1.0, 0.5),浅色三角形的全部顶点的R、G、B颜色值与透明值为(0.772, 0.878, 0.705,1.0)。光栅化阶段的扫描转换过程将会对R、G、B、A通道执行插值计算。因此,深色三角形的全部片元将被赋值为(0, 0, 1.0, 0.5),浅色三角形的全部片元将被赋值为(0.772, 0.878, 0.705, 1.0)。

▲图1-29 深色三角形各顶点的Alpha属性表明该三角形为半透明状态,浅色三角形为不透明状态

图1-29中假定了渲染顺序为先绘制浅色三角形后绘制深色三角形。深色片元将与浅色片元在颜色缓冲区中的(37, 57)、(38, 56)和(39, 56)处进行Alpha混合操作,如图1-30所示。按照式(1-45),混合后的R、G、B颜色值与透明值为(0.386, 0.439, 0.8525, 0.75)。

▲图1-30 在颜色缓冲区中有3个像素执行了Alpha混合操作

前面提过,理论上深度缓冲算法与图元的绘制顺序无关,但实际上要绘制透明图元时无法以任意顺序进行渲染。当前大部分3D引擎在渲染半透明图元时,需要在全部不透明物体渲染完毕后,以从离当前摄像机最远到最近的顺序先后依次渲染。因此,半透明物体应执行排序操作。

对半透明物体进行排序,最精细准确的就是基于每个半透明物体的图元三角形进行排序。然而三角形的数量十分庞大,而且当前摄像机的观察方向可能每帧都会发生变化,基于三角形排序的方法可能无法实时地执行,因此,实时3D引擎若要对半透明物体进行排序,通常都是以单个物体模型为粒度。

实时渲染中对半透明物体的绘制是一个很大的研究课题,除了基于排序的算法之外,近年来还有人研究出不需对半透明物体进行排序的算法。例如,Nvidia公司研究的interactive order-Independent transparency(OIT)算法就是其中有代表性的一种。本节不对这些算法做深入探讨,感兴趣的读者可以自行阅读相关论文。

1.Alpha混合指令

令当前片元的颜色值为Cf,透明值为Af,颜色混合系数为SrcFactor,透明值混合系数为SrcFactorA;颜色缓冲区中对应的像素点颜色值为Cp,透明值为Ap,颜色混合系数为DstFactor,透明值混合系数为DstFactorA;颜色混合操作符为OpColor,透明值混合操作符为OpAlpha,则混合操作后的结果颜色值Color和透明值Alpha为

(1-46)

混合操作符OpColor和OpAlpha可以是同一个操作符号,也可以不同;片元或像素的颜色操作系数和透明值混合系数也可以是同一个系数,也可以不同。表1-1列出了在Unity 3D ShaderLab语言中常用的混合操作符和混合系数。

表1-1 常用的Alpha混合操作符及混合系数

Alpha混合操作符和混合系数

功能描述

Blend Off

关闭Alpha混合。Unity 3D ShaderLab着色器程序默认是关闭Alpha混合

Blend SrcFactor DstFactor

启用Alpha混合功能,SrcFactor系数用来乘以当前片元RGBA颜色值、透明值,DstFactor用来乘以颜色缓冲区中像素RGBA颜色值、透明值,然后两者相加后赋值到颜色缓冲区

Blend SrcFactor DstFactor, SrcFactorA DstFactorA

功能和Blend SrcFactor DstFactor语句类似。SrcFactor系数用来乘以当前片元RGB颜色值,DstFactor用来乘以颜色缓冲区中像素RGB颜色值,然后两者相加后赋值到颜色缓冲区的RGB通道;SrcFactorA系数用来乘以当前片元的Alpha值,DstFactorA用来乘以颜色缓冲区中像素的Alpha值,然后两者相加后赋值到颜色缓冲区的A通道

BlendOp Op

指定一个操作符,以代替默认的“两者颜色值、透明值相加”操作符

BlendOp OpColor, OpAlpha

功能和BlendOp Op语句类似。指定两个操作符,OpColor代替默认的“两者颜色值相加”操作符,OpAlpha代替默认的“两者透明值相加”操作符

Blend N SrcFactor DstFactor

和Blend SrcFactor DstFactor语句功能相同,用于多渲染目标中某个渲染目标的Alpha混合操作指令的SrcFactor和DstFactor,N是渲染目标的编号,取值范围是0~7

Blend N SrcFactor DstFactor, SrcFactorA DstFactorA

和“Blend N SrcFactor DstFactor, SrcFactorA DstFactorA”语句功能相同,用于多渲染目标中某个渲染目标的Alpha混合操作指令的SrcFactor和DstFactor,以及SrcFactorA和DstFactorA。N是渲染目标的编号,取值范围是0~7

BlendOp N Op

和Blend Op Op语句功能相同,用于指定某个渲染目标的Alpha混合操作符,以代替默认的“两者颜色值、透明值相加”操作符。N是渲染目标的编号,取值范围是0~7

BlendOp N OpColor, OpAlpha

功能和“BlendOp OpColor, OpAlpha”语句功能相同,用于指定某个渲染目标的两个混合操作符。OpColor代替默认的“两者颜色值相加”操作符,OpAlpha代替默认的“两者透明值相加”操作符。N是渲染目标的编号,取值范围是0~7

表1-2列出了常用的颜色值透明值混合系数。

表1-2 常用的颜色值透明值混合系数

Alpha混合系数种类

对应数值

One

1

Zero

0

SrcColor

Cf

SrcAlpha

Af

DstColor

Cp

DstAlpha

Ap

OneMinusSrcColor

1−Cf

OneMinusSrcAlpha

1−Af

OneMinusDstColor

1−Cp

OneMinusDstAlpha

1−Ap

下面的代码演示了如何在着色器代码中声明Alpha混合所用到的颜色值透明值混合操作符和混合操作系数。

// 下面代码默认已经使用过BlendOp Add Add语句声明颜色混合操作符和透明值混合操作符
// 这些代码要放在一个pass块内
Blend SrcAlpha OneMinusSrcAlpha //
Blend One OneMinusSrcAlpha      //,表示片元颜色以预先乘过其Alpha值
Blend One One                   //,表示两颜色直接相加
Blend OneMinusDstColor One      //
Blend DstColor Zero             //,表示两颜色直接相乘
Blend DstColor SrcColor         //

2.深度测试指令

Unity 3D ShaderLab语言提供了控制是否写入当前片元的深度值到深度缓冲区的控制函数,以及和当前深度缓冲区对应点的深度值相比满足何种关系时才写入的一系列判断函数,如以下代码所示。

//这些代码要放在一个pass块内
ZWrite On      //表示当满足深度测试条件时,允许把当前片元的深度值写入深度缓冲区的相应位置
ZWrite Off     //表示不允许把当前片元的深度值写入深度缓冲区的相应位置,如果正在绘制一个半透明物
//体,应该选中此项
ZTest Less     //表示当前片元的深度值小于深度缓冲区对应点的深度值时,可把深度值写入缓冲区
ZTest Greater  //表示当前片元的深度值大于深度缓冲区对应点的深度值时,可把深度值写入缓冲区
ZTest LEqual   //表示当前片元的深度值小于或等于深度缓冲区对应点的深度值时,可把深度值写入缓冲区
ZTest Gequal   //表示当前片元的深度值大于或等于深度缓冲区对应点的深度值时,可把深度值写入缓冲区
ZTest Equal    //表示当前片元的深度值等于深度缓冲区对应点的深度值时,可把深度值写入缓冲区
ZTest NotEqual //表示当前片元的深度值不等于深度缓冲区对应点的深度值时,可把深度值写入缓冲区
ZTest Always   //表示当前片元的深度值为任何值时都可以把深度值写入缓冲区

[1]  render pipeline一词有些中文书籍翻译为“渲染管线”,本书统一使用“渲染流水线”,类似于工业生产中的流水线概念,更能体现图形处理中的数据流动性和处理阶段性。

[2] 颜色缓冲区是指某一片存储区域,该区域存储了要在屏幕上显示的像素点信息。

[3] 在Direct3D中经常把“片元”和“像素”混为一谈,如把OpenGL中的片元着色器(fragment shader)称为像素着色器(pixel shader)。本书将较严格地区分“片元”和“像素”的概念,把最后在颜色缓冲区中且将要显示到屏幕的数据称为像素,把Direct3D语境中的像素着色器统称为片元着色器。

[4] 凸多边形的定义和属性可参考相关资料。

[5] 有些书籍把model coordinate和model space译作局部坐标系与局部空间,本书统一译为模型坐标系和模型空间。

[6] 有些书籍使用摄像机空间/相机空间(camera space)表示view space的概念,这两者是等价的。本书统一使用view space,译作观察空间。

[7] 有些书籍把view frustum称作视锥,本书统一称作视截体。

[8] Direct3D中的轴对齐立方体的z的取值范围是[0,1],而OpenGL中z的取值范围是[-1,1]。本书推导投影矩阵所使用的z的取值范围是[-1,0],对说明原理没有影响。

[9] 有些书籍把back face culling翻译为背面拣选。

[10] 有些书籍把normalized device coordinates翻译为规格化设备坐标。

[11] 关于Direct3D 9平台上的半像素偏移问题,可参考MSDN(microsoft Developer network)上的技术文章Directly Mapping Texels to Pixel(Direct3D 9)及Drilian编写的Understanding Half-Pixel and Half-Texel Offset

[12] 关于引擎如何在后台处理这个问题,可以查看由Unity 3D工程师Aras Pranckevicius编写的Solving DX9 Half-Pixel Offset


辐射度学(radiology)是一门以整个电磁波段(electromagnetic band)的电磁辐射能(electromagnetic radiation energy)测量为研究对象的科学。计算机图形学中涉及的辐射度学,则集中于整个电磁波段中的“光学谱段”(optical spectrum)中的“可见光谱段”的辐射能的计算。

光学谱段是指从波长为0.1nm的X射线到波长约为0.1cm的极远红外线这一范围内的电磁波。波长小于0.1nm的是伽马射线,大于0.1cm的则属于微波和无线电波。光学谱段按波长分为X射线、远紫外线、近紫外线、可见光、近红外线、短波红外线、中波红外线、长波红外线和远红外线。可见光谱段即能对人眼产生目视刺激而形成光亮感和色感的谱段。可见光谱段的波长范围一般是0.38~0.76μm。电磁波段如图2-1所示。

▲图2-1 电磁波段

对人眼产生总的目视刺激的度量是光度学(photometry)的研究范畴。光度学除了对可见光辐射能的客观度量之外,还考虑了人眼视觉的生理反应等因素。在研究光度学时,通常会定义一个和“标准人眼”对光的感应效果相当的探测器模型以作抽象研究。

对人眼产生色感刺激的度量是色度学(colorimetry)的研究领域。色度学研究人眼辨认物体的明亮程度、颜色类别和颜色的纯洁度,即明度(brightness)、色调(tone)和饱和度(saturation),解决对颜色的定量描述和测量的问题。

辐射度学研究的电磁波的辐射能是客观独立的,与观察者无关,即测量计算者和观察者的人眼感受没有关系。辐射度学建立在几何光学的基础上,即辐射是以直线传播的,不考虑电磁波干涉、衍射等性质。本节以可见光作为阐述辐射度学所用到的“电磁波”的一种特例来说明。

根据波粒二象性(wave-particle duality),如果把光视为一束粒子流(particle stream),则构成光的粒子就称为光子(photon),即一个在真空中以直线运动的能量包(energy packet)。在真空中光子的速度为常量c,约为299 792 458m/s。每个光子所携带的能量为E,光子的能量和它的频率f的关系为

E=hf

(2-1)

式中,h为普朗克常数。

光子既然可视为一个能量包,那么为了研究在单位时间内,通过某表面的各个光子所携带的能量之和是多少,物理学中引入了辐射通量(radiation flux)的概念。辐射通量定义为以辐射的形式发射、传输或者接受的功率,即单位时间内的辐射能,单位是W。不同波长的光引起人对不同颜色的感知,不同的辐射通量则引起人对光的不同亮度的感知。

考察在某一给定空间V中的总辐射量。在空间中,虽然光子在物体的表面上不断地流过,但从总体上来说,光子的分布保持着一个常数——因为只要光源是恒定的,在被光源所照亮的空间中某部分不会自发地变得忽明忽暗。又由于光速非常快,对于考察光照效果的观察者而言,空间中的某个光源突然开启时,光的能量几乎立即分布到空间中的每个角度,所以一切看起来都是保持恒定的,即光子分布保持着一个常数。

在空间中,光子的流动和传播遵循能量守恒定律。在给定空间中,所有流入本空间的光子的总能量,一定等于从本空间流出的光子能量与被本空间内物体所吸收的光子的能量之和。某一空间中光可以有两种方式进入:一种是从外部空间中流进来(称为入射),另一种就是本空间内部的物体发射出来;光从空间中向外部流出时有三种情况:一是不经过本空间内任何物体的干扰直接向外流出,二是可能被本空间内物体完全反射后流出(称为出射),三是被本空间内的物体吸收掉了。那么在给定单位时间内,某个给定空间V中的辐射通量方程如下。

(2-2)

在给定空间V中,如果在某一个面积微元,或者说在某一点p处,方向上,由自发光物体发出的辐射通量为函数所得到的值,那么在单位时间内,给定空间V中,点,方向。其中,表示p为空间V中的某一个三维位置点;Γ是指在点p中所有存在的入射出射的方向值的集合,是这个集合中的一个值。那么在单位时间内,给定空间V中,各点各向上的总自发射光的辐射通量,就应是变量p的二重积分值,如下式所示。

(2-3)

在给定空间V中,由函数得到的值是在点p处方向为的辐射通量。而光的特性决定了光子在点p处,沿着方向运动时,在单位时间内可能被吸收的概率由概率密度函数决定。因此,光子在点p处,沿着方向运动时,被吸收的辐射通量值应为。那么在单位时间内,给定空间V中,各点各向上的总被吸收的辐射通量,就应该是变量p的二重积分值,如下式所示。

(2-4)

和上述的各点各向上的总被吸收量类似,设光子在点p以方向入射,然后以方向出射出去的概率密度函数为,并且出射方向值所在的集合为Ω。那么在单位时间内,给定空间V中,各点各向总的出射光的辐射通量,就应该是变量p的三重积分值,如下式所示。

(2-5)

各点各向上总的入射光的辐射通量的计算公式和总的出射光的辐射通量形式上相似,只需把对换即可。

各点各向上总的直接流出辐射通量是指光进入给定空间V后,未经过任何可传递光的介质,直接从空间的边界表面S上流出的辐射通量。例如,给定空间V是一个正球体,则考察的边界表面S的面积就是。那么在给定空间V中,点,方向。各点各向上总的直接流出辐射通量为

(2-6)

事实上,如果知道了任意一点p在任意一个方向上的辐射通量的值,就能得到计算机图形学中的光照问题的完全解决方案。因为得知了某处的辐射通量,就能得到在单位时间中的能量,从而能算出光的波;根据光的波长便能重构出能被人类感知的场景的颜色,从而完成计算机图形学的核心内容——对场景的渲染。举个例子,假设在场景中有一个胶卷相机,要用这个相机来拍摄该场景的一张相片,或者用图形学的术语来说,要渲染出一张该场景的图像。这等同于要算出:进入相机镜头(该镜头对应于上述公式中的点p的集合),到达胶片的所有的光线(这些光线的方向就对应于上述公式中的方向的集合)。每个单位时间内,入射到胶卷的光线的能量就决定了该点对应的颜色。

正如上面说到的,直接利用上述公式进行计算的计算量很大,要通过直接解上述方程得到去辐射通量函数是不可能的。计算机图形学主要通过使用一系列近似公式来逼近上述公式的解,而且不同的图形渲染要求,对得出计算式的解的要求也不同——一个能满足实时渲染性能要求的解,和产生真实光照感的解,是完全不同的。上面提到的这些理论,则是本书要剖析的基于物理渲染的Unity 3D着色器的理论基础。

给定一个正球体,它的半径为R。然后给定一个正圆锥体,正圆锥体的顶点和球心重合,到圆锥底面圆边上任意一点的连线,即正圆锥体斜高,它的值也为R。由正圆锥体的底面圆S所截取的那一部分球面的面积A和球体半径R的平方的比称为立体角(solid angle),其国际单位是球面度(steradian)。图2-2展示了一个立体角的截面剖视示意图。

▲图2-2 立体角的截面剖视示意图

若以表示立体角,则立体角的微分形式定义如下式所示。

(2-7)

式中,dA为圆锥底面截取的球面A的微元。

若在球面坐标系下对立体角进行定义,如图2-3所示,面积微元dA的公式可以写为

(2-8)

▲图2-3 在球面坐标系下定义立体角

那么由式(2-8),整个球面的立体角可写为关于的二重积分形式:

(2-9)

点光源和力学中的质点类似,只要当用来测定光的辐射的某个位置点,其与光源的距离是光源的最大尺寸的某倍时,该光源就可以被视为点光源,通常该倍数不小于15。点光源以球面波的方式向空间辐射电磁波。如果在传输介质中没有反射、散射和吸收,那么在给定方向上的某一个立体角内,无论辐射距离有多远,其辐射通量是不变的。

辐射强度(radiation intensity)定义为在给定传输方向上,单位立体角内光源发出的辐射通量。令辐射通量为,立体角为,辐射强度为I,则以微分形式定义光的辐射强度的公式如下:

(2-10)

辐射亮度(radiance)定义为辐射表面在其单位投影面积的单位立体角内发出的辐射通量。这个定义很复杂,念起来也很拗口,所以通过图形来说明最为直观,如图2-4所示。

▲图2-4 投影面积和辐射亮度示意图

图2-4中,在待考察的辐射表面中,取表面面积微元dA进行讨论,光线穿过面积微元dA,与面积微元的法线夹角为,则面积微元的投影面积 ,然后将视为一个正圆锥体的锥顶点(应用极限的观念看待一个面积微元收缩成一个“点”的做法),乘以一个立体角值(单位是球面度),便得到立体角所截的球面面积。令辐射亮度为L,则辐射亮度的微分定义如下。

(2-11)

辐射亮度描述光源的面积微元在垂直传输方向上的辐射强度特性。例如,描述一个白炽灯时,描述白炽灯每一局部表面的发射特性通常是没有实际意义的;而把它视为一个点光源时,就可以描述在某个给定观察方向上的辐射强度。

辐射出射度(radiant exitance)定义为离开光源表面的单位面积的辐射通量,令辐射出射度为M,则以微分形式定义辐射出射度的公式为

(2-12)

面积微元对应的立体角是光源辐射的整个半球空间(注意是半球而不是整个球)。例如,太阳表面的辐射出射度指太阳的单位表面积向外部空间发射的辐射通量。

辐射入射度(irradiance,又译为“辐照度”)定义为单位面积被照射的辐射通量,令辐射入射度为E,则以微分形式定义辐射入射度的公式为

(2-13)

由式(2-13)可以推得:对于点光源,给定辐射通量为,则它在一个半径为R的球面某一面积微元上的辐射入射度遵循距离平方反比定律,如下式所示。

(2-14)

辐射入射度和辐射出射度的定义方程和单位是相同的,分别用来描述面积微元发射和接受辐射通量的特性。如果一个表面能反射出入射至其表面的全部辐射通量,那么该表面可以视为一个辐射源表面。辐射度相关的物理量可以视为电磁波波长的函数。因此,在描述辐射度相关的物理量时,可以在相应的名称加上波长符号,写成函数形式,如辐射通量可以记为。辐射入射度和辐射出射度的概念和基于物理的渲染模型有直接的关系,在后续章节中会使用到这两个重要的概念。

和辐射度学相比,光度学的研究对象只限于可见光范围内,并且要以人眼的视觉特性为基础。辐射度学中的所有概念,如光通量、光照强度等,都和视觉函数有关。光度量和辐射度量的定义是一对应的,表2-1列出了基本的辐射度量和光度量的名称、符号、方程和单位名称。有时为了避免混淆,在辐射度量符号上加上下标e,在光度学符号上加上下标v,如辐射度量IeMe等,光度量IvMv等。

表2-1 辐射度量和光度量的名称、符号和定义式

辐射度量

光度量

符号

方程

辐射度量单位名称和符号

光度量单位名称和符号

辐射量

光量

Q

 

焦,J

流秒,lm•s

辐射通量

光通量

瓦(焦每秒),W(J/s)

流,lm

辐射强度

发光强度

I

瓦每球面度,W/sr

坎,cd

辐射亮度

光亮度

L

瓦每球面度平方米,
W/(sr•m2)

坎每平方米,cd/m2

辐射出射度

光出射度

M

瓦每平方米,W/m2

流每平方米,lm/m2

辐射入射度

光照度

E

瓦每平方米,W/m2

勒(流每平方米),lx(lm/m2)

下面介绍人眼的视觉特性和视见函数,光通量和辐射通量可以通过人眼的视觉特性进行转换。人眼的视觉特性有以下几种。

视敏特性:指人眼对不同波长的光具有不同敏感度的特性,即对辐射功率相同的各种颜色的光具有不同的亮度感觉。在相同辐射功率的条件下,人眼感到最亮的光是黄绿光(波长约为555nm),感觉最暗的光是红光和紫光。视敏特性可用视敏函数和相对视敏函数来描述。

亮度感觉特性:指人眼能够感觉到的亮度范围,该范围可达109:1。人眼总视觉范围很宽,但不能在同一时间感受这么大的亮度范围。当平均亮度适中时亮度范围为1000:1,平均亮度较高或较低时亮度范围只有10:1。通常情况下为100:1,电影银幕亮度范围大致为100:1,CRT显像管亮度范围约为30:1。人眼对景物亮度的主观感觉不仅取决于景物实际亮度值,而且还与周围环境的平均亮度有关。人眼的明暗感觉是相对的,在不同环境亮度下,对同一亮度的主观感觉会不同。

彩色视觉:人眼的锥状细胞有3种,分别对红、绿、蓝三种色光最敏感,称为红感细胞、绿感细胞、蓝感细胞。当一束光射入人眼时,3种锥状细胞就会产生不同的反应,不同颜色的光对3种锥状细胞的刺激量是不同的,产生的颜色视觉各异,使人能够分辨出各种颜色。

分辨力:指人眼分辨景物细节的能力,分辨力是有限的,其大小用分辨角表示,分辨角也称视敏角或称视角。视力正常的人在中等亮度和中等对比度情况下观察静止图像时,人眼能分辨的最小视角为1'~1.5'。人眼的分辨力因人而异,分辨力还与景物照度和对比度有关。

光通量和辐射通量的转换公式为

(2-15)

式中,是由国际照明委员会(Commision Internationale de L’Eclairage,CIE)推荐的平均人眼光谱光视效率,即视见函数;为辐射通量对波长的函数;为光通量对波长的函数;Km为最大光谱光视效能值,它是一个常数。

对于波长为555nm的明视觉,Km=683lm/W;对于波长为507nm的暗视觉,Km=1725lm/W。图2-5给出了人眼在明视觉和暗视觉下的视见函数。

▲图2-5 人眼在明视觉和暗视觉下的视见函数

明视觉(photopic vision)与暗视觉(scotopic vision)相对,是不同波长的光刺激在两种亮度范围内作用于视觉器官而产生的视觉现象。人眼视网膜上分布有视锥细胞(cone cell),集中在视网膜的中央窝及其附近,接受强光的刺激,在强光下起作用,所以称为明视觉器官。锥体细胞能分辨物体的细节和颜色,视网膜不同部位视敏度的判别与视锥细胞的分布情况是一致的。视网膜一定区域的视锥细胞数量决定着视觉的敏锐程度。

视杆细胞(rod cell)只在较暗条件下起作用,适宜于微光视觉,但不能分辨颜色与细节。视网膜中央的“视锥细胞视觉”和视网膜边缘的“视杆细胞视觉”即明视觉和暗视觉。

明视觉指人眼在光亮度超过3cd/m2的环境所产生的视觉,此时视觉主要由视锥细胞起作用。如图2-5所示,最大的视觉响应在光谱蓝绿区间的555nm处,即波长为555nm处的光产生了明视觉函数的峰值。暗视觉指人眼在环境亮度低于10−3cd/m2单位所产生的视觉,此时视杆细胞是主要作用的感光细胞,即波长为507nm处的光产生了暗视觉函数的峰值。中间视觉介于明视觉和暗视觉亮度之间,此时人眼的视锥和视杆细胞同时响应,并且随着亮度的变化,两种细胞的活跃程度也发生了变化。一般从白天晴朗的太阳到晚上台灯的照明,都是在明视觉范围内的;而在路灯照明和明朗的月夜下,为中间视觉照明;昏暗的星空下就是暗视觉了。

光度量中最基本的物理量是发光强度,其单位是坎德拉(candela),记作cd。它是国际单位制中7个基本单位之一,其定义为波长为555nm的光产生的辐射。在给定方向上的辐射强度为时,光源在该方向上的光强度为1cd。光通量的单位是流明(lumen),记作lm。1lm是指光强度为1cd的均匀点光源在1球面度内发出的光通量。

颜色或色彩是人们通过眼部、脑部及生活经验所产生的一种对光的主观感觉效应。人的视网膜上布满了感光细胞,当有光线传入人眼时,这些细胞就会将光线的输入刺激转化为传递给视神经的电信号,最终在大脑得到解释,形成“颜色”这一种意识感觉。

2.2节提到在视网膜上有两类感光细胞:视锥细胞和视杆细胞。视锥细胞分为S、M和L三种类型,大都集中在视网膜的中央,每个视网膜大概有700万个。视锥细胞能在较明亮的环境中辨别颜色和形成精细视觉。每种视锥细胞包含有一种感光色素,分别对红、绿、蓝三种有着不同波长的光敏感。其中,L型视锥细胞对较长波长的光波(红光)敏感,M型视锥细胞对中等波长的光波(绿光)敏感,S型视锥细胞对较短波长的光波(蓝光)敏感,即是不同波长的光线刺激视锥细胞就能让人感觉到不同的颜色。

视杆细胞分散分布在视网膜上,每个视网膜有1亿个以上。这类细胞对光线更为敏感,很微小的光线能量就可以激发它对光的感应。视杆细胞无法根据光线的波长感受到对应的颜色,但在较弱的光照环境下对环境有分辨能力,如在夜里可以看到物体的轮廓。

当一束光线进入人眼后,感光细胞会产生4个不同强度的信号:由视锥细胞产生的3种信号(对应于红、绿、蓝3种颜色)和由视杆细胞产生的信号。只有视锥细胞产生的信号能转化为颜色的感觉。3种视锥细胞对不同波长的光线会有不同的反应,每种细胞对某一段波长的光会更加敏感,如图2-6所示。图2-6中,横坐标是光的波长,单位是nm;纵坐标是对波长的反应值,又称刺激值,标准化反应值在区间[0,1]内。这些信号的组合就是人眼能分辨的颜色总和。

▲图2-6 三种视锥细胞对不同波长的标准化反应曲线(本图取自维基百科)

格拉斯曼[1]经过大量的实验,总结出格拉斯曼定律。该定律指出人眼对不同颜色光线混合的刺激反应是线性的。假设有两束不同颜色的光C1C2,某视锥细胞对这两束光的刺激反应分别是r1r2。将它们按照一定比例进行混合,得到第3种颜色C3,有,则视锥细胞对颜色C3的刺激反应值r3r1r2的线性叠加,有

因为人眼中有3种感知色彩的视锥细胞,所以理论上用3种不同颜色的光就可以混合出自然界中任何一种颜色来。人们通过大量的实验,通过对3种颜色的光源进行匹配,得到了人眼对于不同颜色光的匹配函数。

这一类实验的过程大致如下:把一个银幕用不透光的挡板分割成两个区域,用一束待测试颜色的光线照射左区域,待测试颜色记为C(以下用大写字母表明颜色,用小写字母表明分量大小)。同时用3种不同颜色的光同时照射右区域,这3种不同颜色的光称为源颜色光,记为C1C2C3。然后调节3种源颜色光的强度,直到银幕左右两边区域上的颜色看上去一样为止。假设此3种源颜色光的强度分别为r1r2r3,根据格拉斯曼定律所揭示的叠加线性性质,有以下公式。

(2-16)

从实践中可知,在可视光范围内,任何一种波长的光刺激视锥细胞所产生的颜色感觉,可以经由最多3种精心选择的波长的光混合而成的“混合光”等价刺激而成。例如,某种波长让人生成“黄色”感觉的光,可以由两种分别让人生成“红色”和“绿色”感觉的不同波长的光混合刺激而成。任何一种“目标颜色”,由最多3种“基准颜色”按一定比例叠加而成,这就是式(2-16)中所描述的三色加法模型。

根据上面的理论,只需要选定三原色且对其进行量化,就可以将颜色量化为数字信号。在三色加法模型中,如果某一种目标颜色C和另外一种三原色混合色Cmix给人的感觉相同时,三原色混合色中的3种基准颜色的份量就称为该目标颜色C的三色刺激值。对于如何选定三原色及对其量化、如何确定刺激值等问题,国际照明委员会于1931年定义了一套标准:CIE1931-RGB标准色度系统。

CIE1931-RGB颜色模型[2]分别选择了波长为700nm、546 nm和436nm的这3种波长的光,作为产生三原色的基准,这3种光可称为三原色基准光,它们刺激光锥细胞,可以分别让人感觉到红、绿、蓝3原色。这3种波长的光可以由汞弧光谱滤波精确且稳定地产生出来。

假设某个波长为的目标光,对应生成目标颜色C。依据普朗克公式,可以把该目标光的能量视为波长的函数,写为,则目标光的能量应为三原色基准光各自的能量乘以系数后之和。如果把三原色基准光各自的能量写成目标光波长的函数,并且称这些函数为颜色匹配函数(color matching function),则有,如式(2-17)所示。

(2-17)

最终可以写为

(2-18)

式(2-18)实质就是用波长及能量的形式,对式(2-16)进行改写。式(2-18)的则分别对应于三原色基准光各自的光亮度值。可以理解为在定义一个RGB颜色值时,R分量、G分量、B分量各占多少。如果直接用RGB的方式描述,式(2-18)和式(2-16)都可以改写成如下形式:

(2-19)

式中,C为目标光的颜色;RGB对应于红、绿、蓝3种基准光;rgb为混合产生目标光时需要3种基准光的强度,其取值范围在0~1。

如果把rgb这些值视为坐标系的纵坐标,光的波长视为横坐标,则图2-7表示它们之间关系。

▲图2-7 波长rgb的关系(本图取自维基百科)

图2-7中有曲线的纵坐标值出现了负数,这是因为,在前面提到的实验过程中,左边区域是待测试颜色的光,右边区域是3种可以调整各自比例的源颜色光。在实验中会碰到这样的情况:右边3种源颜色光无论如何调节比例,都不能混合得到待测试颜色光的颜色,如某个源颜色光的光强已经减小到0了,但是还需要继续减小才能与左边的待测试颜色光的颜色相匹配。因为自然界中不可能有“负强度”的光,所以这时需要往左边的待测试颜色光中掺入源颜色光中的一种或者几种,继续调节至左右两边的颜色匹配为止。在左边的待测试颜色光中添加某强度值的源颜色光,等价于在右边的混合光中减去某强度值的源颜色光,这就导致了图2-7中曲线的纵坐标值出现了负数。

例如,对于波长为510nm的待测试颜色光,色匹配函数的值是(−0.09, 0.09, 0.03),即意味着将0.09份的绿光与0.03份的蓝光放在右边,左边放上1份波长为510nm的待测试颜色光,以及 0.09份的红光,这样左右两边的光色看上去就一样了。

根据用三原色基准光组合成一个任意颜色光的这一特性,可以沿着正交坐标轴画出每个基准光的值,所形成的空间可以称为三原色基准空间。如图2-8所示,任意颜色均可以由这个三原色基准空间中的一个矢量表示,即以坐标系原点为起点,分量为rRgGbB的矢量。矢量与单位平面的焦点代表为获得颜色所需要的相对权因子,相对权因子又称为色度值或者色度坐标,即

(2-20)

式中,。

▲图2-8 RGB颜色空间中的单位平面图

单位平面在坐标平面上的投影产生色度图如图2-9所示。色度图直接给出了rg两种基准光颜色之间的函数关系,并且间接地给出与第三种基准光颜色的关系,如

▲图2-9 色度图

图2-8中,边缘的曲线表示单色的光谱。例如,波长为540nm的单色光,由r=0、g=1、b=1-r-g=0三个基准光颜色的分量组成。再如,380~540nm波段的单色光,由于图2-7中的r(λ)曲线中存在负值,因此该段色域落在了r轴的负区间内。自然界中,人眼可分辨的颜色都落在光谱曲线包围的范围内。

CIE1931-RGB颜色模型是根据实验结果制定的,出现的负值使得计算和转换时非常不便。所以,国际照明委员会提出了一个假想模型,该模型假定人对色彩的感知是线性的(实际上并不是线性的,2.4节会提到)。该模型对CIE1931-RGB系统色度图进行了线性变换,将可见光色域变换到正数区域内。其方法是首先在CIE1931-RGB系统中选择了一个三角形,该三角形覆盖了所有可见光的色度,之后将该三角形进行如式(2-21)所示的线性变换,将可见色域变换到(0, 0)、(0, 1)及(1, 0)构成的正数区域内。也就是说,假想出3个不存在于自然界,但较之基准光RGB更方便计算的基准光XYZ,构成一个新的CIE1931-XYZ颜色模型。

(2-21)

CIE1931-XYZ颜色模型的色度图如图2-10所示。但务必注意,图中的颜色只是一个效果示意。事实上,没有设备能把自然界中所有的颜色完全显示出来。

▲图2-10 CIE1931-XYZ颜色模型的色度图

CIE1931-XYZ颜色模型的色度图有如下性质需要注意。

1)该色度图所示意的颜色包含了一般人可见的所有颜色,即人类可见的颜色范围。色度图的弧线边界对应自然中的单色光。图下方直线的边界则是由多种单色光混合而成。

2)在该图中任意选定两点,两点间直线上的颜色可由这两点的颜色混合成。给定3个点,3个点构成的三角形内颜色可由这3个点的颜色混合成。

3)给定3个真实光源,混合得出的色度只能是三角形框定的范围,无法完全覆盖人类视觉色域。

这就是CIE1931-XYZ标准色度学系统。该系统是国际上色度计算、颜色测量和颜色表征的统一标准,是绝大多数测色仪器的设计与制造依据。

CIE1931-Yxy颜色模型由CIE1931-XYZ模型衍生得来。Yxy中的Y表示光的亮度,xy则可以依据以下公式,从CIE1931-XYZ模型换算而来:

(2-22)

这个模型投影到x-y平面上,即得到图2-10中的CIE1931-XYZ色度图,其中xy分量的取值范围是[0,1]。有时该模型也被称为CIE-xyY。

一切计算机图形学理论,在建立了一系列的数学和物理模型去描述待描述的物体是长什么样子之后,最终还是要能被人看见才有意义。从这个角度说,待描述物体的客观的不依赖于人的心理生理感知的物理量和描述人的心理特征的一些心理量也会存在着一定的有规律的联系。所以,在开始讨论伽马校正(gamma correction)相关理论之前,首先看看由德国生理学家韦伯•费希纳所发现的描述人的心理量和物理量的韦伯-费希纳定律。

(2-23)

式中,k为常数;为差别阈限;I为标准刺激强度。

韦伯发现,同一个刺激差别量必须达到一定比例才能引起差别感觉,这一比例就是式(2-23)中的常数k。把人类能感觉到的最小可觉差(连续的差别阈限)作为感觉量的单位,即每增加一个差别阈限,心理量增加一个单位。感觉量与物理量的对数值成正比,这也是说,感觉量的增加落后于物理量的增加,物理量呈几何级数增长,而心理量呈算术级数增长,这个经验公式即韦伯-费希纳定律。一般地,这个定律适用于中等强度的刺激。

上面的韦伯-费希纳定律有一些比较难以理解的术语,用通俗易懂的话来说就是:人对外界刺激的感知是非线性的。如果以非线性的方式加强外界刺激,人对这个外界刺激的感觉程度是均匀增长的。可以用日常生活中的一些生活经验大致地描述韦伯-费希纳定律的含义:

假设有一个完全封闭、隔绝外部光线进入的黑屋中,在屋内位置均匀地安装了100盏功率为20W的电灯。我们站在房子里,一开始时,这100盏灯都是关闭的,室内完全黑暗。

现在我们开启第一盏灯。显然这第一盏灯对照亮室内的贡献是显著的——从完全黑暗什么都看不见,到有光亮能看到东西了。尽管这时候可能光线还比较昏暗,但我们在视觉上已经感受到极大的亮度提升。

接下来再开第二盏灯,因为刚开第一盏灯时,光线还是比较昏暗的,所以这时我们也还是能感觉到,比只开第一盏灯时,室内要变得更亮些。

接下来我们依次打开每一盏灯,可能当开到第90盏灯时,我们发现已经灯光足够明亮,能让我们看清楚室内的每一处地方的细节了。接下来我们再依次打开剩下的10盏灯。这个过程中,我们发现这时的亮度变化已经感觉很不明显了,甚至可能会觉得,开不开这剩下的10盏灯,室内的明亮程度都没发生什么变化。这也就是说,我们对“只开第一盏灯时”引起的明亮变化的感觉,和“开第100盏灯时比开了99盏灯时”引起的明亮变化的感觉,两者相比,前者远远强烈于后者。

假设我们是一个绘画颜料生产商,有纯黑和纯白两种原材料。纯黑的材料完全不反射光,所以设定它对白光的反射率为0;纯白的材料完全反射光,所以设定它对白光的反射率为1。

现在我们要用这些原材料生产出256种不同灰度的灰色颜料。这256种灰色颜料将会被从0到255编上号。其中,纯黑颜料的编号为0,纯白颜料为255。并且我们用的是物理化学检验方法,而不是用人眼感觉测定的方法,使得每相邻编号的两个颜料其反射率相差为0.00390625,即1/256。那么生产出的这256种颜料编号和对白光的客观反射率如表2-2所示。

表2-2 不同颜料对白光的客观反射率

颜料编号

对白光的客观反射率

0

0

1

0.003 906 25

2

0.007 812 5

3

0.011 718 75

253

0.992 187 5

254

−0.996 093 75

255

1

当把这些生产出来的颜料按编号依次排列在我们眼前时,由于韦伯-费希纳定律所揭示的原因,我们会发现:编号比较靠前的颜色,如1、2、3号,为什么看起来一个比一个白得那么快?而编号靠后的那些颜色,如253、254、255号,为什么看起来都没有什么颜色变化,都是白色?为什么编号127的那个颜色,好像并不是不偏不倚不黑也不白的中灰色,而是偏白一点了。而大家印象中的那个中灰色,反而和编号为56的那个颜色更接近……这一组颜料的颜色,并不是我们想要的看起来从黑到白均匀变化的颜料组。

现在问题就变成了:在给定编号范围的前提下,如何调配各个编号的颜料客观反射率,使得这一系列颜料的颜色看起来是从黑到白均匀变化的。例如,编号为127的颜色的客观反射率是0.5,人眼看起来却并不像是0.5客观反射率产生的视觉效果,现在我们要给编号为127的颜色找到一个客观反射率,使得人眼看起来,是由0.5客观反射率产生的视觉效果。而这个问题,其实就和计算机图形学领域中的伽马校正类似。

人们经过大量的实践研究和总结,发现客观的物理量数值O和人类的心理感应数值P呈幂函数关系,即

P=Oγ

(2-24)

γ取不同的值时,幂函数图像如图2-11所示。

▲图2-11 γ值取不同值时的幂函数图像,横坐标表示客观物理量数值O,纵坐标表示心理感应数值P

放到颜料颜色的问题来讨论,横坐标为客观反射率,纵坐标为人眼看到该客观反射率后觉得的主观反射率。人们经过大量的实践研究得到,在亮度领域,人眼的伽马值约为1/2.2,其幂函数图像也接近于指数值(伽马值)为1/2的曲线。这也揭示了生产出来的颜料明明其客观反射率是均匀变化(线性变化)的,但是看起来不是均匀变化的原因。

理解了颜料颜色的问题,也就能理解计算机中的伽马校正的含义。假设使用数码相机拍摄到一个看起来是中灰色的物体,该中灰色对白光的客观反射率是0.217 63。如果数码相机在采样并编码的过程中做一次伽马校正操作,即给定一个伽马值0.454 54作为指数,对作为底数的客观反射率0.21763做求幂操作,得到中灰色的主观反射率为0.5左右。此时,如果把这些主观反射率对应的灰度值编码进一个8位通道的数组,无论是比中灰色暗的暗部,还是比中灰色亮的明部,都能各自分配到128位的灰阶,这样黑白过渡就显得很均匀。如果不做伽马校正,中灰色便记录在0.2左右并存储,那比中灰色暗的暗部仅分配到50个灰阶,采样严重不足;而比中灰色亮的明部因为分配到200多个灰阶,则显得冗余。但如果把存储空间变为32位通道的数组,那么伽马校正就没有必要了。因为即使把中灰色记录在0.2左右的位置,比中灰色暗的暗部也能分到,约为858 993 459个灰阶,足够存储均匀地描述人能感觉到的暗部信息了。

从上面列举的两个例子可以看出,之所以会存在伽马校正这样一个概念,是因为:①基于韦伯-费希纳定律所揭示的原因,人对颜色暗部细节变化的感觉要比对明部细节变化敏感得多;②由于存储空间的限制,只能在有限数量的存储空间中存放尽可能多的颜色数据,而且要使得这一系列颜色变化得均匀,需要在对颜色数据进行编码时尽可能地进行调整,以保留暗部细节。该调整过程就是伽马校正。

很多书籍在介绍伽马校正时,都会从阴极射线管(cathode-ray tube,CRT)显示器谈起。CRT显示器在显示最终像素值时,其电子枪输入电压与屏幕的光线输出存在一种物理关系,这种关系也可以用如下幂函数来表示。

(2-25)

式中,V为输入电压;(gamma)为显示器常量;为显示器的亮度级;I为最终生成的光强度。

一般地,CRT显示器的gamma取值范围在2.3~2.6。其曲线形状和走向接近于图2-11中gamma值为2的曲线,此时的横坐标为显示器电子枪的电压(可以理解为客观反射率),纵坐标为光强度。这时,CRT显示器在显示像素时恰好就做了一次符合人眼视觉的伽马校正:假如CRT要显示一个客观亮度值为0.5的颜色,因为式(2-25)所产生的伽马校正,这个值为0.5的客观亮度值被显示器输出成一个0.2左右的客观亮度值。由于韦伯•费希纳定律所解释的伽马校正,对于0.2左右的客观亮度值,人就感觉成了0.5左右的值,这样两个互为逆操作的伽马校正恰好使得客观亮度值和人眼看到的主观亮度值基本一致。这时,在计算器存储器中的客观亮度值就不需要像前面提到的数码相机的采样编码那样进行转换,直接存0.5即可。

上面的CRT显示器做的伽马校正,其伽马值大于1,称为解码伽马值(decoding gamma),该操作通常称为伽马展开(gamma expansion)。摄像机(人眼)做的伽马校正,其伽马值小于1,称为编码伽马值(encoding gamma),该校正操作称为伽马压缩(gamma compression)。图2-12描述了摄像机拍摄图片后在显示器上的显示过程,这也是抽象的颜色数据采集端和还原端的流程。

▲图2-12 摄像机拍摄图片后在显示器上的显示过程

从图2-12可以看出,讨论伽马校正时,要对颜色数据采集端与颜色数据还原端两个端同时讨论。如果解码伽马值和编码伽马值的乘积为1,理论上可以让还原端精确重现实际场景的视觉观感。但是,由于颜色数据采集端与颜色数据还原端之间存在两个差异:首先,机器设备能够采集和显示的颜色数量是不可能达到真实场景的颜色数量的,自然界的颜色数量可谓无穷,而机器设备能够采集到的颜色数量,如果用8位通道进行RGB编码,也只有(28)3=167 772 16种而已;其次,在真实场景中,原始的场景充满于观察者整个视野,而机器显示的画面往往只局限在屏幕中所显示的场景的一部分,真实场景中的未被机器设备显示的部分对被机器设备显示的部分所造成的人主观上的对比度感受在机器设备中还原的场景是不能产生的。因此,这两个原因造成了解码伽马值和编码伽马值的乘积为1反而不一定能让还原端精确重现实际场景的视觉观感。

为了使还原端能尽可能重现采集端采集的实际场景的视觉观感,在实践中不需要两端的伽马值的乘积为1,Real-time Rendering(ISBN:978 15688 14247)一书推荐在电影院这种漆黑的环境中乘积为1.5,在明亮的室内乘积为1.125。

sRGB颜色空间是惠普公司与微软公司于1996年一起开发的用于显示器、打印机及互联网图像存储的一种标准RGB颜色空间。这种颜色空间在编码上的定义充分利用了值为2.2的伽马校正系数,其很大程度上是为了和当时主流的CRT显示器的特性相匹配。

使用sRGB颜色空间对颜色进行编码的一些图像文件,可以不经转换就能在当时的CRT显示器中正常显示。其原因如图2-12所示,在数据采集端中,图片中对实际场景中的场景颜色信息,利用编码伽马值编码到图片的像素颜色值中去了,因此作为数据还原端的CRT显示器可以不用经过额外处理,就能利用其自身的电气特性较为准确地还原出实际场景的颜色信息。

sRGB定义了红色、绿色与蓝色三原色的颜色,即在其他两种颜色值都为0时该颜色的最大值。从CIE xyY坐标系计算sRGB中的三原色,首先需要将它变换到CIE XYZ三值模式:

(2-26)

这样XYZ值就可以用矩阵转换到线性的RGB值,但这些线性值并不是最终的结果。

(2-27)

式中,RlinearGlinearBlinear的取值范围是[0,1]。

sRGB是反映真实世界中伽马值为2.2的CRT显示器的效果,因此使用下式可以将定义在CIE-XYZ颜色空间的线性颜色值转换到sRGB颜色值。令ClinearRlinearGlinearBlinearCsrgbRsrgbGsrgbBsrgb

(2-28)

这些经过伽马校正后的sRGB颜色值的范围为[0,1]。如果需要使用0~255的取值范围,通常将它乘以255然后取整。

把颜色值从sRGB颜色值转换为线性颜色值则使用以下公式:

(2-29)

函数g(K)的定义如下:

(2-30)

式中,KRsrgbGsrgbBsrgb

Unity 3D有两个颜色空间(color space),即伽马空间(gamma space)和线性空间(linear space);以及基于这两种颜色空间所定义的工作流,即伽马工作流(gamma workflow)和线性工作流(linear workflow)。

所谓线性空间和伽马空间,除了前面提到的颜色空间的定义之外,还可以粗略地理解为:图2-11中横坐标的客观反射率(此时称为源颜色),选用指数值为1的线性函数对其进行变换,得到的是依然呈线性分布的主观反射率(或目标颜色),称这些目标颜色构成的颜色空间为线性空间。而选用一个指数值不为1的幂函数,把源颜色变换成目标颜色时,称这些目标颜色构成的颜色空间为伽马空间。至于为什么要使用伽马空间,就是前面所说的两个原因:人的主观感觉及存储空间的限制。Unity 3D引擎允许开发者在伽马空间或线性空间这两种颜色空间中工作。虽然长久以来伽马空间是标准的开发用颜色空间,但使用线性颜色空间将能得到更精确的渲染结果。

要指定伽马工作流或线性工作流,可以选择Edit | Projection Setting | Player选项,打开Player Settings对话框,进入Other Settings子页面,在Rendering选项组的Color Space下拉列表中选择Linear或者Gamma选项,各自对应于在引擎中使用线性工作流和伽马工作流,如图2-13所示。

▲图2-13 设置颜色空间

如果选择使用线性工作流,那么存储了颜色数据的纹理无论是在线性空间或伽马空间中创建的,都能正常工作。基于历史原因,图片文件中保存的颜色数据很多都已经转换到了伽马空间。而在渲染时,渲染器则需要使用基于线性空间的颜色数据,所以直接使用这些在伽马空间中的颜色数据会导致结果不准确。因此,在渲染计算时,需要使用sRGB采样器,这种采样器在对纹理采样过程中能把颜色数据从伽马空间转换到线性空间。

Unity 3D引擎的计算生成光照贴图过程始终是在线性空间中完成的,而计算结果则变换到伽马空间后存储到光照贴图文件中去,即无论当前的工作流是线性工作流或者伽马工作流,Unity 3D引擎所生成的光照纹理都是相同的。当使用线性工作流时,Unity 3D引擎会对这些存储着在伽马空间中的颜色数据的光照贴图使用sRGB采样器,在采样阶段自动把它们转换到线性空间中。如果使用伽马工作流,则不做该转换操作。因此,如果改变了工作流模式,需要重新烘焙光照贴图,引擎的光照处理系统会自动重新烘焙光照贴图。

在线性工作流下,且未使用HDR时,将使用特殊的帧缓冲器类型以支持sRGB读取和sRGB写入(读取时从伽马空间转换为线性空间,写入时则相反)。当此帧缓冲区用于混合或作为纹理绑定时,在使用之前将这些值转换为线性空间。写入这些缓冲区时,正在写入的值将从线性空间转换为伽马空间。如果以线性模式和非HDR模式进行渲染,则所有后期处理特效的源缓冲区和目标缓冲区都将启用sRGB读到和写入,以便后期处理和后期处理混合发生在线性空间中。

在线性工作流下,如果提供给渲染器的纹理文件中的颜色数据原本就是基于线性空间,那需要设置此纹理文件禁用sRGB采样。如图2-14所示,在纹理的Inspector界面中,取消选中sRGB(Color Texture)复选框,即设置该纹理文件禁用sRGB采样。

▲图2-14 启用/禁用sRGB采样的选项

如果当前的是伽马工作流,那么选中与取消选中sRGB(Color Texture)复选框都是没有意义的,因为在伽马工作流下是不会进行sRGB采样。另外,如果这些纹理是用来当作数据查找表,或者说是一个法线贴图,也不应对其进行校正,而是保持数据的原有值。

虽然使用线性工作流能确保得到更精确的渲染画面,但若有些平台上的硬件只支持伽马空间,就不得不需要使用伽马工作流。当使用伽马工作流时,尽管用来计算的纹理颜色数据是在伽马空间中存储的,采样器在采样过程中也不会将其转换到线性空间中去。但Unity 3D着色器的计算代码依然以计算线性空间颜色的方法去计算处理这些被编码成伽马空间中的颜色数据,而且为了确保一个最终可接受的渲染结果,引擎会在向最终显示给用户看的帧缓冲区写入颜色数据时调整一些不匹配(mismatched)的值,而且在这个调整过程中不做伽马校正。

当使用伽马工作流时,提供给着色器的颜色数据已在伽马空间中了。当使用这些颜色数据时,高亮区域的颜色其亮度值比使用线性工作流时更亮,这意味着随着光强度的增加,待绘制的表面以非线性方式变得过亮了。当使用线性工作流时,随着光强度的增加,表面的亮度保持线性变化,画面效果更真实。

[1] 赫尔曼•京特•格拉斯曼(Hermann Günther Graßmann),德国语言学家、数学家、物理学家。

[2] 在互联网上有一个称为brucelindbloom的网站,其提供了大量的颜色空间理论的相关资料,包括各种颜色空间的动态示意图。


相关图书

计算机图形学入门:3D渲染指南
计算机图形学入门:3D渲染指南
Creo Parametric 8 中文版从入门到精通
Creo Parametric 8 中文版从入门到精通
Origin科技绘图与数据分析实战
Origin科技绘图与数据分析实战
UG NX中文版三维电气布线设计
UG NX中文版三维电气布线设计
趣学3D One——青少年三维创意与设计(第2版)
趣学3D One——青少年三维创意与设计(第2版)
AutoCAD 2020中文版三维造型设计从入门到精通
AutoCAD 2020中文版三维造型设计从入门到精通

相关文章

相关课程