OpenGL 실전 가이드 | 3D 그래픽스 프로그래밍

OpenGL 실전 가이드 | 3D 그래픽스 프로그래밍

이 글의 핵심

OpenGL 설치부터 셰이더·텍스처·3D 렌더링까지 한 번에 훑을 수 있게 정리했다. 튜토리얼을 따라가다 막히는 지점을 줄이는 쪽에 초점을 뒀다.


목차

  1. OpenGL이란?
  2. 개발 환경 설정
  3. 첫 번째 삼각형
  4. 셰이더 프로그래밍
  5. 텍스처 매핑
  6. 3D 변환
  7. 조명 (Lighting)
  8. 3D 모델 로딩
  9. 고급 기법
  10. 실전 프로젝트

사전 지식 (초보자를 위한 기초)

1. 그래픽스 파이프라인이란?

그래픽스 파이프라인은 3D 데이터를 화면에 그리는 단계별 과정이다.

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

3D 모델 데이터

[1] Vertex Shader (정점 처리)
    - 3D 좌표 → 2D 화면 좌표
    - 변환 행렬 적용

[2] Rasterization (래스터화)
    - 삼각형 → 픽셀 변환

[3] Fragment Shader (픽셀 색상)
    - 각 픽셀의 색상 계산
    - 텍스처, 조명 적용

[4] Frame Buffer (화면 출력)
    - 최종 이미지

Vertex Shader는 정점을 어디에 둘지 정하고, 래스터화는 삼각형을 픽셀로 쪼개며, 프래그먼트 셰이더는 픽셀 색을 정한다. 프레임버퍼에 그 결과가 쌓인다고 보면 된다.

2. 좌표계

OpenGL 좌표계:

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

        Y (위)

        |
        |
        +-----→ X (오른쪽)
       /
      /
     Z (앞)

좌표 범위:
X: -1.0 ~ 1.0
Y: -1.0 ~ 1.0
Z: -1.0 ~ 1.0

(0, 0, 0): 화면 중앙

3. 정점 (Vertex)과 삼각형

정점 (Vertex):

  • 3D 공간의 한 점
  • 위치 (x, y, z), 색상, 텍스처 좌표 등

삼각형 (Triangle):

  • 3개의 정점으로 이루어진 면
  • 모든 3D 모델은 삼각형으로 구성됨

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

삼각형 예시:

    V0 (0.0, 0.5, 0.0)
    /\
   /  \
  /    \
 /______\
V1      V2
(-0.5,  (0.5,
-0.5,   -0.5,
0.0)    0.0)

3개 정점 → 1개 삼각형

4. 셰이더란?

셰이더 (Shader)는 GPU에서 돌아가는 작은 프로그램이다.

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

CPU (프로그램):
- 게임 로직
- 물리 계산
- AI

GPU (셰이더):
- 정점 변환
- 픽셀 색상 계산
- 조명 효과

GPU는 수천 개의 코어로 병렬 처리!
→ 매우 빠름

5. 버퍼 (Buffer)

버퍼는 GPU 메모리에 정점·인덱스 등을 올려두는 공간이다.

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

VBO (Vertex Buffer Object):
- 정점 데이터 저장
- 위치, 색상, 텍스처 좌표

VAO (Vertex Array Object):
- VBO 설정 저장
- 여러 VBO 관리

EBO (Element Buffer Object):
- 인덱스 저장
- 정점 재사용

1. OpenGL이란?

OpenGL (Open Graphics Library)크로스 플랫폼 그래픽스 API다.

OpenGL의 역할

2D·3D 렌더링, GPU 제어, 셰이더·텍스처·조명 등을 다룬다. 게임·CAD·시뮬레이션 등에서 오래 쓰였다.

OpenGL vs 다른 API

OpenGL은 Windows·macOS·Linux에서 같은 코드에 가깝게 쓸 수 있고 자료도 많다. 대신 구버전과의 공존 때문에 API가 무겁게 느껴질 수 있다. Vulkan은 제어는 세밀하지만 부담도 크다. DirectX는 Windows·Xbox 쪽에 맞춰져 있고, Metal은 Apple 플랫폼 전용이다.

OpenGL 버전

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

OpenGL 1.x (1992~):
- 고정 파이프라인
- 레거시

OpenGL 2.0 (2004):
- 셰이더 도입 (GLSL)

OpenGL 3.x (2008):
- 현대적 API
- Deprecated 기능 제거

OpenGL 4.x (2010~):
- 테셀레이션
- 컴퓨트 셰이더
- 현재 표준

OpenGL ES:
- 모바일/임베디드 버전

WebGL:
- 웹 브라우저 버전

2. 개발 환경 설정

필수 라이브러리

GLFW (윈도우 관리):

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# macOS
brew install glfw

# Ubuntu
sudo apt install libglfw3-dev

# Windows (vcpkg)
vcpkg install glfw3

GLAD (OpenGL 함수 로더):

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# https://glad.dav1d.de/ 에서 생성
# OpenGL 버전: 4.6
# Profile: Core
# Generate a loader: 체크
# 다운로드 후 프로젝트에 포함

GLM (수학 라이브러리):

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# macOS
brew install glm

# Ubuntu
sudo apt install libglm-dev

# Windows (vcpkg)
vcpkg install glm

C++ 프로젝트 설정

CMakeLists.txt:

다음은 cmake를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

cmake_minimum_required(VERSION 3.10)
project(OpenGLApp)

set(CMAKE_CXX_STANDARD 17)

# GLFW 찾기
find_package(glfw3 REQUIRED)

# GLM 찾기
find_package(glm REQUIRED)

# 실행 파일
add_executable(app
    src/main.cpp
    src/glad.c
)

# 라이브러리 링크
target_link_libraries(app
    glfw
    ${CMAKE_DL_LIBS}
)

# 인클루드 디렉토리
target_include_directories(app PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

빌드:

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

mkdir build
cd build
cmake ..
make
./app

3. 첫 번째 삼각형

기본 윈도우 생성

main.cpp:

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

// 윈도우 크기 변경 콜백
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}

// 입력 처리
void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true);
    }
}

int main() {
    // GLFW 초기화
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return -1;
    }
    
    // OpenGL 버전 설정 (4.1)
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
    
    // 윈도우 생성
    GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Tutorial", nullptr, nullptr);
    if (!window) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    
    // GLAD 초기화
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cerr << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    
    // 뷰포트 설정
    glViewport(0, 0, 800, 600);
    
    // 렌더링 루프
    while (!glfwWindowShouldClose(window)) {
        // 입력 처리
        processInput(window);
        
        // 렌더링
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        // 버퍼 스왑 및 이벤트 처리
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    // 정리
    glfwTerminate();
    return 0;
}

