凹凸纹理
一、凹凸纹理的作用和原理
- 纹理除了可以用来进行颜色映射外,另外一种常见的应用就是进行凹凸映射,凹凸映射的目的是使用一张纹理来修改模型表面的法线,让我们不需要增加顶点,而让模型看起来有凹凸效果。
- 原理:光照的计算都会利用法线参与计算,决定最终的颜色表现效果。那么在计算“假凹凸面时,使用“真”凹凸面的法线参与计算,呈现出来的效果可以以假乱真


二、主流实现的两种方式
1.高度纹理贴图
1.)基本概念
高度纹理贴图一般简称高度图 它存储了模型表面上每个点的高度信息。 通常它使用灰度图像,其中不同灰度值表示不同高度。较亮区域通常对应较高的点,较暗的区域对应较低的点。 它主要用于模拟物体表面的位移
2.)存储规则
图片中的某一个像素点的 RGB 值是相同的,都表示高度值,A 值一般高度值范围一般为 0~1,0 代表最低,1 代表最高情况下为 1。
3.)优缺点
- 优点 可以通过高度图很明确的知道模型表面的凹凸情况
- 缺点 无法在 Shader 中直接得到模型表面点的法线信息而是需要通过额外的计算得到,因此会增加性能消耗,所以我们几乎很少使用它。
我们在使用凹凸纹理时,一般都会使用法线纹理贴
2.法线纹理贴图
1.)基本概念
法线纹理贴图一般简称法线贴图 或 法线纹理它存储了模型表面上每个点的法线方向。
2.)存储规则
图片中的 RGB 值分别存储法线的 X、Y、Z 分量值,A 值可以用于存储其他信息比如材质光滑度等。
3.)优缺点
- 优点:从法线贴图中取出的数据便是法线信息,可以直接简单处理后就参与光照计算,性能表现更好
- 缺点:我们无法直观的看出模型表面的凹凸情况
4.)读取规则
- 存储图片:像素分量 = (法线分量 + 1)/2
- 读取数据时:法线的分量 = 像素分量 * 2 - 1
原因:法线的 XYZ 分量范围在[-1,1]之间 像素 RGB 的分量范围在[0,1]之间 因此我们需要进行映射运算
5.)两种不同的存储方式
A、基于模型空间下的法线纹理
2024-09-04 095625.png
a. 定义
模型数据中自带的法线数据,是定义在模型空间中的,因此最直接的存储法线贴图数据的方式就是存储基于模型空间下的法线信息。
(注意:模型数据中的法线数据是“真”数据,法线贴图中对的法线数据是“假”数据)
b. 模型空间的法线为五颜六色的原因
法线(0,1,0)映射到像素后(法线线量+1)/2 是(0.5,1.0.5)绿色
法线(0,-1,0)映射到像素后(法线线量+1)/2 是(0.5,0,0.5)紫色
因此基于模型空间的法线纹理一般是五颜六色的这种法线纹理贴图数据取出来直接参与 Shader 计算即可
B、基于切线空间下的法线纹理
2024-09-04 095628.png
a. 什么时切线空间
每个顶点都有自己的切线空间原点:顶点本身
X 轴:顶点切线
Z 轴:法线方向(顶点的原法线)
Y 轴:X 和 Z 的又乘结果,也被称为副切线
2024-09-04 101022.png
b. 大部分为蓝色的原因
在切线空间下,如果该顶点的法线不变化(不需要“凹凸感”)那么它的坐标是(0,0,1),因为在切线空间下,Z 轴就是原法线方向.
因此:
法线(0,0,1)映射到像素后(法线分量+1)/2 是(0.5,0.5,1)浅蓝色
这个浅蓝色就是 切线空间下法线贴图 存在大片蓝色的原因
因为大部分顶点的法线和模型本身法线是一致的只有凹凸部分的颜色才会有些许差异
这种法线纹理贴图数据取出来后需要进行坐标空间转换再参与 Shader 计算
C、建议使用切线空间下的法线纹理贴图的原因
- 可以用于不同的模型处理——如果模型空间下的法线,不可以用于其他模型
- 方便处理模型变形——同上
- 可以复用——一个砖块,6 个面的贴图都一样,可以只用一张法线贴图,即可用于六个面的计算
- 可以压缩——可以只存储两个轴的分量
- 方便制作 UV 动画——UV 坐标改变可以实现凹凸移动效果,如果时模型下法线贴图会有问题等等
三、法线贴图的计算方式
1.两种主流的计算方式
实现凹凸效果---主要就是使用基于切线空间的法线贴图中的法线信息参与到光照计算中
因此在计算光照模型时,通常有两种选择
- 切线空间下进行光照计算 需要将光照方向、视角方向变换到切线空间下参与计算
- 世界空间下进行光照计算 需要将法线方向变换到世界空间下参与计算
2.两种主流的计算方式的优缺点
- 效率
- 切线空间中计算,效率高。 因为矩阵的变换在顶点着色器中进行
- 世界空间中计算,效率低。 因为矩阵变换在片元着色器中进行
- 全局效果
- 切线空间中计算,全局效果表现可能会不够准确 例如:处理镜面反射、环境映射效果时表现效果可能不够准确
- 世界空间中计算,全局效果表现更准确 可以更容易的应用于全局效果
注意: 根据需求不同的计算方式,没有全局效果要求时,多使用切线空间下计算
3.切线空间下的计算方式
1.)关键点
计算 模型空间->切线空间 的变换矩阵
- 变换矩阵为子到父的
逆矩阵
- 由于我们主要使用变换矩阵来进行矢量变换而非点的变换,因此可以变为 3X3 的矩阵

