아직 정체모를 이미지를 결과랍시고 내놓았다. 그래도 이정도 성과로도 충분한 의의가 있고 발전시킬 여지가 있다고 생각한다. 시간만 좀 더 투자한다면 APV 비스무리한 결과를 얻을 수도 있지 않을까?
기본 아이디어
라이트맵과 라이트프로브
라이트맵은 나온지 오래된 기술이지만 오래된 만큼 성능에 무리가 안가고 모바일에서 부담없이 쓰기 좋은 간접광 솔루션이다. 유니티 엔진(내가 다루는 엔진은 2021~2022 버전이다. Unity 6는 안써봤다...)에서 기존에 간접광 처리를 위한 방법을 다음과 같이 제공하고 있다.
배경(Static) : UV2 채널을 LightMapUV로 써서 Static Lighting 결과를 텍스처 아틀라스에 저장하고 셰이더에서는 이 라이트맵을 샘플링하여 간접광을 표현한다.
하지만 라이트맵은 결국 Mesh에 미리 구워진 라이트맵 UV를 이용해서 텍스처를 샘플하는 방식이다보니 Dynamic 오브젝트에는 적용이 불가능한 큰 단점이 있다.
그래서 보통 Dynamic Object에 적용하기 위한 Light Probe를 같이 구워서 사용하는데, Light Probe 데이터는 CPU에서 Per Object 단위로 그 뭐시냐, 테트라 머시기 사면체 관련된 무슨 알고리즘을 이용해서 가장 근접한 4개의 Light Probe를 보간해서 최종 SH 데이터를 전달하는 식으로 동작한다.
문제는 이 Per Object 단위로 전달하는 방식인데, 이 오브젝트가 얼마나 크고 어떻게 생긴지에 관계없이 항상 이 오브젝트의 Bounding Box의 중심점을 기준으로 Probe 데이터를 계산하게 된다. 물론 보통은 문제가 되진 않는다. 배경과 달리 캐릭터는 간접광이 디테일하지 않아도 티가 잘 나지 않기 때문이다. 왜냐하면 캐릭터 자체적인 AO 맵도 있고 캐릭터의 부분마다 로컬 조명이 깔리는 경우는 없으니까.
아무튼 이런 식으로 투 트랙으로 라이트맵과 라이트 프로브를 세팅해서 간접광을 표현할 경우 Dynamic Object는 간접광의 영향을 디테일하게 받지 못하는 문제점이 있다.
APV
다음으로 Unity6가 출시하면서 밀고 있는 기술(아마도?)이 Adaptive Probe Volume으로, Probe 데이터를 아예 텍스처에 저장해서 Fragment Shader에서 실시간 샘플링을 하도록 만들어놓은 간접광 솔루션이 있다. 결국 베이크한 데이터인 건 마찬가지지이지만, Dynamic, Static Object 모두에 적용할 수 있고, LightMap 전용 UV를 펴는 등의 아티스트의 수고가 덜하다는 장점이 있다. 하지만 SH 데이터를 텍스처에 저장하려면 L2 데이터는 27개의 float값이 필요하므로 최소한 7장의 텍스처가 있어야 한다.
(4*7 = 28 이므로 rgba 4개 채널을 사용하는 텍스처가 7개 있어야 27개의 데이터를 넣을 수 있다.)
텍스처 샘플을 7장을 해야 간접광을 처리할 수 있다는 건 그 자체로 엄청난 부담이다. 그래서 모바일 최적화를 위해 L1 데이터만 가져다 쓴다거나, L0, L1 데이터는 Fragment Shader에서 처리하고 L2 데이터는 Vertex Shader에서 계산하는 등의 최적화 옵션을 제공하고 있다.
거의 유사한 개념으로 언리얼 엔진의 Volumetric LightMap이 있다.
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/volumetric-lightmaps-in-unreal-engine
커스텀 APV를 만들기 위해서 넘어야 할 허들
구면 조화 계수를 저장하고 이용하는 방법
APV를 2022버전에서 한번 써보고 싶었는데, 안되는 것 같아서 열받아서 한번 만들어보기로 했다. 구면 조화가지고 예전에 장난을 쳐본 경험이 있기 때문에 조금 자신감이 붙은 것도 있다.
음.. 지금 와서보면 흑역사 같긴 하지만 유니티의 셰이더 환경을 섭스턴스 페인터에서 그대로 만드는 법에 대해서 Adobe에서 발표했던 적이 있다. 부끄럽다.
(어도비 코리아) 섭스턴스 페인터에서 커스텀 셰이더 작성 : https://youtu.be/s1-L72HmlhM?si=b3-u2mtJ-2eK4DJv
섭스턴스 페인터용 커스텀 셰이더를 작성하면서 다른 라이팅 계산은 별거 없었는데 유니티의 Lighting Setting에 있는 Gradient를 어떻게 가져와야 하는지 알 수 없어 엄청 고생했던 기억이 있다.
뭐 계산된 SH 수치를 그대로 넘기면 특정 환경에서의 간접광 처리는 문제 없었겠지만 내가 하고 싶었던 건 유니티와 동일하게 Sky, Equator, Ground 색상을 입력했을 떄 이 색상 데이터가 SH 값으로 변환되는 과정에 대해서 규명하고 싶어서 구면 조화 함수를 열심히 팠었다.
구면 조화 함수... 수학을 잘 못하는 나로서 이해하기 상당히 어려웠고 지금도 사실 잘 이해하고 있다는 생각은 들지 않는다.
간단히 정리하자면 테일러 급수, 매클로린 급수와 같이 초월 함수나 뭔가 노이즈가 섞인 임의의 신호 데이터를 다항식과 같이 상대적으로 간단하고 규칙성 있는 형태로 근사화 하는 방식이라 생각했다.
이 구면 조화 함수를 위해서 어떤 수학 이론이 사용되었고 어떻게 증명하고 이런 부분에 대해선 아무것도 이해를 할 수 없지만 코드만 보면 결국 곱하고, 더한다. 이 2개의 계산으로 심플하게 정의가 가능하므로 응용에 있어서는 무리가 없다.
구면 조화 함수에 대한 디테일한 부분에 대해서는 나중에 따로 글을 쓰도록 한다.
Bake까지 구현을 직접해주어야 하나? 그러면 부피가 너무 커진다.
상수 시간으로 접근 가능한 Sparse Volume 데이터
sh에 대해서 이해가 없더라도 결국 floating point 27개를 텍스처 7개에 기록만 할 수 있으면 쓰는 건 문제가 안 된다. 진짜 어려운 건 분명히 메쉬의 밀도나 중요도에 따라서 volume의 밀도가 변하는데, 이러한 구조는 보통 트리 형태로 쓰게 될 것이다. 트리 구조의 탐색은 내가 배운 매우 기초적인 지식으로는 루트 노드에서 리프 노드까지 순차적으로 접근하는 식으로 동작해야 한다. 즉 원하는 데이터를 얻기 위한 탐색 시간이 추가되어야 한다.
하지만 셰이더에서 동작해야 하는 기능이 탐색 시간이 존재한다? 이건 말이 안된다. 예전에 글로 작성했던 Curve 관련 내용에서도 0~1 사이로 들어온 타임 값이 실제로 배열 인덱스 몇에서 몇 사이에 있는지를 알아내는 과정이 상수 시간으로 가능한지 모르겠다. 알고리즘이 뭔가 있을 것 같은데, 아니면 미리 상수 시간으로 탐색할 수 있도록 메모리를 추가로 사용한다던지...
그리고 데이터가 Cache 친화적인 형태로 배열이 되어 있어야 한다. 데이터가 무작위 위치에 들어가 있으면 텍스처 샘플링 퍼포먼스가 매우 낮아질 것이다. 이걸 어떻게 해결할 것인가?
Visiability Buffer 만드는 거, 그러니까 언리얼 엔진의 나나이트 메쉬도 트리 구조를 이용하던데, 어떻게 데이터를 구성했길래 트리 구조를 셰이더에서 사용하기 적합한 형태로 만들어낸 것일까?
스트리밍 및 여러 시나리오를 블렌딩하여 TOD(Time Of Day) 연출 등이 가능하도록 하기
이건... 정말 이게 잘 되면 생각해보도록 한다.
크게 보면 위의 2가지 요소를 해결해야 한다. 일단 코어에 해당하는 Sparse Volume은 글을 쓰고 있는 현 시점에서 하나도 모르겠다. 구조를 어떻게 짜야 하는지 모르겠다.
우선 제일 간단한 구조를 생각했다. 3D Grid 구조를 사용하는 것이다.
3D Grid 데이터를 이용한 Irradiance Volume
아주 원시적인 형태의 Probe Volume을 계획했다.
Grid x,y,z를 미리 정의해주면 이 Grid 카운트와 Bounding Box에 맞게 Probe를 배치한다.
3D Grid의 장점
World Position 좌표를 Grid Index 값으로 간단하게 변환할 수 있다. World Position 하나만으로 바로 즉각적으로 Index를 알아낼 수 있다.
예를 들어 크기가 10,10,10 인 Bounding Box가 있고 Grid 개수를 x,y,z 축 각각 10개씩 지정했다고 하면 grid 내에 10 * 10 * 10 = 총 1000개의 Probe를 배치하게 된다. 그리고 Position 값은 다음과 같이 계산해서 Grid Index로 변환 가능하다.
(World Position - Bounding Box Min) / Bounding Box Size * (Grid Count - 1)
월드 좌표를 Bounding Box 내의 로컬 좌표로 변환하고 크기로 나눠주면 Bounding Box 내에서의 정규화된 좌표를 얻을 수 있다. 0~1사이로 증가하는 데이터 값에다가 GridCount-1 을 곱해주면 0~GridCount-1 값이 되어 최종적으로 Grid Index로 활용할 수 있다.
'TA' 카테고리의 다른 글
(작성중) [Unity] URP환경에서 GPU Instancing Tool 만들기 (0) | 2025.01.20 |
---|---|
[Unity] Custom Shadow System 제작하기 (1) - 커스텀 그림자 제작의 필요성 (0) | 2024.12.30 |
[Unity] Blend State 프로퍼티로 관리할 경우 Blend 옵션 비활성화 (0) | 2024.06.18 |
그래픽 리소스 / 렌더링 최적화에 대한 생각 (0) | 2024.06.09 |
[Unity] 유니티의 Curve 구현에 대한 추측과 Curve 데이터 활용 (0) | 2023.12.03 |