일단, 조명을 추가하기 전 RenderBase의 구성을 살짝 바꿔주었다.

 

기존에는 init()이 순수가상함수여서, 하위 클래스에서 구현해주도록 했지만 조명에 대한 상수버퍼는 모든 렌더러가 보유하고 있도록 하기 위해 부모함수의 Init()에 조명에 대한 상수버퍼를 생성하는 코드를 작성하고 하위 클래스의 Init에선 Renderbase::Init()을 호출해주도록 하였다.

 

하지만, 이렇게 수동으로 부모 클래스의 함수를 호출하는 경우, 실수로 빼먹을 수도 있다.

이를 방지하기 위해 bool값을 하나 선언하였다.

 

렌더러를 생성하는 함수에서 렌더러의 init함수를 호출한 뒤, 렌더러의 bool값이 false라면 콘솔창에 메세지를 출력해주도록 설정하여, Init을 호출하지 않은 경우 이를 바로 알아볼 수 있도록 해주었다.

 

void RenderBase::Init()
{
    isCallInit = true;
}

 

RenderBase의 init에 일단 isCallInit을 true로 만드는 코드를 추가해주었다.

template <typename T>
static std::shared_ptr<T> CreateRenderer()
{
    std::shared_ptr<class RenderBase> NewRenderer = std::make_shared<T>();
    NewRenderer->Init();

    if (NewRenderer->isCallInitFunction() == false)
    {
        std::cout << NewRenderer->Name << " : RenderBase::Init() is not called. " << std::endl;
    }

    EngineBase::GetInstance().AddRenderer(NewRenderer);

    return std::dynamic_pointer_cast<T>(NewRenderer);
}

 

렌더러를 생성하는 함수에서, 해당 렌더러의 isCallInit이 false라면, 콘솔창에 메세지를 출력해주도록 하였다.

bool isCallInitFunction()
{
    return isCallInit;
}

isCallInitFunction은 그냥 bool값을 반환하는 함수이다. 아무래도, 해당 bool값은 init함수 이외의 다른 곳에서 변경되면 안되기 때문에 private로 안전하게 숨겨두고 함수를 통해 값을 확인할 수 있도록 하였다.

 

이제 조명을 구성해보자.

먼저 조명 정보를 담을 구조체를 하나 선언하였다.

struct LightData
{
	DirectX::SimpleMath::Vector3 Strength = { 1.0f, 1.0f, 1.0f };
	float FallOffStart = 0.0f;

	DirectX::SimpleMath::Vector3 Direction = { 0.0f, 0.0f, 1.0f };
	float FallOffEnd = 10.0f;

	DirectX::SimpleMath::Vector3 Position = { 0.0f, 0.0f, -2.0f };
	float SpotPower = 1.0f;
};

Strength는 빛의 강도이다.

 

FallOffStart와 FallOffEnd는 빛의 감쇠 거리이다.

빛과의 거리가 FallOfStart 이하 일 때 빛은 최대 강도이고, 거리가 멀어질수록 감쇠하다 FallOfEnd에 도달하면 빛은 사라지게 된다.

Direction은 빛의 방향이며, Positon은 광원의 위치이다.

 

#define LIGHT_NUM 3

struct LightCBuffer
{
	DirectX::SimpleMath::Vector3 EyeWorld;
	float Ambient = 0.1f;

	Light Lights[LIGHT_NUM];
};

 

빛의 개수를 중간에 계속 바꿔가며 상수버퍼를 업데이트 하는 것은 매우 비효율적이기 때문에 컴파일 타임에 전역 조명의 개수를 미리 정해놓고, 고정된 개수의 조명을 사용하도록 하기 위해 위와 같이 LIGHT_NUM을 define한 뒤, 해당 개수에 맞는 조명을 가지고 있는 구조체를 만들어주었다.

 

Specular 를 계산하기 위해선 눈의 위치 정보가 필요하기 때문에 Eye의 WorldPositon도 함께 보내주었고, 저수준의 조명모델에선 ambient를 상수로 계산하는 것이 일반적이기 때문에 Ambient는 0.1f의 상수로 설정하였다.

 

LightCBuffer& GetWorldLight()
{
    return WorldLight;
}
    
LightCBuffer WorldLight;

 

EnigineBase엔 위처럼 WorldLight변수를 선언하여 주었고, 이를 반환해주는 함수도 만들어주었다.

 

void RenderBase::Init()
{
    isCallInit = true;

    CreateConstantBuffer(EngineBase::GetInstance().GetWorldLight());
}

 

RenderBase의 init에서 조명에 대한 상수버퍼를 만들어주었다.

#define LIGHT_NUM 3

struct Light
{
    float3 Strength;
    float FallOffStart;

    float3 Direction;
    float FallOffEnd;

    float3 Position;
    float SpotPower;
};

cbuffer WorldLights : register(b0)
{
    Light Lights[LIGHT_NUM];
};

 

쉐이더에서도 조명에 대한 정보를 추가해주었다.

 

이제, 쉐이더 코드를 작성할 차례이다.

버텍스 쉐이더에선 조명 계산을 하지 않고, 픽셀 쉐이더에서 할 것이다.

그렇기 때문에, 일부 트랜스폼 정보만 버텍스 쉐이더에서 계산한 뒤 픽셀쉐이더로 보내줄 것이다.

 

#include "LightHeader.hlsli"

cbuffer WorldLights : register(b0)
{
    float3 EyeWorld;
    float Ambient;
    
    Light Lights[LIGHT_NUM];
};

cbuffer WorldViewProjection : register(b1)
{
    matrix World;
    matrix View;
    matrix Projection;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 Normal : NORMAL;
    float2 TexCoord : TEXCOORD;
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD0;
    
    float3 WorldPos : POSITION1;
    float3 WorldNormal : NORMAL;
};

PixelShaderInput main(VertexShaderInput _Input)
{
    PixelShaderInput Output;
    float4 Pos = float4(_Input.pos, 1.0f);
    
    Pos = mul(Pos, World);
    Pos = mul(Pos, View);
    Pos = mul(Pos, Projection);

    Output.pos = Pos;
    Output.TexCoord = _Input.TexCoord;
    
    Output.WorldPos = mul(float4(_Input.pos, 1.0f), World).rgb;
    Output.WorldNormal = mul(float4(_Input.Normal, 0.0f), World).rgb;
    
    return Output;
}

 

보면, WorldPos와 WorldNormal을 계산한 뒤, 픽셀 쉐이더로 전달해주고 있다.

조명 계산을 world공간에서 진행할 것이기 때문이다.

 

#include "LightHeader.hlsli"

cbuffer WorldLights : register(b0)
{
    float3 EyeWorld;
    float Ambient;
    
    Light Lights[LIGHT_NUM];
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
    
    float3 WorldPos : POSITION1;
    float3 WorldNormal : NORMAL;
};

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
    
    float LightSum = 0.0f;
    
    float DiffuseLight = CalDiffuseLight(Lights[0], _Input.WorldNormal);
    float SpecularLight = CalSpecular_Phong(Lights[0], _Input.WorldNormal, EyeWorld - _Input.WorldPos);
    float AmbientLight = Ambient;
    
    LightSum = DiffuseLight + SpecularLight + AmbientLight;
     
    Color.rgb *= LightSum;
    
    return Color;
}

 

 

