기존에는 각 렌더러의 Update에서 트랜스폼을 업데이트 해주고 있었는데, 이젠 세팅만 해주고 엔진에서 트랜스폼 업데이트 함수를 호출하도록 수정하였다.

 

 

Renderer의 멤버변수에 크기, 위치, 회전값을 만들어주었다.

로컬트랜스폼과 월드트랜스폼도 구분해야 하는데, 나중에 추가할 것이고 일단은 하나만 두었다.

 

멤버함수를 호출하여 값을 설정할 수 있다.

 

Renderer의 멤버함수로 트랜스폼 업데이트 위치를 옮겨주었다.

엔진에서 렌더러의 업데이트를 호출한 뒤에, 렌더러의 트랜스폼 업데이트를 호출해주도록 변경하였다.

 

각 렌더러의 Init함수에서 초기값을 세팅해주기만 하면 된다.

물론 업데이트에서 트랜스폼값을 변경해도 된다.

 

기존과 동일하게 실행이 잘 된다!

지난 번에 일단 블러까지는 적용이 잘 되는 것을 확인하였다.

이제, 본격적으로 블룸을 적용해보자.

 

먼저, 여러개의 렌더타겟을 사용해야하기 때문에 외부에서도 렌더타겟을 만들어서 사용할 수 있도록 함수를 추가해주었다.

RenderTarget EngineBase::CreateRenderTarget(UINT _Width, UINT _Height)
{
    RenderTarget NewRenderTarget;

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

    D3D11_TEXTURE2D_DESC txtDesc;
    ZeroMemory(&txtDesc, sizeof(txtDesc));
    txtDesc.Width = _Width;
    txtDesc.Height = _Height;
    txtDesc.MipLevels = txtDesc.ArraySize = 1;
    txtDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    txtDesc.SampleDesc.Count = 1;
    txtDesc.Usage = D3D11_USAGE_DEFAULT;
    txtDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS;
    txtDesc.MiscFlags = 0;
    txtDesc.CPUAccessFlags = 0;

    D3D11_RENDER_TARGET_VIEW_DESC viewDesc;
    viewDesc.Format = txtDesc.Format;
    viewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
    viewDesc.Texture2D.MipSlice = 0;

    HRESULT Result;
    Result = Device->CreateTexture2D(&txtDesc, NULL, Texture.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D() failed" << std::endl;
    }

    Result = Device->CreateRenderTargetView(Texture.Get(), &viewDesc, NewRenderTarget.RTV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateRenderTargetView() failed" << std::endl;
    }

    Result = Device->CreateShaderResourceView(Texture.Get(), nullptr, NewRenderTarget.SRV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateShaderResourceView() failed" << std::endl;
    }

    return NewRenderTarget;
}

해당 함수를 사용하면, 렌더타겟의 SRV와 RTV를 반환해주기 때문에 이를 활용해서 렌더링할 수 있게 된다.

 

DetectTarget = EngineBase::GetInstance().CreateRenderTarget(WindowSize.first, WindowSize.second);
BlurTarget = EngineBase::GetInstance().CreateRenderTarget(BlurData.Width, BlurData.Height);

먼저 렌더타겟을 생성해주었다.

float ClearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

//추출
SetTexture(DoubleBufferSRV);

EngineBase::GetInstance().GetContext()->ClearRenderTargetView(DetectTarget.RTV.Get(), ClearColor);
EngineBase::GetInstance().GetContext()->OMSetRenderTargets(1, DetectTarget.RTV.GetAddressOf(), DepthStencilView.Get());

PostProcessRenderer->SetPSShader(L"BrightDetectPixelShader.hlsl");
PostProcessRenderer->Render(_DeltaTime);

그리고 PostProcess클래스의 Render 부분에서 이렇게 DetectTarget 렌더타겟에 밝은 부분을 추출하도록 하였다.

#include "LightHeader.hlsli"

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

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

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
    
    float Luminance = 0.2126f * Color.r + 0.7152f * Color.g + 0.0722f * Color.b;
    
    if (Luminance > 0.8f)
    {
        return Color;
    }
    else
    {
        return (float4) 0.0f;
    }
}

 

추출하는 쉐이더 코드는 위와 같다. 픽셀의 휘도를 계산하여 일정 수치 이상이라면 해당 색상을 렌더타겟에 렌더링하도록 하였다.

 

//블러
SetTexture(DetectTarget.SRV);
EngineBase::GetInstance().GetContext()->RSSetViewports(1, &DownViewPort);

EngineBase::GetInstance().GetContext()->ClearRenderTargetView(BlurTarget.RTV.Get(), ClearColor);
EngineBase::GetInstance().GetContext()->OMSetRenderTargets(1, BlurTarget.RTV.GetAddressOf(), DepthStencilView.Get());

PostProcessRenderer->SetPSShader(L"BlurPixelShader.hlsl");
PostProcessRenderer->Render(_DeltaTime);

EngineBase::GetInstance().GetContext()->RSSetViewports(1, &UpViewPort);

다음은 블러이다.

추출된 렌더타겟을 BlurPixelShader에서 사용할 텍스쳐로 연결해주었다.

그리고, 해당 텍스쳐에 블러처리를 해주었다.

#include "LightHeader.hlsli"

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

cbuffer EBloomData : register(b0)
{
    int Width;
    int Height;
    int Padding1;
    int Padding2;
};

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

static float Gau[5][5] =
{
    { 1, 4, 6, 4, 1 },
    { 4, 16, 24, 16, 4 },
    { 6, 24, 36, 24, 6 },
    { 4, 16, 24, 16, 4 },
    { 1, 4, 6, 4, 1 }
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = (float4)0.0f;
    
    float WidthRatio = 1.0f / (float) Width;
    float HeightRatio = 1.0f / (float) Height;
    
    float2 StartTexCoord = float2(_Input.TexCoord.x - WidthRatio * 2, _Input.TexCoord.y - HeightRatio * 2);
    
    for (int i = 0; i < 5; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            Color += DiffuseTexture.Sample(Sampler, StartTexCoord.xy) * Gau[j][i];
            StartTexCoord.y += HeightRatio;
        }
        
        StartTexCoord.x += WidthRatio;
        StartTexCoord.y = _Input.TexCoord.y - HeightRatio * 2;
    }

    Color /= 256.0f;
    
    return Color;
}

