Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

게임공장

[Learn OpenGL 번역] 5-9. 고급 OpenGL - Geometry Shader 본문

OpenGL

[Learn OpenGL 번역] 5-9. 고급 OpenGL - Geometry Shader

짱승_ 2018. 8. 23. 22:31

Geometry Shader

고급 OpenGL/Geometry Shader

  Vertex shader와 fragment shader 사이에 geometry shader라고 불리는 선택적인 shader 단계가 존재합니다. Geometry shader는 입력으로 예를 들어 점이나 삼각형같은 하나의 기본 도형을 이루는 vertex들의 모음을 받습니다. 이 geometry shader는 이 vertex들을 다음 shader 단계에 이들을 보내기 전에 적절한 형태로 변환시킬 수 있습니다. 하지만 geometry shader가 만드는 흥미로운 점은 vertex들을 원래 주어진 vertex들보다 더 많은 vertex들을 생성하는 완전히 다른 기본 타입 도형으로 변환시킬 수 있다는 점입니다.

  Geometry shader의 예제를 보여드리면서 시작하겠습니다.


#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();
}  

  모든 geometry shader의 시작지점에 vertex shader에서 받을 기본 타입 입력의 유형을 선언해야 합니다. in 키워드 앞에 layout 지정자를 선언하여 이를 수행할 수 있습니다. 이 입력 layout 식별자는 vertex shader으로부터 다음의 기본 타입 값들을 취할 수 있습니다.

  • points: GL_POINTS 기본 타입으로 그릴 때(1)
  • lines: GL_LINES 혹은 GL_LINE_STRIP 기본 타입으로 그릴 때(2)
  • lines_adjacency: GL_LINES_ADJACENCY 혹은 GL_LINE_STRIP_ADJACENCY (4).
  • triangles: GL_TRIANGLES, GL_TRIANGLE_STRIP 혹은 GL_TRIANGLE_FAN (3).
  • triangles_adjacency : GL_TRIANGLES_ADJACENCY 혹은 GL_TRIANGLE_STRIP_ADJACENCY (6).

  이 것들은 glDrawArrays 함수 같은 렌더링 명령에 줄 수 있는 거의 모든 기본 타입들입니다. 만약 vertex들을

GL_TRIANGLES

로 그리도록 선택했다면 입력 식별자를 triangles로 설정해야 합니다. 괄호안의 숫자는 하나의 기본 타입 도형이 가지고 있는 vertex의 갯수입니다.

  그런 다음 또한 이 geometry shader가 실제로 출력할 기본 타입 유형을 설정해야 합니다. out 키워드 앞에 layout 지정자를 사용하여 이를 수행할 수 있습니다. 입력 layout 식별자와 마찬가지로 출력 layout 식별자도 여러가지 기본 타입 값을 취할 수 있습니다.

  • points
  • line_strip
  • triangle_strip

  단지 이 3개의 출력 지정자를 사용하여 거의 모든 도형을 생성할 수 있습니다. 예를 들어 하나의 삼각형을 생성하기 위해 출력으로 triangle_strip를 지정하면 3개의 vertex들을 출력합니다.

  이 geometry shader는 출력할 vertex의 최대 갯수를 설정해주기를 원합니다(만약 이 숫자를 초과하면 OpenGL은 추가적인 vertex들은 그리지 않습니다). 우리는 이를 out 키워드 앞의 layout 식별자 안에서 수행할 수 있습니다. 이 경우에 우리는 line_strip으로 출력하며 vertex의 최대 갯수는 2개로 설정할 것입니다.

  Line strip이 무엇인지 궁금할 것입니다. Line strip은 하나의 연속적인 선을 이루는 최소한 2개의 점의 모음을 서로 묶습니다. 렌더링 명령에 넘겨진 추가적인 점들은 그 전의 점과 새로운 선을 형성합니다. 다음 그림은 5개의 점 vertex들을 가지고 있을 때의 상황을 보여줍니다.

