[목차]

 

- DepthStencilView랑 DepthStencilState는 무슨 차이가 있는가

- DepthStencilState 생성

- Z-Buffer, Z-Test, Z-Culling

- Z-Sorting

- Stencil

- OutputMerger 단계

 

 

[DepthStencilView랑 DepthStencilState는 무슨 차이가 있는가]

 

맨 처음 프레임워크를 설계할 떄, DepthStencilView 값을 담고 있는 DepthTexture를 만들어두고 그냥 바인딩해서 사용하고 있었습니다.

https://umtimos.tistory.com/150

 

[DirectX11] SwapChain과 RenderTarget 생성

[목차] - SwapChain- SwapChain 생성- DXGI- RenderTarget 이란- Depth/Stencil Buffer 생성- 백 버퍼 출력해보기 [SwapChain] GPU는 프레임 버퍼에 그림을 그리고 그걸 화면(Window)에 출력합니다. 그런데 여기서 프레임

umtimos.tistory.com

 

이 상태에서는 Depth Test와 Stencil Test가 기본 설정으로 실행되는데, 이걸 사용자 임의로 조절할 수가 있습니다. 이를 위해 DirectX에서는 DepthStencilState를 지원해줍니다.

 

- DepthStencilView : 어떤 깊이 데이터를 쓸지 GPU에 알려주는 버퍼

- DepthStencilState : 이제 이 버퍼 데이터 어떻게 쓸것인지, "사용 설명서"

 

두 가지 모두 바인딩되어야 GPU가 Z-Test, Z-Culling, Stencil Test 등을 사용자가 원하는 방식대로 진행할 수 있습니다.

 

 

[DepthStencilState 생성]

 

일단 DepthStencilState 값을 정의해줍니다.

// DirectX11 DepthStencilState 생성
void Ext_DirectXResourceLoader::MakeDepth() 
{
	D3D11_DEPTH_STENCIL_DESC Desc = { 0, };

	Desc.DepthEnable = true;
	Desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
	Desc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL;
	Desc.StencilEnable = false;
	/*1. 깊이 테스트 수행함(true)*/
	/*2. 깊이 값을 Z버퍼에 기록*/
	/*3. 새 픽셀이 더 가깝거나 같으면 통과*/
	/*4. 스텐실 안함*/

	Ext_DirectXDepth::CreateDepthStencilState("EngineDepth", Desc);
    // EngineDepth 이름을 리소스 매니저에 저장하고, CreateDepthStencilState() 호출
}

 

DepthStencilState 값은 D3D11_DEPTH_STENCIL_DESC를 통해 정의할 수 있으며, 여기서 활용한 값들의 설명은 다음과 같습니다.

DepthEnable - Depth를 쓰는지 여부
- true 전달(씀)
DepthWriteMask - D3D11_DEPTH_WRITE_MASK_ALL
- 깊이 값을 Z버퍼에 기록한다는 뜻
DepthFunc - D3D11_COMPARISON_LESS_EQUAL
- 새 픽셀이 더 가깝거나 같으면 통과

> 만약 State를 정의하지않고, 디폴트를 사용하고 있었다면 해당 부분이 D3D11_COMPARISON_LESS로 설정되어 Depth Test가 실행
StencilEnable - Stencil을 쓰는지 여부
- false 전달(안씀)

 

사실 사용할 DepthStencilState가 디폴트 State와 크게 차이가 없습니다. 한 가지 다른건 DepthFunc인데, 기존에는 D3D11_COMPARISON_LESS였지만 여기서는 D3D11_COMPARISON_LESS_EQUAL로 설정했습니다. 둘의 차이는 다음과 같습니다.

D3D11_COMPARISON_LESS(기본값) - 일반적으로 많이 사용
- 완전히 앞에 있는 픽셀만 통과
- 깊이 버퍼에 동일한 값이 있으면 새로운 픽셀은 버려짐
D3D11_COMPARISON_LESS_EQUAL(설정값) - 픽셀의 깊이가 기존과 같은 경우에도 그림
- 여러 번 동일한 깊이로 그릴 때 필요
> 같은 깊이에 여러 Pass가 겹칠 때 필요
- 잘못 사용하면 중복 필셀로 Z-Fighting 발

 

여기서 D3D11_COMPARISON_LESS_EQUAL로 설정한 이유는 나중에 렌더링 과정을 Deferred 렌더링으로 변경하기 위해서 입니다. 디퍼드 렌더링에서는 동일한 깊이값에 여러 Pass가 렌더링될 수 있는데, 그 때를 대비해서 이렇게 먼저 선언해 두었습니다.

 

추가로 Stencil을 기존과 마찬가지로 사용하지 않는 것으로 설정했습니다. 나중에 마스킹을 할 때 사용할 수도 있긴 한데, 그때가 되면 그냥 MRT를 사용할 수도 있어서 그냥 마스킹될 대상을 따로 그리는 방식을 취할 수도 있습니다.

 

이제 CreateDepthStencilState()를 호출하면 DSS를 만들 수 있습니다. 이러면 Ext_DirectXDepth 클래스 내부로 들어가 다음의 함수를 실행할 것입니다.

// 위에서 전달받은 정보를 통해 CreateDepthStencilState()로 DSS 생성
static std::shared_ptr<Ext_DirectXDepth> CreateDepthStencilState(const std::string_view& _Name, const D3D11_DEPTH_STENCIL_DESC& _Desc)
{
	std::shared_ptr<Ext_DirectXDepth> NewDepth = Ext_DirectXDepth::CreateNameResource(_Name);
	NewDepth->CreateDepthStencilState(_Desc);
	return NewDepth;
}

// DSS를 GPU에 바인딩, 아웃풋머저에 적용
void Ext_DirectXDepth::Setting()
{
	if (nullptr == DSS)
	{
		MsgAssert("깊이버퍼 스테이트가 만들어지지 않았습니다.");
	}

	Ext_DirectXDevice::GetContext()->OMSetDepthStencilState(DSS.Get(), 0);
	/*1. 생성한 DSS 전달*/ 
	/*2. 2번째 인자 0은 스텐실 안쓴다는 뜻*/
}

// DepthStencilState 생성
void Ext_DirectXDepth::CreateDepthStencilState(const D3D11_DEPTH_STENCIL_DESC& _Value)
{
	DepthStencilInfo = _Value;

	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateDepthStencilState(&DepthStencilInfo, DSS.GetAddressOf()))
	{
		MsgAssert("깊이 버퍼 스테이트 생성에 실패했습니다.");
	}
	// D3D11_DEPTH_STENCIL_DESC(DSS 생성 설명서)를 바탕으로 DSS 생성
}

 

생성된 DepthStencilState는 Material에 바인딩해서 사용해주도록 합니다.

// 일반(단일 메시)
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("Basic");
	NewRenderingPipeline->SetVertexShader("Basic_VS");
	NewRenderingPipeline->SetPixelShader("Basic_PS");
	NewRenderingPipeline->SetBlendState("BaseBlend"); // 아직 없음
	NewRenderingPipeline->SetDepthState("EngineDepth");
	NewRenderingPipeline->SetRasterizer("EngineRasterizer"); // 아직 없읍
}

 

 

[Z-Buffer, Z-Test, Z-Culling]

 

Z-Buffer(Depth Buffer)는 3D 공간 상에서 픽셀이 카메라로부터 얼마나 떨어져 있는지(깊이 정보)를 저장하는 버퍼입니다. 각 픽셀이 가진 깊이값(Z값)은 0.0f ~ 1.0f의 범위를 가집니다. 이 값은 NDC.z값(w나누기 한 값)에 뷰포트 행렬을 적용한 값(Screen Space)입니다.

복습

 

이 Screen Space 좌표값으로 변환된 z값을 가지고 Z-Test(Depth Test)를 실시하게 되는데, Z-Test는 렌더링 시 현재 출력하려는 픽셀이 기존에 출력된 픽셀보다 카메라 기준으로 더 앞쪽에 있는지를 판별하는 과정입니다. 이 판별이 끝나면 겹쳐진 오브젝트 중 앞에 있는 것만 그려지는 효과가 발생하는데, 이걸 Z-Culling이라고 합니다.

 

 

[Z-Sorting]

 

여기까지 했으면 정상적으로 Z-Culling이 발생해야하겠지만, 아쉽게도 현재의 프레임워크 설계에서는 다음의 문제가 발생하게 됩니다.

 

영상을 보시면 회전하고 있는 Object(B)는 가운데 있는 Object(A)의 Y축을 기준으로 주변을 회전하고 있습니다. 이러면 뒤로 가는 순간이 분명 있을 텐데, 뒤로 갔음에도 불구하고 A보다 앞에 그려집니다.

 

이런 경우가 발생하는 이유는 현재 DrawIndexed()를 Object를 순회하면서 실시하고 있기 때문입니다. 아무리 B가 A보다 뒤에 있다고 해도 A를 먼저 그리고 다음에 B를 그려버리면 당연히 B가 A보다 앞에 그려질 것입니다. 그 현상이 위의 영상에 나타나고 있습니다.

 

위 문제를 해결하기 위해 임의로 Z-Sorting을 실시했습니다. 이러면 MeshComponent의 Z값 기준으로 맨 뒤에 있는 결과물부터 순서대로 그려줄 것이기 때문에 예상했던 대로 그려지게 됩니다.

 

 

[Stencil]

 

해당 프레임워크에서는 사용할 예정이 딱히 없기 때문에, 개념만 짚고 넘어가겠습니다.

 

DSV에서 각 픽셀의 Depth와 Stencil 값 표현 자료형을 DXGI_FORMAT_D24_UNORM_S8_UINT로 정의했기 때문에, 앞의 3byte를 제외한 나머지 1byte는 Stencil값을 표현하기 위해 사용됩니다(0~255). 이걸 활용하면 픽셀의 스텐실 값을 비교하여, 조건 만족 시에만 렌더 타겟 뷰에 그리도록 설정할 수 있습니다(그림자 영역, 외곽선, 복잡한 후처리 효과 등).

 

사용 예시는 다음과 같습니다. State Desc를 만들 때, 아래의 인자들을 활용해주면 됩니다.

Desc.StencilEnable = TRUE;
Desc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
Desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;

 

이런 식으로 활용해서, 특정 부분을 1로 마킹한 다음 그것만 따로 그리는 방식이 가능합니다.

출처 : https://heinleinsgame.tistory.com/25

 

 

[OutputMerger 단계]

 

OutputMerger는 렌더링 파이프라인의 가장 마지막 단계입니다. 여기서 모든 픽셀 처리 결과가 RenderTarget에 실제로 기록될지 말지를 결정하게 되는데, 해당 단계에서 Z-Test, Stencil Test, Blending이 수행됩니다.

 

1. Z-Test

: 픽셀 Z값과 Z-Buffer를 비교하여 멀면 버리고, 가깝거나 같으면 통과시킴

 

2. Stencil Test

: 픽셀 스텐실값 비교 후 실패시 버리는 과정 실시, 복잡한 마스킹 등

 

3. Blending

: 기존 픽셀 색상과 새 픽셀 색상을 혼합, 알파 블랜딩, 가산 혼합 등 다양하게 활용

 

4. Render Target Output

: 위 세 조건을 통과한 픽셀만 렌더 타겟 뷰에 기록

// 1. 깊이/스텐실 상태 설정
Context->OMSetDepthStencilState(DepthStencilState, StencilRef);

// 2. 블렌드 상태 설정
Context->OMSetBlendState(BlendState, BlendFactor, SampleMask);

// 3. 렌더 타겟 및 깊이 버퍼 바인딩
Context->OMSetRenderTargets(NumViews, RenderTargetViewArray, DepthStencilView);

 

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/135807

 

 

[풀이]

 

먼저 두 벡터의 요소들에 대한 GCD를 구한다. 그리고 GCD가 1일 경우를 제외하고, 다른 벡터에서 구한 GCD를 요소에 대해 모두 나눠보는 시도를 진행한다. 이때 한 번도 나눠지지 않는다면 true가 되면서 조건을 만족하게 된다. 하지만 다른 벡터도 조건식을 통과하는지 검사하기 때문에, 두 벡터 모두 만족하는 수가 나올 수 있다. 이때는 가장 큰 값을 판별하기 위해 max를 활용하여 값 리턴하면 된다.

#include <string>
#include <vector>

using namespace std;

int GCD(int a, int b)
{
    while (b != 0)
    {
        int temp = a % b;
        a = b;
        b = temp;
    }

    return a;
}

int GetArrayGCD(const vector<int>& arr)
{
    int Result = arr[0];
    for (int i = 1; i < arr.size(); i++)
    {
        Result = GCD(Result, arr[i]);
    }
    return Result;
}

bool Try(const vector<int>& _Arr, int _GCD)
{
    if (1 == _GCD) return false;

    for (int val : _Arr)
    {
        if (val % _GCD == 0)
        {
            return false;
        }
    }

    return true;
}

// 다음 두 조건 중 하나를 만족하는 가장 큰 양의 정수 a의 값
// 1. arrayA는 모두 나눌 수 있고, arrayB는 모두 나눌 수 있는 수
// 2. arrayA는 모두 나눌 수 없고, arrayB는 모두 나눌 수 있는 수
int solution(vector<int> arrayA, vector<int> arrayB)
{
    int answer = 0;
    int AGcd = GetArrayGCD(arrayA);
    int BGcd = GetArrayGCD(arrayB);

    // 조건 1: A를 모두 나눌 수 있고, B를 하나도 나눌 수 없어야 함
    if (Try(arrayB, AGcd))
    {
        answer = max(answer, AGcd);
    }

    // 조건 2: B를 모두 나눌 수 있고, A를 하나도 나눌 수 없어야 함
    if (Try(arrayA, BGcd))
    {
        answer = max(answer, BGcd);
    }

    return answer;
}

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/389479

 

 

[풀이]

 

단순 구현 문제이다. 문제의 요지를 잘 파악하면 쉽게 풀 수 있다.

 

1. 어느 시간대 이용자가 n x m명 이상, (n + 1) x m명 미만이라면 최소 n대의 증설 서버가 필요

: m이 3이라면, 0 ~ 2명까지는 증설이 필요없고, 3명부터는 증설을 해야한다. 3 ~ 5명은 1대, 6 ~ 8명은 2대, 9 ~ 11명은 3대 이런 식으로 서버가 필요한 것이다.

2. k에 따라 서버 운영 수명이 정해져 있음

: k는 증설 시점으로 부터 k시간 후 회수하는 인자이기 때문에, 서버의 수명이다

 

이 두개만 기억하고 시뮬레이션을 돌리면 된다. 루프 한번은 시간대이다(0~1시간대, 1~2시간대, ..., 23~24시간대). 증설된 서버는 list로 관리하면서, 서버가 증설중인 상태라면 검사를 통해 수명 관리를 실시했다. Time이 k가 되면 list에서 제거하는 것이다.

#include <string>
#include <vector>
#include <list>

using namespace std;

struct Server
{
    int Time = 0;

    bool TimeCheck(int _Limit)
    {
        ++Time;

        if (Time == _Limit)
        {
            return true;
        }

        return false;
    }
};

// m명 늘어날 때마다 서버 1대가 추가
// 어느 시간대의 이용자가 n x m명 이상 (n + 1) x m명 미만 = 최소 n대의 증설된 서버가 운영 중
// 서버는 k시간 동안 운영하고 그 이후에는 반납, k = 5 일 때 10시에 증설한 서버는 10 ~ 15시에만 운영
// 하루 동안 모든 게임 이용자가 게임을 하기 위해 서버를 최소 몇 번 증설해야 하는지 알고 싶습니다. 같은 시간대에 서버를 x대 증설했다면 해당 시간대의 증설 횟수는 x회
int solution(vector<int> players, int m, int k) 
{
    int answer = 0;
    std::list<Server> ServerList;

    for (int i = 0; i < players.size(); i++)
    {
        int CurPlayer = players[i];
        int NeedCount = CurPlayer / m;

        if (ServerList.empty())
        {
            for (int i = 0; i < NeedCount; i++)
            {
                ServerList.push_back({ 0 });
                ++answer;
            }
        }

        if (!ServerList.empty())
        {
            int CurServerCount = ServerList.size();

            if (NeedCount > CurServerCount)
            {
                for (int i = 0; i < NeedCount - CurServerCount; i++)
                {
                    ServerList.push_back({ 0 });
                    ++answer;
                }
            }

            for (auto It = ServerList.begin(); It != ServerList.end();)
            {
                if (It->TimeCheck(k))
                {
                    It = ServerList.erase(It); // erase는 삭제 후 다음 iterator를 반환
                }
                else
                {
                    ++It;
                }
            }
        }
    }

    return answer;
}

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/155651

 

[풀이]

 

우선순위 큐 두 개를 사용하여 시뮬레이션을 실시한다.

 

1. 예약들을 Data 구조체 형태로 DataQueue에 저장한다.

2. 이때 시작 시간이 빠른 순서로 정렬할 수 있도록 한다.

3. 이후 퇴실 시간(청소 시간 10분 포함)을 기준으로 저장할 RoomEndTimes 우선순위 큐를 하나 더 만든다.

 

