首先,解释清楚什么是环境光剔除(Ambient Occlusion short for AO)
AO技术可以让虚拟环境更加真实而有效地模拟出现实世界的光照,其基本思想是,从一个点的每个方向散射的环境光,被场景远处的地方接收到的数量,估算出接收到的光数量,就代表该点处的AO值。所以,有了这个想法的基础,通过数学上或经验的论证,可以给定点可见的半球表面积来计算:
给场景添加环境光剔除,可以极大的提高视觉保真度,正因如此,如何计算环境光剔除的方法已经引起了很多的思考,但就目前的状况来看,有两种方式实现:
- 静态的算法:提前计算好场景中几何体的环境光剔除
- 动态的算法:每帧计算场景中几何体的环境光剔除
著名的算法是:SSAO(Screen—Space Ambient Occlusion),其基本思路是,通过摄像机的深度缓冲(z缓冲区),来近似计算出每个像素的可访问性,这样就可以用来遮蔽屏幕上的所有像素了。SSAO技术的亮点在于,很容易集成到现有的渲染管线中(尤其是延迟着色—后期效果处理),但是不足之处在于,深度缓冲区并不能真实地表示场景中几何体,所以有时候会出现许多奇怪的伪像。
体素场景中的环境光剔除技术
讲到这里,对于体素类的游戏,要是先环境光剔除,有一个非常快的算法。其基本思想是,一个体素的每个顶点处的环境光剔除值仅跟其相邻的体素相关。基于此,一个顶点可能最多有4种的剔除类型:
由此,我们可以推导出一个计算公式,边1和边2的值(0和1)取决于其侧面的体素的存在与否,Corner的值取决于其角部的体素存在与否。公式如下:
具体来讲,将一个cube划分成六个面,每个面上四个点,每个点搜索它的左、右、上、对角四个方向上有没有cube。
/// <summary>
/// 24 个点,每个点下有四个方向:上、左、右、对角
/// </summary>
public static List<List<Vector3>> Pads = new List<List<Vector3>>
{
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(1,0,1),
new Vector3(0,-1,1),
new Vector3(1,-1,1),
},
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(-1,0,1),
new Vector3(0,-1,1),
new Vector3(-1,-1,1),
},
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(1,0,1),
new Vector3(0,1,1),
new Vector3(1,1,1),
},
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(-1,0,1),
new Vector3(0,1,1),
new Vector3(-1,1,1),
},
new List<Vector3>()
{
new Vector3(0,1,0),
new Vector3(0,1,-1),
new Vector3(1,1,0),
new Vector3(1,1,-1),
},
new List<Vector3>()
{
new Vector3(0,1,0),
new Vector3(-1,1,0),
new Vector3(0,1,-1),
new Vector3(-1,1,-1),
},
new List<Vector3>()
{
new Vector3(0,0,-1),
new Vector3(1,0,-1),
new Vector3(0,-1,-1),
new Vector3(1,-1,-1),
},
new List<Vector3>()
{
new Vector3(0,0,-1),
new Vector3(-1,0,-1),
new Vector3(0,-1,-1),
new Vector3(-1,-1,-1),
},
new List<Vector3>()
{
new Vector3(0,1,0),
new Vector3(1,1,0),
new Vector3(0,1,1),
new Vector3(1,1,1),
},
new List<Vector3>()
{
new Vector3(0,1,0),
new Vector3(-1,1,0),
new Vector3(0,1,1),
new Vector3(-1,1,1),
},
new List<Vector3>()
{
new Vector3(0,0,-1),
new Vector3(1,0,-1),
new Vector3(0,1,-1),
new Vector3(1,1,-1),
},
new List<Vector3>()
{
new Vector3(0,0,-1),
new Vector3(-1,0,-1),
new Vector3(0,1,-1),
new Vector3(-1,1,-1),
},
new List<Vector3>()
{
new Vector3(0,-1,0),
new Vector3(1,-1,0),
new Vector3(0,-1,-1),
new Vector3(1,-1,-1),
},
new List<Vector3>()
{
new Vector3(0,-1,0),
new Vector3(1,-1,0),
new Vector3(0,-1,1),
new Vector3(1,-1,1),
},
new List<Vector3>()
{
new Vector3(0,-1,0),
new Vector3(-1,-1,0),
new Vector3(0,-1,1),
new Vector3(-1,-1,1),
},
new List<Vector3>()
{
new Vector3(0,-1,0),
new Vector3(-1,-1,0),
new Vector3(0,-1,-1),
new Vector3(-1,-1,-1),
},
new List<Vector3>()
{
new Vector3(-1,0,0),
new Vector3(-1, -1,0),
new Vector3(-1,0,1),
new Vector3(-1,-1,1),
},
new List<Vector3>()
{
new Vector3(-1,0,0),
new Vector3(-1,1,0),
new Vector3(-1,0,1),
new Vector3(-1,1,1),
},
new List<Vector3>()
{
new Vector3(-1,0,0),
new Vector3(-1,1,0),
new Vector3(-1,0,-1),
new Vector3(-1,1,-1),
},
new List<Vector3>()
{
new Vector3(-1,0,0),
new Vector3(-1,-1,0),
new Vector3(-1,0,-1),
new Vector3(-1,-1,-1),
},
new List<Vector3>()
{
new Vector3(1,0,0),
new Vector3(1,-1,0),
new Vector3(1,0,-1),
new Vector3(1,-1,-1),
},
new List<Vector3>()
{
new Vector3(1,0,0),
new Vector3(1,1,0),
new Vector3(1,0,-1),
new Vector3(1,1,-1),
},
new List<Vector3>()
{
new Vector3(1,0,0),
new Vector3(1,1,0),
new Vector3(1,0,1),
new Vector3(1,1,1),
},
new List<Vector3>()
{
new Vector3(1,0,0),
new Vector3(1,0,1),
new Vector3(1,-1,0),
new Vector3(1,-1,1),
},
};
构造完一个 cube 上 24 个顶点处的4个方向(这24个点的顺序必须和方块 mesh 中的顶点一一对应),接下来就判断每个点的每个方向上时候有没有方块存在:
private static int isOccupied(Vector3 pos)
{
int x = (int)pos.x;
int y = (int)pos.y;
int z = (int)pos.z;
if (x < 0 || y < 0 || z < 0)
{
return 0;
}
if (// constrain condition, x y z can not beyond ths largest shape)
{
return // your condition, if cube exists at position (x,y,z) 1,else 0;
}
else
{
return 0;
}
}
准备完成之后,需要计算,其主要逻辑如下:
/// <summary>
/// 计算 pos 处的 cube 在全体 cubes 中的 AO 值,保存在每个顶点颜色的 alpha 值中
/// </summary>
/// <param name="pos">该 cube 的位置信息</param>
/// <param name="color">该 cube 的颜色</param>
/// <returns></returns>
public static Color[] GetAOVetexColorsByPosAndColor(Vector3 pos, Color color)
{
Color[] colors = new Color[24];
for (int i = 0; i < 24; i++)
{
List<Vector3> offset = Pads[i];
int u = isOccupied(pos + offset[0]);
colors[i] = new Color(color.r, color.g, color.b);
if (u == 1)
{
colors[i].a = 1;
continue;
}
int r = isOccupied(pos + offset[1]);
int l = isOccupied(pos + offset[2]);
int c = isOccupied(pos + offset[3]);
if (l == 1 && r == 1)
{
colors[i].a = 0.6f;
continue;
}
else
{
int type = 3 - (r + l + c);
switch (type)
{
case 0:
colors[i].a = 0.7f;
break;
case 1:
colors[i].a = 0.8f;
break;
case 2:
colors[i].a = 0.85f;
break;
case 3:
colors[i].a = 1f;
break;
default:
break;
}
}
}
return colors;
}
调用如下代码实现:
public void UpdateAOColor()
{
this.vertexAOColors = VertexAOUtils.GetAOVetexColorsByPosAndColor(this.point.pos, this.color);
this.mesh.colors = this.vertexAOColors;
}
以上的逻辑,最主要的是那24个点的构造,每个点有四个方向:up、left、right、corner;这四个方向决定了这个点出的AO值类型,得到类型之后,我们定义一个规则:左右都有的类型alpha值为 0.6,0类型的 alpha 值0.75, 1 类型的为0.8, 2 类型的为 0.85,3 类型的为1(即不做处理)因此,将一个 mesh 的 AO 信息存储在 mesh 的 colors 中,由此可见,跟上面的分类对上号。
当然,计算完颜色信息之后,还需要在 shader 中使用顶点的颜色信息信息,计算一些效果,关于这部分先不表,下次我们详细说明如何在 shader 中使用 mesh 的颜色信息在屏幕上正确渲染出 AO 效果。
参看:https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/
这位大神说的很明白,关于这个帖子的其他问题,我们后面一一进行拆解。下回见。