쉐이더 프로그래밍 기초 (GLSL)
오늘은 OpenGL 쉐이더 언어(GLSL)를 사용하여 기본적인 쉐이더를 작성하고, 다양한 그래픽 효과를 구현하는 방법을 학습하겠습니다. 쉐이더는 GPU에서 실행되는 작은 프로그램으로, 그래픽스 파이프라인에서 중요한 역할을 합니다.
1. GLSL 쉐이더의 개요
- 버텍스 쉐이더(Vertex Shader): 각 정점의 위치와 속성을 처리합니다.
- 프래그먼트 쉐이더(Fragment Shader): 각 프래그먼트(픽셀)의 색상을 계산합니다.
2. OpenGL 초기화
먼저 OpenGL 컨텍스트를 초기화하고, GLSL 쉐이더를 로드하고 컴파일하는 방법을 구현하겠습니다.
헤더 파일 수정
include/GameEngine.h
파일에 OpenGL 초기화와 쉐이더 로딩을 위한 함수를 추가합니다.
#ifndef GAMEENGINE_H
#define GAMEENGINE_H
#include <SDL2/SDL.h>
#include <GL/glew.h>
#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;
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
파일에 OpenGL 초기화와 쉐이더 로딩 코드를 추가합니다.
#include "GameEngine.h"
#include <iostream>
#include <fstream>
#include <sstream>
const int FPS = 60;
const int FRAME_DELAY = 1000 / FPS;
GameEngine::GameEngine()
: window(nullptr), glContext(nullptr), shaderProgram(0), 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("path/to/vertex_shader.glsl", "path/to/fragment_shader.glsl");
if (shaderProgram == 0) {
return false;
}
isRunning = true;
return true;
}
bool GameEngine::InitializeOpenGL() {
glViewport(0, 0, 800, 600);
glEnable(GL_DEPTH_TEST);
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() {
// 게임 상태 업데이트 로직을 여기에 추가합니다.
}
void GameEngine::Render() {
// 화면을 검은색으로 지우기
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
// 여기에서 OpenGL 렌더링 작업을 수행합니다.
SDL_GL_SwapWindow(window);
}
void GameEngine::Shutdown() {
if (shaderProgram) {
glDeleteProgram(shaderProgram);
shaderProgram = 0;
}
if (glContext) {
SDL_GL_DeleteContext(glContext);
glContext = nullptr;
}
if (window) {
SDL_DestroyWindow(window);
window = nullptr;
}
SDL_Quit();
}
3. GLSL 쉐이더 작성
쉐이더 파일을 작성하고, 간단한 그래픽 효과를 구현합니다.
버텍스 쉐이더
shaders/vertex_shader.glsl
파일을 생성하고 다음과 같이 작성합니다.
#version 330 core
layout(location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos, 1.0);
}
프래그먼트 쉐이더
shaders/fragment_shader.glsl
파일을 생성하고 다음과 같이 작성합니다.
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 빨간색 출력
}
4. 삼각형 렌더링
삼각형을 렌더링하기 위해 필요한 VAO(Vertex Array Object)와 VBO(Vertex Buffer Object)를 설정합니다.
헤더 파일 수정
include/GameEngine.h
파일에 VAO와 VBO를 추가합니다.
#ifndef GAMEENGINE_H
#define GAMEENGINE_H
#include <SDL2/SDL.h>
#include <GL/glew.h>
#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;
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>
const int FPS = 60;
const int FRAME_DELAY = 1000 / FPS;
GameEngine::GameEngine()
: window(nullptr), glContext(nullptr), shaderProgram(0), VAO(0), VBO(0), 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("path/to/vertex_shader.glsl", "path/to/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[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.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, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
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) {
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() {
// 게임 상태 업데이트 로직을 여기에 추가합니다.
}
void GameEngine::Render() {
// 화면을 검은색으로 지우기
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
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();
}
5. 프로젝트 빌드 및 실행
- Visual Studio에서 CMake 프로젝트 열기:
- Visual Studio를 실행하고,
File
->Open
->CMake...
를 선택합니다. GameEngine
디렉토리를 선택하여 프로젝트를 엽니다.
- Visual Studio를 실행하고,
- 프로젝트 빌드:
- Visual Studio 상단의
Build
메뉴에서Build All
을 선택하여 프로젝트를 빌드합니다.
- Visual Studio 상단의
- 프로젝트 실행:
Debug
메뉴에서Start Without Debugging
을 선택하여 프로그램을 실행합니다.- 윈도우 창이 생성되고, 삼각형이
마무리
오늘은 OpenGL과 GLSL을 사용하여 기본적인 쉐이더를 작성하고, 삼각형을 렌더링하는 방법을 학습했습니다. 다음 단계에서는 2D 조명과 그림자 효과를 구현하여 게임 그래픽을 더욱 현실감 있게 만드는 방법을 배워보겠습니다.
질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요.
Day 16 예고
다음 날은 "2D 조명과 그림자 효과"에 대해 다룰 것입니다. 다양한 조명 효과와 그림자를 구현하여 게임의 그래픽 품질을 향상시키는 방법을 배워보겠습니다.
'-----ETC----- > C++로 배우는 게임 엔진 개발' 카테고리의 다른 글
[C++로 배우는 게임 엔진 개발] Day 11: 타일맵 렌더링 (0) | 2024.08.01 |
---|---|
[C++로 배우는 게임 엔진 개발] Day 12: 간단한 물리 엔진 구현 (AABB 충돌) (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 13: 사운드 시스템 기초 (SDL_mixer) (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] Day 14: 엔티티 컴포넌트 시스템 (ECS) 기초 (0) | 2024.08.01 |
[C++로 배우는 게임 엔진 개발] 목차 (0) | 2024.06.26 |