【OpenGL】openGL 法线贴图
参考文章:https://www.opengl-tutorial.org/cn/intermediate-tutorials/tutorial-13-normal-mapping/
一、法线纹理
下图是一张法线纹理:
每个纹素的RGB值实际上表示的是XYZ向量:颜色的分量取值范围为0到1,而向量的分量取值范围是-1到1;可以建立从纹素到法线的简单映射
normal = (2*color)-1 // on each component
由于法线基本都是指向”曲面外侧”的(按照惯例,X轴朝右,Y轴朝上),因此法线纹理整体呈蓝色。
法线纹理的映射方式和漫反射纹理相似。麻烦之处在于如何将法线从各三角形局部空间(切线空间tangent space,亦称图像空间image space)变换到模型空间(着色计算所采用的空间)。
二、切线和副切线(Tangent and Bitangent)
大家对矩阵已经十分熟悉了,应该知道定义一个空间(本例是切线空间)需要三个向量。现在Up向量已经有了,即法线:可用Blender生成,或由一个简单的叉乘计算得到。下图中蓝色箭头代表法线(法线贴图整体颜色也恰好是蓝色)。
然后是切线T:垂直于法线的向量。但这样的切线有很多个:
这么多切线中该选哪个呢?理论上哪一个都行。但我们必须保持连续一致性,以免衔接处出现瑕疵。标准的做法是将切线方向和纹理空间对齐:
定义一组基需要三个向量,因此我们还得计算副切线B(本可以随便选一条切线,但选定垂直于另外两条轴的切线,计算会方便些)。
算法如下:记三角形的两条边为deltaPos1和deltaPos2,deltaUV1和deltaUV2是对应的UV坐标下的差值;则问题可用如下方程表示:
deltaPos1 = deltaUV1.x * T + deltaUV1.y * B
deltaPos2 = deltaUV2.x * T + deltaUV2.y * B
求解T和B就得到了切线和副切线!(代码见下文)
已知T、B、N向量之后,即可得下面这个漂亮的矩阵,完成从切线空间到模型空间的变换:
[TxBxNxTyByNyTzBzNz]\begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix}TxTyTzBxByBzNxNyNz
有了TBN矩阵,我们就能把(从法线纹理中获取的)法线变换到模型空间。
可我们需要的却是从切线空间到模型空间的变换,法线则保持不变。所有计算均在切线空间中进行,不会对其他计算产生影响。
只需对上述矩阵求逆即可得逆变换。这个矩阵(正交阵,即各向量相互正交的矩阵,参见下文”延伸阅读”小节)的逆矩阵恰好也就是其转置矩阵,计算十分简单:
invTBN = transpose(TBN)
亦即:
[TxTyTzBxByBzNxNyNz]\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \\ N_x & N_y & N_z \end{bmatrix}TxBxNxTyByNyTzBzNz
三、准备VBO
3.1 计算切线和副切线
我们需要为整个模型计算切线、副切线和法线。我们用一个单独的函数完成这些计算
void computeTangentBasis(// inputsstd::vector & vertices,std::vector & uvs,std::vector & normals,// outputsstd::vector & tangents,std::vector & bitangents
){
为每个三角形计算边(deltaPos)和deltaUV
for ( int i=0; i<vertices.size(); i+=3){// Shortcuts for verticesglm::vec3 & v0 = vertices[i+0];glm::vec3 & v1 = vertices[i+1];glm::vec3 & v2 = vertices[i+2];// Shortcuts for UVsglm::vec2 & uv0 = uvs[i+0];glm::vec2 & uv1 = uvs[i+1];glm::vec2 & uv2 = uvs[i+2];// Edges of the triangle : postion deltaglm::vec3 deltaPos1 = v1-v0;glm::vec3 deltaPos2 = v2-v0;// UV deltaglm::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 bitangentsout_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, // attribute3, // sizeGL_FLOAT, // typeGL_FALSE, // normalized?0, // stride(void*)0 // array buffer offset
);// 2nd attribute buffer : UVs
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, uvbuffer);
glVertexAttribPointer(1, // attribute2, // sizeGL_FLOAT, // typeGL_FALSE, // normalized?0, // stride(void*)0 // array buffer offset
);// 3rd attribute buffer : normals
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(2, // attribute3, // sizeGL_FLOAT, // typeGL_FALSE, // normalized?0, // stride(void*)0 // array buffer offset
);// 4th attribute buffer : tangents
glEnableVertexAttribArray(3);
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer);
glVertexAttribPointer(3, // attribute3, // sizeGL_FLOAT, // typeGL_FALSE, // normalized?0, // stride(void*)0 // array buffer offset
);// 5th attribute buffer : bitangents
glEnableVertexAttribArray(4);
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer);
glVertexAttribPointer(4, // attribute3, // sizeGL_FLOAT, // typeGL_FALSE, // normalized?0, // stride(void*)0 // array buffer offset
);// Index buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);// Draw the triangles !
glDrawElements(GL_TRIANGLES, // modeindices.size(), // countGL_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 spacevec3 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也必须位于同一空间中。大功告成!
五、结果
这是目前得到的结果,您可以看到:
- 砖块看上去凹凸不平,这是因为砖块表面法线变化比较剧烈
- 水泥部分看上去很平整,这是因为这部分的法线纹理全都是蓝色
六、延伸阅读
6.1 正交化(Orthogonalization)
顶点着色器中,为了计算速度,我们没有进行矩阵求逆,而是进行了转置。这只有当矩阵表示的空间正交时才成立,而这个矩阵还不是正交的。好在这个问题很容易解决:只需在computeTangentBasis()末尾让切线与法线垂直。
t = glm::normalize(t - n * glm::dot(n, t));
这个公式有点难理解,来看看图:
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)。镜面纹理看起来像这样:
请注意,由于如上镜面纹理中没有镜面分量,水泥部分均呈黑色。
七、完整代码
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;
}