본문 바로가기

Studies/Shader

Shader Study 후기 - Spherical Harmonics (22/09/19)

계기

발표 내용에 대한 후기에 앞서 셰이더 스터디에 참석한 후기를 간단히 설명드리자면, TA 스터디 월간 온라인 토크에서 자주 이야기를 나누던 Aniz 님의 추천으로, Shader Study 오프라인 모임에 처음으로 참석하게 되었습니다. 해당 모임에 관한 내용은 네이버 카페 - 셰이더 스터디 https://cafe.naver.com/shader 에서 일정을 확인하실 수 있습니다.

 

 

후기

회사 업무에 예상치못한 이슈가 생겨 아슬아슬한 시간에 퇴근을 성공하고, 거의 약속 시간에 맞추어 모임 장소에 도착했습니다. Spherical Harmonics와 Streaming Virtual Texture 2가지 토픽으로 발표가 있었는데요, 두 가지 모두 자주 사용하지만 세세하게 어떤식으로 동작하는지에 대해서는 명확히 알지 못했던 내용이라 매우 반가웠습니다. 대부분 직접적으로는 모르던 내용이었지만, 이미 알고 있던 지식과 부분적으로 연계되어 발표를 이해하고 생각하는데에는 크게 어려움이 없었습니다. 최근 다소 나이브해졌던 개인적인 공부 욕구이 자극됨을 느꼈고 충분한 동기 부여가 되었습니다.

 

 

본 글은 임정환 님의 발표를 토대로 남긴 후기 및 개인 복습 기록입니다. 
보다 자세하고 정확한 내용은 셰이더 스터디 카페에 게시된 자료를 참조 부탁드립니다. (_ _)

 

내용 복습

얻은 지식을 간단히 정리하는 글입니다.
지식의 개인화 및 추상화하는 과정에서 오개념이 포함되었을 수 있습니다.

 

기존에는 라이트 프로브 또는 Indirect Light Cache 기능에서 Precomputed 방식으로 SH를 사용한다는 점은 알고 있었지만, 비교적 최근 Lumen에서 실시간으로 간접광 일루미네이션에 사용될 거라고까지는 생각을 못했었습니다. 더 나아가서는 SH의 존재성과 계산 및 압축의 수단이라고만 알고 있었지 명확히 어떤 알고리즘을 통해 수행되는지에 대한 내용도 전혀 모르고 있었는데요,

그림 1

이 이미지와 곁들여 설명을 듣고나니, 푸리에 변환이 떠오르며 어떤 식으로 동작하는지에 대한 느낌을 알 수 있었습니다. 푸리에 변환은 https://www.jezzamon.com/fourier/ko.html 이 사이트를 통해서 이해하는데 도움을 받았었는데요, 추후에 SH의 이해를 돕기 위해 위 사이트와 비슷한 방식으로 3D 버전 구현을 시도해 볼 수도 있겠다는 아이디어도 얻게 되었습니다. 다만 GI에서의 SH는 메쉬의 형태를 나타내기 위한 것이 아닌, 특정한 점으로 들어오는 라이팅 정보들을 SH의 계수만 저장하는 거라 어떤식으로 쉬운 이해를 불러올 수 있을지는 고민이 필요한 부분이겠습니다.

(아니나 다를까, 발표 설명 도중에 푸리에 변환에 대한 얘기를 해 주셔서, '역시!'라는 마음에 기쁘기도 했습니다. ^^)

 

 

다시 돌아와서, GI가 SH를 어떤 식으로 사용하는 걸까요?

따로 이미지를 생성하지 않고, 글로만 남기려니 다소 난해할 수 있겠지만...

 

공간 어딘가에 존재하는 한 점을 생각해봅시다.

그 한 점에서 모든 방향을 바라보면서, 그 방향에서 보내오는 빛 정보를 기록합니다.

그러면 한 점을 기준으로 빛 정보들의 집합은 구의 모양을 이룰텐데요.

