法线贴图的实现【OpenGL】

文在Blinn-phong光照模型基础上添加法线贴图。

看案例的时候总感觉很简单,但是自己写了之后发现还是有很多细节要注意的。

1.法线贴图

1.1 基本原理

借助一张纹理贴图颜色值(RGB)存储物体的表面法线方向,比如下面这张图,大部分是蓝色RGB:(0,0,1),对应了法线方向(0,0,1).

但是法线范围应该是【-1,1】,表示正反两个方向,所以要进行转化。

适合少顶点的情况下增加真实感,在多顶点情况下对比不是很大。

1.2 基本实现

仍然是基于原来的漫反射贴图计算光照,但是在漫反射和高光反射的计算上会更加细致,因为两者都与法线相关。

漫反射:

  

高光反射:

Blinn-phong光照计算公式:

2. TBN矩阵

2.1 基本原理

一开始想一个向量如何转换到另一个坐标,就是进行平移然后旋转(不考虑缩放)。

平移的话直到两个原点就行了,但是旋转还需要知道沿着三个向量的旋转角度。所以并不好求。

引入TBN矩阵 

给定三个相互垂直的基向量确定一个坐标系,坐标其实就是该点在三个基向量上的投影大小,也就是点乘。

也就是说如果想将一个坐标P从A坐标系变换到B坐标系,知道三个相互垂直的基向量(A坐标系),将这三个基向量用B坐标系表示即可,然后对点P分别对三个向量求点乘即可(得到投影大小)。

T、B、N是行向量表示的

:正交矩阵(每个轴既是单位向量同时相互垂直)的置换矩阵与它的逆矩阵相等。这个属性很重要因为逆矩阵的求得比求置换开销大;结果却是一样的。

2.2 求TBN矩阵

因为已经直到原来的法线,求TBN矩阵其实只需要求其中一个切线即可,因为副切线可以用切线和法线的叉乘求到。

对于简单的平面

我们可以手工计算切线。

这里的计算都是在切线空间下计算的(后续再转化到世界空间里),默认T和B是沿着xy方向。

两条线缺点一个平面,将两个方向向E1、E2用基函数进行表示,可以得到下面的式子:

矩阵表示如下:

 两边左乘UV矩阵的逆矩阵,结果最终可以表示为:

对于复杂模型

当我们需要对复杂多面体进行计算时,就需要为每个加载的顶点计算出柔和的切线和副切线向量,这明显是很庞大的工作量。因为每个顶点对应的切线空间可能都是不同的。

好在我们不需要手工计算每个顶点对应的切线和副切线,assimp库(Open Asset Import Library)在加载模型时候帮我们计算好了每个顶点的信息。我们只需要读取,然后放入缓存中传递到着色器程序。

2.3 格拉姆-施密特正交化(Gram-Schmidt process)

原因:求得TBN三个基函数后,其实这三个基向量不一定是两两垂直的。这意味着TBN矩阵不再是正交矩阵了,法线贴图可能会稍稍偏移。

首先副切线B是用切线和法线的叉乘求到,所以B和T、N是相互垂直的。

所以对T或者N,需要进行正交化,这里对T进行修正,简单理解如下:T' + N(NT)=T,求出T'然后单位化即可。

3. 代码实现

3.1 传递数据到着色器

这里可以用LearnOpenGL的代码,把assimp中模型的顶点信息都存储到VBO里面了,并且帮我们绑定到VAO中了,我们只需要在着色器按照顺序获取数据即可。

一个顶点有如下的信息:

// render data 
    unsigned int VBO, EBO;

    // initializes all the buffer objects/arrays
    void setupMesh()
    {
        // create buffers/arrays
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glGenBuffers(1, &EBO);

        glBindVertexArray(VAO);
        // load data into vertex buffers
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        // A great thing about structs is that their memory layout is sequential for all its items.
        // The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which
        // again translates to 3/2 floats which translates to a byte array.
        glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);  

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

        // set the vertex attribute pointers
        // vertex Positions
        glEnableVertexAttribArray(0);	
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
        // vertex normals
        glEnableVertexAttribArray(1);	
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
        // vertex texture coords
        glEnableVertexAttribArray(2);	
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
        // vertex tangent
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
        // vertex bitangent
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
		// ids
		glEnableVertexAttribArray(5);
		glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));

		// weights
		glEnableVertexAttribArray(6);
		glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));
        glBindVertexArray(0);
    }

