본문 바로가기

Game/Graphics

Learn OpenGL - Getting started : Hello Triangle

link : https://learnopengl.com/Getting-started/Hello-Triangle


Hello Triangle


 OpenGL에서는 모든 것이 3D 공간에 있지만 화면과 창은 2D 픽셀 배열이므로


OpenGL의 많은 작업은 모든 3D 좌표를 화면에 맞는 2D 픽셀로 변환하는 것이라고 볼 수 있다.


3D 좌표를 2D 좌표로 변환하는 프로세스는 OpenGL의 그래픽 파이프라인에 의해 관리된다.


그래픽 파이프 라인은 크게 두 부분으로 나눌 수 있다.


첫 번째 부분은 3D 좌표를 2D 좌표로 변환하고,


두 번째 부분은 2D 좌표를 실제 색이 있는 픽셀로 변환하는 것이다.


이 튜토리얼에서는 그래픽 파이프 라인에 대해 간략히 논의하고 멋진 픽셀을 만드는데


어떻게 활용하는지에 대해 간략히 설명할 것이다.

2D 좌표와 픽셀에는 차이가 있다. 2D 좌표는 점이 2D 공간에 있는 위치를 나타내는 매우 정확한 표현이고,
2D 픽셀은 screen/window 해상도로 제한된 지점의 근사치이다.

 그래픽 파이프 라인은 입력으로 3D 좌표 집합을 가져와 화면의 컬러 2D 픽셀로 변환한다.


그래픽 파이프 라인은 여러 단계로 나눌 수 있다. 각 단계에서는 입력으로 이전 단계의 결과가 필요하다.


이 모든 단계는 고도로 전문화되어 있으며 병렬로 쉽게 실행할 수 있다.


병렬성으로 인해 현재 대부분의 그래픽 카드에는 파이프 라인의 각 단계마다


GPU에서 작은 프로그램을 실행해 그래픽 파이프 라인 내에서 신속하게 데이터를 처리할 수 있는


수천 개의 작은 프로세싱 코어가 있다. 이러한 작은 프로그램을 쉐이더라고 한다.




 이러한 쉐이더 중 일부는 개발자가 구성할 수 있으므로 기존의 쉐이더를 대체할 쉐이더를 만들 수 있따.


이렇게 하면 파이프 라인의 특정 부분에 대한 세분화된 제어가 가능하며 GPU에서 실행되기 때문에


CPU 시간을 절약할 수 있따. 쉐이더는 GLSL (OPenGL Shading Language)로 작성되었으며


다음 튜토리얼에서 더 자세히 설명할 것이다.



 아래에서 그래픽 파이프 라인의 모든 단계에 대한 추상 표현을 찾을 수 있다.


파란색 섹션은 우리가 우리 자신의 쉐이더를 삽입할 수 있는 섹션을 나타낸다.


The OpenGL graphics pipeline with shader stages


 보시다시피 그래픽 파이프 라인에는 정점 데이터를 완전히 렌더링된 픽셀로 변환하는 특정 부분을


처리하는 여러 섹션이 있다. 우리는 간략하게 파이프 라인의 각 부분을 설명해 파이프 라인이


어떻게 작동하는지를 간략하게 설명할 것이다.



 그래픽 파이프라인의 입력으로 우리는 3개의 3D 좌표 목록을 전달한다. 이 좌표는 역시 정점 데이터라는


배열의 삼각형을 형성해야한다. 이 정점 데이터는 정점 모음이다. 정점은 기본적으로 3D 좌표당 데이터모음이다.


이 정점의 데이터는 우리가 원하는 모든 데이터를 포함할 수 있는 정점 속성을 사용해 나타내지만


단순화를 위해 각 정점은 단지 3D 위치와 일부 색상 값으로 구성된다고 가정하자.

OpenGL이 좌표 및 색상 값 모음을 만들지를 알기 위해서는 OpenGL에서 데이터로 어떤 종류의 렌더 유형을 만들 것인지 힌트를 표시해야한다. 데이터를 점들의 집합, 삼각형의 집합 또는 한 개의 긴 선으로 렌더링하고 싶습니까? 이러한 힌트는 프리미티브라고 하며 drawing 명령을 호출하는 동안 OpenGL에 제공된다. 이러한 힌트 중 일부는 GL_POINT, GL_TRIANGLES, GL_LINE_STRIP이다.

 파이프 라인의 첫번째 부분은 입력으로 단일 정점을 취하는 정점 쉐이더이다. 정점 쉐이더의 주요 목적은


3D 좌표를 다른 3D 좌표로 변환하는 것이다. 정점 쉐이더를 사용하면 정점 속성에 대한 기본적인 처리를 할 수 있다.



 primitive assembly 단게에서는 주어진 프리미티브를 형성하고 모든 프리미티브를 어셈블하는