이 빛 정보를 세기에 따라 다른 거리에 점을 찍게 되면 아래와 같은 모양이 됩니다.

그림 2

라이팅의 색과 그 강도를 기록하게 되는데요, 이 정보 그대로 저장하려면 꽤 많은 데이터량이 필요하고... 구현되는 비주얼과 퀄리티에 비해 요구되는 메모리 자원량은 타협 불가능한 수준입니다.

 

그런데, 이 SH * 빛의 세기의 조합을 적절히 사용하면, 적은 개수의 계수(coefficient)만을 저장하고도 원래 형태에 가까운 정보를 복원해 사용할 수 있게 됩니다. 그림 1에 있는 표에서, (m, l) 기준으로

(-2, 2) * a + (-1,2) * b + (0,2) * c + (1,2) * d + (2,2) * e + (-1,1) * f + (0,1) * g + (1,1) * h + (0,0) * i

계산을 통해 a,b,c,d,e,f,g,h,i 총 8개의 데이터 저장만으로 원래 데이터 복원이 가능한 것이죠.

 

l이 커지면 복잡한 모양의 원본 데이터에 가까워지지만, 레벨이 많아질수록 계산량이 많아지고 투자 연산 대비 효율이 떨어지기 때문에 일반적으로는 l=2,3 정도에서 타협을 본다고 합니다.

 

이게 어떻게 가능할까요?

추상화해 간단하게 얘기하면 SH가 정규직교의 성질을 띄기 때문에, 각자 다른 기저 간에 영향을 끼치지 않기 때문입니다. Orthonormal은 학부시절 선형대수와 미분기하학을 하면서 지긋지긋하게 다뤘던 것 같습니다. 구면 조화 함수에 관련된 자세한 내용은 수학적 지식으로 받아들이는 과정이 필요하기 때문에, 차근차근 살펴보시는 것을 권장드립니다.

 

여담으로, 이런 방식의 손실압축은 여러 곳에서 많이 쓰이고 있습니다. 

JPEG, 소리 파일 저장 등등...

 

엔진 둘러보기

유니티 URP

GI 계산 관련...

// GlobalIllumincation.hlsl

// Samples SH L0, L1 and L2 terms
half3 SampleSH(half3 normalWS)
{
    // LPPV is not supported in Ligthweight Pipeline
    real4 SHCoefficients[7];
    SHCoefficients[0] = unity_SHAr;
    SHCoefficients[1] = unity_SHAg;
    SHCoefficients[2] = unity_SHAb;
    SHCoefficients[3] = unity_SHBr;
    SHCoefficients[4] = unity_SHBg;
    SHCoefficients[5] = unity_SHBb;
    SHCoefficients[6] = unity_SHC;

    return max(half3(0, 0, 0), SampleSH9(SHCoefficients, normalWS));
}

// SH Vertex Evaluation. Depending on target SH sampling might be
// done completely per vertex or mixed with L2 term per vertex and L0, L1
// per pixel. See SampleSHPixel
half3 SampleSHVertex(half3 normalWS)
{
#if defined(EVALUATE_SH_VERTEX)
    return SampleSH(normalWS);
#elif defined(EVALUATE_SH_MIXED)
    // no max since this is only L2 contribution
    return SHEvalLinearL2(normalWS, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);
#endif

    // Fully per-pixel. Nothing to compute.
    return half3(0.0, 0.0, 0.0);
}

// SH Pixel Evaluation. Depending on target SH sampling might be done
// mixed or fully in pixel. See SampleSHVertex
half3 SampleSHPixel(half3 L2Term, half3 normalWS)
{
#if defined(EVALUATE_SH_VERTEX)
    return L2Term;
#elif defined(EVALUATE_SH_MIXED)
    half3 res = SHEvalLinearL0L1(normalWS, unity_SHAr, unity_SHAg, unity_SHAb);
#ifdef UNITY_COLORSPACE_GAMMA
    res = LinearToSRGB(res);
#endif
    return max(half3(0, 0, 0), res);
#endif

    // Default: Evaluate SH fully per-pixel
    return SampleSH(normalWS);
}

 

