코승호딩의 메모장

[Deferred Rendering] 본문

DirectX12/DirectX12 응용

[Deferred Rendering]

코승호딩 2023. 10. 30. 13:25

이번 글에서는 이 전에 렌더 타겟을 통해서 출력한 값들을 기반으로 모두 합쳐서 지연 렌더링 Deferred Rendering을 구현해 보도록 한다. 렌더 타겟은 총 5가지로 포지션, 노말, 텍스처, 프레넬, 매끄러움으로 SV_Target0~4까지 출력하였다. 그리고 이들을 합치는 것은 그렇게 어려운 작업은 아니다. 그러나 프레넬과 매끄러움은 굳이 UI로 표현해 줄 필요성을 못 느꼈기 때문에 이 자리에 대신 조명 처리를 통해 얻은 diffuse와 specular 텍스처를 추가하도록 할 것이다. 


오류 수정

이 전에 구현하였을 때는 데스크탑으로 실행하여서 별 문제가 뜨지 않았다. 그러나 같은 프로젝트를 노트북으로 실행했을 때는 Ref Count 문제와 Back Buffer Index 문제가 떠서 실행조차 되지 않았다. 환경이 문제인가 싶기도 하고 아니면 프레임 문제인가 싶기도 했지만 우선은 이 문제가 어디서 발생하는지를 찾아봐야 했다. Ref Count 문제는 결국 ComPtr 혹은 스마트 포인터의 참조 카운터가 남아 있어서 문제가 발생하는 것이었고 SceneManager 부분에서 오브젝트를 모두 주석처리한 결과 문제가 뜨지 않았다. 그렇게 디버깅을 통해서 오류가 발생하는 곳을 줄여나간 결과 텍스처 부분에서 문제가 있었던 것이었다. 씬에서 생성되는 모든 텍스처와 머티리얼을 SharedPtr 즉, 스마트 포인터로 사용했는데도 불구하고 참조 카운트에서 문제가 발생하였다는 것은 어딘가 해제가 잘 되지 않았다는 것이다. 따라서 우선 SceneManager에서 소멸자가 잘 불리는지 확인을 해봐야 했다. 

 

현재 우리의 프로젝트에서 SceneManager는 싱글턴으로 구현되어 있다. 때문에 싱글턴은 프로그램이 종료되면 알아서 해제가 되어야 한다. 따라서 이를 확인하기 위해 SceneManager의 소멸자에 파일 입출력 함수를 통해서 소멸 시 파일 하나를 생성하도록 하였다.

 

class SceneManager
{
	SINGLETON(SceneManager)
public:
	~SceneManager() {
    	ofstream out{"test.txt"};
        out << "Create File!";
    }
};

다음과 같이 씬 매니저의 소멸자에 파일 입출력을 사용한 결과 역시 텍스트 파일이 생성되지 않는 문제가 발생하였다. 그렇다면 결국 답은 하나이다. 씬 매니저의 소멸자가 호출되고 있지 않다는 것이다. 따라서 싱글턴에서 싱글턴을 프로그래머가 직접 해제를 할 수 있도록 Destroy 함수를 하나 생성하여 엔진 혹은 게임 종료 시 모든 싱글턴 객체를 수동으로 해제해야 한다.

 

#define SINGLETON(type)					
private:								
	type() {}							
public:									
	//...
	static void Destroy()				
	{									
		if (instance) delete instance;	
		instance = nullptr;				
	}									
										
private:								
	static type* instance;
    
#define DESTROY_SINGLE(type)		type::Destroy()

다음과 같이 싱글턴에서 인스턴스를 직접 해제할 수 있는 Destroy 함수를 정의하였다. 

 

Game::~Game()
{
	DESTROY_SINGLE(SceneManager);
}

그리고 클라이언트 코드에서의 게임 부분에서 싱글턴 객체를 해제하는 DESTROY_SINGLE 함수를 통해 해제를 하였다. 그 결과 씬 매니저의 소멸자가 잘 호출이 되었고 텍스트 파일도 잘 생성이 되었다. 이후에 모든 싱글턴 객체를 수동으로 직접 해제하였고 그 결과 다행히 Ref Count 문제가 모두 사라진 것을 확인할 수 있었다.


