目录

OpenGLopenGL-法线贴图

【OpenGL】openGL 法线贴图

参考文章:

一、法线纹理

下图是一张法线纹理:

https://i-blog.csdnimg.cn/img_convert/356800677ddebbca304953400ea1bf55.jpeg

每个纹素的RGB值实际上表示的是XYZ向量:颜色的分量取值范围为0到1,而向量的分量取值范围是-1到1;可以建立从纹素到法线的简单映射

normal = (2*color)-1 // on each component

由于法线基本都是指向”曲面外侧”的(按照惯例,X轴朝右,Y轴朝上),因此法线纹理整体呈蓝色。

法线纹理的映射方式和漫反射纹理相似。麻烦之处在于如何将法线从各三角形局部空间(切线空间tangent space,亦称图像空间image space)变换到模型空间(着色计算所采用的空间)。

二、切线和副切线(Tangent and Bitangent)

大家对矩阵已经十分熟悉了,应该知道定义一个空间(本例是切线空间)需要三个向量。现在Up向量已经有了,即法线:可用Blender生成,或由一个简单的叉乘计算得到。下图中蓝色箭头代表法线(法线贴图整体颜色也恰好是蓝色)。

https://i-blog.csdnimg.cn/img_convert/0ca8992e7d57c007b3e8e0bcab0d1bf3.png

然后是切线T:垂直于法线的向量。但这样的切线有很多个:

https://i-blog.csdnimg.cn/img_convert/9f379b5321f1cb2d95d81fc4997ea7e2.png

这么多切线中该选哪个呢?理论上哪一个都行。但我们必须保持连续一致性,以免衔接处出现瑕疵。标准的做法是将切线方向和纹理空间对齐:

https://i-blog.csdnimg.cn/img_convert/2584f5b79da0a251f1121cbba28b05c7.png

定义一组基需要三个向量,因此我们还得计算副切线B(本可以随便选一条切线,但选定垂直于另外两条轴的切线,计算会方便些)。

https://i-blog.csdnimg.cn/img_convert/dcbd5c90f322fb0b5bee8b8d50599178.png

算法如下:记三角形的两条边为deltaPos1和deltaPos2,deltaUV1和deltaUV2是对应的UV坐标下的差值;则问题可用如下方程表示:

deltaPos1 = deltaUV1.x * T + deltaUV1.y * B
deltaPos2 = deltaUV2.x * T + deltaUV2.y * B

求解T和B就得到了切线和副切线!(代码见下文)

已知T、B、N向量之后,即可得下面这个漂亮的矩阵,完成从切线空间到模型空间的变换:

[ T x B x N x T y B y N y T z B z N z ] \begin{bmatrix} T_x & B_x & N_x \ T_y & B_y & N_y \ T_z & B_z & N_z \end{bmatrix}

​Tx​Ty​Tz​​Bx​By​Bz​​Nx​Ny​Nz​​

有了TBN矩阵,我们就能把(从法线纹理中获取的)法线变换到模型空间。

可我们需要的却是从切线空间到模型空间的变换,法线则保持不变。所有计算均在切线空间中进行,不会对其他计算产生影响。

只需对上述矩阵求逆即可得逆变换。这个矩阵(正交阵,即各向量相互正交的矩阵,参见下文”延伸阅读”小节)的逆矩阵恰好也就是其转置矩阵,计算十分简单:

invTBN = transpose(TBN)

亦即:

[ T x T y T z B x B y B z N x N y N z ] \begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z \ N_x & N_y & N_z \end{bmatrix}

​Tx​Bx​Nx​​Ty​By​Ny​​Tz​Bz​Nz​​

三、准备VBO

3.1 计算切线和副切线

我们需要为整个模型计算切线、副切线和法线。我们用一个单独的函数完成这些计算

void computeTangentBasis(
    // inputs
    std::vector & vertices,
    std::vector & uvs,
    std::vector & normals,
    // outputs
    std::vector & tangents,
    std::vector & bitangents
){

为每个三角形计算边(deltaPos)和deltaUV

for ( int i=0; i<vertices.size(); i+=3){
	// Shortcuts for vertices
	glm::vec3 & v0 = vertices[i+0];
	glm::vec3 & v1 = vertices[i+1];
	glm::vec3 & v2 = vertices[i+2];

	// Shortcuts for UVs
	glm::vec2 & uv0 = uvs[i+0];
	glm::vec2 & uv1 = uvs[i+1];
	glm::vec2 & uv2 = uvs[i+2];

	// Edges of the triangle : postion delta
	glm::vec3 deltaPos1 = v1-v0;
	glm::vec3 deltaPos2 = v2-v0;

	// UV delta
	glm::vec2 deltaUV1 = uv1-uv0;
	glm::vec2 deltaUV2 = uv2-uv0;

现在用公式来算切线和副切线:

float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
glm::vec3 tangent = (deltaPos1 * deltaUV2.y   - deltaPos2 * deltaUV1.y)*r;
glm::vec3 bitangent = (deltaPos2 * deltaUV1.x   - deltaPos1 * deltaUV2.x)*r;

最后,把这些_切线_和_副切线_缓存起来。记住,我们还没为这些缓存的数据生成索引,因此每个顶点都有一份拷贝

// Set the same tangent for all three vertices of the triangle.
// They will be merged later, in vboindexer.cpp
tangents.push_back(tangent);
tangents.push_back(tangent);
tangents.push_back(tangent);

// Same thing for binormals
bitangents.push_back(bitangent);
bitangents.push_back(bitangent);
bitangents.push_back(bitangent);

}

3.2 索引

索引VBO的方法和之前类似,仅有些许不同。

找到相似顶点(相同的坐标、法线、纹理坐标)后,我们不直接用它的切线、副法线,而是取其均值。因此,只需把老代码修改一下:

// Try to find a similar vertex in out_XXXX
unsigned int index;
bool found = getSimilarVertexIndex(in_vertices[i], in_uvs[i], in_normals[i],     out_vertices, out_uvs, out_normals, index);

if ( found ){ // A similar vertex is already in the VBO, use it instead !
	out_indices.push_back( index );

	// Average the tangents and the bitangents
	out_tangents[index] += in_tangents[i];
	out_bitangents[index] += in_bitangents[i];
}else{ // If not, it needs to be added in the output data.
	// Do as usual
	[...]
}

注意,这里没有对结果归一化。这种做法十分便利。由于小三角形的切线、副切线向量较小;相对于大三角形来说,对模型外观的影响程度较小。

四、着色器

4.1 新增缓冲和uniform变量

我们需要再加两个缓冲,分别存储切线和副切线:

GLuint tangentbuffer;
glGenBuffers(1, &tangentbuffer);
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer);
glBufferData(GL_ARRAY_BUFFER, indexed_tangents.size() * sizeof(glm::vec3), &indexed_tangents[0], GL_STATIC_DRAW);

GLuint bitangentbuffer;
glGenBuffers(1, &bitangentbuffer);
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer);
glBufferData(GL_ARRAY_BUFFER, indexed_bitangents.size() * sizeof(glm::vec3), &indexed_bitangents[0], GL_STATIC_DRAW);

还需要一个uniform变量存储新增的法线纹理:

[...]
GLuint NormalTexture = loadTGA_glfw("normal.tga");
[...]
GLuint NormalTextureID  = glGetUniformLocation(programID, "NormalTextureSampler");

另外一个uniform变量存储3x3的模型视图矩阵。严格地讲,这个矩阵可有可无,它仅仅是让计算更方便罢了;详见后文。由于仅仅计算旋转,不需要平移,因此只需矩阵左上角3x3的部分。

GLuint ModelView3x3MatrixID = glGetUniformLocation(programID, "MV3x3");

完整的绘制代码如下:

// Clear the screen
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Use our shader
glUseProgram(programID);

// Compute the MVP matrix from keyboard and mouse input
computeMatricesFromInputs();
glm::mat4 ProjectionMatrix = getProjectionMatrix();
glm::mat4 ViewMatrix = getViewMatrix();
glm::mat4 ModelMatrix = glm::mat4(1.0);
glm::mat4 ModelViewMatrix = ViewMatrix * ModelMatrix;
glm::mat3 ModelView3x3Matrix = glm::mat3(ModelViewMatrix); // Take the upper-left part of ModelViewMatrix
glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix;

// Send our transformation to the currently bound shader,
// in the "MVP" uniform
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glUniformMatrix4fv(ModelMatrixID, 1, GL_FALSE, &ModelMatrix[0][0]);
glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]);
glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]);
glUniformMatrix3fv(ModelView3x3MatrixID, 1, GL_FALSE, &ModelView3x3Matrix[0][0]);

glm::vec3 lightPos = glm::vec3(0,0,4);
glUniform3f(LightID, lightPos.x, lightPos.y, lightPos.z);

// Bind our diffuse texture in Texture Unit 0
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, DiffuseTexture);
// Set our "DiffuseTextureSampler" sampler to user Texture Unit 0
glUniform1i(DiffuseTextureID, 0);

// Bind our normal texture in Texture Unit 1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, NormalTexture);
// Set our "Normal    TextureSampler" sampler to user Texture Unit 0
glUniform1i(NormalTextureID, 1);

// 1rst attribute buffer : vertices
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
	0,                  // attribute
	3,                  // size
	GL_FLOAT,           // type
	GL_FALSE,           // normalized?
	0,                  // stride
	(void*)0            // array buffer offset
);

// 2nd attribute buffer : UVs
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, uvbuffer);
glVertexAttribPointer(
	1,                                // attribute
	2,                                // size
	GL_FLOAT,                         // type
	GL_FALSE,                         // normalized?
	0,                                // stride
	(void*)0                          // array buffer offset
);

// 3rd attribute buffer : normals
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(
	2,                                // attribute
	3,                                // size
	GL_FLOAT,                         // type
	GL_FALSE,                         // normalized?
	0,                                // stride
	(void*)0                          // array buffer offset
);

// 4th attribute buffer : tangents
glEnableVertexAttribArray(3);
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer);
glVertexAttribPointer(
	3,                                // attribute
	3,                                // size
	GL_FLOAT,                         // type
	GL_FALSE,                         // normalized?
	0,                                // stride
	(void*)0                          // array buffer offset
);

// 5th attribute buffer : bitangents
glEnableVertexAttribArray(4);
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer);
glVertexAttribPointer(
	4,                                // attribute
	3,                                // size
	GL_FLOAT,                         // type
	GL_FALSE,                         // normalized?
	0,                                // stride
	(void*)0                          // array buffer offset
);

// Index buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);

// Draw the triangles !
glDrawElements(
	GL_TRIANGLES,      // mode
	indices.size(),    // count
	GL_UNSIGNED_INT,   // type
	(void*)0           // element array buffer offset
);

glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(3);
glDisableVertexAttribArray(4);

// Swap buffers
glfwSwapBuffers();

4.2 顶点着色器

如前所述,所有计算都摄像机空间中做,因为在这一空间中更容易获取片段坐标。这就是为什么要用模型视图矩阵乘T、B、N向量。

vertexNormal_cameraspace = MV3x3 * normalize(vertexNormal_modelspace);
vertexTangent_cameraspace = MV3x3 * normalize(vertexTangent_modelspace);
vertexBitangent_cameraspace = MV3x3 * normalize(vertexBitangent_modelspace);

这三个向量确定了TBN矩阵,其创建方式如下:

mat3 TBN = transpose(mat3(
	vertexTangent_cameraspace,
	vertexBitangent_cameraspace,
	vertexNormal_cameraspace
)); // You can use dot products instead of building this matrix and transposing it. See References for details.

此矩阵是从摄像机空间到切线空间的变换(若矩阵名为XXX_modelspace,则是从模型空间到切线空间的变换)。我们可以利用它计算切线空间中的光线方向和视线方向。

LightDirection_tangentspace = TBN * LightDirection_cameraspace;
EyeDirection_tangentspace =  TBN * EyeDirection_cameraspace;

4.3 片段着色器

切线空间中的法线很容易获取–就在纹理中:

    // Local normal, in tangent space
    vec3 TextureNormal_tangentspace = normalize(texture( NormalTextureSampler, UV ).rgb*2.0 - 1.0);

一切准备就绪。漫反射光的值由切线空间中的n和l计算得来(在哪个空间中计算并不重要,关键是n和l必须位于同一空间中),并用_clamp( dot( n,l ), 0,1 )_截取。镜面光用_clamp( dot( E,R ), 0,1 )_截取,E和R也必须位于同一空间中。大功告成!

五、结果

这是目前得到的结果,您可以看到:

  • 砖块看上去凹凸不平,这是因为砖块表面法线变化比较剧烈
  • 水泥部分看上去很平整,这是因为这部分的法线纹理全都是蓝色

https://i-blog.csdnimg.cn/direct/0ac1c5163a194260ae5b1a5eefe33267.png

六、延伸阅读

6.1 正交化(Orthogonalization)

顶点着色器中,为了计算速度,我们没有进行矩阵求逆,而是进行了转置。这只有当矩阵表示的空间正交时才成立,而这个矩阵还不是正交的。好在这个问题很容易解决:只需在computeTangentBasis()末尾让切线与法线垂直。

t = glm::normalize(t - n * glm::dot(n, t));

这个公式有点难理解,来看看图:

https://i-blog.csdnimg.cn/img_convert/64deac4eb6a0e19af2ab05c61b1868e2.png

n和t差不多是相互垂直的,只要把t沿-n方向稍微”推”一下,幅度是dot(n,t)。

有一个applet也演示得很清楚(仅含两个向量)。

6.2 左手系还是右手系?

一般不必担心这个问题。但在某些情况下,比如使用对称模型时,UV坐标方向会出错,导致切线T方向错误。

判断是否需要翻转坐标系很容易:TBN必须形成一个右手坐标系–向量cross(n,t)应该和b同向。

用数学术语讲,”向量A和向量B同向”则有”dot(A,B)>0”;故只需检查dot( cross(n,t) , b )是否大于0。

若dot( cross(n,t) , b ) < 0,就要翻转t:

if (glm::dot(glm::cross(n, t), b) < 0.0f){
     t = t * -1.0f;
 }

