본문 바로가기

그래픽스

[그래픽스] HDR 파이프라인

PBR을 사용할 때 중요한 요소는 환경맵을 HDRI로 사용하는 것이다. 

그말은 조명을 HDR로 사용하겠다는 것이고 결과적으로 전체적인 렌더링을 HDR로 해야 한다. 

그러면 렌더링 파이프라인 자체를 HDR로 업그레이드 해줘야 한다. 

 

 

Scene을 래스터라이징을 통해 렌더링을 해서 MSAA Render Target을 만든다. MSAA는 한 픽셀의 여러 샘플을 가지고 있으므로 Resolve를 해서 한 픽셀당 하나의 샘플을 가지고 있는 일반적인 Texture로 만든다. 그런데 이제 UNORM이 아닌 

FP (Floating Point) 형태이다. 이걸 가지고 Post Process (이미지 처리) 를 할 수 있다. 그리고 ToneMap을 해서 SDR로 

바꿔준다. SDR은 LDR(Low Dynamic Range)과 같은 의미이다. 여기에 또 Post Process를 할 수 있다. 

 

 

    m_context->ClearRenderTargetView(m_floatRTV.Get(), clearColor);
    
    ...
    
    // MSAA로 Texture2DMS에 렌더링 된 결과를 Texture2D로 변환(Resolve)
    m_context->ResolveSubresource(m_resolvedBuffer.Get(), 0,
                                  m_floatBuffer.Get(), 0,
                                  DXGI_FORMAT_R16G16B16A16_FLOAT);

 

코드에서 변경점은 먼저 RenderTargetView를 픽셀 포맷을 float으로 사용하는 RTV로 바꿔주고, 

MSAA Render Target은 Texture2DMS 형식인데 ResolveSubResource를 통해 Texture2D로 변환한다. 

 

 

    DXGI_SWAP_CHAIN_DESC sd;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; //ImGui 폰트가 두꺼워짐

 

스왑체인의 픽셀포맷이 앞서 HDRI 설명글에서는 FLOAT 형식이었는데 다시 UNORM 형태로 돌아왔다. 모니터들이 

내부적으로 사용하는 포맷이기 때문에 메모리 낭비를 줄이기 위함이다. 그리고 SwapEffect 가 FLIP_DISCARD로 바뀌었는데, 스왑체인에서 MSAA를 사용하지 않을 때 사용가능한 옵션이다. HDR 파이프라인에서는 MSAA RenderTarget이

따로 있고 스왑체인 자체는 MSAA를 사용하지 않으므로 이 옵션을 사용할 수 있으며 속도가 더 빠르다고 한다.

 

 

 // 레스터화 -> float/depthBuffer(MSAA) -> resolved -> backBuffer

    // BackBuffer는 화면으로 최종 출력되기 때문에  RTV만 필요하고 SRV는 불필요
    ComPtr<ID3D11Texture2D> backBuffer;
    ThrowIfFailed(
        m_swapChain->GetBuffer(0, IID_PPV_ARGS(backBuffer.GetAddressOf())));
    ThrowIfFailed(m_device->CreateRenderTargetView(
        backBuffer.Get(), NULL, m_backBufferRTV.GetAddressOf()));

    // FLOAT MSAA RenderTargetView/ShaderResourceView
    ThrowIfFailed(m_device->CheckMultisampleQualityLevels(
        DXGI_FORMAT_R16G16B16A16_FLOAT, 4, &m_numQualityLevels));

    D3D11_TEXTURE2D_DESC desc;
    backBuffer->GetDesc(&desc);
    desc.MipLevels = desc.ArraySize = 1;
    desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
    desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
    desc.Usage = D3D11_USAGE_DEFAULT; // 스테이징 텍스춰로부터 복사 가능
    desc.MiscFlags = 0;
    desc.CPUAccessFlags = 0;
    if (m_useMSAA && m_numQualityLevels) {
        desc.SampleDesc.Count = 4;
        desc.SampleDesc.Quality = m_numQualityLevels - 1;
    } else {
        desc.SampleDesc.Count = 1;
        desc.SampleDesc.Quality = 0;
    }

    ThrowIfFailed(
        m_device->CreateTexture2D(&desc, NULL, m_floatBuffer.GetAddressOf()));

    ThrowIfFailed(m_device->CreateShaderResourceView(
        m_floatBuffer.Get(), NULL, m_floatSRV.GetAddressOf()));

    ThrowIfFailed(m_device->CreateRenderTargetView(m_floatBuffer.Get(), NULL,
                                                   m_floatRTV.GetAddressOf()));

 

RenderTarget이나 Post Processing에 사용되는 Texture들을 만들때  FLOAT로 MSAA 체크를 하고  Texture을 만들때도

FLOAT와 MSAA 설정을 한다. 이 Texture로 SRV, RTV도 만든다.

 

 

  // FLOAT MSAA를 Relsolve해서 저장할 SRV/RTV
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;  //여기까지 멀티샘플링 끄기
    ThrowIfFailed(m_device->CreateTexture2D(&desc, NULL,
                                            m_resolvedBuffer.GetAddressOf()));
    ThrowIfFailed(m_device->CreateShaderResourceView(
        m_resolvedBuffer.Get(), NULL, m_resolvedSRV.GetAddressOf()));
    ThrowIfFailed(m_device->CreateRenderTargetView(
        m_resolvedBuffer.Get(), NULL, m_resolvedRTV.GetAddressOf()));

 

MSAA RTV를 다시 가져온다. 멀티샘플링하는 Texture2DMS를 다른 곳에 리소스뷰로 넣어주기 위해서 FP RenderTarget으로 만들어야 한다. 그래서 멀티 샘플링 꺼주고 다시 SRV, RTV를 만든다.

 

 

 vector<ID3D11RenderTargetView *> renderTargetViews = {m_floatRTV.Get()};
    m_context->OMSetRenderTargets(UINT(renderTargetViews.size()),
                                  renderTargetViews.data(),
                                  m_depthStencilView.Get());            
                                  
                                  
     ... 물체들 렌더링
     
     
     // MSAA로 Texture2DMS에 렌더링 된 결과를 Texture2D로 변환(Resolve)
    m_context->ResolveSubresource(m_resolvedBuffer.Get(), 0,
                                  m_floatBuffer.Get(), 0,
                                  DXGI_FORMAT_R16G16B16A16_FLOAT);

    m_postProcess.Render(m_context);

 

정리하면 floatRTV << MSAA RT로 물체들을 렌더링하고 floatBuffer을 Resolve해서 resolvedBuffer에 저장하고 

resolvedBuffer은 다른 쉐이더에 넣어줄 수 있는데 postProcess에 넣어준다. 

 

 

            m_postProcess.Initialize(m_device, m_context, {m_resolvedSRV},
                                     {m_backBufferRTV}, m_screenWidth,
                                     m_screenHeight, 4);

 

postProcess는 resolvedSRV를 입력받고 m_backBufferRTV에 출력한다. 내부적으로 이미지 필터를 이용해 처리한다.

 

 

 // Combine + ToneMapping
    m_combineFilter.Initialize(device, context, m_combinePixelShader, width,
                               height);
    m_combineFilter.SetShaderResources({resources[0], m_bloomSRVs[0]});
    m_combineFilter.SetRenderTargets(targets);
    m_combineFilter.m_constData.strength = 0.0f; // Bloom strength
    m_combineFilter.m_constData.option1 = 1.0f;  // Exposure로 사용
    m_combineFilter.m_constData.option2 = 2.2f;  // Gamma로 사용
    m_combineFilter.UpdateConstantBuffers(device, context);

 

postProcess에서 이미지 필터를 통해 처리할 때 마지막에 원본과 적용된 효과를 합치는 combineFilter에서 ToneMapping까지 함께 수행한다. 

 

 

 

HDRI를 렌더링할 때 어둡게 렌더링된다. 컴퓨터는 모니터가 사람 눈에 맞춰서 밝기를 조절하도록 일부러 밝기를 낮춘다.

HDRI의 픽셀 값은 세개의 선 중 중간 직선인데, 컴퓨터가 아래쪽 곡선으로 감마를 낮추는 것이다. 그래서 감마를 보정하기 

위해 위쪽 곡선으로 일부러 이미지의 감마를 올린다. 그러면 이미지의 감마가 모니터의 감마를 상쇄시켜 원래 표현하려던

이미지의 감마가 표현된다.

 

그런데 이러면 HDRI가 아닌 이미지들은 더 밝게 렌더링된다. 그 Texture는 원래 UNORM 픽셀 포맷을 사용하고 있었는데

UNORM_SRGB로 바꿔주면 해결된다. 내부적으로 Gamma Correction을 해서 HDRI와 같은 공간에서 (Linear Space - 가운데 직선) 작업을 하기 때문이다. 

 

다시 정리를 하자면, 스왑체인의 픽셀 포맷은 UNORM, HDRI 이미지 픽셀 포맷은 FLOAT을 사용한다. HDRI 이미지는 FLOAT를 사용하기 때문에 Linear Space에 있다. HDR 기반 이미지 라이팅과 PBR을 사용할 때 Linear Space에서 계산을 해야 한다. HDR이 아닌 이미지는 픽셀 포맷 뒤에 SRGB를 붙여주면 내부적으로 Gamma Correction을 해서 Linear Space에서 계산할 수 있게 해준다. 그리고 MSAA를 사용하는 FLOAT Texture을 만들고, 이것을 RTV로 삼아 렌더링을 한다.

렌더링이 끝나면 다른 FLOAT Texture에 Resolve해서 Texture2DMS->Texute2D로 변환한다. 이 Texture로 Post Process를  진행하고 마지막에 ToneMapping(expose,gamma)을 한다. ToneMapping 마지막에 Gamma Correction을 한다. Gamma Correction은 color에 1/gamma를 제곱한 것이다. (gamma는 2.2가 표준) 마지막에 UNORM을 사용하는 스왑체인으로 Present해주면 CRT에 의해 감마값이 내려가기 때문에 Linear Space에 대해 렌더링할 수 있다. 

 

백버퍼의 픽셀 포맷을 UNORM_SRGB나 FLOAT을 사용하면 gamma correction을 할 필요 없다.

하지만 아직까지 UNORM을 쓰는 경우가 많기 때문에 gamma correction을 사용하는 방향으로 하겠다.

 

 

 

'그래픽스' 카테고리의 다른 글

[그래픽스] Depth Map  (0) 2023.07.23
[그래픽스] 렌더링 엔진 구조 변경  (0) 2023.07.23
[그래픽스] HDRI(High Dynamic Range Image)  (0) 2023.07.22
[그래픽스] Height Map  (0) 2023.07.21
[그래픽스] Normal Mapping  (0) 2023.07.21