이제 시뮬레이션 과정이다.

 

1. DataQueue에서 가장 시작 시간이 빠른 예약을 꺼낸다.

2. RoomEndTimes의 top 값을 확인한다.

3. 만약 현재 손님의 시작 시간이 top값(가장 빨리 끝나는 방 시간)보다 크거나 같다면, 해당 방은 예약이 가능한 구조이기 때문에 pop을 실시한다.

4. 그 외의 경우에는 새로운 방이 필요한 상황이기 때문에 그냥 진행

5. 현재 손님의 End 시간을 RoomEndTimes에 push 한다.

 

위의 과정을 모두 진행하면, RoomEndTimes에 남아 있는 방의 개수가 필요한 최소 객실 수이다. 이 방법은 매번 끝나는 시간 중 가장 빠른 것을 기준으로 비교하므로, 불필요한 방 및 예약 중복 탐색을 피할 수 있다.

#include <string>
#include <vector>
#include <queue>

using namespace std;

struct Data
{
    Data(int _Start, int _End)
        : Start(_Start), End(_End) {}

    int Start = 0;
    int End = 0;
};

struct Compare
{
    bool operator()(const Data& A, const Data& B) const
    {
        return A.Start > B.Start; // Start가 작은 게 먼저 나오는 min heap
    }
};

int ClockToMinute(string _Str)
{
    string Hour = _Str.substr(0, 2);
    string Min = _Str.substr(3, 2);
    int H = atoi(Hour.data());
    int M = atoi(Min.data());

    return M + (H * 60);
}

// 호텔을 운영 중인 코니는 최소한의 객실만을 사용하여 예약 손님들을 받으려고 합니다
// 한 번 사용한 객실은 퇴실 시간을 기준으로 10분간 청소를 하고 다음 손님들이 사용
int solution(vector<vector<string>> book_time) 
{
    int answer = 0;
    // 끝나는 시간 기준 넣을 수 있는 값이 있는지 확인
    // 넣을 수 있는 값중 끝나는 시간이 제일  넣어줌
    priority_queue<Data, vector<Data>, Compare> DataQueue;
    for (int i = 0; i < book_time.size(); i++)
    {
        int Start = ClockToMinute(book_time[i][0]);
        int End = 10 + ClockToMinute(book_time[i][1]);
        DataQueue.push(Data(Start, End));
    }

    priority_queue<int, vector<int>, greater<int>> RoomEndTimes;
    // 3. 예약 순회하면서 방 할당
    while (!DataQueue.empty())
    {
        Data Cur = DataQueue.top();
        DataQueue.pop();

        // 가장 빨리 비는 방의 시간이 현재 시작 시간보다 작거나 같다면 → 재사용 가능
        if (!RoomEndTimes.empty() && RoomEndTimes.top() <= Cur.Start)
        {
            RoomEndTimes.pop(); // 기존 방 재사용
        }

        RoomEndTimes.push(Cur.End); // 새로운 방 또는 재사용된 방의 끝 시간 기록
    }
    
    return answer = RoomEndTimes.size();
}

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/86971

 

[풀이]

 

간선과 노드를 정리하고, 정리된 컨테이너를 활용하여 BFS를 실시한다. 노드를 들리면 Count를 하나 증가시키고 해당 노드 방문 여부인 Visited를 true로 마킹한다. 이 과정을 반복하면 연결된 요소에 대해서는 탐색을 진행할 수 있다. 이후 탐색을 모두 완료한 갯수에서 전체 노드 갯수를 빼주면 나머지는 방문하지 않아도 네트워크 구성망이 몇개인지 바로 알 수 있다. 

#include <string>
#include <vector>
#include <algorithm>
#include <queue>

using namespace std;

int BFS(int _Start, const vector<vector<int>>& _Graph, vector<bool>& _Visited)
{
    queue<int> Q;
    Q.push(_Start);
    _Visited[_Start] = true;

    int Count = 1;

    while (!Q.empty())
    {
        int Cur = Q.front();
        Q.pop();

        for (int Next : _Graph[Cur])
        {
            if (!_Visited[Next])
            {
                _Visited[Next] = true;
                Q.push(Next);
                ++Count;
            }
        }
    }

    return Count;
}

// n개의 송전탑이 전선을 통해 하나의 트리 형태로 연결
// 이 전선들 중 하나를 끊어서 현재의 전력망 네트워크를 2개로 분할
// 두 전력망이 갖게 되는 송전탑의 개수를 최대한 비슷하게
int solution(int n, vector<vector<int>> wires) 
{
    int answer = n;

    for (int i = 0; i < wires.size(); ++i)
    {
        vector<vector<int>> Graph(n + 1); // Graph 매 회 생성

        for (int j = 0; j < wires.size(); ++j)
        {
            if (i == j) continue; // i 번째 간선 제거

            int From = wires[j][0];
            int To = wires[j][1];

            Graph[From].push_back(To);
            Graph[To].push_back(From);
        }

        vector<bool> Visited(n + 1, false);

        // 하나의 컴포넌트에서 BFS 시작 (보통 1번 노드부터)
        int A = BFS(1, Graph, Visited);
        int B = n - A;

        answer = min(answer, abs(A - B));
    }

    return answer;
}

[목차]

 

- Scene Update

- Rendering Pipeline

- 결과

- 다양한 출력

 

 

[Scene Update]

 

앞서 렌더링 파이프라인 세팅을 위해 여러 과정을 알아내고, 구조를 만들어냈기 때문에 바로 결과물을 확인해볼 수 있습니다.

 

<버퍼 생성>

https://umtimos.tistory.com/178

 

[DirectX11] InputAssembler 단계를 진행하여 삼각형 출력하기