在computeTangentBasis()末对每个顶点都做这个操作。

6.3 镜面纹理(Specular texture)

为了增强趣味性,我在代码里加上了镜面纹理;取代了原先作为镜面颜色的灰色vec3(0.3,0.3,0.3)。镜面纹理看起来像这样:

https://i-blog.csdnimg.cn/img_convert/c954b07c1f8cd5a54d783c8bae7f81ef.jpeg

https://i-blog.csdnimg.cn/direct/3b02f017c9774471b60641fc2fab9aa0.png

请注意,由于如上镜面纹理中没有镜面分量,水泥部分均呈黑色。

七、完整代码

my_glwidget.h

//  
// Created by liuhang on 2025/9/16.  
//  
  
#ifndef OPENGL_LEARNING_MY_GLWIDGET_H  
#define OPENGL_LEARNING_MY_GLWIDGET_H  
  
#include<QOpenGLWidget>  
#include<QOpenGLFunctions>  
#include<QMatrix4x4>  
#include<QTimer>  
#include "objloader.hpp"  
  
class MyGLWidget : public QOpenGLWidget,protected QOpenGLFunctions  
{  
    Q_OBJECT  
public:  
    explicit MyGLWidget(QWidget* parent = nullptr);  
    ~MyGLWidget() override;  
  
protected:  
    void initializeGL() override;  
    void paintGL() override;  
    void resizeGL(int w,int h) override;  
  
    void doMVP();  
private:  
    void loadShader(std::string const& vertex_shader_path,std::string const& fragment_shader_path);  
    void loadModel();  
    void loadEbo();  
    GLint loadBmpTexture(QString const& bmp_path);  
    void computeTangentBasis(  
            // inputs  
            std::vector<QVector3D> & vertices,  
            std::vector<QVector2D> & uvs,  
            std::vector<QVector3D> & normals,  
            // outputs  
            std::vector<QVector3D> & tangents,  
            std::vector<QVector3D> & bitangents  
    );  
  
private:  
    GLuint vertex_array_id;  
    GLuint vertex_buffer_id;  
  
    GLuint element_buffer_id;  
  
    GLuint uv_buffer_id;  
  
    GLuint normal_buffer_id;  
  
    GLuint vertex_shader_id;  
    GLuint fragment_shader_id;  
  
    GLuint shader_program_id;  
  
    GLint uniform_model_matrix_location;  
    GLint uniform_view_matrix_location;  
    GLint uniform_projection_matrix_location;  
    GLint uniform_mvp_matrix_location;  
    GLint uniform_diffuse_texture_sampler_location;  
    GLint uniform_specular_texture_sampler_location;  
    GLint uniform_normal_texture_sampler_location;  
  
    GLint uniform_MV3x3_location;  
  
    GLint uniform_LightPosition_worldspace;  
  
    GLuint diffuse_texture_id;  
    GLuint specular_texture_id;  
    GLuint normal_texture_id;  
  
  
    GLuint tangent_buffer_id;  
    GLuint bitangent_buffer_id;  
  
    //MVP矩阵  
    QVector3D camera_pos = {4,4,-0};  
    QVector3D look_dir = {0,0,0};  
    QVector3D up_dir = {0,1,0};  
  
    float angle = 45;  
    float aspect;  
  
    //距离相机的位置,渲染范围:0.1-100  
    float near_plane = 0.1f;  
    float far_plane = 100.f;  
  
    QtOBJLoader objLoader;  
    std::vector<QVector3D> model_vertices;  
    std::vector<QVector2D> model_uvs;  
    std::vector<QVector3D> model_normals;  
    std::vector<QVector3D> model_tangent;  
    std::vector<QVector3D> model_bitangent;  
  
    std::vector<QVector3D>model_index_vertices;  
    std::vector<QVector2D>model_index_uvs;  
    std::vector<QVector3D>model_index_normals;  
    std::vector<QVector3D> model_index_tangent;  
    std::vector<QVector3D> model_index_bitangent;  
  
    std::vector<unsigned short>model_index;  
};  
  
  
#endif //OPENGL_LEARNING_MY_GLWIDGET_H

my_glwidget.cpp

//  
// Created by liuhang on 2025/9/16.  
//  
  
#include "my_glwidget.h"  
#include<string>  
#include<fstream>  
#include<sstream>  
#include<iostream>  
#include<filesystem>  
#include<QThread>  
#include<QImage>  
#include"vboindex.hpp"  
#include"my_texture.hpp"  
  
MyGLWidget::MyGLWidget(QWidget *parent): QOpenGLWidget(parent)  
{  
#if 0  
    timer.setInterval(1);  
    connect(&timer,&QTimer::timeout,this,[this](){        static float count = 0;        count+= 0.01f;        global_i = std::fabs(sin(count));  
        this->update();  
        timer.setInterval(1);        timer.start();    });  
#endif  
  
#if 0  
    timer.setInterval(1);  
    connect(&timer,&QTimer::timeout,this,[this](){        global_i+= 1;        if(fabs(global_i - 100) < 0.1f){            global_i = 0;        }        this->update();  
        timer.setInterval(1);        timer.start();    });  
    timer.start();#endif  
  
    aspect = this->width() * 1.0f / this->height();  
}  
  
MyGLWidget::~MyGLWidget()  {  
    makeCurrent();  
  
    //vao  
    glDeleteVertexArrays(1,&vertex_array_id);  
  
    //坐标顶点vbo  
    glDeleteBuffers(1,&vertex_buffer_id);  
  
    //纹理顶点vbo  
    glDeleteBuffers(1,&uv_buffer_id);  
  
    //法线顶点vbo  
    glDeleteBuffers(1,&normal_buffer_id);  
  
    //切线顶点vbo  
    glDeleteBuffers(1,&tangent_buffer_id);  
  
    //副切线顶点vbo  
    glDeleteBuffers(1,&bitangent_buffer_id);  
  
    //顶点索引ebo  
    glDeleteBuffers(1,&element_buffer_id);  
  
    //shader_program  
    glDeleteShader(shader_program_id);  
  
    //纹理单元  
    glDeleteTextures(0,&diffuse_texture_id);  
    glDeleteTextures(1,&specular_texture_id);  
    glDeleteTextures(2,&normal_texture_id);  
  
    doneCurrent();  
}  
  
