Notice
Recent Posts
Recent Comments
Link
반응형
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

RenderLog

Light Indexed Deferred Rendering 본문

Graphics/Graphics

Light Indexed Deferred Rendering

scahp 2020. 4. 3. 01:24

 

 

Light Indexed Deferred Rendering

 

최초작성 : 2020-04-03

마지막수정 : 2020-04-03

최재호

 

목표

Light Indexed Deferred Rendering의 이해와 구현

 

 

소개

Light Indexed Deferred Rendering (이하 LIDR)은 Forward Rendering에서 다수의 Light를 빠르게 렌더링하기 위해서 사용됩니다.

 

Forward Rendering은 Deferred Rendering과는 다르게 Light 개수가 많을수록 성능에 영향을 많이 받습니다.

 

예를 들어 Pixel Shader에서 1개의 픽셀을 대상으로 Shading을 수행한다고 합시다. 이때 1 개의 픽셀이 어떤 Light로 부터 영향을 받는지 알아내기 위해서는 모든 라이트의 바운드와 교차 테스트 및 적용이 필요합니다. (즉, 화면의 총 픽셀 개수 * 라이트 수, 화면 총 픽셀이 1024*768 이고, 라이트 개수가 255개 라면, 교차테스트는 1024 * 768 * 255 번 이상 발생). 하지만 이게 전부가 아니라 Pixel Shader에서 연산은 일어났지만 Depth Test나 Stencil Test와 같은 Per-Pixel Operation 단계에서 제거되어진 Pixel도 이 연산을 그대로 수행합니다.

 

 

 

이런 부분을 해결하기 위해서 LIDR에서는 Light를 Light Index Buffer 에 미리 써두고, 각 픽셀에서는 Light Index Buffer를 참조하여 적용할 Light를 즉시 읽어와 적용하는 형태로 Lighting 처리 합니다. Light Index Buffer는 이름 그대로 Light의 Index를 저장하고 있습니다. Light Index 를 저장하는 방법은 몇가지 있지만 저는 Bit Shift 방식을 선택하여 구현했습니다. 

 

Rendering Pass는 총 3 단계입니다.

1. Depth only pass

2. Light Index Pass

 - Light Index Texture에 라이트 바운드 렌더링하며, 텍스쳐에 Light Index 들을 저장한다.

3. Base Pass 

 - 일반적인 ForwardRendering 에 Lighting 연산은 Light Index Texture를 사용

 

Light Index를 저장할 수 있는 최대 개수는 버퍼의 타입에 따라 다를 수 있는데, [번역]Light Indexed Deferred Lighting 에서는 RGBA8 형식의 텍스쳐를 버퍼로 사용합니다. 그리고 이 텍스쳐에는 한 텍셀당 최대 4개의 Light Index를 저장할 수 있습니다. 인덱스는 8 bit이기 때문에 Index (0~255)까지 저장할 수 있습니다. 이 구현을 그대로 사용하였습니다.

 

4개의 라이트를 RGBA8에 담기 위해서 Bit Packing 하는 방법이 소개 됩니다. 작동방식은 다음과 같습니다.

 

Packing

8 Bit의 LightIndex가 있다고 할때, 10110100 이 있다고 합시다.

1. 10 11 01 00 로 나눕니다.

2. 나눠진 픽셀은 8 Bit 인 4개의 변수 RGBA에 가장 상위 2비트에 담습니다.

3. 각 픽셀을 255 로 나눠서 0~1 사이 값으로 Normalize 합니다.

4. Light Bound를 렌더링할 때 이 값을 Texture에 기록합니다.

 

이해를 편하게 하기위해서 3, 4번 과정이 없었다고 하면 아래와 같은 형태로 Light Indxe Buffer의 Light Index가 기록되었을 것입니다.

 

Light Index가 R, G, B, A에 고르게 저장됨

 

 

이제 1 개의 Light Indxe를 Light Index Buffer에 저장하였습니다.

같은 Pixel의 두번째 Light Index 를 저장하기 위해서 Blend mode를 설정합니다.

(사실 Blend mode는 첫번째 Light Index를 그리기 전에 이미 설정되어있는 상태인데, 설명의 흐름상 여기서 설명하고 있습니다.)

 

Blend function는 Src를 ONE 그리고 Dst를 CONSTANT_COLOR로 설정합니다. 그리고 Constant Color는 (0.25, 0.25, 0.25, 0.25)로 설정합니다. Blend Equation은 Add 입니다. (이건 기본 값)

 

Add 의 공식을 보면, 최종색상 = SrcColor * SrcFactor + DestColor * DstFactor 가 됩니다.

