OpenGL.Shader:9-學習光照-法線貼圖(計算TBN矩陣)

OpenGL.Shader:9-學習光照-法線貼圖(計算TBN矩陣)

這次文章學習法線貼圖,法線貼圖在遊戲開發和GIS系統開發當中尤爲廣泛,其表現力特別的強,繪製的效果特別接近真實。更重要的一點就是,我們可以用很少的代價就可以製作出非常經濟化的模型所表現的效果。這一點在遊戲大作中尤爲重要。

              

我們先來看看上方兩個立方體貼圖效果。左側一方是普通的紋理貼圖,右側一方是法線貼圖,兩者視覺效果有巨大的差別,這就是法線貼圖的魅力所在。

在show代碼前,這裏簡述一下法線貼圖的相關知識。

1、法線映射

在模擬光照效果的時候,是什麼使表面被視爲完全平坦的表面來照亮?答案就是表面的法線向量。以一塊磚塊爲例子,以光照算法的角度,磚塊表面只有一個法線向量,表面完全根據這個法線向量被以一致的方式照亮,所以細節效果的實現往往比較單一。如果每個片元都有自己的不同的法線會怎樣?這樣我們就可以根據表面細微的細節對法線向量進行改變;這樣就會獲得一種表面看起來要複雜得多的視覺效果:

每個片元都使用了自己的法線,我們就可以讓一個表面由很多微小的(不同的法線向量的)平面所組成,這樣的物體表面的細節將會得到極大提升。這種每個片元像素使用各自的法線,替代一個面上所有片元使用同一個法線的技術叫做法線映射(normal mapping)

由於法線向量是一個3元的幾何模型,2D紋理不僅可以儲存顏色和光照數據,還可以儲存法線向量。思考一番,紋理中的顏色分量r、g、b用一個vec3的數學模型代表。同樣是vec3的法線向量x、y、z元素也是能儲存到紋理當中,代替顏色的r、g、b元素,組成一張法線紋理。這樣我們就可以同時根據一組位置數據,從法線紋理中採樣得到對應位置的法線向量。這樣法線貼圖就可以像紋理貼圖一樣工作了。

2、切線空間

既然我們把法線向量(x、y、z)映射到一張貼圖紋理的(r、g、b)分量上,那麼第一思維直覺,每個片元的法向量肯定是垂直於這個紋理所在的平面(即UV座標組成的平面),三個分量的比重大部分都在z(b)分量上,所以的法線紋理多數就是呈現出偏藍色的外觀視覺。(如下圖示)但是這樣會有一個嚴峻的問題,在我們例子當中,正方體六個面,只有頂部面的法向量是(0,0,1),其他方向的面豈不是不能引用這張法線紋理?

思考一下,我們是怎麼把模型頂點/紋理座標,轉換成世界座標?法向量是如何同步到模型的變化操作?都是通過座標系的矩陣運算,這裏引入切線空間(tangent space)座標系。普通2維紋理座標包含U、V兩項,其中U座標增長的方向, 即切線空間中的切線方向(tangent軸),V座標增加的方向,爲切線空間中的副切線方向(bitangent軸)模型中不同的面,都有對應的切線空間,其tangent軸和bitangent軸分別位於繪製的平面上,結合對應的法線方向,我們稱tangant軸(T)、bitangent軸(B)及法線軸(N)所組成的座標系,即切線空間(TBN)(如下圖)

有了TBN切線空間座標系,從法線紋理提取出的法向量,就是基於TBN的數值,然後我們再數學矩陣運算,乘以一個TBN轉換矩陣,就可以正確的轉換成模型所需要的正確方向的法線向量了。
(其中TBN矩陣的求法,更深層次的數學轉換原理、請參考以下鏈接)
https://blog.csdn.net/jxw167/article/details/58671953   
https://blog.csdn.net/yuchenwuhen/article/details/71055602

 

3、代碼運用

class CubeTBN {
    struct V3N3UV2 {
        float x, y, z; //位置座標
        float nx, ny, nz; //法向量
        float u,v; //紋理座標
    };

    struct V3N3UV2TB6
    {
        float x, y, z;
        float nx, ny, nz;
        float u, v;
        float tx,ty,tz;
        float bx,by,bz;
    };
    // ...
};

首先我們定義兩個結構體,V3N3UV2就是我們之前一直使用的標準數據結構(位置頂點vec3,法向量vec3,紋理座標vec2)。在V3N3UV2增加2個vec3,分別是切線方向(tangent軸)和副切線方向(bitangent軸)。通過方法convertTBN求出具體的數值

public:
    V3N3UV2TB6       _data[36];
    
    void        init(const CELL::float3 &halfSize)
    {
        // 之前的標準數據,通過傳入size確定大小。
        V3N3UV2 verts[] =
        {
                // 前
                {-halfSize.x, +halfSize.y, +halfSize.z, 0.0f,  0.0f,  +1.0f, 0.0f,0.0f},
                {-halfSize.x, -halfSize.y, +halfSize.z, 0.0f,  0.0f,  +1.0f, 1.0f,0.0f},
                {+halfSize.x, +halfSize.y, +halfSize.z, 0.0f,  0.0f,  +1.0f, 0.0f,1.0f},
                {-halfSize.x, -halfSize.y, +halfSize.z, 0.0f,  0.0f,  +1.0f, 1.0f,0.0f},
                {+halfSize.x, -halfSize.y, +halfSize.z, 0.0f,  0.0f,  +1.0f, 1.0f,1.0f},
                {+halfSize.x, +halfSize.y, +halfSize.z, 0.0f,  0.0f,  +1.0f, 0.0f,1.0f},
                // 後
                {+halfSize.x, -halfSize.y, -halfSize.z, 0.0f,  0.0f,  -1.0f, 1.0f,0.0f},
                {-halfSize.x, -halfSize.y, -halfSize.z, 0.0f,  0.0f,  -1.0f, 1.0f,1.0f},
                {+halfSize.x, +halfSize.y, -halfSize.z, 0.0f,  0.0f,  -1.0f, 0.0f,0.0f},
                {-halfSize.x, +halfSize.y, -halfSize.z, 0.0f,  0.0f,  -1.0f, 1.0f,0.0f},
                {+halfSize.x, +halfSize.y, -halfSize.z, 0.0f,  0.0f,  -1.0f, 0.0f,0.0f},
                {-halfSize.x, -halfSize.y, -halfSize.z, 0.0f,  0.0f,  -1.0f, 1.0f,1.0f},
                // 左
                {-halfSize.x, +halfSize.y, +halfSize.z, -1.0f, 0.0f,  0.0f,  0.0f,0.0f},
                {-halfSize.x, -halfSize.y, -halfSize.z, -1.0f, 0.0f,  0.0f,  1.0f,1.0f},
                {-halfSize.x, -halfSize.y, +halfSize.z, -1.0f, 0.0f,  0.0f,  1.0f,0.0f},
                {-halfSize.x, +halfSize.y, -halfSize.z, -1.0f, 0.0f,  0.0f,  0.0f,1.0f},
                {-halfSize.x, -halfSize.y, -halfSize.z, -1.0f, 0.0f,  0.0f,  1.0f,1.0f},
                {-halfSize.x, +halfSize.y, +halfSize.z, -1.0f, 0.0f,  0.0f,  0.0f,0.0f},
                // 右
                {+halfSize.x, +halfSize.y, -halfSize.z, +1.0f, 0.0f,  0.0f,  0.0f,0.0f},
                {+halfSize.x, +halfSize.y, +halfSize.z, +1.0f, 0.0f,  0.0f,  0.0f,1.0f},
                {+halfSize.x, -halfSize.y, +halfSize.z, +1.0f, 0.0f,  0.0f,  1.0f,1.0f},
                {+halfSize.x, -halfSize.y, -halfSize.z, +1.0f, 0.0f,  0.0f,  1.0f,0.0f},
                {+halfSize.x, +halfSize.y, -halfSize.z, +1.0f, 0.0f,  0.0f,  0.0f,0.0f},
                {+halfSize.x, -halfSize.y, +halfSize.z, +1.0f, 0.0f,  0.0f,  1.0f,1.0f},
                // 上
                {-halfSize.x, +halfSize.y, +halfSize.z, 0.0f,  +1.0f, 0.0f,  0.0f,1.0f},
                {+halfSize.x, +halfSize.y, +halfSize.z, 0.0f,  +1.0f, 0.0f,  1.0f,1.0f},
                {+halfSize.x, +halfSize.y, -halfSize.z, 0.0f,  +1.0f, 0.0f,  1.0f,0.0f},
                {-halfSize.x, +halfSize.y, -halfSize.z, 0.0f,  +1.0f, 0.0f,  0.0f,0.0f},
                {-halfSize.x, +halfSize.y, +halfSize.z, 0.0f,  +1.0f, 0.0f,  0.0f,1.0f},
                {+halfSize.x, +halfSize.y, -halfSize.z, 0.0f,  +1.0f, 0.0f,  1.0f,0.0f},
                // 下
                {+halfSize.x, -halfSize.y, -halfSize.z, 0.0f,  -1.0f, 0.0f,  1.0f,1.0f},
                {+halfSize.x, -halfSize.y, +halfSize.z, 0.0f,  -1.0f, 0.0f,  1.0f,0.0f},
                {-halfSize.x, -halfSize.y, -halfSize.z, 0.0f,  -1.0f, 0.0f,  0.0f,1.0f},
                {+halfSize.x, -halfSize.y, +halfSize.z, 0.0f,  -1.0f, 0.0f,  1.0f,0.0f},
                {-halfSize.x, -halfSize.y, +halfSize.z, 0.0f,  -1.0f, 0.0f,  0.0f,0.0f},
                {-halfSize.x, -halfSize.y, -halfSize.z, 0.0f,  -1.0f, 0.0f,  0.0f,1.0f}
        };
        // 根據位置/紋理 -> TBN
        convertTBN(verts, _data);
    }

    void convertTBN(V3N3UV2* vertices,V3N3UV2TB6* nmVerts)
    {
        for (size_t i = 0; i <36; i += 3) // 一次操作一個三角形的三個點
        {
            // copy xyz normal uv
            nmVerts[i + 0].x  = vertices[i + 0].x;
            nmVerts[i + 0].y  = vertices[i + 0].y;
            nmVerts[i + 0].z  = vertices[i + 0].z;
            nmVerts[i + 0].nx = vertices[i + 0].nx;
            nmVerts[i + 0].ny = vertices[i + 0].ny;
            nmVerts[i + 0].nz = vertices[i + 0].nz;
            nmVerts[i + 0].u  = vertices[i + 0].u;
            nmVerts[i + 0].v  = vertices[i + 0].v;

            nmVerts[i + 1].x  = vertices[i + 1].x;
            nmVerts[i + 1].y  = vertices[i + 1].y;
            nmVerts[i + 1].z  = vertices[i + 1].z;
            nmVerts[i + 1].nx = vertices[i + 1].nx;
            nmVerts[i + 1].ny = vertices[i + 1].ny;
            nmVerts[i + 1].nz = vertices[i + 1].nz;
            nmVerts[i + 1].u  = vertices[i + 1].u;
            nmVerts[i + 1].v  = vertices[i + 1].v;

            nmVerts[i + 2].x  = vertices[i + 2].x;
            nmVerts[i + 2].y  = vertices[i + 2].y;
            nmVerts[i + 2].z  = vertices[i + 2].z;
            nmVerts[i + 2].nx = vertices[i + 2].nx;
            nmVerts[i + 2].ny = vertices[i + 2].ny;
            nmVerts[i + 2].nz = vertices[i + 2].nz;
            nmVerts[i + 2].u  = vertices[i + 2].u;
            nmVerts[i + 2].v  = vertices[i + 2].v;

            // Shortcuts for vertices
            CELL::float3  v0  = CELL::float3(vertices[i + 0].x,vertices[i + 0].y,vertices[i + 0].z);
            CELL::float3  v1  = CELL::float3(vertices[i + 1].x,vertices[i + 1].y,vertices[i + 1].z);
            CELL::float3  v2  = CELL::float3(vertices[i + 2].x,vertices[i + 2].y,vertices[i + 2].z);
            CELL::float2  uv0 = CELL::float2(vertices[i + 0].u, vertices[i + 0].v);
            CELL::float2  uv1 = CELL::float2(vertices[i + 1].u, vertices[i + 1].v);
            CELL::float2  uv2 = CELL::float2(vertices[i + 2].u, vertices[i + 2].v);
            // 構建triangle平面的方向向量 (position delta, δ)
            CELL::float3  deltaPos1 = v1 - v0;
            CELL::float3  deltaPos2 = v2 - v0;
            // 構建UV平面的兩個方向的向量 (uv delta, δ)
            CELL::float2 deltaUV1   = uv1 - uv0;
            CELL::float2 deltaUV2   = uv2 - uv0;

            float   r  = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);  // uv叉積作底
            CELL::float3 tangent    = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y)*r; // 得出切線
            CELL::float3 binormal   = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x)*r; // 得出副切線

            // 賦值到t b
            nmVerts[i + 0].tx = tangent.x;  nmVerts[i + 0].bx = binormal.x;
            nmVerts[i + 0].ty = tangent.y;  nmVerts[i + 0].by = binormal.y;
            nmVerts[i + 0].tz = tangent.z;  nmVerts[i + 0].bz = binormal.z;

            nmVerts[i + 1].tx = tangent.x;  nmVerts[i + 1].bx = binormal.x;
            nmVerts[i + 1].ty = tangent.y;  nmVerts[i + 1].by = binormal.y;
            nmVerts[i + 1].tz = tangent.z;  nmVerts[i + 1].bz = binormal.z;

            nmVerts[i + 2].tx = tangent.x;  nmVerts[i + 2].bx = binormal.x;
            nmVerts[i + 2].ty = tangent.y;  nmVerts[i + 2].by = binormal.y;
            nmVerts[i + 2].tz = tangent.z;  nmVerts[i + 2].bz = binormal.z;
        }
    }

至此,每個基準點的TBN矩陣都計算出來了。有了數據之後,我們就可以開始學習着色器程序的編寫。

首先我們來看頂點着色器部分:

#version 320 es
in      vec3    _position; // 外部輸入
in      vec3    _normal;
in      vec2    _uv;
in      vec3    _tagent;
in      vec3    _biTagent;
uniform mat4    _mvp; // mvp矩陣
uniform mat3    _normalMatrix; // 法線矩陣
uniform mat4    _matModel; // 模型轉換矩陣
out     vec2    _outUV;
out     vec3    _outPos;
out     mat3    _TBN;
void main()
{
        _outUV             =   _uv;  //// 輸出紋理座標到片元着色器,用於提取紋理貼圖和法線貼圖
        vec4     pos       =   _matModel*vec4(_position,1);
        _outPos           =   pos.xyz;  //// 輸出模型頂點位置,保證每個片元都有細化的光照方向
        vec3    normal  =   normalize(_normalMatrix * _normal);  // 乘以法線矩陣,以保證模型變換操作後的一致性。
        vec3    tagent   =   normalize(_normalMatrix * _tagent);
        vec3    biTagent=   normalize(_normalMatrix * _biTagent);
        _TBN               =   mat3x3(tagent,biTagent,normal); // 構建TBN矩陣 輸出到片元着色器
        gl_Position       =   _mvp * vec4(_position,1.0); // 輸出最終繪製的頂點座標
}