[목표] 이번 포스팅에서는 InputAssembler에 필요한 재료들을 준비하고, Direct3D 함수 호출을 통해 삼각형과 사각형을 그려보겠습니다. [InputLayout(버텍스 정보) 정의] 렌더링의 가장 기본 자원은 정점(V

umtimos.tistory.com

 

<뷰, 프로젝션>

https://umtimos.tistory.com/182

 

[DirectX11] 카메라와 뷰, 프로젝션, 뷰포트 행렬

[종횡비 문제] 이전 단계에서 정사각형을 출력하고 싶었지만, 정사각형이 출력되는 문제가 있었습니다. 다시 설명드리자면, 이 문제는 화면의 종횡비와 NDC(Normalized Device Coordinates) 좌표 공간 차

umtimos.tistory.com

 

<셰이더와 리소스>

https://umtimos.tistory.com/183

 

[DirectX11] 셰이더와 상수버퍼

[셰이더] 아직 셰이더와 상수버퍼는 설명하지 않아, 여기서 설명하고 넘어가도록 하겠습니다. DirectX11에서 셰이더는 GPU에서 실행되는 프로그램으로, 렌더링 과정의 특정 단계를 담당합니다. 주

umtimos.tistory.com

 

<렌더링 파이프라인 세팅>

https://umtimos.tistory.com/184

 

[DirectX11] 메시와 머티리얼 클래스 제작

[클래스 정의] 이전 단계에서 버텍스 버퍼, 인덱스 버퍼, 인풋 레이아웃, 버텍스 셰이더, 픽셀 셰이더, 상수 버퍼(리소스)를 만들고 각 클래스에 저장하거나, 리소스 매니저에 저장했습니다. 이제

umtimos.tistory.com

 

 

렌더링 업데이트는 현재 선택된 Scene의 MainCamera에 의해 진행됩니다. 현재 선택된 Scene은 TestScene이고, Start() 함수에 RectActor를 Create하여 활용했습니다.

///////////////////// Contents_Core.cpp
#include "PrecompileHeader.h"
#include "Contents_Core.h"
#include <DirectX11_Extension/Ext_Core.h>
#include "TestScene.h"
#include "RectActor.h"

/// <summary>
/// Scene 생성하는 곳
/// </summary>
void Contents_Core::Start()
{
	Ext_Core::CreateScene<TestScene>("TestScene");
	Ext_Core::ChangeScene("TestScene");
}

///////////////////// TestScene.cpp
#include "PrecompileHeader.h"
#include "TestScene.h"
#include "RectActor.h"

void TestScene::Start()
{
	CreateActor<RectActor>("RectActor");
}

void TestScene::Update(float _DeltaTime)
{
	__super::Update(_DeltaTime);
}

///////////////// RectActor.cpp
#include "PrecompileHeader.h"
#include "RectActor.h"

#include <DirectX11_Extension/Ext_MeshComponent.h>
#include <DirectX11_Extension/Ext_Transform.h>

void RectActor::Start()
{
	GetTransform()->SetWorldPosition({ 0.f, 0.f, 100.0f });
	std::shared_ptr<Ext_MeshComponent> MeshComp = CreateComponent<Ext_MeshComponent>("BasicMesh", true);
	MeshComp->CreateMeshComponentUnit("Rect", "Basic");
}

void RectActor::Update(float _DeltaTime)
{
}

 

Actor는 생성하면서 +Z방향으로 이동시켰습니다. 이러면 카메라에서 100.0f만큼 앞으로 떨어진 부분에 생성될 것입니다.

 

 

[Rendering Pipeline]

 

내용이 길어, 단계별로 간단하게 정리한 것 먼저 알려드리겠습니다.

ClearRenderTargetView() // 렌더타겟뷰 클리어
ClearDepthStencilView() // 뎁스/스텐실뷰 클리어
OMSetRenderTargets() // 렌더타겟뷰 바인딩
RSSetViewports() // 뷰포트 바인딩
LookToLH() // 뷰 행렬 생성
PerspectiveForLH() // 프로젝션 행렬 생성
SetViewProjectionMatrixToMeshComponent() // 뷰, 프로젝션 행렬 적용
IASetInputLayout() // 정점 입력 레이아웃 설정(정점 버퍼 데이터 해석 설명서 전달)
IASetVertexBuffers() // 정점 버퍼 종류 설정
IASetPrimitiveTopology() // 도형 종류 설정
IASetIndexBuffer() // 인덱스 버퍼 설정
VSSetShader() // 버텍스 셰이더 설정
PSSetShader() // 픽셀 셰이더 설정
Map(), Unmap() // 프레임마다 GPU에게 버퍼 변경내용 전달
VSSetConstantBuffers() // 정점 셰이더 바인딩
PSSetConstantBuffers() // 픽셀 셰이더 바인딩
DrawIndexed() // 정점 출력 명령
Present() // 렌더타겟뷰에 그려진 결과 백버퍼에 출력

 

 

위의 단계를 모두 진행해야합니다. 하나씩 함수 호출 부분을 살펴보겠습니다. 먼저 백 버퍼 클리어를 한 뒤 세팅을 진행합니다.

// Textures에 저장된 렌더 타겟 뷰들을 모두 클리어
void Ext_DirectXRenderTarget::RenderTargetViewsClear()
{
	for (size_t i = 0; i < Textures.size(); i++) 
	{
		for (size_t j = 0; j < Textures[i]->GetRTVSize(); j++)
		{
			COMPTR<ID3D11RenderTargetView> RTV = Textures[i]->GetRTV(j); // 하나의 텍스쳐가 여러 개를 가지고 있을 수 있음

			if (nullptr == RTV)
			{
				MsgAssert("존재하지 않는 랜더타겟뷰를 클리어할 수는 없음");
				return;
			}

			Ext_DirectXDevice::GetContext()->ClearRenderTargetView(RTV.Get(), Colors[i].Arr1D); // 기본 컬러(파란색)으로 클리어
			// 1. 클리어 대상
			// 2. 무슨 색으로 클리어 할것인가
		}
	}
}
// 렌더링 시작 시 깊이 및 스텐실 버퍼를 초기화하여, 이전 프레임에 남아있는 데이터 제거
void Ext_DirectXRenderTarget::DepthStencilViewClear()
{
	COMPTR<ID3D11DepthStencilView> DSV = DepthTexture->GetDSV(); // 깊이/스텐실 텍스쳐를 담고 있는 객체

	if (nullptr == DSV)
	{
		MsgAssert("존재하지 않는 뎁스스텐실뷰를 클리어할 수는 없음");
		return;
	}

	Ext_DirectXDevice::GetContext()->ClearDepthStencilView(DSV.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
	// 1. 클리어 대상
	// 2. 어떻게 클리어 할것인가
	// 3. 깊이 클리어값(가장 멀리)
	// 4. 스텐실 클리어값(일반적인 0을 넣음)
}

// 렌더 타겟 바인딩 담당, Draw(), Clear(), Shader Binding 등이 올바른 렌더 타겟에 수행될 수 있도록 설정
void Ext_DirectXRenderTarget::RenderTargetSetting()
{
	COMPTR<ID3D11RenderTargetView> RTV = Textures[0]->GetRTV(0); // 첫 번째 RTV(RenderTargetView) 를 가져옴(백 버퍼임)

	if (nullptr == RTV)
	{
		MsgAssert("랜더타겟 뷰가 존재하지 않아서 클리어가 불가능합니다.");
	}

	COMPTR<ID3D11DepthStencilView> DSV = DepthTexture->GetDSV(); // 깊이 텍스처에서 DSV(DepthStencilView) 획득

	//if (false == DepthSetting)
	//{
	//	DSV = nullptr;
	//}

	if (nullptr == DSV)
	{
		MsgAssert("뎁스스텐실뷰 왓");
	}
	
	Ext_DirectXDevice::GetContext()->OMSetRenderTargets(static_cast<UINT>(RTVs.size()), RTV.GetAddressOf(), DSV.Get()); // Output-Merger 스테이지에 렌더 타겟 + 뎁스 설정
	// 1. 바인딩할 렌더타겟뷰 갯수
	// 2. RTV의 시작 주소
	// 3. 깊이/스텐실 뷰 포인터(딱히 없으면nullptr 가능)
	Ext_DirectXDevice::GetContext()->RSSetViewports(static_cast<UINT>(ViewPorts.size()), &ViewPorts[0]); // Rasterizer 스테이지에 현재 프레임에서 사용할 ViewPort 영역 설정, 이게 있어야 NDC > 픽셀 공간 변환이 올바르게 수행됨
}

 

다음으로 MeshComponent에 적용될 카메라의 뷰, 프로젝션 행렬을 계산해줍니다.

void Ext_Camera::CameraTransformUpdate()
{
	float4 EyePos = GetTransform()->GetWorldPosition();
	float4 EyeDir = GetTransform()->GetLocalForwardVector();
	float4 EyeUp = GetTransform()->GetLocalUpVector();
	ViewMatrix.LookToLH(EyePos, EyeDir, EyeUp);

	switch (CameraType)
	{
	case ProjectionType::Perspective:
	{
		ProjectionMatrix.PerspectiveFovLH(FOV, Width / Height, Near, Far);
		break;
	}
	case ProjectionType::Orthogonal:
	{
		// Projection.OrthographicLH(Width * ZoomRatio, Height * ZoomRatio, Near, Far); 일단 패스
		break;
	}
	case ProjectionType::Unknown:
	{
		MsgAssert("카메라 투영이 설정되지 않았습니다.");
		break;
	}
	}

}

 

행렬 정보는 바로 MeshComponent에 반영해줍니다.

// 카메라의 MeshComponents들에 대한 업데이트 및 렌더링 파이프라인 리소스 정렬
void Ext_Camera::Rendering(float _Deltatime)
{
	// MeshComponents 렌더링 업데이트 시작
	for (auto& [Key, MeshComponentList] : MeshComponents)
	{
		// [!] 필요하면 ZSort 실시(나중에)

		for (const std::shared_ptr<Ext_MeshComponent>& CurMeshComponent : MeshComponentList)
		{
			if (!CurMeshComponent->GetIsUpdate()) continue;
			else
			{
				CurMeshComponent->Rendering(_Deltatime, GetViewMatrix(), GetProjectionMatrix()); // [3] 현재 MeshComponent에게 카메라의 View, Projection 곱해주기
				// [!] 필요하면 픽셀 셰이더에서 활용할 Value들 업데이트
			}
		}

	}
    //....
}

 

이제 메시 컴포넌트에 달려있는 MeshComponentUnits의 버퍼값과 셰이더정보를 활용하여 렌더링 파이프라인을 진행합니다.

// Mesh, Material의 RenderingPipeline Setting
void Ext_MeshComponentUnit::RenderUnitSetting()
{
	if (nullptr == Mesh)
	{
		MsgAssert("매쉬가 존재하지 않는 유니트 입니다");
	}

	if (nullptr == Material)
	{
		MsgAssert("파이프라인이 존재하지 않는 유니트 입니다");
	}

	InputLayout->InputLayoutSetting(); // InputLayout으로 IASetInputLayout 호출
	Mesh->MeshSetting();					 // InputAssembler 1단계, InputAssembler 2단계 호출 : IASetVertexBuffers, IASetPrimitiveTopology, IASetIndexBuffer
	Material->MaterialSetting();			 // VertexShader, PixelShader 호출(렌더링 파이프라인 단계 실행) : VSSetShader, PSSetShader
	BufferSetter.BufferSetting();			 // Map -> memcpy -> UnMap, VSSetConstantBuffers, PSSetConstantBuffers 실시
}

 

순서대로 확인해보겠습니다.

// 입력 조립기 단계 중 정점 입력 레이아웃을 설정하는 함수, GPU가 정점 버퍼의 데이터를 올바르게 해석하기 위해 필수
void Ext_DirectXInputLayout::InputLayoutSetting()
{
	if (nullptr == InputLayout)
	{
		MsgAssert("생성되지 않은 인풋레이아웃을 세팅하려고 했습니다.");
	}

	Ext_DirectXDevice::GetContext()->IASetInputLayout(InputLayout);
	// POSITION, COLOR, TEXTCOOR, NORMAL 등 정보를 GPU에 알려줘서 셰이더와 정점 데이터가 정확하게 매칭되도록 함
}

// 정점 버퍼 종류 설정
void Ext_DirectXVertexBuffer::VertexBufferSetting()
{
	if (nullptr == VertexBuffer)
	{
		MsgAssert("버텍스 버퍼가 없어 세팅 불가");
		return;
	}

	Ext_DirectXDevice::GetContext()->IASetVertexBuffers(0, 1, VertexBuffer.GetAddressOf(), &VertexSize, &Offset);
	// 1. 가본 정점 슬롯
	// 2. 버퍼 하나만 사용
	// 3. 버텍스 버퍼 주소
	// 4. 정점 하나당 크기(Stride)
	// 5. 버퍼 시작 오프셋(일반적으로 0)
}

// 정점 버퍼 / 도형 종류 설정
void Ext_DirectXMesh::InputAssembler1()
{
	if (nullptr == VertexBufferPtr)
	{
		MsgAssert("버텍스 버퍼가 존재하지 않아서 인풋어셈블러1 과정을 실행할 수 없습니다.");
		return;
	}

	Ext_DirectXDevice::GetContext()->IASetPrimitiveTopology(Topology); // 도형 종류 설정
	// GPU에 삼각형 리스트, 라인 스트립, 포인트 리스트 등을 설정
	// 정점들을 삼각형으로 해석하도록 하기 위해 D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST 전달
}

// 인덱스 버퍼 종류 설정
void Ext_DirectXIndexBuffer::IndexBufferSetting()
{
	if (nullptr == IndexBuffer)
	{
		MsgAssert("ID3DBuffer가 만들어지지 않은 버텍스 버퍼 입니다.");
		return;
	}

	Ext_DirectXDevice::GetContext()->IASetIndexBuffer(IndexBuffer.Get(), Format, Offset);
	// 1. 인덱스 버퍼
	// 2. 인덱스 데이터 타입, DXGI_FORMAT_R32_UINT 전달
	// 3. Offset 전달, 보통 0부터 시작
}

// VSSetShader 호출로 정점 셰이더 세팅
void Ext_DirectXVertexShader::VertexShaderSetting()
{
	if (nullptr == VertexShader.Get())
	{
		MsgAssert("정점 셰이더가 존재하지 않아 세팅에 실패");
	}

	Ext_DirectXDevice::GetContext()->VSSetShader(VertexShader.Get(), nullptr, 0);
	// 1. 바인디할 셰이더 객체
	// 2. 셰이더에 사용할 클래스 배열 전달(보통 nullptr)
	// 3. 클래스 개수(보통 0)
}

void Ext_DirectXMaterial::PixelShaderSetting()
{
	if (nullptr == PixelShader)
	{
		MsgAssert("픽셀 쉐이더가 존재하지 않아서 픽셀 쉐이더 과정을 실행할 수 없습니다.");
		return;
	}

	PixelShader->PixelShaderSetting();
}

// Map(), Unmap() 실시
void Ext_DirectXConstantBuffer::ChangeData(const void* _Data, UINT _Size)
{
	// 머티리얼들은 상수버퍼나 이런걸 통일해 놓은 것이다.
	if (nullptr == _Data)
	{
		std::string CurName = Name;
		MsgAssert(CurName + "에 nullptr인 데이터를 세팅하려고 했습니다.");
		return;
	}

	if (ConstantBufferInfo.ByteWidth != _Size)
	{
		std::string CurName = Name;
		MsgAssert(CurName + "크기가 다른 데이터가 들어왔습니다.");
		return;
	}

	D3D11_MAPPED_SUBRESOURCE SettingResources = { 0, };

	// 매 프레임마다 카메라 위치, 뷰/프로젝션 행렬, 시간, 조명 데이터 등을 셰이더에 넘겨야 할 때(버퍼의 내용을 바꾸는 경우)를 위해 D3D11_CPU_ACCESS_WRITE 설정, 이러면 반드시 D3D11_USAGE_DYNAMIC 이어야함
	// 설정이 없으면 Map이 불가능 → UpdateSubresource()를 사용해야 함 (비효율적), GPU에서만 읽고 CPU가 직접 쓰지 못해 프레임별 갱신이 불가능
	Ext_DirectXDevice::GetContext()->Map(ConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &SettingResources); // GPU 버퍼를 CPU에서 쓸 수 있도록 잠금
	// 1. 실제 GPU 상수 버퍼 객체
	// 2. 서브 리소스 index(항상 0)
	// 3. 기존 내용을 폐기하고 새로 쓰기(가장 빠름)
	// 4. ?
	// 5. 매핑된 데이터 포인터를 받을 구조체

	if (SettingResources.pData == nullptr)
	{
		std::string CurName = Name;
		MsgAssert(CurName + " 그래픽카드에게 메모리 접근을 허가받지 못했습니다.");
		return;
	}
	memcpy_s(SettingResources.pData, ConstantBufferInfo.ByteWidth, _Data, ConstantBufferInfo.ByteWidth); // CPU측 메모리 _Data의 내용을 GPU측 상수 버퍼로 복사
	Ext_DirectXDevice::GetContext()->Unmap(ConstantBuffer, 0); // 쓰기 완료(잠금 해제), GPU에게 알려줌, 이거 안하면 렌더링 중 GPU가 해당 버퍼를 읽으려할 때 충돌 발생 가능
}

// 정점 셰이더 바인딩
void Ext_DirectXConstantBuffer::VSSetting(UINT _Slot)
{
	Ext_DirectXDevice::GetContext()->VSSetConstantBuffers(_Slot, 1, &ConstantBuffer);
	// 1. 셰이더 내 CBuffer가 바인딩된 슬롯 인덱스(cbuffer TransformData : register(b0) 한개만 있으니, 0 전달됨)
	// 2. 설정할 버퍼 개수(하나만 바인딩)
	// 3. ID3D11Buffer* 주소 전달
}

// PSSetConstantBuffers() 호출
void Ext_DirectXConstantBuffer::PSSetting(UINT _Slot)
{
	Ext_DirectXDevice::GetContext()->PSSetConstantBuffers(_Slot, 1, &ConstantBuffer);
	// 아직 뭐 없음
}

// 정점 정보들과 셰이더를 통해 메시 Draw 실시(DrawIndexed Call)
void Ext_MeshComponentUnit::RenderUnitDraw()
{
	UINT IndexCount = Mesh->GetIndexBuffer()->GetVertexCount();
	Ext_DirectXDevice::GetContext()->DrawIndexed(IndexCount, 0, 0);
}

// 정점 정보들과 셰이더를 통해 메시 Draw 실시(DrawIndexed Call)
void Ext_MeshComponentUnit::RenderUnitDraw()
{
	UINT IndexCount = Mesh->GetIndexBuffer()->GetVertexCount();
	Ext_DirectXDevice::GetContext()->DrawIndexed(IndexCount, 0, 0); // 파이프라인 설정 완료 후, GPU에게 그리라고 명령
	// 1. 그릴 인덱스 갯수(Mesh가 가진 전체 인덱스 갯수)
	// 2. 인덱스 버퍼 내에서 시작할 위치(보통 0부터 시작)
	// 3. 정점 버퍼 내 정점 인덱스의 시작값 오프셋(주로 0)
}

 

위의 일련의 렌더링 파이프라인 단계를 거쳐, DrawIndexed를 실시하면 백버퍼에 출력 결과가 그려집니다. 마지막으로 Present를 실시해주면 됩니다.

// MeshComponent Render 업데이트 후 백버퍼에 Present 호출
void Ext_DirectXDevice::RenderEnd()
{
	HRESULT Result = SwapChain->Present(0, 0);
	if (Result == DXGI_ERROR_DEVICE_REMOVED || Result == DXGI_ERROR_DEVICE_RESET)
	{
		// 디바이스 다시만들기
		MsgAssert("랜더타겟 생성에 실패했습니다.");
		return;
	}
}

 

 

[결과]

 

화면을 자세히 보면 가운데 초록색 점이 하나 보입니다. 이건 Object의 크기가 너무 작아서 그런 것인데, 크기를 키워도 되고 거리를 가깝게 해도 됩니다. 거리를 가깝게 해보겠습니다. 지금은 카메라의 월드 위치가 { 0.0f, 0.0f, 100.0f }에 위치하고 있는데 { 0.0f, 0.0f, 1.0f }로 변경해보겠습니다.

 

생각한대로 잘 출력되었습니다.

 

[다양한 출력]

 

Component는 여러 개를 등록할 수가 있습니다.

#include "PrecompileHeader.h"
#include "TestScene.h"

#include "RectActor.h"
#include <DirectX11_Extension/Ext_Scene.h>
#include <DirectX11_Extension/Ext_Camera.h>
#include <DirectX11_Extension/Ext_Transform.h>
#include <DirectX11_Extension/Ext_MeshComponent.h>

TestScene::TestScene()
{
}

TestScene::~TestScene()
{
}

void TestScene::Start()
{
	GetMainCamera()->GetTransform()->SetWorldPosition({ 0.f, 0.f, 0.f });

	std::shared_ptr<RectActor> Rect1 = CreateActor<RectActor>("RectActor");
	Rect1->GetTransform()->SetWorldPosition({ 0.f, 0.f, 10.f });
	std::shared_ptr<Ext_MeshComponent> MeshComp1 = Rect1->CreateComponent<Ext_MeshComponent>("BasicMesh", true);
	MeshComp1->CreateMeshComponentUnit("Rect", "Basic");

	std::shared_ptr<RectActor> Rect2 = CreateActor<RectActor>("RectActor");
	Rect2->GetTransform()->SetWorldPosition({ 10.f, 0.f, 10.f });
	std::shared_ptr<Ext_MeshComponent> MeshComp2 = Rect2->CreateComponent<Ext_MeshComponent>("BasicMesh", true);
	MeshComp2->CreateMeshComponentUnit("Rect", "Basic");

	std::shared_ptr<RectActor> Rect3 = CreateActor<RectActor>("RectActor");
	Rect3->GetTransform()->SetWorldPosition({ -10.f, 0.f, 10.f });
	std::shared_ptr<Ext_MeshComponent> MeshComp3 = Rect3->CreateComponent<Ext_MeshComponent>("BasicMesh3", true);
	MeshComp3->CreateMeshComponentUnit("Rect", "Basic");

}

void TestScene::Update(float _DeltaTime)
{
	__super::Update(_DeltaTime);
}

void TestScene::Destroy()
{

}

 

각각 회전 처리도 가능합니다.

#include "PrecompileHeader.h"
#include "RectActor.h"

#include <DirectX11_Extension/Ext_MeshComponent.h>
#include <DirectX11_Extension/Ext_Transform.h>

void RectActor::Start()
{

}

void RectActor::Update(float _DeltaTime)
{
	std::shared_ptr<Ext_Transform> Transform = GetTransform();

	float MoveSpeed = 100.0f; // 초당 100 단위 회전
	GetTransform()->AddWorldRotation({ 0.f, MoveSpeed * _DeltaTime, 0.f });
}

 

물론 정점 버퍼와 정점 인덱스를 큐브 형태로 만든 다음, 그것을 출력할 수도 있습니다.

// 정육면체
{
	std::vector<Ext_DirectXVertexData> Vertex =
	{
		// Front (+Z)
		{ {-0.5f,  0.5f,  0.5f, 1.0f}, {1, 0, 0, 1}, {0, 0}, {0, 0, 1} },
		{ { 0.5f,  0.5f,  0.5f, 1.0f}, {0, 1, 0, 1}, {1, 0}, {0, 0, 1} },
		{ { 0.5f, -0.5f,  0.5f, 1.0f}, {0, 0, 1, 1}, {1, 1}, {0, 0, 1} },
		{ {-0.5f, -0.5f,  0.5f, 1.0f}, {1, 1, 0, 1}, {0, 1}, {0, 0, 1} },

		// Back (-Z)
		{ { 0.5f,  0.5f, -0.5f, 1.0f}, {1, 0, 0, 1}, {0, 0}, {0, 0, -1} },
		{ {-0.5f,  0.5f, -0.5f, 1.0f}, {0, 1, 0, 1}, {1, 0}, {0, 0, -1} },
		{ {-0.5f, -0.5f, -0.5f, 1.0f}, {0, 0, 1, 1}, {1, 1}, {0, 0, -1} },
		{ { 0.5f, -0.5f, -0.5f, 1.0f}, {1, 1, 0, 1}, {0, 1}, {0, 0, -1} },

		// Left (-X)
		{ {-0.5f,  0.5f, -0.5f, 1.0f}, {1, 0, 0, 1}, {0, 0}, {-1, 0, 0} },
		{ {-0.5f,  0.5f,  0.5f, 1.0f}, {0, 1, 0, 1}, {1, 0}, {-1, 0, 0} },
		{ {-0.5f, -0.5f,  0.5f, 1.0f}, {0, 0, 1, 1}, {1, 1}, {-1, 0, 0} },
		{ {-0.5f, -0.5f, -0.5f, 1.0f}, {1, 1, 0, 1}, {0, 1}, {-1, 0, 0} },

		// Right (+X)
		{ { 0.5f,  0.5f,  0.5f, 1.0f}, {1, 0, 0, 1}, {0, 0}, {1, 0, 0} },
		{ { 0.5f,  0.5f, -0.5f, 1.0f}, {0, 1, 0, 1}, {1, 0}, {1, 0, 0} },
		{ { 0.5f, -0.5f, -0.5f, 1.0f}, {0, 0, 1, 1}, {1, 1}, {1, 0, 0} },
		{ { 0.5f, -0.5f,  0.5f, 1.0f}, {1, 1, 0, 1}, {0, 1}, {1, 0, 0} },

		// Top (+Y)
		{ {-0.5f,  0.5f, -0.5f, 1.0f}, {1, 0, 0, 1}, {0, 0}, {0, 1, 0} },
		{ { 0.5f,  0.5f, -0.5f, 1.0f}, {0, 1, 0, 1}, {1, 0}, {0, 1, 0} },
		{ { 0.5f,  0.5f,  0.5f, 1.0f}, {0, 0, 1, 1}, {1, 1}, {0, 1, 0} },
		{ {-0.5f,  0.5f,  0.5f, 1.0f}, {1, 1, 0, 1}, {0, 1}, {0, 1, 0} },

		// Bottom (-Y)
		{ {-0.5f, -0.5f,  0.5f, 1.0f}, {1, 0, 0, 1}, {0, 0}, {0, -1, 0} },
		{ { 0.5f, -0.5f,  0.5f, 1.0f}, {0, 1, 0, 1}, {1, 0}, {0, -1, 0} },
		{ { 0.5f, -0.5f, -0.5f, 1.0f}, {0, 0, 1, 1}, {1, 1}, {0, -1, 0} },
		{ {-0.5f, -0.5f, -0.5f, 1.0f}, {1, 1, 0, 1}, {0, 1}, {0, -1, 0} },
	};

	std::vector<UINT> Index;
	for (int i = 0; i < 6; ++i)
	{
		int Base = i * 4;

		Index.push_back(Base + 0);
		Index.push_back(Base + 1);
		Index.push_back(Base + 2);

		Index.push_back(Base + 2);
		Index.push_back(Base + 3);
		Index.push_back(Base + 0);
	}

	Ext_DirectXVertexBuffer::CreateVertexBuffer("Box", Vertex);
	Ext_DirectXIndexBuffer::CreateIndexBuffer("Box", Index);
	std::shared_ptr<Ext_DirectXMesh> Mesh = Ext_DirectXMesh::CreateMesh("Box");
}
#include "PrecompileHeader.h"
#include "CubeActor.h"

#include <DirectX11_Extension/Ext_MeshComponent.h>
#include <DirectX11_Extension/Ext_Transform.h>

void CubeActor::Start()
{
	GetTransform()->SetWorldPosition({ 0.f, 0.f, 20.0f }); // 위치 설정
	GetTransform()->SetWorldScale({ 10.f, 10.f, 10.f }); // 크기 설정
	std::shared_ptr<Ext_MeshComponent> MeshComp = CreateComponent<Ext_MeshComponent>("BasicMesh", true); // 메시 생성
	MeshComp->CreateMeshComponentUnit("Box", "Basic"); // 렌더링 세팅 설정
}

void CubeActor::Update(float _DeltaTime)
{
	float MoveSpeed = 100.0f; // 초당 10 단위 회전

	GetTransform()->AddWorldRotation({ MoveSpeed * _DeltaTime, 0.f, 0.f });
	GetTransform()->AddWorldRotation({ 0.f, MoveSpeed * _DeltaTime, 0.f });
	GetTransform()->AddWorldRotation({ 0.f, 0.f, MoveSpeed * _DeltaTime });
}

 

 

이상으로 렌더링 파이프라인 세팅 과정을 마치겠습니다. 이후에는 블렌드, 샘플러 등 추가 기능들을 알아보도록 하겠습니다.

[목차]

 

- 클래스 정의

- Ext_DirectXMesh

- Ext_DirectXMaterial

- Set Unit

 

 

[클래스 정의]

 

렌더링 파이프라인 설정값을 렌더링될 Object마다 원하는 세팅으로 설정해주면 좋을 것입니다. 이렇게 하는게 가능한 이유가 해당 프레임워크에서는 리소스를 만들때마다 리소스 매니저를 통해 값들을 이름으로 저장하고 있기 때문입니다.

 

이걸 십분 활용해서 Ext_DirectXMesh 클래스와 Ext_DirectXMaterial 클래스를 만들었습니다. Mesh 클래스는 InputAssembler 단계에 대한 정보를 담은 클래스, Material 클래스는 그 외의 모든 단계에 대한 정보를 담은 클래스입니다.

 

사용 방식은 다음과 같습니다.

#include "PrecompileHeader.h"
#include "CubeActor.h"

#include <DirectX11_Extension/Ext_MeshComponent.h>
#include <DirectX11_Extension/Ext_Transform.h>

void CubeActor::Start()
{
	GetTransform()->SetWorldPosition({ 0.f, 0.f, 200.0f });
	GetTransform()->SetWorldScale({ 100.f, 100.f, 100.f }); // 크기 확대
	std::shared_ptr<Ext_MeshComponent> MeshComp = CreateComponent<Ext_MeshComponent>("BasicMesh", true);
	MeshComp->CreateMeshComponentUnit("Box", "Basic");
}

 

현재의 프레임워크 구조에서, Actor 내부에서 Component를 Create 하는 함수를 호출하면 Component가 생성됩니다. 여기서 MeshComponent의 경우 CreateMeshComponentUnit() 함수를 호출하면서 Mesh와 Material을 이름으로 하여 세팅하도록 설계해뒀습니다. 이러면 이름으로 값을 찾아서 내부에서 세팅해줍니다. 물론 먼저 만들어둔 값이 있어야 할 것입니다.

 

 

[Ext_DirectXMesh]

 

먼저 Mesh 클래스입니다.

////////////////////////// Ext_DirectXMesh.h
#pragma once
#include "Ext_ResourceManager.h"
#include "Ext_DirectXVertexBuffer.h"
#include "Ext_DirectXIndexBuffer.h"

// 만들어진 버텍스들의 정보를 저장하기 위한 클래스
class Ext_DirectXMesh : public Ext_ResourceManager<Ext_DirectXMesh>
{
	friend class Ext_MeshComponentUnit;

public:
	// constrcuter destructer
	Ext_DirectXMesh() {}
	~Ext_DirectXMesh() {}

	// delete Function
	Ext_DirectXMesh(const Ext_DirectXMesh& _Other) = delete;
	Ext_DirectXMesh(Ext_DirectXMesh&& _Other) noexcept = delete;
	Ext_DirectXMesh& operator=(const Ext_DirectXMesh& _Other) = delete;
	Ext_DirectXMesh& operator=(Ext_DirectXMesh&& _Other) noexcept = delete;

	// 버텍스 버퍼와 인덱스 버퍼 정보 입력 및 메시 생성
	static std::shared_ptr<Ext_DirectXMesh> CreateMesh(std::string_view _Name)
	{
		return CreateMesh(_Name, _Name, _Name);
	}

	// 버텍스 버퍼와 인덱스 버퍼 정보 입력 및 생성
	static std::shared_ptr<Ext_DirectXMesh> CreateMesh(std::string_view _Name, std::string_view _VBName, std::string_view _IBName)
	{
		std::shared_ptr<Ext_DirectXMesh> NewMesh = Ext_ResourceManager::CreateNameResource(_Name);
		NewMesh->VertexBufferPtr = Ext_DirectXVertexBuffer::Find(_VBName);
		NewMesh->IndexBufferPtr = Ext_DirectXIndexBuffer::Find(_IBName);

		if ((nullptr == NewMesh->VertexBufferPtr) || (nullptr == NewMesh->IndexBufferPtr))
		{
			MsgAssert("메시 생성 실패");
		}

		return NewMesh;
	}

	// Getter
	std::shared_ptr<class Ext_DirectXVertexBuffer> GetVertexBuffer() { return VertexBufferPtr; }
	std::shared_ptr<class Ext_DirectXIndexBuffer> GetIndexBuffer() { return IndexBufferPtr; }

protected:
	
private:
	D3D11_PRIMITIVE_TOPOLOGY Topology = D3D11_PRIMITIVE_TOPOLOGY::D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
	std::shared_ptr<class Ext_DirectXVertexBuffer> VertexBufferPtr; // 상수버퍼 데이터 저장용
	std::shared_ptr<class Ext_DirectXIndexBuffer> IndexBufferPtr; // 인덱스버퍼 데이터 저장용
	
	void MeshSetting(); // InputAssembler1(), InputAssembler2() 호출
	void InputAssembler1(); // 인풋어셈블러 1단계 실시, IASetVertexBuffers(), IASetPrimitiveTopology() 실시
	void InputAssembler2(); // 인풋어셈블러 2단계 실시, IndexBufferSetting() 호출
};

////////////////////////// Ext_DirectXMesh.cpp
#include "PrecompileHeader.h"
#include "Ext_DirectXMesh.h"
#include "Ext_DirectXVertexBuffer.h"
#include "Ext_DirectXIndexBuffer.h"
#include "Ext_DirectXDevice.h"

void Ext_DirectXMesh::MeshSetting()
{
	InputAssembler1();
	InputAssembler2();
}

void Ext_DirectXMesh::InputAssembler1()
{
	if (nullptr == VertexBufferPtr)
	{
		MsgAssert("버텍스 버퍼가 존재하지 않아서 인풋어셈블러1 과정을 실행할 수 없습니다.");
		return;
	}

	VertexBufferPtr->VertexBufferSetting();

	Ext_DirectXDevice::GetContext()->IASetPrimitiveTopology(Topology);
}

void Ext_DirectXMesh::InputAssembler2()
{
	// 그리는 순서에 대한 데이터를 넣어준다 // 012023
	if (nullptr == IndexBufferPtr)
	{
		MsgAssert("인덱스 버퍼가 존재하지 않아서 인풋 어셈블러2 과정을 실행할 수 없습니다.");
		return;
	}

	IndexBufferPtr->IndexBufferSetting();
}

 

Mesh 클래스는 Ext_DirectXVertexBuffer, Ext_DirectXIndexBuffer 정보를 가지고 있는 클래스입니다. 아래와 같이 사용해봅시다.

// 삼각형
{
	std::vector<Ext_DirectXVertexData> Vertices;
	Vertices.resize(3);

	Vertices[0] = { { 0.0f, 0.5f, 0.0 }, { 1, 0, 0, 1 } };
	Vertices[1] = { { 0.5f, -0.5f, 0.0f }, { 0, 1, 0, 1 } };
	Vertices[2] = { { -0.5f, -0.5f, 0.0f }, { 0, 0, 1, 1 } };

	std::vector<UINT> ArrIndex = { 0, 2, 1 };

	Ext_DirectXVertexBuffer::CreateVertexBuffer("Triangle", Vertices);
	Ext_DirectXIndexBuffer::CreateIndexBuffer("Triangle", ArrIndex);
	Ext_DirectXMesh::CreateMesh("Triangle");
}

// Rect
{
	std::vector<Ext_DirectXVertexData> ArrVertex;
	ArrVertex.resize(4);

	ArrVertex[0] = { { -0.5f,  0.5f, 0.0f, 1.0f }, { 1, 0, 0, 1 }, /*{ 0.0f, 0.0f }*/ };
	ArrVertex[1] = { {  0.5f,  0.5f, 0.0f, 1.0f }, { 0, 1, 0, 1 }, /*{ 1.0f, 0.0f }*/ };
	ArrVertex[2] = { { -0.5f, -0.5f, 0.0f, 1.0f }, { 0, 0, 1, 1 }, /*{ 1.0f, 1.0f }*/ };
	ArrVertex[3] = { {  0.5f, -0.5f, 0.0f, 1.0f }, { 1, 1, 0, 1 }, /*{ 0.0f, 1.0f }*/ };

	std::vector<UINT> ArrIndex = { 0, 2, 1, 2, 3, 1 };

	Ext_DirectXVertexBuffer::CreateVertexBuffer("Rect", ArrVertex);
	Ext_DirectXIndexBuffer::CreateIndexBuffer("Rect", ArrIndex);
	Ext_DirectXMesh::CreateMesh("Rect");
}

 

CreateVertexBuffer(), CreateIndexBuffer()을 실시할 때 Triangle, Rect라는 이름으로 저장해줬습니다. 이러면 CreateMesh()를 실시할 때 동일한 이름을 넣어줘서 내부에서 Triangle, Rect라는 이름의 Vertex Buffer, Index Buffer를 Find()한 뒤 저장해두는 것입니다. 이러면 Mesh 클래스 하나로 두 가지 클래스를 바로 활용할 수 있게 됩니다.

 

 

[Ext_DirectXMaterial]

 

다음은 Material 클래스입니다.

////////////////////// Ext_DirectXMaterial.h
#pragma once
#include "Ext_ResourceManager.h"

// 렌더링 파이프라인 세팅 실시를 위한 클래스
class Ext_DirectXMaterial : public Ext_ResourceManager<Ext_DirectXMaterial>
{
	friend class Ext_MeshComponentUnit;

public:
	// constrcuter destructer
	Ext_DirectXMaterial() {};
	~Ext_DirectXMaterial() {};

	// delete Function
	Ext_DirectXMaterial(const Ext_DirectXMaterial& _Other) = delete;
	Ext_DirectXMaterial(Ext_DirectXMaterial&& _Other) noexcept = delete;
	Ext_DirectXMaterial& operator=(const Ext_DirectXMaterial& _Other) = delete;
	Ext_DirectXMaterial& operator=(Ext_DirectXMaterial&& _Other) noexcept = delete;

	// 머티리얼 생성
	static std::shared_ptr<class Ext_DirectXMaterial> CreateMaterial(const std::string_view& _Name)
	{
		std::shared_ptr<class Ext_DirectXMaterial> NewRes = Ext_ResourceManager<Ext_DirectXMaterial>::CreateNameResource(_Name);
		return NewRes;
	}

	// 머티리얼 세팅 실시
	void SetVertexShader(std::string_view _Name);
	void SetPixelShader(std::string_view _Name);
	void SetBlendState(std::string_view _Name);
	void SetDepthState(std::string_view _Name);
	void SetRasterizer(std::string_view _Name);

	// Getter, Setter
	std::shared_ptr<class Ext_DirectXVertexShader> GetVertexShader() { return VertexShader; }
	std::shared_ptr<class Ext_DirectXPixelShader> GetPixelShader() { return PixelShader; }

protected:
	
private:
	// 렌더링 파이프라인 세팅
	void MaterialSetting(); // 아래의 함수들 차례대로 실행
	void VertexShaderSetting();
	void HullShaderSetting();
	void TessellatorSetting();
	void DomainShaderSetting();
	void GeometryShaderSetting();
	void RasterizerSetting();
	void PixelShaderSetting();
	void OutputMergerSetting();

	std::shared_ptr<class Ext_DirectXVertexShader> VertexShader; // 버텍스 셰이더 저장
	std::shared_ptr<class Ext_DirectXPixelShader> PixelShader; // 픽셀 셰이더 저장
	
};

////////////////////// Ext_DirectXMaterial.cpp
#include "PrecompileHeader.h"
#include "Ext_DirectXMaterial.h"
#include <DirectX11_Base/Base_String.h>

#include "Ext_DirectXVertexShader.h"
#include "Ext_DirectXPixelShader.h"

void Ext_DirectXMaterial::SetVertexShader(std::string_view _Name)
{
	std::string UpperName = Base_String::ToUpper(_Name);
	VertexShader = Ext_DirectXVertexShader::Find(UpperName);

	if (nullptr == VertexShader)
	{
		MsgAssert("존재하지 않는 버텍스셰이더를 세팅할 순 없음");
		return;
	}
}

void Ext_DirectXMaterial::SetPixelShader(std::string_view _Name)
{
	std::string UpperName = Base_String::ToUpper(_Name);
	PixelShader = Ext_DirectXPixelShader::Find(UpperName);

	if (nullptr == PixelShader)
	{
		MsgAssert("존재하지 않는 픽셀 셰이더를 세팅할 순 없음");
	}
}

void Ext_DirectXMaterial::SetBlendState(std::string_view _Name)
{
}

void Ext_DirectXMaterial::SetDepthState(std::string_view _Name)
{
}

void Ext_DirectXMaterial::SetRasterizer(std::string_view _Name)
{
}

void Ext_DirectXMaterial::MaterialSetting()
{
	VertexShaderSetting();
	// HullShader();
	// Tessellator();
	// DomainShader();
	// GeometryShader();
	// Rasterizer();
	PixelShaderSetting();
	OutputMergerSetting();
}

void Ext_DirectXMaterial::VertexShaderSetting()
{
	if (nullptr == VertexShader)
	{
		MsgAssert("버텍스 쉐이더가 존재하지 않아서 버텍스 쉐이더 과정을 실행할 수 없습니다.");
		return;
	}

	VertexShader->VertexShaderSetting();
}

void Ext_DirectXMaterial::HullShaderSetting()
{

}
void Ext_DirectXMaterial::TessellatorSetting()
{

}
void Ext_DirectXMaterial::DomainShaderSetting()
{

}
void Ext_DirectXMaterial::GeometryShaderSetting()
{
}

void Ext_DirectXMaterial::RasterizerSetting()
{
}

void Ext_DirectXMaterial::PixelShaderSetting()
{
	if (nullptr == PixelShader)
	{
		MsgAssert("픽셀 쉐이더가 존재하지 않아서 픽셀 쉐이더 과정을 실행할 수 없습니다.");
		return;
	}

	PixelShader->PixelShaderSetting();
}

void Ext_DirectXMaterial::OutputMergerSetting()
{
}

 

머티리얼 클래스는 InputAssembler 이외의 단계를 진행하기 위한 정보들을 담고, 렌더링 파이프라인 세팅 순간에 그 정보들을 가져와 Setting을 실시해주는 클래스입니다.

 

물론 각 정보들은 이미 만들어져 있어야하며, 지금은 Vertex Shader와 Pixel Shader만 만들어졌기 때문에 이 값들만 저장하고 있는 클래스가 될 것입니다. 나중에 단계들을 배우고 추가하게 되면 여기에 바로바로 추가해주면 됩니다. 우선은 아래와 같이 사용하면 됩니다.

// 일반(단일 메시)
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("Basic");
	NewRenderingPipeline->SetVertexShader("Basic_VS");
	NewRenderingPipeline->SetPixelShader("Basic_PS");
	NewRenderingPipeline->SetBlendState("BaseBlend"); // 아직 없음
	NewRenderingPipeline->SetDepthState("EngineDepth"); // 아직 없음
	NewRenderingPipeline->SetRasterizer("EngineRasterizer"); // 아직 없읍
}

// 다양한 텍스쳐가 있는 메시
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("PBR");
	NewRenderingPipeline->SetVertexShader("Basic_VS");
	NewRenderingPipeline->SetPixelShader("PBR_PS");
	NewRenderingPipeline->SetBlendState("BaseBlend"); // 아직 없음
	NewRenderingPipeline->SetDepthState("EngineDepth"); // 아직 없음
	NewRenderingPipeline->SetRasterizer("EngineRasterizer"); // 아직 없음
}

 

