ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Diffuse IrradianceMap과 Spherical harmonics를 통한 최적화
    Graphics/Graphics Study 자료 2020. 12. 12. 07:29

     

    Diffuse IrradianceMap과 Spherical harmonics를 통한 최적화

    최초 작성 : 2020-12-12

    마지막 수정 : 2020-12-12

    최재호

     

    목차

    1. 목표

    2. 내용

      2.1. IrradianceMap의 용도

      2.2. IrradianceMap의 생성

        2.2.1. Brute force 방식

        2.2.2. IrradianceMap의 사용

      2.3. Spherical Harmonics를 통한 IrradiancMap 최적화

        2.3.1. Spherical Harmonics를 사용 가능한 이유

        2.3.2. Spherical Harmonics로 IrradianceMap 생성 및 복원

          2.3.2.1. Basis Function (Ylm)

          2.3.2.2. Weight Function (Llm)와 Weight Function의 생성

          2.3.2.3. 복원

        2.3.3. Spherical Harmonics로 변경 결과

    3. 구현 결과

    4. 실제 구현 코드

    5. 레퍼런스

     

     

    1. 목표

    IrradianceMap이 무엇인지 이해하고 실제로 생성 및 사용해봅니다.

    Spherical Harmonics를 이해 및 사용하여 IrradianceMap을 근사해보며, 근사 결과가 얼마나 차이가 나는지 확인해봅니다.

    Spherical Harmonics에는 좋은 다양한 성질이 있지만 여기에서는 IrradianceMap을 근사하는데 필요한 기능만 알아볼 것입니다. 그 이상의 내용은 다른 글을 통해서 소개할 예정입니다.

    이 글은 내용 이해에 목적을 두고 있기 때문에 최적화된 코드가 아닌 원본식을 최대한 그대로 활용합니다.

     

    2. 내용

    2.1. IrradianceMap의 용도

    IrradianceMap은 주어진 표면의 Normal 방향을 중심으로 반구 영역으로부터 표면의 단위 면적에 입사하는 모든 빛(Irradiance)을 담고 있는 텍스쳐입니다.
    식으로 표현하면 아래와 같습니다.

     

     

    식1 (출처 : 레퍼런스1)

     

     

    IrradianceMap은 표면에서 모든 방향으로 균일한 양의 빛(Radiance)을 반사시키는 Diffuse BRDF를 사용하여 관찰자 방향으로 향하는 빛(Radiance) 계산에 유용합니다. 

    표면 Normal 중심으로 반구 영역에서 입사한 모든 빛(Irradiance) 중 관찰자의 눈으로 들어오는 Radiance는 아래의 식으로 표현됩니다.

     

    식2. 단위 면적에 입사한 Irradiance들 중 관찰자에게로 향하는 Radiance양을 얻는 식, p : 표면위치, wi : 들어오는 빛의 방향, wo : 나가는 빛의 방향, cosθi : dot(normal, wi) (출처 : 레퍼런스5)

     

     

    이 식에서 모든 방향으로 균일하게 반사하는 Diffuse BRDF를 사용한다고 한다면, BRDF 함수는 상수로 취급할 수 있습니다. 그렇게 되면 위 식에서 피적분 함수 바깥으로 BRDF 함수를 뺄 수 있습니다. 그 결과 위의 식2는 아래의 식3 과 같이 표현될 수 있습니다.

     

    식3. 상수 취급할 수 있는 BRDF를 적분 밖으로 빼냄. 이제 적분식은 반구상에서 단위면적으로 입사하는 Irradiance의 합이 됨.

     

     

    이제 상수를 제외한 적분의 의미는 Normal을 중심으로 반구 영역에서 입사한 Irradiance의 합으로 볼 수 있습니다. 그래서 모든 Normal 방향에 대한 Irradiance를 IrradianceMap에 미리 계산해둔다면, 상수인 Diffuse BRDF를 곱하여 즉시 관찰자 방향으로 향하는 빛(Radiance)을 구할 수 있습니다.

     

    식4. 반구영역에서 단위면적으로 들어오는 Irradiance
    식5. 단위면적에서 바깥으로 나가는 Irradiance는 Radiant Exitance라 부르고 Irrdiance와 같은 양임. 들어오느냐 나가느냐에 따라서 달라짐. p : 알베도

     

    Lambertian BRDF를 통해 관찰자로 들어오는 Radiance를 계산하면 아래와 같습니다.

     

    식6. Lambertian BRDF는 1/pi * Radiant Exitance 가 관찰자에게 들어오는 Radiance가 된다.

     

     

    2.2. IrradianceMap의 생성

    2.2.1. Brute force 방식

    SphereMap_TwoMirrorBall(레퍼런스3 참고)으로 구성된 환경맵을 PostProcess 방식으로 렌더링 하여 IrradianceMap을 생성합니다.

    픽셀 쉐이더에서 아래 코드를 사용하여 UV를 3D Direction 정보로 변경합니다.

    // 코드1. UV에서 Normal 추출
    vec3 normal = GetNormalFromTexCoord_TwoMirrorBall(TexCoord_);

    이 Normal 값을 중심으로 하는 반구 영역에서 입사하는 Irradiance를 모두 합산합니다.

    아래 코드를 통해서 Normal 방향을 중심으로 하는 반구 영역에서 입사하는 Irradiance를 모두 합산할 수 있습니다. 이 코드는 레퍼런스2를 참고하였습니다.

    // 코드2. Normal 방향으로 부터 반구로 부터 들어오는 Irradiance들의 합을 구함.
    vec3 GenerateIrradiance(vec3 InNormal)
    {
        vec3 up = normalize(vec3(0.0, 1.0, 0.0));
        if (abs(dot(InNormal, up)) > 0.99)
            up = vec3(0.0, 0.0, 1.0);
        vec3 right = normalize(cross(up, InNormal));
        up = normalize(cross(InNormal, right));
    
        float nrSamples = 0.0;
    
        vec3 irradiance = vec3(0.0, 0.0, 0.0);
        for (float phi = sampleDelta; phi <= 2.0 * PI; phi += sampleDelta)
        {
            for (float theta = sampleDelta; theta <= 0.5 * PI; theta += sampleDelta)
            {
                // spherical to cartesian (in tangent space)
                vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // tangent space to world
                vec3 sampleVec =
                    tangentSample.x * right +
                    tangentSample.y * up +
                    tangentSample.z * InNormal;
    
                sampleVec = normalize(sampleVec);
                vec3 curRGB = FetchEnvMap(GetSphericalMap_TwoMirrorBall(sampleVec));
    
                irradiance += curRGB * cos(theta) * sin(theta);
                nrSamples++;
            }
        }
        irradiance = PI * (irradiance * (1.0 / float(nrSamples)));
        return irradiance;
    }

    이 픽셀 쉐이더에서는 현재 픽셀이 처리 중인 표면 Normal 기준으로 반구 상에서 입사하는 모든 Irradiance를 적분을 하므로 연산량이 상당히 많습니다. (그래픽 카드에 성능에 따라 프로그램이 거의 멈출 정도로 상당히 느림)

     

    Burte force 방식으로 생성한 IrradianceMap은 아래와 그림1과 같습니다. Irradiance 연산의 결과가 HDR이라서 0~1 사이 값이 아니라 상당히 커질 수 있습니다. 실제로 톤매핑 없이 바로 결과를 보면 대부분의 색상이 하얗게 표시됩니다. 레퍼런스1의 IrradianceMap 결과와 유사하게 보이기 위해서 적당한 톤매핑 함수를 사용하였습니다. 결과는 아래와 같습니다.

     

    그림1. BruteForce 형태로 만든 IrradianceMap (출처 : 직접 구현)

     

     

    2.2.2. IrradianceMap의 사용

    IrradianceMap의 사용은 특정 3D Direction 방향으로 환경맵을 Fetch하는 방식과 같이 사용하면 됩니다.

    // 코드3. IrradianceMap의 사용
    vec2 uv = GetSphericalMap_TwoMirrorBall(normal);
    color = vec4(FetchEnvMap(uv), 1.0);

     

    2.3. Spherical Harmonics를 통한 IrradiancMap 최적화

    2.3.1. Spherical Harmonics를 사용 가능한 이유

    Diffuse Irradiance는 표면의 노멀의 변화에 비해서 아주 느리게 변화하기 때문에 저해상도 IrradianceMap을 사용해도 충분히 표현 가능합니다. 그래서 Spherical Harmonics의 9개의 항을 조합하여 간단히 IrradianceMap을 나타낼 수 있으며, 왜 9개의 항인가는 2.3.2.3에서 다시 알아보겠습니다.

     

    우리는 Spherical Harmonics로 IrradianceMap에서 Weight Function 9개(Vector3 9개)를 얻어내며, 이 9개의 Weight Function가 IrradianceMap을 대체합니다.

     

    Weight Function과 Basis Function의 Weighted Sum(혹은 Linear Combination이라고 할 수도 있음)을 통해서 IrradianceMap에 있던 원본 Irradiance를 복원합니다. Basis/Weight Function은 2.3.2에서 계속해서 알아볼 것입니다.

     

    이렇게 Basis / Weight Function의 Weighted Sum으로 원본 값을 복원하는 아이디어는 푸리에 급수의 아이디어와 유사합니다. 하지만 이 글에서 반드시 푸리에 함수를 이해할 필요는 없으며, IrradianceMap(원본함수)와 Basis Function 사용해서 Weight Function을 얻어내고, 얻어내진 Weight Function으로 다시 원본 IrradianceMap을 복원할 수 있다는 점만 이해하면 됩니다.

     

    그림2. 6개의 sine파를 합성하여 만든 원본 함수 s(x) (출처 : https://en.wikipedia.org/wiki/Fourier_series)

     

     

    2.3.2. Spherical Harmonics로 IrradianceMap 생성 및 복원

    2.3.2.1. Basis Function (Ylm)

    • Spherical Harmonics의 항에 해당됩니다. 레퍼런스4 에서 Spherical Harmonics 각 항을 볼 수 있습니다. Ylm 의 형태로 표시되는데, l은 행 m은 열을 나타냅니다. l의 경우 0~n까지 증가하며, m의 경우 -l~l 사이의 값이 됩니다. 이 중 여기서는 l 이 [0~2] 인 경우 (총 9개)만 볼 것입니다.

     

     

    그림3. Spherical Harmonics 각 항을 직접 렌더링한 결과 (출처 : 직접 구현)

     

     

    2.3.2.2. Weight Function (Llm)와 Weight Function의 생성

    • 원본 함수와 Basis 함수를 Convolution 하여 Weight Function을 얻습니다.
      • 원본 함수는 구 전체로 입사하는 Radiance의 합입니다.
    • Basis와 Weight 함수를 Convolution 하여 다시 IrradianceMap을 복원하는 과정에도 쓰입니다.
    • Weight Function의 생성은 아래의 식7을 참고해주세요.
    • 실제 구현도 아래의 코드를 참고하시면 됩니다. PixelShader / ComputeShader / CPU에서 생성하는 코드를 만들었으며 필요한 상황에 맞게 사용하시면 됩니다. 코드4, 코드5, 코드6 를 참고해주세요.

     

     

    식7. 구 전체에 대해서 입사하는 Radiance에 대한 Weight 함수를 Basis함수를 사용하여 구함

     

     

    // 코드4. Llm을 PixelShader에서 생성
    void GenerateLlm(out vec3 Llm[9])
    {
      Llm[0] = vec3(0.0);
      Llm[1] = vec3(0.0);
      Llm[2] = vec3(0.0);
      Llm[3] = vec3(0.0);
      Llm[4] = vec3(0.0);
      Llm[5] = vec3(0.0);
      Llm[6] = vec3(0.0);
      Llm[7] = vec3(0.0);
      Llm[8] = vec3(0.0);
      
      // 개선이 필요함.
      // 현재는 픽셀마다 Llm 9개를 모두 구해서 계산하는 방식임
      float nrSamples = 0.0;
      for (float phi = sampleDelta; phi <= 2.0 * PI; phi += sampleDelta)
      {
        for (float theta = sampleDelta; theta <= 0.5 * PI; theta += sampleDelta)
        {
          // spherical to cartesian (in tangent space)
          vec3 sampleVec = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // tangent space to world
          sampleVec = normalize(sampleVec);
          vec3 curRGB = FetchEnvMap(GetSphericalMap_TwoMirrorBall(sampleVec));
          
          float Ylm[9];
          GenerateYlm(Ylm, sampleVec);
          
          // Radiance를 기록하는 것이므로 Alm 은 곱해주지 않음
          Llm[0] += Ylm[0] * curRGB * sin(theta);
          Llm[1] += Ylm[1] * curRGB * sin(theta);
          Llm[2] += Ylm[2] * curRGB * sin(theta);
          Llm[3] += Ylm[3] * curRGB * sin(theta);
          Llm[4] += Ylm[4] * curRGB * sin(theta);
          Llm[5] += Ylm[5] * curRGB * sin(theta);
          Llm[6] += Ylm[6] * curRGB * sin(theta);
          Llm[7] += Ylm[7] * curRGB * sin(theta);
          Llm[8] += Ylm[8] * curRGB * sin(theta);
          
          ++nrSamples;
        }
      }
      
      for (int i = 0; i < 9; ++i)
        Llm[i] = PI * (Llm[i] * (1.0 / float(nrSamples)));
    }
    // 코드5. Llm을 ComputeShader에서 생성
    #version 430 core
    
    precision highp float;
    
    #define SAMPLE_INTERVAL_X 16
    #define SAMPLE_INTERVAL_Y 16
    
    layout (local_size_x = SAMPLE_INTERVAL_X, local_size_y = SAMPLE_INTERVAL_Y) in;
    
    layout(binding = 0, rgba16f) uniform readonly image2D tex_object;
    
    const float PI = 3.1415926535897932384626433832795028841971693993751058209749;
    const float SHBasisR = 1.0;
    const float sampleDelta = 0.015f;
    
    uniform int TexWidth;
    uniform int TexHeight;
    
    // SSBO
    layout(std430, binding = 0) buffer LlmBuffer
    {
      vec4 Llm[9];        // w component is padding
    };
    
    shared vec3 GlobalLlm[SAMPLE_INTERVAL_X * SAMPLE_INTERVAL_Y][9];
    shared int GlobalNumOfSamples;
    
    ...
    ...
    
    void GenerateYlm(out float Ylm[9], vec3 InDir)
    {
      Ylm[0] = 0.5 * sqrt(1.0 / PI);
      Ylm[1] = 0.5 * sqrt(3.0 / PI) * InDir.y / SHBasisR;
      Ylm[2] = 0.5 * sqrt(3.0 / PI) * InDir.z / SHBasisR;
      Ylm[3] = 0.5 * sqrt(3.0 / PI) * InDir.x / SHBasisR;
      Ylm[4] = 0.5 * sqrt(15.0 / PI) * InDir.x * InDir.y / (SHBasisR * SHBasisR);
      Ylm[5] = 0.5 * sqrt(15.0 / PI) * InDir.y * InDir.z / (SHBasisR * SHBasisR);
      Ylm[6] = 0.25 * sqrt(5.0 / PI) * (-InDir.x * InDir.x - InDir.y * InDir.y + 2.0 * InDir.z * InDir.z) / (SHBasisR * SHBasisR);
      Ylm[7] = 0.5 * sqrt(15.0 / PI) * InDir.z * InDir.x / (SHBasisR * SHBasisR);
      Ylm[8] = 0.25 * sqrt(15.0 / PI) * (InDir.x * InDir.x - InDir.y * InDir.y) / (SHBasisR * SHBasisR);
    }
    
    void main(void)
    {
      if (gl_LocalInvocationIndex == 0)    // Execute only first LocalInvocation
        GlobalNumOfSamples = 0;
      barrier();    // Sync for all LocalInvocations
      vec3 LocalLlm[9];
      LocalLlm[0] = vec3(0.0);
      LocalLlm[1] = vec3(0.0);
      LocalLlm[2] = vec3(0.0);
      LocalLlm[3] = vec3(0.0);
      LocalLlm[4] = vec3(0.0);
      LocalLlm[5] = vec3(0.0);
      LocalLlm[6] = vec3(0.0);
      LocalLlm[7] = vec3(0.0);
      LocalLlm[8] = vec3(0.0);
      ivec2 LocalGroupSize = ivec2(SAMPLE_INTERVAL_X, SAMPLE_INTERVAL_Y);
      ivec2 StartInterval = ivec2(gl_LocalInvocationID.xy) + ivec2(1, 1);
      int nrSamples = 0;
      for (float phi = sampleDelta * StartInterval.y; phi <= 2.0 * PI; phi += sampleDelta * LocalGroupSize.y)
      {
        for (float theta = sampleDelta * StartInterval.x; theta <= 0.5 * PI; theta += sampleDelta * LocalGroupSize.x)
        {
          // spherical to cartesian (in tangent space)
          vec3 sampleVec = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // tangent space to world
          sampleVec = normalize(sampleVec);
          float Ylm[9];
          GenerateYlm(Ylm, sampleVec);
          vec2 TargetUV = GetSphericalMap_TwoMirrorBall(sampleVec);
          int DataX = int(TargetUV.x * TexWidth);
          int DataY = int((1.0 - TargetUV.y) * TexHeight);
          vec3 curRGB = imageLoad(tex_object, ivec2(DataX, DataY)).xyz;
          // Radiance를 기록하는 것이므로 Alm 은 곱해주지 않음
          LocalLlm[0] += Ylm[0] * curRGB * sin(theta);
          LocalLlm[1] += Ylm[1] * curRGB * sin(theta);
          LocalLlm[2] += Ylm[2] * curRGB * sin(theta);
          LocalLlm[3] += Ylm[3] * curRGB * sin(theta);
          LocalLlm[4] += Ylm[4] * curRGB * sin(theta);
          LocalLlm[5] += Ylm[5] * curRGB * sin(theta);
          LocalLlm[6] += Ylm[6] * curRGB * sin(theta);
          LocalLlm[7] += Ylm[7] * curRGB * sin(theta);
          LocalLlm[8] += Ylm[8] * curRGB * sin(theta);
          nrSamples++;
        }
      }
      uint GlobalIndex = gl_LocalInvocationID.y * SAMPLE_INTERVAL_X + gl_LocalInvocationID.x;
      GlobalLlm[GlobalIndex][0] = LocalLlm[0];
      GlobalLlm[GlobalIndex][1] = LocalLlm[1];
      GlobalLlm[GlobalIndex][2] = LocalLlm[2];
      GlobalLlm[GlobalIndex][3] = LocalLlm[3];
      GlobalLlm[GlobalIndex][4] = LocalLlm[4];
      GlobalLlm[GlobalIndex][5] = LocalLlm[5];
      GlobalLlm[GlobalIndex][6] = LocalLlm[6];
      GlobalLlm[GlobalIndex][7] = LocalLlm[7];
      GlobalLlm[GlobalIndex][8] = LocalLlm[8];
      atomicAdd(GlobalNumOfSamples, nrSamples);    // Accumulate all SampleCounts
      barrier();    // Sync for all LocalInvocations
      if (gl_LocalInvocationIndex == 0)  // Execute only first LocalInvocation
      {
        // All put in togather to finalize computing Llm
        LocalLlm[0] = vec3(0.0);
        LocalLlm[1] = vec3(0.0);
        LocalLlm[2] = vec3(0.0);
        LocalLlm[3] = vec3(0.0);
        LocalLlm[4] = vec3(0.0);
        LocalLlm[5] = vec3(0.0);
        LocalLlm[6] = vec3(0.0);
        LocalLlm[7] = vec3(0.0);
        LocalLlm[8] = vec3(0.0);
        for (int k = 0; k < SAMPLE_INTERVAL_X * SAMPLE_INTERVAL_Y; ++k)
        {
          for (int m = 0; m < 9; ++m)
            LocalLlm[m] += GlobalLlm[k][m];
        }
        for (int i = 0; i < 9; ++i)
          Llm[i].xyz = PI * LocalLlm[i] * (1.0f / float(GlobalNumOfSamples));
      }
    }
    // 코드6. Llm을 CPU에서 생성
    auto GenerateLlm = [&dataTwoMirrorBall, &GetSphericalMap_TwoMirrorBall](Vector (&OutLlm)[9]){
      memset(&OutLlm[0], 0, sizeof(OutLlm));
    
      Vector* dataPtr = (Vector*)&dataTwoMirrorBall.ImageData[0];
      float sampleDelta = 0.015f;
      int32 nrSamples = 0;
      float SHBasisR = 1.0f;
      for (float phi = sampleDelta; phi <= 2.0 * PI; phi += sampleDelta)
      {
        for (float theta = sampleDelta; theta <= 0.5 * PI; theta += sampleDelta)
        {
          // spherical to cartesian (in tangent space)
          Vector sampleVec = Vector(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // tangent space to world
          sampleVec = sampleVec.GetNormalize();
    
          float LocalYlm[9];
          LocalYlm[0] = 0.5f * sqrt(1.0f / PI);       // 0
          LocalYlm[1] = 0.5f * sqrt(3.0f / PI) * sampleVec.y / SHBasisR;      // 1
          LocalYlm[2] = 0.5f * sqrt(3.0f / PI) * sampleVec.z / SHBasisR;      // 2
          LocalYlm[3] = 0.5f * sqrt(3.0f / PI) * sampleVec.x / SHBasisR;      // 3
          LocalYlm[4] = 0.5f * sqrt(15.0f / PI) * sampleVec.x * sampleVec.y / (SHBasisR * SHBasisR);      // 4
          LocalYlm[5] = 0.5f * sqrt(15.0f / PI) * sampleVec.y * sampleVec.z / (SHBasisR * SHBasisR);      // 5
          LocalYlm[6] = 0.25f * sqrt(5.0f / PI) * (-sampleVec.x * sampleVec.x - sampleVec.y * sampleVec.y + 2.0f * sampleVec.z * sampleVec.z) / (SHBasisR * SHBasisR);     // 6
          LocalYlm[7] = 0.5f * sqrt(15.0f / PI) * sampleVec.z * sampleVec.x / (SHBasisR * SHBasisR);  // 7
          LocalYlm[8] = 0.25f * sqrt(15.0f / PI) * (sampleVec.x * sampleVec.x - sampleVec.y * sampleVec.y) / (SHBasisR * SHBasisR); // 8
    
          Vector2 TargetUV = GetSphericalMap_TwoMirrorBall(sampleVec);
          int32 DataX = (int32)(TargetUV.x * dataTwoMirrorBall.Width);
          int32 DataY = (int32)((1.0 - TargetUV.y) * dataTwoMirrorBall.Height);
          Vector curRGB = *(dataPtr + (DataY * dataTwoMirrorBall.Width + DataX));
          //Vector InnerIntegrate = curRGB * cos(t) * sin(t);
    
          // Radiance를 기록하는 것이므로 Alm 은 곱해주지 않음
          OutLlm[0] += LocalYlm[0] * curRGB * sin(theta);
          OutLlm[1] += LocalYlm[1] * curRGB * sin(theta);
          OutLlm[2] += LocalYlm[2] * curRGB * sin(theta);
          OutLlm[3] += LocalYlm[3] * curRGB * sin(theta);
          OutLlm[4] += LocalYlm[4] * curRGB * sin(theta);
          OutLlm[5] += LocalYlm[5] * curRGB * sin(theta);
          OutLlm[6] += LocalYlm[6] * curRGB * sin(theta);
          OutLlm[7] += LocalYlm[7] * curRGB * sin(theta);
          OutLlm[8] += LocalYlm[8] * curRGB * sin(theta);
          nrSamples++;
        }
      }
      for (int i = 0; i < 9; ++i)
        OutLlm[i] = PI * OutLlm[i] * (1.0f / float(nrSamples));
    };
    Vector ResultLlm[9];
    GenerateLlm(ResultLlm);
    

    2.3.2.3. 복원

    • 위의 Basis와 Weight Function의 Weighted Sum을 통해서 원본 함수를 구합니다.

     

    식8. 구 전체에 대한 Radiance의 Weight Function을 Irradiance에 대한 Weight Function으로 변환

     

    • 식8의 내용 중 Al은 아래의 내용입니다. 이것은 Radiance를 우리가 관심 있는 표면에 투영시켜 Irradiance로 만들기 위해 사용하는 cosθ 항입니다. 

     

    식9. Al의 정의

     

    • Al은 SH의 중 m이 0인 항목들만 사용합니다. 아래 그림4의 빨간 동그라미 부분을 봐주세요.

     

    그림4. Al에서 사용하는 SH 는 3개

     

    • 왜냐하면 cosθ는 구면 좌표계에서 θ에만 영향을 받기 때문입니다. 아래 그림5을 봐주세요.

     

    그림5. 좌측의 방위각에만 cosθ가 영향을 준다.

     

    • 식8을 간략화하기 위해서 아래 식9처럼 A'l을 정의합니다.

     

    식9. 식8의 간소화 하기 위한 기호 정의

     

    • A'l은 l이 3 이상부터 값이 급격히 작아지므로 l이 2까지인 경우 까지만 사용합니다. 이것이 l이 3 이상인 경우가 필요 없는 이유입니다. 그래서 총 9개의 항으로 IrradianceMap의 근사가 가능한 것입니다.
      • A'l은 아래와 같은 특성이 있습니다.
        • l > 1 이고 l이 홀수인 경우 : 값이 굉장히 빠르게 감소함
        • 짝수인 경우 l^(-5/2) 으로 빠르게 감쇄
      • 아래 식10에서 l의 증가에 따른 A'l 값 감소를 보실 수 있습니다.
      • 레퍼런스6을 통하여 Al들을 미리 계산해두었기 때문에 이 값을 그대로 사용하도록 합니다. 자세한 유도는 레퍼런스6을 참고해주세요.

     

     

    식10. A'l이 l>=3 부터 급격히 감쇠하는 것을 보여줌. 그래서 A'0, A'1, A'2 까지만 사용함. l을 0~2까지만 사용하기 때문에 총 9개의 SH 만 사용하게 된 것임.

     

    • 이제 특정 방향(Normal)을 중심으로 반구 영역에서 단위면적으로 입사하는 모든 Irradiance의 합을 아래 식10을 사용하여 구할 수 있습니다.
      • Radiance에 대한 weight인 Llm을 구할 때 구 전체에 대해 입사하는 Llm을 구하였습니다. 하지만 식8에 나오는 Al의 max(cos(θ), 0) 항목이 곱해지면서 Normal 방향의 반대 방향은 모두 0으로 상쇄되므로, 반구상에 대한 Irradiance 정보만 남을 것입니다.

     

    식11. 특정 방향을 중심으로 반구영역에서 부터 단위면적으로 입사하는 모든 Irradiance의 합을 얻는 식

     

    // 코드7. 주어진 방향에 대한 Irradiance를 복원하는 코드
    void GenerateYlm(out float Ylm[9], vec3 InDir)
    {
        Ylm[0] = 0.5 * sqrt(1.0 / PI);
        Ylm[1] = 0.5 * sqrt(3.0 / PI) * InDir.y / SHBasisR;
        Ylm[2] = 0.5 * sqrt(3.0 / PI) * InDir.z / SHBasisR;
        Ylm[3] = 0.5 * sqrt(3.0 / PI) * InDir.x / SHBasisR;
        Ylm[4] = 0.5 * sqrt(15.0 / PI) * InDir.x * InDir.y / (SHBasisR * SHBasisR);
        Ylm[5] = 0.5 * sqrt(15.0 / PI) * InDir.y * InDir.z / (SHBasisR * SHBasisR);
        Ylm[6] = 0.25 * sqrt(5.0 / PI) * (-InDir.x * InDir.x - InDir.y * InDir.y + 2.0 * InDir.z * InDir.z) / (SHBasisR * SHBasisR);
        Ylm[7] = 0.5 * sqrt(15.0 / PI) * InDir.z * InDir.x / (SHBasisR * SHBasisR);
        Ylm[8] = 0.25 * sqrt(15.0 / PI) * (InDir.x * InDir.x - InDir.y * InDir.y) / (SHBasisR * SHBasisR);
    }
    
    vec3 SHReconstruction(float InAlYlm[9], vec3 InLlm[9])
    {
      /* Optimized code in paper
      // All constant put in c[0~5].
      float c[5];
      c[0] = 0.429043;
      c[1] = 0.511664;
      c[2] = 0.743125;
      c[3] = 0.886227;
      c[4] = 0.247708;
    
      return c[0] * InLlm[8] * (x * x - y * y) + c[2] * InLlm[6] * z * z + c[3] * InLlm[0] - c[4] * InLlm[6]
          + 2.0 * c[0] * (InLlm[4] * x * y + InLlm[7] * x * z + InLlm[5] * y * z)
          + 2.0 * c[1] * (InLlm[3] * x + InLlm[1] * y + InLlm[2] * z);
      */
    
      vec3 result = vec3(0.0);
      for (int i = 0; i < 9; ++i)
          result += InAlYlm[i] * InLlm[i];
      return result;
    }
    
    void main()
    {
    ...
    // 특정 Normal 방향을 기준으로 들어온 Irradiance의 값을 얻는 과정
    
    float Ylm[9];
    GenerateYlm(Ylm, normal);
    
    float AlYlm[9] = float[9](
      Al[0] * Ylm[0],
      Al[1] * Ylm[1], Al[1] * Ylm[2], Al[1] * Ylm[3],
      Al[2] * Ylm[4], Al[2] * Ylm[5], Al[2] * Ylm[6], Al[2] * Ylm[7], Al[2] * Ylm[8]);
    
    color.xyz = SHReconstruction(AlYlm, Llm);
    ...
    }

     

     

    그림6. Brute force 방식와 SH 근사 방식으로 만든 IrradianceMap (출처 : 직접 구현)

     

     

    2.3.3. Spherical Harmonics로 변경 결과

    • IrradianceMap이 텍스쳐에서 9개의 Vector3로 압축
      • Texture Fetch 없이 연산만으로 Diffuse BRDF 계산 가능
    • IrradianceMap의 생성시간 단축
      • S : 원본 환경맵의 총 텍셀 수
      • T : 생성할 IrradianceMap의 텍셀 수
      • Brute force 방식의 경우 : O(T*S)
        • 실제로 IrradianceMap을 그리는 과정이 필요합니다. 그래서 IrradianceMap 텍셀 하나하나 마다 원본 환경맵의 수만큼 반구에 대한 적분이 필요합니다.
      • Spherical Harmonics 방식 : O(9*S)
        • SH 9개의 항에 대해서만 Irradiance를 얻으면 됩니다. 그래서 총 9개의 항에 대한 환경맵 반구 적분이 필요합니다.

     

    3. 구현 결과

     

     

     

    그림7. 이 그림은 Reflection 방향이 아닌 노멀방향에 그대로 Fetch. IrradianceMap과 비교를 위해서 같이 방식으로 만듬

     

     

    4. 실제 구현 코드

    http://github.com/scahp/Shadows/tree/IrradianceMap

     

    5. 레퍼런스

    1. An Efficient Representation for Irradiance Environment Maps - Ravi Ramamoorthi, Pat Hanrahan

    2. Diffuse irradiance - LearnOpenGL

    3.SphereMap TwoMirrorBall - 360도 방향 커버 됨

    4. Real spherical harmonics Table

    5. www.pbr-book.org/

    6. R. Ramamoorthi and P. Hanrahan. On the relationship between radiance and irradiance: Determining the illumination from images of a convex lambertian object. To appear, Journal of the Optical Society of America A, 2001.

     

     

    댓글

Designed by Tistory & scahp.