南邮|计算机图形学——导入模型、添加天空盒
導(dǎo)入模型
1、網(wǎng)格
#include <string> #include <fstream> #include <sstream> #include <iostream> #include <vector>using namespace std;// GL Includes #include <GL/glew.h> // Contains all the necessery OpenGL includes #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp>struct Vertex {// Positionglm::vec3 Position;// Normalglm::vec3 Normal;// TexCoordsglm::vec2 TexCoords;};struct Texture {GLuint id;string type;aiString path;};class Mesh {public:/* Mesh Data */vector<Vertex> vertices;vector<GLuint> indices;vector<Texture> textures;/* Functions */// ConstructorMesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures){this->vertices = vertices;this->indices = indices;this->textures = textures;// Now that we have all the required data, set the vertex buffers and its attribute pointers.this->setupMesh();}// Render the meshvoid Draw(Shader shader){// Bind appropriate texturesGLuint diffuseNr = 1;GLuint specularNr = 1;for (GLuint i = 0; i < this->textures.size(); i++){glActiveTexture(GL_TEXTURE0 + i); // Active proper texture unit before binding// Retrieve texture number (the N in diffuse_textureN)stringstream ss;string number;string name = this->textures[i].type;if (name == "texture_diffuse")ss << diffuseNr++; // Transfer GLuint to streamelse if (name == "texture_specular")ss << specularNr++; // Transfer GLuint to streamnumber = ss.str();// Now set the sampler to the correct texture unitglUniform1i(glGetUniformLocation(shader.Program, (name + number).c_str()), i);// And finally bind the textureglBindTexture(GL_TEXTURE_2D, this->textures[i].id);}// Also set each mesh's shininess property to a default value (if you want you could extend this to another mesh property and possibly change this value)glUniform1f(glGetUniformLocation(shader.Program, "material.shininess"), 16.0f);// Draw meshglBindVertexArray(this->VAO);glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);// Always good practice to set everything back to defaults once configured.for (GLuint i = 0; i < this->textures.size(); i++){glActiveTexture(GL_TEXTURE0 + i);glBindTexture(GL_TEXTURE_2D, 0);}}private:/* Render data */GLuint VAO, VBO, EBO;/* Functions */// Initializes all the buffer objects/arraysvoid setupMesh(){// Create buffers/arraysglGenVertexArrays(1, &this->VAO);glGenBuffers(1, &this->VBO);glGenBuffers(1, &this->EBO);glBindVertexArray(this->VAO);// Load data into vertex buffersglBindBuffer(GL_ARRAY_BUFFER, this->VBO);// A great thing about structs is that their memory layout is sequential for all its items.// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which// again translates to 3/2 floats which translates to a byte array.glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex), &this->vertices[0], GL_STATIC_DRAW);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint), &this->indices[0], GL_STATIC_DRAW);// Set the vertex attribute pointers// Vertex PositionsglEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)0);// Vertex NormalsglEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, Normal));// Vertex Texture CoordsglEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, TexCoords));glBindVertexArray(0);}};2、加載模型
網(wǎng)格(Mesh)代表的是單個的可繪制實體,我們現(xiàn)在先來定義一個我們自己的網(wǎng)格類。
我們會使用Assimp來加載模型,并將它轉(zhuǎn)換(Translate)至多個Mesh對象。
我會先把Model類的結(jié)構(gòu)給你:
class Model {public:/* 函數(shù) */Model(char *path){loadModel(path);}void Draw(Shader shader); private:/* 模型數(shù)據(jù) */vector<Mesh> meshes;string directory;/* 函數(shù) */void loadModel(string path);void processNode(aiNode *node, const aiScene *scene);Mesh processMesh(aiMesh *mesh, const aiScene *scene);vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName); };Model類包含了一個Mesh對象的vector(譯注:這里指的是C++中的vector模板類,之后遇到均不譯),構(gòu)造器需要我們給它一個文件路徑。在構(gòu)造器中,它會直接通過loadModel來加載文件。
這個模型被輸出為一個.obj文件以及一個.mtl文件,.mtl文件包含了模型的漫反射、鏡面光和法線貼圖(這個會在后面學(xué)習(xí)到),你可以在這里下載到(稍微修改之后的)模型,
聲明一個Model對象,將模型的文件位置傳入。接下來模型應(yīng)該會自動加載并(如果沒有錯誤的話)在渲染循環(huán)中使用它的Draw函數(shù)來繪制物體,這樣就可以了。不再需要緩沖分配、屬性指針和渲染指令,只需要一行代碼就可以了。接下來如果你創(chuàng)建一系列著色器,其中片段著色器僅僅輸出物體的漫反射紋理顏色,最終的結(jié)果看上去會是這樣的:
Shader ourShader("1.model_loading.vs", "1.model_loading.fs");// load models// -----------Model ourModel(FileSystem::getPath("resources/objects/nanosuit/nanosuit.obj"));這次我們將會加載Crytek的游戲孤島危機(jī)(Crysis)中的原版納米裝(Nanosuit)。這個模型被輸出為一個.obj文件以及一個.mtl文件,.mtl文件包含了模型的漫反射、鏡面光和法線貼圖(這個會在后面學(xué)習(xí)到),,你可以在這里下載到(稍微修改之后的)模型,
現(xiàn)在在代碼中,聲明一個Model對象,將模型的文件位置傳入。接下來模型應(yīng)該會自動加載并(如果沒有錯誤的話)在渲染循環(huán)中使用它的Draw函數(shù)來繪制物體,這樣就可以了。不再需要緩沖分配、屬性指針和渲染指令,只需要一行代碼就可以了。接下來如果你創(chuàng)建一系列著色器,其中片段著色器僅僅輸出物體的漫反射紋理顏色,最終的結(jié)果看上去會是這樣的:
? // Setup and compile our shadersShader shader("../res/model/model_loading.vs", "../res/model/model_loading.frag");// Load modelsModel ourModel((GLchar *)"../res/nanosuit/nanosuit.obj");?3、導(dǎo)入模型
shader.Use(); // <-- Don't forget this one!// Transformation matricesglm::mat4 projection = glm::perspective(camera.GetZoom(), (float)screenWidth / (float)screenHeight, 0.1f, 100.0f);glm::mat4 view = camera.GetViewMatrix();glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));// Draw the loaded modelglm::mat4 model;model = glm::translate(model, glm::vec3(0.0f, -1.75f, 0.0f)); // Translate it down a bit so it's at the center of the scenemodel = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f)); // It's a bit too big for our scene, so scale it downglUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));ourModel.Draw(shader);著色器
#version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; layout (location = 2) in vec2 texCoords;out vec2 TexCoords;uniform mat4 model; uniform mat4 view; uniform mat4 projection;void main() {gl_Position = projection * view * model * vec4(position, 1.0f);TexCoords = texCoords;} #version 330 core out vec4 color;in vec2 TexCoords;uniform sampler2D texture_diffuse1;void main() { color = vec4(texture(texture_diffuse1, TexCoords)); }?
立方體貼圖
立方體貼圖就是一個包含了6個2D紋理的紋理,每個2D紋理都組成了立方體的一個面:一個有紋理的立方體。你可能會奇怪,這樣一個立方體有什么用途呢?為什么要把6張紋理合并到一張紋理中,而不是直接使用6個單獨的紋理呢?立方體貼圖有一個非常有用的特性,它可以通過一個方向向量來進(jìn)行索引/采樣。假設(shè)我們有一個1x1x1的單位立方體,方向向量的原點位于它的中心。使用一個橘黃色的方向向量來從立方體貼圖上采樣一個紋理值會像是這樣:
使用立方體的實際位置向量來對立方體貼圖進(jìn)行采樣了。接下來,我們可以將所有頂點的紋理坐標(biāo)當(dāng)做是立方體的頂點位置。最終得到的結(jié)果就是可以訪問立方體貼圖上正確面(Face)紋理的一個紋理坐標(biāo)。
1、創(chuàng)建立方體貼圖
立方體貼圖是和其它紋理一樣的,所以如果想創(chuàng)建一個立方體貼圖的話,我們需要生成一個紋理,并將其綁定到紋理目標(biāo)上,之后再做其它的紋理操作。這次要綁定到GL_TEXTURE_CUBE_MAP:
unsigned int textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);因為立方體貼圖包含有6個紋理,每個面一個,我們需要調(diào)用glTexImage2D函數(shù)6次,參數(shù)和之前教程中很類似。但這一次我們將紋理目標(biāo)(target)參數(shù)設(shè)置為立方體貼圖的一個特定的面,告訴OpenGL我們在對立方體貼圖的哪一個面創(chuàng)建紋理。這就意味著我們需要對立方體貼圖的每一個面都調(diào)用一次glTexImage2D。
由于我們有6個面,OpenGL給我們提供了6個特殊的紋理目標(biāo),專門對應(yīng)立方體貼圖的一個面。
| GL_TEXTURE_CUBE_MAP_POSITIVE_X | 右 |
| GL_TEXTURE_CUBE_MAP_NEGATIVE_X | 左 |
| GL_TEXTURE_CUBE_MAP_POSITIVE_Y | 上 |
| GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | 下 |
| GL_TEXTURE_CUBE_MAP_POSITIVE_Z | 后 |
| GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | 前 |
和OpenGL的很多枚舉(Enum)一樣,它們背后的int值是線性遞增的,所以如果我們有一個紋理位置的數(shù)組或者vector,我們就可以從GL_TEXTURE_CUBE_MAP_POSITIVE_X開始遍歷它們,在每個迭代中對枚舉值加1,遍歷了整個紋理目標(biāo):
int width, height, nrChannels; unsigned char *data; for(unsigned int i = 0; i < textures_faces.size(); i++) {data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); }?
這里我們有一個叫做textures_faces的vector,它包含了立方體貼圖所需的所有紋理路徑,并以表中的順序排列。這將為當(dāng)前綁定的立方體貼圖中的每個面生成一個紋理。
因為立方體貼圖和其它紋理沒什么不同,我們也需要設(shè)定它的環(huán)繞和過濾方式:
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);不要被GL_TEXTURE_WRAP_R嚇到,它僅僅是為紋理的R坐標(biāo)設(shè)置了環(huán)繞方式,它對應(yīng)的是紋理的第三個維度(和位置的z一樣)。我們將環(huán)繞方式設(shè)置為GL_CLAMP_TO_EDGE,這是因為正好處于兩個面之間的紋理坐標(biāo)可能不能擊中一個面(由于一些硬件限制),所以通過使用GL_CLAMP_TO_EDGE,OpenGL將在我們對兩個面之間采樣的時候,永遠(yuǎn)返回它們的邊界值。
在繪制使用立方體貼圖的物體之前,我們要先激活對應(yīng)的紋理單元,并綁定立方體貼圖,這和普通的2D紋理沒什么區(qū)別。
在片段著色器中,我們使用了一個不同類型的采樣器,samplerCube,我們將使用texture函數(shù)使用它進(jìn)行采樣,但這次我們將使用一個vec3的方向向量而不是vec2。使用立方體貼圖的片段著色器會像是這樣的:
in vec3 textureDir; // 代表3D紋理坐標(biāo)的方向向量 uniform samplerCube cubemap; // 立方體貼圖的紋理采樣器void main() { FragColor = texture(cubemap, textureDir); }天空盒
1、提取紋理
因為天空盒本身就是一個立方體貼圖,加載天空盒和之前加載立方體貼圖時并沒有什么不同。為了加載天空盒,我們將使用下面的函數(shù),它接受一個包含6個紋理路徑的vector:
unsigned int loadCubemap(vector<std::string> faces) {unsigned int textureID;glGenTextures(1, &textureID);glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);int width, height, nrChannels;for (unsigned int i = 0; i < faces.size(); i++){unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);if (data){glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);stbi_image_free(data);}else{std::cout << "Cubemap texture failed to load at path: " << faces[i] << std::endl;stbi_image_free(data);}}glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);return textureID; }函數(shù)本身應(yīng)該很熟悉了。它基本就是上一部分中立方體貼圖的代碼,只不過合并到了一個便于管理的函數(shù)中。
2、加載紋理
在調(diào)用這個函數(shù)之前,我們需要將合適的紋理路徑按照立方體貼圖枚舉指定的順序加載到一個vector中。
vector<std::string> faces {"right.jpg","left.jpg","top.jpg","bottom.jpg","front.jpg","back.jpg" }; unsigned int cubemapTexture = loadCubemap(faces);現(xiàn)在我們就將這個天空盒加載為一個立方體貼圖了,它的id是cubemapTexture。我們可以將它綁定到一個立方體中,替換掉用了很長時間的難看的純色背景。
3、天空盒著色器
由于天空盒是繪制在一個立方體上的,和其它物體一樣,我們需要另一個VAO、VBO以及新的一組頂點。你可以在這里找到它的頂點數(shù)據(jù)。
用于貼圖3D立方體的立方體貼圖可以使用立方體的位置作為紋理坐標(biāo)來采樣。當(dāng)立方體處于原點(0, 0, 0)時,它的每一個位置向量都是從原點出發(fā)的方向向量。這個方向向量正是獲取立方體上特定位置的紋理值所需要的。正是因為這個,我們只需要提供位置向量而不用紋理坐標(biāo)了。
要渲染天空盒的話,我們需要一組新的著色器,它們都不是很復(fù)雜。因為我們只有一個頂點屬性,頂點著色器非常簡單:
#version 330 core layout (location = 0) in vec3 aPos;out vec3 TexCoords;uniform mat4 projection; uniform mat4 view;void main() {TexCoords = aPos;gl_Position = projection * view * vec4(aPos, 1.0); }注意,頂點著色器中很有意思的部分是,我們將輸入的位置向量作為輸出給片段著色器的紋理坐標(biāo)。片段著色器會將它作為輸入來采樣samplerCube:
#version 330 core out vec4 FragColor;in vec3 TexCoords;uniform samplerCube skybox;void main() { FragColor = texture(skybox, TexCoords); }片段著色器非常直觀。我們將頂點屬性的位置向量作為紋理的方向向量,并使用它從立方體貼圖中采樣紋理值。
4、渲染
有了立方體貼圖紋理,渲染天空盒現(xiàn)在就非常簡單了,我們只需要綁定立方體貼圖紋理,skybox采樣器就會自動填充上天空盒立方體貼圖了。繪制天空盒時,我們需要將它變?yōu)閳鼍爸械牡谝粋€渲染的物體,并且禁用深度寫入。這樣子天空盒就會永遠(yuǎn)被繪制在其它物體的背后了。
glDepthMask(GL_FALSE); skyboxShader.use(); // ... 設(shè)置觀察和投影矩陣 glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glDepthMask(GL_TRUE); // ... 繪制剩下的場景環(huán)境映射
我們現(xiàn)在將整個環(huán)境映射到了一個紋理對象上了,能利用這個信息的不僅僅只有天空盒。通過使用環(huán)境的立方體貼圖,我們可以給物體反射和折射的屬性。這樣使用環(huán)境立方體貼圖的技術(shù)叫做環(huán)境映射(Environment Mapping),其中最流行的兩個是反射(Reflection)和折射(Refraction)。
1、因為我們使用了法線,你還需要更新一下頂點數(shù)據(jù),并更新屬性指針。
2、還要記得去設(shè)置cameraPos這個uniform。
3、著色器
#version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal;out vec3 Normal; out vec3 Position;uniform mat4 model; uniform mat4 view; uniform mat4 projection;void main() {gl_Position = projection * view * model * vec4(position, 1.0f);Normal = mat3(transpose(inverse(model))) * normal;Position = vec3(model * vec4(position, 1.0f)); } #version 330 core in vec3 Normal; in vec3 Position; out vec4 color;uniform vec3 cameraPos; uniform samplerCube skybox;void main() {float ratio = 1.00 / 1.52;vec3 I = normalize(Position - cameraPos);vec3 R = refract(I, normalize(Normal), ratio);color = texture(skybox, R); }4、我們在渲染箱子之前先綁定立方體貼圖紋理:
glActiveTexture(GL_TEXTURE0);glUniform1i(glGetUniformLocation(shadercub.Program, "skybox"), 0);glBindTexture(GL_TEXTURE_CUBE_MAP, textureCubemap);glBindVertexArray(cubeVAO);//glBindTexture(GL_TEXTURE_CUBE_MAP, textureCubemap);glDrawArrays(GL_TRIANGLES, 0, 36);glBindTexture(GL_TEXTURE_2D, 0);glBindVertexArray(0);參考文獻(xiàn):
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/06%20Cubemaps/
https://learnopengl-cn.github.io/03%20Model%20Loading/03%20Model/
總結(jié)
以上是生活随笔為你收集整理的南邮|计算机图形学——导入模型、添加天空盒的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法运行时间计算
- 下一篇: 2021年全球CAN按键面板行业调研及趋