본문으로 건너뛰기
뒤로가기

일러스트풍 3D가 FPS 카메라에서 버티는 조건: 외곽선 두 번, 텍스처 다섯 번, 그림자 한 번 갈아엎고 남은 것

TL;DR — 풀 3D 장면을 Pixiv풍 일러스트처럼 보이게 만드는 가장 큰 함정은 “예쁜 스크린샷”이 만들어진다는 점입니다. FPS 카메라로 들어가는 순간 외곽선·텍스처·그림자가 차례로 무너집니다. 오브젝트별 외곽선은 화면 후처리 Sobel(depth+normal)로, 자동 텍스처 후처리는 authored 텍스처로, 검정 그림자는 receiver material의 shadow_color mix로 갈아치우고 나서야 화면이 묶였습니다. 이 글은 그 수렴 과정과 채택된 5개의 룰입니다.

Table of contents

Open Table of contents

들어가며

Pixiv풍 일러스트는 평면적인 색면, 선명한 외곽선, 의도적으로 설계된 그림자, 부드럽지만 선명한 팔레트가 매력입니다. 정지 이미지에서는 비교적 잘 성립합니다. 문제는 플레이어가 직접 카메라를 움직이는 3D 게임에서는 같은 인상이 쉽게 무너진다는 점입니다.

이 글은 작은 실내 테이블탑 씬을 기준으로, 풀 3D 장면이 FPS 카메라 안에서도 일러스트풍으로 읽히게 만드는 조건을 찾는 실험 기록입니다. 카메라, 외곽선, 텍스처, 머티리얼, 그림자, 색보정을 차례로 검증하면서 폐기한 직관과 남긴 룰을 정리합니다.

이 글에서 다루는 내용:

사전 지식: Godot 4 셰이더, 화면 후처리, depth/normal buffer, 머티리얼 정책에 대한 기본적인 이해를 전제로 합니다. 구현 세부사항보다 의사결정 흐름 중심으로 정리합니다.


1. 풀 3D에서 일러스트풍이 무너지는 지점

이번 실험의 첫 결정은 “어디서 약점을 측정할 것인가”였습니다. 정지된 직교 카메라에서는 어떤 방식이든 어느 정도 그럴듯해 보입니다. 그래서 검증 기준 카메라를 직교가 아니라 FPS로 잡았습니다. FPS는 가까운 거리에서 큰 빈 면, 사진 질감의 이질감, 외곽선 떨림, 그림자 과잉을 가장 먼저 드러내는 조건입니다.

같은 장면을 직교, TPS, FPS 세 카메라로 비교했고, 외곽선 알고리즘을 적용하기 전 baseline FPS 화면을 먼저 잡았습니다.

baseline FPS

가까운 거리에서는 벽과 바닥의 큰 면이 비어 보였고, 오브젝트 경계가 일러스트처럼 읽히지 않았습니다. TPS와 직교에서는 어느 정도 분위기가 유지되어도 FPS에서는 단순한 3D 프리미티브 인상이 빠르게 드러났습니다. 이 결과 때문에 이후 모든 실험의 1차 판정 기준은 “FPS에서 버티는가”로 고정했습니다.


2. 외곽선: 두 번 갈아엎고 화면 후처리로 수렴

2.1 Inverted hull은 자유 카메라에서 부담이 컸습니다

첫 후보는 Inverted hull 계열의 오브젝트별 외곽선이었습니다. 오브젝트 바깥쪽에 외곽선용 형태를 하나 더 두고 어두운 색으로 렌더링하는 익숙한 방식입니다.

직교 카메라에서는 충분히 작동했습니다.

object outline ortho

같은 방식을 FPS로 옮기자 곧장 한계가 보였습니다.

object outline FPS

