写在前面
原文链接。原文应该是github上的一个项目,本文主要用来记录一些知识点和自己遇到的问题。
和箱子模型告别
所以,让我们导入一个由真正的艺术家所创造的模型,替代我这个天才的作品(你要承认,这些箱子可能是你看过的最漂亮的立方体了),测试一下我们的实现吧。由于我不想让我占太多的功劳,我会偶尔让别的艺术家也加入我们,这次我们将会加载Crytek的游戏孤岛危机(Crysis)中的原版纳米装(Nanosuit)。这个模型被输出为一个.obj文件以及一个.mtl文件,.mtl文件包含了模型的漫反射、镜面光和法线贴图(这个会在后面学习到),你可以在这里下载到(稍微修改之后的)模型,注意所有的纹理和模型文件应该位于同一个目录下,以供加载纹理。
现在在代码中,声明一个Model对象,将模型的文件位置传入。接下来模型应该会自动加载并(如果没有错误的话)在渲染循环中使用它的Draw函数来绘制物体,这样就可以了。不再需要缓冲分配、属性指针和渲染指令,只需要一行代码就可以了(真爽!)。接下来如果你创建一系列着色器,其中片段着色器仅仅输出物体的漫反射纹理颜色,最终的结果看上去会是这样的:
你可以在这里找到完整的源码。
这里有一个很坑的问题,可能让你得不到正确结果……事实上我也费了很多时间来debug,下面来说一下。
坑点
主要是由0号纹理单元引起的……
原文这里的代码其实是有点问题的,我们看一下他这里给出的着色器代码:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture_diffuse1;
void main()
{
FragColor = texture(texture_diffuse1, TexCoords);
}
再看我们在Mesh对象的Draw方法中设置的采样器名称:
很明显这里是对不上的,那么为什么我们还能看到模型呢?是因为0号纹理单元默认是一直激活的,在GLSL中的纹理采样器默认都会绑定到0号纹理单元,而我们在代码中恰好又绑定了纹理对象到这个纹理单元,所以能看到模型。那么我们可以改一下这个函数,让其激活并设置非零号纹理单元,就可以解决问题了——实际上并不能,你可以看一下texture对象的构造函数,其内调用了glBindTexture函数,也就是说只要使用了纹理对象,0号纹理单元上就一定绑定了某个纹理对象,那么在GLSL中进行采样时就一定要小心,采样的结果可能并不是黑色!出于这个原因我决定修改命名规则,我们使用sampler2D数组,同时传递两个整数给gpu,告诉它我们使用了多少个漫反射贴图和镜面光贴图。
第二个坑点是图片y轴的翻转问题。我们在纹理章节已经学过了:OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。那么我们利用这个函数翻转图片的y轴:
但是还记得我们在导入obj文件的时候设置了什么吗?
aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标。如果你感觉贴图很奇怪的话,可能就是这里的问题,两个地方都翻转了等于没有翻转。
回到主题
修改后的代码如下:
m e s h : mesh: mesh:
#pragma once
#ifndef MESH_H
#define MESH_H
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "shader.h"
#include <vector>
#include "texture.h"
#include <string>
#include <iostream>
using std::vector;
using std::string;
using std::cout;
using std::max;
struct Vertex
{
// 位置 法向量 纹理坐标
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
class Mesh
{
public:
/* 网格数据 */
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
/* 函数 */
Mesh(const vector<Vertex>& vertices, const vector<unsigned int>& indices, const vector<Texture>& textures);
void Draw(const Shader& shader);
private:
/* 渲染数据 */
unsigned int VAO, VBO, EBO;
void setupMesh();
};
#endif // !MESH_H
#include "mesh.h"
Mesh::Mesh(const vector<Vertex>& vertices, const vector<unsigned int>& indices, const vector<Texture>& textures) :
vertices(vertices), indices(indices), textures(textures)
{
setupMesh();
}
void Mesh::setupMesh()
{
// 初始化VAO VBO EBO
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
// 绑定VAO
glBindVertexArray(VAO);
// 绑定VBO 复制顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
// 绑定EBO 复制索引数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
// 顶点位置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
glEnableVertexAttribArray(0);
// 顶点法线
// C++内置的offsetof函数 能自动返回结构对象中 某变量距离结构体对象首地址的偏移值:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
glEnableVertexAttribArray(1);
// 顶点纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
glEnableVertexAttribArray(2);
// 解绑VAO
glBindVertexArray(0);
}
unsigned int maxDiffuseNr = 0;
unsigned int maxSpecularNr = 0;
void Mesh::Draw(const Shader& shader)
{
// 当前漫反射纹理和镜面光纹理的编号
unsigned int diffuseNr = 0;
unsigned int specularNr = 0;
for (unsigned int i = 0; i < textures.size(); i++)
{
// 激活纹理单元 并绑定
textures[i].use(i);
// 获取纹理序号和类型
string number;
string type = textures[i].getName();
if (type == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if (type == "texture_specular")
number = std::to_string(specularNr++);
shader.setInt("material." + type + "[" + number + "]", i);
}
shader.setInt(string("material.") + "texture_diffuse_num", diffuseNr);
shader.setInt(string("material.") + "texture_specular_num", specularNr);
maxDiffuseNr = max(maxDiffuseNr, diffuseNr);
maxSpecularNr = max(maxSpecularNr, specularNr);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// always good practice to set everything back to defaults once configured.
glActiveTexture(GL_TEXTURE0);
}
m o d e l : model: model:
#pragma once
#ifndef MODEL_H
#define MODEL_H
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "mesh.h"
#include <unordered_map>
using std::unordered_map;
class Model
{
/* 函数 */
public:
Model(const string& path)
{
loadModel(path);
}
void Draw(const Shader& shader);
private:
/* 模型数据 */
vector<Mesh> meshes;
/* 模型数据所在目录 */
string directory;
/* 记录已经加载过的纹理 */
vector<Texture> texturesLoaded;
/* 纹理文件到索引的哈希表 */
unordered_map<string, unsigned int> texturesHashTable;
void loadModel(const string& path);
void processNode(aiNode* node, const aiScene* scene);
Mesh processMesh(aiMesh* mesh, const aiScene* scene);
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, const string& typeName);
};
#endif // !MODEL_H
#include "model.h"
#include <iostream>
using std::cout;
using std::endl;
extern unsigned int maxDiffuseNr;
extern unsigned int maxSpecularNr;
bool firstDraw = false;
void Model::Draw(const Shader& shader)
{
for (unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
if (!firstDraw)
{
cout << "First Draw finish\n";
cout << "Max diffuse texture number is " << maxDiffuseNr << '\n';
cout << "Max specular texture number is " << maxSpecularNr << '\n';
firstDraw = 1;
}
}
void Model::loadModel(const string& path)
{
Assimp::Importer import;
// 读取模型文件 第二个参数用于后期处理
const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
// 是否读取成功
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
// 获取文件目录
directory = path.substr(0, path.find_last_of('/'));
// 从场景根节点开始递归处理所有节点
processNode(scene->mRootNode, scene);
// 打印obj的信息
cout << "Loading success!\n";
cout << "This model have " << meshes.size() << " meshes\n\n";
}
void Model::processNode(aiNode* node, const aiScene* scene)
{
// 处理节点的所有网格
for