Vertex shader에 대해 살펴보자.
우리의 기존 파이프라인에서는 Vertex들과 관련된 연산을 VT(Vertex Transformer)에서 처리해주고 있었다.
그러나 이를 하나의 과정으로 분리시켜 Vertex들과 관련된 연산을 처리해주는 Vertex shader가 dx8부터 등장했다.
현재 VT역할을 맡고 있는 코드는 Pipeline.h의 ProcessVertices 함수인데, 이 함수가 하는 역할을 별도의 모듈로 분리시켜보자.
우선 rotation과 translation 관련 연산을 처리해주는 함수들 및 변수들을 전부 Pipeline 클래스에서 삭제해주자.
또 Vertex shader는 입력받은 Vertex와 다른 타입의 Vertex를 반환할 수도 있기 때문에 Pipeline의 ProcessVertices 내부 함수들에서
기존 Vertex 템플릿 대신 새로운 VSOut 템플릿을 사용하도록 수정한다.
Pipeline 내의 Vertex shader 이후 등장하는 정점을 다루는 모든 함수들에 대해 변경해준다.
// Pipeline.h
// 템플릿은 Pipeline 내에 정의되어 있는데, 보면 Effect 내의 VertexShader 내의
// Output이라는 템플릿이 있다고 가정하고 이를 참조하고 있다.
// 이 Output이라는 템플릿은 해당 Vertex shader가 어떤 Vertex를 반환하는지를 나타내는 템플릿이다.
class Pipeline
{
public:
// vertex type used for geometry and throughout pipeline
typedef typename Effect::Vertex Vertex;
typedef typename Effect::VertexShader::Output VSOut;
Effects 클래스들 내부의 Vertex 클래스들에게 각각의 정점에 맞는 VertexShader를 지정해 이에 맞는 템플릿을 만들어준다.
(템플릿명은 VertexShader로 통일한다)
#pragma once
#include "Pipeline.h"
#include "DefaultVertexShader.h"
// basic texture effect
class TextureEffect
{
public:
// the vertex type that will be input into the pipeline
class Vertex
{
// default vs rotates and translates vertices
// does not touch attributes
// 각 Effect들의 Vertex 클래스 내에 VertexShader 템플릿을 정의해준다.
// (주석: 이러한 방식이 구조적으로 좋다고 생각되지는 않는데, 본 학습과정에서는 그래픽스의 원리를 이해하는 것이 목표이므로 그냥 넘어가자)
typedef DefaultVertexShader<Vertex> VertexShader;
// ...
public:
VertexShader vs;
PixelShader ps;
};
// DefaultVertexShader.h
// 기본 Vertex shader로, rotation과 translation 연산만 처리해준다.
// 기존 Vertex transformer가 해주던 역할만을 고스란히 가져왔다.
#pragma once
template<class Vertex>
class DefaultVertexShader
{
public:
typedef Vertex Output;
public:
void BindRotation( const Mat3& rotation_in )
{
rotation = rotation_in;
}
void BindTranslation( const Vec3& translation_in )
{
translation = translation_in;
}
Output operator()( const Vertex& in ) const
{
return{ in.pos * rotation + translation,in };
}
private:
Mat3 rotation;
Vec3 translation;
};
이렇게 VertexShader를 성공적으로 만들었다!
사실 지금으로써는 Vertex shader가 해주는 기능이 기존 VT가 해주던 기능밖에 없는데,
Vertex shader로는 다양한 기능을 만들 수 있다.
사진과 같은 망토 효과를 만들어보자.
망토효과의 원리 자체는 간단한데, 각 정점을 y축으로 위아래로 흔들어주면 된다.
당연히 표면의 정점이 4개밖에 없으면 흔들어봤자 큰 임팩트가 없기 때문에,
편의를 위해 직사각형 표면을 원하는 정도로 Tessellate할 수 있게 해주는 Plane 오브젝트를 만들자.
// Plane.h
// 세부적인 내용은 그리 어렵지 않으며 굳이 깊게 다룰 필요가 없다.
#pragma once
#include <vector>
#include <array>
#include "Vec2.h"
#include "Vec3.h"
#include "IndexedTriangleList.h"
class Plane
{
public:
template<class V>
// divisions와 size에 따라 평면에 속하는 정점들을 동적으로 할당하는 함수
static IndexedTriangleList<V> GetPlain( int divisions = 7,float size = 1.0f )
{
const int nVerticesSide = divisions + 1;
std::vector<V> vertices( sq( nVerticesSide + 1 ) ); // divisions의 제곱에 비례하는 만큼의 크기를 할당한다. (정점을 다 저장해야 하므로)
// give V a ctor for pos only %%%improvements
{
const float side = size / 2.0f;
const float divisionSize = size / float( divisions );
const Vec3 bottomLeft = { -side,-side,0.0f };
for( int y = 0,i = 0; y < nVerticesSide; y++ )
{
const float y_pos = float( y ) * divisionSize;
for( int x = 0; x < nVerticesSide; x++,i++ )
{
vertices[i].pos = bottomLeft + Vec3{ float( x ) * divisionSize,y_pos,0.0f };
}
}
}
std::vector<size_t> indices;
indices.reserve( sq( divisions ) * 6 );
{
const auto vxy2i = [nVerticesSide]( size_t x,size_t y )
{
return y * nVerticesSide + x;
};
for( size_t y = 0; y < divisions; y++ )
{
for( size_t x = 0; x < divisions; x++ )
{
const std::array<size_t,4> indexArray =
{ vxy2i( x,y ),vxy2i( x + 1,y ),vxy2i( x,y + 1 ),vxy2i( x + 1,y + 1 ) };
indices.push_back( indexArray[0] );
indices.push_back( indexArray[2] );
indices.push_back( indexArray[1] );
indices.push_back( indexArray[1] );
indices.push_back( indexArray[2] );
indices.push_back( indexArray[3] );
}
}
}
return{ std::move( vertices ),std::move( indices ) };
}
// 테셀레이션 된 (정점 여러개로 나눠진) Plane의 각 vertex에 대해 텍스쳐 좌표를 설정하는 함수
template<class V>
static IndexedTriangleList<V> GetSkinned( int divisions = 7,float size = 1.0f )
{
auto itlist = GetPlain<V>( divisions,size );
{
const int nVerticesSide = divisions + 1;
const float tDivisionSize = 1.0f / float( divisions );
const Vec2 tBottomLeft = { 0.0f,1.0f };
for( int y = 0,i = 0; y < nVerticesSide; y++ )
{
const float y_t = -float( y ) * tDivisionSize;
for( int x = 0; x < nVerticesSide; x++,i++ )
{
itlist.vertices[i].t = tBottomLeft + Vec2{ float( x ) * tDivisionSize,y_t };
}
}
}
return itlist;
}
};
참고:
Tessellation이란, Dx11 이상부터는 별도의 과정(Tessellator)으로 아예 분리되어있으며,
간단히 말해 폴리곤들로 구성되어있는 모델(mesh)을 더 작은 삼각형들로 쪼개 더 부드러운 표면을 갖도록 만들어주는 기술이다.
(https://computergraphics.stackexchange.com/questions/2018/what-is-tessellation-in-computer-graphics)
만들어 주었다면 이제 망토 효과를 주는 Vertex shader를 만들어보자.
// WaveVertexTextureEffect.h
#pragma once
#include "Pipeline.h"
class WaveVertexTextureEffect
{
public:
class Vertex
{
public:
Vertex() = default;
Vertex( const Vec3& pos )
:
pos( pos )
{}
Vertex( const Vec3& pos,const Vertex& src )
:
t( src.t ),
pos( pos )
{}
Vertex( const Vec3& pos,const Vec2& t )
:
t( t ),
pos( pos )
{}
Vertex& operator+=( const Vertex& rhs )
{
pos += rhs.pos;
t += rhs.t;
return *this;
}
Vertex operator+( const Vertex& rhs ) const
{
return Vertex( *this ) += rhs;
}
Vertex& operator-=( const Vertex& rhs )
{
pos -= rhs.pos;
t -= rhs.t;
return *this;
}
Vertex operator-( const Vertex& rhs ) const
{
return Vertex( *this ) -= rhs;
}
Vertex& operator*=( float rhs )
{
pos *= rhs;
t *= rhs;
return *this;
}
Vertex operator*( float rhs ) const
{
return Vertex( *this ) *= rhs;
}
Vertex& operator/=( float rhs )
{
pos /= rhs;
t /= rhs;
return *this;
}
Vertex operator/( float rhs ) const
{
return Vertex( *this ) /= rhs;
}
public:
Vec3 pos;
Vec2 t;
};
// perturbes vertices in y axis in sin wave based on
// x position and time
class VertexShader
{
public:
typedef Vertex Output;
public:
void BindRotation( const Mat3& rotation_in )
{
rotation = rotation_in;
}
void BindTranslation( const Vec3& translation_in )
{
translation = translation_in;
}
Output operator()( const Vertex& in ) const
{
Vec3 pos = in.pos * rotation + translation;
pos.y += amplitude * std::sin( time * freqScroll + pos.x * freqWave );
return{ pos,in.t };
}
void SetTime( float t )
{
time = t;
}
private:
Mat3 rotation;
Vec3 translation;
float time = 0.0f;
float freqWave = 10.0f;
float freqScroll = 5.0f;
float amplitude = 0.05f;
};
// texture clamped ps
class PixelShader
{
public:
template<class Input>
Color operator()( const Input& in ) const
{
return pTex->GetPixel(
(unsigned int)std::min( in.t.x * tex_width + 0.5f,tex_xclamp ),
(unsigned int)std::min( in.t.y * tex_height + 0.5f,tex_yclamp )
);
}
void BindTexture( const std::wstring& filename )
{
pTex = std::make_unique<Surface>( Surface::FromFile( filename ) );
tex_width = float( pTex->GetWidth() );
tex_height = float( pTex->GetHeight() );
tex_xclamp = tex_width - 1.0f;
tex_yclamp = tex_height - 1.0f;
}
private:
std::unique_ptr<Surface> pTex;
float tex_width;
float tex_height;
float tex_xclamp;
float tex_yclamp;
};
public:
VertexShader vs;
PixelShader ps;
};
사실 핵심이 되는 코드는 매우 짧은데, sin함수의 성질을 이용해 y값을 부드럽게 위아래로 흔들리도록 바꿔주는 것을 알 수 있다.
앞서 본 사진에서 망토가 흔들리는 모습을 잘 보면 가로축을 따라 일자로 요동치는 게 아니라 대각선 방향으로 파동이 형성되는 걸 볼 수 있는데,
이는 sin함수 내부에서 +pos.x를 해주었기 때문이다. (y축만 위아래로 움직이므로 pos.x는 상수임에 유의)
또 시간에 따라 망토가 하늘하늘 휘날리게 해주는 부분은 time을 입력으로 주었기 때문인 것도 알 수 있다.
(time은 VertexWaveScene.h의 Update()에서 dt를 더해주고, 매 Draw() call마다 vs.Settime()을 통해 값을 갱신해주는 것을 볼 수 있다.)
// VertexWaveScene.h
// ...
virtual void Update( Keyboard& kbd,Mouse& mouse,float dt ) override
{
// ...
time += 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 );
pipeline.effect.vs.SetTime( time );
// render triangles
pipeline.Draw( itlist );
}
// ...
씬을 실행하면 아까 본 망토 효과를 볼 수 있다.
Vertex shader로 여러 재미있는 효과들을 만들 수 있는데, 간단한 예시 하나만 더 들어보면 어떤 정점의 색이 그 색의 pos 좌표값들에 따라 변하게 함으로써 재미있는 시각적 효과를 연출할 수 있다.
사진상에서는 알 수 없지만 회전할 때마다 각 정점들의 색상이 변화한다.
원리는 단순하다.
// VertexPositionColorEffect.h
Output operator()( const Vertex& in ) const
{
const auto pos = in.pos * rotation + translation;
return{ pos,Vec3{ std::abs( pos.x ),std::abs( pos.y ),std::abs( pos.z ) } * 255.0f }; // pos가 -1~1이라는 것에 유의
}
'computer graphics > 3D Graphics' 카테고리의 다른 글
[3DGraphics] 14. Flat shading and Mesh loading (0) | 2022.09.09 |
---|---|
[3DGraphics] 13. Geometry shader (0) | 2022.07.24 |
[3DGraphics] 11. Z-buffer (0) | 2022.07.15 |
[3DGraphics] 10. Perspective correction (0) | 2022.07.15 |
[3DGraphics] 9. Pixel shader (0) | 2022.07.15 |