ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE4] A Scalable and Production Ready Sky and Atmosphere Rendering Technique 리뷰 - 코드분석 (2/2)
    Graphics/Graphics Study 자료 2023. 3. 9. 23:58

    [UE4] A Scalable and Production Ready Sky and Atmosphere Rendering Technique 리뷰 - 코드분석 (2/2)

    최초 작성 : 2023-03-09
    마지막 수정 : 2023-03-09
    최재호

     

    목차

    1. 목표
    2. 내용
     2.1. 4 종의 LUT 생성 코드 분석
      2.1.1. Transmittance LUT
      2.1.2. Multiple scattering LUT
      2.1.3. Sky-View LUT
      2.1.4. Aerial perspective LUT
     2.2. LUT 텍스쳐를 사용한 최종 렌더링 이미지
    3. 레퍼런스

     

    1. 목표

    이번 글은 이전글(레퍼런스1)을 기반으로 논문저자가 제공하는 구현코드를 분석해 볼 것입니다. 혹시 이전글을 보시지 않았다면 먼저 보고 오시는 것을 추천드립니다. 저자의 원본 코드는 https://github.com/sebh/UnrealEngineSkyAtmosphere(레퍼런스2) 에 있습니다.

    이전글에서 우리는 4개의 LUT 를 생성하는 부분을 확인했습니다. 실제 구현에서는 4개의 구현을 생성한 다음 마지막 포스트프로세스 단계에서 LUT 를 사용하여 최종 장면을 렌더링 합니다.

     

    코드가 길어서 창을 두 개 띄워두고 코드와 설명을 같이 보시는 것을 추천합니다.

    보시다가 틀린 점이 발견되면 지적해 주시면 감사하겠습니다.


    2. 내용

    렌더독 캡쳐를 보고 분석할 부분을 파악해 봅시다. 그림1을 봐주세요.

    • 초록색 네모 안에 있는 커맨드는 Shadow map과 행성을 렌더링 합니다. 렌더타겟은 그림2 를 참고해 주세요.
    • 빨간색 네모 안에 있는 커맨드는 이전글에서 본 4개의 LUT를 생성합니다. 렌더타겟은 그림3 을 참고해 주세요.
    • 파란색 네모 안에 있는 커맨드는 생성한 LUT 로 대기를 렌더링 하고, 톤맵을 합니다. 렌더타겟은 그림4 를 참고해 주세요.

    그림1. 렌더독으로 캡쳐한 렌더커맨드. 초록색 부분은 ShadowMap 과 행성을 렌더링하는 부분. 빨간색 부분이 LUT 생성, 파란색 부분이 생성한 LUT 를 사용하여 대기를 렌더링 하고 톤맵 처리 하는 부분. (출처 : 레퍼런스2 코드로 렌더독 촬영)
    그림2. 행성 렌더링하는 Terrain 의 결과. (출처 : 레퍼런스2 코드로 렌더독 촬영)
    그림3. LUT 만든 결과. (출처 : 레퍼런스2 코드로 렌더독 촬영)
    그림4. 대기렌더링과 톤맵 후 결과. (출처 : 레퍼런스2 코드로 렌더독 촬영)

    이 글에서 중점적으로 파악해 볼 부분은 4 개의 LUT 생성과 그것을 사용하여 대기를 렌더링 하는 부분입니다. 그럼 해당 코드를 차근차근 알아봅시다.

     

    2.1. 4 종의 LUT 생성 코드 분석

    2.1.1. Transmittance LUT

    Transmittance LUT 의 경우 Fullscreen Quad 를 렌더링 하여 생성합니다.

    • LUT 입력값 : uv.x : 대기 내에서의 높이 (0은 Bottom, 1은 Top), uv.y : CosZenithAngle
    • LUT 출력값 : Extinction(Absorb + out-scattering) 뒤 남아있는 luminance 비율

     

    여기서 호출되는 가장 중요한 함수는 UvToLutTransmittancePrams 와 IntegrateScatteredLuminance 입니다. 전체 코드 흐름을 확인해 봅시다.

    1. RenderTransmittanceLutPS 함수를 통해 함수가 시작됩니다.
    2. 현재 처리 중인 텍셀(텍스쳐 해상도 기준)을 [0, 1] 범위인 UV 로 변환합니다.
    3. UV 를 Lut Trancemittance Parameter 로 변환합니다. 여기서는 고도(viewHeight)와 ZenithCosAngle 을 얻어냅니다. 어떻게 이 값을 얻어내는지는 그림6 에서 자세히 알아봅시다.
    4. 이전 과정에서 구한 고도와 ZenithCosAngle 을 사용하여 월드에서의 위치, 그리고 월드에서의 방향을 생성합니다. 코드를 보면 월드 위치를 계산할때 고도만 고려하고 있는 것을 볼 수 있습니다. 이것은 행성이 둥글기 때문에 어느 위치던 고도가 동일하면 결과가 같기 때문입니다. 그리고 ZenithCosAngle 을 사용하여 월드에서의 방향을 구합니다. 이것 또한 업 벡터가 (0, 0, 1) 을 기준으로 가정하고 생성한 벡터입니다.
    5. Transmittance 를 구하기 위해서 레이마칭을 수행합니다. 총 40번의 샘플링을 통해 레이마칭을 수행합니다. 나머지 변수들은 모두 기능을 사용하지 않는 옵션을 설정되어있습니다. IntegrateScatteredLuminance 의 결과중 OpticalDepth 만 사용하게 됩니다. OpticalDepth 로 Transmittance 를 구하는 식은 레퍼런스4의 Extinction 항목을 참고해 주세요. IntegrateScatteredLuminance 함수는 중요한 함수이기 때문에 그림11에서 더 자세히 알아봅시다.
    6. 최종 결과를 Transmittance LUT 에 저장합니다.

    그림5. Transmittance LUT 생성 함수.

    다음은 UV 에서 Transmittance Parameter 로 변경하는 코드입니다.

    1. uv.y 값을 사용하여 ViewHeight 를 결정하는 코드입니다. uv.y 가 0이면 ViewHeight 는 BottomRadius, uv.y 가 1이면 ViewHeight 는 TopRadius 입니다. 그림7 의 좌측이미지는 H 에 대해서 설명하고, 우측 이미지는 H 와 uv.y 를 통해 rho 를 구하는 부분을 보여줍니다. uv.y 가 0~1 로 변하면 ViewHeight 는 BottomRadius 에서 TopRadius 로 변화합니다.
    2. 이제 ZenithAngle을 구할 줄 알았는데, ZenthAngle에 따라 이동할 수 있는 최소/최대 거리를 먼저 계산합니다. 이 거리를 기반으로 ViewZenithAngle 을 유도합니다. 이렇게 하는 이유는 d 변수의 계산을 보면 유추해볼 수 있을 것입니다. 먼저 d는 uv.x 가 0~1 로 변화하면 d_min~d_max 로 변화하는 것으로 확인됩니다. 그리고 ViewHeight 값을 변경시켜가며 확인해보면 d_max 와 ZenithAngle 은 rho 가 H 에 근접할 수록 계속해서 더 커집니다. 그렇다면 ZenithAngle 은 ViewHeight 에 따라 달라지면 사용하는 각의 범위도 달라진다는 말입니다. 그래서 ViewHeight 에 정확히 필요한 ZenithAngle 의 범위를 계산하기 위해서 d 값을 기준으로 ZenithAngle 을 역으로 생성합니다.
    3. 여기서는 ViewHeight 와 이동거리 d 를 사용하여 ViewZenithCosAngle 을 구합니다. 근의공식을 사용하여 유도하는데 식이 조금 복잡할 수 있어서 그림 9에서 유도과정을 자세히 설명하겠습니다. 조금 특이한 점은 3번의 (H * H - rho * rho - d * d) / (2.0 * viewHeight * d) 식을 레퍼런스9의 다른 식을 통해서 유도해보면 (TopRadius * TopRadius - viewHeight * viewHeight - d * d) / (2.0 * viewHeight * d) 가 되어야 한다는 것입니다. 하지만 (H * H - rho * rho) 와 (TopRadius * TopRadius - viewHeight * viewHeight) 부분의 결과가 동일하게 나와서 이 부분에 대해 문제는 없습니다. 또한 H, rho 를 사용하는 식이 변수의 크기가 작기 때문에 오차 발생도 적습니다. 일단 이 부분은 제가 이해할 수 있는 (TopRadius * TopRadius - viewHeight * viewHeight) 기반으로 해석을 하고 넘어가고 추후에 이 부분에 대해서 알게되면 이 부분에 추가글을 달겠습니다.

    그림6. UV 를 TransmittanceLUT Param 으로 변경하는 코드, viewHeight 와 viewZenithCosAngle 로 변환시킴.

     

    그림7. H 와 rho 를 구하는 부분
    그림8. ZenithAngle 그리고 d_min, d_max 를 구하는 부분.

     

     

    d 를 사용하여 ZenithCosAngle 을 유도하는 부분을 보여줍니다. 이 내용은 약간 복잡하기 때문에 번호를 붙여서 차례대로 한번 보겠습니다.

    우리는 ViewHeight 가 p 인 지점에서 점 p와 점 i 사이의 거리인 ||pi|| or d 를 알고 있을 때, u=cos(theta) 를 구하는 식을 만들 예정입니다.

    1. OP 단위벡터와 PI 단위벡터를 내적 하여 cos(theta) 를 얻을 수 있음을 확인합니다.
    2. 삼각함수에 의해서 sin(theta) = sqrt(1 - cos(theta)^2) 임을 알 수 있습니다. 이 사실을 사용하여 점 i 의 좌표를 구할 수 있습니다. 위에 그림에 이 좌표에 대한 정보를 빨간색 라인으로 그려뒀습니다.
    3. 2에서 구한 좌표에 피타고라스 정리를 사용하면 대각의 길이(||oi||) 를 구할 수 있을 것입니다. 그리고 우리는 ||oi|| 값이 r_top 이라는 것을 알고 있습니다. 그래서 최종적으로 식을 정리합니다.
    4. 3번에서 구한 식에 근의 공식을 사용해 u 로 정리한식을 볼 수 있습니다. u는 cos(theta) 를 의미하고 우리가 원하는 cos(ZenithAngle) 를 얻었습니다.

    그림9. ZenithCosAngle 을 d 로 부터 유도하는 식 (출처 : 레퍼런스9).

     

    LutTransmittanceParam 으로 부터 uv 를 얻어내는 식도 한번 알아보겠습니다. 어차피 뒤에서 보게 될 것이기 때문에 같이 확인해 봅시다.

    1. 그림7에 나오는 H 와 rho 를 구하는 부분입니다. 이 부분은 위의 내용과 동일합니다.
    2. 근의 공식을 구하는 부분입니다. 근의 공식에서 판별식에 해당하는 부분과 나머지 부분을 차례로 계산한 후 ||pi|| or d 를 구하는 것을 볼 수 있습니다. ||pi|| 는 현재 위치에서 ZenithAngle 방향으로 진행할 때 대기의 상단지점에 도착하는 거리입니다. 근의 공식은 여기(레퍼런스10) 를 참고해 주세요. 그리고 근의 공식에 대입할 식은 그림9의 3번에 있는 식 ||pi||^2 + 2ru||pi|| + r^2 = r_top^2 을 사용합니다.
    3. d_min 과 d_max 를 구하는 식을 볼 수 있습니다. 그림8에서 확인했었습니다. 그리고 x_mu 를 구하는데 이 값은 d의 위치를 d_min 과 d_max 를 사용하여 0~1 의 스케일 값으로 만들어줍니다. 이 값은 uv.x 값에 저장됩니다.
    4. 마지막으로 x_r = rho / H 를 사용하여 rho / H 비율을 저장합니다. 이 값은 uv.y 값에 저장됩니다.

    그림10. TransmittanceParams(viewHeight 와 ZenithCosAngle) 로 부터 UV 를 구하는 코드.

    그럼 마지막으로 IntegrateScatteredLuminance 함수를 확인해 봅시다. 이 함수는 레이마칭에 관련한 모든 기능을 갖고 있습니다. 하지면 여기서는 OpticalDepth 를 계산하는 부분만 확인해봅시다.

    1. earth0 은 행성의 중심점입니다. 여기서는 원점으로 설정합니다. 현재위치인 WorldPos 에서 WorldDir 방향으로 Ray를 발사해서 행성 대기의 BottomRadius 와 TopRadius 와 교차되는 지점을 찾습니다. 리턴되는 tBottom, tTop 은 Ray 방정식의 교차지점에 대한 t 값입니다. 이 값이 + 이면 WorldDir 방향으로 이동 중 교차한 것이고 - 이면 WorldDir 반대방향에서 교차했다는 의미입니다. tBottom 과 tTop 중 더 빨리 충돌한 지점을 tMax 에 저장합니다.
    2. tBottom 과 tTop 의 조합에 따라서 총 4가지 케이스가 있을 수 있을 것입니다. 대기가 끝나는 가장 가까운 t 값을 tMax 에 저장합니다.
      • tBottom -, tTop - : 둘 다 마이너스인 경우는 대기권 바깥에서 대기가 없는 방향으로 Ray 를 발사한 경우입니다. 이 경우는 대기가 없기 때문에 라이트가 그대로 전달됩니다. 그래서 Transmittance 값의 변화도 없을 것이므로 바로 함수 실행을 종료합니다.
      • tBottom -, tTop + : 이 경우는 카메라가 대기 내에 있다는 의미일 것입니다. 이 경우 바닥 방향이 아닌 하늘 방향으로 레이마칭을 할 것입니다.
      • tBottom +, tTop - : Bottom 만 교차하고 Top 은 교차하지 않는 경우는 발생하지 않습니다. Bottom 은 Top Sphere 의 내부에 있기 때문입니다.
      • tBottom +, tTop + : 이 경우는 카메라가 대기 내에 있고, 지면 방향으로 레이마칭을 수행하는 상황일 것입니다. Transmittance LUT 생성 시에는 지면으로 쏘는 레이마칭은 고려하지 않아서 이런 케이스는 없을 것입니다.
    3. SampleCount 는 레이마칭 횟수를 의미하고 이 함수가 호출될 때 40 으로 전달받았습니다.
    4. 1회 샘플링 시 이동거리인 dt 를 구합니다. 하지만 이 값은 무시되고 매 루프마다 새로 만들어지니 이 코드는 무시하세요.
    5. 현재는 태양의 위치는 고려하지 않는 Transmittance 정보만 다루기 때문에 WorldDir 만 고려합니다.
    6. 레이마칭을 반복합니다. 샘플 당 0.3 의 거리를 사용하기 위해 설정해 두는 것을 볼 수 있습니다.
    7. dt 를 새로 계산합니다. 샘플 당 dt 를 0.3 거리를 이동하는 것으로 설정합니다.
    8. 레이마칭 후 현재 위치인 P 를 구합니다.
    9. 현재 위치 기준으로 Rayleigh, Mie, Ozone 에 대한 extinction 정보를 모아 medium 에 저장합니다. Transmittance 는 extinction 후 남은 라이트의 비율을 의미합니다. 현재 위치를 기반으로 한 extinction 을 dt 거리만큼 감쇄하는 점을 고려해서 SampleOpticalDepth 를 생성합니다. 40회의 레이마칭 동안의 OpticalDepth 를 누적합니다.
    10. 누적을 마친 OpticalDepth 를 리턴합니다. 리턴된 받은 측에서 이 값을 사용하여 exp(-OpticalDepth) 방식으로 Transmittance 를 계산합니다.

    그림11. Transmittance LUT 에서 레이마칭으로 Transmittance 를 얻는 코드.

     

    2.1.2. Multiple scattering LUT

    다음은 Multiple scattering LUT 생성입니다. 여기서 만들어지는 Multiple scattering LUT 는 Sky-View LUT 와 Aerial perspective LUT 에 모두 쓰이기 때문에 그 두 LUT 보다 먼저 만들어집니다. Multiple scattering 을 지원하기 위해서 주변 Sphere 방향에서 현재위치로 Single scattering 되는 부분을 모아서 L_2ndOrder 와 F_ms 를 만들었습니다. 그리고 두 값을 곱하여 최종적으로 Multiple scattering LUT 에 저장할 Ψ_ms = L_2ndOrder * F_ms 을 생성합니다. 그리고 Ψ_ms 는 Transfer function 이기 때문에 추후 태양의 illuminance 를 곱하여 luminance 얻을 수 있었습니다. 코드가 조금 긴데 먼저 전체적으로 둘러보고 중요한 부분은 세부사항을 보도록 합시다.

    이 작업은 Compute Shader 를 사용합니다. Dispatch(32, 32, 0) 이고, Thread(1, 1, 64) 를 사용합니다.

    • LUT 입력값 : uv.x 태양의 CosZenithAngle, uv.y 대기 내에서의 높이(0이 Bottom, 1 이 Top)
    • LUT 출력값 : 현재 지점에서의 Multiple scattering 결과 (이 결과에 illuminance E 를 곱하여 실제 luminance 를 얻을 수 있음)

     

    1. groupshared 변수 2개가 선언된 것을 볼 수 있습니다. groupshared 는 Compute Shader 의 WorkGroup 내에서 공유되는 변수입니다. MultiScatAs1SharedMem 은 f_ms, LSharedMem 은 L_2ndOrder 를 구하는 데 사용됩니다. 각각은 64개의 배열을 갖고 있는데, 이 함수에서는 Sphere 를 64 부분으로 쪼개서 각각 계산하여 마지막에 Thread 0 이 모든 내용을 합산하기 때문입니다.
    2. 이 함수가 WorkGroup 당 64 개의 Thread 를 사용할 것이라고 명세되어 있습니다. 64 개인 이유는 위에서 말했듯 Sphere 를 64 개 부분으로 쪼개서 합산하기 때문입니다.
    3. MultiScatteringLUTRest 는 32로 Dispatch 된 WorkGroup 의 x, y 크기와 같습니다. 그래서 ThreadId.xy / MultiScatteringLUTRes 는 0~1 사이의 uv 값을 만들어줍니다. 그 외에 자잘한 연산이 있는데 0~31 값을 각각 0~1 사이값으로 매핑하는 내용입니다.
    4. uv 를 사용하여 LUT 의 입력값으로 변경하는 과정입니다. uv.x 는 태양의 CosSunZenithAngle 을 얻어냅니다. Cos 은 -1~1 사이의 값을 가지기 때문에 값을 조정해 줍니다. uv.y 는 BottomRadius 와 TopRadius 사이의 위치인 viewHeight 구하는 데 사용합니다. 0이면 Bottom, 1 이면 Top 입니다. WorldPosition 은 (0, 0, viewHeight) 로 설정합니다. 행성이 둥글다고 가정하면 고도와 ZenithAngle 에 따라서만 레이마칭 결과가 달라질 것이기 때문에 원점을 기반으로 한 가장 간단한 WorldPosition 을 구한 것으로 보입니다.
    5. 추후 레이마칭에 사용할 변수들입니다. Multiple scattering LUT 생성에는 2가지 옵션만 사용합니다.
      • ground 옵션은 L0 를 계산할지 여부입니다. 레이마칭 시 지면과 충돌하는 경우 거기서 오는 luminance 까지 고려한다는 의미입니다.
      • SampleCountIni 는 레이마칭 횟수입니다. 20으로 설정되어 있습니다.
    6. 필요한 변수들을 미리 만듭니다. Isotropic phase function 을 만드는데, 구의 표면적에 동일한 양으로 산란될 것이기 때문에 “1 / 구표면적” 식을 사용한 것을 볼 수 있습니다.
    7. 여기서는 Thread.z 값 (0~63)을 기준으로, 2개의 변수를 만들어냅니다. 이 두 개의 변수는 64개로 나뉜 구의 부분 중 하나를 선택하기 위해서 사용됩니다. 구면좌표계를 사용하여 선택할 수 있으며, 필요한 변수는 θ, Φ 가 될 것입니다. 여기서는 Thread.z 가 0~63 으로 변경되는 동안 RandA, RanB 값이 어떻게 변하는지 봅시다.
      • 0~7 구간
        • i 값은 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625
        • j 값은 0.0625, 0.1875, 0.3125, 0.4375, 0.5625, 0.6875, 0.8125, 0.9375
      • 그리고 8~15 구간은 i 가 0.1875 로 고정된 채로 j 값은 0~1 사이 값으로 변경되고… i 가 0.9375 가 될 때까지 반복될 것입니다.
    8. 위에서 구한 RandA, RandB 를 사용하여 θ, Φ 를 구하게 됩니다. 구면좌표계에서 θ 는 0 ~ 2π 이고, Φ 는 0 ~ π 구간입니다. 이때 Sphere 의 반지름 r = 1 입니다. 이제 구면좌표계를 직교좌표계로 변경합니다. 변환식은 레퍼런스5를 참고해 주세요. (레퍼런스5의 설명에 θ, Φ 가 바뀌어있는데 나라에 따라 바뀌기도 한다고 하니 신경 쓰지 마시고 식만 확인해 보세요). 변환한 결과는 WorldDir 입니다.
    9. 이제 위에서 구한 WorldPos 와 WorldDir 를 사용하여 레이마칭을 수행합니다. 5번에서 본 것처럼, ground 옵션을 사용하며 레이마칭 수는 20회입니다. 이 부분은 그림14 에서 더 자세히 확인해 봅시다.
    10. 레이마칭을 마친 결과를 누적합니다. result.MultiScatAs1 과 result.L 은 각각 Sphere 의 64개 중 한 방향에 대한 f_ms, L_2ndOrder 입니다. SphereSolidAngle / (sqrtSample * sqrtSample) 식에서 sqrtSample 은 8 입니다. 즉, 구의 표면적을 64개로 나눠서 레이마칭을 수행하기 때문에 추가된 식으로 보이며, 64 개의 값이 모두 합쳐지면 1이 될 것입니다.
    11. 64 개의 스레드에 있는 모든 값을 합산하는 코드입니다. 최종 결과는 Thread 0 번만 처리합니다. 합산결과에 6에서 구한 isotropic phase function 을 곱해줍니다. 논문에서 2차 이상의 scattering order 에서는 isotropic phase function 이 발생하는 것으로 확인했었고, 그 부분이 반영된 것 같습니다.
    12. f_ms 를 무한등비급수를 사용하여 무한번의 scattering order 를 얻도록 합니다. 그리고 그 결과를 F_ms 로 둡니다. 최종적으로 우리가 원하는 Ψ_ms = L_2ndOrder * F_ms 를 얻습니다.
    13. Ψ_ms 를 저장합니다. 이 값을 스케일링할 수 있는 요소도 곱해주는 부분이 있습니다. 현재는 이 부분은 1로 두고 봐도 무방할 것 같습니다.

    그림12. Multiple scattering LUT 를 생성하는 코드.

    계속해서 Multiple scattering LUT 생성에 사용되는 레이마칭 부분을 확인해 봅시다. Transmittance LUT 를 만들 때 사용했던 IntegrateScatteredLuminance 를 사용합니다. 이번에는 ground 옵션을 추가로 사용합니다. 이전에 봤던 코드들이라 중복되는 부분들이 많지만 개인적으로 이전 분석과 현재 분석을 섞어보는 것보다는 전체 흐름을 한 번에 훑어보는 게 더 도움이 된다고 생각합니다. 그래서 IntegrateScatteredLuminance 코드를 다시 한번 보겠습니다. 코드가 길어서 창을 2개 띄우고 코드와 설명을 같이 보시는 것을 추천드립니다.

    이번 코드에서 구현하는 식은 바로 아래의 그림13 의 식들입니다. 레이마칭 한 개는 L’ 와 Lf 함수에 해당할 것입니다. 이 레이마칭을 모두 모으면 L_2ndOrder, f_ms 가 될 것입니다.

    그림13. Multiple scattering LUT 를 생성하는데 필요한 공식.(출처 : 레퍼런스1).

     

    1. 레이마칭 할 거리를 파악하기 위해서 수행하는 코드입니다. 대기가 끝나는 가장 가까운 t 값을 구하기 위해서 tBottom, tTop 을 모두 구합니다. 그리고 이 행성은 earth0 을 통해 원점에 있다는 것을 알 수 있습니다.
    2. tBottom 과 tTop 의 조합에 따라 총 4가지 케이스가 있을 수 있을 것입니다. 대기가 끝나는 가장 가까운 t 값을 tMax 에 저장합니다. 4가지 케이스에 대한 설명은 주석이나 위에서 본 설명에서 확인해 주세요.
    3. 레이마칭 횟수를 지정합니다. 여기서는 20회 수행합니다.
    4. SunDir, WorldDir 를 설정합니다.
    5. 이제 레이마칭을 수행합니다. 레이마칭 당 0.3 거리를 사용하기 위해 세그먼트 크기를 설정해 줍니다.
    6. dt 를 새로 계산합니다. 레이마칭 당 dt 를 0.3 거리를 이동하는 것으로 합니다.
    7. 레이마칭 후 현재 위치인 P 를 구합니다.
    8. 태양의 ZenithCosAngle 과 현재 위치의 고도를 구해서 Transmittance LUT 에서 사용할 uv 를 얻어냅니다. 이것을 통해 태양에서부터 온 라이트가 대기를 뚫고 현재 위치로 이동한 뒤 남은 라이트의 비율인 TransmittanceToSun 을 얻습니다.
    9. 현재 위치에서 발생하는 Scattering (Mie + Rayleigh) 에 Uniform Phase function (UniformPhase = 1.0 / (4.0 * PI))을 곱합니다. 이걸로 현재 카메라 위치로 스케터링 되는 라이트의 비율을 얻을 수 있습니다. Uniform phase function 을 사용하는 것은, 논문에 2 order 이상의 스케터링의 경우 isotropic phase function 이 적용되기 때문입니다.
    10. 쉐도우맵에서 현재 위치 기준 차폐여부를 확인합니다. 변수 이름과는 다르게 shadow 가 1이면 그림자 외부, 0이면 그림자 내부인 상태입니다.
    11. 이 식은 그림13의 (6) 식을 만들기 위한 일부분입니다. (6)식의 우측에 적분의 내부식과 코드의 변수를 맞춰봅시다. globalL 의 경우 식의 Ei 입니다. 이 값은 자리표시자(placeholder) 로 1입니다. 현재 위치에서의 스케터링 비율 σs(x) 와 Phase function pu 의 곱을 PhaseTimesScattering 변수로 나타냅니다. (shadow * earthShadow * TransmittanceToSun) 은 Shadowing function 인 S(x, ws) 를 나타냅니다. earthShadow 는 따로 설명 하지 않았지만 태양이 지평선 보다 아래로 내려가있는지 확인하는 부분입니다. multiScatteredLuminance 는 MULTISCATAPPROX_ENABLED 매크로가 꺼져있기 때문에 현재는 0으로 설정됩니다. 이 부분은 추후에 Multiple scattering LUT 를 사용할 때 활성화 되는 코드입니다. 적분 내부의 식중에 T(x, x - tv) 를 제외하고 모두 확인된 거 같습니다. 이 부분은 이후 과정에서 계속해서 수행됩니다.
    12. 그림13 의 (8) 식의 적분 내부에 대한 식입니다. 적분은 구간내를 세그먼트 단위로 잘라서 모두 합산합니다. 그래서 현재 구간내의 한개의 세그먼트를 처리한다고 생각하고 코드를 보면 좋습니다. 원래 코드는 Scattering * T * dt 가 현재 세그먼트에 대한 값이고 이 값을 계속 누적하여 결과를 만들면 적분이 완료될 것입니다. 하지만 여기서는 레퍼런스6 에 있는 그림15 를 참고해서 에너지 보존이 되는 식으로 대신해서 사용한 것으로 보입니다. 식이 어려워 보일 순 있지만 의미는 현재 처리중인 세그먼트에 대한 Scattering * T * dt 입니다. 이 식에서 throughput 은 위에서 설명하지 않았는데, 레이마칭 시작 부터 현재 위치까지의 Transmittance 정보를 누적한 값입니다. 현재 세그먼트의 Transmittance 는 exp(-SampleOpticalDepth) 와 같고 반복문 가장 밑에서 이 값이 계속 누적됩니다. (누적이 곱셈으로 표현되는데 extinction 은 multiplicative 연산이기 때문임)
    13. 다시 그림13 (6) 식의 L’ 의 적분 내부 식으로 돌아옵니다. 11번 단계에서 σs(x) * S(x, ws) * pu * Ei 를 구해둔 것이 S 였습니다. 추가로 T 만 곱해주면 됩니다. 이 부분도 12번 식과 같이 현재 세그먼트에 대한 Transmittance 를 위해 그림15 의 식을 사용합니다. 그리고 이전에 적용되어 온 Transmittance 정보를 throughput 을 곱해주어서 누적합니다. Transmittance 는 multiplicative 연산이기 때문에 이전 작업들을 곱해주는 형태로 누적합니다. 이 부분이 잘 이해가지 않는다면 이전글(레퍼런스3)의 2.1.4. luminance function 을 다시 한번 참고해 보면 좋을 것 같습니다. 이제 (6)의 적분식의 분석은 모두 마쳤습니다.
    14. 남은 것은 그림13 (6)식의 T(x, p) * L0(p, v) 입니다. 이 코드는 레이마칭 결과가 지면에 닿은 경우만 처리됩니다. Transmittance LUT 텍스쳐로 부터 TransmittanceToSun 을 구합니다. TransmittanceToSun 태양에서 현재 쉐이딩 처리를 하고 있는 지면에 도착한 라이트의 양을 나타냅니다. 지면의 기울기를 고려하기 위해서 NdotL 을 곱하여 최종적으로 지면에 도달한 라이트 양을 결정합니다. 이제 GroundAlbedo/PI 부분을 확인해 볼 것입니다. 논문에서 행성의 경우 순수한 Diffuse 이고 알베도는 0.3 으로 고정한다고 가정했습니다. 순수 Diffuse 기 대문에 Albedo 만 고려하는 것으로 보이며, Lambertian BRDF 를 적용하기 위해 1/PI 이기 때문에 적용한 것으로 보입니다. 이 부분의 유도는 레퍼런스7의 2.1. IrradianceMap 의 용도 부분과 1/PI 의 유도는 레퍼런스8 부분을 참고해 주세요. 이제 식에서 TransmittanceToSun * NdotL * GroundAlbedo / PI 를 봤습니다. 남은 것은 globalL 과 throughput 입니다. globalL 의 경우 자리표시자(placeholder)로 1로 고정일 것입니다. 그리고 throughput 의 경우 현재까지 누적해 온 Transmittance 즉, T(x, p) 일 것입니다.
    15. 그림13 (6) 식의 최종결과를 result.L 에 담고 함수를 종료합니다.

    그림14. Multiple scattering LUT 를 생성하기 위해서 Sphere 를 64 개의 방향으로 나눠 레이마칭을 수행함. 그 중 1개에 대한 레이마칭 결과를 계산하는 코드.
    그림15. 거리 D 만큼 이동시 transmittance 되는 양을 계산하는 식 (출처 : 레퍼런스6).

    정말 긴 호흡이었습니다. 방금 본 레이마칭함수는 앞으로도 계속 나오기 때문에 아주 중요하며 반복되는 코드입니다. 충분히 숙지하셨다면 이 함수를 다시 만날 때 훨씬 쉬울 거라 생각됩니다.

     

    2.1.3. Sky-View LUT

    다음은 Sky-View LUT 생성입니다. 앞서 만든 Transmittance LUT, Multiple scattering LUT 를 여기서 사용합니다. 이 LUT 는 이전글(레퍼런스3)에서 리뷰한 것처럼 다른 물체에 걸리는 것 없이 그대로 하늘을 렌더링 하는 경우 사용하는 LUT 입니다. 이 LUT 의 uv.y 인 경도(Latitude) 에는 non-linear 방식으로 된 처리가 추가되었습니다. 지평선에 가까워질 수로 결과가 High frequency 를 띄는 특징 때문입니다. 이런 점은 LUT 를 저해상도로 렌더링 하여 업스케일링 해서 사용하는 경우 그 결과의 차이가 더 뚜렷한 것도 확인했었습니다.

    • LUT 입력값 : uv.x : 위도(0~180도), uv.y : 경도(-90~90도)
    • LUT 출력값 : Sky 를 렌더링 할 때 사용되는 luminance(L) 값.

     

    SkyView LUT 코드를 전체적으로 둘러보기 전에 먼저 SkyViewLutParamsToUv, UvToSkyViewLutParams 함수를 차례로 둘러봅시다.

    시작하기 전에 LUT 의 v 값을 그림16의 식으로 압축한다고 논문에 언급했었는데, 이 부분은 실제 코드에서 더 최적화된 형태로 변경되었습니다. 어떤 형태로 구성되었는지 코드를 보면서 같이 알아가면 좋을 것 같습니다.

    그림16. 지평선에 가까울수록 High frequency 인점을 고려하여, v 를 non-linear 로 만든 식. 실제 구현에서는 조금 다른 방식을 사용함. (출처 : 레퍼런스1).

    먼저 SkyViewLutParamsToUv 입니다.

    1. Vhorizon, ZenithHorionAngle, Beta 변수를 준비합니다. 이 변수들은 다음과 같은 의미를 가집니다. 그리고 그림18 에서 그림으로 어떤 의미인지 조금 더 자세히 설명합니다.
      • VHorizon : 지면에서 지평선과 현재 viewHeight 위치 사이의 거리
      • ZenithHorizonAngle : viewHeight 위치에서의 Zenith 를 기준으로 지평선을 바라봐야 할 때 필요한 각
      • Beta : viewHeight 위치에서 지평선 방향을 보고 있을 때 거기서 지면을 수직으로 바라보는데 까지 필요한 각
    2. 이 경우 지평선 보다 위쪽을 바라보는 경우입니다. 지평선 보다 위쪽을 바라보는 경우는 viewZenithAngle 은 0~ZenithHorizonAngle 범위 내에 있을 것입니다. 그래서 viewZenithAngle 을 ZenithHorizoneAngle 을 기반으로 [0, 1] 범위로 정규화 합니다. 그 것이 바로 coord 변수 입니다. 현재는 coord 가 1에 가까울수록 viewZenithAngle 이 지평선을 바라봅니다. sqrt(coord) 로 지평선 근처 데이터를 압축하기 위해서 coord 의 0 과 1을 반전 시킨 후 sqrt(coord) 작업을 하고 다시 coord 의 0과 1을 반전시킵니다. 지평선 위쪽의 결과는 DirectX TexCoord 기준으로 상단에 배치됩니다. 이 영역은 [0, 0.5] 범위이고 uv.y = coord * 0.5 를 통해 그 작업을 합니다.
    3. 이 경우 지평선 보다 아래쪽을 바라보는 경우입니다. 지평선 보다 아래쪽을 바라보는 경우는 viewZenithAngle 이 항상 ZenithHorizonAngle~PI 범위내에 있으며 viewZenithAngle 은 항상 ZenithHorizonAngle 보다 큽니다. ZenithHorizonAngle~PI 사이가 이루는 각은 Beta 입니다. 여기서 구한 coord 는 0일 수록 지평선에 가깝기 때문에 그대로 sqrt(coord) 를 처리합니다. 그리고 지평선 아래의 결과는 DirectX TexCoord 기준으로 하단에 배치됩니다. 이 영역은 [0.5, 1.0] 범위이고 uv.y = coord * 0.5 + 0.5 를 통해 그 작업을 합니다.
    4. 2, 3번 연산을 통해 경도에 대한 작업은 마쳤습니다. 여기서는 위도에 대한 작업을 합니다. 여기서의 lightViewCosAngle 은 위도를 나타내는 값입니다. Cos 값을 0~1 범위로 저장하기 위한 연산과 0 도에 가까울수록 coord 를 더 압축하기 위해 sqrt 역시 적용한 것을 볼 수 있습니다. 0.0, 0.5, 1.0 이 차례로 0, 90, 180도가 되도록 매핑시킵니다.
    5. 마지막으로 최종결과를 저장합니다. 여기서 나오는 192, 108은 현재 사용하고 있는데 Sky-View LUT 의 해상도입니다. fromUnitToSubUvs 는 픽셀 -0.5 픽셀 정도 이동시켜 주는 역할을 합니다.

    그림17. SkyViewLutParam(viewZenithCosAngle, lightViewCosAngle, viewHeight) 를 사용하여 uv 를 만들어내는 코드.
    그림18. VHorizon, Beta, ZenithHorizonAngle 에 대한 그림 설명.

     

    다음은 UvToSkyViewLutParams 입니다.

    1. SkyViewLutParamsToUv 에서 본 것과 동일한 내용입니다. uv 경우 artifact 를 피하기 위해서 마지막 저장하기 전처럼 fromSubUvsToUnit 을 사용하여 -0.5 픽셀 이동시켜 줍니다. Vhorizon, ZenithHorizonAngle, Beta 은 그림18 를 참고해 주세요.
    2. 그림17 의 2번에서 지평선보다 위쪽에 있는 경우 coord 를 저장하는 방식을 역으로 계산하는 것입니다. [0.0, 0.5] 범위였던 값을 다시 [0.0, 1.0] 범위로 바꾸고, 지평선 부분에 압축해둔 coord 정보를 풀기 위해 coord 에서 0과 1을 을 반전하고 coord * coord 처리 후 0 과 1을 다시 반전합니다. 지평선의 상단을 바라보는 경우 ZenithHorizonAngle 에 대한 사용 비율 값이 coord 에 저장됩니다. 그래서 viewZenithAngle 은 ZenithHorizoneAngle * coord 로 얻을 수 있습니다.
    3. 지평선 아래를 바라보는 경우입니다. 이 경우도 그림17 의 3번 과정을 역으로 수행합니다. 그리고 이를 통해 얻어낸 coord 는 Beta 각의 사용 비율입니다. 그리고 지평선 아래를 다루기 때문에 ZenithHorizonAngle + Beta * coord 한 것이 실제로 사용할 viewZenithAngle 이 됩니다.
    4. uv.x 의 경우도 coord 를 sqrt 통해 압축했기 때문에 복원해 줍니다. 그리고 0~180 까지의 범위로 변경합니다.

    그림19. UV 와 viewHeight 를 사용하여 SkyViewLutParams (viewZenithCosAngle, lightViewCosAngle)을 만드는 코드.

    이제 우리가 보고자 하는 SkyViewLutPS 함수입니다.

    1. WorldDir 은 아래 코드에서 다시 만들기 때문에 그 부분은 건너뛰고 바로 WorldPos 생성하는 데로 이동합니다. WorldPos 을 카메라 위치를 기반으로 생성합니다. uv 값을 구하는데 192, 108은 SkyViewLUT 텍스쳐의 해상도입니다. 다음으로 WorldPos 를 기준으로 현재의 viewHeight 를 구합니다. 그리고 uv 정보와 viewHeight 를 기반으로 SkyViewLutParams 를 구합니다. 이 과정은 위의 그림19 에서 보았습니다. UvToSkyViewLutParams 을 호출하고 나면 우리는 구면좌표계 기준 값인 viewZenithCosAngle (경도), lightViewCosAngle(위도) 값을 얻습니다.
    2. 이제 UpVector 기준으로 태양의 ZenithAngle 을 얻습니다. 그리고 이것으로 SunDir 을 만들어냅니다. WorldPos 도 viewHeight 를 사용하여 더 간단한 형태로 갱신합니다.
    3. 1번 과정에서 구한 위도, 경도에 각을 사용하여 구면좌표계를 직교좌표계로 변경합니다. 그렇게 해서 WorldDir 을 구성합니다.
    4. 대기 바깥 우주 공간에 있다면, 대기가 시작하는 위치에서부터 레이마칭을 해야 할 것입니다. 그래서 대기의 TopRadius 와 충돌할 때까지 이동합니다. 만약 이미 TopRadius 내부라면 그냥 현재위치가 사용될 것입니다. 만약 WorldDir 방향으로 이동해도 대기와 충돌하지 않는다면 레이마칭을 더 이상 수행하지 않습니다.
    5. 실제 레이마칭을 수행하는 부분입니다. 30 회 레이마칭을 수행하며, VariableSampleCount 기능과 Mie, Rayleight phase function 을 사용합니다. 이 코드에 대한 것은 그림22 에서 자세히 확인해 봅시다.
    6. 레이마칭을 마친 최종 결과인 L 을 리턴합니다.

    그림20. Sky-View LUT 를 생성하는 코드.

    이제 마지막으로 Sky-View LUT 에서의 레이마칭 함수를 봅시다. 이미 2번 정도 전체 흐름을 확인했기 때문에 이번에는 기존과의 차이점 위주로 알아보려고 합니다. 이번 레이마칭의 특징은 30회의 레이마칭을 수행하고, MieRayPhase function 을 사용한다는 점과 VariableSampleCount 를 사용하는 점입니다. 그리고 이전 과정에서 구한 Multiple scattering LUT 도 적용합니다. 그림21 의 식을 참고해 주세요.

    그림21. Multiple scattering 이 고려된 대기산란 식.(출처 : 레퍼런스1).

    계속해서 그림20에 나온 레이마칭 코드를 확인해 봅시다.

    1. 이전 설명에서 본 것처럼 대기 내의 레이마칭 할 거리를 구하기 위한 코드입니다.
    2. tMax 의 거리에 따라서 레이마칭의 샘플 개수를 다르게 합니다. tMax * 0.01 이기 때문에 100 km 거리에 대해서 최소 4, 최대 14 샘플링 수를 사용합니다.
    3. SunDir, WorldDir 를 사용하여 Mie, Rayleigh phase function 을 준비합니다. 디렉셔널 라이트라고 가정하고 있기 때문에 이 값이 변경되진 않을 것이므로 반복문 바깥에서 미리 만들어둡니다.
    4. 레이마칭 중인 현재 샘플 t0 와 다음 샘플 t1 을 준비합니다. 이 값은 [0, 1] 범위입니다. 그리고 non-linear 분포를 만들기 위해서 t0, t1 를 제곱합니다. t0, t1 은 범위가 [0, 1] 이기 때문에 제곱하게 되도 숫자범위가 1이하입니다. 이제 t0 과 t1 를 tMaxFloor 에 곱하여 실제로 이동하는 거리를 나타내는 t 값으로 변환합니다. 마지막으로 dt = t1 - t0 로 dt 를 구합니다.
    5. 태양에서 현재 샘플링 중인 위치까지의 Transmittances 정보를 얻습니다.
    6. Phase function 을 사용하기로 했기 때문에 3번에서 구해둔 Phase function 에 각 매체별 스케터링 되는 비율 곱해줍니다. 스케터링 되는 라이트 중 Phase function 만큼이 카메라 위치로 들어오기 때문입니다. 그리고 Ozone 이 빠진 것은 Ozone 은 흡수만 하기 때문입니다.
    7. 이 식은 그림14 의 11 항목에서 봤던 식입니다. 이 코드에는 그림21 의 Lscat 식 T * S * p * σs * Ei 가 있습니다. 하나 다른점은 이번에는 multiScatteredLuminance * scattering 를 사용한다는 점입니다. 이 코드 바로 위를 보면 MULTISCATAPPROXY_ENABLE 매크로로 감싸진 부분을 봅시다. 이전에 만든 Multiple scattering LUT 을 현재위치와 SunZenithCosAngle 을 기반으로 샘플링하는 것을 볼 수 있습니다.
    8. 이 부분은 그림14 의 13번 내용과 동일합니다. 현재 세그먼트에 대한 Transmittance 를 적용하고, 이전에 중첩해둔 Transmittance 도 여기서 적용합니다. 그리고 현재 샘플에 대한 Transmittance 를 throughput 에 누적합니다.
    9. 이제 레이마칭이 완료되었습니다. 지금까지 그림21 의 아래쪽 식을 실행한 것이며, 이 Lut 는 Sky 에 대해서만 처리하기 때문이 T(c, p) * L0(p, v) 에 대해서는 처리되지 않았습니다.

    그림22. Sky-View LUT 를 생성하기 위해 수행하는 레이마칭 코드. 앞에서 생성한 TransmittanceLUT, Multiple scatteringLUT 사용.

     

    2.1.4. Aerial perspective LUT

    이제 마지막 LUT 인 Aerial perspective LUT 생성입니다. 앞서 만든 Transmittance LUT, Multiple scattering LUT 를 여기서 사용합니다. 이 LUT 는 대기와 하늘사이에 물체가 걸리는 경우에 사용하기 위해 쓰입니다. 이 LUT 는 현재 카메라 뷰를 기준으로 생성하며 3D Texture 로 구성됩니다. 물체의 경우 어떤 거리에 위치할지 모르기 때문에 3D Texture 의 z 값을 기준으로 거리 기반하여 대기의 Luminance 를 캐싱합니다.

    • LUT 입력값 : 카메라 뷰 기준 Screen uv 값, 거리기반 Slice index
    • LUT 출력값 : luminance(L) 값, Transmittance 값

     

    1. 카메라 뷰 기준으로 현재 픽셀로의 WorldDir 를 구합니다. 그리고 CameraPos (WorldDir) 와 SunDir 변수 등을 준비합니다.
    2. sliceId 는 0~31까지 들어옵니다. 이 값을 2차 함수 형태로 구성될 수 있도록 갱신해 줍니다.
    3. Slice 를 기준으로 tMax 를 구합니다. 여기서는 Slice 당 4 km 로 설정하여 사용합니다. 논문에서는 1 km 를 기준으로 했지만 구현에서는 4 km 기준으로 설정한 점을 확인할 수 있습니다. tMax 를 만들었기 때문에 현재 Slice 가 지원하는 newWorldPos 를 생성할 수 있습니다.
    4. 만약 3에서 만든 newWorldPos 가 BottomRadius 보다 작다면 지면 아래로 들어간 것일 겁니다. 그러면 이것을 지면 위로 끌어올려줍니다. 그리고 갱신된 위치를 기반으로 WorldDir, tMax 를 갱신해 줍니다. 여기까지 확인한 tMax 값은 tMaxMax 에 저장합니다.
    5. 만약 현재의 카메라 위치가 대기의 TopRadius 보다 더 높다면? WorldDir 방향으로 이동하는 경우 대기가 시작되는 곳까지 이동합니다. 대기가 시작되지 않는 곳을 고려할 필요는 없기 때문입니다. 여기서 WorldDir 로 이동해도 대기를 찾을 수 없다면 대기 산란이 일어나지 않을 것이므로 함수를 종료합니다. 계속해서 카메라 위치에서 대기를 찾는데 까지 이동한 거리 LengthToAtmosphere 를 구합니다. 그리고 만약 tMaxMax < LengthToAtmosphere 이라면 현재 처리중인 Slice 가 대기를 만나기 전에 레이마칭을 종료한다는 의미일 것입니다. 그런 경우는 여기서 함수를 종료합니다. 그렇지 않다면 tMaxMax 를 tMaxMax = (tMaxMax - LengthToAtmosphere) 설정하여 대기 내에서 이동하는 거리로 변경합니다. 물론 이전 과정을 통해서 WorldPos 는 대기가 시작되는 위치로 갱신되어 있습니다.
    6. 또다시 레이마칭을 수행합니다. 이전 과정에서 우리는 다양한 옵션에 대해 레이마칭 과정을 확인했기 때문에 여기서는 옵션을 확인하는 것으로 그 결과를 예측할 수 있을 것입니다. 여기서는 sliceId(0~31)에 따라서 레이마칭을 수를 2~64 로 결정하며, MieRayPhase function 을 사용합니다. 또한 Multiple scattering LUT 를 사용하여 L 을 구합니다. 여기서도 ground 의 정보를 넣지 않았기 때문에 그림21 의 아래식을 사용하되 T(c, p) * L0(p, v) 를 제외한 값을 되돌려 줄 것입니다.
    7. 이제 최종결과인 L 과 1.0-Transmittance 정보를 리턴하며 함수를 종료합니다. Transmittance 는 RGB 별로 구분하지 않고 하나의 스칼라로 변환하여 알파 채널에 저장합니다. Transmittance 는 Extinction 을 통해 감쇄되고 남은 비율을 나타냅니다. 하지만 1.0 - Transmittance 라고 하면 감쇄한 비율을 나타냅니다. 이 값을 어떻게 활용하는지 이후 과정에서 확인해 봅시다.

    그림23. Aerial perspective LUT 를 생성하는 코드. 앞에서 생성한 TransmittanceLUT, Multiple scatteringLUT 사용.

     

    2.2. LUT 텍스쳐를 사용한 최종 렌더링 이미지

    마지막으로 Sky-View LUT 와 Aerial perspective LUT 그리고 SceneDepth texture 로 대기를 렌더링 하는 단계입니다.

    1. 현재 카메라의 위치(WorldPos) 그리고 현재 픽셀을 기반으로 WorldDir 을 구해냅니다.
    2. 현재 픽셀의 Depth 값을 얻어옵니다. 여기서 Depth 가 1 인 경우 아무것도 렌더링 되지 않은 경우입니다.
    3. 아무것도 렌더링 되지 않았고, 현재 위치가 대기의 TopRadius 내부라면 하늘을 바라보는 상태일 것입니다. 이 경우 Sky만 렌더링 하면 되기 때문에 Sky-View LUT 를 사용합니다. viewZenithCosAngle 과 lightViewCosAngle 을 구하여 Sky-View LUT 에 사용할 uv 를 구합니다. 최종적으로 Sky-View LUT 에서 샘플링한 값과 GetSunLuminance 값을 더하여 리턴합니다. GetSunLuminance 의 태양의 Disk 모양을 렌더링 하기 위해 추가된 부분으로 중요하지 않아서 자세히 보진 않겠습니다.
    4. 만약 하늘이 아닌 곳을 렌더링 하는 경우 현재 픽셀과 Depth 값을 사용하여, DepthBuffer 의 WorldPos 를 복원합니다. 그리고 복원한 WorldPos.z 와 현재 카메라의 거리를 사용하여 실제 거리 값을 얻습니다. 실제 거리 값을 통해 Slice 를 얻어냅니다. 이제 Aerial perspective LUT 에 현재 픽셀위치와 slice 정보를 전달하여 샘플링합니다. 샘플링 결과에는 rgb 채널에 Luminance(그림21의 아래식에서 적분에 해당하는 부분) 그리고 a 채널에는 1 - transmittance 정보가 들어있습니다.
      • 1 - transmittance 의 의미는 감쇄비율에 대한 정보입니다. 그래서 Opacity 라는 변수에 담는 건 의미가 맞지 않는 것 같습니다. 이 Opacity 가 어떻게 작동하는지는 렌더링 파이프라인에 설정된 블랜드 옵션과 함께 살펴봅시다.
      • 먼저 그림21의 아래에 있는 식을 만족하기 위해서 우리가 어떤 것이 더 필요한지 알아봅시다. Aerial perspective LUT 를 사용한 경우는 Sky-View LUT 와는 다르게 T(c,p) * L0(p, v) 항도 필요합니다. 왜냐하면 대기가 아닌 물체로 부터 라이트가 반사되기 때문입니다. L0(p, v) 의 정보는 이미 Terrain 패스를 통해서 렌더타겟에 기록되어있습니다. 그림2 의 초록색 네모에 있는 Terrain 패스입니다. 그리고 현재 패스에서 렌더타겟을 클리어하지 않고 추가로 렌더링하게 됩니다. 렌더타겟에 있는 정보가 L0(p, v) 이기 때문에 T(c, p) 만 곱해주면 필요한 것들을 모두 얻을 수 있습니다. T(c, p) 값은 바로 a 채널에 넣은 (1- Transmittance) 로 부터 얻을 수 있습니다. 그림25 의 알파블랜딩 설정을 보면 Col Dst 가 (1 - SrcAlpha)입니다. 이 연산은 알파에 있는 (1 - Transmittance) 를 Transmittance 로 변경해서 Dst Color(L0) 에 곱합니다. 이제 Dst 에 해당하는 부분은 T(c,p) * L(p, v) 가 된다는 걸 알았습니다. 그리고 알파브랜드 함수가 Add 이기 때문에 이번 패스에서 구한 Src 와 더해지게 됩니다. 최종적으로 그림21의 아래에 있는 L 식을 완성할 수 있습니다.
    5. 위에서 구한 결과를 float4(L, 1-transmittance) 서 리턴합니다.

    그림24. 최종적으로 대기를 렌더링하는 코드, 앞에서 생성한 Sky-View LUT 와 Aerial perspective LUT 를 사용하여 대기 렌더링을 수행함.
    그림25. 최종적으로 대기 렌더링 시 사용하는 블랜딩 옵션.

     

    추가로 Terrain 패스에 대해서 한번 보도록 합시다.

    1. PCF Shadow 처리
    2. 태양에서부터 현재 처리 중인 위치까지의 Transmittance(Trans) 을 구합니다. 그리고 Trans * NoL * input.color * sunShadow 를 사용하여 현재 위치에서의 L0 를 구하는 코드를 확인할 수 있습니다. L0 의 결과는 그림2의 좌측이미지에서 확인할 수 있습니다.

    그림26. Terrain 패스에서 L0 를 렌더링하고 있는 것을 확인할 수 있음.

     

    3. 레퍼런스

    1. A Scalable and Production Ready Sky and Atmosphere Rendering Technique, Sébastien Hillaire

    2. https://github.com/sebh/UnrealEngineSkyAtmosphere

    3. [UE4] A Scalable and Production Ready Sky and Atmosphere Rendering Technique 리뷰 (1/2)

    4. Absorption and Scattering (흡수와 산란)

    5. https://ko.wikipedia.org/wiki/구면좌표계

    6. https://www.ea.com/frostbite/news/physically-based-unified-volumetric-rendering-in-frostbite

    7. Diffuse IrradianceMap과 Spherical harmonics를 통한 최적화

    8. BRDF 정리 노트 (작성중)

    9. https://ebruneton.github.io/precomputed_atmospheric_scattering/atmosphere/functions.glsl.html

    10. https://ko.wikipedia.org/wiki/%EA%B7%BC_(%EC%88%98%ED%95%99)

     

     

     

    댓글

Designed by Tistory & scahp.