Unity-Shader读书笔记(6)

基础纹理

        纹理最初目的就是使用一张图片来控制模型外观。建模软件中的纹理展开技术就是把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。这些坐标用一个二维变量来表示(u,v),其中u是横向坐标。v是纵向坐标。因此纹理映射坐标又被称为uv坐标。

        纹理的大小可以多变比如256*256或是1028*1028。但是uv坐标的范围只有在[0,1]之间。但是纹理采样时使用的纹理坐标不一定是在这个范围内。这种不在范围内的纹理坐标有时也十分有用,比如纹理的平铺模式,它将决定渲染引擎遇到不在[0,1]范围内的纹理坐标时如何纹理采样。

平台差异

        在OpenGL中,纹理空间坐标的原点位于左下角,而在DirectX中原点位于左上角。Unity纹理空间是符合OpenGL,并且Unity自身也帮我们解决这个差异。

Ps:GitHub - candycat1992/Unity_Shaders_Book: 书籍《Unity Shader入门精要》源代码这个是Unity Shader入门精要的源代码地址,里面也有对应的资源。

单张纹理

(这里使用的纹理是Unity_Shaders_Book-master_Diffuse.JPG)

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
Shader "Learn/SingleTextureShader"
{
Properties
{
_Color ("Color",COLOR) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_SpecularColor ("SpecularColor",Color) = (1,1,1,1)
// 高光系数
_Gloss ("Gloss",Range(8,255)) = 20
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

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

struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD0;
float2 uv : TEXCOORD2;
};

float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _SpecularColor;
fixed _Gloss;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
o.worldNormal = worldNormal;
// 计算得到纹理映射值
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 通过uv坐标得到纹理中的数组和设定的颜色值一起组成了该点的漫反射颜色值
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color;
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 得到反射光
fixed3 reflectDir = reflect(-worldLight,i.worldNormal);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(i.worldNormal,worldLight));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
//fixed3 color = specular;
return fixed4(color,1);
}
ENDCG
}
}

FallBack "SPECULAR"
}

        _MainTex是我们新增加的属性,但是我们却新增了两个变量 sampler2D类型的_MainTex和 float4类型的_MainTex_ST。显然_MainTex和属性中的_MainTex相对应,而新增加的_MainTex_ST名字也不是乱取的。在Unity Shader中,我们需要使用纹理名_ST的方式来声明某个纹理的属性。其中ST是缩放(scale)和平移(translation)的缩写,其中_MainTex_ST.xy表示缩放值,_MainTex_ST.zw表示平移值。在Inpsector界面我们也可以看到,如下图所示:

        在顶点着色器的输入结构体a2v中,我们新加入了float4类型的texcoord变量语义声明为TEXCOORD0。这样Unity就会将模型的第一组纹理坐标存储到该变量中。在v2f结构体中,我们添加了float2类型的uv变量用于存储纹理坐标的uv值,以便片元着色器使用该坐标进行纹理采样。

        在顶点着色器中,我们只添加了一行计算uv坐标的代码。

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

它的意思先对纹理中的xy坐标进行缩放然后再进行平移。这行代码其实Unity提供了一个函数来帮我们计算上述过程:TRANSFORM_TEX。之后我们只需按照下面的代码调用就是了。

1
TRANSFORM_TEX(v.texcoord,_MainTex);