삼각형 그리기

정점 데이터:

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 삼각형 정점 (x, y, z)
float vertices[] = {
    -0.5f, -0.5f, 0.0f,  // 왼쪽 아래
     0.5f, -0.5f, 0.0f,  // 오른쪽 아래
     0.0f,  0.5f, 0.0f   // 위
};

// VBO 생성
unsigned int VBO;
glGenBuffers(1, &VBO);

// VBO 바인딩 및 데이터 전송
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// VAO 생성
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

// 정점 속성 설정
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 언바인딩
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

Vertex Shader

vertex_shader.glsl:

아래 코드는 glsl를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#version 410 core

layout (location = 0) in vec3 aPos;

void main() {
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

Fragment Shader

fragment_shader.glsl:

아래 코드는 glsl를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#version 410 core

out vec4 FragColor;

void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);  // 주황색
}

셰이더 컴파일

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Vertex Shader 컴파일
const char* vertexShaderSource = R"(
#version 410 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";

unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);

// 컴파일 에러 확인
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
    glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
    std::cerr << "Vertex Shader Compilation Failed:\n" << infoLog << std::endl;
}

// Fragment Shader 컴파일
const char* fragmentShaderSource = R"(
#version 410 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";

unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);

glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
    glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
    std::cerr << "Fragment Shader Compilation Failed:\n" << infoLog << std::endl;
}

// 셰이더 프로그램 링크
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
    glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
    std::cerr << "Shader Program Linking Failed:\n" << infoLog << std::endl;
}

// 셰이더 삭제 (프로그램에 링크됨)
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

렌더링

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 렌더링 루프 내부
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

전체 코드:

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

const char* vertexShaderSource = R"(
#version 410 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";

const char* fragmentShaderSource = R"(
#version 410 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";

int main() {
    // GLFW 초기화 및 윈도우 생성 (이전 코드와 동일)
    // ...
    
    // 정점 데이터
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f
    };
    
    // VBO, VAO 생성
    unsigned int VBO, VAO;
    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);
    
    // 셰이더 컴파일 및 링크 (이전 코드)
    // ...
    
    // 렌더링 루프
    while (!glfwWindowShouldClose(window)) {
        processInput(window);
        
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        // 삼각형 그리기
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    // 정리
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);
    
    glfwTerminate();
    return 0;
}

4. 셰이더 프로그래밍

색상 보간

Vertex Shader (색상 포함):

아래 코드는 glsl를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main() {
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
}

Fragment Shader:

아래 코드는 glsl를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#version 410 core

// 실행 예제
in vec3 ourColor;
out vec4 FragColor;

void main() {
    FragColor = vec4(ourColor, 1.0);
}

정점 데이터 (위치 + 색상):

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

float vertices[] = {
    // 위치              // 색상
    -0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // 빨강
     0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // 초록
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // 파랑
};

// VBO 설정
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);

Uniform 변수

Shader에서 값 받기:

아래 코드는 glsl를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#version 410 core

out vec4 FragColor;

uniform vec4 ourColor;  // CPU에서 전달받는 값

void main() {
    FragColor = ourColor;
}

C++ 코드:

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Uniform 위치 찾기
int colorLocation = glGetUniformLocation(shaderProgram, "ourColor");

// 렌더링 루프
while (!glfwWindowShouldClose(window)) {
    // 시간에 따라 색상 변경
    float timeValue = glfwGetTime();
    float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
    
    glUseProgram(shaderProgram);
    glUniform4f(colorLocation, 0.0f, greenValue, 0.0f, 1.0f);
    
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

5. 텍스처 매핑

텍스처 로딩

stb_image 사용:

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// 텍스처 로딩 함수
unsigned int loadTexture(const char* path) {
    unsigned int textureID;
    glGenTextures(1, &textureID);
    
    int width, height, nrChannels;
    unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);
    
    if (data) {
        GLenum format = (nrChannels == 3) ? GL_RGB : 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 << "Failed to load texture: " << path << std::endl;
    }
    
    return textureID;
}

텍스처 좌표

정점 데이터 (위치 + 텍스처 좌표):

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

float vertices[] = {
    // 위치              // 텍스처 좌표
    -0.5f, -0.5f, 0.0f,  0.0f, 0.0f,  // 왼쪽 아래
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f,  // 오른쪽 아래
     0.5f,  0.5f, 0.0f,  1.0f, 1.0f,  // 오른쪽 위
    -0.5f,  0.5f, 0.0f,  0.0f, 1.0f   // 왼쪽 위
};

unsigned int indices[] = {
    0, 1, 2,  // 첫 번째 삼각형
    0, 2, 3   // 두 번째 삼각형
};

// VBO, VAO, EBO 설정
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO);

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

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 위치 속성
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 텍스처 좌표 속성
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

Vertex Shader:

아래 코드는 glsl를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

void main() {
    gl_Position = vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}

Fragment Shader:

아래 코드는 glsl를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

// 실행 예제
in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D texture1;

void main() {
    FragColor = texture(texture1, TexCoord);
}

렌더링:

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 텍스처 로딩
unsigned int texture = loadTexture("container.jpg");

// 렌더링 루프
while (!glfwWindowShouldClose(window)) {
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 텍스처 바인딩
    glBindTexture(GL_TEXTURE_2D, texture);
    
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

6. 3D 변환

변환 행렬

GLM 사용:

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// Model 행렬 (객체 변환)
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 0.0f, 1.0f));

// View 행렬 (카메라)
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));

// Projection 행렬 (원근)
glm::mat4 projection = glm::perspective(
    glm::radians(45.0f),  // FOV
    800.0f / 600.0f,      // 종횡비
    0.1f,                 // Near plane
    100.0f                // Far plane
);

Vertex Shader:

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

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

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}

C++ 코드:

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Uniform 전달
unsigned int modelLoc = glGetUniformLocation(shaderProgram, "model");
unsigned int viewLoc = glGetUniformLocation(shaderProgram, "view");
unsigned int projectionLoc = glGetUniformLocation(shaderProgram, "projection");

glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

회전하는 큐브

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 큐브 정점 (36개 정점 = 12개 삼각형 = 6개 면)
float vertices[] = {
    // 앞면
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
    // ... 나머지 5개 면
};

