查看原文
其他

程序丨Unity 渲染系列教程(三):使用多张纹理贴图

2017-06-27 张乾光 Gad-腾讯游戏开发者平台

译者:张乾光(星际迷航)

审校:崔国军(飞扬971)


系列回顾:

Unity 渲染教程(一):矩阵

Unity 渲染教程(二):着色器基础


  • 对多张纹理贴图进行采样。

  • 使用细节纹理贴图。

  • 在线性空间中处理颜色

  • 使用splat贴图。


这是关于渲染基础的系列教程的第三部分。前面两个部分介绍了着色器和纹理。我们已经看到了如何使用单个纹理来让平坦表面看起来更复杂。现在我们继续向前进,试下同时使用多个纹理。


这个系列教程是使用Unity 5.4.0开发的,这个版本目前还是开放测试版本。我使用的是build5.4.0b10版本。


同时使用多个纹理。


细节贴图


纹理对于渲染的效果是很好的,但是纹理也有自己的局限性。纹理的像素数目是固定数量的,无论它们显示出来的尺寸是什么。如果它们被渲染得很小的时候,我们可以使用mipmap来尽量让纹理看起来还不错。


但是当纹理被渲染得很大的时候,它们会变得模糊。我们不能无条件地提供额外的细节,所以没有办法处理放大的问题。或者是否有其他办法可以处理纹理被放大的问题?


当然,我们肯定可以使用更大的纹理。而更多的纹理的像素意味着更多的细节。但是纹理的大小有限制。而且存储大量额外的数据是很浪费的,这些数据只能在用户很靠近的时候才会注意到。

增加纹理的像素密度的另一种方法是平铺纹理。


然后你可以得到你所期望的一个很小的纹理,但你会明显得到一个重复的模式。虽然这可能在很靠近看的时候不是很明显。毕竟,当你站在你的鼻子就能触到一堵墙的位置的时候,你只会看到整个墙一个非常小的一部分。


因此,我们应该能够通过将一个没有进行平铺操作的纹理与一个进行了平铺操作的纹理组合来添加细节。为了尝试这个组合效果,让我们使用具有明显重复模式的纹理。这是一个方格网格。下载得到它,并把它放到你的项目中去,使用默认的导入设置。我稍微了扰乱了下网格线,使它更加的有趣,也能感知到它处于平铺模式。


网格线稍微扭曲下的纹理贴图。


复制My First Shader里面的代码到新的文件里面并将它命名为TexturedWith Detail。从现在开始,我们将使用这个新的着色器。



使用这个着色器创建一个新的材质,然后为这个材质分配网格贴图。


带有网格的详细材质。


将材质分配给一个四边形并看下效果。从远处看的话,它的效果会看起来很好。但是如果离得太近的话,它会变得模糊和含糊不清。除了缺乏细节以外,由纹理压缩引起的瑕疵也将变得愈发明显。


网格的特写,显示了纹理像素过低和DXT1这种压缩方式引起的瑕疵。


多个纹理的采样


现在我们采用的是单个纹理的采样,并使用单个纹理的采样作为我们的片段着色器的结果。因为我们要改变这种方法,所以将采样的颜色存储在一个临时变量中是很方便的。



我们推测我们可以通过引入平铺纹理来增加纹理像素的密度。让我们简单地执行对第二个纹理的采样,这个纹理采样的密度是原始纹理采样的密度的10倍。 第二个纹理实际上替换原来的颜色,只是还有添加它。



这产生了更小的网格。你可以在它开始看起来效果不好之前能够离得更近进行观察。当然,因为网格是不规则的,它很显然在不停的重复。


硬编码进行的平铺。


需要的注意是,在现在进行处理的时候,我们执行的是两个纹理采样,但最终只使用了其中一个采样的结果。这似乎是一种浪费。是吗?看下编译后的顶点程序。就像在前面的教程所做的中一样,我将包括OpenGLCore平台和Direct3D 11平台上的相关编译代码。



