[OpenGL] Normal Mapping 法线映射 - 附我的实现

【更新】我的新博客:www.ryuzhihao.cc,当然这个csdn博客也会更新

              本文在新博客中的链接:点击打开链接

     最近准备填一下坑儿,整理一下之前写过的一些shader程序。程序都是在Qt下进行OpenGL ES的相关开发,不过对于Shader代码,不管是何处使用都没有什么太大差异。


填坑篇(一): Normal Mapping

一、我的实现结果:

     (下图的代码会在文章末尾给出)

     还是惯例贴一下我的程序结果:

     左侧是未使用Normal Map的情况。

     右侧是使用Normal Map的渲染结果,经过了切线空间的变换从而能得到真实的光照效果。

         

二、背景 Background

       法线贴图(Normal Mapping)多用在CG动画的渲染以及游戏画面的制作上。我们从具有高细节的模型通过映射烘焙出法线贴图,贴在低端模型的法线贴图通道上,使低细节模型也能拥有高细节模型的层次效果,同时可以大大降低渲染时需要的面数和计算内容,从而达到优化动画渲染和游戏渲染的效果。

       下面的图片是一个非常出名的例子,能够很好的说明Normal Mapping的强大作用:

        

       第1副图片是一个高细节的模型,使用了4百万个三角面片显示出了非常精细的细节。

       第2副图片中,我们将原先的4百万个三角面片简化到了500个三角面片,此时如果不使用Normal Mapping技术,模型会变得非常粗糙(低细节)。

       在第3副图像中,我们在简化后的低细节模型上,应用Normal Mapping技术,便可以实现近似于第1副图片中的精细程度。但是不要忘记,我们仅仅使用了500个三角面片就实现了4百万个面片的效果。这大大降低了渲染的时间。



三、法线图 Normal Map

       在Normal Mapping技术中,我们试图在一个平滑的表面(smooth flat)上,展现出粗糙表面的凹凸效果,在这里我们使用的是法线图。我们将物体表面的法线事先存储在一张法线图中,将法线图的(r,g,b)三个分量分别作为法线向量的(x,y,z),如下图所示。

       

       在法线图中,法线一般是近似垂直于表面XOZ的,因此y轴分量要大一些,这也是为什么大多数法线图的颜色偏蓝绿色的原因。此时,我们通过如下代码,就可以获取到法线图中存储的法线值:

    vec3 MapNormal = texture2D(normalMap, v_texcoord).xyz; // normal map 中的值
    vec3 normal = 2.0*MapNormal-vec3(1.0,1.0,1.0);   // 将法线从[0,1]变换到[-1,1];

       那么此时,我们将法线图读入顶点着色器,并按照法线图的rgb直接作为法线计算光照,就可以得到法线贴图的初步结果。如下图所示:

                                                   

       虽然已经能够看到凹凸不平的效果,但是不难发现这种做法存在的问题:每个面的光照都像是被光源直射一样。这违背了现实中的光照特性。其原因是:我们直将法线图的rgb直接取出作为物体表面的法线,这样正方体的每一个面的法线并非是与物体表面垂直的,而是所有六个面的法线的大致朝向都相同(即整体平行于y轴,个体有微小偏转),因此我们这样做的结果就导致了:所有的面的光照效果都一样,不管这个面是否被阳光直射。

四、切线空间 Tangent Space

      为了解决上面出现的问题,我们需要将法线图中的切线变换到物体坐标系(object local space)。这个时候就需要借助切线空间了。

      为了描述物体表面某位置的切线空间,我们可以用互相垂直的T、B、N三个向量表示:

             N(Normal):法向量

             T(Tangent):切向量

             B(BitTangent):副切向量

      对于我们常采用的网格模型,法向量N通常是已知的,但是切向量T和切向量B却不是那么轻易能够获得的,这里我们要借助一定的数学方法进行计算:

      下图是法线图纹理在变换前的TBN向量的示意图,可以发现T、B分别平行于纹理图的U、V轴。我们可以利用这一特性计算出两个切向量。

                                                              

      贴图后的纹理会被应用到三角面片上,我们对于模型中的一个三角面片进行说明(如下图)。

                                                               

      现在已知三角面片的三个顶点坐标:P1(x1, y1 ,z1 ),P2(x2, y2, z2),P3(x3, y3, z3),以及纹理坐标(u1,v1),(u2,v2),(u3,v3)。那么可以求出边E1和边E2的向量值,即:

                           E1 = (E1x, E1y, E1z) = (x1-x2, y1-y2, z1-z2)

                           E2 = (E2x, E2y, E2z) = (x3-x2, y3-y2, z3-z2)

       此时,我们假设△U1 = (U1-U2),△V1 = (v1-v2), △U3 = (U2-U2),△V3 = (v2-v2), 那么,我们可以利用简单的向量知识,建立如下等式:(粗体表示向量)

                           E1 = △U1*T+△V1*B      ①

                           E2 = △U2*T+△V2*B      ②

      在上式中,只有T、B未知,方程可解。所以我们只需要在外部利用上面的公式计算出T、B,即可将正副切线作为属性值传入着色器。

       下面给出在外部计算T、B的代码(使用VBO):

