跳转到内容

Cg 编程/Unity/纹理球体

来自维基教科书,开放世界的开放书籍
从阿波罗 17 号上看到的地球。地球的形状接近一个相当光滑的球体。

本教程介绍了纹理映射

这是关于 Unity 中 Cg 着色器纹理的一系列教程中的第一个。在本教程中,我们从球体上的单个纹理贴图开始。更具体地说,我们将地球表面的图像映射到球体上。基于此,后续教程将涵盖诸如纹理表面的照明、透明纹理、多重纹理、光泽映射等主题。

逼近球体的三角形网格。
地球表面的图像。水平坐标代表经度,垂直坐标代表纬度。

纹理映射

[编辑 | 编辑源代码]

“纹理映射”(或“纹理化”)的基本思想是将图像(即“纹理”或“纹理贴图”)映射到三角形网格上;换句话说,将平面图像放到三维形状的表面上。

为此,定义了“纹理坐标”,它们只是指定纹理(即图像)中的位置。在 OpenGL 中,水平坐标称为S,垂直坐标称为T。然而,通常将它们称为xy。在动画和建模工具中,纹理坐标通常称为UV

为了将纹理图像映射到网格,网格的每个顶点都给定一对纹理坐标。(这个过程(以及结果)有时被称为“UV 映射”,因为每个顶点都映射到 UV 空间中的一个点。)因此,每个顶点都映射到纹理图像中的一个点。然后可以为三个顶点之间的任何三角形的每个点插值顶点的纹理坐标,因此网格的所有三角形的每个点都可以有一对(插值的)纹理坐标。这些纹理坐标将网格的每个点映射到纹理贴图中的特定位置,因此映射到该位置的颜色。因此,渲染纹理映射的网格包括对所有可见点执行两个步骤:纹理坐标的插值和纹理图像在由插值纹理坐标指定的位置的颜色查找。

通常,任何有效的浮点数都是有效的纹理坐标。但是,当 GPU 被要求查找纹理图像的像素(或“纹素”(例如使用下面描述的“tex2D”指令))时,它将在内部将纹理坐标映射到 0 到 1 之间的范围内,方式取决于导入纹理时指定的“包裹模式”:包裹模式“重复”基本上使用纹理坐标的小数部分来确定 0 到 1 之间的纹理坐标。另一方面,包裹模式“钳位”将纹理坐标钳位到此范围。然后,这些 0 到 1 之间的内部纹理坐标用于确定纹理图像中的位置: 指定纹理图像的左下角; 右下角; 左上角;等等。

在 Unity 中纹理化球体

[编辑 | 编辑源代码]

要在 Unity 中将地球表面的图像映射到球体上,您首先必须将图像导入 Unity。单击图像,直到您获得较大的版本,并将其保存到您的计算机上(通常使用右键单击)(记住您保存的位置)。然后切换到 Unity 并从主菜单中选择资产 > 导入新资产...。选择图像文件,然后在文件选择框中单击导入。导入的纹理图像应出现在项目窗口中。(或者,您也可以简单地将图像文件拖放到项目窗口中。)通过在其中选择它,关于导入方式的详细信息将出现在检查器窗口中(并且可以更改)。

现在创建一个球体、一个材质和一个着色器,并将着色器附加到材质,并将材质附加到球体,如“最小着色器”部分中所述。着色器代码应该是