void MyGLWidget::initializeGL() {  
    initializeOpenGLFunctions();  
  
    glClearColor(0.0f,0.0f,0.0f,1.0f);  
  
    //启动深度测试  
    glEnable(GL_DEPTH_TEST);  
    // Accept fragment if it closer to the camera than the former one  
    glDepthFunc(GL_LESS);  
  
    //启用混合  
    glEnable(GL_BLEND);  
    glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); //(1-src)权重  
  
    //关闭背面剔除  
    glDisable(GL_CULL_FACE);  
  
    //加载着色器  
    loadShader("/Users/liuhang/CLionProjects/opengl-learning/opengl2-13-normal-mapping/shader/shader.vert",  
               "/Users/liuhang/CLionProjects/opengl-learning/opengl2-13-normal-mapping/shader/shader.frag");  
  
    //加载模型  
    loadModel();  
  
    //加载索引  
    loadEbo();  
  
    //===============vao===============  
    glGenVertexArrays(1,&vertex_array_id);  
    glBindVertexArray(vertex_array_id);  
  
    //===============模型坐标数据vbo===============  
    glGenBuffers(1,&vertex_buffer_id);  
    glBindBuffer(GL_ARRAY_BUFFER,vertex_buffer_id);  
    glBufferData(GL_ARRAY_BUFFER, sizeof(QVector3D) * model_index_vertices.size(),model_index_vertices.data(),GL_STATIC_DRAW);  
  
    //===============顶点纹理数据vbo===============  
    glGenBuffers(1,&uv_buffer_id);  
    glBindBuffer(GL_ARRAY_BUFFER,uv_buffer_id);  
    glBufferData(GL_ARRAY_BUFFER,sizeof(QVector3D) * model_index_uvs.size(),model_index_uvs.data(),GL_STATIC_DRAW);  
    //===============加载纹理===============  
    diffuse_texture_id = loadDDS("/Users/liuhang/CLionProjects/opengl-learning/resource/diffuse.DDS");  
    specular_texture_id = loadDDS("/Users/liuhang/CLionProjects/opengl-learning/resource/specular.DDS");  
    normal_texture_id = loadBmpTexture("/Users/liuhang/CLionProjects/opengl-learning/resource/normal.bmp");  
    //设置纹理uniform  
    uniform_diffuse_texture_sampler_location = glGetUniformLocation(shader_program_id,"diffuse_texture_sampler");  
    uniform_specular_texture_sampler_location = glGetUniformLocation(shader_program_id,"specular_texture_sampler");  
    uniform_normal_texture_sampler_location = glGetUniformLocation(shader_program_id,"normal_texture_sampler");  
  
    //===============顶点法线数据vbo===============  
    glGenBuffers(1,&normal_buffer_id);  
    glBindBuffer(GL_ARRAY_BUFFER,normal_buffer_id);  
    glBufferData(GL_ARRAY_BUFFER,sizeof(QVector3D) * model_index_normals.size(),model_index_normals.data(),GL_STATIC_DRAW);  
  
    //===============切线数据vbo===============  
    glGenBuffers(1,&tangent_buffer_id);  
    glBindBuffer(GL_ARRAY_BUFFER,tangent_buffer_id);  
    glBufferData(GL_ARRAY_BUFFER,sizeof(QVector3D) * model_tangent.size(),model_index_tangent.data(),GL_STATIC_DRAW);  
  
    //===============副切线数据vbo===============  
    glGenBuffers(1,&bitangent_buffer_id);  
    glBindBuffer(GL_ARRAY_BUFFER,bitangent_buffer_id);  
    glBufferData(GL_ARRAY_BUFFER,sizeof(QVector3D) * model_bitangent.size(),model_index_bitangent.data(),GL_STATIC_DRAW);  
  
    //===============顶点索引ebo===============  
    glGenBuffers(1,&element_buffer_id);  
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,element_buffer_id);  
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,model_index.size()*sizeof(unsigned int),model_index.data(),GL_STATIC_DRAW);  
  
    //获取MVP矩阵的uniform  
    uniform_model_matrix_location = glGetUniformLocation(shader_program_id,"model_matrix");  
    uniform_view_matrix_location = glGetUniformLocation(shader_program_id,"view_matrix");  
    uniform_projection_matrix_location = glGetUniformLocation(shader_program_id,"projection_matrix");  
    uniform_mvp_matrix_location = glGetUniformLocation(shader_program_id,"mvp_matrix");  
    uniform_MV3x3_location = glGetUniformLocation(shader_program_id,"MV3x3");  
  
    //获取LightPosition_worldspace的location  
    uniform_LightPosition_worldspace = glGetUniformLocation(shader_program_id,"LightPosition_worldspace");  
  
    //解绑vao  
    glBindVertexArray(0);  
}  
  
void MyGLWidget::paintGL() {  
    //清除颜色缓存和深度缓存  
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
    glBindVertexArray(vertex_array_id);  
  
    //使用着色器程序  
    glUseProgram(shader_program_id);  
  
    //上传MVP矩阵  
    doMVP();  
  
    //绑定vao  
    glBindVertexArray(vertex_array_id);  
  
    //加载layout = 0 : vertexs  
    glEnableVertexAttribArray(0);  
    glBindBuffer(GL_ARRAY_BUFFER,vertex_buffer_id);  
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,nullptr);  
  
    //加载layout = 1 : uvs  
    glEnableVertexAttribArray(1);  
    glBindBuffer(GL_ARRAY_BUFFER,uv_buffer_id);  
    glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,0,nullptr);  
  
    //加载layout = 2 : normals  
    glEnableVertexAttribArray(2);  
    glBindBuffer(GL_ARRAY_BUFFER,normal_buffer_id);  
    glVertexAttribPointer(2,3,GL_FLOAT,GL_FALSE,0,nullptr);  
  
    //加载layout = 3 : tangents  
    glEnableVertexAttribArray(3);  
    glBindBuffer(GL_ARRAY_BUFFER,tangent_buffer_id);  
    glVertexAttribPointer(3,3,GL_FLOAT,GL_FALSE,0,nullptr);  
  
    //加载layout = 4 : bitangents  
    glEnableVertexAttribArray(4);  
    glBindBuffer(GL_ARRAY_BUFFER,bitangent_buffer_id);  
    glVertexAttribPointer(4,3,GL_FLOAT,GL_FALSE,0,nullptr);  
  
    //绑定纹理单元0:diffuse  
    glActiveTexture(GL_TEXTURE0);  
    glBindTexture(GL_TEXTURE_2D,diffuse_texture_id);  
    glUniform1i(uniform_diffuse_texture_sampler_location, 0);  
  
    //绑定纹理单元1:specular  
    glActiveTexture(GL_TEXTURE1);  
    glBindTexture(GL_TEXTURE_2D,specular_texture_id);  
    glUniform1i(uniform_specular_texture_sampler_location,1);  
  
    //绑定纹理单元2:normal  
    glActiveTexture(GL_TEXTURE2);  
    glBindTexture(GL_TEXTURE_2D,normal_texture_id);  
    glUniform1i(uniform_normal_texture_sampler_location,2);  
  
    //上传光照位置 uniform    QVector3D light_pos = {4.0f,4.0f,4.0f} ;  
    glUniform3f(uniform_LightPosition_worldspace,light_pos.x(),light_pos.y(),light_pos.z());  
  
    //绑定EBO  
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,element_buffer_id);  
    //使用EBO绘制  
    glDrawElements(GL_TRIANGLES,model_index.size(),GL_UNSIGNED_SHORT,nullptr);  
  
    //解绑layout  
    glDisableVertexAttribArray(0);  
    glDisableVertexAttribArray(1);  
    glDisableVertexAttribArray(2);  
    glDisableVertexAttribArray(3);  
    glDisableVertexAttribArray(4);  
  
    //解绑着色器  
    glUseProgram(0);  
}  
  
void MyGLWidget::resizeGL(int w, int h) {  
    glViewport(0, 0, w, h);  
}  
  