다음 문제는 Back Buffer Index 문제이다. 처음에는 디버그 출력 창에 이 오류가 발생하였을 때, 리소스를 위한 커맨드 리스트를 사용하지 않고 모든 코드에서 하나의 커맨드 리스트만 사용해서 그런가 생각하였다. 그러나 리소스를 위한 커맨드 리스트와 할당자를 넣어줬음에도 불구하고 문제는 사라지지 않았다. 그저 하얀 화면만 뜨고 실행이 되지 않는 상태였다. 그런데 이 문제는 이번에만 발생한 것은 아니었다. 오브젝트를 그릴 때, 오브젝트 당 상수 버퍼를 함께 넘기는데 이때, 이 전에 동적 색인화를 통해서 머티리얼과 텍스처를 색인을 통해 접근하도록 변경하였는데 어떠한 머티리얼을 사용할지 오브젝트 당 상수 버퍼의 MatIndex를 채워주지 않으면 똑같이 오류가 발생하였다. 결국 쉐이더의 구조적 버퍼 혹은 텍스처 배열에 엉뚱한 색인 값이 들어가면 뜨는 오류였다. 따라서 CPU 상에서 GPU로 넘겨야 할 데이터를 담은 구조체에서 인덱스 부분을 기본 -1로 세팅하고 만약 쉐이더에서 사용하는데도 불구하고 인덱스를 넘겨주지 않았다면 -1을 넘겨주도록 하였고 -1이라면 텍스처 샘플링을 하지 않도록 변경하였더니 문제가 깔끔히 해결이 되었다.


Deferred Rendering - Directional Light

이제 본격적으로 지연 렌더링을 구현해보도록 하자. 우선 쉐이더 부분에서는 기존에 하나의 포워드 쉐이더에서 모든 조명 계산을 처리했다면 디퍼드 렌더링에서는 앞서 렌더 타겟을 통해 전달받은 텍스처를 기준으로 화면에서 보이는 부분만 조명을 처리하도록 변경해야 한다. 우선 Directional Light를 살펴보자.

 

// DirLighting_vs.hlsl
VS_OUT VS_Main(VS_IN vin)
{
    VS_OUT vout = (VS_OUT)0.f;
    
    vout.pos = float4(vin.pos * 2.f, 1.f);
    vout.uv = vin.uv;
    
    return vout;
}

Directional Light의 정점 쉐이더는 다음과 같다. 이 조명은 물체가 어디에 있든 하나의 방향으로 모든 물체에 빛을 적용해야 한다. 따라서 하나의 화면 전체를 덮는 텍스처에서 조명을 계산하면 된다. 마치 포스트 프로세싱이나 앞서 만든 렌더 타겟과 비슷하다. 모델 좌표계의 포지션에 2를 곱한 이유는 화면 전체를 덮기 위해서 조명을 렌더링 할 때, Rectangle Mesh를 사용하는데 Rectangle을 구현할 때, -0.5~0.5로 크기를 만들었기 때문에 2를 곱하여 -1~1 즉, 투영 좌표계로 맞춰주기 위함이다.

 

// DirLighting_ps.hlsl
struct PS_OUT
{
    float4 diffuse : SV_Target0;
    float4 specular : SV_Target1;
};

PS_OUT PS_Main(VS_OUT pin)
{
    PS_OUT pout = (PS_OUT)0;
    
    float3 posW = gTextureMaps[POSITIONMAP_INDEX].Sample(gsamAnisotropicWrap, pin.uv).xyz;
    float4 posV = mul(float4(posW, 1.f), gPassConstants.view);
    if(posV.z <= 0.f)
        clip(-1);
    
    // Matrial Sampling...
    
    LightColor directLight = ComputeDirectionalLight(gPassConstants.lights[matData.lightIndex], mat, normalW, toEyeW);
    
    pout.diffuse = float4(directLight.diffuse, 0.f) + ambient;
    pout.specular = float4(directLight.specular, 0.f);

    return pout;
}

픽셀 쉐이더에서는 우선 이 전에 구한 PositionTarget 텍스처를 샘플링하여 값을 받아온다. 그리고 뷰 변환을 통해서 카메라 좌표계로 변환하고 이 때, 카메라의 뒤에 있는 부분은 라이팅이 필요 없기 때문에 포지션 값이 0보다 작다면 clip을 사용한다. 마지막으로 SV_Target0~4를 통해 받은 텍스처들을 사용해서 조명을 계산하고 SV_target0,1에 각각 출력한다.


Deferred Rendering - Point Light, Spot Light

다음은 포인트 조명의 쉐이더를 구현해보자.

 

