Unity shader樹的陰影技巧

2022-01-22 遊戲蠻牛
需求與痛點

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範圍了。

普通無烘焙樹在 實時陰影外光影很平

用了該簡單方案 光影提升明顯2進階方案 ,基於prefab烘焙陰影

這個方案其實是我當年轉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;
}

}


}
}

來源知乎專欄:遊戲技術輪子

相關焦點

  • Unity Shader技巧七則!
    上圖是我在遊戲裡截的,在上傳以後被壓縮了 所以有點糊,但應該還是能辨別出來,攝像機前面是有一棵樹的,在遮擋住主角後變成半透了,並且透明的方式並不是傳統的半透,而是像扣掉了像素一樣的馬賽克形狀。ocias.com/blog/unity-stipple-transparency-shader
  • 更優雅地編寫Unity Shader
    notepad寫的,當然不適合我這種弱雞233按理說,應該選擇宇宙第一IDE Visual Studio是吧,嗯,應該是這樣,然而。。怎麼這麼醜啦!!VS插件嗯,不要慌,百度一下找個插件好了,果不其然很多人吐槽VS寫Unity的ShaderLab如何如何不便,然後順利找到了一個插件
  • 【乾貨】菜雞都能學會的Unity草地shader!
    → 曲面細分shader(可選) → 幾何shader(可選) → 片元shader接下來老規矩,貼上Reference之後,開始笨拙的復刻流程整理:https://roystan.net/articles/grass-shader.html
  • Unity 自定義Shader GUI
    最近抽空學習並弄了一個通用的Shader GUI,你可以使用他輕鬆的組織你的shader屬性。
  • Unity shader實例3則:輪廓渲染,溼滑的馬路,毛茸茸的大尾巴!
    unity shader實例#1 輪廓渲染-描邊1.法線外擴一般期望的描邊效果,就是在模型外面有一圈選邊,因此我們可以把模型擴大一點點,利用這個擴大的邊緣來實現描邊效果
  • Unity3dCG語言編寫Shader之渲染管線
    引言shadershader的工作原理是什麼?shader中文名叫著色器,顧名思義,它的作用可以先簡單理解為給屏幕上的物體畫上顏色。而什麼東西負責給屏幕上畫顏色?當然是GPU,所以我們寫shader的目的就是告訴GPU往屏幕哪裡畫、怎麼畫。說到這其實大家應該很明白了,如果我們連GPU的工作原理都不知道,何談指揮它?GPU的渲染管線又是什麼?
  • Unity實現簡易體積光/體積陰影
    剛接觸渲染時就被下面這些大佬的實現驚豔到,這裡也用到了許多裡面的技巧體積光+體積陰影1.採樣陰影信息首先我們需要一個方法來知道某個點是否是處於陰影中,對於平行光的話如果了解unity自帶shadowmap的實現就能知道可以通過對比燈光空間的shadowmapTexture
  • Unity Shader實現《死亡擱淺》掃描效果!
    深度圖那麼接下來我們可以算出相機遠平面四個點的局部坐標,也就是相對於相機的坐標,然後傳入到shader中,因為是在post posing中的操作,對於屏幕而言就只有四個頂點而已,我們就可以將局部坐標塞入到頂點中
  • [源碼]Unity Shader - Bloom(光暈、泛光)!
    UnityEngine;// --【Bloom 全屏泛光後期】--//編輯狀態下也運行 [ExecuteInEditMode]public class Bloom : PostEffectsBase{ public Shader bloomShader; private Material mMaterial; //bloom處理的shader
  • 優化unity地形的曲面細分
    內置算法的問題unity的曲面細分,特別是surface
  • Unity3D 淺談美術那些事 - PBR技術
    標準版這邊的 _Metallic(金屬性)、_MetallicGlossMap(金屬光澤貼圖),被高光版的 _SpecColor(高光顏色)、_SpecGlossMap(高光顏色法線貼圖)所代替。標準著色器主要是針對硬質表面(也就是建築材質)而設計的,可以處理大多數現實世界的材質,例如石頭、陶瓷、銅器、銀器或橡膠等。同時,它也可以非常出色地處理一些非硬質表面的材質,例如皮膚、頭髮或布料等。
  • 零基礎入門Unity Shader(一)
    Shader其實就是專門用來渲染圖形的一種技術,通過shader,我們可以自定義顯卡渲染畫面的算法,使畫面達到我們想要的效果。小到每一個像素點,大到整個屏幕,比如下面這兩個遊戲內比較常見的效果。幾個不同的圖形API都有各自的Shader語言,在DirectX中,頂點shader叫做 Vertex Shader ,像素Shader叫做 Pixel Shader; 在OpenGL中,頂點Shader也叫做 Vertex Shader ,但像素Shader叫做 Fragment Shader,也就是我們常說的片斷Shader或者片元Shader。
  • Unity通用渲染管線(URP)系列(六)——陰影遮罩(Shadow Masks)
    烘焙的陰影不會被剔除,但是它們也無法變化。理想情況下,我們可以使用最大陰影距離以下的實時陰影,並使用超出此範圍的烘焙陰影。Unity的陰影遮罩的混合光照模式可以實現。這將在需要時啟用shader關鍵字。將其對應的多重編譯指令添加到Lit著色器的CustomLit傳遞中。
  • Unity熱擾動(熱扭曲)特效
    基本原理生成當前背景紋理,利用mask面片在相應位置採樣該背景紋理,採樣的uv利用噪聲進行擾動,達到採樣結果與原背景紋理相比呈現出不規則錯位的效果,既熱擾動效果具體實現我將在Unity中實現該效果,具體有兩種思路在mask面片的shader
  • 如何用Unity Shader製作類似《爐石傳說》卡牌的動態效果?
    這裡所指的混合效果,並非直接用shader中的Blend,因為Blend是指片段著色器的返回顏色和當前顏色緩衝區顏色進行計算的邏輯,而我們的片段著色器返出的顏色,是將各個特效層顏色混合結果進行混合(有點繞)。換言之:此提及的「混合」只是將每層特效的顏色(之前提及的三層),通過某種方式疊加在一起的邏輯。
  • Unity Shader-遮擋處理(X-Ray,遮擋描邊,遮擋半透,遮擋溶解)【U3D遊戲開發必備乾貨效果】
    我們之前的文章寫過CommandBuffer,CmdBuffer是允許我們用任意一個自定義的shader渲染一個物體的,所以可以直接用一個RimLight的shader作為把人物渲染到RT上的prepass shader,其他與做法與描邊類似,只是不進行Blur圖與原圖相減的操作,C#代碼如下:/*******************************************
  • Unity Shader實現模型的描邊效果
    第二層,繪製正常狀態的效果合併兩層為最終效果下面進入shader講解:材質屬性Properties{ _Albedo ("Albedo", 2D) = "white" {} _Specular ("Specular Glossiness(RGB A)", 2D) = "black"
  • Unity 實用技巧 - 從實踐中總結經驗
    當你在 Unity3D 中編輯場景,突然死機時,可以在項目文件目錄中找到 Temp 文件夾,雙擊文件夾,找到_Backupscenes 文件夾,把後綴為 .backup 的文件後綴改為 .unity ,然後拖進 Unity3D 的 Project 界面裡面,這樣就可以還原死機前場景最後情況。
  • 2021遊戲大廠Unity面試題
    使用過哪些插件 shader graph製作shader光影效果;cinemachine+timeline+postprocessingstack製作過場動畫;nodecanvas製作怪物ai;easytouch手遊觸摸控制。
  • Unity免費的優質場景資源
    針對本項目的光照、陰影、遮蔽輸入和計算所做的自定義特殊處理。遮蔽探頭:應用於在樹葉上製作高效的天空遮蔽效果的烘焙解決方案。草地遮蔽系統:應用於為地形上的較小植被資源創建額外的遮蔽。音效和一個功能齊全的音頻環境。