2D 游戏光照分析

Table Of Contents

  1. 1. Ⅰ、基础知识简介
  2. 2. Ⅱ、渲染管线概述
  3. 3. Ⅲ、基于 SDF 的蒙版光照计算
  4. 4. Ⅳ、辉光处理
  5. 5. Ⅵ、混合

本文将会介绍一个我正在制作的项目 PaperCraft 中有关游戏 2D 光照的实现. 提供一个低成本的基于有符号距离场可用于实时 2D 光照渲染的可行思路. 本文将会提供一种可能的 SKSLSkia Shader Language代码来实现该方法.

基础知识简介

SDFSigned distance function有符号距离函数.是一个多元函数具体几元取决于研究对象所处的维度本文记作 .其返回一个值该值为点 到目标几何体的最短距离.不同的几何体有不同的 SDF 函数.例如圆的 SDF 为 .其中 为圆的半径.若 则说明点在圆内 则说明点在圆上 则说明点在园外.本文不提供任何几何体的 SDF 函数推导过程读者若有兴趣可自行查找相关资料.

法线贴图Normal Texture是一种凹凸贴图Bump Map.可以表示物体的表面细节如凹凸划痕.一个常见的法线贴图如下所示

事实上法线贴图就是将法向量 映射到了 RGB 空间中由于 于是就存在如下映射关系

因此可以将物体表面粗糙的法向量 全部映射到 RGB 空间中并储存为图片在渲染中我们可以用这一低成本的方法实现物体表面粗糙平面的渲染得到更好的细节事实上如果放大文章封面

你可以清晰地看到法线贴图的效果红石块和石块表面看起来来凹凸不平极具立体感

关于法线贴图和 SDF 的详细描述请分别参考 LearnOpenGL - 法线贴图知乎 Jacks0n - Rendering (Signed) Distance Function.

渲染管线概述

下图简单直观地展示了本方法的渲染管线

这里再给出这几个步骤的简述

  1. 基础场景渲染

    该流程使用原始贴图渲染出未经光照的原始地图.

  2. 法线场景渲染

    该流程使用法线贴图渲染出未经光照的原始地图.

  3. 光照贴图渲染

    该流程将会基于 SDF 函数计算出原始光照蒙版贴图.

  4. 辉光处理

    该流程基于高斯模糊对阶段三生成的光线蒙版贴图进行辉光处理.

  5. 混合

    该步骤将会将步骤一四得到的结果混合并计算出最终结果.

下文将会详细讲述步骤三至五.

基于 SDF 的蒙版光照计算

根据现实生活中的经验可以发现一个发光的物体照亮的区域形成一个圆圈.且先保持强度不变再衰减具体示意图如下

那么便可以通过 SDF 模拟该过程

假设某点处的光源强度 其中 为最亮 为最暗

我们假设有一个光源 颜色为 其光强为 SDF 函数为 衰减半径为小 . 为一满足在 方向上使得 的一点 处的辐射度 可以为

注意1 实际上是由 推到得到的.显然因此我们可以考虑使用一个非线性插值 函数来对平滑光照衰减.特别地我们需要确保 . 此处采用 作为衰减平滑函数

根据在 1986 年由 James T.Kajiya 提出的渲染方程

这里我们无需过多关注这个公式本身如果你想了解这个公式可以参考 James T.Kajiya 的论文 The Rendering Equation在 2D 平面中我们可以认为所有光线都将射入摄像机且任意点上收到的光照等于所有光线辐射度的总和可以将球面积分改写成很简洁的形式

其中 表示 方向上受到的光亮然而由于我们的光源是确定的在不考虑阴影渲染的情况下我们可以认为有

之所以是在不考虑阴影的情况下才有上述等式成立是因为当有一个物体遮挡时光线就并不总是可以完全照射到指定点处这时就要考虑可能的辐射度衰减

因此假设有 个光源 点处的辐射度 则为

此处给出一个可能的 SKSL 着色器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// The light type of the shader
// If LightType < 10. = Ellipse
// Else = Rectangle
uniform float LightType;
// The X position of the light
uniform float2 Center;
// The radius of the light source object
uniform float Radius;
// The range of the light source
uniform float ValidRadius;
// The light brighteness level of the light source
uniform float Intensity;
// The color of the light source
uniform float3 Color;