픽셀 쉐이더에선 이렇게 WorldPos와 worldNormal을 받아서 DiffuseLight와 SpecularLight를 계산하는 함수로 보내주었다.

계산된 총 합을 현재 색상에 곱함으로써 결과 색상을 도출하였다.

 

조명 계산 함수는 아래와 같다.

float CalDiffuseLight(Light _Light, float3 _Normal)
{
    float3 LightDir = -_Light.Direction;
    LightDir = normalize(LightDir);
    
    float3 Normal = normalize(_Normal);
    
    float CosTheta = dot(LightDir, Normal);
    
    return CosTheta;
}

 

디퓨즈 라이트 계산이다. 간단하게 빛의 방향과 노멀벡터를 내적하여 값을 구해주었다.

 

float CalSpecular_Phong(Light _Light, float3 _Normal, float3 _EyeDir)
{
    float3 LightDir = _Light.Direction;
    LightDir = normalize(LightDir);
    
    float3 Normal = _Normal;
    Normal = normalize(Normal);
    
    float3 ReflectedLightDir = reflect(LightDir, Normal);
    
    float3 EyeDir = _EyeDir;
    EyeDir = normalize(EyeDir);
    
    float Specular = max(0.0f, dot(ReflectedLightDir, EyeDir));
    Specular = pow(Specular, 32.0f);
    
    return max(Specular, 0.0f);
}

 

위 코드는 스페큘러 라이트 계산식이다.

 

원래는 FallOffStart와 FallOffEnd도 적용해야 하고 이것저것 추가해야할게 많지만, 테스트를 하면서 천천히 추가하기 위해 기초적인 계산식만 작성하였다.

 

조명을 계산하기 위해, 렌더러의 트랜스폼 구조도 살짝 바꿔주었다.

시점과 관련된 뷰행렬은 총 1개만 존재한다고 가정하여, EngineBase에 선언해주었다.

그리고, 모든 렌더러는 EngineBase의 뷰행렬을 사용하여 트랜스폼을 계산하도록 하였다.

 

WorldLight.EyeWorld = DirectX::SimpleMath::Vector3::Transform(DirectX::SimpleMath::Vector3(0.0f), ViewMat.Invert());

EyeWorld는 (0.0f, 0.0f, 0.0f)에 뷰행렬의 역행렬을 곱해서 계산하였다.

 

ViewMat = DirectX::XMMatrixLookAtLH(EyePos, FocusPos, UpDir);
ViewMat *= DirectX::SimpleMath::Matrix::CreateRotationX(ViewRot.x) *
           DirectX::SimpleMath::Matrix::CreateRotationY(ViewRot.y) *
           DirectX::SimpleMath::Matrix::CreateRotationZ(ViewRot.z);

 

IMGUI를 이용해 뷰행렬의 회전을 조작하기 위해 ViewRot이라는 vector3를 선언하였고, 이를 이용해 뷰행렬을 프레임마다 갱신해줄 것이다.

 

일단 여기까지 적용한 뒤 테스트를 해보자.

 

 

얼추 잘 실행되는 듯 하다.

스페큘러도 잘 적용되는 듯 하고, 뷰행렬의 회전도 잘 적용된다.

 

다음엔 Specular의 강도를 조절하기 위한 shiness와 전체 빛의 강도를 조절할 Strengh, 그리고 빛의 감쇠까지 적용해 볼 예정이다.

광원을 추가하려고 하는데, 하기 전에 IMGUI를 먼저 프로젝트에 추가하고 나서 하는게 좋을듯 해서 IMGUI를 추가하기로 하였다.

 

if (!ImguiInit())
{
    return FALSE;
}

 

EngineBase의 Init에 IMGUI를 초기화하는 함수도 추가해주었다.

 

BOOL EngineBase::ImguiInit()
{
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::StyleColorsDark();

    if(!ImGui_ImplWin32_Init(hWnd))
    {
        std::cout << "IMGUI Win32 Init Failed" << std::endl;
        return FALSE;
    }

    if (!ImGui_ImplDX11_Init(Device.Get(), Context.Get()))
    {
        std::cout << "IMGUI Dx11 Init Failed" << std::endl;
        return FALSE;
    }

    return TRUE;
}

 

코드는 위와 같다. IMGUI에서 제공해주는 예제 코드에서 필요해보이는 함수만 가져와서 작성하였다.

 

 ImguiUpdate();
 ImGui::Render();

 Update();
 Render();

 ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());

 SwapChain->Present(1, 0);

 

위 코드는 EngineLoop의 코드이다. 원래는 update,Render, Present만 있었지만, Imgui에 대한 코드도 추가해주었다.

void EngineBase::ImguiUpdate()
{
    ImGui_ImplDX11_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("Hello, world!");                         
    ImGui::Text("This is some useful text.");            
    ImGui::End();
}

 

일단 테스트를 위해, GUI엔 간단한 텍스트만 출력하도록 해주었다.

 

 

상단에 GUI가 잘 만들어진 것을 볼 수 있다.

 

근데 뭔가 메모리 해제가 안됐나보다.

WPARAM EngineBase::End()
{
    ImGui_ImplDX11_Shutdown();
    ImGui_ImplWin32_Shutdown();
    ImGui::DestroyContext();

    return msg.wParam;
}

 

EngineBase의 End에 Imgui의 메모리를 해제하는 함수들도 추가해주니 메모리 누수는 더이상 발견되지 않았다.

 

이제 GUI업데이트 부분을 다시 수정하고자 한다.

아무래도, 렌더러에서 GUI에 변수를 연결하는 경우가 많을 것 같아서 렌더러쪽에서 GUI 업데이트 함수를 직접 작성할 수 있도록 callback방식으로 구성하고자 한다.

 

std::list<std::function<void()>> GUIFunctions;

 

Enginebase에는 위와 같이 함수포인터를 원소로 하는 list를 선언해주었다.

 

void AddGUIFunction(const std::function<void()> _Func)
{
    GUIFunctions.push_back(_Func);
}

 

외부에선 위 함수를 이용해 함수포인터를 list에 추가해줄 수 있다.

 

void EngineBase::ImguiUpdate()
{
    ImGui_ImplDX11_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("Hello, world!"); 
    
    for (const std::function<void()> _Func : GUIFunctions)
    {
        _Func();
    }

    ImGui::End();
}

 

GUI 업데이트 코드는 위와 같이 수정해주었다.

 

테스트를 해보자.

void BoxRenderer::Init()
{
    CreateVertexAndIndex();
    
    RenderBase::CreateVertexBuffer();
    RenderBase::CreateIndexBuffer();
    RenderBase::CreateConstantBuffer<Transform>(TransFormData);
    RenderBase::CreateConstantBuffer(UV);

    SetTexture("BoxTexture.png");
    SetSampler("LINEARWRAP");

    EngineBase::GetInstance().AddGUIFunction([] {ImGui::Text("AAA"); });
    EngineBase::GetInstance().AddGUIFunction([] {ImGui::Text("BBB"); });
    EngineBase::GetInstance().AddGUIFunction([] {ImGui::Text("CCC"); });
}

 

