DirectX

42. Light, Material_Global Shader

devRiripong 2024. 2. 11.
반응형

지난 시간에 구에다 텍스쳐를 붙이는 작업까지 끝내 봤다.

 

1. Shader의 World, View, Projection, Texture0, LightDir 등의 변수를 전역 변수처럼 사용하면 안 되는 이유와 대안과 오늘 작업 목표

 

지난 시간에 MeshRenderer를 맨 마지막에 작업을 해서 이런저런 기능들을 넣어 놨다.

쉐이더도 07. Normal.fx 에서 World, View, Projection, Texture0, LightDir을 받고 있고, 그거를 MeshRenderer::Update에서 설정을 해주고 있다.

    auto world = GetTransform()->GetWorldMatrix();
    _shader->GetMatrix("World")->SetMatrix((float*)&world);
    
    _shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
    _shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
    _shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
    
    // TEMP
    Vec3 lightDir = {0.f, 0.f, 1.f};
    _shader->GetVector("LightDir")->SetFloatVector((float*)&lightDir);

원래는 ConstantBuffer로 하던 거를 이렇게 편리하게 하고 있다. 전역변수 같은 느낌으로 사용할 수 있다.

 

하지만 앞으로 계속 이런 식으로 작업을 할 수는 없다.

원래 shader를 이용해 직접 작업을 했을 때는

matrix World; 
matrix View; 
matrix Projection; 
Texture2D Texture0;
float3 LightDir;

이렇게 전역으로 만들지 않고,

ConstantBuffer를 이용해 접근을 했었다 .

이렇게 전역으로 접근할 수 있었던

cbuffer GLOBAL
{

}

이런 글로벌 영역의 상수 버퍼로 만들어 준 거였다.

사실상

cbuffer GLOBAL
{
	matrix World; 
	matrix View; 
	matrix Projection; 
	Texture2D Texture0;
	float3 LightDir;
}

이렇게 들어가 있는 거랑 마찬가지였다.

 

1) 기본의 방식대로 쓰면 안 되는 이유 - 하나만 바꿔도 전부 갱신된다.

이럴 때 단점이 뭘까?

_shader->GetMatrix("World")->SetMatrix((float*)&world);

이렇게 접근할 떄 하나만 고치는 게 아니라

모든 애들이 다 같이 갱신이 된다. 하나만 건드려서 하는 게 fake였다는 거다.

 

그냥 테스트할 때는 전역처럼 했던 게 무방하긴 했지만

결국에는 조금 더 세분화해서 관리를 할 필요가 생긴다.라고 하면 이렇게 모든 것들을 전역인 것처럼 할 수 없다는 얘기가 된다.

 

2) 대안 - {World}와 {View, Projection}을 분리한다.

특히 World, View, Projection 3 총사가 있는데 이 중에서 사실상 2개는 짝이 있다. 2개는 묶여서 같이 관리가 되고 하나는 물체별로 독립적으로 있다.

World는 모든 물체들의 Trnasform이랑 연관이 있는 것이고, 물체들의 Transform에 따라 SRT를 하고, Parent를 곱해서 스자이공부 작업을 해서 그게 결과물이 World로 나오는 것이고,

View, Projection은 개개인 물체랑은 딱히 상관이 없고, 카메라가 바라보고 있는 방향, 카메라의 위치는 어디인지, 그리고 어떠한 투영 방식을 사용하고 있는지에 따라서 달라지는 부분이었다.

World와 View, Projection을 분리할 필요가 있다는 얘기가 된다.

마찬가지로 LightDir도 조명이랑 끼워있는 부분이고, 조명은 따로 빼야 한다. 카메라랑 별개이기 때문이다.

 

이런 식으로 분리 작업을 할 필요가 있다.

 

이게 오늘의 첫 번째 목표다.

 

2. Shader에서 공용 코드  빼서 #include로 사용하고, 테스트 하기 위한 준비하기

 

그리고 더 해볼 것은

Shader 작업을 할 때도 지금은 반복적인 코드가 많다.

C++ 작업 시 헤더 파일에서 다른 헤더를 포함할 수 있는 거랑 유사하게 Shader를 작업할 때도 다른 셰이더 파일을 include 해서 추가할 수 있다.

#include 하면 빌드하는 순간에 전처리기가 추가해 주는 작업이기 때문에 이것도 공용으로 묶는 부분들을 따로 빼서 관리할 수 있다는 얘기가 된다.

1) 필터 정리하고 10. GlobalTestDemo 클래스 생성하기

Client/Game 필터에 Week1 필터를 만들어서 지난 작업물들을 넣어 준다.

Week2라는 필터를 만들고 09. MeshDemo클래스를 복제를 한 다음에 10. GlobalTestDemo라고 한다.

Global 셰이더를 만들어서 테스트를 하는 그런 용도로 활용한다고 보면 된다.