이러면 Basic, PBR이라는 이름으로 Material들이 값들을 하나로 묶어서 저장하기 때문에, 클래스 하나만 써서 바로 활용할 수 있게 됩니다.

 

 

[Set Unit]

 

이제 다시 MeshComponentUnitInitialize() 함수로 돌아가보겠습니다. 함수 내에서는 앞서 생성해둔 Mesh와 Material 정보를 가져와 자신에게 세팅해줍니다. 그리고 InputLayout을 Create해서 Mesh, Material, InputLayout 값을 지니고 있게 됩니다.

// 메시 컴포넌트 유닛 생성 시 호출, Mesh, Material, ConstantBuffer 세팅
void Ext_MeshComponentUnit::MeshComponentUnitInitialize(std::string_view _MeshName, std::string_view _MaterialName)
{
	Mesh = Ext_DirectXMesh::Find(_MeshName); // 메시 설정
	Material = Ext_DirectXMaterial::Find(_MaterialName); // 머티리얼 설정
	
	if (nullptr == Mesh || nullptr == Material)
	{
		MsgAssert("존재하지 않는 메시나 머티리얼을 메시유닛에 넣을 수는 없습니다.")
	}

	InputLayout->CreateInputLayout(Mesh->GetVertexBuffer(), Material->GetVertexShader()); // InputLayout 설정

	// 상수버퍼 세팅
	// [1] 버텍스 셰이더 정보 가져오기
	const Ext_DirectXBufferSetter& VertexShaderBuffers = Material->GetVertexShader()->GetBufferSetter();
	BufferSetter.Copy(VertexShaderBuffers);

	// [2] 픽셀 셰이더 정보 가져오기
	const Ext_DirectXBufferSetter& PixelShaderBuffers = Material->GetPixelShader()->GetBufferSetter();
	BufferSetter.Copy(PixelShaderBuffers);

	// [3] 트랜스폼 상수버퍼 세팅하기
	const TransformData& Data = *(OwnerMeshComponent.lock()->GetTransform()->GetTransformData().get());
	BufferSetter.SetConstantBufferLink("TransformData", Data);

	// [4] 카메라에 넣기
	GetOwnerMeshComponent().lock()->GetOwnerCamera().lock()->PushMeshComponentUnit(GetSharedFromThis<Ext_MeshComponentUnit>(), RenderPath::Unknown);
}

 

