没想到距离上篇日志已经过了一个月……也是应该写第二篇开发日志了。感觉如果太久不写把东西攒在一起写的话,就会说的很杂乱,不如写的勤快一点,每篇日志专注于一个主题。
今天我想偏重技术一些,说一下上个月完成的最有趣的一块: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 透明物体的渲染
透明物体我目前还没有解决的一个问题。由于我们每次只有一张深度图,所以我们是不可能将透明物体的深度信息包含进去的。在网上查资料以后,折衷方案基本有两个方向:
重新跑一遍渲染流程,渲染所有透明物体
对透明物体使用前向渲染(Forward Rendering)路径
两者都需要一定的工作量,所以我暂时决定把透明物体的处理放置,晚点再来解决。
3 总结
下面这张流程图归纳了整体的渲染流程:
总之,我实现了一套基本的2D像素游戏中可以使用的光照系统,它会考虑到场景中的深度,做出一些类似3D的感觉。当然,它还有很多需要改进的地方,比如透明物体的渲染就还没有解决。在开发过程中,还会慢慢迭代完善。还有一些可以更加提升场景氛围的技巧,例如法线贴图等,在未来也可能会尝试使用。
4 参考资料
[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:ok了
@WeAthFolD:ok了