Programming/컴퓨터그래픽스 (DX 11)

[DirectX11] IBL (Image-Based Lighting)

양양줘 2025. 12. 11. 15:55

 

 

지난 글에서 PBR의 직접광을 구현하였다.

이번 글은 PBR에서 간접광 구현에 대한 기본적인 개념과

IBL 구현에 대한 내용이다.

IBL 간접광, 금속성 -> 환경 반사

 

 

 

조명(Illumination)의 종류


  • Local Illumination : 직접광
  • Global Illumination : 직접광 + 간접광

 

 

GI (Global Illumination)


 

Global Illumination(GI)표면에서 반구 방향으로 들어오는 모든 광량(직접광 + 간접광)을 BRDF를 통해 적분하여 계산하는 이상적인 조명 모델이다. 이 모델은 빛의 다중 반사(Bounce)까지 모두 고려하므로 현실적인 조명 표현이 가능하지만, 전체 적분을 실시간으로 계산하기에는 연산 비용이 매우 크다.

그래서 실제 그래픽스 파이프라인에서는 다음과 같이 직접광과 간접광을 분리하여 처리한다.

 

1. 직접광 (Direct Lighting)

실시간 정보를 반영하여 실시간으로 계산한다.

(DiffuseBRDF + SpecularBRDF) * Radiance * N·L

 

2. 정적 간접광 (Static Indirect Lighting)

실시간 정보를 반영하여 실시간으로 계산한다.

(DiffuseBRDF + SpecularBRDF)를 모두 포함한 GI 적분식

실제 GI 적분식(다중 Bounce 포함)을 완전히 계산하는 대신, 이를 오프라인에서 미리 계산해 저장한 데이터를 사용한다. Lightmap, Light Probe, IBL 등 다양한 형태로 사전에 구축하여, 렌더링 시에는 매우 빠르게 간접광을 조회할 수 있다.

 

3. 동적 간접광 (Dynamic Indirect Lighting)

장면이 변화할 때 간접광도 변해야 하는 상황에서, 레이트레이싱, SSGI, Voxel GI 등의 기법을 사용하여

일부 또는 전체 간접광을 실시간으로 근사한다.

 

 

요약하면,

GI는 원래 ‘Direct + Indirect’를 의미하지만, 실제 렌더링에서는 연산량 때문에 직접광은 실시간, 간접광은 정적 또는 동적으로 분리하여 처리하는 구조를 사용한다. 때문에 실무에서 말하는 GI는 정적 간접광의 사용을 의미한다.

 

 


정적 간접광(Static Indirect Lighting)


정적 간접광(static indirect lighting)은 광원에서 표면에 바로 닿는 직접광이 아닌, 환경에서 여러번 반사되어 들어오는 간접광을 사전에 계산해 저장해두고, 런타임에서는 이를 조회하는 방식이다. 실시간 비용을 크게 줄이면서도 자연스러운 간접광 표현이 가능하다.

간접광은 크게 3가지로 구분된다.

[ 정적 간접광 ]
1. 라이트 맵 (Lightmap)
2. 라이트 프로브 (Light Probe)
3. IBL 기반 환경광 (Image-Based Lighting)

 

  1. 라이트 맵 (Lightmap)
    정적 지오메트리에 대해 간접광과 그림자를 오프라인에서 미리 계산해 텍스처로 굽는(Baking) 방식이다.
    • 다중 Bounce까지 포함한 고품질 간접광을 저장
    • 실시간 비용이 거의 없음
    • 건물 실내 조명, 벽면 반사, 색 번짐(color bleeding)의 표현에 매우 유용
    • 단, 정적인 오브젝트에만 적용이 가능하여 동적 오브젝트는 사용이 불가
  2. 라이트 프로브 (Light Probe, Spherical Harmonics)
    정적 간접광을 공간의 여러 위치에 샘플점(Probe) 형태로 저장한 뒤, 이를 SH(Spherical Harmonics)로 압축하여 동적 오브젝트에 적용하는 방식이다.
    • 라이트맵을 사용할 수 없는 캐릭터나 움직이는 물체가 환경 간접광을 자연스럽게 받을 수 있음
    • 부드러운 ambient 간접광을 제공
    • 정확도는 라이트맵보다 낮지만 매우 가볍고 실용적임
    • 실내/ 실외 모두에서 필수적으로 사용됨
  3. IBL 기반 환경광 (Image-Based Lighting)
    IBL은 환경의 HDL 큐브맵을 이용해 간접광을 계산하는 방식으로, 환경 자체가 하나의 거대한 광원처럼 작동한다. 즉, 광원을 직접 두지 않고 환경 자체가 조명 역할을 하도록 한다.
    • Diffuse IBL : 주변의 환경광(soft lighting)
    • Specular IBL : 주변의 반사광(Reflection)
    • BRDF LUT : 재질 특성 조정