블러는 앞의 게시물에서 본 것과 동일하게 가우시안 필터를 사용하여 구현하였다.

 

이 때, 포스트프로세스 클래스의 코드를 보면 뷰포트를 세팅해주고 있는데 이유는 블러를 할 때 다운샘플링을 했기 때문이다. 블러의 범위를 넓게 적용하기 위해 해상도를 낮춘채로 블러를 적용하였고, 화면에 제대로 렌더링되도록 하기 위해 뷰포트와 크기를 맞춰준 것이다.

////병합
SetTexture(BlurTarget.SRV, 0);

EngineBase::GetInstance().GetContext()->OMSetRenderTargets(1, DoubleBufferRTV.GetAddressOf(), DepthStencilView.Get());
EngineBase::GetInstance().GetContext()->OMSetBlendState(BlendState.Get(), NULL, 0xFFFFFFFF);

PostProcessRenderer->SetPSShader(L"BloomPixelShader.hlsl");
PostProcessRenderer->Render(_DeltaTime);

블러까지 적용한 뒤, 기존 이미지에 합성해주었다.

블러된 이미지가 더블버퍼의 원본 이미지와 제대로 합성될 수 있도록 BlendState도 만들어주었다.

void BloomPostProcess::CreateBlendState()
{
	D3D11_BLEND_DESC Desc = { 0, };

	Desc.AlphaToCoverageEnable = false;
	Desc.IndependentBlendEnable = false;
	Desc.RenderTarget[0].BlendEnable = true;
	Desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
	Desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
	Desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
	Desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;

	Desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_MAX;
	Desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
	Desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;

	EngineBase::GetInstance().GetDevice()->CreateBlendState(&Desc, BlendState.GetAddressOf());
}

 

결과는 위와 같다.

디테일은 많이 떨어지지만, 일단 블룸 자체는 잘 적용되는 듯 하다.

제대로 하려면, 다운샘플링하는 과정과 업샘플링하는 과정을 여러번 반복해야하지만, 아직은 이정도로 만족하자.

 

이제, 어느정도 쉐이더를 적용해보았으니 이제는 엔진 자체를 개선할 때가 된 것 같다.

엔진 내에 하드코딩된 부분도 너무 많고, 너무 안좋게 설계된 부분이 많다고 생각되기 때문에 천천히 엔진을 고쳐나가보자!

오브젝트에 대한 렌더링을 모두 마친 이후에, 전체 화면을 대상으로 추가적인 후처리를 적용하는 것을 포스트 프로세스라고 한다. 포스트 프로세스를 프로젝트에 구현해볼 생각이다.

 

먼저, 렌더링 구조를 살짝 바꿔주었다.

기존에는 오브젝트를 백버퍼에 바로 그리는 형식이었지만, 현재는 렌더타겟을 추가로 만들고 해당 렌더타겟에 오브젝트를 모두 그린 뒤에, 후처리를 적용하여 백버퍼에 복사하여 그리는 구조로 바꿔주었다.

BOOL EngineBase::CreateDoubleBuffer()
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;

    D3D11_TEXTURE2D_DESC txtDesc;
    ZeroMemory(&txtDesc, sizeof(txtDesc));
    txtDesc.Width = WindowWidth;
    txtDesc.Height = WindowHeight;
    txtDesc.MipLevels = txtDesc.ArraySize = 1;
    txtDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 이미지 처리용도
    txtDesc.SampleDesc.Count = 1;
    txtDesc.Usage = D3D11_USAGE_DEFAULT;
    txtDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS;
    txtDesc.MiscFlags = 0;
    txtDesc.CPUAccessFlags = 0;
    
    D3D11_RENDER_TARGET_VIEW_DESC viewDesc;
    viewDesc.Format = txtDesc.Format;
    viewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
    viewDesc.Texture2D.MipSlice = 0;

    HRESULT Result;
    Result = Device->CreateTexture2D(&txtDesc, NULL, Texture.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D() failed" << std::endl;
        return FALSE;
    }

    Result = Device->CreateRenderTargetView(Texture.Get(), &viewDesc, DoubleBufferRTV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateRenderTargetView() failed" << std::endl;
        return FALSE;
    }

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

    return TRUE;
}

 

이렇게, SRV와 RTV를 모두 생성해주었고, 이 doublebuffer에 1차적인 렌더링이 모두 진행될 것이다.

다음은 그렇게 그려진 렌더링 장면에 포스트 프로세스를 적용하기 위해, 화면 크기만한 사각형 메쉬를 만들어주었다.

 

#include "ScreenRenderer.h"

ScreenRenderer::ScreenRenderer()
{
}

ScreenRenderer::~ScreenRenderer()
{
}

void ScreenRenderer::Init()
{
    Renderer::Init();

    SetModelToSquare(1.0f);
    SetTransform();
}

void ScreenRenderer::Update(float _DeltaTime)
{

}

void ScreenRenderer::SetTransform()
{
    TransFormData.WorldMatrix = DirectX::SimpleMath::Matrix::CreateScale(1.0f) * DirectX::SimpleMath::Matrix::CreateRotationY(0.0f) *
        DirectX::SimpleMath::Matrix::CreateTranslation(DirectX::SimpleMath::Vector3(0.0f, 0.0f, 0.0f));

    TransFormData.WorldMatrix = TransFormData.WorldMatrix.Transpose();

    TransFormData.ViewMAtrix = EngineBase::GetInstance().ViewMat;
    TransFormData.ViewMAtrix = TransFormData.ViewMAtrix.Transpose();

    TransFormData.ProjMatrix =
        DirectX::XMMatrixOrthographicOffCenterLH(-1.0f, 1.0f, -1.0f, 1.0f,
            0.01f, 100.0f);

    TransFormData.ProjMatrix = TransFormData.ProjMatrix.Transpose();

    TransFormData.InvTranspose = TransFormData.WorldMatrix;
    TransFormData.InvTranspose.Translation({ 0.0f, 0.0f, 0.0f });
    TransFormData.InvTranspose = TransFormData.InvTranspose.Transpose().Invert();
}

 

다른 렌더러와 다를거 없어보이지만, 위의 렌더러는 직교투영을 적용하였다. 화면의 범위를 매쉬 크기에 맞춰주었기 떄문에 메쉬의 표면에 색을 입히면 화면 전체에 입혀지게 된다.

 