SRP 에서의 라이트 프로브 관련

// ProbeReferenceVolume.cs
		/// <summary>
        /// Update the capture location for the probe request.
        /// </summary>
        /// <param name ="requestID"> The request ID that has been given by the manager through a previous EnqueueRequest.</param>
        /// <param name ="newPositionnewPosition"> The position at which a probe is baked.</param>
        public int UpdatePositionForRequest(int requestID, Vector3 newPosition)
        {
            if (requestID >= 0 && requestID < m_RequestPositions.Count)
            {
                Debug.Assert(ComputeCapturePositionIsValid(newPosition));
                m_RequestPositions[requestID] = newPosition;
                m_SHCoefficients[requestID] = new SphericalHarmonicsL2();
                m_SHValidity[requestID] = kInvalidSH;
                return requestID;
            }
            else
            {
                return EnqueueRequest(newPosition);
            }
        }

 

언리얼 5

공간 음향 관련 SH 계산 코드

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "DSP/BufferVectorOperations.h"


/*
	Azimuth angle is measured CCW from front.
	Elevation is 0 horizontal plane, + is above horizontal plane
*/

class SOUNDFIELDRENDERING_API FSphericalHarmonicCalculator
{
public:
	enum AmbiChanNumber
	{
		/* 0th-Order */	ACN_0 = 0,
		/* 1st-Order */	ACN_1, ACN_2, ACN_3,
		/* 2nd-Order */	ACN_4, ACN_5, ACN_6, ACN_7, ACN_8,
		/* 3rd-Order */	ACN_9, ACN_10, ACN_11, ACN_12, ACN_13, ACN_14, ACN_15,
		/* 4th-Order */	ACN_16, ACN_17, ACN_18, ACN_19, ACN_20, ACN_21, ACN_22, ACN_23, ACN_24,
		/* 5th-Order */	ACN_25, ACN_26, ACN_27, ACN_28, ACN_29, ACN_30, ACN_31, ACN_32, ACN_33, ACN_34, ACN_35
	};

	static void ComputeSoundfieldChannelGains(const int32 Order, const float Azimuth, const float Elevation, float* OutGains);

	static void GenerateFirstOrderRotationMatrixGivenRadians(const float RotXRadians, const float RotYRadians, const float RotZRadians, FMatrix& OutMatrix);
	static void GenerateFirstOrderRotationMatrixGivenDegrees(const float RotXDegrees, const float RotYDegrees, const float RotZDegrees, FMatrix& OutMatrix);

	static void AdjustUESphericalCoordinatesForAmbisonics(FVector2D& InOutVector)
	{
		InOutVector.X = -(InOutVector.X - HALF_PI);
		InOutVector.Y *= -1.0f;
		FMemory::Memswap(&InOutVector.X,&InOutVector.Y, sizeof(InOutVector.X));
	}
};

 

이 외에는 Lightmass Smoothing, ScreenSpaceDenoiser, Gaussian 관련 코드에서 직접적으로 이름이 쓰이는 것을 볼 수 있었습니다. SphericalHarmonic으로 검색한거라, 더 자세히 보려면 렌더링 코드 다 따라가면서 SH가 있는지 봐야할 것 같아요.

 

 

마무리

굉장히 유익했습니다. UE5 엔진 코드에서 Spherical Harmonics를 검색해서 어떤 파트에서 사용되는지 추적을 해보고 있습니다. LumenScene및 Cards 계산, Meta Sounds에 사용되고 있는것을 어렴풋이 발견했습니다. 조금 더 디벨롭해서 다른 포스팅으로 정리할 예정입니다.

 

 

발표 자료는 직접 게시하지 않습니다.
셰이더 스터디 카페에서 확인 부탁드립니다.

 

레퍼런스