vertex and fragment shader 사이에, geometry shader라 불리는 부가적인 쉐이더 단계가 있다. 기하 쉐이더는 예를들어 한 점이나 삼각형인 단일의 primitive를 형성하는 점들의 집합을 입력으로 받는다. 기하 쉐이더는 그러고나서 이러한 정점들을 다음 쉐이더 단계에 보내기 전에 기하 쉐이더가 보기에 맞게 변환한다. 그러나 기하 쉐이더를 흥미롭게 만드는 것은 그것이 정점들 (의 집합)을 완전히 다른 primitives로 바꿀 수 있다는 것이다. 이것은 그럴듯하게 초기에 주어진 것 보다 좀 더 많은 정점들을 생성한다.
우리는 너에게 기하쉐이더의 예를 보여주어 곧바로 깊게 알려줄 것이다:
#version 330 core layout (points) in; layout (line_strip, max_vertices = 2) out; void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive(); }
모든 기하 쉐이더의 시작에서, 우리는 우리가 vertex shader로부터 받고자 하는 primitive input의 type을 선언할 필요가 있다. 우리는 in keyword 앞에 layout specifier를 선언하여 이것을 한다. 이 input layout qualifier는 vertex shader로부터 다음의 primitive values 중 아무거나 받을 수 있다:
- points : GL_POINTS primitives를 그릴 때 (1)
- lines : GL_LINES 또는 GL_LINE_STRIP을 그릴 때 (2)
- lines_adjacency : GL_LINES_ADJACENCY 또는 GL_LINE_STRIP_ADJACENCY (4)
- triangles : GL_TRIANGLES, GL_TRIANGLE,_STRIP, or GL_TRIANGLE_FAN (3)
- triangles_adjacency : GL_TRIANGLES_ADJACENCY or GL_TRIANGLE_STRIP_ADJACECNY(6)
이러한 것들은 우리가 glDrawArrays 같은 렌더링 호출에 줄 수 있는 모든 렌더링 primitives이다. 만약 우리가 정점들을 GL_TRIANGLES로 그리는 것을 선택했다면, 우리는 그 input qualifer를 triangles로 설정해야 한다. 괄호안의 숫자는 단일 primitive가 포함하는 정점들의 최소 개수를 나타낸다.
그러고나서 우리는 또한 기하 쉐이더가 실제로 만들어낼 primitive type을 명시할 필요가 있고, 우리는 이것을 out keyword 앞에 layout specifier를 통해 이것을 한다. input layout qualifer처럼, output layout qualifier는 또한 몇 가지 primitive 값들을 받을 수 있다:
- points
- line_strip
- triangle_strip
이러한 세 개의 output specifiers들로, 우리는 input primitives로부터 우리가 원하는 어떤 모양이든지 거의 만들 수 있다. 예를들어, single triangle을 생성하기 위해서, 우리는 output으로서 triangle_strip을 명시하고, 3개의 정점을 만들어 낼 것이다.
기하 쉐이더는 또한 우리가 그것이 만들어내는 정점들의 최대 개수를 설정하기를 기대한다. (만약 너가 이 숫자를 초과한다면, OpenGL은 추가 정점들을 그리지 않을 것이다.) 우리는 이것 또한 out keyword의 layout qualifier내에서 할 수 있다. 이 특정한 경우에, 우리는 2개의 정점들을 최대 숫자로 line_strip을 만들어 낼 것이다.
line strip이 무엇인지 궁금한 경우에: line strip은 최소 두 개의 점들 사이에서 점들의 한 집합을 하나의 연속된 선을 만들기 위해 함께 묶여있다. 렌더링 호출시에 주어지는 각각의 추가 점은 새로운 점과 이전의 점 사이의 새로운 line을 만들어 낼 것이다. 너는 우리가 5개의 정점들을 가진 이미지를 아래에서 볼 수 있다:
현재 쉐이더로, 우리는 오직 하나의 선 밖에 만들어 내지 못한다. 정점들의 최대 개수가 2이기 때문이다.
의미있는 결과를 만들어 내기 위해서, 우리는 이전의 쉐이더 단계로부터 output을 얻어내는 몇 가지 방법이 필요하다. GLSL은 우리에게 gl_in이라고 불리는 내장 변수를 준다. 이것은 내부적으로 (아마도) 이렇게 생겼을 것이다:
여기에서 그것은 (이전 튜토리얼에서 다뤄진) interface block으로서 선언되어 있다. 이것은 몇 가지 흥미로운 변수들을 포함하는데, 가장 흥미로운 것은 우리가 vertex shader의 output으로 설정한 유사한 벡터를 포함하는 gl_Position이다.
그것이 배열로 선언되어있다는 것에 유의해라. 왜냐하면 대부분의 render primitives는 1개 이상의 정점으로 구성되고, 기하 쉐이더가 한 primitive의 모든 정점들을 그것의 입력으로서 받기 때문이다.
이전 vertex shader stage로부터 vertex data를 사용하여, 우리는 두 개의 기하 쉐이더 함수들을 통해 새로운 데이터를 생성하기 시작할 수 있다. 그 함수들은 EmitVertex와 EndPrimitive라고 불려진다. 기하 쉐이더는 적어도 너가 output으로서 명시한 그 primitives 중의 하나를 만들어내기를 기대한다. 우리의 경우에, 우리는 적어도 one line strip primitive를 만들어내기를 원한다.
우리가 EmitVertex를 호출할 때 마다, gl_Position에 현재 설정된 벡터는 primitive에 더해진다. EndPrimitive가 호출될 때 마다, 이 primitive에 대해 모든 emitted된 정점들은명시된 output render primitive에 결합된다. 한 개 이상의 EmitVertex 호출후에 EndPrimitive를 반복적으로 호출하여, 수 많은 primitives가 생성되어질 수 있다. 이 특정한 케이스는 원래의 정점 위치로부터의 작은 offset에 의하여 이동된 두 개의 정점을 emit(방출)하고, 그런 후에 EndPrimitive를 호출한다. 이것은 이러한 두 정점들을 2개의 정점들로 된 single line strip으로 합친다.
(어느정도) 기하 쉐이더가 어떻게 작동하는지 알았으니, 너는 아마도 이 기하 쉐이더가 무엇을 하는지 추측할 수 있다. 이 기하쉐이더는 point primitive를 입력받아 그것의 중심으로서 input point로 수평 line primitive를 생성한다. 만약 우리가 이것을 렌더링한다면 이것은 이것과 같아 보인다:
아직 인상적이지 않지만, 이 결과가 다음의 렌더링 호출을 사용하여서만 만들어진 것을 고려하는 것은 흥미롭다:
glDrawArrays(GL_POINTS, 0, 4);
이것이 상대적으로 간단한 예제이지만, 그것은 너에게 우리가 어떻게 새로운 모형을 (동적으로) 생성하기위해 기하 쉐이더를 사용할 수 있는지를 보여준다. 이 튜토리얼의 나중에서, 우리는 기하 쉐이더를 사용하여 우리가 얻을 수 있는 흥미로운 효과를 다룰 것이지만, 지금까지는 우리는 간단한 기하 쉐이더를 만들어서 시작할 것이다.
Using geometry shaders
기하 쉐이더의 사용을 보여주기위해서, 우리는 정말 간단한 scene을 렌더링 할 건데, 그 scene에서 우리는 그냥 NDC좌표에서 z-평면에 4개의 점을 그린다. 그 점들의 좌표는
그 vertex shader는 오직 그 점을 z-평면에서만 그릴 필요가 있다. 그래서 우리는 오직 기본 vertex shader를 필요로 한다:
그리고 우리는 모든 점들에 대해 fragment shader에 하드코딩한 초록색을 간단히 만들어낼 것이다:
그 점의 vertex data에 대해 VAO와 VBO를 생성하고, glDrawArrays를 통해 그것들을 그려라:
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);
그 결과는 (보기 어려운) 4개의 초록색 점들과 dark scene이다:
그러나 우리는 이 모든 것을 하는 것을 아직 안배웠지 않느냐? 그래. 이제 우리는 기하 쉐이더를 추가하여 scene을 양념할 것이다.
학습 목적으로 우리는 한 point primitive를 입력으로 받아 그것을 수정하지 않은채 다음 쉐이더로 보내는 소위 pass-through geometry shader를 만들 것이다:
이제까지, 기하쉐이더는 이해하기에 꽤 쉽다. 그것은 그것이 입력으로서 받은 수정되지 않은 정점 위치를 방출하고, point primitive를 만들어낸다.
기하 쉐이더는 vertex and fragment shader처럼 컴파일되고 한 프로그램에 링크되어야 할 필요가 있다. 그러나 이번에 우리는, GL_GEOMETRY_sHADER를 사용하여 shader type을 사용하여 쉐이더를 만들 것이다:
그 쉐이더 컴파일 코드는 기본적은 vertex and fragment shader 코드와 같다. compile or linking errors를 체크하도록 해라.
만약 너가 이제 컴파일하고 실행한다면, 다음의 것과 같은 결과를 볼 것이다:
(나는 line strip으로 만들었다.)
그것은 기하 쉐이더 없이도 같은 것이다. 그것은 조금 따분하다. 나도 인정한다. 그러나 우리가 여전히 그 점들을 그릴 수 있다는 사실은 기하 쉐이더가 작동한다는 것을 의미한다. 그래서 이제 좀 더 멋있는 것을 할 시간이다!
Let's build some houses
점들과 선들을 그리는 것은 그렇게 흥미롭지 않다. 그래서 우리는 각 점의 위치에서 우리를 위해 집을 그려줄 기하쉐이더를 사용하여 좀 더 창의적이게 될 것이다. 우리는 기하 쉐이더의 output을 triangle_strip으로 설정하여 이것을 얻을 수 있고, 총 세 개의 삼각형을 그린다: 두 개는 한 사각형을 위해서고 하나는 지붕을 위해서이다.
OpenGL에서 triangle strip은 더 적은 정점들로 삼각형들을 그리는데 좀 더 효율적인 방법이다. 첫 번째 삼각형이 그려지고나서, 각 이후의 정점은 첫 번째 삼각형 다음에 또 다른 삼각형을 만들어 낼 것이다: 모든 3개의 인접한 정점들이 한 삼각형을 형성할 것이다. 만약 우리가 triangle strip을 형성하는 총 6개의 정점을 갖는다면, 우리는 다음의 삼각형을 얻는다: (1,2,3), (2,3,4), (3,4,5), and (4,5,6) 이것은 총 4개의 삼각형을 만들어낸다. triangle strip은 적어도 3개의 정점을 필요로하고 N-2개의 삼각형을 만들어 낼 것이다; 6개의 정점들로 우리는 6-2=4 의 삼각형을 만들었다. 다음의 이미지가 이것을 보여준다:
triangle strip을 geometry shader에서 output으로 사용하여, 우리는 정확한 순서로 3개의 인접한 삼각형을 생성하여 쉽게 house shape를 만들 수 있다. 다음의 이미지는 어떤 순서로 우리가 그려야 하는지에서, 삼각형을 얻기위해 우리가 입력 점인 파란색 점과 무슨 정점들을 필요로하는지를 보여준다:
이것은 다음의 기하쉐이더로 바뀐다:
이 기하쉐이더는 각 정점이 정점의 위치 + offset을 하여, 하나의 큰 삼각형 strip을 만들어내기 위해 5개의 정점을 만들어낸다.결과 primitive는 그러고나서 rasterized 되고, fragment shader는 전체 triangle strip에 대해 작동하고, 우리가 그린 각 점에 대해 초록색의 집을 만들어낸다:
너는 각 집이 정말로 3개의 삼각형으로 구성된 것을 볼 수 있다 - 공간에서 한 점을 이용하여 그려진 것이다. 초록색의 집들은 그래도 좀 지루해 보인다. 그래서 각 집에 독특한 color를 주어 그것을 생기있게 해보자. 이것을 하기 위해서, 우리는 vertex shader에서 정점마다 color information이 있는 추가 vertex attribute를 더할 것이고, 그것을 fragment shader로 보내는 getometry shader에 보낼 것이다.
업데이트된 vertex data는 아래에서 주어진다:
그러고나서 우리는 interface block을 사용하여 color attribute를 기하 쉐이더에게 보내기 위해 vertex shader를 업데이트 한다:
그러고나서 우리는 (다른 interface name을 가진) 같은 interface block을 기하 쉐이더에 선언할 필요가 있다:
in VS_OUT
{
vec3 color;
} gs_in[];
기하 쉐이더는 그것의 입력으로서 정점들의 집합으로 작동하기 때문에, vertex shader로부터 온 그것의 입력 데이터는 항상 데이터의 배열로서 표현된다. 비록 우리가 지금 당장은 오직 한 개의 점만 가질지라도.
Green Box
우리는 필수적으로 데이터를 기하 쉐이더에 보내기위해 interface blocks을 사용할 필요는 없다. 우리는 또한 그것을 in vec3 vColor[]; 라고 쓸 수 있다. 만약 그 vertex shader가 그 color vector를 out vec3 vColor로 보낸다면. 그러나, interface blocks은 기하쉐이더같은 쉐이더들에서 작업하기에 훨씬 더 쉽다. 실제로, 기하 쉐이더의 입력들은 꽤 클 수 있고, 그것들을 하나의 큰 interface block array에 그룹화 시키는 것은 좀 더 이해하기 쉽다.
그러고나서 우리는 또한 다음의 fragment shader stage를 위해 output color vector를 선언해야만 한다:
out vec3 fColor;
fragment 쉐이더가 오직 하나의 (보간된) 컬러를 기대하기 때문에, 많은 컬러들을 보내는 것은 말이 안된다. fColor vector는 따라서 배열이 아닌 하나의 벡터이다. 한 정점을 방출할 때, 각 정점은 그것의 fragment shader run에 대해 마지막에 저장된 값을 fColor에 저장할 것이다. 그 집들에 대해서, 우리는 따라서 fColor를 vertex shader로부터온 컬러로 한 번 채울 수 있다. 그 첫 번째 정점이 전체 집을 색칠하기위해 방출되어지기 전에:
모든 방출된 정점들은 fColor에 있는 마지막저장된 값이 그것들의 데이터로 들어가게 할 것이다. 그 데이터는 우리가 그것들의 attributes에 정의한 vertex의 컬러와 동일하다. 모든 집들은 이제 그것들 자신의 컬러를 가질 것이다:
재미를 위해서, 우리는 또한 겨울인척 할 수 있고, 마지막 정점에 그것 자신의 컬러인 하얀색을 주어 그것들의 지붕에 작은 눈을 줄 수 있다:
그 결과는 이제 이것처럼 보인다.
너는 너의 소스코드와 OpenGL 코드를 여기에서 비교할 수 있다.
너는 기하 쉐이더로 가장 간단한 도형들로 조차도 꽤 창의적일 수 있다는 것을 볼 수 있다. 도형을 매우 빠른 너의 GPU 하드웨어로 동적으로 생성되기 때문에, 이것은 이러한 도형들을 vertex buffers 내에서 정의하는 것보다 좀 더 효율적이다. Geometry buffers는 그러므로 voxel world에서 큐브들 또는 큰 야외 필드에서 grass leaves같은 간단히 종종 반복되는 도형들에 대해 최적화를 위한 훌륭한 도구이다.
Exploding objects
집들을 그리는 것은 재미있지만, 우리가 그렇게 할려는 것은 아니다. 그것은 우리가 그것을 한 번 해보고 오브젝트들을 터뜨리기 위해서이다. 그것은 우리가 또한 그렇게 많이 쓰는 것도 아니지만, 그것은 너에게 기하 쉐이더의 힘을 보여줄 것이다.
우리가 한 오브젝트를 exploding한다고 말할 때, 우리는 실제로 우리의 소중한 정점들의 집합이 날아가게 하지 않을 것이지만, 우리는 적은 시간동안에 대해 그것들의 normal vector의 방향을 따라 그것들의 삼각형을 움직일 것이다. 그 효과는 전체 오브젝트의 삼각형들이 삼각형의 법선 벡터들의 각각의 방향을 따라 폭발하는 것처럼 보인다. nanosuit model에서 폭발하는 삼각형의 효과는 이것처럼 보인다:
그러한 기화 쉐이더 효과에 훌륭한 것은 그것은 모든 오브젝트들에 대해 작동한다는 것이다. 그것들의 복잡성에 상관없이.
우리는 각 vertex를 삼각형의 법선 벡터의 방향에 따라 각 정점을 이동시키려고 하기 때문에, 우리는 처음에 이 법선 벡터를 계산할 필요가 있다. 우리가 할 필요가 있는 것은 우리가 접근할 수 있는 3개의 정점을 사용하여 한 삼각형의 표면에 수직인 벡터를 계산하는 것이다. 너는 transformations tutorial로부터 우리가 외적을 사용하여 두 다른 벡터들에 수직인 한 벡터를 얻을 수 있다는 것을 transformation tutorial로부터 기억할지도 모른다. 만약 우리가 삼각형의 표면에 평행한 두 벡터들 a와 b를 얻으려 한다면, 우리는 그러한 벡터들에 외적을 하여 법선 벡터를 얻을 수 있다. 다음의 기하 쉐이더 함수는 3개의 input vertex coordinates를 사용하여 법선을 얻기위해 정확히 이것을 한다:
여기에서 빼기를 사용하여 삼각형의 표면에 평행한 두 벡터 a와 b를 얻는다. 서로로부터 뺄셈하는 것은 두 벡터의 차이인 벡터를 만들어내고, 모든 3개의 점들이 삼각형 면에 놓여있기 때문에, 서로로부터 그것의 정점들 아무거나 뺴는 것은 평면에 평행한 벡터를 만들어 낸다. 만약 우리가 a와 b를 cross 함수에서 바꾼다면, 우리는 반대방향을 가리키는 법선 벡터를 얻게 된다 - 순서는 여기에서 중요하다!
우리는 이제 법선 벡터를 어떻게 계산하는지 알았으니, 우리는 vertex position vector와 함께 이 법선벡터를 취하는 explode function을 만들 수 있다. 그 함수는 법선 벡터의 방향에 따라 위치 벡터를 이동시키는 새로운 벡터를 만든다:
함수 자체는 너무 복잡하지는 않을 것이다. sin 함수는 그것의 인자로서 time 변수를 받는다. 그 시간에 기반으로하여 -1.0과 1.0사이의 값을 반환한다. 우리는 그 오브젝트를 implode시키기 원치 않기 때문에, 우리는 sin 값을 [0,1]의 범위로 바꾼다. 그 결과 값은 그러고나서 normal vector에 의해 곱해지고 그 결과 direction vector는 position vector에 더해진다.
우리의 model loader를 사용하여 불러온 모델을 그리면서 explode effect에 대한 그 완전한 기하 쉐이더는 이것처럼 보인다:
우리가 vertex를 방출하기전에 적절한 텍스쳐 좌표를 만들어내고 있다는 것을 주목해라.
또한 실제로 너의 OpenGL 코드에서 time variable을 설정하는 것을 잊지마라.
(GLSL 에러를 해결하느라 좀 걸렸다. 폭발하는 기하 쉐이더랑 이전의 lighting 쉐이더를 같이 써서, 저 닭이 이상하게 되었지만 어쨋든 구현했다.)
(닭도 그냥 터트렸다.)
Visualizing normal vectors
이 섹션에서, 우리는 너에게 실제로 유용한 기하 쉐이더를 사용하는 예제를 줄 것이다: 어떤 오브젝트의 normal vectors들을 시각화 하는 것이다. lighting shaders를 프로그래밍 할 때, 너는 실제로 결정하기에 어려운 원인의 이상한 시각적 결과가 나올 것이다. lighting errors의 흔한 원인은 부정확하게 vertex data를 불러와서 야기되는 정확하지 않은 법선 벡터 때문이다. 이것은 부적절하게 그것들을 vertex attributes로 명시하거나 또는 무정확하게 쉐이더들에서 그것들을 다루게 된다. 우리가 원하는 것은 어느정도 우리가 넣은 법선 벡터들이 정확한지 살펴보는 것이다. 너의 법선 벡터들이 정확한지 결정하는 훌륭한 방법은 그것들을 시각화하여 하는 것이다. 그리고 기하 쉐이더가 이 목적에 매우 유용한 도구이다.
그 아이디어는 다음과 같다: 우리는 처음에 기하 쉐이더 없이 normal로서 scene을 그린다. 그러고나서 우리는 두 번째에 scene을 그리지만, 이번에 오직 기하 쉐이더를 통해 생성한 법선 벡터만을 보여준다. 기하 쉐이더는 triangle primitive를 입력으로 받아, 법선 벡터의 방향으로 그것들로부터 3개의 선을 생성한다. - 각 점에 대한 법선벡터로. 슈도코드로, 그것은 이것처럼 보일 것이다:
shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();
이번에, 모델에 의해 공급된 vertex normals를 사용하는 기하 쉐이더를 만들 것이다. scaling과 회전을 고려하기 위해서 (view와 model matrix 때문에) 우리는 우리는 처음에 그것을 clip-space coordinates로 변환하기 전에 normal matrix로 그 법선을 처음에 변환할 것이다. (기하 쉐이더는 clip-space coordinates로서 position vectors를 받는다. 그래서 우리는 또한 그 법선 벡터들을 같은 공간으로 변환해야 한다.) 이것은 vertex shader에서 처리 될 수 있다:
그 변환된 clip-space normal vector는 그러고나서 interface block을 통해 다음 쉐이더 단계로 넘겨진다. 기하 쉐이더는 그러고나서 각 정점 (position과 normal vector를 가진)을 취하고, 각 position vector로부터 normal vector를 그린다.
이러한 것과 같은 기하쉐이더의 내용들은 이제 자기 설명적이다. 우리가 normal vector에 보여지는 normal vectors의 크기를 제한하기 위해 MAGNITUDE vector를 곱한 것을 유의해라. (그렇지 않으면 그것들은 너무 커질 것이다).
normals를 시각화하는 것은 대개 디버깅 목적으로 사용되기 때문에, 우리는 그것들을 단순한 컬러의 선들로서 보일 수 있다. (또는 너가 원한다면 엄청 멋진 선들로) fragment shader의 도움으로:
이제 너의 모델을 보통 shader로 처음에 렌더링하고 그러고나서 특별한 normal-visualizing shader로 하면, 너는 이것과 같은 것을 볼 것이다:
우리의 nanosuit가 이제 부엌 벙어리장갑을 가진 털난 사내처럼 보이는 사실을 제외하고, 그것은 우리에게 한 model의 법선 벡터들이 정말로 올바른지를 결정하는데 있어서 유용한 방식을 준다. 너는 이것과 같은 기하 쉐이더들이 또한 오브젝트들에게 fur을 추가하는데 자주 사용되는 것을 상상할 수 있다.
너는 OpenGL의 소스코드를 여기에서 볼 수 있다.
=============================================
이번 파트에서는 디버깅하느라 시간을 오래 썼다.
쉐이더가 커짐에 따라, 디버깅이 만만치 않아진다.
확실히 쉐이더를 여러개로 나누어서 compact하게 유지해야, 디버깅하기도 수월할 거 같다.
그리고 저 normal visualizing하는데, uniform 변수 설정시 model이라고 써야하는데 Model이라고 써서 이거알아내는데 1시간이나 걸려버렸다.
Uniform 변수 설정 항상 유의하자. 이것을 예방할 수 있는 방법 있으면 미리 강구해놔야겠다.
내가 생각하는 것은, 쉐이더 파일의 uniform 변수의 이름을 파싱해서, 그 파싱한 데이터를 통해서 하게끔하면 에러가 일어나지 않을 것 같다.
기하 쉐이더는 또한 우리가 그것이 만들어내는 정점들의 최대 개수를 설정하기를 기대한다. (만약 너가 이 숫자를 초과한다면, OpenGL은 추가 정점들을 그리지 않을 것이다.) 우리는 이것 또한 out keyword의 layout qualifier내에서 할 수 있다. 이 특정한 경우에, 우리는 2개의 정점들을 최대 숫자로 line_strip을 만들어 낼 것이다.
line strip이 무엇인지 궁금한 경우에: line strip은 최소 두 개의 점들 사이에서 점들의 한 집합을 하나의 연속된 선을 만들기 위해 함께 묶여있다. 렌더링 호출시에 주어지는 각각의 추가 점은 새로운 점과 이전의 점 사이의 새로운 line을 만들어 낼 것이다. 너는 우리가 5개의 정점들을 가진 이미지를 아래에서 볼 수 있다:
현재 쉐이더로, 우리는 오직 하나의 선 밖에 만들어 내지 못한다. 정점들의 최대 개수가 2이기 때문이다.
의미있는 결과를 만들어 내기 위해서, 우리는 이전의 쉐이더 단계로부터 output을 얻어내는 몇 가지 방법이 필요하다. GLSL은 우리에게 gl_in이라고 불리는 내장 변수를 준다. 이것은 내부적으로 (아마도) 이렇게 생겼을 것이다:
in gl_Vertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[];
여기에서 그것은 (이전 튜토리얼에서 다뤄진) interface block으로서 선언되어 있다. 이것은 몇 가지 흥미로운 변수들을 포함하는데, 가장 흥미로운 것은 우리가 vertex shader의 output으로 설정한 유사한 벡터를 포함하는 gl_Position이다.
그것이 배열로 선언되어있다는 것에 유의해라. 왜냐하면 대부분의 render primitives는 1개 이상의 정점으로 구성되고, 기하 쉐이더가 한 primitive의 모든 정점들을 그것의 입력으로서 받기 때문이다.
이전 vertex shader stage로부터 vertex data를 사용하여, 우리는 두 개의 기하 쉐이더 함수들을 통해 새로운 데이터를 생성하기 시작할 수 있다. 그 함수들은 EmitVertex와 EndPrimitive라고 불려진다. 기하 쉐이더는 적어도 너가 output으로서 명시한 그 primitives 중의 하나를 만들어내기를 기대한다. 우리의 경우에, 우리는 적어도 one line strip primitive를 만들어내기를 원한다.
void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive(); }
우리가 EmitVertex를 호출할 때 마다, gl_Position에 현재 설정된 벡터는 primitive에 더해진다. EndPrimitive가 호출될 때 마다, 이 primitive에 대해 모든 emitted된 정점들은명시된 output render primitive에 결합된다. 한 개 이상의 EmitVertex 호출후에 EndPrimitive를 반복적으로 호출하여, 수 많은 primitives가 생성되어질 수 있다. 이 특정한 케이스는 원래의 정점 위치로부터의 작은 offset에 의하여 이동된 두 개의 정점을 emit(방출)하고, 그런 후에 EndPrimitive를 호출한다. 이것은 이러한 두 정점들을 2개의 정점들로 된 single line strip으로 합친다.
(어느정도) 기하 쉐이더가 어떻게 작동하는지 알았으니, 너는 아마도 이 기하 쉐이더가 무엇을 하는지 추측할 수 있다. 이 기하쉐이더는 point primitive를 입력받아 그것의 중심으로서 input point로 수평 line primitive를 생성한다. 만약 우리가 이것을 렌더링한다면 이것은 이것과 같아 보인다:
아직 인상적이지 않지만, 이 결과가 다음의 렌더링 호출을 사용하여서만 만들어진 것을 고려하는 것은 흥미롭다:
glDrawArrays(GL_POINTS, 0, 4);
이것이 상대적으로 간단한 예제이지만, 그것은 너에게 우리가 어떻게 새로운 모형을 (동적으로) 생성하기위해 기하 쉐이더를 사용할 수 있는지를 보여준다. 이 튜토리얼의 나중에서, 우리는 기하 쉐이더를 사용하여 우리가 얻을 수 있는 흥미로운 효과를 다룰 것이지만, 지금까지는 우리는 간단한 기하 쉐이더를 만들어서 시작할 것이다.
Using geometry shaders
기하 쉐이더의 사용을 보여주기위해서, 우리는 정말 간단한 scene을 렌더링 할 건데, 그 scene에서 우리는 그냥 NDC좌표에서 z-평면에 4개의 점을 그린다. 그 점들의 좌표는
float points[] = { -0.5f, 0.5f, // top-left 0.5f, 0.5f, // top-right 0.5f, -0.5f, // bottom-right -0.5f, -0.5f // bottom-left };
그 vertex shader는 오직 그 점을 z-평면에서만 그릴 필요가 있다. 그래서 우리는 오직 기본 vertex shader를 필요로 한다:
#version 330 core layout (location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); }
그리고 우리는 모든 점들에 대해 fragment shader에 하드코딩한 초록색을 간단히 만들어낼 것이다:
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 1.0, 0.0, 1.0); }
그 점의 vertex data에 대해 VAO와 VBO를 생성하고, glDrawArrays를 통해 그것들을 그려라:
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);
그 결과는 (보기 어려운) 4개의 초록색 점들과 dark scene이다:
그러나 우리는 이 모든 것을 하는 것을 아직 안배웠지 않느냐? 그래. 이제 우리는 기하 쉐이더를 추가하여 scene을 양념할 것이다.
학습 목적으로 우리는 한 point primitive를 입력으로 받아 그것을 수정하지 않은채 다음 쉐이더로 보내는 소위 pass-through geometry shader를 만들 것이다:
#version 330 core layout (points) in; layout (points, max_vertices = 1) out; void main() { gl_Position = gl_in[0].gl_Position EmitVertex(); EndPrimitive(); }
이제까지, 기하쉐이더는 이해하기에 꽤 쉽다. 그것은 그것이 입력으로서 받은 수정되지 않은 정점 위치를 방출하고, point primitive를 만들어낸다.
기하 쉐이더는 vertex and fragment shader처럼 컴파일되고 한 프로그램에 링크되어야 할 필요가 있다. 그러나 이번에 우리는, GL_GEOMETRY_sHADER를 사용하여 shader type을 사용하여 쉐이더를 만들 것이다:
// geometry Shader geometry = glCreateShader(GL_GEOMETRY_SHADER); glShaderSource(geometry, 1, &gShaderCode, NULL); glCompileShader(geometry); if (checkCompileErrors(geometry, "GEOMETRY") == false) return false; // fragment shader fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); if (checkCompileErrors(fragment, "FRAGMENT") == false) return false; // shader Program ID = glCreateProgram(); glAttachShader(ID, vertex); glAttachShader(ID, geometry); glAttachShader(ID, fragment); glLinkProgram(ID); if (checkCompileErrors(ID, "PROGRAM") == false) return false;
그 쉐이더 컴파일 코드는 기본적은 vertex and fragment shader 코드와 같다. compile or linking errors를 체크하도록 해라.
만약 너가 이제 컴파일하고 실행한다면, 다음의 것과 같은 결과를 볼 것이다:
(나는 line strip으로 만들었다.)
그것은 기하 쉐이더 없이도 같은 것이다. 그것은 조금 따분하다. 나도 인정한다. 그러나 우리가 여전히 그 점들을 그릴 수 있다는 사실은 기하 쉐이더가 작동한다는 것을 의미한다. 그래서 이제 좀 더 멋있는 것을 할 시간이다!
Let's build some houses
점들과 선들을 그리는 것은 그렇게 흥미롭지 않다. 그래서 우리는 각 점의 위치에서 우리를 위해 집을 그려줄 기하쉐이더를 사용하여 좀 더 창의적이게 될 것이다. 우리는 기하 쉐이더의 output을 triangle_strip으로 설정하여 이것을 얻을 수 있고, 총 세 개의 삼각형을 그린다: 두 개는 한 사각형을 위해서고 하나는 지붕을 위해서이다.
OpenGL에서 triangle strip은 더 적은 정점들로 삼각형들을 그리는데 좀 더 효율적인 방법이다. 첫 번째 삼각형이 그려지고나서, 각 이후의 정점은 첫 번째 삼각형 다음에 또 다른 삼각형을 만들어 낼 것이다: 모든 3개의 인접한 정점들이 한 삼각형을 형성할 것이다. 만약 우리가 triangle strip을 형성하는 총 6개의 정점을 갖는다면, 우리는 다음의 삼각형을 얻는다: (1,2,3), (2,3,4), (3,4,5), and (4,5,6) 이것은 총 4개의 삼각형을 만들어낸다. triangle strip은 적어도 3개의 정점을 필요로하고 N-2개의 삼각형을 만들어 낼 것이다; 6개의 정점들로 우리는 6-2=4 의 삼각형을 만들었다. 다음의 이미지가 이것을 보여준다:
triangle strip을 geometry shader에서 output으로 사용하여, 우리는 정확한 순서로 3개의 인접한 삼각형을 생성하여 쉽게 house shape를 만들 수 있다. 다음의 이미지는 어떤 순서로 우리가 그려야 하는지에서, 삼각형을 얻기위해 우리가 입력 점인 파란색 점과 무슨 정점들을 필요로하는지를 보여준다:
이것은 다음의 기하쉐이더로 바뀐다:
#version 330 core layout (points) in; layout (triangle_strip, max_vertices = 5) out; void build_house(vec4 position) { gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1 : bottom_left EmitVertex(); gl_Position = position + vec4(0.2, -0.2, 0.0, 0.0); // 2 : bottom_right EmitVertex(); gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3 : top_left EmitVertex(); gl_Position = position + vec4(0.2, 0.2, 0.0, 0.0); // 4 : top_right EmitVertex(); gl_Position = position + vec4(0.0, 0.4, 0.0, 0.0); // 5 : top EmitVertex(); EndPrimitive(); } void main() { build_house(gl_in[0].gl_Position); }
이 기하쉐이더는 각 정점이 정점의 위치 + offset을 하여, 하나의 큰 삼각형 strip을 만들어내기 위해 5개의 정점을 만들어낸다.결과 primitive는 그러고나서 rasterized 되고, fragment shader는 전체 triangle strip에 대해 작동하고, 우리가 그린 각 점에 대해 초록색의 집을 만들어낸다:
너는 각 집이 정말로 3개의 삼각형으로 구성된 것을 볼 수 있다 - 공간에서 한 점을 이용하여 그려진 것이다. 초록색의 집들은 그래도 좀 지루해 보인다. 그래서 각 집에 독특한 color를 주어 그것을 생기있게 해보자. 이것을 하기 위해서, 우리는 vertex shader에서 정점마다 color information이 있는 추가 vertex attribute를 더할 것이고, 그것을 fragment shader로 보내는 getometry shader에 보낼 것이다.
업데이트된 vertex data는 아래에서 주어진다:
float points[] = { -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // top-left 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // top-right 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-right -0.5f, -0.5f, 1.0f, 1.0f, 0.0f // bottom-left };
그러고나서 우리는 interface block을 사용하여 color attribute를 기하 쉐이더에게 보내기 위해 vertex shader를 업데이트 한다:
#version 330 core layout (location = 0) in vec2 aPos; layout (location = 1) in vec3 aColor; out VS_OUT { vec3 color; } vs_out; void main() { gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); vs_out.color = aColor; }
그러고나서 우리는 (다른 interface name을 가진) 같은 interface block을 기하 쉐이더에 선언할 필요가 있다:
in VS_OUT
{
vec3 color;
} gs_in[];
기하 쉐이더는 그것의 입력으로서 정점들의 집합으로 작동하기 때문에, vertex shader로부터 온 그것의 입력 데이터는 항상 데이터의 배열로서 표현된다. 비록 우리가 지금 당장은 오직 한 개의 점만 가질지라도.
Green Box
우리는 필수적으로 데이터를 기하 쉐이더에 보내기위해 interface blocks을 사용할 필요는 없다. 우리는 또한 그것을 in vec3 vColor[]; 라고 쓸 수 있다. 만약 그 vertex shader가 그 color vector를 out vec3 vColor로 보낸다면. 그러나, interface blocks은 기하쉐이더같은 쉐이더들에서 작업하기에 훨씬 더 쉽다. 실제로, 기하 쉐이더의 입력들은 꽤 클 수 있고, 그것들을 하나의 큰 interface block array에 그룹화 시키는 것은 좀 더 이해하기 쉽다.
그러고나서 우리는 또한 다음의 fragment shader stage를 위해 output color vector를 선언해야만 한다:
out vec3 fColor;
fragment 쉐이더가 오직 하나의 (보간된) 컬러를 기대하기 때문에, 많은 컬러들을 보내는 것은 말이 안된다. fColor vector는 따라서 배열이 아닌 하나의 벡터이다. 한 정점을 방출할 때, 각 정점은 그것의 fragment shader run에 대해 마지막에 저장된 값을 fColor에 저장할 것이다. 그 집들에 대해서, 우리는 따라서 fColor를 vertex shader로부터온 컬러로 한 번 채울 수 있다. 그 첫 번째 정점이 전체 집을 색칠하기위해 방출되어지기 전에:
void build_house(vec4 position) { fColor = gs_in[0].color; gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1 : bottom_left EmitVertex(); gl_Position = position + vec4(0.2, -0.2, 0.0, 0.0); // 2 : bottom_right EmitVertex(); gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3 : top_left EmitVertex(); gl_Position = position + vec4(0.2, 0.2, 0.0, 0.0); // 4 : top_right EmitVertex(); gl_Position = position + vec4(0.0, 0.4, 0.0, 0.0); // 5 : top EmitVertex(); EndPrimitive(); }
모든 방출된 정점들은 fColor에 있는 마지막저장된 값이 그것들의 데이터로 들어가게 할 것이다. 그 데이터는 우리가 그것들의 attributes에 정의한 vertex의 컬러와 동일하다. 모든 집들은 이제 그것들 자신의 컬러를 가질 것이다:
재미를 위해서, 우리는 또한 겨울인척 할 수 있고, 마지막 정점에 그것 자신의 컬러인 하얀색을 주어 그것들의 지붕에 작은 눈을 줄 수 있다:
void build_house(vec4 position) { fColor = gs_in[0].color; gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1 : bottom_left EmitVertex(); gl_Position = position + vec4(0.2, -0.2, 0.0, 0.0); // 2 : bottom_right EmitVertex(); gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3 : top_left EmitVertex(); gl_Position = position + vec4(0.2, 0.2, 0.0, 0.0); // 4 : top_right EmitVertex(); fColor = vec3(1.0); gl_Position = position + vec4(0.0, 0.4, 0.0, 0.0); // 5 : top EmitVertex(); EndPrimitive(); }
그 결과는 이제 이것처럼 보인다.
너는 너의 소스코드와 OpenGL 코드를 여기에서 비교할 수 있다.
너는 기하 쉐이더로 가장 간단한 도형들로 조차도 꽤 창의적일 수 있다는 것을 볼 수 있다. 도형을 매우 빠른 너의 GPU 하드웨어로 동적으로 생성되기 때문에, 이것은 이러한 도형들을 vertex buffers 내에서 정의하는 것보다 좀 더 효율적이다. Geometry buffers는 그러므로 voxel world에서 큐브들 또는 큰 야외 필드에서 grass leaves같은 간단히 종종 반복되는 도형들에 대해 최적화를 위한 훌륭한 도구이다.
Exploding objects
집들을 그리는 것은 재미있지만, 우리가 그렇게 할려는 것은 아니다. 그것은 우리가 그것을 한 번 해보고 오브젝트들을 터뜨리기 위해서이다. 그것은 우리가 또한 그렇게 많이 쓰는 것도 아니지만, 그것은 너에게 기하 쉐이더의 힘을 보여줄 것이다.
우리가 한 오브젝트를 exploding한다고 말할 때, 우리는 실제로 우리의 소중한 정점들의 집합이 날아가게 하지 않을 것이지만, 우리는 적은 시간동안에 대해 그것들의 normal vector의 방향을 따라 그것들의 삼각형을 움직일 것이다. 그 효과는 전체 오브젝트의 삼각형들이 삼각형의 법선 벡터들의 각각의 방향을 따라 폭발하는 것처럼 보인다. nanosuit model에서 폭발하는 삼각형의 효과는 이것처럼 보인다:
그러한 기화 쉐이더 효과에 훌륭한 것은 그것은 모든 오브젝트들에 대해 작동한다는 것이다. 그것들의 복잡성에 상관없이.
우리는 각 vertex를 삼각형의 법선 벡터의 방향에 따라 각 정점을 이동시키려고 하기 때문에, 우리는 처음에 이 법선 벡터를 계산할 필요가 있다. 우리가 할 필요가 있는 것은 우리가 접근할 수 있는 3개의 정점을 사용하여 한 삼각형의 표면에 수직인 벡터를 계산하는 것이다. 너는 transformations tutorial로부터 우리가 외적을 사용하여 두 다른 벡터들에 수직인 한 벡터를 얻을 수 있다는 것을 transformation tutorial로부터 기억할지도 모른다. 만약 우리가 삼각형의 표면에 평행한 두 벡터들 a와 b를 얻으려 한다면, 우리는 그러한 벡터들에 외적을 하여 법선 벡터를 얻을 수 있다. 다음의 기하 쉐이더 함수는 3개의 input vertex coordinates를 사용하여 법선을 얻기위해 정확히 이것을 한다:
vec3 GetNormal() { vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position); vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position); return normalize(cross(a,b)); }
여기에서 빼기를 사용하여 삼각형의 표면에 평행한 두 벡터 a와 b를 얻는다. 서로로부터 뺄셈하는 것은 두 벡터의 차이인 벡터를 만들어내고, 모든 3개의 점들이 삼각형 면에 놓여있기 때문에, 서로로부터 그것의 정점들 아무거나 뺴는 것은 평면에 평행한 벡터를 만들어 낸다. 만약 우리가 a와 b를 cross 함수에서 바꾼다면, 우리는 반대방향을 가리키는 법선 벡터를 얻게 된다 - 순서는 여기에서 중요하다!
우리는 이제 법선 벡터를 어떻게 계산하는지 알았으니, 우리는 vertex position vector와 함께 이 법선벡터를 취하는 explode function을 만들 수 있다. 그 함수는 법선 벡터의 방향에 따라 위치 벡터를 이동시키는 새로운 벡터를 만든다:
vec3 explode(vec4 position, vec3 normal) { float magnitude = 2.0; vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; return position + vec4(direction, 0.0); }
함수 자체는 너무 복잡하지는 않을 것이다. sin 함수는 그것의 인자로서 time 변수를 받는다. 그 시간에 기반으로하여 -1.0과 1.0사이의 값을 반환한다. 우리는 그 오브젝트를 implode시키기 원치 않기 때문에, 우리는 sin 값을 [0,1]의 범위로 바꾼다. 그 결과 값은 그러고나서 normal vector에 의해 곱해지고 그 결과 direction vector는 position vector에 더해진다.
우리의 model loader를 사용하여 불러온 모델을 그리면서 explode effect에 대한 그 완전한 기하 쉐이더는 이것처럼 보인다:
#version 330 core layout (triangles) in; layout (triangle_strip, max_vertices = 3) out; in VS_OUT { vec2 texCoords; } gs_in[]; out vec2 TexCoords; uniform float time; vec3 GetNormal() { vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position); vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position); return normalize(cross(a,b)); } vec3 explode(vec4 position, vec3 normal) { float magnitude = 2.0; vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; return position + vec4(direction, 0.0); } void main() { vec3 normal = GetNormal(); for(int i = 0; i < 3; ++i) { gl_Position = explode(gl_in[i].gl_Position, normal); TexCoords = gs_in[i].texCoords; EmitVertex(); } EndPrimitive(); }
우리가 vertex를 방출하기전에 적절한 텍스쳐 좌표를 만들어내고 있다는 것을 주목해라.
또한 실제로 너의 OpenGL 코드에서 time variable을 설정하는 것을 잊지마라.
(GLSL 에러를 해결하느라 좀 걸렸다. 폭발하는 기하 쉐이더랑 이전의 lighting 쉐이더를 같이 써서, 저 닭이 이상하게 되었지만 어쨋든 구현했다.)
(닭도 그냥 터트렸다.)
Visualizing normal vectors
이 섹션에서, 우리는 너에게 실제로 유용한 기하 쉐이더를 사용하는 예제를 줄 것이다: 어떤 오브젝트의 normal vectors들을 시각화 하는 것이다. lighting shaders를 프로그래밍 할 때, 너는 실제로 결정하기에 어려운 원인의 이상한 시각적 결과가 나올 것이다. lighting errors의 흔한 원인은 부정확하게 vertex data를 불러와서 야기되는 정확하지 않은 법선 벡터 때문이다. 이것은 부적절하게 그것들을 vertex attributes로 명시하거나 또는 무정확하게 쉐이더들에서 그것들을 다루게 된다. 우리가 원하는 것은 어느정도 우리가 넣은 법선 벡터들이 정확한지 살펴보는 것이다. 너의 법선 벡터들이 정확한지 결정하는 훌륭한 방법은 그것들을 시각화하여 하는 것이다. 그리고 기하 쉐이더가 이 목적에 매우 유용한 도구이다.
그 아이디어는 다음과 같다: 우리는 처음에 기하 쉐이더 없이 normal로서 scene을 그린다. 그러고나서 우리는 두 번째에 scene을 그리지만, 이번에 오직 기하 쉐이더를 통해 생성한 법선 벡터만을 보여준다. 기하 쉐이더는 triangle primitive를 입력으로 받아, 법선 벡터의 방향으로 그것들로부터 3개의 선을 생성한다. - 각 점에 대한 법선벡터로. 슈도코드로, 그것은 이것처럼 보일 것이다:
shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();
이번에, 모델에 의해 공급된 vertex normals를 사용하는 기하 쉐이더를 만들 것이다. scaling과 회전을 고려하기 위해서 (view와 model matrix 때문에) 우리는 우리는 처음에 그것을 clip-space coordinates로 변환하기 전에 normal matrix로 그 법선을 처음에 변환할 것이다. (기하 쉐이더는 clip-space coordinates로서 position vectors를 받는다. 그래서 우리는 또한 그 법선 벡터들을 같은 공간으로 변환해야 한다.) 이것은 vertex shader에서 처리 될 수 있다:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; out VS_OUT { vec3 normal; } vs_out; uniform mat4 projection; uniform mat4 view; uniform mat4 model; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); mat3 normalMatrix = mat3(transpose(inverse(view * model))); vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 0.0))); }
그 변환된 clip-space normal vector는 그러고나서 interface block을 통해 다음 쉐이더 단계로 넘겨진다. 기하 쉐이더는 그러고나서 각 정점 (position과 normal vector를 가진)을 취하고, 각 position vector로부터 normal vector를 그린다.
#version 330 core layout (triangles) in; layout (line_strip, max_vertices = 6) out; in VS_OUT { vec3 normal; } gs_in[]; const float MAGNITUDE = 0.4; void GenerateLine(int index) { gl_Position = gl_in[index].gl_Position; EmitVertex(); gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE; EmitVertex(); EndPrimitive(); } void main() { for(int i = 0; i < 3; ++i) GenerateLine(i); }
이러한 것과 같은 기하쉐이더의 내용들은 이제 자기 설명적이다. 우리가 normal vector에 보여지는 normal vectors의 크기를 제한하기 위해 MAGNITUDE vector를 곱한 것을 유의해라. (그렇지 않으면 그것들은 너무 커질 것이다).
normals를 시각화하는 것은 대개 디버깅 목적으로 사용되기 때문에, 우리는 그것들을 단순한 컬러의 선들로서 보일 수 있다. (또는 너가 원한다면 엄청 멋진 선들로) fragment shader의 도움으로:
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.85, 0.46, 0.039, 1.0); }
이제 너의 모델을 보통 shader로 처음에 렌더링하고 그러고나서 특별한 normal-visualizing shader로 하면, 너는 이것과 같은 것을 볼 것이다:
우리의 nanosuit가 이제 부엌 벙어리장갑을 가진 털난 사내처럼 보이는 사실을 제외하고, 그것은 우리에게 한 model의 법선 벡터들이 정말로 올바른지를 결정하는데 있어서 유용한 방식을 준다. 너는 이것과 같은 기하 쉐이더들이 또한 오브젝트들에게 fur을 추가하는데 자주 사용되는 것을 상상할 수 있다.
너는 OpenGL의 소스코드를 여기에서 볼 수 있다.
=============================================
이번 파트에서는 디버깅하느라 시간을 오래 썼다.
쉐이더가 커짐에 따라, 디버깅이 만만치 않아진다.
확실히 쉐이더를 여러개로 나누어서 compact하게 유지해야, 디버깅하기도 수월할 거 같다.
그리고 저 normal visualizing하는데, uniform 변수 설정시 model이라고 써야하는데 Model이라고 써서 이거알아내는데 1시간이나 걸려버렸다.
Uniform 변수 설정 항상 유의하자. 이것을 예방할 수 있는 방법 있으면 미리 강구해놔야겠다.
내가 생각하는 것은, 쉐이더 파일의 uniform 변수의 이름을 파싱해서, 그 파싱한 데이터를 통해서 하게끔하면 에러가 일어나지 않을 것 같다.
댓글 없음:
댓글 쓰기