// The texture shader for background texture
uniform shader BackgroundTexture;

// The SDF of ellipse
float EllipseSDF(in vec2 R, in float Radius) {
return length(R) - Radius;
}
// The SDF of rectangle
float RectangleSDF(in vec2 R, in float Radius) {
vec2 Box = vec2(Radius, Radius);
vec2 Delta = abs(R) - Box;
return length(max(Delta, vec2(0.))) + min(max(Delta.x, Delta.y), 0.0);
}
// The phi interpolation function
float Phi(in float Value) {
return 1.0 - pow(Value, 0.3);
}

// Calculate the light brightness contribution from the SDF
vec3 SDFContribution(in float Distance, in float Intensity, in float ValidRadius, in vec3 Color) {
vec3 col = vec3(0., 0., 0.);
if (Distance <= 0.0) {
float D = Intensity + 1.;
col = Color * D;
}
else if (Distance <= ValidRadius) {
float F = (Distance / ValidRadius);
float D = Phi(F) * (Intensity + 1.);
col = vec3(Color[0] * D, Color[1] * D, Color[2] * D);
}

return col;
}
// Sample the coord light from a ellipse object
vec3 SampleEllipse(in vec2 Center,
in vec3 Color,
in float Radius,
in float ValidRadius,
in float Intensity,
in vec2 Coord) {
vec2 R = Center - Coord;

float sdf = EllipseSDF(R, Radius);
return SDFContribution(sdf, Intensity, ValidRadius, Color);
}
// Sample the coord light from a rectangle object
vec3 SampleRectangle(in vec2 Center,
in vec3 Color,
in float Radius,
in float ValidRadius,
in float Intensity,
in vec2 Coord) {
vec2 R = Center - Coord;

float sdf = RectangleSDF(R, Radius);
return SDFContribution(sdf, Intensity, ValidRadius, Color);
}

// Fix the gamma value
vec3 GammaFixed(in vec3 R) {
return vec3(pow(R[0], 0.9), pow(R[1], 0.9), pow(R[2], 0.9));
}

vec4 main(in vec2 fragCoord) {
vec4 fragColor = BackgroundTexture.eval(fragCoord);
if (LightType < 10.) fragColor += vec4(SampleEllipse(Center, Color, Radius, ValidRadius, Intensity, fragCoord), 1.);
else fragColor += vec4(SampleRectangle(Center, Color, Radius, ValidRadius, Intensity, fragCoord), 1.);

return fragColor;
}

辉光处理

尽管在章节 Ⅲ 中已经引入了光照平滑函数 来尽可能地让光照平滑自然但如下图所示如果要让光照足够的自然最好还是模拟现实中发光物体的辉光效果

为了实现辉光效果一个可行的思路就是通过高斯模糊来对原来的蒙蔽光照贴图进行处理.

高斯模糊使用到了二阶正态分布通过在目标点指定半径大小内计算每个点相当对于该点的权重 来实现模糊效果

关于高斯模糊的详细描述请参考维基百科.

一个可能的辉光 SKSL 实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
uniform shader BackgroundTexture;

float pow2(in float x) {
return ((x) * (x));
}

const float pi = 3.14159265358979323846;
const float samples = 30;
const float sigma = samples * 0.25;

float gaussian(float2 i) {
return 1.0 / (2.0 * pi * pow2(sigma)) * exp(-((pow2(i.x) + pow2(i.y)) / (2.0 * pow2(sigma))));
}

vec4 main(float2 coord) {
vec4 color = vec4(0.0);
float accum = 0.0;

for (float x = -samples / 2; x <= samples / 2; ++x) {
for (float y = -samples / 2; y <= samples / 2; ++y) {
float2 offset = float2(float(x), float(y));
float weight = gaussian(offset);
color += BackgroundTexture.eval(coord + offset) * weight;
accum += weight;
}
}

color /= accum;
return color;
}

混合

在最后阶段中我们需要将前几个步骤得到的结果混合起来完成光照渲染混合公式为

实际上就是把 个颜色的 rgb 数值相乘后组成新的颜色事实上这相当于 PhotoShop 中的 正片叠底效果