unity的樹有個痛點不論引擎默認還是speedtree shader都是強制 不支持烘焙的。原因也很直觀大量的樹實例需要那麼多lightmap 和uv2計算 又慢又大。所以對於實時陰影距離外的 樹渲染一直是unity項目需要解決的肉眼可見的畫質短板。
這裡不涉及更為複雜的 需要大量投入的大場景陰影方案。僅僅用2個小技巧幫助大部分小團隊提升這方面的畫質表現。
1簡單方案 ,適合手遊 0採樣大部分都是用speedtree來做資源,而speedtree最後有個billboard特殊旋轉方式,為此 speedtree的樹常常具備 旋轉對稱性的 特點。而且地形樹的碰撞體早版本引擎默認是不支持旋轉的 所以也需要他具有旋轉對稱性。那麼對於喜歡數學的人眼裡看什麼都是數學規律,旋轉對稱性這個規律 就等於可以推算 背光的一面了。看下圖,根據 vertex相對於樹實例0點的世界坐標方向 oa,與平行光方向的dot規律就能得到是否在 背光面/陰影裡。dot為0 剛好是在分界線上。如此一來代碼就很簡單了讓他們在xz平面上投影 求dot就可以了。如果你使用frag/vert 很好設置到shadowmask上或簡單的修改miancolor,如果用surface 需要看他編譯後代碼進行修改,稍微複雜些,需要改UnityShadowLibrary 的計算shadowmask函數。這個需要的人不多不詳細說了。
這個方式優點是非常簡單高效 但效果卻不錯。看以下效果測試對比。地板上的模糊陰影可以看到已經是出了shadowmap到了shadowmask範圍了。
這個方案其實是我當年轉TA後第一個創造的輪子,但這種思路屬於人人都可輕鬆獨立發明出來的。他的思想非常樸素,同時喜歡數學與計算機的人都會發現一個很底層規律。設備有限 數學無限,用離散資源代替連續描述。我們不知道360度下的陰影情況,但是可以烘焙4個方向的,然後根據當前朝向,對這4個方向進行插值即可。我選4個方向是因為 rgba剛好存儲。具體多少個更合適可根據自己項目調整。
4方向烘焙圖與合併後結果為了讓一個樹的shadowmask可以單獨完整鋪滿一個圖 可以在max烘焙,也可以在unity烘焙。但是在unity烘焙他按場景烘焙常常不能鋪滿,需要設置獨立的 bake tag才行。首先新建一個烘焙參數配置文件
然後隨便取一個不是-1的tag 就可以保持獨立分配貼圖了,如果覺得麻煩可以直接把烘焙參數調高讓他不止一張圖大小 他就會自動縮放並鋪滿一張圖了
插值角度計算
這是比較難寫對的地方,常規的插值是判斷邏輯是這樣的
求出當前燈光相對於旋轉後樹 在xz平面上的角度。
找到這個角度所在象限的2個確定方向的軸,取出這2個軸對應在rgba裡2個通道的float值 a和b
求當前角度在2個軸之間的權重,更靠近哪個。然後用lerp(a,b,該權重)得到當前角度float值 做完燈光強度疊加
這個計算對數學不好的人太暈,所以我又想了一種更直觀的坐標軸投影法。就是我們高中力學常用的, 力在 某方向上大小與方向=力在x軸投影 +力在y軸投影 矢量合的 大小與方向。光照也一樣,這些都是數學的基礎矢量定義。投影xy的大小 用數學表示就是 cos(a),和sin(a),就是cos(a),cos(90-a) ,圖形裡用dot(x軸,v),dot(y軸,v), 表達。
但是 這樣需要判斷方向性 x與-x,z與-z 需要採樣的顏色不同。為了不做判斷 其實可以 看成4個軸的 dot ,但是其中2個<0 所以max(0,dot())即可得到2個需要的合成。我們實際關注下 燈光方向與這4個軸的關係。
可以看出 r通道時 對應樹的局部坐標系時 +y,g時時-x,其他2個方向求反就可以。為什麼不是xz這是與模型建模的坐標系有關,自己測下就可以調整。為什麼光照方向與坐標系相反 因為shader裡用的不是光照朝向 而是_WorldSpaceLightPos0
所以對應的代碼就是這樣
計算局部角度根據角度插值採樣的代碼看下最終效果很不錯
因為我們已經考慮了各種方向插值 所以不管樹怎麼旋轉,平行光不同場景y軸角度的不同 觀察角度的不同 都比較正常,限制就是 平行光不要出現 太陡和太平,否則按45度烘焙會對應不上,然後樹只能繞y旋轉 不能其他角度旋轉 這一點幾乎所有遊戲都可以遵循。
結束語:這一篇沒什麼技術 都是技巧的分享, 希望我自己獨自孤單摸索出來的這些邊邊角角對改善他人項目有幫助。
相關代碼shader 代碼就幾行上面有截圖了,發一個工具代碼。順便吐槽一下 圖程小弟一直沒招到,需要自己寫這些簡單小工具,算不算上班摸魚呢?
工具長這樣
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Artplugins
{
public class TreeShadowmaskBakerEditor : EditorWindow {
private Light bakeLight;
private Renderer bakeRenderer;
public string info;
[MenuItem("地形/烘焙樹陰影")]
public static void OpenWindow()
{
var win = GetWindow<TreeShadowmaskBakerEditor>("烘焙樹陰影");
win.showInit();
}
private void showInit() {
bakeLight = null;
foreach (var item in FindObjectsOfType<Light>())
{
if (item.type == LightType.Directional) {
bakeLight = item;
break;
}
}
Show();
}
private void OnGUI()
{
bakeLight = EditorGUILayout.ObjectField("烘焙陰影的燈光", bakeLight, typeof(Light), true) as Light;
bakeRenderer = EditorGUILayout.ObjectField("烘焙陰影的樹", bakeRenderer, typeof(Renderer), true) as Renderer;
if (GUILayout.Button("烘焙陰影")) {
bakeShadowMask();
}
EditorGUILayout.LabelField(info);
}
private void bakeShadowMask()
{
if (bakeRenderer == null || bakeLight == null)
{
info = "需要設置 燈光 和 烘焙對象 參數";
return;
}
Quaternion initRot = bakeLight.transform.rotation;
Vector3 rot = bakeLight.transform.forward;
float xzLen = Mathf.Sqrt(1 - rot.y * rot.y);
Color32[][] colors = new Color32[4][];
GameObjectUtility.SetStaticEditorFlags(bakeRenderer.gameObject, StaticEditorFlags.LightmapStatic | StaticEditorFlags.ReflectionProbeStatic);
for (int i = 0; i < 4; i++)
{
rot.z = Mathf.Cos(i * Mathf.PI / 2) * xzLen;
rot.x = Mathf.Sin(i * Mathf.PI / 2) * xzLen;
bakeLight.transform.rotation = Quaternion.LookRotation(rot.normalized, Vector3.up);
Lightmapping.Bake();
if ((uint)bakeRenderer.lightmapIndex > LightmapSettings.lightmaps.Length)
{
//throw new System.Exception("bakeRenderer lightmap index error");
info = "bakeRenderer lightmap index error";
return;
}
var maskT = LightmapSettings.lightmaps[bakeRenderer.lightmapIndex].shadowMask;
UnityEditor.AssetDatabase.RenameAsset(UnityEditor.AssetDatabase.GetAssetPath(maskT), "tempTreeMask");
colors[i] = maskT.GetPixels32();
}
var mask0 = LightmapSettings.lightmaps[0].shadowMask;
var finalMask = new Texture2D(mask0.width, mask0.height, TextureFormat.ARGB32, false, true);
var finalColors = finalMask.GetPixels32();
for (int i = 0; i < finalColors.Length; i++)
{
finalColors[i].r = colors[0][i].r;
finalColors[i].g = colors[1][i].r;
finalColors[i].b = colors[2][i].r;
finalColors[i].a = colors[3][i].r;
}
finalMask.SetPixels32(finalColors);
finalMask.Apply();
string path = Path.GetDirectoryName(UnityEditor.AssetDatabase.GetAssetPath(bakeRenderer.sharedMaterial.mainTexture));
File.WriteAllBytes(path + "/TreeShadowMask.png", finalMask.EncodeToPNG());
AssetDatabase.Refresh();
info = "烘焙成功 >>>>>>>>>>>>> "+ path + " / TreeShadowMask.png";
}
}
public class TreeShadowmaskBakerImporter : AssetPostprocessor
{
private void OnPreprocessTexture()
{
string fileName = Path.GetFileName(assetPath);
TextureImporter importer = TextureImporter.GetAtPath(assetPath) as TextureImporter;
if (fileName.StartsWith("TreeShadowMask")) {
importer.sRGBTexture = false;
importer.maxTextureSize = 256;
}
if (fileName.StartsWith("tempTreeMask"))
{
importer.isReadable = true;
importer.textureCompression = TextureImporterCompression.Uncompressed;
}
}
}
}
來源知乎專欄:遊戲技術輪子