你注意到编译后代码中只有一个纹理采样么?没错,编译器删除了不必要的代码!基本上,它的工作方式是从最终结果往回推所需要的东西,并丢弃任何在最终结果中未使用的东西。


当然,我们不想替换原始的纹理采样。我们想结合这两个纹理采样。让我们试验下将它们相乘在一起。但是,我们再一次给纹理贴图添加一点扭曲。对纹理采样两次,并使用完全相同的UV坐标。



着色器的编译器到底做了什么?



再次,我们最终得到了一个单一的纹理采样。着色器的编译器检测到重复代码并对其进行了优化。所以纹理贴图只采样一次。纹理采样的结果存储在寄存器中并重复使用。着色器的编译器足够聪明,可以检测出这样的代码重复,即使你使用了中间变量也是如此。着色器的编译器将一切都追溯到提供给编译器的原始输入。然后它尽可能有效地重组所有的一切。

现在把第二个纹理贴图乘以10以后 UV坐标进行打包。我们终于看到大的和小的网格的结合。



两个不同的贴图相乘的结果。


由于纹理贴图的采样不再相同,编译器也必须使用两个纹理采样。



单独的细节纹理贴图


当两个纹理贴图相乘在一起的时候,所得到结果将变得更暗。除非至少有一个纹理是白色的。这是因为纹理像素的每个颜色通道的值介于0和1之间。当向纹理添加细节贴图的时候,你可能需要进行变暗处理,也可以进行增亮处理。


要使原始纹理变亮,你需要的值必须大于1。假设这个值最多为2,这将使原始的颜色的亮度加倍。这可以通过在将细节贴图与原始颜色相乘之前将细节贴图的采样值加倍来给予支持。



颜色值加倍后的细节贴图。


这种方法需要我们重新解释用于细节贴图的纹理。对细节贴图的颜色值乘以1不会更改任何内容。但是当我们对细节贴图的颜色值加倍的时候,现在只有对原来采样值为1/2的地方不会产生影响。这意味着,一个只有灰色 -不是白色-的纹理将不会产生变化。而所有原来采样值低于1/2的地方将变得变暗,而任何原来采样值高于1/2的地方将变得变亮。


所以我们需要一个特殊的细节贴图,以灰色为中心。下面就是这样的一个纹理贴图。


网格的细节纹理贴图。


细节纹理贴图必须是灰度图么?


细节纹理贴图不是必须是灰度图,但通常情况下细节纹理贴图都是灰度图。用灰度图作为细节纹理贴图将通过增亮和变暗来严格调整原始的颜色。这是相对直接的工作。因为与非灰度图进行乘法往往产生比较不直观的结果。但没有什么理由一定阻止你这样做。用彩色图作为细节纹理贴图往往用于产生细微的色彩偏移。


要使用这个单独的细节纹理贴图,我们必须向我们的着色器添加第二个纹理属性。我们使用灰色作为默认值,因为这不会更改主纹理的外观表现。



将细节纹理贴图分配给我们的材质,并将细节纹理贴图的平铺设置为10。


使用两张纹理贴图。


当然,我们必须添加变量来访问细节纹理贴图以及它的平铺和偏移数据。



使用两对UV坐标


我们不再是直接使用乘以10这种硬编码方案,而是应该使用细节纹理贴图的平铺和偏移数据。我们可以在在顶点程序中像计算主纹理贴图的UV坐标那样计算出最终的细节纹理贴图的UV。这意味着我们需要插入额外的UV坐标对。



新的细节纹理贴图的UV坐标是通过将原始纹理贴图的UV坐标与细节纹理贴图的平铺和偏移一起转换而创建出来的。