포인트 라이트는 조명의 영향을 미치는 끝 부분까지를 반지름으로 한 하나의 구 형태의 볼륨 메쉬를 생각할 수 있다. 조명에 해당하는 구를 렌더링 하면 결국 픽셀 쉐이더에서는 구의 내부에만 해당하는 부분만 조명이 적용될 것이다. 따라서 Directional Light와는 다르게 Point Light의 정점 쉐이더에서는 조명의 월드, 뷰, 프로젝션 행렬 변환을 수행해야 한다.

 

// PointLighting_vs.hlsl
VS_OUT VS_Main(VS_IN vin)
{
    VS_OUT vout = (VS_OUT)0.f;
    
    float4 posW = mul(float4(vin.posL, 1.0f), gObjConstants.world);
    vout.posH = mul(posW, gObjConstants.viewProj);
    vout.uv = vin.uv;
    
    return vout;
}

다음과 같이 Point Light에서는 월드, 뷰, 프로젝션 변환을 수행하게 된다. 마침 구 또한 Rectangle처럼 -0.5~0.5로 되어있지만 CPU에서 Transform 컴포넌트를 통해서 Scale을 해주는 부분이 있기 때문에 안 해도 괜찮다. 

 

PS_OUT PS_Main(VS_OUT pin)
{
    PS_OUT pout = (PS_OUT)0;
    
    MaterialData matData = gMaterialData[gObjConstants.materialIndex];
    LightInfo light = gPassConstants.lights[matData.lightIndex];
    
    float2 uv = float2(pin.posH.x / gPassConstants.width, pin.posH.y / gPassConstants.height);
    float3 posW = gTextureMaps[POSITIONMAP_INDEX].Sample(gsamAnisotropicWrap, uv).xyz;
    float4 posV = mul(float4(posW, 1.f), gPassConstants.view);
    if(posV.z <= 0.f)
        clip(-1);
    
    float3 toEyeW = normalize(gPassConstants.eyePosW.xyz - posW);
    float distance = length(light.position - posW);
    if (distance > light.fallOffEnd)
        clip(-1);
    
    // Material Sampling...
    
    float4 ambient = gPassConstants.ambientLight * diffuseAlbedo;
    
    Material mat = { diffuseAlbedo, fresnelR0, shininess };
    LightColor directLight = ComputePointLight(light, mat, posW, normalW, toEyeW);
    
    pout.diffuse = float4(directLight.diffuse, 0.f);
    pout.specular = float4(directLight.specular, 0.f);
    
    return pout;
}

픽셀 쉐이더에서는 우선 uv 좌표를 새로 정의하는데 이때, 다음 코드와 같이 해주는 이유는 레스터라이저를 통해서 넘어온 좌표는 픽셀 좌표이다. 즉, -1에서 1의 NDC 좌표에서 0~800, 0~600인 픽셀 좌표로 바뀌는 것이다. 따라서 전체 스크린의 크기인 800x600으로 넘어온 픽셀 좌표의 포지션 값들을 나눠주는 것이다. 그렇게 되면 uv 좌표가 될 것이다. 마찬가지로 카메라 좌표로 변환 후 z값이 0보다 작은 것들은 clip을 진행하며 그리고 해당 픽셀의 포지션과 PointLight의 포지션까지의 길이가 light의 fallOffEnd값보다 작다면 빛이 닿지 않는 것이기 때문에 clip을 진행한다. 그런데 생각해 보면 어차피 구 크기만큼만 픽셀 쉐이더에 들어가기 때문에 이 과정이 필요한지는 더 생각해봐야 한다. 일단 안전성을 위해서 넣어두자.

 

SpotLight는 PointLight를 재활용하여서 그냥 ComputePointLight의 조명 함수를 ComputeSpotLight로만 변경하여 조명계산을 하였다. 구로 표현하는 것이 쉽기 때문이다. 


// Lighting Group
{
	vector<RenderTarget> rtVec(RENDER_TARGET_LIGHTING_COUNT);

	rtVec[0].target = GET_SINGLE(Resources)->CreateTexture("DiffuseLightTarget",
		DXGI_FORMAT_R8G8B8A8_UNORM, _window.width, _window.height,
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::LIGHTING,
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	rtVec[1].target = GET_SINGLE(Resources)->CreateTexture("SpecularLightTarget",
		DXGI_FORMAT_R8G8B8A8_UNORM, _window.width, _window.height,
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::LIGHTING,
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	_mrt[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::LIGHTING)] = make_shared<MultipleRenderTarget>();
	_mrt[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::LIGHTING)]->Create(RENDER_TARGET_GROUP_TYPE::LIGHTING, rtVec, dsTexture);
}

