En aquest tema, aprendrem a crear un visualitzador de models 3D utilitzant OpenGL. Aquest projecte ens permetrà aplicar molts dels conceptes apresos en els mòduls anteriors, com ara la càrrega de models, la gestió de textures, la il·luminació i les transformacions.
Objectius del Tema
- Càrrega de Models 3D: Aprendre a carregar models 3D des de fitxers.
- Renderització de Models: Renderitzar models 3D utilitzant OpenGL.
- Gestió de Càmeres: Implementar una càmera que permeti navegar pel model.
- Il·luminació i Materials: Aplicar tècniques d'il·luminació i materials per millorar l'aparença del model.
- Interacció de l'Usuari: Permetre la interacció de l'usuari per rotar, escalar i moure el model.
- Càrrega de Models 3D
1.1. Llibreria Assimp
Per carregar models 3D, utilitzarem la llibreria Assimp (Open Asset Import Library). Aquesta llibreria suporta molts formats de fitxers 3D com OBJ, FBX, COLLADA, entre d'altres.
Instal·lació d'Assimp
# En sistemes basats en Debian/Ubuntu sudo apt-get install libassimp-dev # En sistemes basats en Arch sudo pacman -S assimp
1.2. Càrrega de Models amb Assimp
A continuació, es mostra un exemple de com carregar un model 3D utilitzant Assimp.
#include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> #include <iostream> void loadModel(const std::string& path) { Assimp::Importer importer; const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { std::cerr << "ERROR::ASSIMP::" << importer.GetErrorString() << std::endl; return; } // Processar el node arrel processNode(scene->mRootNode, scene); } void processNode(aiNode* node, const aiScene* scene) { // Processar tots els meshes (malles) del node actual for (unsigned int i = 0; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; processMesh(mesh, scene); } // Processar els nodes fills for (unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene); } } void processMesh(aiMesh* mesh, const aiScene* scene) { // Aquí es processa la malla (vertices, textures, normals, etc.) // Exemple: carregar vertices std::vector<float> vertices; for (unsigned int i = 0; i < mesh->mNumVertices; i++) { vertices.push_back(mesh->mVertices[i].x); vertices.push_back(mesh->mVertices[i].y); vertices.push_back(mesh->mVertices[i].z); } // Continuar amb la càrrega de textures, normals, etc. }
- Renderització de Models
2.1. Configuració de Buffers
Per renderitzar el model, necessitem configurar els buffers de vèrtexs (VBO) i els objectes de matriu de vèrtexs (VAO).
unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), &vertices[0], GL_STATIC_DRAW); // Posicions de vèrtexs glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
2.2. Shader Program
Necessitem un shader program per renderitzar el model. A continuació es mostra un exemple de shaders bàsics per a vèrtexs i fragments.
Vertex Shader (vertex_shader.glsl)
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); }
Fragment Shader (fragment_shader.glsl)
2.3. Render Loop
Finalment, necessitem un bucle de renderització per dibuixar el model.
while (!glfwWindowShouldClose(window)) { // Processar entrades processInput(window); // Renderitzar glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Activar shader program shader.use(); // Configurar matrius de transformació glm::mat4 model = glm::mat4(1.0f); glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); shader.setMat4("model", model); shader.setMat4("view", view); shader.setMat4("projection", projection); // Renderitzar model glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, vertices.size() / 3); glBindVertexArray(0); // Intercanviar buffers glfwSwapBuffers(window); glfwPollEvents(); }
- Gestió de Càmeres
3.1. Implementació de la Càmera
Implementarem una càmera que permeti a l'usuari navegar pel model utilitzant el teclat i el ratolí.
class Camera { public: glm::vec3 Position; glm::vec3 Front; glm::vec3 Up; glm::vec3 Right; glm::vec3 WorldUp; float Yaw; float Pitch; float MovementSpeed; float MouseSensitivity; float Zoom; Camera(glm::vec3 position, glm::vec3 up, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(2.5f), MouseSensitivity(0.1f), Zoom(45.0f) { Position = position; WorldUp = up; Yaw = yaw; Pitch = pitch; updateCameraVectors(); } glm::mat4 GetViewMatrix() { return glm::lookAt(Position, Position + Front, Up); } void ProcessKeyboard(Camera_Movement direction, float deltaTime) { float velocity = MovementSpeed * deltaTime; if (direction == FORWARD) Position += Front * velocity; if (direction == BACKWARD) Position -= Front * velocity; if (direction == LEFT) Position -= Right * velocity; if (direction == RIGHT) Position += Right * velocity; } void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true) { xoffset *= MouseSensitivity; yoffset *= MouseSensitivity; Yaw += xoffset; Pitch += yoffset; if (constrainPitch) { if (Pitch > 89.0f) Pitch = 89.0f; if (Pitch < -89.0f) Pitch = -89.0f; } updateCameraVectors(); } void ProcessMouseScroll(float yoffset) { if (Zoom >= 1.0f && Zoom <= 45.0f) Zoom -= yoffset; if (Zoom <= 1.0f) Zoom = 1.0f; if (Zoom >= 45.0f) Zoom = 45.0f; } private: void updateCameraVectors() { glm::vec3 front; front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch)); front.y = sin(glm::radians(Pitch)); front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch)); Front = glm::normalize(front); Right = glm::normalize(glm::cross(Front, WorldUp)); Up = glm::normalize(glm::cross(Right, Front)); } };
- Il·luminació i Materials
4.1. Configuració de la Il·luminació
Afegirem il·luminació bàsica al nostre shader per millorar l'aparença del model.
Fragment Shader amb Il·luminació (fragment_shader.glsl)
#version 330 core out vec4 FragColor; in vec3 Normal; in vec3 FragPos; uniform vec3 lightPos; uniform vec3 viewPos; uniform vec3 lightColor; uniform vec3 objectColor; void main() { // Ambient float ambientStrength = 0.1; vec3 ambient = ambientStrength * lightColor; // Difús vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; // Especular float specularStrength = 0.5; vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * lightColor; vec3 result = (ambient + diffuse + specular) * objectColor; FragColor = vec4(result, 1.0); }
4.2. Aplicació de Materials
Podem definir materials per als nostres models per controlar com interactuen amb la llum.
struct Material { glm::vec3 ambient; glm::vec3 diffuse; glm::vec3 specular; float shininess; }; Material material = { glm::vec3(1.0f, 0.5f, 0.31f), glm::vec3(1.0f, 0.5f, 0.31f), glm::vec3(0.5f, 0.5f, 0.5f), 32.0f }; shader.setVec3("material.ambient", material.ambient); shader.setVec3("material.diffuse", material.diffuse); shader.setVec3("material.specular", material.specular); shader.setFloat("material.shininess", material.shininess);
- Interacció de l'Usuari
5.1. Control de la Càmera
Permetrem a l'usuari controlar la càmera utilitzant el teclat i el ratolí.
void processInput(GLFWwindow *window) { 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); } 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); }
Conclusió
En aquest tema, hem après a crear un visualitzador de models 3D utilitzant OpenGL. Hem cobert la càrrega de models 3D amb Assimp, la renderització de models, la gestió de càmeres, la il·luminació i materials, i la interacció de l'usuari. Aquest projecte ens permet aplicar molts dels conceptes apresos en els mòduls anteriors i ens prepara per a projectes més complexos en el futur.
Curs de Programació OpenGL
Mòdul 1: Introducció a OpenGL
- Què és OpenGL?
- Configurar el Teu Entorn de Desenvolupament
- Crear el Teu Primer Programa OpenGL
- Entendre el Pipeline d'OpenGL
Mòdul 2: Renderització Bàsica
- Dibuixar Formes Bàsiques
- Entendre les Coordenades i les Transformacions
- Coloració i Ombrejat
- Ús de Buffers
Mòdul 3: Tècniques de Renderització Intermèdies
- Textures i Mapeig de Textures
- Il·luminació i Materials
- Barreja i Transparència
- Prova de Profunditat i Prova de Plantilla
Mòdul 4: Tècniques de Renderització Avançades
Mòdul 5: Optimització del Rendiment
- Optimitzar el Codi OpenGL
- Ús d'Objectes de Matriu de Vèrtexs (VAOs)
- Gestió Eficient de la Memòria
- Perfilat i Depuració