Week2에 넣어준다. GlobalTestDemo에 맞게 코드를 수정해 준다.

 

2) Main에서 10. GlobalTestDemo 세팅하기

Main.cpp에서

#include "10. GlobalTestDemo.h"

WinMain에서

desc.app = make_shared<GlobalTestDemo>(); // 실행 단위

이렇게 세팅한다.

 

Client를 빌드하면 빌드가 된다.

 

3) 필터 정리하고 00. Global.fx와 08. GlobalTest.fx를 생성하기

그다음에 해야 할 건 Shader의 공용 부분을 따로 빼서 관리하는 게 가장 큰 목표이다라고 볼 수 있는 것이다.

Client/Shaders필터에서도 Week1 필터를 만들어 지난 시간에 했던 것들을 넣어준다.

Week2와 Common이란 필터를 만든다.

그리고 탐색기에서 07. Normal.fx 셰이더를 복제를 2개 해서

하나는 00. Global.fx라 해서 전역으로 사용할 모든 잡동사니들을 몰빵을 해줄 예정이고, Shaders/Common필터에 넣어준다.

또 하나는 08. GlobalTest.fx로 만들어서 Shaders/Week2 필터에 넣어준다.

 

3. 00. Global.fx 셰이더 코드 작성하기

00. Global.fx 쉐이더부터 작업을 한다. 원래 있던 코드를 삭제한다.

일종의 헤더파일처럼 다른 데서 이 아이를 사용하는 용도로 만들 것이다.

 

08. GlobalTest.fx 윗단에

#include "00. Global.fx"

이렇게 적고 작업을 한다고 가정을 해본다.

 

matrix World; 
matrix View; 
matrix Projection; 
Texture2D Texture0;
float3 LightDir;

이 부분을 Global.fx로 옮겨서 배치해도 08. GlobalTest.fx에 있던 것과 같은 의미로 받아들여진다.

 

1) 중복 방지 코드 작성하기

헤더 작업을 할 때 위에 #pragma once라는 게 붙었다.

예전엔 ifdef을 만들어 썼는데 헤더가 중복 추가 되지 않고 한 번만 사용하게끔 막아주는 기능을 한다.

그래서 00. Global.fx에서 동일한 작업을 해준다.

#ifndef _GLOBAL_FX_
#define _GLOBAL_FX_

matrix World;
matrix View;
matrix Projection;
Texture2D Texture0;
float3 LightDir;

#endif

이렇게 해주면 Global.fx를 다른 데에 넣었는데 그 넣은 애를 다시 include 한다거나 이럴 때 중복이 되는 걸 막아준다.

 

이제 모든 쉐이더에서 공용적으로 활용할 수 있는 애들을 모아 놓을 것이다.

View, Projection은 각 V, P라 한다.

 

2) V, P를 따로 곱해주는 부분을 VP로 한 번에 곱해주게 하기

다른 쉐이더를 보면

VertexOutput VS( VertexInput input)
{
    VertexOutput output;
    output.position = mul(input.position, World);
    output.position = mul(output.position, View);
    output.position = mul(output.position, Projection);

World, View, Projection을 각각 따로 곱해줬다.

VS라는 게 모든 정점들을 대상으로 실행이 되는 것이다.

모든 정점들을 대상으로 V, P를 곱해주고 있다.

이걸 성능을 조금 향상할 방법 중 하나는

 

행렬에서 VP를 따로 곱해도 되지만

결합하는 특징이 있기 때문에 VP를 곱한 연산의 결과물을 곱해도 된다.

VP를 연산한 것을 같이 넘겨주는 것을 고려할 수 있다.

V와 P는 넣어줄 필요 없는 건 아니다. 경우에 따라 코드 내부에서 V와 P를 연산할 필요가 있기 때문에다.

VP를 계산하는 주체는 누구일까?

GPU일까, CPU일까? CPU가 먼저 연산해서 결과물을 전달해 주게 되면 전달받은 걸 이용해서 VS에서

VertexOutput VS( VertexInput input)
{
    VertexOutput output;
    output.position = mul(input.position, World);
    output.position = mul(output.position, VP);

VP를 한 번만 곱해줄 수 있다는 얘기가 된다.

 

3) Constant Buffer를 V, P와 W를 나눠서 만들기

cbuffer GlobalBuffer
{
    matrix V;
    matrix P; 
    matrix VP; 
};

여기서 별 걸 다 넘겨줘도 된다.

V의 역행렬을 한 번만 계산해서 넘겨줘도 되고, 여러 가지 방법이 있다.

여기에 matrix W; 까지 넣어주는 게 현명할까?

하지만 이렇게 되면 World는 오브젝트 마다 다르니까 의미가 없다. 매번 갱신해줘야 하니까.

이렇게 정말 안 바뀌는 애들을 전역으로 하나 넣어주고,

또 하나의 버퍼를 빼가지고,

cbuffer TransformBuffer
{
    matrix W; 
};

이렇게 분리하는 게 합리적이라고 볼 수 있다.

 

#ifndef _GLOBAL_FX_
#define _GLOBAL_FX_

/////////////////
// ConstBuffer //
/////////////////

cbuffer GlobalBuffer
{
    matrix V;
    matrix P; 
    matrix VP; 
};

cbuffer TransformBuffer
{
    matrix W; 
};

Texture2D Texture0;
float3 LightDir;

#endif

이렇게 상수 버퍼 2개가 만들어졌다.

 

4) 필요한 VertexBuffer들을 정의해주기 