종류 대상 데이터 형태 특징
Lightmap 정적 Mesh Texture 정적 오브젝트용, 정확함
Light Probe (SH) 동적 Mesh SH 계수 동적 오브젝트에 간접광 제공
Diffuse IBL 모든 오브젝트 Irradiance Map / SH 확산 간접광
Specular IBL 반사/금속 Prefiltered EnvMap + BRDF LUT 하이라이트/반사 간접광

 

 

 

 

 


IBL (Image-Based Lighting)


IBL(Image-Based Lighting)주변 환경을 HDR(큐브맵)에 저장해 두고, 이 이미지로부터 간접광을 계산하는 조명 기법이다. 환경 자체가 하나의 거대한 조명 역할을 한다고 이해하면 된다.

 

 

IBL은 크게 두 요소로 구성된다.

1. Diffuse IBL (확산 간접광)
    거친 표면이나 비금속 표면에서 환경 전체가 퍼지듯 들어오는 부드러운 간접광을 계산한다.
    
2. Specular IBL (반사 간접광)   
    매끄러운 표면이나 금속 표면에서 주변 환경을 거울처럼, 또는 흐릿하게 반사한다.

 

 

1. IBL Diffuse Term

IBL Diffuse Term에서는 환경 전체에서 들어오는 빛이 표면에 난반사로 퍼지는 간접광을 계산한다. 난반사는 방향성이 약하고 표면의 roughness에 크게 영향받지 않기 때문에, 반구 적분된 환경광(Irradiance Map)을 미리 만들어두고 표면 법선 방향으로 샘플링하는 것만으로 빠르게 간접광을 계산할 수 있다.

  • 원래는 반구 위 모든 방향에 대해 Radiance × N·L을 적분해야 하는데
  • IBL에서는 이 적분을 “미리 계산한 Irradiance Map”이 대신한다.

결과적으로 실시간에서는 ‘kd × albedo/π × irradiance‘만으로 환경에서 반사된 부드러운 간접 diffuse 조명을 계산한다.

 

Irradiance Map

  • 입력 : HDR 큐브맵 이미지
  • 출력 : 매우 블러 처리된 저해상도 큐브맵 (dds)
  • 목적 : 환경광의 부드러운 확산 반사 계산 (Diffuse IBL)
  • 샘플링 키 값 : N(normal)
  • 내용 : Lambert(NdotL)의 적분
    Irradiance(방사 조도)는 표면이 모든 방향에서 받는 Radiance의 총합(적분)으로, 표면이 받는 입사광량을 의미한다. Diffuse IBL 텀에 사용될 Irradiance를 미리 계산하여 텍스처로 저장해두면 실시간 비용을 크게 줄일 수 있다.

    표면의 p 지점을 기준으로 반구 전체에서 다양한 방향 Wi 로 빛이 들어온다고 가정한다. 이때 Radiance는 특정 방향에서 들어오는 빛의 색과 세기를 의미하며, 표면은 해당 방향에 대해 Lambert 법칙에 따라 N·L만큼만 밝게 기여한다. 즉, 어떠한 방향에서 Radiance가 아무리 강해도 표면 법선과의 각도가 크면 기여도가 줄어든다.

    결국, 반구의 모든 방향에 대해 Radiance(Wi) × (N dot Wi)를 누적한 값이 그 표면이 받는 전체 Diffuse 간접광이다. Irradiance Map은 이 적분 결과를 큐브맵 형태로 저장한 것으로, 큐브맵의 각 픽셀 방향은 표면의 법선 방향에 해당한다. Diffuse 조명은 기본적으로 방향성이 퍼져 있으므로, 이 Irradiance Map은 낮은 해상도로도 충분히 부드럽고 자연스러운 간접광을 제공한다.

 

실시간 Diffuse IBL 계산
// 1. Irradiance Map 샘플링 - 미리 계산된 난반사 간접광
// 텍스처는 표면 법선(N)으로 샘플링한다.
float3 irradiance = txIBL_Diffuse.Sample(samplerLinear, N).rgb;

// 2. 프레넬 반사율 F 계산
// 프레넬(시선-빛 방향)에 따른 반사율을 최소 반사율(비금속/금속)을 고려하여 구한다.
// 빛 방향은 특정 할 수 없으므로 cosLo = dot(Normal,View)을 사용한다.
float3 F0 = lerp(Fdielectric, albedo, metalness);
float3 F = fresnelSchlick(F0, cosLo);   

// 3. 난반사(표면산란) 비율 계산
// 금속일수록 표면 산란을 제거하며 비금속일수록 표면 산란이 그대로 표현된다. 
float3 kd = lerp(1.0 - F, 0.0, metalness);

// 4. Diffuse IBL
// txIBL_Diffuse 맵에는 Lambertian BRDF를 가정하여 포함되어 있다.
float3 diffuseIBL = kd * BaseColor * irradiance / PI;

 

 

 

2. IBL Specular Term

Specular IBL Term에서는 환경광이 표면에서 반사(Specular) 형태로 반사되는 간접광을 계산한다. Specular IBL은 사실 아래의 적분(환경 전체를 대상으로 한 반사 계산)을 해야하는데 이 적분은 실시간으로 절대 계산할 수 없다.

IBL에서는 이 적분을 실시간에 수행하는 대신, 적분을 쪼개서 두 텍스처로 저장해두고 실시간 연산에 샘플링하여 사용한다.

환경 방향에 대한 적분 → Prefiltered EnvMap
BRDF 자체에 대한 적분 → BRDF LUT
  • 원래는 모든 방향 Wi 에 대해 D × G × F / (4 N·V N·L) × (N·L) dL 을 적분해야 한다.
  • IBL에서는 이 적분을 Prefiltered EnvMap + BRDF LUT가 대신 수행한다.

결과적으로, 실시간에서는 ‘SpecularIBL = PrefilteredColor * (F0 * specularBRDF.x + specularBRDF.y)’ 의 형태로 매우 빠르게 스페큘러 간접광을 계산한다.

 

Prefiltered Environment Map (Specular EnvMap)

  • 입력 : HDR 큐브맵 이미지
  • 출력 : Roughness 단계(Mip)별로 블러된 CubeMap (DDS)
  • 샘플링 키 값 : R(반사 벡터)로 샘플링, roughness * specularTextureLevels로 LOD level 결정
  • 내용 : 환경 Radiance + D(미세면 분포) + roughness 관련 적분
    Prefiltered EnvMap은 Specular IBL에서 환경 쪽에서 미리 계산해둘 수 있는 부분을 만들어둔 텍스처이다. 즉, 환경 Radiance가 각 roughness에서 어떻게 퍼지고 흐려지는지(D함수 포함)를 모두 계산한 결과를 저장한다.

    roughness에 따라 반사는 달라진다.
    • roughness 낮음 → D값 큼 → 날카로운 highlight → 선명한 reflection
    • roughness 높음 → D값 작음→ 퍼진 highlight → 흐릿한 reflection
    따라서, Prefiltered EnvMap은 roughness별로 블러링된 MipMap CubeMap으로 구성된다.


BRDF Lookup Table (LUT)
  • 입력 : N·V, roughness
  • 출력 : 2D LUT (R=scale, G=bias)
  • 샘플링 키 값 : float2(NdotL, roughness)).rg
  • 내용 : F(프레넬) + G(기하 감쇠) 적분
    BRDF LUT은 Specular IBL에서 표면 BRDF 쪽에서 미리 계산해둘 수 있는 부분을 저장한 텍스처이다.
    Specular BRDF의 항 중에서 F와 G는 환경과 독립적이며, NdotV와 roughness만으로 결정되므로 이를 미리 적분해 2D Lookup Table에 저장한다. 실시간에서는 LUT에서 두 값(scale, bias)을 샘플링하고 프레넬 Schlick F0와 조합하여 최종 Specluar IBL을 계산한다.

 

실시간 Specular IBL 계산
// 1. 텍스처 Mipmap 레벨 구하기
uint specularTextureLevels, width, height;
txIBL_Specular.GetDimensions(0, width, height, specularTextureLevels);

