Scop 구현기 2편: VAO와 VBO의 세계

2025. 4. 9. 19:04·Dev/Grapics

윈도우를 띄우는 것까지 했으니 이제 삼각형을 그려보겠습니다.

 

삼각형 그리기 전체 코드

#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이 이루어짐

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에서 렌더링 파이프라인의 첫 번째 관문이자 정점 처리의 핵심이다.

 

각 줄에 대해서 설명

  1. #version 330 core
    -> 이 셰이더는 OpenGL 3.3 Core Profile에 맞는 GLSL(OpenGL Shading Language) 문법을 사용하겠다는 뜻.
  2. layout (location = 0) in vec3 aPos;
    -> 이 부분은 GPU로부터 입력받을 정점 속성(Attribute)을 선언.
    Location = 0은 C++ 코드에서 glVertexAttribPointer(0, ...)로 연결해 준 버퍼 데이터의 위치와 매핑된다.
    Loaction = 1일 경우 C++코드에서 glVertexAttribPointer(1, ....)로 연결한 것
  3. void main()
    -> 셰이더 프로그램은 항상 main() 함수부터 실행된다, GPU에서 수천 개의 정점을 동시에 처리할 때 이 main()이
    병렬 실행된다.
  4. 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 과정을 거친 후, 화면의 픽셀을 어떤 색으로 칠할지 결정하는 역할.

 

각 줄에 대해서 설명

  1. #version 330
    -> Vertex Shader와 동일하게, OpenGL 3.3 Core Profile에 맞춰 작성한 셰이더 버전.
  2. out vec4 FragColor;
    -> 이건 출력 변수. vec4는 RGBA로 구성된 최종 색상을 의미.
    이 변수가 GPU 파이프라인을 통해 화면에 실제로 출력되는 색상.
  3. void main()
    -> Fragment Shader도 각 픽셀 단위(Fragment)로 실행된다.
    삼각형 내부 픽셀마다 이 함수가 호출되며, 하나하나 색을 지정한다.
  4. 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();
}

 

이제 렌더링 루프 안에서 드로우 코드를 넣고 실행해 보면

simple Triangles

이렇게 주황색 삼각형이 보이면 성공한 것이다!


만들며 궁금했던 점들

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
'Dev/Grapics' 카테고리의 다른 글
  • Scop 구현기 4편: Model 클래스
  • Scop 구현기 3편: 셰이더 클래스를 만들기로 했다
  • Scop 구현기 1편: 윈도우 하나 띄우는 게 뭐라고 (Object Viewer)
  • Rasterization vs Ray Tracing
onepaperhoon
onepaperhoon
한장훈님의 블로그 입니다.
  • onepaperhoon
    OnePaperHoon Blog
    onepaperhoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • Dev (14)
        • System Programming (2)
        • Linux (0)
        • CS (0)
        • Network, Protocol (0)
        • Grapics (11)
        • Web, App (0)
        • Design Pattern (1)
      • Projects (0)
      • TIL (0)
      • Life (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    Vertex Shader
    Process
    C언어
    c++
    그래픽스
    graphics
    glfw
    named pipe
    shader
    Pipe
    shared memory
    공유 메모리
    Game
    graphicapi
    IPC
    OpenGL
    cpp
    싱글톤 패턴
    3d
    디자인 패턴
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
onepaperhoon
Scop 구현기 2편: VAO와 VBO의 세계
상단으로

티스토리툴바