Cocoon DevLog——光照系统

作者:WeAthFolD
2018-01-10
31 28 3

前言

没想到距离上篇日志已经过了一个月……也是应该写第二篇开发日志了。感觉如果太久不写把东西攒在一起写的话,就会说的很杂乱,不如写的勤快一点,每篇日志专注于一个主题。

今天我想偏重技术一些,说一下上个月完成的最有趣的一块:Cocoon 的光照系统。

1. 概述

像素光照一直是我们心心念念想要实现的一个 Feature。毕竟,拥有一个简单的光照系统可以很轻松的为场景带来氛围和视觉上的提升,也会降低美术的重复工作量。而且,我们还希望这个光照系统是有深度的——对于游戏中不同的视觉层,一方面有着对应的视差滚动速度,一方面受到光照的影响也有所不同。点光源会优先照亮近的东西,而远的受到影响较少,就好像处于一个真的3d 空间一样。

做这件事,也是受到最近很多独立游戏的启发,例如 The Last Night 和我上半年作为实习参与了的 Eastward:

虽然我们暂时还不能做到这两个游戏那么酷炫,但我们也可以借鉴它们在像素风格上进一步作尝试的想法,营造出我们游戏独特的风格。

这是我们美术给出的 Cocoon 的一张氛围图。可以看到光照起了很重要的渲染气氛的作用。接下来,我就大概归纳一下我在构建这整套系统过程中的思路。

2. 技术实现

这是实现光照系统前的效果:

然后看看最终效果。这是两个测试关卡。它们用的是同一套素材,只是光照配置不同:

首先,我归纳了一些在我们游戏中会用到的基本需求:

  1. 基本的光源类型(点光源、环境光源、方向光源)
  2. 场景中有物件会对光源造成遮挡
  3. 光源要对深度有反应。例如点光源,对于深度较远的像素,其照亮程度较低

为了支撑后续所有内容的开发,首先需要尝试做一个渲染流程,来满足这些需求。


2.1 渲染流程

最基本的思路是使用 Deferred Rendering(延迟渲染),先将 Diffuse Map、Depth Map、Light Map 分别渲染,然后再用一个最终的上色 shader 将其输出到屏幕上。这些中间的 Map 都是一个 RenderTexture,这样的话我们就可以配置各个摄像机渲染到它们上。Unity 有一个很便利的方法 `RenderTexture.GetTemporary`,可以在每帧获取临时的 RT,正好适合用在这里。


2.2 场景物体的实现

对于场景中的物体,我们需要让它一方面输出颜色到 Diffuse Map,一方面输出深度到 Depth Map,这就需要 MRT (Multiple Render Target)出场了。我们通过修改 Unity 的 Sprites-Default Shader,在片元着色阶段输出到 Diffuse Map 和 Depth Map,就像这样:

struct FragOutput {
    fixed4 dest0 : SV_Target0; // dest0对应 diffuse map(颜色)
    fixed4 dest1 : SV_Target1; // dest1对应 depth map(深度)
};

FragOutput Frag(VertOutput IN)
{
    FragOutput o;
    fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
    c.rgb *= c.a;
    o.dest0 = c; // 输出颜色
    o.dest1 = float4(IN.depth, 0, 0, 1); // 输出顶点阶段计算的深度
    clip(c.a < 0.1 ? -1 : 1);
    return o;
}


2.3 光源的实现

对于光源,基本思路是渲染到 Light Map,但根据具体光源类型其处理略有不同。目前实现完善的有两种光源:Ambient Light(环境光)和 Point Light(点光)。

2.3.1 环境光

环境光顾名思义,用来处理整体环境的光照。我们规定了一个 Ambient Area(环境区域),并且对于每个环境区域可以给不同的环境光配置,这让我们可以做出室内和室外的光照氛围区别。同时,我们还希望环境光的亮度可以对深度有所反应,因此环境光的配置并不是一个单一的颜色,而是环境光对深度的一个渐变。同时,在环境区域中还添加了雾效的支持。


