[목차]

 

- 리플렉션 프로브를 위한 클래스 생성

- 결과 확인

 

 

[리플렉션 프로브를 위한 클래스 생성]

 

스카이박스에서 큐브맵을 로드하여 활용해봤습니다. 큐브맵을 여기서만 쓰는게 아니고, 리플렉션 프로브를 위해 사용할 수도 있습니다. 리플렉션 프로브는 3D 실시간 렌더링에서 간접 반사를 구현하기 위한 기술로, 오브젝트 표면에 환경이 반사되는 것처럼 보이게 만드는 데 사용됩니다.

 

실시간 반사는 성능 부담이 크기 때문에, 리플렉션 프로브를 활용하여 특정 위치의 반사 환경을 미리 저장해두고, 주변 오브젝트에 이 큐브맵을 적용하여 효율적으로 간접 반사를 구현할 수 있습니다.

 

바로 시작해보겠습니다. 클래스를 하나 만들어줍니다.

#pragma once
#include "Ext_Component.h"

class Ext_ReflectionComponent : public Ext_Component
{
public:
	// constrcuter destructer
	Ext_ReflectionComponent() {}
	~Ext_ReflectionComponent() {}

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

	void ReflectionInitialize(std::shared_ptr<class Ext_Actor> _Owner, std::string_view _CaptureTextureName, const float4& _Scale = float4(128, 128));
	std::shared_ptr<class Ext_DirectXTexture> GetReflectionCubeTexture() { return CubeTexture; }

protected:
	
private:
	std::shared_ptr<class Ext_DirectXTexture> CubeTexture = nullptr;
	
};

 

ReflectionInitialize() 함수가 조금 중요한데, 다음과 같습니다.