// 렌더링 루프
while (!glfwWindowShouldClose(window)) {
    // 깊이 테스트 활성화
    glEnable(GL_DEPTH_TEST);
    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // 회전 애니메이션
    float angle = glfwGetTime() * 50.0f;
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.5f, 0.0f));
    
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    
    glDrawArrays(GL_TRIANGLES, 0, 36);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

7. 카메라 시스템

카메라 클래스

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

class Camera {
public:
    glm::vec3 position;
    glm::vec3 front;
    glm::vec3 up;
    glm::vec3 right;
    glm::vec3 worldUp;
    
    float yaw;
    float pitch;
    float movementSpeed;
    float mouseSensitivity;
    float zoom;
    
    Camera(glm::vec3 pos = glm::vec3(0.0f, 0.0f, 3.0f)) 
        : position(pos),
          worldUp(glm::vec3(0.0f, 1.0f, 0.0f)),
          yaw(-90.0f),
          pitch(0.0f),
          movementSpeed(2.5f),
          mouseSensitivity(0.1f),
          zoom(45.0f)
    {
        updateCameraVectors();
    }
    
    glm::mat4 getViewMatrix() {
        return glm::lookAt(position, position + front, up);
    }
    
    void processKeyboard(int direction, float deltaTime) {
        float velocity = movementSpeed * deltaTime;
        
        if (direction == 0) position += front * velocity;  // W
        if (direction == 1) position -= front * velocity;  // S
        if (direction == 2) position -= right * velocity;  // A
        if (direction == 3) position += right * velocity;  // D
    }
    
    void processMouseMovement(float xoffset, float yoffset) {
        xoffset *= mouseSensitivity;
        yoffset *= mouseSensitivity;
        
        yaw += xoffset;
        pitch += yoffset;
        
        // 피치 제한 (위아래 90도)
        if (pitch > 89.0f) pitch = 89.0f;
        if (pitch < -89.0f) pitch = -89.0f;
        
        updateCameraVectors();
    }
    
    void processMouseScroll(float yoffset) {
        zoom -= yoffset;
        if (zoom < 1.0f) zoom = 1.0f;
        if (zoom > 45.0f) zoom = 45.0f;
    }
    
private:
    void updateCameraVectors() {
        glm::vec3 newFront;
        newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
        newFront.y = sin(glm::radians(pitch));
        newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
        front = glm::normalize(newFront);
        
        right = glm::normalize(glm::cross(front, worldUp));
        up = glm::normalize(glm::cross(right, front));
    }
};

사용:

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));

// 마우스 콜백
float lastX = 400, lastY = 300;
bool firstMouse = true;

void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    if (firstMouse) {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
    
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos;  // Y는 반대
    
    lastX = xpos;
    lastY = ypos;
    
    camera.processMouseMovement(xoffset, yoffset);
}

glfwSetCursorPosCallback(window, mouse_callback);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

// 렌더링 루프
float deltaTime = 0.0f;
float lastFrame = 0.0f;

while (!glfwWindowShouldClose(window)) {
    float currentFrame = glfwGetTime();
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;
    
    // 키보드 입력
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.processKeyboard(0, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.processKeyboard(1, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.processKeyboard(2, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.processKeyboard(3, deltaTime);
    
    // View 행렬 업데이트
    glm::mat4 view = camera.getViewMatrix();
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
    
    // 렌더링
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 36);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

8. 조명 (Lighting)

Phong 조명 모델

Fragment Shader:

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

out vec4 FragColor;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
uniform sampler2D texture1;

void main() {
    // Ambient (환경광)
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
    
    // Diffuse (확산광)
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // Specular (반사광)
    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 * lightColor;
    
    // 최종 색상
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
}

Vertex Shader:

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;

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

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

C++ 코드:

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 조명 설정
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 objectColor(1.0f, 0.5f, 0.31f);

// Uniform 전달
glUniform3fv(glGetUniformLocation(shaderProgram, "lightPos"), 1, glm::value_ptr(lightPos));
glUniform3fv(glGetUniformLocation(shaderProgram, "viewPos"), 1, glm::value_ptr(camera.position));
glUniform3fv(glGetUniformLocation(shaderProgram, "lightColor"), 1, glm::value_ptr(lightColor));
glUniform3fv(glGetUniformLocation(shaderProgram, "objectColor"), 1, glm::value_ptr(objectColor));

다중 조명

다음은 glsl를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

struct Light {
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform Light lights[4];
uniform int numLights;

void main() {
    vec3 result = vec3(0.0);
    
    for (int i = 0; i < numLights; i++) {
        // Ambient
        vec3 ambient = lights[i].ambient;
        
        // Diffuse
        vec3 lightDir = normalize(lights[i].position - FragPos);
        float diff = max(dot(Normal, lightDir), 0.0);
        vec3 diffuse = lights[i].diffuse * diff;
        
        // Specular
        vec3 viewDir = normalize(viewPos - FragPos);
        vec3 reflectDir = reflect(-lightDir, Normal);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
        vec3 specular = lights[i].specular * spec;
        
        result += ambient + diffuse + specular;
    }
    
    FragColor = vec4(result * objectColor, 1.0);
}

9. 3D 모델 로딩

Assimp 사용

설치:

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# macOS
brew install assimp

# Ubuntu
sudo apt install libassimp-dev

# Windows (vcpkg)
vcpkg install assimp

모델 로더:

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

struct Vertex {
    glm::vec3 position;
    glm::vec3 normal;
    glm::vec2 texCoords;
};

struct Mesh {
    std::vector<Vertex> vertices;
    std::vector<unsigned int> indices;
    unsigned int VAO, VBO, EBO;
    
    void 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(unsigned int), &indices[0], GL_STATIC_DRAW);
        
        // 위치
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
        glEnableVertexAttribArray(0);
        
        // 노멀
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
        glEnableVertexAttribArray(1);
        
        // 텍스처 좌표
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoords));
        glEnableVertexAttribArray(2);
        
        glBindVertexArray(0);
    }
    
    void draw(unsigned int shaderProgram) {
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }
};

class Model {
public:
    std::vector<Mesh> meshes;
    
    Model(const std::string& path) {
        loadModel(path);
    }
    
    void draw(unsigned int shaderProgram) {
        for (auto& mesh : meshes) {
            mesh.draw(shaderProgram);
        }
    }
    
private:
    void loadModel(const std::string& path) {
        Assimp::Importer importer;
        const aiScene* scene = importer.ReadFile(path, 
            aiProcess_Triangulate | 
            aiProcess_FlipUVs | 
            aiProcess_CalcTangentSpace);
        
        if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
            std::cerr << "Assimp Error: " << importer.GetErrorString() << std::endl;
            return;
        }
        