그다음에 들어가야 하는 게 Vertex와 관련된 정보들이다.

여기다가 ConstantBuffer만 넣는 게 아니라 온갖 잡동사니를 넣어줄 수 있다.

VertexInput, VertexOutput도 쉐이더 별로 받아주는 형태가 다를 수 있다.

VertexData.h에서 매핑을 해줘서 쉐이더와 연결을 해줄 수 있었다.

마찬가지로 VertexInput, VertexOutput도 여러 가지 형태가 준비가 되어 있을 건데

공용적인 부분들을 묶어서 관리하면 좋겠다는 생각이 든다.

//////////////////
// VertexBuffer //
//////////////////

struct Vertex
{
    float4 position : POSITION; 
};

struct VertexTexture
{
    float4 position : POSITION; 
    float2 uv : TEXCOORD; 
};

struct VertexColor
{
    float4 Position : POSITION; 
    float4 Color : COLOR; 
};

struct VertexTextureNormal
{
    float4 position : POSITION; 
    float2 uv : TEXCOORD; 
    float3 normal : NORMAL; 
};

이렇게 필요한 VertexData의 정의를 늘리면 된다.

 

5) VertexOutput을 정의하기

마찬가지로 VertexOutput 관련된 정보를 넣어준다.

VertexInput처럼 많지는 않을 것이다. 뭘 그리고 싶은지에 따라 Mesh 하나 출력하고 싶은지, 애니메이션이 붙은 걸 출력하고 싶은지 등등에 따라 살짝 달라지는 부분이라고 볼 수 있는데 다 넣어줄 수 있다.

//////////////////
// VertexOutput //
//////////////////

struct VertexOutput
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};

어떤 걸 출력하고 싶은지에 따라 예를 들어 MeshOutput이라 하거나 용도에 따라 이름을 붙이는 게 편할 수 있다.

지금은 일단 VertexOutput으로 한다.

position, uv, normal이 기본이니까 이렇게 내버려두도록 한다.

 

6) SamplerState들을 정의하기

//////////////////
// SamplerState //
//////////////////

SamplerState LinearSampler
{
    Filter = MIN_MAG_MIP_LINEAR; 
    AddressU = Wrap; 
    AddressV = Wrap; 
}; 

SamplerState PointSampler
{
    Filter = MIN_MAG_MIP_POINT;
    AddressU = Wrap;
    AddressV = Wrap;
};

Sampler도 자주 쓰이는 두 개를 둔다.

 

7) RasterizerState를 정의하기

/////////////////////
// RasterizerState //
/////////////////////

RasterizerState FillModeWireFrame
{
    FillMode = WireFrame; 
};

이 정도 하면 많이 한 거 같다.

 

8) Texture0, LightDir 삭제하기

Texture2D Texture0;
float3 LightDir;

는 나중에 얘기해보도록 하고 삭제한다. 공통적인 부분이라 하기 애매하다.

 

9) pass를 정하는 Macro 정의하기

그리고 많이 활용되는 부분 중 하나가

technique11 T0
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS())); // 버전은 5.0, main 함수는 VS다는 뜻
        SetPixelShader(CompileShader(ps_5_0, PS())); 
    }

    pass P1
    {
        SetRasterizerState(FillModeWireFrame);

        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
};

pass를 설정하는 부분이다.

연결해 주는 함수 이름인 VS(), PS() 이 부분 주로 바뀌는 것이다.

매크로로 만들어 두면 반복적인 작업을 안 할 수 있다.

///////////
// Macro //
///////////

#define PASS_VP(name, vs, ps)                           \
pass name                                               \
{                                                       \
    SetVertexShader(CompileShader(vs_5_0, vs()));       \
    SetPixelShader(CompileShader(ps_5_0, ps()));        \
}

이렇게 만들면 깔끔하다.

 

이 PASS_VP를 어떻게 사용할 수 있냐면

08. GlobalTest.fx에서

technique11 T0
{
    PASS_VP(P0, VS, PS)
};

이렇게 하면

    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS())); // 버전은 5.0, main 함수는 VS다는 뜻
        SetPixelShader(CompileShader(ps_5_0, PS())); 
    } 

