Vertex Shader
Vertex Shader는 3D 공간에 있는 vertex를 화면에 출력하기 위해 2D 좌표로 변환하는 과정이라고 생각하면 된다. 구체적인 좌표 변환 방법은 다른 포스트에서 정리해보도록 하고, 이번 포스트에서는 셰이더 문법을 통해 셰이더를 작성하고 Vertex Shader을 컴파일하는 과정을 정리해보겠다.
HLSL
HLSL은 DirectX에서 사용하는 셰이더 문법이다. 문법은 C와 비슷하고, Semantic을 통해 Graphics Pipeline에서 사용할 데이터를 인식하는 것이 특징이다.
셰이더 파일 작성
셰이더를 .fx 파일로 만들어 보자. Visual Studio에서 .fx 파일을 셰이더로서 사용하기 위해서 속성에서 설정을 해야 한다. 셰이더 형식을 효과(/fx), 셰이더 모델은 셰이더 모델 5.0으로 설정한다.
셰이더 파일은 헤더파일 처럼 다른 셰이더에서 include를 할 수 있다. 이 때 중복 정의를 방지하기 위해서 C++의 헤더 파일에서 #pragma once를 하는 것처럼, 한 번 정의된 경우 추가로 정의하지 않아야 하므로 #ifndef #define 를 이용한다. ifndef는 '뒤에 나오는 매크로가 정의되어 있지 않은 경우'를 의미하는 조건문이다. 아래 코드의 경우, __SHADER가 정의되어 있지 않은 경우에만 __SHADER을 정의 하면서 셰이더 코드에서 변수나 함수 등을 정의하면, 다음에 동일한 .fx 파일이 include 되면 __SHADER가 이미 정의되어 있기 때문에 중복 정의를 방지할 수 있다.
#ifndef __SHADER
#define __SHADER
// 셰이더 코드
#endif
Vertex Shader는 3d 공간의 Vertex 정보를 입력으로 받아서 2d 공간 상의 vertex 정보로 변환해서 반환해야 한다. 이를 위해 셰이더 안에서 input 구조체와 output 구조체를 정의하고, Semantic을 통해 vertex의 정보를 지정해줘야 한다.
Input 구조체는 관행적으로 VS_IN이라고 많이 짓는 것 같다. 각 변수 뒤에 ' : ' 뒤에 붙는 것이 Semantic이다. VS_IN 에서 POSITION, COLOR, TEXCOORD의 Semantic은 Input Assembler에서 input layout 구조체를 만들어서 전달할 때 정한 Semantic name이다. float3은 HLSL에서 float 3개로 이루어진 구조체로, 우리가 사용했던 Vec3와 구조적으로 동일한 구조체로 생각하면 된다. Input layout에서 정의한 순서와 Format대로 변수를 잘 정의했다면 메모리(버퍼)를 셰이더에서 적절하게 인식할 수 있는 것이다.
struct VS_IN
{
float3 Pos : POSITION;
float4 Color : COLOR;
float2 UV : TEXCOORD;
};
Output 구조체는 관행적으로 VS_OUT이라고 짓는 듯 하다. 마찬가지로 각 변수 뒤에 ':' 과 함께 Semantic name이 붙는데, Position 정보는 float4로 바뀌었음을 확인할 수 있다. 이는 Position 정보를 Affine Space에서 관리함을 의미하는데, 이에 관해서는 나중에 좌표 변환을 공부할 때 알아보자. 또 다른 차이점은 Semantic name으로 SV_Position을 사용한다는 점을 확인할 수 있다. 이로 부터 SV_Position은 이후 Graphics Pipeline에서 vertex의 위치 값으로 인식하기 위한 Semantic임을 알 수 있다. 다른 정보는 부가적인 정보이고, 화면에 렌더링하기 위해 Position 정보는 필수적이라는 것이다.
struct VS_OUT
{
float4 Pos : SV_Position;
float4 Color : COLOR;
float2 UV : TEXCOORD;
};
Vertex Shader로 사용할 함수를 정의한다. input type은 VS_IN이고, return type이 VS_OUT인 함수로 정의해야 한다. 아래함수는 Position만 float4로 변환해서 그대로 전달하도록 구현한 함수이다. 게임처럼 물체를 화면에 렌더링하기 위해서는 좌표계 변환 작업이 들어가서 Position을 계산해야 하지만 일단 이 포스트에서는 vertex shader의 사용법만 알아보자.
// Vertex Shader
VS_OUT VS_Std2D(VS_IN _in)
{
VS_OUT output = (VS_OUT) 0.f;
output.Pos = float4(_in.Pos, 1.f);
output.Color = _in.Color;
output.UV = _in.UV;
return output;
}
여태까지 작성한 파일이 아래와 같이 정리될 것이다.
// std2d.fx 파일
#ifndef _SHADER
#define _SHADER
struct VS_IN
{
float3 Pos : POSITION;
float4 Color : COLOR;
float2 UV : TEXCOORD;
};
struct VS_OUT
{
float4 Pos : SV_Position;
float4 Color : COLOR;
float2 UV : TEXCOORD;
};
// Vertex Shader
VS_OUT VS_Std2D(VS_IN _in)
{
VS_OUT output = (VS_OUT) 0.f;
output.Pos = float4(_in.Pos, 1.f);
output.Color = _in.Color;
output.UV = _in.UV;
return output;
}
#endif
이제 작성한 Vertex Shader을 DirectX에서 사용하기 위해 컴파일해서 DirectX 객체로 저장해야 한다. 셰이더 파일을 컴파일 하기 위해 사용하는 함수가 D3DCompileFromFile()이다. 셰이더 파일의 경로, Vertex Shader로 사용할 함수의 이름, 셰이더 버전 등을 전달해서 m_VSBlob에 함수의 바이너리 코드를 받게 된다.
// #pragma comment(lib, "d3dcompiler")
// #include <d3dcompiler.h>
UINT Flag = D3DCOMPILE_DEBUG;
// .fx 파일을 컴파일해서 _FuncName의 함수를 셰이더 진입점으로 보고, 관련된 코드를 ID3DBlob 객체에 저장함.
// m_VSBlob : Blob 구조체 (바이너리 코드를 저장하고 있는 구조체), Vertex Shader, ComPtr<ID3D11Blob>
// m_ErrBlob : Blob 구조체, 에러내용을 저장하고 있음, ComPtr<ID3D11Blob>
D3DCompileFromFile(L"D:\\std2d.fx", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE
, L"VS_Std2D", "vs_5_0", Flag, 0, m_VSBlob.GetAddressOf(), m_ErrBlob.GetAddressOf());
이후 CreateVertexShader()를 호출해서 Vertex Shader을 생성한다.
// m_VS : vertex shader, ComPtr<ID3D11VertexShader>
DEVICE->CreateVertexShader(m_VSBlob->GetBufferPointer(), m_VSBlob->GetBufferSize(), nullptr, m_VS.GetAddressOf());
이렇게 열심히 생성한 Vertex Shader을 Graphics Pipeline에서 사용하기 위해서는 Device Context의 vertex shader로 등록해주면 된다. 이 때는 VSSetShader() 함수를 사용한다.
CONTEXT->VSSetShader(m_VS.Get(), nullptr, 0);
[DirectX11] Graphics Pipeline 4 - Pixel Shader (0) | 2024.08.27 |
---|---|
[DirectX11] Graphics Pipeline 3 - Rasterizer (0) | 2024.08.26 |
[DirectX11] Graphics Pipeline 1 - Input Assembler (0) | 2024.08.24 |
[DirectX11] Render Target View, Depth Stencil View (0) | 2024.08.23 |
[DirectX11] ComPtr (0) | 2024.08.23 |