여기서 SrcColor가 이번에 그려지는 컬러, SrcFactor가 ONE 입니다. 그래서 SrcColor는 그대로 유지됩니다.

그리고 DstColor는 원래 있던 색상, Dst Factor는 CONSTANT_COLOR로 지정했기 때문에 (0.25, 0.25, 0.25, 0.25)입니다.

 

출처 : https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBlendEquation.xhtml

 

 

 

이 Blend 설정의 의미는 0.25 값을 이해하면 쉽게 이해할 수 있습니다. 0.25는 1/4 와 같으며, 즉 우측으로 비트를 2번 shift 한 것과 같습니다. (>> 2).

즉, 기존에 있던 Light Index 값은 우측으로 2번 시프트 하여 00XX0000 의 XX 에 비트로 옮겨주고, 새로운 Light Index를 최상위 비트에 저장합니다.

 

이렇게 하면 Blend equation만으로 특별한 추가 연산없이 4개의 라이트를 Light Index Buffer 차곡차곡 저장할 수 있습니다. 여기서 기억해야 할 부분은 4개 이상의 라이트가 저장되면 가장 처음에 저장되었던 라이트가 없어진다는 점입니다.

 

Unpacking

[번역]Light Indexed Deferred Lighting 에서는 Light Index를 0~1 사이의 Normalize 된 값으로 Unpacking 해서 Light의 Position과 Color 값을 Texture로 부터 가져옵니다. 저는 이 구현을 좀 더 간단히 하기 위해서 Texture를 사용하지 않았습니다. 대신 UniformBuffer로 255개의 Light를 Shader에 전달하고, Light Index는 Integer 타입으로 복원하여 사용하였습니다. 이 부분이 원본 소스와 차이가 있다는 점 기억해주세요.

 

1. Light Index Texture에서 현재 픽셀위치에 대응하는 Vector4 값을 얻습니다. 이것을 PackedLightIndex라 하겠습니다.

2. Normalized 되어 저장되어있는 PackedLightIndex에 ceil(packedLightIndex * 254.5) 를 계산하여 0~255 값으로 복원해줍니다.

2. 이제 첫번째 Light Index를 얻기 위해서 PackedLightIndex *= 0.25 를 해줍니다. 0.25는 위에서 소개한 것과 같이 우측으로 2번 Shift (>> 2)한 것과 같습니다.

3. 그리고 PackedLightIndex - floor(PackedLightIndex)를 계산하여 우측으로 Shift한 값만 남깁니다. 이렇게 RGBA의 맨 오른쪽 2 개 Bit만 얻어낼 수 있습니다. 이렇게 얻어낸 값을 fracParts라 하겠습니다.

4. fracParts를 좌측으로 2비트 시프트 하면, 첫번째 Light Index는 아마 아래와 같은 모양이 되었을 것입니다.

 

 

그리고 이제 각 RGBA 컴포넌트에 있는 값을 원래 있던 위치로 Shift 해준다음 모두 더 해주면 Light Index가 복원됩니다.

이 과정을 한번에 Shift 로 처리하기 위해서 모든 Shift를 더할 것입니다. fractParts를 좌측으로 2번 시프트 한 것과 각 자리수를 복원하기 위한 Shift를 합치기 위해서 아래의 변수를 선언합니다.

vec4 unpackConst = vec4(4, 16, 64, 256);  차례대로 좌측 시프트 한다는 의미 (<< 2, << 4, << 6, << 8)

이제 dot(fracPart, unpackConst)를 해주면 각각의 컴포넌트 끼리 곱해진다음 더해지기 때문에 LightIndex 복원이 완료됩니다.

추가 : 원래의 구현에서는 Light Index를 0~1 사이 값으로 구해내어 Texture Look up에 사용합니다. 그래서 저의 구현과 다르게 vec4 unpackConst = vec4(4.0, 16.0, 64.0, 256.0) / 256.0, 각각 (>> 6, >> 4, >> 2, >> 0)의 의미인 변수를 사용합니다. 이것은 fracPart가 Light Index를 우측으로 2번 시프트 되어있는 상태이라서 unpackConst 값을 dot(fracPart, unpackConst) 해주게 되면, 총 시프트되는 값은 (>> 8, >> 6, >> 4, >> 2) 입니다. 즉 / 256.0 과 같습니다. 이렇게 복원하게 되면 Light Index 는 0~1 사이 값이 됩니다.