void MyGLWidget::loadShader(std::string const& vertex_shader_path,std::string const& fragment_shader_path) {  
    //创建顶点着色器  
    vertex_shader_id = glCreateShader(GL_VERTEX_SHADER);  
  
    //读取shader.vert  
    std::string vertex_shader_code;  
    std::ifstream vertex_shader_stream(vertex_shader_path,std::ios::in);  
    if(vertex_shader_stream.is_open()){  
        std::stringstream ss;  
        ss << vertex_shader_stream.rdbuf();  
  
        vertex_shader_code = ss.str();  
        vertex_shader_stream.close();  
    }  
    else{  
        std::cout << "current path = " << std::filesystem::current_path() << std::endl;  
        if (!std::filesystem::exists(vertex_shader_path)) {  
            std::cerr << "File not found: " << vertex_shader_path << std::endl;  
        }  
  
        std::cerr << "read vertex_shader fail!" << std::endl;  
        return;  
    }  
  
    //加载vertex着色器程序代码  
    const char* vertex_shader_code_pointer = vertex_shader_code.c_str();  
    glShaderSource(vertex_shader_id,1,&vertex_shader_code_pointer,nullptr);  
  
    //编译vertex shader  
    glCompileShader(vertex_shader_id);  
  
    //查看vertex编译结果  
    GLint Result = GL_FALSE;  
    int infoLogLength;  
    glGetShaderiv(vertex_shader_id,GL_COMPILE_STATUS,&Result);  
    glGetShaderiv(vertex_shader_id,GL_INFO_LOG_LENGTH,&infoLogLength);  
    if(infoLogLength > 0){  
        std::vector<char> vertex_shader_error_message(infoLogLength+1);  
        glGetShaderInfoLog(vertex_shader_id, infoLogLength, nullptr, &vertex_shader_error_message[0]);  
        std::cout << "vertex shader:" << &vertex_shader_error_message[0] << std::endl;  
    }  
  
    //创建片段着色器  
    fragment_shader_id = glCreateShader(GL_FRAGMENT_SHADER);  
  
    //读取shader.frag  
    std::string fragment_shader_code;  
    std::ifstream fragment_shader_stream(fragment_shader_path,std::ios::in);  
    if(fragment_shader_stream.is_open()){  
        std::stringstream ss;  
        ss << fragment_shader_stream.rdbuf();  
  
        fragment_shader_code = ss.str();  
        fragment_shader_stream.close();  
    }  
    else{  
        std::cout << "current path = " << std::filesystem::current_path() << std::endl;  
        if (!std::filesystem::exists(vertex_shader_path)) {  
            std::cerr << "File not found: " << vertex_shader_path << std::endl;  
        }  
  
        std::cerr << "read fragment_shader fail!" << std::endl;  
    }  
  
    //加载fragment着色器程序代码  
    const char* fragment_shader_code_pointer = fragment_shader_code.c_str();  
    glShaderSource(fragment_shader_id,1,&fragment_shader_code_pointer,nullptr);  
  
    //编译fragment shader  
    glCompileShader(fragment_shader_id);  
  
    //查看fragment编译结果  
    glGetShaderiv(fragment_shader_id,GL_COMPILE_STATUS,&Result);  
    glGetShaderiv(fragment_shader_id,GL_INFO_LOG_LENGTH,&infoLogLength);  
    if(infoLogLength > 0){  
        std::vector<char> fragment_shader_error_message(infoLogLength+1);  
        glGetShaderInfoLog(fragment_shader_id, infoLogLength, nullptr, &fragment_shader_error_message[0]);  
        std::cout << "fragment shader compile error:" << &fragment_shader_error_message[0] << std::endl;  
    }  
  
    //创建着色器程序  
    shader_program_id = glCreateProgram();  
  
    //附加着色器到程序  
    glAttachShader(shader_program_id,vertex_shader_id);  
    glAttachShader(shader_program_id,fragment_shader_id);  
  
    //链接shader  
    glLinkProgram(shader_program_id);  
  
    //检查程序链接结果  
    glGetProgramiv(shader_program_id,GL_LINK_STATUS,&Result);  
    glGetProgramiv(shader_program_id,GL_INFO_LOG_LENGTH,&infoLogLength);  
    if(infoLogLength > 0){  
        std::vector<char>program_error_message(infoLogLength + 1);  
        glGetProgramInfoLog(shader_program_id,infoLogLength,nullptr,&program_error_message[0]);  
        std::cout << "program link:" << &program_error_message[0] << std::endl;  
    }  
  
    //删除着色器编译结果  
    glDeleteShader(vertex_shader_id);  
    glDeleteShader(fragment_shader_id);  
}  
  
void MyGLWidget::doMVP()  
{  
    //模型矩阵  
    QMatrix4x4 model_matrix;  
    model_matrix.setToIdentity();  
  
    //lookAt  
    QMatrix4x4 view_matrix;  
    view_matrix.lookAt(camera_pos,look_dir,up_dir);  
  
    //投影矩阵  
    QMatrix4x4 projection_matrix;  
    projection_matrix.perspective(angle,aspect,near_plane,far_plane);  
  
    //计算mvp矩阵  
    QMatrix4x4 mvp_matrix = projection_matrix * view_matrix * model_matrix;  
    QMatrix3x3 MV3x3_matrix = (view_matrix * model_matrix).toGenericMatrix<3,3>();  
  
    //矩阵传入shader  
    glUniformMatrix4fv(uniform_model_matrix_location,1,GL_FALSE,model_matrix.data());  
    glUniformMatrix4fv(uniform_view_matrix_location,1,GL_FALSE,view_matrix.data());  
    glUniformMatrix4fv(uniform_projection_matrix_location,1,GL_FALSE,projection_matrix.data());  
    glUniformMatrix4fv(uniform_mvp_matrix_location,1,GL_FALSE,mvp_matrix.data());  
    glUniformMatrix3fv(uniform_MV3x3_location,1,GL_FALSE,MV3x3_matrix.data());  
  
}  
  
void MyGLWidget::loadModel()  
{  
    //读取模型  
    bool ret = objLoader.loadOBJ("/Users/liuhang/CLionProjects/opengl-learning/resource/cylinder.obj");  
    if(!ret){  
        std::cerr << "读取模型错误" << std::endl;  
        return;  
    }  
  
    //获取属性  
    model_vertices = objLoader.vertices();  
    model_uvs = objLoader.uvs();  
    model_normals = objLoader.normals();  
}  
  
void MyGLWidget::loadEbo()  
{  
    //计算切线和副切线  
    computeTangentBasis(model_vertices,model_uvs,model_normals,model_tangent,model_bitangent);  
  
    //加载使用索引后的顶点数据  
    indexVBO_TBN(model_vertices,model_uvs,model_normals,model_tangent,model_bitangent,  
                 model_index,model_index_vertices,model_index_uvs,model_index_normals,model_index_tangent,model_index_bitangent);  
}  
  
