본문 바로가기
<LIBRARY>/OPENGL

2. 삼각형 그리기 (1)

by CodeGrimie 2021. 1. 14.

배경을 그릴 수 있게 되었으니 이제 아주 간단한 삼각형을 그려볼 차례다.

미리 말하지만 쉽지 않고 하드코딩으로 되어있다.

코드 정리는 모든 코드가 완성되고 난 뒤에 코드 리뷰를 진행하면서 할 예정이다.

 

▼ //----와 //----로 묶인 영역들이 오늘 우리가 할 작업이다.

    // GLEW의 모든 기능 활성화
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK)
    {
        wprintf(L"GLEW 초기화가 실패했습니다.\n");
        // mainWindow 삭제
        glfwDestroyWindow(mainWindow);
        glfwTerminate();
        return (1);
    }

    //---------------------------------------------
    // 위치 1) 삼각형 정보를 만듭니다.
    //---------------------------------------------

    // OpenGL Viewport 생성
    glViewport(0, 0, bufferWidth, bufferHeight);
    
    // GLFW가 종료되지 않는 한 계속 도는 순환문
    while (glfwWindowShouldClose(mainWindow) == GLFW_FALSE)
    {
        // GLFW 이벤트 입력
        glfwPollEvents();

        // 연두색 화면 그리기
        glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
        // OpenGL 배경색상 초기화
        glClear(GL_COLOR_BUFFER_BIT);

        //---------------------------------------------
        // 위치 2)
        // VBO에 있는 데이터 바인딩
        // 데이터를 바탕으로 그리기
        // 데이터 바인딩 해제
        //---------------------------------------------

        // GLFW 더블 버퍼링
        glfwSwapBuffers(mainWindow);
    }

OpenGL 그래픽 파이프라인

삼각형을 그리기 전에 우리는 OpenGL이 어떻게 도형을 그리는지를 알아보자.

지금 당장은 완벽하게 이해할 필요는 없다.

직접 코드를 작성해보면서 흐름을 이해하는 것만으로도 지금은 충분한 성장이다.

 

기본적으로 OpenGL은 버퍼, 텍스쳐 두 개의 데이터를 사용한다.

그중 버퍼는 정점의 좌표부터 색상 정보까지 거의 모든 정보들을 저장한다.

 

오늘 다뤄볼 VAO, VBO는 여기에 해당한다.

 

VBO(Vertex Buffer Object)는 우리나라 말로 정점 간이 저장 객체 정도로 해석된다.

이름 그대로 정점의 정보들을 저장하는데 정점의 좌표뿐 아니라 다양한 부차적인 데이터를 저장할 수 있다.

우리가 그릴 삼각형의 정점 좌표는 여기에 저장한다.

 

이렇게 만들어진 VBO는 VAO(Vertex Array Object)에 보내 연결(Bind)한다.

이름에서도 배열(Array)가 있듯이 VBO의 배열이란 느낌으로 받아들여도 좋다.

동시에 색인 저장소(Index Buffer)도 하나 가지는데 이 색인 저장소를 사용하여 VBO에 접근한다.

 

요약하면 이제 우린 VBO란 객체를 만들어서 VAO 배열에 집어넣고 그릴 것이다.

삼각형 정점(Vertex) 위치값 선언하기

위치 1에 와서 삼각형 정점값을 저장할 배열을 하나 선언한다.

이 배열은 우리가 어떤 모양을 그릴 지 저장한다.

 

이번에 그릴 삼각형은 화면의 크기에 비례한 아주 간단한 삼각형을 그릴 건데 좌표는 아래의 그림과 같다.

 

▼ 화면 중심을 기준으로 좌표를 지정한다.

▼ 위치 1)에 선언한다.

GLfloat vertices[] = {
    -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f,
    0.0f, 1.0f, 0.0f
};

여기서 우리는 좌표가 반시계 방향으로 선언된다는 것을 알 수 있다.

OpenGL이 도형을 그리는 방법은 두가지 방법이 있는데 시계 방향(CW)과 반시계 방향(CCW)으로 나뉜다.

그저 어떤 순서대로 좌표를 그리는 거라 이 두 방법 중 무엇을 사용하던 작동상 차이는 전혀 없다.

 

하지만 많은 코드들이 반시계 방향을 채택하는데 이는 다른 라이브러리들이 CCW를 기준으로 작업해둔 경우가 많기 때문이다.

 

우리는 다른 라이브러리를 사용할 예정이 없지만 예제를 찾기 쉽고 범용적인 CCW로 작업한다.

 

삼각형 정점 위치를 선언했으니 먼저 VAO, VBO를 작업하기 위해 미리 선언한다.

VAO, VBO 선언하기

▼ VAO, VBO를 GLint 변수형으로 선언한다.

GLfloat vertices[] = {
    -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f,
    0.0f, 1.0f, 0.0f
};

GLuint VAO, VBO;

