《OpenGL 模型》 渲染出帅气的暗影战士

news2024/11/19 22:37:47

模型

  • Assimp
  • 流程
  • 网格
  • 模型
  • 效果

Assimp

3D建模工具,可以让艺术家创建复杂的形状,Assimp库用于加载,如加载obj格式的文件到我们的程序之中,下载CMAKE用于构建该库(会有很多问题),不过!我已经为大家整理好了,大家加入到自己的ide中,设置好链接,头文件加入就好,文件我放在这里,直接用。
我的VX:18268044262 或者 私信我CSDN 我发你

流程

在这里插入图片描述

  • 所有的场景/模型数据都包含在Scene对象
  • Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引
  • 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质
  • Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)
  • 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图

网格

理解模型构建的基础单位 是网格,就可以了,相当于积木块一样,组成一个大玩具,我们先需要定义一个OpenGL的网格类,来接收通过Assimp解析后的数据(网格(Mesh)代表的是单个的可绘制实体)
话不多说,直接上代码,看看网格类
Mesh.h

// _MESH_H

//定义网格所需要的基本属性  顶点数据  纹理数据
//顶点数据结构体
struct Vertex {
	// 顶点数据
	glm::vec3 Position;
	// 法线数据
	glm::vec3 Normal;
	// 坐标
	glm::vec2 TexCoords;
	// 切线
	glm::vec3 Tangent;
	// 副切线
	glm::vec3 Bitangent;
};
//纹理属性结构体
struct Texture {
	unsigned int id;    //ID
	std::string type;        //类型/名字
	std::string path;        //路径 
};

//网格类
class Mesh {
public:
	/*  网格数据  */
	std::vector<Vertex> vertices;
	std::vector<unsigned int> indices;
	std::vector<Texture> textures;
	unsigned int VAO;

	/*  函数  */
	// 构造函数 初始化  拷贝构造函数
	Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
	{
		this->vertices = vertices;
		this->indices = indices;
		this->textures = textures;

		// 设置网格
		setupMesh();
	}

	// 绘制网格
	void Draw(Shader shader)
	{
		// 绑定贴图ID
		unsigned int diffuseNr = 1;
		unsigned int specularNr = 1;
		unsigned int normalNr = 1;
		unsigned int heightNr = 1;
		//遍历结贴图构体中贴图的数量
		for (unsigned int i = 0; i < textures.size(); i++)
		{
			glActiveTexture(GL_TEXTURE0 + i);    //在绑定纹理之前先激活纹理单元

			std::string number;  //各种类型贴图的数量   与Shader里面定义贴图名字匹配
			std::string name = textures[i].type;      //判断贴图结构体数据贴图的名字 是否为 diffuse Speular.. 如果是相应的加上1 为了匹配Shader里面定义贴图名字
			if (name == "texture_diffuse")
				number = std::to_string(diffuseNr++);
			else if (name == "texture_specular")
				number = std::to_string(specularNr++);
			else if (name == "texture_normal")
				number = std::to_string(normalNr++);
			else if (name == "texture_height")
				number = std::to_string(heightNr++);

			// 赋值材质里的贴图uniform
			//为遍历出的每个贴图分配纹理单元
			glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);

			// 绑定贴图采样 
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
		}

		// 绘制网格
		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);
	}

private:
	/*  渲染数据  */
	unsigned int VBO, EBO;

	/*  函数    */
	// 初始化各种缓冲
	void setupMesh()
	{
		// 创建 VBO 顶点缓冲对象 VAO顶点数组对象 EBO索引缓冲对象
		glGenVertexArrays(1, &VAO);
		glGenBuffers(1, &VBO);
		glGenBuffers(1, &EBO);
		//绑定VAO,VBO与EBO对象
		glBindVertexArray(VAO);
		glBindBuffer(GL_ARRAY_BUFFER, VBO);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

		// 复制顶点数据到缓冲内存中
		glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

		// 复制顶点索引到缓冲内存中
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

		//链接顶点属性,设置顶点属性指针
	   //顶点位置 0 vec3
	  //属性位置值为0的顶点属性
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
		//顶点法线坐标 1 vec3
		//属性位置值为1的顶点属性
		//预处理指令offsetof(s, m),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
		//顶点UV坐标 2 vec2
	   //属性位置值为2的顶点属性
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
		//顶点切线坐标 3 vec3
	   //属性位置值为3的顶点属性
		glEnableVertexAttribArray(3);
		glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
		//顶点副切线坐标 4 vec3
	   //属性位置值为4的顶点属性
		glEnableVertexAttribArray(4);
		glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));

		glBindVertexArray(0);
	}
};