이러면 MeshComponent의 계층 구조는 다음과 같아질 것입니다.

MeshComponent
└── MeshComponentUnit
    ├── Ext_DirectXMesh
    │   ├── Ext_DirectXVertexBuffer
    │   └── Ext_DirectXIndexBuffer
    ├── Ext_DirectXMaterial
    │   ├── Ext_DirectXVertexShader
    │   ├── Ext_DirectXPixelShader
    │   ├── Ext_DirectXBlend
    │   ├── Ext_DirectXDepth
    │   └── Ext_DirectXRasterizer
    └── Ext_DirectXInputLayout

 

InputLayout을 여기서 Create() 하는 이유는 InputLayout이 Vertex Shader 컴파일 후 호출되어야 하기 때문입니다.

void Ext_DirectXInputLayout::CreateInputLayout(std::shared_ptr<Ext_DirectXVertexBuffer> _VertexBuffer, std::shared_ptr< Ext_DirectXVertexShader> _VertexShader)
{
	Relase(); // 한번 해줘야 CreateInputLayout가 정상 동작함

	if (nullptr == _VertexBuffer->GetInputLayout())
	{
		MsgAssert("레이아웃 정보를 만들수 없는 버텍스 버퍼 입니다.");
	}

	const std::vector<D3D11_INPUT_ELEMENT_DESC>& LayOutInfo = _VertexBuffer->GetInputLayout()->GetInputLayoutDescs();

	// CreateInputLayout은 정점 버퍼 구조와 셰이더 입력 구조 간의 매핑을 정의
	HRESULT hr = Ext_DirectXDevice::GetDevice()->CreateInputLayout
	(
		&LayOutInfo[0],
		static_cast<UINT>(LayOutInfo.size()),
		_VertexShader->GetBinaryCode()->GetBufferPointer(),
		_VertexShader->GetBinaryCode()->GetBufferSize(),
		&InputLayout
	);

	if (S_OK != hr)
	{
		char Buffer[256] = {};
		sprintf_s(Buffer, "CreateInputLayout failed: 0x%08X", hr);
		MsgAssert(Buffer)
		return;
	}
}

 

const D3D11_INPUT_ELEMENT_DESC* 정점 입력 요소 배열의 시작 주소, 각 요소가 어떤 데이터인지 정의
UINT 위 배열의 요소 수(입력 요소가 몇개인가)
const void* 정점 셰이더의 바이트 코드 시작 주소, 여기에는 입력 시그니처가 포함되어 있어야 함
SIZE_T 위 바이트코드의 길이
ID3D11InputLayout** 호출의 결과로 생성될 InputLayout 객체의 포인터 주소 저장->IASetInputLayout에서 활용

 

위의 과정이 완료되면 셰이더 컴파일 진행 후 생성된 리소스 정보들(바인딩 슬롯은 몇번째인지, 슬롯 이름은 무엇인지)을 가져와서 저장해줍니다.

// 버퍼 세팅 복사/붙여넣기 실시
void Ext_DirectXBufferSetter::Copy(const Ext_DirectXBufferSetter& _OtherBufferSetter)
{
	for (const std::pair<std::string, ConstantBufferSetter>& Setter : _OtherBufferSetter.ConstantBufferSetters)
	{
		ConstantBufferSetters.insert(Setter);
	}
    //...
}

 

이러면 Unit에 있는 Setter에 값이 복사/붙여넣기 될 것입니다.

 

상수버퍼에 한해서는 매 프레임마다 실시간으로 렌더링 과정에 적용될 수 있도록 플래그를 세워뒀습니다. 근데 당연히 이 값은 어딘가에 존재해야하기 떄문에(실체가 있어야지 활용할 것), 할당된 상태여야 합니다.

 

하지만 리플렉션 과정에서는 값이 할당된 상태가 이니기 떄문에 주소 위치와 크기를 특정할 수 없어서 실제로 이 값을 활용하는 Unit에서 SetConstantBufferLink()를 실시해줍니다.

// 호출
const TransformData& Data = *(OwnerMeshComponent.lock()->GetTransform()->GetTransformData().get());
BufferSetter.SetConstantBufferLink("TransformData", Data);

// 상수 버퍼에 한하여 호출, cbuffer 슬롯 이름과 크기를 나중에 따로 지정해주기 위해 호출하는 함수
void Ext_DirectXBufferSetter::SetConstantBufferLink(std::string_view _Name, const void* _Data, UINT _Size)
{
	std::string UpperName = Base_String::ToUpper(_Name);
	std::multimap<std::string, ConstantBufferSetter>::iterator FindIter = ConstantBufferSetters.find(UpperName);

	if (ConstantBufferSetters.end() == FindIter)
	{
		MsgAssert("존재하지 않는 상수버퍼를 세팅하려고 했습니다." + UpperName);
		return;
	}

	std::multimap<std::string, ConstantBufferSetter>::iterator NameStartIter = ConstantBufferSetters.lower_bound(UpperName);
	std::multimap<std::string, ConstantBufferSetter>::iterator NameEndIter = ConstantBufferSetters.upper_bound(UpperName);

	for (; NameStartIter != NameEndIter; ++NameStartIter)
	{
		ConstantBufferSetter& BufferSetter = NameStartIter->second;

		if (BufferSetter.ConstantBuffer->GetBufferSize() != _Size)
		{
			MsgAssert("상수버퍼와 세팅하려는 데이터의 크기가 다릅니다. 상수버퍼 : " + std::to_string(BufferSetter.ConstantBuffer->GetBufferSize()) + "유저가 세팅한 데이터" + std::to_string(_Size) + UpperName);
			return;
		}

		BufferSetter.CPUData = _Data;
		BufferSetter.CPUDataSize = _Size;
	}
}

 

여기까지 완료했다면 정상적으로 Unit 마다의 렌더링 파이프라인 단계를 진행할 준비가 마무리 된 것입니다.

[목차]

 

- 셰이더에 대해 알아보려는 이유

- 셰이더란

- 상수 버퍼

- Vertex Shader 선언

- Pixel Shader 선언

- 셰이더 오토 컴파일-1

- 셰이더 오토 컴파일-2

 

 

[셰이더에 대해 알아보려는 이유]

 

https://umtimos.tistory.com/178

 

[DirectX11] InputAssembler 단계 준비와 도형 출력

[목차] - InputLayout(버텍스 정보) 정의- Vertex Buffer- Index Buffer- 버퍼 생성하기- 셰이더 컴파일- CreateInputLayout- 결과 출력- 예상치 못한 결과들 [InputLayout(버텍스 정보) 정의] 렌더링의 가장 기본 자원은

umtimos.tistory.com

 

해당 포스팅에서 삼각형, 사각형을 화면에 출력하기 위해 간단한 Vertex Shader, Pixel Shader를 작성하여 바로 컴파일 한 뒤 사용했습니다. 하지만 이후 진행될 단계들에서는 다양한 그래픽스 효과를 알아볼 것이고, 여기에는 조금 더 복잡한 기능을 갖거나 한 번에 많은 양의 셰이더를 사용하는 로직이 필요할 수 있습니다.

 

결국 이러면 셰이더의 양은 많아질 것이기 때문에, 이들을 저 포스팅을 진행했을 때와 같이 각각 컴파일하는 과정을 진행한다면 코드의 양이 늘어나고 복잡해질 것으로 예상되었습니다. 이에 따라 셰이더 컴파일의 구조를 변경할 겸, 셰이더와 상수버퍼를 알아본 뒤 넘어가고자 합니다.

 

 

[셰이더란]

 

셰이더(Shader)는 GPU에서 실행되는 작은 프로그램으로, DirectX에서는 HLSL(Hight Level Shader Language)을 사용하여 작성됩니다.

Vertex Shader 정점의 위치 변환
Pixel Shader 픽셀 단위 색상 계산
Geometry Shader 정점 간 도형 생성
Compute Shader 일반 계산용(병렬 연산)
Tessllation Shader 정점 세분화

 

다양한 종류가 있지만, 해당 프레임워크에서는 일단 Vertex Shader, Pixel Shader만 다루고 이후 시간적 여유가 생긴다면 Particle System을 만드는 과정을 진행함에 따라 다른 셰이더도 알아보도록 하겠습니다.

 

 

[상수 버퍼(Constant Buffer)]

 

셰이더를 알아보기 전에 먼저 Constant Buffer에 대해 알아보도록 하겠습니다. Constant Buffer도 Vertex Buffer, Index Buffer와 마찬가지로 버퍼입니다. 결국 셰이더(GPU)에서 활용할 수 있도록 렌더랑 파이프라인에 바인딩을 진행해줄 수 있어야 합니다. 일단 만드는 과정을 알아보겠습니다.

// CreateBuffer()를 통해 상수 버퍼를 생성
void Ext_DirectXConstantBuffer::CreateConstantBuffer(const D3D11_SHADER_BUFFER_DESC& _BufferDesc)
{
	ConstantBufferInfo.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	ConstantBufferInfo.ByteWidth = _BufferDesc.Size;
	ConstantBufferInfo.CPUAccessFlags = D3D11_CPU_ACCESS_FLAG::D3D11_CPU_ACCESS_WRITE;
	if (0 == ConstantBufferInfo.CPUAccessFlags)
	{
		ConstantBufferInfo.Usage = D3D11_USAGE_DEFAULT;
	}
	else 
	{
		ConstantBufferInfo.Usage = D3D11_USAGE_DYNAMIC;
	}

	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateBuffer(&ConstantBufferInfo, nullptr, ConstantBuffer.GetAddressOf()))
	{
		MsgAssert("상수 버퍼 생성에 실패했습니다.");
		return;
	}
}

 

위의 함수는 이후 셰이더 리플렉션 과정에서 호출될 함수인데, 어쨋든 Constant Buffer도 만들어지려면 CreateBuffer를 호출해줘야 합니다. 하지만 여기서 다른 버퍼와 중요한 차이점이 하나 있는데, 바로 CPUAccessFlags가 D3D11_CPU_ACCESS_WRITE라는 점입니다.

 

이 부분이 중요한 이유는 이게 있어야 cbuffer로 등록한 값들에 대해 CPU가 접근해서 수정할 것임을 명시할 수 있기 때문입니다. Vertex Buffer와 Index Buffer는 한 번 정해지면 변경되지 않는 고정된 값으로 볼 수 있습니다. 하지만 Constant Buffer는 만들어진 후 매프레임마다 계속해서 변경됩니다. CPU가 계속 수정하고 GPU가 이걸 넘겨받아서 사용하는 것입니다.

 

이 과정을 위해 Map(), memcpy(), Unmap()을 활용합니다.

// Map(), Unmap() 실시
void Ext_DirectXConstantBuffer::ChangeData(const void* _Data, UINT _Size)
{
	// 머티리얼들은 상수버퍼나 이런걸 통일해 놓은 것이다.
	if (nullptr == _Data)
	{
		std::string CurName = Name;
		MsgAssert(CurName + "에 nullptr인 데이터를 세팅하려고 했습니다.");
		return;
	}

	if (ConstantBufferInfo.ByteWidth != _Size)
	{
		std::string CurName = Name;
		MsgAssert(CurName + "크기가 다른 데이터가 들어왔습니다.");
		return;
	}

	D3D11_MAPPED_SUBRESOURCE SettingResources = { 0, };

	// 그래픽카드야 너한테 보낼께있어 잠깐 멈춰봐 
	// D3D11_MAP_WRITE_DISCARD 최대한 빠르게 처리하는 
	Ext_DirectXDevice::GetContext()->Map(ConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &SettingResources);

	if (SettingResources.pData == nullptr)
	{
		std::string CurName = Name;
		MsgAssert(CurName + " 그래픽카드에게 메모리 접근을 허가받지 못했습니다.");
		return;
	}
	memcpy_s(SettingResources.pData, ConstantBufferInfo.ByteWidth, _Data, ConstantBufferInfo.ByteWidth);
	Ext_DirectXDevice::GetContext()->Unmap(ConstantBuffer, 0);
}

 

+) 위의 방식이 아니면 UpdateSubresource()를 활용해야하는데, 이건 매프레임별 갱신이 불가능한 방식이라 실시간 렌더링을 진행해야하는 해당 프레임워크에서 적합한 방식이 아니라 따로 설명하지는 않겠습니다.

 

GPU는 기본적으로 병렬처리가 기본이기 때문에 언제 무슨일이 벌어질지 알 수 없습니다. 그렇다면 어떻게 싱글 스레드 연산(기본적으로)을 실시하는 CPU와 연동이 가능할까에 대한 해답이 Map() 함수입니다. 해당 함수를 호출하면 GPU에게 해당 값에 대해 변경이 이루어질 것이니 잠시 기다려달라고 말하는 것과 다름 없습니다. 이 함수 호출과 함께 memcpy로 전달할 값을 덮어쓴 뒤 Unmap() 함수로 작업이 끝났음을 알려줌으로써 CPU와 GPU를 동기화 시키는 것입니다. 이 과정을 마친 다음에야 상수 버퍼를 렌더링 파이프라인에 바인딩 시키는 것입니다.

 

 

[Vertex Shader 선언]

 

Vertex Shader를 다음과 같이 작성했습니다.

cbuffer TransformData : register(b0)
{
    float4 LocalPosition;
    float4 LocalRotation;
    float4 LocalScale;
    float4 LocalQuaternion;
    float4x4 LocalMatrix;

    float4 WorldPosition;
    float4 WorldRotation;
    float4 WorldQuaternion;
    float4 WorldScale;
    float4x4 WorldMatrix;

    float4x4 ViewMatrix;
    float4x4 ProjectionMatrix;
    float4x4 WorldViewMatrix;
    float4x4 WorldViewProjectionMatrix;
}

struct VSInput
{
    float4 Position : POSITION;
    float4 Color : COLOR;
    float4 TexCoord : TEXCOORD;
    float4 Normal : NORMAL;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR;
    float4 TexCoord : TEXCOORD;
    float4 Normal : NORMAL;
};

VSOutput Basic_VS(VSInput _Input)
{
    VSOutput Output;
    _Input.Position.w = 1.0f;
    Output.Position = mul(_Input.Position, WorldViewProjectionMatrix);
    Output.Color = _Input.Color;
    Output.TexCoord = _Input.TexCoord;
    Output.Normal = _Input.Normal;
   
    return Output;
}

 

확인해보면 구조체로 보이는 것이 3개, 함수처럼 보이는 것이 한 개 있습니다. 하나씩 알아보도록 하겠습니다.

 

1. cbuffer

: 이게 위에서 말씀드린 Constant Buffer입니다. 여기서는 TransformData를 넘겨받는 것으로 되어있는데, 해당 프레임워크에서 이 값의 위치는 각 Component(정확히는 렌더링 컴포넌트)의 Trasnform 내부입니다. 렌더링 파이프라인 과정 진행 전 TransformData를 Updata하는 과정(이전에 말씀드렸던 것과 같이 카메라의 뷰, 프로젝션 행렬을 연산하는 과정)을 거치기 때문에 값이 변경될 것이고, Map, Unmap으로 변경된 값을 적용시킨 뒤 바인딩되어 여기로 전달됩니다.

 

그리고 일반적인 struct와는 다르게 register(b0)이라는 것이 달려있는데, 이것은 바인딩 슬롯입니다. 셰이더 프로그램에는 각자의 바인딩 슬롯이 있습니다. 종류는 크게 네 가지입니다.

 

[1] b 슬롯

: 상수버퍼, 슬롯 갯수는 14개(b0 ~ b13), 크기 제한 4096byte

 

[2] t 슬롯

: 텍스쳐(정확히는 셰이더 리소스 뷰(SRV) 바인딩 슬롯이긴 함), 슬롯 갯수는 128개(t0 ~ t127), 크기 제한 없음

 

[3] s 슬롯

: 샘플러, 슬롯 갯수는 16개(s0 ~ s15), 데이터 형태가 아니라 메모리 크기 개념 없음

 

[4] u 슬롯