우리가 삼각형의 좌표를 설정했던 곳 바로 밑에 VAO, VBO를 선언한다.

VBO를 담을 곳이 VAO이기 때문에 VAO를 먼저 작업한다.

VAO 생성 및 해제 코드

▼ VAO를 생성하고 연결한 뒤에 해제까지 해야 한다.

GLfloat vertices[] = {
    -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f,
    0.0f, 1.0f, 0.0f
};

GLuint VAO, VBO;

// OpenGL 정점 배열 생성기를 사용해서 VAO를 생성
glGenVertexArrays(1, &VAO);
// 우리가 생성한 VAO를 현재 수정가능하도록 연결한다.
glBindVertexArray(VAO);

// VBO CODE

// 수정이 완료 되면 연결을 끊기 위해 초기값으로 연결한다.
glBindVertexArray(0);

CPP에서 항상 잊지 말이야 하는 패턴(생성, 할당, 해제)은 여기서도 마찬가지다.

먼저 세상 똑똑한 사람들이 만든 간결하고 직관적인 함수 이름을 보아라.

 

glGenVertexArrays(OpenGL 정점 배열 생성기)

glBindVertexArray(OpenGL 정점 배열 연결기)

 

우리가 VAO를 선언했다고 해도 실제로 사용하겠다고 등록을 한 건 아니다.

그래서 OpenGL 정점 배열 생성기를 사용해서 VAO를 생성한다.

그리고 현재 우리가 생성한 VAO를 수정한다고 연결해준 뒤에 VBO를 작업해서 넣을 것이다.

마지막으로는 다시 초기값으로 연결해서 수정을 완료한다.

 

마치 종이를 꺼내서 그림을 그리고 종이를 다시 집어넣어 버리는 것과 같다.

그리고 코드의 흐름에서도 알 수 있듯이 단 한번 그리기 위한 순서다.

만약 움직임이 있어야 한다면 이 과정은 순환문 안에 위치해야 한다.

지금은 삼각형을 그리는 것만 목표로 하기 때문에 이대로 작업한다.

 

이제 VBO CODE 위치에 VBO를 생성해보자.

VBO 생성 및 연결

VBO 역시 생성하고 연결한 뒤에 해제까지 해야 한다.

// OpenGL 정점 배열 생성기를 사용해서 VBO를 생성
glGenBuffers(1, &VBO);
// 우리가 생성한 VBO를 현재 수정 가능하도록 연결한다.
glBindBuffer(GL_ARRAY_BUFFER, VBO);

// 우리가 만든 삼각형 정점 좌표를 VBO에 저장한다.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// VAO에 이 VAO를 어떻게 해석해야 할 지 알려줍니다.
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

// VAO 사용 허용
glEnableVertexAttribArray(0);
// VBO 수정 종료 및 연결 초기화
glBindBuffer(GL_ARRAY_BUFFER, 0);

VAO와 크게 다르지 않은 순서인 할당하고 연결하고 수정하고 연결을 끊는다고 눈에 띈다.

 

glBufferData는  위에서 만든 정점 좌표를 VBO에 정점 데이터로서 저장하는 역할을 한다.

마지막 인자로 들어가는 GL_STATIC_DRAW는 지금처럼 정점 데이터가 변동될 일이 없는 경우에 사용하는 속성 값이다.

 

glVertexAttribPointer는 VAO에게 이 VBO의 속성 값(Attribute)을 전달한다.

어떻게 보면 이 부분이 가장 복잡한데 이 함수의 매개 인자는 아래와 같다.

(VAO 색인 값, 좌표수(x, y, z), 타입, 정상화 여부, 바이트 오프셋 , 시작 색인 값)

 

아래의 공식 매뉴얼을 읽어보면 좋다.

www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glVertexAttribPointer.xml

그리기 코드

▼ 그리는 코드는 짧다.

while (glfwWindowShouldClose(mainWindow) == GLFW_FALSE)
{
    // GLFW 이벤트 입력
    glfwPollEvents();

    // 연두색 화면 그리기
    glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
    // OpenGL 배경색상 초기화
    glClear(GL_COLOR_BUFFER_BIT);

    //---------------------------------------------
    // VBO에 있는 데이터 연결
    glBindVertexArray(VBO);
    // 데이터를 바탕으로 그리기
    glDrawArrays(GL_TRIANGLES, 0, 3);
    // 데이터 연결 해제
    glBindVertexArray(0);
    //---------------------------------------------

    // GLFW 더블 버퍼링
    glfwSwapBuffers(mainWindow);
}

이제 드디어 그리기 코드를 작성해본다.

배경색을 채우는 코드에 이어 작업하는데 준비작업보다 실제 코드는 매우 짧다.

 

단순히  우리가 설정한 VBO를 연결해서 그리고 해제한다.

glDrawArrays 함수에서 GL_TRIANGLE을 통해 삼각형(Polygon)으로 그리도록 했다.