void MyGLWidget::computeTangentBasis(  
        std::vector<QVector3D> &vertices,  
        std::vector<QVector2D> &uvs,  
        std::vector<QVector3D> &normals,  
        std::vector<QVector3D> &tangents,  
        std::vector<QVector3D> &bitangents  
) {  
    for (int i = 0; i < vertices.size(); i += 3) {  
        // 使用Qt的向量类  
        QVector3D v0 = vertices[i];  
        QVector3D v1 = vertices[i+1];  
        QVector3D v2 = vertices[i+2];  
  
        QVector2D uv0 = uvs[i];  
        QVector2D uv1 = uvs[i+1];  
        QVector2D uv2 = uvs[i+2];  
  
        // 计算边向量和UV差  
        QVector3D deltaPos1 = v1 - v0;  
        QVector3D deltaPos2 = v2 - v0;  
        QVector2D deltaUV1 = uv1 - uv0;  
        QVector2D deltaUV2 = uv2 - uv0;  
  
        // 计算切线/副切线  
        float r = 1.0f / (deltaUV1.x() * deltaUV2.y() - deltaUV1.y() * deltaUV2.x());  
        QVector3D tangent = (deltaPos1 * deltaUV2.y() - deltaPos2 * deltaUV1.y()) * r;  
        QVector3D bitangent = (deltaPos2 * deltaUV1.x() - deltaPos1 * deltaUV2.x()) * r;  
  
        // 正交化处理(可选)  
        QVector3D normal = normals[i];  
        tangent = (tangent - normal * QVector3D::dotProduct(normal, tangent)).normalized();  
        bitangent = QVector3D::crossProduct(normal, tangent).normalized();  
  
        // 存储结果  
        tangents.push_back(tangent);  
        tangents.push_back(tangent);  
        tangents.push_back(tangent);  
        bitangents.push_back(bitangent);  
        bitangents.push_back(bitangent);  
        bitangents.push_back(bitangent);  
    }  
}  
  
GLint MyGLWidget::loadBmpTexture(QString const& bmp_path) {  
    QImage texture_image(bmp_path);  
    if (texture_image.isNull()) {  
        std::cerr << bmp_path .toStdString() << "路径错误或文件损坏!" << std::endl;  
        return 0;  
    }  
  
    // 转换为RGB格式并垂直翻转(OpenGL坐标原点在左下角)  
    QImage gl_image = texture_image.convertToFormat(QImage::Format_RGB888);  
  
    GLuint textureID;  
    glGenTextures(1, &textureID);  
    glBindTexture(GL_TEXTURE_2D, textureID);  
  
    // 上传纹理数据  
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, gl_image.width(), gl_image.height(), 0,  
                 GL_RGB, GL_UNSIGNED_BYTE, gl_image.bits());  
  
    // 设置纹理参数  
  
    //===============设置纹理过滤===============  
  
#if 0  
    //GL_NEAREST:最近像素点采样  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);  
#endif  
  
#if 0  
    //GL_LINEAR:线性过滤  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);  
#endif  
  
#if 1  
    //GL_MIPMAP_NEAREST: mipmap  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_NEAREST);  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);  
  
    //生成mipmap,多加这一行  
    glGenerateMipmap(GL_TEXTURE_2D);  
#endif  
    //解绑  
    glBindTexture(GL_TEXTURE_2D, 0); // 解绑  
  
    return textureID;  
}

objloader.hpp

#ifndef QT_OBJLOADER_H  
#define QT_OBJLOADER_H  
  
#include <QVector3D>  
#include <QVector2D>  
#include <QMatrix4x4>  
#include <vector>  
#include <QFile>  
#include <QTextStream>  
  
class QtOBJLoader {  
public:  
    // 加载OBJ文件,返回是否成功  
    bool loadOBJ(const QString& path);  
  
    // 获取解析后的数据  
    const std::vector<QVector3D>& vertices() const { return m_vertices; }  
    const std::vector<QVector2D>& uvs() const { return m_uvs; }  
    const std::vector<QVector3D>& normals() const { return m_normals; }  
  
private:  
    std::vector<QVector3D> m_vertices;  // 顶点坐标  
    std::vector<QVector2D> m_uvs;       // 纹理坐标  
    std::vector<QVector3D> m_normals;   // 法线向量  
};  
  
#endif // QT_OBJLOADER_H

objloader.cpp

#include "objloader.hpp"  
#include <QDebug>  
  
bool QtOBJLoader::loadOBJ(const QString& path) {  
    QFile file(path);  
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {  
       qWarning() << "Failed to open file:" << path;  
       return false;  
    }  
  
    std::vector<QVector3D> temp_vertices;  
    std::vector<QVector2D> temp_uvs;  
    std::vector<QVector3D> temp_normals;  
    std::vector<unsigned int> vertexIndices, uvIndices, normalIndices;  
  
    QTextStream in(&file);  
    while (!in.atEnd()) {  
       QString line = in.readLine().trimmed();  
       if (line.isEmpty() || line.startsWith("#")) continue;  
  
       QStringList parts = line.split(" ", Qt::SkipEmptyParts);  
       if (parts.isEmpty()) continue;  
  
       QString type = parts[0];  
       if (type == "v") {  // 顶点坐标  
          if (parts.size() < 4) continue;  
          QVector3D vertex(parts[1].toFloat(), parts[2].toFloat(), parts[3].toFloat());  
          temp_vertices.push_back(vertex);  
       }  
       else if (type == "vt") {  // 纹理坐标  
          if (parts.size() < 3) continue;  
          QVector2D uv(parts[1].toFloat(), -parts[2].toFloat());  // 反转V坐标适配DDS纹理  
          temp_uvs.push_back(uv);  
       }  
       else if (type == "vn") {  // 法线向量  
          if (parts.size() < 4) continue;  
          QVector3D normal(parts[1].toFloat(), parts[2].toFloat(), parts[3].toFloat());  
          temp_normals.push_back(normal);  
       }  
       else if (type == "f") {  // 面数据  
          for (int i = 1; i < parts.size(); ++i) {  
             QStringList indices = parts[i].split("/");  
             if (indices.size() >= 1) vertexIndices.push_back(indices[0].toUInt() - 1);  
             if (indices.size() >= 2) uvIndices.push_back(indices[1].toUInt() - 1);  
             if (indices.size() >= 3) normalIndices.push_back(indices[2].toUInt() - 1);  
          }  
       }  
    }  
    file.close();  
  
    // 根据索引填充输出数据  
    for (size_t i = 0; i < vertexIndices.size(); ++i) {  
       if (vertexIndices[i] < temp_vertices.size())  
          m_vertices.push_back(temp_vertices[vertexIndices[i]]);  
       if (uvIndices.size() > i && uvIndices[i] < temp_uvs.size())  
          m_uvs.push_back(temp_uvs[uvIndices[i]]);  
       if (normalIndices.size() > i && normalIndices[i] < temp_normals.size())  
          m_normals.push_back(temp_normals[normalIndices[i]]);  
    }  
  
    return !m_vertices.empty();  
}

vboindex.hpp

#ifndef VBOINDEXER_HPP  
#define VBOINDEXER_HPP  
  
#include <QVector>  
#include <QMap>  
#include <QVector3D>  
#include <QVector2D>  
  
void indexVBO(  
        std::vector<QVector3D> &in_vertices,  
        std::vector<QVector2D> &in_uvs,  
        std::vector<QVector3D> &in_normals,  
        std::vector<unsigned short> &out_indices,  
        std::vector<QVector3D> &out_vertices,  
        std::vector<QVector2D> &out_uvs,  
        std::vector<QVector3D> &out_normals  
);  
  
