[DirectX11 3D] Deferred Rendering 3 - Lighting

한창희·2024년 5월 17일

DirecX3D 엔진 만들기

목록 보기
9/14

📌 Lighting 단계 추가하기

기존 Deferred Rendering을 이용해서 한번에 Merge 하는 것이 아닌, lighting 단계를 중간에 두는 것으로 구조를 변경, 먼저 deferred rendering에서 물체의 Emissive를 저장하여 남겨두고 Light 단계에서는 각각의 광원들이 자신이 영향을 주는 픽셀에 정보를 남긴다. 이 때 Emissive 데이터를 활용하여 광원이 주는 빛 뿐만아니라 물체가 스스로 빛을 내는 경우도 계산하여 픽셀에 광원의 대한 정보만 남기도록 한다.
그리고 merge 단계에서는 deferred 단계에서 저장해놓은 색과 lighting 단계에서 저장된 광원의 세기를 계산하여 merge하도록 구조를 변경한다.

📌 Light MRT 만들기

	// ============
	// Light MRT
	// ============
	{
		Ptr<CTexture> pRTTex[2] =
		{
			CAssetMgr::GetInst()->CreateTexture(L"DiffuseTargetTex"
											  , vResolution.x, vResolution.y
											  , DXGI_FORMAT_R8G8B8A8_UNORM
											  , D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE),
			CAssetMgr::GetInst()->CreateTexture(L"SpecularTargetTex"
											  , vResolution.x, vResolution.y
											  , DXGI_FORMAT_R8G8B8A8_UNORM
											  , D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE),			
		};

		Vec4 arrClearColor[2] = {
			Vec4(0.f, 0.f, 0.f, 1.f),
			Vec4(0.f, 0.f, 0.f, 1.f),			
		};

		m_arrMRT[(UINT)MRT_TYPE::LIGHT] = new CMRT;
		m_arrMRT[(UINT)MRT_TYPE::LIGHT]->Create(pRTTex, 2, nullptr);
		m_arrMRT[(UINT)MRT_TYPE::LIGHT]->SetClearColor(arrClearColor, 2);
	}

Light를 수행할 MRT를 제작한다.

📌 Light render

이제 광원은 직접 자신이 영향을 주는 픽셀에 대한 렌더링을 수행해야 하므로 light class에 render 함수를 만들어 주고, render를 수행해야하므로 render를 수행하기 위한 mesh와 머터리얼을 멤버 변수로 받도록 한다.