문제는 두 가지였습니다. 첫째, 카메라 각도에 따라 외곽선이 표면에서 떠 보이는 순간이 생겼습니다. 둘째, 외곽선 두께·색·제외 대상·작은 소품 처리를 오브젝트마다 수동 관리해야 했습니다. 자유 카메라가 들어가는 게임에서 모든 메시에 외곽선용 형태와 파라미터를 붙이는 비용은 너무 컸습니다. 고정 카메라 연출용으로는 남기되, 자유 카메라용 주 경로에서는 폐기했습니다.

2.2 Sobel 커널을 색이 아닌 depth와 normal에 적용했습니다

두 번째 후보는 화면 후처리 외곽선이었습니다. 개별 오브젝트에 선을 붙이는 대신 최종 화면에서 경계를 검출합니다.

핵심 결정은 Sobel 커널의 입력이었습니다. 일반적인 이미지 처리에서는 밝기에 적용하지만, 3D 화면에서 색만 보면 같은 색면 안의 형태 경계를 놓치고, 노이즈가 많은 텍스처에서는 가짜 선이 잡힙니다. 그래서 입력을 두 가지로 분리했습니다.

두 결과를 합쳐야 하는 이유는 단순합니다. depth만 쓰면 같은 메시 내부의 면 꺾임을 놓치고, normal만 쓰면 앞뒤로 떨어진 오브젝트 실루엣을 놓치기 때문입니다.

같은 FPS 조건에 적용한 결과입니다.

sobel FPS

디버그 패스에서 어떤 입력에서 어떤 경계가 나왔는지 색으로 분리해서 확인했습니다.

sobel debug

추가로 가까운 카메라에서 모든 경계가 두껍게 보이는 문제를 막기 위해 거리 기반 완화를 넣었습니다. 가까이 있는 모든 선이 두껍게 보이면 FPS 화면이 빠르게 지저분해지기 때문입니다.

이 시점에서 외곽선 문제는 일단 닫혔습니다. 하지만 외곽선이 안정되어도 큰 면이 비어 보이는 문제와 사진 텍스처가 튀는 문제는 그대로 남아 있었습니다. 그 두 가지는 외곽선이 풀 수 있는 문제가 아니었습니다.


3. 텍스처: 자동 변환은 밑그림이고, 최종은 authored

3.1 외부 자산 자동 변환 다섯 가지를 비교했습니다

씬에 들어온 라디오 같은 외부 PBR/사진 기반 prop은 그대로 두면 한 화면 안에서 혼자 사실적으로 보입니다. 자동 후처리로 톤만 맞출 수 있는지 확인하기 위해 다섯 가지 방향을 모두 시도했습니다.

방식입력결과판정
Bilateral픽셀 + 주변 색 가중치경계 보존하면서 표면 노이즈 감소. 구조는 살았지만 실사 인상도 같이 남음support
Posterize명암 단계 축소빠르게 단순해지지만 원본의 오염·경계도 같이 남김support
Quantize색 수 축소라디오 기준 부품 구조가 뭉개짐미채택
Kuwahara회화적 면 평탄화사진 인상은 줄지만 부품 구조까지 유화처럼 녹음미채택
Palette remap (전역)RGB nearest mapping의도하지 않은 영역까지 색이 이동미채택
Palette remap (masked)SAM 마스크 기반 부위별 remap부품별로 톤 통제 가능. 채택adopted support

채택된 경로는 마스크 기반 부분 remap + 필요 시 부위별 스크립트 repaint 조합이었습니다. 자동 변환은 최종 텍스처를 만드는 도구가 아니라 외부 자산을 빠르게 톤 정렬(tone alignment)하기 위한 입력으로 정리했습니다.

자동 변환을 통합한 텍스처 통합 패스의 FPS 화면입니다.

texture pass integration

3.2 Authored 텍스처가 큰 색면을 책임집니다

자동 변환만으로는 한계가 분명했습니다. 종이, 카드, 앨범, 포스터, 벽, 바닥처럼 화면의 큰 색면을 만드는 대상은 처음부터 직접 설계한 텍스처를 사용했습니다.

paper/card material을 적용한 화면입니다.