模型

创建另一个类来完整地表示一个模型,或者说是包含多个网格,甚至是多个物体的模型
不多解释,直接上代码
model.h

#ifndef MODEL_H
#define MODEL_H

#include <glad/glad.h> 

#include <glad/glad.h>
#include <glm-0.9.9.8/glm-0.9.9.8/glm/glm.hpp>
#include <glm-0.9.9.8/glm-0.9.9.8/glm/gtc/matrix_transform.hpp>
#include <glm-0.9.9.8/glm-0.9.9.8/glm/gtc/type_ptr.hpp>
//#define STB_IMAGE_IMPLEMENTATION    //通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了
//#include "stb_image.h"             //图片处理
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#include "Mesh.h"            //基础的网格
#include "shader.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 Model
{
public:
	/*  模型数据  */
	vector<Texture> textures_loaded;      //贴图对象数组  将所有加载过的纹理储存在一个vector中
	vector<Mesh> meshes;                 //网格对象数组
	string directory;                    //目录  
	bool gammaCorrection;

	/*  函数   */
	// 构造器 参数模型的路径
	Model(string const &path, bool gamma = false) : gammaCorrection(gamma)
	{
		//导入模型的路径
		loadModel(path);
	}

	// 绘制模型里的多个网格 
	void Draw(Shader shader)
	{
		//std::cout << meshes.size() << std::endl;
		for (unsigned int i = 0; i < meshes.size(); i++)
			meshes[i].Draw(shader);
	}

private:
	/*  函数  */
	// 从构造器中直接调用loadModel函数加载模型路径,用Assimp来加载模型至Assimp的一个叫做scene的数据结构中
	//这是Assimp数据接口的根对象。一旦我们有了这个场景对象,我们就能访问到加载后的模型中所有所需的数据了。
	void loadModel(string const &path)
	{
		// 读取模型路径  读取函数在Assimp命名空间中Importer
		Assimp::Importer importer;
		//第一个参数一个文件路径
		//第二个参数是一些后期处理(Post-processing)的选项
		//除了加载文件之外,Assimp允许我们设定一些选项来强制它对导入的数据做一些额外的计算或操作。
		//通过设定aiProcess_Triangulate,告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。
		//aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标(在OpenGL中大部分的图像的y轴都是反的,所以这个后期处理选项将会修复这个)
		//aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
		//aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
		//aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
		const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
		// 检查场景和其根节点不为null,并且检查了它的一个标记(Flag),来查看返回的数据是不是不完整的
		if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
		{
			//通过导入器的GetErrorString函数来报告错误并返回
			cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
			return;
		}
		// 获取文件路径的目录路径
		directory = path.substr(0, path.find_last_of('/'));

		// 将第一个节点(根节点)传入了递归的processNode函数
		//因为每个节点(可能)包含有多个子节点,首先处理参数中的节点,再继续处理该节点所有的子节点,以此类推。
		processNode(scene->mRootNode, scene);
	}

	// 递归函数
	//处理自身所有节点下所有网格
	//在Assimp的结构中,每个节点包含了一系列的网格索引,每个索引指向场景对象中的那个特定网格。
	//我们接下来就想去获取这些网格索引,获取每个网格,处理每个网格,接着对每个节点的子节点重复这一过程
	//当一个节点不再有任何子节点之后,这个函数将会停止执行。
	void processNode(aiNode *node, const aiScene *scene)
	{
		// 处理节点所有的网格(如果有的话)
		for (unsigned int i = 0; i < node->mNumMeshes; i++)
		{
			//检查每个节点的网格索引,并索引场景的mMeshes数组来获取对应的网格。
			//返回的网格将会传递到processMesh函数中,它会返回一个Mesh对象,我们可以将它存储在meshes列表 / vector
			aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
			//在插入meshes前要转化自己封装的类
			meshes.push_back(processMesh(mesh, scene));
		}
		// 接下来对它的子节点重复这一过程
		for (unsigned int i = 0; i < node->mNumChildren; i++)
		{
			processNode(node->mChildren[i], scene);
		}

	}
	//将Assimp的数据解析到Mesh类
	Mesh processMesh(aiMesh *mesh, const aiScene *scene)
	{
		// 声明要填充的数据
		vector<Vertex> vertices;
		vector<unsigned int> indices;
		vector<Texture> textures;

		// 获取所有的顶点数据
		//遍历网格中的所有顶点(使用mesh->mNumVertices来获取)
		for (unsigned int i = 0; i < mesh->mNumVertices; i++)
		{
			Vertex vertex;
			//储存顶点数据
			glm::vec3 vector;
			// 位置
			vector.x = mesh->mVertices[i].x;
			vector.y = mesh->mVertices[i].y;
			vector.z = mesh->mVertices[i].z;
			vertex.Position = vector;
			// 法线
			vector.x = mesh->mNormals[i].x;
			vector.y = mesh->mNormals[i].y;
			vector.z = mesh->mNormals[i].z;
			vertex.Normal = vector;
			// UV
			//Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,这里不会用到那么多,只关心第一组纹理坐标
			if (mesh->mTextureCoords[0]) // 不是所有的Mesh 都有UV坐标 进行判断
			{
				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);
			// 切线
			vector.x = mesh->mTangents[i].x;
			vector.y = mesh->mTangents[i].y;
			vector.z = mesh->mTangents[i].z;
			vertex.Tangent = vector;
			// 副切线
			vector.x = mesh->mBitangents[i].x;
			vector.y = mesh->mBitangents[i].y;
			vector.z = mesh->mBitangents[i].z;
			vertex.Bitangent = vector;
			vertices.push_back(vertex);
		}
		//设置索引
		//Assimp的接口定义了每个网格都有一个面(Face)数组,每个面代表了一个图元
		//在我们的例子中(由于使用了aiProcess_Triangulate选项)它总是三角形。
		//一个面包含了多个索引,它们定义了在每个图元中,我们应该绘制哪个顶点,并以什么顺序绘制。
		//所以如果我们遍历了所有的面,并储存了面的索引到indices这个vector中就可以了。
		for (unsigned int i = 0; i < mesh->mNumFaces; i++)
		{
			aiFace face = mesh->mFaces[i];

			for (unsigned int j = 0; j < face.mNumIndices; j++)
				indices.push_back(face.mIndices[j]);
		}
		// 处理材质
		//一个网格只包含了一个指向材质对象的索引。如果想要获取网格真正的材质,我们还需要索引场景的mMaterials数组
		//网格材质索引位于它的mMaterialIndex属性中;
		//检测一个网格是否包含有材质
		if (mesh->mMaterialIndex >= 0)
		{
			aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];

			//一个材质对象的内部对每种纹理类型都存储了一个纹理位置数组
			//不同的纹理类型都以aiTextureType_为前缀。
			//使用loadMaterialTextures的工具函数来从材质中获取纹理。这个函数将会返回一个Texture结构体的vector
			//我们将在模型的textures vector的尾部之后存储它。

			// 1. diffuse maps
			vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
			textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
			// 2. specular maps
			vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
			textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
			// 3. normal maps
			std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
			textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
			// 4. height maps
			std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
			textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
		}
		// 返回Mesh的所需数据
		return Mesh(vertices, indices, textures);
	}

	//loadMaterialTextures函数遍历了给定纹理类型的所有纹理位置,获取了纹理的文件位置,并加载并生成纹理,将信息储存在了一个textures结构体中
	vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
	{
		vector<Texture> textures;
		//GetTextureCount函数检查储存在材质中纹理的数量
		for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
		{
			aiString str;
			//GetTexture获取每个纹理的文件位置,它会将结果储存在一个aiString中
			mat->GetTexture(type, i, &str);

			// 检查它有没有被加载过
			bool skip = false;
			for (unsigned int j = 0; j < textures_loaded.size(); j++)
			{
				//将纹理的路径与储存在textures_loaded这个vector中的所有纹理进行比较,看看当前纹理的路径是否与其中的一个相同。
				//如果是的话,则跳过纹理加载/生成的部分,直接使用定位到的纹理结构体为网格的纹理。
				if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
				{
					textures.push_back(textures_loaded[j]);
					skip = true;
					break;
				}
			}
			if (!skip)
			{   // 如果纹理还没有被加载,则加载它
				Texture texture;
				//TextureFromFile的工具函数,它将会(用stb_image.h)加载一个纹理并返回该纹理的ID
				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 unnecesery 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;
}
#endif

main.cpp

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <KHR/khrplatform.h>
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <glm-0.9.9.8/glm-0.9.9.8/glm/glm.hpp>
#include <glm-0.9.9.8/glm-0.9.9.8/glm/gtc/matrix_transform.hpp>
#include <glm-0.9.9.8/glm-0.9.9.8/glm/gtc/type_ptr.hpp>

#define STB_IMAGE_IMPLEMENTATION

#include "shader.h"
#include "stb_image.h"
#include "camera.h"
#include "model.h"

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

//设置窗口的宽和高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

//初始化相机的位置
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;

//缓冲渲染两帧之差 的时间
float deltaTime = 0.0f;
float lastFrame = 0.0f;
//主函数
int main()
{
	//初始化GLFW
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);         //主版本号
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);         //次版本号  
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //适配IOS苹果
#endif

	//创建窗口并设置其大小,名称,与检测是否创建成功
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	//创建完毕之后,需要让当前窗口的环境在当前线程上成为当前环境,就是接下来的画图都会画在我们刚刚创建的窗口上
	glfwMakeContextCurrent(window);
	//告诉GLFW我们希望每当窗口调整大小的时候调用这个函数
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	//监听鼠标移动事件 回调鼠标响应函数
	glfwSetCursorPosCallback(window, mouse_callback);
	监听鼠标滚轮的移动
	glfwSetScrollCallback(window, scroll_callback);

	//隐藏鼠标的光标 光标会一直停留在窗口中
	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

	//glad寻找opengl的函数地址,调用opengl的函数前需要初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	//启用深度测试,默认是关闭的
	glEnable(GL_DEPTH_TEST);

	//构建和编译Shader  读取着色器路径
	Shader ourShader("nanosuitVS.txt", "nanosuitFS.txt");

	//导入模型
	Model ourModel(("../resources/meshs/tree/12150_Christmas_Tree_V2_L2.mtl"));
	//glViewport(0, 0, 4000, 3000);
	// 绘制线框
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	//渲染循环
	while (!glfwWindowShouldClose(window))
	{
		//计算上下两帧的渲染时间差
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		//响应键盘输入
		processInput(window);

		//设置清除颜色
		glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
		//清除当前窗口,把颜色设置为清除颜色
		//清除深度信息
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		//激活链接程序,激活着色器,开始渲染
		ourShader.use();

		// 视口,投影矩阵
		glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
		glm::mat4 view = camera.GetViewMatrix();
		ourShader.setMat4("projection", projection);
		ourShader.setMat4("view", view);

		// 模型空间--世界空间矩阵
		glm::mat4 model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(0.0f, -1.75f, 0.0f));
		model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f));
		ourShader.setMat4("model", model);
		ourModel.Draw(ourShader);


		//交换颜色缓冲
		glfwSwapBuffers(window);
		//处理事件
		glfwPollEvents();
	}

	//释放前面所申请的内存
	glfwTerminate();
	return 0;
}