// 2. Prefiltered EnvMap 샘플링
// View-Reflection 벡터(Lr) 기반으로 샘플링
// roughness * specularTextureLevels 로 LOD를 결정하여 거칠기에 따른 블러 효과 적용
float3 PrefilteredColor = txIBL_Specular.SampleLevel(samplerLinear, Lr, roughness * specularTextureLevels).rgb;

// 3. Cook-Torrance BRDF 근사용 LUT 샘플링
// NdotL, roughness를 기반으로 F*G 평균값과 Geometry term(G) 샘플링
float2 specularBRDF = txIBL_SpecularBRDF_LUT.Sample(samplerClamp, float2(NdotL, roughness)).rg;

// 4. Specular IBL 계산
// 쿡토런스 Spceular BRDF 근사식
float3 F0 = lerp(Fdielectric, albedo, metalness);
float3 specularIBL = PrefilteredColor * (F0 * specularBRDF.x + specularBRDF.y);

// 최종 간접광 합산
// 난반사(diffuseIBL)와 반사광(specularIBL)을 합치고 AmbientOcclusion 곱하여 최종 IBL 계산
indirectIBL = (diffuseIBL + specularIBL) * AmbientOcclusion;

 

 

 

 

 

DirectX 11 IBL Pixel Shader 구현


/*
    [ PBR Pixel Shader ]
    - Direct BRDF(Cook-Torrance)
    - Indirect IBL(BRDF)
    - Texture Map Support
        - Diffuse Map (Albedo Map)
        - Normal Map
        - Emissive Map
        - Metallic Map
        - Roughness Map
        - IBL_IrradianceMap
        - IBL_SpecularEnvMap
        - IBL_BRDF_LUT
    - Shadow Mapping Support
*/

#include <PBR_Common.fxh>

// --- Texture Bind Slot ------------------
Texture2D diffuseMap : register(t0);
Texture2D normalMap : register(t1);
Texture2D emissiveMap : register(t3);
Texture2D shadowMap : register(t6);
Texture2D metallicMap : register(t7);
Texture2D roughnessMap : register(t8);
TextureCube IBL_IrradianceMap : register(t9);
TextureCube IBL_SpecularEnvMap : register(t10);
Texture2D IBL_BRDF_LUT : register(t11);


// --- Sampler Bind Slot ------------------
SamplerState samLinear : register(s0);
SamplerComparisonState samShadow : register(s1);
SamplerState samLinearClamp : register(s2);