请注意,两个UV坐标如何输出是定义在两个编译器顶点程序中的。OpenGLCore使用两个输出:vs_TEXCOORD0和vs_TEXCOORD1,而这正如你所期望的那样。相比之下,Direct3D 11只使用单个输出o1。 这到底如何工作是由输出的注释部分进行解释的,我通常会省略这些代码段。



这意味着两个UV坐标对被打包到单个输出寄存器中去。第一个UV坐标对使用的是X和Y通道,第二个UV坐标对使用的是Z和W通道。 这种做法是可能的,因为寄存器总是有四个通道。Direct3D 11编译器利用了这一点。


你能像这样手动打包输出吗?


是的,你可以输出任何你想要的内容。因此,在四个通道值的输出包中分离出一些信息进行打包是很正常的。如果内插值最终成为瓶颈的话,使用较少的输出寄存器可能会提高着色器的性能。


手动打包输出的常见原因是因为只有几个内插值器可以使用。符合Shader Model 2规范的硬件支持8个通用内插器,而符合Shader Model 3规范的硬件支持10个通用内插器。复杂的着色器可以用满这个限制。


现在我们可以在片段程序中使用额外的UV坐标对。



我们的着色器现在功能齐全。主纹理通过基于细节纹理贴图的调制,可以变得更亮或者更暗。


通过细节纹理贴图的调制,实现的亮化和调光。


让细节纹理贴图淡出


我们添加细节纹理贴图的原因是,它们可以在离得很近或者纹理被放大的时候改善材质的外观表现。在离得很远或者纹理被缩小的时候它们被认为不应该是可见的,因为这会使得纹理平铺更加的明显。所以我们需要一种方法来在纹理的显示尺寸减小的时候,能够对细节纹理贴图进行淡出处理。我们可以通过将细节纹理贴图逐渐淡化为灰色来实现,因为这不会导致颜色的变化。


这个想法我们以前实现过!我们需要做的是在细节纹理贴图的导入设置中启用Fadeout Mip贴图。需要注意的是,这也自动将过滤器模式切换为三线性滤波,使得细节纹理贴图逐渐淡化为灰色。


对细节纹理贴图进行淡出处理。


网格从有细节纹理贴图的状态到没有细节纹理贴图的状态的过渡非常的明显,但是你通常不会注意到这个变化。让我们举个简单的例子来说明,这里是大理石材质的主纹理贴图和细节纹理贴图。得到它们,并使用我们用于网格纹理贴图的相同纹理导入设置导入到项目中去。


大理石材质的主纹理贴图和细节纹理贴图。


一旦我们的材质使用这些纹理贴图,那么细节纹理贴图的淡出效果就不再明显。


大理石材质。


然而,由于细节纹理贴图的存在,大理石看起来离得非常近的时候的视觉效果更好。


离得非常近的时候有细节纹理贴图和没有细节纹理贴图的效果对比。


线性颜色空间


当我们在伽马颜色空间中渲染我们的场景的时候,我们的着色器能够正常工作,但如果我们把渲染切换到线性颜色空间就会出错。选择使用哪个颜色空间属于项目设置的范围。它可以在播放器设置的其他设置面板中进行配置,你可以通过Edit / Project Settings / Player访问到。


选择对应的颜色空间。


什么是伽马空间?


伽马空间是指伽玛校正后的颜色空间。伽玛校正是对光强度的调整。最简单的方法是将原始值做一些次方,因此是。 Gamma值为1表示没有变化。 Gamma值为2表示对原始值进行平方处理。


这种转换最初被引入是为了适应阴极射线显像管显示监视器在显示颜色方面的非线性性质。伽玛校正的另一个好处是,它大致对应于我们的眼睛对不同光强度的敏感程度。我们注意到深色之间的差异比对明亮的颜色之间的差异更加明显。因此,将数字的更多位用于比较暗的颜色值比用于比较亮的颜色值更有意义。 通过使用指数可以允许我们在较大范围上拉伸比较小的值,同时还能压缩比较大的值。