이 부분을 대체할 수 있다.

 

물론 P0안에서 Rasterizer를 설정한다거나 그거에 따라가지고 #define 매크로를 늘려주면 된다.

 

10) 00. Global.fx에 들어 있어서 08. GlobalTest.fx 에서 중복된 코드 삭제하기

08. GlobalTest.fx에서

RasterizerState FillModeWireFrame
{
    FillMode = Wireframe;
};

도 공용으로 빼놓았으니 삭제해도 된다.

이런 식으로 공용부는 제거되면서 깔끔하게 될 수 있다.

나중에는 VS 부분도 매크로로 만들어 준다거나 하는 것을 고려할 수 있을 것이다.

 

#ifndef _GLOBAL_FX_
#define _GLOBAL_FX_

/////////////////
// ConstBuffer //
/////////////////

cbuffer GlobalBuffer
{
    matrix V;
    matrix P; 
    matrix VP; 
};

cbuffer TransformBuffer
{
    matrix W; 
};

//////////////////
// VertexBuffer //
//////////////////

struct Vertex
{
    float4 position : POSITION; 
};

struct VertexTexture
{
    float4 position : POSITION; 
    float2 uv : TEXCOORD; 
};

struct VertexColor
{
    float4 Position : POSITION; 
    float4 Color : COLOR; 
};

struct VertexTextureNormal
{
    float4 position : POSITION; 
    float2 uv : TEXCOORD; 
    float3 normal : NORMAL; 
};

//////////////////
// VertexOutput //
//////////////////

struct VertexOutput
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};

//////////////////
// SamplerState //
//////////////////

SamplerState LinearSampler
{
    Filter = MIN_MAG_MIP_LINEAR; 
    AddressU = Wrap; 
    AddressV = Wrap; 
}; 

SamplerState PointSampler
{
    Filter = MIN_MAG_MIP_POINT;
    AddressU = Wrap;
    AddressV = Wrap;
};

/////////////////////
// RasterizerState //
/////////////////////

RasterizerState FillModeWireFrame
{
    FillMode = WireFrame; 
}; 

///////////
// Macro //
///////////

#define PASS_VP(name, vs, ps)                           \\
pass name                                               \\
{                                                       \\
    SetVertexShader(CompileShader(vs_5_0, vs()));       \\
    SetPixelShader(CompileShader(ps_5_0, ps()));        \\
} 

//////////////
// Function //
//////////////                                                     

#endif

Global.fx를 여기까지 했으면 된 거 같고,

나중엔 함수 같은 것들도 넣어서 재사용할 수 있게 된다.

 

굉장히 많은 부분들이 이렇게 들어가고 있는데

나중에 필요한 게 생각이 나면 그때 또 돌아와서 채우면서 진행을 해주면 된다.

이렇게 많은 애들을 채워줬다.

 

4. #include "00. Global.fx"을 적용시 08. GlobalTest.fx를 수정하기 

이거에 따라 08. GlobalTest.fx에서 없는 부분들은 문제가 날 것이다. 그런 부분들을 고쳐 주면 된다. 

 

VertexInput, VertexOutput에 해당하는 걸 00. Global.fx에 만들어 놨으니 삭제하고

VertexOutput VS( VertexInput input)
{
    VertexOutput output;
    output.position = mul(input.position, World);
    output.position = mul(output.position, View);
    output.position = mul(output.position, Projection);
    
    output.uv = input.uv; 
    output.normal = mul(input.normal, (float3x3) World); 
    
    return output;     
}

이 코드는

VertexOutput VS(VertexTextureNormal input)
{
    VertexOutput  output;
    output.position = mul(input.position, W);
    output.position = mul(output.position, VP);
    
    output.uv = input.uv; 
    output.normal = mul(input.normal, (float3x3)W); 
    
    return output;     
}

이렇게 만들어 주면 된다.

 

SamplerState Sampler0;

도 00. Global.fx의 LinearSampler를 쓰면 되니 삭제한다.

 

Texture2D Texture0;

나중에 Light를 다룰 때 그때 묶어서 관리할 부분이라서 이렇게 내버려두도록 한다.

 

float4 PS(VertexOutput input) : SV_TARGET
{
    float3 normal = normalize(input.normal);
    float3 light = -LightDir; 
    
    // return float4(1, 1, 1, 1) * dot(light, normal);
    
    return Texture0.Sample(Sampler0, input.uv) * dot(light, normal);
}

이 부분은

지난 시간에 했던 normal과 관련된 람베르트 연산은 없애주고

float4 PS(VertexOutput input) : SV_TARGET
{
    return Texture0.Sample(LinearSampler, input.uv);
}

이렇게 만들어 준다.

#include "00. Global.fx"