关于它具体的实现,大家可以去查看其对应的源码(它是在#include "UnityCG.cginc"中)。

        在片元着色器中,我们通过tex2D函数得到纹理坐标对应的纹素值。tex2D函数需要两个变量:第一个变量是纹理信息,第二个变量是纹理坐标。然后我们将得到的纹素值和我们设定颜色值进行叠加,让其作为材质的漫反射系数进行后续的计算。我们的环境光也不再是简单取UNITY_LIGHTMODEL_AMBIENT,而是将其也与我们得到的新的漫反射系数进行叠加得到新的环境光。

        我想这里大家可能有一个疑惑:为什么我们之前是直接拿我们设定的值和UNITY_LIGHTMODEL_AMBIENT作为漫反射系数和环境光,而不是像现在这样用纹素叠加后的值?简单来说,如果我们没有设置纹理图片,那么我们的默认纹理值就是"white" {}。这就意味着我们取值只能是白色rgb值为(1,1,1)。用这个做相乘更本没有意义还浪费算力。而实际上所谓纹理就是设定了材质的一些信息,而这些信息自然包含漫反射系数。而环境光本身也会受到物体影响所以我们才需要进行上面的操作。纹理(计算机图形学术语)_百度百科

Unity中的纹理属性

        在Unity点击一张图片,你就会看到如下的信息(我使用的Unity版本为2019.4.40f1):

如图所示。(这里我给的网站都是2019.4的版本,大家可以自行替换成自己的Unity版本)关于Texture inspector的信息,大家可以看官网的说明.Texture Import Settings - Unity 手册

凹凸映射

        凹凸映射(bump mapping)是纹理的另一种常见的应用。凹凸映射的目的是使用一张纹理来改变模型表面的法线,以便给模型提出更多的细节。但是这个方法并不会真正改变模型的顶点位置只是模型看起来凹凸不平。

        凹凸映射的实现有两种方法:一种是使用高度纹理(height map)来模拟表面位移(displacement)来得到一个修改后的法线值,另一种是法线映射(normal map)来直接存储表面法线。

        高度纹理:高度纹理中存储的是强度值(intensity)。纹理中的强度值表示对应模型表面局部的海拔高度。在高度图中颜色越浅表明改位置的表面越向外凸起,反之越向里凹(这个我觉得要看具体的实现,但是这个可能是大家约定俗成的规定。所以我还是记录下来了。)。这种方法的好处是非常直观,我们可以从高度图中明确地知道一个模型的凹凸情况,但缺点是计算复杂,在实时计算中不能直接得到表面法线而是需要消耗性能去计算。高度图通常会和法线映射一起使用,用来给表面凹凸额外的信息。

        法线纹理:法线纹理中存储的就是表面的法线方向。但是像素的分量范围是[0,1],而法线方向的范围是在[-1,1],因此我们需要做一个映射,通常的映射是:

        pixel = (normal + 1)/ 2。

这里法线的方向范围是[-1,1]是说法线的xyz的这三个分量值在这个范围内。比如当x绝对值为1的时候,那么因为法线是单位向量所以其值为(1,0,0),或是(-1,0,0)。但是像素rgb值在[0,1]之间大家可能会觉得奇怪,因为大多数我们使用的值是在[0,255]之间。但其实也可以算到[0,1]之间的,Unity改变颜色值的时候就有这两种方式。如下图:    

所以如果我们想用像素存储法线值,那么我们必须先对法线值做处理让其范围到[0,1]因此有了上面的公式,之后在取的时候则要将其转换回去。

        这里仍然存在一个问题就是我们存储的法线是要存储哪个坐标空间下的法线?如果我们存储的是模型空间下的法线,这种方法存储下来的纹理被称为模型空间法线纹理(object-space normal map)。但是在实际过程中,我们会存储的是切线空间(tangent space)下的法线纹理。对于模型中的每一个顶点,它都有属于它们自身的切线空间。而切线空间的原点就是顶点自身,z轴是该顶点的法线方向,x轴是顶点的切线方向而y轴是z轴和x轴的叉积。y轴又被称为副切线(bitangent)或副法线。以这种方式存储法线的纹理被称为切线空间的法线纹理(tangent-space normal map)。其实法线存储在哪个坐标空间下都可以,但是我们需要的不仅仅是法线的信息,后续的光照计算才是我们的目的。而坐标空间的选择就会影响我们坐标系的信息转换。

        如果使用模型空间有以下的优点:

  1. 实现简单,更加直观。我们甚至不需要模型元素的法线和切线等信息,也就是说,计算量更少。生成它也简单,而如果于要生成切线空间下的法线纹理,由于模型的切线一般是和uv方向相同,因此得到的效果比较好的法线映射就要求纹理映射也是连续的。

  2. 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的同一坐标系的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能在边缘处或者尖锐的部分造成更多可见的缝合迹象。

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

  1. 自由度更高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的哪个模型而应用到其他的某些上效果就完全错误了。而切线空间下法线纹理记录的是相对法线信息,这就意味着,即便把该纹理应用到完全不同的网格上,也可以得到一个合理的结果。

  2. 可进行UV动画。比如我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。原因同上。这种UV动画在水或者火山熔岩这种类型的物体上会经常用到。

  3. 可以重用法线纹理。比如一个砖块,我们仅使用一张法线纹理就可以用到所有的6面上。

  4. 可压缩。由于切线空间下的法线纹理中法线的z方向总是正方向,因此我们可以仅存储XY方向,而推导Z方向。而模型空间中法线纹理由于每一个方向都可能,因此必须存储3个方向的值不可压缩。

凹凸映射的实现(世界坐标)

PS:这里使用的纹理是资源中的Brick_Normal.JPG

        在世界坐标下实现凹凸映射,我们要将法线从切线空间转变到世界空间下。这就需要我们先得到切线空间到世界空间的转变矩阵。

        我们可以先想世界空间如何转变到切线空间。切线空间中的xyz轴,我们可以看做是世界空间的xyz轴进行某种变换得到的。那么向量的变换中平移是没有意义的,那么这个变换矩阵就剩下旋转和缩放的可能。但切线空间中的xyz分量和世界空间下的xyz分量我们默认都是单位向量所以它们之间的转换就不存在缩放。因此我们可以得出结论世界空间转变到切线空间仅需要一个旋转矩阵。且世界空间的xyz形成的3*3矩阵是单位矩阵,所以世界空间转变到切线空间的旋转矩阵就是切线空间xyz形成的3*3矩阵。因为这个矩阵又是仅有旋转行为的矩阵,这就意味着这个矩阵是正交矩阵。那么其逆矩阵就是切线空间xyz形成的3*3矩阵的转置矩阵。这也就是说从切线空间转变到世界空间的转变矩阵就是切线空间xyz形成的3*3矩阵的转置。

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
107
Shader "Learn/NormalWorldSpaceShader"
{
Properties
{
_Color("Color",COLOR) = (1,1,1,1)
_MainTex("Texture", 2D) = "white" {}
_SpecularColor("SpecularColor",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8,255)) = 20
// 法线贴图
_NormalTex("NormalTex", 2D) = "bump" {}
_NormalScale ("Normal Scale",Float) = 1.0
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

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

struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 T2W0 : TEXCOORD1;
float4 T2W1 : TEXCOORD2;
float4 T2W2 : TEXCOORD3;
};

float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
float _NormalScale;
float4 _SpecularColor;
fixed _Gloss;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
float3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
// 副法线
float3 worldBinormal = cross(normalize(worldNormal),normalize(worldTangent)) * v.tangent.w;

// 计算得到纹理映射值
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _NormalTex_ST.xy + _NormalTex_ST.zw;
// 得到变换矩阵,并将世界坐标存储其中
o.T2W0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.T2W1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.T2W2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 通过uv坐标得到纹理中的数组和设定的颜色值一起组成了该点的漫反射颜色值
fixed3 albedo = tex2D(_MainTex,i.uv.xy).rgb * _Color;
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 从信息中取出我们想要的值
float3 worldPos = fixed3(i.T2W0.w,i.T2W1.w,i.T2W2.w);
// 得到在切线空间下的法线值
fixed3 bump = UnpackNormal(tex2D(_NormalTex,i.uv.zw));
bump.xy *= _NormalScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy,bump.xy)));
// 将其转变到世界空间下,代替之前的法线值
bump = normalize(half3(dot(i.T2W0.xyz,bump),dot(i.T2W1.xyz,bump),dot(i.T2W2.xyz,bump)));
// 得到反射光
float3 reflectDir = reflect(-worldLight,bump);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos);

// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(bump,worldLight));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}

FallBack "Specular"
}

        对于法线纹理,我们使用“bump”作为它的默认值。“bump”是Unity内置的法线纹理,当没有提供任何法线纹理时,“bump”就对应了模型自带的法线信息。_NormalScale是用于控制法线纹理作用的凹凸程度。如果它为0,那么就意味着法线纹理不会产生任何作用。与其他所有的贴图一样,法线贴图也是要有一个对应的缩放平移变量。在上述代码中,这个变量就是_NormalTex_ST。

        在顶点着色器的结构体中,我们使用的新的语义TANGENT来告诉Unity把顶点的切线方向填充到tangent中。

        而在片元着色器的结构体中,我们抛弃了之前的世界坐标变量和世界法线变量。并且我们还添加了其他的3个float4类型的变量。我们使用这三个变量用来存储模型该点对应的世界切线坐标、世界副法线坐标、世界法线坐标和世界坐标。在顶点着色器函数的这段代码中就体现这样的思想:

1
2
3
4
// 得到变换矩阵,并将世界坐标存储其中
o.T2W0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.T2W1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.T2W2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);

这样做法可以充分利用插值寄存器的存储空间。而这3个变量xyz值所对应的3*3矩阵就是我们要的从切线空间转变到世界空间的变换矩阵。这里大家可能有些疑惑即为什么矩阵要这么排列,不能是下面这样的排列吗?

