Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MiEngine - Relatório do Projeto

Aluno: André Leandro (a22509641) Disciplina: Computação Gráfica (Videojogos) - Universidade Lusófona, professor Diogo de Andrade.


Índice

  1. Introdução
  2. Organização do Projeto
  3. Tecnologias e Bibliotecas Utilizadas
  4. Implementação Técnica
  5. Compilação e Execução
  6. Melhorias Futuras
  7. Bibliografia

Introdução

Este projeto é uma entrega final para a disciplina de Computação Gráfica que consiste numa engine render 3D ECS, usando OpenGL e que recebe inputs para movimentar no cenário.

Dei o nome de MiEngine(Vindo de MiGames, um nome que dei ao meu grupo solo de desenvolvimento de jogos) e que implementa funcionalidades básicas como:

  • Render de modelos 3D no formato OBJ.
  • Sistema de texturas nos modelos.
  • Câmara em primeira pessoa usando teclado e rato.
  • Iluminação básica através de shaders, com suporte para costumização do shaders e suporte para múltiplos shaders.
  • Arquitetura Entity Component System (ECS).


Organização do Projeto

O projeto segue uma arquitetura Entity Component System (ECS), que separa dados (Components) de comportamento (Systems). Desta forma facilita a manutenção e scability do projeto.


Tecnologias e Bibliotecas Utilizadas

Linguagem

  • C++20

Bibliotecas Principais

Biblioteca Versão Propósito Link
OpenGL 3.3 Core API gráfica para renderização 3D opengl.org
GLFW 3.x Gestão de janelas e input glfw.org
GLAD - Carregador de funções OpenGL glad.dav1d.de
GLM - Matemática para gráficos (vetores, matrizes) glm.g-truc.net
stb_image - Carregamento de imagens GitHub

Build System

  • CMake 3.12+ (Minimum version)

Implementação Técnica

Sistema de Renderização

O sistema de renderização utiliza OpenGL 3.3 Core Profile com as seguintes técnicas:

Vertex Array Objects (VAO) e Vertex Buffer Objects (VBO):

glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);

Vértices (32 bytes por vértice):

  • Posição: 3 floats (X, Y, Z) - offset 0 (12 bytes)
  • Coordenadas de textura: 2 floats (U, V) - offset 12 bytes (8 bytes)
  • Normais: 3 floats (nX, nY, nZ) - offset 20 bytes (12 bytes)

Depth Testing e Face Culling

Foi adicionado um depth testing para garantir que desenha os objetos com as distânicas corretas(objetos mais perto são desenhados por cima dos mais longe) e face culling para remover faces não visíveis(traseiras) para melhorar performance retirando coisas que não são necessárias.

Implementado na app.cpp:

// Define a cor de fundo do ecrã
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);

// Define a região de renderização para o tamanho real do ecrã
int framebufferWidth, framebufferHeight;
glfwGetFramebufferSize(window, &framebufferWidth, &framebufferHeight);
glViewport(0, 0, framebufferWidth, framebufferHeight);

// Ativar depth testing para objetos 3D renderizarem corretamente
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);

// Ativar face culling para melhorar performance
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);

Carregamento de Modelos 3D

O motor suporta o formato Wavefront OBJ, um formato de ficheiro de modelo 3D amplamente utilizado.

Estrutura do Ficheiro OBJ

Um ficheiro OBJ contém:

  • v: Posições dos vértices
  • vt: Coordenadas de textura
  • vn: Vetores normais
  • f: Faces (triângulos ou polígonos)

Processo de Parsing (Duas Passagens)

Primeiro (factory.cpp): Contar elementos para reservar memória.

// Primeira passagem: contar elementos
size_t vertexCount = 0;
size_t texcoordCount = 0;
size_t normalCount = 0;
size_t triangleCount = 0;

while (std::getline(file, line)) {
    words = split(line, " ");
    if (!words[0].compare("v")) ++vertexCount;
    else if (!words[0].compare("vt")) ++texcoordCount;
    else if (!words[0].compare("vn")) ++normalCount;
    else if (!words[0].compare("f")) triangleCount += words.size() - 3;
}

Segundo (factory.cpp): Carregar dados nos vetores.

// Reservar memória para eficiência
positions.reserve(vertexCount);
textureCoordinates.reserve(texcoordCount);
normals.reserve(normalCount);
vertices.reserve(triangleCount * 3 * 8);

// Segunda passagem: carregar dados
while (std::getline(file, line)) {
    words = split(line, " ");
    if (!words[0].compare("v")) {
        positions.push_back(read_vec3(words, preTransform, 1.0f));
    }
    else if (!words[0].compare("vt")) {
        textureCoordinates.push_back(read_vec2(words));
    }
    else if (!words[0].compare("vn")) {
        normals.push_back(read_vec3(words, preTransform, 0.0f));
    }
    else if (!words[0].compare("f")) {
        read_face(words, positions, textureCoordinates, normals, vertices);
    }
}