        processNode(scene->mRootNode, scene);
    }
    
    void 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 processMesh(aiMesh* mesh, const aiScene* scene) {
        std::vector<Vertex> vertices;
        std::vector<unsigned int> indices;
        
        // 정점 처리
        for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
            Vertex vertex;
            
            // 위치
            vertex.position = glm::vec3(
                mesh->mVertices[i].x,
                mesh->mVertices[i].y,
                mesh->mVertices[i].z
            );
            
            // 노멀
            if (mesh->HasNormals()) {
                vertex.normal = glm::vec3(
                    mesh->mNormals[i].x,
                    mesh->mNormals[i].y,
                    mesh->mNormals[i].z
                );
            }
            
            // 텍스처 좌표
            if (mesh->mTextureCoords[0]) {
                vertex.texCoords = glm::vec2(
                    mesh->mTextureCoords[0][i].x,
                    mesh->mTextureCoords[0][i].y
                );
            } 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]);
            }
        }
        
        Mesh result;
        result.vertices = vertices;
        result.indices = indices;
        result.setupMesh();
        
        return result;
    }
};

// 사용
Model backpack("models/backpack.obj");

// 렌더링 루프
while (!glfwWindowShouldClose(window)) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glUseProgram(shaderProgram);
    
    // 변환 행렬 설정
    glm::mat4 model = glm::mat4(1.0f);
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    
    backpack.draw(shaderProgram);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

10. Python에서 OpenGL 사용

PyOpenGL 설치

pip install PyOpenGL PyOpenGL_accelerate
pip install glfw
pip install PyGLM

첫 번째 삼각형 (Python)

다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import glfw
from OpenGL.GL import *
import numpy as np

# Vertex Shader
vertex_shader_source = """
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
"""

# Fragment Shader
fragment_shader_source = """
#version 330 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
"""

def compile_shader(source, shader_type):
    """셰이더 컴파일"""
    shader = glCreateShader(shader_type)
    glShaderSource(shader, source)
    glCompileShader(shader)
    
    # 에러 확인
    if not glGetShaderiv(shader, GL_COMPILE_STATUS):
        error = glGetShaderInfoLog(shader).decode()
        print(f"Shader compilation error:\n{error}")
        return None
    
    return shader

def create_shader_program(vertex_source, fragment_source):
    """셰이더 프로그램 생성"""
    vertex_shader = compile_shader(vertex_source, GL_VERTEX_SHADER)
    fragment_shader = compile_shader(fragment_source, GL_FRAGMENT_SHADER)
    
    program = glCreateProgram()
    glAttachShader(program, vertex_shader)
    glAttachShader(program, fragment_shader)
    glLinkProgram(program)
    
    # 링크 에러 확인
    if not glGetProgramiv(program, GL_LINK_STATUS):
        error = glGetProgramInfoLog(program).decode()
        print(f"Program linking error:\n{error}")
        return None
    
    glDeleteShader(vertex_shader)
    glDeleteShader(fragment_shader)
    
    return program

def main():
    # GLFW 초기화
    if not glfw.init():
        return
    
    # OpenGL 버전 설정
    glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
    glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
    glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
    
    # 윈도우 생성
    window = glfw.create_window(800, 600, "OpenGL Python", None, None)
    if not window:
        glfw.terminate()
        return
    
    glfw.make_context_current(window)
    
    # 정점 데이터
    vertices = np.array([
        -0.5, -0.5, 0.0,
         0.5, -0.5, 0.0,
         0.0,  0.5, 0.0
    ], dtype=np.float32)
    
    # VAO, VBO 생성
    VAO = glGenVertexArrays(1)
    VBO = glGenBuffers(1)
    
    glBindVertexArray(VAO)
    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
    
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * vertices.itemsize, ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)
    
    # 셰이더 프로그램
    shader_program = create_shader_program(vertex_shader_source, fragment_shader_source)
    
    # 렌더링 루프
    while not glfw.window_should_close(window):
        # 입력 처리
        if glfw.get_key(window, glfw.KEY_ESCAPE) == glfw.PRESS:
            glfw.set_window_should_close(window, True)
        
        # 렌더링
        glClearColor(0.2, 0.3, 0.3, 1.0)
        glClear(GL_COLOR_BUFFER_BIT)
        
        glUseProgram(shader_program)
        glBindVertexArray(VAO)
        glDrawArrays(GL_TRIANGLES, 0, 3)
        
        glfw.swap_buffers(window)
        glfw.poll_events()
    
    # 정리
    glDeleteVertexArrays(1, [VAO])
    glDeleteBuffers(1, [VBO])
    glDeleteProgram(shader_program)
    
    glfw.terminate()

if __name__ == '__main__':
    main()

3D 큐브 (Python)

다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import glfw
from OpenGL.GL import *
import numpy as np
import pyrr
from math import sin, cos

# 정점 데이터 (위치 + 색상)
vertices = np.array([
    # 위치              색상
    -0.5, -0.5, -0.5,  1.0, 0.0, 0.0,
     0.5, -0.5, -0.5,  0.0, 1.0, 0.0,
     0.5,  0.5, -0.5,  0.0, 0.0, 1.0,
    -0.5,  0.5, -0.5,  1.0, 1.0, 0.0,
    -0.5, -0.5,  0.5,  1.0, 0.0, 1.0,
     0.5, -0.5,  0.5,  0.0, 1.0, 1.0,
     0.5,  0.5,  0.5,  1.0, 1.0, 1.0,
    -0.5,  0.5,  0.5,  0.5, 0.5, 0.5
], dtype=np.float32)

indices = np.array([
    0, 1, 2, 2, 3, 0,  # 앞면
    4, 5, 6, 6, 7, 4,  # 뒷면
    0, 1, 5, 5, 4, 0,  # 아래면
    2, 3, 7, 7, 6, 2,  # 위면
    0, 3, 7, 7, 4, 0,  # 왼쪽면
    1, 2, 6, 6, 5, 1   # 오른쪽면
], dtype=np.uint32)

vertex_shader = """
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

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

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ourColor = aColor;
}
"""

fragment_shader = """
#version 330 core
in vec3 ourColor;
out vec4 FragColor;

void main() {
    FragColor = vec4(ourColor, 1.0);
}
"""

