Geometry shader을 알아보자.
Geometry shader는 TA(Triangle assembly) 이루 얻은 삼각형들을 가지고 각 삼각형들에 대해 연산을 처리해주는 모듈로,
Vertex shader에서는 접근할 수 없었던 정보들에 접근 가능하다.
TA에서 조립된 삼각형들에는 id가 부여되는데, Geometry shader에서는 이 id와 함께 그 삼각형에 포함된 각 정점들의 정보를 얻을 수 있다.
Geometry shader의 차별점은 한 가지 더 있는데, 기존 Vertex shader에서 어떤 한 정점의 성질을 바꾸면 그 정점을 indices로 갖는 모든 삼각형들이 영향을 받는 것에 반해,
Geometry shader는 이미 개별적인 삼각형으로 분리가 된 상태로 정보들을 받아오기 때문에 설사 원본이 같은 vertex였다고 하더라도 삼각형별로 개별적 조작이 가능하다는 장점이 있다.
Geometry shader를 구현하기 위해 pipeline의 구조를 바꿔야 하는데, 사실 바로 전에 다뤘던 vertex shader를 추가할 때와 상당히 유사하므로 가볍게만 다뤄보겠다.
Geometry shader도 vertex shader처럼 입력으로 받은 삼각형들을 구성하는 정점과 반환하는 삼각형들을 구성하는 정점이 다를 수 있기 때문에
GSOut이라는 템플릿을 정의해둔다.
파이프라인의 ProcessTriangle 함수에 Geometry shader를 거치도록 기능을 추가한다.
또 Geometry shader를 거친 이후의 모든 Vertex에 대한 자료형은 이제 VSOut이 아니라 GSOut을 사용하도록 바꿔준다.
물론 그 이전의 자료형은 VSOut 그대로 사용해주면 된다.
// Pipeline.h
void AssembleTriangles( const std::vector<VSOut>& vertices,const std::vector<size_t>& indices ) // VSOut을 사용한다
{
// assemble triangles in the stream and process
for( size_t i = 0,end = indices.size() / 3;
i < end; i++ )
{
// determine triangle vertices via indexing
const auto& v0 = vertices[indices[i * 3]];
const auto& v1 = vertices[indices[i * 3 + 1]];
const auto& v2 = vertices[indices[i * 3 + 2]];
// cull backfacing triangles with cross product (%) shenanigans
if( (v1.pos - v0.pos) % (v2.pos - v0.pos) * v0.pos <= 0.0f )
{
// process 3 vertices into a triangle
ProcessTriangle( v0,v1,v2,i ); // i를 triangle_index로 넘겨준다. 이때 모든 삼각형들이 다 id를 가지는데,
// 이는 즉 hull 된 삼각형들(즉 카메라에 안잡혀서 렌더링되지 않는 삼각형들)도 인덱스에 포함됨을 의미하고,
// 이는 다시말해 렌더링되는 삼각형들의 id가 띄엄띄엄 분포되어있을 수도 있다는 것을 의미한다.
}
}
}
// triangle processing function
// passes 3 vertices to gs to generate triangle
// sends generated triangle to post-processing
void ProcessTriangle( const VSOut& v0,const VSOut& v1,const VSOut& v2,size_t triangle_index )
{
// generate triangle from 3 vertices using gs
// and send to post-processing
PostProcessTriangleVertices( effect.gs( v0,v1,v2,triangle_index ) ); // gs를 거쳐간다
}
void PostProcessTriangleVertices( Triangle<GSOut>& triangle ) // GSOut을 사용한다
// ...
각 Effect들에 대해 예전 vs를 추가해줄때와 마찬가지로 Geometry shader를 추가해준다.
이로써 Geometry shader가 완성되었다!
Geometry shader를 실제로 사용해보자.
GS를 이용해 기존에 우리가 만들었던 각 단면이 단색인 큐브를 더 적은 메모리를 사용하게끔 최적화할 수 있는데, 기존에 우리는 이 큐브를 만들 때 위치가 중복되는 점이더라도 최대 세 가지 다른 색상을 칠해야 했기 때문에 indices로 최적화하지 못하고 그냥 정점들을 개별적으로 저장했었다. (즉 큐브 하나에 정점 24개가 필요했었다)
이는 색상을 정점 단위로 저장했기 때문에 발생한 문제인데, 이를 정점 단위로 저장하는 대신, 셰이더 단위에서 동적으로 특정 폴리곤에 적합한 색상을 지정해주는 것으로 중복 정점들이 저장되지 않아도 되게끔 만들어 메모리 사용을 줄일 수 있다.
코드와 함께 살펴보자.
// SolidGeometryEffect.h
#pragma once
#include "Pipeline.h"
#include "DefaultVertexShader.h"
// solid color attribute taken from table in gs and not interpolated
class SolidGeometryEffect
{
public:
// the vertex type that will be input into the pipeline
class Vertex
{
public:
Vertex() = default;
Vertex( const Vec3& pos )
:
pos( pos )
{}
Vertex( const Vec3& pos,const Vertex& src )
:
pos( pos )
{}
Vertex& operator+=( const Vertex& rhs )
{
pos += rhs.pos;
return *this;
}
Vertex operator+( const Vertex& rhs ) const
{
return Vertex( *this ) += rhs;
}
Vertex& operator-=( const Vertex& rhs )
{
pos -= rhs.pos;
return *this;
}
Vertex operator-( const Vertex& rhs ) const
{
return Vertex( *this ) -= rhs;
}
Vertex& operator*=( float rhs )
{
pos *= rhs;
return *this;
}
Vertex operator*( float rhs ) const
{
return Vertex( *this ) *= rhs;
}
Vertex& operator/=( float rhs )
{
pos /= rhs;
return *this;
}
Vertex operator/( float rhs ) const
{
return Vertex( *this ) /= rhs;
}
public:
Vec3 pos;
};
// default vs rotates and translates vertices
// does not touch attributes
typedef DefaultVertexShader<Vertex> VertexShader;
// gs colors vertices using their index from a table
// every two triangles are colored from the same entry
class GeometryShader
{
public:
class Output
{
public:
Output() = default;
Output( const Vec3& pos )
:
pos( pos )
{}
Output( const Vec3& pos,const Output& src )
:
color( src.color ),
pos( pos )
{}
Output( const Vec3& pos,const Color& color )
:
color( color ),
pos( pos )
{}
Output& operator+=( const Output& rhs )
{
pos += rhs.pos;
return *this;
}
Output operator+( const Output& rhs ) const
{
return Output( *this ) += rhs;
}
Output& operator-=( const Output& rhs )
{
pos -= rhs.pos;
return *this;
}
Output operator-( const Output& rhs ) const
{
return Output( *this ) -= rhs;
}
Output& operator*=( float rhs )
{
pos *= rhs;
return *this;
}
Output operator*( float rhs ) const
{
return Output( *this ) *= rhs;
}
Output& operator/=( float rhs )
{
pos /= rhs;
return *this;
}
Output operator/( float rhs ) const
{
return Output( *this ) /= rhs;
}
public:
Vec3 pos;
Color color;
};
public:
Triangle<Output> operator()( const VertexShader::Output& in0,const VertexShader::Output& in1,const VertexShader::Output& in2,size_t triangle_index ) const
{
return{
{ in0.pos,triangle_colors[triangle_index/2] },
{ in1.pos,triangle_colors[triangle_index/2] },
{ in2.pos,triangle_colors[triangle_index/2] }
}; // 삼각형 두 개가 하나의 면을 이루기 때문에 index/2를 해준다.
};
void BindColors( std::vector<Color> colors )
{
triangle_colors = std::move( colors );
}
private:
std::vector<Color> triangle_colors;
};
// invoked for each pixel of a triangle
// takes an input of attributes that are the
// result of interpolating vertex attributes
// and outputs a color
class PixelShader
{
public:
template<class Input>
Color operator()( const Input& in ) const
{
return in.color;
}
};
public:
VertexShader vs;
GeometryShader gs;
PixelShader ps;
};
// CubeSolidGeometryScene.h
#pragma once
#include "Scene.h"
#include "Cube.h"
#include "Mat3.h"
#include "Pipeline.h"
#include "SolidGeometryEffect.h"
class CubeSolidGeometryScene : public Scene
{
public:
typedef Pipeline<SolidGeometryEffect> Pipeline;
typedef Pipeline::Vertex Vertex;
public:
CubeSolidGeometryScene( Graphics& gfx )
:
itlist( Cube::GetPlain<Vertex>() ), // GetPlain()을 사용한다는 점에 주의. 기존에 단색 면들로 이루어진 큐브를 그릴 때는 각 면들을 따로따로 분리하는 (=메모리를 많이 사용하는) 방식의 GetPlainIndependentFaces()를 사용했다.
pipeline( gfx ),
Scene( "Colored cube geometry solid face scene" )
{
pipeline.effect.gs.BindColors(
{ Colors::Red,Colors::Green,Colors::Blue,Colors::Magenta,Colors::Yellow,Colors::Cyan }
);
}
virtual void Update( Keyboard& kbd,Mouse& mouse,float dt ) override
{
if( kbd.KeyIsPressed( 'Q' ) )
{
theta_x = wrap_angle( theta_x + dTheta * dt );
}
if( kbd.KeyIsPressed( 'W' ) )
{
theta_y = wrap_angle( theta_y + dTheta * dt );
}
if( kbd.KeyIsPressed( 'E' ) )
{
theta_z = wrap_angle( theta_z + dTheta * dt );
}
if( kbd.KeyIsPressed( 'A' ) )
{
theta_x = wrap_angle( theta_x - dTheta * dt );
}
if( kbd.KeyIsPressed( 'S' ) )
{
theta_y = wrap_angle( theta_y - dTheta * dt );
}
if( kbd.KeyIsPressed( 'D' ) )
{
theta_z = wrap_angle( theta_z - dTheta * dt );
}
if( kbd.KeyIsPressed( 'R' ) )
{
offset_z += 2.0f * dt;
}
if( kbd.KeyIsPressed( 'F' ) )
{
offset_z -= 2.0f * dt;
}
}
virtual void Draw() override
{
pipeline.BeginFrame();
// generate rotation matrix from euler angles
// translation from offset
const Mat3 rot =
Mat3::RotationX( theta_x ) *
Mat3::RotationY( theta_y ) *
Mat3::RotationZ( theta_z );
const Vec3 trans = { 0.0f,0.0f,offset_z };
// set pipeline transform
pipeline.effect.vs.BindRotation( rot );
pipeline.effect.vs.BindTranslation( trans );
// render triangles
pipeline.Draw( itlist );
}
private:
IndexedTriangleList<Vertex> itlist;
Pipeline pipeline;
static constexpr float dTheta = PI;
float offset_z = 2.0f;
float theta_x = 0.0f;
float theta_y = 0.0f;
float theta_z = 0.0f;
};
잘 동작하는 것을 볼 수 있다.
Geometry shader는 이 밖에도 다양한 곳에 쓰일 수 있는데, 일단 삼각형마다 개별적으로 어떤 처리를 해주어야 할 때 쓸 수 있다. 특히 vertex의 속성을 수정하려 할 때 vertex shader에서 접근하면 해당 vertex를 가진 삼각형 모두가 영향받기 때문에 Geometry shader를 사용해야 하는 경우가 생긴다.
또 동적으로 삼각형들의 face normal을 계산해주는 연산을 할 때 쓸 수 있다.
각 삼각형들의 정점들 값을 가져오기 때문에 외적을 통해 face normal을 구해줄 수 있다.
우리가 만든 Geometry shader가 D3D를 비롯한 실제 하드웨어 3D 렌더링 파이프라인과 어떻게 다른지 간단하게 살펴보자.
차이점은 다음과 같다.
- D3D에서 Geometry shader는 Optional하다! 즉 Vertex shader, Pixel shader가 필수적인 것과는 달리 D3D에서는 없어도 동작한다.
- D3D에서 Geometry shader는 입력받은 Primitive와 다른 종류의 Primitive를 반환할 수도 있다.
보다 자세히 설명해보겠다.
우리의 렌더링 파이프라인이 정점으로 이루어진 기본 도형(=Primitives)이 삼각형만이 존재하는 것에 반해, D3D에서는 여러 Primitive가 존재한다.
Triangle, Point, Line, Strip 등이 그 예시이다.
갑자기 이 이야기를 하는 것은, D3D의 GS는 이런 여러 종류의 Primitives를 인풋으로 받아 또 다른 여러 종류의 Primitives들을 반환해줄 수 있다.
사실 직관적으로 이런 기능이 어디에 쓰이는지 이해가 안 갈 수도 있는데, 한 가지 예시를 들어보겠다.
만약 어떤 스프라이트(Sprite)를 렌더링하고 싶다고 하자. 스프라이트를 렌더링하려면 Vertex 4개짜리 primitive (혹은 삼각형 두개)를 사용해야 하고, 각 Vertex들에 Texture coordinates와 같은 정보들을 저장해야 할 것이다.
그러나 Geometry shader로 이때 사용되는 메모리 양을 획기적으로 줄일 수 있는데, GS에 어떤 정점 하나와 그 정점에 해당하는 특정 스프라이트의 id만을 전달하고, 그 후 GS에서 그 정점을 기준으로 점 세개를 더 찍어주고 그 id값을 기반으로 스프라이트를 찾아 텍스쳐를 입힌 후 이렇게 만들어진 새로운 Primitive를 반환해주도록 만들 수 있다. 쉽게 말해 동적으로 스프라이트를 입힌 모델을 생성하는 것이다. 이렇게 해주면 우리가 스프라이트 모델을 저장하기 위해 필요한 공간이 정점 4개에서 정점 1개 + 스프라이트 id로 줄어들게 되고, 또 렌더링 파이프라인의 각 과정을 거쳐가는 정보들의 양 자체가 줄어드므로 CPU 연산속도 측면에서도 효율적이다.
- 2번과 유사한데, D3D에서 GS는 입력받은 삼각형의 갯수와 관계없이 더 많거나 더 적은 삼각형을 반환할 수 있다. (당연히 삼각형 이외의 다른 primitive들도 마찬가지이다.)
쉽게 말해 삼각형 5개를 입력받고 50개를 반환하거나(=amplifying the geometry라고 표현한다), 혹은 0개를 반환하는 것 모두 가능하다.
-
Shader model (SM) 4.0부터는 GS를 통해 삼각형을 입력받으면 그 인접 삼각형들에 대한 정보까지도 접근할 수 있다.
이렇게 강력한 기능들을 제공하는 Geometry shader는 실제 practice에서는 그리 자주 사용되지 않는다.
그 가장 큰 이유는 속도 문제이다. 앞서 Geometry shader를 통해 입력받은 primitive 이상으로 더 많은 primitive를 반환할 수 있다고 했는데(=amplification), 이 기능이 상당히 느리다.
(원래 GS는 이 amplification을 이용해 Tessellation을 처리해주기 위해 고안되었지만, 속도가 너무 느려 D3D에서는 아예 테셀레이션만을 처리해주는 Tessellator(혹은 Tessellation shader)가 따로 존재한다.)
심지어 앞서 언급한, 스프라이트를 입힌 모델을 사용할 때 메모리 사용량을 최적화하는 과정 또한 어디까지나 "이렇게도 사용될 수 있다"를 보여주는 예시일 뿐이지, 실제로는 GS 말고 다른 방법을 사용해 더 빠른 최적화가 가능하기에 거의 쓰이지 않는 방식이다.
그러나 Shadow mapping 등 여전히 GS가 사용되는 곳들이 있으니 제대로 이해하고 있는 것이 좋다.
'computer graphics > 3D Graphics' 카테고리의 다른 글
[3DGraphics] 15. Gouraud shading (0) | 2022.09.09 |
---|---|
[3DGraphics] 14. Flat shading and Mesh loading (0) | 2022.09.09 |
[3DGraphics] 12. Vertex shader (0) | 2022.07.24 |
[3DGraphics] 11. Z-buffer (0) | 2022.07.15 |
[3DGraphics] 10. Perspective correction (0) | 2022.07.15 |