- 而 x、y、z 轴分别为切线空间中顶点的切线、副切线、法线
- 其中切线、法线可以从模型数据中获取,副切线为切线,法线的叉乘,并且三个轴是互相垂直的单位向量,因此
是正交矩阵
- 因此改变换矩阵的逆矩阵为
的转置矩阵
2.)计算时的核心知识
- 得到模型空间光的方向 :ObjectSpaceLightDir(模型空间顶点坐标)
- 得到模型空间视角的方向 :ObjectSpaceViewDir(模型空间顶点坐标)
- 得到俩方向后 与 变换矩阵 进行矩阵运算,再参与后续切线空间下的计算
4.世界空间下的计算方式
1.)关键点
计算 切线空间->世界空间 的变换矩阵
- 变换矩阵为子到父的变换

- 由于我们主要使用变换矩阵来进行矢量变换而非点的变换,因此可以变为 3X3 的矩阵

- 而 x、y、z 轴分别为切线空间中顶点的切线、副切线、法线
- 我们只需要得到三个轴相对世界空间下的向量表达,即可以得到该矩阵
2.)计算时的核心知识
- 法线从模型空间到世界空间 :UnityObjectToWorldNormal(模型空间法线数据)
- 切线从模型空间到世界空间 :UnityObjectToWorldDir(模型空间切线数据)
- 世界空间的副切线 :上面计算结果叉乘即可
- 由上面三个计算得到的向量 组成变换矩阵

