관측 가능한 우주
오래전부터 만들고 싶었지만 우주를 구현할 엄두가 나지 않아 생각만 하던 게임이 있습니다. 어느 날 택시 안에서 폰으로 프롬프트를 넣기 시작해 그 게임을 만들었습니다. 관측은 되지만 도달할 수 없는 거리에서 느끼는 고독에 대한 이야기입니다.
병원에서 진료를 마치고 회사로 돌아오는 택시 안이었습니다. 별다른 일은 없었고 창밖으로 익숙한 거리가 흘러가는 평범한 오전이었습니다. 손에 쥔 폰을 들여다보다가 오래된 생각이 다시 떠올랐습니다. 십수 년쯤 묵은 생각이었습니다. 작은 우주선 하나를 타고 광활한 우주를 유영하면서 우주가 정말로 얼마나 큰지를 몸으로 느끼고 같은 우주 안에 분명히 다른 사람들이 있으면서도 사실상 누구와도 만나지 못하는 그 고독을 맛보게 하는 게임. 한 번도 만들지 못했고 만들 방법도 몰랐던 그냥 가끔 떠오르기만 하던 생각이었습니다. 그런데 그날따라 그 생각 끝에 평소에는 들지 않던 다른 생각이 갑자기 나타났습니다. 이제는 이걸 정말 만들 수 있을는지도 모르겠다는 생각입니다. 택시가 신호에 걸려 멈춰 선 사이에 폰의 자판을 열고 프롬프트를 적기 시작했습니다.
만들고 싶었던 경험은 분명했습니다. 군중 속의 고독이라고 부를 수 있는 경험입니다. 데이비드 리스먼이 같은 제목의 책에서 짚은 것처럼 사람은 무리 한가운데에서 오히려 더 외로워질 수 있습니다[^The Lonely Crowd]. 우주는 그 경험을 물리적으로 가장 잘 구현할 수 있는 무대입니다. 우주는 너무 넓어서 같은 하늘 아래 다른 우주선이 떠 있다는 걸 알면서도 그 우주선까지 가는 데 며칠이 걸리고 말을 걸어도 그 말이 빛의 속도로 나아가 한참 뒤에야 도착합니다. 분리되어 있어서 외로운 게 아니라 연결되어 있지만 상호작용 할 수 없어서 외롭습니다. 이 경험을 주는 게임을 오래 기다렸지만 아무도 만들지 않았습니다. 당연한 일이었습니다. 이런 경험은 상업적으로 아무런 가치가 없기 때문입니다. 하루에 한 번 들어와서 위치를 확인하고 방향을 정하고 마는 자극도 보상도 거의 없는 느린 게임을 누가 돈을 들여 만들 수 있을까요. 그래서 늘 아쉬웠고 아쉬운 채로 십수 년이 지났습니다.
만들지 못한 이유는 따로 있었습니다. 우주를 구현하는 일이 한 사람이 감당할 수 있는 규모가 아니었기 때문입니다. 태양계 하나만 제대로 움직이게 하려 해도 행성들의 실제 위치를 알아야 하고 궤도역학을 풀어야 하고 별 하늘을 그리려면 수만 개의 별을 어딘가에서 가져와야 하고 행성 표면과 성운과 은하를 그리려면 아티스트가 평생 그려도 모자랄 텍스처가 필요했습니다. 거기에 광속 지연과 시간 지연 같은 상대론에 따라 만들려면 이건 개인이 주말에 손댈 물건이 아니었습니다. 그래서 매번 생각만 하다가 접었습니다. 엄두가 안 나는 일이었습니다.
그날 택시 안에서 그 엄두가 바뀐 건 온전히 이제 저 혼자 개발하지 않아도 되었기 때문입니다. 지난 '기계와 함께'에서 적었듯이 기계와 함께 일하는 건 먼 미래의 이야기가 아니라 제 폰 안에서 벌어지는 현실이고 '자동완성'에서 살펴본 것처럼 그 기계는 다음 토큰을 예측하는 단순한 원리 위에서 생각보다 훨씬 많은 것을 할 수 있습니다. 혼자서는 평생 손도 못 댈 분량의 코드를 방향만 정확히 잡아 주면 기계가 한 줄 한 줄 작성할 수 있게 되었습니다. 우주를 구현하는 일이 어려운 건 그대로였지만 그 어려움을 혼자 감당하지 않아도 되는 시대가 온 것이었습니다. 택시에서 시작한 프롬프트는 이후에도 이어졌고 그렇게 'SPACE'라는 이름의 게임이 실제로 서비스하기 시작해 지금까지 639번을 수정했습니다. 대부분의 작업은 방향을 정하면 기계가 자율적으로 수행하는 구간이었습니다.
설계의 첫 줄은 처음부터 끝까지 한 문장입니다. 우주는 정교하게 우주선은 단순하게. 플레이어가 할 수 있는 일은 네 가지로 한정했습니다. ISS에서 분리하고 목적지를 정하고 가속하고 감속하는 것이 전부입니다. 우주선은 엔진이 하나뿐이라 방향을 바꾸려면 기체를 통째로 돌려세워야 하고 감속하려면 180도 뒤집어 거꾸로 엔진을 켜는 플립 앤 번을 해야 합니다. 조작을 이렇게까지 줄인 건 이 게임이 실시간으로 손가락을 놀리는 게임이 아니기 때문입니다. 모든 시뮬레이션은 서버에서 수행하고 클라이언트는 그 결과를 그리기만 하고 접속을 끊어도 서버에서 제 우주선은 계속해서 이동하고 있습니다. 그래서 하루에 한 번이나 며칠에 한 번 들어와 그동안 얼마나 멀리 왔는지를 확인하는 방치형입니다. 느린 게 흠이 아니라 느린 게 목적인 게임입니다. 우주의 크기는 빠르게 돌아다녀서는 결코 느낄 수 없고 한 번의 결정이 며칠 뒤에야 결과로 돌아오는 그 간격 속에서만 체감되기 때문입니다.
한 번의 접속이 실제로 어떻게 흘러가는지 설명드리겠습니다. 며칠 만에 들어가면 우주선은 떠난 그 자리에 있지 않습니다. 그동안 서버가 쉬지 않고 시뮬레이션했기 때문에 지난번 잡아 둔 방향으로 한참을 더 나아가 있고 누적 비행 거리와 선내 시계가 바깥 시계로부터 얼마나 벌어졌는지가 그동안의 여정을 숫자로 보여 줍니다. 콕핏의 계기판을 훑어 지금 어느 천체의 중력권에 들어와 있는지 다음 목적지까지 얼마나 남았는지 도착 예정 시각은 언제인지를 확인합니다. 그러고 나서 하는 일은 대개 하나입니다. 방향을 살짝 틀거나 도착이 가까웠으면 미리 감속 번을 걸어 두거나 멀리 흐릿하게 보이는 성운 쪽으로 새 목적지를 찍습니다. 결정을 내리고 나면 다시 접속을 끊습니다. 그 한 번의 결정이 며칠 뒤 다음 접속에서야 결과로 돌아오기 때문에 이 게임은 손이 아니라 기다림으로 플레이됩니다. 매일 잠깐 접속해 제 작은 점이 거대한 배경 위를 천천히 나아가는 걸 지켜보는 게 한 사이클의 거의 전부입니다. 보상이라 부를 만한 건 레벨도 아이템도 아니고 다음에 들어왔을 때 '그동안 이만큼이나 왔구나' 하는 그 한순간뿐입니다.
방금 콕핏의 계기판이라고 적었지만 그 콕핏 자체도 처음부터 이런 모습은 아니었습니다. 초기에는 다른 게임처럼 화면 한쪽 구석에 납작한 정보창을 띄워 두고 거기 붙은 버튼으로 우주선을 조종했습니다. 1인칭으로 콕핏 안에 들어갈 수는 있었지만 그때 콕핏의 모니터들은 시계나 궤적 같은 숫자를 수동적으로 비춰 줄 뿐이었고 실제 조작은 여전히 바깥의 그 납작한 창에서 이뤄졌습니다. 화면 위에 떠 있는 메뉴와 우주선 안에 앉아 있다는 경험이 따로 노는 게 신경 쓰였습니다. 그래서 바깥 창에 있던 기능을 전부 콕핏 안의 모니터로 옮기기로 했습니다. 문제는 기능이 너무 많아서 한 화면에 다 담으면 정면으로 봐도 빽빽해 알아보기 어렵다는 점이었습니다. 처음엔 모니터 세 대에 나눠 보려 했지만 자꾸 한 작업이 두 화면에 쪼개졌고 화면을 늘려 가면서 역할을 다시 나눈 끝에 지금은 모두 여섯 대입니다. 여섯 대는 정면을 사이에 두고 왼쪽에 세 대 오른쪽에 세 대씩 두 줄로 섰고 화면마다 무엇을 맡을지는 항해를 머릿속에서 어떻게 그리는지에 맞춰 배치했습니다. 왼쪽 세 대는 어디로 갈지를 정하는 항행 계열이라 위에서부터 목적지를 고르는 화면과 항로와 비행계획을 짜는 화면과 그 계획을 자동으로 실행하는 오토파일럿 화면이 차례로 놓입니다. 오른쪽 세 대는 상태에서 조종을 거쳐 교신으로 이어지는 계열이라 위에서부터 속도와 시계를 비추는 읽기 전용 비행계기와 점화를 직접 누르는 조종간과 다른 우주선에 신호를 보내는 교신 화면이 차례로 놓입니다. 자주 보는 건 가운데 가까이 두고 가끔 쓰는 건 바깥으로 밀었고 읽기만 하는 계기와 눌러서 상태를 바꾸는 버튼은 되도록 다른 화면에 나누어 두었습니다. 이제 1인칭으로 앉으면 메뉴를 부를 일 없이 모니터를 톡톡 건드리는 것만으로 항해의 전 과정이 끝납니다. 떠 있는 창을 들여다보는 게 아니라 정말로 콕핏에 앉은 경험을 위해 인터페이스를 옮겼습니다.
이 경험을 만들려면 우주를 실제로 구현해야 했습니다. 광활함은 속일 수가 없습니다. 물리법칙을 따르도록 했습니다. 출발점은 실제 ISS의 현재 위치입니다. CelesTrak이 공개하는 ISS의 궤도 정보를 받아 SGP4라는 표준 전파 모델로 지금 이 순간의 위치를 계산해 거기서 시작합니다[^Two-line element set]. SGP4는 위성의 궤도 요소로부터 임의의 시각의 위치를 구해 내는 표준 모델입니다. 지구가 완전한 구가 아니어서 생기는 궤도의 일그러짐과 대기 저항 같은 교란까지 반영하기 때문에 그 위치가 실제 ISS와 거의 어긋나지 않습니다. 태양과 행성과 달은 천문학자 스탠디시가 정리한 케플러 궤도요소를 써서 실제 위치에 배치되고[^Approximate Positions of the Planets] 우주선은 그 천체들의 중력 합을 받아 날아갑니다. 적분기는 장기 항해에서도 궤도 에너지가 폭주하지 않도록 에너지를 보존하는 대칭 적분기를 골랐습니다[^Verlet integration]. 그리고 무엇보다 광속은 절대적입니다. 우주선은 빛의 속도를 넘을 수 없고 빛의 99.99퍼센트까지 가속하면 시간 지연 때문에 선내 시계가 바깥의 약 70분의 1 속도로 흐릅니다[^The Relativistic Rocket]. 선내에서 8일이 흐르는 동안 바깥세상에서는 1년 반이 지나는 식입니다.
군중 속의 고독은 바로 이 광속의 벽에서 나옵니다. 게임 안의 모든 플레이어는 단 하나의 우주를 공유하지만 다른 우주선을 봐도 보이는 것은 그 우주선의 지금 모습이 아니라 빛이 출발하던 과거의 모습입니다. 거리가 멀수록 더 오래된 유령이 보이고 너무 멀면 그 우주선이 지금 어디 있는지조차 알 수 없습니다. 말을 거는 것도 마찬가지입니다. 직접 메시지를 만들 수 없고 미리 정해진 짧은 신호를 전파로 전송할 수 있을 뿐입니다. 그 신호가 도착할 즈음 상대는 이미 다른 곳에 가 있어서 어지간해서는 빗나갑니다. 같은 우주에 있고 분명 관측되지만 상호작용 할 수는 없습니다. 관측 가능한 우주라는 말은 천문학에서 빛이 우리에게 도달할 시간이 있었던 범위인 465억 광년을 가리키는 말인데[^Planck 2018 results. VI. Cosmological parameters] 이 게임에서는 이 말에 한 가지 경험이 추가됩니다. 관측은 가능하지만 만남은 불가능하다는 뜻입니다. 멀리 있는 별과 은하는 방향만 정할 수 있을 뿐 영영 도달할 수 없고 가까운 동료조차 빛의 지연 너머의 유령으로만 보입니다. 외로움이 설계가 아니라 물리로부터 기인한 것이 이 게임의 전부입니다.
이 외로움은 생각할 때와 겪을 때가 달랐습니다. 항해 중에 레이더에 다른 우주선으로부터 반사된 전파가 수신되었습니다. 혹시 도착할까 싶어 메시지를 보냈습니다. 그런데 그 신호는 즉시 도달하는 게 아니라 빛의 속도로 천천히 나아갑니다. 게다가 화면에 보이는 그 우주선의 위치마저 한참 전에 출발한 빛이 그린 과거의 잔상이라 신호가 실제로 그 자리에 도착할 즈음 상대는 다른 곳으로 사라진 뒤였습니다. 응답은 오지 않았습니다. 분명히 같은 우주에 있었고 분명히 관측은 됐지만 끝내 도달하지 못합니다. 만들고 싶었던 경험이 이것이었습니다. 누군가 거기 있었다는 것을 알 수 있을 뿐입니다.
한 사람이 이 우주를 다 그릴 수 없다는 원래의 문제는 우주를 저장하는 대신 재현하는 방식을 사용했습니다. 우주를 통째로 저장하는 건 물리적으로 불가능합니다. 별만 해도 그 수가 수천억 곱하기 수천억이라 어떤 스토리지에도 저장할 수 없습니다. 그래서 원칙을 하나 세웠습니다. 실측 데이터가 있으면 데이터를 쓰고 없으면 결정론적으로 생성했습니다. 알려진 것들인 밝은 별과 행성 궤도와 유명한 외계행성과 관측된 블랙홀은 실제 카탈로그에서 가져옵니다. 가까운 별들의 3차원 위치와 트라피스트-1 같은 실제 외계행성계의 행성 배치[^NASA Exoplanet Archive]와 사건의 지평선 망원경이 촬영한 우리 은하 중심의 궁수자리 A* 같은 것들입니다[^Astronomers snap first-ever image of supermassive black hole Sagittarius A*]. 그리고 나머지 모르거나 너무 많은 것들인 먼 별 하나하나와 은하 대부분과 오르트 구름 같은 것들은 같은 시드에 기반한 절차 생성으로 만들어냅니다. 결정론이 핵심입니다. 같은 시드에 기반해 서버와 클라이언트가 각자 계산해도 정확히 같은 우주가 나오기 때문에 무거운 우주를 주고받을 필요 없이 시드만 공유하면 됩니다. 압축이 아니라 재현입니다.
같은 원리를 시각효과에도 적용했습니다. 이 게임에는 아티스트가 그린 텍스처도 모델러가 만든 우주선 모형도 없습니다. 6킬로바이트짜리 지구 해안선 비트맵 하나를 빼면 전통적인 의미의 아트 에셋이 단 한 개도 없습니다. 행성 표면은 구면 위에서 샘플링한 노이즈 함수로 칠하고 지구의 대륙만은 실제 해안선 데이터로 윤곽을 잡은 다음 사막 벨트와 빙상과 밤면의 도시 불빛을 절차적으로 생성했습니다. 성운과 은하는 이미지를 직접 사용하는 대신 그 천체의 알려진 모양과 색을 참고해 로그 나선과 방출선 색으로 칠한 빌보드를 사용합니다. 우주선들은 기본 도형을 조립해 만듭니다. ISS도 허블도 골든 레코드를 단 보이저도 전부 도형의 조합입니다. 우주를 아트 에셋 대신 계산으로 만들었고 덕분에 텍스처 해상도를 높이지 않아도 세밀한 표현이 가능합니다. 아티스트 없이 관측 가능한 우주 전체를 표현할 수 있었던 것은 이 방식 때문입니다.
다만 계산으로 그렸다고 해서 처음부터 그럴듯해 보인 건 결코 아니었습니다. 게임의 규칙과 시스템을 설계하는 사람이어서 셰이더가 뭔지도 톤매핑이 뭔지도 제대로 몰랐습니다. 그래서 이 게임의 그래픽은 처음부터 멋졌던 게 아니라 문제에 하나씩 부딪혀 효과를 한 개씩 배워 붙여 나간 결과에 가깝습니다. 맨 처음 버전은 실제 밝은 별 쉰네 개를 올바른 위치에 배치하고 행성 표면은 노이즈 함수로 칠한 게 전부였습니다. 여기서 노이즈란 난수를 부드럽게 이어 붙인 얼룩무늬인데 주파수를 배로 올리고 진폭을 반으로 줄인 여러 겹을 포개면 구름이나 암석 같은 자연스러운 결이 나옵니다. 이걸 프랙털 노이즈라고 부른다는 것도 그때 처음 알았습니다[^Fractal Brownian Motion]. 그런데 평평한 노이즈 이미지를 공에 둘러씌우니 양극에서 무늬가 풍차처럼 빨려 들어가는 흉한 자국이 생겼고 2차원 이미지가 아니라 구 표면의 3차원 좌표에서 직접 노이즈를 뽑도록 바꾸고 나서야 이음매 없는 행성을 그릴 수 있었습니다. 별 하나에도 함정이 있었습니다. 멀리 있는 별을 거리에 따라 작아지게 했더니 태양을 가까이서 볼 때 별들이 화면을 뒤덮는 거대한 얼룩으로 부풀어 배경이 온통 연한 갈색으로 물드는 현상이 일어났습니다. 별은 거리와 무관하게 일정한 점으로 찍어야 한다는 걸 그 갈색 화면을 보고서야 배웠습니다.
이런 식으로 기본기를 갖춰 갈 무렵 목표를 좀 더 분명하게 잡았습니다. 그냥 우주가 아니라 망원경으로 찍은 천체사진처럼 거기서 살짝 과장된 인상을 내자는 것이었습니다. 그러려면 빛을 다루는 법을 배워야 했습니다. 가장 먼저 붙인 건 블룸이었습니다. 아주 밝은 부분이 주변으로 부드럽게 번져 나가게 하는 효과인데 별이나 강착원반처럼 눈부신 광원이 실제 사진에서 그렇듯 빛무리를 두르자 단숨에 카메라로 찍은 것 같은 느낌이 살아났습니다. 다음은 톤매핑이었습니다. 우주에는 눈이 멀 듯 밝은 것과 칠흑같이 어두운 것이 한 화면에 같이 있습니다. 이 어마어마한 밝기 차이를 모니터가 표현할 수 있는 좁은 범위로 그냥 우겨넣으면 밝은 쪽이 허옇게 다 타 버립니다. 톤매핑은 그 넓은 밝기를 영화 필름처럼 부드럽게 눌러 담아 색이 타지 않게 해 주는 변환이었고 영화에서 쓰는 ACES라는 방식을 가져다 썼습니다. 그 위에 사진 같은 잔재주를 몇 개 더 더했습니다. 화면 가장자리에 미세한 색번짐과 어두운 비네팅을 주고 채도를 살짝 올리는 스스로도 예쁜 거짓말이라 부른 보정입니다. 밝은 별에는 십자 모양의 광선을 달았습니다. 이건 망원경 내부의 지지대에 빛이 회절해 생기는 실제 현상이라 허블이나 제임스 웹 사진의 별이 죄다 그 십자를 달고 있습니다[^Diffraction spike]. 행성 가장자리에는 시선이 대기를 비스듬히 스치는 곳만 빛나는 프레넬이라는 효과로 얇은 빛 테두리를 둘렀습니다. 그리고 가장 욕심을 부린 건 블랙홀이었습니다. 화면을 한 번 그린 다음 그 그림 자체를 블랙홀 주위에서 휘게 만드는 중력 렌즈를 넣느라 가장 오래 헤맸습니다. 이 효과들을 순서대로 통과시키는 처리 파이프라인 하나로 묶고 나니 똑같은 기하 모양인데도 결과가 영 딴판으로 나왔습니다. 그 사이 배운 교훈 하나는 사실적으로 만든다고 다 좋은 게 아니라는 점이었습니다. 우주 거대구조를 처음엔 가는 선으로 이어 그렸더니 다들 그래픽 버그로 오해해서 선을 지우고 은하 점들의 무리로만 표현하자 비로소 실제처럼 보였습니다.
이쪽은 게임에서 가장 자신 없던 영역이라 지금도 가장 손이 많이 가고 앞으로 할 일이 가장 많이 남은 부분이기도 합니다. 당장은 행성을 더 살아 있게 만들 계획입니다. 화성에는 시간에 따라 끼었다 걷히는 먼지 폭풍을 목성의 대적점에는 나타났다 옅어지는 장주기 위상을 결정론적 시간 함수로 넣어 같은 시각에 접속한 누구에게나 같은 모습이되 들를 때마다 조금씩 다르게 보이도록 할 생각입니다. 토성은 고리를 보는 데서 그치지 않고 고리를 이루는 바위 사이로 직접 날아 들어갈 수 있게 만들고 성운은 멀리서 바라보다 안으로 진입할 수 있게 만들고 있습니다. 다른 별의 행성계도 가까이 가면 태양계만큼 디테일하게 그리는 것이 목표입니다. 문제는 이 모든 걸 휴대폰 브라우저에서도 돌려야 한다는 점이라 같은 모양을 수천 번 찍어 내는 별이나 입자를 한 번에 그리는 인스턴싱과 화면 밖이나 너무 먼 것을 아예 그리지 않고 건너뛰는 컬링과 기기 성능을 감지해 저사양에서는 별 수와 효과를 자동으로 줄이는 단계별 프리셋 같은 최적화가 로드맵의 절반을 차지합니다. 원칙은 지금 보는 한 곳만 최고 화질로 그리고 나머지는 적당히 그리는 것입니다. 더 멀리는 지금 쉰 개 남짓 쓰는 실제 별 목록을 십수만 개나 수억 개 단위의 실측 카탈로그로 통째로 갈아 끼우는 것도 욕심이 납니다. 정리하고 보면 이 게임에서 물리는 처음부터 거의 정해져 있었지만 그 물리를 눈에 보이게 만드는 일은 지금도 한 효과씩 익혀 가면서 배우는 중입니다.
이 계산이 실제로 어떤 장소들을 만들어냈는지 집에서 가장 먼 곳까지 몇 군데만 둘러보겠습니다. 모든 항해는 지구에서 시작합니다. 지구의 대륙만은 실제 해안선 데이터로 윤곽을 잡고 그 위에 구면 노이즈로 사막과 빙상과 구름을 그린 다음 밤인 면에만 도시의 불빛이 켜지도록 했습니다. 가장자리를 감싼 푸른빛은 텍스처가 아니라 시선이 대기를 비스듬히 스칠 때만 밝아지는 프레넬 산란 식으로 그린 것입니다. 우주선이 ISS에서 분리되어 처음 마주하는 장면이 이 푸른 구체입니다.

