US3S2L6——切线空间下计算法线纹理贴图
导入法线贴图资源
对于法线贴图,它的纹理类型需要设置为 Normal Map
关键知识点补充
-
模型空间下的切线数据
模型数据中的切线数据为 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坐标缩放偏移计算
-
副切线计算
用模型空间中的法线和切线进行叉乘,再乘以切线中的 w
(确定副切线方向)
1 2
| float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w;
|
-
构建模型空间到切线空间的变换矩阵
模型空间到切线空间的变换矩阵为:———切线副切线法线———
1 2 3 4 5 6 7 8 9
|
float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w;
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal, )
|
-
将光照方向和视角方向转换到模型空间(利用 ObjSpaceLightDir 和 ObjSpaceViewDir 内置函数)
-
将光照方向和视角方向转换到切线空间(利用变换矩阵进行乘法运算)
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
| 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 lightDir: TEXCOORD1; float3 viewDir: 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;
float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w; float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal ); data.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); data.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
return data; }
|
片元着色器回调函数内容
执行步骤:
- 取出法线贴图中的法线信息(利用纹理采样函数 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
| struct v2f { float4 pos: SV_POSITION; float4 uv: TEXCOORD0; float3 lightDir: TEXCOORD1; float3 viewDir: TEXCOORD2; };
float4 _MainColor; sampler2D _MainTex; sampler2D _BumpMap; float _BumpScale; float4 _SpecularColor; fixed _SpecularNum;
fixed4 frag (v2f i) : SV_Target { float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal *= _BumpScale;
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _MainColor.rgb;
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir))); float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, 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
| Shader "TeachShader/Lesson51" { 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 lightDir: TEXCOORD1; float3 viewDir: 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;
float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w; float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal ); data.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); data.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
return data; }
fixed4 frag (v2f i) : SV_Target { float4 packedNormal = tex2D(_BumpMap, i.uv.zw); float3 tangentNormal = UnpackNormal(packedNormal); tangentNormal *= _BumpScale;
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _MainColor.rgb;
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir))); float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfA)), _SpecularNum); fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
return fixed4(color.rgb, 1); } ENDCG } } }
|
显示效果(左图为不使用法线贴图的方块,右图为使用法线贴图的方块):
可见,使用法线纹理贴图的模型,虽然实际上没有做凹凸面,但是通过法线影响光照,通过光线模拟出了凹凸感
修改凹凸系数的计算方式,让法线系数不影响光照
目前的 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
| fixed4 frag (v2f i) : SV_Target { 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)));
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _MainColor.rgb;
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir))); float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfA)), _SpecularNum); fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
return fixed4(color.rgb, 1); }
|
效果如下(左侧方块的凹凸系数为1,右侧方块的凹凸系数为0.3):
可见,现在的凹凸系数可以在不影响光照的情况下,改变模型凹凸感了
修改后的完整 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
| Shader "TeachShader/Lesson51" { 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 lightDir: TEXCOORD1; float3 viewDir: 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;
float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w; float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal ); data.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); data.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
return data; }
fixed4 frag (v2f i) : SV_Target { 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)));
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _MainColor.rgb;
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir))); float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfA)), _SpecularNum); fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
return fixed4(color.rgb, 1); } ENDCG } } }
|