《shader入门精要》笔记-第7章-基础纹理

纹理的最初目的是使用一张图片来控制模型的外观.使用纹理映射(texture mapping)技术,我们可以把一张图片"黏"在模型表面,逐纹素(texel)地控制模型的颜色

在美工人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上.

单张纹理

实践

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
81
82
83
84
85
86
87
88
Shader "Custom/7.1"{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)

// 声明一个2D纹理
_MainTex ("Main Tex", 2D) = "white" {}

_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("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;

// _MainTex的名字不是随意起的,在Unity中我们使用纹理名_ST来声明某个纹理的属性.
// 其中ST是缩放(scale)和平移(translation)的缩写.
// 可以在材质面板的纹理属性调整这些值控制材质的平移和缩放.
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;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

// 使用纹理缩放和偏移属性对顶点纹理坐标进行变换
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// 或使用unity的内置函数
// o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

// 用tex2D对纹理进行采样
// 第一个参数是需要被采样的纹理,第二个参数是float2类型的纹理坐标
// 返回计算得到的纹素值
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);
}

ENDCG
}
}
FallBack "Specular"
}

20190321091817.png

纹理的属性

Texture Type

书上讲的TextureType好像过时了,现在是这样的:
20190321092454.png 20190321092516.png
这里使用最普通的texture,在后面的法线纹理一节,我们会使用Normal map类型,在后面的章节中,我们还会看到Cubemap等高级纹理类型.
我们之所以要为纹理选择合适的类型,是因为只有这样才会让Unity知道我们的意图,为Unity Shader传递正确的纹理,并在一些情况下让Unity对该纹理进行优化

Alpha Source

如果选择了From Gray Scale,那么透明通道的值将会由每个像素的灰度值生成.
关于透明效果会在第8章讲到

Wrap Mode

当纹理坐标超过[0, 1]返回后将会如何被平铺.
Repeat下,纹理将会重复.
Clamp下将会对纹理进行截取.

Filter Mode

决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式.
Filter Mode支持三种模式: Point, Bilinear, Trilinear.它们得到的图片滤波效果会依次提升,但需要消耗的性能也依次增大
纹理滤波会影响放大或缩小纹理时得到的图片的质量.
20190321094748.png
纹理的缩小过程比放大更复杂一些.缩小时,原纹理中的多个像素将会对应一个目标像素.纹理缩小更加复杂的原因在于我们往往需要处理抗锯齿问题,一个最常使用的方法就是使用多级渐远纹理(mipmapping)技术.
多级渐远纹理技术将原纹理提前用滤波处理来得到很多更小的图像,形成一个图像金字塔,每一层都是对上一层图像降采样的结果.这样在实时运行时,就可以快速得到结果像素.
例如当摄像机较远的时候,可以直接使用较小的纹理.
缺点是需要使用一定空间用于存储这些多级渐远纹理,通常会多占33%的内存空间.
在Unity中,我们可以在纹理导入面板中,首先将texture type选择成advanced,再勾选Generate Mip Maps即可开启多级渐远纹理技术.


20190321112732.png
这张图是,从一个倾斜角度观察一个网格结构的地板时,使用不同的Filter Mode(同时也使用了多级渐远纹理技术)得到的效果.

在内部实现上,Point模式使用了最邻近(nearest neighbor)滤波.在放大或缩小时,它的采样像素数目通常只有一个,因此图像看起来可能会有像素风格的效果.

而Bilinear滤波则使用了线性滤波,对于每个像素,它会找到四个临近像素,然后对它们进行线性插值混合后得到最终像素,因此图像看起来模糊了.

而Trilinear滤波几乎是和Bilinear一样的,只是Trilinear还会在多级渐远纹理之间进行了混合.如果一张纹理没有使用多级渐远纹理技术,那么Trilinear得到的结果就和Bilinear的完全一样了.

通常,我们选择Bilinear滤波格式.需要注意的是,有时我们不希望纹理看起来是模糊的,例如一些类似棋盘的纹理,我们希望它是像素风的,这时我们可能会选用Point模式.

