momodudu.zip

#9 Geometry shader 본문

Graphics/OpenGL

#9 Geometry shader

ally10 2022. 8. 2. 13:01

vertex shader <-> fragment shader 사이에는 optional programmble stage인 geometry shader가 존재한다. geometry shader는 하나의 primitive를 구성하는 vertex set을 받을 수 있다. Geometry shader는 이 입력으로 받은 primitive의 vertex들을 변형하고, 다음 stage로 보낸다. 여기서 Geometry shader는 단순히 primitive vertex만을 바꿔서 보내는게 아니라, primitive 자체를 바꿔서 완전히 다른 primitive로 다음 스테이지로 보낼 수 있다. 예를 들어서, Geometry shader stage로 들어온 Input Primitive는 Triangles였지만, Geometry shader에서 이를 line strip으로 바꿔서 내보낼 수 있다.

#version 330 core
layout (points) in; 				// input primitive type
layout (line_strip, max_vertices = 2) out;	// output primitive type

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로부터 받아올 input primitive type을 지정할 수 있다. layout specifier - primitive type - in/out을 지정해주면 된다. primitive type은 아래의 네가지중에서 어떤것이든 사용할 수 있다.

 

- 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와 같은 렌더링 콜에서 설정할 수 있는 primitive type들이다. 즉, glDrawArrays와 같은 콜에서 설정한 primitive type들이 이 geometry의 in layout의 타입과 일치해야한다. 괄호 안의 숫자들은 하나의 primitive가 포함하는 최소한의 vertex 개수이다.

 

geometry shader에서 나가는 primitive type또한 지정해 줄 수 있다. output은 

- points

- line_strip

- triangle_strip

으로 설정할 수 있다. 만약 1개의 삼각형을 그리고 싶다면, type을 triangle_strip으로 설정하고, output 숫자는 3으로 지정하면 하나의 삼각형을 그릴 수 있다. 이 세개의 output primitive type만으로도 거의 모든 형태의 shape을 만들 수 있다. 

Geometry shader에서는 또한 output으로 나가는 최대 vertex 개수를 지정할수도 있다. 만약 이 개수를 초과하면, OpenGL에서는 추가 vertex는 그리지 않는다. 이 max값 지정 또한 layout 선언문에 같이 지정할 수 있다.

 

Geometry shader로 들어오는 built-in variable gl_in[] 을 좀더 살펴보자.

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

gl_in은 몇가지 변수들이 묶인 interface block으로 선언되어있는데, 그중에서 gl_Position은 vertex shader로부터 넘어온 vertex shader의 output vector이다. Geometry shader의 input은 Primitive이며, point를 제외하고는 대부분의 primitive는 여러개의 vertex로 이루어지므로 array로 선언되어있다. 

 

Geometry shader에서는 EmitVertex() 및 EndPrimivite() 두 개의 함수를 이용해서, vertex shader로부터 나온 vertex data를 이용해서, 새로운 data를 만들어 낼 수 있다. Geometry Shader를 이용해서, 최소한 1개 이상의 명시해둔 primitive type의 output이 나와야한다. 아래의 코드의 경우에는, vertex shader로부터 points를 받아서, 최소한 한개 이상의 Line_strip을 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();
}

EmitVertex()는 호출할때마다 gl_Position에 저장된 벡터값들이 output primitive에 추가된다. EndPrimitive()가 불릴때마다, 추가되었던 vertex모두 지정해두었던 output primitive 하나를 생성하게 된다. 즉, EndPrimitive()가 여러번 호출되면 여러개의 primitive가 나오게 될 것 이다. 위 예제에서는, input으로 들어온 두개의 vertex에 아주 작은 offset을 주어서 이 두개를 primitive로 지정한다. 이 일련의 과정을 거쳐서 2개의 vertex가 emit되고, 하나의 line strip을 구성하게 된다.

즉, 이 과정 자체가 geometry shader의 역할이라고 볼 수 있다. vertex shader로 들어가기전 glDrawArrays 콜에서도 GL_POINTS 타입으로 파이프라인 안으로 들어가기 시작했을것이고, 또한 vertex shader stage를 나갔던 primitive type은 points였으나, geometry shader를 거치면서 Line strip으로 완전히 바뀐다. 

 

이제 예제들을 살펴보자.

 

Using Geometry Shaders

간단한 예제를 살펴보기 위해, NDC z-plane위에 있는 4개의 point를 아래와 같이 선언한다.

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는 아래와 같이 작성한다.

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

 

아래와 같이 gl_points로 draw를 해주면, 초록색의 아주 작은 점이 4개 찍힌다.

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

 

이제 geometry를 작성해보자. 일단 처음에는 Pass-through, 즉 들어온 points를 그대로 points로 내보내는 shader를 작성해보자. position하나마다 emit을 하고, points로 내보낼것이므로 emit을 하고 바로 EndPrimitive()를 호출한다.

#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 shader나 fragment shader와 같이 compile하고, program에 link해야한다.

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

 

Let's build houses

이제 input points를 받아서 triangle strip으로 shape을 만드는 예제를 따라가보자.

파란색깔 point 하나만을 input으로 받아서, 위와 같은 형태의 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);
}

input은 point primitive이므로 이 쉐이더 프로그램에 input vertex는 한개만 들어오게 될 것이다. 이 한개만 가지고 build_house()에서 오프셋을 주어서, max개수가 5개인 triangle strip을 생성한다. 

 

 

이제 각각의 shape마다 색깔을 다르게 줘보자.

 

 

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

총 4개의 house가 그려지는데, 이 4개의 house의 중심점(geometry로 들어가는 Input)마다 색깔을 다르게 지정해준다.

 

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

// Geometry shader로 보낼 interface block선언
out VS_OUT {
    vec3 color;
} vs_out;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    vs_out.color = aColor;
}

위와 같이 color값을 in attribute로 받고, 다음 스테이지로 그대로 전달한다. 여기서 다음 스테이지는 물론 geometry shader를 의미한다!

 

in VS_OUT {
    vec3 color;
} gs_in[];

그리고 똑같이 geometry shader의 in에도 들어오는 컬러값 interface block을 선언해준다. 물론 지금 해당 예제에서는 들어오는 input이 한개밖에 없지만, 기본적으로 geometry shader가 실행되는 단위는 primitive, 즉 여러개의 vertex이므로 interface block을 gs_in[]과 같이 항상 array형태로 선언해준다.

 

out vec3 fColor;

그리고 fragment shader로 내보낼 out 벡터값도 선언해준다.

 

fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
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();

fragment shader는 "하나의 color"만을 받으므로, gs_in 의 컬러값을 그대로 넣을 수 없다. 따라서 out fColor역시 1개의 값이며, geometry shader에서 내보낼때도 gs_in[0]의 컬러값에 접근해서 컬러값을 설정해주어야 한다.

 

혹은 또다른 응용으로, 각 house shape의 꼭대기 top점에만 컬러값을 다르게 주어서 색다른 효과를 줄수도 있다.

 

fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
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, 1.0, 1.0);
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:top
EmitVertex();
EndPrimitive();

 

이렇게 primitive를 구성하는게 gpu단에서 이루어지므로, 이러한 shape들을 vertex buffer를 세팅하는 단계에서 만드는 것 보다 쉐이더에서 만드는게 더 빠르고 강력하다. Geometry shader는 주로 간단하며, 반복되는 큐브, 나뭇잎과 같은 object를 그릴 때 매우 유용하다.