눈팅하는 게임개발자 블로그
OpenGL Getting Started 2-4 Hello Triangle 본문
원문 사이트
learnopengl.com/Getting-started/OpenGL
번역 사이트
heinleinsgame.tistory.com/category/OpenGL
그래픽 파이프라인
그래픽 파이프라인은 크게 2개의 부분으로 나뉠 수 있다.
1. 3D 좌표를 2D좌표(모니터)로 변환
2. 2D좌표를 실제 색이 들어간 픽셀로 변환.
그래픽 파이프라인의 사용법
그래픽 파이프라인의 병렬성으로 인해 최신 그래픽카드는 그래픽 파이프라인의 각 단계에서 GPU 위의
작은 프로그램들을 실행시킴으로써 데이터를 빠르게 처리하기 위해 수천 개의 작은 프로세싱 코어를 가지고 있음.
이 작은 프로그램들을 쉐이더(Shader)라고 부른다.
이 쉐이더 중 일부는 개발자가 직접 작성하여 개발자에 의해 설정 가능한 쉐이더가 존재한다.(Programmable Shaders)
이는 파이프라인의 특정 부분에 좀 더 세밀한 조작을 할 수 있게 한다.
그리고 쉐이더는 GPU 위에서 실행되기 때문에 귀중한 CPU의 시간을 절약할 수 있다.
OpenGL에서 쉐이더는 OpenGL Shading Language(GLSL)으로 작성된다.
그래픽 파이프라인의 모든 단계가 추상화된 모형
파란색인 Vertex Shader, Geometry Shader, Fragment Shader가
개발자가 직접 설정할 수 있는 Programmable Shader이다.
위의 그래픽 파이프라인의 입력 데이터는 삼각형을 구성할 수 있는 정점 데이터라 불리는 3개의 3D 좌표 리스트이다.
이 정점의 데이터는 정점 속성(vertex attribute)을 사용하여 나타낼 수 있다.
정점 속성은 모든 데이터를 포함할 수 있지만 여기선 간단하게 3D 위치와 컬러 값을 가진다고 가정한다.
파이프라인의 첫 번째 부분은 하나의 정점을 입력으로 받는 정점 쉐이더.
Vertex 쉐이더의 주 목적은 3D 좌표를 다른 3D 좌표로 변환하는 것이다.
또한 vertex 속성에 대한 기본적인 처리를 할 수 있도록 한다.
primitive assembly 단계는 primitive를 구성하고 primitive 도형의 모든 점들을 조립하는
vertex shader로부터의 입력 값으로 모든 정점들을 받는다. 위의 경우에는 삼각형이다.
primitive assembly 단계의 결과값은 geometry shader로 전달된다.
geometry shader는 입력 값으로 정점들의 집합을 받는다.
이 정점들의 집합은 primitive를 구성하고 새로운 정점을 방출하여 새로운 primitive를 형성함으로써
다른 도형으로 변환될 수 있는 정점들이다. 이 예제에서는 주어진 도형에서 두 개의 삼각형을 생성한다.
geometry shader의 출력 값은 rasterization stage로 넘어가게 되며
rasterization stage는 결과 primitive를 최종 화면의 적절한 픽셀과 매핑한다.
그 결과로 다음 단계인 fragment shader에서 사용할 fragment가 도출된다.
OpenGL에서 fragment는 하나의 픽셀을 렌더링하기 위해 필요한 모든 데이터가 된다.
또한 fragment Shader를 실행하기 전에 clipping이 수행된다.
clipping은 성능을 증가시키기 위해 카메라 뷰 밖에 있는 모든 fragment들을 폐기한다.
fragment shader의 주 목적은 픽셀의 최종 컬러를 계산하는 것이다.
그리고 이 단계는 일반적으로 OpenGL의 모든 고급 효과들이 발생하는 단계이다.
일반적으로 fragment Shader는 3D Scene에 대한 데이터를 가지고 있다.
이 데이터는 최종 픽셀 컬러(광원, 그림자, 빛의 색 등)를 계산하기 위해 사용될 수 있다.
모든 컬럼 값들이 결정된 후 최종 결과물은 하나의 단계를 더 거친다.
이 단계는 alpha test와 blending 단계로서
fragment의 해당 깊이와 값을 체크한다.
최종 fragment가 다른 오브젝트보다 앞에 있는지 뒤에 있는지 확인하여 다른 오브젝트보다 뒤에 있는 fragment는 지워진다.
또한 이 단계에서 alpha값(투명도)을 확인하고 그에 맞춰 다른 오브젝트와 섞여진다.(blend)
그래서 fragment shader에서 픽셀 출력 색이 계산되었더라도 최종 픽셀 컬러는
여러 개의 삼각형을 렌더링할 때 완전히 다른 색이 될 수 있다.
이와 같이 그래픽 파이프라인은 꽤 복잡하고 많은 설정해야할 부분들을 포함하고 있다.
하지만 대부분의 예제에서 우리는 vertex shader와 fragment shader만을 다룰 것이다.
geometry shader는 선택적으로 사용하고 일반적으로 기본 값으로 남겨 놓는다.
현대 OpenGL에서는 최소한 vertex shader와 fragment shader는 개발자가 스스로 작성한 것을
사용하기를 요구한다.(GPU에 기본 vertex / fragment shader가 존재하지 않는다.)
Vertex 입력
그래픽 파이프라인의 입력 값으로 주어질 Vertex 데이터가 필요하다.
OpenGL은 모든 3D 좌표를 화면 상의 2D 픽셀로 변환하기 위해
3D 좌표가 모든 3개의 축(x, y, z)에서 값이 모두 -1.0과 1.0 사이에 있어야만 처리가 가능하다.
소위 normalized device coordinates 범위라고 불리는 범위 안에 있는 모든 좌표들은 최종적으로 화면에 나타나게 된다.
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
정의된 정점 데이터를 vertex shader에 전달할 때
GPU에 정점 데이터를 저장할 공간의 메모리를 할당하고
OpenGL이 어떻게 메모리를 해석할 것인지 구성하고
데이터를 어떻게 그래픽카드에 전달할 것인지에 대해 명시함으로써 작업을 완료한다.
OpenGL에서는 Vertex Buffer Objects(VBO)라고 불리는 것들을 통해 이 메모리를 관리한다.
VBO는 많은 양의 정점들을 GPU 메모리 상에 저장할 수 있다.
이러한 버퍼 객체를 사용하면 대량의 데이터를 한꺼번에 그래픽 카드로 전송할 수 있다는 장점이 있따.
CPU에서 그래픽카드로 데이터를 전송하는 것은 비교적 느리기 때문에 가능한 한 많은 데이터를 보내야 한다.
데이터가 그래픽 카드의 메모리에 할당되기만 하면 vertex shader는 거의 즉각적으로 빠르게 정점들에 접근할 수 있다.
VBO가 이 예제에서 사용할 첫 번째 OpenGL 객체이다.
OpenGL의 모든 객체처럼 이 버퍼도 버퍼에 맞는 고유한 ID를 가지고 있다.
glGenBuffers() 함수를 사용하여 버퍼 ID를 생성할 수 있다.
unsigned int VBO;
glGenBuffers(1, &VBO);
OpenGL에는 다양한 버퍼 객체가 존재하며 VBO의 버퍼 유형은 GL_ARRAY_BUFFER이다.
OpenGL은 버퍼 유형이 다른 여러가지 버퍼를 바인딩할 수 있다.
새롭게 생성된 버퍼를 glBindBuffer함수를 사용해 GL_ARRAY_BUFFER로 바인딩할 수 있다.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
이 시점부터 호출하는 모든 버퍼(GL_ARRAY_BUFFER를 타겟으로 하는)는 현재 바인딩 된 버퍼를 사용하게 된다.
그 다음 glBufferData 함수를 호출할 수 있는데, 이 함수는 미리 정의된 정점 데이터를 버퍼의 메모리에 복사한다.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
첫 번째 파라미터는 데이터를 복사하여 집어넣을 버퍼의 유형(GL_ARRAY_BUFFER)
두 번째 파라미터는 버퍼에 저장할 데이터의 크기(sizeof(vertices))
세 번째 파라미터는 버퍼에 보낼 실제 데이터를 의미한다.(vertices)
네 번째 파라미터는 그래픽 카드가 주어진 데이터를 관리하는 방법을 받는다(GL_STATIC_DRAW)
GL_STATIC_DRAW의 경우 데이터가 거의 변하지 않는 경우
GL_DYNAMIC_DRAW의 경우 데이터가 자주 변경되는 경우
GL_STREAM_DRAW의 경우 데이터가 그려질 때마다 변경되는 경우 사용한다.
삼각형의 위치 데이터는 모든 렌더링 호출 때마다 변하지 않고 항상 같으므로 위의 경우에는 STATIC_DRAW를 사용.
자주 바뀔 수 있는 데이터가 들어있는 버퍼일 경우 GL_DYNAMIC_DRAW, GL_STREAM_DRAW로 설정하면
그래픽 카드가 빠르게 쓸 수 있는 메모리에 데이터를 저장한다.
위의 단계를 끝마치면 정점 데이터를 그래픽 카드의 메모리에 저장한다.
이 메모리는 VBO가 관리하게 된다.
Vertex Shader
GLSL을 통해 vertex shader를 작성한다.
#version 330 core // 3.3버전을 선언. Core profile 기능을 사용할 것이라고 명시함.
layout (location = 0) in vec3 aPos; // in 키워드를 사용하여 모든 입력 정점 속성을 선언해야 한다.
// 현재는 위치 데이터만 사용하므로 오직 하나의 정점 속성만이 필요하다.
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}// vertex shader의 출력 값으로 사용될 변수 gl_Position 선언, 초기화
vertex shader의 출력 값을 설정하기 위해 미리 선언된 gl_Position 변수에 위치 데이터를 할당.
이 변수는 vec4 타입의 변수이다.
Main 함수의 끝에 gl_Position에 설정한 것을 vertex shader의 출력 값으로 사용한다.
입력 값은 크기가 3인 vector이기 때문에 이를 크기가 4인 vector로 형변환 해야 한다.
(x, y, z, w)값 중 w 값을 1.0f로 설정하여 형변환을 수행.
현재 이 vertex shader는 가장 간단한 vertex shader이다.
입력 데이터에 대해 아무런 처리를 하지 않고 간단히 shader의 출력 값으로 전달했기 때문이다.
실제 응용 프로그램에서는 입력 데이터는 일반적으로 normalized device coordinates 영역에 포함되지 않는 값들이다.
따라서 OpenGL의 표시할 수 있는 영역 내에 있는 좌표로 변환할 필요가 있다.
Shader Compile
vertex shader의 소스 코드를 작성했지만 OpenGL이 해당 shader를 사용하기 위해서는 런타임 시에
shader 소스 코드를 동적으로 컴파일해야 한다.
먼저 해야할 일은 ID를 참조하여 shader 객체를 생성하는 것이다.
vertex shader를 unsigned int 타입으로 저장해야 하고 glCreateShader 함수를 사용하여 shader를 생성한다.
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
vertex shader를 생성하기 위해서 GL_VERTEX_SHADER를 파라미터로 입력.
다음 shader의 코드를 shader 객체에 첨부한 후 이를 컴파일.
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource 함수는 shader를 컴파일하기 위해
첫 번째 파라미터로 shader 객체를.
두 번째 파라미터로 소스 코드가 몇 개의 문자열로 되어 있는지에 대한 값을(여기서는 1개)
세 번째 파라미터로는 vertex shader의 소스 코드를 받고
네 번째 파라미터는 NULL로 남겨둔다.
이후 컴파일이 제대로 진행되어 있는지 확인한다.
컴파일 에러를 확인하는 코드
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
에러가 발생하지 않았다면 vertex shader의 컴파일이 완료된 것.
Fragment Shader
삼각형을 렌더링하기 위해 생성해야 할 두 번째 쉐이더.
픽셀의 출력 컬러 값을 계산하는 것에 관한 쉐이더.
간단하게 하기 위해서 fragment shader는 항상 주황색을 출력하게 할 예정이다.
#version 330 core
out vec4 FragColor; // 출력 변수로 사용할 크기가 4인 vector 변수, out 키워드로 출력 값을 선언.
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} // 주황색을 간단하게 할당. 알파 값은 1.0f로 설정
fragment shader 컴파일.
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//컴파일 성공 체크
glGetShader(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERRORR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
이제 마지막으로 shader program으로 두 개의 shader 객체를 서로 연결하는 것만이 남았다.
shader program은 렌더링 할 때 사용 가능하다.
Shader Program
shader program 객체는 여러 shader를 결합한 마지막 연결된 버전이다.
컴파일된 shader들을 사용하기 위해 shader들을 shader program 객체로 link 해주어야 한다.
그 다음 오브젝트를 렌더링할 때 이 shader program을 활성화시키면 된다.
shader들을 program에 연결할 때 shader들의 출력 값을 다음 shader의 입력 값으로 연결한다.
출력과 입력이 일치하지 않으면 연결 오류가 발생한다.
Program 객체 생성
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
이후 컴파일된 shader들을 연결시킨다.
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
//링크 성공 체크
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
링크된 결과물은 glUseProgram함수로 활성화 시킬 수 있는 program 객체가 된다.
glUseProgram(shaderProgram);
이후 모든 shader와 렌더링 명령은 이 Program 객체(또는 내부의 shader들)를 사용하게 된다.
그리고 shader들을 program 객체로 연결하고 나면 shader 객체들을 지워주는 것이 좋다.
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
이제 입력 정점 데이터를 GPU로 보냈고 GPU에게 정점 데이터를 어떻게 처리해야 하는지 vertex shader와
fragment shader를 통해 지시했다.
이제 OpenGL이 메모리 상의 정점 데이터를 어떻게 해석해야 하는지, 정점 데이터를 vertex shader의 속성들과
어떻게 연결해야 하는지를 알려주면 된다.
Linking Vertex Attributes
vertex shader는 우리가 원하는 모든 입력들을 정점 속성의 형식으로 지정할 수 있도록 해준다.
또한 유연성이 좋은 반면에 입력 데이터의 어느 부분이 vertex shader의 어떠한 정점 속성과 맞는지 직접 지정해야 한다.
이는 렌더링 하기 전에 OpenGL이 정점 데이터를 어떻게 해석해야 하는지를 직접 지정해주어야 한다는 뜻이다.
현재 vertex buffer 데이터의 형식
각 위치는 X, Y, Z의 3가지 값으로 구성된다.
각 3개 값의 집합들 사이에 공백은 없다.
데이터의 첫 번째 값은 버퍼의 시작 지점에 존재한다.
glVertexAttribPointer 함수를 사용하여 위의 사실들과 함께 OpenGL에게 vertex 데이터를 어떻게 해석해야 하는지 알려줄 수 있다.
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
첫 번째 파라미터는 설정할 vertex 속성을 지정, vertex shader에서
layout (location = 0)
코드를 사용해 position vertex 속성의 위치를 지정했었던 것을 기억하여
vertex 속성의 위치(location)을 0으로 설정하고 데이터를 이 vertex 속성에 전달해야 한다. 따라서 0에 전달.
두 번째 파라미터는 vertex 속성의 크기를 지정, 이 vertex 속성은 vec3 타입이므로 3을 전달.
세 번째 파라미터는 데이터의 타입을 지정, 실수형 점이므로 GL_FLOAT으로 지정.
네 번째 파라미터는 데이터를 정규화 할 것인지를 지정. 이 파라미터를 GL_TRUE로 설정하면
0과 1 사이에 있지 않은 데이터들이 그 사이의 값들로 매핑된다. GL_FALSE로 설정.
다섯 번째 파라미터는 stride라고도 불리며 연이은 vertex 속성 세트들 사이의 공백을 알려준다.
다음 포지션 데이터의 세트는 정확히 float 타입 3개 크기 뒤에 떨어져 있다. 따라서 이 값을 stride로 지정.
해당 배열이 빈 공간 없이 채워져 있다는 것을 알고 있다면 stride를 0으로 지정하여
OpenGL이 직접 stride를 지정하게 할 수 있다.(채워져 있는 경우에만 동작)
더 많은 vertex 속성들이 존재한다면 각 vertex 속성들 사이의 공간을 조심스럽게 정의할 필요가 있다.
마지막 파라미터는 void*타입이므로 형변환이 필요하다.
이는 버퍼에서 데이터가 시작하는 위치의 offset이다. 위치 데이터가 데이터 배열의 시작 부분에 있기 때문에
이 파라미터는 0으로 지정한다.
이제 OpenGL이 vertex 데이터를 어떻게 해석해야 하는지 지정했으므로
glEnableVertexAttribArray 함수의 파라미터로 vertex 속성 location을 전달하고 호출하여 vertex 속성을
사용할 수 있도록 해야 한다.
glEnableVertexAttribArray(0);
OpenGL이 오브젝트를 그리는 형식
0. 정점 배열을 OpenGL에서 사용하기 위해 버퍼에 복사
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
1. 다음 vertex 속성 포인터를 설정
glVertexAttribPointer(0, 3, GL_FALOT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
2. 오브젝트를 그리고 싶을 때 생성해 둔 shader program을 사용.
glUseProgram(shaderProgram);
3. 오브젝트를 그린다.
오브젝트를 그려야 할 때마다 이 모든 과정을 반복해야 한다.
모든 오브젝트들에 대해 신속히 적절한 buffer 객체를 바인딩하는 것과 모든 vertex 속성들을 구성하는 것은
굉장히 번거로운 과정이 된다.
이 모든 상태 설정을 객체에 저장하고 간단히 이 객체를 바인딩하여 상태를 복원할 수 있는 방법이 있다.
Vertex Array Object(VAO)
Vertex Buffer object(VBO)와 같이 바인딩 될 수 있으며 그 이후 vertex 속성 호출은 VAO 내에 저장된다.
이는 vertex 속성 포인터를 구성할 때 오직 한번 호출하기만 하면 되고 오브젝트를 그려야 할 때마다
해당 VAO를 바인딩하기만 하면 된다는 장점을 가지고 있다.
이는 서로 다른 vertex 데이터와 속성들을 다른 VAO를 바인딩함으로써 손쉽게 교체할 수 있다.
설정한 모든 상태가 VAO 내부에 저장된다.
core OpenGL은 정점 입력과 관련하여 VAO를 사용하도록 요구한다.
VAO를 바인딩하는데 실패한다면 OpenGL은 동작하지 않는다.
Vertex Array Object는 다음 항목들을 저장한다.
glEnableVertexAttribArray 함수나 glDisableVertexAttribArray 함수의 호출.
glVertexAttribPointer 함수를 통한 vertex 속성의 구성.
glVertexAttribPointer 함수를 통해 vertex 속성과 연결된 vertex buffer objects(VBOs)
VAO를 생성하는 과정은 VBO와 비슷하다.
Unsigned int VAO;
glGenVertexArrays(1, &VAO);
VAO를 사용하기 위해 glBindVertexArray 함수를 이용하여 VAO를 바인딩해야 한다.
그 후부터 해당 VBO와 속성 포인터를 바인딩/구성하고 VAO를 나중에 사용하기 위해 언바인딩 해야 한다.
오브젝트를 그리려면 그 전에 간단히 원하는 세팅과 함께 VAO를 바인딩 하기만 하면 된다.
코드는 다음과 같다.
// ..:: 초기화 코드 (한번만 실행됩니다(오브젝트가 자주 변경되지 않는 한)) :: ..
// 1. Vertex Array Object 바인딩
glBindVertexArray(VAO);
// 2. OpenGL이 사용하기 위해 vertex 리스트를 복사
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 그런 다음 vertex 속성 포인터를 세팅
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 드로잉 코드 (렌더링 루프 내부) :: ..
// 4. 오브젝트를 그립니다.
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
오브젝트를 그리기 위해 OpenGL은 glDrawArrays 함수를 제공한다.
이 함수는 현재 활성화된 shader, 이전에 정의된 vertex 속성 구성,
VBO의 vertex 데이터(VAO를 통해 간접적으로 바인딩 된)를 사용하여 primitive를 그린다.
glDrawArrays(GL_TRIANGLES, 0, 3);
첫 번째 파라미터로 그리려 하는 primitive 유형을 지정, 삼각형을 그리고자 하기 때문에 GL_TRIANGLES로 지정.
두 번째 파라미터로 vertex 배열의 시작 인덱스를 지정. 0으로 지정
마지막 파라미터로 몇 개의 vertex를 그리기 원하는지를 지정. 3개의 정점을 지정.
오브젝트를 그리는 과정을 다시 정리
준비 1. GPU에게 정점 데이터를 보내고 이 정점 데이터를 어떻게 처리할 것인지
vertex shader와 fragment shader를 통해 지시한다.
Unsigned int vertexShader
vertexShader = glCreateShader(GL_VERTEX_SHADER); // ID를 참조하여 shader 객체를 생성.
glShaderSource(vertexShader, 1, &vertexShaderSoruce, NULL); //shader 코드를 객체에 첨부.
glCompileShader(vertexShader); // 컴파일.
// 이후 컴파일 성공 Check
Unsigned int fragmentShader
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); // ID를 참조하여 shader 객체를 생성.
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); // shader 코드를 객체에 첨부
glCompileShader(vertexShader); // 컴파일
// 이후 컴파일 성공 Check
Unsigned int shaderProgram;
shaderProgram = glCreateProgram(); // shader 프로그램을 생성, 생성된 객체의 ID를 받아온다.
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader); // 두 개의 쉐이더를 연결시키고.
glLinkProgram(shaderProgram); // 링크.
// 이후 링크 성공 Check
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader); //링크 이후 쉐이더들은 지워준다.
준비 2. OpenGL이 메모리 상의 정점 데이터를 어떻게 해석해야 하는지, vertex shader의 속성들과 어떻게 연결해야 하는지를 알려준다.
Unsigned int VBO, VAO; // VBO, VAO 선언
glGenVertexArrays(1, &VAO); //VAO를 생성
glGenBuffers(1, &VBO); // VBO를 생성
glBindVertexArray(VAO); // VAO를 바인드.
glBindBuffer(GL_ARRAY_BUFFER, VBO); // VBO를 GL_ARRAY_BUFFER로 바인딩.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 정의된 정점 데이터 vertices를 버퍼의 메모리에 복사.
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); //vertex 속성 Pointer를 설정
glBindBuffer(GL_ARRAY_BUFFEr, 0); // 언바인딩.
glBindVertexArray(0); // 언바인딩.
이제 삼각형을 그려보자.
Element Buffer Objects
정점들을 렌더링할 때 하나 더 생각해봐야 할 것이 있다.
사각형을 렌더링하기 위해 2개의 삼각형을 이용할 수 있는데.
이 때 사각형은 4개의 정점만으로도 그려질 수 있지만
2개의 삼각형은 6개의 정점이 필요하다.
겹치는 2개의 점점 떄문에 약 50%의 오버헤드가 발생하는 것이다.
오직 고유한 정점들만 저장하고 그들의 순서를 지정하는 방법으로서 EBO가 있다.
EBO는 VBO와 같은 버퍼이다.
EBO는 OpenGL이 어떠한 정점들을 그려야 할 지 결정할 수 있는 인덱스들을 저장한다.
이는 indexed drawing이라고도 하며 위의 문제에 대한 정확한 해결 방법이다.
이를 위해 먼저 정점들을 지정해야 하고, 사각형을 그릴 정점들의 인덱스를 지정해야 한다.
Float vertices[] = {
0.5f, 0.5f, 0.0f, // 우측 상단
0.5f, -0.5f, 0.0f, // 우측 하단
-0.5f, -0.5f, 0.0f, // 좌측 상단
-0.5f, 0.5f, 0.0f // 좌측 상단
};
Unsigned int indices[] = {
0, 1, 3, // 첫 번째 삼각형
1, 2, 3 // 두 번째 삼각형
};
EBO를 생성해준다.
Unsigned int EBO;
glGenBuffers(1, &EBO);
VBO와 비슷하게 glBufferData 함수를 사용하여 EBO를 바인딩하고 인덱스들을 버퍼에 복사한다.
또한 VBO와 마찬가지로 바인딩과 언바인딩 사이에 이러한 호출을 배치한다.
이번에는 버퍼 유형을 GL_ELEMENT_ARRAY_BUFFER로 지정한다.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
이제 남은 것은 glDrawArrays 함수를 glDrawElements 함수로 대체하는 것이다.
glDrawElements 함수를 사용할 때 현재 바인딩된 EBO의 인덱스들을 사용하여 그리게 된다.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements 함수는 GL_ELEMENTS_ARRAY_BUFFER을 타겟으로 현재 바인딩된 EBO로부터
인덱스들을 가져온다. 이는 해당 EBO를 렌더링할 때마다 바인딩해야 한다는 것을 의미한다.
번거로운 작업이지만
VAO가 EBO의 바인딩도 저장할 수 있다.
VAO가 바인딩 되어있는 동안 EBO가 바인딩되면 VAO의 버퍼 객체로서 저장된다.
VAO를 바인딩하면 자동으로 내부에 있는 EBO도 바인딩 된다.
초기화와 드로잉 코드는 다음과 같다.
// ..:: 초기화 코드 :: ..
// 1. Vertex Array Object 바인딩
glBindVertexArray(VAO);
// 2. OpenGL이 사용하기 위해 vertex 리스트를 vertex 버퍼에 복사
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. OpenGL이 사용하기 위해 인덱스 리스트를 element 버퍼에 복사
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 그런 다음 vertex 속성 포인터를 세팅
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 드로잉 코드 (렌더링 루프 내부) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
이제 프로그램은 아래와 같은 화면을 보여준다.
'공부한거 > OpenGL' 카테고리의 다른 글
OpenGL Getting Started 2-9 Camera (0) | 2020.09.29 |
---|---|
OpenGL Getting Started 2-8 Coordinate System (0) | 2020.09.29 |
OpenGL Getting Started 2-7 Translation (0) | 2020.09.27 |
OpenGL Getting Started 2-6 Texture (0) | 2020.09.24 |
OpenGL Getting Started 2-5 Shaders (0) | 2020.09.24 |