VertexOutput VS(VertexTextureNormal input)
{
    VertexOutput output;
    output.position = mul(input.position, W);
    output.position = mul(output.position, VP);
    
    output.uv = input.uv; 
    output.normal = mul(input.normal, (float3x3)W); 
    
    return output;     
}

Texture2D Texture0; 

float4 PS(VertexOutput input) : SV_TARGET
{
    return Texture0.Sample(LinearSampler, input.uv);
}

technique11 T0
{
    PASS_VP(P0, VS, PS)
};

이렇게 만들어 줄 수 있다. 

 

빌드를 하면 통과가 되는 걸 볼 수 있다.

 

VS는 크게 달라지지 않고 PS에서 달라질 것이다.

어떤 색상일지가 관건이기 때문이다.

 

VS은 어느 정도 안정화가 되면 규칙을 찾아서 따로 공통적으로 빼준다거나 하는 식으로 관리를 해주면 된다.

 

 

5. MeshRenderer에서 Shader의 World, View, Projection, Texture0를 세팅해 주는 부분을 RenderManager로 따로 빼서 관리해 주는 이유

 

또 한 가지 고민인 부분은

지난 시간에 만든 MeshRenderer가 중요한 역할을 해줬다

GlobalTestDemo도 쉐이더랑 세팅하는 부분들이 들어가 있지 않으니 바꿔준다.

GlobalTestDemo::Init에서

_obj->AddComponent(make_shared<MeshRenderer>());
{
    auto shader = make_shared<Shader>(L"08. GlobalTest.fx"); 
    _obj->GetMeshRenderer()->SetShader(shader);
}

auto shader = make_shared<Shader>(L"08. GlobalTest.fx"); 이 코드는 눈에 잘 띄게 Init의 맨 윗줄로 옮긴다.

 

"08. GlobalTest.fx"이 쉐이더를 사용할 것인데

이거랑 관련되어서 World, View, Projection 등등을 방금 만들어준 00. Global.fx의

/////////////////
// ConstBuffer //
/////////////////

cbuffer GlobalBuffer
{
    matrix V;
    matrix P; 
    matrix VP; 
};

cbuffer TransformBuffer
{
    matrix W; 
};

요 아이에 맞게끔 세팅을 해줘야 하는 부분이 있는데

그 부분을 지금까지는 MeshRenderer안에서 Update라는 함수 안에서 해주고 있었다.

void MeshRenderer::Update()
{
	if (_mesh == nullptr || _texture == nullptr || _shader == nullptr)
		return;

	auto world = GetTransform()->GetWorldMatrix();
	_shader->GetMatrix("World")->SetMatrix((float*)&world);
	
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
	
	// TEMP
	Vec3 lightDir = {0.f, 0.f, 1.f};
	_shader->GetVector("LightDir")->SetFloatVector((float*)&lightDir);

	uint32 stride = _mesh->GetVertexBuffer()->GetStride();
	uint32 offset = _mesh->GetVertexBuffer()->GetOffset();

	DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
	DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	_shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
}

이렇게 해주고 있었는데

MeshRenderer에 이렇게 하드코딩으로 넣어주는 게 맞는지 의심이 된다.

 

World, View, Projection, Texture0 이 부분이 언제 바뀔지 모를뿐더러, 어떻게 할지 확실하지 않은데 여기다 하는 게 위험하단 생각이 든다.

 

World라는 게 물체마다 따로 있는 것이고, 물체가 케어하는 것이 맞기 때문에

World랑 View, Projection을 같이 여기서 하는 게 이상하다.

 

이런저런 이유로 분리해야 하긴 하는데 정확히 어떻게 할지 모르겠으니 공용적으로 케어할 수 있게

RenderManager 빼가지고 전역에서 관리해 가지고 밀어 넣는 작업을 해주게 될 것이다.

 

그중에서 하나 골라서 사용하는 식으로 뭘 밀어 넣을지를 매니저를 통해 가지고 밀어 넣는 식으로 일단은 분리를 해놓도록 한다.

6. RendererManager 클래스에서 셰이더의 GlobalBuffer와 TransformBuffer를 매핑하고 데이터를 넣어주는 코드 작성하기

나중에 맨 마지막까지 그 구조를 사용할지는 고민이 되긴 하지만 일단은 그건 나중에 생각하고 RenderManager라는 클래스를 추가해서 Engine/02. Managers에 넣는다.

 

설계는 나중에 익숙해지고 모든 그림을 그릴 수 있을 때 신경 써서 하는 거지 지금 단계에서 신경 쓸 부분은 아니니 Singleton을 사용한다. 

 

이 RenderManager의 역할은 작업했던 잡동사니들을 연결해 주는 역할을 하게 될 것이다.

#pragma once
#include "ConstantBuffer.h"

class Shader; 

