本文重点内容:
1、变换顶点 2、给像素着色 3、使用Shader属性 4、顶点向片元传递数据 5、检查编译着色器代码 6、采样贴图,带有平铺和偏移(tiling and offset)
这是渲染系列的第二篇文章,第一篇讲述的是矩阵,这次我们会写我们的第一个Shader并且导入一张纹理。
教程使用Unity5.4.0b10。
(纹理化一个球体)
1 默认场景
在Unity中创建新场景时,会带有一个默认的相机和一个定向光。通过GameObject/ 3D Object/ Sphere创建一个简单的球体,将其放在原点,然后将摄影机放置在它的前面。
(默认的场景里有一个默认的球体)
这是一个非常简单的场景,但其实已经发生了许多复杂的渲染。为了更好地控制渲染过程,它需要摆脱多余的花哨的东西,那么首先来关注一下我们的基础方面。
1.1 剥离
通过“Window / Lighting ”查看场景的照明设置。这将打开一个带有三个选项卡的照明窗口。我们只对“Scene”选项卡感兴趣,该选项卡默认情况下处于活动状态。
(默认的灯光设置)
这里有一个关于环境照明的部分,你可以在其中选择一个天空盒。该天空盒会作用于场景背景,环境照明和反射。先将其设置为none,以便将其关闭。
使用它时,你还可以关闭Precomputed Realtime GI。因为我们不会这么快就用到它们。
(没有天空盒了)
如果没有天空盒,环境光源将自动切换为纯色。默认颜色为深灰色,略带蓝色。如截图所示,反射则变为纯黑色。
正如你看到的那样,球体会变更暗,背景现在变为纯色。但是,背景现在为深蓝色,这颜色从哪里来的呢?
(简单的光照)
每个摄像机定义了背景色。默认情况下,它会渲染天空盒,但它也可以回退到纯色。
(默认的摄像机设置)
为什么背景色的alpha值为5,而不是255? 真的不知道为什么这是默认值。但没关系。此颜色会完全替代之前的图像,并且它不会发生混合。
为了进一步简化渲染,请禁用定向光源对象或将其删除。这将消除场景中的直接照明以及由此产生的阴影。剩下的是纯色背景,球体的轮廓为环境颜色。
(黑暗中)
2 从物体到图像
我们这个非常简单的场景分成了两步绘制。首先,用相机的背景色填充图像。然后在此之上绘制球体的轮廓。
Unity如何知道必须绘制一个球体的呢?我们有一个球体对象,这个对象有一个网格渲染器(mesh renderer)组件。如果此对象位于相机的视图内,则应进行渲染。Unity通过检查对象的包围盒(bounding box )是否与摄影机的视锥相交来完成验证。
什么是包围盒? 拿到任何一个网格。找出适合该网格的最小的立方体。就是一个包围盒。它是自动从对象的网格中生成出来的。 你可以认为包围盒是网格所占体积的简单近似值。如果看不到该盒,则肯定看不到网格。
(默认的球体)
变换(transform )组件用于更改网格和包围盒的位置,方向和大小。实际上,如第1部分“矩阵”中所述,使用了整个转换层次结构。如果对象最终出现在相机的视图中,则安排进行渲染。
最后,GPU的任务是渲染对象的网格。具体的渲染说明由对象的材质定义。该材质引用了着色器(它是GPU程序)及其可能具有的任何设置。
(分工明确)
我们的球体对象当前具有默认材质,该材质使用Unity的标准着色器。之后将用我们自己的着色器来替换它,接下来会从头开始构建一个自定义着色器。
2.1 你的第一个着色器
通过“Assets/Create/Shader / Unlit Shader”着色器创建一个新的着色器,并将其命名为“My First Shader”。
(你的第一个着色器)
打开着色器文件并删除所有内容,因为我们要从头开始。
着色器由Shader关键字定义。它后面是描述该着色器菜单项的字符串,可用于选择该着色器(不需要匹配文件名),之后是带有着色器内容的块。
保存文件。你将收到一条警告,指出它是不支持的着色器,因为它没有子着色器或fallbacks。那是因为现在它还是空的。
尽管着色器没有任何作用,但我们已经可以将其分配给材质了。因此,通过“Assets/ Create / Material ”创建新材质,然后从材质球菜单中选择我们的材质球。
(用你自己着色器的材质球)
更改我们的球体对象,使其使用我们自己的材质,而不是默认材质。球体将变为洋红色。发生这种情况是因为Unity切换到错误的着色器了,该着色器使用此颜色来引起你对问题的注意。
(自定义着色器的渲染效果)
着色器编译错误提到了子着色器。你可以使用它们将多个着色器变体组合在一起。这使你可以为不同的构建平台或详细程度提供不同的子着色器。例如,你可能有一个子着色器用于PC,而另一个则用于移动设备。这里我们只需要一个子着色器块。
子着色器必须包含至少一个通道(pass)。着色器通道是实际渲染对象的地方。我们将使用一个通道,但允许有多个。进行一次以上的通道意味着该对象将被多次渲染,这是许多效果所必需的。
现在,由于我们使用的是空的通道的默认行为,因此我们的球体可能会变成白色。如果发生这种情况,则意味着我们不再有任何着色器编译错误了。但是,你可能仍会在控制台中看到残留的错误。它们往往会残留在哪里,而在着色器无错误重新编译时没有被清除。
(一个白色的球体)
2.2 着色器程序
现在是时候编写我们自己的着色器程序了。我们使用Unity的着色语言来实现,它是HLSL和CG着色语言的一种变体。我们必须使用CGPROGRAM关键字来指示代码的开始。并且我们必须以ENDCG关键字终止。
为什么需要这些关键字? 着色器通道可以包含除着色器程序以外的其他语句。因此,程序必须以某种方式分开。
那为什么不使用另一个块呢? 不知道。你后面还会遇到更多这样的奇怪情况。它们通常是曾经一些已经过时的设计决策。由于需要向后兼容,所以,我们仍然需要使用它们。
着色器编译器现在编译错误,说我们的着色器没有顶点和片段程序。着色器包含两个程序,顶点程序负责处理网格的顶点数据。就像我们在第1部分“矩阵”中所做的那样,这包括从对象空间到显示空间的转换。片段程序负责为位于网格三角形内部的单个像素着色。
(顶点和像素着色器)
我们必须通过编译指示来告诉编译器要使用哪些程序。
pragma是啥? pragma这个词来自希腊语,指的是一项行动或需要完成的事情。许多编程语言都使用它来发出特殊的编译器指令。
这次,编译器再次报错,说它找不到我们指定的程序。那是因为我们还没有定义它们。
顶点程序和片段程序被编写为方法,就像在C#中一样,通常也被称为函数。让我们简单地创建两个具有适当名称的空void方法。
此时,着色器将正常编译,但球体将消失。如果没消失,说明你的编译仍然有错。这取决于你的编辑器使用哪个渲染平台。如果使用的是Direct3D 9,则可能会收到错误消息。
2.3 着色器编译
Unity的着色器的编译器采用我们的代码,并将其转换为其他程序,具体取决于目标平台。不同的平台需要不同的解决方案。例如,适用于Windows的Direct3D,适用于Mac的OpenGL,适用于手机的OpenGL ES等。这里我们不处理单个编译器,而是多个。
最终使用哪个编译器取决于你的目标。而且由于这些编译器不完全相同,因此每个平台最终可能会有不同的结果。例如,我们的空程序可以在OpenGL和Direct3D 11上正常运行,但是在Direct3D 9时会失败。
在编辑器中选择着色器,然后查看检查器窗口。它显示有关着色器的一些信息,包括当前的编译器错误。还有一个带有“编译并显示代码”按钮和下拉菜单的“已编译代码”条目。如果单击该按钮,Unity将编译着色器并在编辑器中打开其输出,以便你可以检查生成的代码。
(Shader的展示器,显示了所有平台都有错误)
你可以通过下拉菜单选择手动为其编译着色器的平台。默认为编译器使用的图形设备进行编译。你也可以手动为其他平台进行编译,包括当前的构建平台,拥有许可证的所有平台或自定义选择。这使你就可以快速确保着色器可以在多个平台上编译,而不必进行完整的构建。
(手动选择)
要编译所选程序,请关闭弹出窗口,然后单击“Compile and show”按钮。单击弹出窗口中的小“Show”按钮,将为你显示使用的着色器变体,此功能现在无用。
例如,这是为OpenGlCore编译我们的着色器时的结果代码。
对于顶点和片段程序,生成的代码被分为两个块,vp和fp。但是,对于OpenGL,两个程序都以vp块结尾。这两个主要功能对应于我们的两个空方法。因此,让我们专注于主要功能,而忽略其他代码。
这是Direct3D 11的生成代码,剔除一些没用的代码之后,看起来有很大的不同,但是很明显,代码并没有做太多事情。
在后面处理程序时,我经常会展示OpenGLCore和D3D11的编译代码,以便大家可以了解幕后的情况。
2.4 包含其他文件
要生成功能强大的着色器,你需要很多模板代码。定义公用变量,函数和其他内容的代码。如果这是一个C#程序,我们会将代码放在其他类中。但是着色器没有类。它们只是所有代码的一个大文件,没有类或名称空间提供的分组。
幸运的是,我们可以将代码分成多个文件。你可以使用#include指令将其他文件的内容加载到当前文件中。要包含的典型文件是UnityCG.cginc,所以让我们开始吧。
UnityCG.cginc是与Unity捆绑在一起的着色器包含文件之一。它包括其他一些基本文件,并包含一些常规功能。
(从UnityCG开始包含文件层次结构)
UnityShaderVariables.cginc定义了渲染所需的一堆着色器变量,例如变换,相机和光照数据。这些都在需要时由Unity设置。
HLSLSupport.cginc进行了设置,因此无论代码针对的是哪个平台,都可以使用相同的代码进行编写。无需担心使用特定于平台的数据类型等。
UnityInstancing.cginc专门用于实例化支持,这是一种减少绘制调用的特定渲染技术。尽管它不直接包含文件,但依赖于UnityShaderVariables。
请注意,这些文件的内容将有效地复制到你自己的文件中,从而替换了include指令。这发生在执行所有预处理指令的预处理步骤中。这些指令都是以#开头的所有语句,例如#include和#pragma。完成该步骤后,再次处理代码,并对其进行实际编译。
如果多次包含同一个文件会发生什么? 它的内容会多次复制到你的代码中。通常,你不想这样做,因为重复的定义很可能会导致编译器错误。
有一个包含文件编程约定,可以防止重新定义。当我们编写自己的包含文件时,将使用它。但这是后面的教程内容。
2.5 产出
要渲染某些东西,我们的着色器程序需要产生一些结果。顶点程序必须返回顶点的最终坐标。那是多少个坐标呢?四个,因为我们正在使用4 x 4转换矩阵,如第1部分,矩阵中所述。
将函数的类型从void更改为float4。float4只是四个浮点数的集合。现在返回0。
0这个返回值有效值吗? 当使用这样的单个值时,编译器将对所有float组件重复该值。你也可以是显式的,并根据需要返回float4(0,0,0,0)。
现在,我们收到有关缺少语义的错误。编译器看到我们正在返回四个浮点数的集合,但是它不知道该数据代表什么。因此,它不知道GPU应该如何处理。我们必须对程序的输出非常具体。
在这种情况下,我们试图输出顶点的位置。我们必须通过将SV_POSITION语义附加到我们的方法来表明这一点。SV代表系统值,POSITION代表最终顶点位置。
片段程序应该为一个像素输出RGBA颜色值。我们也可以为此使用float4。返回0将产生可靠的返回。
alpha为0不会完全透明吗? 除非我们的着色器实际上忽略了Alpha通道,不然肯定会。因为我们正在使用不透明的着色器。但如果我们编写的是支持透明度的着色器,这个结果就会是透明的。我们将在以后的教程中进行介绍。
片段程序也需要语义。在这种情况下,我们必须指出最终颜色应写入的位置。我们使用SV_TARGET,这是默认的着色器目标。是帧缓冲区,其中包含我们正在生成的图像。
但是,顶点程序的输出将用作片段程序的输入。这表明片段程序应获取与顶点程序的输出匹配的参数。
给参数指定什么名称都没有关系,但是我们必须确保使用正确的语义。
可以省略位置参数吗? 由于我们不使用它,因此我们最好将其省略。但是,当涉及多个参数时,这会使某些着色器编译器感到困惑。因此,最好将片段程序输入与顶点程序输出完全匹配起来。
我们的着色器再次编译没有错误,但是球体消失了。这并不奇怪,因为我们将其所有顶点折叠到一个点上了。
如果查看已编译的OpenGLCore程序,你会发现它们现在写入输出值。而且我们的单值确实已被四分量向量取代。
尽管语法不同,但对于D3D11程序也是如此。
2.6 变换顶点
为了使球体恢复原状,我们的顶点程序必须产生正确的顶点位置。为此,需要知道顶点的对象空间位置。可以通过在函数中添加具有POSITION语义的变量来访问它。然后将位置提供为以下形式的齐次坐标
,所以它的类型为float4 。
直接返回该位置试试。
现在,已编译的顶点程序将具有一个顶点输入并将其复制到其输出。
(原始顶点位置)
黑色球体将变为可见,但会变形。这是因为我们将对象空间位置当作显示位置使用。因此,在视觉上移动球体不会产生任何影响。
我们必须将原始顶点位置与模型-视图-投影(MVP)矩阵相乘。该矩阵将对象的变换层次结构与摄影机的变换和投影结合在一起,就像我们在第1部分“矩阵”中所做的那样。
4×4 MVP矩阵在UnityShaderVariables中定义为UNITY_MATRIX_MVP。我们可以使用mul函数将其与顶点位置相乘。这将正确地将我们的球体投影到显示器上。你还可以移动,旋转和缩放它,图像都会按预期更改。
(正确的位置)
如果你检查OpenGLCore顶点程序,你会注意到许多uniform 变量突然出现。即使未使用它们,它们也将被忽略,但访问矩阵会触发编译器以包含全部代码。
什么是uniform 变量? uniform表示变量对网格的所有顶点和片段具有相同的值。因此,它在所有顶点和片段上都是统一的。
你可以在自己的着色器程序中将变量显式标记为统一变量,但这不是必需的。
你还将看到矩阵乘法,编码为一堆乘法和加法。
D3D11编译器不包含未使用的变量。它使用mul和三个mad指令对矩阵乘法进行编码。mad指令表示一个乘法,后跟一个加法。
3 给像素上色
现在形状正确了,让我们添加一些颜色。最简单的方法是使用恒定的颜色,例如黄色。
(黄色的球体)
当然,你并不是一直需要黄色物体。理想情况下,我们的着色器可以支持任何颜色。然后,你可以使用材质来配置要应用的颜色。这是通过着色器属性完成的。
3.1 着色器属性
着色器属性在单独的块中声明。将其添加到着色器的顶部。
在新块内放置一个名为_Tint的属性。可以给它起任何名字,但是习惯上是用下划线开头,后跟一个大写字母,后跟小写字母。这并不是规定,而是约定俗成,可以防止意外的重复名称。
属性名称后必须加上括号后的字符串和类型,就像调用方法一样。该字符串用于在材质检查器中标记属性。此时,它的类型为颜色。
属性声明的最后一部分是默认值的分配。让我们将其设置为白色。
现在,我们的着色属性应显示在着色器检查器的“properties”部分中。
(着色器属性)
选择材质后,你将看到新的“Tint ”属性,设置为白色。你可以将其更改为任何喜欢的颜色,例如绿色。
3.2 访问属性
要实际使用该属性,我们必须向着色器代码添加一个变量。它的名称必须与属性名称完全匹配,因此它将为_Tint。然后,我们可以简单地在片段程序中返回该变量。
请注意,必须先定义变量,然后才能使用它。C#类中可以毫无顾及地更改中的字段和方法的顺序,但对于着色器而言并非如此。编译器从上到下工作。它不会向前看。
现在,已编译的片段程序包括tint变量。
(绿色的球)
3.3 从顶点到片元
到目前为止,我们已经为所有像素提供了相同的颜色,但这是非常有限的。通常,顶点数据起着重要作用。例如,我们可以将位置解释为颜色。但是,转换后的位置不是很有用。因此,让我们改为使用网格中的局部位置作为颜色。但如何将多余的数据从顶点程序传递到片段程序呢?
GPU通过栅格化三角形来创建图像。它需要三个已处理的顶点并在它们之间进行插值。对于三角形所覆盖的每个像素,它将调用片段程序,并传递插值数据。
(插值顶点数据)
因此,顶点程序的输出根本不直接用作片段程序的输入。插值过程介于两者之间。在这里是SV_POSITION数据被插值,但是其他东西也可以插值。
要访问插补的局部位置,请将参数添加到片段程序中。因为我们只需要X,Y和Z组件,所以我们可以用float3。然后,我们可以输出位置,就好像它是一种颜色一样。我们必须提供第四个颜色分量,该颜色分量可以简单地保持为1。
再一次,我们必须使用语义来告诉编译器如何解释此数据。我们将使用TEXCOORD0。
我们并没有使用纹理坐标,为什么要使用TEXCOORD0? 插值数据没有通用语义。每个人都只对插入的所有内容(而不是顶点位置)使用纹理坐标语义。TEXCOORD0,TEXCOORD1,TEXCOORD2等。出于兼容性原因完成了此操作。
还有一些特殊的颜色语义,但是很少使用,并且并非在所有平台上都可用。
现在,已编译的片段着色器将使用插值数据而不是统一色调了。
当然,顶点程序必须输出本地位置才能起作用。我们可以通过添加具有相同TEXCOORD0语义的输出参数来做到这一点。顶点和片段函数的参数名称不需要匹配。这都是关于语义的。
要通过顶点程序传递数据,请将X,Y和Z分量从position复制到localPosition。
.xyz是做什么的? 这被称为swizzle操作。就像访问向量的单个组件一样,但是更加灵活。你可以使用它来过滤,重新排序和重复浮动组件。例如.x,.xy,.yx,.xx。在这种情况下,我们使用它来获取头三个分量,而忽略了第四个。所有四个组件均为.xyzw。你也可以使用颜色命名约定,例如.rgba。
额外的顶点程序输出将包含在编译器着色器中,我们将看到球体着色。
(把局部坐标的位置作为颜色的插值)
3.4 使用结构体
现在,我们程序的参数列表看起来是不是很乱?随着我们之间传递越来越多的数据,情况只会变得更糟。由于顶点输出应与片段输入匹配,因此如果可以在一个地方定义参数列表,将非常方便。幸运的是,我们可以做到。
我们可以定义数据结构,它只是变量的集合。类似于C#中的结构,但语法略有不同。这是一个定义我们要插值的数据的结构。注意定义后使用分号。
使用这种结构会使我们的代码更加整洁。
3.5 调整颜色
因为负色被限制为零,所以我们的球体最终变得很暗。由于默认球体的对象空间半径为½,因此颜色通道的最终位置介于-½至½之间。我们想将它们移到0–1范围内,我们可以通过将½加到所有通道来实现。
(重新上色)
我们也可以通过将其加入到结果中来应用我们的色彩。
(具有红色调的本地位置,因为仅保留了X)
4 纹理化
如果要向网格添加更多明显的细节和变化,而又不添加更多三角形,则可以使用纹理。然后将图像投影到网格三角形上。
纹理坐标用于控制投影。这些是二维坐标对,它们以一个单位的正方形区域覆盖整个图像,而不管纹理的实际纵横比如何。水平坐标称为U坐标,垂直坐标称为V。因此,它们通常称为UV坐标。
(一张图片的UV坐标)
U坐标从左到右增加。因此,在图像的左侧为0,在一半处为1/2,在右侧为1。V坐标在垂直方向上的工作方式相同。它从下到上增加,但Direct3D除外,它从上到下。但你几乎不需要担心这种差异。
4.1 使用UV坐标
Unity的默认网格物体具有适合纹理贴图的UV坐标。顶点程序可以通过具有TEXCOORD0语义的参数访问它们。
我们的顶点程序现在使用多个输入参数。再一次,我们可以使用一个结构对其进行分组。
让我们直接将UV坐标传递给片段程序,替换本地位置。
通过将UV坐标解释为颜色通道,可以使它们像局部位置一样可见。例如,U变为红色,V变为绿色,而蓝色始终为1。
你将看到已编译的顶点程序现在将UV坐标从顶点数据复制到插值器输出。
Unity将UV坐标围绕其球体包裹,使图像的顶部和底部在极点处折叠。你会看到一个从北到南极的接缝,图像的左右两侧相连。因此,沿着该接缝,你将拥有0和1的U坐标值。这是通过在接缝上具有重复的顶点来实现的,除了它们的U坐标外,这些顶点是相同的。
(UV作为颜色,正面和上方)
4.2 添加纹理
要添加纹理,你需要导入图像文件。下面我将用于测试目的的一个纹理。
(测试纹理)
你可以通过将图像拖到项目视图中来将其添加到项目中。也可以通过“Asset/ Import New Asset...”菜单项执行此操作。使用默认设置将图像导入为2D纹理就可以了。
(使用默认设置导入纹理)
要使用纹理,我们必须添加另一个着色器属性。常规纹理属性的类型是2D,因为还有其他类型的纹理。默认值是一个字符串,引用Unity的默认纹理之一,可以是白色,黑色或灰色。
一般约定主纹理叫_MainTex,我们也这样命名。如果需要的话,你也可以使用方便的Material.mainTexture属性通过脚本访问它。
大括号是做什么用的? 以前,旧的固定功能着色器具有纹理设置,但现在不再使用。这些设置就是放在这些括号内。
即使它们现在不再有用,着色器编译器仍然期望有它们,如果省略,可能会产生错误。具体来说,如果你在缺少{}的纹理参数之后放置非纹理参数,则会出错。也许在将来的Unity版本中省略它们是安全的。
现在,我们可以通过拖动或通过“Select ”按钮将纹理分配给我们的材质。
(材质选取纹理)
使用类型为sampler2D的变量访问着色器中的纹理。
通过使用tex2D函数,在片段程序中对具有UV坐标的纹理进行采样。
(纹理化球体)
现在已经为每个片段采样了纹理,它将显示在球体上。正如预期的那样,它包裹着它,但是在两极附近它会显得非常不稳定。为什么会这样呢?
发生纹理变形是因为插值在三角形之间是线性的。Unity球体在极点附近只有几个三角形,其中UV坐标变形最大。因此,UV坐标在顶点之间非线性地变化,但是在顶点之间,它们的变化是线性的。结果,纹理中的直线突然在三角形边界处改变了方向。
(跨三角形的线性插值)
不同的网格具有不同的UV坐标,从而产生不同的贴图。Unity的默认球体使用经度-纬度纹理映射,而网格是低分辨率的立方体球体。但这足以进行测试,如果使用自定义球体网格则可以获得更好的结果。
(不同的纹理预览形状)
最后,我们可以考虑色调以调整球体的纹理外观。
(带有黄色色调)
4.3 平铺和偏移
将材质属性添加到着色器后,材质检查器不仅添加了纹理字段。它还添加了平铺和偏移控件。但是,更改这些2D向量现在还没有效果。
这些额外的纹理数据存储在材质中,也可以由着色器访问。你可以通过与关联材质具有相同名称的变量加上_ST后缀来执行此操作。此变量的类型必须为float4。
_ST是什么意思? _ST后缀代表“缩放”和“平移”或类似名称。为什么不使用_TO来指代平铺和偏移?因为Unity一直使用_ST,并且向后兼容要求它保持这种方式,哪怕术语可能已更改了。
tiling 向量用于缩放纹理,因此默认情况下为(1,1)。它存储在变量的XY部分中。要使用它,只需将其与UV坐标相乘即可。这可以在顶点着色器或片段着色器中完成。在顶点着色器中执行此操作很有意义,因此我们仅对每个顶点执行乘法,而不是对每个片段执行乘法。
(Tiling)
偏移部分使纹理移动,并存储在变量的ZW部分中。缩放后将其添加到UV中。
(Offset)
UnityCG.cginc包含一个方便的宏,可为我们简化此样板。我们可以将其用作方便的速记。
什么是宏? 宏类似于一个函数,在预处理代码阶段之前对其进行展开,然后对展开后的代码进行编译。这允许对代码进行文本操作,例如将_ST附加到变量名。TRANSFORM_TEX宏使用此技巧。如果你好奇的话,可以看看它的定义。
宏启用了各种巧妙的技巧,但也可能导致难以理解的代码和非常讨厌的错误。这就是为什么C#没有宏的原因。
我们将在以后的教程中创建自己的宏。
5 纹理设置
到目前为止,我经使用的是默认的纹理导入设置。让我们看一下其中的一些选项,看看它们的作用。
(默认的导入设置)
Wrap Mode 决定了使用UV坐标在0–1范围之外进行采样时会发生什么。当设置为“clamped”时,将限制UV使其保持在0–1范围内。这意味着边缘以外的像素与边缘上的像素相同。当设置为repeat时,UV会环绕。这意味着边缘以外的像素与纹理相反侧的像素相同。默认模式是重复纹理,从而使其平铺。
如果你不是平铺纹理,则需要 clamp UV坐标。这样可以防止纹理重复,它将复制纹理边界,从而导致纹理看起来很拉伸。
(Tiling 为 (2, 2) 模式为clamped)
保持在0–1范围内时,Wrap模式有关系吗?
当UV坐标接触0和1边界时,这很重要。使用双线性或三线性滤波时,在对纹理进行采样时会对相邻像素进行插值。这对于纹理中间的像素很好。但是,位于边缘的像素的相邻像素是什么?答案取决于自动换行模式。
clamped,边缘上的像素会相互融合。这会产生一个很小的区域,像素不融合,但并不明显。
重复时,边缘的像素将与纹理的另一侧融合。如果两边不相似,你会注意到另一边有一点渗入边缘。放大测试纹理的四边形的一角,以查看差异。
(边上的 Tiling)
5.1 Mipmaps和Filtering
当纹理的像素(纹理像素)与投影到的像素不完全匹配时会发生什么?存在不匹配,必须以某种方式解决。如何完成此操作由“Filter Mode ”控制。
最直接的过滤模式是Point (无过滤器)。这意味着当在某些UV坐标处采样纹理时,将使用最近的纹理像素。除非纹理像素精确映射到显示像素,否则这将使纹理具有块状外观。因此,它通常用于像素完美的渲染,或者在需要块状样式时使用。
默认为使用双线性(bilinear filtering)过滤。在两个纹理像素之间的某个位置对纹理进行采样时,将对这两个纹理像素进行插值。由于纹理是2D的,因此沿U轴和V轴都会发生。因此,它是双线性过滤,而不仅仅是线性过滤。
当纹理像素密度小于显示像素密度时,此方法有效,因此当你放大纹理时,结果看起来会很模糊。当你缩小纹理时,它在相反的情况下不起作用。相邻的显示像素最终将获得相距一个以上纹理像素的样本。这意味着将跳过纹理的某些部分,这会导致剧烈的过渡,就像图像被锐化一样。
解决此问题的方法是,每当纹理像素密度变得太高时,都使用较小的纹理。显示屏上出现的纹理越小,应使用的版本越小。这些较小的版本称为mipmap,并且会自动为你生成。每个连续的Mipmap的宽度和高度均为上一个级别的一半。因此,当原始纹理大小为512x512时,mip映射为256x256、128x128、64x64、32x32、16x16、8x8、4x4和2x2。
mipmap是什么意思? 单词mipmap是MIP地图的缩写。字母MIP代表拉丁语multum in parvo,在狭小空间中转换为多种语言。这是兰斯·威廉姆斯(Lance Williams)首次描述点胶(mipmapping)技术时创造的。
(Mipmap级别)
你可以根据需要禁用Mipmap。首先,将“Texture Type ”类型设置为“Advanced ”。然后就可以禁用mipmap并应用更改。观察差异的一种好方法是使用一个类似四边形的平面对象,并从一个角度观察它。
(有mipmap和没有mipmap)
那么应该在哪里使用了哪个mipmap级别呢?它们看起来有什么不同?我们可以通过在高级纹理设置中启用Fadeout Mip Maps 来使过渡可见。启用后,“Fade Range ”滑块将显示在检查器中。它定义了一个mipmap范围,在该范围内,mipmap将过渡为纯灰色。只需一步就可以完成过渡,就可以实现向灰色的过渡。将单步范围向右移动得越远,转换就会越晚。
(mipmap的高级设置)
为什么淡化到灰色? 它用于细节纹理,我们将在以后的教程中进行介绍。
你可能会认为它可以用于雾化效果,但是事实并非如此。使用哪种mipmap取决于纹理像素与显示像素密度,而不是3D距离。
(连续的mipmap级别)
一旦知道了各种mipmap级别在哪里,就应该能够看到它们之间的纹理质量突然变化。随着纹理投影的变小,纹理像素密度增加,这使其看起来更清晰。直到突然出现下一个mipmap级别,然后又变得模糊。
因此,如果没有mipmap,你将会从模糊变为锐利,甚至变得过于锐利。使用mipmap,可以从模糊变成锐利,再到突然变得模糊,再到锐利,再到突然变得模糊,依此类推。
这些模糊的锐利边界是双线性滤波的特征。你可以通过将过滤器模式切换为Trilinear来摆脱它们。此功能与双线性过滤相同,但也可以在相邻的mipmap级别之间进行插值。因此是三线性的。这使采样更加昂贵,但可以平滑mipmap级别之间的转换。
(正常和灰色Mipmap之间的三线性过滤)
另一种有用的技术是各向异性过滤。你可能已经注意到,将其设置为0时,纹理变得模糊。这与选择mipmap级别有关。
各向异性是什么意思?
粗略地说,当事物在不同方向上看起来相似时,则各向同性。例如,无特征的立方体。如果不是这种情况,则是各向异性的。例如,一块木头,因为它的纹理沿一个方向而不是另一个方向。
当纹理由于角度而投影成一个透视角度时,通常会导致其一个维度比另一个维度变形更大。一个很好的例子是带纹理的地平面。在一定距离处,纹理的前后尺寸将比左右尺寸小得多。
选择哪个mipmap级别是基于最差的尺寸。如果差异很大,那么你将获得一维非常模糊的结果。各向异性过滤通过解耦尺寸来减轻这种情况。除了均匀缩小纹理外,它还提供在两个维度上缩放不同数量的版本。因此,您不仅拥有256x256的mipmap,而且还有256x128、256x64等的mipmap。
(没有和有各向异性过滤)
请注意,这些额外的Mipmap不会像常规Mipmap那样预先生成。而是通过执行额外的纹理样本来模拟它们。因此,它们不需要更多空间,但采样成本更高。
(各向异性双线性滤波,过渡为灰色)
各向异性过滤的深度由Aniso Level控制。设为0时,禁用。为1时,它将启用并提供最小的效果。在16,它达到最大。但是,这些设置受项目质量设置的影响。
你可以通过Edit/ Project Settings/ Quality 访问质量设置。在“Rendering ”部分中找到“Anisotropic Textures setting”设置。
禁用各向异性纹理后,无论纹理的设置如何,都不会进行各向异性过滤。设置为“Per Texture ”时,它由每个单独的纹理完全控制。也可以将其设置为“Forced On ”,这就像将每个纹理的“ Aniso Level”设置为至少9一样。但是,“ Aniso Level”设置为0的纹理仍不会使用各向异性过滤。
下一章节介绍 组合纹理。