for (unsigned int i = 0 ; i < Indices.size() ; i += 3) {
    Vertex& v0 = Vertices[Indices[i]];
    Vertex& v1 = Vertices[Indices[i+1]];
    Vertex& v2 = Vertices[Indices[i+2]];

    Vector3f Edge1 = v1.m_pos - v0.m_pos;
    Vector3f Edge2 = v2.m_pos - v0.m_pos;

    float DeltaU1 = v1.m_tex.x - v0.m_tex.x;
    float DeltaV1 = v1.m_tex.y - v0.m_tex.y;
    float DeltaU2 = v2.m_tex.x - v0.m_tex.x;
    float DeltaV2 = v2.m_tex.y - v0.m_tex.y;

    float f = 1.0f / (DeltaU1 * DeltaV2 - DeltaU2 * DeltaV1);

    Vector3f Tangent, Bitangent;

    Tangent.x = f * (DeltaV2 * Edge1.x - DeltaV1 * Edge2.x);
    Tangent.y = f * (DeltaV2 * Edge1.y - DeltaV1 * Edge2.y);
    Tangent.z = f * (DeltaV2 * Edge1.z - DeltaV1 * Edge2.z);

    Bitangent.x = f * (-DeltaU2 * Edge1.x - DeltaU1 * Edge2.x);
    Bitangent.y = f * (-DeltaU2 * Edge1.y - DeltaU1 * Edge2.y);
    Bitangent.z = f * (-DeltaU2 * Edge1.z - DeltaU1 * Edge2.z);

    v0.m_tangent += Tangent;
    v1.m_tangent += Tangent;
    v2.m_tangent += Tangent;
}

for (unsigned int i = 0 ; i < Vertices.size() ; i++) {
    Vertices[i].m_tangent.Normalize();
}

      当然,我们也可以将①②式合并只求出切向量T,将计算副切线B的任务交给shader完成(PS,我就是这么做的)。

      在顶点shader中,我们将顶点的法线和切线向量传入着色器,并乘以model矩阵,转换到物体坐标系:

void main()
{
    v_normal = vec3(normalize(mat_model*vec4(a_normal,0.0)));
    v_tangent = vec3(normalize(mat_model*vec4(a_tangent,0.0)));
}
      在片段shader中,我们利用T、B、N相互垂直的特性求出副切线B(当然这个工作也可以在顶点shader中完成)。

      这里我们对切向量T进行了一个修正,因为顶点着色器的变换,有可能会让T和N不垂直(其实不处理也可以,因为这个不垂直一般是由于乘以Model矩阵后,丢失精度导致的)。修正的方法就是:tangent = normalize(tangent-dot(normal,tangent)*normal);

      在求出B向量后,就可以得到变换到物体坐标系的变换矩阵TBN,即:mat3 TBN = mat3(T, B, N)。此时,将法线图中的法线值乘以TBN矩阵即可得到物体表面的真正法线,处理代码如下:

vec3 calBumpedNormal()
{
    // 利用TNB相互垂直的特性求出B
    vec3 N = normalize(v_normal);
    vec3 T = normalize(v_tangent- dot(v_normal,v_tangent)*v_normal);
    vec3 B = cross(N,T);
    
    // 切线空间的转换矩阵
    mat3 TBN= mat3(T,B,N);

    // 获取法线图中的法线值MapNormal
    vec3 MapNormal = texture2D(normalMap, v_texcoord).xyz;
    
    // MapNormal的值变换到[-1,1]
    vec3 normal = 2.0*MapNormal-vec3(1.0,1.0,1.0);

    // 将MapNormal和TNB矩阵相乘就可以变换到物体坐标系
    normal = normalize(TBN*normal);

    return normal;
}
      经过上面的计算,我们就可以得到这样具有凹凸感的渲染效果啦。

         


       

猜你喜欢

转载自blog.csdn.net/Mahabharata_/article/details/77121611
今日推荐