눈팅하는 게임개발자 블로그

OpenGL Advanced 5-9 Geometry Shader 본문

공부한거/OpenGL

OpenGL Advanced 5-9 Geometry Shader

Palamore 2020. 12. 11. 20:11

원문 사이트

learnopengl.com/Advanced-OpenGL/Geometry-Shader

 

LearnOpenGL - Geometry Shader

Geometry Shader Advanced-OpenGL/Geometry-Shader Between the vertex and the fragment shader there is an optional shader stage called the geometry shader. A geometry shader takes as input a set of vertices that form a single primitive e.g. a point or a trian

learnopengl.com

번역 사이트

heinleinsgame.tistory.com/34?category=757483

 

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

Geometry Shader 고급 OpenGL/Geometry Shader Vertex shader와 fragment shader 사이에 geometry shader 라고 불리는 선택적인 shader 단계가 존재합니다. Geometry shader는 입력으로 예를 들어 점이나 삼각형같..

heinleinsgame.tistory.com

Geometry Shader

Vertex Shader와 fragment shader 사이에 geometry shader라고 불리는 선택적인 shader 단계가 존재한다.

Geometry shader가 받는 입력은 vertex들의 모음인데,

예를 들어 점이나 삼각형 같은 하나의 기본 도형을 이루는 vertex들의 모음을 받는다.

이 geometry shader는 이 vertex들을 다음 shader 단계에 이들을 보내기 전에 적절한 형태로 변환시킬 수 있다.

하지만 geometry shader가 만드는 흥미로운 점은 vertex들을 원래 주어진 vertex들보다 더 많은

vertex들을 생성하는 완전히 다른 기본  타입 도형으로 변환시킬 수 있다는 점이다.

 

먼저 geometry의 예제를 살펴보자.

#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들을 출력한다.

 

이 geomtry shader는 출력할 vertex의 최대 갯수를 설정해줄 것을 요구한다.(만약 이 숫자를 초과한다면

OpenGL은 추가적인 vertex들은 그리지 않는다.)

이를 out 키워드 앞의 layout 식별자 안에서 수행할 수 있다.

이 경우 line_strip으로 출력하며 vertex의 최대 갯수는 2개로 설정한다.

 

Line strip은 하나의 연속적인 선을 이루는 최소한 2개의 점의 모음을 서로 묶은 것이다.

렌더링 명령에 넘겨진 추가적인 점들은 그 전의 점과 새로운 선을 형성한다.

다음 그림은 5개의 vertex들을 가지고 있을 때의 line strip이다.

현재 shader에서는 vertex의 최대 갯수를 2로 설정했기 때문에 오직 하나의 선만 그릴 수 있다.

 

의미있는 결과를 생성하기 위해 이 전의 shader 단계에서 출력을 얻는 어떠한 방법이 필요하다.

GLSL은 gl_in이라고 불리는 내장 변수를 제공해준다.

이 변수는 내부적으로 (아마도) 다음과 같은 형태를 가지고 있을 것이다.

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

gl_in은 흥미로운 몇가지 변수를 포함하고 있는 interface block으로 선언되어 있다.

그 중에서 가장 흥미로운 것은 vertex shader의 출력으로 설정하는 벡터와 비슷한 gl_Position이다.

 

gl_in이 배열로 선언되어 있다는 점을 유념해야 한다.

대부분의 기본 타입 도형들은 하나 이상의 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개의 vertex를 하나의 line strip으로 결합시킨다.

 

이제 geotmery shader가 어떻게 동작하는지 알았으니 아마 이 geometry shader가 무엇을 할 수 있는지를

알아보자. 이 geometry shader는 입력으로 점 기본 타입 도형을 받고 중앙에 수평선 기본 타입 도형을 생성한다.

이를 렌더링 한다면 다음과 같은 것을 볼 수 있다.

아직까지 아주 인상적인 것은 없다.

하지만 이 출력이 다음의 렌더링 호출만으로 생성된 출력이라는 것을 생성하면 흥미로울 것이다.

glDrawArrays(GL_POINTS, 0, 4);  

이는 비교적 간단한 예제인 반면에 어떻게 geometry shader를 사용하여 새로운 도형을 만들 수 있는지 잘 보여준다.

이 강좌의 후반에 흥미로운 효과들을 다룰 것이다.

하지만 지금은 간단한 geomtry shader를 생성하는 것부터 시작한다.

 

Using a geomtry 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개의 녹색 점들이다.

그리고 이제 이 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와 동일하다.

컴파일 혹은 링킹 에러를 확인해야 한다.

지금 컴파일한 후 실행시켜보면 결과는 다음과 같다.

이는 정확히 geometry shader가 작동하지 않을 때와 동일한 결과이다.

재미는 없지만 사실은 점을 그릴 수 있다는 것은 geometry shader가 작동한다는 것을 의미한다.

 

Let's build houses