위와 같이 박스 렌더러의 init에서 AAA,BBB,CCC 텍스트를 출력하는 함수를 EngineBase에 추가해주었다.

 

결과는.. 잘 나오는 것 같긴 한데 왜 Imgui 윈도우 창 크기 조절이 안되는거지?

 

extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

LRESULT CALLBACK EngineBase::WndProc(HWND _hWnd, UINT _message, WPARAM _wParam, LPARAM _lParam)
{
    if (ImGui_ImplWin32_WndProcHandler(_hWnd, _message, _wParam, _lParam))
        return true;

    switch (_message) {
    case WM_SIZE:
        break;
    case WM_SYSCOMMAND:
        if ((_wParam & 0xfff0) == SC_KEYMENU)
            return 0;
        break;
    case WM_MOUSEMOVE:
        break;
    case WM_LBUTTONUP:
        break;
    case WM_RBUTTONUP:
        break;
    case WM_KEYDOWN:
        break;
    case WM_DESTROY:
        ::PostQuitMessage(0);
        return 0;
    }

    return ::DefWindowProc(_hWnd, _message, _wParam, _lParam);
}

 

찾아보니, 윈도우 메세지 처리 함수의 상단에 위와 같이 Imgui함수 코드를 추가해줘야 한다고 한다.

이 함수를 정삭적으로 실행하려면 코드 최상단에 있는 것처럼 전방선언을 해주어야 한다.

 

다시 실행해보면?

 

윈도우 크기 조절도 잘 되고, 텍스트도 잘 추가된 것을 볼 수 있다!

 

이번엔 프로젝트에 델타타임도 추가해보자.

IMGUI엔 계산된 Deltatime을 매 프레임마다 제공해주는 기능이 있다.

 

float CurDelta = ImGui::GetIO().DeltaTime;

위와 같은 함수를 사용하면, 현재 프레임의 델타타임을 얻을 수 있다.

 

Update(CurDelta);
Render(CurDelta);

 

이후, Update와 Render함수에 float형 인자를 추가하여, 델타타임을 인수로 전달해주었다.

void EngineBase::Update(float _DeltaTime)
{
    for (std::shared_ptr<RenderBase> Renderer : Renderers)
    {
        Renderer->Update(_DeltaTime);
    }
}

 

Renderer의 Update와 Render에도 델타타임을 추가해주었다.

 

잘 되는지, 테스트해보자.

Rotation += 18.0f * _DeltaTime;
float RotRad = Rotation / 180.0f * DirectX::XM_PI;

 

박스 렌더러의 Update에서 위의 코드처럼 1초에 18도씩 회전하도록 Rotation을 업데이트해주었다.

 

 

잘 작동하는 것을 볼 수 있다.

이제, 조명을 추가할 준비가 얼추 된 것 같다.

이번엔, 물체 표면의 색상정보를 이미지 파일에서 읽어와서 입히는 텍스쳐링을 추가하였다.

 

먼저, 이미지를 로드하는 기능이 필요한데 이 기능은 오픈소스로 제공되고 있는 stb 라이브러리를 활용하였다.

std::string Path = "../Texture/";
Path += _TextureName;

int Width = 0;
int Height = 0;
int Channels = 0;
    
unsigned char* LoadedImage = stbi_load(Path.c_str(), &Width, &Height, &Channels, 0);
if (LoadedImage == nullptr)
{
    std::cout << "Image Load Failed" << std::endl;
    return FALSE;
}

 

stbi_load는 이미지의 정보를 읽어오는 함수이다.

해당 함수의 첫번째 인자는 파일의 이름인데, 상대경로를 통해 파일을 인식하기 때문에 경로 설정을 잘 해야한다.

본인은 프로젝트 폴더에 텍스쳐 파일을 따로 모아둘 Texture 폴더를 생성하였고, 해당 폴더에서 텍스쳐를 읽어오도록 경로를 ../Texture/(파일이름.확장자) 의 형식으로 설정하였다.

std::vector<uint8_t> Image;

Image.resize(Width * Height * Channels);
memcpy(Image.data(), LoadedImage, Image.size() * sizeof(uint8_t));

 

이후, 로드한 이미지의 색상정보를 벡터에 복사해주었다.

(굳이 복사하지 않아도 기능작동은 잘 된다. 다만 본인은 벡터가 편해서 옮겨주었다. 최적화적인 측면에선 안하는게 좋을 듯 하다.)

 Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
 Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

 D3D11_TEXTURE2D_DESC TexDesc = {};
 TexDesc.Width = Width;
 TexDesc.Height = Height;
 TexDesc.MipLevels = TexDesc.ArraySize = 1;
 TexDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
 TexDesc.SampleDesc.Count = 1;
 TexDesc.Usage = D3D11_USAGE_IMMUTABLE;
 TexDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;

 D3D11_SUBRESOURCE_DATA InitData;
 InitData.pSysMem = Image.data();
 InitData.SysMemPitch = TexDesc.Width * sizeof(uint8_t) * Channels;
 
 HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateTexture2D(&TexDesc, &InitData, Texture.GetAddressOf());
if (Result != S_OK)
{
    std::cout << "CreateTexture2D failed " << std::endl;
    return FALSE;
}

Result = EngineBase::GetInstance().GetDevice()->CreateShaderResourceView(Texture.Get(), nullptr, SRV.GetAddressOf());
if (Result != S_OK)
{
    std::cout << "CreateTexture2D failed " << std::endl;
    return FALSE;
}

 

이후, Texture2D와 ShaderResourcesView를 만들어주었다.

TextureData NewTextureData;
NewTextureData.Texture = Texture;
NewTextureData.ShaderResourceView = SRV;

Textures.insert({ _TextureName, NewTextureData });

stbi_image_free(LoadedImage);

return TRUE;

 

이후, EngineBase에 텍스쳐 정보를 저장하기 위해 선언해둔 자료구조에 데이터를 저장한 뒤, 로드한 이미지의 메모리를 해제해주었다.

 

자료구조는 아래와 같다.

struct TextureData
{
	Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
	Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> ShaderResourceView;
};

std::unordered_map<std::string, TextureData> Textures;

 

텍스쳐를 만들었으니, 샘플러도 만들어야 한다.

 

BOOL EngineBase::CreateSampler()
{
    {
        Microsoft::WRL::ComPtr<ID3D11SamplerState> NewSampler;

        // Texture sampler 만들기
        D3D11_SAMPLER_DESC SamplerDesc;
        ZeroMemory(&SamplerDesc, sizeof(SamplerDesc));

        SamplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
        SamplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
        SamplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
        SamplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
        SamplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
        SamplerDesc.MinLOD = 0;
        SamplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

        // Create the Sample State
        HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateSamplerState(&SamplerDesc, NewSampler.GetAddressOf());
        if (Result != S_OK)
        {
            std::cout << "CreateSamplerState failed : LINEARWRAP" << std::endl;
            return FALSE;
        }

        Samplers.insert({ "LINEARWRAP", NewSampler });
    }

    {
        Microsoft::WRL::ComPtr<ID3D11SamplerState> NewSampler;

        // Texture sampler 만들기
        D3D11_SAMPLER_DESC SamplerDesc;
        ZeroMemory(&SamplerDesc, sizeof(SamplerDesc));

        SamplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
        SamplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
        SamplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
        SamplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
        SamplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
        SamplerDesc.MinLOD = 0;
        SamplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

        // Create the Sample State
        HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateSamplerState(&SamplerDesc, NewSampler.GetAddressOf());
        if (Result != S_OK)
        {
            std::cout << "CreateSamplerState failed : LINEARCLAMP" << std::endl;
            return FALSE;
        }

        Samplers.insert({ "LINEARCLAMP", NewSampler });
    }

    return TRUE;
}

 

