2d shader unity 阴影_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/Unity2DLighting?github.com2D 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 2D?ray-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 光照渲染管線需要實現以下過程:
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 mapvar 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 screencmd.Blit(BuiltinRenderTextureType.CameraTarget, screen);// Blend light map & screen image with custom shadercmd.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 程序式生成,亦或是使用一張光斑貼圖。其核心部分就是:
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 shaderLightMaterial.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 One2D 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 光照系統框架
- 渲染管線擴展
- 全局光照設置
- 2D 光源
- 程序式光源,光照衰減
- 貼圖光源
- 2D陰影
- 硬陰影
- 軟陰影(高斯模糊實現、體積光實現)
陰影投射物體目前僅支持多邊形,未來將加入對 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 語言畫光(一):基礎
總結
以上是生活随笔為你收集整理的2d shader unity 阴影_Unity中实现2D光照系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mycat配置访问oracle_教程 |
- 下一篇: 华为手机输入键盘声音_华为手机键盘隐藏7