조금 나아가면 목성이 있습니다. 가스 행성의 줄무늬는 위도에 사인 함수를 씌워 만들고 거기에 낮은 주파수의 노이즈를 더해 띠의 경계가 자를 댄 듯 곧게 떨어지지 않고 구름처럼 흐르고 일렁이게 했습니다. 한가운데의 대적점은 타원 거리 판정으로 그 자리에만 그려 넣은 거대한 소용돌이입니다. 행성 하나하나가 사진이 아니라 함수의 결과물이라 같은 시드에서는 언제 봐도 같은 무늬가 돌아옵니다.

토성의 고리는 이 글을 쓰는 사이에도 한 번 더 손을 본 곳입니다. 얼마 전까지는 고리를 매끈한 한 장의 원반으로 그리고 있었지만 실제 고리는 셀 수 없이 많은 얼음덩어리와 작은 바위 천체들이 떼 지어 도는 것이라 도무지 매끄러울 수가 없습니다. 그래서 잡음 무늬를 알파 마스크로 깔아 고리 곳곳에 자잘한 틈을 숭숭 뚫어 칠해 놓은 면이 아니라 알갱이의 무리처럼 보이게 했습니다. 가는 고리를 훨씬 더 촘촘하게 쪼개 LP 음반의 홈 같은 결을 주고 고리 면도 완벽한 평면이 아니라 아주 미세하게 들쭉날쭉 흩뜨려 입자들이 흩어진 느낌을 더했습니다. 실제 반지름 구조 자체는 그대로라 카시니 간극과 엥케 간극은 정말로 빈틈으로 비어 있고 전체는 토성의 자전축 기울기인 26.7도만큼 기울어 있습니다. 이렇게 우주의 아름다운 모양에 약간의 과장을 더한 풍경을 한 군데씩 조금씩 손봐 나가는 중입니다.

