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

[C++로 배우는 게임 엔진 개발] Day 16: 2D 조명과 그림자 효과

by cogito21_cpp 2024. 8. 1.
반응형

2D 조명과 그림자 효과

오늘은 OpenGL과 GLSL을 사용하여 2D 조명과 그림자 효과를 구현하는 방법을 학습하겠습니다. 이를 통해 게임 그래픽을 더욱 현실감 있게 만들 수 있습니다.

1. 조명 효과의 개요

조명 효과는 물체의 밝기와 색상을 변경하여 현실적인 느낌을 주는 그래픽 기법입니다. 2D 게임에서는 주로 점광원(Point Light)을 사용하여 조명 효과를 구현합니다.

2. GLSL 쉐이더 수정

조명 효과를 구현하기 위해 기존의 버텍스 쉐이더와 프래그먼트 쉐이더를 수정하겠습니다.

 

버텍스 쉐이더

shaders/vertex_shader.glsl 파일을 다음과 같이 수정합니다.

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;  
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

 

프래그먼트 쉐이더

shaders/fragment_shader.glsl 파일을 다음과 같이 수정합니다.

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;

struct Light {
    vec3 position;
    vec3 color;
    float intensity;
};

uniform Light light;
uniform vec3 viewPos;

void main() {
    // Ambient lighting
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * light.color;

    // Diffuse lighting
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(light.position - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * light.color;

    // Specular lighting
    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 * light.color;

    vec3 result = (ambient + diffuse + specular) * light.intensity;
    FragColor = vec4(result, 1.0);
}

3. 게임 엔진에 조명 설정 추가

이제 게임 엔진에서 조명을 설정하고 렌더링하도록 수정합니다.

 

헤더 파일 수정

include/GameEngine.h 파일을 수정하여 조명 설정을 위한 변수를 추가합니다.

#ifndef GAMEENGINE_H
#define GAMEENGINE_H

#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <string>

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;
    GLuint shaderProgram;
    GLuint VAO, VBO;
    glm::vec3 lightPos;
    glm::vec3 lightColor;
    float lightIntensity;
    bool isRunning;
    Uint32 frameStart;
    int frameTime;

    bool InitializeOpenGL();
    void SetupTriangle();
    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), shaderProgram(0), VAO(0), VBO(0),
      lightPos(1.2f, 1.0f, 2.0f), lightColor(1.0f, 1.0f, 1.0f), lightIntensity(1.0f),
      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;
    }

    if (glewInit() != GLEW_OK) {
        std::cerr << "GLEW Initialization failed!" << std::endl;
        return false;
    }

    shaderProgram = LoadShader("shaders/vertex_shader.glsl", "shaders/fragment_shader.glsl");
    if (shaderProgram == 0) {
        return false;
    }

    SetupTriangle();

    isRunning = true;
    return true;
}

bool GameEngine::InitializeOpenGL() {
    glViewport(0, 0, 800, 600);
    glEnable(GL_DEPTH_TEST);
    return true;
}

void GameEngine::SetupTriangle() {
    float vertices[] = {
        // positions          // normals
        -0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f,
         0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f,
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f
    };

    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

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) {
        gl

GetShaderInfoLog(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() {
    // 게임 상태 업데이트 로직을 여기에 추가합니다.
}

void GameEngine::Render() {
    // 화면을 검은색으로 지우기
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(shaderProgram);

    // 조명 설정
    GLuint lightPosLoc = glGetUniformLocation(shaderProgram, "light.position");
    GLuint lightColorLoc = glGetUniformLocation(shaderProgram, "light.color");
    GLuint lightIntensityLoc = glGetUniformLocation(shaderProgram, "light.intensity");
    glUniform3fv(lightPosLoc, 1, glm::value_ptr(lightPos));
    glUniform3fv(lightColorLoc, 1, glm::value_ptr(lightColor));
    glUniform1f(lightIntensityLoc, lightIntensity);

    // 뷰포트와 프로젝션 설정
    glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -3.0f));
    glm::mat4 projection = glm::perspective(glm::radians(45.0f), (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));

    // 모델 행렬 설정
    glm::mat4 model = glm::mat4(1.0f);
    GLuint modelLoc = glGetUniformLocation(shaderProgram, "model");
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    SDL_GL_SwapWindow(window);
}

void GameEngine::Shutdown() {
    if (VAO) {
        glDeleteVertexArrays(1, &VAO);
        VAO = 0;
    }
    if (VBO) {
        glDeleteBuffers(1, &VBO);
        VBO = 0;
    }
    if (shaderProgram) {
        glDeleteProgram(shaderProgram);
        shaderProgram = 0;
    }
    if (glContext) {
        SDL_GL_DeleteContext(glContext);
        glContext = nullptr;
    }
    if (window) {
        SDL_DestroyWindow(window);
        window = nullptr;
    }
    SDL_Quit();
}

4. 프로젝트 빌드 및 실행

  1. Visual Studio에서 CMake 프로젝트 열기:
    • Visual Studio를 실행하고, File -> Open -> CMake...를 선택합니다.
    • GameEngine 디렉토리를 선택하여 프로젝트를 엽니다.
  2. 프로젝트 빌드:
    • Visual Studio 상단의 Build 메뉴에서 Build All을 선택하여 프로젝트를 빌드합니다.
  3. 프로젝트 실행:
    • Debug 메뉴에서 Start Without Debugging을 선택하여 프로그램을 실행합니다.
    • 윈도우 창이 생성되고, 조명 효과가 적용된 삼각형이 화면에 렌더링됩니다.

마무리

오늘은 OpenGL과 GLSL을 사용하여 2D 조명 효과를 구현하는 방법을 학습했습니다. 조명 효과를 통해 게임 그래픽의 품질을 향상시킬 수 있었습니다. 다음 단계에서는 입자 시스템을 구현하여 게임 내에서 다양한 시각 효과를 추가하는 방법을 배워보겠습니다.

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

Day 17 예고

다음 날은 "입자 시스템 구현"에 대해 다룰 것입니다. 입자 시스템을 사용하여 다양한 시각 효과를 구현하고, 게임 내에서 더욱 생동감 있는 환경을 만드는 방법을 배워보겠습니다.

반응형