Unity Shader入门精要第七章 基础纹理 单张纹理

Unity系列文章目录

前言

纹理最初的目的就是使用一张图片来控制模型的外观。使用纹理映射(texture mapping)技
术,我们可以把一张图“黏”在模型表面,逐纹素(texel)(纹素的名字是为了和像素进行区分)
地控制模型的颜色。
在美术人员建模的时候, 通常会在建模软件中利用纹理展开技术把纹理映射坐标
(texture-mapping coordinates)存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的
2D 坐标。通常,这些坐标使用一个二维变量(u, v)来表示,其中u 是横向坐标,而v 是纵向坐标。
因此,纹理映射坐标也被称为UV 坐标。
尽管纹理的大小可以是多种多样的,例如可以是256×256 或者1024×1024,但顶点UV 坐标
的范围通常都被归一化到[0, 1]范围内。需要注意的是,纹理采样时使用的纹理坐标不一定是在[0,
1]范围内。实际上,这种不在[0, 1]范围内的纹理坐标有时会非常有用。与之关系紧密的是纹理的
平铺模式,它将决定渲染引擎在遇到不在[0, 1]范围内的纹理坐标时如何进行纹理采样。我们将在
7.1.2 节中更加详细地进行阐述。
在本书之前的章节中,我们曾不止一次地提
到过OpenGL 和DirectX 在二维纹理空间中的坐
标系差异问题。重要的事情要说很多次,我们再
来回顾一下。在OpenGL 里,纹理空间的原点位
于左下角,而在DirectX 中,原点位于左上角。
幸运的是,Unity 在绝大多数情况下(特例情况
可以参见5.6 节)为我们处理好了这个差异问题,
也就是说, 即便游戏的目标平台可能既有
OpenGL 风格的,也有DirectX 风格的,但我们
在Unity 中使用的通常只有一种坐标系。Unity
使用的纹理空间是符合OpenGL 的传统的,也就
是说,原点位于纹理左下角,如图7.1 所示。
本章将介绍如何在Unity 中利用纹理采样来
实现更加丰富的视觉效果。在7.1 节中,我们将
学习如何在Unity Shader 中进行最基本的纹理采
样,并介绍纹理的属性等基本概念。7.2 节将介绍游戏中应用广泛的凹凸纹理,还会解释Unity 中
法线纹理的一些实现细节。7.3 节和7.4 节将分别介绍两类特殊的纹理类型,即渐变纹理和遮罩纹
理,这些纹理在游戏中的应用非常广泛。
需要提醒读者注意的是,本章着重讲述纹理采样的原理,因此实现的Shader 往往并不能直接
应用到实际项目中(直接使用的话会缺少阴影、光照衰减等效果)。我们会在9.5 节给出包含了纹
理采样和完整光照模型的可真正使用的Unity Shader。

在这里插入图片描述

一、7.1单张纹理

我们通常会使用一张纹理来代替物体的漫反射颜色。在本节中,我们将学习如何在Unity
Shader 中使用单张纹理来作为模拟的颜色。在学习完本节后,我们会得到类似图7.2 中的效果。
在这里插入图片描述
在本例中,我们仍然使用Blinn-Phong 光照模型来计算光照。准备工作如下。
(1)在Unity 中新建一个场景。在本书资源中,该场景名为Scene_7_1。在Unity 5.2 中,默
认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window ->
Lighting -> Skybox 中去掉场景中的天空盒子。
(2)新建一个材质。在本书资源中,该材质名为SingleTextureMat。
(3)新建一个Unity Shader。在本书资源中,该Unity Shader 名为Chapter7-SingleTexture。把
新的Unity Shader 赋给第2 步中创建的材质。
(4)在场景中创建一个胶囊体,并把第2 步中的材质赋给该胶囊体。
(5)保存场景。
打开新建的Chapter7-SingleTexture,删除所有已有代码,并进行如下修改。
(1)首先,我们需要为这个Unity Shader 起一个名字:
Shader “Unity Shaders Book/Chapter 7/Single Texture” {
(2)为了使用纹理,我们需要在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 的纹理,在3.3.2 节中,我们已经知道2D 是纹理属性
的声明方式。我们使用一个字符串后跟一个花括号作为它的初始值,“white”是内置纹理的名字,
也就是一个全白的纹理。为了控制物体的整体色调,我们还声明了一个_Color 属性。
(3)然后,我们在SubShader 语义块中定义了一个Pass 语义块。而且,我们在Pass 的第一行
指明了该Pass 的光照模式:
SubShader {
Pass {
Tags { “LightMode”=“ForwardBase” }
LightMode 标签是Pass 标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色。
(4)接着,我们使用CGPROGRAM 和ENDCG 来包围住Cg 代码片,以定义最重要的顶点着
色器和片元着色器代码。首先,我们使用#pragma 指令来告诉Unity,我们定义的顶点着色器和片
元着色器叫什么名字。在本例中,它们的名字分别是vert 和frag:
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
(5)为了使用Unity 内置的一些变量,如_LightColor0,还需要包含进Unity 的内置文件
Lighting.cginc:
#include “Lighting.cginc”
(6)我们需要在Cg 代码片中声明和上述属性类型相匹配的变量,以便和材质面板中的属性
建立联系:
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
与其他属性类型不同的是,我们还需要为纹理类型的属性声明一个float4 类型的变量
_MainTex_ST。其中,_MainTex_ST 的名字不是任意起的。在Unity 中,我们需要使用纹理名_ST
的方式来声明某个纹理的属性。其中,ST 是缩放(scale)和平移(translation)的缩写。_MainTex_ST
可以让我们得到该纹理的缩放和平移(偏移)值,_MainTex_ST.xy 存储的是缩放值,而
_MainTex_ST.zw 存储的是偏移值。这些值可以在材质
面板的纹理属性中调节,如图7.3 所示。在7.1.2 节中,
我们将更详细地解释这些纹理属性。
(7)接下来,我们需要定义顶点着色器的输入和
输出结构体:
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,以便在片元着色器中使用该坐标进行纹理采样。
(8)然后,我们定义了顶点着色器:
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 的方式来计算变换后的纹理坐标。
(9)我们还需要实现片元着色器,并在计算漫反射时使用纹理中的纹素值:
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 来计算漫反射光照的结
果,并和环境光照、高光反射光照相加后返回。
(10)最后,我们为该Shader 设置了合适的Fallback:
Fallback “Specular”
保存后返回Unity 中查看。在SingleTextureMat 的面板上,我们使用本书资源中的
Brick_Diffuse.jpg 纹理对Main Tex 属性进行赋值。
7.1.2 纹理的属性
虽然很多资料把Unity 的纹理映射描述得很简单—声明一个纹理变量,再使用tex2D 函数
采样。实际上,在渲染流水线中,纹理映射的实现远比我们想象的复杂。在本书不会过多涉及一
些具体的实现细节,但要解释一些我们认为读者必须要知道的事情。在本节中,我们将关注Unity

中的纹理属性。
在我们向Unity 中导入一张纹理资源后,可以在它的材质面板上调整其属性,如图7.4 所示。
纹理面板中的第一个属性是纹理类型。在本节中,我们使用的是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。图7.5 给出了两种模式下平铺一张纹理的效果(读者可在本书资源中的Scene_7_1_2_a 中找
到相应场景)。
▲图7.5 Wrap Mode 决定了当纹理坐标超过[0, 1]范围后将会如何被平铺
图7.5 展示了在纹理的平铺(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);
我们还可以在材质面板中调整纹理的偏移量,图7.6 给出了两种模式下调整纹理偏移量的一
个例子。
图7.6 展示了在纹理的偏移属性为(0.2, 0.6)时分别使用两种Wrap Mode 的结果,左图使用了
Repeat 模式,右图使用了Clamp 模式。
纹理导入面板中的下一个属性是Filter Mode 属性,它决定了当纹理由于变换而产生拉伸时将
会采用哪种滤波模式。Filter Mode 支持3 种模式:Point,Bilinear 以及Trilinear。它们得到的图片
滤波效果依次提升,但需要耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图

片质量。例如,当我们把一张64×64 大小的纹理贴在一个512×512 大小的平面上时,就需要放
大纹理。图7.7 给出了3 种滤波模式下的放大结果。读者可以在本书资源中的Scene_7_1_2_b 中
找到该场景。
▲图7.6 偏移(Offset)属性决定了纹理坐标的偏移量
▲图7.7 在放大纹理时,分别使用3 种Filter Mode 得到的结果
纹理缩小的过程比放大更加复杂一些,此时原纹理中的多个像素将会对应一个目标像素。纹
理缩小更加复杂的原因在于我们往往需要处理抗锯齿问题,一个最常使用的方法就是使用多级渐
远纹理(mipmapping)技术。其中“mip”是拉丁文“multum in parvo”的缩写,它的意思是“在
一个小空间中有许多东西”。如同它的名字,多级渐远纹理技术将原纹理提前用滤波处理来得到很
多更小的图像,形成了一个图像金字塔,每一层都是对上一层图像降采样的结果。这样在实时运
行时,就可以快速得到结果像素,例如当物体远离摄像机时,可以直接使用较小的纹理。但缺点
是需要使用一定的空间用于存储这些多级渐远纹理,通常会多占用33%的内存空间。这是一种典
型的用空间换取时间的方法。在Unity 中,我们可以在纹理导入面板中,首先将纹理类型(Texture
Type)选择成Advanced,再勾选Generate Mip Maps 即可开启多级渐远纹理技术。同时,我们还
可以选择生成多级渐远纹理时是否使用线性空间(用于伽玛校正,详见18.4.2 节)以及采用的滤
波器等,如图7.8 所示。
图7.9 给出了从一个倾斜的角度观察一个网格结构的地板时,使用不同Filter Mode(同时也
使用了多级渐远纹理技术)得到的效果。读者可以在本书资源中的Scene_7_1_2_c 中找到该场景。
在内部实现上,Point 模式使用了最近邻(nearest neighbor)滤波,在放大或缩小时,它的
采样像素数目通常只有一个,因此图像会看起来有种像素风格的效果。而Bilinear 滤波则使用了
线性滤波,对于每个目标像素,它会找到4 个邻近像素,然后对它们进行线性插值混合后得到最
终像素,因此图像看起来像被模糊了。而Trilinear 滤波几乎是和Bilinear 一样的,只是Trilinear
还会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远纹理技术,那么Trilinear 得

到的结果是和Bilinear 就一样的。通常,我们会选择Bilinear 滤波模式。需要注意的是,有时我们
不希望纹理看起来是模糊的,例如对于一些类似棋盘的纹理,我们希望它就是像素风的,这时我
们可能会选择Point 模式。
在这里插入图片描述
最后,我们来讲一下纹理的最大尺寸和纹理模式。当我们在为不同平台发布游戏时,需要考
虑目标平台的纹理尺寸和质量问题。Unity 允许我们为不同目标平
台选择不同的分辨率,如图7.10 所示。
如果导入的纹理大小超过了Max Texture Size 中的设置值,那
么Unity 将会把该纹理缩放为这个最大分辨率。理想情况下,导
入的纹理可以是非正方形的,但长宽的大小应该是2 的幂,例如
2、4、8、16、32、64 等。如果使用了非2 的幂大小(Non Power
of Two,NPOT)的纹理,那么这些纹理往往会占用更多的内存空
间,而且GPU 读取该纹理的速度也会有所下降。有一些平台甚至
不支持这种NPOT 纹理,这时Unity 在内部会把它缩放成最近的2
的幂大小。出于性能和空间的考虑,我们应该尽量使用2 的幂大
小的纹理。
而Format 决定了Unity 内部使用哪种格式来存储该纹理。如
果我们将Texture Type 设置为Advanced,那么会有更多的Format
供我们选择。这里不再依次介绍每种纹理模式,但需要知道的是,
使用的纹理格式精度越高(例如使用Truecolor),占用的内存空间
越大,但得到的效果也越好。我们可以从纹理导入面板的最下方看到存储该纹理需要占用的内存
空间(如果开启了多级渐远纹理技术,也会增加纹理的内存占用)。当游戏使用了大量Truecolor
类型的纹理时,内存可能会迅速增加,因此对于一些不需要使用很高精度的纹理(例如用于漫反
射颜色的纹理),我们应该尽量使用压缩格式。

参考

Unity Shader入门精要
冯乐乐

猜你喜欢

转载自blog.csdn.net/aoxuestudy/article/details/125713016
今日推荐