[openGL] 사각형 그리기

나우히즈·2024년 9월 30일

Graphics

목록 보기
1/17

오랜만에 블로그 포스팅을 하게 되었다. 최근 openGL을 활용한 수업을 들어왔는데, 그 과정에서 배운 내용들을 복습할 겸 포스팅하기로 하였다.

이번 포스팅은 openGL 에서 간단한 사각형을 그려내는 방식을 정리해보도록 하겠다. 사전 세팅해야하는 GLFW, GLAD 및 빌드 환경 구축은 cmake를 통해 진행하였다.


그림 그리는 과정

openGL에서는 그림을 그리는데에 필요한 것이 다음과 같다.

  1. VBO / EBO
    화면 상에 찍을 점의 정보를 담을 Vertex Buffer obejct, Element Buffer object 가 필요하다.

VBO, 버텍스 버퍼에는 기본적으로 점 찍을 위치를 짚어주기도 하지만 그 외에도 다양한 정보를 담을 수 있다. 점의 RGB 컬러나, 텍스쳐 좌표 등 다양한 점의 정보를 담는 버퍼라고 보면 된다.

EBO, 인덱스 버퍼 오브젝트라고 불리는 녀석은 점을 하나의 인덱스로 묶어주는 역할을 한다. 예를들어, 사각형을 그리는 과정에서 우리는 기본도형으로 삼각형을 만들 것인데, 삼각형 두개로 사각형을 만든다면 6개의 점이 필요하다. 하지만 점 4개에 대한 정보만을 제공하고 그 점들을 어떻게 사용해야할지 정보를 전달해주면 충분히 4개로도 사각형을 그려낼 수 있다. 그 정보를 담아줄 객체가 인덱스 버퍼 오브젝트 되겠다.

  1. VAO

Vertex Array Object라는 이 녀석은 렌더링하기 위한 버퍼들(VBO, EBO)의 정보를 묶어주는 역할을 수행한다. VBO에서 각 점들의 정보를 GPU가 인식할 수 있게 여러가지 특성정보를 포함하여 원활한 렌더링이 될 수 있게 하는 역할을 한다.

  1. Shader

쉐이더에는 vertex shader, fragment shader 가 존재한다. vertex shader, vs는 입력받은 버텍스 정보들을 다루는 쉐이더이고, fragment shader, fs는 버텍스 간에 존재하는 rasterization된 픽셀들을 다루는 쉐이더이다. 우리는 이 쉐이더를 다루기 위해 GLSL 이라는 쉐이더 언어를 활용하게 된다.


Vertices, indices 선언

우선 사용할 vertices, indices를 선언해주고, 그 선언된 배열들이 VBO, EBO로 사용되어 VAO에 세팅되도록 해야한다.

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.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
		-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f,
	};

	uint32_t indices[] = {
		0, 1, 2,
		0, 2, 3,
	};

우측상단 - 하단, 좌측하단 - 상단 순으로 vertices가 구성되어있고,
각 점 별로 6개의 float값을 가지게 된다.
앞선 세 개의 값은 vertex의 위치정보, 뒤 세 개는 점의 색상 정보(RGB)를 나타난다.

인덱스 배열은 삼각형을 찍을 버텍스의 인덱스 정보인데, 첫 삼각형은 0 - 1 - 2 번 버텍스를 가지고 만들고 두 번째 삼각형은 0 - 2 - 3 을 가지고 만든다.

VAO 생성

버텍스 버퍼를 생성하기 전에, 이들을 담아줄 VAO를 만들자.

glGenVertexArrays(1, &m_vertexArrayObject);
glBindVertexArray(m_vertexArrayObject);

각 파라미터는 다음과 같다.

glGenVertexArrays
n: 생성할 VAO의 개수.
arrays: 생성된 VAO의 ID를 저장할 변수(또는 배열). 이 변수에 VAO의 고유 식별자가 저장.

glBindVertexArray
array: 바인딩할 VAO의 ID를 전달. 0을 전달하면 현재 바인딩 되어있던 VAO를 해제함.

glGenVertexArrays로 사용할 VAO를 만들고 생성되는 VAO를 지칭해줄 핸들러를 받아오도록 한다.
생성된 VAO는 glBindVertexArray 함수로 바인딩하여 이후 설정되는 버텍스 속성 상태가 기록되게 한다.

VBO, EBO

버퍼 객체(Buffer Object)를 생성하고 데이터를 전달.
버퍼 객체 정보와 속성을 설정해주면 해당 내용이 바인드된 VAO에 기록되고, GPU 메모리에 데이터를 업로드하여 성능을 높이는 방식

