
简介
在渲染阶段,引擎所做的工作是把所有场景中的对象按照一定的策略(顺序)进行渲染。最早的是画家算法,顾名思义,就是像画家画画一样,先画后面的物体,如果前面还有物体,那么就用前面的物体把物体覆盖掉,不过这种方式由于排序是针对物体来排序的,而物体之间也可能有重叠,所以效果并不好。所以目前更加常用的方式是z-buffer算法,类似颜色缓冲区缓冲颜色,z-buffer中存储的是当前的深度信息,对于每个像素存储一个深度值,这样,我们屏幕上显示的每个像素点都会进行深度排序,就可以保证绘制的遮挡关系是正确的。而控制z-buffer就是通过ZTest,和ZWrite来进行。但是有时候需要更加精准的控制不同类型的对象的渲染顺序,所以就有了渲染队列。今天就来学习一下渲染队列,ZTest,ZWrite的基本使用以及分析一下Unity为了Early-Z所做的一些优化。
Unity中的几种渲染队列
首先看一下Unity中的几种内置的渲染队列,按照渲染顺序,从先到后进行排序,队列数越小的,越先渲染,队列数越大的,越后渲染。
Unity中设置渲染队列也很简单,我们不需要手动创建,也不需要写任何脚本,只需要在shader中增加一个Tag就可以了,当然,如果不加,那么就是默认的渲染队列Geometry。比如我们需要我们的物体在Transparent这个渲染队列中进行渲染的话,就可以这样写:
我们可以直接在shader的Inspector面板上看到shader的渲染队列:
另外,我们在写shader的时候还经常有个Tag叫RenderType,不过这个没有Render Queue那么常用,这里顺便记录一下: Opaque : 用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。 Transparent :用于半透明着色器(透明着色器、粒子着色器、字体着色器、地形额外通道的着色器)。 TransparentCutout : 蒙皮透明着色器(Transparent Cutout,两个通道的植被着色器)。 Background : 天空盒着色器。 Overlay : GUITexture,镜头光晕,屏幕闪光等效果使用的着色器。 TreeOpaque : 地形引擎中的树皮。 TreeTransparentCutout : 地形引擎中的树叶。 TreeBillboard : 地形引擎中的广告牌树。 Grass : 地形引擎中的草。 GrassBillboard : 地形引擎何中的广告牌草。
相同渲染队列中不透明物体的渲染顺序
拿出Unity,创建三个立方体,都使用默认的bump diffuse shader(渲染队列相同),分别给三个不同的材质(相同材质的小顶点数的物体引擎会动态合批),用Unity5带的Frame Debug工具查看一下Draw Call。(Unity5真是好用得多了,如果用4的话,还得用NSight之类的抓帧)
可以看出,Unity中对于不透明的物体,是采用了从前到后的渲染顺序进行渲染的,这样,不透明物体在进行完vertex阶段,进行Z Test,然后就可以得到该物体最终是否在屏幕上可见了,如果前面渲染完的物体已经写好了深度,深度测试失败,那么后面渲染的物体就直接不会再去进行fragment阶段。(不过这里需要把三个物体之间的距离稍微拉开一些,本人在测试时发现,如果距离特别近,就会出现渲染次序比较乱的情况,因为我们不知道Unity内部具体排序时是按照什么标准来判定的哪个物体离摄像机更近,这里我也就不妄加猜测了)
相同渲染队列中半透明物体的渲染顺序
透明物体的渲染一直是图形学方面比较蛋疼的地方,对于透明物体的渲染,就不能像渲染不透明物体那样多快好省了,因为透明物体不会写深度,也就是说透明物体之间的穿插关系是没有办法判断的,所以半透明的物体在渲染的时候一般都是采用从后向前的方法进行渲染,由于透明物体多了,透明物体不写深度,那么透明物体之间就没有所谓的可以通过深度测试来剔除的优化,每个透明物体都会走像素阶段的渲染,会造成大量的Over Draw。这也就是粒子特效特别耗费性能的原因。
我们实验一下Unity中渲染半透明物体的顺序,还是上面的三个立方体,我们把材质的shader统一换成粒子最常用的Particle/Additive类型的shader,再用Frame Debug工具查看一下渲染的顺序:
半透明的物体渲染的顺序是从后到前,不过由于半透相关的内容比较复杂,就先不在这篇文章中说了,打算另起一篇。
自定义渲染队列
Unity支持我们自定义渲染队列,比如我们需要保证某种类型的对象需要在其他类型的对象渲染之后再渲染,就可以通过自定义渲染队列进行渲染。而且超级方便,我们只需要在写shader的时候修改一下渲染队列中的Tag即可。比如我们希望我们的物体要在所有默认的不透明物体渲染完之后渲染,那么我们就可以使用Tag{“Queue” = “Geometry+1”}就可以让使用了这个shader的物体在这个队列中进行渲染。
还是上面的三个立方体,这次我们分别给三个不同的shader,并且渲染队列不同,通过上面的实验我们知道,默认情况下,不透明物体都是在Geometry这个队列中进行渲染的,那么不透明的三个物体就会按照cube1,cube2,cube3进行渲染。这次我们希望将渲染的顺序反过来,那么我们就可以让cube1的渲染队列最大,cube3的渲染队列最小。贴出其中一个的shader:
其他的两个shader类似,只是渲染队列和输出颜色不同。
通过渲染队列,我们就可以自由地控制使用该shader的物体在什么时机渲染。比如某个不透明物体的像素阶段操作较费,我们就可以控制它的渲染队列,让其渲染更靠后,这样可以通过其他不透明物体写入的深度剔除该物体所占的一些像素。
PS:这里貌似发现了个问题,我们在修改shader的时候一般不需要什么其他操作就可以直接看到修改后的变化,但是本人改完渲染队列后,有时候会出现从shader的文件上能看到渲染队列的变化,但是从渲染结果以及Frame Debug工具中并没有看到渲染结果的变化,重启Unity也没有起到作用,直到我把shader重新赋给材质之后,变化才起了效果...(猜测是个bug,因为看到网上还有和我一样的倒霉蛋被这个坑了,本人的版本是5.3.2,害我差点怀疑昨天是不是喝了,刚实验完的结果就完全不对了...)
**ZTest(深度测试)和ZWrite(深度写入) **
上一个例子中,虽然渲染的顺序反了过来,但是物体之间的遮挡关系仍然是正确的,这就是z-buffer的功劳,不论我们的渲染顺序怎样,遮挡关系仍然能够保持正确。而我们对z-buffer的调用就是通过ZTest和ZWrite来实现的。
首先看一下ZTest,ZTest即深度测试,所谓测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则否则不会写入颜色缓冲。ZTest提供的状态较多。 ZTest Less(深度小于当前缓存则通过, ZTest Greater(深度大于当前缓存则通过),ZTest LEqual(深度小于等于当前缓存则通过),ZTest GEqual(深度大于等于当前缓存则通过),ZTest Equal(深度等于当前缓存则通过),ZTest NotEqual(深度不等于当前缓存则通过),ZTest Always(不论如何都通过)。注意,ZTest Off等同于ZTest Always,关闭深度测试等于完全通过。
下面再看一下ZWrite,ZWrite比较简单,只有两种状态, ZWrite On(开启深度写入)和ZWrite Off(关闭深度写入) 。当我们开启深度写入的时候,物体被渲染时针对物体在屏幕(更准确地说是frame buffer)上每个像素的深度都写入到深度缓冲区;反之,如果是ZWrite Off,那么物体的深度就不会写入深度缓冲区。但是,物体是否会写入深度,除了ZWrite这个状态之外,更重要的是需要深度测试通过,也就是ZTest通过,如果ZTest都没通过,那么也就不会写入深度了。就好比默认的渲染状态是ZWrite On和ZTest LEqual,如果当前深度测试失败,说明这个像素对应的位置,已经有一个更靠前的东西占坑了,即使写入了,也没有原来的更靠前,那么也就没有必要再去写入深度了。所以上面的ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的话,一共就是四种情况:
1.深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区; 2.深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区; 3.深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区; 4.深度测试失败,深度写入关闭:不写深度缓冲区,不写颜色缓冲区;
Unity中默认的状态(写shader时什么都不写的状态)是ZTest LEqual和ZWrite On,也就是说默认是开启深度写入,并且深度小于等于当前缓存中的深度就通过深度测试,深度缓存中原始为无限大,也就是说离摄像机越近的物体会更新深度缓存并且遮挡住后面的物体。如下图所示,前面的正方体会遮挡住后面的物体:
写几个简单的小例子来看一下ZTest,ZWrite以及Render Queue这几个状态对渲染结果的控制。
让绿色的对象不被前面的立方体遮挡,一种方式是关闭前面的蓝色立方体深度写入:
通过上面的实验结果,我们知道,按照从前到后的渲染顺序,首先渲染蓝色物体,蓝色物体深度测试通过,颜色写入缓存,但是关闭了深度写入,蓝色部分的深度缓存值仍然是默认的Max,后面渲染的绿色立方体,进行深度测试仍然会成功,写入颜色缓存,并且写入了深度,因此蓝色立方体没有起到遮挡的作用。 另一种方式是让绿色强制通过深度测试:
这个例子中其他立方体的shader使用默认的渲染方式,绿色的将ZTest设置为Always,也就是说不管怎样,深度测试都通过,将绿色立方体的颜色写入缓存,如果没有其他覆盖了,那么最终的输出就是绿色的了。
那么如果红色的也开了ZTest Always会怎么样?
在红色立方体也用了ZTest Always后,红色遮挡了绿色的部分显示为了红色。如果我们换一下渲染队列,让绿色在红色之前渲染,结果就又不一样了:
更换了渲染队列,让绿色的渲染队列+1,在默认队列Geometry之后渲染,最终重叠部分又变回了绿色。可见,当ZTest都通过时,上一个写入颜色缓存的会覆盖上一个,也就是说最终输出的是最后一个渲染的对象颜色。
再看一下Greater相关的部分有什么作用,这次我们其他的都使用默认的渲染状态,绿色的立方体shader中ZTest设置为Greater:
这个效果就比较好玩了,虽然我们发现在比较深度时,前面被蓝色立方体遮挡的部分,绿色的最终覆盖了蓝色,是想要的结果,不过其他部分哪里去了呢?简单分析一下,渲染顺序是从前到后,也就是说蓝色最先渲染,默认深度为Max,蓝色立方体的深度满足LEqual条件,就写入了深度缓存,然后绿色开始渲染,重叠的部分的深度缓存是蓝色立方体写入的,而绿色的深度值满足大于蓝色深度的条件,所以深度测试通过,重叠部分颜色更新为绿色;而与红色立方体重合的部分,红色立方体最后渲染,与前面的部分进行深度测试,小于前面的部分,深度测试失败,重叠部分不会更新为红色,所以重叠部分最终为绿色。而绿色立方体没有与其他部分重合的地方为什么消失了呢?其实是因为绿色立方体渲染时,除了蓝色立方体渲染的地方是有深度信息的,其他部分的深度信息都为Max,蓝色部分用Greater进行判断,肯定会失败,也就不会有颜色更新。 有一个好玩的效果其实就可以考ZTest Greater来实现,就是游戏里面经常出现的,当玩家被其他场景对象遮挡时,遮挡的部分会呈现出X-光的效果;其实是在渲染玩家时,增加了一个Pass,默认的Pass正常渲染,而增加的一个Pass就使用Greater进行深度测试,这样,当玩家被其他部分遮挡时,遮挡的部分才会显示出来,用一个描边的效果渲染,其他部分仍然使用原来的Pass即可。
Early-Z技术
传统的渲染管线中,ZTest其实是在Blending阶段,这时候进行深度测试,所有对象的像素着色器都会计算一遍,没有什么性能提升,仅仅是为了得出正确的遮挡结果,会造成大量的无用计算,因为每个像素点上肯定重叠了很多计算。因此现代GPU中运用了Early-Z的技术,在Vertex阶段和Fragment阶段之间(光栅化之后,fragment之前)进行一次深度测试,如果深度测试失败,就不必进行fragment阶段的计算了,因此在性能上会有很大的提升。但是最终的ZTest仍然需要进行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check,为了检查,如下图:
Early-Z的实现,主要是通过一个Z-pre-pass实现,简单来说,对于所有不透明的物体(透明的没有用,本身不会写深度),首先用一个超级简单的shader进行渲染,这个shader不写颜色缓冲区,只写深度缓冲区,第二个pass关闭深度写入,开启深度测试,用正常的shader进行渲染。其实这种技术,我们也可以借鉴,在渲染透明物体时,因为关闭了深度写入,有时候会有其他不透明的部分遮挡住透明的部分,而我们其实不希望他们被遮挡,仅仅希望被遮挡的物体半透,这时我们就可以用两个pass来渲染,第一个pass使用Color Mask屏蔽颜色写入,仅写入深度,第二个pass正常渲染半透,关闭深度写入。
关于Early-Z技术可以参考ATI的论文Applications of Explicit Early-Z Culling以及PPT,还有一篇Intel的文章。
Unity渲染顺序总结
如果我们先绘制后面的物体,再绘制前面的物体,就会造成over draw;而通过Early-Z技术,我们就可以先绘制较近的物体,再绘制较远的物体(仅限不透明物体),这样,通过先渲染前面的物体,让前面的物体先占坑,就可以让后面的物体深度测试失败,进而减少重复的fragment计算,达到优化的目的。Unity中默认应该就是按照最近距离的面进行绘制的,我们可以看一下Unity官方的文档中显示的:
从文档给出的流程来看,这个Depth-Test发生在Vertex阶段和Fragment阶段之间,也就是上面所说的Early-Z优化。 简单总结一下Unity中的渲染顺序: 先渲染不透明物体,顺序是从前到后;再渲染透明物体,顺序是从后到前 。
Alpha Test(Discard)在移动平台消耗较大的原因
从本人刚刚开始接触渲染,就开始听说移动平台Alpha Test比较费,当时比较纳闷,直接discard了为什么会费呢,应该更省才对啊?这个问题困扰了我好久,今天来刨根问底一下。还是跟我们上面讲到的Early-Z优化。正常情况下,比如我们渲染一个面片,不管是否是开启深度写入或者深度测试,这个面片的光栅化之后对应的像素的深度值都可以在Early-Z(Z-Cull)的阶段判断出来了;而如果开启了Alpha Test(Discard)的时候,discard这个操作是在fragment阶段进行的,也就是说这个面片光栅化之后对应的像素是否可见,是在fragment阶段之后才知道的,最终需要靠Z-Check进行判断这个像素点最终的颜色。其实想象一下也能够知道,如果我们开了Alpha Test并且还用Early-Z的话,一块本来应该被剃掉的地方,就仍然写进了深度缓存,这样就会造成其他部分被一个完全没东西的地方遮挡,最终的渲染效果肯定就不对了。所以,如果我们开启了Alpha Test,就不会进行Early-Z,Z Test推迟到fragment之后进行,那么这个物体对应的shader就会完全执行vertex shader和fragment shader,造成over draw。有一种方式是使用Alpha Blend代替Alpha Test,虽然也很费,但是至少Alpha Blend虽然不写深度,但是深度测试是可以提前进行的,因为不会在fragment阶段再决定是否可见,因为都是可见的,只是透明度比较低罢了。不过这样只是权宜之计,Alpha Blend并不能完全代替Alpha Test。
关于Alpha Test对于Power VR架构的GPU性能的影响,简单引用一下官方的链接以及一篇讨论帖:
最后再附上两篇参考文章
看个人需要Shader Forge是适用于Unity基于节点的着色器编辑器,可以轻松创建自己的着色器,而不需要写代码!着色器 (Shader)应用于计算机图形学领域,指一组供计算机图形资源在执行渲染任务的时使用的指令,用于计算机图形的颜色或明暗。但近来,它也能用于处理一些特殊的效果,或者视频后处理。通俗的说,着色器告诉电脑如何用特有的一种方法去描绘物体。作为渲染器的一部分,负责计算目标的颜色,主要图形软件库有OpenGL和Direct3D。着色器就是用于绘制游戏中的物件资源,用于确定当前正在被渲染的表面颜色,透过使用纹理、光源、光色、网络数据等信息计算该处的颜色。Unity中的默认着色器固然不错,但如果您要在游戏中设计出更 加与众不同的外观,或者想达到一些非常特别效果。如果您有这种想法,那么您就需要编写自E的着色器。但是,编写自己的着色器很困难,而且学起来也很难,对于那些不是程序员出身的人来说更是如此。这就是ShaderForge发挥作用的地方。ShaderForge不需要您知道如何编写代码就能让您创建着色器! ShaderForge是一款为Unity所用的、基于节点操作的Shader插件。笔者一直想写一篇关于ShaderForge的教程,希望可以分享给想学习Shader的美术。通过该插件,美术不需要编写代码就可以制作Shader。同时笔者也希望Shader程序员能客观看待这款插件,美术效果和代码优化需要美术和程序的共同配合来完成。
本文同时发布在我的个人博客上: 有左手坐标系和右手坐标系两种,Unity使用的是右手坐标系。 我们使用两个或三个以上的实数来表示一个点的坐标,如 。 矢量是指n维空间中一种包含了模和方向的有向线段。矢量的表示方法和点类似,如 。 矢量通常由一个箭头表示,由起点指向终点。矢量常被用于表示相对于某个点的偏移,只要矢量的模和方向保持不变,无论在哪里,都是同一个矢量。 对乘法,将矢量的每个分量和标量相乘即可: 对除法,同理: ,标量需非零。 两个矢量加减法,把对应的分量进行相加或相减即可:几何意义上,矢量相加即前者起点连接至后者终点,矢量相减即后者终点指向前者终点。 矢量的模即矢量的长度:单位矢量即模为1的矢量。将某个非零矢量转化为单位矢量的过程称为归一化。矢量之间的乘法有两种,点积和叉积。 点积公式有两个: 1:2( 为两个矢量的夹角):点积的结果在几何意义上是获得矢量 在 上的投影与 的模的乘积,如果二者都是单位矢量,那么点积结果就是前者在后者上的投影。 叉积公式:下面是另一种表示:上述表明矢量叉积的结果的模是两矢量构成平行四边形的面积。 在几何意义的上,叉积的结果是一个新的矢量,分别垂直于 和 ,且沿 、 和叉积的方向可构成一个右手坐标系。 矩阵有行列之分,以 矩阵为例:矢量可以看作时 的列矩阵或 的行矩阵。 将矩阵的每个元素和标量相乘即可。 两个矩阵相乘,要求前一个矩阵的列数等于后一个矩阵的行数,相乘得到的矩阵的行数是第一个矩阵的行数,列数是第二个矩阵的列数。如 的维度是 , 的维度是 ,那么 的维度是 。 矩阵的乘法即前一个矩阵的每一行( 行)与后一个矩阵的每一列( 列)相乘(可看作行构成的矢量和列构成的矢量点积),每次相乘的结果填写在对应的 行 列上。 注意,矩阵乘法不满足交换律,但满足结合律。 方阵即行列数相等的矩阵。 同时,如果除对角线的元素外全是0的方阵称为对角矩阵。 针对上述的对角矩阵,如果对角线的元素全是0的话,那么就称为单位矩阵。 对于一个 的矩阵 ,它的转置 为 的矩阵。转置运算即将原矩阵的行列翻转。原矩阵的 行变为 列, 列变为 行。 注意,矩阵的转置的转置等于原矩阵。 矩阵的串接的转置等于反向串接各个矩阵的转置:只有方阵才有逆矩阵。 一个矩阵和它的逆矩阵的乘积为单位矩阵:零矩阵没有逆矩阵。如果一个矩阵有逆矩阵,那么这个矩阵是可逆的,或非奇异的,相反则是不可逆的或奇异的。 如果一个矩阵的行列式为不为0,那么该矩阵就是可逆的(具体不解释)。 注意,逆矩阵的逆矩阵为原矩阵。 单位矩阵的逆矩阵是它本身。 转置矩阵的逆矩阵是逆矩阵的转置:矩阵串接相乘后的逆矩阵等于反向串接各个矩阵的逆矩阵:注意,如果某个矩阵或矢量进行了一个矩阵变化,那么使用逆矩阵可以还原这个变换:如果一个方阵和它的转置矩阵的乘积是单位矩阵,那么这个方阵就是正交矩阵:将正交矩阵与逆矩阵的概念结合起来,就可以得到,如果一个矩阵正交,那么它的转置矩阵和逆矩阵相等:上述式子在实际计算中非常有用,因为逆矩阵的计算量很大(涉及伴随矩阵),所以如果能判断某个变换矩阵是正交的话,可以很简单地用转置矩阵代替它的逆矩阵进行计算。 那么如何判断一个矩阵是否正交,我们把一个矩阵的列看作是一个矢量,称为基矢量,那么根据正交矩阵定义:根据上述等式,得: , , 都是单位矢量,且两两垂直。那么满足这样条件的矩阵的就是正交矩阵。 在Unity中,常将矢量当做是列矩阵来使用,即放到矩阵的右边来进行乘法。这种情况下,矩阵乘法通常是右乘,即:变换是将一些数据通过某种方式进行转换的过程。 一个非常常见的变换是线性变换,指的是可以保留矢量加和标量乘的变换。数学公式为:缩放是一种线性变换,旋转也是一种线性变换,我们可以使用一个 的矩阵就可以对一个三维向量进行线性变换。除此之外还有错切,镜像,正交投影。 但平移变换不是线性变换,我们不能使用一个 的矩阵对一个三维向量进行变换。 由此出现仿射变换,即合并线性变换和平移变换的变换类型。仿射变换使用一个 的矩阵来表示,这样我们需要将矢量扩展到四维空间下,即齐次坐标空间。 对于一个三维向量,我们扩展一个维度,分量称为 。对于坐标, 常设为1,而对于方向,常设为0。因为我们只想对位置进行平移变换,方向不行。 我们使用一个 的矩阵来表示平移、旋转和缩放。一个基础的变换矩阵可以分解为4个部分:表示旋转缩放的矩阵, 表示平移。 下面是一个平移矩阵(针对点)的例子:如果是方向矢量的话,由于 分量为0,平移变换不会有影响:平移变换的逆矩阵就是反向平移得到的矩阵:平移矩阵并不是正交矩阵。 下面是一个缩放矩阵(针对点)的例子:对方向矢量缩放:如果 ,那么这样的缩放为统一缩放,否则为非统一缩放。由于非统一缩放会改变角度和比例信息,所以针对方向向量的缩放不能使用上述的方式。 缩放矩阵的逆矩阵如下:缩放矩阵不是正交矩阵。 上面的矩阵只适合沿坐标轴方向缩放,如果要在任意方向缩放,就要使用一个复合变换,先缩放轴,再沿坐标轴缩放。 绕x轴旋转:绕y轴旋转:绕z轴旋转:旋转矩阵是正交矩阵。 大多数情况下,将平移、旋转、缩放结合起来的顺序往往是:先缩放,后旋转,再平移,也就是:注意,针对旋转的顺序,Unity的顺序是zxy,即旋转变换矩阵是:旋转时的坐标系有两种可以选择: 在上述两种方式中,旋转的结果不一样,Unity是第一种。 模型空间即模型的局部坐标系,在Unity中是左手坐标系。 世界空间被用来描述物体在场景中的位置,在Unity中,世界空间是左手坐标系。 将顶点从模型空间变换到世界空间的变换称为模型变换(Model)。这一变换通常由 组成。 也被称为摄像机空间。 摄像机决定了我们渲染游戏所使用的视角。在观察空间中,摄像机位于原点,在Unity中,+x指向摄像机的右方,+y指向摄像机的上方,+z指向摄像机的后方,即摄像机指向-z轴。 将顶点从世界空间转换到观察空间的变换为观察变换(View)。 为得到顶点在观察空间中的位置,有两种方法:一是计算观察空间的三个坐标轴在世界空间下的表示,然后构建出从观察空间变换到世界空间的变换矩阵,再对矩阵求逆来得到从世界空间变换到世界空间的矩阵。二是想象平移整个观察空间,让摄像机的原点位于世界坐标的原点,坐标轴与世界空间的坐标轴重合即可。 这里使用第二种方法,即想办法让摄像机变换到世界空间的原点即可(注意,由于观察空间使用右手坐标系,因此需要对z分量取反)。 将顶点从观察空间转换到裁剪空间的变换矩阵称为裁剪矩阵,也被称为投影矩阵(Projection)。裁剪空间由视锥体决定。 视锥体是空间中的一块区域,视锥体内的区域是摄像机可以看到的空间。视锥体由六个平面构成,这些平面称为裁剪平面。视锥体有两种类型,透视投影和正交投影。 在视锥体的裁剪平面中有两块很特殊,分别称为近裁剪平面和远裁剪平面,它们决定了摄像机可以看到的深度范围。 投影矩阵有两个目的: 我们利用上图的参数可以计算出近裁剪平面和远裁剪平面的高度:裁剪平面的宽度我们可以通过横纵比得到。假设摄像机的横纵比为 :根据上述信息,投影矩阵为:使用上述投影矩阵后:可以看到,投影矩阵本质上是对x、y、z坐标进行一些缩放,同时z分量也进行了平移,缩放的目的是为了方便裁剪。此时w分量不再是1,而是-z,通过这个w,我们可以判断一个顶点是否在视锥体内,在的话必须满足下面条件:透视投影后,空间从右手坐标系变为左手坐标系。假设横纵比为 ,那么:正交投影矩阵:使用正交投影矩阵后:使用正交投影后,w分量仍是1。 这一步的变换,我们会得到真正的像素位置。 将顶点从裁剪空间投影到屏幕空间有两个步骤:首先进行透视除法,用x、y、z分量除以w分量。这一步得到的坐标被称为NDC,经过透视除法的裁剪空间变换到一个立方体内: 对于正交投影没什么区别。 在Unity中,屏幕空间左下角的像素坐标是(0,0)。接下来就是将NDC缩放值屏幕坐标系。 透视除法和屏幕映射的过程总结如下:法线是一个方向矢量,垂直于表面,与其垂直的一个矢量被称为切线。 切线由两个顶点之间的差值计算得来,因此可以使用变换顶点的变换矩阵来变换切线。假设不考虑平移变换,变换后的切线为:不过,如何直接这么变换的话,法线就有可能不垂直于表面了: 下面我们可以来推导一下正确的法线变换矩阵。 首先,切线与法线垂直,即 ,假设变换法线的矩阵为 ,变换后法线仍与切线垂直,那么:经推导得:由于 ,如果 ,则上式成立,即 。 如果变换矩阵 是正交矩阵,那么G = M_{A->B},当然,这限于只包含旋转变换。如果只包含旋转和统一缩放,那么 。 内置变量: Built-in shader variables 内置方法: Built-in shader helper functions
247 浏览 3 回答
111 浏览 5 回答
321 浏览 2 回答
91 浏览 4 回答
237 浏览 3 回答
358 浏览 2 回答
136 浏览 3 回答
140 浏览 5 回答
248 浏览 3 回答
262 浏览 3 回答
107 浏览 3 回答
115 浏览 2 回答
248 浏览 4 回答
288 浏览 3 回答
338 浏览 3 回答