3D 모델 로딩 및 렌더링
오늘은 실제 3D 모델을 로딩하고, 게임 엔진에서 렌더링하는 방법을 학습하겠습니다. 이를 위해 Assimp
라이브러리를 사용하여 모델 파일을 로드하고, OpenGL을 사용하여 렌더링합니다.
1. Assimp 설치 및 설정
먼저 Assimp 라이브러리를 설치하고 프로젝트에 설정합니다.
Assimp 다운로드 및 설치
- Assimp 공식 GitHub 페이지에서 Assimp 소스 코드를 다운로드합니다.
- 다운로드한 소스 코드를 프로젝트 디렉토리로 이동합니다.
- Assimp 소스 코드를 빌드하고 설치합니다.
CMakeLists.txt 수정
CMakeLists.txt
파일에 Assimp 라이브러리를 추가합니다.
cmake_minimum_required(VERSION 3.10)
# 프로젝트 이름과 버전 설정
project(GameEngine VERSION 1.0)
# C++ 표준 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 헤더 파일 디렉토리 포함
include_directories(include)
# SDL2 라이브러리 설정
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
# SDL2_image 라이브러리 설정
find_package(SDL2_image REQUIRED)
include_directories(${SDL2_IMAGE_INCLUDE_DIRS})
# SDL2_ttf 라이브러리 설정
find_package(SDL2_ttf REQUIRED)
include_directories(${SDL2_TTF_INCLUDE_DIRS})
# Assimp 라이브러리 설정
find_package(assimp REQUIRED)
include_directories(${ASSIMP_INCLUDE_DIRS})
link_directories(${ASSIMP_LIBRARY_DIRS})
# ENet 라이브러리 설정
find_package(PkgConfig REQUIRED)
pkg_check_modules(ENET REQUIRED enet)
include_directories(${ENET_INCLUDE_DIRS})
link_directories(${ENET_LIBRARY_DIRS})
# 소스 파일 설정
file(GLOB SOURCES "src/*.cpp")
# 실행 파일 생성
add_executable(GameEngine ${SOURCES})
# SDL2 및 SDL2_image, SDL2_ttf, ENet, Assimp 라이브러리 링크
target_link_libraries(GameEngine ${SDL2_LIBRARIES} ${SDL2_IMAGE_LIBRARIES} ${SDL2_TTF_LIBRARIES} ${ENET_LIBRARIES} ${ASSIMP_LIBRARIES})
2. 3D 모델 로더 클래스 설계
3D 모델을 로딩하고 렌더링하기 위한 Model
클래스를 설계합니다.
헤더 파일 작성
include/Model.h
파일을 생성하고 다음과 같이 작성합니다.
#ifndef MODEL_H
#define MODEL_H
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <vector>
#include <string>
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoords;
};
struct Texture {
GLuint id;
std::string type;
aiString path;
};
class Mesh {
public:
std::vector<Vertex> vertices;
std::vector<GLuint> indices;
std::vector<Texture> textures;
Mesh(std::vector<Vertex> vertices, std::vector<GLuint> indices, std::vector<Texture> textures);
void Render(GLuint shaderProgram);
private:
GLuint VAO, VBO, EBO;
void SetupMesh();
};
class Model {
public:
Model(const std::string& path);
void Render(GLuint shaderProgram);
private:
std::vector<Mesh> meshes;
std::string directory;
std::vector<Texture> texturesLoaded;
void LoadModel(const std::string& path);
void ProcessNode(aiNode* node, const aiScene* scene);
Mesh ProcessMesh(aiMesh* mesh, const aiScene* scene);
std::vector<Texture> LoadMaterialTextures(aiMaterial* mat, aiTextureType type, const std::string& typeName);
GLuint TextureFromFile(const char* path, const std::string& directory);
};
#endif // MODEL_H
소스 파일 작성
src/Model.cpp
파일을 생성하고 다음과 같이 작성합니다.
#include "Model.h"
#include <iostream>
#include <stb_image.h>
Mesh::Mesh(std::vector<Vertex> vertices, std::vector<GLuint> indices, std::vector<Texture> textures)
: vertices(vertices), indices(indices), textures(textures) {
SetupMesh();
}
void Mesh::SetupMesh() {
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(GLuint), &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);
}
void Mesh::Render(GLuint shaderProgram) {
for (unsigned int i = 0; i < textures.size(); i++) {
glActiveTexture(GL_TEXTURE0 + i);
glUniform1i(glGetUniformLocation(shaderProgram, (textures[i].type + std::to_string(i)).c_str()), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glActiveTexture(GL_TEXTURE0);
}
Model::Model(const std::string& path) {
LoadModel(path);
}
void Model::Render(GLuint shaderProgram) {
for (auto& mesh : meshes) {
mesh.Render(shaderProgram);
}
}
void Model::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;
}
directory = path.substr(0, path.find_last_of('/'));
ProcessNode(scene->mRootNode, scene);
}
void Model::ProcessNode(aiNode* node, const aiScene* scene) {
for (unsigned int i = 0; i < node->mNumMeshes; i++) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(ProcessMesh(mesh, scene));
}
for (unsigned int i = 0; i < node->mNumChildren; i++) {
ProcessNode(node->mChildren[i], scene);
}
}
Mesh Model::ProcessMesh(aiMesh* mesh, const aiScene* scene) {
std::vector<Vertex> vertices;
std::vector<GLuint> indices;
std::vector<Texture> textures;
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;
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);
}
vertices.push_back(vertex);
}
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]);
}
}
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());
}
return Mesh(vertices, indices, textures);
}
std::vector<Texture> Model::LoadMaterialTextures(aiMaterial* mat, aiTextureType type, const std::string& typeName) {
std::vector<Texture> textures;
for (unsigned int i = 0; i < mat->GetTextureCount(type); i++) {
aiString str;
mat->GetTexture(type, i, &str);
bool skip = false;
for (unsigned int j = 0; j < texturesLoaded.size(); j++) {
if (std::strcmp(texturesLoaded[j].path.C_Str(), str.C_Str()) == 0) {
textures.push_back(texturesLoaded[j]);
skip = true;
break;
}
}
if (!skip) {
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
texturesLoaded.push_back(texture);
}
}
return textures;
}
GLuint Model::TextureFromFile(const char* path, const std::string& directory) {
std::string filename = std::string(path);
filename = directory + '/' + filename;
GLuint 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::cerr << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
3. 게임 엔진에 3D 모델 로더 추가
이제 게임 엔진에 3D 모델 로더를 통합하고, 3D 모델을 로드하고 렌더링합니다.
헤더 파일 수정
include/GameEngine.h
파일을 수정하여 Model
클래스를 포함합니다.
#ifndef GAMEENGINE_H
#define GAMEENGINE_H
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <GL/glew.h>
#include "ParticleSystem.h"
#include "UIManager.h"
#include "LuaManager.h"
#include "NetworkManager.h"
#include "TextureManager.h"
#include "Graphics3D.h"
#include "Model.h"
#include "AudioManager.h"
class GameEngine {
public:
GameEngine();
~GameEngine();
bool Initialize(const char* title, int width, int height);
void Run();
void Shutdown();
private:
SDL_Window* window;
SDL_GLContext glContext;
SDL_Renderer* renderer;
GLuint shaderProgram;
ParticleSystem* particleSystem;
UIManager* uiManager;
LuaManager* luaManager;
NetworkManager* networkManager;
Graphics3D* graphics3D;
Model* model;
bool isRunning;
Uint32 frameStart;
int frameTime;
bool InitializeOpenGL();
GLuint LoadShader(const std::string& vertexPath, const std::string& fragmentPath);
void HandleEvents();
void Update();
void Render();
};
#endif // GAMEENGINE_H
소스 파일 수정
src/GameEngine.cpp
파일을 다음과 같이 수정합니다.
#include "GameEngine.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
const int FPS = 60;
const int FRAME_DELAY = 1000 / FPS;
GameEngine::GameEngine()
: window(nullptr), glContext(nullptr), renderer(nullptr), shaderProgram(0),
particleSystem(nullptr), uiManager(nullptr), luaManager(nullptr), networkManager(nullptr),
graphics3D(nullptr), model(nullptr), isRunning(false), frameStart(0), frameTime(0) {}
GameEngine::~GameEngine() {
Shutdown();
}
bool GameEngine::Initialize(const char* title, int width, int height) {
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
return false;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
if (window == nullptr) {
std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
return false;
}
glContext = SDL_GL_CreateContext(window);
if (glContext == nullptr) {
std::cerr << "SDL_GL_CreateContext Error: " << SDL_GetError() << std::endl;
return false;
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr) {
std::cerr << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl;
return false;
}
if (glewInit() != GLEW_OK) {
std::cerr << "GLEW Initialization failed!" << std::endl;
return false;
}
shaderProgram = LoadShader("shaders/vertex_shader_3d.glsl", "shaders/fragment_shader_3d.glsl");
if (shaderProgram == 0) {
return false;
}
particleSystem = new ParticleSystem(500);
uiManager = new UIManager(renderer);
if (!uiManager->Initialize()) {
return false;
}
uiManager->RenderText("Hello, Game Engine!", 10, 10, 24, { 255, 255, 255, 255 });
luaManager = new LuaManager();
if (!luaManager->Initialize()) {
return false;
}
luaManager->LoadScript("path/to/your/script.lua");
if (!TextureManager::Instance().Load("path/to/your/texture.png", "example", renderer)) {
return false;
}
networkManager = new NetworkManager();
if (!networkManager->Initialize()) {
return false;
}
graphics3D = new Graphics3D();
if (!graphics3D->Initialize()) {
return false;
}
model = new Model("path/to/your/model.obj");
isRunning = true;
return true;
}
GLuint GameEngine::LoadShader(const std::string& vertexPath, const std::string& fragmentPath) {
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
vShaderFile.close();
fShaderFile.close();
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure& e) {
std::cerr << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
return 0;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
GLuint vertexShader, fragmentShader;
GLint success;
GLchar infoLog[512];
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vShaderCode, nullptr);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
std::cerr << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
return 0;
}
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fShaderCode, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
std::cerr << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(program, 512, nullptr, infoLog);
std::cerr << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
return 0;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
void GameEngine::Run() {
while (isRunning) {
frameStart = SDL_GetTicks();
HandleEvents();
Update();
Render();
frameTime = SDL_GetTicks() - frameStart;
if (FRAME_DELAY > frameTime) {
SDL_Delay(FRAME_DELAY - frameTime);
}
}
}
void GameEngine::HandleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
isRunning = false;
}
if (event.type == SDL_KEYDOWN) {
switch (event.key.keysym.sym) {
case SDLK_ESCAPE:
isRunning = false;
break;
default:
break;
}
}
}
}
void GameEngine::Update() {
float deltaTime = frameTime / 1000.0f;
particleSystem->Update(deltaTime);
luaManager->ExecuteFunction("update", deltaTime);
networkManager->Service();
}
void GameEngine::Render() {
// 화면을 검은색으로 지우기
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
// 3D 모델 렌더링
model->Render(shaderProgram);
// SDL 렌더러 사용하여 텍스처 렌더링
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
TextureManager::Instance().Draw("example", 100, 100, 64, 64, renderer);
uiManager->Render();
SDL_RenderPresent(renderer);
SDL_GL_SwapWindow(window);
}
void GameEngine::Shutdown() {
if (particleSystem) {
delete particleSystem;
particleSystem = nullptr;
}
if (uiManager) {
uiManager->Shutdown();
delete uiManager;
uiManager = nullptr;
}
if (luaManager) {
luaManager->Shutdown();
delete luaManager;
luaManager = nullptr;
}
if (networkManager) {
networkManager->Shutdown();
delete networkManager;
networkManager = nullptr;
}
if (graphics3D) {
graphics3D->Shutdown();
delete graphics3D;
graphics3D = nullptr;
}
if (model) {
delete model;
model = nullptr;
}
TextureManager::Instance().ClearTextureMap();
if (shaderProgram) {
glDeleteProgram(shaderProgram);
shaderProgram = 0;
}
if (renderer) {
SDL_DestroyRenderer(renderer);
renderer = nullptr;
}
if (glContext) {
SDL_GL_DeleteContext(glContext);
glContext = nullptr;
}
if (window) {
SDL_DestroyWindow(window);
window = nullptr;
}
SDL_Quit();
}
4. 모델 파일 추가
게임 프로젝트 디렉토리에 3D 모델 파일을 추가합니다. 예를 들어, assets/
디렉토리에 model.obj
파일을 추가합니다.
5. 프로젝트 빌드 및 실행
- Visual Studio에서 CMake 프로젝트 열기:
- Visual Studio를 실행하고,
File
->Open
->CMake...
를 선택합니다. GameEngine
디렉토리를 선택하여 프로젝트를 엽니다.
- Visual Studio를 실행하고,
- Assimp 및 stb_image 라이브러리 설치:
- Assimp 및 stb_image 라이브러리를 프로젝트에 설정합니다.
- 프로젝트 빌드:
- Visual Studio 상단의
Build
메뉴에서Build All
을 선택하여 프로젝트를 빌드합니다.
- Visual Studio 상단의
- 프로젝트 실행:
Debug
메뉴에서Start Without Debugging
을 선택하여 프로그램을 실행합니다.- 윈도우 창이 생성되고, 3D 모델이 화면에 렌더링됩니다.
마무리
오늘은 Assimp 라이브러리를 사용하여 3D 모델을 로딩하고, 게임 엔진에서 렌더링하는 방법을 학습했습니다. 이를 통해 실제 게임 오브젝트를 렌더링할 수 있었습니다. 다음 단계에서는 카메라 시스템을 구현하여 3D 씬을 효과적으로 탐색하고 조작하는 방법을 배워보겠습니다.
질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요.
Day 24 예고
다음 날은 "카메라 시스템 구현"에 대해 다룰 것입니다. 3D 씬을 탐색하고 조작할 수 있는 카메라 시스템을 구현하는 방법을 배워보겠습니다.
'-----ETC----- > C++로 배우는 게임 엔진 개발' 카테고리의 다른 글
[C++로 배우는 게임 엔진 개발] Day 21: 네트워크 기초 (ENet) (0) | 2024.08.01 |
---|---|
[C++로 배우는 게임 엔진 개발] Day 22: 3D 그래픽스 기초 (OpenGL/DirectX) (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 24: 카메라 시스템 구현 (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 25: 3D 물리 엔진 기초 (Bullet) (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 26: 스카이박스와 환경 맵핑 (0) | 2024.08.01 |