이 매쉬의 텍스쳐로 doublebuffer의 SRV를 사용할 것이다.

 

이 렌더러를 이용하여 포스트 프로세스를 적용할 것이기 때문에 포스트 프로세스 클래스도 만들어주었다.

 

#pragma once
#include "BaseHeader.h"

class PostProcess
{

public:

    PostProcess();
    ~PostProcess();

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

    void SetTexture(Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> _SRV);

    virtual void Init();
    virtual void Render(float _DeltaTime);

protected:
    std::shared_ptr<class ScreenRenderer> PostProcessRenderer;

private:
};

 

각 포스트 프로세스는 위에서 만든 ScreenRenderer를 반드시 보유하도록 하였다.

이를 상속받아 여러 종류의 포스트 프로세스를 생성할 것이다.

 

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

    Context->OMSetRenderTargets(1, DoubleBufferRTV.GetAddressOf(), DepthStencilView.Get());
    Context->OMSetDepthStencilState(DepthStencilState.Get(), 0);
    
    if (isWireFrame == false)
    {
        Context->RSSetState(SolidRasterizerState.Get());
    }
    else
    {
        Context->RSSetState(WireRasterizerState.Get());
    }

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

    Context->ClearRenderTargetView(BackBufferRTV.Get(), clearColor);
    Context->OMSetRenderTargets(1, BackBufferRTV.GetAddressOf(), DepthStencilView.Get());

    for (std::shared_ptr<PostProcess> PostProcess : PostProcesses)
    {
        PostProcess->SetTexture(DoubleBufferSRV);
        PostProcess->Render(_DeltaTime);
    }
}

 

엔진의 렌더링 마지막 부분에 PostProcess의 Render를 차례대로 호출해주는 것을 몰 수 있다.

포스트 프로세스 클래스는 생성할 때마다 Engine의 자료구조에 삽입되도록 하였다.

 

(현재는 포스트 프로세스를 적용하지 않으면 모니터에 렌더링이 안되는 괴상한 구조이다. 일단 기능을 구현하느라 이런 구조가 되었는데, 블룸을 적용한 뒤에 엔진 구조를 수정할 예정이라 일단은 그대로 두었다.)

 

BloomShader를 한 번 적용해볼것이다.

먼저, BloomShader는 세가지 단계로 이루어진다.

 

1. 밝은 부분을 추출한다.

2. 화면에 블러처리를 한다.

3. 기존의 화면에 추출된 밝은 색상을 더해준다.

 

그러므로 먼저 블러처리에 대한 포스트 프로세스가 잘 적용되는지부터 테스트해볼것이다.

#pragma once
#include "PostProcess.h"

struct EBloomData
{
	int Width = 1600;
	int Height = 900;
	int Padding1 = 0;
	int Padding2 = 0;
};

class BloomPostProcess : public PostProcess
{

public:

	BloomPostProcess();
	~BloomPostProcess();

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

	virtual void Init() override;
	virtual void Render(float _Deltatime) override;

protected:
	
private:
	EBloomData BloomData;
};

 

포스트프로세스 클래스를 상속받은 BloomPostProcess 클래스를 위와 같이 만들어주었따.

#include "BloomPostProcess.h"
#include "ScreenRenderer.h"

BloomPostProcess::BloomPostProcess()
{
}

BloomPostProcess::~BloomPostProcess()
{
}

void BloomPostProcess::Init()
{
	PostProcessRenderer = std::make_shared<ScreenRenderer>();
	PostProcessRenderer->Init();

	PostProcessRenderer->CreateConstantBuffer(EShaderType::PSShader, L"BloomPixelShader.hlsl", BloomData);

	PostProcessRenderer->SetVSShader(L"BloomVertexShader.hlsl");
	PostProcessRenderer->SetPSShader(L"BloomPixelShader.hlsl");

	PostProcessRenderer->SetSampler("LINEARWRAP");
}

void BloomPostProcess::Render(float _DeltaTime)
{
	PostProcessRenderer->Render(_DeltaTime);

	ID3D11ShaderResourceView* SRV = NULL;
	EngineBase::GetInstance().GetContext()->PSSetShaderResources(0, 1, &SRV);
}

 

초기화는 위와 같다. 렌더러를 먼저 만들어준뒤, 상수버퍼를 연결해주고 쉐이더 세팅을 해주었다.

그 이후, Render함수에선 렌더링을 진행한 뒤, SRV를 비워주도록 하였다.

 

#include "LightHeader.hlsli"

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

cbuffer EBloomData : register(b0)
{
    int Width;
    int Height;
    int Padding1;
    int Padding2;
};

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

static float Gau[5][5] =
{
    { 1, 4, 6, 4, 1 },
    { 4, 16, 24, 16, 4 },
    { 6, 24, 36, 24, 6 },
    { 4, 16, 24, 16, 4 },
    { 1, 4, 6, 4, 1 }
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = (float4)0.0f;
    
    float WidthRatio = 1.0f / (float) Width;
    float HeightRatio = 1.0f / (float) Height;
    
    float2 StartTexCoord = float2(_Input.TexCoord.x - WidthRatio * 2, _Input.TexCoord.y - HeightRatio * 2);
    
    for (int i = 0; i < 5; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            StartTexCoord.y += HeightRatio;
            Color += DiffuseTexture.Sample(Sampler, StartTexCoord.xy) * Gau[j][i];
        }
        
        StartTexCoord.x += WidthRatio;
        StartTexCoord.y = _Input.TexCoord.y;
    }

    Color /= 256.0f;
    
    return Color;
}

 

위는 블러를 적용하는 픽셀쉐이더 코드이다.

전형적인 가우시안 블러 코드를 그대로 구현하였다.

 

좌측이 기본이고 우측이 블러를 적용한 상태이다.

다행히 잘 작동되는 듯 하다.

다음엔 이걸 토대로 더 많은 렌더타겟을 사용하여 블룸효과를 완전히 구현할 것이다.

그 다음엔 엔진 구조를 다소 개편해보고자 한다.

 

기존에는 CubeMap은 무조건 dds파일로, 일반 텍스쳐는 다른 확장자로 로드하는 이상한 구조였다.

이번엔, 확장자 별로 로드방식을 다르게 하되, 큐브맵 텍스쳐와 일반적인 디퓨즈 텍스쳐를 구분하여 로드하도록 수정할 것이다.

