- Ctrl-Alt-Test 팀은 2022년 11월부터 단편 애니메이션처럼 보이는 실시간 데모를 8kB 안에 담는 실험을 시작해, 2023년 4월 The Sheep and the Flower를 공개함
- 결과물은 리소스 파일이나 외부 의존성 없이 단일
.exe가 그래픽과 음악을 생성하며, 장면은 GLSL 셰이더와 레이마칭으로 GPU에서 실시간 계산됨 - 양, 표지판, 카메라, 타임라인은 저장된 모델이나 텍스처가 아니라 SDF, 절차적 3D 텍스처, 하드코딩된 시간 파라미터로 구성됐고 약 25개의 카메라 샷이 수작업으로 조정됨
- 음악은 OpenMPT와 4klang으로 만들었고, 악기 수를 16개에서 13개로 줄이고 반복을 늘려 압축 효율을 높임
- 최종 8kB는 Crinkler 압축과 Shader Minifier 개선으로 가능했으며, 42kB 셰이더 코드는 축소와 압축 후 약 5kB에 들어가 음악과 C++ 코드 공간을 남김
8kB 안에 담긴 단일 실행 파일
- The Sheep and the Flower는 2023년 4월 공개된 Windows 실행 파일 형태의 실시간 애니메이션임
- 하나의
.exe파일이 모든 것을 생성하며, 별도 리소스 파일이나 외부 의존성이 없음- 실행에는 Windows와 최신 드라이버가 필요함
- 목표는 8kB 안에 그래픽, 애니메이션, 연출, 카메라워크, 음악을 모두 넣는 것이었음
- 소스 코드는 GitHub에 공개됨: ctrl-alt-test/mouton
그래픽·음악·압축에 쓰인 도구들
- 모든 시각 요소는 GPU에서 실시간 계산됨
- GLSL 셰이더가 타임라인 정보, 카메라 설정, 장면 표현까지 포함함
- 렌더링은 레이마칭(raymarching)으로 수행됨
- 셰이더는 Shader Minifier로 축소됨
- 음악은 OpenMPT와 4klang 신시사이저로 제작됨
- 악기는 절차적으로 기술됨
- 음표 목록은 압축됨
- 코드는 Visual Studio 2022에서 C++로 작성됨
- 컴파일러 플래그와 초기화는 Leviathan 프레임워크를 기반으로 시작함
- 최종 실행 파일은 Crinkler로 압축됨
왜 8kB였나
- 데모씬에는 4kB와 64kB 같은 크기 제한 카테고리가 흔함
- 기존 작업은 주로 64kB를 목표로 했지만, 8kB는 전혀 다른 제약과 규칙을 제공함
- 4kB 인트로 기법은 흥미롭지만 제대로 된 스토리텔링에는 너무 제한적이라고 봄
- Revision demoparty가 몇 년 전 8kB 경쟁 부문을 추가했고, 이를 실험할 좋은 기회로 삼음
두 개의 삼각형으로 그리는 세계
- 데모씬에서 2008년 이후 표준적으로 쓰인 방식처럼, 화면 전체를 덮는 사각형을 두 개의 삼각형으로 그림
- 이 사각형 위에서 GLSL 셰이더가 각 픽셀과 각 프레임의 색상을 계산함
- 셰이더 함수는 좌표와 시간을 입력받아 색상을 반환함
- 핵심 과제는 이 함수 안에서 양과 장면 전체를 어떻게 표현하느냐였음
SDF로 장면 구성하기
- 부호 있는 거리장(SDF) 은 공간의 한 점에서 가장 가까운 물체까지의 거리를 계산하는 함수임
- 표면 위에서는 0을 반환함
- 물체 내부에서는 음수를 반환함
- 구나 상자 같은 기본 도형은 간단한 수식으로 표현할 수 있음
- 여러 거리장을 조합해 장면을 구성함
- 두 물체의 합집합은 거리의 최솟값으로 만들 수 있음
- 교집합은 최댓값으로 만들 수 있음
- 부드러운 합집합(smooth union)을 쓰면 더 유기적인 형태를 만들 수 있음
- 양은 원뿔, 구 같은 단순 도형을 조립하고 병합해 만들었으며, 몸통과 털은 3D 노이즈 함수로 구성됨
레이마칭으로 픽셀을 결정하는 방식
- 레이마칭은 SDF를 이용해 3D 장면 안에서 광선을 조금씩 전진시키는 렌더링 기법임
- 전통적인 레이트레이싱처럼 교차점을 수학적으로 바로 계산하지 않음
- SDF가 충돌 없이 이동할 수 있는 거리를 알려주고, 그만큼 광선을 전진시키는 과정을 반복함
- 거리가 0 또는 0에 가까워지면 교차점을 찾은 것으로 처리함
- 교차점이 생기면 현재 픽셀이 양, 하늘, 다른 물체 중 무엇에 해당하는지 알 수 있음
- 조명에는 표면 법선이 필요하며, 주변의 기울기를 계산해 법선을 추정함
- 그림자는 교차점에서 태양 방향으로 또 다른 광선을 보내 중간에 물체가 있는지 확인해 계산함
작은 표현력으로 이야기를 전달하는 법
- 데모에는 목소리나 글자가 없고, 캐릭터도 하나뿐임
- 양은 걷기, 머리와 눈 움직임 정도만 가능하며, 회전도 할 수 없음
- 이 제약 안에서 감정을 전달하기 위해 이야기와 무관한 요소를 제거함
- 처음에는 사막과 모래언덕, 하늘을 생각했지만 이야기에는 필요 없다고 판단함
- 배경은 순백으로 유지함
- 의미를 전달하는 표지판과 눈을 제외하고 텍스처를 줄임
- 작은 범위를 유지해 디테일, 폴리싱, 카메라워크, 편집, 음악 동기화에 집중함
- 각 샷은 수작업으로 구성됐고, 애니메이션은 흐름이 자연스럽게 느껴지도록 여러 번 반복 조정됨
카메라와 연출
- 카메라는 이야기 전달 도구로 사용됨
- 양이 오랜 시간 방황하는 장면에는 넓은 샷을 사용해 고독감을 만듦
- 표지판을 알아차리는 순간에는 눈의 익스트림 클로즈업을 사용함
- 양이 표지판을 바라볼 때는 머리 쪽으로 느리게 줌인해 집중도를 높임
- 양의 흥분은 여러 표현을 겹쳐 전달함
- 만화적인 2D 효과
- 빨라진 걸음
- 강조된 꼬리 움직임
- 과장된 머리 흔들림
- 음악의 극적인 변화
빠른 반복을 위한 개발 방식
- 소스 코드에는 많은 하드코딩 상수가 들어 있음
- 양의 눈 크기
- 카메라 이동 속도
- 각 샷의 길이
- 꽃의 색상 같은 값들이 반복 조정됨
- 빠른 피드백을 위해 셰이더를 런타임에 다시 컴파일해 1초 안에 그래픽을 갱신함
- 애니메이션과 카메라 조정을 위해 플레이어가 필요했음
- 일시정지
- 재생
- 시간 제어
- 셰이더 라이브 리로드 후 즉시 확인
- 음악과 애니메이션을 정확히 동기화하려면 음악 지원도 필수였음
- 초기 프로토타입은 Shadertoy에서 만들었고, 이후 KodeLife를 거쳐 C++ 프로젝트로 옮김
4klang으로 만든 음악
- 음악은 이야기 전달에 중요한 요소였고, 여러 분위기와 특정 시점의 전환이 필요했음
- 4kB 인트로에서 쓰는 도구를 그대로 사용하되, 더 복잡한 구성을 위해 공간을 더 배정함
- 4klang synthesizer는 음악 소프트웨어에서 사용할 수 있는 플러그인을 제공하고, 어셈블리 파일을 내보냄
- 이 어셈블리 파일은 데모와 함께 컴파일·링크됨
- 데모 실행 시 신스가 별도 스레드에서 파형 사운드를 절차적으로 생성해 사운드카드로 보냄
- 처음 만든 음악은 예상보다 컸고, 크기를 줄이기 위해 여러 조정을 거침
- 악기 수를 16개에서 13개로 줄임
- 엔딩 테마를 전체 음악 템포에 맞게 다시 작곡함
- 반복을 늘려 압축이 잘 되도록 구성함
- 배경음의 음표 길이처럼 귀로는 거의 느껴지지 않는 변경으로 압축률을 개선함
- 음악의 전체 구조는 유지하면서 품질 손실을 최소화하고 공간을 절약함
애니메이션과 시간 동기화
- 데모는 사전 계산이나 캐시 없이 매 프레임 모든 것을 다시 평가함
- 성능에는 불리하지만, 어떤 요소든 시간에 의존해 변할 수 있어 애니메이션에는 유리함
- 전체 데모는 약 25개의 수작업 카메라 샷으로 구성됨
- 각 샷은 18개 파라미터가 시간에 따라 어떻게 변하는지 기술함
- 각 물체의 위치
- 양의 상태
- 카메라 위치
- 초점
- 카메라가 바라보는 대상 등
- 예시로
camPos = vec3(22., 2., time*0.6-10.);한 줄만으로 샷의 카메라 선형 이동을 표현함 time은 데모 시작 이후 절대 시간이 아니라 샷 시작 이후 시간을 뜻함- 샷을 삽입, 삭제, 조정해도 나머지 데모에 영향을 덜 줌
- 단순 선형 보간은 기계적으로 보일 수 있어, 여러 경우에 smoothstep을 사용함
- 타임라인 코드는 vertex shader 안에 있음
- 샷 정의에 반복 코드가 많지만, 반복은 압축에 유리하게 작용함
텍스처와 재질
- 전통적인 렌더러에서는 2D 텍스처를 3D 모델 표면에 입힘
- 레이마칭 방식에서는 텍스처 좌표를 쉽게 계산하기 어려워, 대신 3D 텍스처를 즉석 계산함
- 레이마처가 렌더링할 3D 위치를 찾으면, 해당 3D 좌표를 텍스처 함수에 전달함
- 교통 표지판은 수학으로 삼각형이나 사각형 테두리를 만들고, 내부 콘텐츠는 여러 함수를 조합해 그림
- 식당 기호는 검은 타원형 모양 4개와 홈을 만드는 흰색 모양 2개로 구성됨
- 시각적 다양성을 위해 텍스처뿐 아니라 재질도 다르게 설정함
- 예를 들어 양의 발굽은 프레넬 계수를 이용해 다른 반사를 갖도록 처리함
눈을 살리는 가짜 반사
- 개발 중 오랫동안 눈이 생기 없고 밋밋하게 보였음
- 눈은 캐릭터 디자인과 이야기 전달에서 중요한 요소였음
- 만화 캐릭터 참고 이미지에서는 홍채가 많지만 필수는 아니며, 동공은 항상 크게 그려지는 경향을 확인함
- 반짝이는 눈을 만들려면 눈 안에 빛 반사가 필요했음
- 기존 조명 방정식으로는 태양과 카메라가 특정 위치에 있을 때만 반사가 생겼음
- 이를 보완하기 위해 표면 법선 벡터를 수정해 태양 반사가 생길 확률을 높임
- 추가로 환경 매핑을 사용함
- 일반적으로 게임에서는 실제 환경 반사를 단순화해 최적화하는 데 쓰임
- 여기서는 실제 세계가 완전한 흰색에 가깝지만, 텍스처로 디테일을 가짜로 추가함
- 눈의 흰자와 동공에는 빈 세계보다 훨씬 복잡한 반사가 들어감
- 여러 가짜 광원
- 어두운 지면과 푸른 하늘을 흉내 내는 그라디언트
후처리와 시각 스타일
- 최종 분위기와 이미지 품질은 후처리에서 다듬음
- 사용한 후처리는 다음과 같음
- 컬러 그레이딩
- 감마 보정
- 약한 비네팅
- 앨리어싱을 줄이기 위한 2패스 FXAA 필터
- 눈의 별 효과나 엔딩 화면 효과처럼 일부 효과도 후처리 단계에서 구현됨
- 이 효과들은 순수 2D이며 3D 세계에는 존재하지 않음
- 한때 오래된 만화풍 스타일도 실험함
- 손그림처럼 보이게 하는 윤곽선 검출
- 흑백 렌더링
- 그레인과 노이즈
- 논의 끝에 이 실험은 포기하고 더 깨끗하고 현대적인 외형에 집중함
Crinkler 압축
- 핵심 아이디어는 데이터를 저장하지 않고, 데이터를 생성하는 코드를 저장하는 방식임
- 음악은 연주할 음표 목록과 각 악기의 명령 목록을 저장함
- Crinkler는 데모씬과 1kB~8kB 인트로를 위해 설계된 압축 도구임
- 실행 파일은 자체 압축 해제가 가능해야 하므로, Crinkler는 나머지 실행 파일을 풀 수 있는 작은 어셈블리 코드를 포함함
- Crinkler는 크기 최적화를 우선함
- 압축 알고리듬은 시간이 걸림
- 압축 해제는 비교적 느림
- 수백 MB의 RAM을 사용함
- 하지만 Crinkler만으로는 충분하지 않았고, 셰이더 소스 코드 42kB를 실행 파일 안에 넣기 위해 추가 축소가 필요했음
Shader Minifier 개선
- 셰이더 소스 코드는 최종 바이너리에 포함되므로 가능한 작아야 함
- 손으로 축소할 수도 있지만, 유지보수와 빠른 반복에 문제가 생김
- Shader Minifier는 2011년부터 개발된 셰이더 축소 도구임
- 공백과 주석을 제거하고, 변수 이름을 줄이며, 더 많은 변환을 수행함
- 8kB 인트로는 4kB보다 훨씬 많은 코드를 포함해 새로운 문제가 생겼고, 필요한 기능을 추가하기 위해 한 달 동안 데모 작업을 멈춤
- 좋은 축소를 위해 단순 축소기가 아니라 Closure Compiler와 비슷한 소스-투-소스 컴파일러가 필요했음
- Shader Minifier가 수행하는 변환에는 다음이 포함됨
- 변수와 함수 이름 변경
- 변수 인라이닝
- 상수 산술식 평가
- 함수 인라이닝
- 죽은 코드 제거
- 선언 병합
- 코드 자체 크기를 줄이는 것만으로는 충분하지 않고, 압축 친화적인 출력이 필요했음
- 어떤 변환은 코드 크기를 줄이지만 압축 후 크기를 늘릴 수 있음
- 데모를 반복 개발하는 동안 압축 후 크기를 계속 확인해야 했음
- 새 Shader Minifier 개선으로 압축 바이너리에서 약 600바이트를 절약함
- 최종적으로 42kB 셰이더 코드는 축소와 압축 후 약 5kB에 들어감
- 남은 공간에 음악과 C++ 코드가 들어감
다른 작업 위에 쌓인 결과
- 이 데모는 레이마칭 기법, 음악 생성 소프트웨어, 압축 알고리듬 등 다른 사람들이 만든 작업 위에 만들어짐
- Shader Minifier에 추가된 새 기능은 앞으로 더 나은 데모를 만드는 데 도움이 되기를 기대함
- 8kB 카테고리는 4kB보다 더 많은 가능성을 제공하며, 더 인기를 얻기를 바란다는 기대가 남아 있음
- 비교를 위해 원문 텍스트는 약 21,000자이며, 그대로라면 21kB가 필요함