float4 main(PS_INPUT input) : SV_TARGET
{
    // --- [Default] ----------------------------------
    // color
    float3 base_color = float3(1.0f, 1.0f, 1.0f);
    float3 emissive_color = float3(0.0f, 0.0f, 0.0f);
    float metallic = 0.0f;
    float roughness = 0.0f;
    float alpha = 1.0f;
    
    // shadowFactor
    float shadowFactor = 1.0f;
    
    
    // --- [ShadowMapping] ---------------------------
    float currentShadowDepth = input.posShadow.z / input.posShadow.w;
    float2 uv = input.posShadow.xy / input.posShadow.w;
    uv.y = -uv.y;
    uv = uv * 0.5 + 0.5;
    
    if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)
    {
        float2 offsets[9] =
        {
            float2(-1, -1), float2(0, -1), float2(1, -1),
            float2(-1, 0), float2(0, 0), float2(1, 0),
            float2(-1, 1), float2(0, 1), float2(1, 1)
        };
        float2 shadowmapsize = { 3000, 3000 };
        float2 texelSize = 1.0 / shadowmapsize; // 텍셀 크기 (ShadowMap 해상도 기준)
        shadowFactor = 0.0f;
       
       //  PCF - 9 texel 평균으로 그림자 팩터 계산
       [unroll]
        for (int i = 0; i < 9; i++)
        {
            float2 sampleUV = uv + offsets[i] * texelSize;
            shadowFactor += shadowMap.SampleCmpLevelZero(samShadow, sampleUV, currentShadowDepth - 0.001);
        }
        shadowFactor = shadowFactor / 9.0f;
    }

    
    // --- [Material]  ----------------------------------
    // base color
    if (useDiffuse)
        base_color = diffuseMap.Sample(samLinear, input.texCoord).rgb;
    
    // normal
    float3 N;
    if (useNormal)
    {
        float3 local_normal = normalMap.Sample(samLinear, input.texCoord).xyz * 2.0f - 1.0f;
        float3 world_normal = normalize(mul(local_normal, input.TBN));
        N = normalize(world_normal);
    }
    else
    {
        N = normalize(mul(input.normal, (float3x3) input.finalWorld));
    }
    
    // emission
    if (useEmissive)
        emissive_color = emissiveMap.Sample(samLinear, input.texCoord).rgb;
    
    // metallic
    if (useMetallic)
        metallic = metallicMap.Sample(samLinear, input.texCoord).r;

    // roughness
    if (useRoughness)
        roughness = roughnessMap.Sample(samLinear, input.texCoord).r;
    
    // alpha
    if (useDiffuse)
        alpha = diffuseMap.Sample(samLinear, input.texCoord).a;
    
    if (alpha < 0.5)
        discard;
    
    // --- [Override] ----------------------------------
    if (useMetallicOverride)
        metallic = metallicOverride;
    if (useRoughnessOverride)
        roughness = roughnessOverride;
    if (useBaseColorOverride)
        base_color = baseColorOverride;
    roughness = max(roughness, 0.04);
    
    // --- [Facotr] -----------------------------------
    metallic *= metallicFactor;
    float rf = max(roughnessFactor, 0.04);
    roughness *= rf;
    


    // --- [Vector]  ----------------------------------
    float3 L = normalize(-lightDirection.xyz);
    float3 V = normalize(cameraPos - input.worldPos);
    float3 H = normalize(L + V);
    
    float NdotL = max(dot(N, L), 0.0);
    float NdotV = max(dot(N, V), 0.0);
    

    
    // --- [Direct Light]  ----------------------------------
    // Specular BRDF (Cook-Torrance)
    float3 F0 = lerp(float3(0.04f, 0.04f, 0.04f), base_color, metallic);
    float D = D_NDFGGXTR(N, H, roughness); // 미세면 정렬정도
    float3 F = F_Schlick(H, V, F0); // 프레넬 반사율
    float G = G_Smith(N, V, L, roughness); // shadowing & masking
    
    float denom = 4.0f * max(NdotL, 0.001) * max(NdotV, 0.001);
    float3 SpecularBRDF = (D * F * G) / denom;

    
    // Diffuse BRDF (Lambertian)
    float3 kd = lerp(1.0 - F, 0.0, metallic); // 표면산란 계수
    float3 DiffuseBRDF = (base_color / PI) * kd;
    
    // Final DirectLight
    float3 DirectColor = (SpecularBRDF + DiffuseBRDF) * lightColor * directIntensity * NdotL;

    
    
    
    
    // --- [Indirect Light]  ----------------------------------
    float3 IndirectColor = { 0, 0, 0 };
    if (useIBL)
    {
        // Diffuse Term --------------------------
        // Irradiance - diffuse BRDF 적분값
        float3 Irradiance = IBL_IrradianceMap.Sample(samLinear, N).rgb;
        float3 DiffuseIBL = base_color * Irradiance * kd;
        
        // Specular Term -----------------------
        uint specularTextureLevels, width, height;
        IBL_SpecularEnvMap.GetDimensions(0, width, height, specularTextureLevels);
        float maxLevel = max(1.0, (float) (specularTextureLevels - 1));
        float mip = saturate(roughness) * maxLevel;
        
        // Prefiltered - 환경 Radiance + D(미세면 분포) + roughness 관련 적분값
        float3 R = normalize(reflect(-V, N));
        float3 PrefilteredColor = IBL_SpecularEnvMap.SampleLevel(samLinear, R, mip).rgb;

        // LUT - F + G 적분값
        float2 BRDF_LUT = IBL_BRDF_LUT.Sample(samLinearClamp, float2(NdotV, roughness)).rg;
        
        // Specular IBL
        float3 SpecularIBL = PrefilteredColor * (F0 * BRDF_LUT.x + BRDF_LUT.y);

        // Final InDirectLight
        IndirectColor = (DiffuseIBL + SpecularIBL) * indirectIntensity;
    }
    
    
    // --- [Final Color]  ----------------------------------
    float3 finalColor = (DirectColor * shadowFactor) + IndirectColor + emissive_color;
    
     // LDR 단독패스일 때만 감마보정
    if (useGamma && !isHDR)
        finalColor = LinearToSRGB(finalColor);

    return float4(finalColor, alpha);
}

 

 

 

 

 

 

 

 


 

GitHub - derkreature/IBLBaker: Light probe generation and BRDF authoring for physically based shading.

Light probe generation and BRDF authoring for physically based shading. - derkreature/IBLBaker

github.com