GraphicsLab Project之光照贴图烘焙(二) - Radiosity Normal Mapping

作者:i_dovelemon
日期:2018-05-30
来源:CSDN
主题:Radiosity Normal Mapping, Tangent Space


GraphicsLab Project之光照贴图烘焙(二) - Radiosity Normal Mapping_第1张图片


引言



前面一篇文章,我们讲述了如何通过Radiosity算法,实现旧式的光照贴图烘焙。正如前面我们讲述的,最终我们希望实现的是Source引擎中带有凹凸效果的烘焙(题图对比)。

我们知道,Normal Mapping是一种增添表面细节的方法。旧式的光照贴图丢失了这种细节。原因是普通的Normal Mapping,需要光照方向,但是对于烘焙的贴图来说没有固定的光照方向,所以丢失了凹凸的信息。为此,Valve在Half Life 2中提出了Radiosity Normal Mapping的改进方法,以此来增加烘焙贴图的表面细节。


Radiosity Normal Mapping



在传统的光照贴图烘焙中,我们保存的是每一个patch的法线所朝向的半个立方体范围内的入射光照的结果。Valve将此种方法进行了扩展,他们实验出了一组正交基,如图所示:


GraphicsLab Project之光照贴图烘焙(二) - Radiosity Normal Mapping_第2张图片


与旧式不同的是,需要计算以这三个切空间正交基为法线的patch的颜色。也就是说,相对于以前只保存一个颜色的光照贴图,现在需要保存这三个法线方向的光照颜色。

当我们获取了这样的光照贴图之后,在实际使用的时候,只要简单的获取对应的法线贴图中的法线数据与三个基向量做点积,并以此为权重进行光照贴图的混合,如图所示:

GraphicsLab Project之光照贴图烘焙(二) - Radiosity Normal Mapping_第3张图片


整个过程并没有多复杂,通过这样的方式就能够近似的模拟带有Normal Maping的效果了。


实现



实现中也没有什么特别的地方,只要注意Valve给出的向量实际是patch法线所在的切空间,所以我们在进行光照贴图计算的时候,需要将这三个正交基变换到世界坐标来,如下代码所示:

void PrepareLightPatch() {
    memset(m_Patch, 0, sizeof(m_Patch));

    // Calculate uv for every patch
    for (int32_t h = 0; h < kLightMapHeight; h++) {
        for (int32_t w = 0; w < kLightMapWidth; w++) {
            m_Patch[h][w].uv = math::Vector((w + 0.5f) * 1.0f / kLightMapWidth, (kLightMapHeight - h - 0.5f) * 1.0f / kLightMapHeight, 0.0f);
        }
    }

    // Collect all faces
    struct Face {
        struct {
            math::Vector uv;
            math::Vector pos;
            math::Vector normal;
            math::Vector tangent;
            math::Vector binormal;
        } vertex[3];
    };
    std::vector faces;
    faces.clear();

    scene::ModelEffectParam effectParam;
    scene::ModelMaterialParam materialParam;
    float* vertexBuf = NULL;
    float* texBuf = NULL;
    float* normalBuf = NULL;
    float* tangentBuf = NULL;
    float* binormalBuf = NULL;
    int32_t faceNum = scene::ModelFile::ExtractModelData(kSceneModelFile, effectParam, materialParam, &vertexBuf, &texBuf, &normalBuf, &tangentBuf, &binormalBuf);

    int32_t vertexOffset = 0, uvOffset = 0, normalOffset = 0, tangentOffset = 0, binormalOffset = 0;
    for (int32_t i = 0; i < faceNum; i++) {
        Face face;

        for (int32_t j = 0; j < 3; j++) {
            face.vertex[j].uv.x = texBuf[uvOffset++];
            face.vertex[j].uv.y = texBuf[uvOffset++];
            face.vertex[j].uv.z = 0.0f;
            face.vertex[j].uv.w = 0.0f;
            face.vertex[j].pos.x = vertexBuf[vertexOffset++];
            face.vertex[j].pos.y = vertexBuf[vertexOffset++];
            face.vertex[j].pos.z = vertexBuf[vertexOffset++];
            face.vertex[j].pos.w = 1.0f;
            face.vertex[j].normal.x = normalBuf[normalOffset++];
            face.vertex[j].normal.y = normalBuf[normalOffset++];
            face.vertex[j].normal.z = normalBuf[normalOffset++];
            face.vertex[j].normal.w = 0.0f;
            face.vertex[j].tangent.x = tangentBuf[tangentOffset++];
            face.vertex[j].tangent.y = tangentBuf[tangentOffset++];
            face.vertex[j].tangent.z = tangentBuf[tangentOffset++];
            face.vertex[j].tangent.w = 0.0f;
            face.vertex[j].binormal.x = binormalBuf[binormalOffset++];
            face.vertex[j].binormal.y = binormalBuf[binormalOffset++];
            face.vertex[j].binormal.z = binormalBuf[binormalOffset++];
            face.vertex[j].binormal.w = 0.0f;
        }

        faces.push_back(face);
    }

    scene::ModelFile::RelaseBuf(&vertexBuf, &texBuf, &normalBuf, &tangentBuf, &binormalBuf);

    // Calculate data for every patch
    for (int32_t h = 0; h < kLightMapHeight; h++) {
        for (int32_t w = 0; w < kLightMapWidth; w++) {
            math::Vector uv = m_Patch[h][w].uv;

            bool found = false;
            for (int32_t i = 0; i < faceNum; i++) {
                // Using triangle's barycentric coordinate system to calculate world position of light patch
                // https://en.wikipedia.org/wiki/Barycentric_coordinate_system
                float x = uv.x;
                float y = uv.y;
                float x1 = faces[i].vertex[0].uv.x;
                float x2 = faces[i].vertex[1].uv.x;
                float x3 = faces[i].vertex[2].uv.x;
                float y1 = faces[i].vertex[0].uv.y;
                float y2 = faces[i].vertex[1].uv.y;
                float y3 = faces[i].vertex[2].uv.y;

                float lambda0 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3));
                if (lambda0 < 0.0f || lambda0 > 1.0f) continue;

                float lambda1 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3));
                if (lambda1 < 0.0f || lambda1 > 1.0f) continue;

                float lambda2 = 1.0f - lambda0 - lambda1;
                if (lambda2 < 0.0f || lambda2 > 1.0f) continue;

                m_Patch[h][w].pos = faces[i].vertex[0].pos * lambda0 + faces[i].vertex[1].pos * lambda1 + faces[i].vertex[2].pos * lambda2;
                m_Patch[h][w].valid = true;

                //math::Vector tangent = faces[i].vertex[0].tangent * lambda0 + faces[i].vertex[1].tangent * lambda1 + faces[i].vertex[2].tangent * lambda2;
                //math::Vector binormal = faces[i].vertex[0].binormal * lambda0 + faces[i].vertex[1].binormal * lambda1 + faces[i].vertex[2].binormal * lambda2;
                //math::Vector normal = faces[i].vertex[0].normal * lambda0 + faces[i].vertex[1].normal * lambda1 + faces[i].vertex[2].normal * lambda2;
                //tangent.Normalize();
                //binormal.Normalize();
                //normal.Normalize();
                math::Vector tangent = faces[i].vertex[0].tangent;
                math::Vector binormal = faces[i].vertex[0].binormal;
                math::Vector normal = math::Cross(tangent, binormal);

                math::Matrix tbn;
                tbn.MakeIdentityMatrix();
                tbn.m_Matrix.m[0][0] = tangent.x;
                tbn.m_Matrix.m[1][0] = tangent.y;
                tbn.m_Matrix.m[2][0] = tangent.z;
                tbn.m_Matrix.m[0][1] = binormal.x;
                tbn.m_Matrix.m[1][1] = binormal.y;
                tbn.m_Matrix.m[2][1] = binormal.z;
                tbn.m_Matrix.m[0][2] = normal.x;
                tbn.m_Matrix.m[1][2] = normal.y;
                tbn.m_Matrix.m[2][2] = normal.z;

                m_Patch[h][w].bais[0] = tbn * math::Vector(sqrt(2.0f / 3.0f), 0.0f, sqrt(1.0f / 3.0f), 0.0f);
                m_Patch[h][w].bais[1] = tbn * math::Vector(-sqrt(1.0f / 6.0f), sqrt(1.0f / 2.0f), sqrt(1.0f / 3.0f), 0.0f);
                m_Patch[h][w].bais[2] = tbn * math::Vector(-sqrt(1.0f / 6.0f), -sqrt(1.0f / 2.0f), sqrt(1.0f / 3.0f), 0.0f);
                m_Patch[h][w].bais[0].Normalize();
                m_Patch[h][w].bais[1].Normalize();
                m_Patch[h][w].bais[2].Normalize();

                m_ValidPatch.push_back(&m_Patch[h][w]);

                found = true;
                break;
            }

            if (found == false) {
                m_Patch[h][w].valid = false;
            }
        }
    }
}


