前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基础渲染系列(二)——着色器

基础渲染系列(二)——着色器

作者头像
放牛的星星
发布2020-07-10 17:12:00
3.8K0
发布2020-07-10 17:12:00
举报
文章被收录于专栏:壹种念头壹种念头

本文重点内容:

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的纹理仍不会使用各向异性过滤。

下一章节介绍 组合纹理。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 默认场景
    • 1.1 剥离
    • 2 从物体到图像
      • 2.1 你的第一个着色器
        • 2.2 着色器程序
          • 2.3 着色器编译
            • 2.4 包含其他文件
              • 2.5 产出
                • 2.6 变换顶点
                • 3 给像素上色
                  • 3.1 着色器属性
                    • 3.2 访问属性
                      • 3.3 从顶点到片元
                        • 3.4 使用结构体
                          • 3.5 调整颜色
                          • 4 纹理化
                            • 4.1 使用UV坐标
                              • 4.2 添加纹理
                                • 4.3 平铺和偏移
                                • 5 纹理设置
                                  • 5.1 Mipmaps和Filtering
                                  相关产品与服务
                                  图像处理
                                  图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档