Image of line_strip primitive in geometry shader

  현재 shader에서는 오직 하나의 선만 그릴 수 있습니다. Vertex의 최대 갯수를 2로 설정하였기 때문이죠.

  의미있는 결과를 생성하기 위해 이 전의 shader 단계에서 출력을 얻는 어떠한 방법이 필요합니다. GLSL은 gl_in이라고 불리는 내장 변수를 제공해줍니다. 이 변수는 내부적으로 (아마도) 다음과 같은 형태를 가지고 있을 것입니다.


in gl_Vertex
{
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];  

  여기에서 이 것은 흥미로운 몇가지 변수를 포함하고 있는 interface block으로 선언되었습니다. 그 중에서 가장 흥미로운 것은 우리가 vertex shader의 출력으로 설정하는 벡터와 비슷한

gl_Position

입니다.

  이 것이 배열로 선언되었다는 것을 알아두세요. 대부분의 기본 타입 도형들은 하나 이상의 vertex들로 이루어져있고 geometry shader는 기본 타입 도형의 모든 vertex들을 얻기 때문입니다.

  이 전의 vertex shader 단계로부터 얻어진 vertex 데이터를 사용하여 geometry shader의 EmitVertex와 EndPrimitive 함수를 통해 새로운 데이터를 생성할 수 있습니다. 이 geometry shader는 여러분이 최소한 하나의 기본 타입은 생성하기를 원합니다. 우리의 경우에 최소한 하나의 line strip 기본 타입을 생성해야 합니다.


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

에 설정된 벡터가 기본 타입 도형에 추가됩니다. EndPrimitive 함수가 호출될 때마다 방출된 모든 vertex들이 지정된 출력 기본 타입으로 결합됩니다. 한 번 이상의 EmitVertex 함수가 호출된 후에 EndPrimitive 함수를 호출하는 것을 반복적으로 수행하면 여러 개의 기본 타입 도형들을 생성할 수 있습니다. 이 경우에는 원래의 vertex 위치에서 작은 offset을 사용하여 변환된 2개의 vertex들을 방출하고 EndPrimitive 함수를 호출하여 이 2개의 vetex를 하나의 line strip으로 결합시킵니다.

  이제 geometry shader가 어떻게 동작하는지 알았으니 아마 이 geometry shader가 무엇을 할 수 있는지 궁금할 것입니다. 이 geometry shader는 입력으로 점 기본 타입 도형을 받고 중앙에 수평선 기본 타입 도형을 생성합니다. 이 것을 렌더링한다면 다음과 같은 것을 볼 수 있습니다.

Geometry shader drawing lines out of points in OpenGL

  아직까진 아주 인상적인 것은 없습니다. 하지만 이 출력이 다음의 렌더링 호출만으로 생성된 출력이라는 것을 생각하면 흥미로울 것입니다.


glDrawArrays(GL_POINTS, 0, 4);  

  이 것은 비교적 간단한 예제인 반면에 우리가 어떻게 geometry shader를 사용하여 새로운 도형을 만들 수 있는지 잘 보여줍니다. 이 강좌의 후반에 우리는 흥비로운 효과들을 다룰 것입니다. 하지만 지금은 간단한 geometry shader를 생성하는 것부터 시작겠습니다.

Geometry shader 사용하기

  Geometry shader 사용을 설명하기 위해 NDC 좌표상의 z 평면에 4개의 점을 그리는 아주 간단한 scene을 렌더링할 것입니다. 이 점들의 좌표는 다음과 같습니다.


float points[] = {
	-0.5f,  0.5f, // 좌측 상단
	 0.5f,  0.5f, // 우측 상단
	 0.5f, -0.5f, // 우측 하단
	-0.5f, -0.5f  // 좌측 하단
};  

  이 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 데이터들에 대한 VAO와 VBO를 생성하고 glDrawArrays 함수를 사용하여 이 것들을 렌더링합니다.


shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4); 

  결과는 검은 배경에 (보기 힘든) 4개의 녹색 점들입니다.