1
2
3
4
// 得到变换矩阵,并将世界坐标存储其中
o.T2W0 = float4(worldTangent.x,worldTangent.y,worldTangent.z,worldPos.x);
o.T2W1 = float4(worldBinormal.x,worldBinormal.y,worldBinormal.z,worldPos.y);
o.T2W2 = float4(worldNormal.x,worldNormal.y,worldNormal.z,worldPos.z);

其实这样也可以但是后面计算就很麻烦。

        在frag函数中,我们使用UnpackNormal函数对采集的法线纹理信息做处理。书上说必须将其纹理的纹理类型手动设定为Normal Texture。如下图

但是我使用的Unity2019.4.40f1没有设置表现似乎差不多,但是最好还是要设置一下防止一些奇奇怪怪的问题。如果我们不用UnpackNormal函数也可以使用下面的方法进行处理:

1
2
fixed3 bump = tex2D(_NormalTex,i.uv.zw);
bump.xy = (bump.xy * 2 - 1) * _NormalScale;

其实这个方法就是将采集到的纹素信息按照pixel = (normal + 1)/ 2(这个是用像素存储切线空间下的法线信息公式)求得normal,然后得到的值再乘上伸缩值。

        在frag函数中,你可能还会对这段代码有些疑惑:

1
bump.z = sqrt(1.0 - saturate(dot(bump.xy,bump.xy)));

先前我们说过,因为法线必定是单位向量,所以我们不需要存储所有的值。而剩下的值可以通过单位向量的计算得到。我们知道所谓单位向量就是

1
X * X + Y * Y + Z * Z = 1

因此我们可以使用这段代码得到存储法线真正的z值。然后再通过切线空间转世界空间的矩阵得到法线在世界空间的值。

        在shader中代码的转换其实并没有使用矩阵计算而是直接使用点乘。实际上就是原本的矩阵进行转置后按照矩阵乘法进行运算。最终得到我们想要的在世界空间下的法线值。在计算光照的时候,我们用这个法线值代替原本的顶点法线。最终得到我们想要的效果。

PS:在书中提供了两种方法实现凹凸映射:在切线空间下进行计算和在世界空间下进行计算。但是这份笔记中我只想记录世界空间下的计算。首先是因为书中对于凹凸映射在切线空间下实现不能适用现在的Unity,且书中的描述也是有错的。这里书籍作者有提及【常见问题】关于法线转换的问题(以及在切线空间下计算法线纹理的问题) · Issue #45 · candycat1992/Unity_Shaders_Book · GitHub。而且因为最近我个人生活的原因,我对编程快失去热情了。所以我不想再太多的探究其中的道理。或许有天我重拾热忱,我再把这个补齐吧。

渐变纹理

PS:这里使用的纹理是资源中的Ramp_Texture0.psd,Ramp_Texture1.psd,Ramp_Texture2.psd

        渐变纹理在书中也只是简单介绍了一下,我个人觉得下面代码的写法只能用在半兰伯特模型上其他的光照模型应该要用其他的方式去采集纹理。有兴趣的话大家可以看一下这篇文章「UnityShader笔记」08. 基础卡通渲染—渐变纹理_睦月兔的博客-CSDN博客

        我们在使用渐变纹理的时候要注意的是,我们需要将纹理的Wrap Mode设置为Clamp模式。这样就可以防止因为浮点数精度问题导致的采样错误。理论上半兰伯特模型取得的值会在[0,1]之间,但是有可能存在意外得到1.00001这样的数值。如果我们不是使用Clamp模式就会出现问题。

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
Shader "Learn/RampTextureShader"
{
Properties
{
_Color ("Color",COLOR) = (1,1,1,1)
// 用于存储渐变纹理
_MainTex ("Texture", 2D) = "white" {}
_SpecularColor ("SpecularColor",Color) = (1,1,1,1)
// 高光系数
_Gloss ("Gloss",Range(8,255)) = 20
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

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

struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD0;
};

float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _SpecularColor;
fixed _Gloss;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
o.worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 半兰伯特值,为了让黑的地方也有光照。并不是真实情况下的光照
fixed halfLambert = 0.5 * dot(normalize(i.worldNormal),worldLight) + 0.5;
// 以改点的半兰伯特值作为uv坐标得到纹理中的数组和设定的颜色值一起组成了该点的漫反射颜色值
fixed3 albedo = tex2D(_MainTex,fixed2(halfLambert,halfLambert)).rgb * _Color.rgb;
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

// 得到反射光
fixed3 reflectDir = reflect(-worldLight,i.worldNormal);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * albedo;
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
//fixed3 color = specular;
return fixed4(color,1);
}
ENDCG
}
}
}