void Ext_ReflectionComponent::ReflectionInitialize(std::shared_ptr<class Ext_Actor> _Owner, std::string_view _CaptureTextureName, const float4& _Scale/* = float4(128, 128)*/)
{
	Base_Directory Dir;
	Dir.MakePath("../Resource/FX/ReflectionTexture");
	std::string Path = Dir.GetPath();

	std::shared_ptr<Ext_DirectXRenderTarget> CaptureTarget = nullptr;

	if (nullptr == Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Forword.png")))
	{
		if (0 == _Scale.x || 0 == _Scale.y)
		{
			MsgAssert("ReflectionProbe : 캡쳐할 텍스쳐의 크기가 0 입니다");
			return;
		}

		if (nullptr == CaptureTarget)
		{
			CaptureTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R16G16B16A16_UNORM, _Scale, float4::ZERONULL);
		}

		float4 CenterPos = _Owner->GetTransform()->GetWorldPosition();
		float4 CenterRot = float4::ZERO;

		auto Scene = _Owner->GetOwnerScene().lock();
		if (Scene == nullptr)
		{
			MsgAssert("Scene이 유효하지 않습니다.");
			return;
		}

		// Forward
		Scene->GetMainCamera()->CaptureCubemap(CenterPos, CenterRot, float4(700, 700));
		CaptureTarget->RenderTargetClear();
		CaptureTarget->Merge(Scene->GetMainCamera()->GetCameraRenderTarget());
		Ext_ScreenShoot::RenderTargetShoot_DxTex(CaptureTarget, Path, _CaptureTextureName.data() + std::string("_Forword.png"));

		// Back
		Scene->GetMainCamera()->CaptureCubemap(CenterPos, CenterRot + float4(0, 180, 0), float4(512, 512));
		CaptureTarget->RenderTargetClear();
		CaptureTarget->Merge(Scene->GetMainCamera()->GetCameraRenderTarget());
		Ext_ScreenShoot::RenderTargetShoot_DxTex(CaptureTarget, Path, _CaptureTextureName.data() + std::string("_Back.png"));

		// Right
		Scene->GetMainCamera()->CaptureCubemap(CenterPos, CenterRot + float4(0, 90, 0), float4(512, 512));
		CaptureTarget->RenderTargetClear();
		CaptureTarget->Merge(Scene->GetMainCamera()->GetCameraRenderTarget());
		Ext_ScreenShoot::RenderTargetShoot_DxTex(CaptureTarget, Path, _CaptureTextureName.data() + std::string("_Right.png"));

		// Left
		Scene->GetMainCamera()->CaptureCubemap(CenterPos, CenterRot + float4(0, -90, 0), float4(512, 512));
		CaptureTarget->RenderTargetClear();
		CaptureTarget->Merge(Scene->GetMainCamera()->GetCameraRenderTarget());
		Ext_ScreenShoot::RenderTargetShoot_DxTex(CaptureTarget, Path, _CaptureTextureName.data() + std::string("_Left.png"));

		// Top
		Scene->GetMainCamera()->CaptureCubemap(CenterPos, CenterRot + float4(-90, 0, 0), float4(512, 512));
		CaptureTarget->RenderTargetClear();
		CaptureTarget->Merge(Scene->GetMainCamera()->GetCameraRenderTarget());
		Ext_ScreenShoot::RenderTargetShoot_DxTex(CaptureTarget, Path, _CaptureTextureName.data() + std::string("_Top.png"));

		// Bottom
		Scene->GetMainCamera()->CaptureCubemap(CenterPos, CenterRot + float4(90, 0, 0), float4(512, 512));
		CaptureTarget->RenderTargetClear();
		CaptureTarget->Merge(Scene->GetMainCamera()->GetCameraRenderTarget());
		Ext_ScreenShoot::RenderTargetShoot_DxTex(CaptureTarget, Path, _CaptureTextureName.data() + std::string("_Bottom.png"));
		CaptureTarget->RenderTargetClear();

		Ext_DirectXTexture::LoadTexture(Path + "\\" + _CaptureTextureName.data() + "_Forword.png");
		Ext_DirectXTexture::LoadTexture(Path + "\\" + _CaptureTextureName.data() + "_Back.png");
		Ext_DirectXTexture::LoadTexture(Path + "\\" + _CaptureTextureName.data() + "_Right.png");
		Ext_DirectXTexture::LoadTexture(Path + "\\" + _CaptureTextureName.data() + "_Left.png");
		Ext_DirectXTexture::LoadTexture(Path + "\\" + _CaptureTextureName.data() + "_Top.png");
		Ext_DirectXTexture::LoadTexture(Path + "\\" + _CaptureTextureName.data() + "_Bottom.png");
	}

	CubeTexture = Ext_DirectXTexture::Find(_CaptureTextureName);

	if (nullptr == CubeTexture)
	{
		std::vector<std::shared_ptr<Ext_DirectXTexture>> CubeTextures;
		CubeTextures.reserve(6);

		CubeTextures.push_back(Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Right.png")));
		CubeTextures.push_back(Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Left.png")));
		CubeTextures.push_back(Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Top.png")));
		CubeTextures.push_back(Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Bottom.png")));
		CubeTextures.push_back(Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Forword.png")));
		CubeTextures.push_back(Ext_DirectXTexture::Find(_CaptureTextureName.data() + std::string("_Back.png")));

		CubeTexture = Ext_DirectXTexture::LoadCubeMap(_CaptureTextureName, CubeTextures);
	}
}

 

미리 준비된 Texture가 없으면 스크린샷을 찍어서 Texture들을 6개 만들고, 그걸로 바로 Load를 실시해서 CubeMap을 만들어줍니다.

 

CaptureCubeMap() 함수는 그냥 해당 방향의 장면을 RenderTarget에 그리는 함수입니다. 이후 이 값을 가지고 Ext_ScreenShoot::RenderTargetShoot_DxTex()를 실시합니다.

HRESULT Ext_ScreenShoot::RenderTargetShoot_DxTex(std::shared_ptr<Ext_DirectXRenderTarget> _CaptureTarget, std::string_view _Path, std::string_view _TextureName)
{
    if (nullptr == _CaptureTarget)
    {
        return S_FALSE;
    }

    ID3D11Texture2D* Resource = _CaptureTarget->GetTexture(0)->GetTexture2D();

    if (nullptr != Resource)
    {
        wchar_t PrevPath[255];
        GetCurrentDirectoryW(255, PrevPath); // 기존 경로 저장
        SetCurrentDirectoryW(Base_String::AnsiToUniCode(_Path).data()); // 경로 변경

        DirectX::ScratchImage image;
        HRESULT result = CaptureTexture(Ext_DirectXDevice::GetDevice(), Ext_DirectXDevice::GetContext(), Resource, image);
        if (SUCCEEDED(result))
        {
            result = DirectX::SaveToWICFile(image.GetImages(), image.GetImageCount(), DirectX::WIC_FLAGS_NONE, GUID_ContainerFormatPng, Base_String::AnsiToUniCode(_TextureName).data());
        }
        
        SetCurrentDirectoryW(PrevPath); // 이전 경로 되돌리기
    }

    return S_FALSE;
}

 

해당 함수는 마이크로소프트에서 만든 Capture() 함수를 프레임워크에 맞게 조금 변형한 함수인데, 이 함수를 쓰기 위해서는 DirectXTK 라이브러리가 필요합니다. 라이브러리 적용은 기존에 제가 설명드렸던 방식대로 동일하게 진행하면 됩니다.

 

위 과정을 진행하면 스크린샷이 생성됩니다.

 

이제 이 Texture들로 스카이박스때와 마찬가지로 Texture를 Load 해줍니다.

 

이 기능을 사용하려면 Scene에서 Actor를 Create()하고, 위치를 정해준 다음 SetReflection() 함수를 호출해주면 됩니다.

// 리플렉션
std::shared_ptr<ReflectionActor> ReflectionActor1 = CreateActor<ReflectionActor>("ReflectionActor1");
ReflectionActor1->GetTransform()->SetLocalPosition({ 0.f, 100.f, -100.f});
ReflectionActor1->SetReflection();

 

Actor 내부에 임의로 구현한 코드인데, 위에 기능들을 만들어놨으니 이대로 사용하면 됩니다.

void ReflectionActor::SetReflection()
{
	Reflection = std::make_shared<Ext_ReflectionComponent>();
	static int n = 0;
	MeshComp->SetSampler("CubeMapSampler", "CubeMapSampler");
	Reflection->ReflectionInitialize(GetSharedFromThis<Ext_Actor>(), "TestReflection" + std::to_string(n++), float4(512, 512));
	MeshComp->SetTexture(Reflection->GetReflectionCubeTexture(), "ReflectionTexture");
}

 

리플렉션용 CubeMap Pixel Shader는 기존 Path와 분리해놨습니다.

#include "LightData.fx"

Texture2D BaseColorTex : register(t0); // 텍스처 자원
TextureCube ReflectionTexture : register(t1);
SamplerState Sampler : register(s0); // 샘플러
SamplerState CubeMapSampler : register(s1); // 샘플러

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 TexCoord : TEXCOORD;
    float3 WorldPosition : POSITION0;
    float3 WorldNormal : NORMAL;
    float3 WorldTangent : TANGENT;
    float3 WorldBinormal : BINORMAL;
    float4 CameraWorldPosition : POSITION1;
};

struct PSOutPut
{
    float4 MeshTarget : SV_TARGET0;
    float4 PositionTarget : SV_TARGET1; // World Position Target
    float4 NormalTarget : SV_TARGET2; // World Normal Target
};

 // 각 벡터에 normalize를 해주는 이유는, 명시적으로 normalize된 벡터를 넣어줬다 하더라도 
 // 임의의 값이 어떻게 들어올지 모르기 때문에 그냥 해주는것(안정성을 위한 처리라고 보면 됨)
PSOutPut GraphicsCUBE_PS(PSInput _Input) : SV_TARGET
{
    PSOutPut Output = (PSOutPut) 0;

    float3 CameraPos = _Input.CameraWorldPosition;
    float3 ViewDir = normalize(CameraPos - _Input.WorldPosition);
    float3 Normal = normalize(_Input.WorldNormal);
    
    float3 ReflectDir = normalize(2.0f * Normal * dot(ViewDir, Normal) - ViewDir);
    float4 ReflectionColor = ReflectionTexture.Sample(CubeMapSampler, ReflectDir);
    
    float metallic = 1.0f;

    Output.MeshTarget = BaseColorTex.Sample(Sampler, _Input.TexCoord);
    Output.MeshTarget += float4(lerp(float3(0, 0, 0), ReflectionColor.rgb * 0.5f, metallic), 0.0f);

    Output.PositionTarget = float4(_Input.WorldPosition, 1.0f);
    Output.NormalTarget = float4(_Input.WorldNormal, 1.0f);

    return Output;
}

 

이렇게 해주면 내부에서 반사값이 적용된 뒤, 물체에 CubeMap 씌워집니다. 공식은 언리얼의 리플렉션 프로브와 동일합니다.

 

 

[결과 확인]

 

구체와 네모(Rect) 물체에 대해 적용하여 확인해봤습니다.

 

[목차]

 

- 큐브맵 제작을 위한 텍스쳐 준비

- SkyBox 세팅

- 결과 확인

 

 

[큐브맵 제작을 위한 텍스쳐 준비]

 

DirectX에서 지원하는 기능으로 큐브맵 SRV를 만들 수 있습니다. 아래는 Unity 가이드에서 보여주는 CubeMap 형태입니다.

 

DirectX에서 CubeMap을 로드할 때, Texture들을  [ +X, -X, +Y, -Y, +Z, -Z ] 순서로 넣어줘야 원하는 형태로 생성됩니다. 

 

바로 한 번 만들어 보겠습니다. 먼저 이미지를 준비합니다. 

 

CubeMap을 만들기 위해서는 위의 텍스쳐들이 Load된 상태여야 합니다. 따라서 다음과 같이 Load를 진행해줍니다.

Base_Directory Dir;
Dir.MakePath("../Resource/FX/ReflectionTexture/SkyBox");
std::vector<std::string> Paths = Dir.GetAllFile({ "png" });
		
std::vector<std::shared_ptr<Ext_DirectXTexture>> Texs;

for (const std::string& FilePath : Paths)
{
	Dir.SetPath(FilePath.c_str());
	std::string ExtensionName = Dir.GetExtension();
	std::string FileName = Dir.GetFileName();
	Texs.push_back(Ext_DirectXTexture::LoadTexture(FilePath.c_str()));
}

Ext_DirectXTexture::LoadCubeMap("SkyBox", Texs);

 

이러면 0번 사진부터 순서대로 로드하고 Texs 컨테이너에 담깁니다. 이후 LoadCubeMap() 함수로 전달해줍니다. 

// 큐브맵 텍스쳐 만들기, 이름 지정해주고 저장 및 로드
static std::shared_ptr<Ext_DirectXTexture> LoadCubeMap(std::string_view _Name, std::vector<std::shared_ptr<Ext_DirectXTexture>>& _Textures)
{
	std::shared_ptr<Ext_DirectXTexture> NewTexture = Ext_ResourceManager::CreateNameResource(_Name);
	NewTexture->CubeMapLoad(_Textures);

	return NewTexture;
}

 

여기서 기존과 같이 리소스 매니저 컨테이너에 등록되고, Load가 실행됩니다.

void Ext_DirectXTexture::CubeMapLoad(std::vector<std::shared_ptr<Ext_DirectXTexture>>& _Textures)
{
	Texture2DInfo = { 0 };

	if (_Textures.empty() || !_Textures[0])
	{
		MsgAssert("CubeMap 텍스처 리스트가 비었거나 첫 번째 텍스처가 null입니다.");
		return;
	}

	UINT Size = _Textures[0]->GetScale().ix();

	Texture2DInfo.ArraySize = 6;
	Texture2DInfo.Width = Size;
	Texture2DInfo.Height = Size;
	Texture2DInfo.Format = _Textures[0]->Texture2DInfo.Format;
	Texture2DInfo.SampleDesc.Count = 1;
	Texture2DInfo.SampleDesc.Quality = 0;
	Texture2DInfo.MipLevels = 1;
	Texture2DInfo.Usage = D3D11_USAGE_DEFAULT;
	Texture2DInfo.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;
	Texture2DInfo.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_RENDER_TARGET | D3D11_BIND_FLAG::D3D11_BIND_SHADER_RESOURCE;

	D3D11_RENDER_TARGET_VIEW_DESC DescRTV;
	DescRTV.Format = Texture2DInfo.Format;
	DescRTV.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
	DescRTV.Texture2DArray.ArraySize = 6;
	DescRTV.Texture2DArray.FirstArraySlice = 0;
	DescRTV.Texture2DArray.MipSlice = 0;

	D3D11_SHADER_RESOURCE_VIEW_DESC DescSRV;

	DescSRV.Format = Texture2DInfo.Format;
	DescSRV.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
	DescSRV.TextureCube.MipLevels = 1;
	DescSRV.TextureCube.MostDetailedMip = 0;

	//Array to fill which we will use to point D3D at our loaded CPU images.
	D3D11_SUBRESOURCE_DATA pData[6];
	for (int cubeMapFaceIndex = 0; cubeMapFaceIndex < 6; cubeMapFaceIndex++)
	{
		DirectX::ScratchImage& CurImage = _Textures[cubeMapFaceIndex]->Image;

		pData[cubeMapFaceIndex].pSysMem = CurImage.GetImages()->pixels;
		pData[cubeMapFaceIndex].SysMemPitch = (UINT)CurImage.GetImages()->rowPitch;
		pData[cubeMapFaceIndex].SysMemSlicePitch = 0;
	}

	//Create the Texture Resource
	HRESULT TextureResult = Ext_DirectXDevice::GetDevice()->CreateTexture2D(&Texture2DInfo, &pData[0], &Texture2D);
	if (S_OK != TextureResult)
	{
		MsgAssert("큐브 텍스쳐 생성에 실패했습니다.");
		return;
	}

	COMPTR<ID3D11RenderTargetView> NewRTV = nullptr;
	HRESULT RTVResult = Ext_DirectXDevice::GetDevice()->CreateRenderTargetView(Texture2D, &DescRTV, &NewRTV);
	RTVs.push_back(NewRTV);

	if (S_OK != RTVResult)
	{
		MsgAssert("큐브 랜더타겟 뷰 생성에 실패했습니다.");
		return;
	}

	HRESULT SRVResult = Ext_DirectXDevice::GetDevice()->CreateShaderResourceView(Texture2D, &DescSRV, &SRV);
	if (S_OK != SRVResult)
	{
		MsgAssert("큐브 쉐이더 리소스 뷰 생성에 실패했습니다.");
		return;
	}
}

 

기존의 RTV, SRV를 만들던 것과 차이가 있다면, MiscFlags 값을 D3D11_RESOURCE_MISC_TEXTURECUBE로 지정해주는 것과 아래와 같이 D3D11_SUBRESOURCE_DATA에 값을 6번 넣어주고, 그걸로 Textrue2D를 Create()하는 것 정도 아닐까 싶습니다.

//Create the Texture Resource
HRESULT TextureResult = Ext_DirectXDevice::GetDevice()->CreateTexture2D(&Texture2DInfo, &pData[0], &Texture2D);
if (S_OK != TextureResult)
{
	MsgAssert("큐브 텍스쳐 생성에 실패했습니다.");
	return;
}

 

여기까지 했다면 CubeMap Texture의 SRV가 생성됩니다.

 

 

[SkyBox 세팅]

 

SkyBox는 기존에 렌더러들을 출력해주는 RenderTarget에 그리지 않고, 따로 RenderTarget을 만들어 거기에 먼저 그려둔 뒤, 나중에 최종 RenderTarget(CameraRenderTarget)에 Blending 했습니다. 먼저 Material Setting입니다.

std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("SkyBox");

NewRenderingPipeline->SetVertexShader("SkyBox_VS");
NewRenderingPipeline->SetPixelShader("SkyBox_PS");
NewRenderingPipeline->SetBlendState("BaseBlend");
NewRenderingPipeline->SetDepthState("SkyDepth");
NewRenderingPipeline->SetRasterizer("SkyRasterizer");

 

Material Setting중 새로 설정한게 Depth State 입니다. SkyRasterizer는 그냥 NONE Culling 설정 Rasterizer입니다.

// SkyDepth
D3D11_DEPTH_STENCIL_DESC DepthStencilInfo = { 0, };

DepthStencilInfo.DepthEnable = true;
DepthStencilInfo.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
DepthStencilInfo.DepthFunc = D3D11_COMPARISON_LESS_EQUAL;
DepthStencilInfo.StencilEnable = false;

Ext_DirectXDepth::CreateDepthStencilState("SkyDepth", DepthStencilInfo);

 

이렇게 하면 깊이 테스트는 하지만, Depth Buffer에는 기록되지 않아 다른 오브젝트들이 스카이박스를 덮어씌울 수 있도록 해줍니다.

 

Camera 생성 전 SkyBoxUnit과 SkyBoxRenderTarget를 만들어줬습니다.

// 스카이박스용
SkyBoxRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);
SkyBoxTransform = std::make_shared<Ext_Transform>();
SkyBoxUnit.MeshComponentUnitInitialize("FullBox", "SkyBox");
std::shared_ptr<Ext_DirectXTexture> Tex = Ext_DirectXTexture::Find("SkyBox");
SkyBoxUnit.GetBufferSetter().SetTexture(Tex, "CubeMapTex");

 

Texture는 앞서 로드해둔 CubeMap Texture를 바인딩해줍니다. 새롭게 FullBox가 추가됐는데, FullRect와 동일하게 -1.0 ~ 1.0 범위를 갖는 Box라고 보시면 됩니다.

 

이제 SkyBox를 사용하는 Scene의 경우에 있어서, Rendering() 함수 실행 초기에 바로 Rendering을 먼저 수행합니다.

// 카메라의 MeshComponents들에 대한 업데이트 및 렌더링 파이프라인 리소스 정렬
void Ext_Camera::Rendering(float _Deltatime)
{
	if (true == bIsSkybox)
	{
		SkyBoxRendering();
	}
    //...
}

// 스카이박스 렌더링
void Ext_Camera::SkyBoxRendering()
{
	SkyBoxRenderTarget->RenderTargetClear();
	SkyBoxRenderTarget->RenderTargetSetting();

	float4 CamPos = GetTransform()->GetWorldPosition();
	SkyBoxTransform->SetLocalPosition(CamPos);
	SkyBoxTransform->SetCameraMatrix(GetTransform()->GetWorldPosition(), ViewMatrix, ProjectionMatrix);
	SkyBoxUnit.GetBufferSetter().SetConstantBufferLink("TransformData", *SkyBoxTransform->GetTransformData());
	SkyBoxUnit.Rendering(0.0f);
	SkyBoxUnit.GetBufferSetter().AllTextureResourceReset();
}

 

스카이 박스가 렌더링되는 Unit은 Camera를 계속 쫓아가는 형태로 만들어주고, Transform을 cbuffer로 바인딩해줍니다.

 

아래는 Shader 입니다.

////////////////////// SkyBos_VS
#include "Transform.fx"

struct VSInput
{
    float4 Position : POSITION;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float3 TexCoord : TEXCOORD0;
};

VSOutput SkyBox_VS(VSInput _Input)
{
    VSOutput Output;
    float4x4 ViewMat = ViewMatrix;
    ViewMat._41 = 0;
    ViewMat._42 = 0;
    ViewMat._43 = 0;
    
    float4 VPosition = mul(_Input.Position, ViewMat);
    Output.Position = mul(VPosition, ProjectionMatrix);
    // Output.Position.w = 1.0f;
    Output.Position.z = Output.Position.w;
    Output.TexCoord = _Input.Position.xyz;
    
    return Output;
}

////////////////////// SkyBos_PS
TextureCube CubeMapTex : register(t0);
SamplerState Sampler : register(s0);

struct PSInput
{
    float4 Position : SV_POSITION;
    float3 TexCoord : TEXCOORD0;
};

float4 SkyBox_PS(PSInput _Input) : SV_TARGET
{
    float4 Color = CubeMapTex.Sample(Sampler, _Input.TexCoord);
    return Color;
}

 

기존과 다른 점은 Pixel Shader에서 Texture2D가 아니라, TextureCube를 사용한다는 점입니다. 마지막으로 Rendering() 함수 종료 전, CameraRenderTarget에 Blending(Merge) 해줍니다.

 

 

[결과 확인]

 

바로 결과를 확인해보겠습니다.

 

[목차]

 

- 포스팅에 앞서

- 디퍼드 렌더링에서 반투명 물체 그리기

- 테스트 결과

 

 

[포스팅에 앞서]

 

해당 포스팅은 디퍼드 렌더링에서의 반투명 물체를 그릴 때 어떤 문제가 있는지 정도만 테스트했습니다. 

 

 

[디퍼드 렌더링에서 반투명 물체 그리기]

 

디퍼드 렌더링에서 가장 큰 문제는 바로 반투명 물체를 그리는 것입니다. 디퍼드 렌더링은 다음의 과정을 거칩니다.

 

1. G-Buffer Pass

: Geometry 정보를 여러 RenderTarget에 기록(Albedo, Normal, WorldPosition 등)

 

2. Lighting Pass

: 모든 조명을 G-Buffer 기반으로 계산

 

3. Final Composition

: 결과 조명값을 픽셀에 적용

 

디퍼드 렌더링은 한 픽셀 당 오직 하나의 Geometry 정보만 기록하기 때문에, 다수의 반투명 물체를 표현할 수 없습니다.

 

- 반투명 물체는 여러 픽셀이 겹쳐서 알파 블렌딩이 이뤄져야 하는데, G-Buffer는 마지막에 그려진 1개의 픽셀 정보만 기록함

- Lighting Pass는 G-Buffer를 기준으로 조명을 계산하기 때문에, 반투명 물체가 G-Buffer에 들어가지 않으면 조명 적용 자체가 불가능함

 

해당 문제는 이런 저런 방법이 있는데, 가장 쉽게 구현할 수 있는 방식이 바로 Alpha의 경우 Forward 렌더링을 따로 진행한 뒤, Depth Test 기반으로 서로 블렌딩 해주는 것입니다.

 

 

[테스트 결과]

 

디퍼드 렌더링 내에서 반투명 처리를 할 경우, 투과되어 모이는 물체에 빛이 적용되지 않는 현상이 발생합니다.

 

반투명을 Forward와 같이 따로 그리고, 그걸 나중에 RenderTarget에 Merge(Blending)하는 방식입니다(정확하게 수행한 것은 아님). 그러면 의도대로 반투명 처리가 되는 것을 확인할 수 있습니다.

 

 

[목차]

 

- 포스트 프로세싱이란

- 포스트 프로세싱 적용해보기

 

 

[포스트 프로세싱이란]

 

포스트 프로세싱은 3D 그래픽스에서 렌더링이 완료된 화면 이미지를 후처리하여 다양한 시각적 효과를 추가하는 단계를 말합니다. RenderTarget에 그려진 결과물(주로 G-Buffer)에 대해 추가로 이미지 필터를 적용하는 것입니다. 다양한 효과들이 있습니다.

 

- 블름(블러)

- 화면 전체 색감 보정

- 피사계 심도

- HDR 톤 매핑

- 안티 앨리어싱(FXAA)

- 디스토션

 

 

[포스트 프로세싱 적용해보기]

 

이제 프레임워크에 실제로 포스트 프로세싱을 적용해보겠습니다. 먼저 PostProcess 인터페이스를 담당하는 클래스를 만들어줍니다.

#pragma once
#include "Ext_DirectXRenderTarget.h"

class Ext_PostProcess : std::enable_shared_from_this<Ext_PostProcess>
{
	friend class Ext_DirectXRenderTarget;

public:
	std::shared_ptr<Ext_DirectXRenderTarget> GetPostTarget() { return PostTarget; }

protected:
	struct FrameData
	{
		float4 ScreenSize; // 화면(또는 텍스처) 크기
		float AccTime = 0.0f; // 실행된 시간(초)
	};

	virtual void Start() = 0;
	virtual void PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime) = 0;

	std::shared_ptr<Ext_MeshComponentUnit> PostUnit;
	std::shared_ptr<Ext_DirectXRenderTarget> PostTarget;

	FrameData FData;

private:
	
};

 

해당 인터페이스를 상속받아서 만들어지는 PostProcess들은 RenderTarget이 들고 있도록 해줍니다.

///////////// Ext_DirectXRenderTarget.h
//...
public:
// 포스트 프로세스 만들기
template<typename PostType>
std::shared_ptr<PostType> CreateEffect()
{
	std::shared_ptr<PostType> NewPostProcess = std::make_shared<PostType>();
	PostProcessInitialize(NewPostProcess);
	PostProcesses.push_back(NewPostProcess);
	return NewPostProcess;
}

void Ext_DirectXRenderTarget::PostProcessing(std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
	for (size_t i = 0; i < PostProcesses.size(); i++)
	{
		PostProcesses[i]->PostProcessing(this, _Camera, _DeltaTime);
	}
}

std::vector<std::shared_ptr<class Ext_PostProcess>>& GetPostProcesses() { return PostProcesses; }

private:
std::vector<std::shared_ptr<class Ext_PostProcess>> PostProcesses = {};

 

이렇게 RenderTarget에 대해 PostProcess를 추가하면 컨테이너 요소를 순회하면서 Update()가 진행될 것입니다.

 

 

 

// 카메라 생성 시 호출
void Ext_Camera::Start()
{
	ViewPortData.TopLeftX = 0;
	ViewPortData.TopLeftY = 0;
	ViewPortData.Width = Base_Windows::GetScreenSize().x;
	ViewPortData.Height = Base_Windows::GetScreenSize().y;
	ViewPortData.MinDepth = 0.0f;
	ViewPortData.MaxDepth = 1.0f;

	Width = ViewPortData.Width;
	Height = ViewPortData.Height;

	// 카메라 최종 렌더타겟
	CameraRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 해당 카메라의 최종 결과물 타겟
	CameraRenderTarget->CreateDepthTexture();
    // 추가할 PostProcess들
	// CameraRenderTarget->CreateEffect<Ext_Blur>();
	// CameraRenderTarget->CreateEffect<Ext_Distortion>();
	// CameraRenderTarget->CreateEffect<Ext_OldFilm>();
	// CameraRenderTarget->CreateEffect<Ext_TextureTest>();
    // ...
}

// 카메라의 MeshComponents들에 대한 업데이트 및 렌더링 파이프라인 리소스 정렬
void Ext_Camera::Rendering(float _Deltatime)
{
	// ...
    // 모든 렌더링 과정이 끝난 후 실시해준다.
	CameraRenderTarget->PostProcessing(GetSharedFromThis<Ext_Camera>(), _Deltatime);
}

 

0. 렌더링 파이프라인 세팅

: Vertex Shader, Pixel Shader만 다르고 Blend State, Depth State, Rasterzier State, Sampler State는 모두 같습니다.

std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("XXXEffect");

NewRenderingPipeline->SetVertexShader("XXXEffect_VS");
NewRenderingPipeline->SetPixelShader("XXXEffect_PS");
NewRenderingPipeline->SetBlendState("BaseBlend");
NewRenderingPipeline->SetDepthState("AlwayDepth");
NewRenderingPipeline->SetRasterizer("NonCullingRasterizer");

// Blend State : 알파 블렌드
D3D11_BLEND_DESC BlendInfo = { 0, };

BlendInfo.AlphaToCoverageEnable = false;
BlendInfo.IndependentBlendEnable = false;
BlendInfo.RenderTarget[0].BlendEnable = true;
BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; // 자주 쓰는 조합 1
BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
BlendInfo.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

// Depth State : 깊이 테스트 진행 X
D3D11_DEPTH_STENCIL_DESC DepthStencilInfo = { 0, };

DepthStencilInfo.DepthEnable = true;
DepthStencilInfo.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
DepthStencilInfo.DepthFunc = D3D11_COMPARISON_ALWAYS;
DepthStencilInfo.StencilEnable = false;

// Rasterizer State : Back-FaceCuliiing 안하는 Rasterizer
D3D11_RASTERIZER_DESC Desc = {};

Desc.CullMode = D3D11_CULL_NONE;
Desc.FrontCounterClockwise = FALSE;
Desc.FillMode = D3D11_FILL_SOLID;

 

 

1. Blur(Bloom)

: 가우시안 블러를 사용했습니다.

#include "PrecompileHeader.h"
#include "Ext_Blur.h"

#include <DirectX11_Base/Base_Windows.h>
#include "Ext_MeshComponentUnit.h"

void Ext_Blur::Start()
{
	PostUnit = std::make_shared<Ext_MeshComponentUnit>();
	PostUnit->MeshComponentUnitInitialize("FullRect", "Blur");
	PostTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);
}

void Ext_Blur::PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
	PostTarget->RenderTargetClear();
	PostTarget->RenderTargetSetting();

	FData.ScreenSize = Base_Windows::GetScreenSize();

	PostUnit->GetBufferSetter().SetConstantBufferLink("FrameData", FData);
	PostUnit->GetBufferSetter().SetTexture(_MainRenderTarget->GetTexture(0), "DiffuseTex");
	PostUnit->Rendering(_DeltaTime);
	PostUnit->GetBufferSetter().AllTextureResourceReset();

	_MainRenderTarget->RenderTargetClear();
	_MainRenderTarget->Merge(PostTarget);
}
// 가우시안 커널
// Total = 16+64+96+64+16 = 256
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 }
};

// 7×7 가우시안 커널 (Pascal row 6: 1, 6, 15, 20, 15, 6, 1)
// 전체 가중치 합 = (1+6+15+20+15+6+1)² = 64² = 4096
static const float Gau7[7][7] =
{
    { 1, 6, 15, 20, 15, 6, 1 },
    { 6, 36, 90, 120, 90, 36, 6 },
    { 15, 90, 225, 300, 225, 90, 15 },
    { 20, 120, 300, 400, 300, 120, 20 },
    { 15, 90, 225, 300, 225, 90, 15 },
    { 6, 36, 90, 120, 90, 36, 6 },
    { 1, 6, 15, 20, 15, 6, 1 }
};