5. 첫번째 라이트 인덱스 복원이 완료되었습니다. 이제 2번째 Light Index의 복원을 진행합니다. 간단하게 앞에서 계산했던 PackedLightIndex *= 0.25 에 floor(PackedLightIndex)로 소수점을 날리고 다시 1번부터 차례대로 계산하면 2번째 Light Index를 얻어낼 수 있습니다. 이러한 방식으로 총 4개의 Light Index를 찾아냅니다.

 

알아둘 점은 Light Index 0의 경우는 라이트가 없는 것으로 처리합니다.

 

구현

C++ 코드

auto& appSetting = jShadowAppSettingProperties::GetInstance();
const bool isLIDRTestTypeDefault = (ELIDR_TestType::Default == appSetting.LIDR_TestType);
int32 MAX_POINT_LIGHT = isLIDRTestTypeDefault ? 255 : 5;

// LIDR 구조체 선언
struct LightData_LIDR
{
	Vector Pos;
	float Radius;
	Vector4 Color;
};

// 라이트 개수만큼 LightData_LIDR 생성
static bool InitLightData = false;
static std::vector<LightData_LIDR> PointLight_LIDR;
if (PointLight_LIDR.size() != MAX_POINT_LIGHT)
{
	PointLight_LIDR.resize(MAX_POINT_LIGHT);
	InitLightData = false;
}

// 라이트가 초기화 되지 않았다면 초기화, 그렇지 않으면 라이트를 업데이트 함.
if (!InitLightData)
{
	InitLightData = true;

	static constexpr float Interval = 60.0f;
	constexpr int count = 15;
	for (int i = 1; i < MAX_POINT_LIGHT; ++i)
	{
		int r = i / count;
		int k = i % count;

		float R = (rand() % 256) / 255.0f;
		float G = (rand() % 256) / 255.0f;
		float B = (rand() % 256) / 255.0f;

		auto& LightData = PointLight_LIDR[r * count + k];
		if (isLIDRTestTypeDefault)
		{
			LightData.Pos = Vector((-count / 2 + r) * Interval, 0.0f, (-count / 2 + k) * Interval);
			LightData.Color = Vector4(R, G, B, 1.0f);
		}
		else
		{
			LightData.Pos = Vector(0.0f);
			if (i == 1)
				LightData.Color = Vector4(0.5f, 0.0f, 0.0f, 1.0f);
			else if (i == 2)
				LightData.Color = Vector4(0.0f, 0.5f, 0.0f, 1.0f);
			else if (i == 3)
				LightData.Color = Vector4(0.0f, 0.0f, 0.5f, 1.0f);
			else if (i == 4)
				LightData.Color = Vector4(0.5f, 0.0f, 0.0f, 1.0f);
		}
		LightData.Radius = 50.0f;
	}
}
else
{
	if (isLIDRTestTypeDefault)
	{
		static bool Sign = true;
		static int32 Temp = 0;
		for (int i = 1; i < MAX_POINT_LIGHT; ++i)
		{
			if (Sign)
			{
				if (++Temp > 20000)
					Sign = false;
			}
			else
			{
				if (--Temp < -20000)
					Sign = true;
			}

			PointLight_LIDR[i].Pos += Vector(
				0.0001f * sinf(i / 255.0f) * (i % 3 + 1)
				, 0.0f
				, 0.0001f * cosf(i / 128.0f)) * (i % 3 + 1) * (float)Temp;
		}
	}
	else
	{
		static bool Sign = true;
		static int32 Temp = 0;
		for (int i = 1; i < MAX_POINT_LIGHT; ++i)
		{
			if (Sign)
			{
				if (++Temp > 200)
					Sign = false;
			}
			else
			{
				if (--Temp < -200)
					Sign = true;
			}

			PointLight_LIDR[i].Pos += Vector(
				0.001f * (i % 4 + 1)
				, 0.0f
				, 0.001f * (i % 4 + 1)) * (float)Temp;
		}
	}
}

auto ClearColor = Vector4(135.0f / 255.0f, 206.0f / 255.0f, 250.0f / 255.0f, 1.0f);	// light sky blue
auto ClearType = ERenderBufferType::COLOR | ERenderBufferType::DEPTH;
auto EnableDepthTest = true;
auto DepthStencilFunc = EComparisonFunc::LESS;
auto EnableBlend = true;
auto BlendSrc = EBlendSrc::ONE;
auto BlendDest = EBlendDest::ZERO;
//auto Shader = jShader::GetShader("Simple");
auto Shader = jShader::GetShader("DepthOnly");

g_rhi->SetClearColor(ClearColor);
g_rhi->SetClear(ClearType);

g_rhi->EnableDepthTest(EnableDepthTest);
g_rhi->SetDepthFunc(DepthStencilFunc);
g_rhi->SetDepthMask(true);						// Depth write on