struct GlobalDesc 
{
	Matrix V = Matrix::Identity; 
	Matrix P = Matrix::Identity; 
	Matrix VP = Matrix::Identity; 
};

struct TransformDesc
{
	Matrix W = Matrix::Identity; 
};

class RenderManager
{
	DECLARE_SINGLE(RenderManager); 

public:
	// 셰이더 마다 연결해줘야 되는게 달라질테니까 셰이더를 세팅하는 부분
	void Init(shared_ptr<Shader> shader); 

	// 정보를 밀어 넣기 위해
	void PushGlobalData(const Matrix& view, const Matrix& projection);
	void PushTransformData(const TransformDesc& desc);

private: 
	shared_ptr<Shader> _shader; 

	GlobalDesc _globalDesc; 
// 프레임마다 한번만 세팅하는 정보들
        
	shared_ptr<ConstantBuffer<GlobalDesc>> _globalBuffer; 
// 상수버퍼라는걸 만들어서 그 정보를 GPU에다가 전달 해줬었다.	
	
    ComPtr<ID3DX11EffectConstantBuffer> _globalEffectBuffer; 
// 쉐이더에게 이 버퍼를 사용하라 했을 때 Effect11를 사용했는데 
// ComPtr<ID3DX11EffectConstantBuffer> 이 아이를 사용하게 될 것이다. 
// 지금까지는 이걸 사용하지 않았다.
// MeshRenderer::Render에서 GetMatrix같이 Get~ 함수를 호출할 때 커서를 올려 보면
// 반환값의 타입이 ComPtr<ID3DX11EffectMatrixVariable> 이었다.
// 이제는 ID3DX11EffectConstantBuffer을 가져올 것이다. 
// _shader->GetConstantBuffer("버퍼이름")를 호출해서 가져올건데 
// 매번마다 Get을 하는 것 보다는 갖고 온 정보를 캐싱해서 그걸 ComPtr로 들고 있어서 하는게 더 효율적이다. 
    
	// 어떠한 정보를 넣고 싶으면 이 삼총사가 마련이 된다고 보면 된다. 
	// 마찬가지로 한번 더 복붙해서 

	TransformDesc _transformDesc;
	shared_ptr<ConstantBuffer<TransformDesc>> _transformBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _transformEffectBuffer;
};

삼총사가 2쌍이 마련되었고, 이걸 이용해서 정보를 밀어 넣을 작업이 끝났다고 볼 수 있다.

 

함수들을 구현해 주면

 

1) Init에서 ConstantBuffer를 Create 하고, Global.fx 셰이더의 cbuffer 와 RenderManager의 ComPtr<ID3DX11EffectConstantBuffer> 변수를 매핑해서 PushData할 준비하기

Init에서는 각각의 ConstantBuffer를 Create하고,  
ComPtr<ID3DX11EffectConstantBuffer> _globalEffectBuffer, _transformEffectBuffer에 Global.fx의 GlobalBuffer와 TransformBuffer 매핑한다.

void RenderManager::Init(shared_ptr<Shader> shader)
{
	_shader = shader; 

	_globalBuffer = make_shared<ConstantBuffer<GlobalDesc>>(); 
	_globalBuffer->Create(); 
	_globalEffectBuffer = _shader->GetConstantBuffer("GlobalBuffer");

	_transformBuffer = make_shared<ConstantBuffer<TransformDesc>>();
	_transformBuffer->Create();
	_transformEffectBuffer = _shader->GetConstantBuffer("TransformBuffer");
}

 

2) Desc를 세팅해 ConstantBuffer에 Copy 하고, ConstantBuffer의 Data를 매핑된 ComPtr<ID3DX11EffectConstantBuffer>를 통해 Global.fx의 cbuffer에 넣기

 Global.fx의 GlobalBuffer가 매핑이 되는 것이기 때문에 앞으로 pushGlobalData를 하고 싶다면 _globalEffectBuffer에다가 뭔가를 밀어 넣는 식으로 작업을 해주면 된다. _trasnsformEffectBuffer도 마찬가지다.

 

구조가 잡혀서 어디다 넣을지 확신이 들면 거기다가 이전을 시키면 되지만 그전까지는 이 Manger를 통해서 원하는 데이터들을 골라서 밀어 넣는 방식으로 작업을 할 것이다.

void RenderManager::PushGlobalData(const Matrix& view, const Matrix& projection)
{
	_globalDesc.V = view;
	_globalDesc.P = projection;
	_globalDesc.VP = view * projection; 
	_globalBuffer->CopyData(_globalDesc); 
	_globalEffectBuffer->SetConstantBuffer(_globalBuffer->GetComPtr().Get()); // 00. Global.fx의 GlobalBuffer로  밀어 넣는 작업을 해주고 있는 거다. 
}

