윈도우를 띄우는 것까지 했으니 이제 삼각형을 그려보겠습니다.
삼각형 그리기 전체 코드
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <OpenGL/gl.h>
#include <iostream>
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos, 1.0);
}
)";
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.5, 0.2, 1.0); // 주황색
}
)";
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
int main(void)
{
if (!glfwInit())
{
std::cerr << "GFLW Init Failed" << std::endl;
return (-1);
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow* window = glfwCreateWindow(500, 500, "scop", NULL, NULL);
glfwMakeContextCurrent(window);
int framebuffer_w, framebuffer_h;
glfwGetFramebufferSize(window, &framebuffer_w, &framebuffer_h);
glViewport(0, 0, framebuffer_w , framebuffer_h);
if (glewInit() != GLEW_OK)
{
std::cerr << "GLEW Init Failed" << std::endl;
glfwTerminate();
return (-1);
}
// 정점 데이터
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
GLuint VAO, VBO;
// VAO & VBO 생성
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// VAO 바인딩
glBindVertexArray(VAO);
// VBO 바인딩 + 데이터 전달
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 정점 속성 지정 (position);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 바인딩 해제
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// --- 셰이더 컴파일 및 프로그램 생성 ---
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 세이더는 프로그램에 연결 후 삭제 가능
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0, 0, 0, 1);
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);
}
1. 삼각형은 그래픽스의 최소 단위
그래픽스에서 화면에 뭔가를 그릴 때, 그 기본은 항상 삼각형인데요
왜 삼각형인지 찾아 보았다
- 평면 상의 가장 단순한 다각형 (모든 다각형은 삼각형으로 나눌 수 있음)
- GPU 하드웨어 친화적 (세 점이면 면이 정의됨)
- 렌더링 파이프 라인 구조상, 삼각형을 기준으로 rasterization이 이루어짐

우리는 오늘 이 삼각형 하나를 그리기 위해 필요한 OpenGL파이프라인 구성을 배워볼 것이다.
2. VAO와 VBO는 왜 필요한가?
OpenGL의 Core Profile에서는 삼각형을 그리려면 정점 데이터 (Vertex Data)를 GPU에게 보내줘야 한다.
그 데이터는 버퍼에 담겨야 하, 그 버퍼를 설명하는 정보도 있어야 한다.
| 요소 | 역할 |
| VBO (Vertex Buffer Object) | 정점 데이터를 GPU에 저장 |
| VAO (Vertex Array Object) | 정점 버퍼(VBO)를 어떻게 해석할지 정의 |
VBO는 데이터 저장소, VAO는 데이터 해석 방법이라고 보면 된다.
쉽게 말해, VBO가 책이라면, VAO는 그 책의 목차다.
참고: VAO는 OpenGL 3.0 이상 Core Profile에선 필수. 없으면 에러!
3. 셰이더 프로그램 생성
OpenGL은 셰이더 없이 그릴 수 없다.
셰이더란 GPU에게 어떻게 그릴 지를 알려주는 프로그램이다.
Vertex Shader
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos, 1.0);
}
이 Vertex Shader는 아주 기본적인 형태이지만, OpenGL에서 렌더링 파이프라인의 첫 번째 관문이자 정점 처리의 핵심이다.
각 줄에 대해서 설명
- #version 330 core
-> 이 셰이더는 OpenGL 3.3 Core Profile에 맞는 GLSL(OpenGL Shading Language) 문법을 사용하겠다는 뜻. - layout (location = 0) in vec3 aPos;
-> 이 부분은 GPU로부터 입력받을 정점 속성(Attribute)을 선언.
Location = 0은 C++ 코드에서 glVertexAttribPointer(0, ...)로 연결해 준 버퍼 데이터의 위치와 매핑된다.
Loaction = 1일 경우 C++코드에서 glVertexAttribPointer(1, ....)로 연결한 것 - void main()
-> 셰이더 프로그램은 항상 main() 함수부터 실행된다, GPU에서 수천 개의 정점을 동시에 처리할 때 이 main()이
병렬 실행된다. - gl_Position = vec4(aPos, 1.0);
-> 핵심 부분! OpenGL은 모든 정점 위치를 동차 좌표(clip space)로 변환해야만 그릴 수 있다.
그래서 vec3인 정점 위치에 w = 1.0을 붙여서 vec4로 만들어 gl_Position에 넣는다.
여기서 gl_Position은 OpenGL 내부 파이프라인에서 꼭 필요한 예약 변수로,
이 값이 나중에 resterization 단계를 거쳐 화면 좌표로 바뀐다.
요약하면:
- 정점 하나하나를 처리해서 화면 좌표계로 넘기는 작업.
- 나중에 카메라, 변환 행렬 등이 추가되면 이 부분이 훨씬 복잡해짐.
- 지금은 아주 기본적인 통과 셰이더(pass-throgh shader) 역할.
Fragment Shader
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.5, 0.2, 1.0); // 주황색
}
이 셰이더는 삼각형이 rasterization 과정을 거친 후, 화면의 픽셀을 어떤 색으로 칠할지 결정하는 역할.
각 줄에 대해서 설명
- #version 330
-> Vertex Shader와 동일하게, OpenGL 3.3 Core Profile에 맞춰 작성한 셰이더 버전. - out vec4 FragColor;
-> 이건 출력 변수. vec4는 RGBA로 구성된 최종 색상을 의미.
이 변수가 GPU 파이프라인을 통해 화면에 실제로 출력되는 색상. - void main()
-> Fragment Shader도 각 픽셀 단위(Fragment)로 실행된다.
삼각형 내부 픽셀마다 이 함수가 호출되며, 하나하나 색을 지정한다. - FragColor = vec4(1.0, 0.5, 0.2, 1.0);
-> 픽셀을 주황색(빨간색 100%, 초록색 50%, 파란색 20%, 알파 100%)으로 칠한다는 뜻.
이걸 gl_FragColor로 쓰던 시절도 있었지만, 지금은 명시적으로 out 변수로 정의해야 함.
요약하면:
- 정점 처리 후 생긴 픽셀마다 실행돼서, 그 픽셀의 최종 색상을 결정.
- 텍스처, 조명, 음영, 하이라이트 등 모두 이 단계에서 구현된다.
- 지금은 단순히 "색만 고정 출력"하는 기본 셰이더지만, 응용하면 거의 모든 렌더링 효과를 만들 수 있다.
4. 렌더링 루프 안에 드로우 코드 추가
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram); // 셰이더 사용
glBindVertexArray(VAO); // VAO 바인딩
glDrawArrays(GL_TRIANGLES, 0, 3); // 삼각형 그리기
glfwSwapBuffers(window);
glfwPollEvents();
}
이제 렌더링 루프 안에서 드로우 코드를 넣고 실행해 보면