4 Points drawn using OpenGL

  하지만 이 모든 것들은 이미 배우지 않았습니까? 맞습니다. 그리고 이제 이 scene에 geometry shader를 추가해볼 것입니다.

  교육 목적으로 우리는 pass-through라고 불리는 geometry shader를 생성할 것입니다. 이 shader는 입력으로 점 기본 타입을 받고 수정하지 않은채로 다음 shader로 pass(전달)합니다.


#version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;

void main() {    
    gl_Position = gl_in[0].gl_Position; 
    EmitVertex();
    EndPrimitive();
}  

  이제 이 geometry shader는 이해하기 쉬울 것입니다. 간단히 수정하지 않은 vertex 위치를 방출하고 점 기본 타입 도형을 생성합니다.

  Geometry shader는 vertex, fragment shader와 마찬가지로 컴파일된 후 program에 연결되어야 합니다. Shader 타입을

GL_GEOMETRY_SHADER

로 설정한다는 점만 다릅니다.


geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);  
...
glAttachShader(program, geometryShader);
glLinkProgram(program);  

  이 shader 컴파일 코드는 기본적으로 vertex, fragment shader와 동일합니다. 컴파일 혹은 링킹 에러를 확인하세요.

  지금 컴파일한 후 실행시켜보면 다음과 같은 것을 볼 수 있을 것입니다.

4 Points drawn using OpenGL (with geometry shader this time!)

  이 것은 정확히 geometry shader가 없을 때와 동일합니다! 좀 재미 없지만 사실은 우리가 점을 그릴 수 있다는 것은 geometry shader가 작동한다는 것을 의미합니다. 이제 좀 더 재미난 것을 해봅시다!

집을 지어봅시다

  점과 선들을 그리는 것은 흥미롭지 않으므로 우리는 geometry shader를 사용하여 각 점의 위치에 작고 독창적인 집을 그릴 것입니다. Geometry shader의 출력으로 triangle_strip을 설정하고 총 3개의 삼각형을 그려 이를 수행할 수 있습니다. 2개는 사각형을 위한 것이고 나머지 하나는 지붕입니다.

  OpenGL의 triangle strip은 적은 vertex들을 가지고 삼각형을 그리는 좀 더 효율적인 방법입니다. 첫 번째 삼각형이 그려진 후 그 후의 vertex들은 첫 번째 삼각형 옆에 다른 삼각형을 생성합니다. 모든 3개의 인접한 vertex들은 삼각형을 형성합니다. 총 6개의 vertex를 가지고 있다면 다음과 같은 삼각형들을 얻습니다: (1,2,3), (2,3,4), (3,4,5), (4,5,6) 총 4개의 삼각형입니다. Triangle strip은 최소 3개의 vertex를 필요로 하고 N-2개의 삼각형을 생성할 것입니다. 6개의 vertex로 6-2 = 4개의 삼각형을 생성합니다. 다음 이미지는 이를 설명해줍니다.

Image of a triangle strip with their index order in OpenGL

  Triangle strip를 geometry shader의 출력으로 사용하여 올바른 순서로 3개의 인접한 삼각형을 생성함으로써 쉽게 집 모양을 만들 수 있습니다. 다음 그림은 파란 점이 입력받은 점일 경우 어떠한 순서로 vertex를 그려야 우리가 필요한 삼각형을 얻을 수 있는지 보여줍니다.

How a house figure should be drawn from a single point using geometry shaders

  이는 다음과 같은 geometry shader로 변환됩니다.


#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);
}  

  이 geometry shader는 5개의 vertex를 생성하고 각 vertex들은 점의 위치에 offset를 더한 곳에 위치하여 하나의 큰 triangle strip을 형성합니다. 최종 기본 타입 도형은 래스터라이즈화되고 fragment shader가 전체 triangle strip에 실행됩니다. 결과적으로 각 점에 대해 녹색 집을 그리게됩니다.