// 9×9 가우시안 커널 (Pascal row 8: 1, 8, 28, 56, 70, 56, 28, 8, 1)
// 전체 가중치 합 = (1+8+28+56+70+56+28+8+1)² = 256² = 65536
static const float Gau9[9][9] =
{
    { 1, 8, 28, 56, 70, 56, 28, 8, 1 },
    { 8, 64, 224, 448, 560, 448, 224, 64, 8 },
    { 28, 224, 784, 1568, 1960, 1568, 784, 224, 28 },
    { 56, 448, 1568, 3136, 3920, 3136, 1568, 448, 56 },
    { 70, 560, 1960, 3920, 4900, 3920, 1960, 560, 70 },
    { 56, 448, 1568, 3136, 3920, 3136, 1568, 448, 56 },
    { 28, 224, 784, 1568, 1960, 1568, 784, 224, 28 },
    { 8, 64, 224, 448, 560, 448, 224, 64, 8 },
    { 1, 8, 28, 56, 70, 56, 28, 8, 1 }
};

cbuffer FrameData : register(b0)
{
    float4 ScreenSize; // 화면 크기
    float AccTime; // 실행된 시간
};

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

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 Texcoord : TEXCOORD;
};

// 5x5 가우시안 블러
//float4 Blur_PS(PSInput _Input) : SV_TARGET
//{
//    // 픽셀 하나 사이즈는 몇입니까
//    float2 PixelSize = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y);
    
//    // UV 중심에서 2픽셀씩 왼쪽위로 이동하여 가우시안 시작 지점 정하기
//    float2 PixelUvCenter = _Input.Texcoord.xy;
//    float2 StartUV = _Input.Texcoord.xy + (-PixelSize * 2.0f);
//    float2 CurUV = StartUV;
//    float4 ResultColor = (float4) 0.0f;
 
//    // 5×5 샘플링 & 가중치 누적
//    for (int y = 0; y < 5; ++y)
//    {
//        for (int x = 0; x < 5; ++x)
//        {
//            ResultColor += DiffuseTex.Sample(Sampler, CurUV.xy) * Gau[y][x];
//            CurUV.x += PixelSize.x;
//        }
        
//        CurUV.x = StartUV.x;
//        CurUV.y += PixelSize.y;
//    }
    
//    // 가중치 총합 256 으로 정규화
//    // 이렇게 해야 모든 샘플 값이 커널 가중치만큼 더해져서 전체 이미지가 지나치게 밝아지거나 어두워지는 것 방지
//    ResultColor /= 256.0f;
    
//    return ResultColor;
//}

// 7x7 가우시안 블러
//float4 Blur_PS(PSInput _Input) : SV_TARGET
//{
//    // 픽셀 하나 사이즈는 몇입니까
//    float2 PixelSize = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y);
    
//    // UV 중심에서 2픽셀씩 왼쪽위로 이동하여 가우시안 시작 지점 정하기
//    float2 PixelUvCenter = _Input.Texcoord.xy;
//    float2 StartUV = _Input.Texcoord.xy + (-PixelSize * 3.0f);
//    float2 CurUV = StartUV;
//    float4 ResultColor = (float4) 0.0f;
 
//    // 7×7 샘플링 & 가중치 누적
//    for (int y = 0; y < 7; ++y)
//    {
//        for (int x = 0; x < 7; ++x)
//        {
//            ResultColor += DiffuseTex.Sample(Sampler, CurUV.xy) * Gau7[y][x];
//            CurUV.x += PixelSize.x;
//        }
        
//        CurUV.x = StartUV.x;
//        CurUV.y += PixelSize.y;
//    }
    
//    // 가중치 총합 256 으로 정규화
//    // 이렇게 해야 모든 샘플 값이 커널 가중치만큼 더해져서 전체 이미지가 지나치게 밝아지거나 어두워지는 것 방지
//    ResultColor /= 4096.0f;
       
//    return ResultColor;
//}

// 9x9 가우시안 블러
float4 Blur_PS(PSInput _Input) : SV_TARGET
{
    // 픽셀 하나 사이즈는 몇입니까
    float2 PixelSize = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y);
    
    // UV 중심에서 2픽셀씩 왼쪽위로 이동하여 가우시안 시작 지점 정하기
    float2 PixelUvCenter = _Input.Texcoord.xy;
    float2 StartUV = _Input.Texcoord.xy + (-PixelSize * 4.0f);
    float2 CurUV = StartUV;
    float4 ResultColor = (float4) 0.0f;
 
    // 7×7 샘플링 & 가중치 누적
    for (int y = 0; y < 9; ++y)
    {
        for (int x = 0; x < 9; ++x)
        {
            ResultColor += DiffuseTex.Sample(Sampler, CurUV.xy) * Gau9[y][x];
            CurUV.x += PixelSize.x;
        }
        
        CurUV.x = StartUV.x;
        CurUV.y += PixelSize.y;
    }
    
    // 가중치 총합 256 으로 정규화
    // 이렇게 해야 모든 샘플 값이 커널 가중치만큼 더해져서 전체 이미지가 지나치게 밝아지거나 어두워지는 것 방지
    ResultColor /= 65536.0f;
    
    return ResultColor;
}

 

9x9까지 해버리면 연산량이 너무 많아집니다. 이에 대한 해결책으로 [원본 이미지 다운 스케일링 -> 5x5정도의 필터 적용 -> 적용된 이미지 원래 크기로 업스케일링]을 실시할 수도 있습니다(효과는 동일한데 연산은 최적화됨). 여기서는 적용에 의의를 두는 것이기 때문에 5x5, 7x7, 9x9를 각각 실시해봤습니다.

 

 

2. OldFilm

: 특수한 텍스쳐를 사용했습니다.

 

예전에 Cuphead라는 게임을 모작할 때 사용했던 리소스인데, 똑같이 가져와서 적용해봤습니다.

 

#include "PrecompileHeader.h"
#include "Ext_OldFilm.h"

#include <DirectX11_Base/Base_Windows.h>
#include "Ext_MeshComponentUnit.h"

void Ext_OldFilm::Start()
{
	// 기본 텍스쳐들 로드
	{
		Base_Directory Dir;
		Dir.MakePath("../Resource/FX/ScreenFX");
		std::vector<std::string> Paths = Dir.GetAllFile({ "png" });
		for (const std::string& FilePath : Paths)
		{
			Dir.SetPath(FilePath.c_str());
			std::string ExtensionName = Dir.GetExtension();
			std::string FileName = Dir.GetFileName();
			Textures.push_back(Ext_DirectXTexture::LoadTexture(FilePath.c_str()));
		}
	}

	OFData.OldFilmValue.x = 3.f;
	MaxIndex = static_cast<int>(Textures.size()) - 1;

	PostUnit = std::make_shared<Ext_MeshComponentUnit>();
	PostUnit->MeshComponentUnitInitialize("FullRect", "OldFilm");
	PostTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);
}

void Ext_OldFilm::PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
	AccTime += _DeltaTime;
	if (AccTime >= 0.1f)
	{
		AccTime = 0.0f;
		++CurIndex;
		if (CurIndex > MaxIndex)
		{
			CurIndex = 0;
		}
	}

	PostUnit->GetBufferSetter().SetTexture(Textures[CurIndex], "DiffuseTex");
	PostUnit->GetBufferSetter().SetConstantBufferLink("OldFilmData", OFData);
	PostTarget->RenderTargetClear();
	PostTarget->RenderTargetSetting();

	PostUnit->Rendering(_DeltaTime);
	PostUnit->GetBufferSetter().AllTextureResourceReset();

	_MainRenderTarget->RenderTargetSetting();
	_MainRenderTarget->Merge(PostTarget);
}

 

cbuffer OldFilmData : register(b0)
{
    float4 OldFilmValue; // x 성분만 사용
}

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

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 Texcoord : TEXCOORD0;
};

float4 OldFilm_PS(PSInput IN) : SV_TARGET
{
    // 1) 스크린 얼룩(Film) 텍스처 샘플
    float4 FlimColor = DiffuseTex.Sample(Sampler, IN.Texcoord);

    // 2) 얼룩 마스크 강도 계산 (1?R) * OldFilmValue.x
    float Mask = (1.0f - FlimColor.r) * OldFilmValue.x;

    // 3) R, G, B, A 모두 동일한 마스크 값으로
    return float4(Mask, Mask, Mask, Mask);
}

 

 

3. Distortion

: 가장 흔하게 사용되는 왜곡 효과입니다. 보통 특정 부분만 마스킹해서 사용하는데, 여기서는 그냥 화면 대상 모든 픽셀에 적용해봤습니다.

#include "PrecompileHeader.h"
#include "Ext_Distortion.h"

#include <DirectX11_Base/Base_Windows.h>
#include "Ext_MeshComponentUnit.h"

void Ext_Distortion::Start()
{
	PostUnit = std::make_shared<Ext_MeshComponentUnit>();
	PostUnit->MeshComponentUnitInitialize("FullRect", "Distortion");
	PostTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);	
}

void Ext_Distortion::PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
	PostTarget->RenderTargetClear();
	PostTarget->RenderTargetSetting();

	FData.ScreenSize = Base_Windows::GetScreenSize();
	FData.AccTime += _DeltaTime;

	PostUnit->GetBufferSetter().SetConstantBufferLink("FrameData", FData);
	PostUnit->GetBufferSetter().SetTexture(_MainRenderTarget->GetTexture(0), "DiffuseTex");
	PostUnit->Rendering(_DeltaTime);
	PostUnit->GetBufferSetter().AllTextureResourceReset();

	_MainRenderTarget->RenderTargetClear();
	_MainRenderTarget->Merge(PostTarget);
}
cbuffer FrameData : register(b0)
{
    float4 ScreenSize; // 화면 크기
    float AccTime; // 실행된 시간
};

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

struct PSInput
{
    float4 Position : SV_POSITION; // 스크린 위치 (필요 없으면 생략 가능)
    float2 Texcoord : TEXCOORD0; // 0~1 범위 UV
};

float4 Distortion_PS(PSInput _Input) : SV_TARGET
{
    float2 UV = _Input.Texcoord;

    // 파라미터, 상수버퍼로 전달하면 조절 가능
    const float Frequency = 10.0f; // 파동 수
    const float Ample = 20.0f; // 진폭 분모

    // 공식
    UV.x += sin(UV.y * Frequency + AccTime) / Ample;

    return DiffuseTex.Sample(Sampler, UV);
}

 

 

[렌더링 파이프라인이란]

 

렌더링을 하기 위해서는 렌더링 파이프라인 개념을 알아야합니다. 렌더링 파이프라인은 Direct3D에서 메모리 자원들을 GPU로 처리하여 하나의 렌더링 이미지로 만드는 일련의 과정을 말합니다.

 

일련의 과정에서 크게 두 가지로 나눌 수 있기도 합니다. 고정 기능 단계와 프로그래밍 가능 단계입니다.

 

- 고정 기능 단계 : 미리 정해진 특정 연산들이 수행되고, 상태 객체라는 개념을 이용하여 연산의 설정을 변경할 수 있으며, 사용자가 임의로 실행을 거부할 수 없음

- 프로그래밍 가능 단계 : HLSL(High Level Shading Languege)로 셰이더 프로그래밍이 가능한 단계이며, 임의로 실행을 거부할 수 있음

 

 

[단계별 정리]

 

단계별 핵심 내용입니다. "렌더링 결과물을 출력한다"를 위해서는 Input Assembler, Vertex Shader, Rasterizer, Pixel Shader, Output Merger만 진행하면 됩니다. Hull Shader, Tessellator, Domain Shader, Geometry Shader는 인스턴싱을 위한 과정입니다. 대표적으로 파티클이 있습니다만, 제가 사용할 프레임워크에서는 크게 다루지 않을 것 같습니다.

Input Assembler
(입력 조립기 단계)
- Vertex Buffer / Index Buffer에 저장된 정점, 인덱스 데이터 수집
- 이 단계에서는 아직 쉐이더가 실행되지 않았으며, 파이프라인에 데이터가 들어오는 입구에 해당
- 설정 정보에는 [Input Layout], [Primitive Topology] 등도 포함

<Index Buffer 활용>
: Vertex Buffer와 관련이 깊으며, 정점 버퍼에 있는 각 정점의 위치를 기록하기 위해 활용
: 이를 통해 Vertex Buffer의 특정 정점(Vertex)을 빠르게 찾거나 정점을 재사용 가능하게 만들어주며, Input Assembler 단계에서 사용
: 예시) 삼각형 하나를 이루기 위한 인덱스: 0, 1, 2 → 정점 0~2를 참조
: 사용 시 중복 정점 제거로 메모리를 절약하며, 그리기 명령 효율성 증가

<Vertex Buffer 활용>
: 정점(Vertex)들의 위치, 노멀, UV, 색상 등(float3 Postion, float3 Normal, float3 TexCoord)의 속성을 담고 있는 버퍼로, Input Assembler에서 활용됨
: IASetVertexBuffers()로 설정 → Vertex Shader
Vertex Shader
(정점 셰이더)
- 각 정점 데이터를 변환하는 첫 번째 쉐이더
- 정점마다 월드 좌표 → 클립 좌표로 변환, 애니메이션 스키닝, 라이팅 계산 등의 처리 수행
- 여기서 수행된 결과는 Pixel Shader에서 사용할 수 있도록 전달

<Constant Buffer 활용>
: 셰이더에 값을 저장하는 메모리로, CPU ↔ GPU간 데이터를 전달에 사용되며, 모든 셰이더 단계에서 활용됨
: 해당 값에는 행렬(World, View, Projection 등) 정보 등 활용
: 최대 14슬롯 바인딩이 가능하며, 내부 데이터는 16byte 단위로 정렬 필요

<Sampler 활용>
: 샘플러를 쓰기도 하지만, 주로 픽셀 셰이더에서 씀
Hull Shader
(덮개 셰이더)
[생략 가능]
- 테셀레이션의 첫 번째 단계
- 패치(기본 도형) 단위로 테셀레이션 정도를 결정하기 위해 테셀레이션 계수를 계산
- 쉐이더가 실행되면 정점 그룹 단위로 계산되며, 곡면 분할이 필요한 경우 사용

<Constant Buffer 활용>
Tessellator
(테셀레이터)
[생략 가능]
- 고정 기능 하드웨어
- 직접 프로그래밍은 불가능하며, Hull Shader에서 제공된 정보를 기반으로 세분화된 정점을 생성
- 삼각형, 선, 사각형 형태로 분할 가능
Domain Shader
(영역 셰이더)
[생략 가능]
- 테셀레이터가 생성한 세분화된 정점의 위치와 속성을 계산
- 곡면의 실제 형상 및 기하 정보를 계산하며, 여기서 생성된 정점은 이후 Geometry Shader로 전달

<Constant Buffer 활용>
Geometry Shader
(기하 셰이더)
[생략 가능]
- 도형(Primitive) 단위의 생성, 제거, 변형
- 삼각형을 분해하거나 더 추가할 수 있고, 쉐도우 볼륨, 실루엣, 아웃라인 효과 등에 활용
- Stream Output을 통해 이 결과를 바로 GPU 버퍼에 저장할 수도 있음

<Stream Output (스트림 출력)>
: Geometry Shader 출력 결과를 GPU 메모리에 직접 저장(Geometry Shader → Stream Output → 버퍼 저장)
: 파티클, 물리 시뮬레이션, LOD 캐싱, 후속 드로우콜 등에 활용
: 렌더링 외의 데이터 가공 목적에도 활용

<Constant Buffer 활용>
Rasterizer
(레스터라이저)
- 벡터 → 픽셀 변환 (Triangle → Pixel)
- 화면 공간으로 변환된 정점 데이터를 픽셀 단위로 분해
- Z-버퍼, 컬링, 클리핑 등의 테스트가 여기서 수행
Pixel Shader
(픽셀 셰이더)
- 화면에 찍힐 각 픽셀의 색상 계산
- 텍스처, 라이팅, 그림자, 블렌딩, 쉐도우맵 등 다양한 시각 효과 구현의 중심
- 결과는 렌더 타겟으로 전달

<Constant Buffer 활용>
: 정점 셰이더에서 Transform 데이터를 주로 활용한다면, 여기는 텍스쳐, Structured(빛, 델타타임) 등을 활용

<Sampler 활용>
: 텍스처 필터링이나 경계 처리 방식 등을 정의한 값으로, Vertex Shader나 PixelShader에서 텍스처 샘플링 시 활용됨
: 상수버퍼와 마찬가지로 바인딩 슬롯으로 데이터가 전달되고 사용됨
: UV값에 WRAP, CLAMP 등을 설정
: SetSamplers()로 최대 16개까지 바인딩 가능

<Texture Buffer 활용>
: 텍스처, 노이즈맵, 그림자 맵 등 샘플링 가능한 데이터 자원이며, Pixel Shader, Compute Shader 등에 활용됨
: Diffuse, Normal, Shadow Map 등이 있으며 SRV(Shader Resource View)로 접근 가능함
: 최대 128개까지 바인딩 가능
Output Merger
(출력 병합)
- 최종 픽셀 결과를 렌더 타겟, 깊이/스텐실 버퍼에 기록
- Z-Test, 스텐실 테스트, 블렌딩 등을 수행하며 최종 출력 픽셀을 결정
- 모든 테스트를 통과한 픽셀만 화면에 그려짐

 

'DirectX11 > 그래픽스' 카테고리의 다른 글

[Graphics] 드로우콜(Draw Call)  (0) 2025.06.11
[Graphics] 포워드 렌더링, 디퍼드 렌더링  (0) 2025.06.11
[Graphics] Normal Mapping  (0) 2025.06.07
[Graphics] Normal  (0) 2025.06.06
[Graphics] Lighting  (2) 2025.06.05