最广泛使用的图像颜色格式是sRGB。它使用比简单求幂更复杂的公式,但它存储的颜色的平均伽玛值为1 / 2.2。这个值在许多情况下是合理的近似。要将sRGB图像颜色格式中的数据转换回其原始颜色,请应用2.2的伽玛校正。


使用gamma 1 / 2.2进行编码和使用gamma 2.2进行解码。


Unity假定纹理贴图和颜色存储为sRGB这种图像颜色格式。当在伽马空间中进行渲染的时候,着色器直接访问原始的颜色和纹理数据。这是我们到现在为止所假设的条件。


当在线性空间中渲染时,这些假设不再为真。图形处理器将纹理采样的结果转换为线性空间的颜色值。此外,Unity还会将材质颜色属性转换为线性空间的颜色值。然后着色器使用这些线性空间的颜色值进行操作。在这之后,片段程序的输出将被转换回伽马空间。


使用线性空间的颜色值的优点之一是它可以实现更加逼真的光照计算。这是因为光的相互作用在现实生活中是在线性空间发生的,而不是在指数上发生的的。不幸的是,它限制了我们的细节材质。切换到线性空间后,细节纹理贴图变得更暗。为什么会发生这种情况呢?


同一贴图在伽马颜色空间和线性颜色空间中的比较。


因为我们将细节纹理贴图的采样值加倍,所以采样值为1/2的话,将导致主纹理的颜色没有变化。然而,转换为线性颜色空间后,这个采样值被改变为接近。 加倍以后大约是0.44,这比1小得多。所以导致了变暗。


我们可以通过在详细纹理贴图的导入设置中启用“Bypass sRGB Sampling”选项来解决这个问题。启用这个选项以后会防止从伽马颜色空间到线性颜色空间的转换,因此着色器将始终访问原始的图像数据。然而,细节纹理贴图是一个sRGB图像,所以结果仍然是错误的。


最好的解决方案是重新调整细节纹理贴图的颜色,使这些颜色值再次以1为中心。我们可以通过乘以而不是2来做到这一点。但是,当我们在线性空颜色间中进行渲染的时候,我们必须这样做


幸运的是,UnityCG定义了一个统一的变量,它将包含要乘以的正确数字。这个统一的变量是一个float4类型,在其rgb组件中的值大概是2或大约4.59,会根据颜色空间选择适当的值。由于伽玛校正不会对透明度通道起作用,因此它始终为2。



有了这种变化,无论我们是在哪个颜色空间进行渲染,我们的细节材质都将看起来是相同的。


纹理splatting技术


细节纹理贴图的限制在于,对于整个表面使用的是完全相同的细节。这对于一个统一的表面,如大理石板来说工作的很正常。然而,如果你的辞职没有统一的外观,你不会想在任何的地方都使用相同的细节纹理贴图。


考虑一个比较大的地形块。它可以有草、沙、岩石、雪等等。你希望这些地形类型在离得很近观察的时候能够有足够详细的细节。但是覆盖整个地形的纹理将不会有足够的纹理像素来提供这么多的细节。你可以通过对每个表面类型使用单独的纹理贴图来解决这个问题,并且对这些单独的纹理贴图进行平铺。但是你怎么知道在哪里该使用哪个纹理贴图呢?


让我们假设我们有一个具有两种不同表面类型的地形。在每一个点上,我们必须决定使用哪个表面纹理贴图。到底是第一个还是第二个。我们可以用一个布尔值来表示该使用哪个表面纹理贴图。


如果这个布尔值设置为true的话,我们使用第一个表面纹理贴图,否则的话我们使用第二个表面纹理贴图。我们可以使用灰度纹理贴图来存储这个选择。值1表示使用第一个表面纹理贴图,值0表示使用第二个表面纹理贴图。事实上,我们可以使用这些值在两个纹理之间进行线性内插值。那么0和1之间的值表示两个纹理之间的混合。这使得平滑过渡成为可能。


