| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- DirectX12
- ue4
- atmospheric
- deferred
- Study
- ShadowMap
- 번역
- Graphics
- VGPR
- GPU Driven Rendering
- SIMD
- wave
- texture
- unrealengine
- Nanite
- optimization
- rendering
- forward
- GPU
- vulkan
- RayTracing
- SGPR
- Wavefront
- shader
- scattering
- hzb
- UE5
- scalar
- DX12
- Shadow
- Today
- Total
RenderLog
Tiled Forward Rendering 본문
Tiled Forward Rendering
최초작성 : 2020-05-12
마지막수정 : 2020-05-27
최재호
목표
Tiled Forward Rendering 의 알고리즘과 구현 이해
내용
Tiled Forward Rendering은 포워드 렌더링에서의 라이팅 연산을 줄이기 위한 방식입니다. 이름 그대로 렌더링 방식은 포워드 렌더링과 동일하지만 각 픽셀별로 적용할 라이팅의 개수가 줄어드는 점이 차이점입니다.
렌더링 과정을 간단히 요약하자면 아래와 같습니다.
1. 화면을 NxN 타일 크기로 나누고, 타일과 교차되는 Light 들을 각 타일에 할당합니다.
2. 실제 오브젝트를 렌더링할때, 현재 픽셀이 어느 타일에 포함되는지 찾고, 찾은 타일에 포함되어있는 라이트들을 가져와 현재 픽셀에 적용합니다.
Tiled Forward Rendering은 Tiled Deferred Rendering의 확장 혹은 수정 버젼입니다.
이 둘과 비교해서 Tiled Forward Rendering의 장점은 아래와 같습니다.
- Transparency 지원
- 디퍼드렌더링과 다르게 Transparent 오브젝트들의 순서만 맞게 그려준다면 Transparent 오브젝트도 쉽게 렌더링 가능합니다.
- 하드웨어 MSAA 지원
- 디퍼드 렌더링에서 MSAA를 지원해주려면 G-Buffer 를 Multisampling 지원 형태로 만들어야 하며, 이렇게 되면서 메모리와 성능이 감소할 수 있습니다.
- 서로 다른 쉐이더 지원
- 디퍼드 렌더링의 단점으로 Uber Shader를 사용하게 된다는 점입니다. 디퍼드 렌더링의 경우 G-Buffer에 저장할 데이터 종류나 라이트 종류에 따라 다양한 Shader 조합이 나옵니다. 이것을 줄이기 위해서 Uber Shader의 Define을 사용하여 최대한 Shader를 조각내지 않고 사용하게 됩니다. Forward Rendering에서는 특정 버퍼에 약속된 지오메트리 속성을 기록할 필요가 없기 때문에 그릴 오브젝트에 딱 맞는 Shader를 사용할 수 있습니다. (픽셀 셰이더 정도만 달라짐)
Tiled Forward Rendering과 퍼포먼스
1. Light 크기에 따른 퍼포먼스
- Light 크기가 전체 화면을 덮을 만큼 크다면, 각 픽셀은 거의 모든 라이트에 대한 라이팅 처리를 진행할 것입니다. 이경우에 Tile을 만들어 Light를 할당하는 작업이 무의미 합니다.
2. Top-View or First-Person View
- Top-View의 경우 대부분의 오브젝트가 View기준 가장 먼쪽에 몰려있습니다. 이 경우는 Tile 최소, 최대 Depth 값을 기준으로 Culling 되는 Light는 많지 않을 것입니다.
- First-Person VIew의 경우는 오브젝트들이 서로다른 Depth 위치에 골고루 배치되어있기 때문에 각 Tile의 최소, 최대 값을 사용해 Light를 Culling하는 것이 도움이 됩니다. (아래 그림 참고)

Opaque vs Transparent Object의 Light Culling과 차이점
1. Opaque의 경우는 그림2 (a) 노란색 영역과 겹치는 라이트만 고려해도 문제 없습니다.
- Depth 기준 Min, Max 영역으로 Light를 제거합니다.
2. Transparent의 경우는 뒤에있는 Opaque 의 색상 또한 다 표시됩니다.
- Depth의 Max 값 기준으로만 Light를 제거 합니다.

