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

這樣做的唯一好處就是避免使用 TextureArray,可能是當(dāng)時(shí)大家都擔(dān)心 TextureArray 在手機(jī)上的兼容性不好,所以都不采取,但事實(shí)上現(xiàn)在絕大多數(shù)手機(jī)都支持 openGL3.0+,也就支持 TextureArray,其實(shí)沒太大問題的
可其壞處很多,又要處理接縫問題,又要處理不同子圖之間的 Tiling 問題等等,這些用 TextureArray 都不需要考慮,且若有多個(gè)場(chǎng)景,它們某些地形紋理是共用的話,還會(huì)出現(xiàn)包體空間浪費(fèi)的情況。網(wǎng)絡(luò)上很多文章介紹這個(gè)思路,基本上都在解決這些問題,而且很多解決的都不太好,所以直接 PASS
其實(shí)使用 TextureArray 也沒多麻煩只是要注意兩點(diǎn)
一是導(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;
}
代碼看上去很長(zhǎng)但是所有細(xì)節(jié)都考慮到了,包括但不限于:①不同平臺(tái)壓縮格式設(shè)置,手機(jī)壓縮為 ASTC8x8,PC 為 DXT5;②鎖定格式為256,可以降采樣解決;③考慮到基于高度的混合方式,所有紋理統(tǒng)一加 alpha 通道
二就是 TextureArray 的組裝
很可惜,Material 并不支持序列化數(shù)組信息,包括 TextureArray,因此這個(gè)需要實(shí)時(shí)組裝:這個(gè)操作只需要做一次,所以沒有常駐性能損耗
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ù)寫個(gè)編輯器界面查看這些導(dǎo)出的地形紋理信息,并支持一些額外設(shè)置:
2.2.2 權(quán)重圖導(dǎo)出與索引計(jì)算
然后就是導(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)出并保存資源,上面代碼也有所以就省略吧,不然太長(zhǎng)了}
}
權(quán)重圖紋理的大小設(shè)置如下:
也可以導(dǎo)出的時(shí)候降采樣,例如這里設(shè)置 2048x2048 也沒問題,導(dǎo)出的時(shí)候降采樣兩次到 512 即可,降采樣的部分可以寫 shader 來實(shí)現(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)上看過這樣的思路:既然我一個(gè) pixel 至多混 2~3 張地形紋理,那我權(quán)重圖也只存 2~3 個(gè)通道不就好了,反正有索引可以知道你當(dāng)前 pixel 需要采樣哪三張,那我按照索引解碼或者索引大小的順序,把這三張地形紋理的權(quán)重依次存儲(chǔ)到 RGB 三個(gè)通道中就好,這樣就可以省掉權(quán)重圖中大部分為值為0的部分
理論可行,但是會(huì)帶來一個(gè)非常嚴(yán)重且不好解決的問題:那就是線性采樣差錯(cuò)
舉一個(gè)例子:默認(rèn)的 Texture 采樣都是雙線性插值,這種插值的前提是本身它的意義是連續(xù)的,但是按照上述思路導(dǎo)出的權(quán)重圖并沒有滿足這個(gè)條件,例如相鄰的兩個(gè)像素 A 和 B,A 融合了 ID=1 權(quán)重 90% ID=7 權(quán)重為 10%,B 融合了 ID=1 權(quán)重 70% ID=6 權(quán)重為 30%,它們第一個(gè)通道的融合是沒問題的,但是第二個(gè)通道它們對(duì)應(yīng)的地形紋理壓根不是同一張(一張 ID=6,一張 ID=7)此時(shí)線性插值得到的結(jié)果會(huì)將兩個(gè)像素的值進(jìn)行(一張 10%,一張 30%)混合,得到的結(jié)果根本沒有意義,并且會(huì)得到錯(cuò)誤的表現(xiàn):
想要解決這個(gè)問題還是比較困難,不采用線性采樣的方式而采用點(diǎn)采樣是不可能的,這樣得到的結(jié)果就是馬賽克,如果強(qiáng)行對(duì)齊 ID,也總會(huì)遇到對(duì)不齊的,并且擴(kuò)像素的話還是會(huì)浪費(fèi)通道(注意無論你怎么對(duì)齊,也不能根治這個(gè)問題,只能改善,特別是混合 3 張以上貼圖的情況)
當(dāng)然還有一個(gè)思路就是遇到邊緣(也就是相鄰像素索引不同的情況)手動(dòng)進(jìn)行插值,不再硬件 Bilinear,盡管這樣會(huì)帶來額外的消耗,但這應(yīng)該是最靠譜的方案
也可以跟美術(shù)規(guī)定,強(qiáng)行指定一張打底的圖作為權(quán)重 R 通道,G 通道存儲(chǔ)圖集中 2-4 區(qū)間的圖,B 通道存儲(chǔ)圖集中 5-8 區(qū)間的圖,然后在筆刷涂抹的時(shí)候記住 2-4 之間的圖不要重合,5-8 之間的圖不要重合這樣,輸出貼圖的時(shí)候也是按照這種方式去輸出,但是這樣極大的限制了美術(shù)的發(fā)揮,落實(shí)起來也比較麻煩
然后就是索引圖的計(jì)算和生成:
邏輯很簡(jiǎn)單,很容易想到暴力權(quán)重圖的每一個(gè)像素,找到權(quán)重最大的 n 個(gè)通道,然后記錄這 n 個(gè)索引存起來存入索引圖中,但是考慮到權(quán)重圖采樣是 Bilinear,因此單看權(quán)重圖像素值為0是不對(duì)的,因?yàn)閷?shí)際采樣結(jié)果可能不為0,所以真正的處理方式是在編輯器下模擬采樣,然后根據(jù)采樣結(jié)果來判斷要不要寫入索引:這個(gè)和降采樣的處理方式一致:
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;
}
這里處理不對(duì)也會(huì)出現(xiàn)馬賽克或者鋸齒,需要非常注意,舉一個(gè)例子:索引值為0意味著采樣第1張紋理,但是索引圖的默認(rèn)值也為0,所以要小心不要出現(xiàn)歧義,否則采樣的時(shí)候權(quán)重會(huì)算錯(cuò)
最后就是索引圖數(shù)據(jù)存儲(chǔ)的問題,例如要確保同一個(gè)像素最多只混4張地形紋理(4張已經(jīng)非常多了,絕大多數(shù)都是2-3張),那么就需要存儲(chǔ)4個(gè)索引值(int 值,范圍 0~7,或者 0~15,取決于你總共有多少張紋理)
- 最無腦的就是直接4個(gè)通道,每個(gè)通道存?zhèn)€ int
- 但是很容易想到2個(gè)通道的存儲(chǔ)方案,既然你的總紋理張數(shù)不會(huì)超過 8or16,那么就可以按照下面方式存儲(chǔ):
即一個(gè)通道存儲(chǔ)兩個(gè)索引值(x, y),由于范圍是 0~15,一個(gè)索引只占 4bit,而一個(gè)通道 8bit 剛好
解碼也很簡(jiǎn)單:
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個(gè)通道真的就是極限了嘛?必然不是!如果你的紋理總數(shù)只有8張,其實(shí)一個(gè)通道就夠了
你可能會(huì)問,就算紋理總數(shù)只有8張,那么一個(gè)索引也會(huì)占 3bit,一個(gè)通道 8bit 必然不夠,但事實(shí)上并沒有說一定要存索引值,可以把位當(dāng)索引,結(jié)果存 bool 值,即取或不取
舉個(gè)例子:如果你的采樣結(jié)果為 164/256,164 對(duì)應(yīng)的二進(jìn)制數(shù)為 10100100,翻譯過來就是取第 1, 3, 6 這三張紋理,搞定,只需要一個(gè)位運(yùn)算即可,代碼略
當(dāng)然如果你支持至多16張紋理的話,一個(gè)通道就不夠了
這兩種存儲(chǔ)方式要根據(jù)實(shí)際情況來選,例如你一個(gè)像素至多只混兩層,那么就要采用前面的方案,因?yàn)樗鼰o論如何只需要一個(gè)通道,如果你至多只支持8張紋理,就可以采取方案②以極限壓縮數(shù)據(jù)
2.3 紋理采樣與細(xì)節(jié)處理
準(zhǔn)備好這些信息之后,工作就完成90%了,采樣的 shader 寫起來并沒有難度,根據(jù)高度采樣的思路代碼其實(shí)就是一樣的,唯一的變化就是多了一個(gè)采樣索引的步驟,以及多了個(gè) 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è)置,以及移動(dòng)平臺(tái)的支持
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;}}
}
- 索引紋理需要點(diǎn)采樣(sampler_PointClamp),其它都需要雙線性采樣(sampler_LinearRepeat),如果你的權(quán)重圖是只保留有效權(quán)重的方式,就需要在過渡邊界手動(dòng)插值
- 索引圖的計(jì)算不能單純暴力權(quán)重圖,需要模擬采樣結(jié)果,否則一定會(huì)出現(xiàn)馬賽克問題
- 適當(dāng)?shù)慕挡蓸邮且粋€(gè)不錯(cuò)的選擇,低分辨率也能得到一個(gè)相對(duì)較好的結(jié)果,離線做法無需關(guān)心性能,如果前面4點(diǎn)包括后面的 mipmap 都處理好的了話,是不可能出現(xiàn)接縫、馬賽克(鋸齒)等問題的,此和最終貼圖分辨率無關(guān)
- 既然使用 TextureArray,像一些高度混合上限、MSE 這種額外的紋理參數(shù),也需要用數(shù)組保存,一樣不可以序列化,Tiling 同理
- 為了方便美術(shù)制作及導(dǎo)出資源,盡量將這些功能集成,包括前面的 TerrainToMesh:
2.3.1 Mipmap 與 VirtualTexture
最后就是不得不提的 mipmap,理論上無論是地形紋理還是權(quán)重理論都是需要開啟 mipmap 的,但是如果無腦開啟 mipmap,在跨紋理采樣的時(shí)候 uv 會(huì)突變,此時(shí)在突變處就會(huì)出現(xiàn)奇怪的縫隙:
這個(gè)是不可以接受的,因此要不直接關(guān)閉 mipmap,要不就在采樣的時(shí)候手動(dòng)指定 mipmap 層級(jí)以避免縫隙出現(xiàn),這要根據(jù)攝像機(jī)距離或者相鄰世界坐標(biāo)差來判斷具體采樣的 LOD 等級(jí)
對(duì)于 URP,可以直接計(jì)算 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;
對(duì)于 VirtualTexture,由于它不止可用于地形,所以后面有機(jī)會(huì)做了的話再單獨(dú)開一篇文章介紹
2.3.2 Unity TerrainTool 編輯器表現(xiàn)與游戲表現(xiàn)一致問題
前面提到過:由于 UnityTerrain 使用的方案和常規(guī) Mesh 不同,因此要準(zhǔn)備兩個(gè)材質(zhì),一個(gè)給編輯器用,一個(gè)給實(shí)際效果用,編輯器那種 AddPass 的思路無需采樣索引圖,直接按照4張圖混合的方式寫就 OK,這點(diǎn)是最大的不同,但是還是有不少地方要注意(按照重要度排序)
0. 由于采取的是 Addtive 的顏色疊加方式,因此像所有的環(huán)境貢獻(xiàn)(shader 里直接做疊加的那種)類似于霧效只需要在 BasePass 里面做一次,AddPass 里面不計(jì)算,除此之外所有 Lerp(color) 的計(jì)算,都需要再 lerp 一下當(dāng)前 Pass 權(quán)重圖的總貢獻(xiàn)(blendTotal),這個(gè)很好理解,其實(shí)本質(zhì)就是乘法分配律
#ifdef SC_EDITOR_ONLYhalf4 newColor = color;FinalColor(newColor, i);color = lerp(color, newColor, blendTotal);
#elseFinalColor(color, i);
#endif
- Tiling 的計(jì)算有所不同,差一個(gè) TerrainTextureLength 的倍數(shù)
- 注意 Gamma 和 Linear 的配置,如果你是 Gamma 的設(shè)置自己寫的軟線性,可能會(huì)出現(xiàn)下圖混合區(qū)間發(fā)白的現(xiàn)象:這種需要自己在計(jì)算權(quán)重時(shí)做一下 Gamma 矯正
- 最后就是高度混合,如果你的高度混合是參考的這篇文章,那么估計(jì)不好在 TerrainTool 下直接實(shí)現(xiàn)這個(gè)效果了,因?yàn)樗幸徊接?jì)算要拿到當(dāng)前所有紋理的高度最值,可是 UnityTerrain 這種 AddPass 的方式,當(dāng)你在第二個(gè) Pass 中計(jì)算第 4~8 張紋理顏色貢獻(xiàn)的時(shí)候,第 1~4 張的貢獻(xiàn)已經(jīng)算完了,也就是說你已經(jīng)拿不到前4張的權(quán)重和高度信息,這種情況下只改 shader 估計(jì)不行,要改源碼,所以在 TerrainTool 刷的時(shí)候,只能先不考慮高度混合,或者把有高度信息的放在一個(gè)組里
之所以做這個(gè)本質(zhì)上還是想白嫖 Unity 的工具,畢竟自己再寫一個(gè) Mesh 的筆刷想想就痛苦
其它參考:
- 地表紋理混合優(yōu)化 - 知乎
- [Unity Shader] 地形紋理合并 - 知乎
- unity32層大地形采樣性能優(yōu)化(1) - 知乎
- 怎么看待Unity 2021.2里最新的terrain地形工具在HDRP和URP里的效果? - 知乎