게임 프로그래밍에선 카메라를 잘 이해해야 한다
새 프로젝트를 열면 씬에 카메라가 하나 놓여 있다. 대부분은 그걸 그냥 쓴다. 위치 옮기고, 각도 맞추고, 나머지 값은 손대지 않는다. 나도 한동안 그랬다. 인스펙터에 있는 숫자들이 뭔가 중요해 보이긴 하는데, 건드릴 이유를 못 찾았다.
그러다 프레임이 안 나와서 프로파일러를 켜고 한참을 헤맨 적이 있다. 셰이더를 의심하고, 배칭을 의심하고, 텍스처 용량을 의심했다. 정작 답은 카메라 인스펙터 위쪽에 있었다.
카메라는 화면을 보여주는 창이 아니다. 무엇을 그리지 않을지 결정하는 필터다. 이 관점 하나만 바꿔도 최적화의 출발점이 완전히 달라진다.
값 하나가 드로우콜을 반으로 줄인다
Unity Korea의 알쓸유잡 카메라와 컬링 편에 실내 씬 예시가 나온다. 컬링 전에는 렌더러가 900개쯤 잡히는데, 오클루전 컬링을 켜면 400개 수준으로 떨어진다. 그런데 화면에 보이는 그림은 두 경우가 사실상 같다.
절반이 사라졌는데 눈에는 티가 안 난다. 이게 카메라를 이해해야 하는 이유의 거의 전부다. 시각적 손실 없이 비용만 걷어내는 최적화는 생각보다 흔하지 않다. 보통은 뭔가를 포기해야 뭔가를 얻는다.
같은 영상에 far clip 조정 사례도 있다. 렌더러 2,000개가 나오던 화면에서 far를 줄이니 1,300개 수준이 됐다. 코드 한 줄 안 쓰고, 인스펙터 숫자 하나 고친 결과다.
카메라가 하는 일은 결국 좌표 변환이다
카메라가 오브젝트를 “비춘다"는 말은 결과만 설명한 표현이다. 내부에서는 매 프레임, 매 버텍스마다 공간 변환이 돈다.
| 단계 | 공간 | 하는 일 |
|---|---|---|
| 1 | 모델(로컬/오브젝트) 공간 | 메시 자체의 원점 기준 좌표 |
| 2 | 월드 공간 | 씬에 배치된 절대 좌표로 이동 |
| 3 | 뷰(카메라) 공간 | 카메라 기준 상대 좌표로 재정렬 |
| 4 | 클립 공간 | 원근 투영을 거쳐 화면 좌표계로 압축 |
셰이더에서 흔히 보는 TransformObjectToHClip 같은 함수가 이 과정을 한 덩어리로 묶어놓은 것이다. 오브젝트 공간(OS)을 클립 공간(CS)으로 바꾸는 함수 하나 안에 오브젝트→월드, 월드→뷰, 뷰→프로젝션 행렬이 전부 들어 있다.
이 순서를 굳이 외워두라고 말하는 이유가 있다. 셰이더를 직접 만지게 되는 순간이 오는데, 그때 이 흐름을 모르면 디버깅이 아예 불가능하다. 오브젝트가 엉뚱한 위치에 그려질 때 어느 단계에서 틀어졌는지 짚을 수가 없다. 나는 이걸 모른 채로 버텍스 셰이더를 건드리다가 반나절을 날린 적이 있다. 화면 밖으로 사라진 메시를 붙잡고 “왜 안 보이지"만 반복했다.
near/far clip은 시야가 아니라 예산이다
프러스텀은 카메라 앞에 놓인 사각뿔이다. 정확히는 near clip과 far clip으로 앞뒤가 잘려나간 사각뿔대다. 이 부피 안에 들어온 것만 그린다.
far를 크게 잡으면 멀리 있는 것까지 전부 그린다. 그래서 모바일 게임에서는 far를 꽤 공격적으로 줄인다. 다만 그냥 줄이면 오브젝트가 툭툭 사라지는 게 그대로 보인다. 팝핑이다.
해결책은 포그와 짝을 맞추는 것이다. 포그의 끝점과 far clip을 비슷하게 세팅하되, 완전히 같게 두면 팝핑이 남으니 10% 정도 여유를 준다. 오브젝트가 포그 안에서 서서히 흐려지다가 사라지게 만드는 식이다.
순서로 정리하면 이렇다.
- far clip을 게임이 실제로 필요한 시야 거리까지 줄인다
- 포그 끝점을 far clip보다 조금 앞에 둔다
- 오브젝트가 포그 안에서 자연스럽게 사라지는지 확인한다
- 시야가 답답해지지 않는 최소값을 찾는다
4번이 핵심이다. 여기엔 정답이 없다. 오픈월드처럼 월드가 큰 게임은 이 기법을 안 쓰면 답이 없고, 반대로 좁은 실내 게임에서 far를 과하게 줄이면 공간감이 죽는다. 숫자를 남에게 물어볼 게 아니라 직접 눈으로 보면서 내려야 하는 결정이다.
컬링은 공짜가 아니다
여기서 오해가 많다. 컬링은 비용을 줄이는 기능이니까 많이 할수록 좋다고 생각하는 것이다.
컬링 자체도 연산이다. 다만 오브젝트 개수에 비례해서 선형으로 늘지는 않는다. 엔진이 공간 분할을 쓰기 때문이다. 2D는 쿼드트리로 4등분, 3D는 옥트리로 8등분해서 재귀적으로 쪼갠다. 블록 하나가 프러스텀 밖이면 그 안에 든 오브젝트는 통째로 건너뛴다. 개별로 검사하는 게 아니라 덩어리로 쳐낸다.
문제는 동적 오브젝트다. 오브젝트가 움직이면 공간 분할 데이터를 계속 갱신해야 한다. 영상에서도 “생각보다 많이 먹지는 않지만 그렇다고 빠르지도 않다"고 표현한다.
나는 이 문장이 이 주제에서 제일 실용적인 조언이라고 본다. 애매하게 들리지만, 애매한 게 사실이니까. 결론은 프로파일러다. 움직이는 오브젝트가 빽빽한 씬이라면 컬링 비용이 프로파일에 실제로 잡힌다. 추측하지 말고 찍어보는 게 맞다.
오클루전 컬링은 실내에서만 값을 한다
프러스텀 컬링이 ‘시야 밖’을 걸러낸다면, 오클루전 컬링은 ‘앞의 무언가에 가려진 것’을 걸러낸다. 벽 뒤에 있는 캐릭터는 프러스텀 안에 있지만 보이지 않는다. 그걸 안 그리는 게 오클루전 컬링이다.
실내 씬에서는 이게 강력하다. 벽과 구조물이 서로를 계속 가리기 때문이다. 반대로 야외에서는 가리는 물체 자체가 적어서 컬링 성공률이 낮고, 연산 비용만 더 나오는 경우가 많다.
| 구분 | 실내(Indoor) | 야외(Outdoor) |
|---|---|---|
| 가림 발생 빈도 | 높음 | 낮음 |
| 오클루전 컬링 효과 | 큼 | 작거나 역효과 |
| 권장 대안 | 포탈 조합 | far clip + LOD 컬링 |
“오클루전 컬링을 켰는데 오히려 느려졌다"는 얘기가 나오는 이유가 여기 있다. 야외 씬에 적용했거나, 공간 분할 파라미터를 못 맞춘 경우다.
영상에서는 이 파라미터 튜닝을 ‘매직 넘버 찾기’라고 부른다. 공간을 너무 크게 자르면 컬링 효율이 떨어지고, 너무 잘게 자르면 데이터와 연산 오버헤드가 늘어난다. 공식이 없고 휴리스틱하게 찾아야 한다. 디바이스마다 다르고 씬마다 다르니 결국 경험치가 쌓여야 하는 영역이다. 이런 건 문서를 읽는다고 해결되지 않는다. 몇 번 굽고, 몇 번 재보는 수밖에 없다.
문 같은 동적 요소는 포탈 컴포넌트로 처리한다. 문이 열리면 포탈을 열고 닫으면 닫아서, 문 뒤 공간의 컬링 여부를 런타임에 토글한다. 오클루더 자체를 움직이는 건 어렵지만 이 정도는 커버가 된다.
컬링 마스크는 컬링을 줄여주지 않는다
이름 때문에 착각하기 딱 좋은 기능이다. 나도 처음에는 레이어로 걸러낸 다음 남은 것만 컬링한다고 생각했다. 이름에 ‘컬링’이 들어 있으니 당연히 그럴 줄 알았다.
순서가 반대다. 컬링을 먼저 하고, 그다음에 레이어 필터링을 한다. 컬링 마스크로 레이어를 빼도 컬링 연산은 이미 다 돌아간 뒤다. 성능이 좋아지지 않는다. 컬링 마스크는 “무엇을 보여줄지” 고르는 필터일 뿐이다.
그래서 카메라를 여러 개 두는 구조는 신중해야 한다. 카메라마다 컬링이 따로 돈다. UI 카메라, 캐릭터 카메라, 배경 카메라를 나눠놓으면 컬링도 그 횟수만큼 일어난다. 각 카메라의 컬링 마스크를 아무리 좁게 잡아놔도 마찬가지다.
멀티 카메라 대신 시네머신을 쓰는 게 낫다. 시네머신의 버추얼 카메라는 실제 카메라가 아니라 실제 카메라를 조종하는 가상 객체다. 렌더링도 컬링도 하지 않으니 여러 개 써도 부담이 다르다. 연출용, 게임용 카메라를 실제 카메라로 여러 대 만들어두던 습관은 여기서 정리하는 게 좋다.
물론 카메라 스태킹처럼 실제 카메라가 꼭 여러 개 필요한 경우도 있다. 이때 원칙은 하나다. 프러스텀이 서로 겹치지 않게 배치할 것. 겹치면 같은 오브젝트를 두 카메라가 각각 컬링한다. 유니티가 제공하는 3D 스카이박스 예제도 두 카메라를 아주 멀리 떨어뜨려 놓는다.
LOD로 아예 안 그리는 것도 컬링이다
멀리 있는 오브젝트를 저해상도 메시로 바꾸는 게 LOD의 기본이지만, 일정 거리 이후에는 아예 안 그리는 설정도 가능하다. LOD 마지막 단계를 Culled로 두는 것이다. 영상 사례에서는 렌더러 230개가 150개 수준으로 줄어드는데, 육안으로는 차이를 느끼기 어렵다.
당연하다. 멀리 있는 물체는 화면에서 몇 픽셀 수준이니까. 다만 이때 유독 팝핑이 눈에 띄는 오브젝트가 있다. 실루엣이 크거나 색이 튀는 것들이다. 그런 것만 골라서 거리를 조정하면 시각적 손실 없이 성능만 가져갈 수 있다.
야외 씬에서는 오클루전 컬링보다 이쪽이 훨씬 효율적이다. 앞의 표에서 말한 ‘권장 대안’이 바로 이 조합이다.
카메라 세팅은 최적화이자 연출이다
여기까지는 성능 얘기였다. 그런데 카메라는 게임의 성격을 규정하는 장치이기도 하다.
탑다운 뷰가 대표적이다. 예전에 탑다운 시점 게임이 많았던 이유 중 하나는 오브젝트를 덜 그리기 위해서였다. 위에서 내려다보면 하늘을 볼 일이 없으니 스카이박스도 뺄 수 있다. 렌더링 비용을 통째로 아끼는 선택이다. 지금 보면 그 시대의 장르 특징처럼 보이지만, 사실은 기술적 제약이 만든 미학이었던 셈이다.
지금은 디바이스 성능이 올라가서 모바일에서도 3인칭 카메라를 많이 쓴다. 그렇다고 무조건 낮추는 것도, 무조건 올리는 것도 답이 아니다. 타깃 디바이스와 게임 장르에 맞게 카메라를 설계하는 것이 시작점이다.
원근 투영(Perspective)과 직교 투영(Orthographic)의 선택도 마찬가지다. 3D 게임은 원근법이 적용된 퍼스펙티브를 쓰고, 2D는 직교를 쓴다. 이건 성능 문제가 아니라 게임이 어떤 세계로 보이길 원하는가의 문제다.
체크리스트
작업 중인 씬에 바로 적용해볼 수 있는 항목만 뽑았다.
- far clip이 실제 필요한 시야 거리보다 과하게 크지 않은가
- 포그 끝점과 far clip이 팝핑 없이 이어지는가
- 실내 씬인가? 그렇다면 오클루전 컬링을 검토했는가
- 오클루전 컬링을 켠 뒤 프로파일러로 실제 이득을 확인했는가
- 실제 카메라가 두 대 이상인가? 시네머신으로 대체 가능한가
- 카메라가 여러 대라면 프러스텀이 서로 겹치지 않는가
- 컬링 마스크를 성능 최적화 수단으로 착각하고 있지 않은가
- LOD 최종 단계에서 컬링(Culled) 처리를 했는가
- 탑다운/쿼터뷰라면 스카이박스가 굳이 필요한가
마치며
- 카메라는 화면을 보여주는 컴포넌트가 아니라 무엇을 그리지 않을지 결정하는 컴포넌트다.
- 프러스텀 컬링은 기본으로 동작하니, 개발자가 손댈 지점은 near/far clip과 포그의 조합이다.
- 오클루전 컬링은 실내에서 강하고 야외에서는 역효과가 날 수 있다. 야외는 LOD 컬링이 답이다.
- 컬링 마스크는 컬링 이후에 동작하므로 성능 최적화 수단이 아니다. 멀티 카메라 대신 시네머신을 쓰는 편이 낫다.
- 컬링 자체도 비용이다. 어떤 설정이든 프로파일러로 확인하고 넘어가야 한다.
자주 묻는 질문
Q. 프러스텀 컬링은 따로 켜야 하나?
아니다. 엔진에서 기본으로 동작한다. 개발자가 조정할 수 있는 부분은 프러스텀의 크기, 즉 near/far clip과 FOV다.
Q. 오클루전 컬링을 켰는데 오히려 프레임이 떨어졌다.
두 가지를 확인해보자. 야외 씬이라면 가리는 물체가 적어 컬링 성공률이 낮고 연산만 늘어난다. 실내인데도 느려졌다면 공간 분할 파라미터가 씬 규모에 안 맞을 가능성이 크다. 이 값은 공식이 없어서 여러 번 시도해 찾아야 한다.
Q. 컬링 마스크로 레이어를 빼면 성능이 좋아지나?
좋아지지 않는다. 유니티는 컬링을 먼저 수행하고 그다음에 레이어 필터링을 한다. 컬링 마스크는 무엇을 보여줄지 고르는 필터일 뿐, 컬링 연산을 줄이지 않는다.
Q. 카메라를 여러 개 쓰면 정말 느려지나?
카메라마다 컬링이 독립적으로 수행되니 그만큼 비용이 늘어난다. 연출용으로 여러 시점이 필요하다면 시네머신 버추얼 카메라를 쓰는 게 낫다. 버추얼 카메라는 렌더링도 컬링도 하지 않는다.
Q. far clip을 줄였더니 오브젝트가 갑자기 사라진다.
포그를 함께 쓰면 된다. 포그의 끝점을 far clip보다 조금 앞에 두면 오브젝트가 서서히 흐려지다 사라진다. 완전히 같은 값으로 맞추면 팝핑이 남을 수 있어서 10% 정도 여유를 두는 방식이 쓰인다.
Q. 컬링은 렌더링 외에 물리나 업데이트에도 영향을 주나?
아니다. 여기서 다룬 컬링은 렌더링에 한정된 개념이다. 매 프레임 Update나 물리 연산은 컬링과 무관하게 계속 수행된다.