Triangulação

[Explicar como o código lida com quads e polígonos, convertendo-os em triângulos - ficheiros OBJ podem ter faces com qualquer número de vértices, mas OpenGL só desenha triângulos. A solução é usar fan triangulation]

Implementado na função read_face() no ficheiro factory.cpp:

void Factory::read_face(std::vector<std::string> words,
    std::vector<glm::vec3>& positions,
    std::vector<glm::vec2>& textureCoordinates,
    std::vector<glm::vec3>& normals,
    std::vector<float>& vertices) {

    size_t triangleCount = words.size() - 3;

    // Fan triangulation: primeiro vértice é partilhado por todos os triângulos
    for (size_t i = 0; i < triangleCount; ++i) {
        read_corner(words[1], positions, textureCoordinates, normals, vertices);
        read_corner(words[2 + i], positions, textureCoordinates, normals, vertices);
        read_corner(words[3 + i], positions, textureCoordinates, normals, vertices);
    }
}

Cada vértice é processado pela função read_corner():

void Factory::read_corner(std::string description,
    std::vector<glm::vec3>& positions,
    std::vector<glm::vec2>& textureCoordinates,
    std::vector<glm::vec3>& normals,
    std::vector<float>& vertices) {

    std::vector<std::string> indices = split(description, "/");

    // Posição do vértice (x, y, z) - índices no ficheiro OBJ começam em 1
    glm::vec3 position = positions[std::stol(indices[0]) - 1];
    vertices.push_back(position[0]);
    vertices.push_back(position[1]);
    vertices.push_back(position[2]);

    // Coordenadas de textura (u, v)
    glm::vec2 textureCoordinate = textureCoordinates[std::stol(indices[1]) - 1];
    vertices.push_back(textureCoordinate[0]);
    vertices.push_back(textureCoordinate[1]);

    // Vetor normal (nx, ny, nz) - usado para cálculo de iluminação
    glm::vec3 normal = normals[std::stol(indices[2]) - 1];
    vertices.push_back(normal[0]);
    vertices.push_back(normal[1]);
    vertices.push_back(normal[2]);
}

Pre-Transform Matrix

Os modelos 3D por norma podem acabar por ser exportados com orientações diferentes, então fazemos esta transformação para alinha-los com o sistema de coordenadas da nossa engine.

Exemplo para o modelo "old_pc.obj":

glm::mat4 preTransform = glm::mat4(1.0f);
preTransform = glm::rotate(preTransform, glm::radians(90.0f), { 1.0f, 0.0f, 0.0f });
preTransform = glm::rotate(preTransform, glm::radians(90.0f), { 0.0f, 1.0f, 0.0f });

Na nossa engine o eixo de altura é o Z em vez de Y e o old_pc foi criado com orientação de altura no Y então precisamos o rodar, bem como maior parte dos modelos feitos para a Unity engine que usa o Y como altura.

O projeto também inclui uma função genérica make_any_model() que permite carregar qualquer modelo OBJ com a sua textura correspondente(Mas por default coloquei para rodar o X e Y por 90 graus mas no futuro ter opção de usar ou não conforme a forma que o modelo foi exportado):

void Factory::make_any_model(std::string objPath, std::string imgPath,
                             glm::vec3 position, glm::vec3 eulers) {
    // Aplica transformação padrão e carrega modelo + textura
    glm::mat4 preTransform = glm::mat4(1.0f);
    preTransform = glm::rotate(preTransform, glm::radians(90.0f), {1.0f, 0.0f, 0.0f});
    preTransform = glm::rotate(preTransform, glm::radians(90.0f), {0.0f, 1.0f, 0.0f});

    const std::string fileObjPath = "../models/" + objPath;
    const std::string fileImgPath = "../img/" + imgPath;

    RenderComponent render = make_obj_mesh(fileObjPath.c_str(), preTransform);
    render.material = make_texture(fileImgPath.c_str());
}

Câmara

Neste projeto temos uma câmara em primeira pessoa com controlo WASD para movimento e rato para rotação. Por default no código coloquei 1000.0f de zFar(distância máxima de renderização), bem como o fov a 45.0f, mas que pode ser alterado no ficheiro app.cpp:

// Variáveis criadas aqui só para explicar o que cada coisa é
const float fov = 45.0f;
const float zNear = 0.1f;
const float zFar = 1000.0f;
// 640f e 480f é o aspecto da perspectiva.
glm::mat4 projection = glm::perspective(fov, 640.0f / 480.0f, zNear, zFar);