샘플러는 일단 2개의 종류를 만들어두었다.

Filter는 LINEAR인 상태에서 WRAP인 샘플러와 CLAMP인 샘플러 2개를 만들었다.

이 역시 EngineBase의 자료구조에 저장해두었다.

 

void SetTexture(const std::string& _TextureName)
{
	TextureName = _TextureName;
}

void SetSampler(const std::string& _SamplerName)
{
	SamplerName = _SamplerName;
}

 

렌더러에선 위처럼 샘플러와 텍스쳐의 이름만 저장하도록 하였다.

Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV = EngineBase::GetInstance().GetTextureData(TextureName).ShaderResourceView;
Microsoft::WRL::ComPtr<ID3D11SamplerState> Sampler = EngineBase::GetInstance().GetSampler(SamplerName);

EngineBase::GetInstance().GetContext()->PSSetShaderResources(0, 1, SRV.GetAddressOf());
EngineBase::GetInstance().GetContext()->PSSetSamplers(0, 1, Sampler.GetAddressOf());

이후, Render함수에 위처럼 샘플러와 텍스쳐를 세팅하는 코드를 추가해주었다.

 

여기서, 하나의 쉐이더에 텍스쳐와 샘플러를 여러개 세팅하는 상황이 발생할 수도 있는데 일단은 1개만 세팅한다고 가정하고 구현하였다.

 

처음부터 염두에 두고 코드를 구성하는 것이 좋은 방법이지만, 본인은 수정하는 과정에서도 배울 것이 있다고 생각하여서 추후 다시 수정하는 과정을 거치려고 한다.

 

cbuffer UV : register(b1)
{
    float4 UV;
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
    float2 TexCoord : TEXCOORD;
};

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
    return Color;
}

 

픽셀 쉐이더에 텍스쳐와 샘플러를 추가한 뒤, 색상을 샘플링해주었다.

결과는 아래와 같이 정상적으로 잘 작동되는 것을 확인할 수 있다.

 

 

기존의 상수버퍼는 하드코딩되어있어서, 업데이트를 할 때마나 데이터 하나하나씩 map, unmap과정을 거쳐주어야 했다.

상수버퍼가 1개일 때는 별 상관 없겠지만, 여러개를 추가하려고 했더니 너무 번거롭고 귀찮은 부분이 많아서 구조를 수정해주었다.

struct ConstantBufferData
{
	Microsoft::WRL::ComPtr<ID3D11Buffer> ConstantBuffer;
	
	void* Data = nullptr;
	UINT DataSize = 0;
};

 

먼저, 상수버퍼의 데이터를 담을 구조체를 선언해주었다.

다양한 자료형에 대응해야하기 때문에 void*타입으로 데이터를 담도록 하였다.

또한, 상수버퍼를 업데이트할 때 데이터의 size가 필요한데 void*타입으로는 이를 계산할 수 없으므로 미리 계산하여 구조체에 함께 저장하도록 하였다.

std::list<ConstantBufferData> ConstantBuffers;

 

 

각 렌더러는 위와 같이, 상수버퍼를 저장하도록 하였다.

template <typename DataType>
void CreateConstantBuffer(DataType& _Data)
{
    D3D11_BUFFER_DESC CBDesc;
    CBDesc.ByteWidth = sizeof(DataType);
    CBDesc.Usage = D3D11_USAGE_DYNAMIC;
    CBDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    CBDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    CBDesc.MiscFlags = 0;
    CBDesc.StructureByteStride = 0;

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = &_Data;
    InitData.SysMemPitch = 0;
    InitData.SysMemSlicePitch = 0;

    Microsoft::WRL::ComPtr<ID3D11Buffer> NewBuffer;

    HRESULT Result =
        EngineBase::GetInstance().GetDevice()->CreateBuffer(&CBDesc, &InitData, NewBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        std::cout << Name << " :" << "CreateConstantBuffer() failed." << std::hex << "\nResult : " << Result << std::endl;
    };

    ConstantBufferData NewBufferData;
    NewBufferData.ConstantBuffer = NewBuffer;
    NewBufferData.Data = reinterpret_cast<void*>(&_Data);
    NewBufferData.DataSize = sizeof(_Data);

    ConstantBuffers.push_back(NewBufferData);
}

 

상수버퍼를 생성하는 함수이다. 코드는 전반적으로 동일하지만, 마지막에 list에 상수버퍼데이터를 삽입하는 과정만 추가해주었다.

 

for (const ConstantBufferData& _Data : ConstantBuffers)
{
    D3D11_MAPPED_SUBRESOURCE Ms;

    EngineBase::GetInstance().GetContext()->Map(_Data.ConstantBuffer.Get(), NULL, D3D11_MAP_WRITE_DISCARD, NULL, &Ms);
    memcpy(Ms.pData, _Data.Data, _Data.DataSize);
    EngineBase::GetInstance().GetContext()->Unmap(_Data.ConstantBuffer.Get(), NULL);
}

 

업데이트는 위와 같이 list의 원소를 순회하며 하나씩 업데이트하도록 해주었다.

 

int Index = 0;
for (const ConstantBufferData& _Data : ConstantBuffers)
{
    EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    EngineBase::GetInstance().GetContext()->PSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    Index++;
}

 

상수버퍼를 세팅하는 부분에선 위와 같이 해주었다.

여기서 본인은 동일한 상수버퍼를 버텍스쉐이더와 픽셀쉐이더에 모두 세팅하도록 해주었다. 구조체에 VS의 버퍼인지 PS의 버퍼인지 아니면 둘 다 사용하는 버퍼인지 enum 을 사용하여 분류해줄 수도 있지만, 쉐이더에서 레지스터의 번호와 상수버퍼를 일치시키는 부분에 있어서 위의 코드처럼 하는 것이 더 유리할 것이라고 생각하였다.

 

예를 들면, 위에서 버퍼를 세팅할 때, list에서 원소의 위치를 기반으로 슬롯 번호를 세팅해주고 있다.

이 때, list의 첫번째 원소는 vs의 버퍼이고 두번째 원소는 ps의 버퍼라고 했을 때, 각각 쉐이더에선 register(b0)을 사용하게 될 것이다. 상수버퍼가 많아지면 레지스터 번호를 매칭하는 것이 다소 복잡해질 수 있다.

 

하지만, 위처럼  PS와 VS를 동시에 세팅해주면, 첫번째원소는 b0을 사용하고, 두번쨰 원소는 b1을 사용하게 된다. 원소의 위치를 기반으로 레지스터 번호를 쉽게 매칭할 수 있다.

 

