纹理最初的目的就是使用一张图片来控制模型的外观。使用纹理映射(texture mapping)技术,我们可以把一张图“黏”在模型表面,逐妏素(texel)(妏素的名字是为了和像素进行区分)地控制模型的颜色。
在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。通常,这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向坐标。因此,纹理映射坐标也被称为UV坐标。
尽管纹理的大小可以是多种多样的,例如可以是256×256或者1028×1028,但顶点UV坐标的范围通常都被归一化到[0,1]范围内。需要注意的是,纹理采样时使用的纹理坐标不一定是在[0,1]范围内。实际上,这种不在[0,1]范围的纹理坐标有时会非常有用。与之关系紧密的是纹理的平铺模式,它将决定渲染引擎在遇到[0,1]范围内的纹理坐标时如何进行纹理采样。
在OpenGL里,纹理空间的原点位于左下角,而在DirectX中,原点位于左上角。Unity在绝大多数情况下(特殊情况参见5.6节)为我们处理好了这个差异问题,也就是说,即便游戏的目标平台可能既有OpenGL风格的,也有DirectX风格的,但我们在Unity中使用的通常只有一种坐标系。Unity使用的纹理空间是符合OpenGL的传统的,也就是说,原点位于左下角。
本章将介绍如何在Unity中利用纹理采样来实现更加丰富的视觉效果。7.1中,我们将学习如何在Unity SHader中进行最基本的纹理采样,并介绍纹理的属性等基本概念。7.2节将介绍游戏中应用广泛的凹凸纹理,还会解释Unity中法线纹理的一些实现细节。7.3节和7.4节将分别介绍两类特殊的纹理类型,即渐变纹理和遮罩纹理,这些纹理在游戏中的应用非常广泛。
7.1 单张纹理
我们通常会使用一张纹理来代替物体的漫反射颜色。
在本例中,仍然使用Blinn-Phong光照模型来计算光照。准备工作如下。
(1)创建新场景。(Scene_7_1)。
(2)新建材质。(SingleTextureMat)
(3)新建 Unity Shader 。(Chapter7-SingleTexture)。把新的Unity Shader赋给第二步中创建的材质。
(4)创建一个胶囊体,并把第二步中的材质赋给该胶囊体。
(5)保存场景。
Shader "Unity Shaders Book/Chapter 7/Single Texture" {
Properties {
//为了使用纹理,在Properties语义块中添加一个纹理属性
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
//上面的代码声明了一个名为_MainTex的纹理,2D是纹理属性的声明方式。
//我们使用一个字符串后跟一个花括号作为它的初始值,“white”是内置
//纹理的名字,也是一个全白的纹理。为了控制物体的整体色调,我们还声明了一个_Color属性。
}
SubShader {
Pass {
//光照模式:
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
//定义顶点着色器和片元着色器叫什么名字。
#pragma vertex vert
#pragma fragment frag
//为了使用Unity内置的一些变量,我们需要包含了;
#include "Lighting.cginc"
//在CG代码中声明和上述属性类型相匹配的变量,以便和材质面板中的属性建立联系
//与其他属性类型不同的是,我们还需要为纹理类型的属性声明一个float4类型的变量
//_MainTex_ST。其中,_MainTex_ST的名字不是任意起的。在Unity中,我们需要使用
//纹理名_ST的方式来声明某个纹理的属性。其中,ST是缩放(scale)和平移(translation)的
//缩写。_MainTex_ST可以让我们得到该纹理的缩放和平移(偏移)值,_MainTex_ST存储的是缩放值,
//_MainTex_ST.xy存储的是缩放值,而_MainTex_ST.zw存储的是偏移值。这些值可以在材质面板的属性纹理
//中调节,
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
//在上面的代码中,我们首先在a2v结构体中使用TEXCOORD0语义块声明了一个新的变量texcoord,
//这样Unity就会将模型的第一组纹理坐标存储到该变量中。然后,我们v2f结构体中添加了用于存储纹理坐标的变量uv,
//以便子片元着色器中使用该坐标进行纹理采样。
//然后,我们定义了顶点着色器:
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// Or just call the built-in function
// o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
//在顶点着色器中,我们使用纹理的属性值_MainTex_ST来对顶点纹理做坐标进行变换,得到
//最终的纹理坐标。计算过程是,首先使用缩放属性_MainTex_ST.xy对顶点纹理坐标进行缩放,
//然后在使用偏移属性_MainTex_ST.zw对结果进行偏移。Unity提供了一个内置宏TRANSFORM_TEX
//来帮我们计算上述过程。TRANSFORM_TEX是在UnityCG.cginc中定义的:
//Transforms 2D UV by scale/bias property
//#define TRANSFORM_TEX(tex,name) (tex.xy * name ## _ST.xy + name ## _ST.zw)
//他接受两个参数,第一个参数是顶点纹理坐标,第二参数是纹理名,在它的实现中,将利用纹理名_ST的
//方式来计算变换后的纹理坐标。
//我们还需要实现片元着色器,并在计算漫反射时使用纹理中的纹素值:
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// Use the texture to sample the diffuse color
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
//上面的代码首先计算了世界空间下的法线方向和光照方向。然后,使用CG的tex2D
//函数对纹理进行采样。它的第一参数是需要被采样的纹理,第二个参数是一个float2
//类型的纹理坐标,它将返回计算得到的纹素值。我们使用采样结果和颜色属相_Color的
//乘积来作为材质的反射率albedo,并把他和环境光照相乘得到环境部分。随后,我们使用albedo
//来计算漫反射光照的结果,并和环境光照、高光反射光照想加后返回/
ENDCG
}
}
//最后,我们为该Shader设置了合适的Fallback;
FallBack "Specular"
}
纹理属性:
在我们向Unity中导入一张纹理资源后,可以在它的材质面板上调整期属性,如下图所示:
纹理面板上的第一个属性是纹理类型。在本节中,我们使用的是Texture类型,在下面的法线纹理一节中,我们会使用Normal map类型。而在后面的章节中,我们还会看到Cubemap等高级纹理类型。我们之所以要为导入的纹理选择合适的类型,是因为只有这样才能让Unity知道我们的意图,为Unity Shader传递正确的纹理,并在一些情况下可以让Unity对该纹理进行优化。
当把纹理类型设置成Texture后,下面会有一个Alpha from Grayscale 复选框,如果勾选了它,那么透明通道的值将会由每个像素的灰度值生成。关于透明效果(8)。
下面一个属性非常重要——Wrap Mode。他决定了当纹理坐标超过[0,1]范围后将会如何被平铺。Wrap Mode有两种模式:一种是Repeat,在这种模式下,如果纹理坐标超过了1,那么他的整数部分将会被舍弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复;另一种是Clamp,在这种模式下,如果纹理坐标大于1,那么将会截取到1,如果小于0,那么将会截取到0。下图给出两种模式下平铺一张纹理的效果。
上图展示了在纹理的平铺(Tiling)属性为(3,3)时分别使用两种Wrap Mode的结果。第一张图使用了Repeat模式,在这种模式下纹理将会不断重复;第二张图使用了Clamp模式,在这种模式下超过范围的部分将会截取到边界值,形成一个条形结构。
需要注意的是,想要让纹理得到这样的效果,我们必须使用纹理的属性(例如上面的_MainTex_ST变量)在Unity Shader 中对顶点纹理坐标进行相应的变换。也就是说,代码中需要类似下面的代码:
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// Or just call the built-in function
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
我们还可以在材质面板中调整纹理的偏移量,
纹理导入面板中的下一个属性是Filter Mode属性,他决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式。Filter Mode支持三种模式:Point,Bilinear以及Trilinear。它们得到的图片滤波效果依次提升,单需要耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图片那质量。例如,当我们把一张64×64大小的纹理贴在512×512大小的平面上时,就需要放大纹理。
在资源Scene_7_1_2_b中可以找到该场景,