이렇게 주황색 삼각형이 보이면 성공한 것이다!
만들며 궁금했던 점들
1. gl_Position은 정의돼 있는 변수인가?
GLSL의 내장(내부) 변수가 맞다
GLSL은 OpenGL 파이프라인과 맞물리는 구조로 설계되어 있어서
그중에서도 gl_Position은 Vertex Shader에서 반드시 지정해야 하는 출력 변수로 설정되어 있다.
정확히 말하자면 gl_Position은 VertexShader가 GPU에 넘기는 최종 정점 위치를 나타내는 내장 변수다.
- 이 값은 클립 공간 (Clip Space) 좌표다.
- 이후 자동으로 NDC(Normalized Device Coordinates)로 변환되고,
- 다시 Viewport 변환을 거쳐 스크린 좌표로 찍히게 된다.
즉, gl_Position에 우리가 지정한 vec4(x, y, z, w)가 그래픽스 파이프 라인에 쭉 따라가서
화면에 삼각형이 보이게 되는 원인이 되는 것.
2. gl_Position과 Fragment Shader는 연결되어 있나?
Vertex Shader와 Fragment Shader는 OpenGL 파이프라인 내에서 다단계 릴레이 시스템처럼 동작한다.
- Vertex Shader가 정점들을 처리해서 삼각형을 정의하고,
- Rasterizer가 그 삼각형을 픽셀 단위(fragment)로 나눔
- 그 픽셀마다 프래그먼트 셰이더가 색상을 결정
즉, gl_Position은 삼각형이 어디에 그려질지를 결정하고,
그 삼각형 안의 픽셀들에 대해서 Fragment Shader가 색을 채우는 역할을 한다.
OpenGL이 굉장히 저수준 API이기 때문에 명확한 지시 없이는 어떤 일도 하지 않는 것 같다.
DirectX도 Vulkan도 마찬기지로. GPU는 매우 병렬적이고 강력한 하드웨어지만, 그만큼 명확한 지시 없이는 어떤 일도 하지 않는다.
'Dev > Grapics' 카테고리의 다른 글
| Scop 구현기 4편: Model 클래스 (0) | 2025.04.14 |
|---|---|
| Scop 구현기 3편: 셰이더 클래스를 만들기로 했다 (0) | 2025.04.11 |
| Scop 구현기 1편: 윈도우 하나 띄우는 게 뭐라고 (Object Viewer) (0) | 2025.04.09 |
| Rasterization vs Ray Tracing (0) | 2025.04.09 |
| 셰이더는 그낭 함수일까? (0) | 2025.04.08 |
