본문으로 건너뛰기
Previous
Next
OpenGL 실전 가이드 | 3D 그래픽스 프로그래밍

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

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

이 글의 핵심

GLFW·GLAD·셰이더·텍스처·3D 변환·조명·모델 로딩까지. C++·Python 예제로 보는 OpenGL 입문. 실전 예제와 코드로 개념부터 활용까지 정리합니다. OpenGL·Graphics·3D 중심으로 설명합니다.

이 글의 핵심

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

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

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

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

3D 모델 데이터

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

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

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

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

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

2. 좌표계

OpenGL 좌표계:

        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 모델은 삼각형으로 구성됨
삼각형 예시:
    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에서 돌아가는 작은 프로그램이다.

CPU (프로그램):
- 게임 로직
- 물리 계산
- AI
GPU (셰이더):
- 정점 변환
- 픽셀 색상 계산
- 조명 효과
GPU는 수천 개의 코어로 병렬 처리!
→ 매우 빠름

5. 버퍼 (Buffer)

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

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 버전

OpenGL 1.x (1992~):
- 고정 파이프라인
- 레거시
OpenGL 2.0 (2004):
- 셰이더 도입 (GLSL)
OpenGL 3.x (2008):
- 현대적 API
- Deprecated 기능 제거
OpenGL 4.x (2010~):
- 테셀레이션
- 컴퓨트 셰이더
- 현재 표준
OpenGL ES:
- 모바일/임베디드 버전
WebGL:
- 웹 브라우저 버전

2. 개발 환경 설정

필수 라이브러리

GLFW (윈도우 관리):

# macOS
brew install glfw
# Ubuntu
sudo apt install libglfw3-dev
# Windows (vcpkg)
vcpkg install glfw3

GLAD (OpenGL 함수 로더):

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

GLM (수학 라이브러리):

# macOS
brew install glm
# Ubuntu
sudo apt install libglm-dev
# Windows (vcpkg)
vcpkg install glm

C++ 프로젝트 설정

CMakeLists.txt:

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
)

빌드:

mkdir build
cd build
cmake ..
make
./app

3. 첫 번째 삼각형

기본 윈도우 생성

main.cpp:

#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;
}

삼각형 그리기

정점 데이터의 의미:

OpenGL은 정규화된 장치 좌표 (NDC, Normalized Device Coordinates)를 사용합니다. 이는 화면 크기와 무관하게 -1.0 ~ 1.0 범위로 좌표를 표현하는 방식입니다.

// 삼각형 정점 (x, y, z)
float vertices[] = {
    -0.5f, -0.5f, 0.0f,  // 왼쪽 아래: 화면 중심에서 왼쪽으로 절반, 아래로 절반
     0.5f, -0.5f, 0.0f,  // 오른쪽 아래: 화면 중심에서 오른쪽으로 절반, 아래로 절반
     0.0f,  0.5f, 0.0f   // 위: 화면 중심(x=0), 위로 절반(y=0.5)
};

VBO (Vertex Buffer Object) - GPU 메모리 관리:

VBO는 CPU 메모리(RAM)에서 GPU 메모리(VRAM)로 정점 데이터를 전송하는 핵심 메커니즘입니다.

// 1. VBO 생성: GPU 메모리에 버퍼 공간 ID 할당
unsigned int VBO;
glGenBuffers(1, &VBO);
// 내부 동작: OpenGL 드라이버가 GPU VRAM에 버퍼 객체 생성
// VBO는 단순히 해당 버퍼를 가리키는 정수 ID

// 2. VBO 바인딩: 이후 모든 버퍼 작업은 이 VBO를 대상으로 함
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// GL_ARRAY_BUFFER: 정점 속성 데이터를 담는 버퍼 타입
// 이 시점 이후 GL_ARRAY_BUFFER 관련 모든 호출은 VBO를 참조

// 3. 데이터 전송: CPU → GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// sizeof(vertices): 전송할 바이트 크기 (9 * sizeof(float) = 36 bytes)
// vertices: CPU 메모리 주소
// GL_STATIC_DRAW: 사용 힌트 (한 번 설정, 여러 번 사용)
//   - STATIC: 데이터가 거의 변하지 않음
//   - DYNAMIC: 데이터가 자주 변함
//   - STREAM: 데이터가 매 프레임 변함
// 힌트를 통해 드라이버가 최적의 메모리 위치(VRAM vs System RAM) 결정

VAO (Vertex Array Object) - 상태 관리의 핵심:

VAO는 정점 속성 설정을 저장하는 컨테이너입니다. 복잡한 모델을 그릴 때 매번 glVertexAttribPointer를 호출하지 않고, VAO를 바인딩하는 것만으로 모든 설정을 복원할 수 있습니다.

// 1. VAO 생성
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// 이제 이 VAO에 모든 정점 속성 설정이 기록됨

// 2. 정점 속성 설정: GPU가 데이터를 어떻게 해석할지 알려줌
glVertexAttribPointer(
    0,                      // location: vertex shader의 layout(location = 0)과 매칭
    3,                      // size: 각 정점당 3개 값 (x, y, z)
    GL_FLOAT,               // type: float 타입
    GL_FALSE,               // normalized: 정규화 불필요 (이미 -1.0 ~ 1.0 범위)
    3 * sizeof(float),      // stride: 다음 정점까지의 바이트 간격 (12 bytes)
    (void*)0                // offset: 버퍼 시작 위치에서의 오프셋 (0 bytes)
);
// 내부 동작:
// GPU가 버퍼에서 데이터를 읽을 때:
// - 위치 0에서 시작
// - 12 bytes마다 하나의 정점
// - 각 정점에서 3개의 float 읽기
// 결과: (vertices[0], vertices[1], vertices[2]), (vertices[3], vertices[4], vertices[5]), ...

// 3. 속성 활성화: location 0 사용 시작
glEnableVertexAttribArray(0);
// 이 호출 없이는 데이터가 전송되어도 shader가 사용 불가

// 4. 언바인딩: 실수로 다른 코드가 현재 상태를 변경하는 것 방지
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// Best Practice: 설정 완료 후 항상 언바인딩하여 상태 오염 방지

메모리 레이아웃 시각화:

CPU (RAM):
vertices[] = {-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0}
             ↓ glBufferData()

GPU (VRAM):
VBO = [정점0: -0.5, -0.5, 0.0 | 정점1: 0.5, -0.5, 0.0 | 정점2: 0.0, 0.5, 0.0]
        ↑                        ↑                        ↑
     stride=12              stride=12                stride=12

VAO가 기억하는 설정:
- VBO ID
- location 0: 3 floats, stride 12, offset 0
- location 0 enabled

왜 이렇게 복잡한가?

  1. 성능: CPU-GPU 데이터 전송은 매우 느립니다. VBO로 한 번 전송한 뒤 GPU에서 반복 사용하면 빠릅니다.
  2. 유연성: 하나의 버퍼에 위치, 색상, 법선, 텍스처 좌표를 함께 담을 수 있습니다 (인터리브).
  3. 상태 관리: VAO로 복잡한 설정을 저장해두면, 나중에 한 줄로 복원 가능합니다.

Vertex Shader - 정점 변환의 핵심

vertex_shader.glsl:

#version 410 core
// GLSL (OpenGL Shading Language) 버전 지정
// 410 = OpenGL 4.1
// core = 레거시 기능 제외 (deprecated 함수 사용 불가)

layout (location = 0) in vec3 aPos;
// layout(location = 0): VAO 설정의 location 0과 매칭
// in: vertex shader 입력 (CPU에서 전달된 정점 데이터)
// vec3: 3개의 float (x, y, z)
// aPos: 변수명 (attribute Position의 약자)

void main() {
    // main(): GPU가 각 정점마다 이 함수를 실행
    // 만약 삼각형(3개 정점)이라면, 이 main()이 3번 호출됨
    
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    // gl_Position: OpenGL 내장 출력 변수 (반드시 설정해야 함)
    // vec4: 4D 동차 좌표계 (Homogeneous Coordinates)
    //   - x, y, z: 3D 공간 위치
    //   - w: 원근 분할에 사용 (보통 1.0)
    // 왜 vec4인가?
    //   - 3D 변환(회전, 이동, 투영)을 4x4 행렬로 표현하려면 4D 벡터 필요
    //   - w=1.0: 점(point), w=0.0: 방향(direction)
}

Vertex Shader의 역할:

  1. Model Space → Clip Space 변환 (이후 예제에서 다룸)
  2. 정점별 데이터 전달: 색상, 법선, 텍스처 좌표 → Fragment Shader로
  3. GPU 병렬 처리: 수천 개 정점을 동시에 처리

실행 흐름:

정점 0: aPos = (-0.5, -0.5, 0.0)
  → gl_Position = (-0.5, -0.5, 0.0, 1.0)

정점 1: aPos = (0.5, -0.5, 0.0)
  → gl_Position = (0.5, -0.5, 0.0, 1.0)

정점 2: aPos = (0.0, 0.5, 0.0)
  → gl_Position = (0.0, 0.5, 0.0, 1.0)

이 3개 정점은 동시에(병렬로) 처리됨!

Fragment Shader - 픽셀 색상 결정

fragment_shader.glsl:

#version 410 core

out vec4 FragColor;
// out: fragment shader 출력
// FragColor: 최종 픽셀 색상 (이름은 자유롭게 변경 가능)
// vec4: RGBA (Red, Green, Blue, Alpha)
//   - 각 값은 0.0 ~ 1.0 범위
//   - (1.0, 0.0, 0.0, 1.0) = 빨강

void main() {
    // main(): GPU가 각 픽셀(Fragment)마다 이 함수를 실행
    // 800x600 화면이면, 이 main()이 최대 480,000번 호출됨!
    // (실제로는 삼각형 내부 픽셀만 처리)
    
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);  // 주황색
    // R: 1.0 (255/255 = 100% 빨강)
    // G: 0.5 (127/255 = 50% 초록)
    // B: 0.2 (51/255 = 20% 파랑)
    // A: 1.0 (불투명)
    // 결과: 주황색 (#FF7F33)
}

Fragment Shader의 역할:

  1. 픽셀 색상 계산: 텍스처, 조명, 그림자 등
  2. 보간된 값 사용: Vertex Shader에서 전달받은 색상/좌표를 보간
  3. 병렬 처리: 수백만 픽셀을 동시에 처리

Rasterization (래스터화)의 마법:

Vertex Shader 출력 (3개 정점):
  V0 (-0.5, -0.5)
  V1 ( 0.5, -0.5)
  V2 ( 0.0,  0.5)

       ↓ Rasterization

Fragment Shader 입력 (수천 개 픽셀):
  화면상의 삼각형 내부의 모든 픽셀
  예: (0.0, 0.0), (0.1, 0.0), (-0.1, 0.1), ...

각 픽셀마다 FragColor 계산 → 화면에 출력

왜 Vertex와 Fragment를 분리했나?

  • Vertex Shader: 정점 수(보통 수천~수만 개) 만큼만 실행 → 가벼움
  • Fragment Shader: 픽셀 수(수백만 개) 만큼 실행 → 무거움
  • 분리로 효율적인 연산 분배 가능
    • 변환 행렬 계산 → Vertex (적게 실행)
    • 조명 계산 → Fragment (정밀하게)

셰이더 컴파일 - 런타임 빌드의 비밀

왜 셰이더는 런타임에 컴파일되나?

일반 C++ 코드는 컴파일 타임에 빌드되지만, 셰이더는 프로그램 실행 중에 컴파일됩니다. 이유는:

  1. GPU마다 다른 아키텍처: NVIDIA, AMD, Intel GPU가 각각 다른 명령어 사용
  2. 드라이버가 최적화: 현재 하드웨어에 맞춰 최적 코드 생성
  3. 유연성: 게임 설정에 따라 동적으로 셰이더 변경 가능
// Vertex Shader 컴파일
const char* vertexShaderSource = R"(
#version 410 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";
// R"(...)": C++11 Raw String Literal
// 이스케이프 없이 GLSL 코드를 문자열로 저장

// 1단계: 셰이더 객체 생성
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
// GL_VERTEX_SHADER: 정점 처리용 셰이더
// 반환값: GPU에서 식별할 수 있는 셰이더 ID

// 2단계: 소스 코드 전달
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
// 파라미터:
//   - vertexShader: 셰이더 ID
//   - 1: 문자열 개수 (여러 파일을 합칠 수 있음)
//   - &vertexShaderSource: 소스 코드 포인터
//   - nullptr: 각 문자열 길이 (nullptr이면 null-terminated 가정)

// 3단계: 컴파일 실행
glCompileShader(vertexShader);
// 내부 동작:
// 1. GLSL 코드 파싱
// 2. 문법 검사 (syntax error 확인)
// 3. 최적화 (불필요한 연산 제거)
// 4. GPU 네이티브 코드로 변환
//    - NVIDIA: SASS (Shader Assembly)
//    - AMD: GCN ISA
// 5. 결과를 vertexShader 객체에 저장

// 4단계: 컴파일 에러 확인 (필수!)
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
// GL_COMPILE_STATUS: 컴파일 성공 여부
// success: GL_TRUE(1) 또는 GL_FALSE(0)

if (!success) {
    // 에러 로그 가져오기
    glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
    // 파라미터:
    //   - 512: 버퍼 크기
    //   - nullptr: 실제 쓰인 길이 (optional)
    //   - infoLog: 에러 메시지를 담을 버퍼
    
    std::cerr << "Vertex Shader Compilation Failed:\n" << infoLog << std::endl;
    // 일반적인 에러:
    // - "syntax error: unexpected IDENTIFIER"
    // - "undeclared identifier 'myVar'"
    // - "type mismatch: cannot convert from 'float' to 'vec3'"
}

셰이더 컴파일 실패 예시:

// 잘못된 셰이더
#version 410 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = aPos;  // ❌ 에러: vec3를 vec4에 할당 불가
}

// 에러 메시지:
// "error: type mismatch, cannot convert from 'vec3' to 'vec4'"

최적화 팁:

// ❌ 나쁜 예: 매 프레임 컴파일
while (rendering) {
    unsigned int shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(shader, ...);
    glCompileShader(shader);  // 너무 느림!
}

// ✅ 좋은 예: 초기화 시 한 번만 컴파일
unsigned int shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(shader, ...);
glCompileShader(shader);
while (rendering) {
    glUseProgram(shaderProgram);  // 이미 컴파일된 셰이더 사용
}

// Fragment Shader 컴파일 (Vertex와 동일한 과정) 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; }

/

  • 셰이더 프로그램 링킹 - 파이프라인 연결
  • 컴파일된 Vertex/Fragment Shader를 하나의 “프로그램”으로 연결합니다.
  • C++의 링커가 .o 파일들을 실행 파일로 만드는 것과 유사합니다. */ unsigned int shaderProgram = glCreateProgram(); // Shader Program: GPU에서 실행되는 완전한 파이프라인 // 여러 셰이더를 묶어 하나의 실행 단위로 만듦

glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); // Attach: 프로그램에 셰이더 연결 // 여러 셰이더를 조합 가능: // - Vertex + Fragment (필수) // - Vertex + Geometry + Fragment (선택) // - Vertex + Tessellation + Fragment (선택)

glLinkProgram(shaderProgram); // 링킹 과정: // 1. 셰이더 간 변수 매칭 검사 // - Vertex의 out과 Fragment의 in이 이름/타입 일치하는지 확인 // 2. Uniform 변수 위치 할당 // - 모든 셰이더의 uniform을 중앙에서 관리 // 3. Attribute 위치 확인 // - Vertex Shader의 layout(location = X)가 유효한지 검사 // 4. 최종 GPU 실행 파일 생성 // - 하나의 통합된 GPU 프로그램으로 변환

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog); std::cerr << “Shader Program Linking Failed:\n” << infoLog << std::endl; // 일반적인 링킹 에러: // - “vertex shader output ‘color’ not matched by fragment shader input” // - “type mismatch between vertex out and fragment in” }

/

  • 셰이더 객체 삭제 - 메모리 정리
  • 프로그램에 링크된 후에는 개별 셰이더 객체가 불필요합니다. */ glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 왜 삭제해도 되나? // - glLinkProgram()이 셰이더 코드를 프로그램에 복사함 // - 원본 셰이더는 더 이상 필요 없음 // - 메모리 절약 (특히 많은 셰이더를 사용할 때)

// 유사한 개념: // C++ 컴파일: .cpp → .o (컴파일) → executable (링크) // └─ .o 파일은 링크 후 삭제 가능 // OpenGL: GLSL → Shader (컴파일) → Program (링크) // └─ Shader는 링크 후 삭제 가능


**전체 파이프라인 흐름:**
  1. Vertex Shader 컴파일 GLSL 코드 → GPU 바이트코드

  2. Fragment Shader 컴파일 GLSL 코드 → GPU 바이트코드

  3. Shader Program 링킹 Vertex + Fragment → 통합 프로그램

  4. 사용 glUseProgram(shaderProgram); glDrawArrays(GL_TRIANGLES, 0, 3);

  5. GPU 실행 정점 0, 1, 2 → Vertex Shader (병렬) ↓ Rasterization (삼각형 → 픽셀) ↓ 수천 픽셀 → Fragment Shader (병렬) ↓ Frame Buffer (화면 출력)


**에러 처리 Best Practice:**

```cpp
// Helper 함수로 중복 제거
bool checkShaderCompileStatus(unsigned int shader, const std::string& type) {
    int success;
    char infoLog[512];
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(shader, 512, nullptr, infoLog);
        std::cerr << type << " Shader Compilation Failed:\n" << infoLog << std::endl;
        return false;
    }
    return true;
}

bool checkProgramLinkStatus(unsigned int program) {
    int success;
    char infoLog[512];
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(program, 512, nullptr, infoLog);
        std::cerr << "Program Linking Failed:\n" << infoLog << std::endl;
        return false;
    }
    return true;
}

렌더링

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

전체 코드:

#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 (색상 포함):

#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:

#version 410 core
// 실행 예제
in vec3 ourColor;
out vec4 FragColor;
void main() {
    FragColor = vec4(ourColor, 1.0);
}

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

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에서 값 받기:

#version 410 core
out vec4 FragColor;
uniform vec4 ourColor;  // CPU에서 전달받는 값
void main() {
    FragColor = ourColor;
}

C++ 코드:

// 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 사용:

#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;
}

텍스처 좌표

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

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:

#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:

#version 410 core
// 실행 예제
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D texture1;
void main() {
    FragColor = texture(texture1, TexCoord);
}

렌더링:

// 텍스처 로딩
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 사용:

#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:

#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++ 코드:

// 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));

회전하는 큐브

// 큐브 정점 (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. 카메라 시스템

카메라 클래스

#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));
    }
};

사용:

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:

#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:

#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++ 코드:

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

다중 조명

#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 사용

설치:

# macOS
brew install assimp
# Ubuntu
sudo apt install libassimp-dev
# Windows (vcpkg)
vcpkg install assimp

모델 로더:

#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)

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)

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:

#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 (그림자 맵 생성):

#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 (그림자 적용):

#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 효과

#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 뷰어

#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: 파티클 시스템

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. 프레임버퍼와 후처리

프레임버퍼 생성

// 프레임버퍼 생성
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);

후처리 셰이더

그레이스케일:

#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);
}

블러:

#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);
}

렌더링:

// 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. 최적화 기법

인스턴싱

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

// 인스턴스 데이터 (위치)
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:

#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)

// 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 에러 확인

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));

셰이더 디버깅

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 클래스

#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

#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”

// 원인: 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”

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

3) 검은 화면

// 원인 1: 깊이 테스트 미활성화
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 원인 2: 카메라 위치 문제
// 카메라가 객체 안에 있거나 너무 멀리 있음
// 원인 3: 셰이더 에러
// 셰이더 컴파일 로그 확인

4) 텍스처가 안 보임

// 원인 1: 텍스처 좌표 누락
// 정점 데이터에 텍스처 좌표 포함 확인
// 원인 2: 텍스처 바인딩 누락
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(shader, "texture1"), 0);
// 원인 3: 이미지 뒤집힘
stbi_set_flip_vertically_on_load(true);

19. 성능 최적화

배치 렌더링

// ❌ 느림 (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 업데이트 최적화

// ❌ 느림 (매 프레임 전체 재생성)
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);

텍스처 아틀라스

// ❌ 느림 (텍스처 바인딩 많음)
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

#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++ 코드:

// 텍스처 생성
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

// 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):

// 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 (디버깅)

RenderDoc:
- 프레임 캡처 및 분석
- Draw Call 확인
- 셰이더 디버깅
- 텍스처 확인
다운로드: https://renderdoc.org/

Nsight Graphics (NVIDIA)

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 실전 가이드 | 3D 그래픽스 프로그래밍」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.

처리 파이프라인(개념도)

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]

경계에서의 지연·실패(시퀀스 관점)

sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(프로세스·런타임·게이트웨이)
  participant D as 의존성(외부 API·DB·큐)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)

알고리즘·프로토콜·리소스 관점 체크포인트

  • 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.

프로덕션 운영 패턴

실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가
용량피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.


확장 예시: 엔드투엔드 미니 시나리오

「OpenGL 실전 가이드 | 3D 그래픽스 프로그래밍」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.

의사코드 스케치(프레임워크 무관)

handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)        // 경계에서 거절
  authorize(validated, ctx)                  // 권한·테넌트
  result = domainCore(validated)             // 순수에 가까운 규칙
  persistOrEmit(result, idempotentKey)       // I/O: 멱등·재시도 정책
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성 불안정, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정이 로컬과 다름프로필·시크릿·기본값, 지역 리전단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

요약

핵심 정리

OpenGL 기본 구조:

// 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();

필수 개념:

// 실행 예제
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로 넘어가면 순서가 덜 꼬인다. 모델 로딩·그림자·후처리는 첫 프로젝트 범위를 정한 뒤에 붙여도 된다.

추천 자료

웹사이트:

  • learnopengl.com — 실습형 튜토리얼로 많이 든다
  • open.gl — 모던 OpenGL 흐름
  • The Book of Shaders — 셰이더 감각 책:
  • 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


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [C++ Segmentation Fault: Five Causes and Debugging with GDB,](/en/blog/cpp-error-02-segmentation-fault/
  • [C++ std::initializer_list](/en/blog/cpp-initializer-list/
  • [C++ std::span | Contiguous Memory View (C++20)](/en/blog/cpp-span/

이 글에서 다루는 키워드 (관련 검색어)

OpenGL, Graphics, 3D, Shader, GLSL, GPU, Rendering, 그래픽스, 렌더링, Game 등으로 검색하시면 이 글이 도움이 됩니다.