위의 자료를 번역한 내용입니다.
-------------------------------------------------------------------------------------------------------------------------
Hello Triangle
OpenGL에서 모든 것은 3D 공간안에 있다. 그러나, 스크린과 윈도우는 픽셀들의 2D 배열이다. 그래서 OpenGL의 작품은 많은 부분은 모든 3D 좌표들을 너의 스크린에 맞게
2D 좌표들로 변환하는 것에 대한 것이다. 3D 좌표를 2D좌표로 변환하는 과정은 OpenGL의 그래픽스 파이프라인에 의해 관리되어 진다. 그래픽스 파이프라인은 두 가지 큰 부분으로 나눠질 수 있다. 첫 번째는 너의 3D 좌표를 2D 좌표로 변환한다. 그리고 두 번째 부분은 그 2D 좌표를 실제 채색된 pixel들로 바꾸는 것이다. 이 튜토리얼에서 우리는 간단하게 그래픽스 파이프라인에 대해 이야기하고, 어떻게 멋진 픽셀을 만들기 위한 우리의 이점에 사용할 수 있을지에 대해 이야기할 것이다.
Green Box
2D 좌표와 픽셀간에는 차이점이 있다. 2D 좌표는 한 점이 2D 공간에 있는 것의 매우 정확한 표기이다. 반면에 2D pixel은 너의 스크린과 window의 해상도에의해 제약된 그 점의 근사치이다.
그래픽스 파이프라인은 입력으로서 3D 좌표의 세트를 받고 그러한 것들을 스크린에 있는 채색된 2D pixel들로 변환한다. 그래픽스 파이프라인은 몇가지 단계로 나눠질 수 있다. 각 단계는 그것의 입력으로서 이전 단계의 output을 요구한다. 이러한 모든 스텝들은 매우 특수화되어 있다. (그것들은 한 가지 특정한 함수를 가진다.) 그리고 쉽게 동시에 실행되어 질 수 있다. 그들의 병행성 때문에, 오늘날의 대부분의 그래픽스 카드들은 파이프라인의 각 단계에 대해 GPU에서 작은 프로그램들을 구동함으로써 그래픽스 파이프라인 내에서 데이터를 빠르게 처리할 수천개의 작은 프로세싱 코어들을 가지고 있다. 이러한 작은 프로그램들은 shader라고 불린다.
몇몇의 이러한 쉐이더들은 개발자에 의해 설정되어질 수 있다. 그리고 그 개발자는 존재하는 기본 쉐이더를 대체하기 위해 우리 자신의 쉐이더를 쓰도록 하는 것을 허용한다. 이것은 우리가 파이프라인의 특정한 부분에 대해 좀 더 세분화된 제어를 할 수 있게 한다. 쉐이더들은 GPU 상에서 작동하기 때문에, 그것들은 우리에게 귀중한 CPU time을 절약할 수 있게 한다. 쉐이더들은 OpenGL Shading Language (GLSL)로 쓰여진다. 그리고 우리는 다음 튜토리얼에서 좀 더 자세히 알아 볼 것이다.
아래에서 당신은 그래픽스 파이프라인의 모든 단계의 추상화된 표현을 발견할 것이다. 파란색 section들은 우리가 우리의 쉐이더를 넣을 수 있는 부분을 나타낸다는 것을 주목해라.
Vertex(꼭지점, 정점) Data -> Vertex Shader -> Shape Assembly -> Geometry Shader -> Rasterization(격자화) -> Fragment Shader -> Tests and Blending
너가 보듯이, 그래픽스 파이프라인은 너의 vertex data를 완전히 rendering된 pixel로 변환하는 것의 각각 특정한 부분을 다루는 수 많은 부분들로 구성되어 있다. 우리는 명료하게 너에게 파이프라인이 어떻게 작동하는지에 대해 좋은 개관을 전달하기 위해 간단화된 방식으로 파이프라인의 각 부분을 설명할 것이다.
그래픽스 파이프라인에 대한 입력으로서, Vertex Data라고 불려지는 배열상의 삼각형을 형성해야만 하는 세개의 3D 좌표의 목록을 넘겨준다. 이 vertex data는 vertices(정점들)의 집합이다. 한 vertex는 기본적으로 3D 좌표당 data의 집합이다.이 vertex의 데이터는 우리가 표현하고자하는 어떤 데이터를 포함하는 vertex 특성들을 사용하여 표기되어진다. 간단하게 말해서, 각각의 vertex가 3D 위치와 몇 가지 color value로 구성되어 있다고 가정하자.
Green Box
OpenGL이 좌표와 color value들의 집합을 어떻게 구성할지를 알게하기 위해서, OpenGL은 너가 데이터로 형성하고자하는 render 타입을 hint하는 것을 요구한다. 우리는 데이터가 삼각형의 한 집합이거나 또는 아마도 단지 하나의 긴 선인 점들의 집합이 렌더링되기를 원하는 것인가? 그러한 hint들은 primitives라고 불려진다. 그리고 drawing command를 호출하는 중에 그 hint들이 OpenGL에게 주어진다. 이러한 hint들은 GL_POINTS, GL_TRIANGLES, 그리고 GL_LINE_STRIP이라는게 있다.
파이프라인의 첫 번째 부분은 입력으로 단일 vertex를 취하는 vertex shader이다. vertex shader의 주된 목적은 3D 좌표를 다른 3D 좌표로 변환하는 것이다. 그리고 그 버텍스 쉐이더는 우리가 vertex attribute에 몇가지 기본적인 처리를 할 수 있게 해준다.
primitive assembly 단계는 입력으로서 primitive를 형성하는 vertex shader로부터 모든 vertices들을 (또는 GL_POINTS가 선택된다면 vertex를) 취한다. 그리고 primitive assembly 단계는 주어진 primitive shape에서 모든 점들을 합친다; 이 경우에는 삼각형이다.
primitive assembly 단계의 output은 geometry shader로 넘어간다. geometry shader는 입력으로서 primitive를 형성하는 verticies들의 집합을 취하고, 새로운 (또는 다른) primitive들을 형성하기 위해 새로운 vertices들을 방출(만들)함으로써 다른 모양을 형성하는 능력을 가진다. 이 예제에서, 그것은 주어진 모양의 두 번째 삼각형을 만들어 낸다.
geometry shader의 output은 그러고나서 rasterization stage로 넘겨진다. 거기에서, 그 단계는 결과 primitive들을 최종 스크린에 일치하는 픽셀들로 사상시킨다. 그리고 fragment shader가 사용할 fragment들을 만들어 낸다. fragment shader가 구동되기 이전에, clipping 이 수행된다. clipping은 너의 시야 밖에 있는 모든 fragments들을 버린다. 그리고 이것은 성능을 증가시킨다.
Green Box
OpenGL에서의 한 fragment는 OpenGL이 단일의 pixel을 렌더링하는데 요구되어지는 모든 데이터이다.
fragment shader의 주된 목적은 픽셀의 최종 color를 계산하는 것이다. 이것은 보통 모든 고급 OpenGL 효과가 일어나는 단계이다. 보통 fragment shader는 최종 pixel color (빛, 그림자, 빛의 색 등과 같은)들을 계산하기 위해 사용할 수 있는 3D 장면에 대한 데이터를 포함한다.
모든 상응하는 color value들이 결정된 후에, 그러고나서 최종 객체는 우리가 alpha test와 blending stage라 부르는 한 단계 이상의 stage로 넘어간다.
이 스테이지는 상응하는 fragment의 depth(그리고 stencil) value를 확인한다. (우리는 나중에 더 알아볼 것이다.) 그리고 이스테이지는 결과 fragment가 다른 객체 앞에 있는지 뒤에 있는지를 알아보기 위해 그러한 값들을 사용한다. 그리고 그에 따라 버려져야만 한다. 그 단계는 또한 alpha 값들을 확인한다. (alpha 값은 객체의 투명도를 정의한다.) 그리고 객체를 그에따라 blend한다. 그래서 비록 pixel output 색이 fragment shader에서 계산 됐을지라도, 최종 pixel color는 다양한 삼각형을 렌더링할 때 여전히 전적으로 다를 수 있는 것이다.
너가 보듯이 그래픽스 파이프라인은 꽤 복잡하고 많은 설정가능한 부분을 포함한다. 그러나 대개 모든 경우에 대해, 우리는 오직 vertex와 fragment shader 부분에서 작업을 해야만 한다. geometry shader는 선택사항이고 보통 그것의 기본 쉐이더에게 맡겨진다.
현대 OpenGL에서 우리는 적어도 우리 자신의 vertex shader와 fragment shader를 정의할 것을 요구된다. (GPU에는 어떠한 기본 vertex/fragment shader들이 없다.) 이러한 이유 때문에, 당신의 첫 번째 삼각형을 렌더링할 수 있기 전에 많은 양의 지식이 요구되므로 현대 OpenGL을 배우기 시작하는 것은 종종 꽤 어려울 수 있다.
일단 너가 이 챕터의 마지막에서 너의 삼각형을 마침내 렌더링하기만 한다면, 너는 결국 그래픽스 프로그래밍에 대한 좀 더 많은 것을 알게될 것이다.
Vertex input
무언가를 그리는 것을 시작하기 위해서, 우리는 OpenGL에게 처음에 몇가지 input vertex data를 주어야만 한다. OpenGL은 3D 그래픽스 라이브러리이다. 그래서 우리가 OpenGL에서 명시하는 모든 좌표들은 3D 상에 있게 된다. (x, y, 그리고 z 좌표). OpenGL은 간단히 모든 너의 3D 좌표들을 너의 스크린 상의 2D pixel로 변형시키지 않는다.; OpenGL은 오직 모든 3개의 축(x, y 그리고 z)에 -1~1사이인 특정한 범위에 좌표들이 있을 때에만 3D 좌표들을 처리한다. normalized device coordinates(표준화 장치 좌표)라고 불려지는 이 범위 내의 모든 좌표들은 너의 스크린에 결국 보이게 될 것이다. (그리고 이 범위 밖의 모든 좌표들은 나타나지 않을 것이다.)
우리는 하나의 삼각형을 렌더링하기 원하기 때문에, 우리는 3D 좌표를 가진 각각의 vertex와 함께 총 3개의 정점들을 명시해야만한다.
우리는 그것들을 float 배열에 (OpenGL에서 보이는 영역인) 표준화 장치 좌표에서 그들을 정의한다.
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 삼각형을 렌더링한다. 이 방식으로 삼각형의 depth는 같게 된다. 그리고 이 같은 depth는 삼각형이 2D처럼 보이게 만든다.
Green Box
Normalized Device Coordinates(NDC, 정규화 장치 좌표)
일단 너의 정점 좌표들인 vertex shader에서 처리되기만 한다면, 그것들은 x,y 그리고 z값이 -1~1 사이의 값이 작은 공간인 정규화 장치 좌표에 있을 것이다.
이 범위 밖에 있는 어떤 좌표든 버려지거나 잘려질 것이고, 너의 화면에서 보이지 않을 것이다. 아래에서 너는 우리가 정규화 좌표 장치로 명시했었던 (z 축은 무시하고)
삼각형을 볼 수 있다.
평범한 화면 좌표와 다르게, 양수 y축은 위쪽 방향이고, 왼쪽 상단 대신에 (0, 0) 좌표는 그래프의 중앙에 있다. 결국 너는 모든 (변환된) 좌표들이 이 좌표 공간에서 마무리 되기를 원한다. 그렇지 않으면 보이지 않을 것이다.
너의 NDC 좌표들은 그러고나서 너가 glViewport와 함께 제공했던 데이터를 사용하는 viewport transform을 통해 screen-space coordinate로 변환될 것이다.
결과로 나타난 화면-공간 좌표들은 그러고나서 너의 fragment shader에 입력으로서 fragments들로 변환된다.
vertex data가 정의된 채로, 우리는 그것을 그래픽스 파이프라인의 첫 번째 과정에 대한 입력으로서 그것을 보내고 싶다. -> vertex shader. 이것은 우리가 vertex data를 저장하고 어떻게 OpenGL이 메모리를 해석하고 그리고 어떻게 데이터를 그래픽 카드에 보낼지를 명시하는 GPU에서 메모리를 만들어서 해결된다. vertex shader는 그러고나서 그것의 메모리로부터 그래픽 카드에 말하는 만큼의 많은 정점들을 처리한다.
우리는 소위 GPU의 메모리에서 많은 정점들을 저장할 수 있는 vertex buffer objects(VBO, 정점 버퍼 객체)라고 불리는 것을 통해서 이 메모리를 관리한다. 그러한 버퍼 객체들을 사용하는것의 이점은 우리가 한 번에 한 vertex data를 보내지 않고 큰 data batch들을 모두 즉시 그래픽 카드로 보낼 수 있다는 것이다. CPU로부터 그래픽스카드로 data를 보내는 것은 상대적으로 느리다, 그래서 우리가 할 수 있는 곳에서 어디든지, 우리는 한 번에 가능한한 많은 데이터를 보내려고 한다. 일단 그 데이터가 그래픽 카드의 메모리에 있기만 한다면, vertex shader는 매우 극한으로 빨라지면서 정점들에 거의 끊임없는 접근을 할 수 있게 된다.
VBO는 우리가 OpenGL 튜토리얼에 대해 이야기할 때, OpenGL 객체의 첫 번째로 발생하는 객체이다. OpenGL의 어떤 다른 객체와 같이, 이 버퍼는 그 버퍼와 상응하는 고유의 ID를 가지고 있다. 그래서 우리는 glGenBuffers 함수를 사용하여 buffer ID를 가진 한 buffer를 발생시킬 수 있다.
unsigned int VBO;
glGenBuffers(1, &VBO);
glGenBuffer는 하나 또는 다수의 버퍼 객체를 생성한다. 버퍼 객체들은 scene들 뒤에서 인스턴스화 된다. 그리고 참조 ID는 그러한 버퍼 객체들에 접근하기 위해
사용자에게 반환된다. 반환된 ID의 type은 GLuint 이다.
glGenBuffers(GLsizei size, GLUint* buffers)의 인자들은 다음과 같다.
size : 얼마나 많은 발생시키고 싶은지를 정의한다. glGenBuffers는 많은 버퍼 ID를 타겟 buffers array에 반환한다.
buffer는 GLuint 배열이다. 그 배열에서 glGenBuffer들은 그것의 결과 버퍼 reference ID들을 저장한다.
OpenGL은 많은 종류의 버퍼 객체를 가지고 있고, 한 정점 버퍼 객체의 버퍼 유형은 GL_ARRAY_BUFFER이다. OpenGL은 그들이 다른 buffer type을 가지고있는 한 우리가 몇가지 버퍼들을 한 번에 bind하는 것을 허용한다. 우리는 glBindBuffer 함수로 새롭게 만들어진 버퍼들을 GL_ARRAY_BUFFER target으로 bind할 수 있다.
glBindBuffer(GL_ARRAY,_BUFFER, VBO);
glBindBuffer는 버퍼 객체를 현재 buffer type target에 bind한다. 오직 단일의 buffer만 각각의 buffer type에 대해 bind 되어질 수 있다. 버퍼로서 0을 bind하는 것은 현재 bound buffer를 NULL과 같은 상태로 다시 설정하게 한다.
glBindBuffer(GLenum target, GLuint buffer)의 인자는 다음과 같다.
target: 은 타겟 버퍼 객체를 명시한다. 가장 흔한 것은 GL_ARRAY_BUFFER와 GL_ELEMENT_ARRAY_BUFFER가 있다.
buffer : 너가 bind하고자하는 buffer의 버퍼 객체 reference ID
그 시점에서 우리가 만든 버퍼 호출은(GL_ARRAY_BUFFER 타겟에서) VBO인 현재 바인딩 된 버퍼를 설정하는데 사용합니다. 그러고나서 우리는 이전에 정의된 vertext data를 버퍼의 메모리에 복사시키는 glBufferData함수에 대해 호출을 할 수 있다.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData는 메모리를 할당하고 현재 bind된 버퍼 객체의 초기화된 메모리내에 데이터를 저장한다.버퍼 객체의 버퍼를 추가하거나 수정할 많은 함수들 중 하나.
glBufferData(GLenum mode, GLsizeiptr size, const GLvoid* data, GLenum usage)의 인자는 다음과 같다.
mode : 는 타겟 버퍼 객체를 명시한다. 가장 흔한 것은 GL_ARRAY_BUFFER와 GL_ELEMENT_ARRAY_BUFFER이다.
size : 는 버퍼 객체의 새 데이터의 byte 사이즈를 명시한다.
data : 버퍼에 복사되어질 데이터에 대한 포인터를 명시한다. 어떠한 데이터도 복사되지 않도록 할려면 NULL을 쓴다. (할당된 메모리는 비어있게 된다.)
USAGE : 데이터의 예상되는 사용패턴을 명시한다. 흔한 것으로 GL_STATIC_DRAW, GL_DYNAMIC_DRAW 그리고 GL_STREAM_DRAW가 있다.
glBufferData는 사용자 정의 data를 현재 bind된 버퍼에 복사하기위해 특정하게 타겟된 함수이다. 그것의 첫 번째 인자는 우리가 data를 복사하고자 하는 buffer의 type이다. vertex buffer 객체는 현재 GL_ARRAY_BUFFER 타겟으로 bind되어 있다. 두 번째 인자는 우리가 버퍼에 넘기고자하는 data size를(byte 단위) 명시한다. vertex data에 대한 간단한 sizeof 면 충분하다. 세 번째 인자는 우리가 보내고자 하는 실제 데이터이다.
4번째 인자는 어떻게 그래픽카드가 주어진 데이터를 관리하게 할지 명시한다. 이것은 3가지 형태를 가지고 있는데,
GL_STATIC_DRAW : 데이터는 거의 전혀 바뀌지 않을 것이다.
GL_DYNAMIC_DRAW : 데이터는 많이 바뀔 가능성이 높다.
GL_STREAM_DRAW : 데이터가 그것이 그려질 때마다 바뀔 것이다.
삼각형의 위치 데이터는 바뀌지 않을 것이고, 모든 렌더링 호출에 대해서 같게 유지될 것이다. 그래서 그것의 사용 type은 GL_STATIC_DRAW이다. 예를들어, 만약 어떤이가 자주 바뀔것 같은 데이터를 가진 버퍼를 가지고 있따면, GL_DYNAMIC_DRAW 또는 GL_STREAM_DRAW의 사용 type이 그래픽카드가 더욱 빠른 쓰기를 고려하는 메모리에 데이터를 위치시키는것을 보장한다.
지금, 우리는 vertex data를 VBO라고 이름지은 vertex 버퍼 객체에 의해 관리되어지는 그래픽카드에 있는 메모리에 저장했다. 다음으로 우리는 이 데이터를 실제로 처리하는 vertex와 fragment shader를 만들길 원한다. 그것들을 만드는걸 시작해보자.
Vertex shader
vertex shader는 우리같은 사람들이 프로그래밍할 수있는 쉐이더중에 하나이다. 현대 OpenGL은 적어도 만약 우리가 몇 가지를 렌더링하길 원한다면 우리가 vertex와 fragment shader를 설정하는 것을 요구한다. 그래서 우리는 명료하게 쉐이더를 도입하고, 우리의 첫 번째 삼각형을 그리기 위한 두 가지 간단한 쉐이더들을 설정할 것이다. 다음 튜토리얼에서 우리는 쉐이더에대해 좀 더 자세하게 이야기할 것이다.
우리가 해야할 첫 번째 것은 shader 언어인 GLSL (OpenGL Shading Language)로 vertex shader를 쓰고나서 이 쉐이더를 compile을 하는 것이다. 그래서 우리는 그것을 우리의 프로그램에서 사용할 수 있다. 아래에서 너는 GLSL로 쓰여진 기본 vertex shader의 소스코드를 볼 수 있을 것이다.
#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의 버전과 일치한다. (예를들어 GLSL 버전 420은 OpenGL 버전 4.2에 해당) 우리는 또한 core profile 기능들을 사용하고 있다고 명백히 언급한다.
다음으로 우리는 in 키워드로 vertex shader에 있는 모든 입력 vertex attribute들을 선언한다. 지금 당장은 우리는 오직 위치 데이터만 신경쓴다. 그래서 우리는 오직 단일의 vertex attribute만을 필요로한다. GLSL은 그것의 후위표기법 숫자를 기반으로하는 1~4개의 float들을 포함하는 벡터 data type를 가진다. 각각의 vertex가 3D 좌표를 가지고 있기 때문에, 우리는 aPOS라는 이름을 가진 vec3 input 변수를 만든다. 우리는 또한 특정하게 layout (location = 0)을 통해 입력 변수의 위치를 설정한다. 그리고 너는 나중에 왜 우리가 그 위치를 필요한지를 알게 될 것이다.
Green Box
Vector
그래픽스 프로그래밍에서, 우리는 꽤 종종 vector라는 수학적 개념을 사용한다. 그것은 깔끔하게 어떤 공간에서의 위치나 방향을 표현하고 유용한 수학적 특징을 가지기 때문이다. GLSL에서의 vector는 4가 최대 크기이고 그것의 값들의 각각은 vec.x, vec.y, vec.z, 그리고 vec.w로 개별적으로 불러올 수 있다. 그리고 거기에서 그들 각각은 공간에서의 좌표를 나타낸다. vec.w 요소는 공간에서 위치로서 사용되지 않는 다는 것을 주목해라. (우리는 4D가 아닌 3D를 다루고 있다.) 그러나 그것은 가끔씩 perspective division으로 불려지는 것을 위한 것이다. 우리는 나중에 더욱 깊게 vector에 대해 이야기할 것이다.
vertex shader의 출력을 설정하기 위해, 우리는 미리 정의된 scene 뒤에 있는 vec4인 gl_Position 변수에 위치 data를 할당해야만 한다. main 함수의 끝에서, 우리가 gl_Position에 무엇을 설정하든 그것이 vertex shader의 output으롯 사용될 것이다. 우리의 입력은 size가 3인 벡터이기 때문에, 우리는 이것을 크기가 4인 vector에 보내야만 한다. 우리는 이것을 vec3 value들을 vec4의 생성자에 넣음으로써 할 수 있다. 그리고 그것의 w 요소를 1.0f로 설정한다. (우리는 나중에 왜 이러는지를 설명할 것이다.)
현재 vertex shader는 아마도 가장 간단한 우리가 상상할 수 있는 vertex shader이다. 왜냐하면 우리는 input data에 있는 것을 아무것도 처리하고 있지 ㅇ낳고 그냥 단순히 그것을 shader의 output으로 보내고 있기 떄문이다. 실제 프로그램에서, 입력 데이터는 보통 이미 정규화 장치 좌표에 있지 않는다. 그래서 우리는 처음에 그 입력 데이터를 OpenGL의 가시 영역에 맞아 떨어지는 좌표로 변환해야만 한다.
Compiling a shader
우리는 vertex shader를 위한 (C string으로 저장된) 소스코드를 썼다. 그러나 OpenGL이 그 shader를 사용하기 위해서, 그것을 동적으로 소스코드로부터 런타임 시간에 컴파일을 해야만한다.
우리가 해야할 첫 번째 것은 ID로 다시 참조되는 shader object를 만드는 것이다. 그래서 우리는 그 vertex shader를 GLuint로서 저장하고 glCreateShader로 쉐이더를 만든다.
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
우리는 우리가 만들고자 하는 shader의 type을 glCreateShader에게 인자로서 제공한다. 우리는 vertex shader를 만들고 있기 때문에, 우리는 GL_VERTEX_SHADER를 넘긴다.
glCreateShader는 scene뒤에서 빈 shader object를 생성하고 unsigned reference ID 를 이 쉐이더 객체에게 나중의 사용을 위해 반환한다. 그 함수는 shader object를 만들면서 에러가 발생했다면 0을 반환한다.
glCreateShader(GLenum shaderType)의 인자는 다음과 같다.
shaderType : 은 만들어질 shader type을 명시한다. 다음의 값 중 하나를 가질 수 있다. : GL_COMPUTE_SHADER, GL_VERTEX_SHADER, GL_TESS_CONTRO_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER or GL_FRAGMENT_SHADER
다음으로, 우리는 Shader 소스코드를 쉐이더 객체에게 넘기고 그 쉐이더를 컴파일 한다.
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource 함수는 컴파일 할 shader object를 그것의 첫 번째 인자로서 가진다. 두 번째인자는 source code로서 우리가 얼마나 많은 string들을 보내고있는지를 명시한다. 그리고 현재 여기서는 오직 1이다. 세 번째 인자는 vertex shader의 실제 소스 코드이다. 그리고 우리는 4번째 파라미터를 NULL로 둔다.
glShaderSource는 주어진 shader 객체의 소스코드를 대체한다. shader가 그리고나서 컴파일 될 때마다, 그것은 주어진 소스를 사용하여 컴파일 한다.
glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length)의 인자는 다음과 같다.
shader : 는 source code가 대체될 shader object를 명시한다.
count : 는 string 배열에서 원소들의 개수를 명시한다.
string : 는 shader로 불려질 source code를 포함하는 string들에 대한 포인터 배열을 명시한다.
length : string 길이의 배열을 명시한다.
Green Box
너는 아마도 glCompileShader를 호출하고 난후 컴파일이 성공했는지를 확인하고 싶어한다. 그렇지 않은 경우에 무슨 에러가 일어났는지를 발견하여 너는 그것들을 해결할 수 있다. compile-time error들을 확인하는 것은 다음과 같이 해결된다.
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
처음에 우리는 성공을 알려주기 위해 정수를 정의한다. 그리고 에러 메세지를 위한 저장 container도 정의한다. 그러고나서 우리는 컴파일이 성공적인지를 glGetShaderiv로 확인한다. 만약 컴파일 실패했다면, 우리는 에러 메세지를 glGetShaderInfoLog로 불러와야만 하고 에러 메세지를 출력해야 한다.
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILE\n" << infoLog << std::endl;
}
vertex shader를 컴파일 하던 도중 어떠한 에러들도 탐지되지 않았다면, 그것은 지금 컴파일 된다.
glComplieShader는 shader object의 부착된 source와함께 주어진 shader object를 컴파일 한다. 컴파일이 실패했는지를 확인하려면, glGetShaderiv와 glGetShaderInfoLog 함수를 사용하여 로그를 불러와라
glCompileShader(GLuint shader)의 인자는 다음과 같다.
shader : 는 컴파일될 쉐이더 객체를 명시한다.
glGetShaderiv는 개발자가 객체 인자에 대한 정보에 대해 쉐이더를 query하도록 한다.
glGetShaderiv(GLuint shader, GLenum pname, GLint * params)의 인자는 다음과 같다.
shader : 질의될 shader object를 명시한다.
pname : 객체 인자를 명시한다. 받아들여지는 symbolic name들은 GL_SHADER_TYPE, GL_DELETE_STATUS, GL_COMPILE_STATUS, GL_INFO_LOG_LENGTH, GL_SHADER_SOURCE_LENGTH이다.
params : 주어진 GLint 배열에서의 요구된 객체 인자를 반환한다.
glGetShaderInfoLog는 shader object에 대한 정보 log를 반환한다. 정보 log는 쉐이더의 컴파일과 관련된 정보들을 포함한다. 쉐이더가 컴파일에 실패를 어디에서 하든, 컴파일 타임 에러를 위해 쉐이더의 info log를 확인하는것이 충고되어진다.
glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei * length, GLchar *infoLog)의 인자는 다음과 같다.
shader : 정보 로그가 질의되어질 shader object를 명시한다.
maxLength : 반환된 정보 log를 저장하기위한 문자 버퍼의 크기를 명시한다.
length : null terminator를 제외하고 infoLog에서 반환된 문자열들의 길이를 반환한다.
infoLog : 정보 로그를 반환하기위해 사용되어질 문자들의 배열을 명시한다.
Fragment shader
fragment shader는 우리가 삼각형을 렌더링하기위해 만들 두 번째이자 마지막 shader이다. fragment shader는 너의 pixel들의 color output을 계산하는 것에 관한 모든 것이다. 간단하게 하기위해서, fragment shader는 항상 오렌지색깔을 output하게 할 것이다.
Green Box
컴퓨터 그래픽스에서 color들은 4가지 값의 배열로 표기된다. Red, Green, Blue and Alpha(Opacity) 요소들은 흔히 RGBA로 축약된다. OpenGL 또는 GLSL에서 색을 정의할 떄, 우리는 각 요소들의 strength를 0.0 ~ 1.0 사이의 값으로 정한다. 예를들어 만약 우리가 red를 1.0f로, green을 1.0f 로 설정한다면, 우리는 두 색깔의 혼합을 얻고 노란색을 얻게 될 것이다. 이러한 3가지 요소들을 고려하면, 우리는 1600만 가지의 다른 색을 만들어낼 수 있다.
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
fragment shader는 오직 한 개의 output 변수만을 요구한다. 그리고 그것은 우리가 우리스스로 계산해야하는 최종 color output을 정의하는 크기가 4인 vector이다. 우리는 out 키워드로 output value들을 선언할 수 있다. 우리는 여기에서 바로 FragColor라고 이름지었다. 다음에 우리는 간단히 vec4를 color output에 오렌지 색깔로서 할당한다. alpha value는 1.0으로 완전히 불투명하다.
fragment shader를 컴파일하는 프로세스는 vertex shader와 비슷하다. 이번에는 비록 우리가 GL_FRAGMENT_SHADER를 shader type 상수로 쓸지라도.
unsinged int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
두 쉐이더들은 지금 컴파일 되고, 해야할 유일할 것은 두 shader 객체들을 우리가 렌더링을 위해 사용할 shader program에 연결 시키는 것이다.
Shader program
shader 프로그램 객체는 혼합된 다양한 shader들의 최종적으로 연결된 버전이다. 최근에 컴파일된 shader들을 사용하기 위해서, 우리는 그것들을 shader program object에 link 시켜야만 하고, 그러고나서 객체를 렌더링할 때, 이 shader program을 활성화해야만 한다. 그 활성화된 shader program들의 shader들은 우리가 render 호룰을 할 때 사용되어 질 것이다.
shader들을 한 프로그램으로 link시킬 때, 그 프로그램은 각각의 shader의 output을 다음 shader의 input으로 연결시킨다. 이것은 또한 만약 너의 output과 input이 부합하지 않는다면 너가 linking error들을 얻는 곳이다.
프로그램 객체를 만드는 것은 쉽다.
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glCreateProgram 함수는 프로그램을 만들고 새롭게 만들어진 프로그램 객체에 대한 ID 참조를 반환한다. 지금 우리는 이전에 컴파일된 쉐이더들을 프로그램 객체에 붙일 필요가있고 그러고나서 그것들을 glLinkProgram으로 연결할 필요가 있다.
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glCreateProgram은 scene뒤에서 program object를 만들고 나중 사용을 위해 unsigend reference ID를 이 프로그램 객체에게 반환한다. 그 함수는 음수의 ID를 반환하지 않고, 프로그램을 만들면서 에러가 발생했다면, 0을 반환한다.
glCreateProgram은 인자가 없다.
glLinkProgram은 하나의 최종 shader program object에 부착되어있는 모든 shader들을 연결시킨다. 연결하는 단계 동안에, 각각의 output은 shader들의 각 입력과 부합된다. 어떤것이 옳게 되지 않을 때 마다, linking은 실패한다.
glLinkProgram(GLuint program)의 인자는 다음과 같다.
program : 연결되어질 program object를 명시한다.
코드는 꽤 스스로 설명하는 것처럼 보일 것이다. 우리는 shader들을 program에 부착하고, 그것들을 glLinkProgram으로 연결한다.
Green Box
shader 컴파일과 같이 우리는 또한 shader program을 연결하는 것이 실패하는지를 확인할 수 있고, 그에 상응하는 로그를 가져올 수 있다. 그러나 glGetShaderiv와 glGetShaderInfoLog를 사용하는 대신에, 우리는 지금 이것을 사용한다.
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
결과는 인자로 새롭게 만들어진 program object를 취하는 glUseProgram을 호출하여 우리가 활성화 할 수 있는 program object이다.
glUseProgram(shaderProgram);
glUseProgram 이후의 모든 shader와 rendering 호출은 지금 이 프로그램 객체를 사용할 것이다. (러니까 쉐이더들)
우리가 그것들을 program object에 연결하고나서 shader object들을 지우는 것을 잊지말아라. 우리는 더 이상 그것들을 필요로하지 않는다.
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
지금 당장 우리는 input vertex data를 GPU에게 보냈다. 그리고 GPU에게 어떻게 그 vertex와 fragment shader에서 vertex data를 처리해야하는지를 알려주었다. 우리는 거의 다 왔지만, 아직은 아니다. OpenGL은 아직 어떻게 메모리에 있는 vertex data를 해석할지 모르고, 어떻게 vertex data와 vertex shader들의 특성들을 연결시킬지를 모른다. 우리는 친절할 것이고, OpenGL에게 어떻게 그것을 하는지 알려줄 것이다.
Linking Vertex Attributes
vertex shader는 우리가 vertex attribute들의 형태에서 우리가 원하는 어떤 input을 명시하도록 해준다. 그리고 vertex shader가 큰 유연성을 고려하는동안, 우리가 수동적으로 우리의 input data의 무슨 부분이 vertex shader의 어떤 특성으로 들어갈지를 명시해야만 한다는 것을 의미한다. 이것은 우리가 어떻게 OpenGL이 렌더링하기 전에 vertex data를 해석해야만 하는지를 명시해야만 하는 것을 의미한다.
우리의 vertex 버퍼 데이터는 다음과 같이 형식화되어 있다.
- 위치 데이터는 32bit (4 byte) 부동 소수점 값으로 저장되어진다.
- 각각의 위치는 그러한 값들의 3개로 구성되어 있다.
- 각각의 3 값들의 세트 사이에는 어떠한 공간도 없다. 그 값들은 배열에서 꽉꽉 들어있다.
- 데이터에서 첫 번째 값은 버퍼의 시작이다.
이러한 지식을 가지고, 우리는 OpenGL에게 그것이 어떻게 vertex data를 해석할지를 (vertex attribute마다) glVertexAttribPointer를 가지고 말할 수 있다.
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer 함수는 꽤 인자를 가지고 있다. 세심하게 그것들을 살펴보자.
- 첫 번째 인자는 우리가 설정하고 싶은 것이 어떤 vertex attribute인지를 명시한다. 기억해라. 우리는 vertex shader에서 layout (location = 0)으로 position vertex attribute의 위치를 명시했다. 이것은 vertex attribute의 위치를 0으로 설정한다. 우리는 데이터를 이 vertex attribute에 넘기고 싶기 때문에, 우리는 0이라고 써놓는다.
- 다음 인자는 vertex attribute의 크기를 명시한다. vertex attribute는 vec3이다. 그래서 그것은 3개의 값들로 구성된다.
- 세 번째 인자는 데이터의 type을 명시하는데 그것은 GL_FLOAT이다. (GLSL에서 vec*은 부동소수점 값으로 구성된다.)
- 다음 인자는 우리가 데이터를 표준화하기를 원하는지 명시한다. 만약 우리가 이것은 GL_TRUE로 설정한다면, 0에서 ( signed data에 대해서는 -1) 1 사이의 값을 가진 모든 데이터는 해당 값에 매핑이 될 것이다. 우리는 이것은 GL_FALSE로 둔다.
- 다섯번째 인자는 stride로 알려져있다. 그리고 이것은 우리에게 연속적인 vertex attribute set들 사이의 공간을 말해준다. 위치 데이터의 다음 set이 정확히 float 크기의 3배 다음으로 위치해 있기 때문에, 우리는 그 값을 stride로서 명시한다. 주목해라. 우리는 배열들이 꽉 차있다는 것을 알기 때문에 (다음 vertex attribute value 사이에 공간은 없다.) 우리는 OpenGL이 STRIDE를 결정하도록 하기위해(이것은 값이 꽉 차있을 때에만 유효하다) 또한 stride를 0으로 명시해야 한다.
- 마지막 인자는 void* type이다. 그래ㅓ 따라서 이상한 cast를 요구한다. 이것은 위치 데이터가 버퍼에서 어디에서 시작하는지에 대한 offset이다. 위치데이터는 data array의 시작이기 때문에, 이 값은 그냥 0이다. 우리는 이 인자에대해서 나중에 자세히 알아볼 것이다.
glVertexAttribPointer 함수는 Draw 호출이 될 때마다 어떻게 OpenGL이 vertex buffer data를 해석해야할지를 명시한다. 그 명시된 해석은 몇 가지 일을 줄여주는 현재 bound된 vertex array object에 저장된다.
glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalizaed, GLsizei stride, const GLvoid* pointer)의 인자는 다음과 같다.
index : vertex attribute의 인덱스를 명시한다.
size: vertex attribute마다 요소들의 개수를 명시한다. 1, 2, 3 ,4 중에 하나가 될 것이다.
type : array에서 각각 요소의 데이터 type을 명시한다.
normalized : 데이터가 정규화되어야 하는지를 명시한다. ( signed 값에 대해서는 -1~1 범위에 물려있고, unsigned 값은 0 ~ 1에 대해 되어있다.)
stride : 연속적인 vertex attribute 사이의 byte offset을 명시한다. 만약 stride가 0이라면, 전체 vertex attribute들이 배열에 꽉 차있다는 것으로 이해된다.
pointer : 배열에서 첫 번째 vertex attribute의 첫 번째 구성요소의 offset을 명시한다.
Green Box
각각의 vertex attribute는 VBO에 의해 관리되는 메모리로부터 데이터를 가져온다. 그리고 그 vertex attribute가 어떤 VBO로부터 데이터를 가져올지는 (어떤 것은 다양한 VBO를 가질 수도 있다.) glVertexAttribPointer를 호출할 때 GL_ARRAY_BUFFER에 현재 bound된 VBO에 의해 결정된다. 이전에 정의된 VBO는 glVertexAttribPointer를 호출하기전에 bound되어 있기 때문에, vertex attribute 0은 지금 그것의 vertex data와 연관이 있다.
우리는 OpenGL이 어떻게 vertex data를 해석해야하는지를 명시했기 때문에, 우리는 또한 vertex attribute 위치를 인자로서 주는 glEnableVertexAttribArray로 vertex attribute를 가능하게 만들어야만 한다. vertex attribute들은 기본적으로 사용할 수 없다. 그 시점에서부터, 우리는 모든 것을 준비한다 : 우리는 vertex buffer object를 사용하여 버퍼에서의 vertex data를 초기화 했고, vertex와 fragment shader를 설정했고 OpenGL에게 어떻게 vertex data를 vertex shader의 vertex attribute와 연결시킬지를 OpenGL에게 말했다. OpenGL에서 객체를 그리는 것은 지금 이것과 같아 보일 것이다.
// 0. copy out 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 attribute 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개 이상의 vertex attribute를 가지고 있고 다른 객체들 100 개를 가지고 있다는 것을 상상해보아라. (흔한 것이다.) 적절한 buffer object들을 bind하고 모든 vertex attribute들을 그러한 각각의 객체들에 대해 빠르게 설정하는 것은
번거로운 과정이 된다. 우리가 모든 이러한 상태 설정을 한 객체에 저장하고 간단하게 이 객체들을 그것의 상태들을 복구하는데 bind하는 방법들이 있다면 어떨까?
Vertex Array Object
vertex array object(VAO로 알려진)는 VBO 처럼 bound될 수 있다. 그리고 그 시점에서부터의 차후의 vertex attribute 호출들은 VAO에 저장되어질 것이다. 이것은 vertex attribute pointer들을 설정할 때, 오직 너가 그러한 호출들을 한 번만 해야한 다는 장점을 가지고 있다. 그리고 객체를 그릴 때마다, 우리는 일치하는 VAO만 bind할 수 있다.
이것은 다른 vertext data와 attribute 설정 사이의 switching을 다른 VAO를 bind하는 것만큼 쉽게 만든다. 우리가 그냥 설정하는 모든 상태는 VAO안에 저장되어진다.
Red Box
Core OpenGL은 우리가 VAO를 사용하는 것을 요구한다. 그래서 그것은 우리의 vertex input들로 무엇을 해야하는지를 안다. 만약 우리가 VAO를 bind하는데 실패한다면, OpenGL은 어떤 것을 그리는 것을 거부할 것이다.
Vertex array object는 다음을 저장한다.
- glEnableVertexAttribArray 또는 glDisableVertexAttribArray에 대한 호출
- glVertexAttribPointer를 통한 vertex attribute 설정
- glVertexAttribPointer에 대한 호출로 vertex attribute들과 연관된 Vertex buffer object들
VAO를 생성시키는 프로세스는 VBO의 프로세스와 유사하게 보인다.
unsigned int VAO;
glGenVertexArrays(1, &VAO);
VAO를 사용하기위해, 너가 해야할 것은 glBindVertexArray를 사용하여 VAO를 bind해야한다. 그 시점으로부터 우리는 그에 상응하는 VBO와 attribute pointer들을 bind하고 설정해야한다. 그러고나서 나중에 사용하기위해 VAO를 unbind한다. 우리가 객체를 그리자마자, 객체를 그리기 전에 우리는 VAO를 선호되는 설정과 함께 bind한다. 코드에서 이것은 다음과 같이 보인다.
// ..:: Initialization code (done once (unless your object frequently changes)) :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy out 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) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
이게 다이다. 우리가 지난 몇 페이지를 걸쳐 했던 모든 것이 이순간을 이끌었다. 우리의 vertex attribute 설정과 어떤 VBO를 사용하는지를 저장하는 VAO. 보통 너가 그리고 싶어하는 다양한 객체들을 가지고 있을 때, 너는 처음에 모든 VAO를 생성하고 설정한다 (그래서 따라서 그 요구되어지는 VBO와 attribute pointer들도) 그리고 나중을 위해서 그것들을 저장한다. 우리가 우리의 객체들 중 하나를 그리고 싶어하는 순간에, 우리는 상응하는 VAO를 가져와서, bind하고 그러고나서 객체를 그리고, 다시 VAO를 unbind한다.
The triangle we've all been wating for
선택된 객체들을 그리려면, OpenGL은 우리에게 현재 활성화된 shader를 사용하여 primitive들을 그리는 glDrawArrays 함수를 제공한다. 이전에 정의된 vertex attribute 설정과 VBO의 vertex data와 함꼐 (간적접으로 VAO를 통해 bound된)
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
그 glDrawArrays 함수는 그것의 첫 번째 인자로 우리가 그리고자하는 OpenGL primitive type을 가진다. 우리는 처음에 삼각형을 그리고 싶다고 말했고, 나는 너에게 거짓말을 하는 것을 좋아하지 않기 때문에, GL_TRIANGLES를 넘긴다. 두 번째 인자는 우리가 그리고자 하는 Vertex array의 시작 인덱스를 명시한다. 우리는 이것은 0에 둔다. 마지막 인자는 얼마나 많이 우리가 vertices들을 그리고자하는지를 명시한다. 이것은 3이다. (우리는 오직 우리의 데이터로부터 1개의 삼각형을 렌더링하고, 그것은 정확히 3개의 정점 길이이다.)
지금 코드를 컴파일 해보고, 에러가 뜬다면 다시 되돌아가 보아라. 너의 프로그램이 컴파일하자마자, 너는 다음과 같은 결과를 봐야만 한다.
=================================================================================================================
아직 이 파트를 번역을 다 한 것은 아니지만, 여행하느라 공부를 못했기에, 내가 정리했던 것을 다시 복습하면서 상기시킨다.
* Knowledge
- OpenGL에서 모든 것은 3D 공간안에 있다.
- 우리 스크린에 표현하기 위해 3D 좌표들을 2D좌표들로 바꾼다 -> 이것은 OpenGL 그래픽스 파이프라인에 의해 관리 된다.
- 그래픽스 파이프라인 : 3D좌표 -> 2D좌표
2D좌표를 채색된 Pixel로 바꿈
위의 두 가지가 그래픽스 파이프라인의 중요한 부분이다.
! 2D 좌표는 한 점이 2D 공간 안에 있는 것의 매우 정확한 표기이다. 2D Pixel은 너의 스크린과 해상도에 의해 제약된 점의 근사치이다.
- 그래픽스 파이프라인은 몇 가지 단계로 나눠질 수 있고 쉽게 동시에 실행되어 질 수 있다.
- Shader는 그래픽스 파이프라인의 각 단계에 대해 데이터를 빠르게 ㅓ리하는 작은 프로그램들을 말한다.
- Shader들은 GPU에서 작동하기 때문에 CPU time을 절약한다.
- Shader는 OpenGL Shading Language(GLSL)로 쓰여진다.
- 그래픽스 파이프라인의 단계
Vertex Data -> Vertex Shader -> Shape Assembly ->Geometry Shader -> Rasterization -> Fragment Shader -> Tests and Blending
- Vertex Shader : 3D좌표를 다른 3D좌표로 변환
Vertex Attribute에 몇 가지 처리를 한다.
- Shape Assembly : Vertex Shader의 정점들을 합쳐서 모양을 만든다.
- Geometry Shader : Shape Assembly의 Vertices들을 위해 새로운 Primitive들을 형성하기 위해 새로운 vertices들을 만들어 다른 모양을 형성한다.
- Rasterization(격자화) : Geometry Shader의 output, 즉 primitive를 최종 스크린에 일치하는 픽셀들로 사상시킨다.
Fragment Shader가 사용할 Fragment를 생성한다.
! Fragment는 OpenGL이 단일 Pixel을 렌더링 하는데 요구되는 모든 데이터이다.
Fragment Shader 작동 전, 시야 밖에 있는 모든 fragments들을 버린다 (Clipping) -> 성능향상이 된다.
- Fragment Shader : 픽셀의 최종 color를 계산. Color에는 빛, 그림자, 빛의 색 등을 포함한다.
- Alpha testand Blending: 상응하는 fragment의 depth를 확인 -> 다른 객체 앞/뒤에 있는지 확인하기 위해서 한다
alpha값을 확인하여 그에 따라 blend한다
* Coding
- Vertex Input
- X, Y, Z 좌표 -> Vertex Shader에서는 -1 ~ 1의 범위들만 좌표변환 (Normalized Device Coordinates 정규화 장치 좌표)
- Vertex data를 CPU를 통해 GPU의 Vertex Shader로 보내야 한다.
- 많은 정점들을 저장할 수 있는 Vertex Buffer Object(VBO)를 통해 실행한다.
glGenBuffer : 버퍼 객체 생성
glBindBuffer : 버퍼 객체를 type에 따라 bind
glBufferData : vertex data를 버퍼의 메모리에 복사시킨다.
static - 데이터가 거의 전혀 바뀌지 않는다.
dynamic - 데이터가 많이 바뀔 가능성이 높다.
stream - 데이터가 Draw할 때마다 바뀐다.
- Vertex Shader
- GLSL
- Version 선언, Core Profile
- in으로 vertex attribute 선언
- vec3 : 후위표기법 숫자를 기반으로 하는 1~4개의 float를 포함한다.
- layout (location = 0)으로 입력 변수의 위치 선정
- vec4인 gl-Position에 vec3값과 vec.w을 1.0으로 설정하여 입력. gl-Position은 vertex shader의 output으로 사용된다.
- Shader 컴파일
- glCreateShader : 쉐이더 객체 생성
- glShaderSource : 쉐이더 소스코드를 객체에 보낸다.
- glCompileShader : 쉐이더 컴파일
- Error log
- Fragment Shader
- GLSL
- Version 선언 , Core profile
- 오직 한 개의 output 변수만요구, vec4로 out vec4 FragColor
- Shader 컴파일
- Error log
- Linking Shader
- 각각의 Shader output을 다음의 input으로 연결
- glCreateProgram : 프로그램 객체 생성
- glAttachShader : 컴파일된 쉐이더를 프로그램 객체에 붙인다.
- glLinkProgram : 부착된 쉐이더들을 연결
- Error log
- glUseProgram : Program Object 활성화 -> 객체를 렌더링할 때 활성화 되어야 한다.
- glDeleteShader : Program 객체를 활성화 하면 그것만 사용하므로 쓰지 않는 Shader 객체 제거
- Vertex Attribute를 연결하여 어떻게 Vertex data를 해석할지 알려주기
- Vertex Buffer Data의 형식
- 위치데이터는 32 bit 부동소수점 값
- 각각의 위치는 그러한 값들로 3개가 구성된다.
- 각 값들 사이에는 어떠한 공간도 없다 (메모리상에서)
- 데이터에서 첫 번째 값은 버퍼의 시작
- Array와 같다.
- glVertexAttribPointer : Draw할 때마다 OpenGL이 어떻게 vertex buffer data를 해석해야하는지를 명시
- glEnableVertexAttribArray : vertex attribute들을 사용 가능하게 만든다.
- 여러 개의 Vertex Data 상태 설정을 한번에 하기(위에 있는 과정 여러 개를 한 번에)
- Vertex Array Object(VAO)
- 저장하는 것들 : glEnable/Disable, vertexAttribArray 호출
- glVertexAttribPointer를 통한 vertex attribute 설정
- glVertexAttribPointer를 통한 vertex attribute와 사상된 vertex buffer object들
- glGenVertexArray : VAO 생성
- glBindVertexArray : VAO bind - render loop에서도 쓴다.
======================================================================================
Element Buffer Objects
정점들을 렌더링할 때 우리가 말하고자하는 한 가지 마지막 것이 남아있다. 그것은 EBO로 축약되는 Element Buffer Objects 이다. EBO가 어떻게 작동하는지를 설명하기 위해서, 예시를 주는 것이 최선이다. 우리가 삼각형 대신에 사각형을 그리길 원한다고 가정하자. 우리는 두 개의 삼각형을 이용해서 사각형을 그릴 수 있다. (OpenGL은 주로 삼각형을 가지고 작동한다.) 이것은 다음 정점들의 세트를 발생시킬 것이다.
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
};
너가 보듯이 정점들이 구체화하는 몇가지 중복이 있다. 우리는 bottom right와 top left를 두 번이나 명시한다. 이것은 50%의 overhead이다 왜나하면 같은 사각형은 또한 오직 6개 대신에 4개의 정점들로 명시되어질 수 있기 때문이다. 이것은 오직 1000개 대의 삼각형들을 가지는 좀 더 복잡한 모델을 가지자 마자 상황이 악화될 것이다. 그리고 거기에서 중복의 큰 덩어리들이 있게될 것이다. 좀 더 좋은 해결책이 될 수 있는 것은 오직 독특한 정점만을 저장하고, 그러고나서 순서를 명시하는 것이다. 그리고 그 순서에서, 우리는 이러한 정점들을 그리길 원한다. 그러한 경우에, 우리는 사각형을 위해 오직 4개의 정점만을 저장해야마 ㄴ한다. 그리고나서 우리가 그것들을 고리고자 하는 순서로 명시해야 한다. OpenGL이 그러한 특징을 우리에게 제공한다면 멋지지 않겠는가?
고맙게도, EBO는 정확히 그렇게 작동한다. VBO 처럼, 그것은 무슨 정점들이 그려질지를 결정하기위해 OpenGL이 사용하는 indices(인덱스들)을 저장한다. 소위 indexed drawing이라고 불리는 이것은 정확히 우리의 문제에 대한 해결책이다. 시작하기 위해서, 우리는 처음에 그 독특한 정점들을 명시해야만 한다. 그리고 사각형으로서 정점들을 그릴 index들도 명시해야한다.
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
};
unsinged int indices[] = { // note that we start from 0!
0, 1, 3,
1, 2, 3
};
너는 index들을 사용할 때, 우리가 6개 대신에 오직 4개의 정점들만을 필요하다는 것을 볼 수 있따. 다음으로 우리는 EBO를 만들 필요가 있다.
unsigned int EBO;
glGenBuffers(1, &EBO);
VBO와 유사하게, 우리는 glBufferData로 EBO를 bind하고 index들을 buffer에 복사한다. VBO처럼, 우리는 그러한 호출들을 bind와 unbind call 사이에 배치하기를 원한다. 비록 이번에 우리는 GL_ELEMENT_ARRAY_BUFFER을 버퍼 타입으로 명시할지라도.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
우리는 지금 GL_ELEMENT_ARRAY_BUFFER를 버퍼 타겟으로서 주고있다는 것에 주목해라. 해야할 남은 마지막 것은 index buffer로부터 삼각형들을 렌더링하고자하는 것을 알려주기 위해 glDrawArray 호출을 glDrawElement로 대체하는 것이다. glDrawElement를 사용할 때, 우리는 현재 bound된 EBO에서 제공된 인덱스들을 사용하여 그리고자 할 것이다.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO)
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
첫 번째 인자는 glDrawArrays와 유사하게 우리가 그리고자하는 모드를 명시한다. 두 번째 인자는 우리가 그리고자 하는 요소의 개수이다. 우리는 6개의 인덱스들 명시했다. 그래서 우리는 총 6개의 정점들을 그리길 원한다. 세 번째 인자는 GL_UNSIGNED_INT
열심히 번역했지만,,, 노트북이 안좋아서 메모리 부족으로 날려버렸다.... 빨리 좋은 노트북을 사야겠다.
어차피 여기 내용은 가장 기초적이면서 가장 확실하게 이해해야할 부분이니까 반복학습을 좀 더 해야된다.
===========================
첫 번째 인자는 glDrawArrays와 유사하게 우리가 그리고자하는 모드를 명시한다. 두 번째 인자는 우리가 그리고자 하는 요소의 개수이다. 우리는 6개의 인덱스들 명시했다. 그래서 우리는 총 6개의 정점들을 그리길 원한다. 세 번째 인자는 GL_UNSIGNED_INT이다. 마지막 인자는 우리가 EBO에서의 offset을 명시하도록 해준다. (또는 index array를 넘기지만, 그것은 너가 EBO를 사용하지 않을 때 쓴다.), 그러나 우리는 이것을 0으로 남겨둘 것이다.
glDrawElements함수들은 GL_ELEMENT_ARRAY_BUFFER 타겟에 현재 bound된 그것의 인덱스들을 가져온다. 이것은 우리가 정점을 가진 한 객체들을 렌더링하길 원할 때마다 그에 상응하는 EBO를 bind해야하는 것을 의미한다. 이것은 번거로운 일처럼 보인다. 그래서 VAO가 또한 EBO bindings들을 추적하는 것이 발생한다. VAO가 bound되어 있는 동안 현재 bound된 EBO는 VAO의 EBO로서 저장된다. 따라서 VAO에 binding하는 것은 또 자동적으로 그것의 EBO를 바인드한다.
Red Box
VAO는 타겟이 GL_ELEMENT_ARRAY_BUFFER일 때, glBindBuffer 호출들을 저장한다. 이것은 또한 VAO가 unbind calls들을 저장하는 것을 의미한다. 그래서 너의 VAO를 unbind하기 이전에 EBO를 unbind하지 않도록 해준다. 만야 그렇지 않는다면, 그것은 EBO를 설정할 수가 없다.
그 결과로 초기화와 드로잉 코드는 지금 이것과 같이 보인다: ~
프로그램을 돌리는 것은 아래에 묘사된 이미지를 줄 것이다. 왼쪽 이미지는 친숙하게 보일 것이다. 그리고 오른쪽 이미지는 wireframe mode로 그려진 사각형이다. wireframe 사각형은 그 사각형이 정말로 두 개의 삼각형으로 구성되어있다는 것을 보여준다.
Green Box
Wireframe Mode
wireframe mode로 삼각형들을 그리기위해서, 너는 OpenGL이 그것의 프리미티브들을 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)을 통해서 그리도록 설정할 수 있다. 첫 번째 인자는 우리가 OpenGL에게 모든 삼각형의 앞면과 뒷면에 적용한다는 것이고, 두 번째 line이라는 것은 그것들을 선으로서 그리라는 것을 말한다. 어떤 그 이후의 drawing call들은 우리가 그것을 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)을 사용하여 기본값으로 되돌릴 때까지 삼각형을 wireframe mode로 렌더링할 것이다.
만약 너가 에러가 있다면, 다시 뒤로 돌아가서 놓친게 있는지 봐라. 또한 너는 완전한 소스코드를 여기에서 볼 수 있고 아래 댓글 창에서 어떤 질문이든지 해라
만약 너가 용케도 삼각형 또는 사각형을 우리가 했던 것처럼 그려냈다면, 축하한다. 너는 현대 OpenGL의 가장 어려운 부분들 중 하나를 넘긴것이다. (너의 첫 번째 삼각형 그리기). 이것은 너의 첫 번째 삼각형을 그릴 수 있기 전에 요구되어지는 상당한 지식량이 있기 때문에, 이것은 어려운 부분이다. 고맙게도 우리는 그 장애물을 치워버렸고, 다가오는 튜토리얼들은 희망스럽게도 이해하기에 더 쉬울 것이다.
Exercise
논의된 개념을 정말 잘 이해하기 위해서, 몇 가지 연습문제들이 만들어졌다. 너가 무엇이 되고있는지를 이해하도록 하기위해 다음 주제를 계속하기전에, 그것들을 하는 것이 충고되어진다.
1. 너의 데이타에 좀 더 많은 정점들을 더해서, glDrawArrays를 사용하여 서로에게 옆에 있는 두 개의 삼각형을 그려라
Vertices와 Indices를 바꾸면 된다.
float vertices[] =
{
-1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f,
0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.0f,
1.0f, 0.0f, 0.0f
};
unsigned int indices[] =
{
0, 1, 2,
2, 3, 4
};
2. 이제 그 같은 두 개의 삼각형을 그들의 데이터로 두 개의 다른 VAO와 VBO를 사용해서 만들어라.
VAO, VBO, EBO의 기본 사용법을 알아야 한다.
먼저 위의 것들은, Bind -> DATA Setting으로 Draw전에 설정을 해준다.
그리고 Draw할 때는, BIND -> Draw를 거치게 된다.
3. 두 개의 쉐이더 프로그램들을 만드는데, 두 번째 프로그램이 색을 노란색으로 만들어내는 다른 fragment shader를 사용해라. 두 개의 삼각형들을 그려라 또 다시, 그리고 거기에서 하는 하나는 노란색을 만들어낸다.
// draw our first triangle
glUseProgram(shaderProgram1);
glBindVertexArray(VAO[0]);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(shaderProgram2);
glBindVertexArray(VAO[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);
Use Shader -> Bind Vertex Array Object -> Draw Arrays의 로직
댓글 없음:
댓글 쓰기