Vetores da Câmara

A câmara utiliza três vetores ortogonais:

  • forwards: direção para onde a câmara aponta
  • right: direção para a direita da câmara
  • up: direção para cima da câmara

Cálculo da Direção (Ângulos de Euler)

float azimuthAngle = glm::radians(cameraEulers.z);    // Yaw (rotação horizontal)
float elevationAngle = glm::radians(cameraEulers.y);  // Pitch (rotação vertical)

forwards = {
    glm::cos(azimuthAngle) * glm::cos(elevationAngle),
    glm::sin(azimuthAngle) * glm::cos(elevationAngle),
    glm::sin(elevationAngle)
};

Movimento WASD

glm::vec3 positionDelta = {0.0f, 0.0f, 0.0f};
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
    positionDelta.x += 1.0f; // Mover para a frente
}

// Normalizar o vetor de movimento, evitando assim velocidades maiores na diagonal
if (glm::length(positionDelta) > 0.1f) {
    positionDelta = glm::normalize(positionDelta);
    cameraPosition += 0.1f * positionDelta.x * forwards;
    cameraPosition += 0.1f * positionDelta.y * right;
}

Controlo do Rato

Como a visualização do cenário é em primeira pessoa, temos de conseguir rodar a câmera com o rato, bem como manter o rato na mesma posição para não "sair da window".

Vamos pegar a posição atual nesse frame, e setar. depois vamos descontar na posição que detetámos os valores default e assim vamos ter a diferença que existiu no movimento do rato.

// Centrar o rato e calcular delta
double mouseX, mouseY;
glfwGetCursorPos(window, &mouseX, &mouseY);
glfwSetCursorPos(window, 320.0, 240.0);

// Aplicar diferença
glm::vec3 eulersDelta = {0.0f, 0.0f, 0.0f};
eulersDelta.z = -0.1f * static_cast<float>(mouseX - 320.0);
eulersDelta.y = -0.1f * static_cast<float>(mouseY - 240.0);

Shaders e Iluminação

O projeto utiliza GLSL 3.30 (OpenGL Shading Language) para programação de shaders.

Vertex Shader (vertex.txt) Fragment Shader (fragment.txt)

Pipeline de Compilação de Shaders

Implementado em shader.cpp:

  1. Ler ficheiros de texto
  2. Compilar módulos individuais
  3. Criar programa e anexar shaders
  4. Linkar programa

Sistema ECS

Entity Component System é um padrão arquitetural usado em desenvolvimento de jogos.

Componentes:

  • Transform (transform_component.h);
  • Physics (physics_component.h);
  • Render (render_component.h);
  • Camera (camera_component.h).

Sistemas:

  • Motion (motion_system.cpp);
  • Camera (camera_system.cpp);
  • Render (render_system.cpp).

Sistema de Texturas

Carregamento com stb_image

stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load(filename, &width, &height, &channels, STBI_rgb_alpha);

Compilação e Execução

Requisitos

  • Compilador C++20: MinGW, GCC 10+, Clang 11+, ou MSVC 2019+
  • CMake 3.12 ou superior
  • OpenGL 3.3 ou superior
  • Sistema operativo: Windows

Executar

# Windows
.\build\Release\miengine.exe

(O projeto começou por se chamar hello_window mas alterei no final para miengine, portanto pode haver nomes/textos ainda com hello_window)

Controlos

  • W/A/S/D: Movimento da câmara
  • Rato: Rotação da câmara
  • ESC: Sair da aplicação

Parsing de Ficheiros OBJ

Problema: Ficheiros OBJ podem ter faces com 3, 4 ou mais vértices, mas o OpenGL só desenha triângulos.

Solução: Implementei fan triangulation para converter todos os polígonos em triângulos.


Melhorias Futuras

Funcionalidades a Adicionar

  • Resize Window dar update do width & height.
  • Implementar objetos com movimento.
  • Criar canvas/UI/HUD overlap on screen.
  • Melhorar a luz criando shadows.
  • Light sources.

Bibliografia

Documentação Oficial

  1. OpenGL Documentation

  2. GLFW Documentation

  3. GLM Documentation

  4. C++ Documentation

Tutoriais

GetIntoGameDevY - OpenGL with C++ (Youtube)

Ferramentas

  1. stb Libraries (GitHub)

  2. GLAD Generator


Nota: Este projeto foi desenvolvido para fins académicos na disciplina de Computação Gráfica do curso Videojogos na Universidade Lusófona de Lisboa sob orientação do Professor Diogo de Andrade.

About

A 3D game engine that renders objects in a 3d view with first person flying controls. Can load objects with textures and can have directional lights.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages