본문 바로가기

프로그래밍/Shader

HDR 구현 과정 1부

HDR은 이론에서 나와있듯이 화면의 픽셀들의 평균 휘도 값을 구해서

ToneMapping 공식으로 LDR로는 표현할 수 없는 밝기를 밝게 해주거나 어둡게 해주는 기능이다.

 

HDR을 구현하기 위해서는 ShaderResourceView와 UnorderedAccessView를 만들어주기 위한 버퍼2개, Input용인 Shader Resource View 2개, Output용으로 사용하는 UnorderedAccessView 2개, ToneMapping에 평균 휘도 값을 넣어줄 ShaderResourceView 1개가 필요하다 (이하 Shader Resource View 를 SRV, UnorderedAccessView를 UAV라 하겠다)

 

버퍼에 들어가는 옵션은

D3D11_BUFFER_DESC		tDesc = {};

tDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;
tDesc.StructureByteStride = sizeof(float);
tDesc.ByteWidth = iWidth * tDesc.StructureByteStride;
tDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;

if (FAILED(DEVICE->CreateBuffer(&tDesc, NULL, &m_pBuffer)))
	return false;

이렇게 된다. FAILED 매크로를 사용해주면 정상적으로 생성이 되었는지 확인할 수 있다.

간혹 저부분 때문에 터지는 현상이 생기기 때문에 디버깅할 때 원인을 찾을 수 있다.

 

SRV에 들어가는 옵션은

D3D11_SHADER_RESOURCE_VIEW_DESC tSRVDesc = {};

tSRVDesc.Format = DXGI_FORMAT_UNKNOWN;
tSRVDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER;
tSRVDesc.Buffer.NumElements = iWidth;
if (FAILED(DEVICE->CreateShaderResourceView(m_pBuffer, &tSRVDesc, &m_pSRV)))
	return false;

다음과 같은데 HDR은 FirstPass와 SecondPass를 거치는데 SRV에 들어가는 옵션이 약간 다르다.

 

tSRVDesc.Buffer.NumElements = iWidth;

이부분인데 FirstPass에서는 전체 화면의 픽셀들을 Group으로 나누어서 DownScale할 때 사용하고

SecondPass에서는 FirstPass에서 나온 float 값 하나를 넣어준다.

 

UAV에 들어가는 옵션은 

D3D11_UNORDERED_ACCESS_VIEW_DESC	tUAVDesc = {};

tUAVDesc.Format = DXGI_FORMAT_UNKNOWN;
tUAVDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
tUAVDesc.Buffer.NumElements = iWidth;
if (FAILED(DEVICE->CreateUnorderedAccessView(m_pBuffer, &tUAVDesc,
	&m_pUAV)))
	return false;

다음과 같다. 여기도 

tUAVDesc.Buffer.NumElements = iWidth;

이부분의 값을 SRV와 동일하게 지정해주어야 한다.

 

만들고 난 뒤, 첫번째 SRV와 UAV를 Compute Shader에 넣어준다.

 

FirstPass에서는 3번의 DownScale이 일어나는데

 

1. 16 픽셀 그룹을 하나의 픽셀로 줄인다.

2. 1024에서 4로 DownScale

3. 4 에서 1로 DownScale

 

이렇게 진행된다.

 

SecondPass에서도 3번의 DownScale이 일어나는데

 

1. 64에서 16으로 DownScale

2. 16에서 4로 DownScale

3. 4에서 1로 DownScale

 

이렇게 진행된다.

 

#include "ComputeShare.fx"

Texture2D HDRTex : register(t0);
StructuredBuffer<float> AverageValues1D : register(t1);
RWStructuredBuffer<float> AverageLumFinal : register(u0);

groupshared float SharedPositions[1024];

cbuffer Adapt_Bloom_CB : register(b3)
{
    float   g_fAdaptation;
    float3  vEmpty;
}

cbuffer BloomThresholdCB : register(b4)
{
    float g_fBloomThreshold;
    float3 g_vEmpty;
}