태양계를 한참 벗어나면 다른 은하가 나타납니다. 안드로메다 같은 나선 은하는 사진을 가져오는 대신 로그 나선 식으로 팔을 그리고 그 은하의 실제 기울기만큼 원반을 납작하게 눌러 세웁니다. 어디에 얼마만 한 크기로 어느 방향으로 둘지는 카탈로그에 적힌 겉보기 각크기와 위치각과 경사 값만으로 정합니다. 사진 한 장 없이 알려진 숫자 몇 개만으로 알아볼 수 있는 은하를 세웁니다.

그리고 가장 멀리 더는 빛으로 도달할 수 없는 관측 가능한 우주의 끝이 있습니다. 그곳에는 초은하단들이 가느다란 필라멘트로 이어진 거대구조가 펼쳐지고 은하들의 색은 거리에 따라 푸른빛에서 붉은빛으로 물듭니다. 멀수록 더 오래된 빛이라 그렇습니다. 화면의 계기판은 ΛCDM 우주론을 적분해 지금 보는 빛이 몇억 년 전의 것인지 그동안 우주가 얼마나 팽창했는지를 실시간으로 적어 줍니다. 이 끝의 벽이 곧 관측 가능한 우주라는 말의 경계이고 여기서 그 말의 두 번째 뜻이 가장 또렷해집니다. 보이기는 하지만 결코 도달할 수는 없습니다.