GL_LINE, GL_POINT와 같은 다른 옵션들도 있다.

 

코드가 완료되었다면 재빨리 컴파일하여 실행해보자.

아래와 같은 사진이 나왔다면 제대로 작성된 것이다.

 

실행화면

삼각형이긴 한데 거무튀튀해서 예쁘지 않다.

우리는 정점 좌표를 통해서 면은 만들어 냈지만 면을 채색하는 Shader를 만들지 않았기 때문이다.

 

다음 글에서 Shader를 통해 면을 채색하는 단계를 작성해본다.

전체 코드

#include <cstdio>
#include <clocale>
#include <cstring>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

const GLint WIDTH = 720, HEIGHT = 480;

int main()
{
	// 로케일 국가 한국 지정
	_wsetlocale(LC_ALL, L"Korean");

	// GLFW 초기화
	if (glfwInit() == GLFW_FALSE)
	{
		wprintf(L"GLFW 초기화 실패\n");
		glfwTerminate();
		return (1);
	}

	// OpenGL 버전 지정
	// OpenGL MAJOR.MINOR 방식으로 표현
	// 이번엔 3.3을 사용한다.
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

	// OpenGL 코어 프로필 설정
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	// OpenGL 상위호환 활성화
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

	// GLFW 윈도우 생성
	GLFWwindow* mainWindow = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL TRIANGLE", NULL, NULL);
	if (mainWindow == NULL)
	{
		wprintf(L"GLFW 윈도우 생성이 실패했습니다.\n");
		glfwTerminate();
		return (1);
	}

	// 버퍼 가로, 버퍼 세로 선언
	int bufferWidth, bufferHeight;
	// mainWindow로부터 버퍼 가로 크기와 버퍼 세로 크기를 받아온다.
	glfwGetFramebufferSize(mainWindow, &bufferWidth, &bufferHeight);

	glfwMakeContextCurrent(mainWindow);

	// GLEW의 모든 기능 활성화
	glewExperimental = GL_TRUE;
	if (glewInit() != GLEW_OK)
	{
		wprintf(L"GLEW 초기화가 실패했습니다.\n");
		// mainWindow 삭제
		glfwDestroyWindow(mainWindow);
		glfwTerminate();
		return (1);
	}

	//---------------------------------------------
	GLfloat vertices[] = {
		-1.0f, -1.0f, 0.0f,
		1.0f, -1.0f, 0.0f,
		0.0f, 1.0f, 0.0f
	};

	GLuint VAO, VBO;
    
	// OpenGL 정점 배열 생성기를 사용해서 VAO를 생성
	glGenVertexArrays(1, &VAO);
	// 우리가 생성한 VAO를 현재 수정 가능하도록 연결한다.
	glBindVertexArray(VAO);

	// OpenGL 정점 배열 생성기를 사용해서 VBO를 생성
	glGenBuffers(1, &VBO);
	// 우리가 생성한 VBO를 현재 수정 가능하도록 연결한다.
	glBindBuffer(GL_ARRAY_BUFFER, VBO);

	// 우리가 만든 삼각형 정점 좌표를 VBO에 저장한다.
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// VAO에 이 VAO를 어떻게 해석해야 할 지 알려줍니다.
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

	// VAO 사용 허용
	glEnableVertexAttribArray(0);
	// VBO 수정 종료 및 연결 초기화
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	// 수정이 완료 되면 연결을 끊기 위해 초기값으로 연결한다.
	glBindVertexArray(0);
	//---------------------------------------------

	// OpenGL Viewport 생성
	glViewport(0, 0, bufferWidth, bufferHeight);

	// GLFW가 종료되지 않는 한 계속 도는 순환문
	while (glfwWindowShouldClose(mainWindow) == GLFW_FALSE)
	{
		// GLFW 이벤트 입력
		glfwPollEvents();

		// 연두색 화면 그리기
		glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
		// OpenGL 배경색상 초기화
		glClear(GL_COLOR_BUFFER_BIT);

		//---------------------------------------------
		// VBO에 있는 데이터 바인딩
		glBindVertexArray(VBO);
		// 데이터를 바탕으로 그리기
		glDrawArrays(GL_TRIANGLES, 0, 3);
		// 데이터 바인딩 해제
		glBindVertexArray(0);
		//---------------------------------------------

		// GLFW 더블 버퍼링
		glfwSwapBuffers(mainWindow);
	}

	return (0);
}

'<LIBRARY> > OPENGL' 카테고리의 다른 글

5. GLM 라이브러리 적용하기  (0) 2021.01.19
4. 삼각형 움직이기  (0) 2021.01.17
3. 삼각형 그리기(2)  (0) 2021.01.17
1. 기본 코드 작성하기  (0) 2021.01.11
0. OpenGL 개발환경 설정하기  (0) 2021.01.09

댓글