[목차]

 

- 그림자 맵 만들기

- 그림자 매핑하기

- 출력하기

 

 

[그림자 맵 만들기]

 

1. 빛을 기준으로 View, Projection 행렬 생성

2. Shadow 영향을 받을 Unit들에게 행렬 적용

3. Shadow 렌더링 파이프라인을 실시하고 DrawIndexed()

4. 출력 : 버퍼의 R채널(x)값에 물체들의 Depth값이 담김, MinBlend를 쓰기 때문에 검은색이 됨

 

현재의 프로젝트는 디퍼드 렌더링 구조입니다. 따라서 그림자 맵도 다른 G-Buffer 처럼 RenderTarget을 만들어주고, 여기에 기록해주면 됩니다. Ext_Light 클래스에 Shadow Render Target을 추가해줍니다.

///////////////////////// Ext_Light.cpp
Ext_Light::Ext_Light()
{
	LTData = std::make_shared<LightData>();
	LTData->bIsLightSet = true;
	LTData->ShadowTargetSizeX = 4096;
	LTData->ShadowTargetSizeY = 4096;
	ShadowRange.x = 2048.0f;
	ShadowRange.y = 2048.0f;
}

void Ext_Light::Start()
{
	GetOwnerScene().lock()->PushLight(GetSharedFromThis<Ext_Light>(), GetName());
	ShadowRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT_R32_FLOAT, { LTData->ShadowTargetSizeX, LTData->ShadowTargetSizeY }, float4::RED);
	ShadowRenderTarget->CreateDepthTexture();
}

// ... Update

///////////////////////// Ext_DirectXRenderTarget.cpp
// 텍스쳐를 생성해서 렌더타겟 생성
void Ext_DirectXRenderTarget::CreateRT(DXGI_FORMAT _Format, float4 _Scale, float4 _Color)
{
	Colors.push_back(_Color);

	D3D11_TEXTURE2D_DESC Desc = { 0 };
	Desc.ArraySize = 1;
	Desc.Width = _Scale.uix();
	Desc.Height = _Scale.uiy();
	Desc.Format = _Format;
	Desc.SampleDesc.Count = 1;
	Desc.SampleDesc.Quality = 0;
	Desc.MipLevels = 1;
	Desc.Usage = D3D11_USAGE_DEFAULT;
	Desc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_RENDER_TARGET | D3D11_BIND_FLAG::D3D11_BIND_SHADER_RESOURCE;

	std::shared_ptr<Ext_DirectXTexture> Tex = Ext_DirectXTexture::CreateViews(Desc);

	D3D11_VIEWPORT ViewPortData;
	ViewPortData.TopLeftX = 0;
	ViewPortData.TopLeftY = 0;
	ViewPortData.Width = _Scale.x;
	ViewPortData.Height = _Scale.y;
	ViewPortData.MinDepth = 0.0f;
	ViewPortData.MaxDepth = 1.0f;

	ViewPorts.push_back(ViewPortData);
	Textures.push_back(Tex);
	RTVs.push_back(Tex->GetRTV());
	SRVs.push_back(Tex->GetSRV());
}

 

이러면 R채널(x값) 하나의 값만 32bit로 사용하는 4096*4096 크기의 빨간 색상 RenderTarget이 하나 만들어집니다. 이 렌더 타겟의 R채널에 물체의 Depth 값을 기록할 것입니다.

 

다음으로 Depth값 추출을 위한 Vertex, Pixel Shader를 만들어줍니다.

////////////////// Shadow_VS
#include "Transform.fx"

struct VSInput
{
    float4 Position : POSITION;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
};

VSOutput Shadow_VS(VSInput _Input)
{
    VSOutput OutPut = (VSOutput) 0;
    OutPut.Position = mul(_Input.Position, WorldViewProjectionMatrix);
    return OutPut;
}

////////////////// Shadow_PS
struct PSInput
{
    float4 Position : SV_POSITION;
};

float4 Shadow_PS(PSInput _Input) : SV_TARGET
{
    return float4(max(0.0f, _Input.Position.z / _Input.Position.w), 0.0f, 0.0f, 1.0f);
}

 

렌더될 대상들에 대해 Pixel Shader에 View, Projection 행렬을 곱한 Clip Space 값을 전달해줍니다. 이러면 깊이를 만들기 위해서 z에 w divide를 수행해야합니다. 그 점을 이용하여 전달받은 값에 대해 z를 w로 나눠 R채널에 깊이값을 기록해줍니다.

float4(max(0.0f, _Input.Position.z / _Input.Position.w), 0.0f, 0.0f, 1.0f);

 

이러면 렌더 대상들의 가진 픽셀들에 대해, 아래의 형태로 깊이값이 기록됩니다.

float4(Rchanel, 0.0f, 0.0f, 1.0f); // Rchanel : Depth-Value(0.0f ~ 1.0f)

 

그리고 각 값들은 Min Blending이 실시됩니다.

D3D11_BLEND_DESC BlendInfo = { 0, };

BlendInfo.AlphaToCoverageEnable = false;
BlendInfo.IndependentBlendEnable = false;

BlendInfo.RenderTarget[0].BlendEnable = true;
BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_MIN;

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

BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

Ext_DirectXBlend::CreateBlend("MinBlend", BlendInfo);

 

하나의 RenderTarget 내에서 위의 블렌딩 공식을 사용하기 때문에, 현재 값보다 더 작은 값이 RenderTarget에 기록됩니다. 아래의 그림을 확인해보겠습니다.

 

빛이 45도 각도로 네모, 삼각형, 동그라미를 내리쬐는 형태로, 순서대로 네모, 세모, 동그라미, 오각형이 그러지며, 모두의 깊이값이 대략 0.45부터 0.8 사이 정도에 존재한다고 가정해보겠습니다.

 

1. 네모

: 아무것도 그려지지 않았기 때문에 그냥 그려지면서, 깊이값을 기록합니다.

 

2. 세모

: 1번(빨간점)부터 2번(보라점)까지의 범위가 네모와 겹칩니다. 해당 부분의 깊이값을 봤을 때, Min Blending을 실시하기 때문에 값이 더 작은 깊이값이 기록됩니다. 네모는 대략 0.5, 세모는 대략 0.7이라고 가정한다면, RenderTarget의 현재 위치는 네모의 0.5가 기록됩니다.

 

3. 동그라미

: 동그라미가 그려졌는데, 3번(초록점)부터 4번(하늘색점)까지의 범위가 기존에 그려졌던 세모와 겹칩니다. 똑같이 Min Blending을 실시하기 때문에 값이 더 작은 깊이값이 기록됩니다. 동그라미는 대력 0.6, 세모는 대략 0.72이라고 가정한다면, RenderTarget의 현재 위치는 동그라미의 0.6으로 갱신됩니다.

 

4. 오각형

: 자기 외에 아무것도 그려지지 않은 상태이기 때문에, 깊이값을 자신의 것으로 기록합니다.

 

5. 이외

: 이외에는 깊이값이 기록되지 않기 때문에 원래의 렌더 타겟 색상(1.0f, 0.0f, 0.0f, 1.0f)을 갖게 됩니다.

 

최종적으로 그려지는 결과는 다음과 같습니다.

 

이렇게 생성되는 것이 바로 그림자 맵입니다. Ext_Camera의 Rendering() 함수 중, 아래의 부분에서 지금까지 설명드린 과정이 진행됩니다.

// 카메라의 MeshComponents들에 대한 업데이트 및 렌더링 파이프라인 리소스 정렬
void Ext_Camera::Rendering(float _Deltatime)
{
	// ...
	// Geometry Pass 진행하는 부분

	// 쉐도우 패스, 뎁스 만들기
	auto& Lights = GetOwnerScene().lock()->GetLights();
	for (auto& [name, CurLight] : Lights)
	{
		std::shared_ptr<Ext_DirectXRenderTarget> CurShadowRenderTarget = CurLight->GetShadowRenderTarget();
		if (!CurShadowRenderTarget) continue; // 세팅 안되어있으면 그릴 필요 없음

		std::shared_ptr<LightData> LTData = CurLight->GetLightData(); // 현재 라이트의 데이터(앞서 업데이트됨)
		CurShadowRenderTarget->RenderTargetSetting(); // 백버퍼에서 지금 렌더 타겟으로 바인딩 변경, 여기다 그리기

		// 쉐도우 뎁스 텍스쳐 만들기
		for (auto& Unit : AllRenderUnits)
		{
			if (!Unit->GetIsShadow()) continue;

			Unit->GetOwnerMeshComponent().lock()->GetTransform()->SetCameraMatrix(LTData->LightViewMatrix, LTData->LightProjectionMatrix); // 라이트 기준으로 행렬 세팅
			Unit->RenderUnitShadowSetting(); 

			std::shared_ptr<Ext_DirectXMaterial> ShadowPipeLine;

			if (ShadowType::Static == Unit->GetShadowType())
			{
				ShadowPipeLine = Ext_DirectXMaterial::Find("Shadow");
			}
			else if (ShadowType::Dynamic == Unit->GetShadowType())
			{
				ShadowPipeLine = Ext_DirectXMaterial::Find("DynamicShadow");
			}
			else
			{
				MsgAssert("여기 들어오면 안되는데 뭔가 잘못됨");
			}

			ShadowPipeLine->VertexShaderSetting();
			ShadowPipeLine->RasterizerSetting();
			ShadowPipeLine->PixelShaderSetting();
			ShadowPipeLine->OutputMergerSetting();
			Unit->RenderUnitDraw();
		}
	}
    
    // ...
    // Deferred 작업 진행하는 부분
}

 

그림자를 활성화할 렌더 대상들의 경우, 아래와 같이 ShadowOn() 함수를 호출해주도록 했습니다.

void SphereActor::Start()
{
	MeshComp = CreateComponent<Ext_MeshComponent>("BasicMesh");
	MeshComp->CreateMeshComponentUnit("Sphere", MaterialType::Static);
	MeshComp->SetTexture("Gray.png");
	MeshComp->ShadowOn(ShadowType::Static);
    // ...
}

void Ext_MeshComponentUnit::StaticShadowOn()
{
	bIsShadow = true;
	
	if (nullptr == ShadowInputLayout)
	{
		ShadowT = ShadowType::Static;
		std::shared_ptr<Ext_DirectXVertexShader> ShadowVS = Ext_DirectXVertexShader::Find("Shadow_VS");
		ShadowInputLayout = std::make_shared<Ext_DirectXInputLayout>();
		ShadowInputLayout->CreateInputLayout(Mesh->GetVertexBuffer(), ShadowVS);
	}
}

 

이러면 셰도우를 위핸 Unit(렌더링 파이프라인)도 생성되고, bIsShadow 값도 변경해주기 때문에 Update() 부분에서 예외 처리를 해줄 수 있게 됩니다.

 

지금까지의 과정을 프레임워크에서 실행하면, 다음의 결과를 얻을 수 있습니다.

 

 

[그림자 매핑하기]

 

이제 그림자 맵을 매핑해주는 단계를 진행합니다. 그림자의 매핑은 Light들을 계산하는 부분에서 함께 실시했습니다. 

// 카메라의 MeshComponents들에 대한 업데이트 및 렌더링 파이프라인 리소스 정렬
void Ext_Camera::Rendering(float _Deltatime)
{
	// ...
	// 그림자 맵 생성 후
    
    // 렌더타겟 세팅
	LightRenderTarget->RenderTargetClear();
	LightRenderTarget->RenderTargetSetting();
	
    // 라이트 업데이트
	GetOwnerScene().lock()->GetLightDataBuffer().LightCount = 0; // 라이트 업데이트 전, 상수버퍼 갯수 초기화(순회하면서 넣어줘야하기 때문)
	for (auto& [name, CurLight] : Lights)
	{
		LightUnit.BufferSetter.SetTexture(CurLight->GetShadowRenderTarget()->GetTexture(0), "ShadowTex");
		LightUnit.Rendering(_Deltatime);
		GetOwnerScene().lock()->GetLightDataBuffer().LightCount++;
	}
    
    // ...
}

 

위의 과정을 진행하면, LightUnit(Light Buffer를 생성하기 위한 렌더링 파이프라인)을 통해 LightRenderTarget에 그림자 맵이 매핑됩니다.

 

#include "LightData.fx"

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 Texcoord : TEXCOORD;
};

struct PSOutPut
{
    float4 DiffuseTarget : SV_TARGET0;
    float4 SpecularTarget : SV_TARGET1;
    float4 AmbientTarget : SV_TARGET2;
    float4 ShadowTarget : SV_TARGET3;
};

Texture2D PositionTex : register(t0); // G-Buffer: 월드-스페이스 위치(x,y,z) + 1
Texture2D NormalTex : register(t1); // G-Buffer: 월드-스페이스 법선(x,y,z) + 1
Texture2D ShadowTex : register(t2);
SamplerState Sampler : register(s0);
SamplerComparisonState ShadowCompare : register(s1);

PSOutPut DeferredLight_PS(PSInput _Input) : SV_TARGET
{
    // ...
    // 기존의 라이트 계산 실시

    if (DiffuseLight.x > 0.0f)
    {
        float4 LightPos = mul(float4(WorldPos.xyz, 1.0f), LTData.LightViewProjectionMatrix);
        float3 LightProjection = LightPos.xyz / LightPos.w;
        
        float2 ShadowUV = float2(LightProjection.x * 0.5f + 0.5f, LightProjection.y * -0.5f + 0.5f);
        float fShadowDepth = ShadowTex.Sample(Sampler, float2(ShadowUV.x, ShadowUV.y)).r;
        
        if (fShadowDepth >= 0.0f && LightProjection.z >= (fShadowDepth + 0.001f))
        {
            OutPut.ShadowTarget.x = 1.0f;
            OutPut.ShadowTarget.a = 1.0f;
        }
    }
    
    OutPut.DiffuseTarget = float4(DiffuseLight, 1.0f);
    OutPut.SpecularTarget = float4(SpecularLight, 1.0f);
    OutPut.AmbientTarget = float4(AmbientLight, 1.0f);
    
    return OutPut; // 카메라 행렬 빛의 위치 그려져있는 빛을 기반으로한 깊이 버퍼 텍스처 리턴
}

 

해당 부분이 LightUnit 렌더링 파이프라인 중 그림자를 매핑해주는 부분입니다(Pixel Shader). 매핑은 Diffuse Light가 닿는 곳에 대해서만 진행하도록 합니다(안비추는 곳은 어차피 계산할 필요가 없습니다).

 

먼저 월드 기준으로 위치한 물체(정점들)에 대해 빛의 View, Projection 행렬을 곱해주어 빛 기준의 Clip Space 값을 얻습니다. 여기서 W Divide를 실시하면 NDC 좌표값이 되면서,  x, y는 텍스쳐 좌표값, z는 깊이값이 담깁니다.

 

해당 과정은 카메라로 시점 내에 물체들이 어디에 위치하는지 값을 구하는 과정과 완전히 동일합니다. 시점이 카메라에서 빛으로 변경된 것 뿐입니다.

원래 우리가 하던거
여기서 하는거

 

위에서 구한 x, y값은 -1 ~ 1까지의 범위를 갖기 때문에, UV로 쓰기 위해서(위치시킨 부분을 UV 값으로 보기 위함) 값을 변형시켜줍니다.

LightProjection.x * 0.5f + 0.5f,     // x: [-1,1] → [0,1]
LightProjection.y * -0.5f + 0.5f     // y: [-1,1] → [1,0] (상하 반전)

 

이러면 x는 0 ~ 1, y는 1 ~ 0 사이 값인 UV값이 됩니다. y에 -0.5f를 곱하는 이유는 NDC 공간의 Y축은 위가 +, 텍스쳐 UV는 위가 0이므로 보정해주는 것입니다(Y축 상하 반전 수행).

 

이 값으로 Shadow Map을 샘플링합니다.

float fShadowDepth = ShadowTex.Sample(Sampler, float2(ShadowUV.x, ShadowUV.y)).r;

 

이렇게 해주면 최종적으로 값들은 다음과 같습니다.

 

- LightProjection.z : 라이트 시점에서의 깊이값

- fShadowDepth : 라이트 시점에서의 Shadow Map의 깊이값

 

이제 두 값을 비교해서 현재 픽셀이 Shadow Map보다 더 뒤에 있으면(가리지 않으면) 그림자가 됩니다. 깊이값은 더 큰게 뒤에 있는 것입니다. [LightProjection.z ≥ fShadowDepth + Bias] 조건식을 수행하여 그림자 영역을 찾아주고, 해당 부분은 float4(1.0f, 0.0f, 0.0f, 1.0f)로 기록해줍니다(R채널 말고 G, B채널 아무대나 해도 상관 없습니다).

 

여기서 한 가지 특이한 점이, fShadowDepth 값에 0.001f를 더해주고 있는 것입니다. 이건 Depth Bias라고 하는데, Self_Shadowing을 막기 위해 현재 Fragment Light 공간 깊이에 아주 작은 Offeset을 더해주는 행위입니다. GPU에서 깊이에 기록되는 값은 부동소수점이고, 이 부동소수점은 정밀도에 한계가 있어 실제 렌더되는 대상의 위치와 완전히 일치하지 않을 수도 있습니다.

 

