作者:i_dovelemon
日期:2018-05-30
来源:CSDN
主题:Radiosity Normal Mapping, Tangent Space
前面一篇文章,我们讲述了如何通过Radiosity算法,实现旧式的光照贴图烘焙。正如前面我们讲述的,最终我们希望实现的是Source引擎中带有凹凸效果的烘焙(题图对比)。
我们知道,Normal Mapping是一种增添表面细节的方法。旧式的光照贴图丢失了这种细节。原因是普通的Normal Mapping,需要光照方向,但是对于烘焙的贴图来说没有固定的光照方向,所以丢失了凹凸的信息。为此,Valve在Half Life 2中提出了Radiosity Normal Mapping的改进方法,以此来增加烘焙贴图的表面细节。
在传统的光照贴图烘焙中,我们保存的是每一个patch的法线所朝向的半个立方体范围内的入射光照的结果。Valve将此种方法进行了扩展,他们实验出了一组正交基,如图所示:
实现中也没有什么特别的地方,只要注意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压缩,降低显存开销。
下面是进行了这种计算之后,与以前旧式的光照贴图的效果对比:
到此,我们的Radiosity Normal Mapping算法基本完成了。不过,有一个问题,我所有的模型中都没有曲面的情况。要知道,对于曲面,它的法线实际上是平滑过渡的,所以对于曲面,在计算patch的法线信息的时候,可能需要额外的操作来实现这种平滑过渡。后面有机会我会实现这种功能。
完整的代码已经上传至Github,大家可以自行浏览。
[1] Half Life 2 / Valve Source Shading
[2] GraphicsLab Project之Normal Mapping