纹理的压缩和混合

纹理的作用并不仅仅只是我们通常认为的保存加载的数据或者像素颜色,同时还有像素点在 xy 方向以及RGBA通道的各种设置。我们能把多张图片压缩进一张单独的RGBA纹理中并且使用它们各自的R,G,B和A元素,因为我们可以在着色器中把它们各自纹理中的这些元素分别解压出来。

将各自的灰度图压缩进一张单独的RGBA纹理的结果可以通过下图看出来:

diagram

为什么说这会有用呢?在你的应用程序实际消耗的大部分中内存当中,贴图占了很大的一部分。所以如果你想要减少应用程序的大小的话,我们能在着色器中查看所有使用的图片并且想想我们是否能将这些纹理合并到一张单独的纹理中。

任何灰度的纹理都可以压缩进另一张有RGBA通道的纹理。第一次听起来可能有点怪怪的,但我们接下来会用这个知识点来演示纹理压缩的用法并且在我们的着色器中使用这张压缩过的纹理。

举其中一个纹理压缩用法例子,比如你想把一套纹理[有好几张]混合进一张单独的纹理中。这在地形类着色器中很常见,在我们的例子中,我们会用一些排好序的控制纹理或者压缩过的纹理,很好的混合进另一张纹理中。这个知识点会讲到这个技术的,同时还会告诉你如何开始编写好这样一个混合四张纹理的着色器。


  • 始前准备

    在我们Unity的着色器文件夹中创建一个新的着色器同时创建一个新的材质与之对应。这两者的命名怎么方便怎么来,不过尽量保证组织和引用方便吧。

    建好着色器和材质后,再创建一个新的场景,好给后面做测试。

    收集好四张你打算混合在一起的纹理。我们直接用它们展示这几张纹理是如何放到物体表面的。

    我们可以用一些非常复杂的混合纹理在地形网格上创建一个非常真实的地形分布纹理,如下所示:

    diagram


  • 操作步骤

    我们通过下面代码来学习如何使用压缩纹理。

    1. 我们在着色器的 属性Properties 块中添加一些属性。我们需要5个 sampler2D 类型的游戏对象或纹理,2个颜色属性:

      Properties
      {_
         MainTint ("Diffuse Tint", Color) = (1,1,1,1)
         //Add the properties below so we can input all of our textures
         _ColorA ("Terrain Color A", Color) = (1,1,1,1)
         _ColorB ("Terrain Color B", Color) = (1,1,1,1)
         _RTexture ("Red Channel Texture", 2D) = ""{}
         _GTexture ("Green Channel Texture", 2D) = ""{}
         _BTexture ("Blue Channel Texture", 2D) = ""{}
         _ATexture ("Alpha Channel Texture", 2D) = ""{}
         _BlendTex ("Blend Texture", 2D) = ""{}
      }
      
    2. 接下来我们在 SubShader{} 块中创建一些变量,记住要跟上一步的属性块对应。

      CGPROGRAM
      #pragma surface surf Lambert
      float4 _MainTint;
      float4 _ColorA;
      float4 _ColorB;
      sampler2D _RTexture;
      sampler2D _GTexture;
      sampler2D _BTexture;
      sampler2D _BlendTex;
      sampler2D _ATexture;
      
    3. 我们现在获得了纹理属性后把它们传递给 SubShader{} 函数。为了能够让使用者可以控制每个纹理的截取比例,我们需要修改 输入结构体Input struct 。这样我们就可以使用每个纹理的截取和偏移量等参数:

      struct Input
      {
         float2 uv_RTexture;
         float2 uv_GTexture;
         float2 uv_BTexture;
         float2 uv_ATexture;
         float2 uv_BlendTex;
      };
      

      注意[译者添加]
      如果遇到 Too many texture interpolators would be used for ForwardBase pass 错误,因为是Input中定义的材质uv变量太多了,当前版本的shader model不支持造成。此时将shader model改为更高的版本可以解决。如果你的#pragma target 3.0不支持3个材质,可以改为#pragma target 4.0试试吧。 如果非要用#pragma target 3.0,只能通过共用uv_MainTex或者使用屏幕坐标来解决了。


    4. 然后在 surf() 函数里,为了便于理解,我们把纹理的信息都分别保存到它们各自的变量中:

      //Get the pixel data from the blend texture
      //we need a float 4 here because the texture//will return R,G,B,and A or X,Y,Z, and W
      float4 blendData = tex2D(_BlendTex, IN.uv_BlendTex);
      //Get the data from the textures we want to blend
      float4 rTexData = tex2D(_RTexture, IN.uv_RTexture);
      float4 gTexData = tex2D(_GTexture, IN.uv_GTexture);
      float4 bTexData = tex2D(_BTexture, IN.uv_BTexture);
      float4 aTexData = tex2D(_ATexture, IN.uv_ATexture);
      
    5. 我们用 lerp() 函数把每一个纹理混合到一起。这个函数接收三个参数,lerp(value : a, value : b, blend: c)lerp() 函数用前面两个参数的纹理跟最后一个浮点型参数进行混合:

      //No we need to contruct a new RGBA value and add all
      //the different blended texture back together
      float4 finalColor;
      finalColor = lerp(rTexData, gTexData, blendData.g);
      finalColor = lerp(finalColor, bTexData, blendData.b);
      finalColor = lerp(finalColor, aTexData, blendData.a);
      finalColor.a = 1.0;
      
    6. 最后,我们把混合后的纹理乘以颜色并且用红色通道来决定到底该用这两个地形颜色的哪一个:

      //Add on our terrain tinting colors
      float4 terrainLayers = lerp(_ColorA, _ColorB, blendData.r);
      finalColor *= terrainLayers;
      finalColor = saturate(finalColor);
      o.Albedo = finalColor.rgb * _MainTint.rgb;
      o.Alpha = finalColor.a;
      

    下图展示了我们通过混合四张地形纹理贴图,并且创建一个地形的技术:

    diagram


  • 原理介绍

    这里代码量好像有点多,但是混合后面的概念是比较简单的。为了展示混合的技术,我们使用了来自CgFX标准库中的内建函数 lerp() 。这个函数允许我们以第三个参数作为混合量,获得一个介于第一个参数和第二个参数之间的数:

    函数描述
    lerp(a,b,f)这里其实是线性插值: (1 – f )*a + b* f 这里的 ab 需要时向量或者标量。但是 f 只能是跟 ab 类型一样的标量或者向量。

    我们举例子来演示,比如我们想要获得一个介于1和2之间的中间值,我们此时可以给 lerp() 函数的第三个参数传一个0.5那么这个函数就会返回1.5。这刚好能满足我们混合纹理的需求,因为在RGBA纹理中的每一个通道的值都是一个浮点值,这些值得取值范围通常都是0到1。

    在这个着色器中,我们都简单的从需要混合的纹理中,拿了一个通道的值,来控制用 lerp() 函数获得的颜色值,这些值最终在赋值给每一个像素点。比如我们用草的纹理和泥土的纹理,再加上我们混合纹理中的红色通道,把这些值分别传递给 lerp() 函数的第一,二,三个参数。这样我们就能获得地表中每一个像素正确混合后的颜色。

    下图更直观的向我们展示了当我们使用 lerp() 函数时,到底发生了什么: diagram 着色器代码简单的使用了混合纹理的四个通道和所有的颜色纹理,最终创建了一个混合后的纹理。这个最终的纹理的颜色用于跟漫反射的光进行相乘的操作。

    [这里找不到这个项目的代码,只能随机应变翻译,没有项目可以跑]