저자: 欧几里得范数
《Call of Dragons》 렌더링 프레임 캡처 분석과 미스터리

0 들어가며
《Call of Dragons》는 LeGou(乐狗)에서 개발하고 Lilith(莉莉丝)에서 퍼블리싱한 인기 SLG 게임입니다. 이 게임 이전에도 같은 조합(LeGou 개발, Lilith 퍼블리싱)으로 또 다른 인기 SLG인 《Rise of Kingdoms》(통칭 ROK)가 있었기 때문에, 《Call of Dragons》는 흔히 ROK2라고도 불립니다.
다만 ROK가 순수 2D 렌더링 파이프라인에 가까웠던 것과 달리, 《Call of Dragons》는 3D 씬에 일부 2D 렌더링을 섞는 방식을 사용했고, 결과적으로 화면 품질과 성능을 꽤 잘 잡아낸 편입니다.
그리고 엔진/그래픽 프로그래머/TA 입장에서는 요즘 프레임 캡처 분석이 거의 필수 스킬이 되어버렸죠(그만 좀 굴리자… 이러다 glsl까지 손으로 찢게 생김). 그래서 저도 여기서 《Call of Dragons》의 렌더링 파이프라인 흐름을 한번 뜯어보려고 합니다.
개인 지식과 경험에는 한계가 있어 분석이 빠진 부분도 분명 있을 겁니다. 같이 머리 맞대고 토론해 주시면 좋겠고, 제가 틀린 부분이 있으면 편하게 지적해 주세요.
이 글에서 다루는 내용:
- 《Call of Dragons》의 한 프레임 안에서 각 Pass가 무엇을 그리는지.
- 프레임 캡처를 통해 확인된 최적화들(GPU 애니메이션, 아틀라스 등).
- 제가 보기에 아쉬운 점과 개선 가능해 보이는 부분(예: 그림자 CloseFit, Bloom 구현 등).
- 텍스처 정밀도/해상도 선택과 트레이드오프.
- “3D→2D(삼전이)” 방식이 부딪히는 문제와 개인적인 의문.
1 테스트 환경
- 에뮬레이터: MuMu 에뮬레이터 12 (V3.7.5)
- 프레임 캡처 도구: Intel GPA
- 게임: Call of Dragons (Version: 1.0.20.32:247868)
- 게임 내 그래픽 품질: 높음 (총 5단계: 매우 낮음, 낮음, 중간, 높음, 매우 높음)
- 하드웨어: i7-10700, RTX2060
2 개요
캡처한 장면은 《Call of Dragons》의 내성(내부 도시) 씬입니다.

본격적으로 들어가기 전에, 《Call of Dragons》 렌더링 파이프라인의 핵심 포인트를 간단히 요약하면 다음과 같습니다.
- 비교적 전통적인 포워드 렌더링
- 그림자 맵 정밀도 최적화
- 고정밀 LUT 사용
- 내성 건물은 3D→2D(삼전이) 2D 판 + 아틀라스 결합
- 3D 캐릭터는 GPU 애니메이션
- 2D 판 텍스처 정밀도는 비교적 높음
- Bloom은 전통적인 방식(최적화는 크게 안 보임)
- 안티앨리어싱을 사용하지 않는 것으로 보임
- UI 요소 다수는 Framebuffer에 직접 렌더링
이제 전체 파이프라인을 Pass 순서대로 보겠습니다.
3 메인 디렉셔널 라이트 그림자 맵 Pass
파이프라인의 첫 번째 Pass는 메인 디렉셔널 라이트의 ShadowCaster Pass입니다.
- 그림자 맵 크기: 2048×2048
- 그림자 맵 포맷: D16_UNORM
D24나 D32가 아니라 D16을 사용함으로써, 그림자 렌더링 비용을 줄이고 모바일 대역폭도 낮추는 최적화가 들어가 있습니다. 물론 이런 최적화는 “D16 정밀도가 충분한가?”를 함께 따져봐야 합니다.
이 Pass에서는 내성 주변의 산 등 3D 지형 오브젝트들이 주로 렌더링됩니다. 여기서 알 수 있는 점이 하나 있는데, 내성 건물은 2D 판(삼전이)이라서 그림자 맵에 렌더링되지 않습니다(삼전이는 뒤에서 조금 더 이야기하겠습니다). 또한 내성의 3D 캐릭터도 그림자 맵에는 포함되지 않는 것으로 보입니다.
그림자 맵 크기 자체는 일반적이지만, ShadowCaster의 프러스텀 범위가 꽤 넓어서 실제 유효 픽셀 사용률이 낮아 보입니다. CloseFit 같은 최적화는 적용되지 않은 듯하고, 쓸모없는 공간이 꽤 남습니다. 다만 이런 보수적인 방식은 오히려 일부 상황에서 안정성을 주는 면도 있겠죠(범위를 넘겨서 잘리는 문제를 피하기 쉬움).

