참고
https://learnopengl.com/Advanced-Lighting/Deferred-Shading
https://www.slideshare.net/slideshow/deferred-shading/1552178?from_search=0#1
https://www.slideshare.net/slideshow/ndc11-deferred-shading/8192630?from_search=1
https://kyuhwang.tistory.com/4?category=523414#Deferred%EC%-D%--%--%EC%-D%B-%EC%A-%--
Deferred Shading
보통 사용하는 방식은 Foward Shading
방식이다. 해당 방식은 각각
의 오브젝트마다 라이팅을 하는 방식으로 라이트의 개수가 늘어날수록 비용이 많아진다.
Deferred Shading
방식은 G-Buffer를 이용한다.
Forwad Shading
기존의 forwad shading의 방식은 아래와 같은 느낌이다. Single-apss -> Multi-light의 코드를 보여주고 있다. 해당 방식의 문제점은 렌더링 되지 않을 Geometry도 라이팅 연산을 하고 mulit lights일 때 관리가 어렵다는 것이다.
무슨 이야기냐면, 라이트가 적용되지 않을 거리의 Geometry도 라이트 모델의 연산을 진행하게 되므로 비용이 증가하게 된다.
for each object do
for eact light do
framebuffer = light_model(object, light);
Multi-pass, multi-light
이 문제를 해결할 수 있는 방식은 존재한다.
for each light do
for each object affected by light do
framebuffer += light_model(object, light);
다만, 중복된 작업이 많이 발생한다는 문제점이 존재한다.
G-buffer
G-Buffer
에 라이팅 리소스들을 렌더링하고 라이팅에 오브젝트들 대신 G-Buffer를 사용하는 방식이다.
for each object do
G-Buffer = lighting properties of object;
for each light do
frameBuffer += light_model(G-Buffer, light);
이러한 방식이면 라이팅이 O(1)
의 시간복잡도로 수행 가능하다. 일종의 Post-Processing
처럼 수행되며, 2D 타겟 간의 계산이다. 결국, 화면에 실제로 렌더링 되는 픽셀
만 라이팅 계산을 수행한다.
라이팅에 필요한 정보는 아래와 같다.
- Normal
- Position
- Diffuse / Specular albedo, other attributes
그렇다면, G-Buffer를 어떻게 만들어?
MRT(Multi Render Target)
을 사용해서, 한 패스에 4개의 렌더 타겟 정보를 렌더링한다
이러한 채널들을 효율적으로 모두 사용하여 최소한의 RenderTarget을 사용하는 것이 제일 중요하다. 최소한의 RTV를 활용하기 위해 position
을 MRT 구조체에 포함시키지 않을 것이다.
// Texture formats
static const DXGI_FORMAT depthStencilTextureFormat = DXGI_FORMAT_R24G8_TYPELESS;
static const DXGI_FORMAT basicColorTextureFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
static const DXGI_FORMAT normalTextureFormat = DXGI_FORMAT_R11G11B10_FLOAT;
여기서 A8의 경우는 알파채널로 전반사 세기(Specualr Power)를 저장하는데 사용된다. 일반적으로 사용하는 전반사 정도는 아주 작은 범위 내에 있으므로 저장하기에 용이하다.
DirectX 9를 사용하던 시절에는 렌더링 타겟 형식이 32비트, 즉 채널당 8비트뿐이었기 때문에 각 버텍스 셰이더에 대한 노말 채널은 24바이트의 데이터를 가졌다. 반면 최신 DirectX 11의 경우 세 채널이 32비트 렌더링 타겟을 사용할 수 있게하는 형식을 제공한다. 따라서 R11G11B10
을 할당할 수 있게 되었다.
이렇게 포맷을 정하고서 Texture2D
-> RTV(or DSV)
-> SRV
를 차근차근 만들어주면 된다.
#include "Common.hlsli"
Texture2D albedoTex : register(t0);
Texture2D normalTex : register(t1);
Texture2D aoTex : register(t2);
Texture2D metallicTex : register(t3);
Texture2D emissiveTex : register(t4);
cbuffer MaterialConstants : register(b0)
{
float3 albedoFactor; // baseColor
float roughnessFactor;
float metallicFactor;
float3 emissionFactor;
int useAlbedoMap;
int useNormalMap;
int useAOMap; // Ambient Occlusion
int invertNormalMapY;
int useMetallicMap;
int useRoughnessMap;
int useEmissiveMap;
float dummy3;
};
struct VSToPS
{
float4 position : SV_Position;
float2 texcoord : TEXCOORD0;
float3 normalWorld : NORMAL0;
float3 tangentWorld : TANGENT0;
};
struct PSOutput
{
float4 normal : SV_TARGET1;
float4 diffuse : SV_TARGET0;
};
float3 GetNormal(VSToPS input)
{
float3 normalWorld = normalize(input.normalWorld);
if (useNormalMap) // NormalWorld를 교체
{
float3 normal = normalTex.SampleLevel(linearWrapSampler, input.texcoord, lodBias).rgb; // 범위 [0, 1]
normal = 2.0 * normal - 1.0; // 범위 조절 [-1.0, 1.0]
// OpenGL 용 노멀맵일 경우에는 y 방향을 뒤집어줍니다.
normal.y = invertNormalMapY ? -normal.y : normal.y;
float3 N = normalWorld;
float3 T = normalize(input.tangentWorld - dot(input.tangentWorld, N) * N);
float3 B = cross(N, T);
// matrix는 float4x4, 여기서는 벡터 변환용이라서 3x3 사용
float3x3 TBN = float3x3(T, B, N);
normalWorld = mul(normal, TBN);
}
normalWorld = 0.5 * (normalWorld + 1.0);
return normalWorld;
}
PSOutput PackGBuffer(float3 baseColor, float3 normal)
{
PSOutput output = (PSOutput) 0;
output.normal = float4(normal, 1.0);
output.diffuse = float4(baseColor, 1.0);
return output;
}
PSOutput main(VSToPS input)
{
float4 albeoColor = useAlbedoMap ? albedoTex.Sample(linearWrapSampler, input.texcoord) : float4(albedoFactor, 1.0);
float3 normal = GetNormal(input);
return PackGBuffer(albeoColor.xyz, normal);
}
위의 코드는 내 GBufferPS.hlsl
에 대한 코드이다. SV_TARGET[n]
을 통해서 OMSetRenderTargets
에 설정한 RTV에 각 버퍼에 대한 것들을 그리는 것이다. 해당 그린 것들을 DeferredLightngPS.hlsl
에서 2D화면에서 작업을 진행한다.
// Unpack GBuffer
float depth = DepthTex.Sample(linearWrapSampler, input.texcoord);
float3 pos = GetViewSpacePosition(input.texcoord, depth);
float3 V = normalize(0.0f.xxx - pos);
float3 baseColorSpec = DiffuseTex.Sample(linearWrapSampler, input.texcoord).xyz;
float4 normal = NormalTex.Sample(linearWrapSampler, input.texcoord);
float3 normalWorld = normal.xyz * 2.0 - 1.0;
위와 같이, packing 한 G-Buffer 정보들에 대해서 하나하나 할당하고 이전에 forward Lighting에서 했던 작업들을 똑같이 진행한다. V
는 화면에서 물체를 바라보는 방향 벡터이다.
기존에 물체와 라이팅을 동시에 진행하게 되면서 O(frag * light)
라는 시간복잡도가 발생하였는데, Deferring Lighting의 경우에는 O(frag + light)
의 시간복잡도가 발생한다.
왼쪽 화면은 순서대로 diffuseTex, normalTex 그리고 depthTex를 화면상에 그려놓은 것이다.
중간 중간 문제점
Normal Texture2D를 불러오고 나서 적용 시킬 때, 기존의 음수값을 가지던 Normal을 처리가 잘 안돼는 문제점이 있다. [0,1]범위로 불러오게 되므로 XYZ값이 모두 0으로 되어서 계산이 이상해지는 문제였다.
해당 문제는 GBuffer에서 노말 값을 Packing 하기전에 [0,1]로 정규화를 해주면 문제가 해결된다.
자잘한 팁(by 참고 블로그)
렌더링 과정에서 Albeodo를 찍어야하는 RenderTarget을 제외하고는 나머지는 1.0
이상의 값이 들어가야 하기 때문에 Format을 R8G8B8A8
을 사용하면 안된다.
Albedo Map에 Alpha값이 들어가 있을 경우가 있는데, 이 경우 해당 RenderTarget은 Blend State를 Alpha Blend Mode로 변경해주어야 한다.
'Graphics' 카테고리의 다른 글
8. Cascade Shadow Mapping(CSM) (0) | 2024.08.11 |
---|---|
7. Screen Space Ambient Occlusion(SSAO) (0) | 2024.07.04 |
5. Normal Mapping (0) | 2024.06.27 |
4. Point Shadow Mapping (1) | 2024.06.24 |
3. Directional Shadow Mapping (0) | 2024.06.21 |