Shader "Cg shader with single texture" {
   Properties {
      _MainTex ("Texture Image", 2D) = "white" {} 
         // a 2D texture property that we call "_MainTex", which should
         // be labeled "Texture Image" in Unity's user interface.
         // By default we use the built-in texture "white"  
         // (alternatives: "black", "gray" and "bump").
   }
   SubShader {
      Pass {	
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         uniform sampler2D _MainTex;	
            // a uniform variable referring to the property above
            // (in fact, this is just a small integer specifying a 
            // "texture unit", which has the texture image "bound" 
            // to it; Unity takes care of this).
 
         struct vertexInput {
            float4 vertex : POSITION;
            float4 texcoord : TEXCOORD0;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 tex : TEXCOORD0;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            output.tex = input.texcoord;
               // Unity provides default longitude-latitude-like 
               // texture coordinates at all vertices of a 
               // sphere mesh as the input parameter 
               // "input.texcoord" with semantic "TEXCOORD0".
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
         float4 frag(vertexOutput input) : COLOR
         {
            return tex2D(_MainTex, input.tex.xy);	
               // look up the color of the texture image specified by 
               // the uniform "_MainTex" at the position specified by 
               // "input.tex.x" and "input.tex.y" and return it
 
         }
 
         ENDCG
      }
   }
   Fallback "Unlit/Texture"
}

请注意,选择名称_MainTex是为了确保回退着色器Unlit/Texture可以访问它(参见“漫反射”部分中关于回退着色器的讨论)。

球体现在应该是白色的。如果它是灰色的,您应该检查着色器是否附加到材质,以及材质是否附加到球体。如果球体是洋红色的,您应该检查着色器代码。特别是,您应该在项目窗口中选择着色器,并在检查器窗口中读取错误消息。

如果球体是白色的,则在层次结构窗口场景视图中选择球体,并查看检查器窗口中的信息。您的材质应出现在网格渲染器下,并且在它下面应该有一个标签纹理图像。(否则,单击材质栏以使其显示。)标签“纹理图像”与我们在着色器代码中为我们的着色器属性_MainTex指定的标签相同。在该标签的右侧有一个空框。要么单击框中的小选择按钮并选择导入的纹理图像,要么将纹理图像从项目窗口拖放到此空框中。

如果一切顺利,纹理图像现在应该出现在球体上。恭喜!

它是如何工作的

[编辑 | 编辑源代码]

由于许多技术使用纹理映射,因此理解这里发生的事情非常有意义。因此,让我们回顾一下着色器代码

Unity 的球体对象的顶点带有每个顶点的纹理坐标,这些坐标位于语义为TEXCOORD0的顶点输入参数texcoord中。这些坐标类似于经度和纬度(但范围从 0 到 1)。这类似于语义为POSITION的顶点输入参数vertex,它指定了对象空间中的位置,只是texcoord指定了纹理图像空间中的纹理坐标。

然后,顶点着色器将每个顶点的纹理坐标写入顶点输出参数output.tex。对于三角形的每个片段(即每个覆盖的像素),对三个三角形顶点处的此输出参数的值进行插值(参见“光栅化”部分中的描述),并且将插值的纹理坐标作为输入参数提供给片段着色器。然后,片段着色器使用它们在由均匀变量_MainTex指定的纹理图像中查找纹理空间中插值位置的颜色,并将此颜色作为片段输出参数返回,然后将其写入帧缓冲区并在屏幕上显示。

为了理解其他教程中介绍的更复杂的纹理映射技术,您必须对这些步骤有一个很好的了解。

重复和移动纹理

[编辑 | 编辑源代码]

在上面的着色器的 Unity 接口中,您可能已经注意到参数平铺偏移,每个参数都有一个x和一个y分量。在内置着色器中,这些参数允许您重复纹理(通过在纹理坐标空间中缩小纹理图像)并在表面上移动纹理图像(通过在纹理坐标空间中偏移它)。为了与这种行为保持一致,必须定义另一个均匀变量

         uniform float4 _MainTex_ST; 
            // tiling and offset parameters of property "_MainTex"

对于每个纹理属性,Unity 提供了一个以“_ST”结尾的 float4 统一变量。(记住:“S”和“T”是纹理坐标的官方名称,通常称为“U”和“V”,或“x”和“y”。)这个统一变量保存了 _MainTex_ST.x_MainTex_ST.yTiling 参数的 xy 组件,而 Offset 参数的 xy 组件则存储在 _MainTex_ST.z_MainTex_ST.w 中。这个统一变量应该像这样使用

            return tex2D(_MainTex, 
               _MainTex_ST.xy * input.tex.xy + _MainTex_ST.zw);

这使得着色器表现得像内置着色器一样。在其他教程中,为了使着色器代码更简洁,通常不会实现此功能。

为了完整起见,以下是包含此功能的完整着色器代码

Shader "Cg shader with single texture" {
   Properties {
      _MainTex ("Texture Image", 2D) = "white" {} 
         // a 2D texture property that we call "_MainTex", which should
         // be labeled "Texture Image" in Unity's user interface.
         // By default we use the built-in texture "white"  
         // (alternatives: "black", "gray" and "bump").
   }
   SubShader {
      Pass {	
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
                   
         uniform sampler2D _MainTex;	
         uniform float4 _MainTex_ST; 
            // tiling and offset parameters of property

         struct vertexInput {
            float4 vertex : POSITION;
            float4 texcoord : TEXCOORD0;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 tex : TEXCOORD0;
         };

         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            output.tex = input.texcoord;
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return tex2D(_MainTex, 
               _MainTex_ST.xy * input.tex.xy + _MainTex_ST.zw);	
               // texture coordinates are multiplied with the tiling 
               // parameters and the offset parameters are added
         }

         ENDCG
      }
   }
   Fallback "Unlit/Texture"
}

Unity 在 UnityCG.cginc 中为这种纹理坐标变换提供了一个宏,即你必须在 Pass 中包含这一行

         #include "UnityCG.cginc"

有了它,你可以使用宏 TRANSFORM_TEX() 来改写上面的 return 语句

         return tex2D(_MainTex, TRANSFORM_TEX(input.tex, _MainTex));

你已经到达了最重要的教程之一的结尾。我们已经了解了

  • 如何导入纹理图像,以及如何将其附加到着色器的纹理属性。
  • 顶点着色器和片段着色器如何协同工作,将纹理图像映射到网格。
  • Unity 的纹理平铺和偏移参数的工作原理,以及如何实现它们。

进一步阅读

[编辑 | 编辑源代码]

如果你想知道更多

  • 关于顶点着色器和片段着色器的数据流进出(即顶点输入和输出参数等),你应该阅读 “可编程图形管道” 部分的描述。
  • 关于片段着色器的顶点输出参数的插值,你应该阅读 “光栅化” 部分的讨论。

< Cg Programming/Unity

除非另有说明,本页面的所有示例源代码均为公共领域。
华夏公益教科书