def main():
    # GLFW 초기화 및 윈도우 생성
    if not glfw.init():
        return
    
    glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
    glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
    glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
    
    window = glfw.create_window(800, 600, "3D Cube", None, None)
    if not window:
        glfw.terminate()
        return
    
    glfw.make_context_current(window)
    glEnable(GL_DEPTH_TEST)
    
    # VAO, VBO, EBO 설정
    VAO = glGenVertexArrays(1)
    VBO = glGenBuffers(1)
    EBO = glGenBuffers(1)
    
    glBindVertexArray(VAO)
    
    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
    
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
    
    # 위치 속성
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * vertices.itemsize, ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)
    
    # 색상 속성
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * vertices.itemsize, ctypes.c_void_p(3 * vertices.itemsize))
    glEnableVertexAttribArray(1)
    
    # 셰이더 프로그램
    shader = create_shader_program(vertex_shader, fragment_shader)
    
    # 변환 행렬
    projection = pyrr.matrix44.create_perspective_projection_matrix(
        45.0, 800/600, 0.1, 100.0
    )
    view = pyrr.matrix44.create_look_at(
        pyrr.Vector3([0.0, 0.0, 3.0]),  # 카메라 위치
        pyrr.Vector3([0.0, 0.0, 0.0]),  # 바라보는 지점
        pyrr.Vector3([0.0, 1.0, 0.0])   # 위 방향
    )
    
    # Uniform 위치
    model_loc = glGetUniformLocation(shader, "model")
    view_loc = glGetUniformLocation(shader, "view")
    proj_loc = glGetUniformLocation(shader, "projection")
    
    # 렌더링 루프
    while not glfw.window_should_close(window):
        glfw.poll_events()
        
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        
        glUseProgram(shader)
        
        # 회전 애니메이션
        angle = glfw.get_time() * 50
        model = pyrr.matrix44.create_from_axis_rotation(
            pyrr.Vector3([1.0, 0.5, 0.0]),
            np.radians(angle)
        )
        
        # Uniform 전달
        glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
        glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)
        glUniformMatrix4fv(proj_loc, 1, GL_FALSE, projection)
        
        # 그리기
        glBindVertexArray(VAO)
        glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)
        
        glfw.swap_buffers(window)
    
    glfw.terminate()

if __name__ == '__main__':
    main()

11. 고급 셰이더 기법

Normal Mapping

Fragment Shader:

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

in vec3 FragPos;
in vec2 TexCoord;
in vec3 TangentLightPos;
in vec3 TangentViewPos;
in vec3 TangentFragPos;

out vec4 FragColor;

uniform sampler2D diffuseMap;
uniform sampler2D normalMap;

void main() {
    // 노멀 맵에서 노멀 가져오기
    vec3 normal = texture(normalMap, TexCoord).rgb;
    normal = normalize(normal * 2.0 - 1.0);  // [0,1] → [-1,1]
    
    // 조명 계산
    vec3 lightDir = normalize(TangentLightPos - TangentFragPos);
    float diff = max(dot(normal, lightDir), 0.0);
    
    vec3 viewDir = normalize(TangentViewPos - TangentFragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    
    vec3 color = texture(diffuseMap, TexCoord).rgb;
    vec3 ambient = 0.1 * color;
    vec3 diffuse = diff * color;
    vec3 specular = vec3(0.2) * spec;
    
    FragColor = vec4(ambient + diffuse + specular, 1.0);
}

Shadow Mapping

Depth Shader (그림자 맵 생성):

아래 코드는 glsl를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

layout (location = 0) in vec3 aPos;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main() {
    gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}

Fragment Shader (그림자 적용):

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in vec4 FragPosLightSpace;

out vec4 FragColor;

uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap;
uniform vec3 lightPos;
uniform vec3 viewPos;

float shadowCalculation(vec4 fragPosLightSpace) {
    // 원근 나누기
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    
    // [-1,1] → [0,1]
    projCoords = projCoords * 0.5 + 0.5;
    
    // 깊이 맵에서 값 가져오기
    float closestDepth = texture(shadowMap, projCoords.xy).r;
    float currentDepth = projCoords.z;
    
    // 그림자 판정
    float bias = 0.005;
    float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
    
    return shadow;
}

void main() {
    vec3 color = texture(diffuseTexture, TexCoord).rgb;
    vec3 normal = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    
    // Ambient
    vec3 ambient = 0.15 * color;
    
    // Diffuse
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * color;
    
    // Specular
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 halfwayDir = normalize(lightDir + viewDir);
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
    vec3 specular = spec * vec3(0.3);
    
    // 그림자 계산
    float shadow = shadowCalculation(FragPosLightSpace);
    
    // 최종 색상 (그림자 영역은 ambient만)
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
    
    FragColor = vec4(lighting, 1.0);
}

Bloom 효과

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D scene;
uniform sampler2D bloomBlur;
uniform float exposure;

void main() {
    vec3 hdrColor = texture(scene, TexCoord).rgb;
    vec3 bloomColor = texture(bloomBlur, TexCoord).rgb;
    
    // Bloom 합성
    hdrColor += bloomColor;
    
    // Tone mapping (HDR → LDR)
    vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
    
    // Gamma correction
    result = pow(result, vec3(1.0 / 2.2));
    
    FragColor = vec4(result, 1.0);
}

12. 실전 프로젝트

프로젝트 1: 3D 뷰어

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "Camera.h"
#include "Shader.h"
#include "Model.h"

// 설정
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// 카메라
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;

// 타이밍
float deltaTime = 0.0f;
float lastFrame = 0.0f;

// 콜백
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow* window);

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " <model_path>" << std::endl;
        return -1;
    }
    
    // GLFW 초기화
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
    
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "3D Model Viewer", nullptr, nullptr);
    if (!window) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    
    glfwMakeContextCurrent(window);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cerr << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    
    glEnable(GL_DEPTH_TEST);
    
    // 셰이더 및 모델 로딩
    Shader shader("shaders/model.vs", "shaders/model.fs");
    Model model(argv[1]);
    
    // 렌더링 루프
    while (!glfwWindowShouldClose(window)) {
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;
        
        processInput(window);
        
        glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        shader.use();
        
        // 변환 행렬
        glm::mat4 projection = glm::perspective(
            glm::radians(camera.zoom),
            (float)SCR_WIDTH / (float)SCR_HEIGHT,
            0.1f, 100.0f
        );
        glm::mat4 view = camera.getViewMatrix();
        glm::mat4 modelMat = glm::mat4(1.0f);
        
        shader.setMat4("projection", projection);
        shader.setMat4("view", view);
        shader.setMat4("model", modelMat);
        
        // 조명
        shader.setVec3("lightPos", glm::vec3(1.2f, 1.0f, 2.0f));
        shader.setVec3("viewPos", camera.position);
        
        model.draw(shader);
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    glfwTerminate();
    return 0;
}

