shader 获取法线_Unity Shader 入门到改行5——法线贴图
the best of blur
1. 法線貼圖理論
1.1 什么是法線貼圖
一般的貼圖中存儲的是表面顏色值(RGBA),而法線貼圖存放的則是法線信息(xyzw),假設某頂點處的 uv 坐標為 (u,v), 那么在法線貼圖 (u,v)處紋素的值表示該頂點的“法線”方向。通常法線貼圖中存儲的并不是這個頂點的真實法線信息。
1.2 法線貼圖的作用
想象一下,如果我們想要表現一個凹凸不平的模型表面(想象一個橙子的表面),有哪些辦法呢?
直接把模型做成凹凸不平。這種方法最理想,效果也最好。但是模型需要太多頂點了,例如橙子表面的一個“坑”,需要增加額外的若干個頂點。
做一個一定精度的平滑模型(例如把橙子做成一個球體模型),把表面的”坑“或”凸點“信息,也就是某一點的”海拔“記錄下來,渲染的時候根據這些信息動態生成頂點信息,得到凹凸不平的模型。不用說,這種方法需要單獨的存儲空間來記錄凹凸信息,而且頂點動態生成將會非常消耗。
和第二種方法一樣,做一個平滑模型,同樣記錄表面的“海拔”,渲染時不是動態生成頂點,而是根據“海拔”信息反推頂點的法線信息,通過光照效果來表現表面的”凹凸“。這種方法在計算光照時需要先進行表面法線的計算,比較消耗。
同樣做一個光滑模型,不是記錄表面的凹凸信息本身,而是記錄”假定的凹凸情形下的法線信息“,渲染時根據“有偏差”的法線信息來進行光照計算,使得渲染出來的畫面看起來凹凸不平。
上面第三種方法稱為基于“高度紋理”的凹凸表現。而第四種方法就是基于“法線紋理”的凹凸表現。
注意:高度貼圖和法線貼圖用來表現“凹凸”,在模型輪廓的邊緣會穿幫。比如你可以用這兩種方法使一個平滑的橙子模型表面看起來凹凸不平,但是在橙子的邊緣總是平滑的。
1.3 法線貼圖紋素取值范圍
通常貼圖紋素用來表示 RGBA,那么每個分量的取值范圍是[0,1],而法線的每個分量取值范圍為[-1,1],所以用貼圖紋素表示一個法線時,需要針對每一個分量做映射
pixel = (normal + 1) / 2;
在針對法線貼圖采樣后,進行逆運算
normal = 2 * pixel - 1;
得到實際的法線分量值。
1.4 法線貼圖基于什么坐標系
法線貼圖儲存了表面法線,而法線是一個方向,那么這個方向是基于什么坐標系?通常跟隨頂點數據一起傳輸到 頂點著色器中的法線,由 NORMAL 語義指定,是基于模型坐標系的。所以我們可以將法線在模型坐標中的值存儲到法線貼圖中,得到模型空間的法線貼圖,而在實際制作中,應用更多的是頂點切線空間的法線貼圖。
對于每個頂點,以頂點自身作為原點,頂點切線方向為x軸,法線方向為z軸,切線和法線方向叉乘得到 y 軸(副法線方向),得到這個頂點的 切線坐標空間,基于這個空間的法線記錄下來得到 頂點切線空間的法線貼圖。
左:模型空間的法線貼圖 右:切線空間的法線貼圖
模型空間法線貼圖的優點
(1)實現簡單,直觀
(2)更平滑的縫合和邊界處的表現。
切線空間法線貼圖的優點
(1)可重用,記錄的是“相對法線信息”,而模型空間的法線貼圖記錄的是“絕對法線信息”。
(2)可以做 UV 動畫來實現凹凸移動效果。
(3)可壓縮。z分量永遠是正方向,可以只存儲xy分量。
1.5 為什么切線空間的法線貼圖看起來都是偏藍色的?
切線空間的法線貼圖保存的是基于頂點的切線空間中的法線數值,而在頂點的切線空間中,真實法線的反向永遠是(0,0,1),經過上述的計算公式得到法線貼圖中存儲的值為 (0.5,0.5, 1),偏藍色。而修改后的法線通常也是 z 值最大,因為你不太可能有90度以上的法線修改,整體還是偏藍。
通常使用頂點切線空間的法線貼圖,而頂點空間中的修改后的法線值,z分量最大,換算成顏色就是 b 分量最大,所以法線貼圖通常看起來偏藍色。
2. 如何在 Shader 中應用法線貼圖
我們使用在切線空間下的法線貼圖,先上完整 shader 代碼,然后逐步分析,代碼如下:
Shader "Shader_Examples/04_NormalTexture_TangentSpace"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_SpecularColor ("SpecularColor", Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8, 256)) = 20
_BumpTex ("BumpTex", 2D) = "bump" {}
_BumpScale ("BumpScale", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _SpecularColor;
float _BumpScale;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _Gloss;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 模型空間副法線
fixed3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
float3 lightDir = ObjSpaceLightDir(v.vertex);
float3 viewDir = ObjSpaceViewDir(v.vertex);
o.lightDir = mul(rotation, lightDir);
o.viewDir = mul(rotation, viewDir);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 lightDir = normalize(i.lightDir);
float3 viewDir = normalize(i.viewDir);
float3 halfDir = normalize(lightDir + viewDir);
float4 packedNormal = tex2D(_BumpTex, i.uv);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, lightDir));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 specular = _SpecularColor * _LightColor0 * pow(saturate(dot(halfDir, tangentNormal)), _Gloss);
return fixed4(diffuse + ambient + specular, 1.0);
}
ENDCG
}
}
}
渲染效果如圖:
法線貼圖效果
2.1 shader 屬性與對應的變量
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_SpecularColor ("SpecularColor", Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8, 256)) = 20
_BumpTex ("BumpTex", 2D) = "bump" {}
_BumpScale ("BumpScale", Float) = 1.0
}
漫反射紋理 _MainTex, 高光顏色 _SpecularColor 和高光系數 _Gloss 沒什么好說的,新增的紋理 _BumpTex 為法線貼圖,默認值為 unity 內置法線貼圖 "bump",_BumpScale 用來控制表面的“凹凸”程度,后面會分析它是怎么起作用的。對應的變量聲明:
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _SpecularColor;
float _BumpScale;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _Gloss;
2.2 著色器輸入結構
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
語義TANGENT指定的切線是一個 float4 類型的變量,而語義NORMAL指定的法線是 float3 類型,因為 TANGENT 的z分量需要用來確定 副法線 的方向,下一個段落會介紹如何計算副法線
因為使用了頂點切線空間下的法線貼圖,我們需要把所有的光照計算都變換到頂點切線空間下,在頂點著色器中將光線方向lightDir和視線方向viewDir變換到頂點切線空間,再輸入到片元著色器中。
因為我們這里沒有涉及到紋理的 ST 變化,所以 _MainTex 和 _BumpTex 功用紋理坐標
v2f 中并沒有定義法線,因為我們這里使用的是發現貼圖中的法線,而不直接使用頂點法線了
2.3 頂點著色器
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 模型空間副法線
fixed3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
// 模型空間到頂點切線空間的變換矩陣
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// 光線方向和視線防線變換到頂點切線空間
float3 lightDir = ObjSpaceLightDir(v.vertex);
float3 viewDir = ObjSpaceViewDir(v.vertex);
o.lightDir = mul(rotation, lightDir);
o.viewDir = mul(rotation, viewDir);
o.uv = v.uv;
return o;
}
頂點的法線:頂點所在的所有平面的法線加權平均,得到頂點法線
頂點的切線:我們都知道頂點切線與頂點法線垂直、但與頂點法線垂直的方向有很多?哪一條是頂點切線呢?約定俗成 切線最終規定為頂點 uv 坐標中的 u 方向,可以參考文末的參考文章1。
頂點的副法線:由法線和切線叉乘得到,方向性由頂點切線的z分量確定。
如何計算模型空間到頂點切線空間的變換矩陣:參考我的推導過程模型空間到頂點切線空間變換矩陣的推導。結論就是:將模型空間下的切線、副法線、法線按行排列得到變換矩陣。
在頂點著色器中將光線方向和視線方向變換到頂點的切線空間并傳遞給片元著色器。
2.4 片元著色器
fixed4 frag (v2f i) : SV_Target
{
float3 lightDir = normalize(i.lightDir);
float3 viewDir = normalize(i.viewDir);
float3 halfDir = normalize(lightDir + viewDir);
float4 packedNormal = tex2D(_BumpTex, i.uv);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, lightDir));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 specular = _SpecularColor * _LightColor0 * pow(saturate(dot(halfDir, tangentNormal)), _Gloss);
return fixed4(diffuse + ambient + specular, 1.0);
}
如何從法線貼圖中得到法線:tex2D采樣 _BumpTex 得到該點的法線像素值,需要計算出對應的xyz值,因我們已經在 Unity 編輯器中將 _BumpTex 設置為 "Normal Map" ,所以內置方法 UnpackNormal 已經執行了這個計算
albedo,diffuse,ambient,specular 的計算不用多說了
_BumpScale 的作用:用來控制“凹凸程度”,當 _BumpScale 為0時,表示該點的頂點法線和法線貼圖中采樣出的法線重合,說明該點沒有“凹凸”,_BumpScale 絕對值越大,表示該點的頂點法線和貼圖中的法線偏差越遠,說明“凹凸感”越明顯。
下面5個膠囊體的 _BumpScale 取值分別為 2/1/0/-1/-2
不同的_BumpScale凹凸效果
3. Unity中的法線貼圖類型設置
在上面的片元著色器中,我們從法線貼圖中采樣出紋素后,使用了 Unity 內置函數 UnpackNormal 來計算最終的法線值。只有正確的設置圖片的類型為 "Normal Map" 時,使用這個內置函數才能得到正確結果,在 Unity 中的設置面板如下:
法線貼圖設置
Create from Grayscale 表示是否“高度圖”生成的紋理貼圖。當我們在貼圖中記錄的是相對高度(黑色表示更低,白色表示更高)時,除了要設置類型為“Normal Map”之外,還要勾選這個選項,這個貼圖就會被當成紋理貼圖使用了。
勾選了 Create from Grayscale 之后,有兩個選項:bumpness表示凹凸程度,filtering 決定了如何生成紋理貼圖,smooth 表示生成的法線過渡比較平滑,而sharp 則表示法線過渡比較鋒利。
總結
以上是生活随笔為你收集整理的shader 获取法线_Unity Shader 入门到改行5——法线贴图的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tl494c封装区别_TL494参数,功
- 下一篇: linux操作系统网络,网络安装linu