class CLight3D :
    public CComponent
{
private:
    tLightInfo      m_Info;
    int             m_LightIdx;

    Ptr<CMesh>      m_VolumeMesh;
    Ptr<CMaterial>  m_LightMtrl;
...

이 때 볼륨 매쉬는 해당 라이트의 영역에 대한 픽셀 셰이더를 호출하기 위한 Mesh로 실제로 화면에 그려지는 매쉬가 아니다.

void CLight3D::SetLightType(LIGHT_TYPE _type)
{
	m_Info.LightType = (int)_type;

	if (LIGHT_TYPE::DIRECTIONAL == (LIGHT_TYPE)m_Info.LightType)
	{
		m_VolumeMesh = CAssetMgr::GetInst()->FindAsset<CMesh>(L"RectMesh");
		m_LightMtrl = CAssetMgr::GetInst()->FindAsset<CMaterial>(L"DirLightMtrl");
	}

	else if (LIGHT_TYPE::POINT == (LIGHT_TYPE)m_Info.LightType)
	{
		m_VolumeMesh = CAssetMgr::GetInst()->FindAsset<CMesh>(L"SphereMesh");
		m_LightMtrl = CAssetMgr::GetInst()->FindAsset<CMaterial>(L"PointLightMtrl");
	}

	else if (LIGHT_TYPE::SPOT == (LIGHT_TYPE)m_Info.LightType)
	{
		m_VolumeMesh = CAssetMgr::GetInst()->FindAsset<CMesh>(L"ConeMesh");
		m_LightMtrl = CAssetMgr::GetInst()->FindAsset<CMaterial>(L"SpotLightMtrl");
	}
}

Light의 Type이 정해질때 각각 맞는 매쉬와 머터리얼을 세팅하도록 Set함수를 수정한다.

void CLight3D::render()
{
	if (LIGHT_TYPE::DIRECTIONAL == (LIGHT_TYPE)m_Info.LightType)
	{
		m_LightMtrl->SetScalarParam(SCALAR_PARAM::INT_0, m_LightIdx);
		m_LightMtrl->UpdateData();
		m_VolumeMesh->render();
	}

	else if (LIGHT_TYPE::POINT == (LIGHT_TYPE)m_Info.LightType)
	{

	}

	else if (LIGHT_TYPE::SPOT == (LIGHT_TYPE)m_Info.LightType)
	{

	}
}

그리고 광원은 자신의 idx를 바인딩하여 shader에서 몇 번째 광원을 렌더링하고 있는지 알게한다.

📌 Camera Lighting 단계 추가

void CCamera::Lighting()
{
	// Light MRT 로 변경
	CRenderMgr::GetInst()->GetMRT(MRT_TYPE::LIGHT)->OMSet();

	// 광원이 자신의 영향범위에 있는 Deferred 물체에 빛을 남긴다.
	const vector<CLight3D*>& vecLight3D = CRenderMgr::GetInst()->GetLight3D();
	for (size_t i = 0; i < vecLight3D.size(); ++i)
	{
		vecLight3D[i]->render();
	}
}

void CCamera::Merge()
{
	// Deferred 정보를 SwapChain 으로 병합
	CRenderMgr::GetInst()->GetMRT(MRT_TYPE::SWAPCHAIN)->OMSet();

	Ptr<CMesh>	   pRectMesh = CAssetMgr::GetInst()->FindAsset<CMesh>(L"RectMesh");
	Ptr<CMaterial> pMergeMtrl = CAssetMgr::GetInst()->FindAsset<CMaterial>(L"MergeMtrl");

	pMergeMtrl->SetTexParam(TEX_PARAM::TEX_0, CAssetMgr::GetInst()->FindAsset<CTexture>(L"PositionTargetTex"));
	pMergeMtrl->UpdateData();
	pRectMesh->render();
}

카메라에서는 Deferred 와 Merge 사이에 빛의 세기를 계산하기 위한 Lighting 함수를 만들고 가지고 있는 광원들에 대해 render를 호출한다.

deferred rendering 쉐이더 코드에서는 4번째 텍스쳐가 data에서 Emissive로 변경되었으므로 수정해준다.

📌 Shader Code

#ifndef _LIGHT
#define _LIGHT

#include "value.fx"
#include "func.fx"


// ========================
// Directional Light Shader
// MRT      : LIGHT
// Mesh     : RectMesh
// DS_TYPE  : NO_TEST_NO_WIRTE
// BS_TYPE  : ONE_ONE , 여러개의 빛이 누적될 수 있게

// Parameter
// g_int_0 : Light Idex
// g_tex_0 : PositionTargetTex
// g_tex_1 : NormalTargetTex
// ========================
struct VS_IN
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
};

struct VS_OUT
{
    float4 vPosition : SV_Position;
    float2 vUV : TEXCOORD;
};

VS_OUT VS_DirLight(VS_IN _in)
{
    VS_OUT output = (VS_OUT) 0.f;
    
    output.vPosition = float4(_in.vPos * 2.f, 1.f);
    output.vUV = _in.vUV;
    
    return output;
}

struct PS_OUT
{
    float4 vDiffuse : SV_Target0;
    float4 vSpecular : SV_Target1;
};

PS_OUT PS_DirLight(VS_OUT _in)
{
    PS_OUT output = (PS_OUT) 0.f;
        
    // PositionTarget 에서 현재 호출된 픽셀쉐이더랑 동일한 지점에 접근해서 좌표값을 확인
    float4 vViewPos = g_tex_0.Sample(g_sam_0, _in.vUV);
    
    // Deferred 단계에서 그려진게 없다면 빛을 줄 수 없다.
    if (-1.f == vViewPos.w)
        discard;
    
    // 해당 지점의 Normal 값을 가져온다.
    float3 vViewNormal = normalize(g_tex_1.Sample(g_sam_0, _in.vUV).xyz);
       
    // 해당 지점이 받을 빛의 세기를 구한다.
    tLightColor LightColor = (tLightColor)0.f;
    CalculateLight3D(g_int_0, vViewPos.xyz, vViewNormal, LightColor);
        
    output.vDiffuse = LightColor.vColor + LightColor.vAmbient;    
    output.vSpecular = LightColor.vSpecular;    
    
    output.vDiffuse.a = 1.f;
    output.vSpecular.a = 1.f;
    
    return output;
}

#endif

DirLight의 경우 전체 픽셀에 대해 수행되어야 하므로 매쉬를 두배로 늘려주고,
픽셀 셰이더에서는 deferred 단계에서 저장된 viewpos와 normal 값을 이용하여 빛의 세기를 구한다.
그리고 diffuse와 specular의 데이터를 나누어 저장하여 merge 단계에서 사용도록 한다.


📌 Merge Shader

#ifndef _MERGE
#define _MERGE

#include "value.fx"


// ===============
// Merge Shader
// MRT : SwapChain
// Mesh : RectMesh
// g_tex_0 : ColorTargetTex
// g_tex_1 : DiffuseTargetTex
// g_tex_2 : SpecularTargetTex
// ===============

struct VS_IN
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
};

struct VS_OUT
{
    float4 vPosition : SV_Position;
    float2 vUV : TEXCOORD;
};

VS_OUT VS_Merge(VS_IN _in)
{
    VS_OUT output = (VS_OUT) 0.f;
    
    output.vPosition = float4(_in.vPos * 2.f, 1.f);
    output.vUV = _in.vUV;    
    
    return output;
}

float4 PS_Merge(VS_OUT _in) : SV_Target
{
    float4 vOutColor = (float4) 0.f;
        
    float4 vColor = g_tex_0.Sample(g_sam_0, _in.vUV);
    float4 vDiffuse = g_tex_1.Sample(g_sam_0, _in.vUV);
    float4 Specular = g_tex_2.Sample(g_sam_0, _in.vUV);
        
    vOutColor = (vColor * vDiffuse) + Specular;
    vOutColor.a = 1.f;   
    
    return vOutColor;
}

#endif

merge 단계에서는 이전 값들을 모두 사용하여 물체의 최종 색상을 출력한다.


📌Light Shader

#ifndef _LIGHT
#define _LIGHT

#include "value.fx"
#include "func.fx"


// ========================
// Directional Light Shader
// MRT      : LIGHT
// Mesh     : RectMesh
// DS_TYPE  : NO_TEST_NO_WIRTE
// BS_TYPE  : ONE_ONE , 여러개의 빛이 누적될 수 있게

// Parameter
// g_int_0 : Light Idex
// g_tex_0 : PositionTargetTex
// g_tex_1 : NormalTargetTex
// ========================
struct VS_IN
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
};

struct VS_OUT
{
    float4 vPosition : SV_Position;
    float2 vUV : TEXCOORD;
};

VS_OUT VS_DirLight(VS_IN _in)
{
    VS_OUT output = (VS_OUT) 0.f;
    
    output.vPosition = float4(_in.vPos * 2.f, 1.f);
    output.vUV = _in.vUV;
    
    return output;
}

struct PS_OUT
{
    float4 vDiffuse : SV_Target0;
    float4 vSpecular : SV_Target1;
};