void ResourceManager::LoadTexture(const std::string& _TextureName, ETextureType _Type)
{
	std::string Format = GetFormat(_TextureName);

	if (Format == "dds")
	{
		LoadDDSTexture(_TextureName, _Type);
	}
	else
	{
		LoadGeneralTexture(_TextureName, _Type);
	}
}

 

먼저 LoadTexture 함수를 위와 같이 수정하였다.

파일 이름에서 확장자를 탐색한 뒤, 확장자를 기준으로 dds라면 LoadDDSTexture함수를 호출하였고 아니라면 LoadGeneralTexture함수를 호출하였다.

 

이렇게 확장자를 구분한 이유는 stb 라이브러리는 dds파일을 지원하지 않는 것도 있고, dds파일의 경우 마이크로 소프트에서 다이렉트X에 맞게 만든 확장자이다 보니 DirectX 함수를 사용하는 것이 여러모로 좋을 것 같기 때문이기도 하다.

 

std::string ResourceManager::GetFormat(const std::string& _FileName)
{
    int Count = 0;

    for (int i = _FileName.size() - 1; i >= 0; i--)
    {
        if (_FileName[i] == '.')
        {
            break;
        }

        Count++;
    }

    std::string Format = _FileName.substr(_FileName.size() - Count, Count);

    return Format;
}

 

GetFormat함수 내부는 위와 같다.

void ResourceManager::LoadDDSTexture(const std::string& _TextureName, ETextureType _Type)
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

    std::wstring Path = L"../Texture/";
    std::wstring TextureName;
    TextureName.assign(_TextureName.begin(), _TextureName.end());

    Path += TextureName;

    UINT Flag = 0;

    if (_Type == ETextureType::CubeMap)
    {
        Flag = D3D11_RESOURCE_MISC_TEXTURECUBE;
    }

    HRESULT Result = DirectX::CreateDDSTextureFromFileEx(
        EngineBase::GetInstance().GetDevice().Get(), Path.c_str(), 0, D3D11_USAGE_DEFAULT,
        D3D11_BIND_SHADER_RESOURCE, 0,
        Flag,
        DirectX::DDS_LOADER_FLAGS(false), (ID3D11Resource**)Texture.GetAddressOf(),
        SRV.GetAddressOf(), nullptr);

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

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

    LoadedTextures.insert({ _TextureName, NewTextureData });
}

 

위 코드는 DDS 파일 로드 함수이다.

Cube텍스쳐의 경우, Flag를 설정하여 SRV를 Cube텍스쳐로 생성하도록 하였다.

void ResourceManager::LoadGeneralTexture(const std::string& _TextureName, ETextureType _Type)
{
    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;
    }

    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;

    if(_Type == ETextureType::CubeMap)
    {
        TexDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;
    }

    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;
    }

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

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

    stbi_image_free(LoadedImage);

    LoadedTextures.insert({ _TextureName, NewTextureData });

    return;
}

 

위 코드는 DDS를 제외한 나머지 텍스쳐를 로드하는 코드이다.

stb라이브러리를 사용해서 로드하고 있으며, DDS파일과 동일하게 ETextureType에 따라 Flag를 설정하여 Cube텍스쳐는 그에 맞게 로드하도록 하였다.

 

 

기존과 동일하게 잘 렌더링이 된다!

 

(나중에 알게된 사실이지만 큐브맵 텍스쳐는 DDS만 된다고 한다. png나 jpg등으로 사용하려면 직접 조립해야한다고...)

환경매핑은 큐브매핑을 활용해서 물체에 주변 사물이 반사되어 비치는 것을 구현하는 기술이다.

 

먼저, 반사를 테스트 하기 위한 구체 렌더러 하나를 추가해주었다.

 

우측에 검정색 동그라미가 하나 보인다. 아직 텍스쳐를 입히지 않아서 새까맣게 보인다.

일단 환경매핑 쉐이더를 작성해보자.

버텍스 쉐이더는 기존과 완전히 동일하다.

 

#include "LightHeader.hlsli"

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

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

TextureCube CubeMapTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float3 EyeDir = _Input.WorldPos - EyeWorld;
    EyeDir = normalize(EyeDir);
    
    _Input.WorldNormal = normalize(_Input.WorldNormal);
    
    float3 RefEyeDir = reflect(EyeDir, _Input.WorldNormal);
    
    float4 Color = CubeMapTexture.Sample(Sampler, RefEyeDir);
    return Color;
}

 

이렇게 픽셀쉐이더를 작성해주었다.

이존의 배경을 만들기 위한 큐브매핑과 다르게 Light 정보를 상수버퍼로 연결해주었다.

일단은 빛을 적용하지 않고, EyeWorld만 가져와서 사용할 것이다.

 

물체가 반사되어 눈으로 들어오는 과정을 역산하여, 눈으로부터 나가는 광선이 반사되었을 때 큐브맵의 어디에 충돌하는가를 계산하여 색을 정하는 것이다.

 

공식은 매우매우 간단하다. EyeDir(눈이 물체를 바라보는 방향 벡터)를 WorldNormal에 대해 반사시킨 벡터로 큐브맵을 샘플링하면 된다.

 

텍스쳐도 적용하고, 해당 쉐이더를 연결해주면 아래와 같은 결과가 나온다.

 

 

구체에 배경이 잘 반사되어 비치는 것을 확인할 수 있다.

이번엔, 커다란 박스를 하나 만들어서 그 안쪽에 큐브맵을 매핑할 것이다.

이를 이용하여, 배경을 만들어볼 생각이다.

 

void ResourceManager::LoadTexture(const std::string& _TextureName, ETextureType _Type)
{
    if (_Type == ETextureType::Diffuse)
    {
        LoadDiffuseTexture(_TextureName);
    }
    else if (_Type == ETextureType::CubeMap)
    {
        LoadCubeMapTexture(_TextureName);
    }
}

 

먼저 텍스쳐를 로드하는 곳에서 디퓨즈텍스쳐인지 큐브맵 텍스쳐인지 분류하여 로드하도록 하였다.

이유는 로드를 하면서 SRV까지 만들어주고 있는데, 큐브맵은 세팅을 조금 다르게 해야하기 때문이다.