glGenBuffers(1, &m_buffer);
glBindBuffer(m_bufferType, m_buffer);
glBufferData(m_bufferType, dataSize, data, usage);

VBO, EBO 모두 동일한 과정을 거치고 m_bufferType에 들어가는 값만 다르다.

glGenBuffer
n: 생성할 VAO의 개수.
m_buffer: 생성된 VAO의 ID를 저장할 변수(또는 배열). 이 변수에 VAO의 고유 식별자가 저장.

glBindBuffer
m_bufferType: 어떤 타입의 버퍼를 바인드할지 결정
m_buffer: 바인드할 버퍼를 전달

glBufferData
target: 어떤 종류의 버퍼에 데이터를 GPU 메모리로 업로드할 것인지 지정합니다. (ex: GL_ARRAY_BUFFER 또는 GL_ELEMENT_ARRAY_BUFFER.)

size: 업로드할 데이터의 크기 (바이트 단위).

data: 업로드할 실제 데이터의 포인터. 만약 아직 데이터를 지정하지 않고 메모리만 할당하고 싶다면 nullptr 사용 가능

usage: 데이터 사용 방식에 대한 힌트.

ex:
GL_STATIC_DRAW: 데이터가 거의 변경되지 않고, 주로 읽기 전용일 때.
GL_DYNAMIC_DRAW: 데이터가 자주 변경될 때.
GL_STREAM_DRAW: 데이터가 매우 자주 변경될 때.

VAO, VBO 의 명확한 구분

VAO는 "상태 객체"로, 어떤 버퍼, 어떤 데이터, 어떻게 매핑. 그 구성을 저장
VBO는 버텍스의 실제 데이터가 담긴 버퍼.

  • VBO(Vertex Buffer Object): 버텍스 데이터를 GPU 메모리에 저장해서, 렌더링에 사용할 데이터를 제공해. VBO는 CPU 메모리에 있는 데이터를 GPU로 전송하고, 이를 렌더링 파이프라인에서 사용해.

  • VAO(Vertex Array Object): VBO와 같은 버텍스 데이터를 어떻게 해석할지에 대한 정보를 기록하고 저장해. 예를 들어, 어떤 버텍스 속성(위치, 색상, 텍스처 좌표 등)이 몇 번째 위치에 있는지, 데이터 형식은 무엇인지 등을 정의해줘.

glGen~ 함수를 사용해서 오브젝트를 생성하면 그 오브젝트는 GPU 메모리에 생성되며, 그를 컨트롤할 수 있는 핸들러로 정수번호를 전달받게된다.

VBO, EBO의 경우 실제로 데이터를 사용해야하기때문에 CPU 메모리에 존재하는 배열정보를 GPU 메모리로 옮기는 glBufferData 함수를 사용해줘야한다.

속성 세팅

각 vertex buffer의 속성을 설정해주자.

glEnableVertexAttribArray(attribIndex)
이 함수는 특정 attribute을 활성화. OpenGL에서 버텍스 셰이더로 데이터를 보내기 위해 버텍스 속성(예: 위치, 색상, 법선)을 사용하는데, 이 함수는 그 속성을 활성화해서 GPU가 해당 데이터를 사용 가능하게 함.

attribIndex: 활성화할 버텍스 속성의 인덱스. 셰이더의 layout(location = x)로 지정된 인덱스와 대응.

glVertexAttribPointer(attribIndex, count, type, normalized, stride, (const void *)offset)

이 함수는 버텍스 속성 데이터가 GPU에서 어떻게 해석될지를 정의.

attribIndex: 이 값은 버텍스 속성의 인덱스로, 셰이더에서 해당 버텍스 속성을 참조하기 위해 사용하는 위치. 이 값은 glEnableVertexAttribArray() 의 인자와 동일.

count: 버텍스 속성 하나당 몇 개의 값이 있는지를 정의. 예를 들어, 3D 좌표를 나타내는 경우 count = 3 (x, y, z 값).

type: 버텍스 속성의 데이터 타입을 지정. GL_FLOAT는 부동소수점(float) 데이터를 의미.

normalized: 이 값이 GL_TRUE이면 정수형 데이터를 -1.0에서 1.0 또는 0.0에서 1.0 범위로 정규화. 부동소수점일 경우는 이 값을 무시.

