인스턴싱과 최적화 기법
오늘은 인스턴싱과 최적화 기법을 통해 게임 성능을 향상시키는 방법을 학습하겠습니다. 인스턴싱은 동일한 메쉬를 여러 개 렌더링할 때 사용되는 기법으로, 그래픽 카드의 성능을 효율적으로 활용할 수 있게 해줍니다.
1. 인스턴싱 기법
인스턴싱을 구현하기 위해 동일한 메쉬를 여러 위치에 배치하는 예제를 만들어 보겠습니다.
헤더 파일 작성
include/Instancing.h
파일을 생성하고 다음과 같이 작성합니다.
#ifndef INSTANCING_H
#define INSTANCING_H
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <vector>
class Instancing {
public:
Instancing();
~Instancing();
bool Initialize();
void Render(const glm::mat4& view, const glm::mat4& projection);
private:
GLuint instanceVBO;
GLuint shaderProgram;
std::vector<glm::mat4> instanceMatrices;
void setupInstances();
GLuint LoadShader(const std::string& vertexPath, const std::string& fragmentPath);
};
#endif // INSTANCING_H
소스 파일 작성
src/Instancing.cpp
파일을 생성하고 다음과 같이 작성합니다.
#include "Instancing.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "Model.h"
Instancing::Instancing() : instanceVBO(0), shaderProgram(0) {}
Instancing::~Instancing() {
glDeleteBuffers(1, &instanceVBO);
}
bool Instancing::Initialize() {
shaderProgram = LoadShader("shaders/instancing_vertex.glsl", "shaders/instancing_fragment.glsl");
if (shaderProgram == 0) {
return false;
}
setupInstances();
return true;
}
void Instancing::setupInstances() {
// 100개의 임의의 위치에 인스턴스 배치
for (int i = 0; i < 100; i++) {
glm::mat4 model = glm::mat4(1.0f);
float x = (float)(rand() % 100) / 10.0f;
float y = (float)(rand() % 100) / 10.0f;
float z = (float)(rand() % 100) / 10.0f;
model = glm::translate(model, glm::vec3(x, y, z));
instanceMatrices.push_back(model);
}
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, instanceMatrices.size() * sizeof(glm::mat4), &instanceMatrices[0], GL_STATIC_DRAW);
}
void Instancing::Render(const glm::mat4& view, const glm::mat4& projection) {
glUseProgram(shaderProgram);
GLuint viewLoc = glGetUniformLocation(shaderProgram, "view");
GLuint projLoc = glGetUniformLocation(shaderProgram, "projection");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
Model model("path/to/your/model.obj");
// 인스턴스 VBO를 바인딩하여 인스턴스 데이터를 전달
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
for (unsigned int i = 0; i < 4; i++) {
glEnableVertexAttribArray(3 + i);
glVertexAttribPointer(3 + i, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(glm::vec4) * i));
glVertexAttribDivisor(3 + i, 1);
}
model.RenderInstances(shaderProgram, instanceMatrices.size());
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
모델 클래스 수정
Model
클래스에 인스턴스 렌더링 기능을 추가합니다.
Model.h
파일에 다음과 같은 함수를 추가합니다.
void RenderInstances(GLuint shaderProgram, size_t instanceCount);
Model.cpp
파일에 다음과 같은 함수를 추가합니다.
void Model::RenderInstances(GLuint shaderProgram, size_t instanceCount) {
for (unsigned int i = 0; i < meshes.size(); i++) {
meshes[i].RenderInstances(shaderProgram, instanceCount);
}
}
Mesh
클래스에 인스턴스 렌더링 기능을 추가합니다.
Mesh.h
파일에 다음과 같은 함수를 추가합니다.
void RenderInstances(GLuint shaderProgram, size_t instanceCount);
Mesh.cpp
파일에 다음과 같은 함수를 추가합니다.
void Mesh::RenderInstances(GLuint shaderProgram, size_t instanceCount) {
glBindVertexArray(VAO);
glDrawElementsInstanced(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0, instanceCount);
glBindVertexArray(0);
}
쉐이더 파일 작성
인스턴싱을 위한 쉐이더 파일을 작성합니다.
버텍스 쉐이더
shaders/instancing_vertex.glsl
파일을 생성하고 다음과 같이 작성합니다.
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoords;
layout(location = 3) in mat4 instanceMatrix;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;
uniform mat4 view;
uniform mat4 projection;
void main() {
FragPos = vec3(instanceMatrix * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(instanceMatrix))) * aNormal;
TexCoords = aTexCoords;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
프래그먼트 쉐이더
shaders/instancing_fragment.glsl
파일을 생성하고 다음과 같이 작성합니다.
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform sampler2D texture_diffuse1;
void main() {
FragColor = texture(texture_diffuse1, TexCoords);
}
2. 게임 엔진에 인스턴싱 통합
이제 게임 엔진에 인스턴싱을 통합하고, 인스턴싱을 사용하여 다수의 오브젝트를 효율적으로 렌더링합니다.
헤더 파일 수정
include/GameEngine.h
파일을 수정하여 Instancing
클래스를 포함합니다.
#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 "Camera.h"
#include "PhysicsManager.h"
#include "Skybox.h"
#include "PostProcessing.h"
#include "Instancing.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;
Camera* camera;
PhysicsManager* physicsManager;
Skybox* skybox;
PostProcessing* postProcessing;
Instancing* instancing;
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;
float lastX = 400, lastY = 300;
bool firstMouse = true;
GameEngine::GameEngine()
: window(nullptr), glContext(nullptr), renderer(nullptr), shaderProgram(0),
particleSystem(nullptr), uiManager(nullptr), luaManager(nullptr), networkManager(nullptr),
graphics3D
(nullptr), model(nullptr), camera(nullptr), physicsManager(nullptr), skybox(nullptr),
postProcessing(nullptr), instancing(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");
camera = new Camera(glm::vec3(0.0f, 0.0f, 3.0f));
physicsManager = new PhysicsManager();
physicsManager->Initialize();
std::vector<std::string> faces{
"path/to/right.jpg",
"path/to/left.jpg",
"path/to/top.jpg",
"path/to/bottom.jpg",
"path/to/front.jpg",
"path/to/back.jpg"
};
skybox = new Skybox();
if (!skybox->Initialize(faces)) {
return false;
}
postProcessing = new PostProcessing();
if (!postProcessing->Initialize(width, height)) {
return false;
}
instancing = new Instancing();
if (!instancing->Initialize()) {
return false;
}
// 물리 엔진에 오브젝트 추가
btCollisionShape* groundShape = new btStaticPlaneShape(btVector3(0, 1, 0), 1);
btCollisionShape* fallShape = new btSphereShape(1);
btDefaultMotionState* groundMotionState = new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), btVector3(0, -1, 0)));
btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0, groundMotionState, groundShape, btVector3(0, 0, 0));
btRigidBody* groundRigidBody = new btRigidBody(groundRigidBodyCI);
physicsManager->GetDynamicsWorld()->addRigidBody(groundRigidBody);
btDefaultMotionState* fallMotionState = new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), btVector3(0, 50, 0)));
btScalar mass = 1;
btVector3 fallInertia(0, 0, 0);
fallShape->calculateLocalInertia(mass, fallInertia);
btRigidBody::btRigidBodyConstructionInfo fallRigidBodyCI(mass, fallMotionState, fallShape, fallInertia);
btRigidBody* fallRigidBody = new btRigidBody(fallRigidBodyCI);
physicsManager->GetDynamicsWorld()->addRigidBody(fallRigidBody);
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();
postProcessing->BeginRender();
Render();
postProcessing->EndRender();
postProcessing->Render(camera->GetViewMatrix(), glm::perspective(glm::radians(camera->Zoom), (float)800 / (float)600, 0.1f, 100.0f));
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;
case SDLK_w:
camera->ProcessKeyboard(FORWARD, frameTime / 1000.0f);
break;
case SDLK_s:
camera->ProcessKeyboard(BACKWARD, frameTime / 1000.0f);
break;
case SDLK_a:
camera->ProcessKeyboard(LEFT, frameTime / 1000.0f);
break;
case SDLK_d:
camera->ProcessKeyboard(RIGHT, frameTime / 1000.0f);
break;
default:
break;
}
}
if (event.type == SDL_MOUSEMOTION) {
float xpos = static_cast<float>(event.motion.x);
float ypos = static_cast<float>(event.motion.y);
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
camera->ProcessMouseMovement(xoffset, yoffset);
}
if (event.type == SDL_MOUSEWHEEL) {
camera->ProcessMouseScroll(static_cast<float>(event.wheel.y));
}
}
}
void GameEngine::Update() {
float deltaTime = frameTime / 1000.0f;
particleSystem->Update(deltaTime);
physicsManager->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);
// 카메라 뷰 및 프로젝션 설정
glm::mat4 view = camera->GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(camera->Zoom), (float)800 / (float)600, 0.1f, 100.0f);
GLuint viewLoc = glGetUniformLocation(shaderProgram, "view");
GLuint projLoc = glGetUniformLocation(shaderProgram, "projection");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 3D 모델 렌더링
model->Render(shaderProgram);
// 인스턴스 렌더링
instancing->Render(view, projection);
// 스카이박스 렌더링
skybox->Render(view, projection);
// 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;
}
if (camera) {
delete camera;
camera = nullptr;
}
if (physicsManager) {
physicsManager->Shutdown();
delete physicsManager;
physicsManager = nullptr;
}
if (skybox) {
delete skybox;
skybox = nullptr;
}
if (postProcessing) {
delete postProcessing;
postProcessing = nullptr;
}
if (instancing) {
delete instancing;
instancing = 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();
}
3. 프로젝트 빌드 및 실행
- Visual Studio에서 CMake 프로젝트 열기:
- Visual Studio를 실행하고,
File
->Open
->CMake...
를 선택합니다. GameEngine
디렉토리를 선택하여 프로젝트를 엽니다.
- Visual Studio를 실행하고,
- 프로젝트 빌드:
- Visual Studio 상단의
Build
메뉴에서Build All
을 선택하여 프로젝트를 빌드합니다.
- Visual Studio 상단의
- 프로젝트 실행:
Debug
메뉴에서Start Without Debugging
을 선택하여 프로그램을 실행합니다.- 윈도우 창이 생성되고, 인스턴싱 기법을 사용하여 다수의 오브젝트가 효율적으로 렌더링됩니다.
마무리
오늘은 인스턴싱과 최적화 기법을 통해 게임 성능을 향상시키는 방법을 학습했습니다. 이를 통해 동일한 메쉬를 다수 렌더링할 때 성능을 효율적으로 개선할 수 있었습니다. 다음 단계에서는 게임 씬 관리 시스템을 구현하여 복잡한 게임 씬을 효과적으로 관리하는 방법을 배워보겠습니다.
질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요.
Day 29 예고
다음 날은 "게임 씬 관리 시스템 구현"에 대해 다룰 것입니다. 복잡한 게임 씬을 효과적으로 관리하고, 다양한 씬 전환을 구현하는 방법을 배워보겠습니다.
'-----ETC----- > C++로 배우는 게임 엔진 개발' 카테고리의 다른 글
[C++로 배우는 게임 엔진 개발] Day 26: 스카이박스와 환경 맵핑 (0) | 2024.08.01 |
---|---|
[C++로 배우는 게임 엔진 개발] Day 27: 포스트 프로세싱 효과 (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 29: 게임 씬 관리 시스템 구현 (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 30: 게임 엔진 배포 및 다음 단계 (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 1: 게임 엔진의 기본 개념과 구조 (0) | 2024.08.01 |