이 장소들을 코드로 만들어 두고 나서 정작 저 자신이 플레이어로 처음 멀리까지 가 본 항해가 오래 기억에 남습니다. 목적지는 토성이었습니다. 출발할 때 토성은 화면 속 작은 점에 불과했고 그 점이 알아볼 만한 크기로 자라기까지 현실의 며칠이 걸렸습니다. 그동안 한 일이라곤 가끔 접속해 방향이 맞는지 확인하고 도착이 가까워지자 감속을 미리 걸어 둔 게 전부였습니다. 그러다 어느 날 접속하니 화면을 거의 채운 고리가 비스듬히 기울어 떠 있었습니다. 카시니 간극의 검은 틈까지 또렷한 그 고리를 한참 바라봤습니다. 누가 박수를 쳐 주는 것도 아니고 점수가 오르는 것도 아니었습니다. 그저 텅 빈 공간에 저와 그 행성뿐이었고 여기까지 오는 데 며칠이 걸렸다는 사실이 그 고요 속에서 비로소 우주의 크기로 다가왔습니다. 그리고 그 토성조차 이 우주에서는 바로 옆집에 지나지 않는다는 걸 떠올리면 정말로 먼 곳에는 무엇이 있을지가 더 궁금해집니다.
이 게임에서 고독이 가장 극단으로 치닫는 곳은 블랙홀입니다. 블랙홀은 새로운 엔진이 아니라 이미 있는 천체 위에 강한 중력장 보정 몇 줄을 더한 것일 뿐인데 그 몇 줄이 만들어 내는 광경이 이 게임의 주제를 끝까지 밀어붙입니다. 사건의 지평선 반지름은 질량과 광속만으로 정해지는 간단한 식이고[^The M-sigma and M-L Relations in Galactic Bulges and Determinations of their Intrinsic Scatter] 지평선 근처에서는 중력 때문에 시간이 더 느려집니다. 거기에 강착원반이 휘어 보이는 광경은 영화 인터스텔라의 블랙홀을 계산했던 연구가 보여 준 상대론적 도플러 효과와 중력 렌즈를 따라 그렸습니다[^Gravitational lensing by spinning black holes in astrophysics, and in the movie Interstellar]. 화면 전체를 한 번 그린 다음 그 결과를 블랙홀 주위에서 휘어 다시 그리는 식이라 뒤에 있는 별과 은하가 검은 그림자 둘레로 끌려와 고리처럼 둘러섭니다.

