8 분 소요

Image Based Lighting

Image

이미지 기반 라이팅은 ‘실제 환경에서 촬연한 이미지’를 이용하여
3D 씬의 조명을 구현하는 방식이다

환경 전체의 ‘빛 정보’를 반구 or 구 형태로 샘플링 하여
오브젝트의 반사,굴절, 확산 등을 사실적으로 표현

  • 환경맵을 사용
    (보통 파노라마 같은 360도 사진을 이용하여
    구형 or 큐브맵으로 사용)
    (동시에 이것이 전방위 ‘광원’의 역할을 한다)

  • Diffuse IBL
    환경맵을 적분하여 부드럽게 퍼지는 간접 조명 계산용
  • Specular IBL
    환경 맵을 러프니스를 이용하여 필터링
    (언뜻 봤을때 이미지에 가까움)

큐브맵? 환경맵?

  • 큐브맵
    환경이나 방향 데이터를 저장하기 위한 6장의 정사각형 텍스쳐이다
  • 환경맵
    특정한 ‘장면’을 둘러싼 ‘주변 환경’의 조명/색상 정보를 담은 텍스쳐
    (특정 점에서 주변 세계의 모습을 묘사)
    (반사,굴절 IBL 등에서 사용)
    구형 맵/ 큐브맵/ 파노라마 등의 형식을 포함
    (환경을 표현하는 전체적인 개념을 뜻함)
용어 범위/역할 예시 또는 형식
환경맵 “주변 환경”을 표현하는 개념 큐브맵, equirectangular 텍스처, 파노라마
큐브맵 환경맵을 구현하는 특정 포맷 6장의 ±X,±Y,±Z 텍스처

샘플링 개념 복습

‘연속된 공간, 이미지, 환경’을 픽셀 or 텍스쳐 좌표의 이산 값으로 읽어오는 과정
(이산 값 : 딱 떨어지는, 셀수 있는 정수값)

PBR(Physically Based Rendering)과의 관계?

구분 IBL의 역할 PBR과의 연계
재질 정의 금속/비금속(Metalness), 러프니스(Roughness), 알베도(Albedo) PBR 쉐이더의 입력 값으로 사용됨
조명 HDR 환경맵 기반의 전역 조명 제공 PBR BRDF 모델(GGX 등)로 광원-재질 상호작용 계산
정확도 다중 샘플링과 적분으로 현실적인 반사 구현 Fresnel, 에너지 보존, 마이크로패싯 모델 등과 함께 사용
최적화 Pre-filtered Environment Map과 LUT로 성능 확보 실시간 렌더링(UE5, Unity HDRP)에서 표준


앞서 배운 phong 의 조명 타입은 3가지
directional, spot, point 를 각각 배웠다

그런데 현실의 조명들은 아주 복잡한 편…
위의 조명들을 잘 조합하더라도 현실과 같은 조명 효과를 내기는
매우 어려운 일이다

  • 컴퓨터 그래픽스는 ‘텍스쳐’를 점점 더 ‘효율적’으로 사용하는 방향으로 진화
    • 먼 배경을 만드는 ‘큐브맵’ 같은 기술
    • 그러한 배경을 물체에 적용하는 ‘환경맵’과 같은 기술
      • 그렇다면 조명도 이런식으로 사용하면 ‘현실’과 비슷하게 사용할 수 있지 않을까?

원칙적인 계산법

Image

특정한 한 점에 대한 빛의 계산은
그림처럼 환경맵의 여러 부분에서 ‘조명 정보’를 얻어와
계산해야 한다

  • 특정한 점에서 여러 방향으로 벡터를 쏘아
    만나는 텍스쳐에 대한 샘플링 정보를 가지고 오고
    그 평균을 구하면 된다

  • 다만 이러한 방식은 ‘게임’ 같은 실시간 렌더링이 필요한 경우에는
    아주 느린 연산이 되어 못하게 된다

    • 그렇기에 1~2번의 샘플링 정도만 진행해야 함
    • 텍스쳐링이 효율적인 이유를 다시 생각해보자
    • 자세하게 만들어진 사진을 텍스쳐로 만들고 GPU에 넣어두고 텍스쳐링을 적용하면
    • 픽셀 쉐이더에서 이미지를 샘플링하여, 색을 아주 빠르게 결정 가능

따라서 우리는 ‘텍스쳐링’을 잘 활용하여
게임과 같은 실시간 렌더링에서도 좋은 그래픽을 만들 수 있어야 함!

그렇다면 IBL을 어떻게 구현하지?

제한 조건은 ‘한 번만’ 텍스쳐링 하는 것!
따라서 ‘미리’ 선처리된 ‘광원 텍스쳐’를 준비하고
해당 텍스쳐를 샘플링 함으로서 IBL을 구현

위에서 설명한

  • Diffuse IBL
    난반사 모델링
  • Specular IBL
    금속/거울의 하이라이트

이 나뉘어져 있는 이유이다

실제로 diffuse 자체는 난반사가 되었기에
반사가 잘 되지 않는, 벽 등에 대한 질감을 표현할 수 있고
Specular는 반사가 잘 되었기에 거울/금속 등의 표현에 적절

  • 이러한 IBL 데이터들은 전부 바르는 텍스쳐(albedo)가 아닌
    ‘빛’을 표현하는 ‘환경광’ 데이터라 생각하자

  • DX에서 SRV 형태로 GPU에 올라가고
    픽셀 쉐이더 쪽에서 Sample() 을 통해 데이터를 샘플링

  • 보통 Roughness,metalic,intensity 같은 파라미터를 통하여
    계산

L IBL​ = kd ​ ⋅LdiffuseIBL​(N) + ks ​⋅ LspecularIBL​(R,α)

kd : diffuse/albedo 성분 (일반적으로 1 - metalic)
ks : specular 부분 (보통 metalic + fresnel term)
LdiffuseIBL : DiffuseIBL로서 보통 Irradiance map(블러된 환경맵) 샘플링 값
              (Normal 을 집어넣어 계산)
LspecularIBL​ : SpecularIBL로서 보통 (Prefiltered Environment Map + BRDF LUT)을 이용한 샘플링 값 
              (R : 반사벡터 , a : 러프니스와 같은 파라미터)

람버트 조명(Lambertian Shading)

표면이 완벽한 난반사(diffuse)를 한다는 가정한 조명 모델

L diffuse ​= kd ​ ⋅ I ⋅ max(0,N⋅L)

L : 광원 방향
N : 법선 벡터(표면)
I : 광원의 세기
Kd : 확산 반사 계수 (diffuse reflectance)

예제 - 이미지 기반 라이팅

Cubemapping.h

#pragma once

#include <wrl.h>

#include "GeometryGenerator.h"
#include "Material.h"
#include "Vertex.h"

namespace hlab {

using Microsoft::WRL::ComPtr;

struct CubeMapping {

    std::shared_ptr<Mesh> cubeMesh;

    ComPtr<ID3D11ShaderResourceView> diffuseResView;  // Add
    ComPtr<ID3D11ShaderResourceView> specularResView; // Add

    ComPtr<ID3D11VertexShader> vertexShader;
    ComPtr<ID3D11PixelShader> pixelShader;
    ComPtr<ID3D11InputLayout> inputLayout;
};
} // namespace hlab

큐브매핑에서 SRV를 2개 추가
각각 ‘큐브맵 텍스쳐’로 받을 예정이며

위에서 말한
DiffuseIBL, SpecularIBL로 사용될 것들이다
(.dds 파일들)

  • 기본적으로 원본 이미지들을 수정한 것들
    (디자이너의 영역에 가깝기는 함)
    (Blur 같은 이미지 처리 기술을 활용할 순 있음)

ExampleApp.cpp

Render()
{
  ...
  // 큐브매핑
  m_context->IASetInputLayout(m_cubeMapping.inputLayout.Get());
  m_context->IASetVertexBuffers(
      0, 1, m_cubeMapping.cubeMesh->vertexBuffer.GetAddressOf(), &stride,
      &offset);
  m_context->IASetIndexBuffer(m_cubeMapping.cubeMesh->indexBuffer.Get(),
                              DXGI_FORMAT_R32_UINT, 0);
  m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

  m_context->VSSetShader(m_cubeMapping.vertexShader.Get(), 0, 0);
  m_context->VSSetConstantBuffers(
      0, 1, m_cubeMapping.cubeMesh->vertexConstantBuffer.GetAddressOf());
  ID3D11ShaderResourceView *views[2] = {m_cubeMapping.diffuseResView.Get(),
                                        m_cubeMapping.specularResView.Get()};
  m_context->PSSetShaderResources(0, 2, views);
  m_context->PSSetShader(m_cubeMapping.pixelShader.Get(), 0, 0);
  m_context->PSSetSamplers(0, 1, m_samplerState.GetAddressOf());

  m_context->DrawIndexed(m_cubeMapping.cubeMesh->m_indexCount, 0, 0);

  // 버텍스/인덱스 버퍼 설정
  for (const auto &mesh : m_meshes) {
      m_context->VSSetConstantBuffers(
          0, 1, mesh->vertexConstantBuffer.GetAddressOf());

      // 물체 렌더링할 때 큐브맵도 같이 사용
      ID3D11ShaderResourceView *resViews[3] = {
          mesh->textureResourceView.Get(), m_cubeMapping.diffuseResView.Get(),
          m_cubeMapping.specularResView.Get()};
      m_context->PSSetShaderResources(0, 3, resViews);

      m_context->PSSetConstantBuffers(
          0, 1, mesh->pixelConstantBuffer.GetAddressOf());

      m_context->IASetInputLayout(m_basicInputLayout.Get());
      m_context->IASetVertexBuffers(0, 1, mesh->vertexBuffer.GetAddressOf(),
                                    &stride, &offset);
      m_context->IASetIndexBuffer(mesh->indexBuffer.Get(),
                                  DXGI_FORMAT_R32_UINT, 0);
      m_context->IASetPrimitiveTopology(
          D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
      m_context->DrawIndexed(mesh->m_indexCount, 0, 0);
  }
}

  • views를 통하여
    큐브맵 쪽에 SRV를 픽셀 쉐이더로 보내준다

  • 또한 해당 환경맵 데이터를 ‘광원’으로 활용할 것이기에
    일반 물체들의 srv에도 추가해준다

cubeMappingPixelShader.hlsl

#include "Common.hlsli" // 쉐이더에서도 include 사용 가능

TextureCube g_diffuseCube : register(t0);
TextureCube g_specularCube : register(t1);
SamplerState g_sampler : register(s0);

float4 main(PixelShaderInput input) : SV_TARGET
{
    // 주의: 텍스춰 좌표가 float3 입니다.
    // 주의: error X4532: texlod not supported on this target -> 쉐이더 모델(버전) 5_X로 올리기
    return g_specularCube.Sample(g_sampler, input.posWorld.xyz);
}

큐브맵 쪽에서는 딱히 무엇을 하지는 않는다
다만, 기본적으로 이미지와 비슷한 역할의 Specular를 이용하여
큐브맵을 그려준다

  • diffuse로 바꾸면 Blur가 심하게 처리된듯한 뿌연 느낌만이 남음

예제 구현 1


BasicPixelShader.hlsl

#include "Common.hlsli" // 쉐이더에서도 include 사용 가능

Texture2D g_texture0 : register(t0);
TextureCube g_diffuseCube : register(t1);
TextureCube g_specularCube : register(t2);
SamplerState g_sampler : register(s0);

cbuffer BasicPixelConstantBuffer : register(b0)
{
    float3 eyeWorld;
    bool useTexture;
    Material material;
    Light light[MAX_LIGHTS];
    float3 rimColor;
    float rimPower;
    float rimStrength;
    bool useSmoothstep;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 toEye = normalize(eyeWorld - input.posWorld);

    float3 color = float3(0.0, 0.0, 0.0);
    
    int i = 0;
    
    // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-for
    // https://forum.unity.com/threads/what-are-unroll-and-loop-when-to-use-them.1283096/
    
    [unroll] // warning X3557: loop only executes for 1 iteration(s), forcing loop to unroll
    for (i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        color += ComputeDirectionalLight(light[i], material, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; ++i)
    {
        color += ComputePointLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
    {
        color += ComputeSpotLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }

    // 쉽게 이해할 수 있는 간단한 구현입니다.
    // IBL과 다른 쉐이딩 기법(예: 퐁 쉐이딩)을 같이 사용할 수도 있습니다.
    // 참고: https://www.shadertoy.com/view/lscBW4
    
    color = g_diffuseCube.Sample(g_sampler, input.normalWorld);
    color += g_specularCube.Sample(g_sampler, reflect(-toEye, input.normalWorld));

    return useTexture ? float4(color, 1.0) * g_texture0.Sample(g_sampler, input.texcoord) : float4(color, 1.0);
}
  • color에 기존의 라이팅 부분을 초기화 시키고
    diffuse와 specular 부분을 적용
    (specular 부분은 이전의 환경맵 방식을 적용)

결과

Image

diffuse 와 specular IBL 텍스쳐들이 적용된 모습
다만 GUI 쪽의
Diffuse와 Specular, Shiniess 부분은 적용되지 않았음

예제 2 - 각 요소들이 적용되게 하기

일단 내가 구현한 방식

BasicPixelShader.hlsl

#include "Common.hlsli" // 쉐이더에서도 include 사용 가능

Texture2D g_texture0 : register(t0);
TextureCube g_diffuseCube : register(t1);
TextureCube g_specularCube : register(t2);
SamplerState g_sampler : register(s0);

cbuffer BasicPixelConstantBuffer : register(b0)
{
    float3 eyeWorld;
    bool useTexture;
    Material material;
    Light light[MAX_LIGHTS];
    float3 rimColor;
    float rimPower;
    float rimStrength;
    bool useSmoothstep;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 toEye = normalize(eyeWorld - input.posWorld);

    float3 color = float3(0.0, 0.0, 0.0);
    
    int i = 0;
    
    // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-for
    // https://forum.unity.com/threads/what-are-unroll-and-loop-when-to-use-them.1283096/
    
    [unroll] // warning X3557: loop only executes for 1 iteration(s), forcing loop to unroll
    for (i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        color += ComputeDirectionalLight(light[i], material, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; ++i)
    {
        color += ComputePointLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
    {
        color += ComputeSpotLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }

    // 쉽게 이해할 수 있는 간단한 구현입니다.
    // IBL과 다른 쉐이딩 기법(예: 퐁 쉐이딩)을 같이 사용할 수도 있습니다.
    // 참고: https://www.shadertoy.com/view/lscBW4
    
    color = g_diffuseCube.Sample(g_sampler, input.normalWorld).xyz * material.diffuse;
    color += g_specularCube.Sample(g_sampler, reflect(-toEye, input.normalWorld)).xyz * material.specular;
    color *= material.shininess;

    return useTexture ? float4(color, 1.0) * g_texture0.Sample(g_sampler, input.texcoord) : float4(color, 1.0);
}

  • 각 요소의 diffuse와 specular를 곱해주었고
    최종적으로 shiniess를 마지막에 곱해주었다

  • 다만 지금 생각해보니 shininess는 specular 쪽에서 처리하는게 맞지 않나 싶기도 하다

결과

Image

  • diffuse를 높일 경우는 전체적으로 밝아지는 느낌이 강하다
    뭔가 조명을 세게 받는 느낌이 됨

  • Specular를 높일수록 주변 환경 색을 잘 받기에
    매끈매끈한 느낌이 강해졌다

  • Shininess가 낮으면 다소 검어지고
    높아질수록 전체색이 흰색에 가까워진다

텍스쳐를 입힌 버젼

Image

  • specular를 높였더니 뭔가 실제 존재하는 잘닦인 지구본 같은 느낌이 되었다!

실제 PBR 기법에서는 ‘거칠기’(Roughness)를 사용하여
텍스쳐를 다르게 샘플링한다 함
(거칠기가 높을수록 빛을 잘 반사하지 않으며 - 나무, 벽 느낌
낮으면 빛을 잘 반사한다 - 거울,금속 등)

예제 코드 방식


float4 diffuse = g_diffuseCube.Sample(g_sampler, input.normalWorld);
diffuse.xyz *= material.diffuse;

float4 specular = g_specularCube.Sample(g_sampler, reflect(-toEye, input.normalWorld));

specular *= pow((specular.x + specular.y + specular.z) / 3.0, material.shininess);
specular.xyz *= material.specular;

return diffuse + specular; // 텍스쳐 x

  • shininess 수치가 올라갈수록 ‘빛나는 부분’을 제외하면 역으로 어두워진다!
  • specular의 빛 수치들의 평균을 낸 후, shininess 만큼 pow (제곱) 해준다

Image

완성된 지구본

#include "Common.hlsli" // 쉐이더에서도 include 사용 가능

Texture2D g_texture0 : register(t0);
TextureCube g_diffuseCube : register(t1);
TextureCube g_specularCube : register(t2);
SamplerState g_sampler : register(s0);

cbuffer BasicPixelConstantBuffer : register(b0)
{
    float3 eyeWorld;
    bool useTexture;
    Material material;
    Light light[MAX_LIGHTS];
    float3 rimColor;
    float rimPower;
    float rimStrength;
    bool useSmoothstep;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 toEye = normalize(eyeWorld - input.posWorld);

    float3 color = float3(0.0, 0.0, 0.0);
    
    int i = 0;
    
    // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-for
    // https://forum.unity.com/threads/what-are-unroll-and-loop-when-to-use-them.1283096/
    
    [unroll] // warning X3557: loop only executes for 1 iteration(s), forcing loop to unroll
    for (i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        color += ComputeDirectionalLight(light[i], material, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; ++i)
    {
        color += ComputePointLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
    {
        color += ComputeSpotLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }

    // 쉽게 이해할 수 있는 간단한 구현입니다.
    // IBL과 다른 쉐이딩 기법(예: 퐁 쉐이딩)을 같이 사용할 수도 있습니다.
    // 참고: https://www.shadertoy.com/view/lscBW4
    
    float4 diffuse = g_diffuseCube.Sample(g_sampler, input.normalWorld);
    diffuse.xyz *= material.diffuse;
    
    float4 specular = g_specularCube.Sample(g_sampler, reflect(-toEye, input.normalWorld));
    
    specular *= pow((specular.x + specular.y + specular.z) / 3.0, material.shininess);
    specular.xyz *= material.specular;
    
    color = diffuse.xyz + specular.xyz;

    return useTexture ? float4(color, 1.0) * g_texture0.Sample(g_sampler, input.texcoord) : float4(color, 1.0);
}

그냥 마지막에 texture 부분을 적용하도록 하였다

Image

  • shininess를 잘 활용하니까 매우 잘닦인 지구본 같이 완성되었다
  • ‘빛이 나는 부분’이 더 강조되는 역할을 해준다

tmi - 젤다로 모델 변경 시

  1. diffuse만 적용시키고 specular는 0에 가까운 경우

Image

‘광원’을 ‘환경맵’인 Diffuse IBL에서 갖고 오기에
은은한 광원을 제공하기에 상당히 자연스러운 광원이 구현되어 보인다

  1. specular 값을 높인 경우

Image

specular가 주변 환경의 색을 읽어오기에
뭔가 ‘실물 동상’ 같은 느낌의 젤다가 완성되었다

댓글남기기