Init에서 Shader에 매핑해 주고 PushGlobalData를 해주면 _globalDesc에 있는 정보들이 쉐이더의 GlobalBuffer에 매핑이 되어서 들어간다고 볼 수 있다.

void RenderManager::PushTransformData(const TransformDesc& desc)
{
	_transformDesc = desc; 
	_transformBuffer->CopyData(_transformDesc); 
	_transformEffectBuffer->SetConstantBuffer(_transformBuffer->GetComPtr().Get()); 
}

PushTransfromData도 마찬가지다.

이렇게 해서 다 사용할 준비가 끝난 것이다.

 

셰이더를 사용할 때마다 Init을 해서 RenderManager 쪽에 연결을 해주고,

그다음엔 PushGlobalData, PushTransformData를 해줘서 데이터를 밀어 넣으면 끝난다.

 

Engine을 빌드해주고

 

7. MeshRenderer를 EnginePch.h에 추가하고, EnginePch.h의 #define코드들을 Define.h로 옮기기

EnginePch.h에 가서

#include "RenderManager.h"
#define RENDER		GET_SINGLE(RenderManager)

추가해 준다.

빌드를 하면 에러가 발생한다.

이런 식으로 헤더를 추가하게 되면 경우에 따라 지금처럼 꼬일 수 있다.

 

#define CHECK(p)	assert(SUCCEEDED(p))
#define GAME		GET_SINGLE(Game)		
#define GRAPHICS	GET_SINGLE(Graphics)
#define DEVICE		GRAPHICS->GetDevice()
#define DC		GRAPHICS->GetDeviceContext()
#define INPUT		GET_SINGLE(InputManager)
#define TIME		GET_SINGLE(TimeManager)
#define DT		TIME->GetDeltaTime()
#define RESOURCES	GET_SINGLE(ResourceManager)
#define RENDER		GET_SINGLE(RenderManager)

를 Define.h로 옮기니 빌드가 된다.

 

일단은 이렇게 진행을 한다.

 

8. MeshRenderer::Update에서 RenderManager의 PushTransformData를 호출하기

MeshRenderer 쪽에서 바뀌어야 한다.

이전에 사용했던 Shader들이랑 안 맞아질 수 있다는 얘기가 된다.

RenderManager를 사용하는 버전으로 바꿔준다. 

 

MeshRenderer::Update에서 몇 가지만 수정을 해보자면

void MeshRenderer::Update()
{
	if (_mesh == nullptr || _texture == nullptr || _shader == nullptr)
		return;

	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());

	auto world = GetTransform()->GetWorldMatrix();
	RENDER->PushTransformData(TransformDesc{ world }); 
	//_shader->GetMatrix("World")->SetMatrix((float*)&world);
	
	uint32 stride = _mesh->GetVertexBuffer()->GetStride();
	uint32 offset = _mesh->GetVertexBuffer()->GetOffset();

	DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
	DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	_shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
}

이렇게만 남겨준다.

 

9. RenderManager::Update에서 RenderManager의 PushGlobalData를 호출하기

RenderManager에 그 부분만 넣어준다.

매 프레임마다 추가적으로 Update를 한다.

void Update();

이 Update안에 위에서 만든 PushGlobalData를

void RenderManager::Update()
{
	PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);
}

이렇게 호출하고 

#include "Camera.h"

헤더를 추가하고 

 

엔진을 빌드하면 빌드가 된다.

 

조금 정리가 됐다.

 

중요한 건 목적은

/////////////////
// ConstBuffer //
/////////////////

cbuffer GlobalBuffer
{
    matrix V;
    matrix P; 
    matrix VP; 
};

cbuffer TransformBuffer
{
    matrix W; 
};

이 글로벌하게 만든 아이들을 편리하게 사용하는 것이고,

여기에 이런저런 내용이 추가가 될 때마다 그거에 맞춰가지고 MeshRenderer를 다 고치는 건 말이 안 되니까

RenderManager라는 걸 따로 빼가지고,

필요한 것만 골라서 밀어 넣을 수 있게끔 이렇게 헬퍼 역할을 하는

PushGlobalData, PushTransformData 함수를 가진

RenderManager를 만들었다고 볼 수 있다.

 

10. GlobalTestDemo의 Update에서 RenderManager::Update를 호출해서 PushGlobalData로 View와 Projection 데이터를 00. Global.fx의 cbuffer GlobalBuffer에 넣어주기

 10. GlobalTestDemo로 다시 돌아간 다음에

여기서 최종적으로 케어를 해주면 된다.

void GlobalTestDemo::Update()
{
	_camera->Update(); 

	RENDER->Update(); 

	_obj->Update();
}

RENDER->Update();를 넣어준다.

이건 나중에 어디로 갈지 고민이다. 씬이라는 개념이 등장하면 씬에서 하는 게 맞는 거 같기 때문이다.

