본문 바로가기
-----ETC-----/C++로 배우는 게임 엔진 개발

[C++로 배우는 게임 엔진 개발] Day 28: 인스턴싱과 최적화 기법

by cogito21_cpp 2024. 8. 1.
반응형

인스턴싱과 최적화 기법

오늘은 인스턴싱과 최적화 기법을 통해 게임 성능을 향상시키는 방법을 학습하겠습니다. 인스턴싱은 동일한 메쉬를 여러 개 렌더링할 때 사용되는 기법으로, 그래픽 카드의 성능을 효율적으로 활용할 수 있게 해줍니다.

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. 프로젝트 빌드 및 실행

  1. Visual Studio에서 CMake 프로젝트 열기:
    • Visual Studio를 실행하고, File -> Open -> CMake...를 선택합니다.
    • GameEngine 디렉토리를 선택하여 프로젝트를 엽니다.
  2. 프로젝트 빌드:
    • Visual Studio 상단의 Build 메뉴에서 Build All을 선택하여 프로젝트를 빌드합니다.
  3. 프로젝트 실행:
    • Debug 메뉴에서 Start Without Debugging을 선택하여 프로그램을 실행합니다.
    • 윈도우 창이 생성되고, 인스턴싱 기법을 사용하여 다수의 오브젝트가 효율적으로 렌더링됩니다.

마무리

오늘은 인스턴싱과 최적화 기법을 통해 게임 성능을 향상시키는 방법을 학습했습니다. 이를 통해 동일한 메쉬를 다수 렌더링할 때 성능을 효율적으로 개선할 수 있었습니다. 다음 단계에서는 게임 씬 관리 시스템을 구현하여 복잡한 게임 씬을 효과적으로 관리하는 방법을 배워보겠습니다.

질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요.

Day 29 예고

다음 날은 "게임 씬 관리 시스템 구현"에 대해 다룰 것입니다. 복잡한 게임 씬을 효과적으로 관리하고, 다양한 씬 전환을 구현하는 방법을 배워보겠습니다.

반응형