em... 該說的都已經加註釋了。 至於爲什麼要把模型化操作後的(世界座標系的)頂點輸出到片元着色器?計算光強度的主要屬性——光照方向,之前我們都是直接在頂點着色器完成,這是因爲之前我們沒有掌握法線貼圖這一重點內容,沒能把法線向量細化到每個片元當中。頂點位置數據從頂點着色器輸出到達片元着色器之後,片元着色器都會進行插值運算。插值之後,每個片元都有插值後的頂點位置數據,這樣一來就要重新計算更爲細化的光照方向,以配合法線貼圖進行更好的計算效果。

#version 320 es
precision mediump float;
in      vec2          _outUV;
in      vec3          _outPos;
in      mat3          _TBN;
uniform vec3      _lightColor;
uniform vec3      _lightDiffuse;
uniform sampler2D _texture;
uniform sampler2D _texNormal;
uniform vec3           _lightPos;
uniform vec3           _cameraPos;
out     vec3              _fragColor;
void main()
{
        vec3   lightDir     =  normalize(_lightPos  - _outPos);   // 計算每個片元屬於自己的 光照方向
        vec3   normal     =  normalize(_TBN * (texture(_texNormal,_outUV).xyz*2.0 - 1.0));
        // 從法線貼圖提取法向量 歸一化[-1,1]區間 然後 通過TBN矩陣 轉化爲最終法向量
        vec4   materialColor =  texture(_texture,_outUV); 
        float  lightStrength    =  max(dot(normal, lightDir), 0.0);  // 計算光照強度
        vec4   lightColor       =  vec4(_lightColor * lightStrength + _lightDiffuse, 1);   // 光照強度*顏色光 + 漫反射光
        _fragColor.rgb          =  materialColor.rgb * 0.2 + 0.8 * lightColor.rgb;     // 混合輸入效果。

_uv 紋理座標提取紋理貼圖的顏色值,又能提取法線貼圖的法向量值。需要注意的是,texture(_texNormal,_outUV).xyz的範圍值是[0,255],歸一化是[0,1]。但是我們法向量需要的是[-1, 1],我們需要自己轉換。最後混合輸出效果不固定。大家按需調整效果就好。

 

最後加上CubeTBN.render方法。

void        render(Camera3D& camera)
{
    _program.begin();
    static  float   angle = 0;
    angle += 0.1f;
    CELL::matrix4   matRot;
    matRot.rotateYXZ(angle, 0.0f, 0.0f);
    CELL::matrix4   model   =   _modelMatrix * matRot;
    glUniformMatrix4fv(_program._matModel, 1, GL_FALSE, model.data());
    CELL::matrix4   vp = camera.getProject() * camera.getView();
    CELL::matrix4   mvp = (vp * model);
    glUniformMatrix4fv(_program._mvp, 1, GL_FALSE, mvp.data());
    CELL::matrix3   matNormal = mat4_to_mat3(model)._inverse()._transpose();
    glUniformMatrix3fv(_program._normalMatrix, 1, GL_FALSE, matNormal.data());

    glUniform3f(_program._lightDiffuse, 0.1f, 0.1f, 0.1f); // 漫反射 環境光
    glUniform3f(_program._lightColor, 1.0f, 1.0f, 1.0f);  // 定向光源的顏色
    glUniform3f(_program._lightPos, camera._eye.x, camera._eye.y, camera._eye.z);//光源位置

    glActiveTexture(GL_TEXTURE0);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D,  _texMaterial); 
    glUniform1i(_program._texture, 0); // 加載紋理貼圖
    glActiveTexture(GL_TEXTURE1);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D,  _texNormal);
    glUniform1i(_program._texNormal, 1); // 加載法線貼圖

    glVertexAttribPointer(_program._position, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), _data);
    glVertexAttribPointer(_program._normal, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].nx);
    glVertexAttribPointer(_program._uv, 2, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].u);
    glVertexAttribPointer(_program._tagent, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].tx);
    glVertexAttribPointer(_program._biTagent, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].bx);
    glDrawArrays(GL_TRIANGLES, 0, 36);
    _program.end();
}

工程demo源碼:參考文件CubeTBN.hpp CubeTbnProgram.hpp

https://github.com/MrZhaozhirong/NativeCppApp      

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章