void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
    
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.processKeyboard(0, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.processKeyboard(1, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.processKeyboard(2, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.processKeyboard(3, deltaTime);
}

void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    if (firstMouse) {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
    
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos;
    
    lastX = xpos;
    lastY = ypos;
    
    camera.processMouseMovement(xoffset, yoffset);
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    camera.processMouseScroll(yoffset);
}

프로젝트 2: 파티클 시스템

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct Particle {
    glm::vec3 position;
    glm::vec3 velocity;
    glm::vec4 color;
    float life;
};

class ParticleSystem {
private:
    std::vector<Particle> particles;
    unsigned int VAO, VBO;
    unsigned int maxParticles;
    
public:
    ParticleSystem(unsigned int maxParticles) : maxParticles(maxParticles) {
        particles.resize(maxParticles);
        
        // VAO, VBO 설정
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, maxParticles * sizeof(Particle), nullptr, GL_DYNAMIC_DRAW);
        
        // 속성 설정
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)offsetof(Particle, position));
        glEnableVertexAttribArray(0);
        
        glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)offsetof(Particle, color));
        glEnableVertexAttribArray(1);
    }
    
    void emit(glm::vec3 position, int count) {
        for (int i = 0; i < count; i++) {
            // 죽은 파티클 찾기
            for (auto& p : particles) {
                if (p.life <= 0.0f) {
                    p.position = position;
                    p.velocity = glm::vec3(
                        (rand() % 200 - 100) / 100.0f,
                        (rand() % 200) / 100.0f,
                        (rand() % 200 - 100) / 100.0f
                    );
                    p.color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
                    p.life = 1.0f;
                    break;
                }
            }
        }
    }
    
    void update(float deltaTime) {
        for (auto& p : particles) {
            if (p.life > 0.0f) {
                p.life -= deltaTime;
                p.position += p.velocity * deltaTime;
                p.velocity.y -= 9.8f * deltaTime;  // 중력
                p.color.a = p.life;  // 페이드 아웃
            }
        }
    }
    
    void draw(unsigned int shaderProgram) {
        // 데이터 업데이트
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, particles.size() * sizeof(Particle), &particles[0]);
        
        // 그리기
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_POINTS, 0, particles.size());
        
        glDisable(GL_BLEND);
    }
};

// 사용
ParticleSystem particles(1000);

// 렌더링 루프
while (!glfwWindowShouldClose(window)) {
    float currentFrame = glfwGetTime();
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;
    
    // 파티클 방출
    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) {
        particles.emit(glm::vec3(0.0f, 0.0f, 0.0f), 10);
    }
    
    particles.update(deltaTime);
    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    particles.draw(shaderProgram);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

13. 프레임버퍼와 후처리

프레임버퍼 생성

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 프레임버퍼 생성
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

// 텍스처 생성 (색상 버퍼)
unsigned int textureColorbuffer;
glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);

// 렌더버퍼 생성 (깊이 + 스텐실)
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

// 프레임버퍼 완성 확인
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    std::cerr << "Framebuffer is not complete!" << std::endl;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);

후처리 셰이더

그레이스케일:

아래 코드는 glsl를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D screenTexture;

void main() {
    vec3 color = texture(screenTexture, TexCoord).rgb;
    float average = (color.r + color.g + color.b) / 3.0;
    FragColor = vec4(vec3(average), 1.0);
}

블러:

다음은 glsl를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D screenTexture;

const float offset = 1.0 / 300.0;

void main() {
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), vec2(0.0,  offset), vec2(offset,  offset),
        vec2(-offset,    0.0),  vec2(0.0,    0.0),  vec2(offset,    0.0),
        vec2(-offset, -offset), vec2(0.0, -offset), vec2(offset, -offset)
    );
    
    float kernel[9] = float[](
        1.0 / 16, 2.0 / 16, 1.0 / 16,
        2.0 / 16, 4.0 / 16, 2.0 / 16,
        1.0 / 16, 2.0 / 16, 1.0 / 16
    );
    
    vec3 color = vec3(0.0);
    for (int i = 0; i < 9; i++) {
        color += texture(screenTexture, TexCoord + offsets[i]).rgb * kernel[i];
    }
    
    FragColor = vec4(color, 1.0);
}

렌더링:

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 1단계: 프레임버퍼에 렌더링
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 씬 렌더링
model.draw(shader);

// 2단계: 화면에 후처리 적용
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDisable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT);

postProcessShader.use();
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);

14. 최적화 기법

인스턴싱

같은 객체를 여러 번 그리기:

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 인스턴스 데이터 (위치)
glm::vec3 translations[100];
for (int i = 0; i < 100; i++) {
    translations[i] = glm::vec3(
        (i % 10) * 2.0f,
        (i / 10) * 2.0f,
        0.0f
    );
}

// 인스턴스 VBO
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * 100, &translations[0], GL_STATIC_DRAW);

glBindVertexArray(VAO);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
glEnableVertexAttribArray(2);
glVertexAttribDivisor(2, 1);  // 인스턴스당 1번 업데이트

// 인스턴싱 렌더링
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 100);

Vertex Shader:

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec3 aInstancePos;

uniform mat4 view;
uniform mat4 projection;

void main() {
    mat4 model = mat4(1.0);
    model[3] = vec4(aInstancePos, 1.0);
    
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

컬링 (Culling)

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Face Culling (뒷면 제거)
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);

// Frustum Culling (화면 밖 객체 제거)
bool isInFrustum(const BoundingBox& box, const glm::mat4& viewProj) {
    // AABB와 Frustum 교차 테스트
    // ...
}

// 렌더링
for (const auto& object : objects) {
    if (isInFrustum(object.boundingBox, viewProjection)) {
        object.draw();
    }
}

15. 디버깅

OpenGL 에러 확인

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void checkGLError(const char* stmt, const char* fname, int line) {
    GLenum err = glGetError();
    if (err != GL_NO_ERROR) {
        std::cerr << "OpenGL error " << err << " at " << fname << ":" << line << " - " << stmt << std::endl;
    }
}

#define GL_CHECK(stmt) do { \
    stmt; \
    checkGLError(#stmt, __FILE__, __LINE__); \
} while (0)

// 사용
GL_CHECK(glDrawArrays(GL_TRIANGLES, 0, 3));