这样灵活的配置可以让我们很轻松的对不同深度的视觉层进行调色,以及模拟不同深度雾渐渐变浓的效果。

环境光的渲染过程,实际上就是从深度贴图中采样出当前深度,并计算亮度渐变贴图(即上图的 Light Preview)中对应深度位置的光照颜色。我们将亮度渐变贴图生成为一张1024x1的贴图,并将深度转化为 uv 坐标对其进行采样,输出到光照贴图即可。片元着色器代码如下:

float4 frag(v2f IN) : COLOR
{
    float depth = tex2D(_DepthTex, IN.uv).r; // 采样深度
    depth = (depth + 100) / 2100; // 映射到(0, 1)
    fixed4 ambientColor = tex2D(_GradientTex, float2(depth, 0)); // 计算 ambient 颜色
    ambientColor.rgb *= ambientColor.a;
    ambientColor.a = 1;
    return ambientColor;
}
2.3.2 点光

点光是最常见的光源。它的特点是光亮度在中心最强,沿远中心的方向逐渐衰减,形成一个圆形(球型)的照亮区域。点光的衰减遵循平方反比定律。和 Ambient Light 一样,对于每个像素,需要采样深度贴图获得其深度。不同的是我们还需要从该位置的 uv 反算到世界坐标,计算出片元与光中心的世界坐标距离,然后计算光照衰减。

除了衰减之外,点光还需要考虑环境遮挡的问题。这里,我通过 Raytrace 并生成一个对应的 Mesh 来确定点光源遮挡后可以覆盖的区域。具体的思路可以参见参考资料[1]

如果我们直接将生成的 mesh 绘制到 light map 上,我们的点光源将是硬边缘的。为了获得软边缘,我们进行一个两趟的渲染:首先将 mesh 绘制到一个临时的 RenderTexture 上,再进行一次高斯模糊(对于每个片元,模糊的距离和其于光源中央的距离成正比,这样就可以模拟光扩散的效果),最后将结果拷贝到 light map 上。

2.3.3 雾效

雾效的实现方法和环境光几乎一样。我们从深度贴图采样当前像素的深度,得到当前深度对应的颜色,然后输出到雾贴图中,然后在最后的着色阶段进行混合。


2.4 杂项

2.4.1 DoF

有了颜色贴图和深度贴图,我们可以很轻松的实现自定义的 Depth of Field(景深)效果。详细的可以看参考资料。

2.4.2 透明物体的渲染

透明物体我目前还没有解决的一个问题。由于我们每次只有一张深度图,所以我们是不可能将透明物体的深度信息包含进去的。在网上查资料以后,折衷方案基本有两个方向:

  1. 重新跑一遍渲染流程,渲染所有透明物体。
  2. 对透明物体使用前向渲染(Forward Rendering)路径。

两者都需要一定的工作量,所以我暂时决定把透明物体的处理放置,晚点再来解决。

3. 总结

下面这张流程图归纳了整体的渲染流程:

总之,我实现了一套基本的2D 像素游戏中可以使用的光照系统,它会考虑到场景中的深度,做出一些类似3D 的感觉。当然,它还有很多需要改进的地方,比如透明物体的渲染就还没有解决。在开发过程中,还会慢慢迭代完善。还有一些可以更加提升场景氛围的技巧,例如法线贴图等,在未来也可能会尝试使用。

参考资料

[1] SIGHT & LIGHT - how to create 2D visibility/shadow effects for your game

[2] efficiently simulating the Bokeh of Polygonal Apertures in a Post-Process Depth of Field shader

[3] Deferred Shading - learnopengl.com


近期点赞的会员

 分享这篇文章

WeAthFolD 

Game Programmer/Amateur Designer 

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. Relinke 2018-01-10

    羡慕那些光照做得好的 2D 游戏!

  2. 爆裂花生 2018-01-10

    有点厉害

  3. 也是要攻克的一个难题,,,加分项!

您需要登录或者注册后才能发表评论

登录/注册