// 각 스레드에 대해 4x4 다운 스케일을 수행한다
float DownScale4x4(uint2 CurPixel, uint groupThreadId)
{
    float avgLum = 0.f;

    if (CurPixel.y < g_Res.y)
    {
        int3 iFullResPos = int3(CurPixel * 4, 0);
        float4 vDownScaled = float4(0.f, 0.f, 0.f, 0.f);

        [unroll]
        for (int i = 0; i < 4; ++i)
        {
            [unroll]
            for (int j = 0; j < 4; ++j)
            {
                vDownScaled += HDRTex.Load(iFullResPos, int2(j, i));
            }
        }
        vDownScaled /= 16;
        
        // 픽셀별 휘도 값 계산
        avgLum = dot(vDownScaled, LUM_FACTOR);

        // 공유 메모리에 결과 기록
        SharedPositions[groupThreadId] = avgLum;
    }

    // 동기화 후 다음 단계로
    GroupMemoryBarrierWithGroupSync();

    return avgLum;
}

// 위에서 구한 값을 4개의 값으로 다운스케일한다
float DownScale1024to4(uint dispachThreadId, uint groupThreadId,
    float avgLum)
{
    // 다운스케일 코드를 확장
    for (uint iGroupSize = 4, iStep1 = 1, iStep2 = 2, iStep3 = 3;
        iGroupSize < 1024;
        iGroupSize *= 4, iStep1 *= 4, iStep2 *= 4, iStep3 *= 4)
    {
        if (groupThreadId % iGroupSize == 0)
        {
            float fStepAvgLum = avgLum;

            fStepAvgLum += dispachThreadId + iStep1 < g_Domain ?
                SharedPositions[groupThreadId + iStep1] : avgLum;

            fStepAvgLum += dispachThreadId + iStep2 < g_Domain ?
                SharedPositions[groupThreadId + iStep2] : avgLum;

            fStepAvgLum += dispachThreadId + iStep3 < g_Domain ?
              SharedPositions[groupThreadId + iStep3] : avgLum;

            // 결과 값 저장
            avgLum = fStepAvgLum;
            SharedPositions[groupThreadId] = fStepAvgLum;
        }
        // 동기화 후 다음으로
        GroupMemoryBarrierWithGroupSync();
    }
    return avgLum;
}

// 4개의 값을 하나의 평균값으로 다운스케일한 후 저장한다
void DownScale4to1(uint dispatchThreadId, uint groupThreadId,
    uint groupId, float avgLum)
{
    if (groupThreadId == 0)
    {
        //  스레드 그룹에 대한 평균 휘도 값 계산
        float fFinalAvgLum = avgLum;

        fFinalAvgLum += dispatchThreadId + 256 < g_Domain ?
            SharedPositions[groupThreadId + 256] : avgLum;

        fFinalAvgLum += dispatchThreadId + 512 < g_Domain ?
            SharedPositions[groupThreadId + 512] : avgLum;

        fFinalAvgLum += dispatchThreadId + 768 < g_Domain ?
            SharedPositions[groupThreadId + 768] : avgLum;

        fFinalAvgLum /= 1024.f;

        // 최종 값을 ID UAV에 기록 후 다음 과정으로
        AverageLumFinal[groupId] = fFinalAvgLum;
    }
}

// 이렇게 구한 값은 셰이더 엔트리 포인트에 대입된다
[numthreads(1024, 1, 1)]
void DownScaleFirstPass(uint3 groupId : SV_GroupID,
    uint3 dispatchThreadId : SV_DispatchThreadID,
    uint3 groupThreadId : SV_GroupThreadID)
{
    uint2 vCurPixel = uint2(dispatchThreadId.x % g_Res.x,
    dispatchThreadId.x / g_Res.x);

    // 16 픽셀 그룹을 하나의 픽셀로 줄여 공유 메모리에 저장
    float favgLum = DownScale4x4(vCurPixel, groupThreadId.x);

    // 1024에서 4로 다운스케일
    favgLum = DownScale1024to4(dispatchThreadId.x, groupThreadId.x,
         favgLum);

    // 4에서 1로 다운스케일
    DownScale4to1(dispatchThreadId.x, groupThreadId.x, groupId.x,
        favgLum);

    // 이 컴퓨트 셰이더는 x 값 (백버퍼의 총 픽셀 수 / (16 * 1024)) 에 따라 묶어서 처리할 수 있다
}