또한 Bias 없이 [LightProjection.z ≥ fShadowDepth]만 수행한다면, 같은 표면 위에 있는 픽셀도(혹은 +- 0.0000001) [>] 판정이 되어 표면 위에 검은 줄이 생길 수도 있습니다.

 

결과는 다음과 같습니다. 왼쪽 위는 최종 결과이니, 왼쪽 아래 결과를 보시면 됩니다.

 

 

[출력하기]

 

이제 기록된 값을 활용해주면 됩니다. 해당 부분은 모든 Light Buffer 값들을 Merge 해주는 곳입니다.

#include "LightData.fx"

struct PSInput
{
    float4 POSITION : SV_POSITION;
    float2 TEXCOORD : TEXCOORD;
};

struct PSOutput
{
    float4 Color : SV_TARGET;
};

Texture2D BaseColorTex : register(t0);
Texture2D DiffuseTex : register(t1);
Texture2D SpecularTex : register(t2);
Texture2D AmbientTex : register(t3);
Texture2D ShadowTex : register(t4);
SamplerState Sampler : register(s0);

PSOutput DeferredMerge_PS(PSInput _Input) : SV_TARGET
{
    PSOutput OutPut = (PSOutput) 0;
    
    float4 Albedo = BaseColorTex.Sample(Sampler, _Input.TEXCOORD);
    float4 DiffuseRatio = DiffuseTex.Sample(Sampler, _Input.TEXCOORD);
    float4 SpacularRatio = SpecularTex.Sample(Sampler, _Input.TEXCOORD);
    float4 AmbientRatio = AmbientTex.Sample(Sampler, _Input.TEXCOORD);
    float ShadowMask = ShadowTex.Sample(Sampler, _Input.TEXCOORD).x;
    
    // 그림자 어두운 정도 (0.0 = 안 어두움, 1.0 = 완전 검정)
    float ShadowStrength = 0.7;

    // 그림자 계수 계산: 
    // shadowM==0 → 1.0 (원래 라이팅),  
    // shadowM==1 → 1.0-shadowStrength (어두워진 라이팅)
    float ShadowFactor = lerp(1.0, 1.0 - ShadowStrength, ShadowMask);
    
    DiffuseRatio *= ShadowFactor;
    SpacularRatio *= ShadowFactor;
    
    if (Albedo.a)
    {
        OutPut.Color.xyz = Albedo.xyz * (DiffuseRatio.xyz + SpacularRatio.xyz + AmbientRatio.xyz);
        OutPut.Color.a = saturate(Albedo.a + (DiffuseRatio.w + SpacularRatio.w + AmbientRatio.w));
        OutPut.Color.a = 1.0f;
    }
    else
    {
        OutPut.Color.xyz = (DiffuseRatio.xyz + SpacularRatio.xyz + AmbientRatio.xyz);
        OutPut.Color.a = saturate(OutPut.Color.x);
    }
    
    return OutPut;
}

 

샘플링을 실시하면 특정 픽셀에 대해서는 R채널(x) 값이 1로 나옵니다. 해당 위치는 그림자 영역이기 때문에, 음영이 지도록 처리해주면 됩니다. 여기서는 단순하게 해당 영역에 대해서는 Diffuse와 Specular Light 값에 Shadow Factor라고 해서 약간의 값을 처리해줬습니다. 1 이하의 값이기 때문에 곱해주면 원래 빛의 Color 보다 어두워질 것입니다.

 

+) 저는 약간의 노가다를 해서 원래 Light 들로 생겨났던 음영과 비슷한 강도로 설정했습니다.

 

최종 결과를 확인해봅시다.

 

+) 여기서는 따로 언급을 안했는데, Dynamic의 Shadow의 경우, 그림자맵을 만들 때 Vertex Shader에서 기존과 같이 스키닝을 실시해줘야 합니다. 아마 안하면 T-Pose를 취한 상태의 Maximo 캐릭터가 공중에 떠있는 그림자가 생성될 것입니다.

[드로우 콜이란]

 

드로우 콜(Draw Call)은 CPU가 GPU에게 오브젝트를 그리라고 명령을 내리는 것입니다. 자세히는 GPU의 Render State를 변경하는 작업입니다. GPU는 혼자서 아무것도 할 수 없습니다. 오브젝트를 그릴 때 조차 어떤 텍스쳐를 써야하는지, 어떤 셰이더를 써야하는지가 정해져 있지 않아서 CPU가 이것을 알려줘야합니다. 이렇게 알려주면(Draw Call) GPU는 이제 그것에 따라서 세팅된 값(Render State)을 그대로(굉장히 빠르게) 처리합니다.

 

물론 이 Render State에 들어갈 값들은 이미 GPU Memory에 존재해야 합니다(VRAM). 우리는 이미 이 값들이 무엇인지 알고 있습니다.

 

- InputLayout

- Vertex Buffer

- Index Buffer

- Constant Buffer(Texture, Sampler)

- Vertex Shader

- Pixel Shader

- DepthStencilState, BlendState, RasterizerState

 

이 값들을 DirectX11의 함수를 통해 바인딩 해주어 데이터 시작 주소를 가져가 VRAM에 Copy/Paste 해준 뒤, GPU가 이대로 그릴 수 있도록 해줬습니다. 이후 마지막 단계에서 이 값들을 가지고 Draw() 함수를 호출하여 오브젝트를 그리게 됩니다.

 

하지만 이 Render State를 바꾸는 과정은 아주 많은 오버헤드가 발생합니다. CPU와 GPU가 사용하는 언어가 다른데, 이걸 번역해주는 과정이 필요하기 때문입니다. 보통 이 번역은 우리가 GPU를 위해 설치하는 GPU Driver 내 Code 들이 실시해줍니다. 결국 번역 때문에 시간이 걸리는 것인데, CPU가 Vsync 1회(보통 33ms) 안에 값을 정상적으로 전달해줘야 되지만, 가끔은 CPU 자체의 전달 과정이 오래 걸리거나 번역에 이상이 생겨 지연될 경우 Bottle Neck 현상이 발생하기도 합니다.

 

 

[드로우 콜 줄이기]

 

당연히 성능을 위해 많은 최적화 기법들이 생겨났습니다.

 

1. 배칭(Batching)

: 여러 메시를 하나의 버퍼에 묶어서 한 번에 그리는 방법입니다. 여기에는 정적 메시를 미리 합쳐두는 Static Batching과, 매 프레임 동적으로 유니폼 버퍼 값만 바꿔서 그리는 Dynamic Batching이 있습니다.

 

- 런타임 오버 헤드 없음

- 병합된 하나의 버퍼만 바인딩하여 드로우 콜이 1회가 됨

- 병합된 버퍼 크기 증가 시 메모리 사용량이 상승

- 스태틱 배칭의 경우 병합된 메시를 모두 활용하기 때문에 메모리 사용량이 증가하며, 개별 컬링이 불가능함, 변형이 필요한 경우 다시 병합해야 하므로 유연성이 낮음 

- 다이나믹 배칭의 경우 실시간으로 연산하기엔 연산량이 비대하여 버텍스 수가 많으면 오히려 배칭으로 발생하는 손해가 더 커짐

 

2. 인스턴싱(Instancing)

: 동일한 메시가 있다면, 이걸 개별로 드로잉하지 않고 여러 위치, 트랜스폼으로 동시에 그려버릴 수 있습니다. 이걸 인스턴싱이라고 합니다. 인스턴스별로 행렬 정보와 머티리얼 값만 다르게 전달해주면 됩니다.

 

- 오브젝트 수 만큼 드로우콜이 감소(10개 그릴걸 하나에 몰아서 동시에 그리기 때문)

- API 호출을 오브젝트 수만큼 하지 않아서 드로우 콜 오버헤드 감소

- 버텍스 데이터가 한 곳에 있고, 인스턴스 데이터가 연속된 배열로 감싸져 있어서 캐시 로컬리티 좋음

- 너무 크면 메모리가 낭비되고, 너무 작으면 여러 번 분할 업데이트 되기 때문에 적당한 크기를 찾아야함

 

3. 텍스쳐 아틀라스(Atlas)

: 여러 텍스쳐를 하나의 큰 텍스쳐에 합쳐서 활용하는 방법입니다. 이러면 셰이더에서 UV 오프셋만 조정해서 다양한 텍스쳐처럼 사용할 수 있어 Texture 바인딩 횟수가 감소하게 됩니다.

 

- 다른 텍스쳐 바인딩을 따로하지 않고 같이 해서 사용하여 드로우콜이 1회로 감소

- 셰이더 리소스 바인딩 횟수가 줄어 오버헤드 감소

- 텍스쳐 샘플링 시 한 덩어리 텍스쳐에서 샘플링하므로 메모리 로컬리티 좋음

- 텍스쳐 경계에서 이웃 픽셀이 섞일 수록, 각 서브 텍스쳐 사이에 1-2px의 패딩을 넣지 않으면 블리딩(Bleeding) 현상 발생

- 자동 생성된 MipMap이 경계 영역을 섞을 수 있도록, 런타임에 수동으로 MipMap을 만들어거나 패딩을 충분히 확보해야함

- GPU마다 지원하는 최대 텍스쳐 크기가 있어서 서브 텍스쳐 갯수와 크기에 맞춰서 설계해야함

 

4. 프로스트럼, 오클루전 컬링

 

: 카메라 시야 밖이나 가려진 오브젝트를 배제하는 방법으로, DirectX에서는 Rasterizer 단계에서 자체적으로 실행되기도 하며, 사용자가 임의로 오브젝트를 컬링하는 방식도 있습니다.

 

5. LOD(Level of Detail)

: 멀리 있는 객체는 폴리곤 수가 적은 메시로 치환하는 방법입니다. 메시의 폴리곤 수가 적으면 적을수록, 이 메시를 표현하기 위해 사용되는 GPU 자원이 적어지기 때문에 드로우 콜 과정이 단순화됩니다.

 

- 멀리 있는 오브젝트는 픽셀 당 차이가 눈에 보이지 않아, 단순화 시켜서 드로우 연산 자체 부담을 줄임

- 복잡한 씬에서도 가까운 오브젝트만 정밀하게 그리고 안보이거나 제대로 안봐도 되는 경우에는 간단하게 렌더링하여 퍼포먼스 최적화

- 하지만 전환 지점이 너무 짧으면 Popping 현상이 발생(페이드 블렌드, 크로스 페이드 적용 필요)

- LOD 모델 자체에 단계별 메시들이 저장되어 있어 메모리 부담, 따로 로드해도 부담

'DirectX11 > 그래픽스' 카테고리의 다른 글

[Graphics] Rendering Pipeline  (0) 2025.06.18
[Graphics] 포워드 렌더링, 디퍼드 렌더링  (0) 2025.06.11
[Graphics] Normal Mapping  (0) 2025.06.07
[Graphics] Normal  (0) 2025.06.06
[Graphics] Lighting  (2) 2025.06.05

[목차]

 

- 변경 이유

- 변경 결과

- 구조 변경

- 디퍼드 렌더링을 위해 필요한 기능들

 

 

[변경 이유]

 

이후의 단계로 그림자 매핑과 포스트 프로세싱을 진행하려고 하는데, 제가 알고있는 방식이 디퍼드 렌더링 방식이라 조금 고민을 했습니다. 그래도 애초에 프레임워크 제작 이유가 그래픽스 학습과 DirectX11 복습이었기 때문도 있고 시간적인 여유가 부족했기 때문에 그냥 디퍼드 렌더링으로 변경하여 다음 단계를 진행하기로 마음먹었습니다.

 

 

[변경 결과]

 

우선 변경 결과입니다.

 

기존의 결과와 크게 달라진 건 없고, 구조적인 부분만 변경되었습니다. 내용이 상당히 길기 때문에 천천히 읽어보시면 될 것 같습니다.

 

 

[구조 변경]

 

1. fx 파일 생성

: G-Buffer를 만들기 전에, 공통적으로 사용하는 메모리 슬롯이나 struct 부분에 대해서 따로 fx 파일을 만들어서 저장해두었습니다. 

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

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

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

/////////// LightData.fx
struct LightData
{
    float4 LightColor; // RGB(색), w(강도)
    float4 LightWorldPosition;
    float4 LightForwardVector;
    float4 CameraWorldPosition;

    float ShadowTargetSizeX;
    float ShadowTargetSizeY;
    
    float NearDistance;
    float FarDistance;
    float AttenuationValue;
    int LightType;
    bool bIsLightSet;
    
    float4x4 LightViewMatrix;
    float4x4 LightViewInverseMatrix;
    float4x4 LightProjectionMatrix;
    float4x4 LightProjectionInverseMatrix;
    float4x4 LightViewProjectionMatrix;
    float4x4 CameraViewInverseMatrix;
};

cbuffer LightDatas : register(b2)
{
    int LightCount;
    LightData Lights[64];
};

float3 DiffuseLightCalculation(float3 _LightDirection, float3 _Normal)
{
    float3 LightDirection = normalize(_LightDirection); // 빛 계산에 사용하는 LightDirection은 "표면에서 봤을 때 빛이 표면으로 들어오는 방향"을 사용하므로 반대로 뒤집음
    float3 Normal = normalize(_Normal);
    float3 DiffuseLight = saturate(dot(LightDirection, Normal));
    
    return DiffuseLight;
}

float SpecularLightCalculation(float3 _LightDirection, float3 _Normal, float3 _CameraWorldPosition, float3 _PixelPosition, float _Shininess)
{
    // Phong
    //float3 ReflectionVector = normalize(2.0f * _Normal * dot(_LightDirection, _Normal) - _LightDirection); // 반사벡터
    //float3 EyePosition = _CameraWorldPosition;
    //float3 EyeDirection = normalize(EyePosition - _PixelPosition);
    //float RDotV = max(0.0f, dot(ReflectionVector, EyeDirection));
    //float3 SpecularLight = pow(RDotV, _Shininess);
    
    //return SpecularLight;
    
    // Blinn-Phong
    float3 L = normalize(_LightDirection);
    float3 V = normalize(_CameraWorldPosition - _PixelPosition);
    float3 H = normalize(L + V);

    float NdotH = saturate(dot(normalize(_Normal), H));
    float3 SpecularLight = pow(NdotH, _Shininess);
    
    return SpecularLight;
}

float3 AmbientLightCalculation(float _AmbientIntensity)
{
    float3 AmbientLight;
    AmbientLight.x = _AmbientIntensity;
    AmbientLight.y = _AmbientIntensity;
    AmbientLight.z = _AmbientIntensity;
    
    return AmbientLight;
}

 

2. Rendering() 과정 변경

: 업데이트 과정이 변경되었습니다. 그게 Geometry Pass, ShadowMap Pass, Light Pass로 나뉩니다.

void Ext_Camera::Rendering(float _Deltatime)
{
	MeshRenderTarget->RenderTargetClear();
	MeshRenderTarget->RenderTargetSetting();
	
////////////////// 일반 패스(Mesh, Position, Normal Buffer) ///////////////
	// 전체 유닛 Z정렬 후 렌더링
	std::vector<std::shared_ptr<Ext_MeshComponentUnit>> AllRenderUnits;
	for (auto& [RenderPathKey, UnitMap] : MeshComponentUnits)
	{
		for (auto& [IndexKey, UnitList] : UnitMap)
		{
			for (auto& Unit : UnitList)
			{
				auto Owner = Unit->GetOwnerMeshComponent().lock();
				if (!Owner || Owner->IsDeath() || !Owner->IsUpdate())
				{
					continue;
				}
				AllRenderUnits.push_back(Unit);
			}
		}
	}

	float4 CamPos = GetTransform()->GetWorldPosition();
	std::sort(AllRenderUnits.begin(), AllRenderUnits.end(),
		[&](const std::shared_ptr<Ext_MeshComponentUnit>& A, const std::shared_ptr<Ext_MeshComponentUnit>& B)
		{
			auto AMesh = A->GetOwnerMeshComponent().lock();
			auto BMesh = B->GetOwnerMeshComponent().lock();
			return (AMesh->GetTransform()->GetWorldPosition() - CamPos).Size()
			 > (BMesh->GetTransform()->GetWorldPosition() - CamPos).Size();
		});

	std::unordered_set<std::shared_ptr<Ext_MeshComponent>> UpdatedComponents;
	for (auto& Unit : AllRenderUnits)
	{
		auto Owner = Unit->GetOwnerMeshComponent().lock();
		if (!Owner) continue;

		// View/Projection은 한 번만 업데이트
		if (UpdatedComponents.insert(Owner).second)
		{
			Owner->Rendering(_Deltatime, GetViewMatrix(), GetProjectionMatrix()); // 행렬 업데이트
		}

		Unit->Rendering(_Deltatime); // 렌더링 파이프라인 세팅 후 드로우콜
	}

//////////////////////// 쉐도우 패스(Shadow Buffer) /////////////////////
	// 쉐도우 패스, 뎁스 만들기
	auto& Lights = GetOwnerScene().lock()->GetLights();
	for (auto& [name, CurLight] : Lights)
	{
		std::shared_ptr<Ext_DirectXRenderTarget> CurShadowRenderTarget = CurLight->GetShadowRenderTarget();
		if (!CurShadowRenderTarget) continue; // 세팅 안되어있으면 그릴 필요 없음

		std::shared_ptr<LightData> LTData = CurLight->GetLightData(); // 현재 라이트의 데이터(앞서 업데이트됨)
		CurShadowRenderTarget->RenderTargetSetting(); // 백버퍼에서 지금 렌더 타겟으로 바인딩 변경, 여기다 그리기

		// 쉐도우 뎁스 텍스쳐 만들기
		for (auto& Unit : AllRenderUnits)
		{
			if (!Unit->GetIsShadow()) continue;

			Unit->GetOwnerMeshComponent().lock()->GetTransform()->SetCameraMatrix(LTData->LightViewMatrix, LTData->LightProjectionMatrix); // 라이트 기준으로 행렬 세팅
			Unit->RenderUnitShadowSetting(); 
			auto PShadow = Ext_DirectXMaterial::Find("Shadow");
			PShadow->VertexShaderSetting();
			PShadow->RasterizerSetting();
			PShadow->PixelShaderSetting();
			PShadow->OutputMergerSetting();
			Unit->RenderUnitDraw();
		}
	}

//////////// 라이트 패스(Diffuse, Specluar, Ambient Buffer) /////////////
	LightRenderTarget->RenderTargetClear();
	LightRenderTarget->RenderTargetSetting();

	GetOwnerScene().lock()->GetLightDataBuffer().LightCount = 0; // 라이트 업데이트 전, 상수버퍼 갯수 초기화(순회하면서 넣어줘야하기 때문)
	for (auto& [name, CurLight] : Lights)
	{
		//LightUnit.BufferSetter.SetTexture(CurLight->GetShadowRenderTarget()->GetTexture(0), "ShadowTex");
		LightUnit.Rendering(_Deltatime);
		GetOwnerScene().lock()->GetLightDataBuffer().LightCount++;
	}

	// 빛 합치기(Merge)
	LightMergeRenderTarget->RenderTargetClear();
	LightMergeRenderTarget->RenderTargetSetting();
	LightMergeUnit.Rendering(_Deltatime);

////////////////////////////// MRT Merge ///////////////////////////////
	CameraRenderTarget->RenderTargetClear();
	CameraRenderTarget->Merge(MeshRenderTarget, 0);
	CameraRenderTarget->Merge(LightMergeRenderTarget);
}

 

