深入理解法線貼圖


高度圖轉法線

高度圖中保存的是物體表面的高度信息,可以利用u,v方向上高度變化的斜率,計算出tangent和binormal,然后通過向量叉乘得到normal。我們在fragment shader中計算每個fragment的normal:

void InitializeFragmentNormal(inout Interpolators i) {
	// 取兩側點進行采樣
	float2 du = float2(_HeightMap_TexelSize.x * 0.5, 0);
	float u1 = tex2D(_HeightMap, i.uv - du);
	float u2 = tex2D(_HeightMap, i.uv + du);

	float2 dv = float2(0, _HeightMap_TexelSize.y * 0.5);
	float v1 = tex2D(_HeightMap, i.uv - dv);
	float v2 = tex2D(_HeightMap, i.uv + dv);

	float3 tangent = float3(_HeightMap_TexelSize.x, u2 - u1, 0);
	float3 binormal = float3(0, v2 - v1, _HeightMap_TexelSize.y);

	// 注意是B x T
	i.normal = cross(binormal, tangent);
	i.normal = normalize(i.normal);
}

可以看到,得到的法線非常銳利,這是因為叉乘得到的原始法線為float3(_HeightMap_TexelSize.y * (u1 - u2), _HeightMap_TexelSize.x * _HeightMap_TexelSize.y, _HeightMap_TexelSize.x * (v1 - v2))。原始法線的y分量過小,導致歸一化時x和z方向的值會偏大,從而偏離(0,1,0),而顯得效果十分銳利。這里我們可以特殊處理,將得到的tangent和binormal向量先進行縮放,再進行叉乘計算:

	float3 tangent = float3(1, u2 - u1, 0);
	float3 binormal = float3(0, v2 - v1, 1);

法線貼圖采樣

在Unity中,高度圖可以直接導入成法線貼圖,只要在導入設置中進行修改即可:

我們可以使用現成的API函數UnpackScaleNormal提取法線貼圖中的normal:

half3 UnpackScaleNormal(half4 packednormal, half bumpScale)
{
    return UnpackScaleNormalRGorAG(packednormal, bumpScale);
}

half3 UnpackScaleNormalRGorAG(half4 packednormal, half bumpScale)
{
    #if defined(UNITY_NO_DXT5nm)
        half3 normal = packednormal.xyz * 2 - 1;
        #if (SHADER_TARGET >= 30)
            // SM2.0: instruction count limitation
            // SM2.0: normal scaler is not supported
            normal.xy *= bumpScale;
        #endif
        return normal;
    #else
        // This do the trick
        packednormal.x *= packednormal.w;

        half3 normal;
        normal.xy = (packednormal.xy * 2 - 1);
        #if (SHADER_TARGET >= 30)
            // SM2.0: instruction count limitation
            // SM2.0: normal scaler is not supported
            normal.xy *= bumpScale;
        #endif
        normal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy)));
        return normal;
    #endif
}

可以看到,如果引擎編譯shader時發現平台不支持DXT5NM,則會將紋理信息直接按rgb格式解析為法線。bumpScale參數用法就和高度圖的時候類似,用來縮放法線的xy分量來調整凹凸的程度。如果支持DXT5NM,那么法線貼圖里只用了g通道和a通道來儲存法線的y分量和x分量。z分量需要根據向量的歸一化手動計算。另外別忘了,這里得到的法線是基於TBN空間的,如果直接拿來用,還需要手動調換一下y分量和z分量的位置:

	i.normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv), _BumpScale);
	i.normal = i.normal.xzy;
	i.normal = normalize(i.normal);

多張法線貼圖

之前我們提到過detail texture,可以與main texture疊加來豐富紋理細節。類似地,我們可以擁有一張detail normal map,與原來的法線貼圖進行疊加。normal map在unity導入時也可以設置fade range,完全淡出時的效果就跟沒有法線一樣。

那么,怎樣對兩個法線進行疊加呢?顯然,直接加和求平均是不合適的,平均會抵消法線的信息,使得效果變得平整。例如一個法線n1=(0, 1, 0),另外一個法線n2=(0, 0.5, 0.87),平均之后得到的法線n3=(0, 0.75, 0.44),顯然與豎直方向更加接近了,這不是我們想要的。我們希望,當有一個法線的效果是完全平整時,也不會影響另外一個法線產生的效果。