四、关键注意点
1.模型空间下的切线数据
模型数据中的切线数据为 float4 类型,其中的 W 表示副切线的方向
原因: 用法线和切线叉乘得到的副切线有两条,用切线数据中的 W 与之相乘确定副切线的方向
2.Unity 当中的法线纹理类型
当我们把纹理类型设置为 Normal map(法线贴图时),我们可以使用 Unity 提供的内置函数 UnpackNormal 来得到正确的法线方向。该函数不仅可以进行法线分量=像素分量 * 2 - 1的逆运算,还会进行解压运算(Unity 会根据不同平台对法线纹理进行压缩)
3.法线纹理的命名
法线纹理属性一般命名为_BumpMap(凸块贴图)
同时还会声明一个名为_BumpMap(凸块缩放)的 float 属性---主要是用于控制凹凸度
4.高度纹理的处理办法
- 将图片类型设置为 Normal Map(法线贴图)
- 勾选 Creat form Grayscale(从灰度创建)
- 多出的 Bumpniess(颠簸值)---控制凹凸程度
- Filtering(过滤模式)---决定凹凸程度的算法
- Sharp 滤波生成法线
- Smooth 光滑的生成法线
五、切线空间下计算法线贴图的实现
1.书写步骤
属性相关
- 漫反射颜色
- 高光反射颜色
- 光泽度
- 单张纹理
- 法线纹理
- 凹凸程度
结构体相关
- 顶点着色器中传入: 可以使用"UnityCG.cginc" 中的 appdata_full 其中包含了我们需要的顶点、法线、切线、纹理坐标相关数据
- 片元着色器中传入: 自定义一个结构体 其中包含 裁剪空间下的顶点坐标、uv 坐标、光的方向、视角方向
顶点回调函数中
顶点模型空间坐标转裁剪空间
单张纹理和法线纹理 的 UV 坐标缩放偏移计算
副切线计算 切线(tangent) X 法线(normal)·切线中的 w
构建模型空间到切线空间的变换矩阵 —— 切线 ——
—— 副切线 ——
—— 法线 ——
将光照方向和视角方向转换到模型空间
- ObjSpaceLightDir()
- ObjSpaceView()
将光照方向和视角方向转换到切线空间 利用 4 中计算号的变换矩阵
片元着色器回调函数中
- 取出法线贴图中的法线信息(利用纹理采样 tex2D)
- 利用内置的 UnpackNormal 函数对法线信息进行逆运算以及可能的解压
- 得到的法线数据*凹凸程度 来控制凹凸度
- 用切线空间下的 光方向、视角方向、法线方向、进行 BlinnPhong 光照模型的计算
2.实例代码
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
| Shader "Unlit/TangentspaceBumpMap" { Properties { _MainColor("MainColor",Color) = (1,1,1,1) _specularColor("specularColor",Color) = (1,1,1,1) _specularNum("specularNum",Range(0,20)) = 15 _MainTex("MainTex",2D) = ""{} _BumpMap("BumpMap",2D) = ""{} _BumpMapScale("BumpMapScale",Float) = 1
} SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc" #include "Lighting.cginc"
struct v2f { float4 pos:POSITION;
float4 uv:TEXCOORD0; float3 lightDir:TEXCOORD1; float3 viewDir:TEXCOORD2; }; float4 _MainColor; float4 _specularColor; float _specularNum; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpMapScale; v2f vert (appdata_full v) { v2f data; data.pos = UnityObjectToClipPos(v.vertex); data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 binormal = cross(normalize(v.tangent),normalize(v.normal))*v.tangent.w; float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); data.lightDir = ObjSpaceLightDir(v.vertex); data.lightDir = mul(rotation,data.lightDir); data.viewDir = ObjSpaceViewDir(v.vertex); data.viewDir = mul(rotation,data.viewDir);
return data; }
fixed4 frag (v2f i) : SV_Target { float4 packedNormal = tex2D(_BumpMap,i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal *= _BumpMapScale;
fixed3 albedo = tex2D(_MainTex,i.uv.xy) * _MainColor.rgb; fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0,dot(tangentNormal,normalize(i.lightDir))); float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); float3 specularColor = _LightColor0.rgb * _specularColor * pow(max(0,dot(tangentNormal,halfA)) , _specularNum); fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
return fixed4(color.rgb , 1 ); } ENDCG } } }
|
3.演示效果

六、世界空间下计算法线贴图的实现
1.主要思路
- 在顶点着色器中 计算切线空间到世界空间的变换矩阵
- 在片元着色器中进行法线采样转化
2.书写步骤
- 属性相关
- 漫反射颜色
- 高光反射颜色
- 光泽度
- 单张纹理
- 法线纹理
- 凹凸程度
- 结构体相关
- 顶点着色器中传入
- 可以使用 "UnityCG.cginc" 中的 appdata_full
- 其中包含了我们需要的顶点、法线、切线、纹理坐标相关数据
- 片元着色器中传入
- 自定义一个结构体
- 其中包含 裁剪空间下的坐标、uv 坐标、世界空间下顶点位置、变换矩阵
- 顶点着色器回调函数中
- 顶点坐标 模型空间转裁剪空间
- 单张纹理和法线纹理 UV 坐标缩放偏移计算
- 模型空间下顶点转换到世界空间下(之后再片元着色器中用于计算视角方向)
- 将模型空间下的法线、切线转换到世界空间下
- 副切线计算 用世界空间下的法线和切线进行叉乘 在乘以切线中的 w(确定副切线方向)
- 构建模型空间到切线空间的变化矩阵

