基础概念

  • 纹理映射(Texture Mapping)
    纹理映射指的是将一张贴图映射在模型表面,逐纹素(texel)的控制模型的颜色。
  • 纹理映射坐标(Texture-mapping Coordinates)
    通过模型的纹理展开(展uv),我们可以获得一组纹理映射坐标,储存在每个顶点上。通常纹理映射坐标由(u, v)来表示,所以我们会简称纹理映射坐标为UV坐标。
    不管贴图实际上有多大,UV坐标的范围通常被归于[0, 1]之间,并以纹理贴图的左下角为原点
  • 纹理空间(Texture Image Sace)
    纹理空间在纹理映射坐标的基础上乘以了纹理分辨率(如分辨率为256*256,新的uv坐标为(256u,256v))而得到的新坐标,用于寻找纹理贴图上对应的像素点的信息
  • 纹理采样(Texture Sampleing)
    我们获取纹理映射后对应纹理贴图像素点的过程

纹理管线的完整流程,可参考:https://zhuanlan.zhihu.com/p/393323667

Unity中的纹理属性

在向Unity导入纹理之后,检查器中可以对纹理进行一系列设置:

具体的设置见下:

纹理类型(Texture Type)

我们可以选择设置成普通的纹理贴图、法线贴图或者是光照贴图等等。选择合适的类型能让Unity更好的针对贴图进行优化,节约游戏空间

透明通道源(Alpha Source)

设置透明通道的值。默认透明来自于贴图本身,但是也可以通过灰阶来设置透明通道

平铺模式(Wrap Mode)

决定了当模型顶点对应的纹理坐标超过[0, 1]范围之后的平铺方式。按常理来说我们会把纹理差不多完整的弄上去,但可能造成效果不好看,所以需要人为的对纹理进行平移或缩放(_Texture_ST),这时候纹理映射坐标就会超出[0,1]的范畴。

以下是几种常见平铺模式的效果图:

滤波模式(Filter Mode)

相当于PS中改变图像大小时选择的图像处理模式。我们的纹理不一定正好对上物体,可能偏大(几个纹素对应一个像素)、偏小(一个纹素对应几个像素)或存在一定角度的偏差(旋转),因此需要进行纹理过滤。滤波模式决定了当纹理被放大或缩小时使用哪种滤波模式。按照滤波效果和性能排序,三种模式的顺序(又差到优)为:Point < Bilinear < Trilinear

他们的效果图如图所示:

Point:最近邻(Nearst Neighbor),得到类似像素的风格,非常适合本身就是像素风格纹理的放大缩小
Bilinear:双线性插值,对于每个像素与临近的四个像素进行线性插值,得到有些模糊的感觉。计算方式如下:

Trilinear:立方卷积,在基于Bilinear的基础上对多级渐远纹理进行的混合

多级渐远纹理(Mipmapping)

通过勾选下列选项即可开启多级渐远纹理。这通常应用于频繁地需要将纹理缩小、或者纹理延伸到无穷远以至于远处的纹理是十分小的情况。多级渐远通过提前对纹理进行滤波得到纹理金字塔来减少渲染时间(经典的牺牲空间换取时间)

纹理大小(Texture Size)

我们可以设置纹理的最大大小作为纹理的最大分辨率。理想情况下,纹理应该是正方形的,并且长宽的大小是2的幂。NPOT(Non Pow Of Two)类型的纹理处理会占用更多的内存,读取速度也会有所下降甚至不被支持
当对纹理没有特殊要求的时候,请压缩纹理来节约空间

几种纹理贴图介绍

渐变纹理

这种纹理能够更加灵活的控制光线和阴影以达到更为微妙的效果,比如下图中类卡渲风格和阴影异色模糊渐变的效果:

真正意义上的卡渲的高光不是这样的,在之后的章节会提及。另外,需要注意的是,用于渐变渲染的贴图的平铺模式必须设置为Clamp,否则会因为float精度问题在高光区域出现黑点,如下图:

遮罩纹理