void ResourceManager::LoadCubeMapTexture(const std::string& _TextureName)
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

    std::wstring Path = L"../Texture/";
    std::wstring TextureName;
    TextureName.assign(_TextureName.begin(), _TextureName.end());

    Path += TextureName;

    HRESULT Result = DirectX::CreateDDSTextureFromFileEx(
        EngineBase::GetInstance().GetDevice().Get(), Path.c_str(), 0, D3D11_USAGE_DEFAULT,
        D3D11_BIND_SHADER_RESOURCE, 0,
        D3D11_RESOURCE_MISC_TEXTURECUBE,
        DirectX::DDS_LOADER_FLAGS(false), (ID3D11Resource**)Texture.GetAddressOf(),
        SRV.GetAddressOf(), nullptr);

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

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

    LoadedTextures.insert({ _TextureName, NewTextureData });
}

 

큐브맵 텍스쳐를 로드하는 과정은 이와 같다.

큐브맵 텍스쳐를 로드하면서 DDS파일로 로드하다보니 코드가 위와 같아졌다.

 

그런데 구조가 조금 이상한 것 같다. 큐브맵 텍스쳐를 png로 사용할 수도 있고, 디퓨즈 텍스쳐를 dds로 사용할 수도 있기 때문에 조만간 텍스쳐 로드 과정을 수정할 계획이다.

 

 그리고 프로젝트 초반에 만들어두었던 박스형태의 매쉬를 사용할 것이다.

하지만, 기존의 매쉬는 상자 바깥부분에 텍스쳐를 매핑하고 있었지만, 큐브매핑을 하면서 안쪽에 텍스쳐를 매핑할 것이기 때문에 인덱스 버퍼를 뒤집어서 세팅해주었다.

 

그리고 큐브맵 전용 쉐이더도 하나 만들어주었다.

먼저, 빛과 같은 기능들은 모두 제거하고 트랜스폼만 상수버퍼로 전달해주었다.

버텍스 쉐이더는 기존과 동일한 코드로 만들었다. 완전히 동일한 코드이기 때문에 기존의 버텍스 쉐이더를 재활용해도 되지만, 나중에 큐브맵에만 적용할 무언가를 작성할 수도 있기 때문에 일단 분리하여 생성하였다.

 

픽셀 쉐이더는 아래와 같다.

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

TextureCube CubeMapTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = CubeMapTexture.Sample(Sampler, _Input.WorldPos.xyz);
    return Color;
}

 

정말 간단하다. 텍스쳐를 세팅할 때 큐브맵 텍스쳐로 설정하였다면, 샘플링하면서 알아서 큐브맵 매핑을 해준다.

 

실행해서 확인을 해보자.

 

카메라가 박스 범위를 벗어나지만 않으면 잘 작동하는 듯 하다.

이렇게 큐브맵도 세팅을 해보았다!

다음엔 환경매핑을 해볼 예정이다.

림 라이트란, 역광을 의미한다.

밝은 빛을 등지고 서있으면 물체의 테두리가 밝게 빛나는 현상을 우리는 현실에서도 자주 볼 수 있다.

 

컴퓨터 그래픽스 - Rim Light (역광) (tistory.com)

 

컴퓨터 그래픽스 - Rim Light (역광)

사진 출처 : What is Rim Lighting? - NYIP Photo Articles 위의 사진을 보면, 동물의 테두리 부분이 밝게 빛나는 것을 볼 수 있다.이처럼, 광원을 등지고 있을 때 물체의 테두리가 밝게 빛나는 것을 림라이트

yuu5666.tistory.com

 

위의 게시글에 림라이트에 관한 글을 정리해놓았다.

해당 게시글의 내용을 토대로 림라이트를 적용하였다.

 

struct ERimLight
{
    DirectX::SimpleMath::Vector3 RimColor = { 1.0f, 1.0f, 1.0f };
    float RimPower = 3.0f;
    
    float RimStrength = 3.0f;
    DirectX::SimpleMath::Vector3 Padding;
};

 

먼저, RimLight의 정보를 담은 구조체를 선언해주었다.

 

CreateConstantBuffer(EShaderType::PSShader, L"PixelTest.hlsl", RimLightData);

 

그리고 상수버퍼를 쉐이더에 연결해주었다.

 

float Rim = (1 - dot(EyeDir, _Input.WorldNormal));
Rim = pow(abs(Rim), RimLightPower);

float3 RimLight = Rim * RimLightStrength * RimLightColor;
Color.rgb += RimLight;

 

쉐이더에 블린 퐁 쉐이딩을 모두 적용한 뒤, 림라이트를 계산하여 물체의 색상에 더해주었다.

 

결과는 아래와 같다.

 

 

구현 자체가 까다로운 효과가 아니다 보니, 쉽게 적용할 수 있었다.

이제 외부에서 3D 모델링 파일을 로드하는 기능을 추가할 것이다.

 

일단, 리소스를 관리하는 클래스 ResourceManager 클래스를 만들어주었다.

기존엔 텍스쳐 로드를 EngineBase에서 하고있었는데, 텍스쳐를 로드하는 코드도 모두 ResourceManager로 옮겨주었다.

 

ResourceManager에선 외부에서 기능을 쉽게 사용할 수 있도록 변수와 함수를 모두 static으로 선언해주었다.

그리고, 3D 모델링 파일을 로드하는 기능을 추가하기 위해, Assimp 라이브러리를 프로젝트에 추가하였다.

void ResourceManager::Load(const std::string& _FileName, const std::string& _Path)
{
    if (LoadedMeshes.find(_FileName) != LoadedMeshes.end())
    {
        std::cout << _FileName << " is Already Loaded!" << std::endl;
        return;
    }

    Assimp::Importer Importer;

    std::string FullName = _Path + _FileName;
    const aiScene* Scene = Importer.ReadFile(FullName, aiProcess_Triangulate | aiProcess_ConvertToLeftHanded);

    DirectX::SimpleMath::Matrix Transform;

    ProcessNode(Scene->mRootNode, Scene, LoadedMeshes[_FileName], Transform);
}

 

로드 함수이다. 외부에서 파일 로드를 할 때 이 함수만 호출해주면 된다.

함수를 호출하게 되면, ResourceManager 내부에서 static으로 선언된 자료구조에 MeshData를 저장해준다.

그리고 렌더러에서 메시를 세팅할 때, 해당 자료구조 내부의 데이터를 참조하여 메시를 세팅하게 된다.

 

