GLSL 编程/Unity/纹理球体
本教程介绍了 **纹理映射**。
它是关于在 Unity 中使用 GLSL 着色器进行纹理的一系列教程中的第一个。在本教程中,我们从球体上的单一纹理贴图开始。更具体地说,我们将地球表面的图像映射到球体上。在此基础上,后续教程将涵盖诸如纹理表面的光照、透明纹理、多纹理、光泽映射等主题。
“纹理映射”(或“纹理化”)的基本思想是将图像(即“纹理”或“纹理贴图”)映射到三角形网格;换句话说,将平面的图像放到三维形状的表面上。
为此,定义了“纹理坐标”,它们只是指定纹理(即图像)中的位置。水平坐标官方称为 S
,垂直坐标称为 T
。但是,通常将它们称为 x
和 y
。在动画和建模工具中,纹理坐标通常称为 U
和 V
。
为了将纹理图像映射到网格,网格的每个顶点都有一对纹理坐标。(这个过程(和结果)有时被称为“UV 映射”,因为每个顶点都被映射到 UV 空间中的一个点。)因此,每个顶点都被映射到纹理图像中的一个点。然后,可以为三个顶点之间的任何三角形的每个点插值顶点的纹理坐标,从而使网格中所有三角形的每个点都有一对(插值)纹理坐标。这些纹理坐标将网格的每个点映射到纹理贴图中的特定位置,因此映射到该位置的颜色。因此,渲染纹理映射网格包括对所有可见点的两步操作:插值纹理坐标以及根据插值纹理坐标指定的位置在纹理图像中查找颜色。
在 OpenGL 中,任何有效的浮点数都是有效的纹理坐标。但是,当 GPU 被要求查找纹理图像的像素(或“纹素”(例如,使用下面描述的“texture2D”指令)时,它将在内部将纹理坐标映射到 0 到 1 之间的范围,方式取决于导入纹理时指定的“包装模式”:包装模式“repeat”基本上使用纹理坐标的小数部分来确定 0 到 1 之间的纹理坐标。另一方面,包装模式“clamp”将纹理坐标钳制到此范围。然后,使用 0 到 1 之间的这些内部纹理坐标来确定纹理图像中的位置: 指定纹理图像的左下角; 指定右下角; 指定左上角;等等。
要在 Unity 中将地球表面的图像映射到球体,首先必须将图像导入 Unity。点击 图像,直到出现更大的版本,然后将其保存(通常右键单击)到您的计算机上(记住您保存的位置)。然后切换到 Unity,从主菜单中选择 **Assets > Import New Asset...**。选择图像文件,并在文件选择框中点击 **Import**。导入的纹理图像应该出现在 **Project View** 中。通过在其中选择它,关于其导入方式的详细信息将显示在 **Inspector View** 中(并且可以更改)。
现在创建一个球体、一个材质和一个着色器,并将着色器附加到材质,并将材质附加到球体,如 “最小着色器”章节 所述。着色器代码应该是
Shader "GLSL 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 {
GLSLPROGRAM
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).
varying vec4 textureCoordinates;
// the texture coordinates at the vertices,
// which are interpolated for each fragment
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
// Unity provides default longitude-latitude-like
// texture coordinates at all vertices of a
// sphere mesh as the attribute "gl_MultiTexCoord0".
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
// look up the color of the texture image specified by
// the uniform "_MainTex" at the position specified by
// "textureCoordinates.x" and "textureCoordinates.y"
// and return it in "gl_FragColor"
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Unlit/Texture"
}
请注意,_MainTex
的名称是经过选择的,以确保回退着色器 Unlit/Texture
可以访问它(参见 “漫反射”章节 中对回退着色器的讨论)。
现在球体应该变为白色。如果它是灰色的,您应该检查着色器是否附加到材质,以及材质是否附加到球体。如果球体是洋红色的,您应该检查着色器代码。特别是,您应该在 **Project View** 中选择着色器,并阅读 **Inspector View** 中的错误消息。
如果球体是白色的,请在 **Hierarchy View** 或 **Scene View** 中选择球体,并查看 **Inspector View** 中的信息。您的材质应该出现在 **Mesh Renderer** 下面,在其下方应该有一个 **Texture Image** 标签。(否则,点击材质栏以使其显示。)“Texture Image” 标签与我们在着色器代码中为我们的着色器属性 _MainTex
指定的标签相同。该标签右侧有一个空框。点击框中的小 **Select** 按钮,并选择导入的纹理图像,或者将纹理图像从 **Project View** 拖放到此空框中。
如果一切顺利,纹理图像现在应该出现在球体上。恭喜!
由于许多技术使用纹理映射,因此了解这里发生了什么非常值得。因此,让我们回顾一下着色器代码
Unity 球体对象的顶点带有 gl_MultiTexCoord0
中的属性数据(每个顶点),它指定了类似于经度和纬度的纹理坐标(但范围从 0 到 1)。这类似于指定对象空间中位置的属性 gl_Vertex
,不同之处在于 gl_MultiTexCoord0
指定纹理图像空间中的纹理坐标。
然后顶点着色器将每个顶点的纹理坐标写入变化变量 textureCoordinates
。对于三角形的每个片段(即每个覆盖的像素),三角形的三个顶点处此变化变量的值都会被插值(参见 “光栅化”章节 中的描述),并且插值的纹理坐标将被传递给片段着色器。然后,片段着色器使用它们来查找纹理图像(由统一变量 _MainTex
指定)中纹理空间中插值位置的颜色,并在 gl_FragColor
中返回该颜色,该颜色随后被写入帧缓冲区并在屏幕上显示。
为了理解其他教程中介绍的更复杂的纹理映射技术,您必须对这些步骤有很好的了解。
在上面着色器的 Unity 界面中,您可能已经注意到 **Tiling** 和 **Offset** 参数,它们分别具有 **x** 和 **y** 分量。在内置着色器中,这些参数允许您通过在纹理坐标空间中缩小纹理图像来重复纹理,以及通过在纹理坐标空间中偏移纹理图像来移动纹理图像。为了与这种行为保持一致,必须定义另一个统一变量
uniform vec4 _MainTex_ST;
// tiling and offset parameters of property "_MainTex"
对于每个纹理属性,Unity 都提供了一个带有结尾“_ST”的 vec4
统一变量。(记住:“S”和“T”是纹理坐标的官方名称,通常被称为“U”和“V”,或者“x”和“y”。)这个统一变量在 _MainTex_ST.x
和 _MainTex_ST.y
中保存 **Tiling** 参数的 **x** 和 **y** 分量,而在 _MainTex_ST.w
和 _MainTex_ST.z
中保存 **Offset** 参数的 **x** 和 **y** 分量。统一变量应该像这样使用
gl_FragColor = texture2D(_MainTex,
_MainTex_ST.xy * textureCoordinates.xy
+ _MainTex_ST.zw);
这使得着色器的行为与内置着色器一致。在其他教程中,为了使着色器代码更简洁,通常不会实现此功能。
为了完整性,这里附带此功能的完整着色器代码
Shader "GLSL shader with single texture" {
Properties {
_MainTex ("Texture Image", 2D) = "white" {}
}
SubShader {
Pass {
GLSLPROGRAM
uniform sampler2D _MainTex;
uniform vec4 _MainTex_ST;
// tiling and offset parameters of property
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = texture2D(_MainTex,
_MainTex_ST.xy * textureCoordinates.xy
+ _MainTex_ST.zw);
// textureCoordinates are multiplied with the tiling
// parameters and the offset parameters are added
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Unlit/Texture"
}
您已完成最重要的教程之一。我们已经了解了
- 如何导入纹理图像以及如何将其附加到着色器的纹理属性。
- 顶点着色器和片段着色器如何协同工作将纹理图像映射到网格。
- Unity 的纹理平铺和偏移参数的工作原理以及如何实现它们。
如果你想了解更多
- 关于顶点着色器和片段着色器输入输出的数据流(例如,顶点属性、变元等),您应该阅读 “OpenGL ES 2.0 管线” 部分的描述。
- 关于片段着色器中变元变量的插值,您应该阅读 “光栅化” 部分的讨论。