정점 쉐이더에서 모든 정점들(또는 GL_POINTS가 선택된 경우 정점)을 입력으로 사용한다. (이 경우에는 삼각형이다)



 primitive assembly 단계의 출력은 geometry shader로 전달된다. 기하학 쉐이더는 primitive를 형성하고


새로운 꼭지점을 방출해 새로운 (또는 다른) 프리미티브를 형성함으로써 다른 shape를 생성할 수 있는 정점 컬렉션을


입력으로 사용한다. 이 예제에서는 주어진 모양에서 두 번째 삼각형을 생성한다.



 그런 다음 geometry shader의 출력을 rasterization 단계로 전달해 최종 화면의 해당 픽셀에 결과


프리미티브를 매핑해 조각 쉐이더가 사용할 조각을 생성한다. fragment shader가 실행되기 전에 클리핑이 수행된다.


클리핑은 보이지 않는 외부의 모든 조각이 삭제되어 성능이 향상된다.

OpenGL의 fragment는 OpenGL이 단일 픽셀을 렌더링하는데 필요한 모든 데이터이다.

 fragment shader의 주요 목적은 픽셀의 최종 색상을 계산하는 것이고, 이것은 일반적으로 모든 고급 OpenGL 효과가


발생하는 단계이다. 일반적으로 fragment shader에는 최종 픽셀 색상을 계산하는데 사용할 수 있는


3D 장면에 대한 데이터(예: 조명, 그림자, 조명의 색상 등)가 들어 있습니다.



 해당 색상 값이 모두 결정되면 최종 객체는 알파 테스트 및 블렌딩 단계라고 하는 스테이지를


하나 더 통과한다. 이 단계에서는 조각의 해당 깊이(및 stencil) 값을 확인하고 그 결과 조각을 다른 오브젝트의


앞이나 뒤에 있는지 확인해 그에 따라 폐기해야한다. 또한 stage는 알파 값을 확인하고 그에 따라 객체를


혼합한다. 따라서 픽셀 출력 색상이 프래그먼트 쉐이더에서 계산되더라도 최종 픽셀 색상은


여전히 여러 삼각형을 렌더링 할 때 완전히 다른 색상일 수 있다.



 보시다시피, 그래픽 파이프 라인은 상당히 복잡하고 많은 구성 가능한 부분을 포함한다.


그러나 거의 모든 경우에 정점 및 조각 쉐이더만 사용해야한다. geometry shader는 선택 사항이며


대개 기본 쉐이더의 위치에 있다.



 현대 OpenGL에서 우리는 적어도 우리 자신의 정점과 프래그먼트 쉐이더를 정의해야한다.


이런 이유로 현대 OpenGL을 배우기 시작하는 것은 종종 어렵다. 첫 삼각형을 렌더링하기 전에 많은 지식이


필요하기 때문이다. 이 장의 마지막 부분에서 마침내 삼각형을 렌더링하게되면 그래픽 프로그래밍에 대해


더 많이 알게 될 것이다.




Vertex input


 드로잉을 시작하려면 먼저 OpenGL에 입력 정점 데이터를 입력해야한다. OpenGL은 3D 그래픽 라이브러리이므로


OpenGL에서 지정하는 모든 좌표는 3D(x,y,z)입니다. OpenGL은 단순히 모든 3D 좌표를 화면의 2D 픽셀로


변환하지 않는다. OpenGL은 모든 3축(x,y,z)에서 -1.0과 1.0 사이의 특정 범위에 있을 때만 3D 좌표를 처리한다.


소위 정규화된 장치 좌표 범위 내의 모든 좌표는 화면에 표시된다(이 영역 외부의 모든 좌표는 그렇지 않다).



 하나의 삼각형을 렌더링하기를 원하기 때문에 우리는 각 꼭지점이 3D 위치인 총 세 개의 꼭지점을 지정하려고 한다.


float 배열의 정규화된 장치 좌표에서 이를 정의한다.


float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};  

 OpenGL은 3D 공간에서 작동하기 때문에 각 꼭지점이 z 좌표가 0.0인 2D 삼각형을 렌더링한다.


이 방법으로 삼각형의 깊이는 동일하게 유지되어 2D 좌표로 보인다.

Normalized Device Coordinates (NDC)

 정점 쉐이더에서 정점 쉐이더가 처리되면 정규화된 장치 좌표가 있어야한다. 이 좌표는 x,y,z값이 -1.0에서 1.0까지 변하는 작은 공간이다. 이 범위를 벗어난 좌표는 버려지거나 잘리면 화면에 표시되지 않는다. 아래에서 정규화된 장치 좌표에서 지정한 삼각형을 볼 수 있다. (z축 무시)
2D Normalized Device Coordinates as shown in a graph 일반적인 화면 좌표와 달리 위쪽 방향의 양의 y축 지점과 (0,0) 좌표는 왼쪽 상단 대신 그래프의 중앙에 있다.
결국 (변환된) 모든 좌표가 이 좌표 공간에서 끝나기를 원한다. 그렇지 않으면 보이지 않을 것이다.

 그러면 NDView 좌표가 glViewport에서 제공한 데이터를 사용해 뷰포트 변환을 통해 화면 공간 좌표로 변환된다.