: Unordered Access View(UAV) 바인딩 슬롯인데, 이건 Compute Shader 전용, 슬롯 갯수는 64개(u0 ~ u63), 크기 제한 없음

 

각 상수 버퍼들은 모두 각자의 바인딩 슬롯을 가지고 있어야 합니다. 예를 들어, 위에서 TransformData를 b0 슬롯에 바인딩한 상태인데, 다른 상수버퍼인 LightData가 바인딩되어야 한다고 가정해보겠습니다. 이러면 register(b0)에는 이미 TransformData가 바인딩되어 있기 때문에 register(b1)에 바인딩해줘야 합니다. 이런 식으로 상수 버퍼가 추가될 때마다 각자 고유한 슬롯에 바인딩해줄 수 있도록 합니다.

 

그렇다면 TransformData(b0), LightData(b1)이 바인딩 된 상태에서 텍스쳐(t) 상수 버퍼가 새로 바인딩 되어야한다면 어떻게 해야할까요? 이럴 경우에는 그냥 t0에 바인딩하면 됩니다. b랑 t는 서로 다른 슬롯이기 때문에 뒤에 숫자는 상관이 없습니다. 물론 또 다른 텍스쳐를 추가로 바인딩할 경우에는 t1에 바인딩하면 됩니다. 샘플러도 마찬가지입니다.

 

cbuffer에 대해서는 중요한 부분이 하나 더 있습니다. 바로 외부에서 전달하는 셰이더 내부에서 사용할 때의 크기가 완전히 동일해야 한다는 것입니다. 예를 들어, 외부에서 40byte를 전달해줬다면 내부에서도 40byte를 그대로 받아서 사용해야합니다. 물론 GPU는 크기만 맞다면 상관없이 그냥 써버리기 때문에 순서도 중요합니다.

 

이 부분에서 종종 실수할 수 있는 점이 하나 있는데, cbuffer는 내부가 16byte 단위로 정렬된다는 점입니다. 예를 들어 아래와 같이 데이터가 들어온다고 가정해보겠습니다.

cbuffer TransformData : register(b0)
{
    float4x4 WorldMatrix;  // 64 bytes
    float3 LightDir;       // 12 bytes
    float  Padding;        // 4 bytes → align 16 bytes
};

 

이러면 총 크기는 64 + 12 + 4 = 80이 됩니다. 만약 외부에서 이를 인지하지 않고 그냥 보내버리면 분명 동일한 형태로 보냈는데 값이 안맞을 수도 있습니다. 이런 실수를 방지하기 위해 외부에서도 그냥 전달할 구조체에 대해서는 alignas(16)을 실시해서 정렬을 해줘야 합니다.

 

2. VSInput

: 이 값은 CPU에서 정의한 뒤 바인딩해준 Vertex Buffer의 시멘틱들입니다. 여기서는 상수 버퍼와 조금 다른점이, 바깥에서 전달한 데이터와 여기서 받는 데이터의 크기가 굳이 같지 않아도 된다는 점입니다. 예를 들어 외부에서 POSITION, COLOR, TEXCOORD, NORMAL을 전달했다고 가정해봅시다. 이러면 cbuffer는 모두 동일하게 선언해줘야겠지만 Vertex Buffer는 그냥 순서만 맞으면 됩니다. 위에서는 그냥 동일하게 모두 동일하게 선언했지만, 난 그냥 두개만 쓸건데? 하면 아래와 같이 순서만 맞춰주면 된다는 것입니다.

struct VSInput
{
    float4 Position : POSITION;
    float4 TexCoord : TEXCOORD;
};

 

이렇게 정의해주면 셰이더의 엔트리포인트에 입력되어 활용됩니다.

 

3. VSOutput

: 이 부분은 연산 결과에 대해 어떤 값을 내보낼지 정의하는 구조체입니다. 이렇게 내보낸 값은 Rasterizer에서 활용하게 됩니다. 여기서 한 가지 중요한 점은 SV 태그가 붙은 시멘틱이 하나 있어야한다는 점입니다. 여기서는 POSITION에 SV를 붙였는데, 이건 시스템 시멘틱이라는 의미로 이 정점이 최종적으로 화면의 어디에 그려질 것인가를 GPU에게 알려주는 특별한 위치 정보라는 의미를 담고 있습니다.

 

Vertex Shader의 SV_POSITION 출력은 Rasterizer가 픽셀 위치 계산에 활용하게 되고, Pixel Shader의 SV_POSITION에 전달되어 현재 픽셀이 그려질 정확한 화면 좌표가 어딘지를 알아낼 수 있게 됩니다. 이외의 나머지 값은 단순한 속성(attribute)으로, 사용자가 넣고싶으면 넣고, 빼고싶으면 뺄 수 있는 값들 입니다.

 

 

[Pixel Shader 선언]

 

다음은 Pixel Shader입니다.

struct PSInput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR;
};

float4 Basic_PS(PSInput _Input) : SV_TARGET
{
    return _Input.Color;
}

 

위에서 VSOutput으로 전달한 값 중 SV 태그가 붙은 시멘틱은 필수적으로 받고, 나머지 받고싶은 속성을 적어주면 됩니다. 물론 순서는 지켜줘야합니다(안지키면 오류남). 여기서는 단순하게 SV_POSITION과 COLOR를 받고 출력하고 있습니다.

 

픽셀 셰이더는 일단 여기까지만 진행하고, 나중에 SRV(t슬롯), MRT(멀티렌더타겟)을 활용할 때 자세히 알아보도록 하겠습니다.

 

 

[셰이더 오토 컴파일-1]

 

셰이더를 작성했으니, 사용하려면 당연히 컴파일을 진행해야합니다. 하지만 앞으로 다양한 셰이더들이 작성될 것이기 때문에 매 셰이더마다 따로 컴파일하고 값을 저장한 뒤 활용하기는 어려워 셰이더 오토 컴파일 기능을 추가했습니다.

 

일단 오토 컴파일을 위해 이전에 만들어준 Directory 기능을 활용해줍니다. 이걸 활용하면 경로를 찾아가서 존재하는 hlsl 파일들을 모두 확인하여 절대 경로를 만들어줄 것입니다.

 

또한 해당 프로젝트에서는 컴파일을 위해 한 가지 규칙이 필요한데, 바로 셰이더 파일 이름과 엔트리 포인트 이름이 동일해야한다는 것입니다. 예를 들어, Basic_VS라는 이름의 Vertex Shader를 만들었다고 가정해봅시다. 이러면 Vertex Shader 내부의 엔트리 포인트 이름도 Basic_VS로 해줘야 정상적으로 컴파일이 진행됩니다.

// 셰이더 정보 생성(Shader 오토컴파일, ConstantBuffer)
void Ext_DirectXResourceLoader::ShaderCompile()
{
	// 셰이더 생성 규칙 : [1], [2]를 모두 만족해야 정상 컴파일 진행
	// [1] 이름 + "_" + "Type" == Basic_PS
	// [2] 내부 main(EntryPoint) 이름도 동일하게 설정
	Base_Directory Dir;
	Dir.MakePath("../Shader");
	std::vector<std::string> Paths = Dir.GetAllFile({ "hlsl" });
	for (const std::string& ShaderPath : Paths)
	{
		std::string EntryPoint = Dir.FindEntryPoint(ShaderPath);
		Ext_DirectXShader::ShaderAutoCompile(ShaderPath, EntryPoint.c_str());
	}
}

 

위의 기능을 호출하면 아래의 연관 함수들이 호출됩니다.

/////////////////////////////////////// Ext_DirectXVertexShader
Ext_DirectXVertexShader::Ext_DirectXVertexShader()
{
	Type = ShaderType::Vertex;
}

void Ext_DirectXVertexShader::CreateVertexShader(std::string_view _Path, std::string_view _EntryPoint, UINT _VersionHigh, UINT _VersionLow)
{
	CreateVersion("vs", _VersionHigh, _VersionLow);
	EntryPoint = _EntryPoint;

	unsigned int Flag = 0;

#ifdef _DEBUG
	Flag = D3D10_SHADER_DEBUG;

#endif
	Flag |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;

	COMPTR<ID3DBlob> Error;
	std::wstring UniCodePath = Base_String::AnsiToUniCode(_Path);
	if (S_OK != D3DCompileFromFile(UniCodePath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, EntryPoint.c_str(), Version.c_str(), Flag, 0, BinaryCode.GetAddressOf(), Error.GetAddressOf()))
	{
		// 에러를 텍스트로 출력
		std::string ErrorString = reinterpret_cast<char*>(Error->GetBufferPointer());
		MsgAssert(ErrorString);
		return;
	}

	// 컴파일된 바이트코드로 GPU용 Vertex Shader 생성
	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateVertexShader(BinaryCode->GetBufferPointer(), BinaryCode->GetBufferSize(), nullptr, VertexShader.GetAddressOf()))
	{
		MsgAssert("버텍스 쉐이더 핸들 생성에 실패");
		return;
	}

	// 상수버퍼 세팅, 리소스 세팅
	ShaderResourceSetting();
}

/////////////////////////////////////// Ext_DirectXPixelShader
Ext_DirectXPixelShader::Ext_DirectXPixelShader()
{
	Type = ShaderType::Pixel;
}

void Ext_DirectXPixelShader::CreatePixelShader(std::string_view _Path, std::string_view _EntryPoint, UINT _VersionHigh, UINT _VersionLow)
{
	CreateVersion("ps", _VersionHigh, _VersionLow);
	EntryPoint = _EntryPoint;

	unsigned int Flag = 0;

#ifdef _DEBUG
	Flag = D3D10_SHADER_DEBUG;

#endif
	Flag |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;

	COMPTR<ID3DBlob> Error;
	std::wstring UniCodePath = Base_String::AnsiToUniCode(_Path);
	if (S_OK != D3DCompileFromFile(UniCodePath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, EntryPoint.c_str(), Version.c_str(), Flag, 0, BinaryCode.GetAddressOf(), Error.GetAddressOf()))
	{
		// 에러를 텍스트로 출력
		std::string ErrorString = reinterpret_cast<char*>(Error->GetBufferPointer());
		MsgAssert(ErrorString);
		return;
	}

	// 컴파일된 바이트코드로 GPU용 Pixel Shader 생성
	if (S_OK != Ext_DirectXDevice::GetDevice()->CreatePixelShader(BinaryCode->GetBufferPointer(), BinaryCode->GetBufferSize(), nullptr, PixelShader.GetAddressOf()))
	{
		MsgAssert("픽셀 쉐이더 핸들 생성에 실패");
		return;
	}

	// 상수버퍼 세팅, 리소스 세팅
	ShaderResourceSetting();
}

/////////////////////////////////////// Ext_DirectXShader
void Ext_DirectXShader::CreateVersion(std::string_view _ShaderType, UINT _VersionHigt /*= 5*/, UINT _VersionLow /*= 0*/)
{
	// vs_5_0
	Version += _ShaderType;
	Version += "_";
	Version += std::to_string(_VersionHigt);
	Version += "_";
	Version += std::to_string(_VersionLow);
}

// EntryPoint 기준으로, 셰이터 종류를 탐색
ShaderType Ext_DirectXShader::FindShaderType(std::string_view _EntryPoint)
{
	std::string Lower = _EntryPoint.data();

	if (Lower.find("_VS") != std::string::npos) return ShaderType::Vertex;
	if (Lower.find("_PS") != std::string::npos) return ShaderType::Pixel;
	if (Lower.find("_CS") != std::string::npos) return ShaderType::Compute;
	if (Lower.find("_GS") != std::string::npos) return ShaderType::Geometry;
	// if (Lower.find("_HS") != std::string::npos) return ShaderType::Unknown; // 또는 Hull
	// if (Lower.find("_DS") != std::string::npos) return ShaderType::Unknown; // 또는 Domain

	return ShaderType::Unknown;
}

// 셰이더 종류 기준으로 컴파일 진행
void Ext_DirectXShader::ShaderAutoCompile(std::string_view _Path, std::string_view _EntryPoint)
{
	ShaderType Type = FindShaderType(_EntryPoint);

	switch (Type)
	{
	case ShaderType::Vertex: Ext_DirectXVertexShader::LoadVertexShader(_Path, _EntryPoint);	break;
	case ShaderType::Pixel: Ext_DirectXPixelShader::LoadPixelShader(_Path, _EntryPoint); break;
	case ShaderType::Compute: Ext_DirectXComputeShader::LoadComputeShader(_Path, _EntryPoint); break;
	case ShaderType::Geometry: MsgAssert("Geometry 셰이더 타입은 아직 안만듬"); break;
	case ShaderType::Unknown: MsgAssert("EntryPoint 탐색 실패"); break;
	}
}

 

먼저 ShaderAutoCompile() 함수로 진입해서 해당 셰이더가 어떤 종류인지 파악하게 됩니다. Vertex Shader라면 LoadVertexShader(), Pixel Shader라면 LoadPixelShader()를 실행할 것입니다.

 

이렇게 이동하면 각자의 셰이더 종류에 따라 D3DCompileFromFile()을 실시한 뒤 Create_____Shader() 함수를 호출할 것입니다.

D3DCompileFromFile(
    UniCodePath.c_str(),                // [1] 셰이더 파일 경로
    nullptr,                            // [2] #include 시 기본 처리
    D3D_COMPILE_STANDARD_FILE_INCLUDE,  // [3] 표준 인클루드 사용
    EntryPoint.c_str(),                 // [4] 셰이더 진입 함수명
    Version.c_str(),                    // [5] 셰이더 모델 버전 ("vs_5_0" 등)
    Flag, 0,                            // [6,7] 컴파일 플래그
    BinaryCode.GetAddressOf(),         // [8] 성공 시 컴파일 결과 (바이트 코드)
    Error.GetAddressOf()               // [9] 실패 시 에러 메시지 반환
)

 

Ext_DirectXDevice::GetDevice()->Create____Shader(
    BinaryCode->GetBufferPointer(), // [1] 컴파일된 바이너리코드
    BinaryCode->GetBufferSize(),    // [2] 컴파일된 바이너리코드 사이즈
    nullptr,                        // [3] 클래스 포인터(사용안함)
    VertexShader.GetAddressOf()     // [4] 핸들값을 저장할 포인터
)

 

 

[셰이더 오토 컴파일-2]

 

기존과 다른 것은 바로 리플렉션을 활용하여 상수 버퍼를 세팅하는 과정이 추가됐다는 것입니다. 해당 과정은 Create____Shader() 이후 ShaderResourceSetting()을 통해 자동으로 진행됩니다.

 

ID3D11ShaderReflection* CompileInfo = nullptr;

if (S_OK != D3DReflect(
BinaryCode->GetBufferPointer(), // [1] 컴파일된 HLSL 셰이더의 시작 주소
BinaryCode->GetBufferSize(),    // [2] 바이트코드의 크기
IID_ID3D11ShaderReflection,     // [3] 리플렉션 인터페이스 ID, ID는 __uuidof(ID3D11ShaderReflection)를 넣어도 되지만, IID_ID3D11ShaderReflection 플래그를 넣어도 동일하게 동작
reinterpret_cast<void**>(&CompileInfo))) // [4] 값을 전달받을 포인터
{
	MsgAssert("쉐이더 리플렉션에 실패했습니다.");
	return;
}

 

위의 함수 실행을 성공했다면 HLSL 셰이더 바인딩 슬롯에 정의된 리소스(지금은 상수버퍼만 있으니 cbuffer)의 이름, 크기, 바인딩 포인트 등을 런타임에 파악해줄 수 있습니다. 