//响应键盘输入事件
void processInput(GLFWwindow *window)
{
	//ESC 退出窗口
	//glfwGetKey()用来判断一个键是否按下。第一个参数是GLFW窗口句柄,第二个参数是一个GLFW常量,代表一个键。
	//GLFW_KEY_ESCAPE表示Esc键。如果Esc键按下了,glfwGetKey将返回GLFW_PRESS(值为1),否则返回GLFW_RELEASE(值为0)。
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);

	//WASD移动摄像机
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		camera.ProcessKeyboard(FORWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		camera.ProcessKeyboard(BACKWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		camera.ProcessKeyboard(LEFT, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		camera.ProcessKeyboard(RIGHT, deltaTime);
}

// 当用户改变窗口的大小的时候,视口也应该被调整。
//对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	//OpenGL渲染窗口的尺寸大小
	//glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)
	glViewport(0, 0, width, height);
}

//响应鼠标事件 创建鼠标的回调函数
//xpos,ypos鼠标的位置
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
	//如果为第一次进入 记录当前鼠标位置 为初始化鼠标位置
	if (firstMouse)
	{
		lastX = xpos;
		lastY = ypos;
		firstMouse = false;
	}
	//计算当前帧和上一帧的位置差
	float xoffset = xpos - lastX;
	float yoffset = lastY - ypos; // 这里如果不反过来 实际操作过程中向上移动摄像机 物体也会跟着向上移动 实际应该向下

	lastX = xpos;
	lastY = ypos;

	camera.ProcessMouseMovement(xoffset, yoffset);
}