void indexVBO_TBN(  
       std::vector<QVector3D> &in_vertices,  
       std::vector<QVector2D> &in_uvs,  
       std::vector<QVector3D> &in_normals,  
       std::vector<QVector3D> &in_tangents,  
       std::vector<QVector3D> &in_bitangents,  
       std::vector<unsigned short> &out_indices,  
       std::vector<QVector3D> &out_vertices,  
       std::vector<QVector2D> &out_uvs,  
       std::vector<QVector3D> &out_normals,  
       std::vector<QVector3D> &out_tangents,  
       std::vector<QVector3D> &out_bitangents  
);  
#endif

vboindex.cpp

#include "vboindex.hpp"  
#include <cmath>  
#include <cstring>  
  
// 判断浮点数是否近似相等  
bool is_near(float v1, float v2) {  
    return fabs(v1 - v2) < 0.01f;  
}  
  
// 线性搜索相似顶点(慢速版)  
bool getSimilarVertexIndex(  
        const QVector3D &in_vertex,  
        const QVector2D &in_uv,  
        const QVector3D &in_normal,  
        const std::vector<QVector3D> &out_vertices,  
        const std::vector<QVector2D> &out_uvs,  
        const std::vector<QVector3D> &out_normals,  
        unsigned short &result  
) {  
    for (int i = 0; i < out_vertices.size(); i++) {  
        if (  
                is_near(in_vertex.x(), out_vertices[i].x()) &&  
                is_near(in_vertex.y(), out_vertices[i].y()) &&  
                is_near(in_vertex.z(), out_vertices[i].z()) &&  
                is_near(in_uv.x(), out_uvs[i].x()) &&  
                is_near(in_uv.y(), out_uvs[i].y()) &&  
                is_near(in_normal.x(), out_normals[i].x()) &&  
                is_near(in_normal.y(), out_normals[i].y()) &&  
                is_near(in_normal.z(), out_normals[i].z())  
                ) {  
            result = i;  
            return true;  
        }  
    }  
    return false;  
}  
  
// 快速搜索相似顶点(基于哈希表)  
struct PackedVertex {  
    QVector3D position;  
    QVector2D uv;  
    QVector3D normal;  
    bool operator<(const PackedVertex &that) const {  
        return memcmp(this, &that, sizeof(PackedVertex)) > 0;  
    }  
};  
  
bool getSimilarVertexIndex_fast(  
        const PackedVertex &packed,  
        QMap<PackedVertex, unsigned short> &VertexToOutIndex,  
        unsigned short &result  
) {  
    auto it = VertexToOutIndex.find(packed);  
    if (it == VertexToOutIndex.end()) {  
        return false;  
    } else {  
        result = it.value();  
        return true;  
    }  
}  
  
// 主函数:索引化VBO(优化版)  
void indexVBO(  
        std::vector<QVector3D> &in_vertices,  
        std::vector<QVector2D> &in_uvs,  
        std::vector<QVector3D> &in_normals,  
        std::vector<unsigned short> &out_indices,  
        std::vector<QVector3D> &out_vertices,  
        std::vector<QVector2D> &out_uvs,  
        std::vector<QVector3D> &out_normals  
) {  
    QMap<PackedVertex, unsigned short> VertexToOutIndex;  
    for (int i = 0; i < in_vertices.size(); i++) {  
        PackedVertex packed = {in_vertices[i], in_uvs[i], in_normals[i]};  
        unsigned short index;  
        if (getSimilarVertexIndex_fast(packed, VertexToOutIndex, index)) {  
            out_indices.push_back(index);  
        } else {  
            out_vertices.push_back(in_vertices[i]);  
            out_uvs.push_back(in_uvs[i]);  
            out_normals.push_back(in_normals[i]);  
            unsigned short newindex = out_vertices.size() - 1;  
            out_indices.push_back(newindex);  
            VertexToOutIndex[packed] = newindex;  
        }  
    }  
}  
  
// 支持切线空间的版本  
void indexVBO_TBN(  
       std::vector<QVector3D> &in_vertices,  
       std::vector<QVector2D> &in_uvs,  
       std::vector<QVector3D> &in_normals,  
       std::vector<QVector3D> &in_tangents,  
       std::vector<QVector3D> &in_bitangents,  
       std::vector<unsigned short> &out_indices,  
       std::vector<QVector3D> &out_vertices,  
       std::vector<QVector2D> &out_uvs,  
       std::vector<QVector3D> &out_normals,  
       std::vector<QVector3D> &out_tangents,  
       std::vector<QVector3D> &out_bitangents  
) {  
    QMap<PackedVertex, unsigned short> VertexToOutIndex;  
    for (int i = 0; i < in_vertices.size(); i++) {  
        PackedVertex packed = {in_vertices[i], in_uvs[i], in_normals[i]};  
        unsigned short index;  
        if (getSimilarVertexIndex_fast(packed, VertexToOutIndex, index)) {  
            out_indices.push_back(index);  
            out_tangents[index] += in_tangents[i];  
            out_bitangents[index] += in_bitangents[i];  
        } else {  
            out_vertices.push_back(in_vertices[i]);  
            out_uvs.push_back(in_uvs[i]);  
            out_normals.push_back(in_normals[i]);  
            out_tangents.push_back(in_tangents[i]);  
            out_bitangents.push_back(in_bitangents[i]);  
            unsigned short newindex = out_vertices.size() - 1;  
            out_indices.push_back(newindex);  
            VertexToOutIndex[packed] = newindex;  
        }  
    }  
}

my_texture.hpp

#pragma once  
  
#include<QOpenGLFunctions>  
  
#define FOURCC_DXT1 0x31545844 // Equivalent to "DXT1" in ASCII  
#define FOURCC_DXT3 0x33545844 // Equivalent to "DXT3" in ASCII  
#define FOURCC_DXT5 0x35545844 // Equivalent to "DXT5" in ASCII  
  
GLuint loadDDS(const char * imagepath);

my_texture.cpp

  
#include"my_texture.hpp"  
  