stride: 하나의 버텍스 속성 세트(예: 위치, 색상, 텍스처 좌표)에서 다음 세트까지 이동해야 할 바이트 수. 이 값이 0이면 OpenGL이 속성들이 연속적으로 저장되어 있다고 가정.

offset: VBO 내에서 버텍스 속성이 시작되는 위치를 나타내는 바이트 단위의 오프셋. 예를 들어, 위치 데이터가 VBO의 시작 부분에 있다면 offset = 0, 색상 데이터가 그 뒤에 있다면 그 위치에 해당하는 바이트 수를 적어줌.

attribute를 활성화(glEnableVertexAttibArray)한 뒤 glVertexAttribPointer를 세팅해주자.

셰이더 설정

glCreateShader
새로운 셰이더 객체를 생성.
shaderType: 생성할 셰이더의 타입을 지정합니다.
GL_VERTEX_SHADER (버텍스 셰이더) 또는 GL_FRAGMENT_SHADER (프래그먼트 셰이더)와 같은 값을 사용할 수 있습니다.

반환값: 생성된 셰이더 객체의 핸들을 반환합니다.

glShaderSource
생성된 셰이더 객체에 소스 코드를 할당

shader: 소스 코드를 할당할 셰이더 객체의 핸들.
count: 소스 코드 문자열의 개수.
string: 셰이더 소스 코드의 포인터. codePtr은 코드의 시작 주소를 가리켜야 합니다.
codeLength: 각 셰이더 소스 문자열의 길이. 이 값을 통해 OpenGL은 소스 코드의 길이를 알 수 있습니다.

glCompileShader
셰이더 소스를 컴파일합니다. 컴파일된 결과는 OpenGL의 셰이더 객체에 저장됩니다.
shader: 컴파일할 셰이더 객체의 핸들.

glGetShaderiv
셰이더의 속성을 가져오는 데 사용됩니다.
주로 컴파일 성공 여부나 오류 상태를 확인하는 데 사용됩니다.

shader: 상태를 조회할 셰이더 객체의 핸들.
GL_COMPILE_STATUS: 조회할 속성의 종류. 여기서는 컴파일 성공 여부를 조회합니다.
success: 조회된 값을 저장할 변수의 포인터. 컴파일이 성공하면 1, 실패하면 0의 값을 가집니다.

먼저 셰이더 객체를 생성한 다음, 해당 셰이더의 소스 코드를 설정하고, 소스 코드를 컴파일한 후, 컴파일 결과를 확인.


이렇게 셰이더를 불러오고 나서, 셰이더를 관리하기 위해선 프로그램 객체를 이용한다.

프로그램 객체

glCreateProgram
새로운 셰이더 프로그램 객체를 생성. 이 프로그램은 다양한 셰이더(버텍스, 프래그먼트 등)를 포함할 수 있습니다.

생성된 프로그램 객체의 ID를 반환합니다. 이 ID는 이후에 프로그램을 참조하는 데 사용됩니다.

glAttachShader
주어진 셰이더 객체를 셰이더 프로그램에 연결합니다. 여러 개의 셰이더를 프로그램에 첨부할 수 있으며, 각각의 셰이더는 프로그램의 기능을 확장합니다.
program: 셰이더를 첨부할 프로그램 객체의 ID
shader: 첨부할 셰이더 객체의 ID.

glLinkProgram
프로그램에 첨부된 모든 셰이더를 링크하여 하나의 실행 가능한 셰이더 프로그램을 생성합니다. 링크 후에는 프로그램이 GPU에서 실행 가능해집니다.
program: 링크할 프로그램 객체의 ID입니다.

셰이더 프로그램을 생성하고, 필요한 셰이더를 연결(attach)한 후, 링크하여 실행 가능한 프로그램을 만드는 과정.

glUseProgram

파라미터로 주어진 셰이더 프로그램을 활성화. 이후의 렌더링 호출에서 이 프로그램이 사용되고, 이전에 활성화된 프로그램은 비활성화함.

  • 렌더링을 시작하기 전에 원하는 셰이더 프로그램을 활성화. 프로그램은 각 프레임 또는 객체에 대해 다르게 설정할 수 있습니다.

  • 여러 개의 셰이더 프로그램이 존재하는 경우, 특정 상황에 맞는 프로그램을 선택적으로 사용할 수 있습니다. 예를 들어, 특정 물체에 대해 다른 셰이더 효과를 적용해야 할 때 각기 다른 프로그램을 사용해야 합니다.

  • 만약 다양한 물체에 대해 다른 셰이더를 사용해야 한다면, 각 물체를 그리기 전에 적절한 프로그램을 호출해야 합니다. 예를 들어, 버텍스와 프래그먼트 셰이더가 있는 경우, 해당 프로그램을 호출하여 그리기 전에 활성화합니다.