//响应鼠标滚轮
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
	camera.ProcessMouseScroll(yoffset);
}

  1. 首先将模型路径传入,构造model,利用assimp库加载模型
  2. 将场景scene下结点,递归遍历,将其中的网格全部转化成自己封装的网格Mesh,插入到vector容器中
  3. 转化的过程中 包含 顶点数据 位置 法线 切线 UV 设置索引 要处理材质
  4. 随后加载纹理 获取到顶点数组 索引数组 纹理数组
  5. 设置网格初始化各种缓冲等,最后给到着色器去绘画

效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【小程序】小程序代码的构成

目录 项目结构 1. 了解项目的基本组成结构 2. 小程序页面的组成部分 JSON配置文件 1. JSON 配置文件的作用 2. app.json 文件 3. project.config.json 文件 4. sitemap.json 文件 5. 页面的 .json 配置文件 6. 新建小程序页面 7. 修改项目首页 项目结构 1. 了解项…

别再用过时的方式了!全新版本Spring Security,这样用才够优雅!

基本使用 我们先对比下Spring Security提供的基本功能登录认证&#xff0c;来看看新版用法是不是更好。 升级版本 首先修改项目的pom.xml文件&#xff0c;把Spring Boot版本升级至2.7.0版本。 <parent><groupId>org.springframework.boot</groupId><art…