결과 스크린 공간 좌표는 프래그먼트 쉐이더의 입력으로 프래그먼트로 변환된다.

 정점 데이터가 정의되면 이를 그래픽 파이프 라인의 첫 번째 프로세스인 정점 쉐이더에 입력으로 보내고 싶다.

이것은 우리가 정점 데이터를 저장하는 GPU에 메모리를 생성하고, OpenGL이 메모리를 해석하는 방법을 구성하며,


그래픽 카드로 데이터를 보내는 방법을 지정함으로써 수행된다. 그런 다음 정점 쉐이더는 메모리에서 말한만큼의


정점을 처리한다.



 우리는 GPU의 메모리에 많은 수의 정점을 저장할 수 있는 소위 정점 버퍼 객체(VBO)를 통해 이 메모리를 관리한다.


이러한 버퍼 객체를 사용하면 한 번에 정점 데이터를 보내지 않아도 대량의 데이터를 한꺼번에 그래픽 카드로 전송할


수 있다는 이점이 있다. CPU에서 그래픽 카드로 데이터를 보내는 것은 상대적으로 느리기 때문에 가능한 많은 데이터를

한 번에 보내려고 한다. 데이터가 그래픽 카드의 메모리에 저장되면 정점 쉐이더는 정점에 거의 즉각적으로


액세스 할 수 있어 매우 빠르다.



 정점 버퍼 객체는 OpenGL 튜토리얼에서 설명한 것처럼 OpenGL 객체의 첫 번째 발생객체이다. OpenGL의 모든 객체와


마찬가지로 이 버퍼는 해당 버퍼에 해당하는 고유한 ID를 가지므로 glGenBuffers 함수를 사용해 버퍼 ID를 가진 버퍼를


생성할 수 있다:


unsigned int VBO;
glGenBuffers(1, &VBO);  

 OpenGL은 여러 유형의 버퍼 객체를 가지고 있으며, 정점 버퍼 객체의 버퍼 유형은 GL_ARRAY_BUFFER이다.


OpenGL을 사용하면 버퍼 유형이 다른 한 여러 버퍼에 동시에 바인딩 할 수 있다. glBindBuffer 함수를 사용해


새로 생성된 버퍼를 GL_ARRAY_BUFFER 대상에 바인딩할 수 있다.


glBindBuffer(GL_ARRAY_BUFFER, VBO);  

 그 시점부터 우리가 (GL_ARRAY_BUFFER 타겟에서) 호출하는 모든 버퍼 호출은 현재 바인딩 된 버퍼 (VBO)를 구성하는데


사용된다. 그런 다음 이전에 정의된 정점 데이터를 버퍼 메모리에 복사하는 glBufferData 함수를 호출 할 수 있다.


glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

 glBufferData는 사용자가 정의한 데이터를 현재 바인드된 버퍼에 복사하도록 특별히 지정된 함수이다.


첫 번째 인수는 데이터를 복사할 버퍼 유형이다. 현재 GL_ARRAY_BUFFER 대상에 바인딩된 정점 버퍼 객체이다.


두 번째 인수는 버퍼에 전달할 데이터의 크기를 지정한다. 정점 데이터의 단순한 크기이면 충분하다.


세 번째 매개 변수는 보내려는 실제 데이터이다.


네 번째 매개 변수는 그래픽 카드에서 주어진 데이터를 관리하는 방법을 지정한다. 이것은 3가지 형태를 취할 수 있다:



   - GL_STATIC_DRAW : 데이터가 모두 변경되지 않거나 조금 변경되지않는다.


   - GL_DYNAMIC_DRAW : 데이터가 많이 바뀐다.


   - GL_STREAM_DRAW : 데이터가 그릴때마다 바뀐다.



삼각형의 위치 데이터는 변경되지 않고 모든 렌더 호출에 대해 동일하게 유지되므로 GL_STATIC_DRAW가 가장 적합하다.



 현재 우리는 VBO라는 정점 버퍼 객체에 의해 관리되는 그래픽 카드의 메모리 내에 정점 데이터를 저장했다.


다음으로 이 데이터를 실제로 처리하는 정점 및 프래그먼트 쉐이더를 생성하고자 한다.




Vertex shader


 정점 쉐이더는 우리와 같은 사람들이 프로그래밍 할 수 있는 쉐이더 중 하나이다. 현대 OpenGL은 렌더링을


하고 싶다면 적어도 정점 및 조각 쉐이더를 설정해야하므로 쉐이더를 간단히 소개하고


첫 번째 삼각형을 그리기 위한 두 개의 매우 간단한 쉐이더를 구성한다. 다음 튜토리얼에서는 쉐이더에 대해