GLuint loadDDS(const char * imagepath){  
  
    unsigned char header[124];  
  
    FILE *fp;  
  
    /* try to open the file */  
    fp = fopen(imagepath, "rb");  
    if (fp == NULL){  
        printf("%s could not be opened. Are you in the right directory ? Don't forget to read the FAQ !\n", imagepath); getchar();  
        return 0;  
    }  
  
    /* verify the type of file */  
    char filecode[4];  
    fread(filecode, 1, 4, fp);  
    if (strncmp(filecode, "DDS ", 4) != 0) {  
        fclose(fp);  
        return 0;  
    }  
  
    /* get the surface desc */  
    fread(&header, 124, 1, fp);  
  
    unsigned int height      = *(unsigned int*)&(header[8 ]);  
    unsigned int width       = *(unsigned int*)&(header[12]);  
    unsigned int linearSize  = *(unsigned int*)&(header[16]);  
    unsigned int mipMapCount = *(unsigned int*)&(header[24]);  
    unsigned int fourCC      = *(unsigned int*)&(header[80]);  
  
  
    unsigned char * buffer;  
    unsigned int bufsize;  
    /* how big is it going to be including all mipmaps? */  
    bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;  
    buffer = (unsigned char*)malloc(bufsize * sizeof(unsigned char));  
    fread(buffer, 1, bufsize, fp);  
    /* close the file pointer */  
    fclose(fp);  
  
    unsigned int components  = (fourCC == FOURCC_DXT1) ? 3 : 4;  
    unsigned int format;  
    switch(fourCC)  
    {  
        case FOURCC_DXT1:  
            format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;  
            break;  
        case FOURCC_DXT3:  
            format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;  
            break;  
        case FOURCC_DXT5:  
            format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;  
            break;  
        default:  
            free(buffer);  
            return 0;  
    }  
  
    // Create one OpenGL texture  
    GLuint textureID;  
    glGenTextures(1, &textureID);  
  
    // "Bind" the newly created texture : all future texture functions will modify this texture  
    glBindTexture(GL_TEXTURE_2D, textureID);  
    glPixelStorei(GL_UNPACK_ALIGNMENT,1);  
  
    unsigned int blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;  
    unsigned int offset = 0;  
  
    /* load the mipmaps */  
    for (unsigned int level = 0; level < mipMapCount && (width || height); ++level)  
    {  
        unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize;  
        glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height,  
                               0, size, buffer + offset);  
  
        offset += size;  
        width  /= 2;  
        height /= 2;  
  
        // Deal with Non-Power-Of-Two textures. This code is not included in the webpage to reduce clutter.  
        if(width < 1) width = 1;  
        if(height < 1) height = 1;  
  
    }  
  
    free(buffer);  
  
    return textureID;  
  
  
}

shader.vert

#version 330 core  
  
layout(location = 0) in vec3 vertexPosition_modelspace;  
layout(location = 1) in vec2 vertexUV;  
layout(location = 2) in vec3 vertexNormal_modelspace;  
layout(location = 3) in vec3 vertexTangent_modelspace;  
layout(location = 4) in vec3 vertexBitangent_modelspace;  
  
out vec2 UV;  
out vec3 Position_worldspace;  
out vec3 EyeDirection_cameraspace;  
out vec3 LightDirection_cameraspace;  
  
out vec3 LightDirection_tangentspace;  
out vec3 EyeDirection_tangentspace;  
  
uniform mat4 model_matrix;  
uniform mat4 view_matrix;  
uniform mat4 projection_matrix;  
uniform mat4 mvp_matrix;  
uniform mat3 MV3x3;  
  
uniform vec3 LightPosition_worldspace;  
  
void main()  
{  
    //裁剪坐标系  
    gl_Position = mvp_matrix * vec4(vertexPosition_modelspace,1.0f);  
  
    //顶点(世界坐标系)  
    Position_worldspace = (model_matrix * vec4(vertexPosition_modelspace,1)).xyz;  
  
    //顶点(摄像机坐标系)  
    vec3 vertexPosition_cameraspace = (view_matrix * model_matrix * vec4(vertexPosition_modelspace,1)).xyz;  
    //视线方向(摄像机坐标系),原点指向摄像机,假设摄像机看向原点  
    EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;  
  
    //光源所在位置(摄像机坐标系)  
    vec3 LightPosition_cameraspace = (view_matrix * vec4(LightPosition_worldspace,1)).xyz;  
    //光源方向(摄像机坐标系)顶点指向光源,假设摄像机看向原点  
    LightDirection_cameraspace = LightPosition_cameraspace + EyeDirection_cameraspace;  
  
    //uv赋值  
    UV = vertexUV;  
  
    //法线、切线、副切线从模型坐标系->摄像机坐标系 (MV3x3实际上是view_matrix * model_matrix)  
    vec3 vertexTangent_cameraspace = MV3x3 * vertexTangent_modelspace;  
    vec3 vertexBitangent_cameraspace = MV3x3 * vertexBitangent_modelspace;  
    vec3 vertexNormal_cameraspace = MV3x3 * vertexNormal_modelspace;  
  
    //使用法线、切线、副切线计算出TBN矩阵,transpose是求转置  
    mat3 TBN = transpose(mat3(  
    vertexTangent_cameraspace,  
    vertexBitangent_cameraspace,  
    vertexNormal_cameraspace  
    ));  
  
    //光源方向(切线坐标系): TBN * 摄像机坐标系  
    LightDirection_tangentspace = TBN * LightDirection_cameraspace;  
    //视觉方向(切线坐标系): TBN * 摄像机坐标系  
    EyeDirection_tangentspace =  TBN * EyeDirection_cameraspace;  
}

shader.frag

#version 330 core  
in vec2 UV;  
in vec3 Position_worldspace;  
in vec3 EyeDirection_cameraspace;  
in vec3 LightDirection_cameraspace;  
out vec4 color;  
in vec3 LightDirection_tangentspace;  
in vec3 EyeDirection_tangentspace;  
  
uniform sampler2D diffuse_texture_sampler;  
uniform sampler2D specular_texture_sampler;  
uniform sampler2D normal_texture_sampler;  
  
uniform mat4 model_matrix;  
uniform mat4 view_matrix;  
uniform mat4 mvp_matrix;  
uniform mat3 MV3x3;  
  
uniform vec3 LightPosition_worldspace;  
  
  
void main(){  
  
    //光源颜色:白光  
    vec3 LightColor = vec3(1.0,1.0,1.0);  
  
    //环境光强度  
    float LightPower = 120.0;  
  
    //材质属性:反射光  
    vec3 MaterialDiffuseColor = texture(diffuse_texture_sampler, UV ).rgb;  
    //材质属性:环境光  
    vec3 MaterialAmbientColor = vec3(0.5,0.5,0.5) * MaterialDiffuseColor;  
    //材质属性:镜面光  
    vec3 MaterialSpecularColor = texture(specular_texture_sampler, UV ).rgb * 0.3;  
  
    //法线贴图的值需要转换:normal = 2.0 * color - 1.0  
    vec3 TextureNormal_tangentspace = normalize(texture(normal_texture_sampler, vec2(UV.x,UV.y)).rgb*2.0 - 1.0);  
  
    //离光源的距离  
    float distance = length( LightPosition_worldspace - Position_worldspace );  
  
    //法线方向  
    vec3 n = TextureNormal_tangentspace;  
    //光源方向  
    vec3 l = normalize(LightDirection_tangentspace);  
    //法线和光源的夹角  
    float cosTheta = clamp( dot( n,l ), 0,1 );  
  
    //视线方向  
    vec3 E = normalize(EyeDirection_tangentspace);  
    //反射光方向  
    vec3 R = reflect(-l,n);  
    //视线和反射光的夹角  
    float cosAlpha = clamp( dot( E,R ), 0,1 );  
    //镜面光强度  
    float shiness = pow(cosAlpha,5);  
  
    color.xyz =  
    //环境光  
    MaterialAmbientColor +  
    //反射光  
    MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance)+  
    //镜面光  
    MaterialSpecularColor * LightColor * LightPower * shiness / (distance*distance);  
    color.a = 1.0;  
}