4 실시간 LUT 베이킹 Pass
두 번째 Pass는 실시간 LUT 베이킹 Pass입니다.
- LUT 크기: 1024×32
정밀도가 꽤 높은 편입니다(성능만 생각하면 256×16 정도를 쓰는 경우도 많습니다).
LUT 실시간 베이킹은 Draw Call 1회로 끝나며, RT를 Clear하지 않습니다. RT 전체를 완전히 덮어쓰고 Blend도 필요 없으니 Clear가 필요 없는 게 맞습니다.
만약 런타임에서 LUT를 매 프레임 바꿀 필요가 없다면, 첫 프레임에만 베이킹하고 이후에는 생략하거나, Lazy 업데이트를 고려할 여지도 있습니다.

5 불투명(Opaque) 오브젝트 Pass
여기서부터는 메인 카메라 렌더링입니다. 이 Pass는 URP의 Draw Object Pass 중 Opaque 파트에 해당합니다.
- ColorAttachment: R11G11B10_Float (표준 HDR)
- 해상도: 1920×1080
- DepthAttachment: D24_UNORM_S8_UINT (깊이+스텐실)
- 해상도: 1920×1080


초반 2개의 렌더 커맨드는 ColorAttachment와 DepthAttachment를 Clear하는 일반적인 패턴입니다.

Opaque Pass에서는 먼저 내성 바닥의 돌들을 그리는데, 많은 돌들이 Draw Instance로 배치되어 있습니다. 그 다음 내성 주변 지면, 풀 등도 이어서 렌더링합니다.
내성의 삼각형 대부분은 주변 산/나무/돌 같은 환경 오브젝트에 몰려 있으며, 이 오브젝트들의 폴리곤 수는 최소 60면에서 최대 1.1만 면 수준입니다.
정점 Attribute를 보면 slot0는 Float32로 PositionOS를 저장하고, slot1/2/3는 Float16 정밀도를 사용합니다. UV, 노멀 등 일부 데이터를 저장하는 것으로 추정되며, 대부분의 오브젝트는 **노멀맵(대개 512×512)**을 사용해 라이팅 디테일을 보강합니다.
내성의 캐릭터는 3D 모델이며, 동일 Mesh에 대해 Draw Instance로 합쳐 그립니다.

내성의 소형 3D 캐릭터는 약 688면으로 폴리곤 수가 꽤 적습니다. Attribute는 slot0/slot1이 Float32, slot2/slot3이 Float16을 사용합니다. 구성은 PositionOS, Normal, Tangent, UV0, 본 정보 등으로 추정되지만 정확히는 불명입니다. 또한 캐릭터 텍스처에는 노멀맵이 없습니다.

캐릭터 애니메이션은 GPU 애니메이션을 사용합니다. 즉 텍스처에 프레임별 M 행렬 배열을 저장해두고, 매 프레임 프레임 인덱스로 텍스처를 샘플링해 해당 프레임의 M 행렬을 얻은 뒤, 버텍스 셰이더에서 스키닝을 수행합니다. CBuffer에서 _GPUSkinning2BoneInstancing 항목을 통해 이를 확인할 수 있습니다.
모바일 기준으로 보면 GPU 애니메이션은 이미 꽤 성숙한 방식이고, 경험적으로 CPU 시간, GPU 시간, 대역폭 측면에서 균형이 좋습니다.
반대로 전통적인 CPU 스키닝은 멀티스레딩으로 메시 애니메이션을 돌리는데, 기기의 멀티스레드 성능이 약하면 CPU 병목이 생기기 쉽습니다.
또 Unity의 기본 Compute Shader 스키닝은 대역폭 부담이 매우 크고, 일부 모바일에서는 Compute 지원이 썩 우호적이지 않아 발열/스로틀링으로 이어질 수 있어 모바일에서는 흔치 않게 채택됩니다.
아래는 텍스처에 저장된 프레임 애니메이션 M 행렬 배열의 예시입니다.

이후에는 Draw Instance로 대량의 풀/나무를 렌더링하며, 대략 20개 이상의 DrawCall이 사용됩니다.
풀/나무 인스턴스 렌더링 이후에는 내성의 “다른 3D 인물”(영웅 계열로 추정)을 추가로 렌더링합니다. 이쪽 텍스처는 256×256으로 비교적 낮은 편인데, 성능을 위해 텍스처 정밀도를 희생해 GPU 부하와 대역폭을 줄인 것으로 보입니다. 어차피 화면 점유율이 낮은 캐릭터라면 512×512가 크게 필요 없다는 판단도 합리적입니다.
6 Depth Copy Pass
내성의 3D 모델(돌/풀/나무/캐릭터) 렌더링이 끝난 뒤, Opaque 렌더 종료 시점에 DepthAttachment(D24)를 1920×1080 크기의 D16_UNORM 텍스처로 Copy합니다(Draw Call 1회).
이렇게 Copy한 깊이 텍스처는 이후 투명 오브젝트 등에서 카메라 깊이를 샘플링해 효과를 구현하는 용도로 보입니다.
이 구간에서는 RenderTarget 전환 비용이 있고, Color/Depth를 Store해야 하므로 비용이 생길 수밖에 없습니다. 대신 Copy 대상 포맷을 D16으로 해서 비용을 줄인 것으로 보입니다.

7 투명(Transparent) 오브젝트 Pass
RenderTarget을 다시 ColorAttachment+DepthAttachment로 되돌려(Load) 메인 카메라의 Transparent 파트를 렌더링합니다. 내성의 연못 같은 투명 오브젝트가 포함됩니다.

이어서 내성 건물 렌더링이 시작됩니다.
먼저 각 건물의 바닥(기단) 판을 렌더링합니다. 이 기단은 지면에 붙는 형태이고 billboard는 아니며, 그림자 맵을 샘플링해 그림자를 받습니다.

그 다음 내성 바닥의 도로를 렌더링하는데, 도로 텍스처가 512×1024로 꽤 큽니다. 도로는 화면 점유율이 커서 고정밀 텍스처가 필요하다는 판단일 가능성이 큽니다.
이후에는 Transparent Blend 방식으로 3D 캐릭터의 트릭(가짜) 그림자를 그립니다. 앞서 ShadowCaster Pass에서 캐릭터가 그림자 맵에 포함되지 않았기 때문에, 내성 바닥을 그릴 때 캐릭터 그림자가 없고, 여기서 Blend로 다시 “그림자만” 보충하는 형태로 보입니다.
왜 이런 방식을 택했는지는 확실치 않지만, 구현은 대략 다음과 같을 듯합니다.
- 버텍스 셰이더에서 입력 정점과 광원 방향을 이용해 평면에 투영
- 그림자 폴리곤을 만든 뒤
- 프래그먼트 셰이더에서 적절히 블렌딩

그 다음은 몇 가지 투명 파티클.

이어서 내성 건물 2D 판 렌더링이 들어가며, 대부분의 건물 판은 배치 합쳐짐(배치/합치기)이 되어 있습니다. 여러 건물 텍스처가 한 장의 텍스처에 들어가 있는데(아틀라스), 합치기를 위한 것으로 보입니다. 아틀라스 크기는 576×1016이며, 이 크기가 런타임에서 동적으로 바뀌는지는 확실치 않습니다.

아틀라스에서 각 건물 텍스처 가장자리가 좀 이상하게 보이는데, 테두리 한圈을 Clamp 처리한 듯한 형태입니다. 아마 샘플링이 영역 밖으로 나가 검은색을 찍는 문제를 방지하기 위해, 베이킹 단계에서 수동으로 Clamp 여유 픽셀을 넣은 것으로 추정됩니다.

각 건물에는 별도의 마스크 텍스처도 있으며, 채널별로 다른 마스크를 저장합니다. 역시 같은 크기의 아틀라스에 들어가 있습니다(예전에는 2D 노멀이라고 착각했지만, 실제로는 채널별 mask 데이터).

그 다음에는 관목 위 파티클 DrawCall들이 나오는데, 이쪽은 합치기가 되어 있지 않습니다. 크게 신경을 못 쓴 지점일 수도 있겠네요.

8 후처리(Post-Process) Pass
Transparent Pass가 끝나면 후처리로 넘어갑니다.
먼저 Bloom이 들어가며, half-res부터 시작해 다운샘플 해상도는 960×540 → 480×270 → 240×135 → 120×67 → 60×33 → 30×16 → 15×8 순으로 내려갔다가, 다시 업샘플로 올라옵니다.
이 과정 전체가 DrawCall 19회를 소비합니다.
Bloom 구현은 전형적인 방식으로 보이고, 큰 최적화는 보이지 않습니다. 예를 들어 원신처럼 다운샘플 텍스처들을 아틀라스에 합쳐 한 번의 블러로 처리하는 방식은 사용하지 않은 듯합니다(그 방식이면 DrawCall이 훨씬 줄어듦).
Bloom 이후에는 흔히 말하는 Uber 단계로 들어가며, LUT 적용과 Bloom 합성이 포함됩니다.
추가 DrawCall로 AA를 수행하는 흔적은 보이지 않아, 안티앨리어싱을 사용하지 않는 것으로 보입니다(화면이 전체적으로 날카롭고 깔끔함). 만약 있다면 FXAA 정도일 텐데, 여기서는 확인되지 않습니다.
또 하나 눈에 띄는 점은, Uber의 RenderTarget이 Framebuffer가 아니라 별도의 R11G11B10 ColorAttachment라는 점입니다.

후처리에서는 자잘하게 더 깎을 수 있는 포인트도 있습니다. 예를 들면, 전면 스크린 패스에서 쿼드(삼각형 2개) 대신 삼각형 1개로 렌더링하는 패턴입니다. 단일 삼각형은 캐시 히트율과 GPU 활용 측면에서 이점이 있는 편이죠.
예전에 《원신》 프레임 캡처에서는 전면 스크린 패스를 일관되게 단일 삼각형으로 처리하는 것을 봤는데, 《Call of Dragons》에서는 그런 최적화가 보이지 않았습니다.
9 UI Camera HUD Pass
후처리가 끝나면 UI 카메라 Pass로 넘어갑니다. 먼저 HUD 렌더링 Pass가 있고, RenderTarget은 계속 HDR ColorAttachment입니다.
여기서 좀 묘한 포인트가 하나 있습니다.

HUD 텍스처는 1024×1024 아틀라스를 사용합니다. 아틀라스의 장점은:
- 합치기(배치) 편의
- 런타임 메모리 최적화
예를 들어 UI를 화면(메뉴) 단위로 모듈화해, 같은 화면에서 쓰는 UI 이미지를 한 장의 아틀라스로 묶어두면, 화면 on/off에 따라 해당 아틀라스를 메모리에 로드/언로드할 수 있어 상주 메모리를 줄일 수 있습니다.

10 UI Camera UI Pass
HUD 다음에는 일반 UI를 렌더링합니다.
내성 HUD 렌더링이 끝난 뒤, 먼저 ColorAttachment를 R8G8B8A8_UNORM_SRGB 포맷의 Framebuffer로 Blit합니다.
일반적으로 URP의 Framebuffer 포맷이 R8G8B8A8_UNORM_SRGB인 것은, 프래그먼트 출력이 sRGB 변환을 거쳐 적절히 감마 공간으로 매핑되도록 하기 위한 선택입니다.

이어서 Framebuffer의 깊이/스텐실 버퍼를 Clear합니다. 깊이는 UI에서 쓰지 않을 가능성이 크고, 스텐실 값을 정리해 다음 UI 렌더에 쓰려는 의도로 보입니다.

그 다음부터는 UI를 Framebuffer에 직접 렌더링합니다.

대부분의 UI 텍스처 리소스는 아틀라스로 묶여 있으며, 2048×1024, 2048×4096 같은 큰 아틀라스도 보입니다. 전체적으로 UI 텍스처 정밀도는 높은 편입니다.

11 Built-in 출력
마지막 DrawCall에서 R8G8B8A8_UNORM_SRGB Framebuffer를 R8G8B8A8_UNORM 포맷 텍스처로 Blit하고, 최종적으로 화면에 출력합니다.
이 단계는 Built-in 성격의 동작으로, DX 레벨의 내장 DrawCall일 가능성이 큽니다.

12 (번외) 3D→2D(삼전이) 2D 방식에 대한 몇 가지 의문
《Call of Dragons》에서 특히 잘했다고 느낀 부분이 바로 **3D 모델을 2D로 “삼전이”**한 결과입니다. 내성 건물 같은 경우, 자세히 보지 않으면 2D 판이라는 걸 알아채기 어렵습니다.
삼전이의 핵심은 2D 판으로 3D 렌더링을 대체하는 트릭을 통해, 아트 품질을 크게 해치지 않으면서 렌더링 부하를 줄이는 것입니다.
이 포인트가 왜 중요하냐면… 누구나 한번쯤은 겪잖아요.
미술이 엄청 정교한 3D 모델을 만들어 왔는데, 성능이 안 나와서 결국 그래픽/TA가 최적화(감면)를 해야 하는 상황.
삼전이를 쓰면, 성능 최적화 단계에서 모델 감면을 두고 아트와 “배틀”할 일이 줄어듭니다. 바꿔서(전환해서) 해결해 버리는 거죠. 다 바꿀 수 있으면 다 바꾸면 됩니다.
《Call of Dragons》의 내성 건물 렌더링은 2D 판 방식을 쓰지만, 전형적인 케이스와 다른 점이 있습니다.
일반적으로 삼전이 2D 판은 정사영 카메라와 궁합이 좋습니다. 정사영에서는 2D 판이 화면 어느 위치에 있든 렌더 결과가 일관적이니까요. 대표적인 예가 《Clash of Clans》 같은 스타일입니다.

그런데 《Call of Dragons》는 원근(Perspective) 카메라로 렌더링합니다. 사실 이건 필연적이죠. 건물 2D 판 외에도 산, 나무, 강, 돌, 캐릭터 같은 3D 오브젝트가 대량으로 존재하고, 이들은 원근 투영이 필요하니까요(정사영으로 가면 《Clash of Clans》처럼 사방 정렬 렌더 스타일로 가야 함).
문제는 여기서 시작됩니다.
원근 카메라 + 삼전이 2D 판은 **시각적 들통(穿帮)**을 만들기 쉽습니다.
정리하면:
- 실제 3D 오브젝트는 원근이 강합니다(가까운 것은 크고, 먼 것은 작음)
- 삼전이 2D 판은 베이킹이 정사영 기반이라 원근 정보가 약합니다(거리감이 덜함)
- 카메라가 움직이거나, 3D와 2D 판이 함께 있고 참조가 명확할수록, “투영 규칙이 서로 다르다”는 것이 눈에 띕니다
원근이 강해지는 사례를 하나 들면, FPS 게임에서 FOV를 크게 올렸을 때입니다(Apex, CSGO, Overwatch 등). FOV가 커지면 시야에 들어오는 것은 많아지지만, 화면 중심 근처 오브젝트는 더 작아 보이고, 화면 가장자리 오브젝트는 바깥으로 늘어나며, 3D 공간에서의 평행선 수축이 강해집니다. 원근 투영이 강해지기 때문이죠.
생활 예시로는 사진 촬영이 있습니다. 대략 50mm 전후의 초점거리가 사람 눈에 가까운 인상이라고 흔히 말하지만, 어떤 사진가는 더 강한 긴장감(원근감)을 위해 광각을 선택합니다. 아래 사진을 보면 (3D 공간에서) 평행한 선들이 강하게 수축하며 각도 차가 커지는 것을 볼 수 있습니다.