자세히 설명한다.



 가장 먼저해야할 일은 쉐이더 언어 GLSL (OpenGL 쉐이딩언어)에 정점 쉐이더를 작성한 다음


이 쉐이더를 컴파일해 어플리케이션에서 사용할 수 있도록 하는 것이다. 다음은 GLSL에서 매우 기본적인


정점 쉐이더 소스코드이다:


#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

 보시다시피, GLSL은 C와 유사하다. 각 쉐이더는 버전 선언으로 시작한다. OpenGL 3.3 이후 버전의 GLSL은 OpenGL의 버전과 일치한다.


우리는 핵심 프로필 기능을 사용하고 있음을 명시적으로 언급한다.



 다음으로 in 키워드를 사용해 정점 쉐이더에 있는 모든 입력 정점 속성을 선언한다. 지금은 위치 데이터만 신경 써서 하나의


꼭지점 속성만 필요하다. GLSL에는 접미사 자릿수를 기반으로 1~4개의 부동 소수점을 포함하는 벡터 데이터 유형이 있다.


각 꼭지점에는 3D 좌표가 있기 때문에 이름이 aPos인 vec3 입력 변수를 만든다. 우리는 또한 레이아웃 (location=0)을 통해


입력 변수의 위치를 구체적으로 설정한다. 그러면 나중에 그 위치가 왜 필요한지 알게 될 것이다.

Vector
 그래픽 프로그래밍에서 우리는 벡터의 수학적 개념을 자주 사용한다. 어떤 공간에서의 위치/방향을 깔끔하게 표현하고, 유용한 수학적 특성을 갖기 때문이다. GLSL의 벡터는 최대 크기가 '4'이고 각 값은 vec.x, vec.y, vec.z, vec.w를 통해 각각 검색할 수 있다.
vec.w 구성 요소는 공간에서의 위치로 사용되지 않지만, 원근감 분할이라는 용어에 사용된다. 나중에 튜토리얼에서 벡터를 더 자세히 다룰 것이다.

 정점 쉐이더의 출력을 설정하기 위해, 배후의 vec4인 미리 정의된 gl_Position 변수에 위치 데이터를 할당해야한다.


main 함수의 끝에서 우리가 gl_Position을 설정한 것은 정점 쉐이더의 출력으로 사용된다.


우리의 입력은 크기가 3인 벡터이므로 크기 4의 벡터에 이 값을 캐스팅해야한다. vec3 갑승ㄹ vec4의 생성자 안에 삽입하고


w 구성 요소를 1.0f로 설정하면된다. (나중에 튜토리얼에서 이를 설명할 것이다)



 현재의 정점 쉐이더는 아마도 입력 데이터에 대해 아무런 처리도 하지 않고 간단히 쉐이더의 출력으로 전달했기


때문에 상상할 수 있는 가장 간단한 정점 쉐이더일 것이다. 실제 응용 프로그램에서 입력 데이터는 일반적으로


정규화된 장치 좌표에 있지 않으므로 먼저 입력 데이터를 OpenGL의 표시 영역 내에 있는 좌표로 변환해야한다.




Compiling a shaer


 우리는 C 문자열에 저장된 정점 쉐이더의 소스 코드를 작성했지만, OpenGL이 쉐이더를 사용하려면


런타임에 소스코드에서 동적으로 컴파일해야한다.



 먼저 ID로 참조되는 쉐이더 개체를 만드늑 서이 가장 먼저해야 할 일이다. 그래서 우리는 정점 쉐이더를


unsigned int로 저장하고, glCreateShader로 쉐이더를 생성한다.


unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

 우리는 glCreateShader에 인수로 생성하고자하는 쉐이더 유형을 제공한다. 그 이유는 우리가 GL_VERTEX_SHADER로 전달하는


정점 쉐이더를 생성하기 때문이다.



 다음으로 쉐이더 소스 코드를 쉐이더 객체에 첨부하고 쉐이더를 컴파일한다:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

 glShaderSource 함수는 쉐이더 객체를 취해 첫 번째 인수로 컴파일한다.


두 번째 인수는 소스 코드로 전달하는 문자열의 수를 지정한다. 단 하나뿐이다.


세 번째 인수는 정점 쉐이더의 실제 소스 코드이며 네 번째 인수는 NULL로 남겨둘 수 있다..

glCompileShader를 호출 한 후에 컴파일이 성공했는지 확인하고, 그렇지 않은 경우 어떤 오류가 발견되었는지를 확인해 문제를 해결할 수 있다. 컴파일 타임 오류 검사는 다음과 같이 수행된다.


int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

먼저 성공을 나타내는 정수와 오류 메시지(있는 경우)에 대한 저장 컨테이너를 정의한다.
그런 다음 glGetShaderiv를 사용해 컴파일이 성공했는지 확인한다. 컴파일이 실패하면 glGetShaderInfoLog를 사용해 오류 메시지를 검색하고 오류 메시지를 인쇄해야한다.