g_rhi->EnableBlend(EnableBlend);				// Blend on
g_rhi->SetBlendFunc(BlendSrc, BlendDest);		// Src One, Dst Zero

static auto MainRenderTarget = std::shared_ptr<jRenderTarget>(jRenderTargetPool::GetRenderTarget({
	ETextureType::TEXTURE_2D,
	ETextureFormat::RGBA8,
	ETextureFormat::RGBA,
	EFormatType::UNSIGNED_BYTE,
	EDepthBufferType::DEPTH24_STENCIL8,
	SCR_WIDTH,
	SCR_HEIGHT,
	1 }));

// 1. DepthOnly Pass
if (MainRenderTarget->Begin())
{
	g_rhi->SetClear({ERenderBufferType::COLOR | ERenderBufferType::DEPTH});

	const auto& StaticObjectList = jObject::GetStaticObject();
	for (auto& Object : StaticObjectList)
		Object->Draw(MainCamera, Shader, { });
	MainRenderTarget->End();
}

// 2. LightBuffer Pass
Shader = jShader::GetShader("LIDR_LightBuffer");
g_rhi->SetShader(Shader);

g_rhi->SetDepthMask(false);		// depth write off

// Set the dst blend func as CONSTANT_COLOR(0.25) for each texel value shifted by blending
g_rhi->SetBlendFunc(EBlendSrc::ONE, EBlendDest::CONSTANT_COLOR);
g_rhi->SetBlendEquation(EBlendMode::FUNC_ADD);
g_rhi->SetBlendColor(0.251f, 0.251f, 0.251f, 0.251f);

static auto LightBufferRenderTarget = std::shared_ptr<jRenderTarget>(jRenderTargetPool::GetRenderTarget({ 
	ETextureType::TEXTURE_2D,
	ETextureFormat::RGBA8,
	ETextureFormat::RGBA,
	EFormatType::UNSIGNED_BYTE,
	EDepthBufferType::NONE,
	SCR_WIDTH, 
	SCR_HEIGHT, 
	1 }));

// Set the depth buffer of light buffer as depth only pass's depth buffer
LightBufferRenderTarget->SetTextureDetph(MainRenderTarget->GetTextureDepth(), MainRenderTarget->Info.DepthBufferType);

if (LightBufferRenderTarget->Begin())
{
	g_rhi->SetClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	g_rhi->SetClear(ERenderBufferType::COLOR);

	static auto sphere = jPrimitiveUtil::CreateSphere(Vector(0.0f, 0.0f, 0.0f), 0.5, 16, Vector(1.0f), Vector4(1.0f, 1.0f, 1.0f, 1.0f));
	for (int32 i = 1; i < MAX_POINT_LIGHT; ++i)
	{
		auto& Light = PointLight_LIDR[i];
		sphere->RenderObject->Pos = Light.Pos;
		sphere->RenderObject->Scale = Vector(Light.Radius);
		sphere->RenderObject->Color = Light.Color;

		uint8 convertColor = i;
		uint8 redBit = (convertColor & (0x3 << 0)) << 6;
		uint8 greenBit = (convertColor & (0x3 << 2)) << 4;
		uint8 blueBit = (convertColor & (0x3 << 4)) << 2;
		uint8 alphaBit = (convertColor & (0x3 << 6)) << 0;

		Vector4 LightIndex((float)redBit, (float)greenBit, (float)blueBit, (float)alphaBit);
		static float divisor = 255.0f;
		LightIndex = LightIndex / divisor;

		SET_UNIFORM_BUFFER_STATIC(Vector4, "LightIndex", LightIndex, Shader);
		sphere->Draw(MainCamera, Shader, {});
	}

	LightBufferRenderTarget->End();
}

// 3. Base pass (Forward rendering)
Shader = jShader::GetShader("LIDR_BasePass");
g_rhi->SetShader(Shader);

g_rhi->SetDepthMask(true);
g_rhi->SetDepthFunc(EComparisonFunc::LESS);

g_rhi->SetBlendFunc(BlendSrc, BlendDest);
g_rhi->SetBlendEquation(EBlendMode::FUNC_ADD);

if (g_rhi->SetUniformbuffer(&jUniformBuffer<int>("LightBuffer", 0), Shader))
	g_rhi->SetTexture(0, LightBufferRenderTarget->GetTexture());

static auto LightBufferRenderTarget2 = std::shared_ptr<jRenderTarget>(jRenderTargetPool::GetRenderTarget({
	ETextureType::TEXTURE_2D,
	ETextureFormat::RGBA8,
	ETextureFormat::RGBA,
	EFormatType::UNSIGNED_BYTE,
	EDepthBufferType::NONE,
	SCR_WIDTH,
	SCR_HEIGHT,
	1 }));