PS_OUT PS_DirLight(VS_OUT _in)
{
    PS_OUT output = (PS_OUT) 0.f;
        
    // PositionTarget 에서 현재 호출된 픽셀쉐이더랑 동일한 지점에 접근해서 좌표값을 확인
    float4 vViewPos = g_tex_0.Sample(g_sam_0, _in.vUV);
    
    // Deferred 단계에서 그려진게 없다면 빛을 줄 수 없다.
    if (-1.f == vViewPos.w)
        discard;
    
    // 해당 지점의 Normal 값을 가져온다.
    float3 vViewNormal = normalize(g_tex_1.Sample(g_sam_0, _in.vUV).xyz);
       
    // 해당 지점이 받을 빛의 세기를 구한다.
    tLightColor LightColor = (tLightColor)0.f;
    CalculateLight3D(g_int_0, vViewPos.xyz, vViewNormal, LightColor);
        
    output.vDiffuse = LightColor.vColor + LightColor.vAmbient;    
    output.vSpecular = LightColor.vSpecular;    
    
    output.vDiffuse.a = 1.f;
    output.vSpecular.a = 1.f;
    
    return output;
}

#endif

이제는 기존처럼 물체가 모든 광원에 대해서 직접계산하는 것이 아니라 광원마다 볼륨매쉬를 주어서 직접 빛을 주는 개념으로 렌더링한다. Directional 광원은 전역광원이므로 화면 전체에 빛을 줘야하니 Vertex Shader에서는 ndc를 화면 전체가 되도록 수정하여 픽셀셰이더로 보낸다.
그리고 디퍼드 단계에서 저장된 노말, 포지션 값을 가져와 광원을 계산하여 해당 픽셀에 빛을 누적하는 식으로 수정했다.

📌Point Light Shader


// ========================
// Point Light Shader
// MRT      : LIGHT
// Mesh     : SphereMesh
// DS_TYPE  : NO_TEST_NO_WIRTE
// BS_TYPE  : ONE_ONE , 여러개의 빛이 누적될 수 있게

// Parameter
// g_int_0 : Light Idex
// g_mat_0 : ViewInv * WorldInv
// g_tex_0 : PositionTargetTex
// g_tex_1 : NormalTargetTex
// ========================
VS_OUT VS_PointLight(VS_IN _in)
{
    VS_OUT output = (VS_OUT) 0.f;
    
    output.vPosition = mul(float4(_in.vPos, 1.f), g_matWVP);
    output.vUV = _in.vUV;
    
    return output;
}

PS_OUT PS_PointLight(VS_OUT _in)
{
    PS_OUT output = (PS_OUT) 0.f;
    
    // 호출된 픽셀의 위치를 UV 값으로 환산
    float2 vScreenUV = _in.vPosition.xy / g_RenderResolution;
        
    // PositionTarget 에서 현재 호출된 픽셀쉐이더랑 동일한 지점에 접근해서 좌표값을 확인
    float4 vViewPos = g_tex_0.Sample(g_sam_0, vScreenUV);        
    
    // Deferred 단계에서 그려진게 없다면 빛을 줄 수 없다.
    if (-1.f == vViewPos.w)
    {
        discard;
    }
                
    // Sphere 볼륨메쉬의 로컬 공간으로 데려간다.
    float3 vLocal = mul(float4(vViewPos.xyz, 1.f), g_mat_0).xyz;
    
    // 로컬공간에서 구(Sphere) 내부에 있는지 체크한다.
    if (0.5f < length(vLocal))
    {
        discard;
    }
    
    // 해당 지점의 Normal 값을 가져온다.
    float3 vViewNormal = normalize(g_tex_1.Sample(g_sam_0, vScreenUV).xyz);
       
    // 해당 지점이 받을 빛의 세기를 구한다.
    tLightColor LightColor = (tLightColor) 0.f;
    CalculateLight3D(g_int_0, vViewPos.xyz, vViewNormal, LightColor);
        
    output.vDiffuse = LightColor.vColor + LightColor.vAmbient;
    output.vSpecular = LightColor.vSpecular;
    output.vDiffuse.a = 1.f;
    output.vSpecular.a = 1.f;
    
    return output;
}

