Direct X 12 – Ambient Occulusion – 环境光遮蔽

由于性能的限制,实时渲染的游戏一般不会将间接光纳入光照模型中(例如,由场景中其他物体反射的光线)。然而,我们在现实世界中看到的许多光都是间接的。在第八章中,我所介绍的环境光等式为:

颜色向量A(L)表示物体表面某一点所接受的所有间接光(环境光),m(d)表示表面的漫反射率。所以,环境光的光照模型只是让物体整体变量,而不至于在阴影中完全变黑变暗——也就是说,完全没有真实的物理计算。其原理基于,间接光在场景中不断散射与反弹,所以当物体接受到环境光时,光线的来自各个方向且强度相同。下图展示了,如果我们只使用环境光模型来绘制一个模型。

在本章节中,我们将学习一些常用的环境光遮蔽技术来改进我们的环境光光照模型。

目标:

1. 了解环境光遮蔽的原理,并学习如何通过射线检测来实现环境光遮蔽。

2. 学会如何在屏幕空间内实现实时的环境光遮蔽。

21.1 通过射线检测实现环境光遮蔽

环境光遮蔽的原理,物体表面某一点p所接受的环境光的数量与该点对于环境光的遮蔽率成比例,如下图所示:

上图a中,点p完全没有被遮蔽,而p点的半球上方的光线都到达了p点。上图b中,集合体的一部分遮住了p点并且阻止了p点半球上方的光线达到p点。

检测p点的遮盖率的一种方法是通过射线检测。我们随机在p点的半球上方射出射线,检测射线是否与网格模型相交(如下图所示)。如果我们射出N条射线,其中有h条射线与网格模型相交,那么该点的遮蔽率为:

只有当射线与网格模型的交点q到点p的距离小于我们的阈值d时,才能被用于计算遮蔽率;这是因为,如果交点q与点p的距离太远,那么其自然无法遮蔽点p

为了计算方便,在代码中我们将使用遮蔽率的倒数。这样我们就能知道该坐标点能够接受多少环境光线——其被成为accessibility(或者ambient-access),其等式如下所示:

以下代码为每一个三角形进行了射线检测,之后每一个共享该三角形的顶点将会取遮蔽率的平均值。我们将以三角形的中心作为原点随机射出射线。

void AmbientOcclusionApp::BuildVertexAmbientOcclusion(
    std::vector<Vertex::AmbientOcclusion>& vertices,
    const std::vector<UINT>& indices)
{
    UINT vcount = vertices.size();
    UINT tcount = indices.size();

    std::vector<XMFLOAT3> positions(vcount);
    for(UINT i = 0; i < vcount; ++i)
        positions[i] = vertices[i].Pos;

    Octree octree;
    octree.Build(positions, indices);

    // 对于每一个顶点,计算有多少个三角形会包含这个顶点
    std::vector<int> vertexSharedCount(vcount);

    // 为每一个三角形进行射线检测
    for(UINT i = 0; i < tcount; ++i)
    {
        UINT i0 = indices[i * 3];
        UINT i1 = indices[i * 3 + 1];
        UINT i2 = indices[i * 3 + 2];

        XMVECTOR v0 = XMLoadFloat3(&vertices[i0].Pos);
        XMVECTOR v1 = XMLoadFloat3(&vertices[i1].Pos);
        XMVECTOR v2 = XMLoadFloat3(&vertices[i2].Pos);

        XMVECTOR edge0 = v1 - v0;
        XMVECTOR edge1 = v2 - v0;
        XMVECTOR normal = XMVector3Normalize(
            XMVector3Cross(edge0, edge1));

        XMVECTOR centroid = (v0 + v1 + v2) / 3.f;

        // 沿着法线方向平移中心点,
        // 防止射线与三角形本身相交
        centroid += 0.001f * normal;

        const int NumSampleRays = 32;
        float numUnoccluded = 0;
        for(int j = 0; j < NumSampleRays; ++j)
        {
            XMVECTOR randomDir = MathHelper::RandHemisphereUnitVec3(normal);

            // 检测随机射线是否与场景中的网格模型相交
            //
            // 改进:我们不应该纳入距离过远的交点,
            if(!octree.RayOctreeIntersect(centroid, randomDir))
            {
                numUnoccluded++;
            }            
        }

        float ambientAccess = numUnoccluded / NumSampleRays;

        // 记录共享该三角形的顶点
        vertices[i0].AmbientAccess += ambientAccess;
        vertices[i1].AmbientAccess += ambientAccess;
        vertices[i2].AmbientAccess += ambientAccess;

        vertexSharedCount[i0]++;
        vertexSharedCount[i1]++;
        vertexSharedCount[i2]++;
    }

    // 求平均值
    for(UINT i = 0; i < vcount; ++i)
    {
        vertices[i].AmbientAccess /= vertexSharedCount[i];
    }
}

上图中,我们只使用环境光与环境光遮蔽渲染了骷髅模型——也就说场景内没有光源。我们可以注意到,模型的凹陷与缺口比起其他部分更暗;这是因为,当我们进行射线检测时,射线与模型相交,增加了遮蔽率。换句话说,骷髅的顶盖是白色的(未被遮蔽),这是因为在进行射线检测时,射线并未与任何几何体相交。

我们的demo使用了octree来加速射线与三角形的相交检测。对于一个拥有上千个三角形面的网格模型,如果我们为每一个三角形都运行随机的射线检测,处理过程将会极其缓慢。octree能够基于空间挑选出部分三角形,这样我们就能快速找出与射线相交的三角形;这也将大大减少射线/三角形相交检测的数量。octree是一个经典的空间数据结构体。

对于静态模型,我们一般会使用预计算的环境光遮蔽贴图;有许多工具(http://www.xnormal.net/)可以生成环境光遮蔽贴图——其存储了环境光遮蔽数据(例如,遮蔽率)。但是,对于运动的模型,这些预计算的环境光遮蔽贴图显然是不适用的。但是,单单为一个模型计算其环境光遮蔽的数据也会花费很长时间。因此,通过射线检测实时生成动态的环境光遮蔽数据也是不可行的。在下一个小节,我们将学习一个在游戏中常用的以屏幕空间作为基准计算环境光遮蔽的方法。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据