paper card detail pass

album card는 한 번에 만들어지지 않았습니다. mesh-only → texture pass → supersample → grain 순으로 단계별 evidence를 남겼고, edge aliasing과 종이 질감을 동시에 잡기까지 시작과 끝 사이의 차이가 다음과 같습니다.

album card mesh album card grain

벽과 책상처럼 넓은 표면은 강한 패턴 대신 부드러운 mottle/noise grain만 사용했습니다. 긴 직선 fiber stroke는 빠르게 라인 아티팩트로 보입니다.

large surface grain strong

3.3 Alpha card는 cutout을 기본 후보로 두었습니다

포스터처럼 “테두리가 비치는” 얇은 카드를 만들 때는 두 가지 방식이 있습니다.

포스터/카드처럼 가장자리가 약간 거칠어도 되고 진짜 반투명이 필요 없는 케이스는 cutout으로 충분합니다. 그래서 처음부터 cutout을 기본 후보로 잡고, 진짜 반투명이 필요한 경우에만 blend를 별도 검증으로 미뤘습니다. blend를 채택했다가 폐기한 것이 아니라, 알려진 비용과 패스 충돌을 피하기 위해 처음부터 cutout을 선택한 결정입니다.

alpha poster cutout


4. 머티리얼 카테고리: shader가 아니라 정책으로 묶기

검증을 진행할수록 새 셰이더를 늘리는 방향은 확장이 안 됐습니다. 같은 셰이더라도 가구·소품·종이·장비·얇은 카드는 색면 책임이 서로 달라야 했습니다. 그래서 셰이더 종류를 늘리는 대신 머티리얼 카테고리별 uniform 정책으로 묶었습니다.

Category색면 정책shadow 정책
Architecture (벽/바닥)큰 빈 면 회피, 부드러운 grain낮은 contrast, warm violet/rose
Furniture (책상/의자/러그)category 팔레트 기준 색면mauve/brown 계열, texture grain 보존
Props (라디오/램프/화분)masked remap으로 톤 정렬낮은 mix
Paper / Cardwarm cream, coral, teal, mustard 큰 색면약하거나 off
Device / Metalneutral gray 회피, muted blue-gray / warm charcoalblue-gray 계열
Alpha Cardcutout 기본카드 자체에는 약하게

채택된 정책을 통합한 머티리얼 closeout FPS 화면입니다.

material policy closeout

이 결정의 실무적 장점은 셰이더 그래프 트리를 키우지 않고도 외부 자산을 카테고리에 정렬해 검수할 수 있다는 점입니다. 새 prop이 들어오면 “어떤 셰이더에 붙일까”가 아니라 “어떤 카테고리인가”부터 묻습니다.


5. 그림자: caster가 아니라 receiver의 책임

엔진 cast shadow를 그대로 최종 색으로 믿는 방식은 빠르게 한계에 부딪혔습니다. 검정 곱셈은 색면을 죽이고, 화면 전체에 hue rotation을 거는 방식은 그림자만 따로 통제할 수 없었습니다.

이번에 채택한 정책은 다음입니다.

engine shadow      = mask / source
material shadow    = receiver category palette color
ShadowPatch        = authored graphic depth plane (보조)
look tooling       = category profile 기반 자동 적용 (후속)

핵심은 보이는 그림자색을 caster가 아니라 receiver material이 결정한다는 점입니다. 책상이 러그 위에 그림자를 만들더라도, 화면 픽셀은 러그 표면입니다. 따라서 그 픽셀의 그림자색은 책상이 아니라 러그의 카테고리 팔레트에서 옵니다.

전용 셰이더(flat_one_step_shadow_color_lit, flat_one_step_textured_shadow_color_lit)에서 shadow_colorshadow_mix를 receiver 기준으로 합성했습니다. warm receiver는 rose/mauve/muted violet, cool/device receiver는 blue-gray/teal/charcoal을 target으로 잡았습니다.

shadow color mix