상세 코드 분석
1. 화면을 일정한 크기의 2차원 타일공간으로 나눕니다.
2. Pre-Z Pass 에서 Depth Buffer를 렌더링합니다. 이 때 렌더링하는 Depth 값은 View Space를 기준으로 합니다.
3. Depth Buffer를 Tiled 단위로 쪼개고, Tiled 당 최소와 최대 Depth 값을 계산합니다.
void main()
{
vec2 minMax = vec2(1.0f, -1.0f);
ivec2 offset = ivec2(gl_FragCoord.xy) * ivec2(LIGHT_GRID_TILE_DIM_X, LIGHT_GRID_TILE_DIM_Y);
ivec2 end = min(fbSize, offset + ivec2(LIGHT_GRID_TILE_DIM_X, LIGHT_GRID_TILE_DIM_Y));
for (int j = offset.y; j < end.y; ++j)
{
for (int i = offset.x; i < end.x; ++i)
{
for (int sampleIndex = 0; sampleIndex < NUM_MSAA_SAMPLES; ++sampleIndex)
{
// sampleIndex는 MSAA 사용시만 사용됨, 그렇지 않으면 0
float d = texelFetch(depthTex, ivec2(i,j), sampleIndex).x;
// Note: for opaque geometry, this is an optimization as it prevents tiles that show the far plane
// to extend all the way there. However, when there is transparent geoemtry present, this is not
// it may lie in this range.
// if (d < 1.0)
{
minMax.x = min(minMax.x, d);
minMax.y = max(minMax.y, d);
}
}
}
}
// ViewSpace MinMax 값을 계산
minMax = vec2(fetchPosition(vec2(0.0, 0.0), minMax.x).z, fetchPosition(vec2(0.0, 0.0), minMax.y).z);
resultMinMax = minMax;
}
4. 라이트가 교차하는 Tile 을 찾아 할당합니다. (Opaque와 Transparent 분리하여 할당)
모든 라이트에 대해서 ScreenSpace(화면 해상도 기준)에서 차지하는 Rect(min, max) 값을 구함.
void LightGrid::buildRects(const chag::uint2 resolution, const Lights &lights, const chag::float4x4 &modelView, const chag::float4x4 &projection, float near)
{
MY_PROFILE_SCOPE("BuildRects");
m_viewSpaceLights.clear();
m_screenRects.clear();
for (uint32_t i = 0; i < lights.size(); ++i)
{
const Light &l = lights[i];
chag::float3 vp = transformPoint(modelView, l.position);
// 여기서 Light가 화면 해상도 기준으로 차지하는 Rect를 얻어낸다.
ScreenRect rect = findScreenSpaceBounds(projection, vp, l.range, resolution.x, resolution.y, near);
if (rect.min.x < rect.max.x && rect.min.y < rect.max.y)
{
m_screenRects.push_back(rect);
// save light in model space
m_viewSpaceLights.push_back(make_light(vp, l));
}
}
MY_PROFILE_COUNTER("NumVisibleLights", int(m_viewSpaceLights.size()));
}
5. Screen Rect가 포함되는 모든 타일을 찾아서 해당 타일에 Light Count를 증가시켜줍니다.
- (ScreenRect.min / TileSize), ((ScreenRect.max + TileSize - 1) / TileSize) 와 같은 방식으로 Min, Max 사이의 Tile 들을 한번씩만 방문
// Screen Rect 가 포함되는 모든 타일을 찾아서 해당 타일에 라이트 카운트만 올려준다.
for (size_t i = 0; i < m_screenRects.size(); ++i)
{
ScreenRect r = m_screenRects[i];
Light light = m_viewSpaceLights[i];
// TileSize로 나눠줘서 화면공간 -> 타일공간으로 변화 시키고, Light 가 교차하는 타일만 한번씩 방문한다.
chag::uint2 l = clamp(r.min / tileSize, make_vector<uint32_t>(0,0), m_gridDim + 1);
chag::uint2 u = clamp((r.max + tileSize - 1) / tileSize, make_vector<uint32_t>(0,0), m_gridDim + 1);
for (uint32_t y = l.y; y < u.y; ++y)
{
for (uint32_t x = l.x; x < u.x; ++x)
{
// 현재 과정에서는 m_minMaxGridValid 가 false라 무조건 통과
if (!m_minMaxGridValid || testDepthBounds(gridMinMaxZPtr[y * m_gridDim.x + x], light))
{
GRID_COUNTS(x, y) += 1;
++totalus;
}
}
}
}
6. TileLightIndexList 를 Tile에 추가된 라이트 개수만큼 할당한다. 이 컨테이너에 모든 Light의 Index가 기록됩니다.
m_tileLightIndexLists.resize(totalus);
7. 각 Tile 별로 순회하면서, 각 타일의 Light Index가 시작되는 Offset 값을 저장합니다.
- Offset의 시작을 타일에서의 라이트 Index 저장공간의 가장 끝 부분으로 설정합니다.
- 실제 라이트 값을 채워넣을때 뒤에서 부터 라이트 인덱스를 채워넣는다.
// 각 타일별로 라이트가 저장되어있는 Offset 값을 계산한다.
// Offset은 해당 타일에서 저장하는 라이트의 가장 끝 부분에 Offset을 저장하며
// 실제 라이트 값을 채워넣을때 뒤에서 부터 라이트 인덱스를 채워넣는다.
uint32_t offset = 0;
for (uint32_t y = 0; y < m_gridDim.y; ++y)
{
for (uint32_t x = 0; x < m_gridDim.x; ++x)
{
uint32_t count = GRID_COUNTS(x,y);
// 라이트 오프셋을 맨 끝으로 설정해두고 채워 넣을때 Offset을 하나씩 감소시킴
GRID_OFFSETS(x,y) = offset + count;
offset += count;
}
}
8. Screen Sapce의 Light Rect를 이용해, 이 라이트가 포함된 타일을 찾고 해당 타일의 Offset 위치 -1 한 위치에 현재 Light 위치를 기록합니다. 그리고 모든 라이트에 대해서 이과정을 반복합니다.
// 화면공간의 Light Rect를 이용해, 이 라이트가 포함된 타일을 찾고 해당 타일의 Offset 위치 -1 한 위치에 현재 Light 위치를 기록한다.
if (m_screenRects.size() && !m_tileLightIndexLists.empty())
{
int *data = &m_tileLightIndexLists[0];
for (size_t i = 0; i < m_screenRects.size(); ++i)
{
uint32_t lightId = uint32_t(i);
Light light = m_viewSpaceLights[i];
ScreenRect r = m_screenRects[i];
chag::uint2 l = clamp(r.min / tileSize, make_vector<uint32_t>(0,0), m_gridDim + 1);
chag::uint2 u = clamp((r.max + tileSize - 1) / tileSize, make_vector<uint32_t>(0,0), m_gridDim + 1);
for (uint32_t y = l.y; y < u.y; ++y)
{
for (uint32_t x = l.x; x < u.x; ++x)
{
// 이과정에서는 m_minMaxGridValid는 False라 항상 통과
if (!m_minMaxGridValid || testDepthBounds(gridMinMaxZPtr[y * m_gridDim.x + x], light))
{
// 다음 비어있는 슬롯으로 거꾸로 저장한다
uint32_t offset = GRID_OFFSETS(x, y) - 1;
data[offset] = lightId;
GRID_OFFSETS(x,y) = offset;
}
}
}
}
}
9. 이제 화면을 기준으로 한 2D Tiled 포함된 모든 라이트를 전부 모았습니다. 다음로 Opaque와 Transparent 오브젝트들에 따라서 Depth 값을 비교하여 포함되지 않은 라이트를 제거합니다.
- Opaque 와 Transparent 에 각각 Tile 에 포함된 라이트 정보를 별도로 관리합니다. 이유는 Depth 기준으로 Tile에 포함된 라이트를 제거하는 방식이 서로 다르기 때문입니다. 위의 그림2 참고
- 위에서 만든 Tile 정보에서 Depth Buffer의 Tiled 당 최대 최소 Depth 값을 계산했던 자료를 사용합니다.
- 이 값의 Z값을 사용하여 현재 Tile 정보에 들어있는 Light 들 중 [MinZ, MaxZ] 범위 바깥에 있는 라이트들을 모두 제거합니다.
const float2 *gridMinMaxZPtr = m_minMaxGridValid ? &m_gridMinMaxZ[0] : 0;
int *lightInds = &m_tileLightIndexLists[0];
#define GRID_OFFSETS(_x_,_y_) (m_gridOffsets[_x_ + _y_ * LIGHT_GRID_MAX_DIM_X])
#define GRID_COUNTS(_x_,_y_) (m_gridCounts[_x_ + _y_ * LIGHT_GRID_MAX_DIM_X])
int totalus = 0;
m_maxTileLightCount = 0;
for (uint32_t y = 0; y < m_gridDim.y; ++y)
{
for (uint32_t x = 0; x < m_gridDim.x; ++x)
{
uint32_t count = GRID_COUNTS(x,y);
uint32_t offset = GRID_OFFSETS(x,y);
for (uint32_t i = 0; i < count; ++i)
{
// Light는 ViewSpace 에서의 Z 값을 가지고 있다.
const Light &l = m_viewSpaceLights[lightInds[offset + i]];
// GridMinMaxZPtr도 ViewSpace 의 Z 값을 가지고 있음
// 라이트와 Tile이 Z방향으로 교차하지 않는다면, 현재 타일의 Light Index를 저장한 공간에서
// 맨 끝 위치로 옮기고 카운트를 1 감소시켜준다.
if (!testDepthBounds(gridMinMaxZPtr[y * m_gridDim.x + x], l))
{
std::swap(lightInds[offset + i], lightInds[offset + count - 1]);
--count;
}
}
totalus += count;
GRID_COUNTS(x,y) = count; // 감소시킨 카운트를 저장한다.
m_maxTileLightCount = chag::max(m_maxTileLightCount, count);
}
}
- Transparent 오브젝트의 경우 [-near, MaxZ] 범위 바깥이 있는 라이트를 모두 제거합니다
// Transparent Object의 경우는 Tile의 Max Z 값을 기준으로만 컬링할 것이므로,
// Min Z 값을 -Near(Frustum의 Near임)으로 설정하여 카메라 방향에 있는 Light는 컬링하지 않도록 함.
for( std::vector<chag::float2>::iterator it = m_gridMinMaxZ.begin(); it != m_gridMinMaxZ.end(); ++it )
{
it->x = -aNear;
}
// 아래 내용은 Opaque Object와 동일
const float2 *gridMinMaxZPtr = m_minMaxGridValid ? &m_gridMinMaxZ[0] : 0;
int *lightInds = &m_tileLightIndexLists[0];
#define GRID_OFFSETS(_x_,_y_) (m_gridOffsets[_x_ + _y_ * LIGHT_GRID_MAX_DIM_X])
#define GRID_COUNTS(_x_,_y_) (m_gridCounts[_x_ + _y_ * LIGHT_GRID_MAX_DIM_X])
int totalus = 0;
m_maxTileLightCount = 0;
for (uint32_t y = 0; y < m_gridDim.y; ++y)
{
for (uint32_t x = 0; x < m_gridDim.x; ++x)
{
uint32_t count = GRID_COUNTS(x,y);
uint32_t offset = GRID_OFFSETS(x,y);
for (uint32_t i = 0; i < count; ++i)
{
const Light &l = m_viewSpaceLights[lightInds[offset + i]];
if (!testDepthBounds(gridMinMaxZPtr[y * m_gridDim.x + x], l))
{
std::swap(lightInds[offset + i], lightInds[offset + count - 1]);
--count;
}
}
totalus += count;
GRID_COUNTS(x,y) = count;
m_maxTileLightCount = chag::max(m_maxTileLightCount, count);
}
}
10. TileLightIndexList를 R32I 텍스쳐 버퍼 타입에 저장합니다.
- Tile 당 TileLightIndexList에서 얼마나 떨어져있는지를 나타내는 Offset과 몇개의 라이트를 사용하지 여부인 Count를 정보를 Vector2D 형태로 만들어 UniformBuffer로 만듭니다.
- 위에서 만든 픽셀당 Offset과 Count를 저장한 데이터를 UniformBuffer에 저장하여 쉐이더에 넘깁니다.
- TileLightIndexList는 Texture에 저장하여 쉐이더에 넘깁니다.
// 각 Tile이 LightIndexList에서의 Offset과 Count 값을 Vector2D에 담는다.
static chag::int4 tmp[LIGHT_GRID_MAX_DIM_X * LIGHT_GRID_MAX_DIM_Y];
{
const int *counts = lightGrid.tileCountsDataPtr();
const int *offsets = lightGrid.tileDataPtr();
for (int i = 0; i < LIGHT_GRID_MAX_DIM_X * LIGHT_GRID_MAX_DIM_Y; ++i)
{
tmp[i] = chag::make_vector(counts[i], offsets[i], 0, 0);
}
}
// Uniform Buffer에 타일별 Light OFfset과 Count 값을 저장
g_gridBuffer.copyFromHost(tmp, LIGHT_GRID_MAX_DIM_X * LIGHT_GRID_MAX_DIM_Y);
// LightIndexList는 Texture Buffer(R32I) 형태로 저장하여 Shader로 넘긴다.
if (lightGrid.getTotalTileLightIndexListLength())
{
g_tileLightIndexListsBuffer.copyFromHost(lightGrid.tileLightIndexListsPtr(), lightGrid.getTotalTileLightIndexListLength());
// This should not be neccessary, but for amd it seems to be (HD3200 integrated)
glBindTexture(GL_TEXTURE_BUFFER, g_tileLightIndexListsTexture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32I, g_tileLightIndexListsBuffer);
CHECK_GL_ERROR();
}
bindTexture(GL_TEXTURE_BUFFER, TDTU_LightIndexData, g_tileLightIndexListsTexture);
...
g_gridBuffer.bindSlot(GL_UNIFORM_BUFFER, TDUBS_LightGrid);
...
11. Opaque -> AlphaTest -> Tranparent 오브젝트 순서로 렌더링합니다.
// 투명 오브젝트를 렌더링하기 전에 투명 오브젝트용 Tile별 Light 포함 정보 반영
bindLightGridConstants(g_lightGridOpaque);
{
PROFILE_SCOPE_2("Opaque", TT_OpenGl);
g_model->render(g_tiledForwardShader, OBJModel::RF_Opaque);
}
{
PROFILE_SCOPE_2("AlphaTested", TT_OpenGl);
g_model->render(g_tiledForwardShader, OBJModel::RF_AlphaTested);
}
// draw set of transparent geometry, this time enabling blending.
{
PROFILE_SCOPE_2("Transparent", TT_OpenGl);
// 투명 오브젝트를 렌더링하기 전에 투명 오브젝트용 Tile별 Light 포함 정보 반영
bindLightGridConstants(g_lightGridTransparent);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
g_model->render(g_tiledForwardShader, OBJModel::RF_Transparent, modelView);
glDisable(GL_BLEND);
}
12. 렌더링 중 현재 픽셀이 몇번째 타일의 픽셀인지 찾습니다. 찾아낸 Tile에 포함되어있는 Light만 모아서 현재 픽셀에 라이팅 처리를 합니다.
// FOR_EACH_LIGHT_BEGIN/END()
#define FOR_EACH_LIGHT_BEGIN( lightIdent, lightData ) \
{ \
// gl_FragCoord 현재 픽셀이 화면공간(화면해상도기준) 어디있는 위치를 알려줌 (ex. x : 100, y : 200)
ivec2 tileIndex = ivec2(int(gl_FragCoord.x) / LIGHT_GRID_TILE_DIM_X, int(gl_FragCoord.y) / LIGHT_GRID_TILE_DIM_Y); \
ivec2 countOffset = lightGridCountOffsets[tileIndex.x + tileIndex.y * LIGHT_GRID_MAX_DIM_X].xy; \
\
LightData lightData;\
for( int lightIndex = 0; lightIndex < countOffset.x; ++lightIndex ) \
{ \
int lightIdent = texelFetch( tileLightIndexListsTex, countOffset.y + lightIndex).x; \
light_get_data(lightIdent, lightData);
#define FOR_EACH_LIGHT_END() \
} \
}
P.S Forward Plus Rendering은 Tiled Forward Rendering과 같은 구현방식입니다. 별도의 글에서 GPU로 연산하는 형태로 예제를 추가했습니다. 여기 에서 보실 수 있습니다.
레퍼런스
GPU PRO 4, Tiled Forward Shading, Markus Billeter, Ola Olsson and Ulf Assarsson
'Graphics > Graphics' 카테고리의 다른 글
| Forward Plus Rendering (0) | 2020.05.26 |
|---|---|
| PCSS(Percentage-Closer Soft Shadow) (0) | 2020.05.22 |
| Shadow Volume (Stencil Shadow) - 구현 (2/2) (0) | 2020.05.05 |
| Shadow Volume (Stencil Shadow) - 원리 (1/2) (0) | 2020.04.29 |
| Exponential Shadow Map(ESM) (0) | 2020.04.23 |