if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

 정점 쉐이더를 컴파일하는 동안 오류가 발견되지 않으면 컴파일된다.




Fragment shader


 프래그먼트 쉐이더는 삼각형 렌더링을 위해 만들려는 두 번째이자 마지막 쉐이더이다. 프래그먼트 쉐이더는


모두 픽셀의 컬러 출력을 계산한다. 일을 간단하게 하기 위해 프래그먼트 쉐이더는 항상 오렌지 색을 출력한다.

컴퓨터 그래픽의 색상은 R,G,B 및 Alpha 구성 요소의 4가지 값의 배열로 표시된다. OpenGL 또는 GLSL에서 색상을 정의할 때 각 구성 요소의 강도를 0.0~1.0 사이의 값으로 설정한다. 예를 들어 red를 1.0f로 설정하고, green을 1.0f로 설정하면 두 색상이 혼합되어 yellow가 표시된다. 16,000,000가지 이상의 색상을 생성할 수 있는 3가지 색상 구성 요소가 제공된다.


#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

 프래그먼트 쉐이더는 오직 하나의 출력 변수만을 필요로 하며, 이는 우리가 스스로 계산해야하는 최종 컬러 출력을


정의하는 크기 4의 벡터이다. 여기서 FragColor라는 이름을 가진 out 키워드를 사용해 출력 값을 선언할 수 있다.


다음으로 우리는 단순히 vec4를 알파 값 1.0을 가진 주황색 색상 출력에 할당한다.



 프래그먼트 쉐이더를 컴파일하는 프로세스는 정점 쉐이더와 비슷하지만 쉐이더 유형으로 GL_FRAGMENT_SHADER 상수를 사용한다:



unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

 두 쉐이더 모두 컴파일되었고 남은 것은 두 쉐이더 객체를 렌더링에 사용할 수 있는 쉐이더 프로그램에 연결하는 것이다.





Shader program


 쉐이더 프로그램 개체는 결합된 여러 쉐이더의 최종 링크 버전입니다. 최근에 컴파일된 쉐이더를 사용하려면


쉐이더 프로그램 개체에 링크한 다음 개체를 렌더링할 때 이 쉐이더 프로그램을 활성화해야한다.


활성화된 쉐이더 프로그램의 쉐이더는 렌더 호출을 할 때 사용된다.



 쉐이더를 프로그램에 연결할 때 각 쉐이더의 출력을 다음 쉐이더의 입력에 연결한다. 출력과 입력이 일치하지 않으면


연결 오류가 발생한다.


프로그램 객체를 만드는 것은 쉽다:


unsigned int shaderProgram;
shaderProgram = glCreateProgram();

 glCreateProgram 함수는 프로그램을 생성하고 새로 생성된 프로그램 객체에 ID 참조를 반환한다. 이제 이전에 컴파일된


쉐이더를 프로그램 객체에 첨부한 다음 glLinkProgram과 연결해야한다.


glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

 코드는 꽤 자명하다. 쉐이더를 프로그램에 첨부하고 glLinkProgram을 통해 링크한다.

쉐이더 컴파일과 마찬가지로 쉐이더 프로그램을 연결하지 못했는지 확인하고 해당 로그를 검색할 수 있다. 그러나 glGetShaderiv 및 glGetShaderInfoLog를 사용하는 대신 다음을 사용한다.


glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    ...
}

결과는 새로 생성된 프로그램 객체를 인수로 사용해 glUseProgram을 호출해 활성화 할 수 있는 프로그램 객체이다.


glUseProgram(shaderProgram);

 glUseProgram 이후의 모든 쉐이더 및 렌더링 호출은 이제 이 프로그램 개체(또는 쉐이더)를 사용한다.


일단 우리가 그들을 프로그램 객체에 링크시키면 쉐이더 객체를 삭제하는 것을 잊으면 안 된다. 


우리는 더 이상 그들이 필요없다.


glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);  

바로 지금 우리는 입력 정점 데이터를 GPU에 전송하고, GPU에게 정점 및 조각 쉐이더 내에서 정점 데이터를


처리하는 방법을 지시했습니다. OpenGL은 메모리에서 꼭지점 데이터를 해석하는 방법과 꼭지점 데이터를


꼭지점 쉐이더의 속성에 연결하는 방법을 아직 알지 못한다. 우리는 OpenGL에게 어떻게 해야하는지를 말해주는게 좋다.





Linking Vertex Attributes


 정점 쉐이더는 우리가 원하는 모든 입력을 vertex attribute의 형태로 지정할 수 있께 해주며,


정점 쉐이더에서 입력 데이터의 어느 부분이 어떤 정점 속성으로 가는지 수동으로 지정해야한다는 것을 의미한다.


즉, 렌더링하기 전에 OpenGL에서 정점 데이터를 해석하는 방법을 지정해야한다.


정점 버퍼 데이터의 형식은 다음과 같다:

Vertex attribte pointer setup of OpenGL VBO


    - 위치 데이터는 32bit(4byte) 부동 소수점 값으로 저장된다.


    - 각 위치는 3개의 값으로 구성된다.


    - 각 3개의 값 사이에 공백 (또는 다른 값)이 없습니다. 값은 배열에 단단히 묶여 있습니다.


    - 데이터의 첫번째 값은 버퍼의 시작 부분에 있습니다.


이 지식을 통해 우리는 OpenGL에게 glVertexAttribPointer를 사용해 정점 데이터(정점 속성 당)을 해석해야하는 방법을


알 수 있다:


glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  

glVertexAttribPointer 함수는 꽤 많은 매개 변수를 가지고 있으므로 주의 깊게 살펴 보자:


    - 첫 번째 매개 변수는 구성 할 정점 특성을 지정한다. layout(location = 0)으로 정점 쉐이더에서 위치 정점 속성의 위치를 저장했음을


      기억해라. 이것은 정점 속성의 위치를 0으로 설정하고, 데이터를 이 정점 속성에 전달하고자 하므로 0을 전달한다.


    - 다음 인수는 정점 속성의 크기를 지정한다. 정점 속성은 vec3이므로 3개의 값으로 구성된다.


    - 세 번째 인수는 GL_FLOAT인 데이터 유형을 지정한다. (GLSL의 vec *는 부동 소수점 값으로 구성된다)


    - 네 번째 인수는 데이터를 정규화할 것인지를 결정한다. 이것을 GL_TRUE로 설정하면 0과 1사이의 값을 가진 모든 데이터가


      해당 값에 매핑된다. 우리는 이것을 GL_FLASE로 남겨둔다.


    - 다섯 번째 인수는 스트라이드라고 불려, 연속한 정점 속성 세트의 간격을 알려준다. 다음 위치 데이터 세트는


      float 크기의 정확히 3배 위치하기 때문에 그 값을 스트라이드로 지정한다. 배열이 단단히 묶여있다는 것을 알기 때문에


      스트라이드를 0으로 지정해 OpenGL이 스트라이드를 결정할 수 있다. 더 많은 정점 애트리뷰트가 있을 때마다 각


      정점 애트리뷰트 사이의 간격을 신중하게 정의해야하지만, 나중에 더 많은 예제를 보게 될 것이다. 


    - 마지막 매개 변수는 void * 유형이므로 이상한 캐스트가 필요하다. 위치 데이터가 버퍼에서 시작되는 위치의 오프셋이다.


      위치 데이터가 데이터 배열의 시작에 있기 때문에 이 값은 0이다. 이 매개 변수에 대해서는 나중에 자세히 설명할 것이다.

각 정점 속성은 VBO에 의해 관리되는 메모리에서 데이터를 가져오며, 어느 VBO에서 데이터를 가져 오는가는 glVertexAttribPointer를 호출할 때 현재 GL_ARRAY_BUFFER에 바인드된 VBO에 의해 결정된다. glVertexAttribPointer를 호출하기 전에 이전에 정의된 VBO가 바인딩되었으므로 이제 정점 속성 0이 해당 정점 데이터와 연결된다.

 이제 OpenGL에서 꼭지점 데이터를 해석해야하는 방법을 지정했으므로 glEnableVertexAttribArray를 사용해 꼭짓점 속성을


비활성화해야 정점 속성 위치가 인수로 지정된다. 정점 속성은 기본적으로 비활성화되어있따. 이 시점부터 우리는


정점 버퍼 객체를 사용해 버퍼에 정점 데이터를 초기화하고, 정점 및 프래그먼트 쉐이더를 설정하고,


정점 데이터를 정점 쉐이더의 정점 속성에 연결하는 방법을 OpenGL에 알려주었다. OpenGL에서 객체를 그리는 것은


이제 다음과 같아 보일 것이다:


// 0. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  
// 2. use our shader program when we want to render an object
glUseProgram(shaderProgram);
// 3. now draw the object 
someOpenGLFunctionThatDrawsOurTriangle();   

 우리는 물체를 그릴 때마다 이 과정을 반복해야한다. 그렇게 많이 보일지 모르지만 5개 이상의 정점 속성과 100개의 다른 객체가 있는


경우(흔하지는 않음)를 상상해보아라. 적절한 버퍼 객체를 바인딩하고 각 객체에 대한 모든 정점속성을 신속하게 구성하는 것은


번거로운 과정이 된다. 이 모든 상태 구성을 객체에 저장하고 이 객체를 바인딩해 상태를 복원할 수 있는 방법이 있다면 어떨까?





Vertex Array Object


 정점 배열 객체 (VAO)는 정점 버퍼 객체처럼 바인딩 될 수 있으며 그 이후의 정점 애트리뷰트 호출은 VAO 내에 저장된다.


이것은 꼭짓점 속성 포인터를 구성할 때 오직 한 번만 호출해야하고 객체를 그릴 때마다 해당 VAO를 바인딩할 수 있다는


