app軟件設(shè)計鄭州seo排名優(yōu)化公司
一、地形幾何方案:Terrain 與 Mesh
1.1 目前手游主流地形幾何方案分析
先不考慮 LOD 等優(yōu)化手段,目前地形的幾何方案選擇有如下幾種:
- 使用 Unity 自帶的 Terrain
- 使用 Unity 自帶的 Terrain,但是等美術(shù)資產(chǎn)完成后使用工具轉(zhuǎn)為 Mesh
- 直接使用 Mesh,地形直接由美術(shù)通過等 DCC 工具或 UE 工具制作(例如 worldmachine)后導(dǎo)入到 Unity
- 自己實現(xiàn) Terrain 或魔改 Unity Terrain 源碼,走 Heightmap 那一套
如果只看實現(xiàn)原理,本質(zhì)上就是①④(Heightmap)和②③(Mesh)兩種方案,據(jù)目前對多款手游的截幀分析,絕大多數(shù)的手游都還是 Mesh
下面簡要分析一下各方案
1.1.1 Unity 自帶的 Terrain:Heightmap 方案
關(guān)于 Heightmap 實現(xiàn)地形的原理不做介紹,主要講講他的地形混合部分和整體的工具與框架使用:
Unity 地形混合的原理是每4張紋理一個 Pass,每個 Pass 里無腦采樣所有貼圖,這就意味著如果你想要支持至多8張地形紋理混合,Unity 就要畫兩次,每次混4張,先不說多 Pass 已經(jīng)不太可以接受了,采樣次數(shù)也會出奇的多,事實上對于單個像素而言,8張圖都有貢獻(xiàn)是件不可能也不科學(xué)的事情,一般而言 2-4 張混合頂天,采樣8次必然有性能浪費的現(xiàn)象,這還是沒有考慮法線的
其次對于 Unity 源生的這些功能,都是大一統(tǒng)的思路,也就是考慮到的東西不少,能提供高質(zhì)量的美術(shù)資產(chǎn)最后效果也確實不錯,但事實上很多時候你的游戲用不到這么多的功能或者特性(features),因此最重要的還是做減法,減法做的好意味著性能也更優(yōu)秀,更何況 UnityTerrain 對于斜坡陡坡的處理還是有點糟糕,很多時候內(nèi)置的 TerrainTool 也并不能刷出完美的效果
想去做這些客制化就要有源碼,源碼獲取難度大的話這一塊沒法操作確實會比較難受,特別是很多時候性能都是能扣一是一點,如果優(yōu)化不好的話再好的效果也白搭,當(dāng)然最新版的 Terrain 性能提升了很多,再加上智能手機(jī)近兩年的快速發(fā)展,當(dāng)然未來有機(jī)會 使得 UnityTerrain 這一套成為移動平臺的主流
如果有條件的話,當(dāng)然可以自己實現(xiàn) Terrain 或魔改 Unity Terrain 源碼,走 Heightmap 那一套,但這個開發(fā)成本還是挺高的,要有 Unity 源碼以及相關(guān)的技術(shù)人員,一般小公司或者中小型手游都不會去花錢花精力做這件事情
那么哪些手游會去直接使用源生的 Terrain 呢?
那就是部分小體量線性關(guān)卡手游或者部分 2.5D 游戲,因為哪怕它的性能不好,但是奈何你的場景里面東西少,可能除了一個很小塊的地形就幾乎只有零星的人物和 UI 了,那確實也沒什么問題,畢竟這樣制作成本其實反而是最低的,最多做個略微調(diào)整和 shader 部分的源碼修改,如果還有那就是花了功夫的大型游戲了
1.1.2 Mesh 方案
不管是 Terrain 制作好轉(zhuǎn)成 Mesh,還是美術(shù) DCC 直接制作/二次加工導(dǎo)出 FBX,本質(zhì)上最終進(jìn)游戲的還是 Mesh,那就是不依賴 Heightmap 的,可以將地形當(dāng)作場景中的特殊物體來處理
和一般真正的物體不同的是,地形需要以下的額外支持
- 地形紋理混合
- 特殊的 LOD 及性能優(yōu)化手段
相比無腦使用 Terrain 的方案,使用 Mesh 比較麻煩的點就是地形紋理混合這一部分要單獨實現(xiàn),以及美術(shù)資源制作上可能要稍微復(fù)雜一些,因為 DCC 工具上制作最后和場景不契合還是要多次調(diào)整
使用 TerrainTool 后再 TerrainToMesh 看上去可以白嫖 TerrainTool 面板,但是拿到 Mesh 后你還是要調(diào)整,除此之外你想要編輯器效果(此時是 Heightmap 實現(xiàn))和最終效果(Mesh 實現(xiàn))一致,也要花點時間
好處就是可擴(kuò)展性好,整體操作也比較常規(guī),性能上更好把控,本文要介紹的的也正是這個方案
1.2 TerrainToMesh 工具
Amazing Аssets: Terrain To Mesh
當(dāng)然有現(xiàn)成的可以直接用,裝配好 package 后只需要把其中的兩個 dll 文件拿出來就 OK,注意它們的相對位置不能變,即 Editor.dll 要放在 Editor 文件夾中,并且兩個 dll 目錄深度應(yīng)該一致
工具的使用手冊可以直接參考下面這篇文檔
當(dāng)然你也可能需要對生成的 Mesh 進(jìn)行微調(diào),因為 Terrain 生成的 Mesh 頂點是無腦等距排列的,因此若要用 DCC 工具對 Mesh 進(jìn)行二次加工,就需要生成可供 DCC 工具讀取的 .obj 文件而非 Mesh
一般而言,對于比較平坦的部分、或者是水底的部分、不可到達(dá)的區(qū)域等等,都可以適當(dāng)?shù)膭h除部分頂點,不過在修改時要注意 uv 的值,如果改錯的了的最終采樣結(jié)果可能和在 TerrainTool 中不一樣
如果你是直接在 DCC 工具中做的,這些操作就都不需要,因為直接就是 Mesh,導(dǎo)入 Unity 就好
二、地形紋理混合方案
2.1 常規(guī)地形混合方案
目前地形混合主要有兩種思路,一種是直接按照權(quán)重圖進(jìn)行疊加混合:
這個思路非常簡單,拿至多4層地形紋理舉例,權(quán)重圖(對于 UnityTerrain 是 alphaTexture)的4個通道分別對應(yīng)著4張地形紋理的權(quán)重,在計算最終地形顏色時,每個地形紋理采樣后乘上貢獻(xiàn)相加作為最終顏色:當(dāng)然你的地形紋理層數(shù)若多于4張,那么權(quán)重圖四個通道就不夠用,就需要不止一張權(quán)重圖
mixedDiffuse = 0.0h;
mixedDiffuse += diffAlbedo[0] * half4(_DiffuseRemapScale0.rgb * splatControl.rrr, 1.0h);
mixedDiffuse += diffAlbedo[1] * half4(_DiffuseRemapScale1.rgb * splatControl.ggg, 1.0h);
mixedDiffuse += diffAlbedo[2] * half4(_DiffuseRemapScale2.rgb * splatControl.bbb, 1.0h);
mixedDiffuse += diffAlbedo[3] * half4(_DiffuseRemapScale3.rgb * splatControl.aaa, 1.0h);
這樣做的好處就是:每張地形紋理和權(quán)重圖的大小和精度不需要很高(一般256~512大小即可),通過這種方式鋪滿整個場景后最終細(xì)節(jié)效果也不會差,不然你只靠一張有限大小的紋理鋪滿整個場景幾乎是不可能的事,除非采用類似于 GPU Gems2 Chapter2 中的大世界方案
2.1.1 基于高度的地形混合
基于高度的紋理混合 shader
這也是個經(jīng)典算法,其實思路也很簡單,就是每張地形紋理多一個 alpha 通道用于存儲高度信息,最后在計算權(quán)重圖貢獻(xiàn)的時候,通過這個高度信息重算真實權(quán)重以達(dá)到一個非平滑過渡的效果:
half4 Blend(half4 high, half4 control, int4 index)
{half4 blend = half4(.0, .0, .0, .0);half4 weight = 1 - float4(_TerrainHeightWeight[index.r], _TerrainHeightWeight[index.g], _TerrainHeightWeight[index.b], _TerrainHeightWeight[index.a]);blend.r = high.r * control.r;blend.g = high.g * control.g;blend.b = high.b * control.b;blend.a = high.a * control.a;half ma = max(blend.r, max(blend.g, max(blend.b, blend.a)));blend = saturate(blend - ma + weight) * control;half blendTotal = blend.r + blend.g + blend.b + blend.a;return blendTotal == 0 ? half4(1.0, 0.0, 0.0, 0.0) : blend / blendTotal;
}
原文介紹的非常清楚所以這里也不再詳細(xì)描述了
2.1.2 多層地形混合優(yōu)化方案
這個前面也提到過,如果場景足夠大,只給4層地形紋理估計是不夠的,如果增加到8張紋理,那么就需要
- 8張地形紋理(廢話)
- 2張權(quán)重紋理(RGBA,一般512)
- 采樣 8+2 = 10 次,如果算上法線,則需要采樣 8*2 + 2 = 18 次(單個 pixel)
- 如果是 UnityTerrain 這種做法,需要繪制兩次
其實①②還好,因為圖不算大,但是③采樣那么多次是無法接受的,考慮到其實一個像素不可能出現(xiàn)這么多張紋理都有貢獻(xiàn)的情形,可以先采樣權(quán)重圖,再寫 if 判斷權(quán)重是否為0,為0就不采樣對應(yīng)的地形紋理,這樣確實沒問題,但是這種寫 if 的方法,事實上正是 if 的最壞情況,因為每個像素都可能會走向不同的分支,此時性能可能和暴力采樣差不多
在此基礎(chǔ)之上一個優(yōu)化思路就是:可以預(yù)先計算每個 pixel 到底采樣哪幾張地形紋理,把它們的 index 存儲到單獨一張圖上,然后采樣的時候先點采樣這張索引貼圖,根據(jù)信息采樣指定的 n 張地形貼圖即可,一般 n = 2~4 完全足夠
④就不用說了,完全沒有必要,因此在這種優(yōu)化之下,8張紋理的混合成本就為
- 8張地形紋理(沒得優(yōu)化,只能壓縮)
- 2張權(quán)重紋理 + 1張索引紋理(索引紋理可以減通道,但是權(quán)重不太好減!后面會給出原因)
- 采樣 n+3 or n+2 次,n 為一個像素最多混合的紋理個數(shù),一般為3足夠
這也是手游地形混合的主流思路,以多一張索引貼圖(indexTexture)為代價,減少大量無意義的采樣,也完全無需多次繪制
2.2 UnityTerrain 紋理資源導(dǎo)出
下面開始正題,就是思路有了怎么做的問題
考慮最復(fù)雜的情況:美術(shù)使用 TerrainTool 刷地形后導(dǎo)出 Mesh,然后微調(diào)后運用到游戲,這里面會多兩個要處理的事情:
- 確保編輯器下(Terrain)和游戲運行時(Mesh)表現(xiàn)一致
- Mesh 導(dǎo)出可以交給工具,但是紋理導(dǎo)出要自己寫
2.2.1 使用 TextureArray 存儲地形紋理
好了一樣前面①先不管,先解決②
網(wǎng)絡(luò)上很多都是拼接的做法,就是將 8-16 張地形紋理拼成一張大圖:

這樣做的唯一好處就是避免使用 TextureArray,可能是當(dāng)時大家都擔(dān)心 TextureArray 在手機(jī)上的兼容性不好,所以都不采取,但事實上現(xiàn)在絕大多數(shù)手機(jī)都支持 openGL3.0+,也就支持 TextureArray,其實沒太大問題的
可其壞處很多,又要處理接縫問題,又要處理不同子圖之間的 Tiling 問題等等,這些用 TextureArray 都不需要考慮,且若有多個場景,它們某些地形紋理是共用的話,還會出現(xiàn)包體空間浪費的情況。網(wǎng)絡(luò)上很多文章介紹這個思路,基本上都在解決這些問題,而且很多解決的都不太好,所以直接 PASS
其實使用 TextureArray 也沒多麻煩只是要注意兩點
一是導(dǎo)出的所有紋理格式大小必須一致,不一致的話可以寫編輯器給美術(shù)資產(chǎn)處理一下:
Texture2D RefreshSplatTextureMode(Texture2D tex, int newSize = 256)
{RenderTexture renderTex = RenderTexture.GetTemporary(newSize, newSize, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);Graphics.Blit(tex, renderTex);Texture2D resizedTexture = new Texture2D(newSize, newSize, TextureFormat.ARGB32, false);RTToTex(renderTex, ref resizedTexture);if (!Directory.Exists(TerrainTextureFolder + "ExportTerrain/")){Directory.CreateDirectory(TerrainTextureFolder + "ExportTerrain/");}var path = TerrainTextureFolder + "ExportTerrain/" + tex.name + "_" + newSize.ToString() + "x" + newSize.ToString() + ".png";var data = resizedTexture.EncodeToPNG();File.WriteAllBytes(path, data);AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);var textureIm = AssetImporter.GetAtPath(path) as TextureImporter;textureIm.isReadable = true;textureIm.anisoLevel = tex.anisoLevel;textureIm.mipmapEnabled = false;//textureIm.streamingMipmaps = tex.streamingMipmaps;//textureIm.streamingMipmapsPriority = tex.streamingMipmapsPriority;textureIm.wrapMode = tex.wrapMode;textureIm.filterMode = tex.filterMode;var apf = textureIm.GetPlatformTextureSettings("Android");var ipf = textureIm.GetPlatformTextureSettings("iPhone");var wpf = textureIm.GetPlatformTextureSettings("Standalone");apf.overridden = true;ipf.overridden = true;wpf.overridden = true;apf.format = TextureImporterFormat.ASTC_8x8;ipf.format = TextureImporterFormat.ASTC_8x8;wpf.format = TextureImporterFormat.DXT5;textureIm.SetPlatformTextureSettings(apf);textureIm.SetPlatformTextureSettings(ipf);textureIm.SetPlatformTextureSettings(wpf);textureIm.SaveAndReimport();AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);resizedTexture = (Texture2D)AssetDatabase.LoadAssetAtPath(path, typeof(Texture2D));return resizedTexture;
}
代碼看上去很長但是所有細(xì)節(jié)都考慮到了,包括但不限于:①不同平臺壓縮格式設(shè)置,手機(jī)壓縮為 ASTC8x8,PC 為 DXT5;②鎖定格式為256,可以降采樣解決;③考慮到基于高度的混合方式,所有紋理統(tǒng)一加 alpha 通道
二就是 TextureArray 的組裝
很可惜,Material 并不支持序列化數(shù)組信息,包括 TextureArray,因此這個需要實時組裝:這個操作只需要做一次,所以沒有常駐性能損耗
public void SetArray2D()
{if (sourceTextures.Length == 0 || sourceTextures[0] == null){return;}Texture2DArray texture2DArray = new Texture2DArray(sourceTextures[0].width,sourceTextures[0].height, sourceTextures.Length, sourceTextures[0].format,sourceTextures[0].mipmapCount, false);for (int i = 0; i < sourceTextures.Length; i++){Graphics.CopyTexture(sourceTextures[i], 0, texture2DArray, i);//texture2DArray.SetPixels(sourceTextures[i].GetPixels(), i, 0);}texture2DArray.filterMode = FilterMode.Bilinear;texture2DArray.wrapMode = TextureWrapMode.Repeat;material.SetTexture("_SplatArr", texture2DArray);
}
可以給美術(shù)寫個編輯器界面查看這些導(dǎo)出的地形紋理信息,并支持一些額外設(shè)置:
2.2.2 權(quán)重圖導(dǎo)出與索引計算
然后就是導(dǎo)出權(quán)重圖,這里網(wǎng)上代碼還是很多的,可以不做什么特別的操作直接導(dǎo)出:
void ExportAlphaTexture(int textureLength, out string[] textureDataLocal, out string indexTextureDataLocal)
{Texture2D[] alphaTextures = terrainData.alphamapTextures;int alphaWidth = alphaTextures[0].width;int alphaHeight = alphaTextures[0].height;int aimSize = alphaWidth / (int)tar.downSampling;Texture2D[] blendTex = new Texture2D[alphaTextures.Length];for (int i = 0; i < blendTex.Length; i++){blendTex[i] = new Texture2D(alphaWidth, alphaHeight, TextureFormat.RGBA32, false, true);blendTex[i].filterMode = FilterMode.Bilinear;}Texture2D indexTex = new Texture2D(aimSize, aimSize, TextureFormat.RG16, false, true);indexTex.filterMode = FilterMode.Point;for (int j = 0; j < alphaWidth; j++){for (int k = 0; k < alphaHeight; k++){for (int i = 0; i < alphaTextures.Length; i++){blendTex[i].SetPixel(j, k, alphaTextures[i].GetPixel(j, k));}}}Material getIndexmat = (Material)AssetDatabase.LoadAssetAtPath(T4MEditorFolder + "TerrainIndexTexBakeMat.mat", typeof(Material));textureDataLocal = new string[blendTex.Length];for (int i = 0; i < blendTex.Length; i++){EditorUtility.DisplayProgressBar("地形生成中", String.Format("導(dǎo)出第 {0} 張權(quán)重紋理", i + 1), (i + 1.0f) / (textureLength + 4));//這里就是導(dǎo)出并保存資源,上面代碼也有所以就省略吧,不然太長了}
}
權(quán)重圖紋理的大小設(shè)置如下:
也可以導(dǎo)出的時候降采樣,例如這里設(shè)置 2048x2048 也沒問題,導(dǎo)出的時候降采樣兩次到 512 即可,降采樣的部分可以寫 shader 來實現(xiàn),直接采樣鄰近4像素做平均:
//DownSample
RenderTexture toRT = null;
Texture2D temp = null;
for (int i = 0; i < additionalDownSampleTimes; i++)
{toRT = RenderTexture.GetTemporary(blendTexture.width / 2, blendTexture.height / 2, 0, RenderTextureFormat.ARGB32);mat.SetTexture("_Control1", blendTexture);Graphics.Blit(blendTexture, toRT, mat, 1);temp = new Texture2D(blendTexture.width / 2, blendTexture.height / 2, TextureFormat.RGBA32, false, true);temp.filterMode = FilterMode.Bilinear;RTToTex(toRT, ref temp);RenderTexture.ReleaseTemporary(toRT);
}//Shader:這里只貼核心代碼
float4 Tap4Down(float2 uv, float4 d)
{d *= _Control1_TexelSize.xyxy * float4(-1.0, -1.0, 1.0, 1.0);float4 color = SAMPLE_TEXTURE2D(_Control1, sampler_Control1, uv + d.xy);color += SAMPLE_TEXTURE2D(_Control1, sampler_Control1, uv);color += SAMPLE_TEXTURE2D(_Control1, sampler_Control1, uv + d.zy);color += SAMPLE_TEXTURE2D(_Control1, sampler_Control1, uv + d.xw);color += SAMPLE_TEXTURE2D(_Control1, sampler_Control1, uv + d.zw);color *= (1.0 / 5.0);return color;
}float4 frag(v2f i) : SV_Target
{float4 color = Tap4Down(i.uv.xy, 1);return color;
}
當(dāng)然還沒有結(jié)束,你可能在網(wǎng)上看過這樣的思路:既然我一個 pixel 至多混 2~3 張地形紋理,那我權(quán)重圖也只存 2~3 個通道不就好了,反正有索引可以知道你當(dāng)前 pixel 需要采樣哪三張,那我按照索引解碼或者索引大小的順序,把這三張地形紋理的權(quán)重依次存儲到 RGB 三個通道中就好,這樣就可以省掉權(quán)重圖中大部分為值為0的部分
理論可行,但是會帶來一個非常嚴(yán)重且不好解決的問題:那就是線性采樣差錯
舉一個例子:默認(rèn)的 Texture 采樣都是雙線性插值,這種插值的前提是本身它的意義是連續(xù)的,但是按照上述思路導(dǎo)出的權(quán)重圖并沒有滿足這個條件,例如相鄰的兩個像素 A 和 B,A 融合了 ID=1 權(quán)重 90% ID=7 權(quán)重為 10%,B 融合了 ID=1 權(quán)重 70% ID=6 權(quán)重為 30%,它們第一個通道的融合是沒問題的,但是第二個通道它們對應(yīng)的地形紋理壓根不是同一張(一張 ID=6,一張 ID=7)此時線性插值得到的結(jié)果會將兩個像素的值進(jìn)行(一張 10%,一張 30%)混合,得到的結(jié)果根本沒有意義,并且會得到錯誤的表現(xiàn):
想要解決這個問題還是比較困難,不采用線性采樣的方式而采用點采樣是不可能的,這樣得到的結(jié)果就是馬賽克,如果強(qiáng)行對齊 ID,也總會遇到對不齊的,并且擴(kuò)像素的話還是會浪費通道(注意無論你怎么對齊,也不能根治這個問題,只能改善,特別是混合 3 張以上貼圖的情況)
當(dāng)然還有一個思路就是遇到邊緣(也就是相鄰像素索引不同的情況)手動進(jìn)行插值,不再硬件 Bilinear,盡管這樣會帶來額外的消耗,但這應(yīng)該是最靠譜的方案
也可以跟美術(shù)規(guī)定,強(qiáng)行指定一張打底的圖作為權(quán)重 R 通道,G 通道存儲圖集中 2-4 區(qū)間的圖,B 通道存儲圖集中 5-8 區(qū)間的圖,然后在筆刷涂抹的時候記住 2-4 之間的圖不要重合,5-8 之間的圖不要重合這樣,輸出貼圖的時候也是按照這種方式去輸出,但是這樣極大的限制了美術(shù)的發(fā)揮,落實起來也比較麻煩
然后就是索引圖的計算和生成:
邏輯很簡單,很容易想到暴力權(quán)重圖的每一個像素,找到權(quán)重最大的 n 個通道,然后記錄這 n 個索引存起來存入索引圖中,但是考慮到權(quán)重圖采樣是 Bilinear,因此單看權(quán)重圖像素值為0是不對的,因為實際采樣結(jié)果可能不為0,所以真正的處理方式是在編輯器下模擬采樣,然后根據(jù)采樣結(jié)果來判斷要不要寫入索引:這個和降采樣的處理方式一致:
EditorUtility.DisplayProgressBar("地形生成中", "導(dǎo)出索引紋理", 3.0f / (textureLength + 4));
for (int i = 0; i < textureDataLocal.Length; i++)
{Texture blendTexture = (Texture)AssetDatabase.LoadAssetAtPath(textureDataLocal[i], typeof(Texture));getIndexmat.SetTexture("_Control" + (i + 1).ToString(), blendTexture);
}
RenderTexture rt2 = RenderTexture.GetTemporary(aimSize, aimSize, 0, RenderTextureFormat.RG16);
Graphics.Blit(blendTex[0], rt2, getIndexmat, 0);
RTToTex(rt2, ref indexTex);//Shader:這里只貼核心代碼
float4 ExportIndex(float2 uv)
{float4 ctr = Tap4Down(_Control1, uv, 1);float4 ctr2 = Tap4Down(_Control2, uv, 1);bool sum[8] = {ctr.r > 0 ? true : false, ctr.g > 0 ? true : false, ctr.b > 0 ? true : false, ctr.a > 0 ? true : false,ctr2.r > 0 ? true : false, ctr2.g > 0 ? true : false, ctr2.b > 0 ? true : false, ctr2.a > 0? true : false};int index = 0;int indexArray[4] = {0, 0, 0, 0};for (int i = 0; i < 8; i++){if (sum[i]){indexArray[index] = i;index = index + 1;}}return float4((indexArray[0]) / 16.0 + (indexArray[1]) / 256.0, (indexArray[2]) / 16.0 + (indexArray[3]) / 256.0, 0, 0);
}v2f vert(appdata v)
{v2f o;o.vertex = TransformObjectToHClip(v.vertex.xyz);o.uv = v.uv;return o;
}float4 frag(v2f i) : SV_Target
{float4 color = ExportIndex(i.uv.xy);return color;
}
這里處理不對也會出現(xiàn)馬賽克或者鋸齒,需要非常注意,舉一個例子:索引值為0意味著采樣第1張紋理,但是索引圖的默認(rèn)值也為0,所以要小心不要出現(xiàn)歧義,否則采樣的時候權(quán)重會算錯
最后就是索引圖數(shù)據(jù)存儲的問題,例如要確保同一個像素最多只混4張地形紋理(4張已經(jīng)非常多了,絕大多數(shù)都是2-3張),那么就需要存儲4個索引值(int 值,范圍 0~7,或者 0~15,取決于你總共有多少張紋理)
- 最無腦的就是直接4個通道,每個通道存?zhèn)€ int
- 但是很容易想到2個通道的存儲方案,既然你的總紋理張數(shù)不會超過 8or16,那么就可以按照下面方式存儲:
即一個通道存儲兩個索引值(x, y),由于范圍是 0~15,一個索引只占 4bit,而一個通道 8bit 剛好
解碼也很簡單:
int4 GetIndexArray(float2 val)
{int x = floor(val.x * 16);int y = val.x * 256 - x * 16;int z = floor(val.y * 16);int w = val.y * 256 - z * 16;return int4(x, y, z, w);
}
不過2個通道真的就是極限了嘛?必然不是!如果你的紋理總數(shù)只有8張,其實一個通道就夠了
你可能會問,就算紋理總數(shù)只有8張,那么一個索引也會占 3bit,一個通道 8bit 必然不夠,但事實上并沒有說一定要存索引值,可以把位當(dāng)索引,結(jié)果存 bool 值,即取或不取
舉個例子:如果你的采樣結(jié)果為 164/256,164 對應(yīng)的二進(jìn)制數(shù)為 10100100,翻譯過來就是取第 1, 3, 6 這三張紋理,搞定,只需要一個位運算即可,代碼略
當(dāng)然如果你支持至多16張紋理的話,一個通道就不夠了
這兩種存儲方式要根據(jù)實際情況來選,例如你一個像素至多只混兩層,那么就要采用前面的方案,因為它無論如何只需要一個通道,如果你至多只支持8張紋理,就可以采取方案②以極限壓縮數(shù)據(jù)
2.3 紋理采樣與細(xì)節(jié)處理
準(zhǔn)備好這些信息之后,工作就完成90%了,采樣的 shader 寫起來并沒有難度,根據(jù)高度采樣的思路代碼其實就是一樣的,唯一的變化就是多了一個采樣索引的步驟,以及多了個 TextureArray 的定義:
float4 ctr1 = SAMPLE_TEXTURE2D(_Control, sampler_Control, i.uv).rgba;
float4 ctr2 = SAMPLE_TEXTURE2D(_Control2, sampler_Control, i.uv).rgba;
float ctrArray[8] = {ctr1.rgba, ctr2.rgba};
float2 indexTex = SAMPLE_TEXTURE2D(_Index, sampler_Index, i.uv).rgba;
int4 index = GetIndexArray(indexTex);
float4 ctr = {ctrArray[index.x], ctrArray[index.y], ctrArray[index.z], ctrArray[index.w]};
half4 lay1 = SAMPLE_TEXTURE2D_ARRAY(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.r], _tilingY[index.r]), index.r).rgba;
half4 lay2 = SAMPLE_TEXTURE2D_ARRAY(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.g], _tilingY[index.g]), index.g).rgba;
half4 lay3 = SAMPLE_TEXTURE2D_ARRAY(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.b], _tilingY[index.b]), index.b).rgba;
half4 lay4 = SAMPLE_TEXTURE2D_ARRAY(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.a], _tilingY[index.a]), index.a).rgba;
但是整體要注意的細(xì)節(jié)和坑還是挺多的,這里還是列一下吧:
0. 關(guān)于 TextureArray 和 TextureAtlas 方案的選擇:這里選擇的是 TextureArray,問題天然比前者少,但是要注意格式的一致、TextureArray 的設(shè)置,以及移動平臺的支持
public static bool useTexArray
{get{switch (SystemInfo.graphicsDeviceType){case GraphicsDeviceType.Direct3D11:case GraphicsDeviceType.Direct3D12:case GraphicsDeviceType.PlayStation4:case GraphicsDeviceType.Vulkan:case GraphicsDeviceType.OpenGLES3:return true;default:return false;}}
}
- 索引紋理需要點采樣(sampler_PointClamp),其它都需要雙線性采樣(sampler_LinearRepeat),如果你的權(quán)重圖是只保留有效權(quán)重的方式,就需要在過渡邊界手動插值
- 索引圖的計算不能單純暴力權(quán)重圖,需要模擬采樣結(jié)果,否則一定會出現(xiàn)馬賽克問題
- 適當(dāng)?shù)慕挡蓸邮且粋€不錯的選擇,低分辨率也能得到一個相對較好的結(jié)果,離線做法無需關(guān)心性能,如果前面4點包括后面的 mipmap 都處理好的了話,是不可能出現(xiàn)接縫、馬賽克(鋸齒)等問題的,此和最終貼圖分辨率無關(guān)
- 既然使用 TextureArray,像一些高度混合上限、MSE 這種額外的紋理參數(shù),也需要用數(shù)組保存,一樣不可以序列化,Tiling 同理
- 為了方便美術(shù)制作及導(dǎo)出資源,盡量將這些功能集成,包括前面的 TerrainToMesh:
2.3.1 Mipmap 與 VirtualTexture
最后就是不得不提的 mipmap,理論上無論是地形紋理還是權(quán)重理論都是需要開啟 mipmap 的,但是如果無腦開啟 mipmap,在跨紋理采樣的時候 uv 會突變,此時在突變處就會出現(xiàn)奇怪的縫隙:
這個是不可以接受的,因此要不直接關(guān)閉 mipmap,要不就在采樣的時候手動指定 mipmap 層級以避免縫隙出現(xiàn),這要根據(jù)攝像機(jī)距離或者相鄰世界坐標(biāo)差來判斷具體采樣的 LOD 等級
對于 URP,可以直接計算 ddx ddy,再通過 SAMPLE_TEXTURE2D_ARRAY_GRAD 進(jìn)行采樣:
float4 ddxddy = _MipmapCtrl * float4(ddx(i.worldPos.xz), ddy(i.worldPos.xz)); \
half4 lay1 = SAMPLE_TEXTURE2D_ARRAY_GRAD(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.r], _tilingY[index.r]), index.r, ddxddy.xy, ddxddy.zw).rgba;
half4 lay2 = SAMPLE_TEXTURE2D_ARRAY_GRAD(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.g], _tilingY[index.g]), index.g, ddxddy.xy, ddxddy.zw).rgba;
half4 lay3 = SAMPLE_TEXTURE2D_ARRAY_GRAD(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.b], _tilingY[index.b]), index.b, ddxddy.xy, ddxddy.zw).rgba;
half4 lay4 = SAMPLE_TEXTURE2D_ARRAY_GRAD(_SplatArr, sampler_LinearRepeat, i.uvSplat * float2(_tilingX[index.a], _tilingY[index.a]), index.a, ddxddy.xy, ddxddy.zw).rgba;
對于 VirtualTexture,由于它不止可用于地形,所以后面有機(jī)會做了的話再單獨開一篇文章介紹
2.3.2 Unity TerrainTool 編輯器表現(xiàn)與游戲表現(xiàn)一致問題
前面提到過:由于 UnityTerrain 使用的方案和常規(guī) Mesh 不同,因此要準(zhǔn)備兩個材質(zhì),一個給編輯器用,一個給實際效果用,編輯器那種 AddPass 的思路無需采樣索引圖,直接按照4張圖混合的方式寫就 OK,這點是最大的不同,但是還是有不少地方要注意(按照重要度排序)
0. 由于采取的是 Addtive 的顏色疊加方式,因此像所有的環(huán)境貢獻(xiàn)(shader 里直接做疊加的那種)類似于霧效只需要在 BasePass 里面做一次,AddPass 里面不計算,除此之外所有 Lerp(color) 的計算,都需要再 lerp 一下當(dāng)前 Pass 權(quán)重圖的總貢獻(xiàn)(blendTotal),這個很好理解,其實本質(zhì)就是乘法分配律
#ifdef SC_EDITOR_ONLYhalf4 newColor = color;FinalColor(newColor, i);color = lerp(color, newColor, blendTotal);
#elseFinalColor(color, i);
#endif
- Tiling 的計算有所不同,差一個 TerrainTextureLength 的倍數(shù)
- 注意 Gamma 和 Linear 的配置,如果你是 Gamma 的設(shè)置自己寫的軟線性,可能會出現(xiàn)下圖混合區(qū)間發(fā)白的現(xiàn)象:這種需要自己在計算權(quán)重時做一下 Gamma 矯正
- 最后就是高度混合,如果你的高度混合是參考的這篇文章,那么估計不好在 TerrainTool 下直接實現(xiàn)這個效果了,因為它有一步計算要拿到當(dāng)前所有紋理的高度最值,可是 UnityTerrain 這種 AddPass 的方式,當(dāng)你在第二個 Pass 中計算第 4~8 張紋理顏色貢獻(xiàn)的時候,第 1~4 張的貢獻(xiàn)已經(jīng)算完了,也就是說你已經(jīng)拿不到前4張的權(quán)重和高度信息,這種情況下只改 shader 估計不行,要改源碼,所以在 TerrainTool 刷的時候,只能先不考慮高度混合,或者把有高度信息的放在一個組里
之所以做這個本質(zhì)上還是想白嫖 Unity 的工具,畢竟自己再寫一個 Mesh 的筆刷想想就痛苦
其它參考:
- 地表紋理混合優(yōu)化 - 知乎
- [Unity Shader] 地形紋理合并 - 知乎
- unity32層大地形采樣性能優(yōu)化(1) - 知乎
- 怎么看待Unity 2021.2里最新的terrain地形工具在HDRP和URP里的效果? - 知乎