유니폼 설정

프로그램 내에서 고정적으로 사용하게 되는 값들이 존재한다.(변환이나, 특정값 등)
이러한 값들은 셰이더 내에 uniform 변수로 전달하여 주자.

우선 어떤 프로그램의 셰이더를 설정할지를 정해주기 위해,
glProgramUse()를 통해 설정한다.

  1. 유니폼 변수의 위치 가져오기

glGetUniformLocation 함수를 사용하여 셰이더 프로그램에서 유니폼 변수의 위치를 가져옴. 이 함수는 유니폼 변수의 이름과 해당 셰이더 프로그램 ID를 인자로 받음.

  1. 유니폼 변수 값 설정

가져온 유니폼 변수의 위치를 사용하여, glUniform 함수 계열을 통해 해당 유니폼 변수에 값을 입력합니다. 이때 유니폼의 데이터 유형에 맞는 적절한 glUniform 함수를 사용하기.

Drawing

마침내 그리기 파트이다.

openGL에서는 glDraw## 함수를 통해 그림을 그리는데, 주로 glDrawArraysglDrawElements 두 가지 주요 변형이 있음.

1. glDrawArrays

이 함수는 지정된 배열에서 연속된 버텍스를 사용하여 Drawing.

void glDrawArrays(GLenum mode, GLint first, GLsizei count);

파라미터 설명

  • mode: 그릴 도형의 타입을 지정.
    ex: GL_TRIANGLES, GL_LINES, GL_POINTS
  • first: 그리기를 시작할 첫 번째 버텍스의 인덱스.
  • count: 그릴 버텍스의 개수.

2. glDrawElements

이 함수는 인덱스 배열을 사용하여 버텍스를 참조하면서 drawing. 이 방식은 중복된 버텍스를 줄여 메모리 활용이 효율적.

void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void *indices);

파라미터 설명

  • mode: 그릴 도형의 타입을 지정. (예: GL_TRIANGLES)
  • count: 그릴 인덱스의 개수.
  • type: 인덱스 데이터의 타입을 지정. 일반적으로 GL_UNSIGNED_INT 또는 GL_UNSIGNED_SHORT가 사용.
  • indices: 인덱스 배열의 포인터. 버텍스 배열 내에서 그릴 버텍스를 참조하는 데 사용.

GPU와의 관계

  • glDrawArrays 또는 glDrawElements를 호출하면 OpenGL은 현재 바인딩된 VAO에서 정의된 버텍스 데이터를 사용하여 GPU에서 실제 렌더링을 수행. 이때, 셰이더가 활성화되어 있어야 하고, 필요한 유니폼 변수들이 설정되어 있어야 한다.

  • 그리기 명령은 GPU에게 렌더링 파이프라인을 통해 데이터를 처리하고 화면에 그리도록 지시한다. 이 과정에서 GPU는 바인딩된 버퍼 객체(VBO, EBO 등)와 VAO를 참조하여 데이터에 접근.


우리는 EBO를 사용하여 인덱스 데이터를 전달해주었으므로, glDrawElements를 이용하여 그림을 매 렌더마다 그려지도록 한다.

결과

각 버텍스에 R, G, B, Yellow 컬러를 할당하여 사각형이 그려지게 되었다.
렌더링된 화면은 멈춰있는것으로 보이지만 매 프레임이 계산되어 화면에 표시되고 있는 중인데, 각 버텍스의 값이 매 렌더링 시 변하게 설정해준다면 색상이 변화하는 이미지를 얻을 수 있다.


	static float time = 0.0f;
	m_program->Use();
	float t = sinf(time) * 0.5f + 0.5f;
	auto loc = glGetUniformLocation(m_program->Get(), "color");
	glUniform4f(loc, t*t, 2.0f * t * (1.0f - t), (1.0f - t) *(1.0f - t), 1.0f);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

	time += 0.016f;

위 코드는 매 프레임을 렌더링하는 부분인데, static 변수로 타임을 선언하고 매 렌더링 시 반복적인 값이 나오게 삼각함수를 활용하여 색상값이 변화하게 만들었다.

매 렌더링 시 입력되는 유니폼 값이 변하게 하여 셰이더에서 값을 처리할 때 매번 다른 값이 나오게 되어 화면에 찍히는 픽셀의 값이 변한다.

0개의 댓글