위의 코드를 보면 PrecessNode 함수가 있는데, 이 함수가 실질적으로 파일을 로드하는 코드이다.

 

void ResourceManager::ProcessNode(aiNode* _Node, const aiScene* _Scene, std::list<EMeshData>& _MeshList, DirectX::SimpleMath::Matrix _Transform)
{
    DirectX::SimpleMath::Matrix Mat;
    ai_real* Temp = &_Node->mTransformation.a1;
    
    float* MatTemp = &Mat._11;
    for (int i = 0; i < 16; i++)
    {
        MatTemp[i] = Temp[i];
    }

    Mat = Mat.Transpose() * _Transform;

    for (UINT i = 0; i < _Node->mNumMeshes; i++)
    {
        aiMesh* Mesh = _Scene->mMeshes[_Node->mMeshes[i]];
        ProcessMesh(Mesh, _Scene, _MeshList);

        EMeshData& NewMesh = _MeshList.back();
        for (auto& Vertex : NewMesh.Vertices)
        {
            Vertex.Position = DirectX::SimpleMath::Vector3::Transform(Vertex.Position, Mat);
        }
    }

    for (UINT i = 0; i < _Node->mNumChildren; i++)
    {
        ProcessNode(_Node->mChildren[i], _Scene, _MeshList, Mat);
    }
}

 

보면, PrecessNOde를 재귀적으로 호출하고 있다.

그 이유는, 하나의 3D 모델링 파일엔 매쉬가 1개만 있는 것이 아니라 부모 자식 관계로 여러개의 메쉬가 얽혀있다.

이를 재귀적으로 탐색하여 모든 메쉬를 로드하는 것이다.

 

중간에 있는ProcessMesh 함수가 메쉬의 정보를 읽어오는 함수이다.

 

void ResourceManager::ProcessMesh(aiMesh* _Mesh, const aiScene* _Scene, std::list<EMeshData>& _MeshList)
{
    EMeshData NewMesh;
    NewMesh.Indices;

    UINT IndicesCount = 0;
    for (UINT i = 0; i < _Mesh->mNumFaces; i++)
    {
        IndicesCount += _Mesh->mFaces[i].mNumIndices;
    }

    NewMesh.Vertices.reserve(_Mesh->mNumVertices);
    NewMesh.Indices.reserve(IndicesCount);

    for (UINT i = 0; i < _Mesh->mNumVertices; i++)
    {
        EVertex NewVertex;

        NewVertex.Position.x = _Mesh->mVertices[i].x;
        NewVertex.Position.y = _Mesh->mVertices[i].y;
        NewVertex.Position.z = _Mesh->mVertices[i].z;

        NewVertex.Normal.x = _Mesh->mNormals[i].x;
        NewVertex.Normal.y = _Mesh->mNormals[i].y;
        NewVertex.Normal.z = _Mesh->mNormals[i].z;

        NewVertex.Normal.Normalize();

        if (_Mesh->mTextureCoords[0])
        {
            NewVertex.TexCoord.x = (float)_Mesh->mTextureCoords[0][i].x;
            NewVertex.TexCoord.y = (float)_Mesh->mTextureCoords[0][i].y;
        }

        NewMesh.Vertices.push_back(NewVertex);
    }

    for (UINT i = 0; i < _Mesh->mNumFaces; i++) 
    {
        aiFace& Face = _Mesh->mFaces[i];
        
        for (UINT j = 0; j < Face.mNumIndices; j++)
        {
            NewMesh.Indices.push_back(Face.mIndices[j]);
        }
    }

    if (_Mesh->mMaterialIndex >= 0) 
    {
        aiMaterial* Material = _Scene->mMaterials[_Mesh->mMaterialIndex];

        if (Material->GetTextureCount(aiTextureType_DIFFUSE) > 0) 
        {
            aiString TexturePath;
            Material->GetTexture(aiTextureType_DIFFUSE, 0, &TexturePath);
        
            std::string TextureName = std::string(std::filesystem::path(TexturePath.C_Str()).filename().string());

            NewMesh.TextureName = TextureName;

            if (LoadedTextures.find(TextureName) == LoadedTextures.end())
            {
                LoadTexture(TextureName);
            }
        }
    }

    _MeshList.push_back(NewMesh);
}

 

버텍스, 노말, 텍스쳐좌표를 받아와서 저장하고 있으며, 해당 메쉬에 사용되는 텍스쳐의 이름도 받아주고 있다.

해당 텍스쳐가 로드되지 않은 상태라면, 로드까지 해주도록 하였다.

 

이렇게 메쉬 정보를 읽어오면, 렌더러에서 세팅을 해줄것이다.

 

SetModel("zeldaPosed001.fbx");

SetVSShader(L"VertexTest.hlsl");
SetPSShader(L"PixelTest.hlsl");
SetSampler("LINEARWRAP");

 

렌더러에서는 이렇게 모델, 쉐이더, 샘플러를 세팅해주도록 하였다.

 

void Renderer::SetModel(const std::string& _Name)
{
    const std::list<EMeshData>& MeshData = ResourceManager::GetLoadedMeshList(_Name);

    for (const EMeshData& Mesh : MeshData)
    {
        std::shared_ptr<RenderBase> NewRenderUnit = std::make_shared<RenderBase>();
        
        NewRenderUnit->SetMesh(Mesh);
        NewRenderUnit->CreateBuffer();

        RenderUnits.push_back(NewRenderUnit);
    }
}

 

모델 세팅하는 부분을 보면, 매쉬의 개수만큼 RenderUnit을 생성하여 각 유닛별로 메쉬를 1개씩 세팅해주었다.

서로 다른 매쉬이지만, 트랜스폼은 렌더러의 것을 모두 공유하기 때문에 여러개의 매쉬가 렌더러에 의해서 통제될 수 있도록 하였다.

 

void Renderer::SetVSShader(const std::wstring& _Shadername)
{
    for (std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->SetVSShader(_Shadername);
    }
}

void Renderer::SetPSShader(const std::wstring& _Shadername)
{
    for (std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->SetPSShader(_Shadername);
    }
}

void Renderer::SetSampler(const std::string& _Sampler)
{
    for (std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->SetSampler(_Sampler);
    }
}

 

렌더러에서 쉐이더, 샘플러를 세팅할 때에도 렌더유닛에 모두 세팅해주도록 하였다.