이점이 있다. 따라서 다른 VAO를 바인딩하는 것만큼 쉽게 다른 정점 데이터와 속성 구성간에 전환할 수 있다.


방금 설정한 모든 상태가 VAO 내부에 저장된다.

핵심 OpenGL은 정점 입력과 관련해 VAO를 사용하도록 요구한다. VAO를 바인드하지 못하면 OpenGL은 무언가를 그릴 가능성이 높다.

정점 배열 객체는 다음을 저장한다.


    - glEnableVertexAttribArray 또는 glDisableVertexAttribArray를 호출한다.


    - glVertexAttribPointer를 통한 정점 속성 설정


    - glVertexAttribPointer의 호출에 의해 정점 속성에 관련지을 수 있었던 정점 버퍼 오브젝트

Image of how a VAO (Vertex Array Object) operates and what it stores in OpenGL

VAO를 생성하는 프로세스는 VBO와 유사하다.


unsigned int VAO;
glGenVertexArrays(1, &VAO);  

VAO를 사용하려면 glBindVertexArray를 사용해 VAO를 바인드해야한다. 그 시점부터 해당 VBO 및 포인터를


bind/configure 한 다음 나중에 사용할 수 있도록 VAO 바인딩을 해제해야한다. 객체를 그리는 즉시, 객체를 그리기 전에


VAO를 선호하는 설정으로 바인딩하면 된다. 코드에서 이것은 다음과 같이 보일 것이다:


// ..:: Initialization code (done once (unless your object frequently changes)) :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  

  
[...]

// ..:: Drawing code (in render loop) :: ..
// 4. draw the object
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();   

일반적으로 그리려는 객체가 여러 개 있는 경우 먼저 모든 VAO(그리고 필요한 VBO 및 특성 포인터)를 생성 / 구성하고


나중에 사용할 수 있도록 저장합니다. 우리가 객체 중 하나를 그리는 순간, 우리는 해당 VAO를 가져와서


바인드 한 다음 객체를 그리고 VAO를 다시 바인딩 해제합니다.




The triangle we've all been waiting for


 선택한 객체를 그리려면 OpenGL은 현재 활성화된 쉐이더, 이전에 정의된 정점 속성 구성 및 VBO의 정점 데이터를


사용해 프리미티브를 그리는 glDrawArrays 함수를 제공한다.


glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

 glDrawArrays 함수는 첫 번째 인수로 우리가 그려야하는 OpenGL 원시 타입을 취한다. 우리가 처음에 삼각형을 그리기를 원했기


때문에 나는 거짓말하는 것을 싫어한다. 우리는 GL_TRIANGLES를 전달한다.


두 번째 인수는 그릴 정점 배열의 시작 인덱스를 지정한다. 마지막 인자는 그려야하는 정점의 수를 지정한다.


오류가 발생하면 코드를 컴파일하고 뒤로 작업해라. 응용 프로그램이 컴파일 되자마자 다음 결과가 표시된다:


An image of a basic triangle rendered in modern OpenGL






Element Buffer Objects


 정점을 렌더링 할 때 마지막으로 논의해야 할 점이 하나 있다. 요소 버퍼 객체는 EBO로 축약되었다.


요소 버퍼 객체가 어떻게 작동하는지 설명하기 위해 예제를 제시하는 것이 가장 좋다:


삼각형 대신 사각형을 그리기를 원한다고 가정하자. 우리는 두 개의 삼각형을 사용해 사각형을 그릴 수 있다.


그러면 다음과 같은 정점 집합이 생성된다:

	
float vertices[] = {
    // first triangle
     0.5f,  0.5f, 0.0f,  // top right
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f,  0.5f, 0.0f,  // top left 
    // second triangle
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f, -0.5f, 0.0f,  // bottom left
    -0.5f,  0.5f, 0.0f   // top left
}; 

보시다시피 지정된 일부 겹침이 있다. 아래 오른쪽과 왼쪽 위를 두 번 지정한다. 이것은 동일한 직사각형이 6 대신에


4개의 꼭지점으로 지정 될 수 있기 때문에 50%의 오버 헤드이다. 오버랩되는 큰 덩어리가 있는 곳에 삼각형이


1000개가 넘는 복잡한 모델을 가지고 있는 경우에 더 악화될 것이다.


더 나은 해결책은 고유한 꼭짓점만 저장한 다음 이 꼭지점을 그릴 순서를 정하는 것이다.


이 경우 직사각형에 4개의 꼭짓점을 저장한 다음 그리려면 어느 순서로 지정해야한다. 고맙게도, 요소 버퍼 객체는 이와 똑같이


작동한다. EBO는 OpenGL이 그릴 정점을 결정하기 위해 사용하는 인덱스를 저장하는 정점 버퍼 오브젝트와 마찬가지로 버퍼이다.