1. Geometry Pass : Mesh, Position, Normal Buffer 생성

2. Shadow Pass : Shadow Map 생성

3. Light Pass : Diffuse, Specular, Ambient Buffer 생성

 

 

3. G-Buffer 생성

: 셰이더를 통해 값을 생성해주고 G-Buffer에 담습니다. 여기서 중요한 변경점은 Output 구조체의 출력값입니다. 원래는 시스템 시멘틱과 attribute를 출력했지만, 이제부터는 SV_TARGET[n]를 출력합니다. 이러면 OMSetRenderTargets() 함수로 바인딩된 RenderTarget의 RTV들에게 출력 결과물이 저장됩니다.

// Static_VS, Dynamic_VS는 기존과 동일
// Graphics_PS
#include "LightData.fx"

Texture2D BaseColorTex : register(t0); // 텍스처 자원
SamplerState Sampler : register(s0); // 샘플러

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 TexCoord : TEXCOORD;
    float3 WorldPosition : POSITION0;
    float3 WorldViewPosition : POSITION1;
    float3 WorldNormal : NORMAL0;
    float3 WorldViewNormal : NORMAL1;
};

struct PSOutPut
{
    float4 MeshTarget : SV_TARGET0;
    float4 WPositionTarget : SV_TARGET1; // World
    float4 WVPositionTarget : SV_TARGET2; // WorldView
    float4 WNormalTarget : SV_TARGET3; // World
    float4 WVNormalTarget : SV_TARGET4; // WorldView
};

 // 각 벡터에 normalize를 해주는 이유는, 명시적으로 normalize된 벡터를 넣어줬다 하더라도 
 // 임의의 값이 어떻게 들어올지 모르기 때문에 그냥 해주는것(안정성을 위한 처리라고 보면 됨)
PSOutPut Graphics_PS(PSInput _Input) : SV_TARGET
{
    PSOutPut Output = (PSOutPut) 0;
    
    Output.MeshTarget = BaseColorTex.Sample(Sampler, _Input.TexCoord); // 텍스쳐컬러
    Output.WPositionTarget = float4(_Input.WorldPosition.xyz, 1.0f); // 월드스페이스 Position
    Output.WVPositionTarget = float4(_Input.WorldViewPosition.xyz, 1.0f); // 뷰스페이스 Position
    Output.WNormalTarget = float4(_Input.WorldNormal, 1.0f); // 월드스페이스 Normal
    Output.WVNormalTarget = float4(_Input.WorldViewNormal, 1.0f); // 뷰스페이스 Normal

    return Output;
}

 

위에는 Vertex Shader이고, Light는 Pixel Shader에서 진행해줍니다.

/////////////////////// DeferredLight_VS
struct VSInput
{
    float4 Position : POSITION;
    float4 Texcoord : TEXCOORD;
};

struct VSOutput
{
    float4 Position : SV_Position;
    float2 Texcoord : TEXCOORD;
};

VSOutput DeferredLight_VS(VSInput _Input)
{
    VSOutput OutPut = (VSOutput) 0;
    OutPut.Position = _Input.Position;
    OutPut.Texcoord = _Input.Texcoord.xy;
    
    return OutPut;
}

/////////////////////// DeferredLight_PS
#include "LightData.fx"

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 Texcoord : TEXCOORD;
};

struct PSOutPut
{
    float4 DiffuseTarget : SV_TARGET0;
    float4 SpecularTarget : SV_TARGET1;
    float4 AmbientTarget : SV_TARGET2;
    float4 ShadowTarget : SV_TARGET3;
};

Texture2D PositionTex : register(t0); // G-Buffer: 월드-스페이스 위치(x,y,z) + 1
Texture2D NormalTex : register(t1); // G-Buffer: 월드-스페이스 법선(x,y,z) + 1
Texture2D ShadowTex : register(t2);
SamplerState Sampler : register(s0);
SamplerComparisonState ShadowCompare : register(s1);

PSOutPut DeferredLight_PS(PSInput _Input) : SV_TARGET
{
    PSOutPut OutPut = (PSOutPut) 0;
        
    float3 WorldPos = PositionTex.Sample(Sampler, _Input.Texcoord).xyz;
    float3 WorldNorm = NormalTex.Sample(Sampler, _Input.Texcoord).xyz;
        
    LightData LTData = Lights[LightCount];
   
    // 공통 파라미터
    float Shininess = 32.0f;
    float3 EyePosition = LTData.CameraWorldPosition.xyz;
    float3 EyeDirection; // 계산 시점에 맞춰 설정
    
    float3 DiffuseLight = float3(0.0f, 0.0f, 0.0f);
    float3 SpecularLight = float3(0.0f, 0.0f, 0.0f);
    float3 AmbientLight = float3(0.0f, 0.0f, 0.0f);
    
    // Directional Light 분기
    if (LTData.LightType == 0)
    {
        // 라이트 방향: surface → light 이므로 LightForwardVector에 -1을 곱함
        float3 LightDirection = -LTData.LightForwardVector.xyz;

        // Diffuse / Specular / Ambient
        DiffuseLight = DiffuseLightCalculation(LightDirection, WorldNorm);
        SpecularLight = SpecularLightCalculation(LightDirection, WorldNorm, EyePosition, WorldPos, Shininess);
        AmbientLight = AmbientLightCalculation(LTData.LightColor.w);
        
        DiffuseLight *= LTData.LightColor.xyz;
        SpecularLight *= LTData.LightColor.xyz;
        AmbientLight *= LTData.LightColor.xyz;
    }
    else if (LTData.LightType == 1)  // Point Light 분기
    {
        // 픽셀 → 광원 벡터
        float3 PixelToLight = LTData.LightWorldPosition.xyz - WorldPos;
        float Distance = length(PixelToLight);
        float3 Vector = normalize(PixelToLight);

        // Diffuse / Specular / Ambient
        DiffuseLight = DiffuseLightCalculation(Vector, WorldNorm);
        SpecularLight = SpecularLightCalculation(Vector, WorldNorm, EyePosition, WorldPos, Shininess);
        AmbientLight = AmbientLightCalculation(LTData.LightColor.w);

        // 거리 기반 감쇠
        float C0 = 1.0f;
        float C1 = 0.0f;
        float C2 = LTData.AttenuationValue / (LTData.FarDistance * LTData.FarDistance);
        float Attenuation = 1.0f / (C0 + C1 * Distance + C2 * Distance * Distance);
        
        DiffuseLight *= Attenuation;
        SpecularLight *= Attenuation;
        AmbientLight *= Attenuation;
        
        DiffuseLight *= LTData.LightColor.xyz;
        SpecularLight *= LTData.LightColor.xyz;
        AmbientLight *= LTData.LightColor.xyz;
    }

    if (DiffuseLight.x > 0.0f)
    {
        // 나중에 셰도우 처리할 곳
        OutPut.ShadowTarget = float4(0.0f, 0.0f, 0.0f, 0.0f);
    }
    
    OutPut.DiffuseTarget = float4(DiffuseLight, 1.0f);
    OutPut.SpecularTarget = float4(SpecularLight, 1.0f);
    OutPut.AmbientTarget = float4(AmbientLight, 1.0f);
    
    return OutPut; // 카메라 행렬 빛의 위치 그려져있는 빛을 기반으로한 깊이 버퍼 텍스처 리턴
}

 

 

4. Merge

: 위의 셰이더들을 통해 Mesh, Position, Normal, Diffuse, Specular, Ambient, Shadow 값이 Buffer들(RenderTarget)에 담깁니다. 이제 이들을 하나로 합쳐줍니다. 여기서 각 Buffer값(Texture인데 실제로는 SRV)을 합쳐주기 위한 셰이더의 슬롯에 바인딩해줍니다.

//////////////////////// DeferredMerge_VS
struct VSInput
{
    float4 Position : POSITION;
    float4 Texcoord : TEXCOORD;
};

struct VSOutput
{
    float4 Position : SV_Position;
    float2 Texcoord : TEXCOORD;
};

VSOutput DeferredMerge_VS(VSInput _Input)
{
    VSOutput OutPut = (VSOutput) 0;
    OutPut.Position = _Input.Position;
    OutPut.Texcoord = _Input.Texcoord.xy;
    
    return OutPut;
}

//////////////////////// DeferredMerge_PS
#include "LightData.fx"

struct PSInput
{
    float4 POSITION : SV_POSITION;
    float2 TEXCOORD : TEXCOORD;
};

struct PSOutput
{
    float4 Color : SV_TARGET;
};

Texture2D BaseColorTex : register(t0);
Texture2D DiffuseTex : register(t1);
Texture2D SpecularTex : register(t2);
Texture2D AmbientTex : register(t3);
Texture2D ShadowTex : register(t4); // 나중에 사용할 것
SamplerState PointWrap : register(s0);

PSOutput DeferredMerge_PS(PSInput _Input) : SV_TARGET
{
    PSOutput OutPut = (PSOutput) 0;
    
    float4 Albedo = BaseColorTex.Sample(PointWrap, _Input.TEXCOORD);
    float4 DiffuseRatio = DiffuseTex.Sample(PointWrap, _Input.TEXCOORD);
    float4 SpacularRatio = SpecularTex.Sample(PointWrap, _Input.TEXCOORD);
    float4 AmbientRatio = AmbientTex.Sample(PointWrap, _Input.TEXCOORD);
    float ShadowMask = ShadowTex.Sample(PointWrap, _Input.TEXCOORD).x;
    
    // 그림자 어두운 정도 (0.0 = 안 어두움, 1.0 = 완전 검정), 테스트중인 그림자
    float ShadowStrength = 0.7;
    float ShadowFactor = lerp(1.0, 1.0 - ShadowStrength, ShadowMask);
    
    if (Albedo.a)
    {
        OutPut.Color.xyz = Albedo.xyz * (DiffuseRatio.xyz + SpacularRatio.xyz + AmbientRatio.xyz);
        OutPut.Color.a = saturate(Albedo.a + (DiffuseRatio.w + SpacularRatio.w + AmbientRatio.w));
        OutPut.Color.a = 1.0f;
    }
    else
    {
        OutPut.Color.xyz = (DiffuseRatio.xyz + SpacularRatio.xyz + AmbientRatio.xyz);
        OutPut.Color.a = saturate(OutPut.Color.x);
    }
    
    return OutPut;
}

 

위에는 결과들을 합쳐주는 셰이더이고, 그냥 렌더타겟 끼리 합쳐주는 셰이더는 아래를 활용합니다.

//////////////////////// RenderTargetMerge_VS
struct VSInput
{
    float4 Position : POSITION;
    float4 Texcoord : TEXCOORD;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float4 Texcoord : TEXCOORD;
};

VSOutput RenderTargetMerge_VS(VSInput _Input)
{
    VSOutput OutPut = (VSOutput) 0;
    OutPut.Position = _Input.Position;
    OutPut.Texcoord = _Input.Texcoord;
    
    return OutPut;
}

//////////////////////// RenderTargetMerge_PS
Texture2D DiffuseTex : register(t0);
SamplerState AlwaysSampler : register(s0);

struct PSInput
{
    float4 Position : SV_POSITION;
    float4 Texcoord : TEXCOORD;
};

float4 RenderTargetMerge_PS(PSInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTex.Sample(AlwaysSampler, _Input.Texcoord.xy);
    Color.a = saturate(Color.a);
    
    return Color;
}

 

 

5. Material Setting

: Material 값들은 다음과 같이 사용했습니다.

// Deffered
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("DeferredLight");
	NewRenderingPipeline->SetVertexShader("DeferredLight_VS");
	NewRenderingPipeline->SetPixelShader("DeferredLight_PS");
	NewRenderingPipeline->SetBlendState("OneBlend");
	NewRenderingPipeline->SetDepthState("AlwayDepth");
	NewRenderingPipeline->SetRasterizer("NonCullingRasterizer");
}

// Deffered된 결과물 Merge용
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("DeferredMerge");
	NewRenderingPipeline->SetVertexShader("DeferredMerge_VS");
	NewRenderingPipeline->SetPixelShader("DeferredMerge_PS");
	NewRenderingPipeline->SetBlendState("AlphaBlend");
	NewRenderingPipeline->SetDepthState("AlwayDepth");
	NewRenderingPipeline->SetRasterizer("NonCullingRasterizer");
}

// 렌더타겟 간 Merge를 위한 머티리얼
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("RenderTargetMerge");

	NewRenderingPipeline->SetVertexShader("RenderTargetMerge_VS");
	NewRenderingPipeline->SetPixelShader("RenderTargetMerge_PS");
	NewRenderingPipeline->SetBlendState("MergeBlend");
	NewRenderingPipeline->SetDepthState("AlwayDepth");
	NewRenderingPipeline->SetRasterizer("NonCullingRasterizer");
}

 

새로 추가된 항목이 있습니다.

 

- OneBlend와 MergeBlend

// One 블렌드, Deffered용
{
	D3D11_BLEND_DESC BlendInfo = { 0, };

	BlendInfo.AlphaToCoverageEnable = false;
	BlendInfo.IndependentBlendEnable = false;
	BlendInfo.RenderTarget[0].BlendEnable = true;

	BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
	BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;
	BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;

	BlendInfo.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
	BlendInfo.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
	BlendInfo.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;

	BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

	Ext_DirectXBlend::CreateBlend("OneBlend", BlendInfo);
}

// Merge용 블렌드
{
	D3D11_BLEND_DESC BlendInfo = { 0, };

	BlendInfo.AlphaToCoverageEnable = false;
	BlendInfo.IndependentBlendEnable = false;
	BlendInfo.RenderTarget[0].BlendEnable = true;

	BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
	BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
	BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;

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

	BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
        
	Ext_DirectXBlend::CreateBlend("MergeBlend", BlendInfo);
}

 

One 블렌드 공식은 다음과 같습니다.

OutputColor = SrcColor * 1 + DestColor * 1
            = SrcColor + DestColor

 

Additive Blending인데, 디퍼드 라이팅 조명 패스에서 여러 광원 결과를 누적할 때 주로 사용됩니다.

 

Merge 블렌드 공식은 다음과 같습니다.

OutputColor = SrcColor * 1 + DestColor * (1 - SrcAlpha)
            = SrcColor + DestColor * (1 - SrcAlpha)

 

일반적인 투명도 블렌딩 공식과 거의 같은데, Src에 1을 곱하는 것만 다릅니다. 해당 방식은 Deferred Merge나 반투명 오브젝트 렌더링 시에 활용되는 방식입니다.

 

- AlwaysDepth

// AlwayDepth
{
	D3D11_DEPTH_STENCIL_DESC DepthStencilInfo = { 0, };

	DepthStencilInfo.DepthEnable = true;
	DepthStencilInfo.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
	DepthStencilInfo.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_ALWAYS;
	DepthStencilInfo.StencilEnable = false;

	Ext_DirectXDepth::CreateDepthStencilState("AlwayDepth", DepthStencilInfo);
}

 

보통 Depth는 D3D11_DEPTH_WRITE_MASK_ALL에 D3D11_COMPARISON_LESS을 활용하여 깊이 테스트를 진행해 현재 픽셀이 이전 픽셀보다 더 가까울 때만 그리도록 합니다.

 