이제 CPU 부분을 살펴보자. 우선 MRT의 그룹에 라이팅을 추가하고 MRT를 생성하는 부분에서도 라이팅을 다음과 같이 추가하도록 하자. 두 개의 렌더 타겟을 사용할 것이다. 따라서 쉐이더에서도 타입에 따라 변경해야 한다.

 

case SHADER_TYPE::LIGHTING:
	_pipelineDesc.NumRenderTargets = 2;
	_pipelineDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
	_pipelineDesc.RTVFormats[1] = DXGI_FORMAT_R8G8B8A8_UNORM;
	break;

쉐이더의 파이프라인 상태를 셋 하는 부분에서도 만약 쉐이더 타입이 라이팅이라면 파이프라인 디스크립션을 두 개의 렌더 타겟을 사용할 수 있도록 다음과 같이 변경해 준다. 꼭 포맷 형식을 맞춰줘야 한다.

 

ShaderInfo info =
{
	SHADER_TYPE::LIGHTING,
	RASTERIGER_TYPE::CULL_NONE,
	DEPTH_STENCIL_TYPE::NO_DEPTH_TEST_NO_WRITE,
	BLEND_TYPE::ONE_TO_ONE_BLEND
};

다음 조명들의 메쉬를 렌더링 하기 위해서 필요한 쉐이더 정보이다. 조명들은 깊이 결과에 상관없이 그려져야 하며 각 조명들은 블랜딩을 통해서 서로 합쳐져야 한다. 만약 블랜딩을 하지 않으면 마지막 한 조명만 그려지게 될 것이다. 

 

void Scene::Render()
{
	PushPassData();

	int8 backIndex = gEngine->GetSwapChain()->GetBackBufferIndex();
	gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)->ClearRenderTargetView(backIndex);
	gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::G_BUFFER)->ClearRenderTargetView();
	gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::LIGHTING)->ClearRenderTargetView();

	gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::G_BUFFER)->OMSetRenderTargets();
	_mainCamera->SortGameObject();
	_mainCamera->Render_Deferred();

	RenderLights();

	_mainCamera->Render_Forward();

	for (auto& camera : _cameraObjects) {
		if (camera == _mainCamera)
			continue;

		camera->SortGameObject();
		camera->Render_Forward();
	}
}

이제 디퍼드 렌더링을 1Pass로 진행한 뒤, 다음 2Pass로 조명을 렌더링 한다. 

 

void Scene::RenderLights()
{
	gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::LIGHTING)->OMSetRenderTargets();

	for (auto& light : _lightObjects)
	{
		light->Render();
	}
}

조명을 렌더링 할 때는 우선 조명에 해당하는 렌더 타겟에 그려줘야 하기 때문에 OMSetRenderTargets을 사용한다. 

 

void Light::SetLightType(LIGHT_TYPE type)
{
	_lightInfo.lightType = static_cast<int32>(type);

	switch (type)
	{
	case LIGHT_TYPE::DIRECTIONAL_LIGHT:
		_volumeMesh = GET_SINGLE(Resources)->Get<Mesh>("Rectangle");
		_lightMaterial = GET_SINGLE(Resources)->Get<Material>("DirLight");
		break;
	case LIGHT_TYPE::POINT_LIGHT:
		_volumeMesh = GET_SINGLE(Resources)->Get<Mesh>("Sphere");
		_lightMaterial = GET_SINGLE(Resources)->Get<Material>("PointLight");
		break;
	case LIGHT_TYPE::SPOT_LIGHT:
		_volumeMesh = GET_SINGLE(Resources)->Get<Mesh>("Sphere");
		_lightMaterial = GET_SINGLE(Resources)->Get<Material>("SpotLight");
		break;
	}

	GetGameObject()->SetMatIndex(_lightMaterial->GetMatCBIndex());
}

참고로 앞서 말했듯이 조명의 볼륨 메쉬를 설정해야 한다. 각각에 맞게 사각형과 구를 메쉬로 갖고 있도록 한다. 여기서 중요한 점은 SetMatIndex를 통해서 조명의 게임오브젝트 자신이 어떠한 머티리얼의 인덱스를 가지고 있는지를 넣어줘야 한다는 것이다. 왜냐하면 기존에는 MeshRenderer의 Start부분에서 이 과정을 처리했지만 조명의 경우는 MeshRenderer를 사용하지 않기 때문이다. 

 

