UnityShader入门——纹理和透明
一、纹理
1.纹理的概念
(1)纹理是用来干嘛的
- 骨:模型骨骼
- 肉:三角面片(由模型顶点围成模型的轮廓)
- 皮:纹理图片
纹理的主要作用
:使用一张图片
来控制模型的外观
- 使用纹理映射技术,将图片和模型联系起来,让模型能呈现出图片中的颜色表现
纹理映射
:
- 它用来将图像(纹理)映射到三维模型的表面,从而赋予模型更加真实和细致的外观。
- 这个过程实际上是将二维图像映射到三维空间的过程
(2)如何进行纹理映射?
利用纹理展开技术把纹理映射坐标存储在每个顶点上。模型表面的顶点都与纹理坐标相关联
纹理坐标通常使用二维坐标系统(称为UV坐标),其中U表示水平轴,V表示垂直轴
(3)UV坐标对于Shader开发的意义
- 我们在进行Shader开发时,在顶点着色器回调函数传入的数据中可以获取到模型中的UV坐标的数据。
- 有了UV坐标,只要有正确的纹理贴图(图片),就可以用顶点的UV坐标从图片中取出映射的颜色用于渲染。
(4)UV坐标的注意事项
纹理坐标(UV坐标)的横轴和纵轴的取值范围是
被归一化过的
,在[0,1]的范围中
主要目的是为了适应不同大小的纹理图片(256x256、512x512、1024x1024等等)
只有顶点中记录了UV坐标
,因此在数据传递进片元着色器之前,UV坐标会在中间阶段进行插值运算。也就是
每个片元中得到的UV
坐标大部分都是插值计算得结果
,因此我们可以得到图片中非顶点位置的颜色
2. 单张纹理
(1)书写单张纹理颜色采样Shader
- 第一步 完成Shader文件基本结构,让其不报错
- 第二步 纹理属性和CG成员变量声明
- 关键知识点:
- CG中映射ShaderLab中的纹理属性,需要有两个成员变量
- 一个用于映射纹理颜色数据,一个用于映射纹理缩放平移数据
ShaderLab中的属性:
图片属性(2D)。用于利用UV坐标提取其中颜色CG中用于映射属性的成员变量
:
- 1.sampler2D 用于映射纹理图片
- 2.float4 用于映射纹理图片的缩放和平移。(固定命名方式 纹理名_ST (S代表scale缩放 T代表translation平移))
- 第三步 用缩放平移参数参与uv值计算
如何获取模型中携带的uv信息
?
- 在顶点着色器中,我们可以利用
TEXCOORD语义
获取到模型中的纹理坐标信息- 它是一个float4类型的
- xy获取到的是纹理坐标的水平和垂直坐标
- zw获取到的是纹理携带的一些额外信息,例如深度值等
如何计算??
- 固定算法:先缩放,后平(偏)移。缩放用乘法,平(偏)用加法
纹理坐标.xy * 纹理名_ ST.xy + 纹理名_ ST.wz
- 或者直接用内置宏
TRANSFORM_TEX(纹理坐标变量, 纹理变量)
- 第四步 在片元着色器中进行纹理颜色采样
(2)重要纹理相关设置回顾
Texture Type(纹理图片类型) 和 Texture Shape(纹理图片类型)
决定了我们是否能在Shader当中获取正确数据
Wrap Mode(循环模式)
决定了缩放偏移的表现效果
- Repeat:在区块中重复纹理
- Clamp: 拉伸纹理的边缘
- Mirror:在每个整数边界上镜像纹理以创建重复图案
- Mirror Once:镜像纹理一次,然后将拉伸边缘纹理
- Per-axis:单独控制如何在U轴和V轴上包裹纹理
Filter Mode(过滤模式)
决定了放大缩小纹理时看到的图片质量
- Point:纹理在靠近时变为块状
- Bilinear:纹理在靠近时变得模糊
- Trilinear:与Bilinear类似,但纹理也在不同的Mip级别之间模糊
- 过滤模式在开启MipMaps根据实际表现选择,可以达到不同的表现效果
(3)单张纹理结合BlinnPhong光照模型
在计算时,有以下的3点注意点
纹理颜色
需要和漫反射材料颜色
进行乘法叠加
, 它们两共同影响最终的颜色
1 fixed3 col = tex2D(_MainTex, i.uv) * _LambertColor;兰伯特光照模型计算时,漫反射材质颜色使用 1 中的叠加颜色计算
1 fixed3 lambertColor = LambertColor(i.wNormal) * col ;最终使用的环境光叠加时,环境光变量UNITY_LIGHTMODEL_AMBIENT需要和 1 中颜色进行乘法叠加
为了避免最终的渲染效果偏灰
1 fixed3 color = col * UNITY_LIGHTMODEL_AMBIENT + blinnPhongColor +lambertColor;
- 其他的计算步骤同BlinnPhong的逐片元光照实现
(4)实现代码
效果:上为没有光照,下为有光照
代码:
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 Shader "Unlit/SimpleTexture" {
Properties {
//主纹理
_MainTex ("Texture", 2D) = "white" { }
//漫反射材质光照颜色
_LambertColor ("LambertColor", Color) = (1, 1, 1, 1)
//高光材质光照颜色
_BlinnPhongColor ("BlinnPhongColor", Color) = (1, 1, 1, 1)
_HighLightValue ("HighLightValue", Range(0, 20)) = 10
}
SubShader {
Tags { "RenderType" = "Opaque" }
LOD 100
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
// make fog work
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float2 uv : TEXCOORD0;
//裁剪空间下顶点
float4 vertex : SV_POSITION;
//世界空间下法线
float3 wNormal : NORMAL;
//世界空间下顶点
float3 wPos : TEXCOORD1;
};
//映射对应纹理属性的图片颜色数据
sampler2D _MainTex;
//映射对应纹理属性的缩放 平移数据
float4 _MainTex_ST;
fixed3 _LambertColor;
fixed3 _BlinnPhongColor;
float _HighLightValue;
//漫反射颜色计算
//参数:世界空间下法线
fixed3 LambertColor(fixed3 wNormal) {
float3 lightDir = normalize(_WorldSpaceLightPos0);
return _LightColor0.rgb * max(0, dot(lightDir, wNormal));
}
//高光颜色计算
//参数1:世界空间下顶点
//参数2:世界空间下法线
fixed3 BlinnPhongColor(fixed3 wPos, fixed3 wNormal) {
//计算标准化视口方向
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos.xyz);
//计算标准化入射光方向
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
//计算半角向量
float3 halfDir = normalize(viewDir + lightDir);
return _LightColor0 * _BlinnPhongColor * pow(max(0, dot(wNormal, halfDir)), _HighLightValue);
}
v2f vert(appdata v) {
v2f o;
//用平移缩放值参与uv计算
//o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw
//如果没有进行缩放和平移 那么 这个计算后 值是不会产生变化的
//因为缩放默认值是1和1 ,平移默认值是0和0
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(v.vertex);
o.wPos = UnityObjectToWorldDir(v.vertex);
o.wNormal = UnityObjectToWorldNormal(v.normal);
return o;
}
fixed4 frag(v2f i) : SV_Target {
// sample the texture
//这传入的uv 是经过插值运算后的 就是每一个片元都有自己的一个uv坐标
//这样才能精准的在贴图当中取出颜色
//`纹理颜色`需要和`漫反射材质颜色` 进行`乘法叠加`, 它们两共同影响最终的颜色
fixed3 col = tex2D(_MainTex, i.uv) * _LambertColor;
//新知识点:兰伯特光照模型计算时,漫反射材质颜色使用 1 中的叠加颜色计算
fixed3 lambertColor = LambertColor(i.wNormal) * col ;
fixed3 blinnPhongColor = BlinnPhongColor(i.wPos, i.wNormal);
//环境光变量UNITY_LIGHTMODEL_AMBIENT需要和 1 中颜色进行乘法叠加,为了避免最终的渲染效果偏灰
fixed3 color = col * UNITY_LIGHTMODEL_AMBIENT + blinnPhongColor +lambertColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
3. 凹凸纹理
(1)凹凸纹理是用来做什么的?
纹理
除了可以用来进行颜色映射
外,另外一种常见的应用就是进行凹凸映射
。
凹凸映射的目的
:是使用一张纹理来修改模型表面的法线
,让我们不需要增加顶点,而让 模型看起来有凹凸效果。原理:
光照的计算
都会利用法线参与
计算,决定最终的颜色表现效果。那么在计算“假” 凹凸面时``,使用“真”凹凸面的法线参与计算
,呈现出来的效果可以以假乱真
凹凸纹理最大的作用
:
- 就是
让模型
可以在不添加顶点
(不增加面)的情况下 让模型看起来同样充满细节(凹凸感)
,是一种视觉上的“欺骗”技术。要进行凹凸映射,目前有两种主流方式: 1.高度纹理贴图 2.法线纹理贴图
(2)高度纹理贴图(了解)
高度纹理贴图一般简称
高度图
它存储了模型表面上每个点的高度信息
。通常它使用灰度图像,其中不同灰度值 表示不同高度
较亮区域通常对应较高的点
,较暗的区域对应较低的点
。 它主要用于模拟物体表面的位移
。![]()
存储规则:图片中的某一个像素点的
RGB值是相同的
,都表示高度值
,A值一般 情况下为1。高度值范围一般为0~1,0代表最低,1 代表最高
优点:可以通过高度图很
明确的知道模型表面的凹凸情况
缺点:
无法在Shader中直接得到模型表面点的法线信息
, 而是需要通过额外的计算得到,因此会增加性能消耗
,所以 我们几乎很少使用它。
我们在使用凹凸纹理时,一般都会使用法线纹理贴图
。
(3)法线纹理贴图
- 法线纹理贴图一般简称
法线贴图 或 法线纹理
。它存储了模型表面上每个点的法线方向。
- 存储规则:图片中的
RGB
值分别存储法线的X、Y、Z分量值
,A值可以用于存储其他信息, 比如材质光滑度等。- 优点:从法线贴图中取出的数据便是法线信息,可以直接 简单处理后就参与光照计算,性能表现更好
- 缺点:我们无法直观的看出模型表面的凹凸情况
- 法线纹理贴图读取分量数据的规则
- 由于
法线XYZ分量范围在[-1,1]之间
, 而像素RGB分量范围在[0,1]之间
因此我们需要做一个映射计算存储图片时
:像素分量 = (法线分量 + 1) / 2
因此当我们取出像素分量使用时需要进行逆运算读取数据时
:法线分量 = 像素分量 * 2 -1
两种法线纹理贴图的存储方式
基于模型空间的法线纹理
基于切线空间的法线纹理
切线空间下的法线纹理贴图是蓝色的原因
为什么要使用切线空间下的法线纹理贴图
(4)法线贴图的计算方式
- 两种主流计算方式
- 在
切线空间
下进行光照计算,需要把光照方向、视角方向变换到切线空间
下参与计算- 在
世界空间
下进行光照计算,需要把法线方向变换到世界空间
下参与计算
各自的优缺点——效率
在切线空间中计算
,效率更高,因为可以在顶点着色器中就完成对光照、视角方向的矩 阵变换,计算量相对较小。
( 矩阵变换在顶点着色器中计算)
在世界空间中计算,效率较低
,由于需要对法线贴图进行采样,所以变换过程必须在片 元着色器中实现,我们需要在片元着色器中对法线进行矩阵变换。
( 矩阵变换在片元着色器中计算)
各自的优缺点——全局效果
在切线空间中计算,对全局效果的表现可能会不够准确
在处理一些列如镜面反射、环境映射效果时表现效果可能不够准确
在世界空间中计算,对全局效果的表现更准确
可以更容易的应用于全局效果的计算
若没有全局效果要求,我们优先使用在切线空间下进行光照计算,因为它效率较高
在切线空间下计算
在切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下参与计算.
关键点
:
计算模型空间到切线空间的变换矩阵
其它用到的核心知识:
得到模型空间光的方向:
ObjSpaceLightDir
(模型空间顶点坐标)得到模型空间视角方向:
ObjSpaceViewDir
(模型空间顶点坐标)得到光方向和视角方向相对于模型空间的数据表达后,再与模型空间到切线空间的变换矩阵进行运算
即可将他们转换到切线空间下参与后续计算。
在世界空间下计算
在世界空间下进行光照计算,需要把法线方向变换到世界空间下参与计算
关键点:
计算切线空间到世界空间的变换矩阵
关键知识点补充:
模型空间下的切线数据
模型数据中的切线数据为float4类型的,其中的w表示副切线的方向 用法线和切线叉乘得到的副切线方向可能有两个,用切线数据中的w与之相乘确定副切线方向
![]()
Unity当中的法线纹理类型
当我们把纹理类型设置为Normal map(法线贴图)时,我们可以使用Unity提供的内置函数 UnpackNormal来得到正确的法线方向。
该函数内部不仅可以进行法线分量 = 像素分量 * 2 –1 的逆运算
,还会进行解压运算(Unity会根据不同平台对法线纹理进行压缩)
法线纹理属性命名一般为_BumpMap(凸块贴图)
我们还会声明一个名为_BumpScale (凸块缩放) 的float属性 它主要用于控制凹凸程度
当它为0时,表示没有法线效果,法线的影响会被消除 当它为1时,表示使用法线贴图中的原始法线信息,没有缩放 我们可以根据实际需求调整它的值,来达到视觉上令人满意的效果。
如果使用的凹凸纹理不是法线纹理,而是高度纹理,我们需要进行如下设置
图片类型设置为Normal map(法线贴图)
勾选 Create from Grayscale(从灰度创建)
这样我们就可以把高度纹理当成切线空间下的法线纹理处理了
多出的Bumpiness(颠簸值)控制凹凸程度
Filtering(过滤模式)决定计算凹凸程度的算法
Sharp:滤波生成法线
Smooth:平滑的生成法线
(5)法线贴图编写流程
效果展示:从左到右依次为(在切线空间计算,在世界空间计算,无法线)
在
切线空间
下进行光照计算需要把
光照方向、视角方向变换到切线空间
下参与计算关键点:
属性相关
- 漫反射颜色
- 单张纹理
- 法线纹理
- 凹凸程度
- 高光反射颜色
- 光泽度
结构体相关
顶点着色器中传入:
- 可以使用 UnityCG.cginc 中的 appdata_full。其中包含了我们需要的顶点、法线、切线、纹理坐标相关数据
片元着色器中传入:
- 自定义一个结构体:其中包含 裁剪空间下坐标、uv坐标、光的方向、视角的方向。
顶点着色器回调函数中
顶点坐标模型转裁剪
单张纹理和法线纹理 UV坐标缩放偏移计算
副切线计算。用模型空间中的法线和切线进行叉乘 再乘以切线中的w(确定副切线方向)
构建模型空间到切线空间的变换矩阵
—— 切线 ——
—— 副切线 ——
—— 法线 ——
将光照方向和视角方向转换到模型空间(利用ObjSpaceLightDir和ObjSpaceViewDir内置函数)
将光照方向和视角方向转换到切线空间(利用变换矩阵进行乘法运算)
片元着色器回调函数中
- 取出法线贴图中的法线信息(利用纹理采样函数tex2D)
- 利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
- 用得到的切线空间的法线数据 乘以 BumpScale 来控制凹凸程度
- 得到单张纹理颜色和漫反射颜色的叠加颜色
- 用切线空间下的 光方向、视角方向、法线方向 进行Blinn Phong光照模型计算。
切线空间下计算代码:
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
116
117
118 Shader "Unlit/NormalTexture_NW" {
Properties {
//主贴图
_MainTex ("Texture", 2D) = "white" { }
//法线贴图
_BumpMap ("BumpMap", 2D) = "white" { }
//凹凸系数
_BumpScale ("BumpScale", Range(0, 1)) = 1
//漫反射材质颜色
_LambertColor ("LambertColor", color) = (1, 1, 1, 1)
//高光材质颜色
_BlinnPhongColor ("BlinnPhongColor", Color) = (1, 1, 1, 1)
//光泽度
_HighLightColor ("HighLightColor", Range(0, 50)) = 10
}
SubShader {
Tags { "RenderType" = "Opaque" }
LOD 100
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
// make fog work
struct appdata {
//模型顶点
float4 vertex : POSITION;
//模型法线
float3 normal : NORMAL;
//模型切线
float4 tangent : TANGENT;
//主uv
float2 main_uv : TEXCOORD0;
//法线uv
float2 normal_uv : TEXCOORD1;
};
struct v2f {
//可以分开两个uv声明,也可以用一个float4装载
float2 main_uv : TEXCOORD0;
float2 normal_uv : TEXCOORD1;
float4 vertex : SV_POSITION;
float3 lightDir : TEXCOORD2;
float3 viewDir : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
fixed3 _LambertColor;
fixed3 _BlinnPhongColor;
float _HighLightColor;
//凹凸系数
float _BumpScale;
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//UV缩放偏移计算
o.main_uv = TRANSFORM_TEX(v.main_uv, _MainTex);
o.normal_uv = TRANSFORM_TEX(v.normal_uv, _BumpMap);
//副切线计算 用法线和切线叉乘得到的副切线方向可能有两个,用切线数据中的w与之相乘确定副切线方向
float3 biTangent = cross(normalize(v.normal), normalize(v.tangent)) * v.tangent.w;
//计算模型空间到切线空间的变换矩阵
float3x3 rotation = float3x3(v.tangent.xyz, biTangent, v.normal);
//将光照和视角方向转换到模型空间(内置函数)
o.lightDir = ObjSpaceLightDir(v.vertex);
o.viewDir = ObjSpaceViewDir(v.vertex);
//将光照和视角方向转换到切线空间(利用变换矩阵)
o.lightDir = mul(rotation, o.lightDir);
o.viewDir = mul(rotation, o.viewDir);
return o;
}
fixed4 frag(v2f i) : SV_Target {
//取出法线贴图内的法线信息
float4 packedNormal = tex2D(_BumpMap, i.normal_uv);
//将取出的法线进行逆运算并且可能进行解压,最终得到切线空间下的法线
float3 tangentNormal = UnpackNormal(packedNormal);
//乘凹凸系数
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//计算blinnphong光照
//纹理颜色与漫反射材质颜色叠加
fixed3 mainColor = tex2D(_MainTex, i.main_uv) * _LambertColor;
//lambert
fixed3 lambertColor = _LightColor0 * mainColor * max(0, dot(normalize(i.lightDir), tangentNormal));
//半角向量
fixed3 halfDir = normalize(normalize(i.lightDir) + normalize(i.viewDir));
//Blinnphong高光
fixed3 blinnPhong = _LightColor0 * _BlinnPhongColor * pow(max(0, dot(halfDir, tangentNormal)), _HighLightColor);
fixed3 color = mainColor * UNITY_LIGHTMODEL_AMBIENT + lambertColor + blinnPhong;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
在
世界空间
下进行光照计算:需要把
法线方向变换到世界空间
下参与计算主要思路:在顶点着色器``中计算切线空间到世界空间的变换矩阵,在片元着色器中进行法线采样转换。
关键点:
世界空间下计算代码:
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 Shader "Unlit/NormalTexture_WW" {
Properties {
//主贴图
_MainTex ("Texture", 2D) = "white" { }
//法线贴图
_BumpMap ("BumpMap", 2D) = "white" { }
//凹凸系数
_BumpScale ("BumpScale", Range(0, 1)) = 1
//漫反射材质颜色
_LambertColor ("LambertColor", color) = (1, 1, 1, 1)
//高光材质颜色
_BlinnPhongColor ("BlinnPhongColor", Color) = (1, 1, 1, 1)
//光泽度
_HighLightColor ("HighLightColor", Range(0, 50)) = 10
}
SubShader {
LOD 100
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
struct appdata {
//模型空间下顶点
float4 vertex : POSITION;
//主uv
float2 main_uv : TEXCOORD0;
//主法线uv
float2 normal_uv : TEXCOORD1;
//模型空间下法线
float3 normal : NORMAL;
//模型空间下切线
float4 tangent : TANGENT;
};
struct v2f {
float2 main_uv : TEXCOORD0;
float2 normal_uv : TEXCOORD1;
//世界坐标顶点
float3 wPos : TEXCOORD2;
//裁剪空间顶点
float4 vertex : SV_POSITION;
// 变换矩阵
float3x3 rotation : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
fixed3 _LambertColor;
fixed3 _BlinnPhongColor;
float _HighLightColor;
//凹凸系数
float _BumpScale;
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//计算uv偏移缩放值
o.main_uv = TRANSFORM_TEX(v.main_uv, _MainTex);
o.normal_uv = TRANSFORM_TEX(v.normal_uv, _BumpMap);
//坐标转换
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 wNormal = normalize(UnityObjectToWorldNormal(v.normal)) ;
float3 tangent = normalize(UnityObjectToWorldDir(v.tangent)) ;
//计算副切线
float3 biTangent = cross(normalize(wNormal), normalize(tangent)) * v.tangent.w;
o.rotation = transpose(float3x3(tangent, biTangent, wNormal));
return o;
}
fixed4 frag(v2f i) : SV_Target {
//取出法线贴图内的法线信息
float4 packedNormal = tex2D(_BumpMap, i.normal_uv);
//将取出的法线进行逆运算并且可能进行解压,最终得到切线空间下的法线 并且转换为世界空间下
float3 tangentNormal = UnpackNormal(packedNormal) ;
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//把切线空间下的法线转换到世界空间下
float3 worldNormal = mul(i.rotation, tangentNormal);
//主纹理采样
fixed3 col = tex2D(_MainTex, i.main_uv) * _LambertColor;
//计算漫反射颜色
fixed3 lambertColor = _LightColor0 * col * max(0, dot(normalize(_WorldSpaceLightPos0), worldNormal));
//计算半角向量
float3 halfDir = normalize(normalize(_WorldSpaceLightPos0) + normalize(UnityWorldSpaceViewDir(i.wPos)));
//计算高光颜色
fixed3 blinnPhongColor = _LightColor0 * _BlinnPhongColor * pow(max(0, dot(halfDir, worldNormal)), _HighLightColor);
fixed3 color = UNITY_LIGHTMODEL_AMBIENT * col + lambertColor + blinnPhongColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
修改凹凸系数的计算方式,让法线系数不影响光照
只让法线中的xy乘以凹凸系数
tangentNormal.xy *= _BumpScale;
保证法线为单位向量(让法线不会为0,而是趋近于顶点法线)
x² + y² + z² = 1
=>
z² = 1 - (x² + y²)=>
z = 根号下(1 - (x² + y²))
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
- 通过这样的计算,当凹凸系数在0~1之间变化时,会保证法线为单位向量。这样就不会影响光照表现了
- 知识点四 提高性能的写法
- 我们目前在v2f结构体中,世界坐标顶点位置和变换矩阵使用了
float3
和float3x3
的两个变量来存储- 但是在很多世界空间下计算 法线贴图的Shader中,往往会使用
3个 float4
类型的变量来存储它们- 这种写法在很多情况下可以提高性能,因为它更好地与GPU的硬件架构匹配。
- float4 类型的寄存器是非常高效的
- 因为现代GPU通常会以 4 分量的向量为基本单位进行并行计算
- 通常为:xyz存矩阵,w存世界坐标顶点
4. 渐变纹理
(1)渐变纹理是用来做什么的
渐变纹理就是用于控
制漫反射光照结果的``一种存储数据的方式
主要作用:
让游戏中的对象具有插画卡通风格。
渐变纹理的使用可以保证物体的
轮廓线
相比之前使用的传统漫反射光照更加明显
而且还能提供多种色调的变化
,可以让模型更具卡通感
(2)渐变纹理的基本原理
- 基本原理:就是在计算漫反射时
利用半兰伯特
光照模型公式中后半部分
,得到一个0~1区间的值
,将这个值作为uv坐标中的uv值
,从渐变纹理中取出颜色
与 公式中前面部分进行颜色叠加
,最终得到漫反射光照颜色。- 半兰伯特:漫反射光照颜色 = 光源的颜色 * 材质的漫反射颜色
*((标准化后物体表面法线向量 · 标准化后光源方向向量)* 0.5 + 0.5)
- 也就是说: 决定漫反射明暗的
不再是由 0~1这个值决定
而是由渐变纹理中取出的颜色进行叠加达到最终效
(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 Shader "Unlit/RampTexture" {
Properties {
//主贴图
_MainTex ("MainTex", 2D) = "" { }
//纹理贴图
_RampTex ("RampTex", 2D) = "" { }
//漫反射颜色
_MainColor ("MainColor", Color) = (1, 1, 1, 1)
//高光颜色
_SpecularColor ("SpecularColor", Color) = (1, 1, 1, 1)
//光泽度
_SpecularNum ("SpecularNum", Range(0, 200)) = 10
}
SubShader {
Tags { "RenderType" = "Opaque" }
LOD 100
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
// make fog work
struct appdata {
float4 vertex : POSITION;
float2 main_uv : TEXCOORD0;
float2 ramp_uv : TEXCOORD1;
float3 normal : NORMAL;
};
struct v2f {
float2 uv : TEXCOORD0;
float3 wPos : TEXCOORD1;
float3 wNormal : NORMAL;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed3 _MainColor;
fixed3 _SpecularColor;
float _SpecularNum;
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.main_uv, _MainTex);
o.wPos = mul(unity_ObjectToWorld, v.vertex);
o.wNormal = UnityObjectToWorldNormal(v.normal);
return o;
}
fixed4 frag(v2f i) : SV_Target {
//计算兰伯特光照后半公式的值
float halfLambertNum = dot(normalize(i.wNormal), normalize(_WorldSpaceLightPos0)) * 0.5 + 0.5;
//构建uv 取出渐变纹理对应颜色
fixed3 rampColor = tex2D(_RampTex, fixed2(halfLambertNum, halfLambertNum));
//取出主材质颜色
fixed3 col = tex2D(_MainTex, i.uv) * _MainColor;
fixed3 diffuseColor = _LightColor0 * col * rampColor;
//高光反射颜色
//计算半角向量
float3 halfDir = normalize(normalize(UnityObjectToViewPos(i.wPos)) + normalize(_WorldSpaceLightPos0));
fixed3 specularColor = _LightColor0 * _SpecularColor * pow(max(0, dot(halfDir, normalize(i.wNormal))), _SpecularNum);
fixed3 color = UNITY_LIGHTMODEL_AMBIENT * col + diffuseColor + specularColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
(4)修改渐变纹理设置 避免黑点出现
避免渐变纹理接缝处有黑点
我们需要将
Wrap Mode(循环模式)切换为 Clamp(拉伸模式)
出现黑点的原因是:
浮点数计算可能存在误差,会出现超过1的值(1.00001)比如cos
如果使用Repeat(重复模式),会舍弃整数部分,保留小数0.00001
这时对应的颜色会是最左边的值,因此会出现黑色
(5)综合法线纹理和渐变纹理
效果图:(左无法线,右有法线)
![]()
代码:把法线贴图的改一下就行。
5. 遮罩纹理
(1)遮罩纹理是用来做什么的
- 遮罩纹理通常用于
控制或限制某些效果的显示范围
- 它允许我们可以保护某些区域,使它们免于某些修改
- 一般情况下,遮罩纹理也会是一张灰度图,rgba分量都是一样的我们利用它存储的值参与到
光照(指定某些区域受光影响的程度)
透明度(指定某些区域透明的程度)
特效(指定某些区域出现特效)
- 等等相关的计算中 从而来让指定区域达到我们想要的效果
我们以高光遮罩纹理举例
右图三个胶囊体的对比就是 高光遮罩纹理起到的效果
利用高光遮罩纹理 我们可以控制模型上的各个区域 受到高光影响的强弱
(2)高光遮罩纹理的基本原理
从纹理中取出对应的遮罩掩码值(颜色的RGB值都可以使用)
用该掩码值和遮罩系数(我们自己定义的)相乘得到遮罩值
用该遮罩值和高光反射计算出来的颜色相乘
(3)高光遮罩纹理的实现
效果:(左)
![]()
代码:
1
2
3 //计算遮罩系数
fixed3 mainColor = tex2D(_MainTex, i.main_uv) * _LambertColor;
fixed3 blinnPhong = _LightColor0 * _BlinnPhongColor * pow(max(0, dot(halfDir, tangentNormal)), _HighLightColor) * maskColor;
二、透明
1. 知识回顾
回顾 渲染标签— 渲染队列
确定物体的渲染顺序
回顾 渲染状态— 深度缓冲
记录像素的深度值,用于之后进行比较,决定片元(颜色)的去留
回顾 渲染状态— 深度测试
将当前片元深度值和深度缓冲中深度值进行比较,决定片元(颜色)去留
回顾 渲染状态— 混合方式
将通过
深度测试的片元颜色
和颜色缓冲区中的颜色
按指定算法进行混合
并更新颜色缓冲区
2. 渲染顺序的重要性
(1)深度测试和深度写入带来的好处
有了深度测试和深度写入发挥作用 让我们不需要关心不透明物体的渲染顺序
比如:
一个物体A 挡住了物体B 即使底层逻辑中先渲染A,后渲染。我们也不用担心B的颜色会把A覆盖 因为在进行深度测试时,远处的B的深度值无法通过深度测试 因为它的深度会比已经写入深度缓冲中A的深度值大 重合处的片元会被丢弃,颜色自然就不会写入,最终重叠处渲染出来的会是A的颜色。
我们之前写的所有Shader都没有刻意的去设置 深度测试(默认小于判断)、深度写入(默认开启) 、 混合模式(默认不混合) 、渲染队列(默认几何Geometry队列) 相关内容 是因为对于不透明的物体来说,使用默认设置就能够得到正确的渲染效果 但目前我们将要学习的透明相关知识,就需要对他们进行改变
其中,最最最重要的改变就是:
处理透明混合时,需要关闭深度写入
(2)透明混合为什么需要关闭深度写入
在
图形学中模拟出现实世界的半透明效果是通过将
多个颜色进行混合计算呈现出来的
举例:
对于下图来说,如果前方的A对象不关闭深度写入,那么B被A遮挡的部分会在深度测试时被舍弃。
那么我们就无法得到被遮挡部分的颜色,也就无法进行颜色的混合计算。
因此我们
必须关闭透明物体的深度写入
。
让其不会导致被遮挡物体无法通过深度测试从而无法进行颜色混合
![]()
正确的效果:
(3)关闭深度写入带来的问题
- 从这个例子我们看出,想要得到正确的半透明混合效果 需要
先渲染不透明物体
,再渲染透明物体
- 如果先渲染没有开启深度写入的前面的物体 之后渲染后面的物体就会将前面物体的颜色覆盖 或出现错误前后关系表现
(4)如何解决渲染顺序带来的问题
3. 设置深度写入和渲染队列
(1)Unity Shader中设置深度写入
深度写入默认是开启的
我们需要通过渲染状态中的
ZWrite off
指令主动关闭深度写入一
般写在Pass中
(2)Unity Shader中设置渲染队列
- 渲染队列一般都
定义在SubShader语句块
中,影响之后的所有Pass渲染通道- 使用该Shader(着色器)的物体,就会根据你设置的渲染队列在特定的时间进行渲染
- 在
使用渲染队列Queue
时,一般会搭配忽视投影器IgnoreProjector 和渲染类型RenderType 一起使用
4. 设置混合命令
(1)混合的基本原理
当我们在进行渲染时,当片元通过了深度测试后,会进入到混合流程中。
在混合流程中:
当前片元的颜色被称为
源颜色
颜色缓冲区中的颜色被称为
目标颜色
混合 就是:
源颜色和目标颜色用对应的混合算法进行计算后
输出一个新的颜色更新到颜色缓冲区中
注::这些颜色都是RGBA包含透明通道A
(2)混合的计算规则—— 混合因子
![]()
![]()
两种使用方法
(3)混合的计算规则—— 混合操作
在ShaderLab当中除了可以使用Blend 混合命令来设定混合因子
还提供了一个
BlendOp 混合操作命令来设定混合的计算方式
他的基本语法是:
(4)常见的混合类型
5. 透明效果实现【透明测试】
在游戏开发中,对象的
某些部位完全透明
而其他部位完全不透明
,这种透明需求往往不需要半透明效果。相对比较极端,只有看得见和看不见之分 ,比如树叶、草、栅栏等等。
(1)透明测试的基本原理
基本原理:
- 通过一个阈值来决定哪些像素应该被保留,哪些应该被丢弃
具体实现:
- 片元携带的颜色信息中的
透明度(A值)
,在 不满足条件时(通常是小于某个阈值
),- 该
片元就会被舍弃
,被舍弃的片元不会在进行任何处理,不会对颜色缓冲区产生任何影响满足条件时
(通常是大于等于某个阈值),该片元会按照不透明物体
的处理方式来处理阈值判断使用的方法:
利用CG中的内置函数:
clip(参数)
该函数有重载,参数类型可以是 float4 float3 float2 float 等等
如果传入的参数
任何一个分量是负数
就会舍弃当前片元它的内部实现会用到一个
discard 指令
,代表剔除该片元 不再参与渲染
1
2
3
4
5 void clip(float4 x)
{
if(any(x < 0))
discard;
}
(2)透明测试实现
- 我们复制 颜色纹理结合光照模型的Shader(因为我们的测试资源没有法线贴图等数据)
- 在属性中加一个阈值_Cutoff,取值范围为0~1,用来设定用来判断的阈值。并在CG中添加属性的映射成员
- 将渲染队列设置为AlphaTest,并配合IgnoreProjector和RenderType一起设置
- 在片元着色器中获取了颜色贴图颜色后,就进行阈值判断
效果展示:
代码:
1
2
3 Tags { "RenderType" = "TransparentCutout" "Queue" = "AlphaTest" "IgnoreProjector" = "True" }
fixed4 texColor = tex2D(_MainTex, i.uv);
clip(texColor.a - _Cutoff);
6. 透明度混合——半透明效果实现
(1) 透明度混合是用来处理哪种需求的?
- 透明度测试,并不能用于实现半透明效果。一般用来处理镂空效果
透明度混合,主要就是用来实现半透明效果的
(2)透明度混合的基本原理
基本原理:
- 关闭深度写入,开启混合,让片元颜色和颜色缓冲区中颜色进行混合计算
具体实现:
采用半透明的混合因子进行混合 Blend SrcAlpha OneMinusSrcAlpha
目标颜色 = SrcAlpha * 源颜色 + (1-SrcAlpha) * 目标颜色
= 源颜色透明度 * 源颜色 + (1-源颜色透明度) * 目标颜色
声明一个0~1区间的_AlphaScale用于控制对象整体透明度
(3)透明度混合实现
- 我们复制 颜色纹理结合光照模型的Shader(因为我们的测试资源没有法线贴图等数据)
- 在属性中加一个阈值_AlphaScale,取值范围为0~1,用来设定对象整体透明度。并在CG中添加属性的映射成员
- 将渲染队列设置为Transparent,并配合IgnoreProjector和RenderType一起设置
- 关闭深度写入Zwrite off,设置混合因子Blend SrcAlpha OneMinusSrcAlpha
- 在片元着色器中获取了颜色贴图颜色后,修改最后返回颜色的A值为 纹理.a * _AlphaScale
效果:
![]()
代码:
1
2
3
4
5 Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }
//关闭深度写入
ZWrite Off
//混合设置
Blend SrcAlpha OneMinusSrcAlpha
(4)疑问
几何体(渲染队列为几何队列)会先渲染,深度已经写入缓冲区。
半透明物体(渲染队列为透明队列)会后渲染,并且会从远到近的渲染,即使开启深度写入,理论上来说在前面的半透明物体也能够通过深度测试,和颜色缓冲区中的颜色进行混合,那为什么还需要关闭深度写入呢?
渲染规则是:
根据像素从远到近渲染的
。如果开启深度写入:
前提:在都是半透明物体中。如果一个是半透明,一个不透明。永远不会发生这种,
因为半透明始终后渲染。
在重叠的部分,因为后面正放的物体像素在前,会先渲染并且写入深度缓冲中。
这样的话,斜着的物体重叠部分的像素会因为没法通过深度测试而被舍弃。
(5)开启深度写入的半透明效果
- 开启深度写入的半透明效果是用来处理哪种需求的?
- 本身结构较为复杂的模型,使用之前的透明度混合Shader会由于关闭了深度写入。会产生错误的渲染效果。
- 虽然我们可以通过拆分模型的方式解决部分问题,但是对于一些结构复杂的模型,
拆分模型的方式会增加工作量
。- 因此我们可以采用 开启深度写入的半透明Shader 来优化效果
- 开启深度写入的半透明效果的
基本原理
- 使用
两个Pass渲染通道
来处理渲染逻辑- 第一个Pass:
开启深度写入,不输出颜色
(目的是让该模型各片元的深度值能写入深度缓冲)- 第二个Pass:
- 进行正常的透明度混合(和上节课一样)
- 这样做的话,当执行第一个Pass时,会执行深度测试,并进行深度写入
- 如果此时该片元没有通过深度测试会直接丢弃,不会再执行第二个Pass
- 对于同一个模型中处于屏幕同一位置的片元们,会进行该位置的深度测试再决定渲染哪个片元。
如何做到不输出颜色?
- 使用
ColorMask
颜色遮罩 渲染状态(命令)- 它主要用于控制颜色分量是否写入到颜色缓冲区中
- ColorMask RGBA 表示写入颜色的RGBA通道
- ColorMask 0 表示不写入颜色
- ColorMask RB 表示只写入红色和蓝色通道
- 注意:
- 开启深度写入的半透明效果,
模型内部之间不会有任何半透明
效果(因为模型内部深度较大的片元会被丢弃掉)- 由于有两个Pass渲染通道,因此它会带来一定的性能开销
实现 开启深度写入的半透明效果
- 我们透明度混合的Shader代码
- 在SubShader中之前的Pass渲染通道前面加一个Pass渲染通道
- 在新加Pass渲染通道中开启深度写入,并且使用 ColorMask 0 颜色遮罩 渲染命令,不输出颜色
7.【透明效果实现】双面渲染的透明效果
(1)双面渲染的透明效果用来处理哪种需求的?
- 对于现实世界的半透明物体,我们不仅可以透过它看到其他物体的样子,也可以看到这个
物体自己的内部结构
- 但是我们之前实现的 透明度测试 和 透明度混合 相关Shader,都无法看到模型的内部结构。
- 而双面渲染的透明效果Shader就是来解决该问题的
- 让我们不仅可以
透过半透明物体看到其他物体的样子还可以看到自己的内部结构
(2)双面渲染的透明效果的基本原理
- 基本原理:
- 默认情况下,Unity会自动剔除物体的背面,而只渲染物体的正面
- 双面渲染的基本原理就是利用我们之前学习过的 Cull 剔除指令来进行指定操作
- Cull Back 背面剔除
- Cull Front 正面剔除
- 不设置的话,默认为背面剔除
- 对于
透明度测试
Shader
- 由于它无需混合,因此我们直接 关闭剔除即可
- 对于
透明度混合
Shader
- 由于它需要进行混合,需要
使用两个Pass
。一个用于渲染背面,一个用于渲染正面- 两个Pass中除了剔除命令不同 其他代码和之前一致
(3)实现 双面渲染的透明效果Shader
透明度混合
- 复制 透明度混合相关Shader代码
- 复制之前的Pass,变成两个一模一样的Pass
- 在第一个Pass中剔除正面 Cull Front,在第二个Pass中剔除背面Cull Back(相当于一个片元先渲染背面再渲染正面)
效果: