在一些 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.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 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 光照渲染管线需要实现以下过程:

  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 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 程序式生成,亦或是使用一张光斑贴图。其核心部分就是:

  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 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 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 光照系统框架

    • 渲染管线扩展
    • 全局光照设置
  • 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光照系统相关推荐

  1. 2d shader unity 阴影_UNITY崩坏3角色渲染实践

    最近二次元手游,卡通渲染都挺火的.虽然公司没开这类型项目,但是渲染来玩一下也好,原理都是一样,比较简单. 在日式卡通中,<罪恶装备>.<崩坏3>的效果都很不错,都是几年前的产品 ...

  2. 2d shader unity 阴影_【Unity Shader】平面阴影(Planar Shadow)

    来介绍一种适用于移动平台的高性能实时阴影解决方案--平面阴影(Planar Shadow). 由于Unity内置的实时阴影实现方式是屏幕空间阴影贴图(Screen Space Shadow Map)非 ...

  3. dx绘制2d图像_在DirectX 中进行2D渲染

    http://flcstudio.blog.163.com/blog/static/756035392008115111123672/ 最近,我看到很多关于DirectX8在最新的API中摒弃Dire ...

  4. unity 镜面反射_Unity | Diffuse Light漫反射光照

    Hello all 今天跟大家唠一下unity shader中的diffuse 是我木偶心没 我们在游戏开发的过程中光照是必不可少的,而为了使游戏更加真实,获得更好的体验,也会模拟因光照产生的各种镜面 ...

  5. audio unity 加速_unity中如何实现调整视频播放速度的功能?

    using UnityEngine; using System.Collections; public class VideoPlayer : MonoBehaviour { // 控制器将传来的信息 ...

  6. Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照

    转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交 ...

  7. 着色器编程_unity中的基础纹理,使用Unity Shader实现基础纹理的渲染效果

    学习通过使用Unity Shader实现基础纹理的渲染效果 目录 学习通过使用Unity Shader实现基础纹理的渲染效果 问1:详细描述一下漫反射纹理.高度纹理.法线纹理.渐变纹理和遮罩纹理? 问 ...

  8. max unity 方向_在2D游戏中实现方向光照

    老实讲,这个需求是老板提的. 游戏嘛,很多东西都可以做,但是做不做往往不是做的人可以决定的.这个效果虽然没见过有游戏实现过(一般实现的都是无方向的边缘光),但是在一些2D动画里是有的--比如一款叫轮舞 ...

  9. Unity 4 3 制作一个2D横版射击游戏 2

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 在上一篇 ...

最新文章

  1. python引用numpy出错_引用numpy出错详解及解决方法
  2. “编码 5 分钟,命名 2 小时”,这道题究竟怎么解? | 问题征集
  3. 把文件每行的tab键分隔符改成逗号分隔符
  4. 关于silverlight5 打印功能收集
  5. img标签使用默认图片的一种方式
  6. mysql root 提权_mysql以ROOT权限提权方法
  7. python采用哪种编码方式_Python编码格式的指定方式
  8. 53 SD配置-定价配置-定义条件排斥组
  9. CSS cursor 属性-鼠标形状
  10. 【深入浅出通信原理-学习笔记】信号与频谱
  11. java制作qq自动回复,qq自动回复机器人-qq自动回复机器人 v1.6 电脑版
  12. 直播小程序服务器配置,小程序直播obs推流设备直播设置教程
  13. 估计、偏差 、方差
  14. mysql 误删表 恢复数据_MySQL误删数据或者误清空表恢复
  15. 北大人工智能前沿讲座--第二讲 嵌入式人工智能
  16. git工具的使用 、gitlab 服务器的搭建、Jenkins服务的搭建
  17. 国外开源电子商务平台
  18. H.264 H.265 数据量及存储量计算
  19. result returns more than one elements 解决办法
  20. Metal 框架之从可绘制纹理中读取像素数据

热门文章

  1. Java提高—对象克隆(复制)/对象属性拷贝
  2. server2008r2/2012R2遠程桌面-企业协议号
  3. 兄弟,敬你是条汉子,请干了广告们~
  4. 测试如何转为开发人员
  5. 如何判断京东达人文章是否下线
  6. Error与RuntimeException
  7. linux 压缩以及归档
  8. tinyhttpd源码详解
  9. 数据仓库专题(6)-数据仓库、主题域、主题概念与定义
  10. Exchange 2010之接受域