- 片元着色器回调函数中
- 计算光的方向、视角方向
- 取出法线贴图冲的法线信息(利用纹理采样函数 tex2D)
- 利用内置函数的 UnpackNormal 函数堆法线信息进行逆运算以及可能的解压
- 得到的切线空间的法线数据 乘以 BumpScale 来控制凹凸度
- 将计算完成后的切线空间下的法线准换到世界空间下
- 得到单张纹理的颜色和漫反射颜色的叠加颜色
- 用切线空间下的 光方向、视角方向、法线方向 进行 BlinnPhong 光照模型计算
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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| Shader "Unlit/WorldSpaceBumpMap" { Properties { _MainColor("MainColor",Color) = (1,1,1,1) _specularColor("specularColor",Color) = (1,1,1,1) _specularNum("specularNum",Range(0,20)) = 15 _MainTex("MainTex",2D) = ""{} _BumpMap("BumpMap",2D) = ""{} _BumpMapScale("BumpMapScale",Float) = 1 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" float4 _MainColor; float4 _specularColor; float _specularNum; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpMapScale;
struct v2f { float4 pos:SV_POSITION; float4 uv:TEXCOORD0; float3 worldPos:TEXCOORD1; float3x3 rotation:TEXCOORD2;
}; v2f vert (appdata_full v) { v2f data; data.pos = UnityObjectToClipPos(v.vertex); data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
data.worldPos = mul(unity_ObjectToWorld,v.vertex); float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 worldTangent = UnityObjectToWorldDir(v.tangent); float3 worldBinormal = cross(normalize(worldTangent),normalize(worldNormal)) * v.tangent.w; data.rotation = float3x3( worldTangent.x,worldBinormal.x,worldNormal.x, worldTangent.y,worldBinormal.y,worldNormal.y, worldTangent.z,worldBinormal.z,worldNormal.z ); return data;
}
fixed4 frag (v2f i) : SV_Target { fixed3 lightDir = normalize(_WorldSpaceLightPos0); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float4 packedNormal = tex2D(_BumpMap,i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpMapScale; tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
float3 worldNormal = mul(i.rotation,tangentNormal);
fixed3 albedo = tex2D(_MainTex,i.uv.xy) * _MainColor.rgb; fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0,dot(worldNormal,normalize(lightDir))); float3 halfA = normalize(normalize(viewDir) + normalize(lightDir)); float3 specularColor = _LightColor0.rgb * _specularColor.rgb * pow(max(0,dot(worldNormal,halfA)) , _specularNum); fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
return fixed4(color.rgb , 1 ); } ENDCG } } }
|
4.实现效果
2024-09-07 130810.png
七、提高性能优化的写法
- 我们在 v2f 结构体中 世界坐标顶点位置和变换矩阵使用了 float3 和 float3x3 两个变量存储 但是在很多世界空间下计算 法线贴图的 Shader 中 往往会使用 3 个 float4 类型的变量来存储
- 原因 这种写法在很多情况下可以提高性能,因为它更好地与 GPU 的硬件架构匹配 float4 类型的寄存器是非常高效的 因为现代的 GUP 通常是会以 4 分量的向量为基本单位进行并行计算 float3x3 矩阵相对来需要更多的寄存器和指令来表示和计算
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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
| Shader "Unlit/WorldSpaceBumpMap" { Properties { _MainColor("MainColor",Color) = (1,1,1,1) _specularColor("specularColor",Color) = (1,1,1,1) _specularNum("specularNum",Range(0,20)) = 15 _MainTex("MainTex",2D) = ""{} _BumpMap("BumpMap",2D) = ""{} _BumpMapScale("BumpMapScale",Float) = 1 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" float4 _MainColor; float4 _specularColor; float _specularNum; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpMapScale;
struct v2f { float4 pos:SV_POSITION; float4 uv:TEXCOORD0; float4 TtoW0:TEXCOORD1; float4 TtoW1:TEXCOORD2; float4 TtoW2:TEXCOORD3;
}; v2f vert (appdata_full v) { v2f data; data.pos = UnityObjectToClipPos(v.vertex); data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld,v.vertex); float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 worldTangent = UnityObjectToWorldDir(v.tangent); float3 worldBinormal = cross(normalize(worldTangent),normalize(worldNormal)) * v.tangent.w;
data.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x); data.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y); data.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return data;
}
fixed4 frag (v2f i) : SV_Target { fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float4 packedNormal = tex2D(_BumpMap,i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpMapScale; tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
float3 worldNormal = float3(dot(i.TtoW0.xyz,tangentNormal),dot(i.TtoW1.xyz,tangentNormal),dot(i.TtoW2.xyz,tangentNormal));
fixed3 albedo = tex2D(_MainTex,i.uv.xy) * _MainColor.rgb; fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0,dot(worldNormal,normalize(lightDir))); float3 halfA = normalize(normalize(viewDir) + normalize(lightDir)); float3 specularColor = _LightColor0.rgb * _specularColor.rgb * pow(max(0,dot(worldNormal,halfA)) , _specularNum); fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
return fixed4(color.rgb , 1 ); } ENDCG } } }
|