검정 곱셈처럼 딱딱하게 꺼지지 않고, desk/floor texture grain이 그림자 영역에서도 죽지 않습니다. 화면 전체가 pink filter처럼 덮이지 않는 범위 안에서 자연스러운 후보로 유지됩니다.

기존에 사용하던 ShadowPatch(authored graphic shadow plane)는 폐기하지 않고 보조 도구로 남겼습니다. material shadow만으로 접지감이 부족한 경우에 배치하는 별도 graphic layer로 책임을 분리했습니다.


6. LUT: 약한 통합 레이어 이상은 위험합니다

마지막 실험은 1D/3D LUT 기반 색보정이었습니다.

LUT print LUT 3D

약한 LUT는 안전했지만 차이가 작았고, 강한 인쇄 톤은 분위기를 만들 수 있었지만 책상이나 벽이 한쪽 색으로 밀리는 위험이 컸습니다. 디버그 목적으로 극단 LUT를 걸어 보면 한 번에 알 수 있습니다.

LUT debug extreme

결론은 단순했습니다. LUT는 머티리얼·그림자 정책을 대체하지 않고, 이미 맞춰진 장면을 약하게 묶는 통합 레이어로만 둡니다. 약한 자산 색이나 잘못된 그림자를 LUT로 고치려 들면 다음 자산이 들어왔을 때 다시 깨집니다.


7. 수렴한 파이프라인

여섯 단계의 검증을 거치고 남은 룰은 다음 다섯 가지입니다.

  1. 검증 카메라는 FPS를 기본으로 둡니다. 직교/TPS에서 통과한 결과를 신뢰하지 않습니다.
  2. 외곽선은 화면 후처리(Sobel, depth+normal 합성, 거리 기반 완화)를 기본 경로로 둡니다. 오브젝트별 외곽선은 고정 카메라 연출용으로만 남깁니다.
  3. 외부 자산은 마스크 기반 부분 remap으로 톤 정렬만 하고, 화면의 큰 색면은 authored 텍스처가 책임집니다. 자동 변환은 최종 제작기가 아닙니다.
  4. 머티리얼은 셰이더 종류가 아니라 카테고리별 uniform 정책으로 관리합니다. 새 자산은 “어떤 카테고리인가”부터 묻습니다.
  5. 그림자는 receiver material의 shadow_color mix로 설계합니다. 엔진 cast shadow는 mask로 쓰고, ShadowPatch는 보조 graphic layer로 분리합니다. LUT는 약한 통합 레이어로만 둡니다.

8. 남은 한계

이번 검증은 그래픽 방향을 정리하는 데에는 유용했지만, 다음 한계가 남아 있습니다.


정리하며

이번 실험의 결론은 “하나의 강한 효과로 일러스트풍이 만들어지지 않는다”는 점입니다. 외곽선·텍스처·머티리얼·그림자·LUT 중 어느 하나라도 정책이 비면 FPS 카메라에서 같은 자리가 다시 무너졌습니다.

수렴한 다섯 가지 룰의 공통 패턴은 caster가 아니라 receiver, shader가 아니라 카테고리, 효과가 아니라 정책으로 책임을 옮긴다는 점입니다. 그래야 외부 자산이 들어와도 같은 기준으로 통합할 수 있고, 실패한 자산을 LUT로 가리지 않게 됩니다.

핵심 요약:

다음 단계는 실험을 더 늘리는 것이 아니라, 실제 게임 콘셉트와 첫 production 후보 자산 세트를 좁히는 일입니다. 그때부터는 위 다섯 룰이 제작 파이프라인의 기준선이 됩니다.

참고 자료


이 게시물은 프로젝트 그래픽 검증 문서를 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 구조와 문체를 다듬었습니다.


공유하기:

이전 글
[Marching Cubes] 2D에서 3D로: Metaball 렌더링 최적화 일지
다음 글
[LLM Workbench] btwin Memory: 세션 밖에 맥락 남기기