类似于图像遮罩、层遮罩等等,遮罩允许我们保护纹理的某些区域使其不受光照反射影响,从而更精细地控制光照细节,达成微妙的效果
遮罩纹理渲染的一般流程为:

  • 利用tex2D()对遮罩纹理进行采样
  • 利用纹理采样的某个通道解码(或者不用,看情况)之后的某种属性与某个值A(比如高光反射)相乘
    这样一来,当纹素解码出来的属性为0时,该纹素不受某个值A的影响。影响随属性大小改变

图形学的吝啬
只是随口一提罢了,正好书上也有提到:
一张纹理贴图,不同的分量(通道)可能包含了完全不同的信息。比如法线贴图的rgb表示的是法线的扰动大小(是偏移向量的三个分量),但是a通道可能什么也没有表示,这其实是一种浪费。为了最大限度的利用一张纹理贴图,我们可以:

  • R:储存高光反射强度
  • G:储存边缘光照强度
  • B:储存高光反射指数
  • A:自发光强度

并指定相应的映射公式来满足需要



Shader函数学习:tex2D与TRANSFORM_TEX

  • tex2D(sampler2D tex, fixed2 s)
1
tex2D(_MainTex, i.uv);

这个方法其实就是对一张纹理_MainTex中的一个点进行采样。我们要得到整个纹理的颜色,就需要所有像素对应的uv点(并储存到一组纹理uv中)

  • TRANSFORM_TEX(fixed4 tex, sampler2D texName)
    这是一个Unity封装过的内置方法:
1
2
3
4
// 第一个参数:顶点uv
// 第二个参数:要处理的纹理
// o.uv:转移到片元着色器
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

该函数的作用就是对uv进行scale和offset(变化后的纹理坐标),也就等价于:

1
2
// 将_MainTex的变化储存在uv的xy坐标中
uv.xy = uv.xy * _MainTex__ST.xy + _MainTex_ST.zw

单张纹理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Shader "Custom/单张纹理"        // 基于Blinn-Phong高光模型
{
Properties{
_Color ("整体色调", Color) = (1.0, 1.0, 1.0, 1.0) // 其实就是漫反射颜色
// 2D是纹理属性的声明方式
_MainTex("纹理", 2D) = "white" {}
_Specular("高光反射颜色", Color) = (1.0, 1.0, 1.0, 1.0)
_Gloss("光泽度", Range(8.0, 256)) = 20
}
SubShader{
Pass{
Tags{ "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"

fixed4 _Color;
sampler2D _MainTex;
// 这个名字并不是随便起的,而是按照“纹理名字_ST”的方式来**声明对应纹理的属性**。ST指缩放(Scale,xy)和平移(Trans,zw)
// 存在该变量是因为我们可能要对一个物体的纹理进行缩放和偏移来获得更好的效果
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; // 顶点的uv坐标
};

v2f vert(a2v v){
v2f o;
// 顶点坐标从模型空间到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 世界空间的顶点法线
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
// 顶点的世界空间位置
o.worldPos = mul(unity_ObjectToWorld, v.vertex);

// 二维纹理uv坐标,为基础值 * 贴图缩放(分辨率) + 贴图偏移
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // 投影函数,获得uv坐标,与上面那行等同
return o;
}

fixed4 frag(v2f i) : SV_Target{
// 用于漫反射和高光计算
const fixed3 worldNormal = normalize(i.worldNormal); // 映射到0~1之间
const fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 世界空间的入射光单位向量,映射到0~1之间

// 反照率与环境光
const fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; // 反照率,对贴图进行纹理采样(计算出的纹素值与主色调相乘)
const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光,与反照率有关(因为有贴图颜色的存在)

// 计算漫反射光线
const fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

// 计算高光,使用Blinn-Phong公式
const fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // 计算视觉方向(v)的单位向量
const fixed3 halfDir = normalize(worldLightDir + viewDir); // 计算h单位向量,使用Blinn公式
const fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

// 呈现颜色:环境光 + 漫反射 + 高光反射
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}