하지만 이 방법도 좋은 방법은 아니라고 생각한다. 왜냐하면 이렇게 하면 사용가능한 상수버퍼의 수가 줄어들기 때문이다.

PS에서만 사용하는 상수버퍼와 VS에서만 사용하는 상수버퍼를 분리하여 세팅하면 각 쉐이더당 16개의 상수버퍼를 등록할 수 있다. 하지만, 위의 코드처럼 세팅하게 되면 PS와 VS를 합쳐서 16개밖에 사용하지 못한다.

 

그래서 아래와 같은 방법도 생각해보았다.

PS에서 사용하는 상수버퍼와 VS에서 사용하는 상수버퍼를 다른 자료구조에 분리해서 저장한 뒤 사용하는 방식이다.

그런데 이렇게 하면 VS와 PS에서 둘 다 사용하는 상수버퍼를 관리하는 것이 다소 까다로워진다.

 

그래서 본인은 위의 코드와 같은 방식을 일단 사용하기로 하였다. 왜냐하면 16개 이상의 상수버퍼를 사용할 일이 없을 것 같았기 때문이다. 

 

바뀐 구조를 테스트하기 위해 상수버퍼를 하나 추가해보았다.

상수버퍼의 x값보다 텍스쳐좌표의 x값이 더 크면 파란색을 출력하고, 더 작다면 빨간색을 출력하도록 하였다.

cbuffer UV : register(b1)
{
    float4 UV;
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
    float2 TexCoord : TEXCOORD;
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    if(UV.x > _Input.TexCoord.x)
    {
        return float4(1.0f, 0.0f, 0.0f, 1.0f);
    }
    else
    {
        return float4(0.0f, 0.0f, 1.0f, 1.0f);
    }
}

 

위는 간단하게 작성해본 쉐이더 코드이다.

 

 

결과는 아주 만족스럽게 잘 나온다.

x값을 기준으로 했는데 특정 면에서는 좌우가 아니라 상하가 분리되는 이유는 박스를 만들때 버텍스를 그렇게 만들어서 그렇다... 아무튼 잘된다!

픽셀쉐이더로 간단한 테스트를 해보려고 했더니, 텍스쳐 좌표를 버텍스에 추가하지 않은 것을 알게되었다.

이로인해, 간단하게 버텍스에 텍스쳐 좌표를 추가해주었다.

 

struct Vertex
{
	DirectX::SimpleMath::Vector3 Position;
	DirectX::SimpleMath::Vector3 Color;
	DirectX::SimpleMath::Vector3 Normal;
	DirectX::SimpleMath::Vector2 TexCoord;
};

 

먼저, 버텍스 구조체에 TexCoord 변수를 추가해주었다.

텍스쳐 좌표는 x,y만 있으면 되기떄문에 vector2로 선언해주었다.

 

이에맞게 인풋 레이아웃도 변경해주었다.

std::vector<D3D11_INPUT_ELEMENT_DESC> inputElements = 
{
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 4 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 4 * 3 * 2, D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 4 * 3 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0},
};

 

이렇게, TexCoord도 인풋레이아웃에 추가해주었다.

여기서 주의해야 할 점은 데이터 포멧을 꼭 맞춰주자....

 

본인은 TexCoord를 vector2로 선언한 것을 잊고 위에 데이터 포벳을 DXGI_FORMAT_R32G32B32_FLOAT로 설정하였는데, 이로 인해 렌더링이 제대로 안되는 상황을 겪었다..

혹시나 하고 보니까 데이터 포멧이 잘못되어있었고, 위 코드처럼 R32G32로 수정했더니, 아주 잘 되는 것을 확인할 수 있었다.

 

텍스쳐 좌표를 테스트하기 위해, 픽셀쉐이더에 간단한 코드를 추가하였다.

 

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
    float2 TexCoord : TEXCOORD;
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    if(_Input.TexCoord.x > 0.5f)
    {
        return float4(1.0f, 0.0f, 0.0f, 1.0f);
    }
    else
    {
        return float4(0.0f, 0.0f, 1.0f, 1.0f);
    }
}

 

결과는 아래처럼 잘 나온다.

 

 

쉐이더 컴파일도 했고, 이제 렌더링만 해주면 된다.