하지만 위의 DepthStancilState는 깊이 테스트를 무조건 통과시키는 방식입니다. 깊이 버퍼 값도 무조건 현재 픽셀 값으로 덮어씌웁니다. 렌더타겟간 Merge를 실시할 때는 해당 DepthStencilState를 주로 활용합니다.

 

- NonCullingRasterizer

// None컬링 레스터라이저
{
	D3D11_RASTERIZER_DESC Desc = {};

	Desc.CullMode = D3D11_CULL_NONE;
	Desc.FrontCounterClockwise = FALSE;
	Desc.FillMode = D3D11_FILL_SOLID;

	std::shared_ptr<Ext_DirectXRasterizer> NewRast = Ext_DirectXRasterizer::CreateRasterizer("NonCullingRasterizer", Desc);
}

 

이건 아예 후면 컬링을 진행하지 않는다는 Rasterizer인데, 사실 후면 컬링을 진행해도 상관 없습니다. 그래도 2D Texture를 다룰 때는 혹시 모를 상황을 방지하고자 NONE culling을 실시합니다.

 

6. G-Buffer 메모리 공간 확보

: Ext_Camera가 메모리들을 가지고 있을 것입니다. 메모리는 RenderTarget 형태로 존재합니다.

void Ext_Camera::Start()
{
	// ...
    
	// 메인패스 렌더타겟 - MeshTarget, PositionTarget, NormalTarget
	MeshRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 0 MeshTarget
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 1 PositionTarget (World)
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 2 PositionTarget (WorldView)
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 3 NormalTarget (World)
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 4 NormalTarget (WorldView)
	MeshRenderTarget->CreateDepthTexture();

	// 디퍼드 라이트 계산 렌더 타겟(쉐도우 뎁스까지 계산) - DiffuseTarget, SpecularTarget, AmbientTarget, ShadowTarget
	LightRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 0 DiffuseTarget
	LightRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 1 SpecularTarget
	LightRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 2 AmbientTarget
	LightRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 3 ShadowTarget
	LightRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 4 ShadowDepthTarget

	// 디퍼드 라이트 Merge 타겟 - DSA를 하나로 만듬
	LightMergeRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // Light들 Merge하는 렌더타겟

	// 카메라 최종 렌더타겟
	CameraRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 해당 카메라의 최종 결과물 타겟

	// LightRenderTarget(디퍼드 라이트 계산)을 위한 Unit
	LightUnit.MeshComponentUnitInitialize("FullRect", MaterialType::DeferredLight);
	const LightDatas& LTDatas = GetOwnerScene().lock()->GetLightDataBuffer();
	LightUnit.BufferSetter.SetConstantBufferLink("LightDatas", LTDatas);
	LightUnit.BufferSetter.SetTexture(MeshRenderTarget->GetTexture(1), "PositionTex");
	LightUnit.BufferSetter.SetTexture(MeshRenderTarget->GetTexture(3), "NormalTex");
	LightUnit.BufferSetter.SetTexture("Null.png", TextureType::Shadow);

	// LightMergeRenderTarget(디퍼드 라이트 Merge)를 위한 Unit
	LightMergeUnit.MeshComponentUnitInitialize("FullRect", MaterialType::DeferredMerge);
	LightMergeUnit.BufferSetter.SetTexture(MeshRenderTarget->GetTexture(0), "BaseColorTex");
	LightMergeUnit.BufferSetter.SetTexture(LightRenderTarget->GetTexture(0), "DiffuseTex");
	LightMergeUnit.BufferSetter.SetTexture(LightRenderTarget->GetTexture(1), "SpecularTex");
	LightMergeUnit.BufferSetter.SetTexture(LightRenderTarget->GetTexture(2), "AmbientTex");
	LightMergeUnit.BufferSetter.SetTexture(LightRenderTarget->GetTexture(3), "ShadowTex");
}

 

BufferSetting을 각 RenderTarget마다 진행할 것이기 때문에, 렌더링 파이프라인 세팅을 위하여 각각의 Unit들을 만들어줬습니다. 여기서 FullRect를 활용하는데, 이러면 화면에 꽉 차는 사각형이 하나 만들어질 것이며, 여기에 결과물들을 그려주게 될 것입니다.

 

 

[디퍼드 렌더링을 위해 필요한 기능들]

 

1. 셰이더 리소스 뷰(SRV)

: 원래도 Texture를 활용할 때 사용하고 있었는데, RenderTarget을 텍스쳐 슬롯에 바인딩 할때도 똑같이 해당 기능을 활용하게 됩니다.

// 텍스쳐를 사용한 경우, 여기서 추가로 VSSetting 실시
void Ext_DirectXTexture::VSSetting(UINT _Slot)
{
	if (nullptr == SRV)
	{
		MsgAssert("SRV가 존재하지 않는 텍스처를 쉐이더에 세팅할수 없습니다.");
		return;
	}

	Ext_DirectXDevice::GetContext()->VSSetShaderResources(_Slot, 1, &SRV);
}

// 텍스쳐를 사용한 경우, 여기서 추가로 PSSetting 실시
void Ext_DirectXTexture::PSSetting(UINT _Slot)
{
	if (nullptr == SRV)
	{
		MsgAssert("SRV가 존재하지 않는 텍스처를 쉐이더에 세팅할수 없습니다.");
		return;
	}

	Ext_DirectXDevice::GetContext()->PSSetShaderResources(_Slot, 1, &SRV);
}

 

SRV에 대해서 다시 짚고 넘어가자면, 그림판을 어떻게 다룰지에 대한 설명서라고 보시면 됩니다. 어떤 Slot 정보를 가져야 하는지, Format은 뭔지, 몇 차원(Dimentions) 그림인지, MipMap 수준은 어떻게 되는지, 데이터 Array는 어떤지를 SRV가 가지고 있습니다. 

 

SRV 바인딩은 주로 Pixel Shader에서 실시하기 떄문에, PSSetShaderResources() 함수를 호출하여 여기에 텍스쳐의 SRV를 바인딩해주면 현재 바인딩된 Pixel Shader의 t 슬롯에 바인딩 됩니다. Multi Render Target의 경우에는 한꺼번에 넘겨줘서 바인딩시킬 수 있긴 한데, 해당 프레임워크는 그냥 반복문을 돌면서 하나씩 바인딩 해주는 걸로 진행했습니다.

 

 

2. Multi Render Target(MRT)

: 말 그대로 여러 개의 렌더 타겟인데, 위에서 설명한 것과 같이 출력 결과를 RenderTarget 여러 개로 지정할 수 있습니다.

struct PSOutPut
{
    float4 DiffuseTarget : SV_TARGET0;
    float4 SpecularTarget : SV_TARGET1;
    float4 AmbientTarget : SV_TARGET2;
    float4 ShadowTarget : SV_TARGET3;
};

PSOutPut DeferredLight_PS(PSInput _Input) : SV_TARGET
{
 // TODO
}

 

이를 위해서는 RenderTarget이 여러 개의 RTV를 가지고 있어야 하며, OMSetRenderTarget을 통해 렌더링 파이프라인에 바인딩되어야 합니다. 이것도 이미 만들어둔 것이기 때문에 그냥 사용하면 됩니다.

 

void Ext_DirectXRenderTarget::RenderTargetSetting()
{
	ID3D11RenderTargetView** RTV = &RTVs[0];
	COMPTR<ID3D11DepthStencilView> DSV = DepthTexture != nullptr ? DepthTexture->GetDSV() : nullptr;
	
	Ext_DirectXDevice::GetContext()->OMSetRenderTargets(static_cast<UINT>(RTVs.size()), RTV, DSV.Get()); // Output-Merger 스테이지에 렌더 타겟 + 뎁스 설정
	Ext_DirectXDevice::GetContext()->RSSetViewports(static_cast<UINT>(ViewPorts.size()), &ViewPorts[0]); // Rasterizer 스테이지에 현재 프레임에서 사용할 ViewPort 영역 설정, 이게 있어야 NDC > 픽셀 공간 변환이 올바르게 수행됨
}

 

맨 처음에 RenderTarget에 저장되는 자료 구조를 굳이 컨테이너로 만든 이유가 MRT 때문입니다.

private:
	std::vector<float4> Colors; // 생성된 렌더타겟 색상 저장
	std::vector<std::shared_ptr<Ext_DirectXTexture>> Textures = {}; // Ext_DirectXTexture(생성 주체) 포인터 저장 컨테이너
	std::shared_ptr<Ext_DirectXTexture> DepthTexture = {};
	std::vector<D3D11_VIEWPORT> ViewPorts = {}; // 생성 주체의 ViewPort 정보 저장 컨테이너
	std::vector<COMPTR<ID3D11RenderTargetView>> RTVs = {}; // 렌더타겟뷰들 저장
	std::vector<COMPTR<ID3D11ShaderResourceView>> SRVs = {}; // 셰이더리소스뷰들 저장
    //...

 

여러 개의 RTV와 SRV를 만드는 과정을 위해 AddNewTexture() 함수를 추가해뒀습니다.