셰이더 디버깅

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void printShaderLog(unsigned int shader) {
    int length = 0;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
    
    if (length > 1) {
        char* log = new char[length];
        glGetShaderInfoLog(shader, length, nullptr, log);
        std::cout << "Shader Log:\n" << log << std::endl;
        delete[] log;
    }
}

void printProgramLog(unsigned int program) {
    int length = 0;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
    
    if (length > 1) {
        char* log = new char[length];
        glGetProgramInfoLog(program, length, nullptr, log);
        std::cout << "Program Log:\n" << log << std::endl;
        delete[] log;
    }
}

16. 실전 팁

Shader 클래스

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>

class Shader {
public:
    unsigned int ID;
    
    Shader(const char* vertexPath, const char* fragmentPath) {
        // 파일 읽기
        std::string vertexCode = readFile(vertexPath);
        std::string fragmentCode = readFile(fragmentPath);
        
        const char* vShaderCode = vertexCode.c_str();
        const char* fShaderCode = fragmentCode.c_str();
        
        // 컴파일
        unsigned int vertex = compileShader(vShaderCode, GL_VERTEX_SHADER);
        unsigned int fragment = compileShader(fShaderCode, GL_FRAGMENT_SHADER);
        
        // 링크
        ID = glCreateProgram();
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        
        checkCompileErrors(ID, "PROGRAM");
        
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }
    
    void use() {
        glUseProgram(ID);
    }
    
    void setBool(const std::string& name, bool value) const {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
    }
    
    void setInt(const std::string& name, int value) const {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
    }
    
    void setFloat(const std::string& name, float value) const {
        glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
    }
    
    void setVec3(const std::string& name, const glm::vec3& value) const {
        glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, glm::value_ptr(value));
    }
    
    void setMat4(const std::string& name, const glm::mat4& mat) const {
        glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat));
    }
    
private:
    std::string readFile(const char* path) {
        std::ifstream file(path);
        std::stringstream buffer;
        buffer << file.rdbuf();
        return buffer.str();
    }
    
    unsigned int compileShader(const char* source, GLenum type) {
        unsigned int shader = glCreateShader(type);
        glShaderSource(shader, 1, &source, nullptr);
        glCompileShader(shader);
        checkCompileErrors(shader, type == GL_VERTEX_SHADER ? "VERTEX" : "FRAGMENT");
        return shader;
    }
    
    void checkCompileErrors(unsigned int shader, std::string type) {
        int success;
        char infoLog[1024];
        
        if (type != "PROGRAM") {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (!success) {
                glGetShaderInfoLog(shader, 1024, nullptr, infoLog);
                std::cerr << "Shader compilation error (" << type << "):\n" << infoLog << std::endl;
            }
        } else {
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (!success) {
                glGetProgramInfoLog(shader, 1024, nullptr, infoLog);
                std::cerr << "Program linking error:\n" << infoLog << std::endl;
            }
        }
    }
};

// 사용
Shader shader("shaders/vertex.glsl", "shaders/fragment.glsl");

shader.use();
shader.setFloat("time", glfwGetTime());
shader.setVec3("lightPos", glm::vec3(1.2f, 1.0f, 2.0f));
shader.setMat4("model", model);

17. 게임 엔진 통합

OpenGL + SDL2

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <SDL2/SDL.h>
#include <glad/glad.h>

int main() {
    // SDL 초기화
    SDL_Init(SDL_INIT_VIDEO);
    
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    
    // 윈도우 생성
    SDL_Window* window = SDL_CreateWindow(
        "OpenGL SDL2",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        800, 600,
        SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
    );
    
    SDL_GLContext context = SDL_GL_CreateContext(window);
    gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress);
    
    // 렌더링 루프
    bool running = true;
    SDL_Event event;
    
    while (running) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
        }
        
        glClear(GL_COLOR_BUFFER_BIT);
        
        // 렌더링...
        
        SDL_GL_SwapWindow(window);
    }
    
    SDL_GL_DeleteContext(context);
    SDL_DestroyWindow(window);
    SDL_Quit();
    
    return 0;
}

18. 트러블슈팅

일반적인 문제

1) “Failed to create GLFW window”

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 원인: OpenGL 버전 미지원
// 해결: 낮은 버전 시도
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

// 또는 호환성 프로필
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

2) “Failed to initialize GLAD”

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 원인: Context가 current가 아님
// 해결: makeContextCurrent 먼저 호출
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

3) 검은 화면

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 원인 1: 깊이 테스트 미활성화
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 원인 2: 카메라 위치 문제
// 카메라가 객체 안에 있거나 너무 멀리 있음

// 원인 3: 셰이더 에러
// 셰이더 컴파일 로그 확인

4) 텍스처가 안 보임

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 원인 1: 텍스처 좌표 누락
// 정점 데이터에 텍스처 좌표 포함 확인

// 원인 2: 텍스처 바인딩 누락
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(shader, "texture1"), 0);

// 원인 3: 이미지 뒤집힘
stbi_set_flip_vertically_on_load(true);

19. 성능 최적화

배치 렌더링

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 느림 (Draw Call 많음)
for (int i = 0; i < 1000; i++) {
    glBindVertexArray(VAO[i]);
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

// ✅ 빠름 (인스턴싱)
glBindVertexArray(VAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 1000);

VBO 업데이트 최적화

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 느림 (매 프레임 전체 재생성)
glBufferData(GL_ARRAY_BUFFER, size, data, GL_DYNAMIC_DRAW);

// ✅ 빠름 (일부만 업데이트)
glBufferSubData(GL_ARRAY_BUFFER, offset, size, data);

// ✅ 더 빠름 (매핑)
void* ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
memcpy(ptr, data, size);
glUnmapBuffer(GL_ARRAY_BUFFER);

텍스처 아틀라스

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 느림 (텍스처 바인딩 많음)
for (auto& sprite : sprites) {
    glBindTexture(GL_TEXTURE_2D, sprite.texture);
    sprite.draw();
}

// ✅ 빠름 (텍스처 아틀라스)
// 여러 이미지를 하나의 큰 텍스처에 합침
glBindTexture(GL_TEXTURE_2D, textureAtlas);
for (auto& sprite : sprites) {
    sprite.draw();  // 텍스처 좌표만 다름
}

20. 최신 OpenGL 기능

Compute Shader

다음은 glsl를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#version 430 core

layout (local_size_x = 16, local_size_y = 16) in;
layout (rgba32f, binding = 0) uniform image2D imgOutput;

void main() {
    ivec2 pixelCoords = ivec2(gl_GlobalInvocationID.xy);
    ivec2 dims = imageSize(imgOutput);
    
    // 그라디언트 생성
    vec4 color = vec4(
        float(pixelCoords.x) / float(dims.x),
        float(pixelCoords.y) / float(dims.y),
        0.0,
        1.0
    );
    
    imageStore(imgOutput, pixelCoords, color);
}

C++ 코드:

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 텍스처 생성
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 512, 512, 0, GL_RGBA, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// 이미지 바인딩
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F);

// Compute Shader 실행
glUseProgram(computeShader);
glDispatchCompute(512 / 16, 512 / 16, 1);

// 완료 대기
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);

// 결과 사용
glBindTexture(GL_TEXTURE_2D, texture);
// 렌더링...

Tessellation Shader

다음은 glsl를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Tessellation Control Shader
#version 410 core

layout (vertices = 3) out;

void main() {
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
    
    if (gl_InvocationID == 0) {
        gl_TessLevelOuter[0] = 4.0;
        gl_TessLevelOuter[1] = 4.0;
        gl_TessLevelOuter[2] = 4.0;
        gl_TessLevelInner[0] = 4.0;
    }
}

// Tessellation Evaluation Shader
#version 410 core

layout (triangles, equal_spacing, ccw) in;

void main() {
    vec3 p0 = gl_in[0].gl_Position.xyz;
    vec3 p1 = gl_in[1].gl_Position.xyz;
    vec3 p2 = gl_in[2].gl_Position.xyz;
    
    vec3 pos = gl_TessCoord.x * p0 + gl_TessCoord.y * p1 + gl_TessCoord.z * p2;
    
    gl_Position = vec4(pos, 1.0);
}

21. WebGL 비교

OpenGL vs WebGL

데스크톱 OpenGL은 기능과 성능 여유가 있고, WebGL은 브라우저에서 돌기 위해 OpenGL ES 계열로 제한된 API다. 설치 없이 배포할 수 있는 대신 기능·성능 면에서 타협이 있다.

WebGL 예제 (JavaScript):

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Vertex Shader
const vertexShaderSource = `
attribute vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
`;

// Fragment Shader
const fragmentShaderSource = `
precision mediump float;
void main() {
    gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
`;

// Canvas 및 Context
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');

// 셰이더 컴파일
function compileShader(gl, source, type) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('Shader compilation error:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    
    return shader;
}

const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);

// 프로그램 링크
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

// 정점 데이터
const vertices = new Float32Array([
    -0.5, -0.5, 0.0,
     0.5, -0.5, 0.0,
     0.0,  0.5, 0.0
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 속성 설정
const positionLocation = gl.getAttribLocation(program, 'aPos');
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);

// 렌더링
gl.clearColor(0.2, 0.3, 0.3, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);

22. 유용한 도구

RenderDoc (디버깅)

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

RenderDoc:
- 프레임 캡처 및 분석
- Draw Call 확인
- 셰이더 디버깅
- 텍스처 확인

다운로드: https://renderdoc.org/

Nsight Graphics (NVIDIA)

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Nsight Graphics:
- GPU 프로파일링
- 성능 분석
- 셰이더 최적화

다운로드: https://developer.nvidia.com/nsight-graphics

FAQ

Q1. OpenGL과 Vulkan 중 무엇부터 보나?

학습·프로토타입·레거시 유지에는 OpenGL이 부담이 적다. 성능과 멀티스레딩·명시적 동기화까지 손에 쥐고 싶으면 Vulkan 쪽으로 넘어가는 경우가 많다.

Q2. OpenGL은 deprecated인가?

macOS에서는 OpenGL이 더 이상 권장되지 않고 Metal 쪽으로 안내하는 분위기다. Windows·Linux에서는 여전히 쓰이지만, 신규 대형 프로젝트는 API 선택을 다시 짓는 편이 안전하다.

Q3. 몇 버전까지 보면 되나?

모던 튜토리얼은 Core Profile(예: 3.3 이상) 기준이 많다. 고정 파이프라인보다 셰이더 중심으로 잡는 게 이후 학습에도 이어진다.

Q4. 게임 엔진과 직접 OpenGL의 차이는?

엔진은 에디터·물리·에셋 파이프라인까지 묶여 있어 제품을 빨리 만든다. 직접 OpenGL은 제어와 학습에는 좋지만 시간이 많이 든다. 목적이 학습·렌더러 실험이면 OpenGL, 상용 게임 제작이면 엔진 쪽이 현실적이다.

Q5. 셰이더(GLSL)는 어떻게 익히나?

learnopengl.com 같은 튜토리얼로 Vertex·Fragment를 먼저 박고, 조명·텍스처까지 붙인 뒤 필요에 따라 고급 주제로 넓히면 된다. The Book of Shaders, Shadertoy는 감각과 수식을 익히는 데 도움이 된다.


요약

핵심 정리

OpenGL 기본 구조:

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 1. 초기화
// 실행 예제
glfwInit();
GLFWwindow* window = glfwCreateWindow(...);
gladLoadGLLoader(...);

// 2. 데이터 준비
float vertices[] = { ... };
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 3. 셰이더
unsigned int shader = createShaderProgram(...);

// 4. 렌더링 루프
while (!glfwWindowShouldClose(window)) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glUseProgram(shader);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    glfwSwapBuffers(window);
    glfwPollEvents();
}

// 5. 정리
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();

필수 개념:

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 실행 예제
1. VAO/VBO/EBO: 정점 데이터 관리
2. Shader: GPU 프로그램
3. Uniform: CPU → GPU 데이터 전달
4. Texture: 이미지 매핑
5. Transformation: Model/View/Projection 행렬
6. Lighting: Phong 모델
7. Framebuffer: 오프스크린 렌더링

학습 로드맵

윈도우·삼각형·셰이더를 먼저 닫고, 텍스처와 좌표 변환으로 2D를, 그다음 MVP·카메라·조명으로 3D로 넘어가면 순서가 덜 꼬인다. 모델 로딩·그림자·후처리는 첫 프로젝트 범위를 정한 뒤에 붙여도 된다.

추천 자료

웹사이트:

책:

  • OpenGL Programming Guide (Red Book)
  • OpenGL Shading Language (Orange Book)
  • Real-Time Rendering

도구:

  • RenderDoc — 프레임 캡처
  • Nsight Graphics — NVIDIA 프로파일링
  • Shadertoy — 셰이더 실험

다음 글 추천


키워드: OpenGL, Graphics, 3D, Shader, GLSL, GPU, Rendering, 그래픽스, 렌더링, Game, Computer Graphics

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3