这样的纹理贴图被称为splat贴图。这就像你将多个地形特征散布到画布上。 由于插值的存在,这章贴图甚至不需要高分辨率。这里是地图的一个小例子。


splat贴图的二进制表示。


将splat贴图添加到项目亿后,将splat贴图的导入类型切换到高级类型。 启用“Bypass sRGB Sampling”设置,并指示应该在线性颜色空间中生成对应的mipmap。这是必需的,因为纹理不能表示sRGB颜色,但可以选择。因此,在线性颜色空间中进行渲染时不应该进行转换。此外,将这个贴图的围绕模式设置为clamp,因为我们不会平铺这个贴图。


纹理贴图的导入设置。


通过复制My First Shader文件的内容到新文件,并修改新文件的名字来创建新的TextureSplatting着色器。因为地形通常不是均匀着色,让我们摆脱均匀着色的功能。



创建使用这个着色器的新材质,并将splat贴图指定为新材质的主要纹理。 因为我们还没有对这个着色器添加什么内容,所以这个着色器只会显示纹理贴图。


显示的是splat贴图。


增加一张纹理贴图


为了能够在两个纹理贴图之间进行选择,我们必须将两个纹理贴图作为属性添加到我们的着色器中去。让我们把这两个纹理贴图命名为Texture1和Texture2。



你可以使用任何你想要的纹理贴图。我只是选择了我们已经有的网格贴图和大理石纹理贴图。


两张额外的纹理贴图。


当然,我们可以为添加到着色器中的每个纹理铁蹄进行平铺和偏移控制。我们确实为每个纹理贴图进行单独的平铺和偏移控制的支持。但是,这将需要我们将更多的数据从顶点着色器程序传递到片段着色器程序,或者计算像素着色器中的UV调整。这当然很好,但是通常地形中的所有纹理都是平铺的。而splat贴图不是平铺的。因此,我们只需要一个平铺和偏移控件的实例。


你可以向着色器的属性中添加属性,就像在C#代码中所做的那样。NoScaleOffset属性将做为它的名字建议。是的,它指的是将平铺和偏移作为缩放和偏移。这并不是非常一致的命名方式。


让我们将这个属性添加到我们的额外纹理贴图的属性中去,并保持主纹理的平铺和偏移方面的输入。



现在,平铺和偏移控件出现在我们的着色器检查器的顶部。虽然他们挨着splat贴图的设置,我们实际上将平铺和偏移控件应用到其他纹理。


没有额外的平铺和偏移控件。


现在我们必须将采样器变量添加到我们的着色器代码中。但是我们不必添加采样器变量相应的_ST变量。



要检查我们是否可以以这种方式对两个纹理贴图进行采样,请更改片段着色器,以便将这两个纹理贴图添加到一起。



添加以后,现在有两张纹理贴图了。


使用Splat贴图


为了对splat贴图进行采样,我们还必须将未修改的UV坐标从顶点程序传递到片段程序。



然后我们可以在采样其他纹理之前对splat贴图进行采样。



我们决定值为1的话代表第一个纹理贴图。由于我们的splat贴图是单色的,我们可以使用任何RGB通道来检索这个值。让我们使用R通道并将其乘以纹理贴图。



调整第一个纹理贴图。


第一个纹理贴图现在由splat贴图进行调制。为了完成插值,我们必须将另一个纹理贴图乘以1-R。



对两个纹理贴图都进行调整。


使用RGB通道的Splat贴图


我们现在有了一个能起作用的splat材质,但是它只支持两个纹理贴图。我们可以支持更多的纹理贴图吗?我们目前只使用了R通道,那么我们如果添加R和G通道会怎么样呢?然后用(1,0,0)表示第一张纹理贴图,用(0,1,0)表示第二张纹理贴图,用(0,0,1)表示第三张纹理贴图。为了在这三者之间获得正确的插值,我们只需要确保RGB通道的值总是加起来等于1就行了。


但是等一下,当我们只使用一个通道的时候,我们可以支持两个纹理贴图。这是因为第二个纹理贴图的权重是通过1-R导出的。这个技巧适用于任何数量的通道。因此,可以通过1-R-G-B支持另一个纹理贴图。


这导致splat贴图具有三种颜色和黑色。只要三个通道的值加在一起不超过1的话,它就是一个有效的贴图。这里是一张有效的贴图,获取它,并使用与以前相同的导入设置导入到项目中去。


使用RGB通道的splat贴图。


当R+ G + B的值超过1的时候会发生什么?


那么前三个纹理的组合将会变得太强。与此同时,第四个纹理贴图的值将被减去而不是被添加进去。如果误差很小的话,那么你不会注意到,结果也是足够好的。示例中的RGB地图实际上不是完美的,但是你不会注意到。纹理压缩会引入更多的错误,但是它再也不是很明显能让人注意到。


我们可以使用透明度通道吗?


事实上你可以使用透明度通道!这意味着单个使用RGBA通道的splat贴图可以支持多达五种不同的地形类型。但对于这个教程而言,支持四种不同的地形类型就足够了。


如果你想使用超过五个纹理贴图,你必须使用多个splat贴图。虽然这是可能的,但是这会导致你有很多的纹理采样。对于这个问题有更好的技术可以支持,比如说纹理数组。


为了支持使用RGB通道的splat贴图,我们必须为我们的着色器添加两个额外的纹理贴图。我给我们的着色器分配了大理石细节纹理贴图和测试纹理贴图。



四张纹理贴图。


将所需的变量添加到着色器代码之中。再次重申,我们不需要额外的_ST变量。



在片段程序中,添加额外的纹理贴图采样。第二个纹理贴图采样现在使用G通道,第三个纹理贴图采样现在使用B通道。而最终的纹理贴图采样使用(1-R-G-B)作为系数进行调制。



支持四张纹理的splat贴图。


为什么混合区域的表现会在线性颜色空间之中看起来不同?


我们的splat贴图绕过了sRGB采样,所以混合不应该依赖于我们使用的颜色空间,对吧? Splat贴图确实是不受到影响。但是混合发生的颜色空间会受到影响。


如果是在伽马空间中进行渲染,采样后的结果是在伽马空间中混合,这就没什么问题。但是当在线性空间中进行渲染的时候,采样后的结果需要首先转换为线性空间中的值,然后再进行混合,然后再转换回伽马空间。结果会略有不同。在线性空间中,混合也是线性的。但是在伽马空间中,混合会倾向于更暗的颜色。


现在你知道如何使用细节纹理以及如何使用splat贴图来混合多个纹理了。你还可以对这些方法进行组合。


你可以向splat着色器添加四个细节纹理,并使用贴图在它们之间进行混合。 当然,这需要四个额外的纹理采样,所以这么做是要付出一定代价的。

你还可以使用贴图来控制细节纹理会应用到哪里,以及在哪里会省略细节纹理。在这种情况下,你需要一个单色贴图,它用作掩码。当单个纹理包含表示不同材质的区域,但又不像地形有那样大的尺度的时候,这是非常有用的。举个简单的例子来说,如果我们的大理石纹理里面也包含金属块,你不会想要应用那里面的大理石纹理细节。


这个系列的下一篇教程是《第一个光源》


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。


今日推荐


程序员必看资料,Unity精选内容免费下载

U3D扩展精品手册|你与《纪念碑谷》的差距只有9个小时


添加小编微信,可享双重福利

1.加入GAD程序猿交流基地

获取行业干货资讯,观看大牛分享直播

2.直接领取60G独家程序资料库,地址在小编朋友圈

包括腾讯内部分享、文章教程、视频教程等全套资料

 

↓长按添加小编GAD苏苏↓

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存