만약 특정 유닛에만 다른 쉐이더를 세팅하고 싶다면, 직접 호출하여 변경할 수도 있다.

 

잘 되는지 결과를 보도록 하자.

 

 

로드도 잘 되었고, 트랜스폼도 잘 작동한다.

이제 모델링 로드 기능을 구현하였으니, 이걸 토대로 더 많은 쉐이더 효과를 적용하며 공부해보고자 한다.

이제 3D모델을 파일로부터 읽어와서 렌더링하는 기능을 추가하려고 하였으나, 엔진에서 문제가 발견되었다

 

일단, 현재 구조는 하나의 렌더러가 하나의 매쉬를 렌더링하는 방식인데 실제 3D 모델은 대부분 여러개의 메쉬로 구성되어 있다. 각각 다른 쉐이더를 적용할 수도 있고, 서로 다른 버텍스버퍼, 인덱스 버퍼 등을 가지고 있어야 한다. 그렇기 때문에, 하나의 렌더러가 여러개의 메쉬 정보를 보유하도록 구조를 바꾸기로 하였다,.

 

struct EMeshData
{
	std::vector<struct EVertex> Vertices;
	std::vector<uint16_t> Indices;
	std::string TextureName = "";
};

 

일단, 버텍스와 인덱스 그리고 텍스쳐 이름을 하나의 구조체에 담아주었다.

파일로부터 데이터를 읽어올 때, 구조체를 사용하는 편이 아무래도 편할 것 같았다.

std::wstring VSShader = L"";
std::wstring PSShader = L"";

 

멤버변수에 세팅할 버텍스 쉐이더와 픽셀 쉐이더의 이름도 저장해주도록 하였다.

//std::list<EConstantBufferData> ConstantBuffers;

std::unordered_map<std::wstring, std::list<EConstantBufferData>> VSConstantBuffers;
std::unordered_map<std::wstring, std::list<EConstantBufferData>> PSConstantBuffers;

상수버퍼를 담고 있던 자료구조도 수정해주었다.

어떤 쉐이더에 대해 어떤 상수버퍼를 추가할 것인가를 설정하는 것이다.

 

기존에는 모든 상수버퍼를 모든 쉐이더에 세팅하도록 설정했었지만, 앞으로 다양한 쉐이더를 사용할 것에 대비하 이렇게 수정해주었다.

VertexShaderData VSData = EngineBase::GetInstance().GetVertexShaderData(VSShader);
Microsoft::WRL::ComPtr<ID3D11PixelShader> PS = EngineBase::GetInstance().GetPixelShaderData(PSShader);

 

렌더함수에서 쉐이더를 세팅하던 것을 기존엔 리터럴로 하고 있었는데, 이젠 설정된 쉐이더의 이름으로 하도록 하였다.

int Index = 0;
for (const EConstantBufferData& _Data : VSConstantBuffers[VSShader])
{
    EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    Index++;
}

Index = 0;
for (const EConstantBufferData& _Data : PSConstantBuffers[PSShader])
{
    EngineBase::GetInstance().GetContext()->PSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    Index++;
}

 

상수버퍼를 세팅하는 함수도 이처럼 VS와 PS를 따로 세팅해주도록 하였다.

 

상수버퍼가 지금은 RenderBase에 있지만, 나중엔 렌더러에서 가지고 있도록 할 것이다.

 

바꿀 구조는 대충 이렇다.

RenderBase는 메쉬별로 렌더링을 담당하는 객체로 바꿀 것이다. Renderer클래스는 메쉬의 수만큼 RenderBase를 보유할 것이며 트랜스폼과 같은 상수버퍼는 Renderer단위로 보유할 것이다.

 

본격적으로 구조를 수정해보도록 하겠다.

#pragma once
#include "RenderBase.h"

class Renderer
{

public:

    Renderer();
    ~Renderer();

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

protected:

private:
    std::list<RenderBase> RenderUnits;
};

 

먼저 이렇게 렌더러를 만들어 주었다.

멤버변수에는 RenderBase를 저장할 자료구조를 선언해주었고, 하나하나가 렌더링을 실행하는 단위이기 때문에 RenderUnits라는 이름을 붙혀주었다.

 

RenderBase에는 RenderSetting이라는 함수를 만들어주었다.