그런데 정말로 마음을 건드리는 건 시각 효과가 아니라 지평선을 넘는 순간의 비대칭입니다. 넘는 우주선 자신에게는 지평선에서 아무 일도 일어나지 않습니다. 벽도 섬광도 없이 그냥 지나갑니다. 하지만 밖에서 지켜보는 동료에게는 떨어지는 우주선이 점점 느려지고 붉어지고 어두워지다가 지평선에서 영원히 얼어붙은 채 서서히 사라져 갑니다. 동료는 그 우주선이 넘었는지 죽었는지 영영 확인할 수 없습니다. 지평선을 넘은 우주선은 그 위치에 영원히 고정됩니다. 그래서 훗날 블랙홀 곁을 지나는 다른 우주선들은 지평선을 넘으려 한 우주선들을 언제까지나 볼 수 있습니다. 두 사람은 모순되지 않으면서 완전히 다른 경험을 하고 그 사이에는 공유되는 지금이 없습니다. 같은 사건을 두고도 서로에게 도달하지 못하는 이 비대칭이 군중 속의 고독이 도달할 수 있는 가장 먼 자리입니다.
이렇게 외로움을 게임의 거의 전부로 밀어붙이다 보니 정작 플레이해 본 분들에게서는 뜻밖의 걱정이 돌아왔습니다. 이 적막한 비행이 파일럿의 정신건강에 좋을 리 없으니 무슨 대책을 좀 세워 달라는 것이었습니다. 농담 반 진담 반이었지만 일리가 있었습니다. 그래서 콕핏 안에 무중력으로 천천히 떠다니는 스팀덱을 한 대 띄워 두었습니다. 장시간 순항에 지치면 그 게임기를 톡 건드려 눈앞으로 끌어오고 그 화면에서 실제 둠을 플레이할 수 있게 했습니다. 우주선을 모는 모니터들과는 톤을 일부러 다르게 운항이 아니라 딴짓을 위한 장난감처럼 두었습니다. 그런데 둠을 붙이는 과정에서 한 가지를 새로 알게 됐습니다. 여태 둠이 통째로 공개된 줄로만 알고 있었지만 막상 공개된 건 게임을 굴리는 엔진 소스뿐이었습니다[^Doom engine]. 정작 레벨과 그래픽이 든 게임 데이터는 여전히 별도로 저작권이 살아 있었습니다. 엔진은 누구나 가져다 빌드할 수 있지만 게임 자체를 마음대로 끼워 팔 수는 없는 구조였던 겁니다. 그래서 자유 배포가 허용된 셰어웨어 에피소드만 기본으로 동봉하고 전체 캠페인은 각자 정품 데이터나 자유 라이선스 대체본을 사용할 수 있도록 했습니다[^Freedoom]. 우주의 고독을 만들려다 30년도 더 된 게임의 저작권 구조까지 파고들게 된 건 예상에 없던 곁가지였습니다.
이 모든 게 완벽한 물리 시뮬레이션은 아닙니다. 게임으로 성립하려면 실제로부터 벗어나야 할 때가 있었고 그걸 숨기지 않고 다 적어 두었습니다. 실시간으로 굴러가는 우주에서 며칠씩 걸리는 항해를 견딜 만하게 만들려고 엔진 추력을 실제보다 열 배쯤 과장했고 중력 도움도 부풀렸습니다. 연료라는 개념은 아예 없앴습니다. 항성계와 항성계 사이를 오갈 때는 광속의 벽을 넘을 수 없으니 일방통행 웜홀이라는 비물리적 장치를 하나 허락했습니다. 다만 물러선 곳은 좁게 가두었습니다. 광속의 벽과 시간 지연과 궤도역학이라는 핵심은 끝까지 건드리지 않았습니다. 어디를 양보하고 어디를 양보하지 않을지를 분명히 정해 두는 것이 이 게임을 시뮬레이션이라 부를 수 있게 하는 마지막 기준이었습니다.
어디에 경계를 정할지는 사실 이 게임에서 가장 오래 고민한 문제였습니다. 기준은 하나로 잡았습니다. 주려는 경험을 실제로 만들어내는 물리는 건드리지 않고 그 경험과 무관한 비용은 마음껏 깎는다는 것이었습니다. 광속의 벽과 시간 지연과 궤도가 휘는 방식과 도달할 수 없는 거리. 이것들은 군중 속의 고독을 직접 만들어내는 물리라서 절대 양보할 수 없었습니다. 여기를 현실과 다르게 만드는 순간 게임이 주려던 경험 자체가 무너지기 때문입니다. 반대로 엔진의 추력이 정확히 얼마이고 연료가 몇 킬로그램 남았는지는 곧이곧대로 구현해 봤자 플레이어를 며칠씩 더 기다리게 하거나 연료 관리라는 잡일을 하나 더할 뿐 그 고독에는 아무것도 보태지 않습니다. 그래서 추력은 부풀리고 연료는 통째로 없앴습니다. 웜홀도 같은 논리였습니다. 광속의 벽 그 자체는 끝까지 지켜야 하지만 항성계와 항성계 사이를 실제 광속 그대로 두면 한 번의 항해가 수만 년이 걸려 아무도 그 너머를 보지 못합니다. 그래서 벽은 그대로 둔 채 벽을 넘는 게 아니라 비물리적으로 한 번 돌아가는 지름길 하나만 예외로 뚫었습니다. 시뮬레이션이라는 말은 모든 걸 똑같이 복제한다는 뜻이 아니라 무엇이 핵심이고 무엇이 비용인지를 정확히 구분한다는 뜻에 더 가깝다고 생각합니다.
이렇게 그은 선들을 실제 코드로 옮긴 건 거의 다 기계였습니다. 지금까지 쌓인 639번의 변경에서 한 일은 한 줄 한 줄 타이핑하는 게 아니라 무엇을 만들지와 어디에 경계를 둘지를 말로 정해 주고 결과를 보고 다음 방향을 정하는 것이었습니다. 기계는 받은 방향대로 코드를 쓰고 스스로 게임 화면을 캡처해 콕핏이 제대로 그려졌는지를 확인하고 한 번 맞춰 둔 물리가 나중 수정에 깨지지 않도록 기준 결과를 동결해 두는 검증 장치까지 만들어 두고 일했습니다. 상당 구간은 '남은 것들을 알아서 진행하라'고 던져 두면 기계가 항목을 하나씩 처리해 나가는 자율 모드였습니다. 앞서 '자동완성'에서 살펴본 다음 토큰을 예측하는 그 단순한 원리가 이번에는 관측 가능한 우주 하나를 통째로 만드는 데까지 이르렀습니다. 손으로 짰다면 십수 년을 더 미뤘을 분량이 방향만 정확히 잡아 주면 며칠 안에 실제로 굴러가는 경험은 꽤 익숙해진 지금도 여전히 신기합니다. 코드를 쓰는 사람에서 코드를 읽고 방향을 정하는 사람으로 옮겨 갔습니다. 그 역할이 마냥 편하기만 한 건 아니었습니다.
그런데 코드를 짜서 게임이 돌아가게 만드는 건 알고 보니 절반의 일이었습니다. 이걸 여럿이 함께하는 멀티플레이로 만들어 인터넷에 띄우는 순간 전혀 다른 종류의 일거리가 따라왔습니다. 가장 먼저 부딪힌 건 보안입니다. 웹브라우저에서 도는 클라이언트는 누구나 열어 볼 수 있어서 그게 서버로 보내는 값을 단 하나도 곧이곧대로 믿을 수가 없습니다. 그래서 모든 판단을 서버에 두어야 했습니다. 플레이어가 보내는 명령은 전부 '이런 걸 하고 싶다'는 요청일 뿐이고 서버가 미리 정해 둔 목록에 있는 명령인지 숫자 범위가 정상인지 정말로 그 천체에 갈 자격이 있는지를 일일이 다시 검사한 다음에야 받아들입니다. 우주선의 소유권도 남에게 노출되는 이름이 아니라 비밀 토큰에 묶어 두어야 했고 그 위에 사람인지 확인하는 관문과 요청 폭주를 막는 제한과 위조 클라이언트를 거르는 핸드셰이크 절차를 추가했습니다. 혼자 하는 게임이었다면 플레이어를 믿으면 그만이지만 인터넷에 열린 공유 세계에서는 아무도 믿지 않는 데서 출발해야 한다는 걸 보안 절차를 추가하면서 재확인했습니다.
안전하게 돌아가게 만든 뒤에도 일은 끝나지 않았습니다. 사람들에게 이런 게 있다고 알리려면 우선 알릴 창구가 있어야 해서 게임의 사건들과 매일 한 장씩 고른 천체 사진을 자동으로 올리는 SNS 계정을 페디버스에 만들었습니다. 그걸 설명하려면 게임과는 별도로 소개 웹사이트가 필요했고 그 웹사이트의 설명은 빌드가 바뀔 때마다 최신 상태에 맞게 다시 고쳐야 합니다. 그래픽을 한 번 손볼 때마다 사이트와 가이드에 실린 스크린샷을 전부 새로 찍었고 기능이 하나 늘면 그 사용법을 가이드에 보완해야 했습니다. 누가 들어오는지 알기 위해 개인정보 없는 가벼운 접속 통계를 달았고 무엇이 어디서 깨지는지 알기 위해 모든 로그를 파일로 기록하고 갑자기 죽어도 마지막 상태가 남도록 하는 디버그 로그도 따로 만들었습니다. 이 가운데 어느 것도 게임 자체는 아니지만 이게 없으면 라이브 서비스라는 게 성립하지 않습니다. 예전에 '팰월드 서버 운영과 라이브 서비스 운영'을 적으면서도 어렴풋이 느끼긴 했지만 게임을 만드는 사람으로서 정작 게임 바깥의 이 모든 살림살이를 혼자 처음부터 만들어 보고서야 라이브 서비스라는 게 실은 게임이 아닌 부분으로 대부분 이뤄져 있다는 걸 구체적으로 알게 됐습니다.
지금까지 적은 게 게임을 둘러싼 살림이라면 그 한가운데에서 우주를 실제로 돌리는 구조 자체는 의외로 단출합니다. 게임 전체는 단 하나의 권위 서버 프로세스가 떠받치고 그 프로세스 안에 우주가 통째로 메모리로 들어 있습니다. 서버는 1초에 한 번씩 도는 고정 틱 루프를 돌립니다. 한 틱마다 자유비행 중인 우주선 하나하나에 태양과 행성의 중력 합을 적분해 다음 위치를 구하고 들어온 명령을 반영한 뒤 그 결과를 모든 클라이언트에 스냅샷으로 보냅니다. 우주를 만들어 내는 방식은 앞서 적은 재현 원칙 그대로입니다. ISS는 CelesTrak의 실시간 궤도 정보를 SGP4로 전파해 지금 위치에 두고 태양과 행성은 케플러 요소로 실제 배치에 두고 나머지 먼 별과 은하와 항성계는 같은 시드에 기반한 절차 생성으로 만듭니다. 서버와 클라이언트가 같은 시드로 각자 같은 우주를 계산하므로 둘 사이에 오가는 것은 무거운 우주가 아니라 가벼운 시드와 우주선 몇 개의 상태뿐입니다. 다른 우주선을 맞춰 보여 주는 일도 이 틱 안에서 함께 일어납니다. 서버는 우주선마다 지나온 위치의 기록을 들고 있다가 다른 우주선을 그릴 때 지금 위치가 아니라 거리를 빛의 속도로 나눈 만큼 과거의 위치를 계산해 내려보냅니다. 너무 멀어 기록의 한계를 넘으면 그 우주선은 위치가 불확정한 흐릿한 유령으로만 보입니다. 신호도 같은 원리로 빛의 속도로 나아가고 명중인지 빗나감인지를 서버가 결정론으로 판정합니다. 앞에서 적은 광속 너머의 고독이 실은 이 틱 루프 안에서 한 줄씩 계산됩니다.
그러면 이 한 프로세스가 우주선을 몇 대까지 감당하는지가 자연히 궁금해집니다. 합성 부하를 실제로 걸어 재 보았더니 병목은 데이터베이스가 아니라 그 단일 틱 루프의 CPU였습니다. 접속만 유지한 채 도킹해 대기하는 우주선은 한 척당 비용이 스냅샷 전송뿐이라 한계로 정해 둔 천 명까지도 틱이 1초를 지켰습니다. 반대로 모두가 동시에 자유비행하면서 중력 적분과 광지연 계산을 요구하면 측정한 12코어 호스트에서 일흔다섯 척 언저리부터 틱이 1초를 넘기기 시작했고 그 지점이 곧 실시간이 깨지는 한계였습니다. 가장 무거운 궤도 예측 계산을 여러 워커 스레드로 흩어 돌리는 장치를 더한 뒤로는 같은 호스트에서 사백 척까지 자유비행을 실시간으로 버텼습니다. 데이터베이스가 한 번도 한계가 아니었던 까닭은 이 게임이 우주 자체를 저장하지 않기 때문입니다. 우주는 시드에서 언제든 다시 만들어지므로 디스크에 남길 것은 우주선과 주고받은 신호와 시뮬레이션 시계뿐이고 그 정도는 프로세스에 내장된 가벼운 SQLite 파일 하나로 충분합니다. 더 큰 내구성이 필요해지면 코드를 고치지 않고 환경변수 하나만으로 PostgreSQL로 갈아 끼울 수 있게 해 두었지만 아직 그럴 일은 없었습니다. 동시에 시뮬레이션해야 할 우주선이 크게 늘어날 가능성은 높지 않습니다. 그래도 대비는 두 방향으로 나누어 두었습니다. 이미 한 일은 방금 적은 워커 스레드 분산이고 앞으로 가능한 일은 우주선을 여러 권위 프로세스로 쪼개 여러 하드웨어에 나눠 싣는 샤딩입니다. 다만 샤딩까지 필요한 규모에 이 게임이 이를 일은 없으리라 보고 남은 숙제라면 멀티스레드 시뮬레이션을 지금보다 조금 더 밀어붙이는 정도입니다.
이 모든 걸 한데 묶어 인터넷에 올리는 방식도 이번에 처음으로 끝까지 혼자 짜 보았습니다. 게임 서버 전체를 도커 컨테이너 하나에 담아 늘 켜져 있는 단일 프로세스로 띄우고 우주선과 신호가 든 데이터베이스 파일만 호스트의 실제 폴더에 남겨 둡니다. 예전 'pytmux'를 만들 때도 서버와 클라이언트를 나눈 비슷한 구조이긴 했지만 그때는 게임 서버가 호스트 위에 그대로 올라가 있었고 이번에는 게임 서버를 통째로 도커 안에 가두었습니다. 그래서 나중에 다른 머신으로 옮긴다 해도 이미지 하나와 데이터 폴더만 들고 가 그대로 다시 띄우면 게임 서비스 전체를 바로 옮길 수 있습니다. 게임 서버를 통째로 도커에 담아 본 건 이번이 처음인데 아직까지는 나쁘지 않은 선택으로 보입니다. 코드를 고치는 일과 그것을 서비스에 올리는 일은 분리했습니다. 이 저장소는 깃허브가 아니라 퍼포스에 있어 남이 마련해 둔 자동화 파이프라인이 없으므로 제출 전에 통과해야 할 검사들을 한 명령으로 묶은 자체 검사 장치를 따로 만들었습니다. 보안 정적 분석을 먼저 돌리고 여러 갈래의 단위 테스트와 거동을 해시로 동결해 둔 골든 테스트를 돌리고 마지막으로 적대적 입력을 던지는 레드팀 테스트까지 차례로 통과해야 합니다. 골든 테스트는 대표적인 항해 시나리오 몇 개를 똑같이 돌린 다음 그 결과 전체를 하나의 지문처럼 요약해 미리 정해 둔 기준과 대조합니다. 그래서 코드 어딘가를 고쳐 결과가 조금이라도 달라지면 이 지문이 어긋나 검사가 실패하고 거동을 바꾸려던 게 아니었다면 그 변화를 곧바로 잡아 줍니다. 이 검사를 모두 지난 뒤에야 비로소 도커 이미지를 새로 빌드하고 서비스를 재시작해 변경을 라이브에 반영합니다. 검사를 배포 이전에 수행하기 때문에 물리 한 줄을 잘못 고쳐 거동이 바뀌면 골든 테스트에 의해 배포가 중단됩니다.
만들고 나서 든 생각은 이 게임을 만든 과정 자체가 그 안에 깃든 고독과 어딘가 닮았다는 것이었습니다. 상업적으로 아무 가치가 없어서 아무도 만들지 않을 물건을 혼자서는 만들 엄두가 안 나던 물건을 기계에 기대 4일 만에 서비스를 시작했습니다. 지난 '열쇠'에서 기계에 점점 더 많은 열쇠를 쥐여 주는 일을 조심스럽게 적었습니다. 이번에는 그 열쇠로 십수 년 묵은 생각 하나를 꺼내 봤습니다. 돈이 되지 않아서 혼자서는 너무 커서 그 두 가지 이유로 세상에 없던 것을 두 제약이 동시에 사라지는 순간에 비로소 만들었습니다. 그래서 직접 만들었습니다. 누가 시키지도 팔리지도 않을 우주를 하나 띄워 두고 가끔 들어가 제 작은 우주선이 그동안 얼마나 멀리 왔는지를 확인합니다. 어딘가 다른 우주선도 떠 있을 텐데 빛의 지연 너머에서 흐릿한 유령으로만 스칩니다. 관측은 됩니다. 상호작용 할 수는 없습니다. 그 거리에서 느끼는 고독이 궁금하다면 space.woojinkim.org에 로그인 없이 우주선 한 척이 준비되어 있습니다.