在Unity中實現2D光照系統
在一些2D遊戲中引入實時光影效果能給遊戲帶來非常大的視覺效果提升,亦或是利用2D光影實現視線遮擋機制。例如Terraria,Starbound。
2D光影效果需要一個動態光照系統實現,而通常遊戲引擎所提供的實時光照系統僅限於3D場景,要實現圖中效果的2D光影需要額外設計適用於2D場景的光照系統。雖然在Unity Assets Store上有不少2D光照系統外掛,實際上實現一個2D光照系統並不複雜,並且可以藉此機會熟悉Unity渲染管線開發。
本文將介紹通過Command Buffer擴充套件Unity Built-in Render Pipeline實現一個簡單的2D光照系統。所涉及到的前置技術棧包括Unity,C#,render pipeline,shader programming等。本文僅包含核心部分的部分程式碼,完整程式碼可以在我的GitHub上找到:
SardineFish/Unity2DLightinggithub.com
2D Lighting Model
首先我們嘗試仿照3D場景中的光照模型,對2D光照進行理論建模。
在現實世界中,我們通過肉眼所觀測到的視覺影象,來自於光源產生的光,經過物體表面反射,通過晶狀體、瞳孔等眼球光學結構,投射在視網膜上導致視覺細胞產生神經衝動,傳遞到大腦中形成。而在照片攝影中,則是經過鏡頭後投射在感光元件上成像並轉換為數字影象資料。而在圖形渲染中,通常通過模擬該過程,計算攝像機所接收到的來自物體反射的光,從而渲染出影象。
1986年,James T.Kajiya在論文THE RENDERING EQUATION[1]中提出了一個著名的渲染方程:
3D場景中物體表面任意一面元所受光照,等於來自所有方向的光線輻射度的總和。這些光經過反射和散射後,其中一部分射向攝像機(觀察方向)。(通常為了簡化這一過程,我們可以假定這些光線全部射向攝像機)
而在2D平面場景中,我們可以認為,該平面上任意一點所受的光照,等於來自所有方向的光線輻射度的總和,其中的一部分射向攝像機,為了簡化,我們認為這些光線全部進入攝像機。這一光照模型可以用以下方程描述:
即,平面上任意一點,或者說一個畫素(x,y)的顏色,等於在該點處來自[0,2π]所有方向的光的總和。其中Light(x,y,θ)表示在點(x,y)處來自θ方向的光量。
該方程來自 Milo Yip的一篇文章:
Milo Yip:用C語言畫光(一):基礎zhuanlan.zhihu.com
基於這一光照模型,我們可以實現一個2D空間內的光線追蹤渲染器。去年我在這系列文章的啟發下,基於js實現了一個簡單的2D光線追蹤渲染器demo
Raytrace 2Dray-trace-2d.sardinefish.com
關於該渲染器,我寫過一篇Blog:2D光線追蹤渲染,借用該渲染器渲染出來的2D光線追蹤影象,我們可以對2D光照效果做出一定的分析和比較。
2D Lighting System
Light Source
相較於3D實時渲染中的點光源、平行光源和聚光燈等多種精確光源,在2D光照中,通常我們只需要點光源就足以滿足對2D光照的需求。
由於精確光源的引入,我們不再需要對光線進行積分計算,因此上文中的2D光照方程就可以簡化為:
即空間每點的光照等於場景中所有點光源在(x,y)處光量的總和。為了使光照更加真實,我們可以對點光源引入光照衰減機制:
其中d為平面上一點到光源的距離,t為可調節引數,取值範圍[0,1]
所得到的光照效果如圖(t=0.3):
光照衰減模型還有很多種,可以根據需求進行更改。
Light Rendering
在有了光源模型之後,我們需要將光照繪製到螢幕上,也就是光照的渲染實現。計算光照顏色與物體固有顏色的結合通常採用直接相乘的形式,即color=lightColor.rgb*albedo.rgb,與Photoshop等軟體中的“正片疊底”是同樣的。
在3D光照中,通常有兩種光照渲染實現:Forward Rendering和Deferred Shading。在2D光照中,我們也可以參考這兩種光照實現:
Forward:對場景中的每個Sprite設定自定義Shader材質,渲染每一個2D光源的光照,然而由於Unity渲染管線的限制,這一過程的實現相當複雜,並且對於具有N個Sprite,M個光源的場景,光照渲染的時間複雜度為O(MN)。
Deferred:這一實現類似於螢幕後處理,在Unity完成場景渲染後,對場景中的每個光源,繪製到一張螢幕光照貼圖上,將該光照貼圖與螢幕影象相乘得到最終光照效果,過程類似於上圖。
顯然在實現難度和執行效率上來說,選擇Deferred的渲染方式更方便
Render Pipeline
在Unity中實現這樣的一個光照渲染系統,一些開發者選擇生成一張覆蓋螢幕的Mesh,用該Mesh渲染光照,最終利用Unity渲染管線中的透明度混合實現光照效果。這樣的實現具有很好的平臺相容性,但也存在可擴充套件性較差,難以進行更復雜的光照和軟陰影生成等問題。
因此我在這裡選擇使用CommandBuffer對Unity渲染管線進行擴充套件,設計一條2D光照渲染管線,並新增到Unity Built-in Render Pipeline中。對於使用Unity Scriptable Render Pipeline的開發者,本文提到的渲染管線亦有一定參考用途,SRP也提供了相應擴充套件其渲染管線的相關API。
總結一下上文關於2D光照系統的建模,以及光照渲染的實現,我們的2D光照渲染管線需要實現以下過程:
1.針對場景中每個需要渲染2D光照的攝像機,設定我們的渲染管線
2.準備一張空白的Light Map
3.遍歷場景中的所有2D光源,將光照渲染到Light Map
4.抓取當前攝像機目標Buffer中的影象,將其與Light Map相乘混合後輸出到攝像機渲染目標
Camera Script
要使用CommandBuffer擴充套件渲染管線,一個CommandBuffer例項只需要例項化一次,並通過Camera.AddCommandBuffer方法新增到攝像機的某個渲染管線階段。此後需要在每次攝像機渲染影象前,即呼叫OnPreRender方法時,清空該CommandBuffer並重新設定相關引數。
這裡還設定ExecuteInEditMode和ImageEffectAllowedInSceneView屬性以確保能在編輯器的Scene檢視中實時渲染2D光照效果。
這裡選擇CameraEvent.BeforeImageEffects作為插入點,即在Unity完成了場景渲染後,準備渲染螢幕後處理前的階段。
- using System.Collections;
- using System.Linq;
- using UnityEngine;
- using UnityEngine.Rendering;
- [ExecuteInEditMode]
- [ImageEffectAllowedInSceneView]
- [RequireComponent(typeof(Camera))]
- public class Light2DRenderer : MonoBehaviour
- {
- CommandBuffer cmd;
- // Init CommandBuffer & add to camera.
- void OnEnable()
- {
- cmd = new CommandBuffer();
- GetComponent<Camera>().AddCommandBuffer(CameraEvent.BeforeImageEffects, cmd);
- }
- void OnDisable()
- {
- GetComponent<Camera>().RemoveCommandBuffer(CameraEvent.BeforeImageEffects, cmd);
- }
- void OnPreRender()
- {
- // Setup CommandBuffer every frame before rendering.
- RenderDeffer(cmd);
- }
- }
Setup CommandBuffer
由於我們要繪製一張光照貼圖,並將其與螢幕影象混合,我們需要一個臨時的RenderTexture(RT),這裡設定Light Map的貼圖格式為ARGBFloat,原因是我們希望光照貼圖中每個畫素的RGB光照分量是可以大於1的,這樣可以提供更精確的光照效果和更好的擴充套件性,而預設的RT會在混合前將緩衝區中每個畫素的值裁剪到[0,1]。
在臨時RT使用完畢後,請務必Release!請務必Release!請務必Release!(別問,問就是顯示卡崩潰)
- public void RenderDeffer(CommandBuffer cmd)
- {
- cmd.Clear();
- // Render light map
- var lightMap = Shader.PropertyToID("_LightMap");
- cmd.GetTemporaryRT(lightMap, -1, -1, 0, FilterMode.Bilinear, RenderTextureFormat.ARGBFloat);
- cmd.SetRenderTarget(lightMap);
- cmd.ClearRenderTarget(true, true, Color.black);
- var lights = GameObject.FindObjectsOfType<Light2D>();
- foreach (var light in lights)
- {
- light.RenderLight(cmd);
- }
- var screen = Shader.PropertyToID("_ScreenImage");
- cmd.GetTemporaryRT(screen, -1, -1);
- // Grab screen
- cmd.Blit(BuiltinRenderTextureType.CameraTarget, screen);
- // Blend light map & screen image with custom shader
- cmd.Blit(screen, BuiltinRenderTextureType.CameraTarget, LightingMaterial, 0);
- // DONT FORGET to release the temp RT!!!
- // OR your graphic card may crash after a while due to the memory overflow (may be) :)
- cmd.ReleaseTemporaryRT(lightMap);
- cmd.ReleaseTemporaryRT(screen);
- cmd.SetRenderTarget(BuiltinRenderTextureType.CameraTarget);
- }
最終用於光照混合的Shader程式碼非常簡單,這裡使用了UNITY_LIGHTMODEL_AMBIENT引入一個場景全域性光照,全域性光照可以在Lighting>Scene皮膚裡設定:
- fixed4 frag(v2f i) : SV_Target
- {
- float3 ambient = UNITY_LIGHTMODEL_AMBIENT;
- float3 light = ambient + tex2D(_LightMap, i.texcoord).rgb;
- float3 color = light * tex2D(_MainTex, i.texcoord).rgb;
- return fixed4(color, 1.0);
- }
Render Lighting
渲染光源光照貼圖的過程,對於不同的光源型別有不同的實現方式,例如直接使用Shader程式式生成,亦或是使用一張光斑貼圖。其核心部分就是:
1.生成一張用於渲染的Mesh(通常就是一個簡單的Quad)
2.設定CommandBuffer將該Mesh繪製到Light Map
Quad就是一個正方形,可以用以下程式碼生成:
- Mesh = new Mesh();
- Mesh.vertices = new Vector3[]
- {
- new Vector3(-.5, -.5, 0),
- new Vector3(.5, -.5, 0),
- new Vector3(-.5, .5, 0),
- new Vector3(.5, .5, 0),
- };
- Mesh.triangles = new int[]
- {
- 0, 2, 1,
- 2, 3, 1,
- };
- Mesh.RecalculateNormals();
- Mesh.uv = new Vector2[]
- {
- new Vector2 (0, 0),
- new Vector2 (1, 0),
- new Vector2 (0, 1),
- new Vector2 (1, 1),
- };
需要注意的是,Mesh資源不參與GC,也就是每次new出來的Mesh會永久駐留記憶體直到退出(導致Unity記憶體洩漏的一個主要因素)。因此不應該在每次渲染的時候new一個新的Mesh,而是在每次渲染時,呼叫Mesh.Clear()方法將Mesh清空後重新設定。
這裡生成的Mesh基於該GameObject的本地座標系,在呼叫CommandBuffer.DrawMesh以渲染該Mesh,我們還需要設定相應的TRS變換矩陣,以確保渲染在螢幕上的正確位置。
- public void RenderLight(CommandBuffer cmd)
- {
- if (!LightMaterial)
- LightMaterial = new Material(Shader.Find("Lighting2D/2DLight"));
-
- // You may want to set some properties for your lighting shader
- LightMaterial.SetTexture("_MainTex", LightTexture);
- LightMaterial.SetColor("_Color", LightColor);
- LightMaterial.SetFloat("_Attenuation", Attenuation);
- LightMaterial.SetFloat("_Intensity", Intensity);
- cmd.SetGlobalVector("_2DLightPos", transform.position);
-
- var trs = Matrix4x4.TRS(transform.position, transform.rotation, transform.localScale);
- cmd.DrawMesh(Mesh, trs, LightMaterial);
- }
由於我們需要同時將多個光照繪製到同一張光照貼圖上,根據光照物理模型,光照強度的疊加應當使用直接相加的方式,因此用於渲染光照貼圖的Shader應該設定Blend屬性為One One:
- Tags {
- "Queue"="Transparent"
- "RenderType"="Transparent"
- "PreviewType"="Plane"
- "CanUseSpriteAtlas"="True"
- }
- Lighting Off
- ZWrite Off
- Blend One One
2D Shadow
要在該光照系統中引入2D陰影,只需要在每次繪製光照貼圖時,額外對每個陰影投射光源繪製一個陰影貼圖(Shadow Map),並應用在渲染光照貼圖的Shader中取樣即可。
- var lights = GameObject.FindObjectsOfType<Light2D>();
- foreach (var light in lights)
- {
- cmd.SetRenderTarget(shadowMap);
- cmd.ClearRenderTarget(true, true, Color.black);
- if (light.LightShadows != LightShadows.None)
- {
- light.RenderShadow(cmd, shadowMap);
- }
- cmd.SetRenderTarget(lightMap);
- light.RenderLight(cmd);
- }
關於2D陰影貼圖的生成,可以參考偽人的這篇文章:
偽人:如何在unity實現足夠快的2d動態光照zhuanlan.zhihu.com
或者我有時間繼續填坑再寫一個。(FLAG)
Source Code
完整的project放在了GitHub上:https://github.com/SardineFish/Unity2DLighting
截止本文,已實現的功能包括:
•2D光照系統框架
o渲染管線擴充套件
o全域性光照設定
•2D光源
o程式式光源,光照衰減
o貼圖光源
•2D陰影
o硬陰影
o軟陰影(高斯模糊實現、體積光實現)
陰影投射物體目前僅支援多邊形,未來將加入對Box和Circle等2D碰撞體的陰影實現。
Git Tag:https://github.com/SardineFish/Unity2DLighting/tree/v0.1.0
References
[1]Kajiya,James T."The rendering equation."ACM SIGGRAPH computer graphics.Vol.20.No.4.ACM,1986.
https://currypseudo.github.io/2018-12-14-2d-dynamic-light/-CurryPseudo-在unity實現足夠快的2d動態光照(一)
https://docs.unity3d.com/Manual/GraphicsCommandBuffers.html-Unity-Graphics Command Buffers
https://zhuanlan.zhihu.com/p/30745861-Milo Yip-用C語言畫光(一):基礎
作者:SardineFish
專欄地址:https://zhuanlan.zhihu.com/p/67923713
相關文章
- Unity——基於ShaderLab實現光照系統Unity
- 如何在unity實現足夠快的2d動態光照Unity
- 在Unity中實現手部跟蹤Unity
- Unity 2D全球釋出 開發者夢想照進現實Unity
- 在Unity實現遊戲命令模式Unity遊戲模式
- 在 Unity 多人遊戲中實現語音對話Unity遊戲
- Unity 2D遊戲製作Unity遊戲
- 【Unity】(2D)物體拖拽Unity
- 在Python中實現你自己的推薦系統Python
- CpuMemSets在Linux作業系統中的實現(轉)Linux作業系統
- Unity的Forward+ FPTL光照剔除解析(一)UnityForward
- Unity的Forward+ FPTL光照剔除解析(三)UnityForward
- Unity的Forward+ FPTL光照剔除解析(四)UnityForward
- Unity中實現人形角色的攀爬Unity
- 在Unity中為即時戰略遊戲實現戰爭迷霧(上)Unity遊戲
- 在Unity中為即時戰略遊戲實現戰爭迷霧(下)Unity遊戲
- 在Unity中實現一個簡單的訊息管理器Unity
- Unity中的光源型別(向前渲染路徑進行光照計算)Unity型別
- Unity5 PBR如何實現天氣系統的雪景效果Unity
- Unity 揹包系統的完整實現(基於MVC框架思想)UnityMVC框架
- Unity3D 2D實戰遊戲開發Unity3D遊戲開發
- Unity3D 的物理渲染和光照模型Unity3D模型
- [Unity3D] 2D畫素遊戲(一) Hello Unity!Unity3D遊戲
- 在spring boot中訊息推送系統設計與實現Spring Boot
- 在Linux中,如何實現檔案系統的快照和克隆?Linux
- 傳統光照模型模型
- 揭祕《Sherman》:使用Unity製作影視級光照效果Unity
- 如何在Unity中實現水體互動?Unity
- Unity實現“籠中窺夢”的渲染效果Unity
- daedalOS:基於WebAssembly在瀏覽器中實現桌面作業系統Web瀏覽器作業系統
- 在Linux作業系統中實現內部程式通訊(轉)Linux作業系統
- Unity——技能系統(一)Unity
- Unity——技能系統(二)Unity
- Unity——技能系統(三)Unity
- Unite 2019|Unity的光照烘焙技術(上)Unity
- 在Linux系統下實現Server Push(轉)LinuxServer
- 在Linux系統下實現ServerPush(轉)LinuxServer
- Unity3D中實現幀同步 - Part 2Unity3D