找回密碼
 注冊帳號

掃一掃,訪問微社區

士郎 在Unity中實現2D光照系統

16
回復
1529
查看
打印 上一主題 下一主題
[ 復制鏈接 ]
排名
1
昨日變化

8029

主題

8587

帖子

3萬

積分

Rank: 16

UID
1231
好友
186
蠻牛幣
12029
威望
30
注冊時間
2013-7-29
在線時間
4101 小時
最后登錄
2019-8-9

活力之星原創精華達人突出貢獻獎財富之證游戲蠻牛QQ群會員蠻牛妹VIP

馬上注冊,結交更多好友,享用更多功能,讓你輕松玩轉社區。

您需要 登錄 才可以下載或查看,沒有帳號?注冊帳號

x


一些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)處來自θ方向的光量。

基于這一光照模型,我們可以實現一個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完成了場景渲染后,準備渲染屏幕后處理前的階段。
[AppleScript] 純文本查看 復制代碼
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!(別問,問就是顯卡崩潰)
[AppleScript] 純文本查看 復制代碼
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面板里設置:
[AppleScript] 純文本查看 復制代碼
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就是一個正方形,可以用以下代碼生成:
[AppleScript] 純文本查看 復制代碼
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變換矩陣,以確保渲染在屏幕上的正確位置。
[AppleScript] 純文本查看 復制代碼
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:

[AppleScript] 純文本查看 復制代碼
Tags { 
    "Queue"="Transparent" 
    "RenderType"="Transparent" 
    "PreviewType"="Plane"
    "CanUseSpriteAtlas"="True"
}

Lighting Off
ZWrite Off
Blend One One


2D Shadow

要在該光照系統中引入2D陰影,只需要在每次繪制光照貼圖時,額外對每個陰影投射光源繪制一個陰影貼圖(Shadow Map),并應用在渲染光照貼圖的Shader中采樣即可。
[AppleScript] 純文本查看 復制代碼
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

知乎@SardineFish

回復

使用道具 舉報

排名
64931
昨日變化

0

主題

29

帖子

72

積分

Rank: 2Rank: 2

UID
259926
好友
0
蠻牛幣
148
威望
0
注冊時間
2017-12-16
在線時間
41 小時
最后登錄
2019-8-12
沙發
2019-6-24 16:52:41 只看該作者
66666666666666666
回復 支持 反對

使用道具 舉報

排名
64931
昨日變化

0

主題

29

帖子

72

積分

Rank: 2Rank: 2

UID
259926
好友
0
蠻牛幣
148
威望
0
注冊時間
2017-12-16
在線時間
41 小時
最后登錄
2019-8-12
板凳
2019-6-24 16:53:52 只看該作者
支持大佬~~~~
回復

使用道具 舉報

排名
64931
昨日變化

0

主題

29

帖子

72

積分

Rank: 2Rank: 2

UID
259926
好友
0
蠻牛幣
148
威望
0
注冊時間
2017-12-16
在線時間
41 小時
最后登錄
2019-8-12
地板
2019-6-24 16:54:53 只看該作者
支持大佬~~~~
回復

使用道具 舉報

5熟悉之中
713/1000
排名
10706
昨日變化

0

主題

463

帖子

713

積分

Rank: 5Rank: 5

UID
301976
好友
1
蠻牛幣
1069
威望
0
注冊時間
2018-10-31
在線時間
152 小時
最后登錄
2019-8-12
5#
2019-6-24 20:13:11 只看該作者
點贊大佬,感謝分享經驗
回復 支持 反對

使用道具 舉報

7日久生情
2068/5000
排名
4092
昨日變化

0

主題

1350

帖子

2068

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
254705
好友
1
蠻牛幣
1883
威望
0
注冊時間
2017-11-16
在線時間
356 小時
最后登錄
2019-8-12
6#
2019-6-25 08:07:28 只看該作者
66666666666666666666666666
回復 支持 反對

使用道具 舉報

7日久生情
4901/5000
排名
21
昨日變化

0

主題

256

帖子

4901

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
29678
好友
1
蠻牛幣
9303
威望
0
注冊時間
2014-6-14
在線時間
1389 小時
最后登錄
2019-8-12
7#
2019-6-25 08:25:28 只看該作者
點贊大佬,感謝分享經驗
回復 支持 反對

使用道具 舉報

7日久生情
1914/5000
排名
1192
昨日變化

1

主題

559

帖子

1914

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
87577
好友
0
蠻牛幣
7999
威望
0
注冊時間
2015-3-31
在線時間
376 小時
最后登錄
2019-8-12

錦衣玉食

8#
2019-6-25 08:41:35 只看該作者
too good too strong!
回復 支持 反對

使用道具 舉報

0

主題

56

帖子

77

積分

Rank: 2Rank: 2

UID
321303
好友
0
蠻牛幣
97
威望
0
注冊時間
2019-5-6
在線時間
21 小時
最后登錄
2019-7-11
9#
2019-6-25 08:57:53 只看該作者
回復

使用道具 舉報

2初來乍到
121/150
排名
16219
昨日變化

0

主題

31

帖子

121

積分

Rank: 2Rank: 2

UID
292037
好友
0
蠻牛幣
301
威望
0
注冊時間
2018-8-1
在線時間
46 小時
最后登錄
2019-7-26
10#
2019-6-25 09:51:17 只看該作者
感謝樓主分享!
回復

使用道具 舉報

6蠻牛粉絲
1183/1500
排名
3615
昨日變化

5

主題

486

帖子

1183

積分

Rank: 6Rank: 6Rank: 6

UID
269155
好友
2
蠻牛幣
2749
威望
0
注冊時間
2018-2-22
在線時間
278 小時
最后登錄
2019-8-8
11#
2019-6-25 09:52:30 只看該作者
謝謝分線阿阿嘎
回復

使用道具 舉報

6蠻牛粉絲
1248/1500
排名
1815
昨日變化

8

主題

217

帖子

1248

積分

Rank: 6Rank: 6Rank: 6

UID
131585
好友
0
蠻牛幣
2751
威望
0
注冊時間
2015-12-13
在線時間
291 小時
最后登錄
2019-8-10
12#
2019-6-26 09:56:12 只看該作者
回復

使用道具 舉報

排名
64931
昨日變化

0

主題

29

帖子

72

積分

Rank: 2Rank: 2

UID
259926
好友
0
蠻牛幣
148
威望
0
注冊時間
2017-12-16
在線時間
41 小時
最后登錄
2019-8-12
13#
2019-6-28 19:50:51 只看該作者
66666666666厲害
回復 支持 反對

使用道具 舉報

3偶爾光臨
228/300
排名
10465
昨日變化

2

主題

82

帖子

228

積分

Rank: 3Rank: 3Rank: 3

UID
21010
好友
0
蠻牛幣
344
威望
0
注冊時間
2014-4-11
在線時間
42 小時
最后登錄
2019-7-16
14#
2019-7-10 12:24:12 只看該作者
66666666666
回復

使用道具 舉報

5熟悉之中
657/1000
排名
5899
昨日變化

0

主題

310

帖子

657

積分

Rank: 5Rank: 5

UID
11484
好友
0
蠻牛幣
38
威望
0
注冊時間
2013-12-31
在線時間
111 小時
最后登錄
2019-8-12
15#
2019-7-11 08:48:30 只看該作者
感謝分享經驗
回復

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 注冊帳號

本版積分規則

女校游泳队彩金