void Ext_DirectXShader::ShaderResourceSetting()
{
	if (nullptr == BinaryCode)
	{
		MsgAssert("쉐이더가 컴파일 되지 않아서 쉐이더의 리소스를 조사할 수 없습니다.");
		return;
	}

	// Reflection 
	// RTTI의 비슷한 개념으로 
	ID3D11ShaderReflection* CompileInfo = nullptr;

	if (S_OK != D3DReflect(BinaryCode->GetBufferPointer(), BinaryCode->GetBufferSize(), IID_ID3D11ShaderReflection, reinterpret_cast<void**>(&CompileInfo)))
	{
		MsgAssert("쉐이더 리플렉션에 실패했습니다.");
		return;
	}

	D3D11_SHADER_DESC Info;
	CompileInfo->GetDesc(&Info);
	D3D11_SHADER_INPUT_BIND_DESC ResDesc;

	// 내가 사용한 상수버퍼 텍스처 샘플러등의 총합입니다.
	for (UINT i = 0; i < Info.BoundResources; i++)
	{
		// 리소스 정보를 얻어오게 되고
		CompileInfo->GetResourceBindingDesc(i, &ResDesc);
		std::string Name = ResDesc.Name;
		D3D_SHADER_INPUT_TYPE Type = ResDesc.Type;
		std::string UpperName = Base_String::ToUpper(ResDesc.Name);

		switch (Type)
		{
		case D3D_SIT_CBUFFER:
		{
			ID3D11ShaderReflectionConstantBuffer* CBufferPtr = CompileInfo->GetConstantBufferByName(ResDesc.Name);

			D3D11_SHADER_BUFFER_DESC BufferDesc;
			CBufferPtr->GetDesc(&BufferDesc);

			std::shared_ptr<Ext_DirectXConstantBuffer> ConstantBuffer = Ext_DirectXConstantBuffer::CreateConstantBuffer(UpperName, BufferDesc, BufferDesc.Size);

			ConstantBufferSetter Set;
			Set.OwnerShader = GetSharedFromThis<Ext_DirectXShader>();
			Set.Name = UpperName;
			Set.BindPoint = ResDesc.BindPoint;
			Set.ConstantBuffer = ConstantBuffer;

			BufferSetter.InsertConstantBufferSetter(Set);

			break;
		}
		// ...
	}
}

 

Info.BoundResources를 통해 셰이더 내부의 리소스 갯수 만큼 반복문을 돕니다. ResDesc.Name으로는 이름을 알아낼 수 있고, ResDesc.Type는 상수 버퍼 종류가 무엇인지를 알아냅니다.

 

이러면 Type에 따라 switch문으로 들어가서 종류에 맞게 세팅을 진행합니다. 초기 값은 대부분 Null값을 넣어줍니다. 현재 상태에서는 Vertex Shader에 TransformData를 바인딩 해뒀기 때문에 D3D_SIT_CBUFFER로 들어갈 것입니다.

 

내부에서는 상수 버퍼를 생성(CreateBuffer())하고, ConstantBufferSetter라는 자료형에 값을 넣어주고 있습니다. ConstantBufferSetter는 그냥 유동적으로 상수 버퍼를 세팅하기 위해 해당 프레임워크에서 따로 만들어둔 클래스입니다. 내부는 아래와 같이 되어 있습니다.

#pragma once

// 상수버퍼 저장용 인터페이스
struct ConstantBufferSetter
{
	std::string Name;
	std::weak_ptr<class Ext_DirectXShader> OwnerShader;
	int BindPoint = -1; // b0 t0 같은 몇번째 슬롯에 세팅되어야 하는지에 대한 정보.
	std::shared_ptr<class Ext_DirectXConstantBuffer> ConstantBuffer;
	const void* CPUData;
	UINT CPUDataSize;

	void Setting();
};

// 다양한 종류의 상수 버퍼를 저장하고 가져오기 위해 사용하는 클래스
class Ext_DirectXBufferSetter
{
	friend class Ext_MeshComponentUnit;

public:
	// constrcuter destructer
	Ext_DirectXBufferSetter();
	~Ext_DirectXBufferSetter();

	// delete Function
	Ext_DirectXBufferSetter(const Ext_DirectXBufferSetter& _Other) = delete;
	Ext_DirectXBufferSetter(Ext_DirectXBufferSetter&& _Other) noexcept = delete;
	Ext_DirectXBufferSetter& operator=(const Ext_DirectXBufferSetter& _Other) = delete;
	Ext_DirectXBufferSetter& operator=(Ext_DirectXBufferSetter&& _Other) noexcept = delete;

	// 생성된 상수버퍼 저장하기
	void InsertConstantBufferSetter(const ConstantBufferSetter& _Setter) { ConstantBufferSetters.insert(std::make_pair(_Setter.Name, _Setter)); } 

	// 상수버퍼 데이터 저장
	template<typename Type>
	void SetConstantBufferLink(std::string_view _Name, const Type& _Data)
	{
		SetConstantBufferLink(_Name, reinterpret_cast<const void*>(&_Data), sizeof(Type));
	}

protected:
	
private:
	std::multimap<std::string, ConstantBufferSetter> ConstantBufferSetters; // 상수 버퍼 저장용 컨테이너
	
	void SetConstantBufferLink(std::string_view _Name, const void* _Data, UINT _Size); // 상수버퍼 데이터 저장
	void Copy(const Ext_DirectXBufferSetter& _OtherBufferSetter); // 버퍼 세팅 복사/붙여넣기 실시
	void BufferSetting(); // 버퍼 세팅 호출
};

 

나중에 다양한 렌더링 파이프라인을 활용할 때, 해당 클래스를 통해 쉽게쉽게 바인딩해서 사용할 수 있도록 해뒀습니다. 이 부분은 나중에 실제로 사용할 때 따로 설명드리도록 하겠습니다.

[목차]

 

- 중요한 요

- 현재의 문제

- 카메라 생성

- 뷰 행렬

- 프로젝션 행렬

- 뷰포트 행렬

- 결과 적용

 

 

[중요한 요약]

 

 

 

[현재의 문제]

 

이전 단계에서 정사각형을 출력하고 싶었지만, 직사각형이 출력되는 문제가 있었습니다.

 

문제의 상황에 대해 다시 설명드리자면, 화면의 종횡비와 NDC(Normalized Device Coordinates) 좌표 공간 차이로 인해 발생하는 것입니다. Direct3D에서 정점 셰이더까지의 최종 좌표는 NDC로 변환되고, 좌표계는 각각 다음의 범위를 가집니다.

범위
X -1.0 ~ 1.0
Y -1.0 ~ 1.0
Z 0.0 ~ 1.0(혹은 -1.0 ~ 1.0)

 

하지만 윈도우창은 1280x720로 생성되어 16:9의 종횡비를 가져, 가로가 세로보다 1.78배 더 긴 상태입니다. 때문에 실제 픽셀 매핑 시(ViewPort 변환 시) NDC의 X축 범위(-1.0 ~ 1.0)은 1280픽셀, Y축 범위(-1.0 ~ 1.0)는 720픽셀에 분배됩니다. 이로 인해 정사각형이 실제 화면에서는 가로로 늘어난 직사각형처럼 보이게 됩니다.

 

 

[카메라 생성]

 

해당 문제를 해결하기 위해서는 뷰, 프로젝션 행렬을 만들어서 사용해야합니다. 뷰포트 행렬은 기존에 RenderTarget에 정의한 것을 그대로 쓸 것입니다.

뷰(View) - "시점"의 기준을 정의하는 행렬
- 뷰 스페이스 생성
프로젝션(Projection) - 3D 물체를 2D 화면으로 투영하여 왜곡을 보정해주는 행렬
- Clip 스페이스 생성
뷰포트(Viewport) - NDC 좌표를 실제 화면 픽셀 좌표로 변환하기 위한 행렬
- 이건 이전에 RenderTarget 만들 때 이미 만들었음

 

뷰, 프로젝션 행렬을 생성하기 위해서는 기준이 하나 필요합니다. 이를 위해 카메라라는 것을 생성했습니다. 해당 프레임워크에서 카메라는 Ext_Actor 클래스를 상속받으며, 내부에 Ext_Transform을 가지고 있습니다. 최초 생성 시에 따로 설정하지 않으면 위치 { 0.0f, 0.0f, 0.0f }, 회전 { 0.0f, 0.0f, 0.0f }, 크기 { 1.0f, 1.0f, 1.0f }의 데이터 값을 갖습니다. 이제 이 좌표를 기준으로 RenderTarget에 출력 결과물들이 그려질 것입니다.

 

 

[뷰 행렬(View Matrix)]

 

뷰 행렬은 월드 공간에 존재하는 모든 객체들을 카메라 기준으로 재배치해주는 행렬입니다. 3D상의 물체는 위치속성 뿐만 아니라 회전 속성도 보유하고 있습니다.

 

카메라가 (0, 0, 0)에 있고, 캐릭터가(0, 0, 100)에 위치해있으며, 카메라 시점이 30도 회전한 상태라고 가정해보겠습니다.

 

뷰 행렬은 여기서 카메라가 30도 회전한 것으로 만드는 것이 아니라, 카메라는 { 0, 0, 0 }에 위치시키고 회전값도 { 0, 0, 0 }으로 만들면서 나머지 물체들을 그만큼 이동시키는 것입니다.

 

 

행렬의 생성은 DirectX의 XMMatrixLookToLH() 함수를 활용합니다.

DirectX::XMMatrixLookToLH(_EyePos, _EyeDir, _EyeUp);

 

이 함수는 카메라의 위치와 시선 방향, 위쪽 방향을 입력 받아 뷰 행렬을 생성하는 함수입니다.

FXMVECTOR EyePosition 카메라의 위치
FXMVECTOR FocusPosition 바라보는 위치
FXMVECTOR UpDirection 카메라의 위 방향(보통 Y축으로 설정)

 

함수 내부에서는 다음의 동작이 수행되면서 행렬이 생성됩니다.

float4 EyeDir = _EyeDir.NormalizeReturn();        // 카메라 정면 (+Z 방향)
float4 EyeUp = _EyeUp.NormalizeReturn();          // 월드 업 벡터
float4 EyeRight = float4::Cross3DReturn(EyeUp, EyeDir).NormalizeReturn(); // 오른쪽

ArrVector[0] = EyeRight;  // X축
ArrVector[1] = EyeUp;     // Y축
ArrVector[2] = EyeDir;    // Z+축 (정면)

// 카메라 위치를 원점으로 이동시키기 위한 보정값 생성
float4 NegEyePos = -_EyePos;
float D0Value = float4::DotProduct3D(EyeRight, NegEyePos);
float D1Value = float4::DotProduct3D(EyeUp,    NegEyePos);
float D2Value = float4::DotProduct3D(EyeDir,   NegEyePos);

// 행렬 전치 (Row → Column)
Transpose();

// 마지막 행 = 위치
ArrVector[3] = { D0Value, D1Value, D2Value, 1.0f };

//////////////////////////////////////////////////////////////////////
////// Transpose 전 : 열(row) 기준
| R.x  R.y  R.z  0 |
| U.x  U.y  U.z  0 |
| F.x  F.y  F.z  0 |
| Tx   Ty   Tz   1 |

////// Transpose 후 : 행(Column) 기준
| R.x  U.x  F.x  Tx |
| R.y  U.y  F.y  Ty |
| R.z  U.z  F.z  Tz |
|  0    0    0    1 |

 

여기서 보정값 생성에 내적을 쓰는 이유는 해당 축 방향으로 얼마만큼 이동시켜야하는 것을 계산하는 것으로, 카메라의 위치를 원점에 맞추는데 필요한 축 방향의 투영 거리를 의미합니다. 

 

해당 프로젝트에서는 각 인자로 다음의 값을 전달했습니다.

//////////////////// Ext_Camera.cpp의 CameraTransformUpdate() 함수 내부
float4 EyeDir = GetTransform()->GetLocalForwardVector();
float4 EyeUp = GetTransform()->GetLocalUpVector();
float4 EyePos = GetTransform()->GetWorldPosition();

ViewMatrix.LookToLH(EyePos, EyeDir, EyeUp);

//////////////////// Ext_Transform.h
float4 GetLocalForwardVector() { return TFData->WorldMatrix.ArrVector[2].NormalizeReturn(); }
float4 GetLocalUpVector() { return TFData->WorldMatrix.ArrVector[1].NormalizeReturn(); }
float4 GetLocalRightVector() {	return TFData->WorldMatrix.ArrVector[0].NormalizeReturn(); }

//////////////////// float4.h(Base_Math)
void Normalize()
{
	DirectVector = DirectX::XMVector3Normalize(*this);
}

// 벡터(float4)를 단위벡터로 변경후 리턴
float4 NormalizeReturn()
{
	float4 Result = *this;
	Result.Normalize();
	return Result;
}

 

EyePos는 카메라 현재 위치, EyeDir는 +Z축(전방), EyeUp은 +Y축(위)으로 전달했습니다. 이러면 카메라가 바라보는 시점으로 좌표계가 정의되면서 3D 카메라 공간(View Space)이 구축됩니다.

 

아래는 생성된 View Space에 대한 좌표값이 어떤 것을 의미하는지 나타내는 표입니다.

+X 오른쪽
-X 왼쪽
+Y
-Y 아래
+Z
-Z

 

 

[프로젝션 행렬(Projection Matrix)]

 

뷰 행렬을 활용하면 Object들이 View Space에 정렬된 상태가 됩니다. 하지만 결국 모니터에 띄워야하는 화면은 2D 공간이기 때문에 3D 좌표값으로는 출력할 수가 없습니다. 따라서 View Space 좌표값을 2D 좌표값으로 투영시키기 위한 행렬이 필요합니다. 여기서 쓰이는 행렬이 바로 프로젝션 행렬입니다. 프로젝션 행렬이 적용되면 Object들은 Clip Space에 압축된 좌표값을 갖게 됩니다.

 

프로젝션의 방식은 직교(Othology)와 원근(Perspective)가 있는데, 지금은 원근 투영만 알아보도록 하겠습니다. 원근 투영 행렬의 생성은 DirectX의 XMMatrixPerspectiveFovLH() 함수를 활용합니다.

DirectX::XMMatrixPerspectiveFovLH(_FovAngle, _AspectRatio, _NearZ, _FarZ);
FovAngle - 수직 시야각(Field of View; FOV)으로, 단위는 라디안(Radian)을 넣어줘야함
- 현재 프로젝트에서는 수직이 Y축
- 3D 렌더링 시스템은 AspectRatio(해상도)가 가변적이기 때문에 수직 시야각을 사용하고 가로 시야각은 종횡비로 보정하는게 일관성 유지에 유리하기 때문
float Aspect - 종횡비(가로/세로)
- ScreenWidth / ScreenHeight
float NearZ - 가까운 클리핑 거리(Near Plane), 근평면이라고 함
- 이 거리보다 가까우면 렌더링에서 제외
float FarZ - 먼 클리핑 거리(Far Plane), 원평면이라고 함
- 이 거리보다 멀면 렌더링에서 제외

 

함수 내부에서는 다음의 동작이 수행되면서 행렬이 만들어집니다.

float FovRad = _FovAngle * GameEngineMath::DegToRad; // 라디안으로 값 변환
float YScale = 1.0f / tanf(FOV / 2.0f); // y축 시야각이 클수록 값이 작아짐 → 멀리 보임
float XScale = YScale / _AspectRatio; // 가로 시야는 종횡비에 따라 정해짐
float ZRange = _FarZ - _NearZ; // Z 거리 범위 (Far - Near)
float Zn = _NearZ;
float Zf = _FarZ;

Arr2D[0][0] = XScale; // [1]
Arr2D[1][1] = YScale; // [2]
Arr2D[2][2] = Zf / ZRange; // [3]
Arr2D[2][3] = 1.0f; // [4]
Arr2D[3][2] = -Zn * Zf / ZRange; // [5]
Arr2D[3][3] = 0.0f; // [6]
// [1] X축 스케일링 → NDC 공간에서 좌우 시야 조절
// [2] Y축 스케일링 → NDC 공간에서 상하 시야 조절
// [3] Z값 정규화 (0~1로 변환하기 위한 깊이 매핑), FarZ에 가까울수록 값이 1에 가까워짐
// [4] 원근 분할을 위해 Z 값을 W로 복사 → 투영 후 divide 수행 가능하게 설정
// [5] 정규화된 Z값 계산을 위한 보정 항, -Zn * Zf / (Zf - Zn) 형태는 원근 보정된 깊이 값 계산에 사용됨
// [6] 투영 행렬 마지막 요소: 0, W 성분을 Z로부터 받아야 하므로 이 위치는 0으로 둬야 함

//////////////////////////////////////////////////////////////////////
////// 생성된 열(row) 기준 원근 투영 행렬
| XScale    0         0             0 |
|   0     YScale      0             0 |
|   0       0      Zf/(Zf-Zn)       1 |
|   0       0   -Zn*Zf/(Zf-Zn)      0 |

 

XScale, YScale, Zf/(Zf-Zn)으로 인해 Object의 X, Y값은 -1.0 ~ 1.0, Z값은 0.0 ~ 1.0으로 압축됩니다. 원근 투영은 헷갈리는 부분이 많기 때문에, 하나씩 차근차근 알아보도록 하겠습니다.

 

1. YScale

: 공식은 [float YScale = 1.0f / tanf(FOV / 2.0f)]입니다. 그림을 통해 알아보겠습니다.

 

같은 크기의 Object(파란색 사각형)일지라도 멀리 있을 수록 더 작게 보여야 합니다. 이때 Z값이 증가하면 그림과 같이 X, Y값도 증가하게 됩니다(뒤에 있을수록 약간씩 왼쪽으로 이동함). 이때 사각형의 위치는 탄젠트(tan)로 구할 수 있습니다.

// 탄젠트는 밑변에 대한 높이의 비
tan(θ) = 반대변 / 인접변
tan(θ) = (Y의 길이) / (Z의 길이)
tan(FOV / 2) = y / z

따라서, y = z * tan(FOV / 2)

 

+) FOV / 2인 이유는 그림에서 보시는 것과 같이 전체 FOV값에서 절반만 활용해서 값을 구하기 때문입니다.

 

이제 이 값을 원근 투영 행렬에 변환하기 위해 사용하는 것이 YScale = 1 / tan(FOV / 2) 입니다. y값을 압축해서 -1 ~ 1 범위로 맞추는 스케일 계수인 것입니다.

 

2. XScale

: X축 시야각은 Y축과 동일하게 구할 수 있지만, 추가로 종횡비를 나눠줍니다. 3D 렌더링 시스템은 AspectRatio(해상도)가 가변적이기 때문에 Y시야각(세로 시야각)을 고정으로 구하고 X시야각(가로 시야각)은 종횡비로 보정하기 때문입니다.

