unity png alpha 太小会被当做透明 unity改变image透明度_贴图

在一些 2D 游戏中引入实时光影效果能给游戏带来非常大的视觉效果提升,亦或是利用 2D 光影实现视线遮挡机制。例如 Terraria, Starbound。



unity png alpha 太小会被当做透明 unity改变image透明度_贴图_02

unity png alpha 太小会被当做透明 unity改变image透明度_unity 设置image透明度_03

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 上找到:

https://github.com/SardineFish/Unity2DLighting

2D Lighting Model

首先我们尝试仿照 3D 场景中的光照模型,对 2D 光照进行理论建模。

在现实世界中,我们通过肉眼所观测到的视觉图像,来自于光源产生的光,经过物体表面反射,通过晶状体、瞳孔等眼球光学结构,投射在视网膜上导致视觉细胞产生神经冲动,传递到大脑中形成。而在照片摄影中,则是经过镜头后投射在感光元件上成像并转换为数字图像数据。而在图形渲染中,通常通过模拟该过程,计算摄像机所接收到的来自物体反射的光,从而渲染出图像。

1986年,James T. Kajiya 在论文 THE RENDERING EQUATION [1] 中提出了一个著名的渲染方程:



unity png alpha 太小会被当做透明 unity改变image透明度_unity 设置image透明度_04

3D 场景中物体表面任意一面元所受光照,等于来自所有方向的光线辐射度的总和。这些光经过反射和散射后,其中一部分射向摄像机(观察方向)。(通常为了简化这一过程,我们可以假定这些光线全部射向摄像机)

而在 2D 平面场景中,我们可以认为,该平面上任意一点所受的光照,等于来自所有方向的光线辐射度的总和,其中的一部分射向摄像机,为了简化,我们认为这些光线全部进入摄像机。这一光照模型可以用以下方程描述:



unity png alpha 太小会被当做透明 unity改变image透明度_渲染管线_05

即,平面上任意一点,或者说一个像素 (x, y) 的颜色,等于在该点处来自 [0, 2π] 所有方向的光的总和。其中 Light(x, y, θ) 表示在点 (x, y) 处来自 θ 方向的光量。

该方程来自 Milo Yip 的一篇文章:

https://zhuanlan.zhihu.com/p/30745861

基于这一光照模型,我们可以实现一个 2D 空间内的光线追踪渲染器。去年我在这系列文章的启发下,基于 js 实现了一个简单的 2D 光线追踪渲染器 demo

https://ray-trace-2d.sardinefish.com/

关于该渲染器,我写过一篇 Blog: 2D光线追踪渲染,借用该渲染器渲染出来的2D光线追踪图像,我们可以对2D光照效果做出一定的分析和比较。



unity png alpha 太小会被当做透明 unity改变image透明度_渲染管线_06

2D Lighting System

Light Source

相较于 3D 实时渲染中的点光源、平行光源和聚光灯等多种精确光源,在 2D 光照中,通常我们只需要点光源就足以满足对 2D 光照的需求。

由于精确光源的引入,我们不再需要对光线进行积分计算,因此上文中的 2D 光照方程就可以简化为:



unity png alpha 太小会被当做透明 unity改变image透明度_unity 设置image透明度_07

即空间每点的光照等于场景中所有点光源在 (x, y) 处光量的总和。为了使光照更加真实,我们可以对点光源引入光照衰减机制:



unity png alpha 太小会被当做透明 unity改变image透明度_3D_08

其中 d 为平面上一点到光源的距离,t 为可调节参数,取值范围 [0, 1]

所得到的光照效果如图(t = 0.3):



unity png alpha 太小会被当做透明 unity改变image透明度_渲染管线_09

光照衰减模型还有很多种,可以根据需求进行更改。

Light Rendering

在有了光源模型之后,我们需要将光照绘制到屏幕上,也就是光照的渲染实现。计算光照颜色与物体固有颜色的结合通常采用直接相乘的形式,即 color = lightColor.rgb * albedo.rgb,与 Photoshop 等软件中的“正片叠底”是同样的。



unity png alpha 太小会被当做透明 unity改变image透明度_unity 设置image透明度_10

在 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 并重新设置相关参数。

这里还设置ExecuteInEditModeImageEffectAllowedInSceneView属性以确保能在编辑器的 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 阴影贴图的生成,可以参考 伪人 的这篇文章:

https://zhuanlan.zhihu.com/p/52423823

或者我有时间继续填坑再写一个。(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.

currypseudo.github.io/2 - CurryPseudo - 在unity实现足够快的2d动态光照(一)

docs.unity3d.com/Manual - Unity - Graphics Command Buffers

zhuanlan.zhihu.com/p/30 - Milo Yip - 用 C 语言画光(一):基础

声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。

作者:SardineFish