模型加载库
一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。由于Assimp的数据结构保持不变,不论导入的是什么种类的文件格式,它都能够将我们从这些不同的文件格式中抽象出来,用同一种方式访问我们需要的数据。
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:
![](https://i-blog.csdnimg.cn/blog_migrate/f08b0130b20bbcbedd0cb8b1e6853713.png)
和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的(见你好,三角形)。
最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
所以,我们需要做的第一件事是将一个物体加载到Scene对象中,遍历节点,获取对应的Mesh对象(我们需要递归搜索每个节点的子节点),并处理每个Mesh对象来获取顶点数据、索引以及它的材质属性。最终的结果是一系列的网格数据,我们会将它们包含在一个Model对象中。
网格
当使用建模工具对物体建模的时候,艺术家通常不会用单个形状创建出整个模型。通常每个模型都由几个子模型/形状组合而成。组合模型的每个单独的形状就叫做一个网格(Mesh)。比如说有一个人形的角色:艺术家通常会将头部、四肢、衣服、武器建模为分开的组件,并将这些网格组合而成的结果表现为最终的模型。一个网格是我们在OpenGL中绘制物体所需的最小单位(顶点数据、索引和材质属性)。一个模型(通常)会包括多个网格。
网格
通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。我们从上一节中学到,网格(Mesh)代表的是单个的可绘制实体,我们现在先来定义一个我们自己的网格类。
首先我们来回顾一下我们目前学到的知识,想想一个网格最少需要什么数据。一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。
既然我们有了一个网格类的最低需求,我们可以在OpenGL中定义一个顶点了:
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
我们将所有需要的向量储存到一个叫做Vertex的结构体中,我们可以用它来索引每个顶点属性。除了Vertex结构体之外,我们还需要将纹理数据整理到一个Texture结构体中。
struct Texture {
unsignedint id;
string type;
};
我们储存了纹理的id以及它的类型,比如是漫反射贴图或者是镜面光贴图。
知道了顶点和纹理的实现,我们可以开始定义网格类的结构了:
class Mesh {
public:
/* 网格数据 */
std::vector<Vertex> vertices;
std::vector<unsignedint> indices;
std::vector<Texture> textures;
/* 函数 */
Mesh(std::vector<Vertex> vertices, std::vector<unsignedint> indices, std::vector<Texture> textures);
voidDraw(Shader shader);
private:
/* 渲染数据 */
unsignedint VAO, VBO, EBO;
/* 函数 */
void setupMesh();
};
在构造器Mesh函数中,我们将所有必须的数据赋予了网格,我们在setupMesh函数中初始化缓冲,并最终使用Draw函数来绘制网格。注意我们将一个着色器传入了Draw函数中,将着色器传入网格类中可以让我们在绘制之前设置一些uniform(像是链接采样器到纹理单元)。
构造器的内容非常易于理解。我们只需要使用构造器的参数设置类的公有变量就可以了。我们在构造器中还调用了setupMesh函数:
Mesh(vector<Vertex> vertices, vector<unsignedint> indices, vector<Texture> textures)
{
//理解为将传入的三组数据拷贝一份;
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();//用于初始化缓冲;
}
我们接下来讨论setupMesh函数。
初始化
由于有了构造器,我们现在有一大列的网格数据用于渲染。在此之前我们还必须配置正确的缓冲,并通过顶点属性指针定义顶点着色器的布局。现在你应该对这些概念都很熟悉了,但我们这次会稍微有一点变动,使用结构体中的顶点数据:
voidsetupMesh(){
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsignedint),
&indices[0], GL_STATIC_DRAW);
// 顶点位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// 顶点法线
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// 顶点纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
C++结构体有一个很棒的特性,它们的内存布局是连续的(Sequential)。也就是说,如果我们将结构体作为一个数据数组使用,那么它将会以顺序排列结构体的变量,这将会直接转换为我们在数组缓冲中所需要的float(实际上是字节)数组。比如说,如果我们有一个填充后的Vertex结构体,那么它的内存布局将会等于:
Vertex vertex;
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f);
vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
由于有了这个有用的特性,我们能够直接传入一大列的Vertex结构体的指针作为缓冲的数据,它们将会完美地转换为glBufferData所能用的参数:
//这里传入一系列Vertex结构体的指针作为缓冲的对象;
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
自然sizeof运算也可以用在结构体上来计算它的字节大小。这个应该是32字节的(8个float * 每个4字节)。
结构体的另外一个很好的用途是它的预处理指令offsetof(s, m),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)。这正好可以用在定义glVertexAttribPointer函数中的偏移参数:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
//(void*)offsetof(Vertex,Normal)表示偏移量是从顶点结构体Vertex的Normal位置开始;
偏移量现在是使用offsetof来定义了,在这里它会将法向量的字节偏移量设置为结构体中法向量的偏移量,也就是3个float,即12字节。注意,我们同样将步长参数设置为了Vertex结构体的大小。
使用这样的一个结构体不仅能够提供可读性更高的代码,也允许我们很容易地拓展这个结构。如果我们希望添加另一个顶点属性,我们只需要将它添加到结构体中就可以了。由于它的灵活性,渲染的代码不会被破坏。
渲染
我们需要为Mesh类定义最后一个函数,它的Draw函数。在真正渲染这个网格之前,我们需要在调用glDrawElements函数之前先绑定相应的纹理。然而,这实际上有些困难,我们一开始并不知道这个网格(如果有的话)有多少纹理、纹理是什么类型的。所以我们该如何在着色器中设置纹理单元和采样器呢?
为了解决这个问题,我们需要设定一个命名标准:每个漫反射纹理被命名为texture_diffuseN,每个镜面光纹理应该被命名为texture_specularN,其中N的范围是1到纹理采样器最大允许的数字。比如说我们对某一个网格有3个漫反射纹理,2个镜面光纹理,它们的纹理采样器应该之后会被调用:
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
//这里这么多但实际不一定全部需要,或者有的还更多,在后续对纹理单元进行命名时要注意;
根据这个标准,我们可以在着色器中定义任意需要数量的纹理采样器,如果一个网格真的包含了(这么多)纹理,我们也能知道它们的名字是什么。根据这个标准,我们也能在一个网格中处理任意数量的纹理,开发者也可以自由选择需要使用的数量,他只需要定义正确的采样器就可以了(虽然定义少的话会有点浪费绑定和uniform调用)。
最终的渲染代码:
voidDraw(Shader shader){
unsignedint diffuseNr = 1;
unsignedint specularNr = 1;
for(unsignedint i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // 在绑定之前激活相应的纹理单元// 获取纹理序号(diffuse_textureN 中的 N)string number;
string name = textures[i].type;
if(name == "texture_diffuse")
number = std::to_string(diffuseNr++);
elseif(name == "texture_specular")
number = std::to_string(specularNr++);
shader.setInt(("material." + name + number).c_str(), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
glActiveTexture(GL_TEXTURE0);
// 绘制网格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
我们首先计算了每个纹理类型的N-分量,并将其拼接到纹理类型字符串上,来获取对应的uniform名称。接下来我们查找对应的采样器,将它的位置值设置为当前激活的纹理单元,并绑定纹理。这也是我们在Draw函数中需要着色器的原因。我们也将"material."添加到了最终的uniform名称中,因为我们希望将纹理储存在一个材质结构体中(这在每个实现中可能都不同)。
下面是Mesh类目前的代码:
#ifndef MESH_H
#define MESH_H
#include <E:\OpenGl\glm\glm-master\glm\glm.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\matrix_transform.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\type_ptr.hpp>
#include <string>
#include <vector>
#include <iostream>
#include "shader s.h"
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};//我们将所有需要的向量储存到一个叫做Vertex的结构体中,我们可以用它来索引每个顶点属性。
//我们还要将纹理数据整合到一个Texture结构体中;
//这里我们存储了纹理的id,以及纹理的类型;
struct Texture {
unsigned int id;//纹理的id;
std::string type;//纹理的类型,比如漫反射贴图或者镜面光贴图;texture_specular/texture_diffuse;
std::string path;//纹理的路径;
};
//Mesh类;
class Mesh {
public:
//网格数据;Mesh Data;
std::vector<Vertex> vertices;//顶点数组;
std::vector<unsigned int> indices;//索引数组;
std::vector<Texture> textures;//纹理数组;
//函数;(Mesh函数会传入三个数组);
Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();//启用setupMesh函数,进行初始化缓冲!!!;
}
//接下来会进行Draw,要调用着色器;[将着色器传入网格类中可以让我们在绘制之前设置一些uniform(像是链接采样器到纹理单元)]
void Draw(Shader shader)//调用着色器;
{
//包括两个大步:
// 1.将纹理传入对应的纹理单元;
// 2.根据前面求得的VAO,EBO等缓冲绘制网格上的所有三角形;
//我们把每一个镜面光贴图命名为texture_specularN;(N是1,2,3...);同样把每一个漫反射贴图命名为texture_diffuseN;
unsigned int diffuseNr = 1;//分别对应material中对应纹理采样器的数字;
unsigned int specularNr = 1;
for (unsigned int i = 0; i < textures.size(); i++)//对每一个传入的纹理;
{
glActiveTexture(GL_TEXTURE0 + i);//先激活每一个纹理单元,可以直接在后面加上i;
std::string number;
std::string name = textures[i].type;//获取纹理类型;
if (name == "texture_diffuse")//如果类型为漫反射贴图;
number = std::to_string(diffuseNr++);
else if (name == "texture_specular")//如果为镜面光贴图;
number = std::to_string(specularNr++);//就把指定镜面光贴图采样器的个数设为下一个;
shader.setInt(("material."+name+number).c_str(), i);//将此张纹理的采样器设置为当前的纹理单元;
glBindTexture(GL_TEXTURE_2D, textures[i].id);//绑定纹理到GL_TEXTURE_2D对象;
//为后面的绘制做准备;
}
//绘制网格;
glBindVertexArray(VAO);//绑定顶点数组;(会绑定EBO和VBO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);//根据索引数组绘制三角形;
glBindVertexArray(0);//解除绑定;
}
private:
//渲染部分;
unsigned int VAO, VBO, EBO;//建立三种数组;
//setupMesh函数;(进行初始化缓冲);
void setupMesh()
{
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);//绑定VAO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(Vertex), &indices[0], GL_STATIC_DRAW);
//激活顶点,法线,以及纹理属性;
glVertexAttribPointer(0, 3, GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0);
glEnableVertexAttribArray(0);
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);
glBindVertexArray(0);//解除绑定;
}
};
#endif
模型
class Model {
public:
//构造器;加载函数;
Model(std::string path)
{
loadModel(path);
}
void Draw(const Shader& shader)
{
for (unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
private:
/*模型数据*/ //包括文件目录,网格,以及一个存放纹理路径的容器;
std::string directory;//记录传入文件的目录;
std::vector<Mesh> meshes;//用于记录传入的模型文件的网格的容器,类型为Mesh;
std::vector<Texture> texture_loaded;//用于记录加载纹理的路径;
/*各种具体函数*/
void loadModel(std::string path);//用Assimp将模型数据加载到scene中;
void processNode(aiNode* node, const aiScene* scene);//递归对每一个节点操作;
Mesh processMesh(aiMesh* mesh, const aiScene* scene);//对每一个网格进行操作;
std::vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName);
};
Model包含了一个Mesh对象的vector,构造器会直接通过loadModel来加载文件;
Draw函数遍历了所有网格,并调用它们各自的Draw函数。
voidDraw(Shader shader){
for(unsignedint i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
//分别调用每一个Mesh以及其draw函数对每一个网格进行绘制;
}
导入3D模型到OpenGL
要想导入一个模型,并将它转换到我们自己的数据结构中的话,首先我们需要包含Assimp对应的头文件.
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
首先需要调用的函数是loadModel,它会从构造器中直接调用。在loadModel中,我们使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中。你可能还记得在模型加载章节的第一节教程中,这是Assimp数据接口的根对象。一旦我们有了这个场景对象,我们就能访问到加载后的模型中所有所需的数据了。
Assimp很棒的一点在于,它抽象掉了加载不同文件格式的所有技术细节,只需要一行代码就能完成所有的工作:
Assimp::Importer importer;//创建一个构造器;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
//ReadFile的第一个参数是文件路径,第二个参数是一些后期处理;
//注意此处aiProcess_FlipUVs后处理指令会修复上下颠倒问题;
我们首先声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数。这个函数需要一个文件路径,它的第二个参数是一些后期处理(Post-processing)的选项。除了加载文件之外,Assimp允许我们设定一些选项来强制它对导入的数据做一些额外的计算或操作。通过设定aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标(你可能还记得我们在纹理教程中说过,在OpenGL中大部分的图像的y轴都是反的,所以这个后期处理选项将会修复这个)。其它一些比较有用的选项有:
aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
Assimp提供了很多有用的后期处理指令,你可以在这里找到全部的指令。实际上使用Assimp加载模型是非常容易的(你也可以看到)。困难的是之后使用返回的场景对象将加载的数据转换到一个Mesh对象的数组。
完整的loadModel函数将会是这样的:
voidloadModel(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);//将根结点传入,进行递归遍历所有子节点;
}
如果什么错误都没有发生,我们希望处理场景中的所有节点,所以我们将第一个节点(根节点)传入了递归的processNode函数。因为每个节点(可能)包含有多个子节点,我们希望首先处理参数中的节点,再继续处理该节点所有的子节点,以此类推。这正符合一个递归结构,所以我们将定义一个递归函数。递归函数在做一些处理之后,使用不同的参数递归调用这个函数自身,直到某个条件被满足停止递归。在我们的例子中退出条件(Exit Condition)是所有的节点都被处理完毕。
你可能还记得Assimp的结构中,每个节点包含了一系列的网格索引,每个索引指向场景对象中的那个特定网格。我们接下来就想去获取这些网格索引,获取每个网格,处理每个网格,接着对每个节点的子节点重复这一过程。processNode函数的内容如下:
//对所有节点进行索引,并在scene中找到对应的mesh对象,最后返回;
//别忘了要对子节点进行递归查询,此函数最后会找到所有节点对应的网格位置;
void processNode(aiNode *node, const aiScene *scene){
// 处理节点所有的网格(如果有的话)
for(unsignedint i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 接下来对它的子节点重复这一过程
for(unsignedint i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
//最后返回的Mesh数组会存储在meshes列表中;
我们首先检查每个节点的网格索引,并索引场景的mMeshes数组来获取对应的网格。返回的网格将会传递到processMesh函数中,它会返回一个Mesh对象,我们可以将它存储在meshes列表/vector。
所有网格都被处理之后,我们会遍历节点的所有子节点,并对它们调用相同的processMesh函数。当一个节点不再有任何子节点之后,这个函数将会停止执行。
//上述提到的processMesh函数返回的是一个Mesh对象;
//processMesh函数会对传入的scene中的每一个网格进行处理,分别对顶点,法线,以及材质三个属性进行配置;
Mesh Model::processMesh(aiMesh*mesh,const aiScene* scene)
{
//建立三个临时数组;
std::vector<Vertex> tempVetices;
std::vector<unsigned int> tempIndices;
std::vector<Texture> tempTextures;
//Vertex;
//遍历网格中的所有顶点;>---->顶点处理;
for (unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;//创建一个临时结构体;
glm::vec3 vector;//用来进行接收;
//position;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;//顶点位置传入;
//normal;
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;//顶点法线传入;
//TexCoords;注意对于纹理要进行一次判断,判断第一组纹理坐标是否存在;
if (mesh->mTextureCoords[0])
{
glm::vec2 temVector;
temVector.x = mesh->mTextureCoords[0][i].x;
temVector.y = mesh->mTextureCoords[0][i].y;
//这里获取的是mesh网格的第一组纹理坐标中的第i个纹理坐标;
vertex.TexCoords = temVector;//纹理坐标传入;
}
else
{
//如果没有纹理坐标则将坐标设置为0.0f,0.0f;
vertex.TexCoords = glm::vec2(0.0f);
}
tempVetices.push_back(vertex);//最后把顶点结构体压入数组中;
}
//Indices;
//对索引进行处理;索引传入的是面;
for (unsigned int i = 0; i < mesh->mNumFaces; i++)
{
//要进行的操作是遍历每一个面,并将每一个面的索引传入Indices数组中;
aiFace face = mesh->mFaces[i];//每一个面;
//一个面有多个索引,下面要循环将每一个索引加入索引数组中;
for (unsigned int j = 0; j < face.mNumIndices; j++)
{
tempIndices.push_back(face.mIndices[j]);//把每一个面的索引加入到索引数组中;
}
}
//Textures;材质;
//和节点一样,一个网格只包含了一个指向材质对象的索引。
if (mesh->mMaterialIndex)//如果存在材质索引;
{
// 索引场景对象获得该网格对应的材质数据
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];//通过索引获得面mesh对于的纹理;
//下面分别对两种纹理进行操作,将不同纹理结构体均加入到textures数组当中;
std::vector<Texture> diffuseMap = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
tempTextures.insert(tempTextures.end(), diffuseMap.begin(), diffuseMap.end());
std::vector<Texture> specularMap = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
tempTextures.insert(tempTextures.end(), specularMap.begin(), specularMap.end());
//注意insert函数加入的方法,以specular为例,此处是从tempTextures的末尾开始添加,添加的部分是specularMap数组的头到尾的部分;
}
return Mesh(tempVetices, tempIndices, tempTextures);//最后返回一个Mesh;
}
处理网格的过程主要有三部分:获取所有的顶点数据,获取它们的网格索引,并获取相关的材质数据。处理后的数据将会储存在三个vector当中,我们会利用它们构建一个Mesh对象,并返回它到函数的调用者那里。
获取顶点数据非常简单,我们定义了一个Vertex结构体,我们将在每个迭代之后将它加到vertices数组中。我们会遍历网格中的所有顶点(使用mesh->mNumVertices来获取)。在每个迭代中,我们希望使用所有的相关数据填充这个结构体。顶点的位置是这样处理的:
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
注意我们为了传输Assimp的数据,我们定义了一个vec3的临时变量。使用这样一个临时变量的原因是Assimp对向量、矩阵、字符串等都有自己的一套数据类型,它们并不能完美地转换到GLM的数据类型中。
Assimp将它的顶点位置数组叫做mVertices,这其实并不是那么直观。
处理法线的步骤也是差不多的:
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
纹理坐标的处理也大体相似,但Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,我们不会用到那么多,我们只关心第一组纹理坐标。我们同样也想检查网格是否真的包含了纹理坐标(可能并不会一直如此)
if(mesh->mTextureCoords[0]) // 网格是否有纹理坐标?
{
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
vertex结构体现在已经填充好了需要的顶点属性,我们会在迭代的最后将它压入vertices这个vector的尾部。这个过程会对每个网格的顶点都重复一遍。
索引
Assimp的接口定义了每个网格都有一个面(Face)数组,每个面代表了一个图元,在我们的例子中(由于使用了aiProcess_Triangulate选项)它总是三角形。一个面包含了多个索引,它们定义了在每个图元中,我们应该绘制哪个顶点,并以什么顺序绘制,所以如果我们遍历了所有的面,并储存了面的索引到indices这个vector中就可以了。
for(unsignedint i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for(unsignedint j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
所有的外部循环都结束了,我们现在有了一系列的顶点和索引数据,它们可以用来通过glDrawElements函数来绘制网格。然而,为了结束这个话题,并且对网格提供一些细节,我们还需要处理网格的材质。
材质
和节点一样,一个网格只包含了一个指向材质对象的索引。如果想要获取网格真正的材质,我们还需要索引场景的mMaterials数组。网格材质索引位于它的mMaterialIndex属性中,我们同样可以用它来检测一个网格是否包含有材质:
//要先对网格材质索引进行判断,如果存在索引则进一步在scene场景中获取其真正的材质;
if(mesh->mMaterialIndex >= 0)
{
aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
std::vector<Texture> diffuseMaps = loadMaterialTextures(material,
aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
std:: vector<Texture> specularMaps = loadMaterialTextures(material,
aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
我们首先从场景的mMaterials数组中获取aiMaterial对象。接下来我们希望加载网格的漫反射和/或镜面光贴图。一个材质对象的内部对每种纹理类型都存储了一个纹理位置数组。不同的纹理类型都以aiTextureType_为前缀。我们使用一个叫做loadMaterialTextures的工具函数来从材质中获取纹理。这个函数将会返回一个Texture结构体的vector,我们将在模型的textures vector的尾部之后存储它。
loadMaterialTextures函数遍历了给定纹理类型的所有纹理位置,获取了纹理的文件位置,并加载并和生成了纹理,将信息储存在了一个Vertex结构体中。它看起来会像这样:
std::vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
std::vector<Texture> textures;
for(unsignedint i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);//获取每一个纹理的文件位置;
//将三种信息传入texture;
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
}
return textures;
}
我们首先通过GetTextureCount函数检查储存在材质中纹理的数量,这个函数需要一个纹理类型。我们会使用GetTexture获取每个纹理的文件位置,它会将结果储存在一个aiString中。我们接下来使用另外一个叫做TextureFromFile的工具函数,它将会(用stb_image.h)加载一个纹理并返回该纹理的ID。
TextureFromFile函数就是前面纹理章节中的一些列操作;
这个函数会返回纹理的ID;
unsigned int TextureFromFile(const char* path, const std::string& directory)
{
std::string filename = path;//oringe-->string(path);
filename = directory + "/" + filename;//这里为完整路径;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrchannels;//这里先把纹理加载进来;
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrchannels,0);
if (data)
{
GLenum format;
if (nrchannels == 1)
format = GL_RED;
else if (nrchannels == 3)
format = GL_RGB;
else if (nrchannels == 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 << "Fail to load a Image" << std::endl;
stbi_image_free(data);//同样要记得释放数据;
}
return textureID;//返回纹理的ID;
}
重大优化
我们会将加载过的纹理传入一个全局变量的容器vector数组中,这样在以后的操作中就可以不用经过查询并加载等步骤,节省了时间;
接下来我们将所有加载过的纹理储存在另一个vector中,在模型类的顶部声明为一个私有变量:
vector<Texture> textures_loaded;
之后,在loadMaterialTextures函数中,我们希望将纹理的路径与储存在textures_loaded这个vector中的所有纹理进行比较,看看当前纹理的路径是否与其中的一个相同。如果是的话,则跳过纹理加载/生成的部分,直接使用定位到的纹理结构体为网格的纹理。更新后的函数如下:
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsignedint i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
bool skip = false;
for(unsignedint j = 0; j < textures_loaded.size(); j++)
{
//注意此处字符串的比较方法;
if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
//若存在则仅需要把该纹理传入到texture_loaded当中即可;
textures.push_back(textures_loaded[j]);
skip = true;
break;
}
}
if(!skip)
{ // 如果纹理还没有被加载,则加载它
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // 添加到已加载的纹理中
}
}
return textures;
}
光照和贴图
下面是我添加了镜面光贴图和光照后的代码:
片段着色器
#version 330 core
out vec4 FragColor;
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
//定向光的结构体;
struct DirLight{
vec3 direction;//定向光的方向;
vec3 ambient;
vec3 diffuse;
vec3 specular;//定向光的三种分量;
};
//点光源的结构体;
struct PointLight{
vec3 position;//点光源不用考虑光照方向;光照方向即为与片段的连线;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//聚光的结构体;
struct SpotLight{
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform Material material;
uniform DirLight dirlight;//声明一个定向光结构体的变量;
uniform PointLight pointlights[NR_POINT_LIGHTS];//创建一个包含4个元素的PointLight结构体数组;
uniform SpotLight spotlight;//声明一个聚光的结构体变量;
vec3 CalcDirLight(DirLight light,vec3 normal,vec3 viewDir);//定向光;
vec3 CalcPointLight(PointLight light,vec3 normal,vec3 fragPos,vec3 viewDir);//点光源;
vec3 CalcSpotLight(SpotLight light,vec3 normal,vec3 fragPos,vec3 viewDir);//聚光;
//漫反射采样器;
uniform sampler2D texture_diffuse0;
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
//镜面光采样器;
uniform sampler2D texture_specular0;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
uniform sampler2D texture_specular3;
void main()
{
vec3 norm = normalize(Normal);//法线方向;
vec3 viewDir = normalize(viewPos - FragPos);//视觉方向;
//计算定向光;
vec3 result = CalcDirLight(dirlight,norm,viewDir);
//计算点光照;
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointlights[i],norm, FragPos, viewDir);
//计算聚光;
result += CalcSpotLight(spotlight, norm, FragPos, viewDir);
//输出最终片段;
FragColor = vec4(result,1.0);//最终片段的颜色是三种光照的相加值;
}
//一个用于计算定向光最后的颜色函数;(返回的是一个vec3的向量);
vec3 CalcDirLight(DirLight light,vec3 normal,vec3 viewDir)//函数传入的是定向光的结构体,以及片段已经单位化的法线,还有viewDir向量;
{
vec3 lightDir = normalize(-light.direction);
//漫反射着色;
float diff = max(dot(normal, lightDir), 0.0);
//镜面着色;
vec3 reflectDir = reflect(-lightDir,normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
//进行合并;
vec3 ambient = light.ambient * vec3(texture(material.diffuse,TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse,TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular,TexCoords));
return (ambient + diffuse + specular);
}
//一个用于计算点光源最终输出颜色的函数;
vec3 CalcPointLight(PointLight light,vec3 normal,vec3 fragPos,vec3 viewDir)//normal已经单位化;
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 镜面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰减
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// 合并结果
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
//一个用于计算聚光最终输出的函数;
//传入聚光结构体数组,单位化后的法线,片段位置,以及viewDir视角方向;
vec3 CalcSpotLight(SpotLight light,vec3 normal,vec3 fragPos,vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);//光线方向;
//漫反射着色;
float diff = max(0.0,dot(lightDir,normal));
//镜面光着色;
vec3 reflectDir = reflect(-lightDir,normal);
float spec = pow(max(0.0,dot(reflectDir,viewDir)),material.shininess);
//衰减;
float distance = length(light.position - fragPos);
float atten = 1.0 / (light.constant+distance * light.linear+light.quadratic*(distance*distance));
//柔和;
float theta = dot(lightDir,normalize(-light.direction));
float epison = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epison,0.0,1.0);//利用clamp将柔和限制在0到1之间;
//进行整合;
vec3 ambient = light.ambient * vec3(texture(material.diffuse,TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse,TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular,TexCoords));
//柔和衰减的添加;
diffuse *= intensity * atten;
specular *= intensity * atten;
ambient *= intensity * atten;
return (ambient+diffuse+specular);
}
main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include<E:\OpenGl\练习1.1\3.3.shader_class\shader s.h>
//以下三行为glm的头文件代码;
#include <E:\OpenGl\glm\glm-master\glm\glm.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\matrix_transform.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\type_ptr.hpp>
//#define STB_IMAGE_IMPLEMENTATION
//#include <E:/OpenGl/stb_image.h/stb-master/stb_image.h>//这两行代码加入了stb_image库;
#include"Model s.h"
#include"Mesh s.h"
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_back(GLFWwindow* window, double xoffset, double yoffset);
//三个调整摄像机位置的全局变量;
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
float deltatime = 0.0f;//上一帧与这一帧的时间差;
float lastime = 0.0f;//上一帧的时间;
//用来存储上一帧鼠标的位置!,设置为屏幕中心;
float lastX = 400.0;
float lastY = 300.0;
//仰俯角和偏航角;
float pitch = 0.0f;
float yaw = -90.0f;//从下往上;
float fov = 45.0f;//视域角;
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);//声明一个光源,表示光源在空间中的位置;
int main()
{
//先进行初始化glfw;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "MY OPENGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Fail to create a window" << std::endl;
glfwTerminate();//释放资源;
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//创建完告诉将上下文设置为进程上下文;
//以下两步用于摄像机操作中的设置,由于是窗口的操作,因此放在此处!!;
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);//告诉GLFW隐藏光标并捕捉他;
glfwSetCursorPosCallback(window, mouse_callback);//此句代码当发生鼠标移动时,就会调用mouse_callback函数改变两个欧拉角,
//进而改变cameraFront方向向量,进而可以实现3D旋转;
//还要对视域角fov做出变化,可以进行放大缩小;
glfwSetScrollCallback(window, scroll_back);//当滚动鼠标滚轮的时候,我们就可以通过调用该函数来改变fov,进而改变透视投影矩阵,
//以此来进一步形成放大和缩小!!;
//对glad进行初始化;
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Fail to initnite glad" << std::endl;
return -1;
}
//引入着色器类,着色器被封装到了class Shader里面;
//这里要创建两个着色器程序,分别用在物体和光源上面;
//
// light positions
float vertices[] = {
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
};
//光源
unsigned int lightVAO;
unsigned int lightVBO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
glGenBuffers(1, &lightVBO);
glBindBuffer(GL_ARRAY_BUFFER, lightVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glEnable(GL_DEPTH_TEST);//启用深度测试;
Shader lightshader("3.2.shader2.vs", "3.2.shader2.fs");
Shader lightCubeshader("3.2.shader.light.vs", "3.2.shader.light.fs");//用于光源的着色器程序;//后面会绘制两个光源;
//模型导入:
Model OurModel("E:/OpenGl/Model/Model1/nanosuit.obj");
//光源位置;
glm::vec3 pointLightPositions[] = {
glm::vec3(-2.0f,6.0f,1.0f),
glm::vec3(2.0f,15.0f,0.0f)
};
//准备引擎:
while (!glfwWindowShouldClose(window))
{
float currentFrame = static_cast<float>(glfwGetTime());
deltatime = currentFrame - lastime;
lastime = currentFrame;
processInput(window);
glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// don't forget to enable shader before setting uniforms
lightshader.useProgram();//启用着色器;
lightshader.setFloat("material.shininess", 32.0f);//设置material中的选项;
//人眼设置;//这里设置为摄像机的位置;
lightshader.setVec3("viewPos", cameraPos);
//定向光参数设置;
lightshader.setVec3("dirlight.position", glm::vec3(-2.0f, -0.3f, -1.0f));
lightshader.setVec3("dirlight.ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("dirlight.specular", glm::vec3(1.0f, 1.0f, 1.0f));
lightshader.setVec3("dirlight.diffuse", glm::vec3(0.5f));
//点光源参数设置;下面有两个电光源需要进行设置;
//point1;
lightshader.setVec3("pointlights[0].position", pointLightPositions[0]);
lightshader.setVec3("pointlights[0].ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("pointlights[0].specular", glm::vec3(50.0f, 50.0f, 50.0f));
lightshader.setVec3("pointlights[0].diffuse", glm::vec3(0.5f, 0.5f, 0.5f));
lightshader.setFloat("pointlights[0].constant", 1.0f);
lightshader.setFloat("pointlights[0].linear", 0.09f);
lightshader.setFloat("pointlights[0].quadratic", 0.032f);
//point2;
lightshader.setVec3("pointlights[1].position", pointLightPositions[1]);
lightshader.setVec3("pointlights[1].ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("pointlights[1].specular", glm::vec3(10.0f, 10.0f, 10.0f));
lightshader.setVec3("pointlights[1].diffuse", glm::vec3(0.5f, 0.5f, 0.5f));
lightshader.setFloat("pointlights[1].constant", 1.0f);
lightshader.setFloat("pointlights[1].linear", 0.09f);
lightshader.setFloat("pointlights[1].quadratic", 0.032f);
//聚光参数设置;
lightshader.setVec3("spotlight.position", cameraPos);
lightshader.setVec3("spotlight.direction", cameraFront);
lightshader.setVec3("spotlight.ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("spotlight.specular", glm::vec3(1.0f, 1.0f, 1.0f));
lightshader.setVec3("spotlight.diffuse", glm::vec3(0.5f, 0.5f, 0.5f));
lightshader.setFloat("spotlight.constant", 1.0f);
lightshader.setFloat("spotlight.linear", 0.09f);
lightshader.setFloat("spotlight.quadratic", 0.032f);
lightshader.setFloat("spotlight.outOff", glm::radians(12.5f));
lightshader.setFloat("spotlight.outerCutOff", glm::radians(17.5f));
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(fov), (float)800 / (float)600, 0.1f, 100.0f);
glm::mat4 view = glm::lookAt(cameraPos,cameraPos+cameraFront,cameraUp);
lightshader.setMat4("projection", projection);
lightshader.setMat4("view", view);
// render the loaded model
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); // translate it down so it's at the center of the scene
model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
lightshader.setMat4("model", model);
OurModel.Draw(lightshader);//调用Draw进行绘制相关操作;
//下面是对灯源的操作,会在场景中画出两个灯光位置,灯光均为白色;
lightCubeshader.useProgram();//启用另外一个着色器;
glBindVertexArray(lightVAO);
lightCubeshader.setMat4("view", view);
lightCubeshader.setMat4("projection", projection);
for (unsigned int i = 0; i < 2; i++)
{
model = glm::translate(glm::mat4(1.0f), pointLightPositions[i]);
model = glm::rotate(model, glm::radians(180.0f), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::scale(model, glm::vec3(0.25f, 0.25f, 0.25f));
lightCubeshader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
glfwSetWindowShouldClose(window, true);
float cameraSpeed = 10.0f * deltatime;//移动速度;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraUp * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraUp * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
}
bool firstMouse = true;
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
//计算鼠标距上一帧的偏移量。
//把偏移量添加到摄像机的俯仰角和偏航角中。
//对偏航角和俯仰角进行最大和最小值的限制。
//计算方向向量。
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;//否则每一次都会进行循环;
}
//1.计算鼠标距上一帧的偏移量。
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;//更新存储的上一帧的值;
float sensitivity = 0.1f;//设置灵敏度;
xoffset *= sensitivity;
yoffset *= sensitivity;
//2.把偏移量添加到摄像机的俯仰角和偏航角中。
pitch = pitch + yoffset;
yaw = yaw + xoffset;
//3.对偏航角和俯仰角进行最大和最小值的限制
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
//计算方向向量。
glm::vec3 direction;
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(direction);
}
void scroll_back(GLFWwindow* window, double xoffset, double yoffset)
{
//我们要把fov限制在1.0到45.0之间!!;
if (fov >= 1.0f && fov <= 45.0f)
{
fov -= yoffset;
}
if (fov >= 45.0f)
{
fov = 45.0f;
}
if (fov <= 1.0f)
{
fov = 1.0f;
}
}
结果:
![](https://i-blog.csdnimg.cn/blog_migrate/ec31abc3cd2432927e15a54ad0dbff85.png)
不过感觉这个镜面光贴图是不是没有贴上去?........emmmmmm,在线求大佬看看代码,有点奇怪。。。
那就到此结束啦!!;