Lua 元表(Metatable)

在 Lua table 中我们可以访问对应的 key 来得到 value 值&#xff0c;但是却无法对两个 table 进行操作(比如相加)。 因此 Lua 提供了元表(Metatable)&#xff0c;允许我们改变 table 的行为&#xff0c;每个行为关联了对应的元方法。 例如&#xff0c;使用元表我们可以定义 …

STM32程序设计规范浅析

这篇博客写到“STM32基础知识篇”里&#xff0c;一方面是一个很好地对过往工作的总结&#xff0c;另一方面也是整个专栏撰写计划的开端&#xff0c;古人云&#xff1a;良好的开端是成功的一半&#xff0c;在文章的最后详细地规划了整个专栏后期的更新计划。 笔者前段时间休息的…

无人机遥感图像拼接与处理操作技术

【内容简述】&#xff1a; 无人机遥感图像采集流程&#xff1a; 无人机遥感监测介绍 无人机航线规划设计 无人机飞行软件操作 无人机航拍一般过程 无人机遥感图像拼接软件操作&#xff1a; Photoscan软件介绍 软件基本操作与实践 遥感图像拼接的一般流程 遥感图像分组拼接与点…

【centos】安装nvida CUDA平台附带安装cudnn库

目录1.安装 CUDAToolKit2.安装cudnn库1.安装 CUDAToolKit 使用 lspci | grep -i nvidia列出所有支持的GPU 安装内核开发依赖包&#xff1a; yum install kernel-devel查看内核版本号&#xff0c;用来看与开发包版本号是否一致&#xff1a; uname -r查看nvida显卡驱动&#…

设计模式之迭代器模式

Iterator design pattern 迭代器模式的概念、迭代器模式的结构、迭代器模式的优缺点、迭代器模式的使用场景、迭代器模式的实现示例、迭代器模式的源码分析 1、迭代器模式的概念 迭代器模式&#xff0c;即提供一种方法来顺序访问聚合对象内的元素&#xff0c;而不暴露聚合对象…

LeetCode HOT 100 —— 448. 找到所有数组中消失的数字

题目 给你一个含 n 个整数的数组 nums &#xff0c;其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字&#xff0c;并以数组的形式返回结果。 思路 原地哈希&#xff08;简单模拟&#xff09;&#xff1a; 核心思路&#xff1a; 因为…

Python 和 PyQt5 实现打地鼠小游戏

Python 和 PyQt5 实现打地鼠小游戏 实现效果&#xff1a; 视频效果&#xff1a; https://live.csdn.net/v/264602https://live.csdn.net/v/264602 代码&#xff1a; import random import sysfrom PyQt5.QtCore import QBasicTimer, Qt, QTimer from PyQt5.QtGui import QCo…