void Light::Render()
{
	GetTransform()->PushData();

	_lightMaterial->SetLightIndex(_lightIndex);
	_lightMaterial->Update();

	switch (static_cast<LIGHT_TYPE>(_lightInfo.lightType))
	{
	case LIGHT_TYPE::POINT_LIGHT:
	case LIGHT_TYPE::SPOT_LIGHT:
		float scale = 2 * _lightInfo.fallOffEnd;
		GetTransform()->SetLocalScale(Vec3(scale, scale, scale));
		break;
	}

	_volumeMesh->Render();
}

이제 모든 셋 부분을 마쳤으니 렌더링을 할 차례이다. 이 또한 기존에는 MeshRenderer에서 PushData를 통해서 오브젝트 당 상수 버퍼를 업데이트하였지만 이제는 없기 때문에 수동으로 직접 불러준다. 그리고 자신이 몇 번째의 조명인지 정보를 넘겨준 후 머티리얼을 업데이트하고 만약 포인트 라이트 거나 스폿 라이트라면 스케일을 2배 하여 구의 크기에 맞게 조절한다. 그다음 볼륨 메쉬를 렌더링 하면 된다. 

 

실행 결과이다. 오른쪽 상단에 보이듯이 diffuse와 specular가 잘 들어간 것을 확인할 수 있다. 그러나 이제 이들을 합치는 작업이 아직 남아 있다. 현재는 depth값으로 인해 물체들이 그려지지 않았지만 검은색으로 그려진 것처럼 보이는 것이다.


Final

마지막은 굉장히 간단하다. Directional Light처럼 모든 스크린을 덮는 사각형 하나를 렌더링 하는데 이때 픽셀 쉐이더에서 diffuse와 specular 텍스처를 받아서 합쳐주면 끝이다. 

 

#define POSITIONMAP_INDEX 0
#define NORMALMAP_INDEX 1
#define DIFFUSEMAP_INDEX 2
#define FRESNELMAP_INDEX 3
#define SHININESSMAP_INDEX 4
#define DIFFUSELIGHT_INDEX 5
#define SPECULARLIGHT_INDEX 6
float4 PS_Main(VS_OUT pin) : SV_Target
{
    float4 pout = (float4)0;

    float4 diffuse = gTextureMaps[DIFFUSELIGHT_INDEX].Sample(gsamAnisotropicWrap, pin.uv);
    float4 specular = gTextureMaps[SPECULARLIGHT_INDEX].Sample(gsamAnisotropicWrap, pin.uv);
    
    pout = diffuse + specular;

    return pout;
}

그저 몇 줄 없다. 이렇게 쉽게 할 수 있는 이유는 동적 색인화를 사용하였기 때문이 아닐까 싶다. 동적 색인화를 사용하였기 때문에 어떤 쉐이더에서도 쉽게 모든 텍스처에 접근할 수 있다. 위에 hlsl에 정의되어 있는 define 변수들처럼 고정된 렌더 타깃 텍스처들을 이용하여 각자의 인덱스 위치값들만 알고 있다면 이 색인을 통해 접근할 수 있기 때문이다. 어떠한 인덱스가 어떠한 텍스처를 가리키고 있는지 명확하게 알 수 있는 장점도 있다.

 

void Scene::RenderFinal()
{
	int8 backIndex = gEngine->GetSwapChain()->GetBackBufferIndex();
	gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)->OMSetRenderTargets(1, backIndex);

	GET_SINGLE(Resources)->Get<Material>("Final")->Update();
	GET_SINGLE(Resources)->Get<Mesh>("Rectangle")->Render();
}

이제 씬에서 스왑체인의 백 버퍼에 하나의 Final 쉐이더를 셋 하고 하나의 사각형을 렌더링 하면 된다. 실행해 보자.

 

(???...)

또한 다음과 같은 디버그 오류 메시지가 출력이 된다. 왜 이러한 현상이 발생하는 것일까? 잘 생각해 보면 우리는 MRT를 생성하고 MRT를 통해서 생성한 리소스를 사용하여 스왑 체인의 백 버퍼를 대신하였고, 다양한 렌더 타겟을 만들어서 해당 리소스에 출력을 하였다. 그렇다면 빠질 수 없는 작업이 있다. 바로 리소스의 상태를 전이하는 것이다. 따라서 MRT에서 만든 리소스들을 그리기 전에 clear 할 때, COMMON에서 RENDER_TARGET으로 상태를 전이해야 하고 모두 그리고 나서는 RENDER_TARGET에서 COMMON으로 상태를 전이해야 한다. 

 

