[목차]
- 변경 이유
- 변경 결과
- 구조 변경
- 디퍼드 렌더링을 위해 필요한 기능들
[변경 이유]
이후의 단계로 그림자 매핑과 포스트 프로세싱을 진행하려고 하는데, 제가 알고있는 방식이 디퍼드 렌더링 방식이라 조금 고민을 했습니다. 그래도 애초에 프레임워크 제작 이유가 그래픽스 학습과 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 콜 해주면, 최종 결과물이 나옵니다.


'DirectX11 > 프레임워크 제작' 카테고리의 다른 글
| [DirectX11] 포스트 프로세싱 적용해보기 (0) | 2025.06.21 |
|---|---|
| [DirectX11] 그림자 매핑 해보기 (0) | 2025.06.12 |
| [DirectX11] 노말 매핑 해보기 (0) | 2025.06.07 |
| [DirectX11] Lighting 추가하기 (0) | 2025.06.06 |
| [DirectX11] DirectX11 프레임워크를 위한 기능 추가 - 3 + 간단한 Lighting 해보기 (0) | 2025.06.04 |