遮罩纹理

        遮罩纹理允许我们可以保护某些区域,使它们免于某些修改。比如之前的我们使用高光反射的Shader。当我们使用的时候,这Shader就应用到物体所有表面上。但是如果我需要某些地方再暗一点,某些地方再亮一点。那么我就需要使用遮罩纹理去实现这个功能。当然遮罩纹理不只这些用处。

        使用遮罩纹理的流程一般是通过采得到遮罩纹理的纹素值,然后使用其中某个通道的值来与Shader的一些属性进行相乘,这样当通道的值为0的时候就可以保护表面不受这些属性的影响。

PS:这里用到的纹理是:Wall_Diffuse.tga(普通纹理),Wall_Height.tga(遮罩纹理),Wall_Normal.tga(法线纹理)

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
107
108
109
110
111
112
113
114
115
Shader "Learn/MaskTextureShader"
{
Properties
{
_Color("Color",COLOR) = (1,1,1,1)
_MainTex("Texture", 2D) = "white" {}
_SpecularColor("SpecularColor",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8,255)) = 20
// 法线贴图
_NormalTex("NormalTex", 2D) = "bump" {}
_NormalScale ("Normal Scale",Float) = 1.0
// 遮罩贴图
_MaskTex("MaskTex",2D) = "while"{}
_MaskScale("MaskScale",Float) = 1.0
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

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

struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 T2W0 : TEXCOORD1;
float4 T2W1 : TEXCOORD2;
float4 T2W2 : TEXCOORD3;
};

float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
float _NormalScale;
float4 _SpecularColor;
fixed _Gloss;
sampler2D _MaskTex;
float4 _MaskTex_ST;
float _MaskScale;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
float3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
// 副法线
float3 worldBinormal = cross(normalize(worldNormal),normalize(worldTangent)) * v.tangent.w;

// 计算得到纹理映射值
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _NormalTex_ST.xy + _NormalTex_ST.zw;
// 得到变换矩阵,并将世界坐标存储其中
o.T2W0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.T2W1 = float4(worldTangent.y,worldBinormal.y,worldNormal.x,worldPos.y);
o.T2W2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 通过uv坐标得到纹理中的数组和设定的颜色值一起组成了该点的漫反射颜色值
fixed3 albedo = tex2D(_MainTex,i.uv.xy).rgb * _Color;
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 从信息中取出我们想要的值
float3 worldPos = fixed3(i.T2W0.w,i.T2W1.w,i.T2W2.w);
// 得到在切线空间下的法线值
fixed3 bump = UnpackNormal(tex2D(_NormalTex,i.uv.zw));
bump.xy *= _NormalScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy,bump.xy)));
// 将其转变到世界空间下,代替之前的法线值
bump = normalize(half3(dot(i.T2W0.xyz,bump),dot(i.T2W1.xyz,bump),dot(i.T2W2.xyz,bump)));
// 得到反射光
float3 reflectDir = reflect(-worldLight,bump);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos);

// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(bump,worldLight));
// 获取遮罩值
fixed specularMask = tex2D(_MaskTex,i.uv.xy).r * _MaskScale;
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss) * specularMask;
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}

FallBack "Specular"
}

这里的高光反射是用纹素中的r值进行遮罩取舍。并且用一个属性 _MaskScale控制强度。