XScale = 1 / (tan(FovY / 2) * AspectRatio)
       = (1 / tan(FovY / 2)) / AspectRatio
       = YScale / AspectRatio

 

3. Zf/(Zf - Zn) & -Zn * Zf / (Zf - Zn) & Arr2D[2][3] = 1.0f

: Z값은 0.0 ~ 1.0 사이 값으로 압축해야 합니다. 하지만 이걸 직선적으로 매핑해버리면 원근 효과가 사라지게 됩니다. 그래서 원근 투영 행렬에서는 아래와 같이 비선형적으로 변환해줍니다.

z_clip = A * z + B       ← 투영 행렬의 z 변환 결과
w_clip = z               ← 투영 행렬의 w 성분
z_ndc  = z_clip / w_clip = (A * z + B) / z

따라서 z_ndc = A + (B / z)   ← 원근 분할 (Perspective Divide)

 

이렇게 나온 z_ndc 값으로 연립 방정식을 풀면 A와 B 값을 알 수 있습니다, NearZ일 경우에는 0, FarZ일 경우에는 1을 넣어서 풀어보면 됩니다.

//// NearZ의 경우
A + (B / Zn) = 0
→ A = -B / Zn

//// FarZ의 경우
A + (B / Zf) = 1
→ (-B / Zn) + (B / Zf) = 1
→ B * (-1/Zn + 1/Zf) = 1
→ B = 1 / ( -1/Zn + 1/Zf ) = (Zn * Zf) / (Zf - Zn)

 

이러면 A값은 다음과 같습니다.

A = -B / Zn = - (Zf) / (Zf - Zn)

따라서 A(Projection[2][2]) = Zf / (Zf - Zn)

 

4. Arr2D[3][3] = 0.0f

: w 성분 계산 시의 상수 항으로, w_clip = z 값을 보존하기 위해(이외에는 아무것도 더해지지 않도록) 0.0f 값을 갖습니다.

 

5. 예시

: FOV가 60도, 16:9 해상도, NearZ = 0.1, FarZ = 1000이 입력된 예시를 확인해보도록 하겠습니다.

float FovY = XMConvertToRadians(60.f); // 1.0472
float Aspect = 16.0f / 9.0f;
float NearZ = 0.1f;
float FarZ = 1000.0f;

float yScale = 1.0f / tanf(FovY / 2);       // ≈ 1.732
float xScale = yScale / Aspect;             // ≈ 0.974
float zCompress = FarZ / (FarZ - NearZ);    // ≈ 1.0001
float zBias = -NearZ * FarZ / (FarZ - NearZ); // ≈ -0.10001

// 결과 행렬:
| 1.732    0       0        0 |
| 0        0.974   0        0 |
| 0        0       1.0001   1 |
| 0        0      -0.10001  0 |

 

이제 View Space에 있는 점 ViewP(2, 3, 10, 1)에 이 행렬을 적용해봅시다. ProjectionMatrix * ViewP를 실시하면 원근 투영이 적용되어 Clip Space로 이동됩니다(ViewP -> ClipP).

 

- ClipX : x * XScale = 2 * 0.974 = 1.948

- ClipY : y * YScale = 3 * 1.732 = 5.196

- ClipZ : z * (Z압축 + Z보정) = 10 * 1.0001 + (-0.10001) = 10.001 - 0.10001 = 9.901

- ClipW : z = 10

 

이제 이것을 NDC 공간으로 옮기게 되는데, 이 때는 W Divide(W 나누기)를 수행해줍니다.

 

- NDC.x = ClipX / ClipW = 1.948/10 = 0.1948
- NDC.y = ClipY / ClipW = 5.196/10 = 0.5196
- NDC.z = ClipZ / ClipW = 9.901/10 = 0.9901

- NDC.w = ClipW / ClipW = 10/10 = 1

 

해당 단계는 Rasterizer에서 수행됩니다. W Divide가 수행되기 때문에 Z값이 크면 클수록(시점에서 멀어지면 멀어질수록) 크기가 작아지게 됩니다.

 

Clip Space 정점들이 다음의 값을 갖는다고 가정해봅시다.

 

- A: (2, 2, 2, 2)

- B : (2, 2, 10, 10)

- C : (2, 2, 50, 50)

 

이러면 A는 이후 2로, B는 10으로, C는 50으로 각 요소를 나눠줘야합니다. 

 

- NDCA : (1.0, 1.0, 1.0)

- NDCB : (0.2, 0.2, 0.2)

- NDCC : (0.04, 0.04, 0.04)

 

확인할 수 있는 것과 같이, X, Y 크기가 같아도 Z값이 다르면 크기가 달라지는 것을 확인할 수 있습니다.

 

 

[뷰포트 행렬]

 

프로젝션 행렬을 통해 Clip Space 좌표계로 값이 변경된 후, Rasterizer에서 W 나누기를 수행하면 NDC 좌표계로 값이 변경됩니다. 이제 이 Object 좌표값을 픽셀 공간(Screen Space)로 매핑해주기 위해 뷰포트 행렬을 활용합니다.

 

하지만 DirectX11에서는 뷰포트 행렬을 구하는 함수를 따로 지원하지 않습니다. 왜냐하면 하드웨어 고정 기능으로 처리하기 때문입니다. 우리는 이것을 RenderTarget을 만들 때 이미 정의해줬으며, 바인딩은 RSSetViewports() 함수를 호출하여 수행해줬습니다.

void Ext_DirectXRenderTarget::RenderTargetSetting()
{
	COMPTR<ID3D11RenderTargetView> RTV = Textures[0]->GetRTV(0);

	if (nullptr == RTV)
	{
		MsgAssert("랜더타겟 뷰가 존재하지 않아서 클리어가 불가능합니다.");
	}

	COMPTR<ID3D11DepthStencilView> DSV = DepthTexture->GetDSV();

	if (nullptr == DSV)
	{
		MsgAssert("뎁스스텐실뷰 왓");
	}
	
	Ext_DirectXDevice::GetContext()->OMSetRenderTargets(static_cast<UINT>(RTVs.size()), RTV.GetAddressOf(), DSV.Get());
	Ext_DirectXDevice::GetContext()->RSSetViewports(static_cast<UINT>(ViewPorts.size()), &ViewPorts[0]);
}

 

앞서 정의해준 뷰포트 값은 변환 수식에서 다음과 같이 사용됩니다.

ScreenX = (NDC.x + 1.0f) * 0.5f * Viewport.Width  + Viewport.TopLeftX;
ScreenY = (1.0f - NDC.y) * 0.5f * Viewport.Height + Viewport.TopLeftY;
ScreenZ = NDC.z * (MaxDepth - MinDepth) + MinDepth;
Viewport.Width, Height 실제 렌더링할 영역 크기(픽셀 단위)
TopLeftX, TopLeftY 뷰포트 시작 위치(기본 0, 0)
MinDepth, MaxDepth 0.0 ~ 1.0(깊이 버퍼 범위

 

 

[결과 적용]

 

세 가지 행렬을 모두 정의해줬다면, 이 값들을 구해줘야합니다. 해당 프레임워크에서는 Rendering() 함수를 호출하기 전에 Ext_Scene 클래스의 Update 함수에서 구해줬습니다.

// 현재 카메라 타입에 따라 뷰, 프로젝션, 뷰포트 행렬 세팅
void Ext_Camera::CameraTransformUpdate()
{
	float4 EyePos = GetTransform()->GetWorldPosition();
	float4 EyeDir = GetTransform()->GetLocalForwardVector();
	float4 EyeUp = GetTransform()->GetLocalUpVector();
	ViewMatrix.LookToLH(EyePos, EyeDir, EyeUp);

	switch (CameraType)
	{
	case ProjectionType::Perspective:
	{
		ProjectionMatrix.PerspectiveFovLH(FOV, Width / Height, Near, Far);
		break;
	}
	case ProjectionType::Orthogonal:
	{
		// Projection.OrthographicLH(Width * ZoomRatio, Height * ZoomRatio, Near, Far); 일단 패스
		break;
	}
	case ProjectionType::Unknown:
	{
		MsgAssert("카메라 투영이 설정되지 않았습니다.");
		break;
	}
	}

	ViewPortMatrix.ViewPort(Width, Height, 0.0f, 0.0f);
}

 

이제 렌더링될 물체들의 Transform에 행렬들을 적용해줍니다.

void Ext_Camera::Rendering(float _Deltatime)
{
	// MeshComponents 렌더링 업데이트 시작
	for (auto& [Key, MeshComponentList] : MeshComponents)
	{
		// [!] 필요하면 ZSort 실시(나중에)

		for (const std::shared_ptr<Ext_MeshComponent>& CurMeshComponent : MeshComponentList)
		{
			if (!CurMeshComponent->GetIsUpdate()) continue;
			else
			{
				CurMeshComponent->Rendering(_Deltatime, GetSharedFromThis<Ext_Camera>()); // [3] 현재 MeshComponent에게 카메라의 View, Projection 곱해주기
				// [!] 필요하면 픽셀 셰이더에서 활용할 Value들 업데이트
			}
		}

	}
    
    //...
}

 

프레임워크에서 위의 호출 구조는 객체화된 MeshComponent의 TransformData에 값을 전달하도록 설계되어 있습니다. 함수를 계속 따라 들어가면, 아래의 함수가 호출됩니다.

// 카메라 기준으로 월드, 뷰, 프로젝션 행렬 생성
void TransformData::SetViewProjectionMatrix(const float4x4& _View, const float4x4& _Projection)
{
    ViewMatrix = _View;
    ProjectionMatrix = _Projection;
    WorldViewMatrix = WorldMatrix * ViewMatrix;
    WorldViewProjectionMatrix = WorldMatrix * ViewMatrix * ProjectionMatrix;
}

 

사실 View, Projection Matrix를 단순하게 전달만 하고 Vertex Shader에서 연산 처리를 실시해도 되지만, 굳이 이런 식으로 연산한 이유는 값을 따로 확인해보기 위함입니다.

[목차]

 

- 기능추가 2가 진행된 이유

- 프로젝트 구성

- 프레임워크 Flow

- 추가된 기능들

 

 

[기능추가 2가 진행된 이유]

 

단순하게 하나의 결과물만 출력한다면 이전 포스팅까지 진행했던 방식대로 하나씩 출력해보면 되지만, 궁극적으로는 게임 프레임워크가 어떻게 이루어져있는가와 더불어 그래픽스 학습을 위해 만들고 있는 프레임워크이기 때문에(물론 이 프레임워크로 게임을 제작하지 않을 수도 있습니다. 이외에 할 게 많기 때문입니다) 렌더 결과물을 하나씩 출력하거나, 출력 결과물마다 렌더링 파이프라인을 일일이 설정해줄 수 없는 노릇이었습니다.

 

이에 따라 필요한 기능들을 추가했고, 다음과 같은 결과를 얻을 수 있었습니다.

아마 25.05.22~23 쯤
일주일 후, 25.05.28~29일쯤

 

 

[프로젝트 구성]

 

추가된 기능들을 간단하게 살펴보기에 앞서, 현재 프레임워크가 어떻게 이루어져 있는지, 어떤 흐름을 가지는지에 대해 간략하게 설명드리고 넘어가겠습니다. 프레임워크의 프로젝트 구성은 다음과 같습니다.

DirectX11_Base - 가장 상위 프로젝트
- 프레임워크에 기본적으로 필요한 기능들을 담아둠(윈도우창 생성, float4 자료형, 델타타임 등)
- 빌드하면 정적 라이브러리(,lib)가 됨
DirectX11_Extension - 프레임워크 Loop간 필요한 기능과 DirectX 관련 기능 모음 프로젝트
- 사실 이름을 Extention이 아니고 Engine이라고 지었어야 하지 않았을까?
- 빌드하면 정적 라이브러리(,lib)가 됨
DirectX11_Contents - 사용자를 위한 프로젝트
- 여기서 Actor, Scene, gui 등 자유롭게 만들고 테스트하면 됨
- 빌드하면 정적 라이브러리(,lib)가 됨
DirectX11_RenderingPipeline - 시작 프로젝트
- exe 파일로 빌드되어 이걸로 실행하면 프로세스 동작
Shader - 필요한 셰이더 목록
ThirdPaty - DirectXTK : 텍스쳐 로드용
- assimp : 메시 로드용
- imgui : 사실 여기 있어야하는데, 그냥 Extension 프로젝트에 넣어놨다.

 

+) 사실 DirectX11_Extension 프로젝트의 경우, DirectX11_Engine이라는 이름이 맞습니다만, 작명 실수입니다. 이걸 고치기에는 너무 먼 길을 와버려서 그냥 넘어가기로 했습니다.

 

 

[프레임워크 Flow]

 

프레임워크의 전체적인 흐름은 다음과 같습니다.

Window Create - exe 파일 시작 시, 윈도우창 생성
- 생성 즉시 Window Loop 진입
Level(Scene) Create - Engine Start 부분에서 필요한 Scene이 있다면 Create 실시
- Scene이 생성됨과 동시에 Ening Core에 저장
- 여기서 Actor랑 Component 생성해도 됨
Program Loop Check - 윈도우창이 파괴될 경우 프로세스 종료로 진입, 아니라면 루프 진행
Level(Scene)
Change Check
- Scene이 변경됐는지 체크, 변경됐다면 해당 레벨의 SceneChangetStart를 실시하고, 이전 Scene은 SceneChangeEnd를 실시하여 설정 변경
- Scene 변경 시 필요한 Actor, Component 생성 및 저장
- 생성된 Actor와 Component는 Window Loop에서 Update 실시
Deltatime Calculation - Deltatime을 산출
- 현재 프로세스는 60FPS로 제한된 상태(따로 설정함)
Level(Scene) Update - 선택된 Scene의 Update 실시
Actor Update - 선택된 Scene에 존재하는 Actor와 Component의 Update 실시
- 이동, 피격(아직 안함) 등 설정, 이외 업데이트 시 실행할 동작들을 정의(카메라 포함)
Rendering Setting - 렌더 타겟 뷰 클리어 및 세팅
- 카메라 뷰, 프로젝션, 뷰포트 행렬 계산
- Actor에 부착된 MeshComponent의 Rendering Setting 진행
- Rendering Pipeline으로 설정된 옵션별로 세팅 진행
- 이후 DrawIndexed로 렌더타겟뷰에 모두 그려줌
Present - 렌더타겟뷰를 Present하여 화면에 출력
Release - 위에는 표시 안되어있지만, 루프 한번 돈 이후 만약 Destroy를 호출한 액터나 컴포넌트가 있다면 제거
Window End - 윈도우 종료

 

+) 이후 Destroy 기능을 추가해서 Present 이후 단계에서 Release 단계를 실시합니다. 이전에 Destroy를 호출한 Actor나 Component가 있다면, Release 단계에서 지워줍니다(메모리에서 소거).

 

 

[추가된 기능들]

 

1. Scene

: 액터와 컴포넌트, 카메라를 관리하는 클래스입니다. 여러 개가 존재할 수 있으며 자신에게 포함된 액터, 컴포넌트, 카메라의 라이프사이클을 관리합니다.

 

2. Actor

: 트랜스폼을 갖는 오브젝트입니다. 이동, 회전, 크기 변경이 가능하고 컴포넌트를 탈부착하여 기능을 확장할 수 있습니다.

 

3. Component

: 액터에 탈부착하는 객체입니다. 마찬가지로 트랜스폼을 가지고, 액터에 부착되면 트랜스폼을 통해 액터의 자식으로 등록됩니다. 이러면 액터의 트랜스폼을 부모 트랜스폼으로 인식하고, 이동, 회전, 크기 변경 대해 액터의 영향을 받게 됩니다. 저의 프로젝트에서는 주로 Mesh를 위한 컴포넌트로 설계되어있는데, 충돌체나 물리 컴포넌트 등 다양한 기능들을 탈부착할 수도 있습니다(아마 안할듯..).

 

4. Transform

: 액터와 컴포넌트의 기하구조를 위한 클래스입니다. Local Matrix(로컬 행렬)과 World Matrix(월드 행렬)로 나뉘며, 부모가 있을 때와 없을 때로 나뉩니다.

> 부모가 없을 때 = 로컬 행렬이 지정되면 그건 바로 월드 행렬이 됩니다(로킬이 곧 월드).

> 부모가 있을 때 = 로컬 행렬이 지정되고 부모 월드 행렬의 영향을 받아 부모+자신으로 월드 행렬이 생성됩니다. 여기에 대해선 많은 옵션이 있을 수 있지만, 기본적으로는 이것만 만들어두었습니다(크게 쓸 일이 없을 것으로 예상됨)

 

5. Input

: 간단한 카메라 조작을 위해 Input을 관리하는 클래스를 만들었습니다. 매 프레임 시작 부분에서 GetAsyncKeyState()로 입력이 발생했는지 확인합니다. 입력이 발생했다면 눌렀는지, 누르고 있는지, 안누르는지만 확인해줍니다.

+ Recent posts