LightBufferRenderTarget2->SetTextureDetph(MainRenderTarget->GetTextureDepth(), MainRenderTarget->Info.DepthBufferType);

if (LightBufferRenderTarget2->Begin())
{
	g_rhi->SetClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	g_rhi->SetClear(ERenderBufferType::COLOR | ERenderBufferType::DEPTH);

	static auto LightDataUniformBlock = g_rhi->CreateUniformBufferBlock("PointLight_LIDR");
	LightDataUniformBlock->UpdateBufferData(&PointLight_LIDR[0], sizeof(PointLight_LIDR[0]) * PointLight_LIDR.size());
	LightDataUniformBlock->Bind(Shader);

	const auto& StaticObjectList = jObject::GetStaticObject();
	for (auto& Object : StaticObjectList)
		Object->Draw(MainCamera, Shader, { });

	LightBufferRenderTarget2->End();
}

Shader = jShader::GetShader("ColorCopy");
static jFullscreenQuadPrimitive* s_fullscreenQuad = jPrimitiveUtil::CreateFullscreenQuad(LightBufferRenderTarget2->GetTexture());
s_fullscreenQuad->Draw(MainCamera, Shader, {});

Shader (Light Index Texture 생성)

// Vertex Shader
#version 330 core

precision mediump float;

layout(location = 0) in vec3 Pos;

uniform mat4 MVP;

void main()
{
    gl_Position = MVP * vec4(Pos, 1.0);
}

// Fragment Shader
#version 330 core

precision mediump float;

uniform vec4 LightIndex;
out vec4 color;

void main()
{
    color = LightIndex;
}

Shader (Base Pass)

// Vertex Shader
#version 430 core

precision mediump float;

layout(location = 0) in vec3 Pos;
layout(location = 1) in vec4 Color;

uniform mat4 M;
uniform mat4 MVP;

out vec3 WorldPos_;
out vec4 Color_;
out vec4 ClipSpacePos_;

void main()
{
    Color_ = Color;
    WorldPos_ = (M * vec4(Pos, 1.0)).xyz;
    gl_Position = MVP * vec4(Pos, 1.0);
    
    ClipSpacePos_ = gl_Position;
}

// Fragment Shader
#version 430 core

precision mediump float;

uniform sampler2D LightBuffer;

struct LightData_LIDR
{
    vec3 Pos;
    float Radius;
    vec4 Color;
};

layout (std140) uniform PointLight_LIDR
{
    LightData_LIDR PointLight[255];
};

in vec3 WorldPos_;
in vec4 Color_;
in vec4 ClipSpacePos_;
out vec4 color;

void main()
{
    vec4 Temp = ClipSpacePos_ / ClipSpacePos_.w;
    vec2 Coord = Temp.xy * vec2(0.5) + vec2(0.5);
    vec4 packedLightIndex = texture(LightBuffer, Coord);
    
    color = vec4(Color_) * vec4(0.2, 0.2, 0.2, 1.0);	// * Ambient Color
    
    vec4 unpackConst = vec4(4, 16, 64, 256);
    vec4 floorValues = ceil(packedLightIndex * 254.5);

    for (int i = 0; i < 4; ++i)
    {
        packedLightIndex = floorValues * 0.25;
        floorValues = floor(packedLightIndex);
        vec4 fracParts = packedLightIndex - floorValues;

        float tempIndex = dot(fracParts, unpackConst);
        int lightIndex = int(tempIndex);

        float dist = distance(PointLight[lightIndex].Pos, WorldPos_);
        float distSQ = dist * dist;

        if (lightIndex > 0)
        {
            const float LightIntensity = 4.0f;
            color += (PointLight[lightIndex].Color * (LightIntensity / distSQ));
        }
    }
}

 

결과

255개의 라이트를 출력한 결과

 

 

 

4개의 라이트까지 컬러 블랜딩 가능 테스트

 

 

 

예제코드

https://github.com/scahp/Shadows/tree/LightIndexedDeferredRendering

 

Reference

[번역]Light Indexed Deferred Lighting

 

반응형

'Graphics > Graphics' 카테고리의 다른 글

Variance Shadow Map(VSM)  (0) 2020.04.16
Signed Distance Fields  (2) 2020.04.13
Tangent Space, Tanget Vector 생성  (0) 2020.03.28
DeepShadowMap(DSM)  (0) 2020.02.25
Dual Paraboloid ShadowMap  (0) 2020.02.18