Opengl实时渲染器(二)光照计算与模型导入
- 05 4 月, 2023
- by
- pladmin
引言
没啥可说的,这节直接来复现光照。
Part1. Lambert模型
我们假设场景中某处有一个无限小点光源,它的位置是L;那么我们物体上每一点O的着色结果就是:
$$L_{out} = L_{light} * \rho_d *dot(n,l)$$
其中L_light是光照强度,rho_d是漫反射分量(此时就理解成纹理的颜色),n是表面法线,l是片元指向光源的单位向量。
所以我们可以分析出,首先我们需要向shader中传入光源的强度以及光源的位置,然后在片元着色器中要获得当前片元的法线和位置,这样就可以得到最终的光线出射结果了。
添加光源信息:
while (!glfwWindowShouldClose(window)) //开始渲染循环
{
processInput(window); //自定义的检测键盘输入函数
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
shader.setVec3("lightPos", glm::vec3(0, 3, 2));
shader.setVec3("lightColor", glm::vec3(1, 1, 1));
将光源位置置于前上方,然后光强设置为白色。接下来改一下顶点着色器和片元着色器:
///////////////////////顶点着色器///////////////////////
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aUV;
layout (location = 2) in vec3 aNormal;
out vec3 normal;
out vec2 texcoord;
out vec3 FragPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
normal = aNormal;
texcoord = aUV;
}
///////////////////////片元着色器///////////////////////
#version 330 core
in vec3 normal;
in vec2 texcoord;
in vec3 FragPos;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform vec3 lightPos;
uniform vec3 lightColor;
void main()
{
vec3 ambient = lightColor * 0.15;
vec3 norm = normalize(normal);
vec3 lightDir = normalize(lightPos - FragPos);
vec3 diffuse = vec3(texture(ourTexture, texcoord));
float cosine = max(dot(norm, lightDir), 0.0);
FragColor = vec4(ambient + lightColor * diffuse * cosine, 1.0);
}
顶点着色器需要把顶点所处的世界坐标传给片元,这样管线插值后片元可以得到自己所在的世界坐标;片元着色器就分别计算纹理颜色、法线和光源方向点乘得到的cosine项、光源强度,将三者乘在一起便是lambert模型了。但是对于接收不到光照的地方是全黑,显然也不是很合适,因此可以再额外加一个恒定值ambient(环境光),让全黑的地方也能有一点亮度。

这样看起来没什么问题。但是如果我们给模型旋转90°,就会发现猫腻:

为什么旋转90度以后,表面就黑了呢?这是因为我们旋转顶点的时候,只改变了它的世界坐标,却没有改变它的法线方向。原本面朝上的那个四边面,经过旋转90度后面朝屏幕,然而此时的法线仍然朝上,所以猫腻就出现了。解决办法就是在变换顶点的时候,将法线一并变换。
然而从数学推理结果来看,顶点乘以一个模型-世界矩阵就可以从模型空间转移到世界空间,但是法线却不是乘一个模型-世界矩阵就可以变换成功的,它要乘的是模型-世界矩阵的逆转置矩阵,所以我们这样改:
///////////////////////顶点着色器///////////////////////
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aUV;
layout (location = 2) in vec3 aNormal;
out vec3 normal;
out vec2 texcoord;
out vec3 FragPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
normal = mat3(transpose(inverse(model))) * aNormal;;
texcoord = aUV;
}
此时无论如何旋转立方体,光照结果都是正确的了:

Part2. Phong模型
Lambert模型只有漫反射分量,而Phong模型正式引入高光分量。我们假设光线的镜面反射向量是R,片元指向相机的向量是V:
$$L_{out} = L_{light} * (\rho_d + \rho_s*\frac{dot(R,V)^n}{dot(n,l)}) *dot(n,l)$$
$$L_{out} = L_{light} * (\rho_d * dot(n,l) + \rho_s*dot(R,V)^n)$$
这个式子比Lambert模型多了一个R和一个V,其中R可以根据入射光向量l和法线n计算得到,而V的计算需要相机的位置参数,这个参数就必须由客户端从外部传进shader了。
while (!glfwWindowShouldClose(window)) //开始渲染循环
{
processInput(window); //自定义的检测键盘输入函数
...
shader.setVec3("lightPos", glm::vec3(0, 3, 2));
shader.setVec3("lightColor", glm::vec3(1, 1, 1));
shader.setVec3("cameraPos", camera->cameraPos);
...
在shader中,我们直接抄phong模型的公式:
#version 330 core
in vec3 normal;
in vec2 texcoord;
in vec3 FragPos;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform vec3 lightPos;
uniform vec3 lightColor;
uniform vec3 cameraPos;
void main()
{
vec3 ambient = lightColor * 0.15;
vec3 norm = normalize(normal);
vec3 lightDir = normalize(lightPos - FragPos);
vec3 rho_d = vec3(texture(ourTexture, texcoord));
float cosine = max(dot(norm, lightDir), 0.0);
float rho_s = 0.5;
vec3 viewDir = normalize(cameraPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float cosine2 = pow(max(dot(viewDir, reflectDir), 0.0), 16);
FragColor = vec4(ambient + lightColor * (rho_d * cosine + rho_s * cosine2), 1.0);
}
这样就能看到模型表面有明显的高光了。

Part3. 材质参数
我们重新审视一下phong模型,就会发现有四个参数是可以自己定义的:rho_d(漫反射颜色)、rho_s(高光分量强度)、n(高光亮点凝聚度)、ambient(环境光添补)。现在我们是将rho_s、n、ambient的值在shader里写死成固定值了,但事实上不同材质这些参数的值都是不一样的。因此,我们必须将他们做成可调整的材质参数。
其实方法很简单,自定义一个材质结构体,把各个物体的材质信息写入,然后将它以uniform的格式传入片元着色器即可。
#version 330 core
in vec3 normal;
in vec2 texcoord;
in vec3 FragPos;
out vec4 FragColor;
uniform vec3 lightPos;
uniform vec3 lightColor;
uniform vec3 cameraPos;
struct Material {
vec3 ambient;
sampler2D rho_d_tex;
vec3 rho_s;
float shininess_n;
};
uniform Material material;
void main()
{
vec3 norm = normalize(normal);
vec3 lightDir = normalize(lightPos - FragPos);
vec3 rho_d = vec3(texture(material.rho_d_tex, texcoord));
float cosine = max(dot(norm, lightDir), 0.0);
vec3 viewDir = normalize(cameraPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float cosine2 = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess_n);
FragColor = vec4(material.ambient + lightColor * (rho_d * cosine + material.rho_s * cosine2), 1.0);
}
材质信息传递部分:
shader.setVec3("material.ambient", glm::vec3(0.0f, 0.0f, 0.0f));
shader.setInt("material.rho_d_tex", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TEX[0]);
shader.setVec3("material.rho_s", glm::vec3(0.3f, 0.3f, 0.3f));
shader.setFloat("material.shininess_n", 64.0f);
glBindTexture(GL_TEXTURE_2D, TEX[0]);
glBindVertexArray(VAO[0]);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
glBindTexture(GL_TEXTURE_2D, TEX[1]);
glBindVertexArray(VAO[1]);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
这样就完成了材质的封装和外部导入。

但是这里我发现了一个不太合理的现象:没有正面对着光源的那几个面都是纯灰白色,不能显示出其纹理细节。经过思考后我提出一个疑问:ambient是不是也应该在漫反射贴图上采样,而不是直接设一个纯色?(当然我也不确定这个理论是否正确,只是按照我这个理论做出来的结果看起来不错)
所以我对shader又做了一点修改,让ambient乘一个纹理颜色,此时的结果就比较合理了,背光的部分依旧能看清纹理 而不是一片灰:

按照learnOpengl的思路,接下来要对光源也定义rho_d、rho_s、ambient这些参数,我也觉得不是很合理,所以这部分先跳过了;后面一节要做高光贴图,但我也没找到比较合适的素材,所以就先不做了。
不过这里要提一下多张纹理的绑定。既然要shader同时获取到漫反射贴图和高光贴图,那么我们就需要绑定两张纹理。首先GPU中一共有16个纹理槽位(就像寄存器一样数量固定,随用随拿),我们首先调用glActiveTexture(GL_TEXTUREk)来激活k号纹理槽位,然后调用glBindTexture(GL_TEXTURE_2D, id)将编号为id的纹理绑定到对应的k号槽位中。这样gpu就成功将多张纹理载入,然后我们要调用shader.setInt(“material.xxx”, k)表示shader中的某个采样器使用的是k号槽位的贴图,这样就可以在shader中用多张贴图了。
Part5. 直接光
前面几节我们一直在讨论材质的着色,即“某个片元接收到某个方向的光照后,该输出什么颜色”。这节开始我们讨论光源的类型,即“某个片元会接收到什么方向的、什么强度的光照”。
由于光照类型多样,每种光照也有很多参数,所以我们也要将light像material一样封装起来。首先我们封装直接光:
struct DirectionLight {
vec3 dir;
vec3 color;
};
uniform DirectionLight dl[6];
void main()
{
vec3 norm = normalize(normal);
FragColor = vec4(0,0,0,1);
//vec3 lightDir = normalize(lightPos - FragPos);
vec3 rho_d = vec3(texture(material.rho_d_tex, texcoord));
vec3 viewDir = normalize(cameraPos - FragPos);
for(int i = 0; i < 6; i++){
vec3 lightDir = normalize(-dl[i].dir);
float cosine = max(dot(norm, lightDir), 0.0);
vec3 reflectDir = reflect(-lightDir, norm);
float cosine2 = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess_n);
FragColor += vec4(dl[i].color * (rho_d * cosine + material.rho_s * cosine2), 1.0);
}
FragColor += vec4(material.ambient * rho_d, 0);
}
直接光只需要两个参数:方向向量、光强,不需要定义位置。这里我们一共允许传入6个直接光源,在片元着色器中,我们分别计算若干个光源的光源方向,然后代入phong模型中计算着色贡献,最后将所有灯的着色贡献全部加和在一起即可。

Part6. 带衰减的点光源
这里要新定义的点光源其实和之前那个无限点光源大同小异,不同的是这里的点光源要带衰减效应,距离光源越远的物体,接收到的光强越微弱。具体能保留多少百分比的光强由以下式子表示:
$$F_{att} = \frac{1}{K_c + K_l * d + K_q * d^2}$$
分母的三个K分别是常数项、一次项、二次项的系数,分别表示基础衰减、一次下降的速率、二次下降的速率。我们可以用constant、linear、quadratic来表示它们:
struct DirectionLight {
vec3 dir;
vec3 color;
};
struct PointLight{
vec3 pos;
vec3 color;
float constant;
float linear;
float quadratic;
};
uniform DirectionLight dl[6];
uniform PointLight pl[6];
void main()
{
vec3 norm = normalize(normal);
FragColor = vec4(0,0,0,1);
vec3 rho_d = vec3(texture(material.rho_d_tex, texcoord));
vec3 viewDir = normalize(cameraPos - FragPos);
for(int i = 0; i < 6; i++){
if(dl[i].color == vec3(0,0,0)) continue;
vec3 lightDir = normalize(-dl[i].dir);
float cosine = max(dot(norm, lightDir), 0.0);
vec3 reflectDir = reflect(-lightDir, norm);
float cosine2 = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess_n);
FragColor += vec4(dl[i].color * (rho_d * cosine + material.rho_s * cosine2), 0.0);
}
for(int i = 0; i < 6; i++){
if(pl[i].color == vec3(0,0,0)) continue;
vec3 lightDir = normalize(pl[i].pos - FragPos);
float distance = length(pl[i].pos - FragPos);
float cosine = max(dot(norm, lightDir), 0.0);
vec3 reflectDir = reflect(-lightDir, norm);
float cosine2 = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess_n);
vec3 pl_color = pl[i].color / (pl[i].constant + pl[i].linear * distance + pl[i].quadratic * distance * distance);
FragColor += vec4(pl_color * (rho_d * cosine + material.rho_s * cosine2), 0.0);
}
FragColor += vec4(material.ambient * rho_d, 0);
}
这样场景中就可以同时存在若干盏点光源和平行光了:

按照learnOpengl的顺序,接下来要做聚光灯(spot-light)了。但是我对聚光灯的印象一直不好,做场景打光也基本上不怎么用探照灯,因此这一小节直接跳过。
Part7. 模型导入
说句实话,这个opengl渲染器是我的第四个渲染器,基本上每个渲染器都要和模型导入打交道,要么是自己手搓obj_parser,要么是学习tiny_objloader,现在learnOpengl又给我推荐了个叫Assimp的屌炸天模型库,以至于我实在不想一点一点去分析模型导入的逻辑和思路了。所以这一节不会再像之前导入模型那样长篇大论分析代码。
首先简单说一下Assimp库,这是一个极强的模型parse库,可以分析多达10+种格式的模型文件,不仅可以提炼出顶点、法线、uv、顶点色等信息,甚至能解析出骨骼、蒙皮、帧动画的内容。
在建模软件中,我们经常会绑定父子物体,子物体还可以绑定更低一级的子物体,如此循环往复,因此Assimp分析模型的规律主要就是递归分析——整个场景是一个父节点,然后遍历第一层子物体,如果发现子物体内还有子物体,就继续遍历第二层子物体,直到遍历到的物体是纯网格物体(即没有更低级的子物体)了为止。
所以我们可以理解为,Assimp库可以通过递归,把模型文件中的所有网格物体找出来,并将每个网格物体的指针交给我们。言外之意就是,我们要对每个网格物体绑定一个单独的VAO、VBO、EBO。因此,我们第一步要做的事就是对之前定义的myMesh.h做出修改。
对于myMesh类,我发现自己犯了一个很可笑的错误,就是vector<vertice> vertice_struct是可以直接作为顶点指针传入VBO的buffer的,不需要再额外定义一个float* vertices作为中介,因为两者的空间组织是完全一样的。。。除此之外,我们把indices和texture也做成vector形式的数组。最后,我们定义一个setup函数,将mesh的所有顶点信息、索引信息、纹理信息全部绑定到位。
#pragma once
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <vector>
using namespace std;
struct vertice {
glm::vec3 pos;
glm::vec2 uv;
glm::vec3 normal;
glm::vec3 tangent;
glm::vec3 bitangent;
};
class myMesh {
public:
myMesh() {
}
myMesh(glm::vec3 pos_offset, glm::vec3 size, unsigned tex_id) {
Cube(pos_offset, size, tex_id);
}
myMesh(vector<vertice> vertices, vector<unsigned int> indices, vector<unsigned int> textures)
{
this->vertice_struct = vertices;
this->indice_struct = indices;
this->texture_struct = textures;
}
void setup() {
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertice_struct.size() * sizeof(vertice), &vertice_struct[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indice_struct.size() * sizeof(unsigned int), &indice_struct[0], GL_STATIC_DRAW);
// 顶点位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertice), (void*)0);
// 顶点纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertice), (void*)offsetof(vertice, uv));
// 顶点法线
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(vertice), (void*)offsetof(vertice, normal));
glBindVertexArray(0);
}
void Cube(glm::vec3 pos_offset, glm::vec3 size, unsigned int tex_id) {
vertice_struct.push_back(vertice{ glm::vec3(-0.5f, 0.5f, -0.5f),glm::vec2(0.0f, 1.0f),glm::vec3(0.0f,1.0f,0.0f) });
......
for (int i = 0; i < vertice_struct.size(); i++) {
vertice_struct[i].pos = vertice_struct[i].pos * size + pos_offset;
}
for (unsigned int i = 0; i < indice_struct.size(); i++)
indice_struct.push_back(i);
texture_struct.push_back(tex_id);
}
private:
vector<vertice> vertice_struct;
vector<unsigned int> indice_struct;
vector<unsigned int> texture_struct;
unsigned int VAO, VBO, EBO;
};
现在VAO、VAO、EBO都可以绑定好了,唯一剩下的问题是:怎么定义该myMesh的渲染函数?
其实渲染函数最麻烦的地方就是处理贴图。在即将渲染的时候,我们必须将所有贴图全部都绑定到shader中去。然而shader中需要区分:哪些是漫反射纹理,哪些是高光纹理,哪些是法线贴图等等。所以我们的纹理类型不能只有一个unsigned int,还需要有个string来标识它的类型:
struct myTexture {
unsigned int id;
string type;
string path;
};
有了myTexture来描述纹理id、类型和路径后,我们定义一下Draw函数:
void Draw(myShader shader) {
shader.setBool("material.diffuse_texture_use", false);
shader.setBool("material.specular_texture_use", false);
for (unsigned int i = 0; i < texture_struct.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i);
string name = texture_struct[i].type;
shader.setInt(("material." + name).c_str(), i);
shader.setBool(("material." + name + "_use").c_str(), true);
glBindTexture(GL_TEXTURE_2D, texture_struct[i].id);
}
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indice_struct.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
我们用xxx_texture_use表示是否存在xxx贴图,用xxx_texture表示xxx贴图。这里我其实约定了一件事:一个材质只能拥有一个贴图,暂时不考虑多张贴图混合的情况。至于片元着色器也需要将上述的参数进行修改,将漫反射分量分为diffuse和diffuse_texture两部分,如果diffuse_texture_use是true,则漫反射量从贴图上进行采样;如果是false,则直接取diffuse的值作为漫反射量:
struct Material {
vec3 ambient;
vec3 diffuse;
bool diffuse_texture_use;
sampler2D diffuse_texture;
vec3 specular;
bool specular_texture_use;
sampler2D specular_texture;
float shininess_n;
};
uniform Material material;
...
void main()
{
vec3 norm = normalize(normal);
FragColor = vec4(0,0,0,1);
vec3 rho_d;
if(material.diffuse_texture_use == true) rho_d = vec3(texture(material.diffuse_texture, texcoord));
else rho_d = material.diffuse;
vec3 rho_s;
if(material.specular_texture_use == true) rho_s = vec3(texture(material.specular_texture, texcoord));
else rho_s = material.specular;
vec3 viewDir = normalize(cameraPos - FragPos);
...
}
好了,这样我们就将网格的构造、绑定、渲染全部封装好了,并且为shader提供了相应的接口。
那么下一件事,就是请Assimp出马了。这部分代码实在是懒得分析了,就直接粘贴learnOpengl的代码了:
Code Viewer. Source code: src/3.model_loading/1.model_loading/model_loading.cpp (learnopengl.com)
代码如下:
#pragma once
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "myMesh.h"
#include "myShader.h"
#include "myTexture.h"
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
unsigned int TextureFromFile(const char* path, const string& directory, bool gamma = false);
class myModel
{
public:
// model data
vector<myTexture> textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once.
vector<myMesh> meshes;
string directory;
bool gammaCorrection;
// constructor, expects a filepath to a 3D model.
myModel(string const& path, bool gamma = false) : gammaCorrection(gamma)
{
loadModel(path);
Setup();
}
void Setup() {
for (unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Setup();
}
// draws the model, and thus all its meshes
void Draw(myShader& shader)
{
for (unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
private:
// loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector.
void loadModel(string const& path)
{
// read file via ASSIMP
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
// check for errors
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
// retrieve the directory path of the filepath
directory = path.substr(0, path.find_last_of('/'));
// process ASSIMP's root node recursively
processNode(scene->mRootNode, scene);
}
// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).
void processNode(aiNode* node, const aiScene* scene)
{
// process each mesh located at the current node
for (unsigned int i = 0; i < node->mNumMeshes; i++)
{
// the node object only contains indices to index the actual objects in the scene.
// the scene contains all the data, node is just to keep stuff organized (like relations between nodes).
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// after we've processed all of the meshes (if any) we then recursively process each of the children nodes
for (unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
myMesh processMesh(aiMesh* mesh, const aiScene* scene)
{
// data to fill
vector<vertice> vertices;
vector<unsigned int> indices;
vector<myTexture> textures;
// walk through each of the mesh's vertices
for (unsigned int i = 0; i < mesh->mNumVertices; i++)
{
vertice vertex;
glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first.
// positions
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.pos = vector;
// normals
if (mesh->HasNormals())
{
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.normal = vector;
}
// texture coordinates
if (mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
glm::vec2 vec;
// a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't
// use models where a vertex can have multiple texture coordinates so we always take the first set (0).
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.uv = vec;
// tangent
vector.x = mesh->mTangents[i].x;
vector.y = mesh->mTangents[i].y;
vector.z = mesh->mTangents[i].z;
vertex.tangent = vector;
// bitangent
vector.x = mesh->mBitangents[i].x;
vector.y = mesh->mBitangents[i].y;
vector.z = mesh->mBitangents[i].z;
vertex.bitangent = vector;
}
else
vertex.uv = glm::vec2(0.0f, 0.0f);
vertices.push_back(vertex);
}
// now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.
for (unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// retrieve all indices of the face and store them in the indices vector
for (unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
// process materials
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
// we assume a convention for sampler names in the shaders. Each diffuse texture should be named
// as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER.
// Same applies to other texture as the following list summarizes:
// diffuse: texture_diffuseN
// specular: texture_specularN
// normal: texture_normalN
// 1. diffuse maps
vector<myTexture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "diffuse_texture");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. specular maps
vector<myTexture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "specular_texture");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
// 3. normal maps
std::vector<myTexture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "normal_texture");
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height maps
std::vector<myTexture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "height_texture");
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
// return a mesh object created from the extracted mesh data
return myMesh(vertices, indices, textures);
}
// checks all material textures of a given type and loads the textures if they're not loaded yet.
// the required info is returned as a Texture struct.
vector<myTexture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName)
{
vector<myTexture> textures;
for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
// check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
bool skip = false;
for (unsigned int j = 0; j < textures_loaded.size(); j++)
{
if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
break;
}
}
if (!skip)
{ // if texture hasn't been loaded already, load it
myTexture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecessary load duplicate textures.
}
}
return textures;
}
};
unsigned int TextureFromFile(const char* path, const string& directory, bool gamma)
{
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
然后在main函数中setup、draw即可。这部分是很繁杂的细节调整内容,就不多赘述了。
实话说,我怀疑里面存在一个严重的问题:一个mesh只对应一个材质编号。在建模软件里一个物体对应多个材质是很常见的事情,然而调用了assimp的库方法后,一个mesh和一个材质一一对应,这可能是一个相当大的隐患。具体会不会翻车目前尚不可知,不过日后要警惕这件事。