// 카메라 생성 시 호출
void Ext_Camera::Start()
{
	// 렌더 타겟에 Texture2D, RTV, SRV들을 만들고 순서대로 저장
	MeshRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 0 MeshTarget
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 1 PositionTarget (World)
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 2 PositionTarget (WorldView)
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 3 NormalTarget (World)
	MeshRenderTarget->AddNewTexture(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 4 NormalTarget (WorldView)
	MeshRenderTarget->CreateDepthTexture();
    
    // ,,,
}

 

이제 한번에 출력하면 하나의 렌더 타겟에서 여러 개의 결과물을 확인할 수 있습니다.

 

 

3. Merge와 Blending

결과물 출력도 출력이지만, 서로 간의 Merge, 그리고 Blending 과정도 필수입니다. 

 

Light를 그리는 과정을 예시로 확인해보겠습니다. 메인 렌더 패스 이후 진행되는 라이트 패스는 다음의 순서를 따릅니다.

[1] LightRenderTarget->RenderTargetClear();
[2] LightRenderTarget->RenderTargetSetting();

GetOwnerScene().lock()->GetLightDataBuffer().LightCount = 0; // 라이트 업데이트 전, 상수버퍼 갯수 초기화(순회하면서 넣어줘야하기 때문)
for (auto& [name, CurLight] : Lights)
{
	//LightUnit.BufferSetter.SetTexture(CurLight->GetShadowRenderTarget()->GetTexture(0), "ShadowTex");
	[3] LightUnit.Rendering(_Deltatime);
	GetOwnerScene().lock()->GetLightDataBuffer().LightCount++;
}

// 빛 합치기
[4] LightMergeRenderTarget->RenderTargetClear();
[5] LightMergeRenderTarget->RenderTargetSetting();
[6] LightMergeUnit.Rendering(_Deltatime);

 

프레임워크 코드에서 그대로 가져온건데, 1번부터 6번까지 순서대로 설명드리겠습니다.

 

[1], [2] LightRenderTarget->

: 내부에 확인해보시면, 기존에 백버퍼 세팅과 동일하게 Clear를 진행하고 OMSetRenderTargets로 렌더 타겟을 바인딩해주고 있습니다.

 

[3] Rendering

: 기존과 동일하게 Mesh, Material에 대해서 렌더링 파이프라인을 설정하고, 드로우 콜을 호출합니다. 라이트 버퍼 생성에 사용된 머티리얼 세팅이 기존과 다른 점은 Depth, Blend, Rasterizer가 있습니다.

/////////////// Depth
////// 기존 Depth - EQUAL
D3D11_DEPTH_STENCIL_DESC DepthStencilInfo = { 0, };

DepthStencilInfo.DepthEnable = true;
DepthStencilInfo.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
DepthStencilInfo.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL;
DepthStencilInfo.StencilEnable = false;

////// 변경 Depth - ALWAYS
D3D11_DEPTH_STENCIL_DESC DepthStencilInfo = { 0, };

DepthStencilInfo.DepthEnable = true;
DepthStencilInfo.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
DepthStencilInfo.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_ALWAYS;
DepthStencilInfo.StencilEnable = false;

/////////////// Blend
////// 기존 Blend - BaseBlend
D3D11_BLEND_DESC BlendInfo = { 0, };

BlendInfo.AlphaToCoverageEnable = false;
BlendInfo.IndependentBlendEnable = false;
BlendInfo.RenderTarget[0].BlendEnable = true;

BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;

BlendInfo.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;

BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

////// 변경 Blend - OneBlend
D3D11_BLEND_DESC BlendInfo = { 0, };

BlendInfo.AlphaToCoverageEnable = false;
BlendInfo.IndependentBlendEnable = false;
BlendInfo.RenderTarget[0].BlendEnable = true;

BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;

BlendInfo.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;

BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

/////////////// Rasterizer
////// 기존 Rasterizer - BackFace culling
D3D11_RASTERIZER_DESC Desc = {};

Desc.CullMode = D3D11_CULL_BACK;
Desc.FrontCounterClockwise = TRUE;
Desc.FillMode = D3D11_FILL_SOLID;

////// 변경 Rasterizer - Non culling
D3D11_RASTERIZER_DESC Desc = {};

Desc.CullMode = D3D11_CULL_NONE;
Desc.FrontCounterClockwise = FALSE;
Desc.FillMode = D3D11_FILL_SOLID;

 

- Depth

: 기존의 패스(Geometry Pass)에서는 깊이 버퍼에 기록된 것보다 뒤에 존재하는 픽셀은 가린다는 의미로 LESS_EQUAL을 사용했지만, Lighting 패스에서는 전체 화면(FullRect)에 대해 조명 값을 누적할 때,

 

- 이미 기존 패스로 Depth만큼 채워진 픽셀에 대해서만 조명을 적용

- 실제로 깊이 비교(EQUAL) 대신 그냥 모든 스크린 픽셀에 대해서만 조명 계산을 하는게 단순

- 정밀도 차이로 픽셀이 빠져나가는 현상을 방지

 

를 위해서 깊이 테스트를 우회(ALWAYS)하고자 AlwaysDepth로 설정해줍니다. 이러면 조명 대상 컬링은 스텐실이나 마스킹으로 처리하게 됩니다.

 

- Blend : 빛 기여를 정확히 덧셈하여 누적

: 기존의 패스는 투명도 기반 혼합 방식(Src = SRC_ALPHA, Dest = INV_SRC_ALPHA)을 썼지만, Lighting 패스는 덧셈 방식(Additive)으로 값을 누적해야 합니다. Diffuse, Specular, Ambient는 서로 그냥 더해야 최종 밝기가 나오는데, 여기서 만약 기존의 블렌딩 방식을 써버리면 투명도 기반으로만 섞이기 때문에 조명에 관계없는 알파값이 섞여버려 오작동이 발생하거나 부자연스러운 결과가 발생할 수 있습니다. 따라서 Src와 Dest를 ONE, Op를 ADD로 설정하여 빛의 세기만 더하도록 변경해줍니다.

 

- Rasterizer : 전체 화면/라이트 볼륨 모든 면에 대해 조명을 적용

: Light 패스는 FullRect로 화면을 덮어서 활용하는데, 라이트 매스를 활용할 수도 있긴 합니다. 이럴 경우에는 만약 후면 컬링이 켜져 있으면 뷰 방향에 따라서 일부 폴리곤(뒷면)이 잘려나가 제대로 조명이 적용되지 안흔ㄴ 영역이 발생할 수 있습니다. 따라서 아무 면도 걸러내지 않도록 Cull 모드를 NONE으로 설정해줍니다. 사실 FullRect로 화면 전체를 덮어버린다면 그냥 원래 것을 사용해도 무방하긴 합니다.

 

[4], [5] LightMergeRenderTarget->

: 1부터 3번까지 진행하면서 Diffuse, Specular, Ambient 라이트 버퍼를 생성했기 때문에, 이제 이 결과들을 하나로 합쳐줘야 합니다. 이를 위해서 하나로 합쳐줄 렌더 타겟으로 바인딩해줍니다. 여기서는 빛을 합치는 것 외에 다양한 처리를 진행할 수 있습니다(나중에 쉐도우도 활용할 예정)

 

[6] Rendering

: [3]에서 실시한 Rendering 과정과 다른게 딱 하나 입니다. 여기서는 Texture의 Image 값들에 대해서 합치기 때문에, Blend가 다시 원래의 AlphaBlend(BaseBlend)로 해주면 됩니다.

 

1번부터 6번까지 진행했으면, Mesh, Light 값이 저장되어 있을 것입니다. 이걸 하나의 렌더 타겟에 다시 Merge 해줍니다. 뭔가 Merge라는 함수를 만들어서 특별하게 처리하는것 같지만, 실상은 위의 LightMergeRenderTarget에서 Merge 처리했던 방식과 크게 다른게 없습니다. 그냥 다른 렌더 타겟의 Texture2D Image 값을 가져와서 현재의 RenderTarget에 Draw 하는 것입니다. 순서대로 Draw 콜 해주면, 최종 결과물이 나옵니다.

 

[포워드 렌더링]

 

포워드 렌더링은 전통적인 렌더링 방식으로, 라이트 처리를 오브젝트마다 실시하는 방식입니다.

10개의 오브젝트와 10개의 라이트가 있다고 가정해보겠습니다. 이러면 포워드 렌더링의 경우, 각 오브젝트마다 라이팅 연산을 10번씩 실시해주면 됩니다(10*10 = 100). 비교적 단순하게 구현할 수 있는 렌더링 방식이지만, 치명적인 문제가 바로 단순함에 있습니다.

 

방금 말씀드린것 처럼 10개의 오브젝트와 10개의 빛이 있을 때 100번의 연산을 실시한다고 했는데, 만약 100개씩, 1000개씩 있으면 어떻게 될까요? 1000 * 1000번의 연산을 실시하게 될 것입니다. 또한, 물체에 가려져 화면에 보이지 않는 오브젝트에 대해서도 컬링을 하지 않고 빛 연산을 실시하는 경우가 있기도 합니다.

 

단점만 말씀드려서 이걸 왜쓰지라는 생각이 드실 수도 있으실테지만, 포워드 렌더링도 장점이 있습니다.

 

1. 메모리 대역폭

: 이후 말씀드릴 디퍼드 렌더링의 경우에는 G-Buffer라는 것을 사용하기 때문에 다수의 렌더 타겟을 필요로 합니다. 렌더 타겟은 모두 텍스쳐라고 생각하시면 편한데, 렌더 타겟의 크기는 픽셀을 표현할 크기와 픽셀 갯수로 정해집니다. 예를들어 R32G32B32A32로 픽셀을 표현하고 해상도의 크기가 1920 * 1080라고 가정해봅시다. 단순하게 계산해도 하나의 렌더 타겟에 약 2억6천만bit(약 33MB)가 됩니다. 

 

하지만 이게 하나의 렌더 타겟만 있는게 아니라 다수의 렌더 타겟(MRT)을 사용하는 방식이기 때문에 실제로는 33MB보다 훨씬 큽니다. 이걸 프레임별로 계속해서 처리해야하는데, 메모리 대역폭이 제한적인 모바일 환경에서는 감당하기 힘든 수준이 됩니다. 때문에 비교적 효율적이고 전통적인 포워드 렌더링을 활용합니다(물론 그냥 포워드 렌더링이 아니라 포워드+ 렌더링을 사용할 것입니다). 

 

2. 하드웨어 제한

: 메모리 대역폭의 제한 때문도 있지만, 하드웨어 자체의 한계로 인해 포워드 렌더링을 사용하는 것이 좋은 경우도 있습니다. 모바일 환경과 같은 경우 GPU가 MRT를 4개 이상 지원하지 않거나, 지원하더라도 성능 저하가 발생할 우려가 있어 G-Buffer의 구축이 어렵기 때문에 포워드 렌더링을 활용하는 경우가 있습니다.

 

3. 라이트 수와 복잡도

: Scene에 라이트가 디렉셔널, 포인트 라이트 두 개만 있다고 가정해봅시다. 이럴 경우, 굳이 G-Buffer를 쓰면서까지 라이팅 처리를 해야할까요? 라이트가 제한된 환경에서는 포워드 렌더링이 구현 복잡도도 낮고, 메모리 낭비를 하지 않을 수 있기 때문에 디퍼드 렌더링보다 더 효율적 일 수 있습니다.

 

 

[포워드+ 렌더링]

 

그렇다면 포워드 렌더링 방식에서 빛 연산 횟수를 줄이면 좋을 것입니다. 이를 위해 포워드+ 렌더링이 생겨났습니다. 해당 렌더링 방식은 기존과 같이 연산을 실시하는 과정 앞에 조명 컬링을 위한 렌더링 패스를 한 번 진행하게 됩니다.

 

이 과정에서 화면에 그려지는 공간을 균등한 타일 그리드로 나누고, 정보(Light Heatmap)를 저장해둡니다.

https://www.3dgep.com/forward-plus/

 

다음 패스에서 일반적인 포워드 렌더링 패스를 진행하는데, 앞에서 저장한 정보를 활용합니다. 이 정보를 활용하여 조명 처리를 진행할 오브젝트에 대해서만 조명 처리를 실시하게 됩니다. 

 

물론 그리드를 어떻게 나누는지에 따라(많이 나눌 것이냐, 적게 나눌 것이냐) Light Heatmap의 정보 처리가 Light 처리보다 커질 수도 있고(배보다 배꼽이 커지는 경우), 불필요한 연산을 제대로 컬링하지 못하는 수도 있습니다.

 

 

[디퍼드 렌더링]

 

기존의 포워드 렌더링이 오브젝트 하나에 대해 빛 연산을 실시하고 드로우콜을 호출하여 오브젝트를 화면에 바로바로 그리는 방식이었다면, 디퍼드 렌더링은 말 그대로 렌더링을 지연시키는 것입니다. 가장 큰 이유는 바로 빛 연산을 한번에 처리해서 적용시키기 위함입니다.

 

디퍼드 렌더링은 여러 개의 렌더 타겟(멀티 렌더 타겟;MRT)을 버퍼(G-Buffer)로 활용하면서 서로 간에 블렌딩을 통해 하나의 렌더 타겟을 만들어 냅니다.

디퍼드 렌더링의 빛 연산 과정은 다음과 같습니다.

 

1. 각 오브젝트들의 Position, Normal 정보를 담은 Buffer를 만듦

2. Position, Normal Buffer를 통해 Diffuse, Specular, Ambient 등 라이팅 연산 실시 및 Buffer를 만듦

3. 라이트 버퍼와 메시가 렌더링된 렌더 타겟 간에 블렌딩 과정을 통해 최종 렌더링 결과물 출력 및 백 버퍼에 전달

 

위 방식대로 진행하면, 화면에 몇 개의 오브젝트가 존재하던지 간에 라이트의 갯수와 화면의 픽셀 갯수 만큼만 연산을 실시하게 됩니다. 덕분에 오브젝트와 광원 수가 늘어나더라도, 포워드 렌더링과 같이 기하급수적으로 라이팅 처리 비용이 늘어나지는 않습니다.

 

그대로 장점만 있는 것은 아닙니다. 앞서 포워드 렌더링의 장점이 곧 디퍼드 렌더링의 단점이라고 볼 수 있겠습니다.

 

1. G-Buffer를 위한 메모리 대역폭 확보 필요

: MRT를 활용하기 때문에 이들을 저장하고 활용하기 위한 공간 확보는 필수입니다. 이 때문에 기본적으로 디퍼드 렌더링을 위해서는 메모리 대역폭의 확보가 필요합니다.

 

2. MRT 지원 하드웨어 필요

: GPU가 MRT를 지원하지 않거나, 지원하더라고 수의 제한이 있거나, 사용 시에 성능이 현저히 떨어질 수 있습니다. 디퍼드 렌더링을 사용하더라도 하드웨어의 성능이 받쳐줘야합니다.

 

3. 라이트 수에 대한 딜레마

: 포워드 렌더링은 메모리를 덜 쓰고 연산을 많이 하는 방식이었다면, 디퍼드 렌더링은 메모리를 많이 써서 연산을 덜 하는 방식을 취합니다. 하지만 배보다 배꼽이 커지는 경우가 있을 수 있는데, Scene에 라이트가 단 하나만 존재한다면 어떻게 될까요? 이럴 때는 그냥 라이트 하나에 대해서만 라이팅 연산을 진행하고, 구현이 쉬운 포워드 렌더링을 그대로 쓰는 것이 좋을 것입니다. 하지만 굳이 디퍼드 렌더링을 활용하게 되면, 필수적으로 G-Buffer를 만들 것이고, 이를 활용해야하기 때문에 최적화를 위해 만들어진 디퍼드 렌더링 방식이 오히려 독이 될 수 있습니다.

'DirectX11 > 그래픽스' 카테고리의 다른 글

[Graphics] Rendering Pipeline  (0) 2025.06.18
[Graphics] 드로우콜(Draw Call)  (0) 2025.06.11
[Graphics] Normal Mapping  (0) 2025.06.07
[Graphics] Normal  (0) 2025.06.06
[Graphics] Lighting  (2) 2025.06.05

[목차]

 

- 노말 매핑을 한 이유

- 셰이더 변경

- 결과

 

 

[노말 매핑을 한 이유]

 

빛도 만들었겠다, 빛을 가장 다이나믹하게 활용할 수 있는 텍스쳐 기법 중 노말 매핑을 진행해보고자 마음먹었습니다. 이를 위해서 Fab에서 텍스쳐 샘플을 찾아봤는데 적당한게 있어서 가져왔습니다.

 

이름이 Stone_Wall_xxx 이런 식이였는데 그냥 제가 보기 편한대로 이름을 변경했습니다. 여기서 활용할 것은 StoneWall_BaseColor와 StoneWall_Normal입니다.

 

 

[셰이더 변경]

 

먼저 Normal Texture를 사용해야하니까, Texture 슬롯을 하나 선언해야합니다. BaseColorTex에서 t0 슬롯을 이미 사용중이어서, 그냥 적당하게 바로 다음인 t1에 NormalTex라는 이름으로 추가해줬습니다.

Texture2D BaseColorTex : register(t0);
Texture2D NormalTex : register(t1);

 

텍스쳐들에 Roughness, Specular(Metalic), AmbientOcclusion, Emissive 등이 있는데 나중에 시간이 나면 천천히 해볼까 합니다.

 

텍스쳐 슬롯은 등록했으니, 텍스쳐를 로드하고 세팅해주면 됩니다. 그냥 기존에 만들어뒀던 기능을 바로 활용했습니다.

void StoneWallActor::Start()
{
	WallBody = CreateComponent<Ext_CollisionComponent>("WallBody", static_cast<int>(CollisionGroup::Wall));
	WallBody->SetCollsionType(CollsionType::OBB3D);

	WallBodyMesh = CreateComponent<Ext_MeshComponent>("WallBodyMesh");
	WallBodyMesh->CreateMeshComponentUnit("Rect", MaterialType::PBR);
	WallBodyMesh->SetTexture("StoneWall_BaseColor.jpg", TextureType::BaseColor);
	WallBodyMesh->SetTexture("StoneWall_Normal.jpg", TextureType::Normal);

	GetTransform()->SetLocalScale({ 300.f, 300.f, 1.f });
}

 

이렇게 해주면 BaseColorTex 슬롯에는 [StoneWall_BaseColor.jpg]이름의 2DTexture가, NormalTex 슬롯에는 [StoneWall_Normal.jpg]이름의 2DTexture가 저장될 것입니다. Material은 PBR이라고 해서 기존의 Graphics_VS, PS를 그대로 복사한 것을 새로 하나 만들어줬습니다. 기존껀 두고 이걸 수정하려고 합니다.

 

이제 불러온 값을 활용해야합니다. 해당 텍스쳐는 "탄젠트 스페이스 노말맵"입니다. 처음에 노말 매핑에 대해 공부할 때는, 뭐가 월드고 뭐가 탄젠트고 헷갈렸는데, 그림 하나를 보니 바로 구분이 가능해졌습니다.

 

그리고 근래에는 거의 무조건 스태틱이든 다이나믹이든 탄젠트 스페이스 노말맵을 사용한다고 합니다. 하드웨어 성능이 좋아져서 픽셀 셰이더에서 TBN 행렬을 적용하는 과정이 큰 무리가 없나봅니다.

 

이제 정점 셰이더를 수정해줍니다. Input값이 기존과 다르게 변경되었는데, 바로 float4 Tangent : TANGENT, float4 Binormal : BINORMAL가 추가됐다는 점입니다.

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

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

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

struct VSInput
{
    float4 Position : POSITION;
    float4 TexCoord : TEXCOORD;
    float4 Normal : NORMAL;
    float4 Tangent : TANGENT; // 로컬 접선
    float4 Binormal : BINORMAL; // 로컬 이접선
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float4 TexCoord : TEXCOORD;
    float3 WorldPosition : POSITION;
    float3 WorldNormal : NORMAL;
    float3 WorldTangent : TANGENT;
    float3 WorldBinormal : BINORMAL;
};

VSOutput PBR_VS(VSInput _Input)
{
    VSOutput Output;
    
    // Position 설정
    float4 WorldPos = mul(_Input.Position, WorldMatrix);
    float4 ViewPos = mul(WorldPos, ViewMatrix);
    Output.Position = mul(ViewPos, ProjectionMatrix);
    
    // 월드 공간 기준 노말, 접선, 이접선 구하기
    Output.WorldNormal = mul(float4(_Input.Normal.xyz, 0.0f), WorldMatrix).rgb;
    Output.WorldTangent = mul(float4(_Input.Tangent.xyz, 0.0f), WorldMatrix).rgb;
    Output.WorldBinormal = mul(float4(_Input.Binormal.xyz, 0.0f), WorldMatrix).rgb;
    
    // UV 좌표 설정
    Output.TexCoord = _Input.TexCoord;
    
    // 월드 공간 기준 Position 구하기
    Output.WorldPosition = mul(float4(_Input.Position.xyz, 1.0f), WorldMatrix).rgb;
   
    return Output;
}

 

이 두 값은 기존에 만들어뒀던 Ext_VertexData와 Rect, Cube, Sphere, Cylinder Mesh들에 없었기 떄문에, 추가해주고 만들어줬습니다.

 

노말과 탄젠트, 바이노말에 월드 행렬을 곱해서 월드 공간 기준으로 변경해준 뒤 픽셀 셰이더로 넘겨줍니다.

struct LightData
{
    float4 LightColor; // RGB(색), w(강도)
    float4 LightWorldPosition; // 라이트 월드 공간 위치
    float4 LightForwardVector; // 월드 입사 벡터
    float4 CameraWorldPosition; // 시점 월드 공간 위치

    float NearDistance;
    float FarDistance;
    float AttenuationValue;
    int LightType;
    bool bIsLightSet;
};

cbuffer LightDatas : register(b1)
{
    int LightCount;
    LightData Lights[64];
};

Texture2D BaseColorTex : register(t0);
Texture2D NormalTex : register(t1);
SamplerState Sampler : register(s0); // 샘플러

struct PSInput
{
    float4 Position : SV_POSITION;
    float4 TexCoord : TEXCOORD;
    float3 WorldPosition : POSITION;
    float3 WorldNormal : NORMAL;
    float3 WorldTangent : TANGENT;
    float3 WorldBinormal : BINORMAL;
};

// 각 벡터에 normalize를 해주는 이유는, 명시적으로 normalize된 벡터를 넣어줬다 하더라도 
// 임의의 값이 어떻게 들어올지 모르기 때문에 그냥 해주는것(안정성을 위한 처리라고 보면 됨)
float4 PBR_PS(PSInput _Input) : SV_TARGET
{
    // 텍스처 색상
    float4 Albedo = BaseColorTex.Sample(Sampler, _Input.TexCoord.xy);
    
    // 월드공간 기준 픽셀(표면) 위치와 법선 단위 벡터
    float3 PixelPosition = _Input.WorldPosition;
    
    // 누적 조명 컬러 (RGB)
    float3 AccumLightColor = float3(0, 0, 0);
    
    // 노말 맵 샘플링 (Tangent-space 노말 → [-1, 1] 범위)
    float3 SampledNormalTS = NormalTex.Sample(Sampler, _Input.TexCoord.xy).xyz * 2.0f - 1.0f;
    SampledNormalTS = normalize(SampledNormalTS);
    
    // TBN 매트릭스 구성 (열(column) 단위로 저장)
    float3x3 TBNMatrix;
    TBNMatrix[0] = normalize(_Input.WorldTangent); // T
    TBNMatrix[1] = normalize(_Input.WorldBinormal); // B
    TBNMatrix[2] = normalize(_Input.WorldNormal); // N
    
    float3 MappedWorldNormal = normalize(mul(SampledNormalTS, TBNMatrix));
    
    // 빛 구하는 과정
    // 기존에 PixelNormal로 적어둔 곳을 모두 MappedWorldNormal로 바꿔주면 됨
    // ...
}

 

여기서 중요한 것은 노말 텍스쳐 샘플링과 TBN 행렬로 매핑된 노말값을 구하는 과정입니다. 먼저 샘플링 과정에서 [*2.0f - 1.0f]를 해주는데, 이건 그냥 노말 텍스쳐의 리맵핑 공식이라서 그대로 쓰면 됩니다.

 

텍스쳐에 저장된 값이 UV 기준 얼마나 기줄어져있는지면, 정점 셰이더에서 전달해준 값은 해당 월드에 대해 UV값이 어느 방향을 향하는지를 알려주는 값입니다.

 

둘을 mul하고 정규화해주면 우리가 원하는 노말이 나옵니다. 자세한 설명은 아래 그래픽스 포스팅을 확인해보시면 됩니다.

https://umtimos.tistory.com/202

 

[Graphics] Normal Mapping

[눈의 착각] 물체에 굴곡이 있다면 빛의 입사에 대한 반사각도가 달라지게 됩니다. 이렇게 반사된 빛이 얼마만큼 눈에 들어오는지에 따라 음영이 인식되면서 굴곡을 인식하게 됩니다. 하지만 사

umtimos.tistory.com

 

 

[결과]

 

빛 처리를 했을 때 처럼, 굉장히 확연한 차이가 나는 것을 확인할 수 있었습니다. 먼저 적용 전입니다.

 

이제 적용 결과를 확인해보겠습니다.

 

+ Recent posts