讓我們回到之前的高度圖中來。我們知道,法線其實是反映高度在uv方向高度變化程度的向量。即法線可以寫成這樣的形式:

\[\boldsymbol N = (du, 1, dv) \]

在TBN空間中,則為:

\[\boldsymbol N = (du, dv, 1) \]

我們希望法線疊加,就是把uv方向高度變化的量進行疊加。假設從兩張法線貼圖中取出的法線分別為M和D,那么可得到:

\[\boldsymbol M = (m_x, m_y, m_z) = (\dfrac{m_x}{m_z}, \dfrac{m_y}{m_z}, 1) \\ \boldsymbol D = (d_x, d_y, d_z) = (\dfrac{d_x}{d_z}, \dfrac{d_y}{d_z}, 1) \\ \]

那么,最終疊加的法線N為:

\[\boldsymbol N = (\dfrac{m_x}{m_z} + \dfrac{d_x}{d_z}, \dfrac{m_y}{m_z} + \dfrac{d_y}{d_z}, 1) = (m_x \cdot d_z + d_x \cdot m_z, m_y \cdot d_z + d_y \cdot m_z, m_z \cdot d_z) \]

可以看出,M和D的xy分量還是會受到各自z分量的影響,那么直接去掉它:

\[N = (m_x + d_x, m_y + d_y, m_z \cdot d_z) \]

這個就是最終得到的疊加法線。

當然,我們直接可以使用Unity提供的API函數BlendNormals來進行這個操作:

half3 BlendNormals(half3 n1, half3 n2)
{
    return normalize(half3(n1.xy + n2.xy, n1.z*n2.z));
}
切線空間

在使用Unity導入模型時,通常使用MikkTSpace算法來計算切線。MikkTSpace約定了計算binormal的方式為:

binormal = cross(normal.xyz, tangent.xyz) * tangent.w;

可以發現tangent向量是4維的,其中w分量的值為+1/-1。那么這個w分量是做什么用的呢?

我們知道,tangent和binormal實際代表了紋理的uv方向。在DirectX和OpenGL平台上,紋理的u方向是一致的,都是從左向右;而v方向卻有差別,DirectX上v方向是自頂向下的,原點在左上方;OpenGL上v方向是自底向上的,原點在右下方。因此,為了保證binormal的方向始終與紋理的v方向保持一致,需要引入一個分量w來控制是否翻轉binormal。

此外,如果是鏡像模型,那么模型的法線和切線應當是對稱的,但binormal應當還是一致的,即模型兩側的TBN空間不是一致的,而是對稱的。這時,兩邊的tangent的w分量就需要不同了。來看一個例子:

圖中是一個鏡像模型,讓我們導入到Unity中,看看它兩邊的TBN長啥樣:

其中,紅色代表tangent,藍色代表binormal,綠色代表normal。讓我們拉近了來看下:

可以看到,兩邊的TBN空間是對稱的,為了實現這一點,需要借助tangent的w分量。

不過在Unity中,我們發現實際計算binormal的方法是這樣的:

float3 CreateBinormal (float3 normal, float3 tangent, float binormalSign) {
	return cross(normal, tangent.xyz) *
		(binormalSign * unity_WorldTransformParams.w);
}

這里多出了一個變量unity_WorldTransformParams。它的w分量與物體transform的scale有關。如果有奇數個scale的值為負數,那么w取值為-1,否則取值為0。其實就是說,在scale為負數的時候,物體的紋理可能會被翻轉,導致TBN空間不對,這和前面提到的鏡像問題原因類似。來看一個例子:

當scale.x為-1時,原本向上的法線實際上要變得向下,在tangent不變的情況下,需要翻轉binormal:

當scale.x和scale.z都為-1時,原本向上的法線經過兩次翻轉之后依舊向上,就無需翻轉binormal:

如果你覺得我的文章有幫助,歡迎關注我的微信公眾號(大齡社畜的游戲開發之路-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM