첫째, Gaussian Splatting이란 수만~수십만 개의 작은 스플랫(splat)이라 불리는 점(사실상 쿼드quad 형태의 작은 삼각형 모음)을 화면에 흩뿌려(real-time point cloud처럼) 장면을 구성하는 기술입니다. 각 스플랫은 가우시안 분포를 닮은 흐릿한 점으로, 전통적인 삼각형 메시(mesh) 렌더링이 아닌 점 기반 파티클 기법에 가깝습니다. 이때 스플랫 하나하나는 색상, 크기, 위치, 투명도 정보를 가지며, 그것들을 모아 시야(frustum) 안에 보이는 것만 골라 순서에 맞춰 GPU에게 명령을 내리는 것이 핵심입니다.
GaussianSplatRenderer가 활성화되면 RegisterSplat이 호출되어, 첫 스플랫에 한해 Camera.onPreCull 이벤트에 한 번만 콜백을 연결하고, 내부 m_Splats 사전에 ‘렌더러→MaterialPropertyBlock’ 쌍을 저장한다. 반대로 비활성화 시 UnregisterSplat이 호출되어 해당 항목을 삭제하고, 사전이 비면 onPreCull 연결 해제와 CommandBuffer 등 GPU 리소스 해제, 내부 컬렉션 비우기를 한꺼번에 처리한다. 이로써 이벤트 구독 중복과 불필요한 드로우콜을 방지하고, MaterialPropertyBlock으로 매 프레임 머티리얼 생성 부담을 없애 성능을 높이며, 스플랫이 없을 때 자원을 즉시 반환해 메모리·GPU 사용 효율을 극대화한다.
Unity 렌더링 파이프라인은 보통 다음 순서로 움직인다:
- Update(게임 로직 갱신)
- Culling(보이지 않는 오브젝트 걸러내기)
- PreCull(“지금부터 그릴 것들 준비”)
- Render(실제 드로우콜 실행)
- PostRender(마무리 작업)
Gaussian Splatting 시스템은 PreCull 단계에 끼어들어 “내가 관리하는 스플랫을 이번 프레임에 그려라”는 커맨드를 GPU에 추가한다.
- RegisterSplat
- 첫 번째 스플랫 렌더러가 켜질 때만, PreCull 이벤트에 한 번만 콜백 연결 → 이 시점에 CommandBuffer를 붙여 둠
- 동시에 m_Splats라는 사전에 “렌더러 객체 → MaterialPropertyBlock” 쌍을 저장해, 이 렌더러에만 적용할 파라미터를 관리
- UnregisterSplat
- 렌더러가 꺼지면 사전에서 제거
- 사전이 비면 PreCull 이벤트 연결 해제 → 더 이상 스플랫을 그릴 필요가 없으므로 CommandBuffer와 관련 GPU 리소스를 해제
이 구조 덕분에
- PreCull 단계에만 한 번만 진입해 불필요한 드로우콜 중복을 막고
- MaterialPropertyBlock으로 매 프레임 새 머티리얼 생성 없이 파라미터만 교체해 성능을 최적화하며
- 스플랫이 없을 땐 즉시 리소스를 반환해 메모리·GPU 사용을 최소화할 수 있다.
렌더링의 시작점은 Camera.onPreCull 이벤트입니다. ‘PreCull’은 문자 그대로 “카메라가 보이는 영역(Frustum) 밖의 오브젝트를
제거(culling)하기 직전” 단계로, Unity 렌더 파이프라인에서 객체가 실제로 화면에 그려지기 전에 호출됩니다. 이 시점에 우리의 시스템은 “이 카메라에 대해 오늘 그려야 할 스플랫을 GPU에게 알려 주자!”라는 명령을 준비합니다. 전통적인 렌더링이라면 메쉬 렌더러가 내부적으로 처리하지만, 점 기반 렌더링은 직접 CommandBuffer라는 “GPU용 커스텀 명령 모음”을 만들어 카메라에 붙여야 합니다.
그 다음 GatherSplatsForCamera(Camera cam) 메서드가 실행됩니다. 여기선 먼저 컬렉션에 담긴 모든 등록 스플랫을 순회하며,
- 활성화여부(isActiveAndEnabled),
- 에셋 유효성(HasValidAsset),
- 렌더 설정 유효성(HasValidRenderSetup)
등을 검사합니다. 이를 통과한 스플랫만 m_ActiveSplats 리스트에 담고, 리스트가 비어 있으면 더 이상 작업을 수행하지 않습니다. 이후 m_RenderOrder 값을 기준으로 내림차순 정렬(우선 순위가 높은 순서대로)하고, 동일 순위끼리는 카메라 기준 Z축 깊이(Transform.InverseTransformPoint로 구한 z값)가 작은(카메라에 가까운) 순서로 오름차순 정렬합니다. 이 과정 덕분에 투명도나 오버랩이 있는 점들을 올바른 순서로 렌더링할 수 있습니다.
그다음, InitialClearCmdBuffer(Camera cam)가 호출되어 CommandBuffer를 생성·초기화합니다. CommandBuffer는 “GPU에게 보내는 일종의 커스텀 드로우콜 리스트”로, Unity 기본 파이프라인(BIRP: Built-in RP)을 사용할 때 cam.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, m_CommandBuffer)로 카메라 이벤트(여기서는 ‘알파 블렌드 직전’)에 연결합니다. 이때 Clear()를 호출해 이전 프레임 명령을 비우고, 이후에 추가될 렌더·합성 명령만 남도록 준비합니다. 이처럼 미리 GPU 명령을 쌓아 두면, 실제 렌더링 단계에서 순서대로 효율적으로 실행됩니다.
가장 핵심인 SortAndRenderSplats(Camera cam, CommandBuffer cmb) 단계에서는, 앞서 모아둔 m_ActiveSplats 리스트를 순회하며 다음을 수행합니다:
- EnsureMaterials():
- 스플랫을 그리기 위한 Material(셰이더+파라미터 집합)이 아직 생성되지 않았다면, Shader 오브젝트를 이용해 new Material()을 호출합니다.
- 정렬 버퍼 업데이트(SortPoints):
- Compute Shader로 각 스플랫과 카메라 간 거리를 계산 (CalcDistances 커널).
- GPU 소팅(GpuSorting)으로 거리 순서에 맞춰 인덱스 버퍼(m_GpuSortKeys)를 정렬합니다.
- Compute Shader는 GPU 내에서 복잡한 수학 연산을 병렬로 처리하는 프로그램으로, CPU 대신 대량 데이터를 빠르게 처리합니다.
- 뷰 데이터 계산(CalcViewData):
- 카메라의 뷰 행렬(View Matrix), 월드→오브젝트 행렬, 화면 픽셀 크기 등을 Compute Shader에 전달해 “각 스플랫이 화면상 어느 위치에 찍힐지”를 미리 GPU 버퍼(m_GpuView)에 저장합니다.
- 인스턴싱 렌더링(DrawProcedural):
- MeshTopology.Triangles 모드로, 하나의 쿼드(6 인덱스) 또는 디버그용 큐브(36 인덱스)를 인스턴스(instanceCount = 스플랫 개수) 형태로 그립니다.
- MaterialPropertyBlock을 통해 각 인스턴스별 크기(_SplatSize), 투명도(_SplatOpacityScale), 색상 텍스처(_SplatColor) 등을 동적으로 전달합니다.
- “DrawProcedural”은 미리 정의된 메시 없이 GPU가 자체적으로 삼각형을 생성하게 하는 API로, 대량의 점(스플랫)을 효율적으로 처리하는 핵심 메서드입니다.
모든 스플랫에 대한 DrawProcedural 명령이 CommandBuffer에 기록되면, 합성(Composite) 단계로 넘어갑니다. 여기서는 임시 렌더 타겟(퓨전된 스플랫만 담긴 텍스처)을 화면의 백버퍼(실제 최종 화면)에 알파 블렌딩 방식으로 덧씌우는데, **DrawProcedural**을 다시 호출해 전 화면을 덮는 하나의 삼각형(3 정점)으로 빠르게 처리합니다. 이때 사용하는 compositeMat 셰이더는 아래와 같은 역할을 합니다:
- 소스 텍스처(스플랫 RT)에서 픽셀 색상 읽기
- **목적지(카메라 타겟)**에 기존 컬러와 알파 블렌드
마지막으로 **ReleaseTemporaryRT**로 임시 렌더 텍스처를 해제하여 메모리를 반환하고, 이 카메라의 한 프레임 렌더링이 끝납니다. 이렇게 CommandBuffer에 담긴 순서대로 GPU가 처리하면서, CPU와 GPU 간 불필요한 동기화 없이 “점 기반” 대규모 입자 렌더링을 고성능으로 수행할 수 있습니다.
부가적으로, 사용자가 키보드 숫자키 ‘0’(KeyCode.Alpha0)를 누르면 흑백↔컬러 모드를 토글할 수 있는데, 내부적으로는
- 원본 Texture2D m_OriginalColorData를 보관하고,
- ConvertToGrayscale(Texture2D) 메서드로 RenderTexture에 복사한 뒤 CPU에서 픽셀 단위로 그레이 스케일 값을 계산하여 새로운 Texture2D를 만들고,
- 이를 다시 GPU로 업로드해 다음 프레임부터 스플랫에 적용합니다.
ConvertToGrayscale 단계에서는 Graphics.Blit로 GPU→CPU 복사를, ReadPixels로 CPU측 읽기를 수행하기 때문에 다소 비용이 있지만, 간단한 데모나 디버깅 용도로 쓸 수 있습니다.