Houses drawn with points using geometry shader in OpenGL

  각각의 집은 3개의 삼각형으로 이루어져 있는 것을 볼 수 있습니다. 하나의 점으로 인해서 말이죠. 이 녹색 집은 약간 지루해보이므로 고유한 색을 추가해봅시다. 이를 수행하기 위해 vertex의 color 정보를 가지고 있는 vertex attribute를 추가할 것입니다. 이는 vertex shader에서 geometry shader를 거쳐 fragment shader로 향할 것입니다.

  수정된 vertex 데이터는 다음과 같습니다.


float points[] = {
    -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // 좌측 상단
     0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // 우측 상단
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 우측 하단
    -0.5f, -0.5f, 1.0f, 1.0f, 0.0f  // 좌측 하단
};  

  그런 다음 interface block을 사용하여 color attribute를 geometry shader로 보낼 수 있도록 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;
}  

  그런 다음 geometry shader에 동일한 interface block을 선언해야 합니다.


in VS_OUT {
    vec3 color;
} gs_in[];  

  이 geometry shader는 입력으로 받은 vertex들의 모음 위에서 동작하기 때문에 vertex shader으로부터 받은 입력 데이터는 항상 배열 형태로 나타내집니다. 우리가 오직 하나의 vertex만 가지고 있더라도 말이죠.

  우리는 geometry shader에 데이터를 보낼 때 꼭 interface block을 쓰지 않아도 됩니다. 우리는 다음과 같이 작성할 수 있습니다.


in vec3 vColor[];

  Vertex shader가 out vec3 vColor 코드로 color 벡터를 보냈다면 말이죠. 하지만 interface block은 geometry shader와 같은 shader와 같이 작업하기가 더욱 수월합니다. 실제로 geometry shader 입력은 크고 그룹화된 하나의 큰 interface block의 배열로 받는 경우가 많습니다.

  그런 다음 다음 fragment shader를 위한 출력 color 벡터를 선언해야합니다.


out vec3 fColor;  

  이 fragment shader가 오직 하나의 (보간된) color만 원하기 때문에 여러가지 색을 보낸다는 것은 말이 안됩니다. 따라서

fColor

벡터는 배열이 아니라 단일 벡터입니다. Vertex를 방출할 때 각 vertex는

fColor

에 마지막으로 저장된 값을 fragment shader 실행시 사용하게 됩니다. 따라서 집의 경우에 집 전체를 칠하기 위해 첫 번째 vertex가 방출되기 전에

fColor

벡터를 한 번만 채우면 됩니다.


fColor = gs_in[0].color; // 오직 하나의 입력 vertex만 존재하기 때문에 gs_in[0]
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:좌측 하단   
EmitVertex();   
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:우측 하단
EmitVertex();
gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:좌측 상단
EmitVertex();
gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:우측 상단
EmitVertex();
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:꼭대기
EmitVertex();
EndPrimitive();  

  방출된 모든 vertex들은

fColor

에 마지막으로 저장된 값을 가지고 있습니다. 모든 집들은 이제 그들 고유의 색을 가지게 될 것입니다.

Colored houses, generating using points with geometry shaders in OpenGL

  재미삼아 마지막 vertex의 색을 흰색으로 설정하여 겨울철 지붕에 약간의 눈이 쌓인 것처럼 표현할 수 있습니다.


fColor = gs_in[0].color; 
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:좌측 하단   
EmitVertex();   
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:우측 하단
EmitVertex();
gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:좌측 상단
EmitVertex();
gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:우측 상단
EmitVertex();
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:꼭대기
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();  

  최종적으로 다음과 같이 보일 것입니다.

Snow-colored houses, generating using points with geometry shaders in OpenGL

  여기에서 전체 소스 코드를 확인할 수 있습니다.

  Geometry shader를 사용하여 아주 간단한 기본 타입 도형으로 꽤 창의적인 것을 얻을 수 있습니다. 이 도형들은 아주 빠른 GPU 위에서 동적으로 생성되기 때문에 vertex buffer 내부에서 여러분 스스로 이러한 도형들을 정의하는 것보다 효율적입니다. 그러므로 Geometry buffer는 voxel 세계의 큐브나 큰 필드의 풀처럼 자주 반복되는 간단한 도형들을 최적화하는 데에 아주 훌륭한 도구 입니다.