相比于以前,我们的模型需要提供顶点的法线以及切向量等信息。由于我的模型绘制的原点处,所以并没有考虑world matrix的影响,大家自己做的时候需要注意这里。

除此之外,就是在实际绘制的时候,需要计算新的光照贴图的效果,如下代码实现了最终的Radiosity Normal Mapping效果:

vec3 calc_radiosity_normal_map_color(vec2 uv) {
    // Calculate normal
    vec3 normal = texture(glb_NormalMap, uv).xyz;
    normal -= vec3(0.5, 0.5, 0.5);  
    normal *= 2.0;
    normalize(normal);

    // Calculate bais
    vec3 bais0 = vec3(sqrt(2.0 / 3.0), 0.0, sqrt(1.0 / 3.0));
    vec3 bais1 = vec3(-sqrt(1.0 / 6.0), sqrt(1.0 / 2.0), sqrt(1.0 / 3.0));
    vec3 bais2 = vec3(-sqrt(1.0 / 6.0), -sqrt(1.0 / 2.0), sqrt(1.0 / 3.0));
    normalize(bais0);
    normalize(bais1);
    normalize(bais2);

    vec3 lightMapColor0 = texture(glb_LightMap[0], uv).rgb;
    vec3 lightMapColor1 = texture(glb_LightMap[1], uv).rgb;
    vec3 lightMapColor2 = texture(glb_LightMap[2], uv).rgb;

    return lightMapColor0 * max(0.0, dot(bais0, normal)) + lightMapColor1 * max(0.0, dot(bais1, normal)) + lightMapColor2 * max(0.0, dot(bais2, normal));
}


需要注意,我这里为了简化问题,三个不同基向量所计算出来的颜色,我使用了三张不同的贴图来保存了。在实际使用过程中,你可能不会如此的浪费,那么就需要合理的将三种颜色进行pack压缩,降低显存开销。

下面是进行了这种计算之后,与以前旧式的光照贴图的效果对比:


GraphicsLab Project之光照贴图烘焙(二) - Radiosity Normal Mapping_第4张图片


GraphicsLab Project之光照贴图烘焙(二) - Radiosity Normal Mapping_第5张图片




总结



到此,我们的Radiosity Normal Mapping算法基本完成了。不过,有一个问题,我所有的模型中都没有曲面的情况。要知道,对于曲面,它的法线实际上是平滑过渡的,所以对于曲面,在计算patch的法线信息的时候,可能需要额外的操作来实现这种平滑过渡。后面有机会我会实现这种功能。

完整的代码已经上传至Github,大家可以自行浏览。


参考文献



[1] Half Life 2 / Valve Source Shading
[2] GraphicsLab Project之Normal Mapping

你可能感兴趣的:(3D引擎,DirectX,游戏开发,GPU,OpenGL,GraphicsLab,Project,Shader,图形试验室)