# 1993년처럼 그래픽 만들기

> Clean Markdown view of GeekNews topic #30339. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30339](https://news.hada.io/topic?id=30339)
- GeekNews Markdown: [https://news.hada.io/topic/30339.md](https://news.hada.io/topic/30339.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-10T09:39:19+09:00
- Updated: 2026-06-10T09:39:19+09:00
- Original source: [staniks.github.io](https://staniks.github.io/articles/catlantean-3d-blog-1/)
- Points: 1
- Comments: 1

## Topic Body

- **Catlantean 3D**는 1990년대 초반 PC 게임식 제약으로 완성형 1인칭 슈터를 만들려는 사이드 프로젝트이며, 320x240 해상도와 256색 팔레트 기반 렌더링을 목표로 함
- 렌더러는 팔레트 인덱스만 다루기 때문에 거리 기반 어둡게 보이기를 위해 32단계 **colormap**을 사전 계산하고, 실행 중에는 O(1) 조회로 어두운 색을 선택함
- 애셋 제작은 Blender 기반 **사전 렌더링 스프라이트**, 손그림 스프라이트·텍스처, Python 스크립트로 생성하는 절차적 텍스처로 나뉨
- 손그림 HUD와 픽셀 단위 스케일 규칙은 작은 해상도에서 선명도와 가독성을 유지하기 위한 핵심 제약이며, 월드 1유닛을 64픽셀 기준으로 맞춤
- Tiled 대신 자체 맵 에디터를 만들고, 출시 후 플레이어에게도 같은 에디터를 제공할 계획이며, 게임 소스 코드는 GitHub에 오픈소스로 공개할 예정임

---

### 프로젝트 목표와 제약
- Catlantean 3D는 1년 넘게 여가 시간에 천천히 개발 중인 사이드 프로젝트이며, 다음 해 Steam 출시를 목표로 함
- 목표는 1990년대 초반에 흔했던 기법으로 완성되어 출시 가능한 1인칭 슈터를 만드는 것임
- 현대 컴파일러와 플랫폼 추상화 계층은 허용하지만, 추상화 계층은 픽셀을 쓰는 프레임버퍼, 키보드·마우스 입력, 샘플을 쓰는 오디오 버퍼, 파일시스템 I/O 정도로 제한함
- 게임은 애셋까지 전부 처음부터 만들어야 하고, 렌더링과 사운드 믹싱도 모두 직접 구현해야 함
- 목표 해상도는 320x240이고, 화면의 모든 픽셀은 256색 중 하나만 사용할 수 있음
- 게임 로직은 결정적 동작 보장을 위해 고정소수점을 사용하고, 렌더링은 결정성이 덜 중요해 부동소수점을 사용함
- 결과물은 기술 데모가 아니라 재미있게 플레이할 수 있는 완성도 있는 게임이어야 하며, AI 산출물은 사용하지 않음
- 표시된 모든 작업물은 작업 중인 상태이며 크게 바뀔 수 있음

### 팔레트 렌더링
- ## VGA 그래픽
  - VGA 하드웨어의 Mode 13h는 320x200 256색 그래픽 모드였고, 한 세대의 PC 게임을 규정한 유명한 모드였음
  - 프로그래머 관점에서 Mode 13h는 각 픽셀이 256색 팔레트의 인덱스인 1바이트로 표현되는 선형 프레임버퍼를 제공함
  - 픽셀을 그리려면 특정 주소에 1바이트를 쓰면 되었고, 셰이더나 VRAM 같은 개념을 다루지 않아도 됨
  - 최신 게임 애셋은 이미지에 수백만 색을 사용할 수 있지만, 256색 제한에서는 모든 색 선택이 신중하고 의도적이어야 함
  - Doom과 Duke Nukem 같은 게임은 기술적 한계 때문에 그래픽의 선명함과 명료함이 생긴 사례로 제시됨
  - Catlantean 3D는 그 감각을 재현하려 하지만, 320x200 대신 VGA Mode-X에 가까운 320x240을 선택함
  - 320x200을 4:3 디스플레이에 표시하면 정사각형이 아닌 픽셀이 되며, 이 방식은 더 정통적이지만 선호에 따라 피함
- ## 팔레트
  - 팔레트는 768바이트에서 시작하며, 여러 번의 시행착오와 반복을 거쳐 선택됨
  - 팔레트에는 투명색용 선명한 분홍색, 순백색, 순검정색이 각각 하나씩 예약됨
  - 피 표현을 위해 빨간색 계열이 많이 필요했고, 빨강·초록·파랑 키와 색상 구분 문을 위해 초록색과 파란색 계열도 필요함
  - 배경은 고양이 숭배 때문에 고대 이집트와 닮은 패러디 땅 Catlantis로 설정되어 노랑과 갈색 중심의 사막색이 많이 필요함
  - Catlantis가 사이버네틱 개 인간에게 점령된 설정이어서 기술 시설 표현을 위한 회색 계열이 많이 필요함
  - 회색의 단조로움을 깨고 어두워질 때 따뜻한 대체색으로 쓰기 위해 베이지색 계열도 들어감
  - 나머지 색은 텍스처 제작 과정에서 필요할 때 채워졌고, “보기에 맞았다”는 주관적 판단에 따라 정해짐
  - 팔레트는 한 번에 완성되지 않았고, 애셋 제작과 테스트, 반복 과정에서 계속 조정됨

### colormap과 조명 처리
- ## raycaster 구조
  - Catlantean 3D는 전통적인 raycaster이며, 맵은 모두 같은 크기의 타일로 구성됨
  - 일부 타일은 벽이고, 다른 타일은 바닥과 천장만 있는 빈 공간임
  - 렌더러는 화면의 각 열마다 DDA 알고리듬을 사용해 타일맵을 지나가며 맵 지오메트리와 부딪히는 위치를 찾음
  - 충돌 위치에 따라 적절한 텍스처 좌표에서 샘플링한 벽 열을 화면에 그림
  - 바닥과 천장은 이후 수평 스캔라인으로 렌더링되어 화면의 나머지를 채움
  - 단순히 팔레트만으로 게임 월드를 렌더링하면 평평하고 인상적이지 않은 화면이 됨
  - 플레이어에게서 멀어질수록 빛이 줄어들고, 맵 타일의 한쪽 면이 다른 쪽보다 약간 어두우면 깊이감이 생김
- ## 팔레트 기반 어둡게 하기
  - 현대 하드웨어 가속 렌더러에서는 정점 거리 기반으로 색 벡터에 부동소수점 계수를 곱해 셰이더에서 쉽게 어둡게 만들 수 있음
  - 팔레트 렌더러는 색 개념이 아니라 팔레트 인덱스만 다루므로, 특정 색의 더 어두운 색을 찾으려면 팔레트 전체를 탐색해야 함
  - 모든 렌더링 픽셀마다 256색 팔레트를 탐색하면 너무 느리기 때문에, 실행 전 사전 계산으로 빠른 색 조회를 준비함
  - 팔레트를 한 행으로 놓고 32개 명암 단계를 선택하면, 각 색마다 원본을 제외한 31개의 더 어두운 변형이 필요함
  - 각 색의 RGB 값과 명암 인덱스로 목표 어두운 색을 계산하지만, 그 색이 실제 팔레트에 존재하지 않을 수 있음
  - 목표 색과 가장 가까운 팔레트 색을 찾기 위해 팔레트를 탐색해 colormap을 구성함
  - 초기에는 유클리드 거리를 사용했지만, 많은 색이 회색 쪽으로 끌리는 경향이 있었고 어두운 색이 차갑고 생기 없어 보였음
  - 이후 색을 Oklab 색공간으로 변환하고, 인간의 색 차이 지각에 더 가까운 지각 거리 공식을 사용함
  - 색이 어두워질수록 따뜻한 색조로 조금 이동시키는 픽셀 아트 개념인 색조 이동(hue shifting)도 적용함
  - colormap은 각 색의 명암을 나타내는 팔레트 인덱스의 2차원 행렬이며, 여전히 팔레트 색만 사용할 수 있어 그라데이션은 완벽하지 않음
- ## 실행 비용 절감
  - 거리 기반 colormap 행 인덱스가 정해지면, 해당 명암 단계 행에서 N번째 항목을 골라 어두워진 색 N의 팔레트 인덱스를 얻음
  - 이 방식은 실행 중 더 어두운 색 선택을 O(1) 조회로 처리함
  - 벽 렌더링에서는 벽 열이 완전히 수직이고 열 안의 모든 픽셀이 카메라와 같은 거리를 가지므로 화면 열마다 한 번만 colormap 행 인덱스를 계산함
  - 바닥 렌더링에서는 수평 행의 모든 픽셀이 같은 거리를 가지므로 화면 행마다 한 번만 계산함
  - 스프라이트는 모든 픽셀이 카메라와 같은 거리를 갖는 평평한 빌보드이므로 보이는 스프라이트마다 한 번만 계산함
  - 벽은 320번, 바닥은 최대 240번, 보이는 스프라이트는 각각 한 번만 계산하면 되며, raycasting은 가려진 객체 제거를 무료로 제공함
  - Doom과 여러 다른 게임도 비슷한 접근을 사용함

### 애셋 제작 방식
- ## 세 가지 애셋 범주
  - Catlantean 3D의 텍스처와 스프라이트는 세 범주로 나뉨
  - 첫 번째 범주는 Blender에서 만든 3D 모델을 텍스처로 렌더링한 사전 렌더링 스프라이트임
  - 두 번째 범주는 손으로 그린 스프라이트와 텍스처임
  - 세 번째 범주는 손그림 아트를 조합하는 특수 Python 스크립트로 생성한 절차적 텍스처임
- ## 사전 렌더링 스프라이트
  - 복잡한 애니메이션 스프라이트는 여러 프레임을 수정해야 하므로 반복 작업이 어렵고 시간이 많이 듦
  - 더 효율적인 방식은 Blender에서 3D 모델을 만들고 리깅과 애니메이션을 수행한 뒤, Blender Python API를 쓰는 스크립트로 여러 텍스처를 렌더링하는 것임
  - 수정은 모델에서 이루어지고 렌더링 스크립트가 나머지를 처리하므로 반복 작업 시간이 크게 줄어듦
  - 주요 난점은 렌더링된 스프라이트가 매우 흐리고 색이 바랜 것처럼 나온다는 점이었음
  - 고해상도로 렌더링한 뒤 필터링으로 축소하는 방식은 디테일이 필터링에 눌리고 가장자리 선명도가 사라지는 경우가 있어 성과가 섞였음
  - 가장 효과적이고 재사용 가능한 방식은 Blender의 합성 기능으로 적절한 대비와 선명도를 얻는 것이었음
  - 이미지가 준비되면 특수 Python 스크립트가 팔레트 양자화를 수행해 엔진이 사용하는 1바이트 픽셀 이미지를 생성함
  - 스크립트는 원본 이미지의 각 픽셀마다 Oklab 기준으로 지각적으로 가장 가까운 팔레트 색을 찾고, 그 색의 인덱스를 픽셀 값으로 사용함
  - 인덱스 배열과 크기 정보는 게임에서 쓰는 단순한 TEX 형식으로 패킹됨
  - 적 스프라이트는 여러 애니메이션을 가질 수 있고, 각 애니메이션은 스프라이트가 향할 수 있는 8방향의 프레임을 가져야 함
  - Python 스크립트는 애니메이션마다 스프라이트를 회전시키고 모든 프레임을 렌더링한 뒤 다시 회전하는 과정을 반복함
  - 스프라이트 파일명은 스프라이트 이름, 동작 이름, 방향, 프레임 인덱스를 나타내는 규칙으로 저장됨
  - 렌더링된 스프라이트는 저장소에 두지 않고 .gitignore 처리되며, 다른 컴퓨터에서는 컴파일 스크립트가 모든 모델을 렌더링해 스프라이트를 생성함
  - RTX 3070에서 약 15개 모델을 처리하는 데 약 10초가 걸림
- ## 손그림 스프라이트와 텍스처
  - 개발 초기에 상태바 얼굴로 쓰기 위해 고양이 Vilko의 텍스처를 입힌 고양이 모양 머리를 Blender로 만들었음
  - 이 결과물은 게으르고 낮은 노력처럼 보였고, 감정을 잘 전달하지 못했으며, 분위기 피드백에서 사람들이 가장 먼저 지적한 부분이었음
  - 일부 요소는 반드시 손으로 그려야 하며, 애니메이션이 들어간 손그림 버전이 훨씬 낫다고 판단됨
  - 스프라이트 크기 때문에 모든 픽셀이 의도적이어야 하며, Blender 렌더러에 맡길 여지가 없음
  - 대부분의 픽업 아이템에도 같은 논리가 적용됐고, 이전 사전 렌더링 결과물은 작은 스케일에서 Blender 합성기가 안정적으로 좋은 결과를 내지 못함
  - 사람의 손을 거친 뒤 픽업 아이템의 선명도와 가독성이 크게 좋아짐
  - 스프라이트 해상도를 단순히 높이면 게임 래스터라이저가 스케일링할 수는 있지만, 픽셀 스케일이 일관되지 않아 결과가 좋지 않음
  - 화면의 같은 행이나 열에서 앞뒤로 움직일 때 픽셀 크기가 같게 유지된다고 무의식적으로 기대하므로, 스프라이트마다 픽셀 스케일이 다르면 어색해짐
  - Catlantean 3D의 월드 1유닛은 64픽셀이며, 모든 스프라이트는 이 스케일에 맞춰 제작됨
  - 월드 유닛의 4분의 1 높이 스프라이트는 64/4=16픽셀 높이여야 함

### HUD와 절차적 생성 파이프라인
- ## HUD
  - HUD와 그 구성 요소는 거의 전부 손으로 배치하고 그림
  - 화면 하단 상태바, 여러 전환 패널과 화면, 폰트가 손그림 HUD 범주에 들어감
  - 모든 요소를 직접 칠하기보다 Affinity Photo의 레이어 효과와 합성을 많이 사용함
  - 사용한 효과에는 평평한 표면의 3D 느낌을 내는 emboss 효과, 거친 느낌을 위한 노이즈 생성과 오버레이, 색상 오버레이, 블렌딩 모드, glow 효과가 있음
  - HUD 요소를 자주 반복 수정하므로 레이어 기반 재배치 편의성도 중요함
  - 일반적으로 Affinity Photo에서 truecolor로 먼저 작업하며, 많은 요소는 단색 사각형 위에 특수 효과와 블렌딩을 적용한 것임
  - Affinity Photo에서 내보낸 이미지는 안티앨리어싱과 관련된 것으로 보이는 이상한 아티팩트를 포함했고, 이를 안정적으로 끄지 못함
  - 픽셀 정확한 작업에는 적합하지 않아 Aseprite에서 픽셀 퍼펙트 텍스트, 아트워크 조각내기, 더 선명한 경계선 덧칠 같은 추가 작업을 수행함
- ## 절차적 생성 텍스처
  - 일부 텍스처는 직접 그리기 충분히 단순하거나 구체적이지만, 많은 텍스처는 기본 재질 위에 마모, 먼지, 표면 디테일 변형을 공유함
  - 각 변형을 손으로 그리면 지루하고 일관성이 떨어지므로 Python 스크립트로 생성함
  - 생성 파이프라인은 표면 relief를 정의하는 heightmap, 변형용 noise map, 먼지와 마모용 grime map, 두 가지 기본색, brightmap을 입력으로 받음
  - heightmap은 실제로 normal map을 만드는 데 쓰이고, normal map은 단순한 조명과 그림자를 굽는 데 사용됨
  - brightmap은 다른 매개변수와 관계없이 색을 유지할 부분을 지정함
  - 스크립트는 최종 텍스처를 만들고 팔레트 양자화까지 수행해 엔진에서 바로 쓸 수 있게 함
  - 텍스처 수정은 픽셀을 다시 그리는 대신 매개변수를 조정하는 일이 되며, 혼자 작업할 때 시간을 크게 절약함

### gibs와 사전 렌더링 효과
- ## Gibs
  - 적에게 point-blank shotgun blast나 explosion 같은 과도한 피해가 적용되면 gibbing이 일반적으로 발생함
  - 큰 피해의 충격을 전달하기 위해 적이 피 묻은 조각으로 터져 나가는 애니메이션을 사용함
  - 이 파이프라인은 Python 스크립트가 주도하며, 스프라이트, 팔레트, 매개변수 집합을 받아 게임 데이터에 들어갈 애니메이션 프레임을 생성함
  - 첫 단계인 Voronoi decomposition에서는 스프라이트의 불투명 몸체에서 K개 seed 픽셀을 무작위로 고르고, 모든 픽셀을 가장 가까운 seed에 할당함
  - 이렇게 생긴 각 cell은 날아가는 조각 하나가 됨
  - 두 번째 단계인 wound bleeding에서는 서로 다른 조각과 인접한 경계 픽셀을 깊이 0의 상처로 표시하고, BFS가 안쪽으로 퍼지며 깊이 값을 부여함
  - 렌더링 시 경계 근처 픽셀은 게임 팔레트에서 파생한 ramp의 피 색상 쪽으로 섞이고, 조각 안쪽으로 들어갈수록 원래 스프라이트 색이 더 보존됨
  - 팔레트 ramp 선택은 매개변수화되어 특정 적에게 초록색이나 파란색 “피”도 사용할 수 있음
  - 세 번째 단계인 physics에서는 각 조각에 중심점, 스프라이트 중심에서 바깥으로 향하는 무작위 확산 속도, 회전 속도, 중력, drag를 부여함
  - 충돌 감지는 없지만 조각은 바닥과 부딪히면 멈추며, 조잡하지만 충분한 결과를 냄
  - 조각 수, 폭발 힘, 중력, drag, 확산, 상처 깊이는 매개변수로 조정 가능함
  - 보기 좋은 seed를 찾는 데 약간의 시행착오가 필요하지만, 손으로 애니메이션을 그리는 것보다 빠름
  - 같은 기법은 화분, 배럴, 상자 같은 파괴 가능한 환경 오브젝트에도 사용됨
  - 사전 렌더링 애니메이션처럼 gibs 결과물도 저장소에 두지 않고, 체크아웃 후 다시 생성되며 실행 시간은 무시할 만함
- ## 사전 렌더링 파티클 시스템
  - 대부분의 파티클 효과는 Aseprite에서 손으로 그리지만, 일부는 gibs와 같은 방식으로 생성하고 굽음
  - Python 스크립트가 시뮬레이션을 실행해 PNG 프레임 시퀀스를 만들고, 이후 TEX로 양자화함
  - 런타임 파티클 시스템은 없으며, 모든 효과를 미리 구워 소프트웨어 래스터라이저가 최대한 빠르게 렌더링할 수 있게 함
  - 여기서 “particle”이라는 단어는 다소 오해의 소지가 있으며, 실제로는 입자를 시뮬레이션하지 않음
  - 각 프레임은 픽셀별 radial energy field를 계산하고 여러 독립 레이어를 합산해 합성됨
  - core는 애니메이션 동안 바깥으로 확장되는 부드러운 원반임
  - rays는 core 주변의 뾰족한 빛줄기이며, sharpness와 length를 설정할 수 있고 각 ray에는 RNG 기반 길이 흔들림이 들어가 불규칙해 보임
  - ring은 선택 가능한 확장 shockwave이고, noise는 전체 에너지에 value noise를 곱해 깨끗한 형태를 거칠고 불규칙하게 만듦
  - 누적된 픽셀별 에너지는 스크립트 매개변수로 지정한 팔레트 ramp에 맞춰 양자화됨
  - 팔레트 설계상 각 행이 밝음에서 어두움으로 이어지는 그라데이션처럼 다뤄지므로, 블렌딩이나 알파 계산 없이 팔레트 인덱스 산술만으로 픽셀을 어둡게 함
  - 특정 임계값 이상에서는 픽셀을 흰색 쪽으로 밀어 white-hot core 같은 인상을 줌
  - 선택적으로 작은 sparkle을 위에 흩뿌릴 수 있으며, 이 십자 모양은 바깥으로 이동하고 각자 수명 동안 사라짐
  - 애니메이션은 explosion이나 teleport flash처럼 커졌다가 사라지는 one-shot 모드와, 첫 프레임과 마지막 프레임이 맞아 끊김 없는 loop 모드를 지원함
  - loop 모드는 plasma bolts와 energy projectiles 같은 지속 반복 효과에 유용함

### 맵 에디터와 도구 생태계
- 맵 편집은 처음에 Tiled에서 시작했으며, 일반적으로 합리적인 도구였지만 게임에 필요한 구체적 기능이 부족했음
- Tiled에는 셀별 light level painting, cell flags, 게임 고유 속성 개념이 없어서 초기에는 object properties를 남용해 우회함
- Tiled의 JSON 출력을 엔진이 쓰는 바이너리 형식으로 변환하는 Python 스크립트도 필요했으며, 이는 도구와 게임 요구사항의 불일치를 보완하기 위한 추가 구성 요소였음
- 플레이어가 맵을 만들기 위해 Tiled를 설치하고 인터페이스를 배우고 변환 스크립트를 설정해야 한다면, 에디터가 실제로 쓰일 가능성을 없앨 수준으로 부담이 큼
- 자체 에디터는 light level painting, cell flags, 게임이 아는 모든 entity와 property 타입을 네이티브로 지원함
- 게임이 출시되면 플레이어도 개발에 사용한 같은 에디터를 받게 됨
- 에디터는 plug and play 방식이며, 에디터에서 곧바로 레벨을 실행할 수 있음
- 툴바 아이콘이 형편없다는 점을 알고 있으며, 바로 그 이유로 그대로 유지함
- 에디터는 wxPython으로 만들어졌고, tkinter보다 widget, event handling, layout에서 더 잘 맞았음
- wxPython 결과물은 더 네이티브하게 보였고, 반복 작업이 빠르게 진행됨
- MVP 패턴 중심 구조는 UI 로직과 맵 데이터를 깔끔하게 분리하며, 맵 포맷이 아직 안정적이지 않아 양쪽이 자주 바뀌는 상황에서 중요함
- 에디터의 모든 부분이 Python으로 작성된 것은 아니며, model의 많은 부분은 `pybast` 라이브러리에 의존함
- `pybast`는 pybind를 통한 엔진 내부 Python 바인딩이며, game data archive 읽기, game textures 읽기, entity coordinates용 fixed point class, serialization을 제공함
- 이미 C++로 구현된 기능을 Python에서 다시 구현하지 않기 위한 선택이며, 엔진과 도구가 작고 밀접한 생태계를 이룸

### 출시 계획과 공개 방식
- Catlantean 3D는 2027년 1분기 중 공개를 예상함
- 현재 초점은 레벨 디자인, 적과 무기 추가, 진행 중인 polish 작업임
- 가격 목표는 5~8달러 범위임
- 게임 소스 코드는 GitHub에 오픈소스로 공개할 계획임
- 그래픽, 레벨, 사운드, 음악 등이 들어 있는 실제 data archive는 게임을 구매해야 받을 수 있음
- 과정의 투명성은 지속적인 신뢰를 만드는 몇 안 되는 요소로 여겨짐
- 인디 게임은 AAA와 달리 더 작은 관객에게 의존하지만, 관객은 프로젝트를 따라가고 응원하고 다른 사람에게 알릴 의향이 더 큼
- 작업 과정을 보여주는 일은 만들고 있는 것에 실제로 신경 쓰고 있음을 드러내는 가장 정직한 방식으로 제시됨

## Comments



### Comment 59302

- Author: neo
- Created: 2026-06-10T09:39:20+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48459294) 
- **소프트웨어 렌더링**을 가지고 놀고 싶다면, SDL2와 C로 모든 플랫폼에서 메인 메모리의 ARGB8888 2D 배열을 화면에 효율적으로 올리는 가장 짧은 코드에 가까운 예제가 있음: [https://gist.github.com/CoryBloyd/6725bb78323bb1157ff8d4175d...](<https://gist.github.com/CoryBloyd/6725bb78323bb1157ff8d4175d42d789>)  
  320x200x8비트 팔레트 프레임버퍼를 ARGB로 바꾸는 변환은 직접 해야 함 ;)  
  팔레트 프레임버퍼로 무엇을 할 수 있는지 영감을 얻고 싶다면 [http://www.effectgames.com/demos/canvascycle/](<http://www.effectgames.com/demos/canvascycle/>)에서 Show Options를 눌러보거나, 해당 아티스트의 GDC 발표 [https://youtu.be/aMcJ1Jvtef0](<https://youtu.be/aMcJ1Jvtef0>)를 보면 됨  
  그다음 고전적인 Deluxe Paint IIe 느낌을 원하면 [https://github.com/mriale/PyDPainter](<https://github.com/mriale/PyDPainter>)를, 좀 더 현대적인 도구를 원하면 [https://www.aseprite.org/](<https://www.aseprite.org/>)를 켜면 됨
  - 적어도 **SDL3**에서는 더 이상 렌더러나 텍스처가 필요 없음. SDL_GetWindowSurface로 표면을 얻고 SDL_UpdateWindowSurface로 표시하면 됨  
    라이브러리를 이해한 바로는 이게 가장 소프트웨어 그래픽에 가까운 방식이고, SDL이 여전히 이중 버퍼링은 대신 해줌
  - 확실히 가장 기초적인 방식임. 안쪽 반복문에서 작은 최적화를 하려면 픽셀 반복문에 들어가기 전에 **스캔라인 오프셋**을 미리 계산하면 됨:  
    `int s = y*screenRect.w;`
    
    `for (int x = 0; x < screenRect.w; x++) {`  
    `pixels[s + x] = argb(255, frame>>3, y+frame, x+frame);`  
    `}`
  - 공유 고마움. 이미 인기 있는 Quake 포크가 여럿 있지만, Planimeter는 변경을 넣지 않는 **Quake-VS2026** 포크를 배포하고 있음  
    팀은 x64 빌드를 작업 중인데, 이를 위해 오래된 SciTech Multi-platform Graphics Library(x86 전용)를 SDL3로 교체해야 함. 아니면 scitech-mgl을 x64로 포팅해야 하는데 그럴 가능성은 낮아 보이고, 마지막으로 알기로는 소프트웨어 렌더러가 빠질 수도 있었음  
    하지만 소프트웨어 렌더러와 SDL_Texture로 보존할 수 있을지도 모름

- 이 글은 Doom에서 많은 영감을 받았지만, 실제 **광선 투사 엔진**은 Doom의 전작들, 그중 가장 유명한 Wolfenstein 3D에 더 가까움  
  수직 벽, 일정한 바닥과 천장 높이를 쓰는 방식임. Wolf3D는 성능 때문에 텍스처가 있는 바닥과 천장이 없었지만, 비슷한 다른 게임들 중에는 있는 것도 있었음  
  Doom과 기억이 맞다면 Duke Nukem도 훨씬 유연한 BSP 엔진을 썼고, 벽이 임의 각도로 교차하거나 바닥·천장 높이가 변할 수 있었음. 다만 레벨은 여전히 “평면적”이라 한 레벨 안에 여러 층을 만들 수는 없었고, 예를 들어 위아래로 모두 지나다닐 수 있는 다리는 설계할 수 없었음
  - Build 엔진은 **BSP**를 쓰지 않았음. 섹터 간 연결을 포털로 취급하고, 그 포털에 대해 클리핑하면서 벽을 90도 회전한 사다리꼴처럼 래스터화했음  
    덕분에 움직이는 기차나 회전하는 조명 같은 동적 벽 지오메트리가 가능했고, 동시에 두 방을 볼 수 없기만 하면 “방 위의 방” 구성도 가능했음  
    Blood와 Shadow Warrior에서는 같은 모양의 섹터를 만들고 한 섹터의 바닥을 다른 섹터의 천장으로 이어지는 포털처럼 쓰는 우회법으로 더 “3D”에 가까운 공간을 만들었음. 엔진이 원래 지원한 기능은 아니었지만, 소스 접근권도 없던 스튜디오들이 직접 해낼 만큼 유연했음  
    Duke Nukem 3D의 첫 레벨도 몇 가지 Build 트릭을 씀. 예를 들어 스프라이트가 카메라를 따라 회전하지 않고 축 정렬될 수 있고 충돌도 가질 수 있어서, 각 스프라이트를 축 정렬 사각형으로 다루면 기초적인 3D 지오메트리를 만들 수 있음. 첫 레벨에서는 출구 버튼 직전에 두 건물 사이 다리를 만드는 데 쓰임
  - Blake Stone과 Rise of the Triad는 Wolf3D 엔진의 후기 버전을 썼고, **텍스처 바닥과 천장**이 있었음  
    Duke Nukem의 Build 엔진은 BSP를 쓰지 않았음
    
    [https://www.jonof.id.au/forum/topic-137.html#msg1548](<https://www.jonof.id.au/forum/topic-137.html#msg1548>)
  - 나중에 Shadow Warrior에서는 그런 것도 가능했던 것 같음. 포털로 구현한 것으로 기억하고, 편집기에서 설정하기 꽤 고통스러웠음
  - 바닥에 관해서는, 아는 한 **DOOM**조차 정확하게 처리하지 않았음. 수직 벽에서는 특정 벽 조각에 대해 픽셀 열마다 원근 나눗셈을 한 번만 하면 됨  
    바닥에는 안타깝게도 그런 사치가 없고, 기억이 맞다면 DOOM은 바닥을 패치로 나눈 뒤 모서리에서만 올바른 원근을 계산하고 중간은 보간했음
  - 처음에는 그냥 스킨만 바꾼 Wolfenstein 3D인 줄 알았음. 그건 심하게 불공평한 판단이었고, 여기에는 일이 정말 많이 들어갔음

- 훌륭한 글임. 특히 **gib 애니메이션**을 만드는 접근이 재미있었음  
  기술 데모였지만 90년대 중반쯤 나도 비슷한 것을 만든 적이 있음. 이 글에서는 보이지 않았던 점으로, 텍스처에 8x8 또는 16x16 라이트맵을 써서 깜빡이는 횃불이나 복도를 날아가며 빛을 비추는 로켓 같은 효과를 쉽게 만들었음. 원하면 조명을 “구워 넣는” 데도 라이트맵을 쓸 수 있음  
  라이트맵이 “겨우” 8x8이라 각 럭셀, 즉 라이트맵의 각 단위마다 광원까지의 거리와 시야선을 계산해 밝기 값을 구하는 정도의 수학은 감당할 수 있었음. 텍스처를 렌더링할 때는 럭셀을 조회 테이블과 함께 사용해 실제로 그릴 픽셀 색을 결정했음  
  성능을 위해 라이트맵은 기억상 초당 15번 갱신했음. DJGPP 덕분에 렌더링에는 인라인 어셈블리를 썼고, 당시 부동소수점 연산이 느려서 잘 최적화되는 고정소수점 연산을 사용했음. 당시 컴퓨터 기준으로 렌더링 성능은 놀라울 정도였음
  - **고정소수점**이라는 발상은 너무 덜 쓰이고 과소평가되는 느낌임. 더 나은 선택인 분야가 정말 많고, 성능까지 더 좋은 경우도 많음

- 1990년대 초중반의 **그래픽 프로그래밍**은 꽤 재미있었음. 메모리 매핑된 비디오 RAM에 픽셀 데이터를 쓰면 화면에 바로 나타났음  
  0xA0000을 가리키는 포인터 하나면 충분했고, API 같은 건 필요 없었음. 언급된 비정사각 픽셀 320×200 VGA 모드의 이유는 비디오 버퍼가 64000바이트라 16비트 세그먼트 안에 들어가서 16비트 코드와 CPU에서 주소 지정이 쉬웠기 때문임
  - 당시 콘솔에 비해 PC는 CPU가 괴물 같았는데도, 그래픽 구조 때문에 1985년 NES의 Mario 같은 **부드러운 스크롤**을 구현하는 데 애먹었다는 점이 늘 재미있었음  
    하지만 그 약점 덕분에 화면의 픽셀 하나당 훨씬 많은 일을 할 수 있었고, 그래서 이런 광선 투사나 BSP 트리 시스템이 가능했음  
    스프라이트와 배경 레이어용 전용 프로세서는 없었지만, 그만큼 PC가 할 수 있는 일이 딱딱한 고정 기능 구조에 갇히지 않았음  
    90년대 중후반 전용 3D 프로세서가 나오면서 더 이상 문제가 아니게 됐지만, 90년대 초 잠깐 동안은 독특한 시각 렌더링의 놀이터가 있었음
  - 0xA0000 포인터 하나면 충분하긴 했지만, 사용하는 **익스텐더**가 그 부분을 조금 더 귀찮게 만들 수는 있었음 :-P  
    DJGPP와 Free Pascal은 DJ Delorie의 같은 go32 익스텐더를 쓰는데, 완전한 선형 매핑을 하지 않아서 화면에 무언가를 띄우려면 조금 더 손봐야 했음
  - VGA가 나오기 전까지는 이야기가 훨씬 더 복잡했음

- 가장 흥미로운 건 내부 도구들임. gib 애니메이션을 만드는 Python 스크립트나, Blender에서 **2D 스프라이트시트**를 생성하는 다른 Python 스크립트 같은 것들임  
  원글 작성자는 좋은 그림도 만들 수 있는 10x 엔지니어가 분명해 보이고, 이런 경우는 정말 드물다고 봄. 일관된 아트 디렉션이 있다는 점도 꽤 놀라웠음
  - 90년대에 이 장르의 팬으로 보기에는, 이런 **르네상스형 엔지니어**들이 주요 히트작 뒤에 늘 있었던 것 같음. 몇몇 이름은 아직 기억나고, 그들은 진짜 예술가였음  
    지난 15년 게임 업계에서는 CEO나 리드 디렉터를 빼면 거의 누구 이름도 모르겠음

- 이 게임은 여성 주인공이 나오는 드문 슈터 중 하나일 수도 있겠다고 방금 알아챘음. 고양이가 **삼색털 무늬**인데, 그런 고양이는 거의 항상 암컷임 ([https://en.wikipedia.org/wiki/Calico_cat](<https://en.wikipedia.org/wiki/Calico_cat>))
  - 여성 주인공 슈터가 그렇게 드문가? 당장 떠오르는 꽤 주류 작품만 해도 Perfect Dark, Mirror's Edge, Dishonored 1편인지 2편인지, Metroid 등이 모두 일종의 슈터이고 여성 주인공임  
    물론 Mirror's Edge는 100% 정확히 말하면 “슈터”라기보다 “1인칭”에 더 가깝긴 함  
    게다가 “RPG + FPS” 중에는 남자나 여자로 플레이할 수 있는 작품도 많음  
    작성자도 무늬와 고양이의 성별 가능성을 알고 있는 듯함:
    
    `After all, I do need to give the protagonist his fair share. [image] (Yes, I know it's a female, but call it convention rooted in dialect.)`
  - 이건 Perfect Dark 게임이 아님
  - 요즘 **부머 슈터**에는 여성 주인공이 꽤 많음. 예를 들면 Selaco[0], Supplice[1], The Citadel[2]와 후속작[3], Zortch[4]와 예정된 후속작[5], Nightmare Reaper[6], COVEN[7], Viscerafest[8], Hedon[9] 등이 있음  
    오히려 요즘은 여성 주인공 부머 슈터가 아닌 것보다 더 많은 것 같음 :-P Steam 검색에서 “boomer shooter”와 “female protagonist” 태그를 합치면 143개가 나오는데, 캐릭터 성별을 고를 수 있거나 대부분은 남자로 플레이하지만 일부 구간에서 여자로 플레이하는 게임도 포함되긴 함
    
    [0] [https://store.steampowered.com/app/1592280/Selaco/](<https://store.steampowered.com/app/1592280/Selaco/>)
    
    [1] [https://store.steampowered.com/app/1693280/Supplice/](<https://store.steampowered.com/app/1693280/Supplice/>)
    
    [2] [https://store.steampowered.com/app/1378290/The_Citadel/](<https://store.steampowered.com/app/1378290/The_Citadel/>)
    
    [3] [https://store.steampowered.com/app/3371240/Beyond_Citadel/](<https://store.steampowered.com/app/3371240/Beyond_Citadel/>)
    
    [4] [https://store.steampowered.com/app/2443360/Zortch/](<https://store.steampowered.com/app/2443360/Zortch/>)
    
    [5] [https://store.steampowered.com/app/3807500/Zortch_2/](<https://store.steampowered.com/app/3807500/Zortch_2/>)
    
    [6] [https://store.steampowered.com/app/1051690/Nightmare_Reaper/](<https://store.steampowered.com/app/1051690/Nightmare_Reaper/>)
    
    [7] [https://store.steampowered.com/app/1785940/COVEN/](<https://store.steampowered.com/app/1785940/COVEN/>)
    
    [8] [https://store.steampowered.com/app/1406780/Viscerafest/](<https://store.steampowered.com/app/1406780/Viscerafest/>)
    
    [9] [https://store.steampowered.com/app/1072150/Hedon_Bloodrite/](<https://store.steampowered.com/app/1072150/Hedon_Bloodrite/>)
  - 의도적이었을 것 같지는 않지만, 전반적으로 그런 점에는 별로 감명받지 않고 가치도 못 느끼겠음. Hollywood에서 여성이 자기 두 배 크기 남자를 때려눕히는 묘사도 마찬가지임  
    비현실적이고 우스꽝스럽고 해롭다고 봄

- 정말 멋짐. 90년대에 쓰던 또 다른 재미있는 트릭은 **팔레트 애니메이션**이었음. 팔레트를 바꾸는 것만으로도 실행 비용이 낮은데 엄청 멋진 효과를 만들 수 있었음
  - 맞음. 이 기법의 멋진 예제를 보려면 이 웹사이트를 강력히 추천함
    
    [http://www.effectgames.com/demos/canvascycle/](<http://www.effectgames.com/demos/canvascycle/>)
  - 프레임 중간에 **팔레트**를 바꾸는 것도 재미있음. PC에는 Amiga의 copper 같은 것이 없어서 타이밍에 훨씬 더 신경 써야 하지만, 그래도 가능함
  - Diablo 1과 2의 많은 적이 사실상 같은 스프라이트에 다른 팔레트를 입힌 것이라고 기억함. 같은 트릭인가?
  - 파란색은 물, 보라색은 플라즈마, 빨강/주황은 피나 용암임

- 렌더링한 뒤 **양자화된 스프라이트**가 꽤 좋아 보여서 정말 놀랐음. 그 빠른 변환 덕분에 아주 선명해 보였음

- 말도 안 되게 무리한 제약을 둔 **3D 엔진**을 만드는 동료 개발자로서, 여기 설명의 세부와 거쳐 간 과정을 보는 게 정말 좋음

- PlayStation 홈브루로 **복셀 공간 렌더링** 기술 데모를 만지작거리는 중임. 주말 하루 이틀 작업한 뒤에도 10~15 FPS 정도로 괜찮은 결과가 나오고 있고, 아직 DMA나 GTE는 물론 폴리라인 기본 도형도 쓰지 않았음  
  삼각법과 옛날식 저수준 최적화 요령을 다시 꺼내는 게 신선함. 스크래치 버퍼가 1KiB이고 스택은 그중 일부만 쓸 수 있을 때면, 직장에서 쓰는 마이크로컨트롤러들이 얼마나 호사스러운지 깨닫게 됨. 거기서는 스레드마다 8KiB 스택이 할당되고, C++ 템플릿 함수가 50개 넘게 쌓인 백트레이스도 나오니까 말임