오브젝트 폭파

  집을 그리는 것이 재밌었던 반면에 우리가 많이 사용하지는 않을 것 같습니다. 이 것이 우리가 한단계 높여 오브젝트를 폭파시키려고 하는 이유입니다! 이 또한 우리가 자주 사용할 것 같지는 않습니다만 geometry shader의 힘을 보여줄 수 있을 것입니다.

  오브젝트를 폭파시킨다고 말할 때 우리는 실제로 우리의 소중한 vertex들을 날려버리지는 않을 것입니다. 하지만 각 삼각형들을 그들의 법선 벡터 방향으로 시간에 따라 이동시킬 것입니다. 이 효과는 전체 오브젝트의 삼각형들이 그들의 법선 벡터 방향으로 폭발하는 것처럼 보입니다. nanosuit model에 이 효과를 적용시켜보면 다음과 같습니다.

Explosion effect with geometry shaders in OpenGL

  이러한 geometry shader 효과의 멋진 점은 이 것이 모든 오브젝트에 대해서 작동한다는 점입니다. 그들의 복잡성과는 상관없이 말이죠.

  각 vertex들을 삼각형의 법선 벡터 방향으로 이동시킬것이기 때문에 먼저 이 법선 벡터를 계산해야합니다. 우리가 해야할 일은 삼각형의 면에 수직적인 벡터를 계산하는 것입니다. 오직 3개의 vertex를 사용해서 말이죠. 변환 강좌에서 다른 두 벡터에 수직하는 벡터를 cross product(외적)을 사용하여 얻을 수 있다고 배운것을 기억할 것입니다. 삼각형의 면에 평행하는

a

벡터와

b

벡터를 얻는다면 우리는 이 벡터들을 외적하여 법선 벡터를 구할 수 있습니다. 다음 geometry shader 함수는 정확히 이를 수행하여 3개의 입력 vertex 좌표로부터 법선 벡터를 구합니다.


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

를 바꾸어서 crosse 함수를 수행하면 반대 방향의 법선 벡터를 구하게 됩니다. 여기에서 순서는 매우 중요합니다!

  이제 우리는 법선 벡터를 계산하는 방법을 알았으므로 이 법선 벡터와 vertex의 위치 벡터를 파라미터로 받는 explode 함수를 생성할 수 있습니다. 이 함수는 위치 벡터를 법선 벡터 방향으로 이동시키는 새로운 벡터를 리턴합니다.


vec4 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.01.0 사이의 값을 리턴합니다. 우리는 폭파하여 다시 안쪽으로 모이는 것을 원하지 않기 때문에 이 sin 값을 [0,1] 범위로 변환합니다. 최종적인 값은

normal(법선)

벡터와 곱해지고 최종

direction(방향)

벡터는 위치 벡터에 더해집니다.

  완성된 geometry shader는 다음과 같을 것입니다.


#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;

vec4 explode(vec4 position, vec3 normal) { ... }

vec3 GetNormal() { ... }

void main() {    
    vec3 normal = GetNormal();

    gl_Position = explode(gl_in[0].gl_Position, normal);
    TexCoords = gs_in[0].texCoords;
    EmitVertex();
    gl_Position = explode(gl_in[1].gl_Position, normal);
    TexCoords = gs_in[1].texCoords;
    EmitVertex();
    gl_Position = explode(gl_in[2].gl_Position, normal);
    TexCoords = gs_in[2].texCoords;
    EmitVertex();
    EndPrimitive();
}  

  또한 vertex를 방출하기 전에 적절한 텍스처 좌표를 출력해야한다는 것을 알아두세요.

  또한 실제로 OpenGL 코드에서

time