void RenderBase::RenderSetting()
{
    UINT Stride = sizeof(EVertex);
    UINT Offset = 0;

    VertexShaderData VSData = EngineBase::GetInstance().GetVertexShaderData(VSShader);
    Microsoft::WRL::ComPtr<ID3D11PixelShader> PS = EngineBase::GetInstance().GetPixelShaderData(PSShader);

    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()->PSSetShader(PS.Get(), 0, 0);

    //추후 텍스쳐 여러개 세팅할 수도 있다.
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV = EngineBase::GetInstance().GetTextureData(MeshData.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함수에 있던 코드 일부이다.

Context에 쉐이더, 버퍼 등을 세팅하는 코드이다.

 

void RenderBase::Render(float _DeltaTime)
{
    UINT IndexCount = (UINT)MeshData.Indices.size();
    EngineBase::GetInstance().GetContext()->DrawIndexed(IndexCount, 0, 0);
}

 

RenderBase의 Render에는 이렇게 드로우콜을 보내는 코드만 남겨두었다.

 

void Renderer::Render(float _DeltaTime)
{
    for (const std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->RenderSetting();
        SetConstantBuffer(_RenderUnit->GetVSShaderName(), _RenderUnit->GetPSShaderName());
        _RenderUnit->Render(_DeltaTime);
    }
}

void Renderer::SetConstantBuffer(const std::wstring& _VSShaderName, const std::wstring& _PSShaderName)
{
    int Index = 0;
    for (const EConstantBufferData& _Data : VSConstantBuffers[_VSShaderName])
    {
        EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
        Index++;
    }

    Index = 0;
    for (const EConstantBufferData& _Data : PSConstantBuffers[_PSShaderName])
    {
        EngineBase::GetInstance().GetContext()->PSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
        Index++;
    }
}

 

Renderer의 Render함수에선, 먼저 RenderUnit의 RenderSetting을 호출해준 뒤,Renderer가 가지고 있는 상수버퍼를 세팅해주었고, 이후 RenderUnit의 Render함수를 호출하여 드로우콜을 보내도록 하였다.

 

Update코드도 Renderer로 옮겨주었고, Init함수도 옮겨주었다. 그 외에 Name이나 Material같은 변수들도 모두 Renderer로 옮겨주었다.

 

std::list<std::shared_ptr<class Renderer>> Renderers;

 

EngineBase에서도 RenderBase가 아니라 Renderer단위로 렌더링하도록 자료구조를 수정해주었다.

 

기존의 BoxRenderer가 Renderer를 상속받도록 하였고, 임시로 RenderUnit을 하나 추가하여 렌더링을 테스트해보았다.

 

 

 

일단은 실행이 잘된다.

아마 놓친 부분이 꽤 있을 것 같은데, 3D 모델 로딩을 구현하고 테스트하면서 천천히 찾아가면서 고쳐가면 될 듯 하다.

조명에 디테일을 더 넣기 위해 렌더러별로 Material을 보유하게 하였고 해당 Material을 상수버퍼로 전달하여 쉐이더에서 사용하도록 하였다.

struct Material
{
	DirectX::SimpleMath::Vector3 Ambient = { 0.1f, 0.1f, 0.1f };
	float Shininess = 50.0f;

	DirectX::SimpleMath::Vector3 Diffuse = { 0.5f, 0.5f, 0.5f };
	float Padding1;

	DirectX::SimpleMath::Vector3 Specular = { 1.0f, 1.0f, 1.0f };
	float Padding2;
};

 

Material구조체는 위와 같다.

오브젝트 별로 Ambient, Diffuse, Specular를 보유하고 있고, 이는 계산된 빛의 최종 색상에 영향을 주는 변수들이다.

 

상자의 왼쪽에는 빨간색 포인트라이트를 비추고, 위쪽엔 파란색 스팟라이트를 비추고, 정면엔 하얀색 디렉셔널 라이트를 비출 것이다.

 

{
    float3 EyeDir = EyeWorld - _Input.WorldPos;

    float3 LightStrength = Lights[0].Strength * CalDirectionalLight(Lights[0], _Input.WorldNormal);
    LightSum += BlinnPhong(Lights[0], MaterialData, LightStrength, _Input.WorldNormal, EyeDir);
}

{
    float3 EyeDir = EyeWorld - _Input.WorldPos;

    float3 LightStrength = Lights[1].Strength * CalPointLight(Lights[1], _Input.WorldPos, _Input.WorldNormal);
    LightSum += BlinnPhong(Lights[1], MaterialData, LightStrength, _Input.WorldNormal, EyeDir);
}

{
    float3 EyeDir = EyeWorld - _Input.WorldPos;

    float3 LightStrength = Lights[2].Strength * CalSpotLight(Lights[2], _Input.WorldPos, _Input.WorldNormal);
    LightSum += BlinnPhong(Lights[2], MaterialData, LightStrength, _Input.WorldNormal, EyeDir);
}

 

픽셀 쉐이더에선 이렇게 3개의 빛을 계산하도록 하였다.

 

WorldLight.Lights[0].Direction = { 0.0f, 0.0f, 1.0f };

WorldLight.Lights[1].Position = { -2.0f, 0.0f, 1.0f };
WorldLight.Lights[1].Direction = { 0.0f, 1.0f, 0.0f };
WorldLight.Lights[1].Strength = { 1.0f, 0.0f, 0.0f };
WorldLight.Lights[1].FallOffStart = 1.0f;
WorldLight.Lights[1].FallOffEnd = 5.0f;

WorldLight.Lights[2].Position = { 0.0f, 2.0f, 1.0f };
WorldLight.Lights[2].Direction = { 0.0f, -1.0f, 0.0f };
WorldLight.Lights[2].Strength = { 0.0f, 1.0f, 0.0f };
WorldLight.Lights[2].FallOffStart = 1.0f;
WorldLight.Lights[2].FallOffEnd = 2.0f;
WorldLight.Lights[2].SpotPower = 10.0f;

 

C++ 코드에선 빛을 이렇게 세팅해주었다.

 

float3 BlinnPhong(Light _Light, Material _Mat, float3 _LStrength, float3 _Normal, float3 _EyeDir)
{
    float3 LightStrength = _LStrength;
    float3 Specular = _Mat.Specular * CalSpecular_BlinnPhong(_Light, _Mat, _Normal, _EyeDir);
    
    float3 ReturnValue = _Mat.Ambient + (_Mat.Diffuse + Specular) * LightStrength;
    
    return ReturnValue;
}

 

BlinnPhong함수 내부 코드이다.

외부에서 DiffuseLight를 구한 뒤 해당 값을 _LStrength로 보내주면 BlinnPhong에서 최종 빛을 결정하여 반환해준다.

float3 CalSpecular_BlinnPhong(Light _Light, Material _Mat, float3 _EyeDir, float3 _Normal)
{
    float3 LightVec = normalize(-_Light.Direction);
    float3 EyeDir = normalize(EyeDir);
    
    float3 HalfWay = normalize(_EyeDir + LightVec);
    float3 Normal = normalize(_Normal);
    
    float HalfDotN = dot(HalfWay, Normal);
    
    float3 Specular = max(HalfDotN, 0.0f);
    
    Specular = pow(Specular, _Mat.Shininess);
    Specular *= _Mat.Specular;
    
    return Specular;
}

 

위 코드는 Specular을 계산해주는 함수이다. BlinnPhong 공식을 사용하였다.

EyeVector와 Light벡터의 중간벡터를 구한 뒤, 해당 벡터를 Normal과 내적하여 Specular를 구하였다.

 

이제 적용된 결과물을 확인해보자.

먼저 정면에 적용된 디렉셔널 라이트이다.

시야를 바꾸면 SPecular가 사라지는 것도 볼 수 있고, 우측이 정면보다 어두운 것도 확인할 수 있다.

(좌측의 빨간 빛은 포인트 라이트이다. 아래 영상에서 확인하자.)

 

다음은 좌측에 적용된 포인트라이트이다.

 

 

광원의 위치를 옮길수록 빛이 적용되는 위치가 변경되는 것을 확인할 수 있다.

 

다음은 스포트라이트이다.

 

 

SpotFactor를 키울수록 중심에 빛이 더 집중되고, 줄일수록 빛이 확산되는 것을 확인할 수 있다.

이로서 간단한 조명효과는 적용 끝이다.

+ Recent posts