Point Light가 영향을 주는 범위를 먼저 알아야한다. 우리는 볼륨 매쉬를 이용해서 영향을 주는 범위만 픽셀셰이더가 호출되도록 할 수 있다.
그리고 빛이 있더라도 해당 픽셀에 물체가 없으면 빛을 주면 안되므로 디퍼드단계에 저장해놓은 포지션 텍스쳐를 이용해서 물체가 있는지 확인한다.
하지만 포지션이 무조건 존재한다고 빛을 받아야하는건 아니다. 지금 계산하는 과정에서 포지션 타겟텍스쳐에 값이 들어있다는건 카메라기준으로 정사영시켰을때 해당 좌표에 물체가 있었다는 것인데 이 물체가 정사영된 화면 기준 같은 픽셀에 존재하지만 실제로는 더 멀리있어서 포인트 라이트의 범위를 넘어섰다면 빛을 주면 안된다. 이를 계산하기 위해 현재 포지션 타겟에 저장되어있는 포지션을 볼륨매쉬의 로컬좌표계로 가져와 중심에서의 거리를 비교하여 물체가 매쉬 안에있는지 밖에있는지 비교한다. 이를 위해서 미리 view, world 행렬의 역행렬이 필요하다.


📌 해당물체가 Volume Mesh 안에 들어와 있는지 확인하는 방법

현재는 물체를 볼륨매쉬의 로컬좌표계로 가져와서 영역 내부에 있는지 확인하는 방법을 사용하고 있다.
해당방식은 계산은 간단할수 있지만 볼륨매쉬에 따라서 판정하는 방식이 달라야하고 직접 구현해야한다는 단점이 있다.

Stencil을 이용한다면 매쉬의 모양에 종속되지 않고 모든 볼륨매쉬에 사용 가능하도록 개선할 수 있다.

	// BackFaceCheck
	tDesc.DepthEnable = true;
	tDesc.DepthFunc = D3D11_COMPARISON_GREATER;
	tDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
	tDesc.StencilEnable = true;

	tDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
	tDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_INCR;

	hr = DEVICE->CreateDepthStencilState(&tDesc, m_arrDS[(UINT)DS_TYPE::BACKFACE_CHECK].GetAddressOf());
	if (FAILED(hr)) return E_FAIL;


	// FrontCheck
	tDesc.DepthEnable = true;
	tDesc.DepthFunc = D3D11_COMPARISON_LESS;
	tDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
	tDesc.StencilEnable = true;

	tDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
	tDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
	tDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_DECR;

	hr = DEVICE->CreateDepthStencilState(&tDesc, m_arrDS[(UINT)DS_TYPE::FRONTFACE_CHECK].GetAddressOf());
	if (FAILED(hr)) return E_FAIL;


	// Stencil Check
	tDesc.DepthEnable = false;
	tDesc.DepthFunc = D3D11_COMPARISON_NEVER;
	tDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
	tDesc.StencilEnable = true;

	tDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
	tDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_ZERO;
	tDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_ZERO;

	hr = DEVICE->CreateDepthStencilState(&tDesc, m_arrDS[(UINT)DS_TYPE::STENCIL_CHECK].GetAddressOf());
	if (FAILED(hr)) return E_FAIL;

위와 같이 stencil test용 DS State를 만들고, volume mesh를 위와같이 세번에 과정에 나눠서 랜더링한다. 그래서 깊이를 기준으로 볼륨매쉬의 뒤에 있다면 front face 때 stencil의 값이 변하고 볼륨매쉬의 앞에 있다면 backface때 stencil의 값이 변하여 두번 변한 값만 체크하여 해당 위치를 렌더링한다면 볼륨매쉬 안에있다고 볼수 있다.
위와같은 방법으로 볼륨매쉬의 내부를 판정한다면 속도는 좀 느리지만 볼륨매쉬에 상관없이 모든 매쉬에 대해서 내부 영역을 판단할 수 있다는 장점이 있다.

0개의 댓글