US3S2L7——世界空间下计算法线纹理贴图
关键知识点补充
-
模型空间下的切线数据
模型数据中的切线数据为 float4 类型的(四维向量),其中的 w
表示副切线的方向
用法线和切线叉乘得到的副切线方向可能有两个(如下图的两个方向的灰色线),用切线数据中的 w
与之相乘确定副切线方向
-
Unity 当中的法线纹理类型
当我们把纹理类型设置为 Normal map(法线贴图)时,
我们可以使用Unity提供的内置函数 UnpackNormal
来得到正确的法线方向。
该函数内部不仅可以进行 法线分量=像素分量×2−1的逆运算,
还会进行解压运算(Unity 会根据不同平台对法线纹理进行压缩)
-
法线纹理属性命名一般为 _BumpMap
(凸块贴图),
并声明一个我们还会声明一个名为 _BumpScale
(凸块缩放))的 float
属性,它主要用于控制凹凸程度
当它为 0 时,表示没有法线效果,法线的影响会被消除
当它为 1 时,表示使用法线贴图中的原始法线信息,没有缩放
我们可以根据实际需求调整它的值,来达到视觉上令人满意的效果
-
如果使用的凹凸纹理不是法线纹理,而是高度纹理
我们需要进行如下设置:
图片类型设置为 Normal map(法线贴图)并勾选 Create from Grayscale(从灰度创建)
这样我们就可以把高度纹理当成切线空间下的法线纹理处理了
多出的参数分别为:
在世界空间下计算实现法线纹理Shader
主要思路:
- 在顶点着色器中计算切线空间到世界空间的变换矩阵
- 在片元着色器中进行法线采样转换
Shader 属性相关
和 切线空间下计算法线纹理贴图 的属性一致
- 漫反射颜色
- 单张纹理
- 法线纹理
- 凹凸程度(一般使用 0~1 即可)
- 高光反射颜色
- 光泽度
1 2 3 4 5 6 7 8 9
| Properties { _MainColor("MainColor", Color) = (1, 1, 1, 1) _MainTex("MainTex", 2D) = ""{} _BumpMap("BumpMap", 2D) = ""{} _BumpScale("BumpScale", Range(0, 1)) = 1 _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) _SpecularNum("SpecularNum", Range(0, 20)) = 18 }
|
结构体相关
顶点着色器回调函数内容
执行步骤:
-
顶点坐标模型转裁剪
-
单张纹理和法线纹理 UV坐标缩放偏移计算
-
模型空间下顶点转换到世界空间下,使用 unity_ObjectToWorld 矩阵转换(之后在片元着色器中用于计算视角方向)
-
使用 UnityObjectToWorldNormal 和 UnityObjectToWorldDir 将模型空间下的法线、切线转换到世界空间下
-
计算世界空间下的副切线
用世界空间下的法线和切线进行叉乘,再乘以切线中的 w
(确定副切线方向)
-
构建模型空间到切线空间的变换矩阵
∣切线∣∣副切线∣∣法线∣
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
| struct appdata_full { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float3 worldPos: TEXCOORD1; float3x3 rotation: TEXCOORD2; };
float4 _MainTex_ST; float4 _BumpMap_ST;
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; }
|
片元着色器回调函数内容
执行步骤:
- 通过
_WorldSpaceLightPos0
计算世界空间下光的方向,通过 UnityWorldSpaceViewDir 世界空间下的视角方向
- 取出法线贴图中的法线信息(利用纹理采样函数 tex2D)
- 利用内置的 UnpackNormal 函数对法线信息进行逆运算以及可能的解压
- 用得到的切线空间的法线数据 乘以
BumpScale
来控制凹凸程度
- 将计算完毕后的切线空间下的法线转换到世界空间下
- 得到单张纹理颜色和漫反射颜色的叠加颜色
- 用世界空间下的 光方向、视角方向、法线方向 进行BlinnPhong光照模型计算
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
| struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float3 worldPos: TEXCOORD1; float3x3 rotation: TEXCOORD2; };
float4 _MainColor; sampler2D _MainTex; sampler2D _BumpMap; float _BumpScale; float4 _SpecularColor; fixed _SpecularNum;
fixed4 frag (v2f i) : SV_Target { fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal *= _BumpScale; 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, lightDir)); float3 halfA = normalize(viewDir + lightDir); fixed3 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); }
|
代码示例
完整 Shader 代码如下:
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
| Shader "TeachShader/Lesson52" { Properties { _MainColor("MainColor", Color) = (1, 1, 1, 1) _MainTex("MainTex", 2D) = ""{} _BumpMap("BumpMap", 2D) = ""{} _BumpScale("BumpScale", Range(0, 1)) = 1 _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) _SpecularNum("SpecularNum", Range(0, 20)) = 18 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" }
CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc" #include "Lighting.cginc"
struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float3 worldPos: TEXCOORD1; float3x3 rotation: TEXCOORD2; };
float4 _MainColor; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; float4 _SpecularColor; fixed _SpecularNum;
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.xyz); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal *= _BumpScale; 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, lightDir)); float3 halfA = normalize(viewDir + lightDir); fixed3 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 } } }
|
显示效果(左图为不使用法线贴图的方块,右下切线空间下计算法线贴图的方块,右上世界空间下计算法线贴图的方块):
关于光照方向的计算方式
计算光照存在两种方式:
-
模拟定向光源
直接得到 _WorldSpaceLightPos0
光照位置 作为光照方向,
表示光线是平行的,而不是从特定点发射
一般模拟太阳光效果 采用这种方式
-
模拟点光源
用光照位置 _WorldSpaceLightPos0
减去 顶点坐标,
表示光线是从特定点发射的,并朝着顶点方向
一般定点光源 采用这种方式
第二种光照方向代码类似于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| fixed4 frag (v2f i) : SV_Target { fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal *= _BumpScale; 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, lightDir)); float3 halfA = normalize(viewDir + lightDir); fixed3 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); }
|
效果对比(左为使用平行光源方向的方块,右为使用点光源方向的方块):
可以看见,左侧方块的受光面的亮度是均匀的,
而右侧的亮度呈现右下更亮,左上较暗的趋势(这是因为它以光源和顶点的方向计算光照)
修改凹凸系数的计算方式,让法线系数不影响光照
目前的 Shader 写法中,凹凸系数 BumpScale
会影响光照,凹凸系数越小,光照越暗
我们目前这种直接让 法线×凹凸系数 的计算方式,并不是一个标准算法
因为当凹凸系数趋近于 0 时,会影响光照模型的计算(法线也趋于0了,导致计算出来的颜色是偏黑的)
为了让凹凸系数不影响光的效果,有一种专门的算法
-
只让法线中的 xy 乘以凹凸系数
1
| tangentNormal.xy *= _BumpScale;
|
-
同时保证法线为单位向量(让法线不会为0,而是趋近于顶点法线)
若要让法线在 xy 乘以凹凸系数后还要保持为单位向量,就需要让 z 分量进行改变
根据单位向量的性质可得:
∵(x,y,z)为单位向量∴x2+y2+z2=1∴z2=1−(x2+y2)∴z=1−(x2+y2)
又因为 x2+y2=(x,y)⋅(x,y),因此代码内可实现为:
1
| tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
|
其中,saturate
函数是保证计算结果在 0~1 之间的
通过这样的计算,当凹凸系数在 0~1 之间变化时,会保证法线为单位向量,这样就不会影响光照表现了
注意:这种算法并不是来自真实的物理规律,只是为了 “看起来正常”
因此,根据以上规则,片元着色器的 Shader 代码修改为:
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
| fixed4 frag (v2f i) : SV_Target { fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); float4 packedNormal = tex2D(_BumpMap, i.uv.zw); 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 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir)); float3 halfA = normalize(viewDir + lightDir); fixed3 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); }
|
效果如下(左侧方块的凹凸系数为1,右侧方块的凹凸系数为0.3):
可见,现在的凹凸系数可以在不影响光照的情况下,改变模型凹凸感了
提高性能的写法
我们目前在 v2f
结构体中,世界坐标顶点位置和变换矩阵使用了
float3
和 float3x3
的两个变量来存储
1 2 3 4 5 6 7
| struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float3 worldPos: TEXCOORD1; float3x3 rotation: TEXCOORD2; };
|
但是在很多世界空间下计算法线贴图的 Shader 中,往往会使用 3 个 float4
类型的变量来存储它们
这样做的目的是因为:这种写法在很多情况下可以提高性能,因为它更好地与GPU的硬件架构匹配
float4
类型的寄存器是非常高效的,因为现代GPU通常会以 4 分量的向量为基本单位进行并行计算
float3x3
矩阵相对来说需要更多的寄存器和指令来表示和计算
因此,v2f
结构体可以修改为:
1 2 3 4 5 6 7 8 9 10 11
| struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float4 tangentToWorld0: TEXCOORD0; float4 tangentToWorld1: TEXCOORD1; float4 tangentToWorld2: TEXCOORD2;
};
|
其中,tangentToWorld0
~ tangentToWorld2
的 xyz
用来存储整个切线空间到世界空间的变换矩阵
而 tangentToWorld0
~ tangentToWorld2
的 w
用来存储顶点相对于世界坐标的位置
对应的,顶点着色器和片元着色器需要修改为:
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
| 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.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); data.tangentToWorld2 = 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.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal.xy *= _BumpScale; tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); float3 worldNormal = float3( dot(i.tangentToWorld0.xyz, tangentNormal), dot(i.tangentToWorld1.xyz, tangentNormal), dot(i.tangentToWorld2.xyz, tangentNormal) );
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir)); float3 halfA = normalize(viewDir + lightDir); fixed3 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); }
|
因为 3×3 矩阵乘以一个三行向量的计算过程就相当于 3×3 矩阵取三行分量分别去和向量点乘,
因此片元着色器内部不需要去重新构建一个 float3x3
转换矩阵,直接对三行分量分别点乘即可
因此,最终 Shader 代码是这样的:
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 "TeachShader/Lesson52" { Properties { _MainColor("MainColor", Color) = (1, 1, 1, 1) _MainTex("MainTex", 2D) = ""{} _BumpMap("BumpMap", 2D) = ""{} _BumpScale("BumpScale", Range(0, 1)) = 1 _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) _SpecularNum("SpecularNum", Range(0, 20)) = 18 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" }
CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc" #include "Lighting.cginc"
struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float4 tangentToWorld0: TEXCOORD1; float4 tangentToWorld1: TEXCOORD2; float4 tangentToWorld2: TEXCOORD3;
};
float4 _MainColor; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; float4 _SpecularColor; fixed _SpecularNum;
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.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); data.tangentToWorld2 = 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.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal.xy *= _BumpScale; tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); float3 worldNormal = float3( dot(i.tangentToWorld0.xyz, tangentNormal), dot(i.tangentToWorld1.xyz, tangentNormal), dot(i.tangentToWorld2.xyz, tangentNormal) );
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir)); float3 halfA = normalize(viewDir + lightDir); fixed3 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 } } }
|