[Unity Shader] 地形紋理合併

題圖鏈接

寫在前面

開了個專欄,搬一篇之前寫的文章過來看看。以下是之前寫的博客正文。

前一段時間有個朋友找我分析一個地形的shader,代碼很長就主要看了下裡面的紋理合併的部分。Unity目前常見的地形應該是T4M的做法,據說是只支持打包4張紋理,也就是說可以在地形上刷4種紋理,多了的話就不太方便了。而這個shader中的方法可以打包9張紋理,然後靠一張混合紋理來控制混合,感覺還挺巧妙的。

方法

這個shader主要會利用兩張紋理。一張自然是包含了9種地形紋理的atlas紋理,就稱為BlockMainTex吧:

以及一張負責混合紋理的BlendTex:

這張紋理是關鍵所在,它的RG通道存儲了該位置處需要混合的兩種地形紋理的索引值,它的B通道存儲了這兩種紋理的混合係數。下面是上圖的RGB通道圖:

我們最終可以得到類似下面的效果:

可以看出來,我們可以用一個draw call+兩張紋理刷出至多9種不同的地形紋理。

地形紋理的索引

都可以看出來關鍵在於混合紋理BlendTex,它的RG通道存儲了該位置處需要混合的兩種地形紋理的索引值,即每個通道存儲了一個索引值。實際上,由於BlockMainTex是按照九宮格來打包了9種紋理,所以這個索引是一個二維的向量(x,y),也就是說把這個二維(x,y)索引值打包進一個0~1的8 bits小數內(通道值的範圍)。這主要是靠下面的公式:

f = frac{x}{16} + frac{y}{256}

其中,x和y分別表示在索引對應的行列值(我總是把上面的公式理解成把x編碼進了前4個bits,把y編碼進了後4個bits)。

上面是編碼的過程,解碼的相關公式就是:

x = floor(16f) \ y = floor(256f) - 16x

Shader代碼對應:

float2 encodedIndices = tex2D(_BlendTex, i.uv).xy;float2 twoVerticalIndices = floor((encodedIndices * 16.0));float2 twoHorizontalIndices = (floor((encodedIndices * 256.0)) - (16.0 * twoVerticalIndices));float4 decodedIndices;decodedIndices.x = twoHorizontalIndices.x;decodedIndices.y = twoVerticalIndices.x;decodedIndices.z = twoHorizontalIndices.y;decodedIndices.w = twoVerticalIndices.y;decodedIndices = floor(decodedIndices/4)/4;

decodedIndices就是0~3的整數索引值除以4的結果,即該種紋理在BlockMainTex中的起始值。拿圖中櫻花那個block舉例,它對應的xy值是(0,8)(由於xy的範圍是0~15,而圖片索引範圍是0~3,所以要乘以4),所以在BlendTex中的顏色就是8/256。

紋理採樣

知道了兩張地形紋理的索引,就該對它們進行採樣了。

float2 worldScale = (worldPos.xz * _BlockScale);float2 worldUv = 0.234375 * frac(worldScale) + 0.0078125; // 0.0078125 ~ 0.2421875, the range of a blockfloat2 uv0 = worldUv.xy + decodedIndices.xy;float2 uv1 = worldUv.xy + decodedIndices.zw;

整個地形使用xz平面的世界坐標的小數部分作為採樣坐標進行平鋪。由於每個block其實只佔了1/4的長寬值,所以要進行縮放。為了防止接縫處出現問題,還在兩邊稍微拉伸了下,即每邊拉伸了0.0078125個單位(即1/128個單位):

處理接縫

如果直接使用上面的uv0和uv1對紋理採樣,那麼在地形接縫處會出現明顯的問題:

這主要是因為這裡的紋理tiling是我們手動對worldScale取frac得到的,這樣紋理採樣坐標的偏導其實是不連續的,而通常我們使用單張紋理的tiling是連續的,是由圖形API和硬體幫我們處理平鋪類型的。

解決方法也很簡單,我們只需要保證在接縫處的偏導連續不突變即可,這可以靠支持4個參數的tex2D函數來解決。完整的代碼如下:

float blendRatio = tex2D(_BlendTex, i.uv).z;float2 worldScale = (worldPos.xz * _BlockScale);float2 worldUv = 0.234375 * frac(worldScale) + 0.0078125;float2 dx = clamp(0.234375 * ddx(worldScale), -0.0078125, 0.0078125);float2 dy = clamp(0.234375 * ddy(worldScale), -0.0078125, 0.0078125);float2 uv0 = worldUv.xy + decodedIndices.xy;float2 uv1 = worldUv.xy + decodedIndices.zw;// Sample the two texturefloat4 col0 = tex2D(_BlockMainTex, uv0, dx, dy);float4 col1 = tex2D(_BlockMainTex, uv1, dx, dy);// Blend the two texturesfloat4 col = lerp(col0, col1, blendRatio);

其實就是手動算了下採樣坐標worldScale的ddx和ddy,這也是為什麼之前每個block要向每邊拉伸了0.0078125個單位,這樣才不會採樣越境。上面在算ddx和ddy的時候,還把結果截取到(-0.0078125,0.0078125)即(1/128,-1/128)之間,我猜想這是為了在攝像機距地面非常的遠的時候(此時ddx和ddy的絕對值會比較大,紋素密度很大),如果ddx或ddy的絕對值超過了拉伸值0.0078125(1/128),就會在接縫處採樣到隔壁的block,所以要在這裡使用clamp截取一下範圍,下圖顯示了截取範圍前後的區別。在此需要感謝評論區的jim童鞋,我之前只考慮到了正數的情況,沒有考慮負值,這是不正確的(額這麼說來其實某個上線遊戲里也是不對的…)。

寫在最後

這裡只是主紋理採樣,當然還可以加上法線的採樣,比如:

// Sample the two normalfixed3 norm0 = UnpackNormal(tex2D(_BlockNormalTex, uv0, dx, dy));fixed3 norm1 = UnpackNormal(tex2D(_BlockNormalTex, uv1, dx, dy));// Blend the two normalsfixed3 norm = lerp(norm0, norm1, blendRatio);norm = normalize(norm);

還有很多自定義的紋理可以靠這種方法來類推。另外,這種方法顯然要實現一套自定義的刷地形工具給美術用(這應該是最麻煩的部分……)。總結來看,這種方法需要的基本採樣次數是:一次對BlendTex的採樣,兩次BlockMainTex的採樣,共3次來完成9種地形紋理的混合(其實每次只能同時混合兩張)。


推薦閱讀:

Ray Marching Ocean
Substance Designer 中實現形態圖形處理演算法(II) --- 非均勻的腐蝕拓展
Unity的立體幾何問題
Procedure Cloud
讓角色半透明:後期模糊(二)

TAG:計算機圖形學 |