조명과 그림자 (1) - 실시간 조명과 베이크 - soo:bak
작성일 :
조명이라는 비용의 시작
UI 최적화 시리즈에서는 Canvas Rebuild, 오버드로우, 드로우콜 증가가 UI 렌더링 비용을 키우는 과정을 다루었습니다.
이 글부터 다루는 라이팅(Lighting)은 렌더링 비용에서 또 다른 큰 비중을 차지하는 서브시스템입니다.
UI가 2D 요소를 효율적으로 화면에 올리는 문제였다면, 라이팅은 3D 씬의 오브젝트가 어떤 빛을 받고 어떤 그림자를 만드는지 계산하는 문제입니다.
현실 세계에서 빛은 광원에서 출발해 표면에서 반사되고, 그 반사광이 다른 표면에 닿아 다시 반사됩니다.
이 과정을 정확히 시뮬레이션하면 사실적인 이미지를 얻을 수 있지만, 계산량이 너무 커서 실시간 렌더링으로는 감당하기 어렵습니다.
영화 VFX가 한 프레임에 수 분에서 수 시간을 쓰기도 하는 데 반해, 게임은 같은 일을 16ms(60fps) 안에 끝내야 합니다.
이 시간 안에 모든 빛의 반사 과정을 직접 계산할 수는 없으므로, 게임 렌더링은 일부 라이트만 실시간으로 처리하고 나머지는 근사하거나 미리 계산합니다.
이렇게 비용을 줄여도 실시간 라이트는 수가 늘 때마다 부담이 빠르게 커집니다. 라이트가 하나 추가될 때마다 프래그먼트 셰이더가 해야 하는 연산이 늘고, 그림자를 켜면 별도의 렌더 패스까지 필요해지기 때문입니다.
이 비용 구조를 구체적으로 파악하기 위해, 먼저 실시간 라이트 하나가 GPU에서 어떤 연산을 더하는지, 파이프라인 구조에 따라 드로우콜과 셰이더 복잡도가 어떻게 달라지는지를 살펴봅니다.
이어서 베이크 라이팅, Light Probe, Reflection Probe, Mixed 라이팅이 각각 어떤 비용을 줄이고 어떤 제약을 갖는지를 순서대로 다룹니다.
실시간 라이팅의 비용
실시간 라이트 하나가 추가될 때 발생하는 비용은 파이프라인 구조에 따라 드로우콜로 나타나기도 하고, 셰이더 복잡도로 나타나기도 합니다.
어느 쪽이든 비용이 누적되면 씬에 둘 수 있는 실시간 라이트의 개수가 제한됩니다.
라이트 1개의 셰이더 비용
실시간 라이트가 1개 있을 때 프래그먼트 셰이더는 각 프래그먼트마다 라이트와 표면의 상호작용을 계산합니다.
셰이더가 참조하는 기본 정보는 광원의 방향과 색상, 그리고 광원에서 멀어질수록 빛의 세기가 줄어드는 정도를 가리키는 감쇠(Attenuation)입니다.
셰이더는 이 정보로 모든 방향으로 고르게 반사되는 확산 반사(Diffuse)와 특정 방향으로 집중되는 정반사(Specular)를 구한 뒤, 두 성분을 합산해 프래그먼트의 최종 색상을 결정합니다.
실시간 라이트가 2개라면 프래그먼트 셰이더는 이 과정을 2번, 3개라면 3번 반복해야 하는 등, 연산량이 실시간 라이트 수에 비례해 선형으로 늘어납니다.
이 조명 계산은 화면에 보이는 모든 프래그먼트에서 반복됩니다. 1080p 해상도(1920×1080)에서 화면 절반을 차지하는 오브젝트는 약 100만 개의 프래그먼트를 생성합니다. 라이트가 3개라면 프레임당 100만 × 3 = 300만 회의 조명 계산이 수행됩니다.
Built-in 멀티패스에서의 비용
Built-in 파이프라인은 멀티패스 포워드 렌더링을 사용해, 실시간 라이트가 추가될 때마다 같은 오브젝트를 다시 그립니다.
첫 패스 ForwardBase가 메인 Directional Light와 환경광을 처리한 뒤 추가 실시간 라이트마다 ForwardAdd 패스가 한 번씩 더해지므로, 드로우콜은 라이트 수와 화면 오브젝트 수의 곱만큼 발생합니다.
Built-in 파이프라인의 구조와 패스 흐름은 Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조에서 자세히 다룹니다.
이렇게 드로우콜이 늘어나면 CPU 부담도 그만큼 커집니다. CPU가 GPU에 명령을 제출하는 횟수가 늘고, 그중 셰이더나 머티리얼이 바뀌는 호출에서는 GPU의 렌더링 상태를 새로 설정하는 SetPass Call까지 더해지기 때문입니다.
URP 싱글패스에서의 비용
URP는 싱글패스 포워드 렌더링을 사용하므로, GPU 버퍼에 일괄 전달된 실시간 라이트 정보를 셰이더 내부에서 한 번에 처리합니다. 따라서 실시간 라이트마다 ForwardAdd 패스를 더하는 Built-in과 달리, 라이트가 1개든 8개든 오브젝트 100개의 드로우콜은 100회로 일정합니다.
그 대신 GPU 부담이 늘어납니다. 프래그먼트 셰이더가 모든 실시간 라이트에 대해 방향·감쇠·Diffuse/Specular를 한 번씩 다시 계산해야 하기 때문에, 라이트가 늘어날수록 GPU 연산이 그에 비례해 무거워집니다.
즉 이 방식의 부담은 CPU 드로우콜이 아니라 GPU 셰이더 연산에 집중됩니다. URP에서 오브젝트당 실시간 라이트 수를 기본 4개, 최대 8개로 제한하는 이유도 이 GPU 부담을 통제하기 위함입니다.
이 GPU 부담은 게임 성능과 직결됩니다. 셰이더가 무거워질수록 한 프래그먼트의 처리 시간이 길어지고, 같은 시간 안에 처리할 수 있는 프래그먼트 수가 줄어들어 결국 프레임 레이트가 떨어집니다.
GPU의 ALU(Arithmetic Logic Unit) 자원과 라이트 수에 따른 셰이더 비용은 셰이더 최적화 (1) - 셰이더 성능의 원리에서 더 자세히 다룹니다. 본 글 후반 〈필레이트 제한과 프래그먼트 비용〉 섹션에서도 라이팅 관점에서 이 관계를 다시 살펴봅니다.
실시간 라이트 수의 한계
병목 위치는 Built-in의 경우 드로우콜, URP의 경우 셰이더 복잡도로 다르지만, 어느 쪽이든 실시간 라이트 수를 계속 늘리기는 어렵습니다. 라이트가 늘어날수록 CPU가 제출해야 하는 렌더링 명령이 늘거나, GPU가 프래그먼트마다 반복해야 하는 조명 계산이 증가하기 때문입니다.
성능 예산이 제한된 프로젝트에서는 흔히 실시간 라이트를 메인 Directional Light 중심으로 줄입니다. 다만 평행광인 Directional Light는 거리에 따른 감쇠가 없어 간접광이나 국지적인 조명까지 다루기는 어렵기 때문에, 그 부족한 부분을 미리 계산해 라이트맵 텍스처에 담아 두는 베이크 라이팅(Baked Lighting)을 함께 사용합니다.
베이크 라이팅 (Baked Lighting)
베이크 라이팅(Baked Lighting)은 정적 환경의 조명을 미리 계산해 라이트맵에 담아 두는 사전 계산 기법입니다. 라이트 수와 무관하게 런타임 비용을 줄여 주는 대신, 사전 계산의 특성상 적용할 수 있는 대상과 갱신 방식에 한계가 있습니다.
이어지는 세 절에서 베이크 라이팅의 메커니즘과 라이트맵의 구조, 그 한계를 순서대로 다룹니다.
베이크 라이팅의 메커니즘
베이크 단계에서는 Unity의 Lightmapper가 정적 오브젝트의 각 표면에 빛이 어떻게 도달하는지를 광선 추적(Ray Tracing)으로 미리 계산합니다. 광원이 직접 비추는 빛(직접광, Direct Light)뿐 아니라 다른 표면에서 튕겨 나오는 빛(간접광, Indirect Light)까지 함께 추적해, 표면이 받는 실제 밝기와 색을 구합니다.
이렇게 추적한 결과는 표면 위의 작은 격자(텍셀, Texel) 하나하나에 RGB 색 값으로 기록되어 한 장의 텍스처를 이룹니다. 이 텍스처가 라이트맵(Lightmap)이며, 라이트맵 한 텍셀에는 그 자리의 표면이 받는 모든 빛이 이미 합쳐진 최종 조명만 담겨 있습니다.
런타임에는 각 정적 오브젝트가 자신의 UV 좌표를 사용해 라이트맵에서 자기 자리에 해당하는 텍셀을 읽어 옵니다. 읽어 온 조명 값은 오브젝트의 표면 색(알베도, Albedo)과 곱해져 화면에 보이는 최종 색이 됩니다.
라이트맵의 구조
라이트맵은 표면의 각 지점에 도달한 빛의 밝기와 색을 텍셀 하나에 따로 저장합니다. 따라서 각 텍셀이 표면의 어느 지점에 해당하는지 알려 주는 좌표가 필요합니다. 이 좌표를 제공하는 것이 메쉬의 UV 좌표계이며, 라이트맵은 메인 텍스처가 쓰는 채널을 그대로 쓰지 않고 메쉬가 가진 다른 UV 채널을 사용합니다.
메쉬의 UV 좌표계가 3D 표면과 2D 텍스처를 연결하는 방식은 렌더링 기초 (2) - 텍스처와 압축에서 자세히 다룹니다.
라이트맵을 메인 텍스처와 같은 UV0으로 읽지 않는 이유는, 두 텍스처가 좌표를 사용하는 방식이 다르기 때문입니다. 메인 텍스처는 표면의 기본 무늬를 입히는 용도라서, 같은 좌표를 여러 표면에서 반복해서 읽어도 문제가 되지 않습니다. 만약 여러 표면이 같은 벽돌 무늬를 읽는다면, 각 표면에 같은 벽돌 패턴이 반복되어 보일 뿐입니다. 타일링이나 미러링을 UV0에서 자연스럽게 사용할 수 있는 이유도 여기에 있습니다.
반면 라이트맵은 표면의 각 위치가 받은 조명 결과를 저장합니다. 같은 벽돌 벽 안에서도 그늘에 가려진 부분과 햇빛을 받는 부분은 서로 다른 밝기를 가져야 합니다. 만약 두 위치가 라이트맵에서 같은 (u, v) 좌표를 가리키면, 하나의 텍셀이 두 위치의 조명 값을 동시에 대표해야 합니다. 이 경우 어느 한쪽 표면에는 맞지 않는 조명이 적용됩니다.
그래서 라이트맵에는 반복이나 겹침이 없는 별도의 UV가 필요합니다. 이 용도로 사용하는 채널이 두 번째 UV 채널인 UV1입니다. 다만 Unity C# API에서는 이 채널을 Mesh.uv2로 접근하므로, 채널 번호와 프로퍼티 이름이 한 칸씩 어긋나 보입니다.
베이크가 끝나면 벽, 바닥, 기둥처럼 여러 정적 오브젝트의 조명 결과가 하나의 라이트맵 안에 배치됩니다. 이때 라이트맵은 아틀라스처럼 쓰이며, 오브젝트마다 자신이 읽을 영역이 따로 배정됩니다. 각 오브젝트의 UV1은 그 배정된 영역만 가리키므로, 같은 라이트맵을 공유해도 다른 오브젝트의 조명 값을 읽지 않습니다. 다만 한 오브젝트에 배정된 영역이 너무 작으면 사용할 수 있는 텍셀 수가 부족해져, 그림자 경계나 밝기 변화가 거친 계단처럼 보일 수 있습니다.
라이트맵에는 정적 표면이 받을 조명 결과가 미리 저장됩니다. 이 결과에는 광원에서 표면으로 바로 도달하는 직접광(Direct Light)뿐 아니라, 다른 표면에 반사된 뒤 도달하는 간접광(Indirect Light)도 포함될 수 있습니다.
간접광은 장면의 분위기를 만드는 데 큰 영향을 줍니다. 예를 들어 붉은 벽에 닿은 빛이 다시 흰색 바닥으로 반사되면, 바닥에도 은은한 붉은 기운이 더해집니다. 이런 현상을 컬러 블리딩(Color Bleeding)이라고 합니다.
만약 이런 간접광을 런타임에 매 프레임 정확히 계산하려면 많은 광선 추적이나 복잡한 근사 계산이 필요합니다. 일반적인 실시간 렌더링에서는 부담이 크기 때문에, Unity는 베이크 과정에서 직접광과 간접광을 미리 계산해 라이트맵에 기록합니다.
런타임에는 셰이더가 라이트맵을 샘플링해 이미 계산된 조명 값을 읽기만 하면 됩니다. 덕분에 정적 환경에서는 색 번짐, 부드러운 밝기 변화, 간접광이 만든 공간감을 낮은 런타임 비용으로 표현할 수 있습니다.
베이크 라이팅의 제약
베이크 라이팅은 많은 조명 계산을 런타임 밖으로 옮기는 대신, 그 결과를 라이트맵 텍스처에 고정해 둡니다. 이 방식은 런타임 비용을 크게 줄여 주지만, 고정된 데이터라는 특성 때문에 몇 가지 제약이 생깁니다.
가장 기본적인 제약은 적용 대상입니다. 라이트맵은 특정 표면 위치가 받은 조명 값을 UV 좌표에 맞춰 저장합니다. 만약 오브젝트가 움직이거나 회전하면, 라이트맵에 기록된 조명 결과와 실제 표면 위치가 맞지 않게 됩니다. 그래서 캐릭터, NPC, 움직이는 소품 같은 동적 오브젝트에는 라이트맵을 그대로 적용하기 어렵습니다.
라이트맵은 텍스처 자원이므로 메모리도 사용합니다. 씬이 넓거나 정적 오브젝트의 표면이 많을수록 조명 값을 저장해야 할 영역이 늘어납니다. 여기에 그림자 경계나 간접광의 변화를 더 부드럽게 담으려면 더 많은 텍셀이 필요하므로, 라이트맵 해상도를 높이거나 라이트맵 장수를 늘려야 합니다. 예를 들어 압축하지 않은 1024×1024 RGBA 텍스처 한 장은 약 4MB를 차지하므로, 여러 장을 사용하는 씬에서는 라이트맵만으로도 수십 MB의 메모리를 사용할 수 있습니다.
베이크 시간도 고려해야 합니다. 직접광만 계산할 때보다 간접광의 반사까지 함께 계산할 때 처리량이 크게 늘어나고, 씬 구조나 라이트 배치를 수정하면 결과를 다시 베이크해야 합니다. 프로젝트 규모가 커질수록 수정 후 결과를 확인하기까지의 시간이 길어져, 작업 반복 속도에도 영향을 줍니다.
또한 라이트맵은 베이크 시점의 조명 상태를 저장한 결과입니다. 만약 런타임에 해가 이동하거나 라이트가 켜지고 꺼져야 한다면, 그 변화는 라이트맵에 자동으로 반영되지 않습니다. 이런 장면에서는 실시간 라이트, Mixed Lighting, 또는 별도의 연출용 조명 전략을 함께 고려해야 합니다.
이 제약은 동적 오브젝트를 정적 환경과 함께 렌더링할 때 특히 드러납니다. 캐릭터는 라이트맵 대상이 아니지만, 베이크된 환경 안에서 주변 조명과 어울려야 합니다. 만약 캐릭터가 밝은 복도에서 어두운 그늘로 이동했는데도 표면 밝기가 거의 변하지 않는다면, 주변 환경의 조명 상태와 맞지 않아 부자연스럽게 보입니다. 이 문제를 보완하는 장치가 Light Probe입니다.
Light Probe
라이트맵은 벽, 바닥, 기둥처럼 움직이지 않는 표면에 조명 결과를 붙여 두는 방식입니다. 반면 Light Probe는 씬 안의 여러 지점에 조명 샘플을 배치해, 그 위치 주변의 밝기와 색감을 저장합니다. 캐릭터나 움직이는 소품은 라이트맵을 직접 사용할 수 없지만, 현재 위치 주변의 프로브 값을 이용해 표면 밝기와 색을 주변 환경에 맞출 수 있습니다.
Light Probe의 역할
Light Probe는 씬 안의 특정 위치에서 주변 조명이 어떤 상태인지 미리 측정해 둔 지점입니다. 베이크 단계에서는 각 프로브 위치를 기준으로, 어느 방향에서 어떤 색과 밝기의 빛이 들어오는지를 계산합니다.
프로브 하나가 모든 방향의 빛을 세밀한 텍스처처럼 저장한다면 데이터가 너무 커집니다. 그래서 Unity는 이 조명 분포를 몇 개의 숫자로 요약해 저장합니다. 이때 사용하는 표현 방식이 구면 조화 함수(Spherical Harmonics, SH)입니다.
SH는 방향별 조명을 아주 정밀하게 저장하는 방식은 아니지만, 여러 방향에서 들어오는 부드러운 환경광과 간접광을 적은 데이터로 표현하는 데 적합합니다. 대신 날카로운 그림자나 뚜렷한 하이라이트처럼 고주파 변화가 큰 조명에는 적합하지 않습니다.
런타임에 동적 오브젝트가 이동하면, Unity는 오브젝트 주변의 프로브 값을 보간(Interpolation)하여 현재 위치의 조명을 근사합니다. 오브젝트가 어느 프로브들 사이에 있는지에 따라 각 프로브의 영향이 섞이고, 그 결과로 해당 위치에서 사용할 SH 계수가 만들어집니다.
이렇게 구한 SH 계수는 오브젝트의 렌더링에 사용됩니다. 셰이더는 이 값을 이용해 표면 법선 방향에 맞는 주변 조명을 계산하고, 동적 오브젝트의 밝기와 색을 주변 환경에 맞춥니다.
예를 들어 캐릭터가 밝은 복도에서 어두운 그늘로 이동하면, 밝은 쪽 프로브의 영향은 줄고 어두운 쪽 프로브의 영향은 커집니다. 그 결과 캐릭터의 표면 밝기와 색도 위치에 따라 부드럽게 바뀌며, 라이트맵 대상이 아닌 동적 오브젝트도 베이크된 환경 조명과 자연스럽게 어울릴 수 있습니다.
구면 조화 함수(Spherical Harmonics)
구면 조화 함수(Spherical Harmonics, SH)는 방향에 따라 달라지는 값을 적은 수의 계수로 근사하는 방법입니다. Light Probe에서는 한 지점으로 들어오는 빛의 방향별 밝기와 색을 이 계수들로 표현합니다.
직관적으로는 사방에서 들어오는 조명을 몇 개의 숫자로 요약하는 방식에 가깝습니다. Unity의 Light Probe는 보통 L2(2차) SH를 사용하며, 이 경우 빨강, 초록, 파랑 각 색상 채널의 조명 분포를 9개의 계수로 나누어 표현합니다. 이 정도의 정보만으로도 환경광이나 간접광처럼 부드럽게 변하는 조명은 비교적 자연스럽게 표현할 수 있습니다.
SH 계수는 데이터 크기가 작기 때문에 여러 프로브 값을 섞어 쓰기 쉽습니다. 런타임에는 오브젝트 주변 프로브의 SH 계수를 보간해 현재 위치의 조명 값을 만들고, 셰이더는 표면 법선 방향에 맞는 조명 값을 계산합니다. 이 과정은 여러 실시간 라이트를 직접 계산하는 것보다 비용이 낮습니다.
Light Probe의 비용과 한계
Light Probe의 런타임 비용은 비교적 낮습니다. 런타임에는 주변 프로브의 SH 계수를 보간하고, 셰이더에서 표면 법선 방향의 조명 값을 계산하면 됩니다. 여러 실시간 라이트를 오브젝트마다 직접 계산하는 것보다 훨씬 가벼운 방식입니다.
대신 표현할 수 있는 조명에는 한계가 있습니다. SH L2는 부드럽게 변하는 저주파 조명 성분을 근사하는 데 적합하지만, 날카로운 그림자 경계나 좁은 스포트라이트처럼 변화가 급격한 조명은 정확히 표현하기 어렵습니다.
따라서 Light Probe는 부드러운 환경광(Ambient)과 간접광을 동적 오브젝트에 전달하는 역할에 적합합니다. 정밀한 직접광, 선명한 그림자, 강한 하이라이트가 필요하다면 실시간 라이트나 실시간 그림자로 보완해야 합니다.
프로브 배치 밀도도 결과 품질에 큰 영향을 줍니다. 밝은 복도에서 어두운 방으로 넘어가는 지점처럼 조명이 급격히 변하는 구간에는 프로브를 촘촘하게 배치해야 보간 결과가 자연스럽습니다. 반대로 조명 변화가 적은 넓은 공간에서는 프로브 간격을 넓혀도 큰 문제가 없습니다.
Reflection Probe
Light Probe가 주변 조명의 밝기와 색을 동적 오브젝트에 전달한다면, Reflection Probe는 주변 환경이 표면에 비치는 반사 정보를 제공합니다. 금속, 유리, 물처럼 반사가 중요한 재질은 단순한 조명 밝기만으로는 충분하지 않으므로, 별도로 주변 환경을 캡처해 반사에 사용합니다.
환경 반사의 표현
금속, 유리, 물처럼 반사가 강한 재질은 표면에 주변 장면이 비쳐야 자연스럽게 보입니다. 하지만 반사되는 장면은 표면의 위치와 방향에 따라 달라지므로, 이를 매번 정확히 계산하려면 반사가 필요한 지점에서 주변 환경을 다시 렌더링해야 합니다. 이 작업을 실시간으로 반복하면 비용이 매우 커집니다.
Reflection Probe는 이 비용을 줄이기 위해, 특정 위치에서 주변 환경을 큐브맵(Cubemap)으로 캡처해 저장합니다. 큐브맵은 그 위치를 기준으로 위, 아래, 좌, 우, 앞, 뒤 여섯 방향의 장면을 담은 텍스처입니다.
런타임에는 반사가 필요한 표면에서 반사 방향을 계산하고, 해당 위치에 영향을 주는 Reflection Probe의 큐브맵을 그 방향으로 샘플링합니다. 즉, 주변 장면을 매번 다시 렌더링하는 대신, 캡처해 둔 환경 텍스처를 조회해 반사 색상을 얻는 방식입니다.
베이크 vs 실시간
Reflection Probe는 큐브맵을 언제 생성하고 갱신하느냐에 따라 비용과 표현 가능 범위가 달라집니다.
베이크 모드에서는 에디터에서 큐브맵을 생성해 저장합니다. 런타임에는 이미 만들어진 큐브맵을 샘플링하기만 하므로 비용이 낮습니다. 대신 반사에 비치는 장면은 캡처 시점의 모습으로 고정되며, 런타임에 오브젝트가 이동하거나 조명이 바뀌어도 자동으로 반영되지 않습니다.
실시간 모드에서는 런타임에 프로브 위치에서 주변 장면을 다시 렌더링해 큐브맵을 갱신합니다. 큐브맵은 여섯 면으로 이루어진 텍스처이므로, 새 큐브맵을 만들려면 위, 아래, 좌, 우, 앞, 뒤 방향의 장면을 각각 렌더링해야 합니다.
실시간 모드는 환경 변화가 반사에도 반영된다는 장점이 있지만, 큐브맵을 갱신할 때마다 메인 카메라 렌더링과 별도로 추가 렌더링이 발생합니다. 만약 프로브 하나를 매 프레임 갱신한다면, 매 프레임 여섯 방향의 렌더링이 추가됩니다. 프로브 수가 많거나 큐브맵 해상도가 높을수록 부담은 더 커집니다.
따라서 성능 예산이 제한된 프로젝트에서는 Reflection Probe를 기본적으로 베이크 모드로 두는 편이 안정적입니다. 동적으로 변하는 반사가 꼭 필요하다면 매 프레임 갱신을 기본값으로 두기보다, 갱신 주기를 길게 잡거나 특정 상황에서만 갱신하는 식으로 범위를 제한하는 편이 좋습니다. 큐브맵 해상도를 낮추는 것도 비용을 줄이는 방법입니다.
Mixed 라이팅
앞에서 살펴본 베이크 라이팅, Light Probe, Reflection Probe는 런타임 비용을 줄이는 데 효과적이지만, 모든 조명 문제를 해결하지는 못합니다. 정적 환경의 조명은 미리 계산해 비용을 줄이더라도, 캐릭터나 움직이는 오브젝트에는 런타임에 반응하는 조명과 그림자가 필요합니다. 이처럼 베이크 결과와 실시간 조명을 함께 사용하는 방식이 Mixed 라이팅입니다.
베이크와 실시간의 결합
라이팅 모드를 비교하면 Mixed 라이팅의 위치가 더 분명해집니다.
라이팅 모드 비교
| 모드 | 정적 오브젝트 | 동적 오브젝트 |
|---|---|---|
| Realtime | 실시간 조명 계산 | 실시간 조명 계산 |
| Baked | 라이트맵 사용 | Light Probe 등으로 간접 조명 보완 |
| Mixed | 라이트맵 또는 Shadowmask 사용 | 실시간 조명과 그림자 사용 |
Mixed 모드의 핵심은 하나의 라이트가 정적 오브젝트와 동적 오브젝트에 서로 다른 방식으로 적용된다는 점입니다. 정적 오브젝트에는 베이크된 조명과 그림자 정보를 사용하고, 동적 오브젝트에는 런타임에 계산되는 조명과 그림자를 적용합니다. 특히 그림자는 어떤 정보를 라이트맵이나 Shadowmask에 저장하느냐에 따라 비용과 품질이 크게 달라집니다.
Mixed 라이팅의 그림자 모드
Mixed 라이팅의 그림자 모드는 어떤 그림자를 베이크해 둘지, 어떤 그림자를 런타임에 계산할지 정하는 설정입니다. Unity에서는 이 선택을 Baked Indirect, Shadowmask, Subtractive 세 방식으로 나눕니다.
Baked Indirect는 간접광(GI)만 라이트맵과 Light Probe에 저장합니다. 라이트에서 표면으로 바로 도달하는 직접광은 런타임에 계산되고, 그림자도 실시간 Shadow Map으로 처리됩니다. 그래서 조명의 직접적인 밝기 변화는 반영하기 쉽지만, 정적 오브젝트의 그림자까지 매 프레임 다시 계산해야 하므로 그림자 비용이 큽니다. 그림자 예산이 부족한 환경에서는 Shadow Distance나 그림자를 생성하는 오브젝트 수를 제한해야 합니다.
Shadowmask는 Mixed 라이트가 정적 표면에서 얼마나 가려지는지를 미리 저장하는 방식입니다. 직접광 자체는 Baked Indirect처럼 런타임에 계산하지만, 정적 벽이나 기둥이 바닥에 만드는 그림자 정보는 베이크 단계에서 Shadowmask 텍스처에 기록합니다.
런타임에는 바닥 픽셀이 Shadowmask를 읽어 “이 라이트가 여기서는 얼마나 가려지는지”를 확인합니다. 예를 들어 벽 뒤쪽 바닥은 라이트가 적게 도달하는 값으로 저장되어 있으므로 더 어둡게 계산됩니다. 정적 오브젝트의 그림자 판정이 텍스처 조회로 대체되기 때문에, 해당 그림자를 위해 Shadow Map을 매 프레임 다시 렌더링하는 비용을 줄일 수 있습니다.
움직이는 캐릭터는 위치가 계속 바뀌므로 Shadowmask 텍스처에 고정해 둘 수 없습니다. 캐릭터가 바닥에 드리우는 그림자는 실시간 Shadow Map으로 처리하고, 캐릭터가 정적 그림자 영역 안에 들어갔을 때의 조명 변화는 주변 Light Probe에 저장된 가림 정보를 이용해 보완합니다.
따라서 Shadowmask는 실시간 그림자 렌더링 비용을 줄이는 대신, Shadowmask 텍스처와 Light Probe의 가림 정보를 추가로 저장해야 합니다. 그만큼 메모리 사용량은 Baked Indirect보다 늘어날 수 있습니다.
Shadowmask에는 Distance Shadowmask라는 거리 기반 설정도 있습니다. 이 설정은 카메라와 가까운 영역의 그림자는 Shadow Map으로 계산하고, Shadow Distance를 넘는 먼 영역은 Shadowmask에 저장된 베이크 그림자로 처리합니다. 가까운 그림자의 선명도는 높일 수 있지만 그만큼 Shadow Map 렌더링 비용이 다시 늘어나므로, 성능 예산이 작다면 일반 Shadowmask 설정을 우선 검토하는 편이 적절합니다.
Subtractive는 정적 환경의 조명을 최대한 라이트맵에 베이크해 두는 저비용 모드입니다. 이 모드에서는 벽, 바닥, 지형처럼 움직이지 않는 표면에 Mixed 라이트의 직접광, 간접광, 정적 그림자가 모두 라이트맵으로 기록됩니다. 캐릭터처럼 움직이는 오브젝트에는 직접광만 런타임에 계산하고, 주변의 간접광과 정적 그림자 영향은 Light Probe 값을 이용해 맞춥니다. 동적 오브젝트가 만드는 실시간 그림자는 메인 Directional Light 하나에 대해서만 제한적으로 처리됩니다. 예를 들어 캐릭터가 베이크된 바닥 위에 그림자를 드리우면, Unity는 해당 바닥 픽셀의 라이트맵 색을 더 어둡게 만들어 그림자가 생긴 것처럼 보이게 합니다. 이미 계산된 밝기에서 일부를 빼는 방식으로 그림자를 근사하기 때문에 이 모드를 Subtractive라고 부릅니다.
이 방식은 런타임 비용이 낮은 대신 표현 범위가 좁습니다. 런타임에 라이트가 켜지거나 꺼지는 변화, 여러 실시간 라이트의 그림자, 복잡한 그림자 합성에는 적합하지 않습니다. 따라서 사실적인 조명보다 저사양 환경이나 스타일화된 화면에 더 어울립니다.
성능 예산이 제한된 프로젝트의 Mixed 구성
성능 여유가 크지 않은 프로젝트에서는 모든 조명을 실시간으로 계산하기 어렵습니다. 이럴 때는 태양처럼 장면 전체에 영향을 주는 주요 Directional Light만 Mixed로 사용하고, 실내 조명이나 가로등 같은 보조 조명은 베이크하는 구성이 현실적입니다. 동적 오브젝트의 간접광은 Light Probe로, 금속이나 유리 표면의 환경 반사는 Reflection Probe로 보완합니다.
이 구성의 목적은 매 프레임 다시 계산해야 하는 조명을 최소화하는 것입니다. 주요 Directional Light와 꼭 필요한 동적 그림자만 실시간으로 남기고, 정적 조명과 정적 그림자, 간접광, 환경 반사는 베이크 데이터에서 읽습니다. 그 결과 여러 실시간 라이트를 반복 계산하는 부담을 줄이고, 비교적 가벼운 텍스처 샘플링과 Light Probe 평가로 대부분의 조명 결과를 얻을 수 있습니다.
실시간 라이트의 추가 고려 사항
베이크와 프로브를 사용하더라도 모든 실시간 라이트가 사라지는 것은 아닙니다. 남겨 둔 실시간 라이트는 프래그먼트 셰이더에서 계속 계산되므로, 라이트 수와 적용 범위를 함께 관리해야 합니다.
필레이트 제한과 프래그먼트 비용
필레이트 관점에서 보면, 실시간 라이트는 화면에 그려지는 프래그먼트마다 반복되는 조명 계산을 늘립니다. Directional Light 하나만 있으면 한 번의 조명 계산으로 끝나지만, 같은 픽셀에 Point Light나 Spot Light가 추가로 영향을 주면 그 라이트만큼 방향, 감쇠, 색상 합성이 반복됩니다. 그림자를 사용하는 라이트라면 Shadow Map 샘플링도 추가됩니다.
따라서 라이트 하나의 비용은 라이트 개수만으로 결정되지 않습니다. 해당 라이트가 화면의 얼마나 넓은 영역에 영향을 주는지, 그림자를 사용하는지, 그리고 그 영역에 오버드로우가 얼마나 있는지가 함께 비용을 만듭니다.
실시간 라이트 비용을 키우는 조건
| 조건 | 비용이 커지는 이유 |
|---|---|
| Directional Light 추가 | 화면 전체 프래그먼트에 조명 계산이 추가됨 |
| 범위가 큰 Point/Spot Light | 영향을 받는 화면 영역이 넓어짐 |
| 그림자 있는 라이트 | 조명 계산에 Shadow Map 샘플링이 추가됨 |
| 오버드로우가 많은 영역 | 같은 화면 위치에서 조명 계산이 여러 번 반복됨 |
따라서 실시간 라이트의 비용을 판단할 때는 개수뿐 아니라, 화면에서 차지하는 영향 범위와 그림자 사용 여부도 함께 고려해야 합니다.
필레이트 제한의 기본 개념은 게임 루프의 원리 (2) - CPU-bound와 GPU-bound에서 자세히 다룹니다. 모바일 GPU에서 필레이트와 오버드로우가 비용으로 이어지는 구조는 GPU 아키텍처 (2) - 모바일 GPU와 TBDR에서 자세히 다룹니다.
Point/Spot Light의 범위 최적화
Directional Light는 방향만 가진 광원이라 씬 전체에 영향을 줍니다. 반면 Point Light와 Spot Light는 지정된 범위 안에서만 조명을 계산합니다. 범위가 커질수록 더 많은 표면과 프래그먼트가 라이트의 영향을 받으므로, 프래그먼트 셰이더에서 수행되는 조명 계산도 늘어납니다.
Range는 단순히 라이트가 얼마나 멀리 닿는지를 정하는 값이 아니라, 조명 계산 후보가 되는 렌더러와 픽셀의 범위를 정하는 값입니다. Range를 필요 이상으로 크게 잡으면 실제로는 거의 보이지 않는 표면까지 라이트 영향권에 들어가고, 그만큼 조명 계산과 그림자 처리 후보도 늘어납니다.
따라서 Point Light와 Spot Light는 연출에 필요한 범위까지만 좁히는 편이 좋습니다. 비용을 판단할 때도 라이트 개수만 보지 말고, 각 라이트가 화면에서 얼마나 넓은 영역에 영향을 주는지 함께 확인해야 합니다.
Per Object Light Limit 제어
Range가 라이트 하나의 영향 영역을 줄이는 설정이라면, Per Object Light Limit은 오브젝트 하나가 받을 수 있는 추가 라이트 수를 제한하는 설정입니다. URP Forward 렌더링에서는 메인 Directional Light가 별도로 처리되고, Point Light와 Spot Light 같은 Additional Lights가 이 제한의 대상이 됩니다.
오브젝트 주변의 추가 라이트가 한도를 넘으면, URP는 거리와 밝기 등을 기준으로 영향이 큰 라이트를 우선 선택합니다. 예를 들어 Limit이 2이고 주변에 Point Light 5개가 있다면, 그 오브젝트에는 영향이 큰 2개만 전달되고 나머지 3개는 해당 오브젝트의 조명 계산에서 제외됩니다.
Per Object Light Limit을 낮출수록 셰이더가 한 오브젝트에 대해 계산하는 추가 라이트 수가 줄어듭니다. 그만큼 프래그먼트당 조명 계산이 줄어 ALU 비용을 낮출 수 있지만, 선택되지 않은 라이트의 영향은 해당 오브젝트에 적용되지 않습니다. 한도를 지나치게 낮추면 오브젝트가 받던 보조 조명이나 하이라이트가 사라져, 이동 중에 밝기 변화가 부자연스럽게 보일 수 있습니다.
라이팅 구성 요소 선택 기준
라이팅 구성의 핵심은 실시간으로 계산해야 하는 요소와 미리 계산해 둘 수 있는 요소를 구분하는 데 있습니다. Realtime, Mixed, Baked는 라이트 자체의 계산 방식을 정하고, Light Probe와 Reflection Probe는 베이크된 조명 정보를 동적 오브젝트와 반사 표현에 전달하는 보조 시스템입니다.
라이팅 구성 요소 비교
| 구성 요소 | 주된 역할 | 런타임 비용 | 적합한 사용 |
|---|---|---|---|
| Realtime 라이트 | 직접광과 그림자를 런타임에 계산 | 높음 | 움직이는 라이트, 실시간 변화가 중요한 연출 |
| Mixed 라이트 | 정적 표면은 라이트맵/Shadowmask, 동적 오브젝트는 실시간 조명 사용 | 중간 | 태양처럼 장면 전체에 영향을 주는 주요 라이트 |
| Baked 라이트 | 움직이지 않는 조명 결과를 라이트맵에 저장 | 낮음 | 실내 조명, 가로등처럼 고정된 보조 조명 |
| Light Probe | 동적 오브젝트에 베이크된 주변 조명 전달 | 낮음 | 캐릭터, NPC, 움직이는 소품의 밝기 보정 |
| Reflection Probe | 큐브맵으로 주변 환경 반사 제공 | 낮음 | 금속, 유리, 물 표면의 반사 표현 |
정리하면, 런타임에 변해야 하는 직접광과 그림자만 Realtime 또는 Mixed로 남기고, 고정된 조명, 간접광, 환경 반사는 라이트맵과 프로브에서 읽도록 구성하는 것이 기본 방향입니다.
베이크 작업 흐름
Unity에서 베이크 라이팅을 설정할 때는 정적 오브젝트 지정, 라이트 모드 선택, Lighting Settings 구성, 프로브 배치, 베이크 실행 순서로 진행합니다.
베이크를 실행한 뒤에는 결과를 확인하면서 라이트맵 해상도와 프로브 배치를 조정합니다. 그림자 경계가 흐릿한 표면은 라이트맵 해상도나 UV 배치를 확인하고, 동적 오브젝트의 밝기 전환이 어색한 구간은 Light Probe 밀도를 높입니다. 반대로 눈에 잘 띄지 않는 배경 오브젝트에 높은 라이트맵 해상도를 쓰고 있다면, 품질보다 메모리 비용만 늘어날 수 있습니다.
마무리
- 실시간 라이트는 파이프라인 구조에 따라 드로우콜 증가나 프래그먼트 셰이더 복잡도 증가로 비용을 만듭니다.
- 베이크 라이팅은 정적 표면의 조명 결과를 라이트맵에 저장해, 런타임 조명 계산을 텍스처 샘플링으로 대체합니다.
- Light Probe는 동적 오브젝트에 베이크된 주변 조명을 전달하고, Reflection Probe는 큐브맵으로 환경 반사를 제공합니다.
- Mixed 라이팅은 정적 표면에는 베이크 결과를 사용하고, 동적 오브젝트에는 필요한 실시간 조명을 남기는 절충안입니다. Shadowmask를 사용하면 정적 그림자의 실시간 비용을 더 줄일 수 있습니다.
이제 남는 큰 비용은 그림자입니다. 실시간 그림자는 Shadow Map을 만들기 위해 광원 시점의 렌더링 패스를 추가로 실행하므로, 조명 계산 자체보다 더 큰 부담이 될 수 있습니다.
Part 2에서는 그림자의 동작 원리와 비용, 그리고 후처리(Post-Processing)의 비용 구조를 다룹니다.
관련 글
시리즈
- 조명과 그림자 (1) - 실시간 조명과 베이크 (현재 글)
- 조명과 그림자 (2) - 그림자와 후처리
전체 시리즈
- 게임 루프의 원리 (1) - 프레임의 구조
- 게임 루프의 원리 (2) - CPU-bound와 GPU-bound
- 렌더링 기초 (1) - 메쉬의 구조
- 렌더링 기초 (2) - 텍스처와 압축
- 렌더링 기초 (3) - 머티리얼과 셰이더 기초
- GPU 아키텍처 (1) - GPU 병렬 처리와 렌더링 파이프라인
- GPU 아키텍처 (2) - 모바일 GPU와 TBDR
- Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조
- Unity 렌더 파이프라인 (2) - 드로우콜과 배칭
- Unity 렌더 파이프라인 (3) - 컬링과 오클루전
- 스크립트 최적화 (1) - C# 실행과 메모리 할당
- 스크립트 최적화 (2) - Unity API와 실행 비용
- 메모리 관리 (1) - 가비지 컬렉션의 원리
- 메모리 관리 (2) - 네이티브 메모리와 에셋
- 메모리 관리 (3) - Addressables와 에셋 전략
- UI 최적화 (1) - 캔버스와 리빌드 시스템
- UI 최적화 (2) - UI 최적화 전략
- 조명과 그림자 (1) - 실시간 조명과 베이크 (현재 글)
- 조명과 그림자 (2) - 그림자와 후처리
- 셰이더 최적화 (1) - 셰이더 성능의 원리
- 셰이더 최적화 (2) - 셰이더 배리언트와 키워드 관리
- 물리 최적화 (1) - 물리 엔진의 실행 구조
- 물리 최적화 (2) - 물리 최적화 전략
- 파티클과 애니메이션 (1) - 파티클 시스템 최적화
- 파티클과 애니메이션 (2) - 애니메이션 최적화
- 프로파일링 (1) - Unity Profiler와 Frame Debugger
- 프로파일링 (2) - 모바일 프로파일링
- 모바일 전략 (1) - 발열과 배터리
- 모바일 전략 (2) - 빌드와 품질 전략