그럼 삼전이 2D 판이 원근 카메라에서 들통나는 이유를 조금 더 “그림 관점”으로 정리해 보겠습니다(미술 지식이 빈약해서 설명이 다소 추상적일 수 있습니다).
2D 판을 사용하려면, 베이킹 시 카메라는 정사영이어야 합니다. 그래서 베이킹된 결과에서 원래 평행한 선은 계속 평행합니다. 즉 실제로는 소실점이 존재하지 않습니다(그림으로 치면, 평행선의 소실점이 무한대에 있다고 보는 셈).
반면 게임에서 실제로 렌더링되는 3D 오브젝트는 원근 카메라 때문에, 평행선이 결국 같은 소실점으로 수렴합니다.
그래서 씬에 이런 모순이 생깁니다.
- 3D 오브젝트의 평행선은 소실점으로 모임
- “원래는 3D였어야 하지만” 2D 판으로 트릭 처리된 오브젝트의 평행선은 끝까지 평행
이 둘이 같은 화면에 함께 있으면, 투영 규칙이 다르기 때문에 소실점이 어긋나며 어색함이 생깁니다.
또 정사영 기반의 2D 판과 원근 투영 3D 오브젝트는 카메라 이동에 따른 화면상 이동량도 달라져서, 참조가 있을 때 시차가 어색하게 보이는 문제가 더 두드러질 수 있습니다.
그럼 이 문제를 어떻게 완화할 수 있을까요?
가장 단순한 처방은 원근 카메라의 FOV를 낮추는 것입니다. FOV를 낮추면 원근 카메라가 정사영에 가까워지고, 원근감이 약해지면서 들통도 줄어듭니다(완전히 해결되지는 않음).
다만 FOV가 낮아지면 같은 조건에서 화면에 담기는 범위가 줄어드므로, 비슷한 구도를 유지하려면 카메라를 더 멀리 빼야 합니다. 결국 이 방법의 핵심 난제는 FOV와 카메라 위치를 어떻게 같이 튜닝할 것인가입니다.
《Call of Dragons》의 프레임 캡처에서 VP 행렬로 FOV를 역추정해 보면(추정 방식이 엄밀하진 않습니다),
- 내성 기본 시점 메인 카메라 FOV ≈ 6.9
- 대월드 메인 카메라 FOV ≈ 30
정도로 보이며, 내성에서 대월드로 넘어갈 때 FOV를 보간해 전환하는 것으로 추정됩니다.
여기서 내성 FOV가 극단적으로 낮은 이유 중 하나가, 삼전이 2D 판의 들통을 줄이기 위한 선택일 가능성이 큽니다.
참고/인용
- Google Play: Clash of Clans
- Epic Games Store: Call of Dragons
로그
- 2024/2/5 업데이트: 이전에는 2D 판 건물에 2D 노멀 텍스처가 쓰였다고 오해했지만, 실제로는 채널별 mask 데이터였음. 또한 삼전이 2D 판의 문제 현상/원인 설명을 재정리함(예전에는 렌즈 가장자리 왜곡으로 오해했음).
- 2024/2/24 업데이트: 《Call of Dragons》 및 《Rise of Kingdoms》의 개발/퍼블리셔 정보 수정.
원문
(59 封私信 / 34 条消息) 《Call of Dragons》渲染截帧分析与迷思 - 知乎
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] Impostors 상세 해설 — 종이 한 장으로 만들어낸 아름다운 환상 (0) | 2026.02.17 |
|---|---|
| [번역] Unity CSM 셰도우 개조하기 (0) | 2026.02.13 |
| [번역] Unity-URP-원신 스타일 프로시저럴 스카이박스(사례 정리) (2) | 2026.02.10 |
| [번역] REAC2025《오버워치 2》의 GI 솔루션 (0) | 2026.02.09 |
| [번역] 어떤 신입의 바이트덴스 게임 엔진 파트 인터뷰 경험담 (4) | 2026.02.04 |