변수를 설정하는 것을 잊지마세요.


shader.setFloat("time", glfwGetTime());  

  결과는 시간에 따라 계속해서 폭파하는 것처럼 보이는 3D model입니다. 실제로 아주 유용하지는 않지만 geometry shader 사용을 좀 더 알 수 있도록 해주었을 것입니다. here에서 전체 소스 코드를 확인할 수 있습니다.

법선 벡터 시각화

  이번 섹션에서는 실제로 유용한 예제를 보여드릴 것입니다. 오브젝트의 법선 벡터를 시각화하는 것입니다. 조명 shader를 프로그래밍할 때 여러분은 결국 이상한 시각 출력을 실행하게 됩니다. 조명 에러의 흔한 오류는 부적절하게 vertex 데이터를 불러오거나 vertex attribute를 잘못 지정해서 혹은 shader에서 잘못 관리하여 생긴 부정확한 법선 벡터때문에 일어납니다. 우리가 원하는 것은 우리가 제공한 법선 벡터들이 정확한지 판별할 수 있는 어떠한 방법입니다. 법선 벡터가 정확한지 결정하는 훌륭한 방법은 그들을 시각화하는 것입니다. 그리고 이러한 목적에 geometry shader가 아주 유용한 도구입니다.

  이 개념은 다음과 같습니다. 먼저 geometry shader 없이 법선을 사용하여 scene을 그리고 또 scene을 그리는데 이번에는 geometry shader를 통해 생성한 법선 벡터만을 출력합니다. 이 geometry shader는 입력으로 삼각형 기본 타입을 받아들이고 3개의 법선 벡터의 방향을 선으로 생성합니다. 각 vertex 당 하나의 법선 벡터입니다. 의사 코드로 다음과 같습니다.


shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();

  이번에는 우리가 생성한 것이 아닌 model로 부터 제공된 vertex 법선을 사용하는 geometry shader를 생성할 것입니다. scale과 회전을 수용하기 위해 (view, model 행렬 때문에) 우리는 법선을 먼저 clip-space 좌표로 변환하기 전에 법선 행렬을 사용하여 변환합니다(geometry shader는 clip-space 좌표로서 위치 벡터를 입력받으므로 법선 벡터를 동일한 space로 변환해주어야 합니다). 이 모든 것은 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 법선 벡터는 interface block을 통해 다음 shader로 넘겨집니다. 이 geometry shader는 각 vertex들을 받고(위치, 법선 벡터와 함께) 각 위치 벡터로부터 법선 벡터를 그립니다.


#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()
{
    GenerateLine(0); // 첫 번째 vertex 법선
    GenerateLine(1); // 두 번째 vertex 법선
    GenerateLine(2); // 세 번째 vertex 법선
}  

  이러한 geometry shader의 내용은 지금은 따로 설명할 필요가 없습니다. 법선 벡터를

MAGNITUDE

벡터와 곱하여 출력할 법선 벡터의 크기를 규제한다는 것을 알아두세요(그렇지 않으면 너무 클것입니다).

  법선들을 시각화하는 것은 거의 디버깅 목적으로 사용되기 때문에 우리는 단지 법선들을 모노 컬러 선(혹은 원한다면 아주 화려한 선)을 출력할 수 있습니다.


#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}  

  이제 여러분의 모델을 평소의 shader들로 먼저 렌더링한 다음 특별한 normal-visualizing shader로 렌더링해보세요.

Image of geometry shader displaying normal vectors in OpenGL

  우리의 nanosuit가 이제 고무 장갑을 낀 털이 많은 남자처럼 보이는 것을 제외하면 법선 벡터가 정확한지 결정할 수 있는 아주 유용한 방법을 제공해줍니다. 이러한 geometry shader가 오브젝트에 fur을 추가할 때 자주 쓰인다는 것을 알 수 있을 것입니다.

  여기에서 전체 소스 코드를 확인할 수 있습니다.

 

 

출처 : https://learnopengl.com, Joey de Vries

반응형