纹理的最大尺寸和纹理模式

20190321114924.png
当我们在不同平台发布游戏时,需要考虑目标平台的纹理尺寸和质量问题.Unity允许我们为不同目标平台选择不同的分辨率.
如果导入的纹理大小超过了Max Texture Size的设置值,那么Unity将会把该纹理缩放为这个最大分辨率.
理想情况下,导入的纹理可以是非正方形的,单长宽应该是2的幂.如果使用了非2的幂的大小的纹理,那么这些纹理往往会占用更多的内存空间,而且CPU读取该纹理的速度也会下降.有一些平台甚至不支持这种NPOT纹理,这时Unity在内部会把它缩放成最近的2的幂大小.

而Format则决定了Unity内部使用哪种格式来存储该纹理.如果我们将Texture Type设置为Advanced,那么会有更多的Format供我们选择.

凹凸映射

凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便模型提供更多的细节.
这种方法不会真的改变顶点位置,只是是模型看起来凹凸不平

高度纹理

使用一张高度图来实现凹凸映射
高度图中存储的是强度值(intensity),它用于表示模型表面局部的海拔高度.

颜色越浅表明该位置的表面越向外凸起,越深表明该位置的表面越向里凹.

这种方法的优点是比较直观.我们可以从高度图明确的知道一个模型表面的凹凸情况.
缺点是计算更加复杂,在实时计算中不能直接得到表面法线,而是由像素的灰度值计算而得.因此需要消耗更多的性能.

法线纹理

法线纹理中存储的是表面的法线方向.由于法线方向的分量范围在[-1, 1],而像素的分量在[0, 1],因此我们需要做一个映射,通常使用的映射是: pixel = (normal + 1)/2

这要求我们在Shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向.反映射的过程实际就是使用上面映射函数的逆函数: normal = pixwl * 2 - 1

由于方向是相对于坐标空间来说的,那么法线纹理存在哪个坐标空间中呢?

模型空间的法线纹理和切线空间的法线纹理

模型空间的法线纹理

object-space normal map
将修改后的模型空间的表面法线存储在一张纹理中.

切线空间的法线纹理

tangent-space normal space
对于模型的每个顶点,他都有一个属于自己的切线空间.
这个切线空间的原点就是顶点本身,而z轴就是顶点的法线方向,x轴是顶点的切线方向,而y轴可由法线和切线的叉积而得,也被称为副切线(bitangent).

映射到纹理上的区别

20190321152037.png
从图可以看出,模型空间下的法线纹理看起来是五颜六色的,而是因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,而每个点存储的法线方向是各异的.
有的是(0, 1, 0),映射后存储到纹理中就对应了RGB(0.5, 1, 0.5),浅绿色;有的是(0, -1, 0),映射后存储到纹理中对应了RGB(0.5,0,0.5)的紫色.

而切线空间下的法线纹理几乎全部都是浅蓝色.这是因为,每个发现方向所在的坐标空间是不一样的,即表面每点各自的切线空间.
这种法线纹理其实是存储了每个点在各自的切线空间中的法线扰动方向.
也就是说,如果一个点的法线方向不变,那么它在它的切线空间中,新法线方向就是z轴方向,即(0,0,1),经过映射后存储在纹理中就对应了RGB(0.5, 0.5, 1)的浅蓝色.

如何选择

实际上,法线本身存储在哪个空间都是可以的,但问题是,我们的目的是计算光照而非单纯的计算法线.
而选择哪个空间,意味着我们需要把不同的信息转换到相应的坐标系中.
例如,如果选择了切线空间,我们需要把从法线纹理中得到的法线方向从切线空间转换到世界空间或其他空间中.

总体来说,用模型空间来存储法线的优点如下:

  • 实现简单,更佳直观
    我们甚至不需要模型原始的法线和切线等信息,也就是说,计算更少.生成它也很简单.而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的
  • 边界平滑
    在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少.这是因为模型空间下的法线纹理存储的是统一坐标系下的法线信息.因此在边界上通过插值得到的法线可以平滑变换.而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的,可能会在边缘处或尖锐部分造成更多的可见缝合现象