// 첫 번째 컴퓨트 셰이더의 실행이 완료되면 동일한 상수 버퍼를 사용한 두번째 컴퓨트 셰이더를 실행한다
// 중간 값 휘도 SRV와 평균 휘도 UAV 값을 지정해 사용한다
#define MAX_GROUPS 64

// 공유 메모리 그룹에 중간 값 저장
groupshared float SharedAvgFinal[MAX_GROUPS];

[numthreads(MAX_GROUPS, 1, 1)]
void DownScaleSecondPass(uint3 groupId : SV_GroupID,
        uint3 groupThreadId : SV_GroupThreadID,
        uint3 dispatchThreadId : SV_DispatchThreadID)
{
    // 공유 메모리에 ID값 저장
    float favgLum = 0.f;

    if (dispatchThreadId.x < g_GroupSize)
    {
        favgLum = AverageValues1D[dispatchThreadId.x];
    }

    SharedAvgFinal[dispatchThreadId.x] = favgLum;

    GroupMemoryBarrierWithGroupSync(); // 동기화 후 다음 과정으로

    // 64에서 16으로 다운 스케일
    if (dispatchThreadId.x % 4 == 0)
    {
        // 휘도 값 합산
        float fstepAvgLum = favgLum;

        fstepAvgLum += dispatchThreadId.x + 1 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 1] : favgLum;

        fstepAvgLum += dispatchThreadId.x + 2 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 2] : favgLum;

        fstepAvgLum += dispatchThreadId.x + 3 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 3] : favgLum;

        // 결과 값 저장
        favgLum = fstepAvgLum;

        SharedAvgFinal[dispatchThreadId.x] = fstepAvgLum;
    }

    GroupMemoryBarrierWithGroupSync(); // 동기화 후 다음 과정으로

    // 16에서 4로 다운스케일
    if (dispatchThreadId.x % 16 == 0)
    {
        // 휘도 값 합산
        float fstepAvgLum = favgLum;

        fstepAvgLum += dispatchThreadId.x + 4 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 4] : favgLum;

        fstepAvgLum += dispatchThreadId.x + 8 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 8] : favgLum;

        fstepAvgLum += dispatchThreadId.x + 12 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 12] : favgLum;

        // 결과 값 저장
        favgLum = fstepAvgLum;
        SharedAvgFinal[dispatchThreadId.x] = fstepAvgLum;
    }
    
    GroupMemoryBarrierWithGroupSync(); // 동기화 후 다음 과정으로

    // 4에서 1로 다운스케일
    if (dispatchThreadId.x == 0)
    {
        // 휘도 값 합산
        float fFinalLumValue = favgLum;

        fFinalLumValue += dispatchThreadId.x + 16 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 16] : favgLum;

        fFinalLumValue += dispatchThreadId.x + 32 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 32] : favgLum;

        fFinalLumValue += dispatchThreadId.x + 48 < g_GroupSize ?
            SharedAvgFinal[dispatchThreadId.x + 48] : favgLum;

        fFinalLumValue /= 64.f;
        
        AverageLumFinal[0] = max(fFinalLumValue, 0.0001);
    }
}

 

이렇게 과정을 거쳐서 만들어진 평균 휘도값을 가지고 ToneMapping 공식에다 적용해주는데

2부에서 설명하겠다.

'프로그래밍 > Shader' 카테고리의 다른 글

Bloom 구현 과정  (0) 2019.06.18
HDR 구현과정 2부  (0) 2019.06.11
Rim Light 이론과 구현  (0) 2019.04.04
Adaptation 구현 과정  (0) 2019.04.03
기하 셰이더 (Geometry Shader)  (0) 2019.03.29