Catlantean 3D: 현대적 도구를 사용한 1993년 시대 그래픽 재현

현대적 환경에서의 레트로 제약 사항

Catlantean 3D는 현대적인 컴파일러와 플랫폼 추상화 계층을 활용하면서도 1990년대 초반 게임의 기술적 한계를 재현하기 위해 개발된 1인칭 슈팅 게임입니다. 이 프로젝트는 정통 레트로 미학을 보장하고 결정론적 동작을 유지하기 위해 엄격하게 자체적으로 설정한 제약 사항을 준수합니다.

핵심 기술 제약 사항은 다음과 같습니다:

  • 해상도 및 색상: 320x240 타겟 해상도와 256색의 제한된 팔레트.
  • 렌더링 및 오디오: 모든 렌더링과 사운드 믹싱은 처음부터 직접 구현됩니다.
  • 결정론: 플랫폼 간 일관된 동작을 보장하기 위해 게임 로직에는 고정 소수점 연산(Fixed-point math)을 사용하며, 부동 소수점 연산은 렌더링용으로만 예약됩니다.
  • 추상화: 플랫폼 추상화 계층은 기본적인 프레임 버퍼 접근, 키보드/마우스 입력, 오디오 버퍼 쓰기, 그리고 파일 시스템 I/O로 제한됩니다.

팔레트 기반 렌더링 및 라이팅

VGA 시대 게임의 "crisp"한 느낌을 구현하기 위해, Catlantean 3D는 각 픽셀이 256색 팔레트를 인덱싱하는 단일 바이트인 선형 프레임 버퍼를 사용합니다. 이 방식은 화면의 모든 픽셀이 사용 가능한 256가지 색상 중 하나여야 하므로 신중한 색상 선택을 요구합니다.

컬러맵 라이팅 시스템

전통적인 레이캐스팅(raycasting)은 종종 평면적인 라이팅을 초래합니다. 현대적인 셰이더의 오버헤드 없이 깊이감을 만들기 위해, Catlantean 3D는 빠른 O(1) 라이팅 조회를 위해 팔레트 인덱스의 2D 매트릭스를 사용하는 colormap을 구현합니다.

컬러맵 프로세스는 다음과 같이 작동합니다:

  1. 선형화: 256색 팔레트를 하나의 선형 행으로 취급합니다.
  2. 음영 생성: 각 색상에 대해 31개의 더 어두운 변형을 미리 계산합니다. 시스템은 Oklab 색 공간과 그 지각적 거리 공식을 사용하여 팔레트 내에서 가장 유사한 색상을 찾아내며, 이를 통해 Euclidean 거리 방식에서 흔히 발생하는 "차가운" 또는 회색조로 치우친 느낌을 방지합니다.
  3. 색조 변화: 시각적 매력을 높이기 위해 색상이 어두워짐에 따라 따뜻한 색조로 약간의 변화를 적용합니다.
  4. 런타임 조회: 렌더러는 카메라로부터의 거리에 기반하여 음영 인덱스를 계산하고, 컬러맵 매트릭스에서 어두워진 팔레트 인덱스를 가져옵니다.

성능을 최적화하기 위해, 벽의 경우 화면 열(column)당 한 번, 바닥의 경우 화면 행(row)당 한 번, 그리고 스프라이트당 한 번만 컬러맵 행 인덱스를 계산합니다.

에셋 생성 파이프라인

Catlantean 3D의 에셋은 프리렌더링(pre-rendered), 수작업(hand-drawn), 절차적 생성(procedurally generated)의 세 가지 카테고리로 나뉩니다.

프리렌더링 스프라이트

복잡한 애니메이션의 반복 작업을 간소화하기 위해, 3D 모델은 Blender를 사용하여 제작 및 리깅(rigging)됩니다. 이 모델들은 Blender의 API를 활용하는 Python 스크립트를 통해 텍스처로 렌더링됩니다. 다운스케일링 시 발생하는 "blurry"한 느낌을 방지하기 위해, 개발자는 Blender의 컴포지팅 노드를 사용하여 이미지를 Python 스크립트로 전달하기 전에 대비와 선명도를 높입니다. 그 후 Oklab 기반 팔레트 양자화를 수행합니다.

수작업 아트 및 픽셀 일관성

상태 표시줄의 얼굴이나 HUD와 같은 특정 요소들은 감정 표현과 명확성을 보장하기 위해 Aseprite에서 수작업으로 그려집니다. 이 프로젝트의 핵심 규칙은 일관된 픽셀 스케일을 유지하는 것입니다: 1 월드 유닛은 64 픽셀과 같습니다. 이는 저품질 인디 게임에서 흔히 발생하는 문제인, 서로 다른 해상도의 에셋이 혼합될 때 발생하는 시각적 이질감을 방지합니다.

절차적 생성 및 "Gibbing"

반복적인 마모나 먼지 효과가 있는 텍스처를 위해, Python 스크립트는 하이트맵(heightmaps), 노이즈 맵(noise maps), 그리고 그라임 맵(grime maps)을 사용하여 최종 텍스처를 생성합니다. "gibbing" (적의 사망 애니메이션)을 위한 더 복잡한 절차적 파이프라인은 다음과 같습니다:

  1. Voronoi 분할: 스프라이트는 무작위 시드 픽셀을 기반으로 조각각(chunks)으로 분할됩니다. \n2. 상처 출혈: 너비 우선 탐색(BFS)이 조각의 경계로부터 안쪽으로 퍼져나가며, 색상을 팔레트 정의 혈액 램프(blood ramp)로 블렌딩합니다.

  2. 물리 시뮬레이션: 각 조각에는 무게 중심, 속도, 회전, 중력을 할당하여 런타임 충돌 감지 없이 폭발을 시뮬레이션합니다.

커스텀 도구 및 맵 에디터

Tiled와 같은 범용 도구는 라이트 레벨 페인팅이나 특정 셀 플래그에 대한 네이티브 지원이 부족했기 때문에, 개발자는 wxPython을 사용하여 커스텀 맵 에디터를 구축했습니다.

이 에디터는 pybast (Python bindings for C++ engine internals via pybind) 라이라는 라이브러리를 통해 엔진과 통합되어, 에디터가 게임 데이터 아카이브와 텍스처를 직접 읽을 수 있게 합니다. 이는 맵 에디터가 개발 프로세스의 핵심적인 부분으로서 긴밀한 생태계를 구축하며, 게임과 함께 배포되어 커뮤니티 모딩을 가능적으로하게 합니다.

커뮤니티 통찰 및 기술적 맥락

이 프로젝트에 대한 논의는 사용된 제약 사항의 역사적 중요성을 강조합니다. 기여자들이 언급한 바에에 따르면, 오리지널 VGA Mode 13h (320x200)는 64,000바이트 버퍼가 16비트 세그먼트에 완벽하게 들어맞아 초기 CPU의 주소 지정 방식을 단순화하기 위해 비정방형 픽셀(non-square pixels)을 사용했습니다.

다른 개발자들은 Catlantean 3D에 사용된 레이캐스팅 엔진이 Wolfenstein 3D (수직 벽, 일정한 바닥/천장 높이)와 Doom의 BSP 기반 엔진(더 유연한 벽 각도와 가변 높이)보다 더 유사하다는 점을 주목했습니다.

Sources