점과 선들을 그리는 것은 흥미롭지 않으므로 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개의 삼각형을 생성한다. 다음 이미지는 이를 설명해준다.

triangle strip을 geometry shader의 출력으로 사용하여 올바른 순서로 3개의 인접한 삼각형을 생성함으로써

쉽게 집 모양을 만들 수 있다.

다음 그림은 파란 점이 입력받은 점일 경우 어떠한 순서로 vertex를 그려야 필요한 삼각형을 얻을 수 있는지를 보여준다.

이는 다음과 같은 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에 실행된다.

결과적으로 각 점에 대해 녹색 집을 그리게 된다.

각각의 집은 하나의 점에서 발생된 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을 쓰지 않아도 된다.
Vertex shader가 out vec3 vColor 코드로 color 벡터를 보냈을 경우에는 다음과 같이 작성할 수도 있다.
in vec3 vColor[];
하지만 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에 마지막으로 저장된 값을 가지고 있다.

모든 집들은 이제 그들 고유의 색을 가지게 될 것이다.

재미삼아 마지막 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(); 

최종적으로 결과는 다음과 같다.

Geometry shader를 사용하여 아주 간단한 기본 타입 도형으로 꽤 창의적인 것을 얻을 수 있다.

이 도형들은 아주 빠른 GPU 위에서 동적으로 생성되기 때문에 vertex buffer 내부에서 스스로 이러한 도형을

정의하는 것보다 효율적이다.

그러므로 geometry buffer는 voxel 세계의 큐브나 큰 필드의 풀처럼 자주 반복되는 간단한 도형들을 최적화하는 데에

아주 훌륭한 도구이다.

 

Exploding objects

집을 그리는 것이 재밌었던 반면에 이는 실제 많이 사용될 것 같지는 않다.

이제 한 단계 수준 높은 오브젝트를 폭파하는 것을 해보자.

이 또한 자주 사용될 것 같지는 않지만 geometry shader의 힘을 보여줄 수 있을 것이다.

 

오브젝트를 폭파시킨다고 할 때 실제로 소중한 vertex들을 날려버리지는 않을 것이다.

하지만 각 삼각형들을 그들의 법선 벡터 방향으로 시간에 따라 이동시킬 수 있다.

이 효과는 전체 오브젝트의 삼각형들이 그들의 법선 벡터 방향으로 폭발하는 것처럼 보인다.

nanosuit model에 이 효과를 적용시켜보면 다음과 같다.

이러한 geometry shader 효과의 멋진 점은

이것이 그들이 얼마나 복잡한지에 상관없이 모든 오브젝트에 대해서 작동한다는 점이다.

 

각 vertex들을 삼각형의 법선 벡터 방향으로 이동시킬 것이기 때문에 먼저 이 법선 벡터를 계산해야 한다.

오직 3개의 vertex를 사용해서 각 삼각형들의 면에 수직적인 벡터를 계산해야 한다.

변환 강좌에서 다른 두 벡터에 수직하는 벡터를 cross product(외적)을 사용하여 얻을 수 있다고 배웠다.

삼각형의 면에 평행하는 a 벡터와 b 벡터를 얻는다면 이 벡터들을 외적하여 법선 벡터를 구할 수 있다.

다음 geometry shader 함수는 정확히 이를 수행하여 입력 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를 바꾸어서 cross 함수를 수행하면 반대 방향의 법선 벡터를 구하게 된다.

이 경우 순서는 매우 중요하다.

 

이제 법선 벡터를 계산하는 방법을 알았으므로 이 법선 벡터와 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.0과 1.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 사용을 좀 더 알 수 있도록 해줬을 것이다.

결과는 다음과 같다.

 

Visualizing Normal Vector

이번 섹션에서는 실제로 유용한 예제를 다루어본다. 오브젝트의 법선 벡터를 시각화하는 것이다.

조명 shader를 프로그래밍할 때 결국 이상한 시각 출력을 실행하게 된다.

조명 에러의 흔한 오류는 부적절하게 vertex 데이터를 불러오거나 vertex attribute를 잘못 지정해서

혹은 shader에서 잘못 관리하여 생긴 부정확한 법선 벡터 때문에 일어나고는 한다.

여기서 얻어야 할 것은 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);
}  

결과는 다음과 같다.

위의 결과물이 동물의 털처럼 보이지는 않지만

실제로 geometry shader는 오브젝트에 fur을 추가할 때 자주 쓰인다.

 

'공부한거 > OpenGL' 카테고리의 다른 글

OpenGL Advanced 5-11 Anti aliasing  (0) 2020.12.12
OpenGL Advanced 5-10 Instancing  (0) 2020.12.12
OpenGL Advanced 5-8 Advanced GLSL  (0) 2020.12.11
OpenGL Advanced 5-7 Advanced Data  (0) 2020.12.10
OpenGL Advanced 5-5 Framebuffers  (0) 2020.12.10