OpenGL.Shader: 9-обучающая карта света-нормали (вычисление матрицы TBN)

OpenGL.Shader: 9-обучающая карта света-нормали (вычисление матрицы TBN)

В этой статье рассказывается о картах нормалей. Карты нормалей особенно широко используются при разработке игр и ГИС-систем. Их выразительность особенно сильна, а эффект рисования очень близок к реальности. Более важным моментом является то, что мы можем сделать очень экономичную модель с очень небольшими затратами. Это особенно важно в игровом шедевре.

              

Давайте посмотрим на эффект двух текстур куба выше. Левая сторона - это карта нормалей, а правая - карта нормалей. Визуальные эффекты этих двух сильно различаются. Это очарование карты нормалей.

Перед показом кода приведено краткое описание соответствующих знаний о карте нормалей.

1. Нормальное отображение

Что при моделировании световых эффектов делает поверхность полностью плоской? Ответ - это нормальный вектор поверхности. Взяв в качестве примера кирпич, с точки зрения алгоритма освещения, поверхность кирпича имеет только один вектор нормали, и поверхность освещается согласованным образом на основе этого вектора нормали, поэтому реализация эффекта детализации часто бывает относительно простой. Что, если у каждого фрагмента своя нормаль? Таким образом, мы можем изменить вектор нормали в соответствии с тонкими деталями поверхности; это приведет к визуальному эффекту, который на поверхности выглядит намного сложнее:

Каждый фрагмент использует свою нормаль, поэтому мы можем создать поверхность, состоящую из множества крошечных (различных векторов нормали) плоскостей, и детали поверхности такого объекта будут значительно улучшены. Этот метод использования каждого пикселя фрагмента для использования его собственной нормали вместо использования одной и той же нормали для всех фрагментов на поверхности называется отображением нормалей.

Поскольку вектор нормали представляет собой трехэлементную геометрическую модель, двухмерная текстура может не только хранить данные о цвете и освещении, но также и вектор нормали. Подумайте об этом, цветовые компоненты r, g и b в текстуре представлены математической моделью vec3. Элементы вектора нормали x, y и z одного и того же vec3 также могут быть сохранены в текстуре вместо элементов цвета r, g и b для формирования текстуры нормалей. Таким образом, мы можем выбрать вектор нормали из текстуры нормали на основе набора данных положения одновременно. Таким образом, карта нормалей может работать как карта текстуры.

2. Касательное пространство

Поскольку мы сопоставляем вектор нормали (x, y, z) с компонентом (r, g, b) текстуры, то первая интуиция состоит в том, что вектор нормали каждого фрагмента должен быть перпендикулярен текстуре. Плоскость (то есть плоскость, составленная из UV-координат), пропорции трех компонентов - все на компоненте z (b), поэтому нормальная текстура в основном имеет голубоватый вид. (Как показано ниже) Но это будет иметь серьезную проблему. В нашем примере есть шесть граней куба, только вектор нормали верхней грани (0, 0, 1), нельзя ли использовать этот метод для граней в других направлениях? Текстура линии?

Подумайте, как нам преобразовать координаты вершины / текстуры модели в мировые координаты? Как вектор нормали синхронизируется с операцией изменения модели? Все это через матричную операцию системы координат, и здесь вводится система координат касательного пространства . Обычные координаты 2D текстуры включают U и V. Направление, в котором увеличивается координата U, является касательным направлением в касательном пространстве (касательная ось), а направление, в котором увеличивается координата V, является моделью вторичного касательного направления (бит касательной) в касательном пространстве. Различные грани в, все имеют соответствующее касательное пространство, касательная ось и боковая касательная ось соответственно расположены на нарисованной плоскости, в сочетании с соответствующим нормальным направлением, мы называем касательной осью (T), боковой касательной осью (B) и нормальной осью (N ) Состоит ли система координат из касательного пространства ( TBN ) (как показано ниже)

В системе координат касательного пространства TBN вектор нормали, извлеченный из текстуры нормали, основан на значении TBN, а затем мы выполняем математические операции с матрицей и умножаем его на матрицу преобразования TBN, чтобы преобразовать его в правильное направление, требуемое моделью. Нормальный вектор.
(Для расчета матрицы TBN и принципа более глубокого математического преобразования перейдите по следующей ссылке)
https://blog.csdn.net/jxw167/article/details/58671953    
https://blog.csdn.net/yuchenwuhen/article/ подробнее / 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). Добавьте два vec3s к V3N3UV2, которые представляют собой касательное направление (касательная ось) и вторичное касательное направление (бит касательной). Найдите конкретное значение методом 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; // внешний ввод
в vec3 _normal;
в vec2 _uv;
в vec3 _tagent;
в vec3 _biTagent;
uniform mat4 _mvp; // mvp matrix
uniform mat3 _normalMatrix; // normal matrix
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 = _mvpposition, _vec4 ( 1.0); // Выводим окончательные координаты нарисованной вершины
}

эм ... Комментарии добавлены. А почему после операции моделирования вершины (мировой системы координат) должны выводиться во фрагментный шейдер? Вычисление основного атрибута интенсивности света - направления освещения. Раньше мы делали это непосредственно в вершинном шейдере, потому что раньше мы не улавливали ключевое содержимое карты нормалей и не смогли уточнить вектор нормали для каждого фрагмента. среди. После вывода данных о положении вершины из вершинного шейдера во фрагментный шейдер фрагментный шейдер выполнит операции интерполяции. После интерполяции каждый фрагмент имеет интерполированные данные положения вершины, поэтому необходимо пересчитать более подробное направление света, чтобы оно соответствовало карте нормалей для лучших результатов вычислений.

ES 320. #Version
Точность mediump с плавающей точкой,
в vec2 _outUV;
в vec3 _outPos;
в MAT3 _TBN;
Uniform vec3 _lightColor;
Uniform vec3 _lightDiffuse;
Uniform sampler2D _texture;
Uniform sampler2D _texNormal;
Uniform vec3 _lightPos;
Uniform vec3 _cameraPos;
OUT vec3 _fragColor;
вакуум основных ()
{         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 извлекают значение цвета карты текстуры и могут извлекать значение вектора нормали карты нормалей. Следует отметить, что значение диапазона текстуры (_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();
}

Исходный код демонстрации проекта: Справочный файл CubeTBN.hpp CubeTbnProgram.hpp

https://github.com/MrZhaozhirong/NativeCppApp      

рекомендация

отblog.csdn.net/a360940265a/article/details/94719015