在以往的光照模型中,我们没有考虑间接光
以往的环境光照模型为:
A=La∗ma
La
表示物体接受到的间接光的总量
ma
表示物体的材质,指定了射入环境光的反射和吸收的数量
并且认为光线从各个方向均匀的入射到物体表面
但这种计算模型完全不真实
环境光遮蔽的思想是:物体表面某点
p
的间接光数目是与以
p
为球心的半球对入射光线的遮蔽程度成比例的
(图片来源:Introduction to 3D Game Programming With DirectX 11)
左边的图表示入射光线全部通过半球射入到
p
点上
右边的图表明一部分光线被遮挡未能入射到
p
点上,即
p
点被遮蔽了
在现实中我们知道这样一个现象,墙角处看起来总是比旁边要暗一下,那是因为墙角收到墙面的遮蔽,因此为了模拟这样一个情况,我们需要计算出物体顶点的遮蔽程度,或者说遮蔽值
那么我们怎样去计算每个顶点的遮蔽程度呢?
假设 p 为物体表面上一点,以 p 点为球心构造一个半球,半球的方向为该点法线的方向,无需考虑负半球,其无法对 p 点造成遮蔽。
构造随机的入射光线是比较困难的,我们可以逆向处理,如:由 p 点发出N条光线,若由H条方向被遮蔽了,则我们认为该点的遮蔽程度为 HN ,我们也将接受到的光线比例称为ambient access.因此ambient access = 1.0 - HN
AO计算公式:
A(p,n)=1π∫ΩV(w,p)max(w⋅n,0)dw
p
表示表面上一点
Ω
表示以
p
点为球心的正半球,即法线指向的方向
n
表示
p
点的法线
w
表示半球体
Ω
从
p
点发出光线的方向向量
V
表示可视函数
因此Ambient Occlusion的计算是通过对围绕着以 p 点为球心的半球 Ω 上的局部AO值进行积分得到,由此可见, V(w,p) 是一个二值函数,因为对于任意一条光线,要么对点 p 产生遮蔽要么不产生,因此取值只有0 , 1 。但实现起来这显然是不可能也不必要的。我们只需进行一部分采样来进行模拟得到
假设通过点 p 沿着半球 Ω 发出的一条光线,与物体相交于点 q ,但是假如点 q 离 p 点太远时,我们可以认为点 q 对 p 点并没有产生遮蔽,因此我们可以设置一个阀值(threshold value),当距离大于阀值时,认为不产生遮蔽作用
我们可以在进入着色器之前为每个顶点预先计算出其遮蔽值~~
伪代码如下:
std::vector<UINT> VertexSharedCount(VertexCount); // 用于保存顶点被共享的次数,最终用于计算顶点的平均Ambient Access
for(int i = 0; i < TriangleCount; i++)
{
int i0 = indices[3 * i + 0];
int i1 = indices[3 * i + 1];
int i2 = indices[3 * i + 2];
Vertex v0 = vertices[i0];
Vertex v1 = vertices[i1];
Vertex i2 = vertices[i2];
Edge e1 = v1 - v0;
Edge e2 = v2 - v0;
normal = e1 * e2;
center = (v0 + v1 + v2) / 3;
for(int j = 0; j < SampleLightCount; j++)
{
RayDirection = GenerateRandomVector();
if(!octree.RayIntersect(center , normal))
{
UnOcclusionNum++;
}
}
float access = 1.0f - (UnOcclusionNum / SampleLightCount);
vertices[i0].access += access;
vertices[i1].access += access;
vertices[i2].access += access;
VertexSharedCount[i0]++;
VertexSharedCount[i1]++;
VertexSharedCount[i2]++;
}
for(int i = 0; i < VertexCount; i++)
{
vertices[i].access /= VertexSharedCount[i];
}