2026. 4. 25. 17:18ㆍ개인 공부 및 연구
개인 공부를 위한 번역임을 알립니다. 내용에 오역 또는 의역 포함 될 수 있고 검수 과정이 없기 때문에 문법, 맞춤법적 오류 또는 오타가 존재할 수 있으므로, 원문을 반드시 참고 하시길 바랍니다.
모든 저작권은 원문 저자에게 있으며, 저작권자의 요청 시 언제든지 비공개 또는 삭제 처리 될 수 있습니다.
원문: Introduction to Spherical Harmonics for Graphics Programmers
Introduction to Spherical Harmonics for Graphics Programmers
Introduction to Spherical Harmonics for Graphics Programmers published on Apr 12 2026 This post requires JavaScript to properly render mathematics (like $\int f(x) dx$, $Y_\ell^m$). It's not likely to work in your browser's "reader mode". On your computer
gpfault.net

여러분의 컴퓨터 그래픽스 여정에서 어느 시점에 논문이나 코드를 읽다 구면 조화 함수를 마주치게 되셨을 겁니다. 이들은 구면 위에서 정의된 특정한 함수를 겨우 몇 개의 계수만으로 근사 할 수 있도록 해주는 아주 유용한 도구입니다. 특히 복잡한 라이팅을 모델링하는데 유용하죠.
구면 조화는 많이 연구된 주제이긴 하지만, 이를 이해화는 과정은 여전히 조금 까다롭습니다. 하지만, 제 생각에는 실시간 컴퓨터 그래픽스 영역에 대한 구면 조화의 응용은 그렇게 어렵게 생각할 필요는 없다고 생각합니다.
이 아티클에서 저는 최대한 제가 구면 조화에 대해 이해한 방식으로 구면 조화를 설명해보고자 합니다. 이 아티클의 끝에서 독자 여러분이 구면 조화를 사용하는 더 고급 기술 주제들을 터득할 수 있는 기반을 얻어가실 수 있기를 바랍니다.
이 아티클은 독자가 실시간 렌더링, 선형 대수와 적분에 대한 기본적인 수준의 이해가 있다고 가정합니다, 그렇다고 아주 어려운 수준의 이해를 요구하는 것은 아닙니다. 여기서 엄밀한 증명이나 유도는 일절 다루지 않으며 간단한 기본 대수만을 다룰 것입니다.
왜 구면 조화에 대해 알아야 할까요?
시작하기에 앞서, 왜 우리가 컴퓨터 그래픽스 실무자로서 구면 조화를 알아야만 하는가에 대해서부터 적절한 설명이 필요할 거라고 생각합니다.
3D 공간상에서 특정 양/값과 관련된 어떤 함수는 단위 구 영역 위에서 정의된 함수로 생각할 수 있습니다. 실제로 "방향"은 단위 벡터로, 이 벡터의 끝 점은 항상 원점을 중심으로 한 단위 구면 상의 어떤 한 점입니다. 그렇기에 앞으로 "방향에 대한 함수(function of direction)", "구면에 대해 정의된 함수(function defined on a sphere)", 그리고 "단위 구면에 대해 정의된 함수(function defined on the surface of a unit sphere)"와 같은 용어들을 번갈아서 사용할 것이니 참고바랍니다.
흥미롭게도 그 어떤 구면에 대해 정의된 함수라도 몇 가지 특별한 다항식의 무한 가중합으로 표현할 수 있습니다. 사실 이런 다항식을 "구면 조화"라고 부르죠. 그럼 이런 합의 일부를 잘라내어서 유한하게 만든다면, 앞서 말한 함수를 근사 할 수 있다고 할 수 있을 것 입니다.
그럼 이 사실이 왜 중요한 걸까요? 그 이유는 컴퓨터 그래픽스에서는 항상 구의 영역에 대해 정의된 자명하지 않은 함수들이 다뤄지고 이런 함수들에 비해 다항식은 비교적 계산하기 간단하다는 것 입니다.
예를 들어, Radiance $ L_i(p, \vec{\omega_i}) $는 구면에 대해 정의된 함수입니다. 즉 이 함수는 점 $p$에 주어진 방향 $\vec{\omega_i}$로 부터 얼마나 많은 빛이 도달하는지를 알려줍니다. 큐브맵이 이런 종류의 함수를 "표로"만든 형태라고 생각할 수 있죠. 주어진 점에서의 irradiance(단위 면적 당 빛 에너지) 또한 방향에 대한 함수라고 생각 할 수 있습니다 (예시. "표면 노말이 특정 방향을 향할 때, 고정 점에서의 irradiance는 어느 정도 인가?").
잠재적으로 복잡한 라이팅 환경을 그저 다항식을 더해가는 것만으로도 근사할 수 있다는 사실만으로도 아주 매혹적인 제안으로 들립니다. 거기에 더해 라이팅뿐만이 아니라 구면 상에 정의된 그 어떤 다른 함수들 또한 표현할 수 있다는 사실은 이 제안을 더 매혹적으로 만듭니다.
예를 들어, 구면 조화는 각 표면 지점에서 주어진 방향으로의 메쉬나 볼륨의 두께와 같은 것을 근사하는데도 사용될 수 있습니다. 이 방식을 통해 근사화한 두께는 텍스처 아틀라스로 구워둘 수 있고, 서브서페이스 스케터링과 같은 현상을 시뮬레이션 하는데 사용 할 수 있습니다 (저는 이 아이디어를 좋은 친구이자 재능넘치는 그래픽스 프로그래머인 Bagrat Dabaghyan을 통해 처음 알게 되었습니다).
자, 앞서 구면 조화가 라이팅뿐 아니라 다른 곳에서도 사용 될 수 있다는 사실을 알아내긴 하였지만, 이 글에서는 라이팅에서 어떻게 사용되는지를 중점으로 살펴보도록 하겠습니다.
구면 조화 함수의 정의
자 이제, 구면 조화에 대해 학습하기 위해 충분히 동기부여가 되었고 앞으로 배울 내용에 대한 기대가 생기셨기를 바라며, 다음으로 몇 가지 정의를 알아보도록 합시다. 다시 한번 말씀드리지만, 이 글에선 수학적으로 아주 엄밀한 것을 다루기보다는 이 글을 읽는 독자 여러분이 구면 조화를 다루는 코드나 논문들을 읽고 이해할 수 있도록 하기 위해 필요한 최소한의 정보를 제공하는 것에 있습니다.
함수 공간과 기저
선형 대수에서부터 "선형" 또는 "벡터 공간" 그리고 "기저"라는 용어들에 익숙하실 것입니다. 구면 조화를 올바르게 정의하기 위한 단계로, 함수들의 세계에도 이와 유사한 구조가 필요합니다. Kevin Cassel의 비디오가 이 내용에 대해 잘 설명하고있습니다.
자 그 시작점으로, 특정 도메인에 대해 정의된 함수들의 집합을 "공간"으로 생각하도록 하겠습니다. 엄밀히 말하자면, 여기서 다룰 모든 정의는 우리가 정의할 함수들이 어떤 도메인에 대해서 정의되어 있는지 언급해야만 합니다 (위에서 링크된 비디오에선 그렇게하고 있다는 것을 알 수 있습니다). 하지만, 여기서 다룰 주제에선 도메인이 항상 단위 구이므로, 앞으로의 정의에선 도메인은 생략하도록 하겠습니다.
선형대수에서는 벡터들의 집합에서 같은 집합에 속하는 어떠한 벡터도 자신을 제외한 집합 내의 다른 벡터의 선형 결합으로 만들어 낼 수 없을 때, 벡터들의 집합 $\vec{v_0}, \vec{v_1}, ..., \vec{v_n}$가 "선형 독립"이라고 말합니다. 다르게 말하자면, 어떤 $\vec{v_i}$에 대해, $\vec{v_i}=a_0\vec{v_0}+...+a_n\vec{v_n}$를 만족하는 계수 $a_1,...,a_n$가 존재하지 않는다는 것을 의미합니다.
비슷한 방식으로 집합 내의 다른 함수들의 선형 결합으로 같은 집합 내의 특정 함수(자기 자신을 제외하고)를 표현할 수 없을 때, 함수들의 집합 $f_0(x), f_1(x), ... , f_n(x)$는 선형 독립이라고 부릅니다. 벡터들과 동일하게 주어진 어떤 $f_i$에 대해, $f_i(x) = a_0f_0(x) + ... + a_nf_n(x)$를 만족하는 계수 $a_0, a_1, ..., a_n$가 없을 때 선형 독립입니다.
선형 대수에는, 내적(inner/dot product)을 다음과 같이 정의합니다. 주어진 벡터 쌍 $\vec{p} = (p_0, p_1, ..., p_n)$와 $\vec{q} = (q_0, q_1, ..., q_n)$가 주어졌을 때, 내적 $\vec{p} \cdot \vec{q}$은 다음과 같이 정의된다.
$$\vec{p} \cdot \vec{q} = \sum_{i=0}^{n}p_iq_i$$
함수 $f$와 $g$에 대한 내적 $\langle f,g \rangle$은 아래와 같이 함수가 정의된 도메인에 걸쳐서 두 함수 간의 곱의 적분으로 정의됩니다.
$$\langle f, g \rangle = \int f(\omega)g(\omega) \,d\omega$$
직관적으로 함수를 무한한 개수의 차원에서의 벡터로 생각하고, 적분과 합산을 동일선상에 둔다면, 내적에 대한 두 표현 방식 사이의 유사성이 명확해집니다.
비슷한 방식으로, 함수의 norm 또한 함수 스스로에 대한 내적의 제곱근으로 정의할 수 있습니다.
$$ \left\| f \right\| = \sqrt{\langle f, f \rangle } $$
이로부터, "함수들의 정규직교 집합"이 무엇인지 명확해집니다. "함수들의 정규직교 집합"은 자기 자신에 대한 내적이 1이고 (정규 파트), 동시에 집합 내의 다른 함수와의 내적은 0 (직교 파트)인 함수들의 집합입니다.
선형 공간에 대해선, "정규직교 기저"는 정규직교이며, 선형 독립적인 벡터들의 집합을 의미하며. 공간 상의 어떤 벡터라도 이 기저에 속하는 벡터들의 선형 결합으로 유일하게 표현될 수 있습니다(역: 특정 벡터를 표현하기 위한 선형 결합 표현이 단 하나만 존재).
함수의 경우에도 비슷하게, 함수들의 공간에서는 "정규직교 기저"는 정규직교이며, 선형 독립인 함수들의 집합이며, 공간 내의 어떤 함수라도 기저 함수들의 선형 결합을 통해 유일하게 표현될 수 있음을 의미합니다.
여기서 함수 공간에 대해 짚고 넘어가야 할 중요한 사실이 있습니다. 그건 바로 함수 공간의 기저 집합이 무한하다는 사실이죠! 이 사실은 함수를 무한한 수의 차원에서의 벡터로 생각한다면 납득하실 수 있을것 입니다. 다시말해 N-차원 벡터 공간에선 항상 N개의 기저가 존재하므로, "무한 차원의 벡터 공간"이라면 무한한 개수의 기저를 가지고 있다는 사실도 당연하다 볼 수 있습니다.
구면 조화 함수
구면 조화 함수(또는 줄여서 "구면 조화)는 구면 위에 정의된 연속적인 함수들의 모든 공간에 대한 정규직교 기저를 형성하는 구면 위에 정의된 특별한 함수들의 무한 집합입니다.
여기서 잠깐 위 정의가 내포하고 있는 사실에 대해 생각해보고 넘어가보도록 하겠습니다. 위 내용은 대체 무엇을 의미하는 걸 까요? 이 정의가 말하고자 하는 건 구면 위에 정의된 연속적인 함수라면 실제 함수가 얼마나 복잡하고 계산하기 힘든 형태든 간에 구면 조화 함수의 무한 가중합으로 표현될 수 있다는 것입니다.
이는 구면 조화 함수들 스스로는 계산하기 쉽고 가중치나 계수들을 효과적으로 찾아낼 수 있는 방법이 있다고 가정 한다면 엄청나게 실용적인 도구가 될 수 있음을 나타냅니다.
그리고 앞서 말했듯이, 구면 조화 함수들은 다항식 이므로, 실제로 계산하기 간단합니다. 또한 이후에 다루겠지만 계수를 찾아내는 방법 또한 알려져있습니다.
좋습니다 일단 지금 까지는 그럴듯해 보이게 설명해드리긴 했지만, 실제론 무한한 개수의 계수를 컴퓨터에서 다룰 수는 없습니다. 현실적으로 한정된 수의 계수만을 저장할 수 있죠. 하지만 괜찮습니다. 우린 근사화하는 것에 큰 거부감을 가지고 있지 않으니까요. 그렇다고 한다면 얼마나 많은 계수를, 그리고 그중에서도 어떤 계수를 저장해야 하는 걸까요? 그리고 여기서 일부 계수를 저장하지 않음으로써 잃게 되는 정보는 대체 무엇일까요? 이에 대한 답은 다음 섹션에서 설명드리도록 하겠습니다.
구면 조화 Degree와 Order
여러분께 실제로 구면 조화 함수가 어떻게 생겼는지 보여드리기 전에, 함수들의 집합이 어떻게 구성되는지를 먼저 다루도록 하겠습니다.
SH 함수들은 "주파수 밴드"라고 불리는 수를 붙인 그룹들로 나뉩니다. 각 밴드는 자신과 대응되는 $\ell \in {0,1,2,...}$를 가집니다. 이 수는 밴드 내 함수의 "degree"라고 부릅니다. Degree가 $\ell$인 밴드는 $2 \ell + 1$개의 함수를 포함합니다. 관례로, Degree $ \ell$의 밴드 내 함수에는 $- \ell$ 에서 $ \ell$까지의 인덱스가 추가로 붙고, 이때 붙은 인덱스를 함수의 "order"라고 부릅니다.
메모: 일부 자료에서는 제가 이 글에서 "degree"라고 부르는 것을 "order"로 부르거나 "order"라 부르는 것은 "phase"라고 부르는 경우가 있다는 사실을 보았습니다. 여기선 해당 방식을 사용하지 않지만, 이렇게 부르는 경우가 있다는 사실을 인지하시고 추후에 혼란스럽게 느끼시지 않으시길 바라겠습니다!
Degree가 $ \ell$이고 order가 $m$인 구면 조화 함수는 일반적으로 $Y_{\ell}^m$라 표기합니다.
이제 밴드와 order에 따라 정렬된 다양한 구면 조화 함수의 시각화 자료를 보도록하겠습니다.

위 일러스트에서 낮은 degree의 구면 조화가 높은 degree 보다 더 큰 (또는, "낮은 주파수") 디테일을 포착해냄을 볼 수 있습니다.
여기서 앞서했던 질문을 떠올려 봅시다. 무한합을 잘라냄으로써 잃는 정보는 어떤 종류인가요? 일러스트에서 그 정보가 무엇인지 분명하게 드러납니다. 무한합을 잘라냄으로써 잃는 정보는 근사하고자 하는 함수 내의 작은 규모의 변화들. 즉, 디테일을 잃게됩니다.
만약 우리가 근사하고자 하는 함수가 "저주파수" 함수라면 (느리게 변화하는), 첫 몇 개의 밴드에 대한 계수들만 저장해도 충분 할 것 입니다. 그리고 실시간 컴퓨터 그래픽스에서의 실질적인 응용에서는 한 자릿수 degree를 넘어가는 밴드를 사용하지 않습니다. 오히려 대부분의 응용 사례에서 degree $\ell = 2$ 정도로 충분한 경우가 많습니다.
구면 조화 다항식 살펴보기
이때까지 구면 조화 함수에 대해서만 다뤄왔지만 실제로 이 함수들이 어떻게 생겼는지 보지 못하였습니다. 지금부터 이후 예시에서 사용되는 첫 3개 밴드에 속하는 구면 조화 함수의 다항식 형태를 살펴보겠습니다.
아래 코드가 바로 첫 3개 밴드에 속하는 구면 조화 함수의 다항식을 계산한 것입니다. 우리의 예제가 브라우저상에서 돌아갈 예정이기에 자바스크립트로 작성되어 있습니다(역: 실제 예제는 원문을 참고하시길 바랍니다!).
// 아래 상수들은 기저 함수를 좀 더 쉽게 작성 할 수 있도록 해줍니다.
const RECIP_PI = 1/Math.PI;
const C = [
Math.sqrt(RECIP_PI) * 0.5,
Math.sqrt(3 * RECIP_PI) * 0.5,
Math.sqrt(15 * RECIP_PI) * 0.5,
Math.sqrt(5 * RECIP_PI) * 0.25,
Math.sqrt(15 * RECIP_PI) * 0.25,
Math.sqrt(70 * RECIP_PI) * 0.125,
Math.sqrt(105 * RECIP_PI) * 0.5,
Math.sqrt(42 * RECIP_PI) * 0.125,
Math.sqrt(7 * RECIP_PI) * 0.25,
Math.sqrt(105 * RECIP_PI) * 0.25];
// Degree l=3 까지의 SH 기저 함수들
// SH 기저 함수 정의의 원본:
// "Stupid Spherical Harmonics Tricks", Peter-Pike Sloan, 2008
function y00(x,y,z) { return C[0]; }
function y_11(x,y,z) { return C[1] * y; }
function y01(x,y,z) { return C[1] * z; }
function y11(x,y,z) { return C[1] * x; }
function y_22(x,y,z) { return C[2] * y * x; }
function y_12(x,y,z) { return C[2] * y * z; }
function y02(x,y,z) { return C[3] * (3 * z * z - 1.0); }
function y12(x,y,z) { return C[2] * x * z; }
function y22(x,y,z) { return C[4] * (x*x - y*y); }
function y_33(x,y,z) { return C[5] * y * (3*x*x - y*y); }
function y_23(x,y,z) { return C[6] * z * (y*x); }
function y_13(x,y,z) { return C[7] * y * (5*z*z -1); }
function y03(x,y,z) { return C[8] * z * (5*z*z - 3); }
function y13(x,y,z) { return C[7] * x * (5 * z * z - 1); }
function y23(x,y,z) { return C[9] * z * (x*x - y*y); }
function y33(x,y,z) { return C[5] * x * (x*x - 3*y*y); }
// 주어진 방향 d에 대해 degree l 까지의 SH 기저 함수를 계산함, 계산 결과인
// 각 요소가 기저 함수의 값에 해당하는 Float32 배열을 반환함.
// l <= 3인 경우만 지원.
function evalSHBasis(d, l) {
const x = d[0];
const y = d[1];
const z = d[2];
switch(l) {
case 0: return new Float32Array([y00(x,y,z)]);
case 1: return new Float32Array([
y00(x,y,z), // l = 0
y_11(x,y,z), // l = 1
y01(x,y,z),
y11(x,y,z)]);
case 2: return new Float32Array([
y00(x,y,z), // l = 0
y_11(x,y,z), // l = 1
y01(x,y,z),
y11(x,y,z),
y_22(x,y,z), // l = 2
y_12(x,y,z),
y02(x,y,z),
y12(x,y,z),
y22(x,y,z)]);
default: return new Float32Array([
y00(x,y,z), // l = 0
y_11(x,y,z), // l = 1
y01(x,y,z),
y11(x,y,z),
y_22(x,y,z), // l = 2
y_12(x,y,z),
y02(x,y,z),
y12(x,y,z),
y22(x,y,z),
y_33(x,y,z), // l = 3
y_23(x,y,z),
y_13(x,y,z),
y03(x,y,z),
y13(x,y,z),
y23(x,y,z),
y33(x,y,z)]);
}
}
아마 실제로 구면 조화를 코드로 구현할 때는, 이런 기저 함수들을 어디선가 복사해서 사용하는 경우가 많을 것입니다. 이러는 과정에서 기저 함수를 잘못 가져오는 실수가 일어날 수 있습니다.(또는 원본이 애초에 잘못되어 있을 수도 있습니다). 실제로 저 또한 이번 포스트를 준비하면서 이런 실수를 저질렀습니다.
운이 좋게도, 여러분이 가져온 기저 함수들이 제대로된 기저 함수인지를 검증하는 것은 어렵지 않습니다. 구면 조화 기저의 근본적인 속성이 바로 정규직교여야 한다는 점을 떠올려보세요. 어떤 기저 함수라도 자기 자신과의 내적은 1이어야 하고, 자신을 제외한 다른 어떤 기저 함수와의 내적의 결과는 0이어야 합니다.
함수 간의 내적은 적분이지만 이는 간단한 몬테-카를로 구현만으로도 쉽게 수치적으로 계산할 수 있습니다.
아래 코드가 바로 제가 기저 함수들을 검증하기 위해 사용한 코드입니다.
// SH 계수들에 스칼라 값을 곱하기 위한 도우미 함수
function mulScalarBySHCoeffs(scalar, coeffs) {
return coeffs.map(function(c){return scalar*c;});
}
// 두 SH 계수들의 묶음을 더하기 위한 도우미 함수
function addSHCoeffs(coeffs0, coeffs1) {
return coeffs1.map(function(c,i){
return c + (coeffs0.length==0 ? 0 : coeffs0[i]);
});
}
// 기저 함수들이 정규 직교임을 검증하기 위해 간단한 몬테-카를로 적분을 사용
function testBasisFunctions() {
const numSamples = 100000;
const numBasisFuncs = 16; // we support l up to and including 3, so 16 funcs.
// innerProducts[i]는 i-번째 SH 기저 함수를 자기자신을 포함한,
// 다른 SH 기저 함수와 내적한 결과를 가지고 있음.
var innerProducts = [];
for (var b = 0; b < numBasisFuncs; ++b) {
// these arrays will be initialized to 0.
innerProducts.push(new Float32Array(numBasisFuncs));
}
for (var s = 0; s < numSamples;) {
// 구 상의 랜덤한 점을 생성하기 위해 간단한 기각 샘플링을 사용함:
// * [-1, -1, -1] - [1, 1, 1] 큐브 내에서의 점을 생성하고
// * 만약 점이 단위 원 내부에 있따면, 정규화하고 해당 점을 사용함;
// * 그렇지않다면, 다시 시도함.
var xr = Math.random()*2.0-1.0;
var yr = Math.random()*2.0-1.0;
var zr = Math.random()*2.0-1.0;
var n = Math.sqrt(xr*xr+yr*yr+zr*zr);
if (n>1) continue;
s++;
var d = new Float32Array([xr/n, yr/n, zr/n]);
// 생성된 샘플 지점에서의 SH 기저 함수를 계산하고
// 부분 내적을 계산한 다음 총합에 더해줌.
const basisFunctionValues = evalSHBasis(d, 3);
for (var b = 0; b < numBasisFuncs; ++b) {
const bv = basisFunctionValues[b];
const partialInnerProducts =
basisFunctionValues.map(function(v) { return v * bv; });
innerProducts[b] = addSHCoeffs(innerProducts[b], partialInnerProducts);
}
}
// 몬테-카를로 적분의 마지막 단계: 적분 영역의 크기를 곱해주고 (단위 원의 경우 4pi)
// 샘플의 수로 나눠줌.
for (var b = 0; b < numBasisFuncs; ++b) {
innerProducts[b] =
mulScalarBySHCoeffs(4*Math.PI/numSamples, innerProducts[b]);
}
// innerProducts[i][j]는 1에 아주 가까워야하며 j==i인 경우엔 0에 아주 가까워야한다.
console.log(innerProducts);
}
위에서 나열한 다항식 함수들은 간단해 보입니다. 심지어 구면 조화에 대한 실질적인 수학적 정의를 모르고도 사용할 수 있습니다. 하지만 여러분이 이 주제에 대해 더 심도있는 이해를 원하신다면, 이 함수들이 어떻게 생겼는지 파고들어야만 합니다. 그리고 다음 챕터를 구면 조화함수를 유도하는데 할애하여, 구면 조화를 더 깊게 파고들어가 보도록하겠습니다.
구면 조화 함수의 정의
항상 저에게 고통을 안겨주는건 앞서 본 간단한 코드 형태의 구면 조화와, 실제 구면 조화의 수학적 정의 사이의 차이를 메우는 일이였습니다.
만약 여러분이 "구면 조화 기저 함수"에 대해 찾아보거나, 그걸 위해 수학 교재를 들여다보게 된다면, 아마 degree $\ell$와 order $m$인 구면 조화 함수의 괴물같이 무섭게 생긴 정의를 마주하셨을 것 입니다.
$$Y_{\ell}^m(\theta, \varphi) =
(-1)^m
\sqrt{\frac{(2\ell + 1)}{4\pi} \frac{(\ell - m)!}{(\ell + m)!}}
P_{\ell}^m(\cos \theta)
e^{im\varphi}$$
제가 지금 당장 여러분께 말씀드릴 수 있는 사실은 위 형태의 구면 조화는 일반적으로 컴퓨터 그래픽스에서 사용되는 형태는 아니라는 것 입니다. 하지만, 위와 같은 형태는 우리가 사용할 형태와 동일한 기반을 전부 포함하고 있습니다. 그러므로, 위 수식을 조각내어, 하나 하나씩 개별적으로 살펴봄으로써 이 수식을 조금이라도 더 쉽게 이해하실 수 있도록 설명드리겠습니다.
$Y_{\ell}^m(\theta, \varphi)$에 대해 살펴보기에 앞서 알아야 할 사실은 이 함수가 구면 좌표계에 대해 정의되어 있다는 것입니다. 이때까지 저희가 다뤄왔던 함수들은 모두 구면 위에서 정의되었다고 하였으니 당연한 사실입니다. 그러니 시작하기 전에 좌표계 관례에 대해 명확하게 짚고 넘어가 보도록 하죠. 이걸 명확하게 해 두는 건 꽤 중요합니다. 다른 저자들은 이 표기를 조금 다르게 가정할 수도 있거든요. 예를 들어, 여러분이 코드를 복사해 온다면, 복사해온 코드가 어떤 관례를 따랐는지 그리고 사용하는 곳에서 어떤 관례를 따를 것 인지 반드시 고려해야 합니다.
아래 그림이 우리가 따를 구면 좌표계에 대한 관례입니다. 이 표기법은 Green의 구면 조화 논문과 동일합니다.

이 글에서는 각 $\theta$가 Z 축과 방향벡터 사이의 각으로 사용할 것이고, $\Phi$는 X축과 XY 평면에 대한 방향벡터의 투영 사이의 각으로 가정합니다. 즉, 구면에서 데카르트 좌표계로의 변환은 다음과 같이 표현됩니다.
$$x = \cos\phi\sin\theta \\ y=\sin\phi\sin\theta \\ z = \cos\theta$$
이제 좌표계에 대한 것은 정리되었으니, 다시 구면 조화 함수의 정의로 돌아가겠습니다. 첫 번째로 마주하는 건 귀찮게 생긴 $(-1)^m$ 인수입니다. 사실 이 친구에게는 "Condon-Shortley phase"라는 멋진 이름이 있지만, 사실 이 친구는 그냥 수식에서 지워도 아무런 문제가 없습니다. 실제로 대부분의 분야에서 이 인수를 그냥 무시하거나 정의에 포함시키지도 않습니다. 제가 컴퓨터 그래픽스 분야에서 경험한 바로는 대부분의 경우 무시되었고, 고작 하나의 코드 베이스에서만 포함되어 있었던 것으로 기억합니다. 그렇다면 왜 이 부분을 선택적으로 포함시키거나 무시할 수 있는 걸까요?
이 질문은 제가 처음 구면 조화를 마주쳤을 때 저를 괴롭혔던 질문이지만, 시간이 지나서 그럴듯한 답변을 찾을 수 있었습니다. 제가 직관적으로 이해한 바로 우리가 Condon-Shortley phase를 무시할 수 있는 이유는 바로 구면 조화들이 기저 함수라는 점입니다. 다시말해 구면 조화에 -1을 곱한다고 하더라도 구면 조화로 할 수 있는 일 자체가 변하지는 않는다는 것입니다.
다시 이 문제를 벡터로 생각해봅시다. 우리에게 친근한 벡터 $(1, 0, 0)$ $(0, 1, 0)$ $(0, 0, 1)$은 기저를 형성합니다, 하지만 벡터 $(-1, 0, 0)$, $(0, 1, 0)$ 그리고 $(0, 0, -1)$또한 기저를 형성할 수 있죠. 즉 우리가 벡터 중 일부의 부호를 뒤집은들 달라지는게 있는가요? 같은 논리가 구면 조화에 대해서도 적용됩니다. 그러니까, -1로 곱하는 것은 SH의 "기저성"이나 다른 속성들을 전혀 바꾸지 않는다는 것이죠. 함수 자기 자신에 대한 내적은 여전히 1이고, 다른 함수들과의 내적은 여전히 0일 것입니다. -1로 곱하는 것은 그저 주어진 함수를 표현하기 위한 계수에만 영향을 줍니다 (우리가 왼손 좌표계를 쓰냐 오른손 좌표계를 쓰냐에 따라 같은 점이라도 다른 좌표를 가지는 것과 비슷합니다). 물론 물리의 특정 응용 부분에선 Condon-Shortley phase가 실제로 중요합니다. 하지만 우리가 다음 내용을 다루는데에는 더 이상 필요로 하진 않습니다. 다만 이런 것이 있다는 사실을 기억하고 다음에 실전에서 이를 마주치더라도 놀라지 마세요.
자 다음으로 제곱근 아래에 있는 표현식이 어떻게 생겼는지 살펴봅시다.
$$\sqrt{\frac{(2\ell + 1)}{4\pi} \frac{(\ell - m)!}{(\ell + m)!}}$$
제곱근과 팩토리얼이 있어서 머리가 아파오지만, 사실 딱히 특별한 내용은 없습니다. 궁극적으로 이 친구들은 정규화를 위해 필요한 상수들일뿐이거든요 (SH 기저들이 정규직교라는 것을 명심하세요!). 즉 정규화를 위한 상수를 없앤다는 것은, 내적 $\langle Y_\ell^m, Y_\ell^m \rangle$의 결과 값이 더 이상 1이 아니게 된다는 것을 의미합니다.
다음으로 알아볼 것은 신비하게 생긴 $P_{\ell}^m(\cos \theta)$ 입니다.
이 녀석은 "연관 르장드르 다항식(associated Legendre polynomial)", 또는 "연관 르장드르 함수"라고 불립니다. 여기서 $m$은 기호일 뿐, 지수 연산이 아닙니다!
구면 조화 함수의 관점에서, 연관 르장드르 다항식은 극(pole)에서 극으로 갈 때 함수가 어떻게 변화하는지를 정의합니다 (극각(polar angle) $\theta$와 연관되어 있으므로).
일반적으로,
$$P_\ell^m(x) = (1-x^2)^{\frac{m}{2}}\frac{d^m}{dx^m}P_\ell(x) \\ P_\ell^m(x) = \frac{(\ell-m)!}{(\ell+m)!}P_\ell^{-m}(x) $$
여기서 $P_\ell(x)$는 르장드로 다항식 (연관 르장드르 다항식이 아닌), 이며 아래와 같은 수식을 가집니다.
$$P_\ell(x) = \frac{1}{2^\ell\ell!}\frac{d^\ell}{dx^\ell}[(x^2-1)^\ell]$$
몇몇 문헌에서는 Condon-Shortley phase를 연관 르장드르 다항식의 정의에 주입하는 경우도 있으나 여기선 그렇게 하지 않겠습니다.
연습의 일환으로, $P_1^1(x)$을 유도해 보도록 하겠습니다.
$$P_1(x) = \frac{1}{2}[\frac{d}{dx}(x^2-1)] = \frac{1}{2}(2x) = x \\ P_1^1(x) = \sqrt{(1-x^2)}\frac{d}{dx}P_1(x)=\sqrt{(1-x^2)}\frac{d}{dx}x=
\sqrt{(1-x^2)}$$
구면 조화에 사용하기 위해 $x$를 $\cos\theta$로 치환하면.
$$P_1^1(\cos\theta)=\sqrt{1-\cos^2\theta}=\sqrt{\sin^2\theta} = \sin\theta$$
지금 까지의 내용들을 충분히 이해하셨다면, 모든 연관 르장드르 함수를 일일이 유도할 필요는 없습니다. 아래의 연관 르장드로 리스트는 컴퓨터 그래픽스 응용에서 필요한 대부분의 경우를 커버해 줍니다.
$$\begin{align*}
& P_0^0(\cos \theta) = 1 \\[1ex]
& P_1^0(\cos \theta) = \cos \theta \\[1ex]
& P_1^1(\cos \theta) = \sin \theta \\[1ex]
& P_2^0(\cos \theta) = \frac{1}{2}(3\cos^2 \theta - 1) \\[1ex]
& P_2^1(\cos \theta) = 3\cos \theta \sin \theta \\[1ex]
& P_2^2(\cos \theta) = 3\sin^2 \theta \\[1ex]
& P_3^0(\cos \theta) = \frac{1}{2}(5\cos^3 \theta - 3\cos \theta) \\[1ex]
& P_3^1(\cos \theta) = \frac{3}{2}(5\cos^2 \theta - 1) \sin \theta \\[1ex]
& P_3^2(\cos \theta) = 15\cos \theta \sin^2 \theta \\[1ex]
& P_3^3(\cos \theta) = 15\sin^3 \theta \\[1ex]
& P_4^0(\cos \theta) = \frac{1}{8}(35\cos^4 \theta - 30\cos^2 \theta + 3) \\[1ex]
& P_4^1(\cos \theta) = \frac{5}{2}(7\cos^3 \theta - 3\cos \theta) \sin \theta \\[1ex]
& P_4^2(\cos \theta) = \frac{15}{2}(7\cos^2 \theta - 1) \sin^2 \theta \\[1ex]
& P_4^3(\cos \theta) = 105\cos \theta \sin^3 \theta \\[1ex]
& P_4^4(\cos \theta) = 105\sin^4 \theta
\end{align*}$$
다음으로 넘어가기에 앞서 몇 가지 짚고 넘어가야 할 점이 있습니다.
첫 번째, 여기서 등장하는 삼각 함수의 존재로 혼란스러워하지 마세요. 한번 구면 좌표예서 데카르트 좌표로 가고 나면 연관 르장드르는 그저 다항 함수일 뿐입니다.
두 번째, 이 공식들의 출처는 관련 위키백과 글들입니다, 원래 여기엔 Condon-Shortley phase가 포함되어 있지만. 여기선 제가 임의로 제거하였습니다.
세 번째, 이 리스트에서는 양수 $m$에 대한 값들만 포함하고 있는데. 그 이유는 좀 더 뒤에서 설명해드리도록 하겠습니다.
마지막으로, 아마 어렴풋이 느끼실지도 모르겠지만 우리가 위에서 다룬 "정의"가 모든 것을 설명해주지는 않습니다. 위 정의는 구면 조화의 진짜 심장이라 할 수 있는 왜 이런 함수들이 생겨나게 되는지에 대한 수학적 필요성을 담고 있지 않기 때문입니다.
실제로 연관 르장드르 함수의 경우엔 연관 르장드르 방정식이라 불리는 미분 방정식의 해입니다. 아쉽지만, 이 방정식에 대해 자세히 살펴보고, 해결하는 것은 이 포스트의 범위를 크게 벗어납니다. 또한 사실 이 부분은 컴퓨터 그래픽스에서 구면 조화를 사용하는 데 있어서 반드시 알아야 할 부분은 아닙니다.
그래도 이 주제에 대해 여전히 관심이 있으시다면 아래 비디오 시리즈를 시청하는 것을 추천드립니다.
- 르장드르 미분 방정식과 르장드르 다항식
- 닫힌 형태의 르장드르 다항식
- 르장드르 다항식에 대한 로드리게스 수식 (위에서 우리가 $P_\ell$을 정의하는 데 사용한 것)
- 연관 르장드르 미분 방정식
자 그럼 다시 우리의 관심을 $e^{im\phi}$로 돌려봅시다. 이 부분은 방위각(azimuthal angle) $\Phi$가 바뀔 때 SH 함수가 어떻게 행동하는지를 정의합니다. 여러분이 만약 그 유명한 오일러 공식을 알고 계신다면, 이 부분이 $i^2 = -1$일 때, $\cos(m\phi) + i\sin(m\phi)$와 동등하다는 사실을 눈치 채셨을것 입니다.
바로 이 부분이 컴퓨터 그래픽스 분야에서 살짝 다른 형태의 구면 조화를 사용하도록하는 주요 원인입니다. 지금 우리가 보고 있는 정의는 사실 복소 구면 조화에 대한 정의입니다. 즉 복소수를 반환하는 함수를 근사하는 데 사용할 수 있는 녀석이죠. 그렇기에 물리 분야에서 아주 유용하게 사용됩니다.
하지만 컴퓨터 그래픽스에서는 일반적으로 복소함수를 잘 다루지 않습니다. 당연히, 허수 파트를 항상 0읜 복소 함수 버전을 실함수로 취급해도 되긴 하겠지만 사용하기 간편하진 않을 것입니다.
다행히도, 더 간단한 실함수 정의가 존재합니다. 더 나아가, 복소 구면 조화는 실수 버전의 구면 조화의 관점에서 표현될 수 있고 그 반대 또한 가능합니다.
실제 정의, 이번엔 정말로요 (The Actual Definition, for Real This Time)
끝내주고, 간결하며 액기스만 뽑은 실수 버전의 구면 조화 함수는 Robin Green의 논문에서 발췌한 아래 내용에서 찾아볼 수 있습니다.
... SH 함수는 일반적으로 심벌 $y$로 표기한다.
$$\begin{equation}
y_l^m(\theta, \phi) =
\begin{cases}
\sqrt{2} K_l^m \cos(m\phi) P_l^m(\cos \theta) & \text{if } m > 0, \\
K_l^0 P_l^0(\cos \theta) & \text{if } m = 0, \\
\sqrt{2} K_l^m \sin(-m\phi) P_l^{-m}(\cos \theta) & \text{if } m < 0.
\end{cases}
\end{equation}$$
이때 $P_l^m$는 앞서 살펴본 연관 르장드르 다항식과 동일하며 $K_l^m$는 그저 함수를 정규화하기 위한 스케일링 인수이다.
$$\begin{equation}
K_l^m = \sqrt{\frac{(2l + 1)}{4\pi} \frac{(l - |m|)!}{(l + |m|)!}},
\end{equation}$$
컴퓨터 그래픽스 논문에서 "구면 조화"가 언급되면 99.999% 확률로 위 내용을 의미하는 것입니다.
이 정의는 이전 섹션에서 다루었던 정의와 정확히 모든 기반을 재사용하였기에, 여기서 보는 내용들이 이미 친숙하게 느껴지실지도 모르겠습니다. 그래도 여전히 몇 가지 살펴보아야 할 디테일이 있습니다.
첫 번째, 정규화 상수가 다릅니다. 위 버전의 정규화 상수는 order $m$의 절댓값을 사용하고 있습니다. 이는 $m$을 그대로 정규화 상수에 사용하던 복소수 버전의 정의와는 대조적입니다!
두 번째로, $m < 0$일 때 연관 르장드르 다항식과 사인 안의 $m$의 부호를 뒤집고 있습니다. 이것이 바로 앞서 보았던 연관 르장드르 다항식의 리스트에서 양수인 $m$만이 포함되어있었던 이유입니다. 바로, 음수 $m$들은 저희에게 필요 없었던 것이죠.
기저 함수의 전개
이제 몇몇 실수 구면 조화의 기저 함수를 유도해 보도록 하겠습니다.
$y_0^0$에서 부터 시작해봅시다. 위에서 본 정의에 따라, $y_0^0(\theta, \phi) = K_0^0P_0^0(\cos\theta)$.
앞서 언급한 연관 르장드르 표에 따라 $P_0^0(\cos\theta)=1$ 이므로, $K^0_0=\sqrt{\frac{1}{4\pi}}$ 임은 자명합니다. 즉, $y_0^0(\theta, \phi) = \sqrt{\frac{1}{4\pi}}$으로 단순한 상수입니다 (0번째 degree의 다항식이라고도 부릅니다).
그렇다면 $y_{1}^{-1}$는 어떨까요? 다시 한번 위에서 언급한 정의에 따라, $y_{1}^{-1}(\theta, \phi) = \sqrt{2}K_{1}^{-1}\sin\phi P_1^1(\cos\theta)$ 이고, $K_1^{-1}=\sqrt{\frac{3}{4\pi}\frac{1}{2}}$ 이며 $P_1^1(\cos\theta)=\sin\theta$ 이므로.
2의 제곱근은 상쇄되어, $y_{1}^{-1}(\theta, \phi) = \sqrt{\frac{3}{4\pi}}\sin\phi\sin\theta$를 얻게 됩니다.
구면 좌표계에서 데카르트 좌표계로 변환하기 위해 (앞서 합의한 좌표계 관례에 따라), $y=\sin\phi\sin\theta$, 이므로 데카르트 좌표계에서의 최종 결과는 단순히 $y_{1}^{-1}(x,y,z) = \sqrt{\frac{3}{4\pi}}y$로 맨 처음 보았던 공식에 비하면 훨씬 간단해졌습니다! 한 가지, 주의해야 할 점은 좌표계 시스템의 변화에 맞춰서 함수의 파라미터가 $\theta, \phi$에서 $x,y,z$로 변했다는 것입니다.
동일한 방식으로, $y_1^0(x,y,z) = \sqrt{\frac{3}{4\pi}}z$ 이고 $y_1^1(x,y,z) = \sqrt{\frac{3}{4\pi}}x$이라는 사실도 알 수 있습니다.
마지막 예시로, 약간 헷갈릴 수도 있는 $y_2^2$를 유도해 보겠습니다. 연관 르장드르 다항식을 전개하고 간략화하면 다음을 얻습니다.
$$y_2^2(\theta, \phi) =
\sqrt{2}\sqrt{\frac{4+1}{\pi}
\frac{(2-|2|)!}{(2+|2|)!}}\cos{2\phi}P_2^2(\cos\theta) = \sqrt{\frac{5}{48\pi}}\cos{2\phi}P_2^2(cos\theta)= \sqrt{\frac{5}{48\pi}}\cos{2\phi}3\sin^2\theta$$
여기서 성가시게 생긴 3을 제곱근 안으로 넣으면 수식을 더욱 간략화하는 것이 가능합니다.
$$\sqrt{\frac{5}{48\pi}}\cos{2\phi}3\sin^2\theta =
\sqrt{\frac{5*9}{48\pi}}\cos{2\phi}\sin^2\theta =
\sqrt{\frac{5*3}{16\pi}}\cos{2\phi}\sin^2\theta =
\frac{1}{4}\sqrt{\frac{15}{\pi}}\cos{2\phi}\sin^2\theta$$
삼각법에 따라 $\cos{2\phi}=(2cos^2\phi - 1)$이며 $\sin^2\theta+\cos^2\theta=1$라는 사실이 알려져있으므로,
$$\frac{1}{4}\sqrt{\frac{15}{\pi}}\cos{2\phi}\sin^2\theta =
\frac{1}{4}\sqrt{\frac{15}{\pi}}(2cos^2\phi - 1)\sin^2\theta =
\frac{1}{4}\sqrt{\frac{15}{\pi}}(2cos^2\phi\sin^2\theta - \sin^2\theta) =
\frac{1}{4}\sqrt{\frac{15}{\pi}}(2cos^2\phi\sin^2\theta - 1 + \cos^2\theta)$$
이제 유도한 수식을 데카르트 좌표계로 변환해서 다시 쓰면,
$$\frac{1}{4}\sqrt{\frac{15}{\pi}}(2cos^2\phi\sin^2\theta - 1 + \cos^2\theta) =
\frac{1}{4}\sqrt{\frac{15}{\pi}}(2x^2 + z^2 - 1)$$
단위 원상에선 $x^2+y^2+z^2 = 1$이라는 사실을 사용하면 결과를 조금더 깔끔하게 만들 수 있습니다.
$$\frac{1}{4}\sqrt{\frac{15}{\pi}}(2x^2 + z^2 - x^2 -y^2 -z^2) =
\frac{1}{4}\sqrt{\frac{15}{\pi}}(x^2 - y^2)$$
최종적으로,
$$y_2^2(x,y,z) = \frac{1}{4}\sqrt{\frac{15}{\pi}}(x^2 - y^2)$$
이런 연습을 통해서 어떻게 정교해 보이는 수학적 정의에서 앞서 선보였던 간단한 형태의 다항식을 얻게되는지 보여줍니다. 당연히 여러분이 이 모든 걸 일일이 전부 유도할 필요는 없습니다. 왜냐하면 이미 구면 조화 함수 테이블이 이미 알려져있기 때문입니다. 이는 Peter-Pike Sloan의 Stupid Spherical Harmonics Tricks 논문의 부록 A2를 참고하실 것을 추천드립니다. 이 부록엔 degree 5까지의 데카르트 좌표계 상의 SH 기저 함수들이 포함되어 있으며, 제가 아는 한 어떤 오타(오류)도 없는 것으로 알고 있습니다.
구면 조화 계수 얻어내기
이 시점에서 여러분이 구면 조화가 무엇인지에 대한 대략적인 이해를 얻으셨을 것이라 생각합니다. 그러므로 아래와 같이 어떤 조각적으로 연속적인 실함수(piecewise-continuous real-valued function) $f(\theta, \phi)$ 또한 무한합으로 표현될 수 있다는 사실도 이해하실 수 있을 것입니다.
$$f(\theta, \phi) =
c_{0}^{0}y_{0}^{0}(\theta,\phi) +
c_{1}^{-1}y_{1}^{-1}(\theta,\phi) +
c_{1}^{0}y_{1}^{0}(\theta,\phi) +
c_{1}^{1}y_{1}^{1}(\theta,\phi) + ...$$
이때 $y_{\ell}^{m}$는 실구면조화 함수이고, $c_{\ell}^{m}$는 각자의 계수입니다. 여기서 여전히 풀리지 않는 문제는, 대체 주어진 $f(\theta,\phi)$에 대한 계수 $c_{\ell}{^m}$들을 어떻게 찾아낼 것 인가? 입니다. 다르게 말해서, 어떻게 이미지의 큐브맵 표현과 같은 것들을, 이를 근사하는 SH 계수 집합으로 변환하는 걸까요?
이 질문에 대한 답은 다시 벡터/선형 공간에 대한 비유를 돌아볼 필요가 있습니다. 먼저 임의의 3D 벡터 $\vec{v}$와 임의의 기저 $\vec{i}, \vec{j}, \vec{k}$가 주어졌다고 가정해봅시다. 이때 $\vec{i}, \vec{j}, \vec{k}$에 의해 정의되는 틀을 기준으로했을때의 $\vec{v}$의 좌표는 어떻게 계산 할 수 있을까요?
이에 대한 답은 단순하게도 내적 $\vec{v} \cdot \vec{i}, \vec{v} \cdot \vec{j}, \vec{v} \cdot \vec{k}$를 계산하는 것입니다. 각각의 결과는 $\vec{v}$를 $\vec{i}, \vec{j}$ 그리고 $\vec{k}$에 대해 투영했을 때의 길이를 나타내지요. 벡터를 행렬을 사용해서 한 좌표계 공간에서 다른 좌표계 공간으로 변환하는 게 실제론 이런 내적을 계산하는 것과 다를 바 없다는 사실을 기억하세요.
함수에 대해서도 이런 비유를 족용하면, 계수 $c_{\ell}^m$를 계산하기 위해선 아래와 같이 내적을 계산해야 합니다.
$$\langle f, y_{\ell}^m\rangle = \int f(\vec{\omega})y_{\ell}^m(\vec{\omega})d\vec{\omega}$$
이 적분은 보통 수치적으로 계산되고, 다음 섹션에서 어떻게 이런 것이 가능한지 시연해 보도록 하겠습니다.
케이스 스터디: 큐브맵을 구면 조화 기저로 투영하기
자 이제 실용적인 예시로써 큐브맵에 의해서 정의된 radiance를 구면 조화를 통해 근사하는 방법에 대해 알아봅시다. 여기선 작은 큐브맵을 가져와서 큐브맵에 대한 구면 조화 계수를 얻어내서, 최종적으로 이 계수를 계산해 냈을 때 큐브맵의 내용이 근사되도록 할 것입니다.
큐브맵 투영을 위한 SH 계수 $c_{j}^{i}$를 찾기 위해서는, $T$가 큐브맵에 대해 정의된 radiance 함수이고, $y_{\ell}^{m}$가 SH 기저 함수일 때, $\int T(\vec{\omega})y_{\ell}^{m}(\vec{\omega}) d\vec{\omega}$를 계산해 내야 합니다.
이 수식을 어떻게 풀어야 할까요? 이 질문에 답 이전에 단일 변수 실함수의 경우를 먼저 살펴보도록 하겠습니다.
먼저 아래와 같이 생긴 임의의 함수가 있을 때 이 함수를 적분하기 위한 방법에는 무엇이 있을까요?

만약 주어진 함수가 닫힌 형태라면, 미적분의 기본 이론을 사용하면 됩니다, 하지만 radiance 함수는 닫힌 형태로 주어지지 않기 때문에 이 방법은 사용할 수 없습니다.
사실 우리가 처한 상황은 더 최악이라 할 수 있습니다. 큐브맵은 원본 함수에 대한 아주 제한적인 양의 정보만 가지고 있습니다. 실제로 저희가 가지고 있는 정보는 균등한 간격의 그리드 위이 위치한 일정 수의 샘플(텍셀)들뿐 이죠. 만약 같은 방식으로 단일 변수 함수가 주어졌다면 아래와 같이 생겼을것 입니다.

그럼 이런 함수는 어떻게 적분해야 할까요? 음.. 몬테 카를로를 채택하고 샘플 간의 값들을 재구성하기 위해서 nearest-neighbor이나 선형 필터를 적용해야 할까요? 하지만, 지금 상황에서 이 방식은 최적의 방법이 아닙니다. 여러분이 해석학을 잘 알고계신다면 이미지를 보고 단번에 올바른 답을 고르셨을지도 모르겠네요. 이 질문에 대한 답은 바로 리만합을 사용한다 입니다.
리만합은 기초 해석학 수업에서 들어보셨을 것입니다. 이 경우 여러분은 리만합을 단일 변수 함수에 대해서만 적용해 보셨겠지만, 사실 다음과 같은 방식으로 더 높은 차원의 함수에 대해 일반화하는 것이 가능합니다.
- 적분 구간을 겹치지 않는 섹션으로 나눕니다 (단일 변수 함수라면 이 섹션은 1D 구간(interval)이 됩니다).
- 각 섹션에 대해
- 섹션 내에 아무 지점을 뽑아서 그 점에서의 함수를 계산합니다.
- 위에서 계산한 결과 값에 섹션의 "크기"를 곱해줍니다 (단일 변수 함수의 경우에 이건 구간의 길이가 됩니다. 만약에 2D 영역에 대해 정의된 함수라면 "크기"가 "너비"가 되겠지요)
- 각 섹션의 결과 값들을 다 더한 값이 최종적으로 적분값의 근사치가 됩니다.
단일 변수 함수의 경우엔 꽤 간단해 보이지만, 저희가 해결해야할 문제는 이보다 더 복잡합니다. 왜냐면 적분 구간이 '구'이기 때문이죠. 먼저 각 큐브맵 텍셀을 구로 투영시키고 이 적분 구간을 각 섹션으로 나눕니다. 이때 투영된 표면 패치가 각자 "섹션"을 가지게 됩니다.
하지만 1D의 경우와는 다르게 이 패치들의 넓이(area)를 계산하는 것은 완전히 자명하지는 않습니다. 이 넓이들은 상응하는 큐브맵 텍셀들에 대면하는 입체각에 비례하고, 이 값은 패치에 따라 달라집니다. 아래 다이어그램이 바로 이 문제를 시각적으로 나타냅니다.

한번 구가 큐브맵에 내접한다고 생각해 봅시다, 그러면 큐브맵 텍셀들이 구의 표면 위에 투영되기 시작할 것입니다. 이때 위 사진에서 파란색과 초록색 텍셀이 큐브맵에서는 같은 크기를 가지지만, 구에 투영했을 때의 넓이(영역)는 달라진다는 사실을 보여줍니다.
이런 투영들의 넓이를 계산하는 간단한 방법이 존재하지만 Rory Driscoll의 포스트에서 잘 설명되어 있기에, 이 글에선 더 자세히 설명을 포함시키지 않겠습니다.
더 나아가 Peter-Pike Sloan의 Stupid Spherical Harmonic Tricks 논문에서 사용된 이 넓이를 추측하기 위한 더 간단한 방법이 존재합니다. 하지만 앞서 소개드린 포스트에서 설명하는 방법보다는 정확도는 떨어집니다.
자, 위에서 다룬 내용을 모두 사용해서 만든 공략법은 다음과 같습니다.
- 큐브맵이 (-1, -1, -,1) - (1, 1, 1) 큐브에 딱 들어맞는다고 가정
- 적분 영역이 단위 구이고 앞서 가정한 큐브에 내접한다 가정
- 큐브맵 면의 각 텍셀에 대해
- 텍셀 $T(p)$의 값을 가져온다
- 텍셀의 중심 $p$를 구의 표면으로 투영하고, 이걸 점 $p'$라고 한다
- 투영된 점 $p'$를 사용해서 구면 조화 기저 함수를 계산한다: $y_0^0(p'), ... y_{\ell}^\ell(p')$
- 투영된 패치 $S_{p'}$의 넓이를 계산한다
- 각 곱셈 $t_0^0 = T(p)y_0^0(p')S_{p'}, ...,
t_\ell^\ell = T(p)y_\ell^\ell(p')S_{p'}$를 계산하고 상응하는 누적 합계 $c_0^0,...,c_\ell^\ell$에 더해준다.
- $c_0^0,...,c_\ell^\ell$이 우리가 찾고자 했던 SH 계수가 된다.
이 방법을 자바스크립트로 구현하면 다음과 같습니다.
const CUBE_DIM = 128;
const TEXEL_SIZE = 1.0 / CUBE_DIM; // texel size in UV space.
const HALF_TEXEL_SIZE = 0.5 * TEXEL_SIZE;
// 전달 받은 큐브맵을 maxDegree 까지의 degree를 가지는 구면 조화에 투영함
function projectCubemapToSH(cube, maxDegree, gammaCorrection = false) {
// 텍셀에 대응되는 입체각을 계산
// 유도:
// https://www.rorydriscoll.com/2012/01/15/cubemap-texel-solid-angle/
function calcTexelWeight(x,y) {
const x0 = x - TEXEL_SIZE;
const y0 = y - TEXEL_SIZE;
const x1 = x + TEXEL_SIZE;
const y1 = y + TEXEL_SIZE;
function areaElement(a, b) {
return Math.atan2(a*b, Math.sqrt(a*a + b*b + 1));
}
return areaElement(x0, y0) - areaElement(x0, y1) - areaElement(x1, y0)
+ areaElement(x1, y1);
}
// 빨간색, 초록색 그리고 파란색 채널 각각을 근사해야할 함수로 봄.
// 그렇게 함으로써, 출력은 빨간색, 초록색 그리고 파란색 채널 각각에 대응하는 총 3개의
// SH 계수 집합이 될 것임.
var shRgb = [
[],[],[]
];
for(var face = 0; face < 6; ++face) {
for (var col = 0; col < CUBE_DIM; ++col) {
for (var row = 0; row < CUBE_DIM; ++row) {
const texelColor = cube[face].data.slice(
4*(col + row * CUBE_DIM),
4*(col + row * CUBE_DIM) + 4);
// UV (0,0)은 이미지의 좌측 상단 모서리
// UV (1,1)은 이미지의 우측 하단
const faceCoords = cubemapFaceCoords(col, row);
const d = cubemapCoordsToDirection(face, faceCoords);
const texelWeight = calcTexelWeight(faceCoords[0], faceCoords[1]);
const sh = evalSHBasis(d, maxDegree, 1);
const texelColorf_raw = new Float32Array([
texelColor[0]/255.0,
texelColor[1]/255.0,
texelColor[2]/255.0])
const texelColorf =
gammaCorrection ? srgbToLinear(texelColorf_raw) : texelColorf_raw;
const shRed = mulScalarBySHCoeffs(texelWeight * texelColorf[0], sh);
const shGreen = mulScalarBySHCoeffs(texelWeight * texelColorf[1], sh);
const shBlue = mulScalarBySHCoeffs(texelWeight * texelColorf[2], sh);
shRgb[0] = addSHCoeffs(shRgb[0], shRed);
shRgb[1] = addSHCoeffs(shRgb[1], shGreen);
shRgb[2] = addSHCoeffs(shRgb[2], shBlue);
}
}
}
return shRgb;
}
여기서 몇 가지를 살펴보겠습니다. 첫 번째로 'calcTexelWeight' 함수를 살펴봅시다. 앞서드린 설명처럼 이 함수는 큐브맵 텍셀에 대응하는 입체각을 계산합니다. 이 함수를 올바르게 정의하는 것이 적분이 제대로 이루어지도록 하기 위해 가장 중요한 지점입니다. 충고드리자면 왜 이 가중치 함수가 사용되는지에 대한 설명하는 글을 꼭 읽어보시길 바랍니다. 저 또한 이 모든 가중치들의 합이 $4\phi$에 아주 가깝다는 것을 직접 검증해보았습니다.
두 번째로 흥미로운 점은 "감마 보정" 파라미터입니다. 일반적으로 큐브맵을 SH로 투영하게 되는 경우, 실제 radiance 값들을 (보통 렌더링 엔진에서 생성된) 저장하는 HDR 큐브맵을 다루게 될 것입니다. 그러나, 이 예시에선 모든 큐브맵 데이터가 sRGB 인코딩을 사용하는 SDR 이미지로 저장되어 있기에, 코드에 큐브맵에 감마 보정을 적용하기 위한 옵션을 추가하였습니다. 여기선 일반적으로 감마 보정에 사용되는 pow(value, 2.2)를 사용하였으며, 왜 이 방식을 사용했는지에 대해서는 이 글에서 특별히 다루지는 않겠습니다.
마지막으로, 'cubemapFaceCoords'와 'cubemapCoordsToDirection'함수는 특정 큐브맵 텍셀의 중심을 가리키는 단위 벡터를 계산하기 위해 존재합니다. 이 함수들을 제대로 작성하는 것은 중요하긴 하지만, 여기서 일일이 설명할 정도로 흥미롭지는 않습니다. 그렇기에 만약 궁금한 점이 있으시다면 제공되는 샘플 소스 코드를 참고해주시길 바랍니다.
이제 함수의 구면 조화 표면을 계산하기 위해 필요한 마지막 조각을 살펴보도록 하겠습니다. 구면 조화 계수들이 주어졌을 때, 어떻게 하면 근사하고자 하는 함수의 근삿값을 얻어낼 수 있을까요? 사실 꽤 간단합니다.
// 주어진 방향에 대한 l-band 기저에 투영된 함수의 SH 표현을 계산
function evalSHRepresentation(coeffs, d, l) {
// 주어진 방향에 대한 각 기저 함수를 계산함
const basis = evalSHBasis(d, l);
// 주어진 방향에서의 기저 함수들의 값을 각각의 SH 계수와 곱함
const basis_coefs = coeffs.map(function(c,i) { return c * basis[i]; });
// 모든 결과를 더해줌.
return basis_coefs.reduce(function(a, v) { return a+v; }, 0.0);
}
샘플은 SH 표현을 최종적으로 사용자가 보게될 큐브맵의 형태로 되돌리는데 이 과정을 사용합니다.
(역: 샘플은 해당 부분의 원문을 참고하시길 바랍니다!)
이 샘플을 자유롭게 만져보시다 보면 "Deringing 적용"이라 불리는 묘한 옵션이 있다는 것을 발견하실 수 있으실 겁니다. 이 옵션에 대해서는 다음 섹션 전체에 걸쳐 설명드리도록 하겠습니다.
구면 조화의 아티팩트
지금까지는 어떤 임의의 함수든 간에 유한한 SH 기저에 투영하기만 하면, 그 함수에 대한 실사용하기에 충분한 근삿값을 얻을 수 있는 것처럼 다뤘었습니다. 하지만 불행하게도 모든 일이 쉽게 풀리지만은 않습니다.
요코하마 큐브맵을 $\ell = 1$으로 근사화하고 감마 보정을 활성화한 결과물을 보면, 근사된 버전의 하단에 이상한 "구멍"이 있는 것을 보실 수 있을 것입니다.

음수 값을 하이라이트처리 해보면 우리가 만든 SH 표현이 많은 부분에 0보다 작은 값을 계산해내고 있다는 흥미로운 사실이 밝혀지게 됩니다.

감마 보정은 이 현상을 더 심하게 만들긴 하지만, 감마 보정이 이러한 아티팩트의 직접적인 원인이 되지는 않습니다. 예를 들어, 아래 예시 이미지는 금문교 큐브맵을 $\ell = 3$에 대해 근사화하며 감마 보정을 적용하지 않은 결과물입니다.

대체 무슨 일이 일어나고 있는 걸까요? 위에서 사용한 큐브맵 원본에는 음수값들이 있지도 않은데, 왜 근사치에서는 음수값들이 나타나는 것일까요? 그리고 왜 감마 보정은 이 현상을 더 나빠보이게 만드는 걸까요?
앞에서 다루었던 내용을 다시 떠올려 봅시다. 구면에 대해 정의된 어떤 연속적인 실함수 $f(\theta, \phi)$라도 다음과 같은 '무한합'으로 표현될 수 있다.
$f(\theta, \phi) =
c_{0}^{0}y_{0}^{0}(\theta,\phi) +
c_{1}^{-1}y_{1}^{-1}(\theta,\phi) +
c_{1}^{0}y_{1}^{0}(\theta,\phi) +
c_{1}^{1}y_{1}^{1}(\theta,\phi) + ...$
이는 수학적으로 증명된 사실입니다. 하지만, 실전에서 우리가 구면 조화를 사용할 때는 두 가지 중요한 조건을 간과합니다. 첫 번째로 무한한 기저를 사용하는 대신 유한한 기저의 부분 집합을 사용한다는 것. 두 번째로는 불연속적인 함수를 투영하고자 한다는 것(큐브맵처럼).
수학은 이런 위반사항에 관대하지 않습니다. 결과적으로 수학은 이 조건을 어긴 우리에게 "ringing" 또는 "Gibbs 현상"이라고 부르는 끔찍한 아티팩트로 복수하죠. 이런 일이 발생하면 (일반적으로 입력 함수가 갑작스럽게 변화하거나/불연속적인 경우), 우리가 만들어낸 "근사치"는 문제가 되는 부분 주변이 눈에띄게 오버슈트/언더슈트되는 결과로 이어집니다. 이 문제는 완전히 양수인 함수를 근사하려고 시도하더라도 일어납니다! 여러분이 예상하시는대로, 이 문제는 라이팅에 있어서 아주 끔찍하고 심각합니다. 왜냐하면 애초에 우리의 사전에 "음수" 빛이라는 건 존재하지 않으니 말이죠.
이로써 왜 감마 보정을 활성화하는 게 문제를 더 심각해 보이게 만드는지가 설명됩니다. 값들을 선형적으로 만듦으로써 어두운 부분과 밝은 부분이 더 눈에띄게 차이 나게 되고, 그로인해 함수를 더 'ringing' 현상에 취약하게 만듭니다.
이 문제를 해결하기 위해, 직감적으로 선택할 수 있는 방법은 더 많은 주파수 밴드를 쌓아 올리는 것일 겁니다. 하지만 이 해결법은 급속도로 비효율적으로 변합니다. 왜냐하면 필요로 하는 계수의 수가 제곱으로 늘어나기 때문이죠. 이보다 더 중요한 사실은, 신호와 같은 불연속적인 함수는 무한한 주파수를 가지기에, 얼마나 많은 주파수 밴드를 추가한들 결과에서 음수 값이 나오지 않는다고 확실할 수 없다는것 입니다.
이 문제를 다루는 일반적인 방법(Sloan의 논문에서 제안된)으로, 상응하는 degree $\ell$이 증가할수록 점점 0으로 감소시키는 또 다른 계수 집합을 투영된 계수들에 곱해주는 것입니다. 논문에서는 이 작업을 "윈도잉(windowing)"이라고 부릅니다. 이때 윈도잉 계수가 $\ell$에 다라 어떻게 감소할지 정하는 함수를 "윈도잉 함수"라고 부릅니다.
이 샘플에서 사용하는 윈도잉 함수는 sinc 함수입니다.
$$w(\ell)= \begin{cases}
1, \ell = 0 \\
(\frac{\sin\frac{\pi\ell}{q}}{\frac{\pi\ell}{q}})^p, \ell > 0
\end{cases}$$
$q$는 $\ell$이 증가함에 따라 윈도잉 계수가 얼마나 빠르게 0으로 감소하는지를 제어하는 파라미터입니다, 그리고 $p$는 얼마나 "적극적으로" deringing을 적용할지를 제어하는 파라미터 입니다 (이에 대해선 뒤에서 더 설명드리겠습니다).
자 그럼 대체 왜 이 방법이 효과적인 걸까요? 사실 구면 조화는 디지털 신호 처리에서 "주파수 도메인 표현"이라고 불립니다. 주파수 도메인 표현은 임의의 신호를 알려진 다른 주파수에서의 신호들을 가중합으로 표현합니다. 즉 더 높은 degree $\ell$은 더 높은 주파수와 동일한 의미를 가집니다.
윈도잉 함수를 적용하면 더 높은 주파수가 최종 결과에 주는 영향을 점진적으로 줄일 수 있습니다. 그러니까 일종의 로우-패스 필터인 샘이고, 입력 신호를 "블러링"하는 것과 동일한 효과를 냅니다. 물론 입력 신호를 블러링하는 것은 급격한 변화나 불연속성을 줄이고, 이는 디테일을 잃어버리는 결과로 이어집니다.
결론적으로 이 방식을 통해 deringing을 수행하는 것은 실제론 ringing 아티팩트의 원인이 되는 불연속성을 제거하고자 하는 것과 같습니다. 윈도잉 함수의 파라미터 $q$는 블러의 "너비"를 제어하고 (즉, 어느 정도의 주파수에서 잘려야 하는지), 파라미터 $p$는 얼마나 많은 횟수의 블러가 적용되는지 정합니다. 파라미터 $p$가 필요한 이유는, 가끔 음수 값들을 모두 제거하기 위해서는 단 한 번의 블러만으론 충분하지 않은 경우가 있기때문입니다.
// 2026-04-29 (해당 부분까지 검토)
케이스 스터디: Irradiance를 구면 조화에 투영하기
우리가 다른 다음 예시는 어떻게 Irradiance가 구면 조화에 투영되는지를 살펴보도록 하겠습니다. 이 섹션에서는 GDC 2018에서 Yuriy O'Donnel의 발표인 "프로스트바이트 엔진에서의 사전 계산된 글로벌 일루미네이션"의 라이트맵의 디퓨즈 라이팅을 중점적으로 참고하도록 하겠습니다.
구면 조화와 라이트맵
아시다시피 라이트맵은 장면에 존재하는 표면의 한 점에 어느 만큼 의 빛의 양이 도달하는지를 저장합니다. "빛의 양"은 약간 애매한 용어이죠. 대부분의 경우엔 라이트맵은 그 점에서의 irradiance의 근사치를 저장합니다. 즉 아래의 적분값을 말이죠,
$$E(p) = \int L_i(\vec{\omega}, p) \max(0, (\vec{\omega} \cdot \vec{n})) d\vec{\omega}$$
이때 $p$는 표면의 점이고, $L_i(\vec{\omega}, p)$는 방향 $\vec{\omega}$로 부터 점 $p$에 도달하는 빛을 $\vec{n}$은 점 $p$에서의 표면 노멀을 나타냅니다.
실제로, irradiance라는 단위가 위 발표에서 프로스트바이트의 라이트맵 구현에 사용하였다고 언급됩니다.
프로스트바이트는 라이트맵에 irradiance 데이터를 저장하기 위해 low-order L1 구면 조화를 사용합니다.
여기서 잠깐, 정확히 어떻게 irradiance를 구면 조화와 연결 지을 수 있는 걸까요?
만약 표면 위에서의 점 $p$를 고정시킨다 가정하면 irradiance는 그 점에서의 표면 노멀에 대한 함수로 취급할 수 있을 것입니다. 그러므로 irradiance는
$$E(\vec{n}) = \int L_i(\vec{\omega}, p) \max(0, (\vec{\omega} \cdot \vec{n})) d\vec{\omega}$$
이렇게 하면 이 함수는 구의 표면에 대해 정의된 함수라고 할 수 있을 것입니다. 프로스트바이트 발표에서 내용이 바로 이 함수에 대한 구면 조화 계수를 저장하고 (band $\ell=1$ 까지의 band가 포함된), 런타임에 셰이딩에 사용되는 노멀을 사용해서 값을 계산해 낸다고 언급합니다. 하지만 왜 굳이 irradiance를 표면 노멀에 대한 함수로 저장하는 것일까요?
만약 메쉬가 노멀 맵을 사용된다면 사전 계산한 라이팅이 이 노멀맵에 의해 표현되는 디테일 또한 포착해 내길 바랄 수 있습니다. 그러지 않고 단순히 몇 개의 값만을 저장한다면 사전계산된 라이팅이 적용된 결과가 평평하게 보일 테니까요. 아래 이미지가 정확히 이 점을 보여줍니다. 그림자가 진 영역은 라이트맵 덕분에 간접광을 받지만 노멀맵에 의한 풍부함은 완전히 사라져 버렸습니다.

아마 여러분은 이렇게 생각하시겠죠. 이건 그냥 해결하기 쉬운 문제네요. 그냥 오프라인 계산 과정에서 노멀맵에서 추출된 표면 노멀을 포함시키죠! 뭐 이 방식이 이론적으론 잘 먹히겠지만 (비록 런타임에 셰이딩 노멀을 변형시킬 수 없더라도), 이 방식엔 큰 약점이 있습니다. 바로 라이트맵의 각 텍셀이 노멀맵의 하나의 텍셀과 연관되어야 한다는 점이죠.
맞습니다 여러분이 만들어낸 라이트맵은 더 많은 디테일을 품고 있겠죠, 하지만 그렇게 만들어진 라이트맵은 더 커지게 됩니다 (그리고 더 느리게 구워지겠죠). 대신 더 낮은 해상도의 라이트맵을 생성하되 각 텍셀이 함수의 근사치를 포함하도록 만드는 것입니다. 이 함수는 어떤 점에서의 표면 노멀이 특정 방향을 향하고 있을 때 그 점에서의 irradiance가 어느 정도인지 나타냅니다.
자, 그러므로 라이트맵에 저장될 irradiance를 표면 노멀에 대한 함수로 만듦으로써
- 사전 계산된 라이트 정보가 노멀맵의 내용에 반응하도록 할 수 있음
- 런타임에 노멀을 변형시킬 수 있음
- 필요로 하는 저장 공간과 베이크 타임을 줄일 수 있음
이것이 바로 프로스트바이트 발표에서 구면 조화를 사용하여 달성하고자 하는 목표입니다.
구면 조화와 컨볼루션
아래 내용은 발표에서 irradiance의 SH 표현을 계산해 내는 과정을 설명하고 있습니다.
실제 렌더링을 위해서, 특정 노멀 방향에 대해 그 노멀 주변을 감싸는 반구의 모든 방향으로부터 표면으로 들어오는 빛의 양을 계산해야만 합니다. 다르게 말해서, 지오메트리 표면 위의 특정 한 점에서의 irradiance를 계산해야만 합니다.
이는 SH 형태의 radiance 함수와 [...] 클램프 된 코사인을 컨볼루션함으로써 얻어낼 수 있습니다.
SH는 신호의 주파수 공간에서의 표현이고 이 형태에서의 컨볼루션은 SH 기저 인자들의 단순한 컴포넌트 당 곱으로 표현됩니다.
이 내용을 이해하기 위해서든 또 다른 수학적인 개념인 컨볼루션에 대해 설명하고 컨볼루션이 어떻게 구면 조화와 상호작용하는지를 알아야 합니다. 몇 단락만 인내하시면, 왜 이게 irradiance와 연결되었는지 명확하게 보이게 될 것입니다.
아마 여러분은 이미지 처리에서의 컨볼루션이 이미지 전체에 걸쳐서 계수의 행렬 ("필터")를 필터의 이미지의 값과 상응하는 필터의 값과 곱해서 다 더하면서 "슬라이딩"시킨다는 아이디어에 이미 익숙하실지도 모르겠네요.
이 개념은 함수의 세계에서도 일반화될 수 있습니다. 함수 $f$와 $g$가 데카르트 좌표계 상에서 정의되었을 때, 컨볼루션 연산자 ($f*g$)의 정의는 다음과 같습니다.
$$(f*g)(t) = \int f(\tau)g(t-\tau)d\tau$$
여기서 볼 수 있듯이, 데카르트 좌표계에서의 컨볼루션은 하나의 함수를 다른 함수에 "슬라이딩"시키면서 함수들을 곱하고 그 결과를 적분 해내는 것과 마찬가지입니다. 다르게 말하자면, 점 $t$에서의 컨볼루션을 계산하기 위해서, 함수중 하나를 $t$ 주변에 위치시키고 (이런 함수를 "커널"이라고 부르고 위 정의에선 $g$가 그 역할을 하고 있습니다), 위치한 커널과 다른 함수 간의 곱의 결과를 적분한다는 것입니다.
하지만, 저희의 경우엔 구면에 대해 정의된 함수를 다루므로 앞서 살펴본 컨볼루션의 정의는 잘 맞지 않습니다. 그렇기에 구면 위에서 "슬라이드"될 함수가 필요하고, 컨볼루션의 정의가 이에 따라 살짝 변형되어야 합니다.
자, 그럼 구의 표면에 대해 정의된 두 함수 $f(\vec{\omega})$ 그리고 $g(\vec{\omega})$가 있다고 합시다. 더 나아가 $\rho_t(g)$를 함수 $g$의 북극이 방향 $t$와 일치하도록 회전시킨 결과라고 정의합시다. 이때 $f$와 $g$의 컨볼루션은 다음과 같이 정의됩니다.
$$(
f*g)(t)
= \langle f, \rho_t(g) \rangle
= \int f(\omega)\rho_t(g(\omega)) d\omega$$
이 경우 회전 연산자가 데카르트 공간에서 함수(필터)를 위치시키는 것과 동일한 역할을 맡게 됩니다.
자 이제 컨볼루션에 대한 구면 조화의 흥미로운 속성을 살펴보도록 하겠습니다.
$f_\ell^m$가 함수 $f$에 대한 SH 계수라 고하고. $h$가 $Z$ 축에 대한 회전대칭 함수라고 한다면 (즉, 방위각이 변할 때 값이 변하지 않음). 컨볼루션 $f*h$의 SH 계수는 다음 수식을 통해 얻을 수 있습니다.
$$(f*h)_\ell^m = \sqrt{\frac{4\pi}{2\ell+1}}f_\ell^m h_\ell^0$$
어라 왜 컨볼루션의 SH 계수를 찾는 걸까요? 다시 한번 irradiance 적분이 어떻게 생겼는지 살펴봅시다.
$$E(\vec{n})
= \int L_i(\vec{\omega}, p) \max(0, (\vec{\omega} \cdot \vec{n})) d\vec{\omega}$$
$(\vec{\omega} \cdot \vec{n})$는 $g(\theta, \phi) = \cos{\theta}$이므로 함수 $g$을 회전시켜서 함수의 "북극"을 표면 노멀 $\vec{n}$에 정렬시킵니다. 여러분이 이 사실을 이해하는 순간 왜 irradiance가 radiance $L_i$과 코사인 (정확하게는 "클램프"된 코사인)의 컨볼루션의 결과로 얻어지는지 명확하게 이해하실 수 있을 것입니다.
Irradiance를 위한 SH 계수 찾기
이제 어떻게 radiance에 대한 SH 계수를 찾아낼지도 알아냈고. 앞서 살펴본 "클램프 된 코사인"의 SH 계수도 잘 알려져 있습니다. 이 계수는 Ramamoorthi와 Hanrahan의 논문인 "An Efficient Representation of Irradiance Environment Maps(Irradiance 환경맵의 효과적인 표현법)"에서 찾아볼 수 있습니다.
$$A_0 = \frac{\sqrt\pi}{2}, A_1 = \sqrt{\frac{\pi}{3}}$$
자 그럼 이제 끝날 걸까요? 그냥 radiance를 $\ell = 0, 1$에 해당하는 구면 조화 함수에 투영하고 밴드 $\ell=0$와 밴드 $\ell=1$에 속하는 계수들에 각각 $A_0$와 $A_1$를 곱해주기만 하면 모든 일이 잘 풀리는 걸까요?
뭐.. 물론 이렇게 하는 것만으로도 잘 동작할 겁니다. 하지만 조금 개선할 부분이 존재합니다. 프로스트바이트 프레젠테이션은 계산 과정을 간략화시킬 수 있는 환상적인 트릭을 보입니다. 컨볼루션의 SH 계수를 계산하기 위해서 아래 수식을 따라야 한다는 것을 떠올려 보세요.
$$(f*h)_\ell^m = \sqrt{\frac{4\pi}{2\ell+1}}f_\ell^m h_\ell^0$$
Irradiance의 경우엔, 들어가는 함수는 radiance $L_i$과 클램프 된 코사인이고, 클램프된 코사인을 $A$라고 표기한다면 아래와 같습니다.
$$(L_i*A)_\ell^m = \sqrt{\frac{4\pi}{2\ell+1}}L_\ell^m A_\ell^0$$
자, 이 상태에서 $(L_i*A)_0^0$를 간략화시켜보도록 합시다. $A_0 = \frac{\sqrt{\pi}}{2}$ 이므로,
$$(L_i*A)_0^0 = \sqrt{4\pi}L_0^0 \frac{\sqrt{\pi}}{2} = \pi L_0^0$$
$L_{0}^{0}이 radiance의 여러 값들과 $y_{0}^{0}$을 곱한 다음 전부 합한 것이므로, 다음과 같은 상수항을 이끌어 낼 수 있습니다
$L_0^0 = \frac{1}{2\sqrt{\pi}} (...)$. 그러므로
$$(L_i*A)_0^0 = \pi\frac{1}{2\sqrt{\pi}} (...) = \frac{\sqrt{\pi}}{2} (...)$$
하지만 런타임에서 계산 과정에서 이 값에 상수 인자를 가지고 있는 $y_{0}^{0}$을 다시 곱해줘야 한다는 것을 알고 있습니다. 그럼 상수 인자인 $\frac{1}{2\sqrt{\pi}}$를 irradiance 계산에 끼워넣을 수 있지 않을까요?
$$ \frac{1}{2\sqrt{\pi}} (L_i*A)_0^0 = \frac{1}{2\sqrt{\pi}} \frac{\sqrt{\pi}}{2} (...) = \frac{1}{4}(...)$$
이 PDF에서 다른 방식의 유도를 찾아보실수 있습니다, 결론적으로 radiacne의 투영과 컨볼루션 그리고 부분적인 계산을 하나의 짧은 함수로 합칠 수 있게 된다는 것입니다.
SHL1 shEvaluateDiffuseL1(vec3 p)
{
float AY0 = 0.25f;
float AY1 = 0.50f;
SHL1 sh;
sh[0] = AY0;
sh[1] = AY1 * p.y;
sh[2] = AY1 * p.z;
sh[3] = AY1 * p.x;
return sh;
}
이 함수가 반환하는 값이 바로 라이트맵에 저장되야 하는 값입니다. 그리고 정말 멋진 점은 이 값을 주어진 노멀에 대해 평가하는 것이 그냥 그 노멀과 내적 하는 것만으로 이루어진다는 것이죠! 그 외 다른 연산들은 이미 사전 계산 단계에서 모두 수행되었습니다.
결론 및 더 읽어볼거리
이 포스트에서 몇 가지 더 주제를 더 다뤄야했겠지만, 제 목표는 독자들에게 아주 총명한 사람들이 써서 공개한 것들을 이해하는데 필요한 지식과 용어들을 알려드리는 것에 있기에 그러지 않겠습니다. 그러므로, 추가로 더 진행하는데 필요한 읽을 거리들을 몇가지 추천하는 것으로 끝맺음을 짓고자 합니다.
아래 글들부터 시작해서.
- 이 글에서 많이 언급했던, Peter-Pike Sloan, "Stupid Spherical Harmonics Tricks"
- Robin Green, "Spherical Harmonics Lighting: The Gritty Details"
- Ravi Ramamoorthi와 Pat Hanrahan, "An Efficient Representation for Irradiance Environment Maps"
구면 조화를 deringing 하는 건 이 글에서 다뤘던 것보다 더 도전적인 문제입니다. Peter-Pike Sloan의 "Deringing Spherical Harmonics" 논문에서 투영된 irradiance에서 완전히 음수 값을 피해내면서도 최대한 디테일은 보존해 내는 deringing에 대한 더 원칙적인 접근법에 대해 깊게 다룹니다.
Graham Hazel이 블로그에 개시한 두 개의 포스트도 읽어볼 가치가 있습니다.
위 글들은 정의를 살짝 바꿈으로써 L1 구면 조화에서 음수 값을 피하는 방법에 대해 설명합니다 (안타깝게도, 더 높은 degree에서는 동작하지 않습니다). 앞서 살펴본 프로스트바이트 발표에서 이 기법들을 사용하였습니다.
구면 조화만이 라이팅 정보를 표현하기 위한 유일한 기법인 것은 아닙니다. 아래에 몇 가지 대안을 소개해드리도록 하겠습니다.
- Chris Green, "Efficient Self-Shadowed Radiosity Normal Mapping" (HL2)
- Ralf Habel과 Michael Wimmer, "Efficient Irradiance Normal Mapping" (H-Basis)
- Matt Pettineo이 게시한 이 멋진 시리즈는 라이트맵핑과 또 다른 라이팅 표현인 구면 가우시안에 대한 아이디어를 설명하는 훌륭한 자료입니다.
여러분이 이 글을 즐겁게 읽으셨기를 바랍니다. 만약 오류를 발견하신다면 nicebyte at gpfault.net으로 이메일을 보내주세요.
'개인 공부 및 연구' 카테고리의 다른 글
| 블로그에 게시할 장기 연재(?) 주제 선정 (0) | 2026.01.20 |
|---|---|
| Half Vector의 의미와 유도 (1) | 2025.11.22 |
| 언리얼 엔진에서 더 디비전 스타일 UI 재현 해보기 - 3D 위젯 (0) | 2025.11.15 |
| 언리얼 엔진 4 렌더링 시리즈 파트 6 관련 노트 (0) | 2025.04.12 |
| [역] 언리얼 엔진 4.22를 위한 메시 드로잉 파이프라인 변환 가이드 (0) | 2025.04.11 |