CSS3【基础选择器、字体样式、文本样式、行高样式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录基础选择器1. 标签选择器2. 类选择器3. id选择器字体和文本样式1.字体样式1.1 字体大小1.2 字体粗细1.3 字体样式&#xff08;是否倾斜&#xff09;1.4 常见字体系列…

JAVA中实现多线程-单例双重锁(DCL(Double Check Lock)双重锁检查)

一 .多线程 继承 Thread 类实现 Runnable 接口实现 Callable 接口线程池 重写run方法&#xff0c;创建对象&#xff0c;调用start()方法启动线程 1&#xff0c;新生状态 – 用new关键字建立一个线程后&#xff0c;该线程对象就处于新生状态。 – 处于新生状态的线程有自己的…

Netty前置知识

传统IO 这里以文件输入输出流&#xff1a;FileInputStream 、 FileOutputStream 来进行解释。由继承关系得知&#xff0c;这两个输入和输出类继承自 InputStream 和 OutputStream 这两个基础的输入、输出的抽象类&#xff0c;这时我们可以看到当我们需要读写文件的时候&#x…

leetcode--搜索

搜索1.深度优先搜索(DFS)&#xff08;1&#xff09;岛屿的最大面积(695)&#xff08;2&#xff09;省份数量&#xff08;3&#xff09;太平洋大西洋水流问题(417)2.回溯法&#xff08;1&#xff09;全排列(46)&#xff08;2&#xff09;组合(77)&#xff08;3&#xff09;单词搜…

C++ allocator设计内存管理器

文章目录allocator内存管理器基本属性类的设计关键功能的实现完整的内存管理器内存管理器的测试&#xff1a;设计自定义的String类。前情回顾&#xff1a; allocator内存管理类 allocator内存管理器 某些类需要在运行时分配可变大小的内存空间&#xff0c;一般来说我们使用容器…

从零搭建完整python自动化测试框架(UI自动化和接口自动化)

从零搭建完整python自动化测试框架&#xff08;UI自动化和接口自动化&#xff09; 文章目录 总体框架 PO模式、DDT数据驱动、关键字驱动 框架技术选择 框架运行结果 各用例对应的定义方式&#xff08;PO/DDT&#xff09; 测试执行结果 从零开始搭建项目 一、开发环境搭…

泪目,终于有P8大佬把困扰我多年的《计算机网络原理》全部讲明白了

前言 为什么网络协议这么重要呢&#xff1f;集群规模一大&#xff0c;我们首先想到的就是网络互通的问题&#xff1b;应用吞吐量压不上去&#xff0c;我们首先想到的也是网络互通的问题。所以&#xff0c;要成为技术牛人&#xff0c;搞定大系统&#xff0c;一定要过网络这一关&…

Mac怎么清理缓存?这两种方法都非常好用哦

与电脑系统或应用程序非常相似&#xff0c;您的Mac也有自己的系统缓存&#xff0c;它可以在后台临时存储数据&#xff0c;以加快软件安装速度并减少互联网数据使用量&#xff08;通过Apple&#xff09;。与电脑系统或应用程序类似&#xff0c;缓存数据可能会开始堆积——占用存…

unordered系列关联式容器以及哈希表原理实现

Ⅰ. unordered 系列关联式容器 在C98中&#xff0c;STL提供了底层为红黑树结构的一系列关联式容器&#xff0c;在查询时效率可达到 log2nlog_2 nlog2​n&#xff0c;即最差情况下需要比较红黑树的高度次&#xff0c;当树中的节点非常多时&#xff0c;查询效率也不理想。最好的…

Android Studio Profiler 检查内存

Android Studio Profiler 检查内存简单介绍 如何使用&#xff1f; 第一步&#xff1a;点击Profiler按钮 第二步&#xff1a;选择 第三步&#xff1a;选择Capture heap dump 并点击Record 解释相关按钮的功能 垃圾桶按钮&#xff1a;用于强制执行垃圾回收事件的按钮&#xff…

LinkedList(JDK1.8)源码+底层数据结构分析

文章目录前言一、双向链表1.1 双向链表示意图1.2 LinkedList 属性1.3 Node 节点对象二、双向链表的操作2.1 添加元素-add2.2 删除元素-remove2.3 修改元素-set2.4 查询元素-get前言 双向链表是一种数据结构&#xff0c;由若干个节点构成&#xff0c;其中每个节点均由三部分构成…