이러한 소위 인덱싱된 드로잉은 우리의 문제에 대한 해결책이다. 시작하려면 먼저 정점과 인덱스를 지정해 직사각형으로 그려야한다.


float vertices[] = {
     0.5f,  0.5f, 0.0f,  // top right
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f, -0.5f, 0.0f,  // bottom left
    -0.5f,  0.5f, 0.0f   // top left 
};
unsigned int indices[] = {  // note that we start from 0!
    0, 1, 3,   // first triangle
    1, 2, 3    // second triangle
};  

인덱스를 사용할때 6대신 4개의 꼭짓점만 필요하다는 것을 알 수 있다.


다음을 요소 버퍼 객체를 만들어야한다.

unsigned int EBO;
glGenBuffers(1, &EBO);

VBO와 마찬가지로 EBO를 바인드하고 glBufferData를 사용해 인덱스를 버퍼에 복사한다. 또한 VBO와 마찬가지로


바인딩과 바인드 해제 호출간에 이러한 호출을 배치하려고 한다.


이번에는 버퍼 유형으로 GL_ELEMENT_ARRAY_BUFFER를 지정한다.


glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 

이제 GL_ELEMENT_BUFFER를 버퍼 대상으로 지정한다. 마지막으로 할 일은 glDrawArrays 호출을


glDrawElements로 대채해 인덱스 버퍼에서 삼각형을 렌더링하려는 것을 나타낸다. glDrawElements를 사용할 때 현재 바인딩된


요소 버퍼 객체에 제공된 인덱스를 사용해 그릴 것이다:


glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

 첫 번째 인수는 glDrawArrays와 마찬가지로 우리가 드로잉할 모드를 지정한다. 두 번째 인수는 우리가 그릴 요소의


수 또는 개수이다. 우리는 총 6개의 꼭짓점을 그리기 위해 6개의 인덱스를 지정했다.


세 번째 인수는 GL_UNSIGNED_INT 유형의 인덱스 유형이다. 마지막 인수는 EBO에서 오프셋을 지정하거나


인덱스 배열에서 전달하지만 요소 버퍼 객체를 사용하지 않을 때 오프셋을 지정할 수 있다.


하지만 이 값을 0으로 두겠다.



 glDrawElements 함수는 현재 GL_ELEMENT_ARRAY_BUFFER 대상에 바인딩된 EBO의 인덱스를 사용한다.


다시 말하면 약간 번거로운 인덱스를 가진 객체를 렌더링 할 때마다 해당 EBO를 바인딩해야한다는 것을 의미한다.


정점 배열 객체는 요소 버퍼 객체 바인딩도 추적한다. VAO가 바인드되는 동안 현재 바인드된 요소 버퍼


오브젝트는 VAO의 요소 버퍼 오브젝트로 저장된다. 따라서 VAO에 바인드하면 자동으로 EBO가 바인드된다.

Image of VAO's structure / what it stores now also with EBO bindings.

VAO는 대상이 GL_ELEMENT_ARRAY_BUFFER인 경우 glBindBuffer 호출을 저장한다. 이것은 또한 바인드 해제 호출을 저장하므로 VAO 바인딩을 해제하기 전에 요소 배열 버퍼를 바인드 해제하지 않도록 주의해라. 그렇지 않으면 EBO가 구성되지 않는다.

결과 초기화 및 드로잉 코드는 다음과 같다:


// ..:: Initialization code :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a vertex buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. copy our index array in a element buffer for OpenGL to use
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  

[...]
  
// ..:: Drawing code (in render loop) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

프로그램을 실행하면 아래와 같은 이미지가 나타난다. 왼쪽 이미지는 익숙한 모양이고, 오른쪽 이미지는 와이어 프레임 모드로


그려진 사각형이다. 와이어 프레임 직사각형은 실제로 사각형이 두 개의 삼각형으로 구성되어 있음을 보여준다.


A rectangle drawn using indexed rendering in OpenGL


Wireframe mode
 삼각형을 와이어 프레임 모드로 그리려면, OpenGL이 glPolygonMode (GL_FRONT_AND_BACK, GL_LINE)를 통해 프리미티브를 그리는 방법을 구성 할 수 있다. 첫 번째 인수는 모든 삼각형의 앞과 뒤에 그것을 적용하고 두 번째 줄은 선으로 그려줄 것을 말한다. 후속 드로잉 호출은 glPolygonMode (GL_FRONT_AND_BACK, GL_FILL)를 사용해 기본값으로 다시 설정할 때까지 와이어 프레임 모드에서 삼각형을 렌더링한다.



** ** ** ** ** ** ** ** 


와 너무 길다. 너무너무너무너무 긴 챕터였다. 야유회 끝나고 가벼운 마음으로 공부하려고 했는데 졸지에 빡공을 해버렸네.


이 부분이 중요하고 어려운 부분이라고 하니 잘 배웠다고 생각하고 이제 쉬로가자.