这里也帮我们绑定了贴图文件,对应的命名规则要按照这里来:比如texture_diffuse+num

    void Draw(Shader &shader) 
    {
        // bind appropriate textures
        unsigned int diffuseNr  = 1;
        unsigned int specularNr = 1;
        unsigned int normalNr   = 1;
        unsigned int heightNr   = 1;
        for(unsigned int i = 0; i < textures.size(); i++)
        {
            glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding
            // retrieve texture number (the N in diffuse_textureN)
            string number;
            string name = textures[i].type;
            if(name == "texture_diffuse")
                number = std::to_string(diffuseNr++);
            else if(name == "texture_specular")
                number = std::to_string(specularNr++); // transfer unsigned int to string
            else if(name == "texture_normal")
                number = std::to_string(normalNr++); // transfer unsigned int to string
             else if(name == "texture_height")
                number = std::to_string(heightNr++); // transfer unsigned int to string

            // now set the sampler to the correct texture unit
            glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);
            // and finally bind the texture
            glBindTexture(GL_TEXTURE_2D, textures[i].id);

            // TODO : Pass tangential space data to shaders
        }
        
        // draw mesh
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        // always good practice to set everything back to defaults once configured.
        glActiveTexture(GL_TEXTURE0);
    }

3.2 顶点着色器

按照布局顺序获取顶点信息,然后求出TBN矩阵,并把所有位置信息转换到切线空间。

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent;
//layout (location = 4) in vec3 aBitTangent;

out vec3 tFragPos;
out vec2 texCoords;
out vec3 tViewPos;
out vec3 tLightPos;

uniform vec3 viewPos;
uniform vec3 lightPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos,1.0f);
    texCoords = aTexCoords;
    // calculate TBN
    mat3 normalMatrix = transpose(inverse(mat3(model)));
    vec3 T = normalize(normalMatrix * aTangent);
    vec3 N = normalize(normalMatrix * aNormal);
    T = normalize(T - dot(T,N) * N);        //Gram-Schmidt process
    vec3 B = normalize(cross(T,N));

    mat3 TBN = transpose(mat3(T,B,N));            //the transpose and inverse is the same

    tFragPos = TBN * vec3(model * vec4(aPos,1.0f)); // this should be in tangent space
    tViewPos = TBN * viewPos;
    tLightPos = TBN * lightPos;
}

注意mat3(T,B,N)是按照列排,需要转置。然后BitTangent是没必要的,否则不能保证正交化。

3.3 片元着色器

只要所有向量在切线空间计算即可,注意命名规则,这里用的是Blinn-phong光照模型+光线衰减。

#version 330 core

in vec2 texCoords;
in vec3 tFragPos;
in vec3 tViewPos;
in vec3 tLightPos;

uniform vec3 lightColor;
uniform float lightLinear;
uniform float lightQuadratic;

uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;
uniform sampler2D texture_normal1;
//uniform sampler2D texture_height1;

out vec4 fragColor;
void main()
{
    vec3 Normal = texture(texture_normal1, texCoords).rgb;

    Normal = normalize(Normal * 2 - 1.0f) * 1.06f;

    vec3 Diffuse = texture(texture_diffuse1, texCoords).rgb;
    float specStrength = texture(texture_specular1,texCoords).r;
    
    // then calculate lighting as usual
    vec3 ambient = vec3(0.3 * Diffuse);
    vec3 lighting  = ambient; 
    vec3 viewDir  = normalize(tViewPos - tFragPos); 
    // diffuse
    vec3 lightDir = normalize(tLightPos - tFragPos);
    vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * lightColor;
    // specular
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(Normal, halfwayDir), 0.0), 32.0);
    vec3 specular = lightColor * spec * 0.1f;
    
    // attenuation
    float distance = length(tLightPos - tFragPos);
    float attenuation = 1.0 / (1.0 + lightLinear * distance + lightQuadratic * distance * distance);
    diffuse *= attenuation;
    specular *= attenuation;
    lighting += diffuse + specular;

    fragColor = vec4(lighting , 1.0f);
}

注意的是,法线需要转换到【-1,1】,否则不应该高光的地方会出现,高光部分会非常明显。

最后得到的效果应该如下所示: 

猜你喜欢

转载自blog.csdn.net/cycler_725/article/details/124604197