class MultipleRenderTarget
{
public:
	//...
	void WaitTargetToResource();
	void WaitResourceToTarget();

private:
	//...
	D3D12_RESOURCE_BARRIER _targetToResource[8];
	D3D12_RESOURCE_BARRIER _resourceToTarget[8];
};

다음과 같이 두 개의 리소스 장막을 내부 변수로 정의한다. 

 

for (uint32 i = 0; i < _rtCount; ++i) {
	_targetToResource[i] = CD3DX12_RESOURCE_BARRIER::Transition(_rtVec[i].target->GetResource().Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_COMMON);

	_resourceToTarget[i] = CD3DX12_RESOURCE_BARRIER::Transition(_rtVec[i].target->GetResource().Get(),
		D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_RENDER_TARGET);
}

다음과 같이 리소스 상태를 정의하는데 _targetToResource는 렌더 대상에서 일반 리소스로, _resourceToTarget은 일반 리소스에서 렌더 대상으로 상태를 초기화한다. 

 

void MultipleRenderTarget::WaitTargetToResource()
{
	CMD_LIST->ResourceBarrier(_rtCount, _targetToResource);
}

void MultipleRenderTarget::WaitResourceToTarget()
{
	CMD_LIST->ResourceBarrier(_rtCount, _resourceToTarget);
}

그리고 상태 전이를 위해서 다음과 같이 ResourceBarrier를 호출하는데 WaitTargetToResource 함수는 렌더 타겟을 모두 그린 후에 호출하면 되고, WaitResourceToTarget은 그리기 전 호출해 주면 된다.

 

다음과 같이 디버그 출력창에 오류도 뜨지 않고 잘 출력이 된다. 

 

참고로 볼륨 메쉬 안에 카메라가 들어가면 안에 있는 경우 빛이 어두워지고 밖에 있는 경우 밝아지는 현상이 발생한다. 사실 밝아지는 것이 문제가 있는 부분이다. 원래 일반 빛에서 빛의 계산이 한 번 더 되기 때문에 더 밝아지는 것이다. 그렇다면 이 현상이 왜 발생하는 것일까? 바로 볼륨 메쉬 밖에 카메라가 위치하게 되면 볼륨 메쉬를 렌더링 할 때, 어떠한 깊이 테스트와 판정도 하지 않기 때문에 같은 x, y 좌표에서 빛이 두 번 계산되어 밝기 값이 두 배가 되는 것이다. 따라서 한 번만 계산하기 위해서 레스터라이저의 컬링 옵션을 CULL_FRONT로 주게 되면 반 면만 렌더링 하게 되고 결국 정상으로 렌더링 된다.


처음에는 데스크톱에서만 실행하여서 오류가 떴는지 몰랐다. 그러나 어떠한 환경에서는 오류가 뜨고 어떠한 환경에서는 오류가 뜨지 않는 프로그램은 정말 잘못된 프로그램일 것이다. 하드웨어적으로 지원하지 않아서 그런다 하면 이해는 하지만 프로그래머가 잘못 작성하여서 그렇게 된 것은 오로지 프로그래머의 능력이 부족한 탓이 아닐까 싶다. 따라서 오류를 찾는 과정은 길었지만 많은 것을 깨달았다. 그리고 디퍼드 렌더링을 구현하면서 무엇보다 깨달은 것은 조명 계산을 이렇게까지 줄일 수 있다는 것이 굉장히 대단한 일이라는 것을 알게 되었다. 이전에는 모든 오브젝트와 픽셀을 대상으로 계산을 하였지만 디퍼드 렌더링을 통해서 조명 계산을 확실히 줄일 수 있게 되었기 때문이다. 이렇게 렌더 타겟을 다루는 방법을 깨달았으므로 시일 내에 그림자 맵도 만들고 싶어졌다. 하나씩 프로젝트가 발전해 나가는 모습이 굉장히 뿌듯했다.

 

 

 

 

 

 

 

'DirectX12 > DirectX12 응용' 카테고리의 다른 글

[Render Target]  (0) 2023.10.22
[Orthographic Projection]  (1) 2023.10.22
[Dynamic Indexing]  (1) 2023.10.21
[Frustum Culling]  (0) 2023.10.21
[CubeMap]  (1) 2023.10.16
[Normal Mapping]  (0) 2023.10.12
[Light-2]  (1) 2023.10.11