일단은 SceneManager는 복원 안 했기 때문에 여기에 둔다.

 

GlobalTestDemo::Update에서 _camera->Update()로 카메라 업데이트를 하자마자

void RenderManager::Update()
{
	PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);
}

카메라가 바뀐 정보들을

void RenderManager::PushGlobalData(const Matrix& view, const Matrix& projection)
{
	_globalDesc.V = view;
	_globalDesc.P = projection;
	_globalDesc.VP = view * projection; 
	_globalBuffer->CopyData(_globalDesc); 
	_globalEffectBuffer->SetConstantBuffer(_globalBuffer->GetComPtr().Get()); // 00. Global.fx의 GlobalBuffer로  밀어 넣는 작업을 해주고 있는 거다. 
}

여기에 밀어 넣고 있고,

이건 카메라가 한 프레임에 한 번만 바뀔테니까 한번만 세팅하면 된다.

그거에 따라 V, P, VP를 세팅한다.

 

사실상 _camera→Update()도 카메라랑 연동이 있는 거니까 카메라 컴포넌트에 합쳐서 관리하는 게 조금 더 좋을 거 같다는 생각이 든다.

 

그다음에 _obj→Update() 될 때는

MeshRenderer라는 컴포넌트에 의해가지고 자기 자신의 Transform과 Texture0을 세팅하는 부분이 적용이 되고 있다고 볼 수가 있다.

 

그럼 정리는 끝난 것이다.

 

 

이번에 한 게 대단한 걸 한 건 아니지만

공용적인 부분을 00. Global.fx에 몰아넣는 작업을 했고,

이 작업을 한 번만 진행하게 되면 편리하게 다른 것도 작업할 수 있게 된다.

 

11. GlobalTestDemo::Init에서 RenderManager::Init(shared_ptr<Shader> shader)을 호출하기

실행하면 에러가 나는데

RenderManager의 Init을 호출하지 않아서 그렇다.

인위적으로 한 번은 초기화를 할 때 어떤 쉐이더를 사용할 것인지

GlobalTestDemo::Init()에서

RENDER->Init(_shader);

셰이더를 연동을 시켜 줘야지만 그거에 맞춰서 RenderManager::Init에서 _globalEffectBuffer, _transfromEffectBuffer를 만들어 주기 때문이다.

 

GlobalTestDemo.h에 

shared_ptr<Shader> _shader;

를 선언하고, 

void GlobalTestDemo::Init()
{
	_shader = make_shared<Shader>(L"08. GlobalTest.fx");

	// Camera
	_camera = make_shared<GameObject>(); 
	_camera->GetOrAddTransform(); 
	_camera->AddComponent(make_shared<Camera>()); 
	_camera->AddComponent(make_shared<CameraScript>()); 

	// Object
	_obj = make_shared<GameObject>(); 
	_obj->GetOrAddTransform(); 
	_obj->AddComponent(make_shared<MeshRenderer>());
	{
		_obj->GetMeshRenderer()->SetShader(_shader);
	}
	{
		RESOURCES->Init(); 
		auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
		_obj->GetMeshRenderer()->SetMesh(mesh); 
	}
	{
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		_obj->GetMeshRenderer()->SetTexture(texture); 
	}

	RENDER->Init(_shader); 
}

 

실행을 하면 

복원이 됐다.

 

12. 맺음말 

똑같은 결과를 위해 대공사를 했는데 그게 아주 쓸모없는 작업은 아니었다.

한 번만 해놓으면 새로운 쉐이더를 만들고 하게 되면 이제 00. Global.fx를 또 재사용해 만들 수 있으니까 코드가 더 줄 것이고 로직에 집중할 수 있게 될 수 있다. 이런 방향으로 개선을 해 나갈 것이다.

 

Lighting이나 여러 가지 등장하게 될 텐데 그 아이들도 렌더 매니저에다 추가해서 늘려가는 식으로 작업을 하면서 나중에 추후 언젠가 모든 과정이 끝나면 다시 깔끔하게 정리를 하면서 진행을 할 것이다.

 

다시 ConstantBuffer가 들어가니까 전역 변수 방식에 비해 어려워졌지만 전역 변수가 사실 전역 변수가 아니라 간접적인 글로벌 cbuffer가 만들어졌던 거라 분리를 해 봤는데 분리하면 또 이렇게 의미가 있는 게 뭐가 묶여서 관리되는지를 생각할 수 있게 된다.

반응형

'DirectX' 카테고리의 다른 글

44. Light, Material_Ambient  (0) 2024.02.13
43. Light, Material_Depth Stencil View  (0) 2024.02.11
41. DirectX11 3D 입문_Mesh  (0) 2024.02.10
40. DirectX11 3D 입문_Normal  (0) 2024.02.09
39. DirectX11 3D 입문_HeightMap  (0) 2024.02.08

댓글