void EngineBase::Render()
{
    float clearColor[4] = { 0.0f, 1.0f, 0.0f, 1.0f };
    Context->ClearRenderTargetView(RenderTargetView.Get(), clearColor);
    Context->ClearDepthStencilView(DepthStencilView.Get(),
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    Context->OMSetRenderTargets(1, RenderTargetView.GetAddressOf(), DepthStencilView.Get());
    Context->OMSetDepthStencilState(DepthStencilState.Get(), 0);
    Context->RSSetState(RasterizerState.Get());

    for (std::shared_ptr<RenderBase> Renderer : Renderers)
    {
        Renderer->Render();
    }

    SwapChain->Present(1, 0);
}

 

EngineBase에 Render함수를 만들어서 위와 같이 정의해주었다.

렌더타겟뷰를 초기화하면서 초록색으로 초기화해주었다. 그려지지 않은 부분을 좀 명확히 알기 위해서다.

뎁스스텐실 뷰도 지워주었다. 

얘네는 매 프레임 새로 사용해야하기때문에, 렌더링 시작 전에 한 번씩 지워주어야 한다.

 

다음은 렌더타겟을 세팅해주었다.

실제론 여러개의 렌더타겟을 바꿔가며 세팅하겠지만, 일단은 렌더타겟이 백버퍼 하나뿐이라고 가정하고

백버퍼의 렌더타겟으로 세팅해주었다.

뎁스스텐실스테이트와 레스터라이저스테이트도 세팅해준다음 렌더링 해주었다.

void BoxRenderer::Render()
{
    UINT Stride = sizeof(Vertex);
    UINT Offset = 0;
    
    VertexShaderData VSData = EngineBase::GetInstance().GetVertexShaderData(L"VertexTest.hlsl");
    Microsoft::WRL::ComPtr<ID3D11PixelShader> PS = EngineBase::GetInstance().GetPixelShaderData(L"PixelTest.hlsl");
    
    EngineBase::GetInstance().GetContext()->IASetVertexBuffers(0, 1, VertexBuffer.GetAddressOf(), &Stride, &Offset);
    EngineBase::GetInstance().GetContext()->IASetIndexBuffer(IndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
    EngineBase::GetInstance().GetContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    EngineBase::GetInstance().GetContext()->VSSetShader(VSData.VertexShader.Get(), 0, 0);
    EngineBase::GetInstance().GetContext()->IASetInputLayout(VSData.InputLayout.Get());
    EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(0, 1, ConstantBuffer.GetAddressOf());
    EngineBase::GetInstance().GetContext()->PSSetShader(PS.Get(), 0, 0);
    
    UINT IndexCount = Indices.size();
    EngineBase::GetInstance().GetContext()->DrawIndexed(IndexCount, 0, 0);
}

 

다음은 렌더러의 Render함수이다.

먼저, EngineBase가 가지고 있는 자료구조에서 쉐이더를 찾아온 뒤, 해당 쉐이더로 세팅해주었다.

 

원래는 렌더러마다 쉐이더의 이름을 저장한 뒤, 그 이름에 맞는 쉐이더를 탐색하게 할 생각이었으나 아직은 테스트중이라 일단 리터럴로 쉐이더 이름을 대입하였다.

 

쉐이더와 함께 인풋 레이아웃, 버텍스버퍼, 인덱스버퍼, 상수버퍼도 세팅을 해준 뒤, DrawIndexed를 호출해주면 끝이다.

 

 처음엔 화면에 계속 아무것도 안떠서 왜안뜨지..하고 고민하다가 비주얼 스튜디오의 그래픽 디버깅을 사용해봤는데, 프레임 캡쳐가 안되는 걸 보고 설마...스왑체인의 present를 호출안해줬나? 하고 봤더니 역시나 호출을 안하고 있었고, 호출을 해주었더니 잘 작동이 되었다.

 

아래는 결과이다.

 

아직 정육면체 하나 띄우는게 고작이지만, 이제 시작이다!

여기다 많은 걸 그리고 배워보자!

쉐이더 파일의 경우, 동일한 쉐이더가 여러번 컴파일될 필요가 없기 때문에 EngineBase에 쉐이더 파일을 따로 저장하도록 하였다.

 

그리고 쉐이더를 생성할 때, EngineBase에서 해당 쉐이더가 이미 존재하는지를 판단한 뒤 없을때에만 새로 생성하고 컴파일하도록 하였다.

 

struct VertexShaderData
{
    Microsoft::WRL::ComPtr<ID3D11VertexShader> VertexShader;
    Microsoft::WRL::ComPtr<ID3D11InputLayout> InputLayout;
};

 

이렇게, 버텍스쉐이더와 인풋 레이아웃을 함께 담는 구조체를 하나 선언하였다.

std::unordered_map<const std::wstring, VertexShaderData> VertexShaders;

 

EngineBase내부엔 위와 같은 자료구조를 선언하여,  버텍스 쉐이더 데이터를 저장하도록 하였다.

key는 쉐이더 파일의 이름이다.

 

BOOL EngineBase::CreateVertexShader(const std::wstring& _ShaderFileName, std::vector<D3D11_INPUT_ELEMENT_DESC> _InputElement)
{
    if (VertexShaders.find(_ShaderFileName) != VertexShaders.end())
    {
        std::cout << "Don't try to Create existed VertexShader" << std::endl;
        return TRUE;
    }

    Microsoft::WRL::ComPtr<ID3DBlob> ShaderBlob;
    Microsoft::WRL::ComPtr<ID3DBlob> ErrorBlob;

    HRESULT Result =
        D3DCompileFromFile(_ShaderFileName.c_str(), 0, 0, "main", "vs_5_0", 0, 0, &ShaderBlob, &ErrorBlob);

    if (Result != S_OK) 
    {
        if ((Result & D3D11_ERROR_FILE_NOT_FOUND) != 0) 
        {
            std::cout << "File not found." << std::endl;
        }

        if (ErrorBlob)
        {
            std::cout << "Shader compile error\n" << (char*)ErrorBlob->GetBufferPointer() << std::endl;
        }

        return FALSE;
    }

    Microsoft::WRL::ComPtr<ID3D11VertexShader> NewVertextShader;

    Result = 
        EngineBase::GetInstance().GetDevice()->CreateVertexShader(ShaderBlob->GetBufferPointer(), ShaderBlob->GetBufferSize(), NULL,
        &NewVertextShader);

    if (Result != S_OK)
    {
        std::cout << "CreateVertexShader() failed" << std::endl;
        return FALSE;
    }

    Microsoft::WRL::ComPtr<ID3D11InputLayout> _InputLayOut;
    if (!CreateInputLayOut(_InputElement, _InputLayOut, ShaderBlob))
    {
        return FALSE;
    }

    VertexShaders.insert({ _ShaderFileName, {NewVertextShader, _InputLayOut} });

    return TRUE;
}

 

먼저, 지금 만들고자 하는 쉐이더가 이미 생성되어 있다면 따로 만들지 않도록 해주었다.

이후, 입력된 파일 이름을 기반으로 쉐이더 컴파일을 실행하였고 파일이 있는지 없는지, 혹은 다른 오류로 컴파일이 실패하였는지 확인 과정을 거쳐주었다.

 

다음은 컴파일된 쉐이더를 통해 버텍스 쉐이더를 생성해주었다.

버텍스 쉐이더를 만들었으면, 인풋 레이아웃도 만들어야 하기 때문에, 인풋 레이아웃도 만들어주었다.

 

마지막으로, VertexShaders 자료구조에 데이터를 삽입해주었다.

BOOL EngineBase::CreateInputLayOut(std::vector<D3D11_INPUT_ELEMENT_DESC> _InputElement, Microsoft::WRL::ComPtr<ID3D11InputLayout> _InputLayOut, Microsoft::WRL::ComPtr<ID3DBlob> _ShaderBlob)
{
    HRESULT Result = 
    EngineBase::GetInstance().GetDevice()->CreateInputLayout(_InputElement.data(), UINT(_InputElement.size()),
        _ShaderBlob->GetBufferPointer(), _ShaderBlob->GetBufferSize(),
        &_InputLayOut);

    if (Result != S_OK)
    {
        std::cout << "CreateInputLayOut() failed" << std::endl;
        return FALSE;
    }

    return TRUE;
}

인풋 레이아웃 생성 함수는 위와 같다. 간단하다.

 

std::unordered_map<const std::wstring, Microsoft::WRL::ComPtr<ID3D11PixelShader>> PixelShaders;

픽셀 쉐이더에 대해서도 자료구조를 생성해주었다.

 

BOOL EngineBase::CreatePixelShader(const std::wstring& _ShaderFileName)
{
    Microsoft::WRL::ComPtr<ID3DBlob> ShaderBlob;
    Microsoft::WRL::ComPtr<ID3DBlob> ErrorBlob;

    HRESULT Result =
        D3DCompileFromFile(_ShaderFileName.c_str(), 0, 0, "main", "ps_5_0", 0, 0, &ShaderBlob, &ErrorBlob);
    
    if (Result != S_OK)
    {
        if ((Result & D3D11_ERROR_FILE_NOT_FOUND) != 0)
        {
            std::cout << "File not found." << std::endl;
        }

        if (ErrorBlob)
        {
            std::cout << "Shader compile error\n" << (char*)ErrorBlob->GetBufferPointer() << std::endl;
        }

        return FALSE;
    }

    Microsoft::WRL::ComPtr<ID3D11PixelShader> NewPixelShader;

    Result = 
    EngineBase::GetInstance().GetDevice()->CreatePixelShader(ShaderBlob->GetBufferPointer(), ShaderBlob->GetBufferSize(), NULL,
        &NewPixelShader);

    if (Result != S_OK)
    {
        std::cout << "CreatePixelShader() failed" << std::endl;
        return FALSE;
    }

    PixelShaders.insert({ _ShaderFileName, NewPixelShader });

    return TRUE;
}

 

픽셀 쉐이더도 버텍스 쉐이더와 동일하게 만들어주었다.

이제, 조금만 더 하면 화면에 정육면체를 띄울 수 있을 것 같다...!

버텍스 버퍼, 인덱스 버퍼와 동일한 과성으로 상수버퍼도 추가해주었다.

글이 나뉘는게 좀 불편하긴 한데, 글 제목이 너무 길어지는 것도 거슬려서 따로 작성하였다.

 

template <typename DataType>
void CreateConstantBuffer(DataType& _Data)
{
    D3D11_BUFFER_DESC CBDesc;
    CBDesc.ByteWidth = sizeof(DataType);
    CBDesc.Usage = D3D11_USAGE_DYNAMIC;
    CBDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    CBDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    CBDesc.MiscFlags = 0;
    CBDesc.StructureByteStride = 0;

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = &_Data;
    InitData.SysMemPitch = 0;
    InitData.SysMemSlicePitch = 0;

    HRESULT Result =
        EngineBase::GetInstance().GetDevice()->CreateBuffer(&CBDesc, &InitData, ConstantBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        std::cout << Name << " :" << "CreateConstantBuffer() failed." << std::hex << "\nResult : " << Result << std::endl;
    };
}

 

동일한 방식에 옵션만 다르게 하여 상수버퍼도 만들어주었다.

상수버퍼는 일단 Transform에 대해서만 사용할 예정이지만, 추후 어떤 타입이 추가될지 알 수 없기 때문에

템플릿을 이용하여 여러 자료형에 대한 가능성을 열어놓은 채로 정의하였다.

 

일단 실행은 잘 되는데, 화면에 보이지를 않으니 제대로 되고 있는건지 잘 모르겠다.

빨리 쉐이더까지 추가를 해보아야 할 듯 하다.

저번에, EngineBase를 싱글톤 패턴으로 바꾸고 나니, 메모리 누수가 감지되었다.

아무래도 소멸자의 호출 시기가 애매해서 그런 것 같았다.

 

실제로, 메모리 해제 함수를 만든다음 프로그램이 종료되기 전에 명시적으로 메모리를 해제해주니 메모리 누수가 사라졌음을 확인할 수 있었다.

 

하지만, 이렇게 메모리 해제를 직접 호출해야 하는 상황이 그렇게 좋은 방식으로 보이지는 않았고, 그냥 마이어스 싱글톤 형식으로 수정하였다.

public:
    static EngineBase& GetInstance()
    {
        static EngineBase Instance;
        return Instance;
    }

이렇게, 지역 스태틱을 선언하고 참조자를 반환하는 형식으로 구현하였고, 동적으로 메모리를 할당하지 않았기 때문에 당연히 누수도 사라졌음을 확인할 수 있었다.

렌더링 파이프라인을 설정하기 전에, 렌더러의 간단한 구조를 먼저 잡고 가는게 좋을 것 같아서 렌더러를 일단 구성해보았다.

#pragma once
#include <memory>

class RenderBase
{

public:
    RenderBase();
    ~RenderBase();

    RenderBase(const RenderBase& _Other) = delete;
    RenderBase(RenderBase&& _Other) noexcept = delete;
    RenderBase& operator=(const RenderBase& _Other) = delete;
    RenderBase& operator=(RenderBase&& _Other) noexcept = delete;

public:
    virtual void Init() = 0;
    virtual void Render() = 0;

protected:
    std::vector<Vertex> Vertices;
    std::vector<uint16_t> Indices;

    Microsoft::WRL::ComPtr<ID3D11Buffer> VertexBuffer;
    Microsoft::WRL::ComPtr<ID3D11Buffer> IndexBuffer;
    Microsoft::WRL::ComPtr<ID3D11Buffer> ConstantBuffer;
private:

};

 

일단, 모든 렌더러들은 RenderBase를 상속받도록 설정하였다.

EngineBase에서는 RenderBase으로 렌더러들을 저장한 뒤, 렌더링 할 때 Render함수를 실행할 것이기 때문에, Render함수를 순수가상함수로 만들어 하위 클래스에서 반드시 구현하도록 설정하였다.

 

Init도 마찬가지로, 렌더러가 처음 생성될 때 설정해야 하는 것들을 반드시 정의하도록 순수가상함수로 만들어두었다.

그 외에도, 렌더링에 사용될 버텍스 관련 변수들을 선언해주었다.

 

public:
    template <typename T>
    static std::shared_ptr<T> CreateRenderer()
    {
        std::shared_ptr<RenderBase> NewRenderer = std::make_shared<T>();
        NewRenderer->Init();
        Renderers.insert(NewRenderer);

        return std::dynamic_pointer_cast<T>(NewRenderer);
    }

 

렌더러의 생성은 EngineBase 클래스에 static함수로 선언해두었다.

템플릿 함수로 선언하여, RenderBase를 상속받은 클래스라면 어떤 클래스든 생성할 수 있도록 구성하였고

EngineBase가 소유하고 있는 RenderBase를 담는 자료구조인 Renderers에 생성된 Renderer를 insert해주었다.

private:
    std::list<std::shared_ptr<RenderBase>> Renderers;

 

렌더러가 몇 개가 될 지 예측이 불가능하기 때문에, 수시로 Reserve를 해주어야 하는 vector대신 list를 사용하였다.

이후, 렌더링 기능이 추가되면 위의 list의 원소를 순회하면서 Render함수를 호출해줄 것이다.

 

이제, 버텍스 버퍼와 인덱스 버퍼 등 렌더링에 필요한 것들을 추가해보자.

테스트용으로 박스 렌더러 하나를 추가해볼것이다.

#pragma once
#include "RenderBase.h"

class BoxRenderer : public RenderBase
{

public:   
    BoxRenderer();
    ~BoxRenderer();

    BoxRenderer(const BoxRenderer& _Other) = delete;
    BoxRenderer(BoxRenderer&& _Other) noexcept = delete;
    BoxRenderer& operator=(const BoxRenderer& _Other) = delete;
    BoxRenderer& operator=(BoxRenderer&& _Other) noexcept = delete;

public:
    virtual void Render() override{}
    virtual void Init() override{}

protected:

private:
    void CreateVertexAndIndex();
};

 

이렇게 RenderBase 클래스를 상속받았으며, Render함수와 Init함수를 오버라이딩 해주었다.

멤버함수로 Vertex와 Index를 만드는 함수도 만들어주었다.

 

함수 내부는 대강 아래와 같은데, 그냥 노가다이다.

더보기
void BoxRenderer::CreateVertexAndIndex()
{
    std::vector<DirectX::SimpleMath::Vector3> Positions;
    Positions.reserve(24);

    std::vector<DirectX::SimpleMath::Vector3> Colors;
    Colors.reserve(24);

    std::vector<DirectX::SimpleMath::Vector3> Normals;
    Normals.reserve(24);

    //윗면
    Positions.push_back({ -1.0f, 1.0f, -1.0f });
    Positions.push_back({ -1.0f, 1.0f, 1.0f });
    Positions.push_back({ 1.0f, 1.0f, 1.0f });
    Positions.push_back({ 1.0f, 1.0f, -1.0f });
    
    Colors.push_back({1.0f, 0.0f, 0.0f});
    Colors.push_back({1.0f, 0.0f, 0.0f});
    Colors.push_back({1.0f, 0.0f, 0.0f});
    Colors.push_back({1.0f, 0.0f, 0.0f});
   
    Normals.push_back({0.0f, 1.0f, 0.0f});
    Normals.push_back({0.0f, 1.0f, 0.0f});
    Normals.push_back({0.0f, 1.0f, 0.0f});
    Normals.push_back({ 0.0f, 1.0f, 0.0f});

    //아랫면
    Positions.push_back({ 1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, -1.0f, 1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 0.0f, -1.0f, 0.0f });
    Normals.push_back({ 0.0f, -1.0f, 0.0f });
    Normals.push_back({ 0.0f, -1.0f, 0.0f });
    Normals.push_back({ 0.0f, -1.0f, 0.0f });

    //왼쪽
    Positions.push_back({ -1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, -1.0f, 1.0f });
    Positions.push_back({ -1.0f, 1.0f, 1.0f });
    Positions.push_back({ -1.0f, 1.0f, -1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ -1.0f, 0.0f, 0.0f });
    Normals.push_back({ -1.0f, 0.0f, 0.0f });
    Normals.push_back({ -1.0f, 0.0f, 0.0f });
    Normals.push_back({ -1.0f, 0.0f, 0.0f });

    //오른쪽
    Positions.push_back({ 1.0f, 1.0f, -1.0f });
    Positions.push_back({ 1.0f, 1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, -1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 1.0f, 0.0f, 0.0f });
    Normals.push_back({ 1.0f, 0.0f, 0.0f });
    Normals.push_back({ 1.0f, 0.0f, 0.0f });
    Normals.push_back({ 1.0f, 0.0f, 0.0f });

    //앞쪽
    Positions.push_back({ -1.0f, 1.0f, 1.0f });
    Positions.push_back({ -1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, 1.0f, 1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 0.0f, 0.0f, 1.0f });
    Normals.push_back({ 0.0f, 0.0f, 1.0f });
    Normals.push_back({ 0.0f, 0.0f, 1.0f });
    Normals.push_back({ 0.0f, 0.0f, 1.0f });

    //뒷쪽
    Positions.push_back({ -1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, 1.0f, -1.0f });
    Positions.push_back({ 1.0f, 1.0f, -1.0f });
    Positions.push_back({ 1.0f, -1.0f, -1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 0.0f, 0.0f, -1.0f });
    Normals.push_back({ 0.0f, 0.0f, -1.0f });
    Normals.push_back({ 0.0f, 0.0f, -1.0f });
    Normals.push_back({ 0.0f, 0.0f, -1.0f });


    for (size_t i = 0; i < Positions.size(); i++) 
    {
        Vertex NewVertex;
        NewVertex.Position = Positions[i];
        NewVertex.Color = Colors[i];
        NewVertex.Normal = Normals[i];

        Vertices.push_back(NewVertex);
    }

    Indices = { 0, 1, 2,
                0, 2, 3,
                4, 6, 5,
                4, 7, 6,
                8, 9, 10,
                8, 10, 11,
                12, 13, 14,
                12, 14, 15,
                16, 17, 18,
                16, 18, 19,
                20, 21, 22,
                20, 22, 23 };
}

 

이렇게, 버텍스에 관한 정보를 모두 Vertices와 Indices에 담아주었다면, 이 정보를 기반으로 버퍼를 만들어야 한다.

버퍼를 만드는 함수는 RenderBase에 정의해두었다.

 

그런데, RenderBase에서 버텍스 버퍼를 만들려면 Device가 필요했는데, EngineBase의 Device를 어떻게 참조해야하나 고민하다가 그냥 EngineBase 클래스를 전역변수로 바꿔서 생성해주었다.

 

private:
    EngineBase();

    ~EngineBase()
    {
    	if (Instance != nullptr)
    	{
    	    delete (Instance);
    	}
    }

    static EngineBase* Instance;

public:
    static EngineBase* GetInstance()
    {
        if (Instance == nullptr)
        {
            Instance = new EngineBase();
        }

        return Instance;
    }

 

이렇게, Enginebase를 싱글톤 방식으로 구성한 뒤, 전역변수로 선언해주었다.

이후, EngineBase내부에 GetDevice함수를 추가해주었다.

void RenderBase::CreateVertexBuffer()
{
    D3D11_BUFFER_DESC bufferDesc;
    ZeroMemory(&bufferDesc, sizeof(bufferDesc));

    bufferDesc.Usage = D3D11_USAGE_IMMUTABLE; 
    bufferDesc.ByteWidth = UINT(sizeof(Vertex) * Vertices.size());
    bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    bufferDesc.CPUAccessFlags = 0; 
    bufferDesc.StructureByteStride = sizeof(Vertex);

    D3D11_SUBRESOURCE_DATA vertexBufferData = { 0, }; 
    vertexBufferData.pSysMem = Vertices.data();
    vertexBufferData.SysMemPitch = 0;
    vertexBufferData.SysMemSlicePitch = 0;

    const HRESULT Result =
        EngineBase::GetInstance()->GetDevice()->CreateBuffer(&bufferDesc, &vertexBufferData, VertexBuffer.GetAddressOf());

    if (Result != S_OK) 
    {
        std::cout << "CreateVertexBuffer() failed. " << std::hex << Result << std::endl;
    };
}

 

그리고 규칙에 맞게 버텍스 버퍼를 생성해주었다.

이렇게 작성하고 보니, 어떤 렌더러에서 버퍼 생성이 실패했는지 가시적으로 확인하기 위해 이름을 부여해주는 것이 좋겠다 싶었고, RenderBase에 이름을 추가해주었다.

BoxRenderer::BoxRenderer()
{
    Name = "BOX";
}

 

그리고 이렇게 생성자에서 이름을 설정해주고, 위의 버텍스 버퍼 생성 코드의 마지막 중에 있는 출력을 아래와 같이 바꿔주었다.

if (Result != S_OK) 
{
    std::cout << Name << " :" << "CreateVertexBuffer() failed." << std::hex << "\nResult : " << Result  << std::endl;
};

 

void RenderBase::CreateIndexBuffer()
{
    D3D11_BUFFER_DESC bufferDesc = {0, };
    bufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
    bufferDesc.ByteWidth = UINT(sizeof(uint16_t) * Indices.size());
    bufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
    bufferDesc.CPUAccessFlags = 0; 
    bufferDesc.StructureByteStride = sizeof(uint16_t);

    D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
    indexBufferData.pSysMem = Indices.data();
    indexBufferData.SysMemPitch = 0;
    indexBufferData.SysMemSlicePitch = 0;

    HRESULT Result = 
        EngineBase::GetInstance()->GetDevice()->CreateBuffer(&bufferDesc, &indexBufferData, IndexBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        std::cout << Name << " :" << "CreateIndexBuffer() failed." << std::hex << "\nResult : " << Result << std::endl;
    };
}

 

인덱스 버퍼도 만들어주었다.

 

이제, 상수버퍼도 만들고 쉐이더도 연결해야 하는데, 일단 밥먹고 나서 진행해야겠다.

+ Recent posts