但使用切线空间有更多优点:

  • 自由度很高.
    模型空间下的法线纹理记录的是绝对的法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了.而切线空间下的法线纹理记录的是相对法线信息,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的效果.
  • 可进行UV动画.
    比如我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果.这种UV动画经常在水或者火山熔岩这种类型的物体上会经常用到.
  • 可压缩
    由于切线空间下的法线纹理中法线z方向总是正方向,因此我们可以仅存储XY方向,而推导出Z方向.而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储3个方向的值不可压缩.

实践

实践 : 在切线空间下计算光照模型

在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角,光照方向进行计算,得到最终的光照效果.
为此,我们需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中.即我们需要知道模型空间到切线空间的变换矩阵.这个矩阵的逆矩阵,即从切线空间变换到模型空间的变换矩阵,是很容易求得的: 我们在顶点着色器中按切线(x轴),副切线(y轴),法线(z轴)的顺序按列排列即可得到(数学原理见4.6.2节).在4.6.2节我们已经知道,如果一个变换仅存在旋转和平移变换,那么这个矩阵的转置矩阵就等于它的逆矩阵,而从切线空间到模型空间的变换正是符合这样的要求的变换.因此,我们把切线(x轴),副切线(y轴),法线(z轴)的顺序按行排列(因为转置了),即可得到模型空间到切线空间的变换矩阵.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
Shader "Custom/7.1"{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}

// 法线纹理的属性
// "bump"是Unity内置的法线纹理.
// Bump Scale用于控制凹凸程度,当它为0时,意味着法线纹理不对光照产生任何影响
_BumpMap ("Normal Map", 2D) = "bump"{}
_BumpScale ("Bump Scale", Float) = 1.0

_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("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;
float4 _MainTex_ST;

sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;

struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;

// 切线方向赋给tangent
float4 tangent : TANGENT;

float4 texcoord : TEXCOORD0;
};

struct v2f{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

// uv的xy分量存储_MainTex的纹理坐标
// zw分量存储_BumpMap的纹理坐标
// 在回忆一下,_Name_ST的xy代表的是缩放值,zw代表偏移值
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 实际上,_MainTex和_BumpMap使用同一组纹理坐标就行了,可以减少差值寄存器的使用数目.
// o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.xy = o.uv.zw;

// 计算副切线.后面乘以v.tangent.w是决定副切线的方向
float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz)) * v.tangent.w;

// 把切线(x轴),副切线(y轴),法线(z轴)的顺序按行排列来得到模型空间到切线空间的变换矩阵rotation
float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);

// 把光照和视角方向变换到切线空间中
o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;

return o;
}

fixed4 frag(v2f i) : SV_Target{
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);

// 利用tex2D对_BumpMap进行采样
// 法线纹理中存储的是法线经过映射后得到的像素值,因此我们要把它们反映射回来
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);

fixed3 tangentNormal;
tangentNormal = UnpackNormal(packedNormal);

// 乘以凹凸度来得到xy分量.
tangentNormal.xy *= _BumpScale;

// 由xy向量计算z向量
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));


fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}

ENDCG
}
}
FallBack "Specular"
}

实践 : 在世界空间下计算光照模型

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
Shader "Unity Shaders Book/Chapter 7/Normal Map In World Space" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale ("Bump Scale", Float) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("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;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

// Compute the matrix that transform directions from tangent space to world space
// Put the world position in w component for optimization
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

return o;
}

fixed4 frag(v2f i) : SV_Target {
// Get the position in world space
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// Compute the light and view dir in world space
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

// Get the normal in tangent space
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
// Transform the narmal from tangent space to world space
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);

return fixed4(ambient + diffuse + specular, 1.0);
}

ENDCG
}
}
FallBack "Specular"
}

:TODO 好焦躁啊,这些先留着不看…

渐变纹理