우리 은하 항성 투영 프로그램 개발기

NeCu1029·2025년 11월 8일

프로젝트 회고

목록 보기
1/6
post-thumbnail

우리 은하는 약 50000광년의 반지름과 수천억 개의 항성을 가지는 거대한 은하입니다. 저는 우리 은하의 항성들을 그 위치와 색을 고려하여 투영하는 프로그램을 제작했습니다.

시작하기 전에

이 프로젝트는 과학영재 창의연구 (이하 창의R&E 또는 창알)에서 연구한 내용을 블로그 형식으로 재구성한 것임을 알립니다. 또한, '항성'이라는 말을 계속 쓰기 너무 귀찮으니 편의상 '별'로 부르겠습니다.

데이터에 대하여

우리 은하의 별을 투영하려면 가장 먼저 많은 별들의 데이터가 있어야 합니다. 이 데이터로는 Gaia DR3를 이용했습니다. 유럽우주국 ESA의 Gaia 위성이 수집한 데이터로, 무려 18억 개(!)에 달하는 별들의 적경과 적위, 연주시차, 측광 데이터 등을 제공합니다. 이 데이터를 잘 다운로드한 뒤 열심히 가공해서 정보가 확실한 친구들만 남기면 데이터 작업이 끝납니다.

데이터 관련 작업은 같이 연구한 친구가 진행하였기 때문에, 이 블로그에서는 데이터 가공 과정에 대해 따로 언급하지 않습니다. 제가 잘 모르는 부분이기도 하고요. 아무튼 제가 최종적으로 받은 데이터에는 다음이 수록되어 있습니다.

  • 별의 고유 번호 (본 프로그램에서 사용하지 않았습니다)
  • 태양을 원점으로 했을 때 별의 공간상 좌표 (천체 관측에서 일반적으로 쓰는 적도좌표계는 개발하기에 좀 불편해서, 적당한 축을 잡아 직교좌표계로 변환한 것을 받았습니다)
  • 별의 RGB 색상 (이건 진짜 어떻게 얻었는지 감도 안 옵니다)

오차가 많은 데이터를 모두 제거하니 별의 개수가 1억 개도 안 남았다고 합니다. 저에게는 오히려 좋죠. 이 별들을 화면에 그리기만 하면 됩니다...

알고리즘 설계

당연히 1억 개의 별을 모두 화면에 띄울 수는 없습니다. 애초에 픽셀 수조차 1억 개가 안 되는 걸요. 그러니 가장 밝게 보이는 별들만 골라 화면에 띄워야 합니다. 눈으로 보이는 밝기, 즉 플럭스는 별의 광도에 비례하고 거리의 제곱에 반비례하니 다음과 같이 문제 상황을 정리할 수 있습니다.

NN개의 별이 존재한다. 각 별은 (x,y,z,I)(x,y,z,I)로 주어지는데, x,y,zx,y,z는 별의 좌표이고 II는 광도이다. 기준 좌표 (X,Y,Z)(X,Y,Z)가 주어졌을 때, IR\frac{I}{R}의 값이 가장 큰 MM개의 별을 찾으시오. (단, R=(Xx)2+(Yy)2+(Zz)2R=(X-x)^2+(Y-y)^2+(Z-z)^2

이제 이 문제를 어떻게 풀지 고민해 봅시다. (NN은 1억, MM은 1만 정도 된다고 합시다) 우선 NN이 매우 크므로 O(N)O(N) 이상의 시간 복잡도를 쓰면 안 될 것 같지만...

여기는 PS가 아닙니다.

그래서 그냥 O(NlogM)O(N\log{}M)으로 했습니다. 크기가 MM인 우선순위 큐에 NN개의 별을 순서대로 넣는 방식입니다. 이제 구현을 해 봅시다.

구현

우선순위 큐 라이브러리를 이용해 소위 "딸-깍"을 시전하려 했으나, 뜻하지 않은 난관에 봉착하게 되었습니다. 우선순위 큐가 없습니다! 정확히는 C#에서 우선순위 큐가 .NET 6.0에 추가되었는데, 제가 쓰는 Unity 엔진에서는 .NET 6.0 아래 버전을 써야 합니다. 대신 SortedSet이라고 하는 자료구조가 있으니 그걸 써 줍시다. 먼저 별을 저장하기 위한 Star 구조체를 만듭니다. 구조체에는 별의 위치, 상대적 광도, 색상 RGB 값이 들어갑니다. 비교 기준을 적당히 설정해 주고, SortedSet을 이용해 위 알고리즘을 구현합니다. 코드는 생략하겠습니다.

이렇게 가장 밝은 별 MM개를 찾았다면 이 별들을 화면에 찍어 주어야 합니다. 구형 오브젝트를 하나 만들고 복제한 뒤, 복제마다 색상을 지정해 주면 됩니다. 여기서 이런 생각을 해볼 수 있습니다.

GameObject를 1만 개씩 만들어서 화면에 띄우는 건 미친 짓 아닐까?

맞습니다. 그래서 구를 Prefab으로 저장하고 GPU Instancing을 이용해 렌더링만 하도록 했습니다. GPU Instancing을 사용해도 복제마다 색을 다르게 할 수 있기 때문에 큰 문제는 없었습니다.

그러나 저희의 구현은 끝이 아닙니다. 이 프로젝트에서는 시점이 우주 공간에서 움직여야 합니다. 따라서 적경/적위/거리를 입력받아 Main Camera를 움직이는 스크립트를 따로 작성했고, 마우스 드래그를 통해 회전 기능도 만들었습니다. 물론 카메라가 이동할 때마다 밝게 보이는 별 계산과 GPU Instancing도 모두 새로 해 줍니다. 이제 모든 것이 된 줄 알았습니다...

어서오세요 디버깅지상주의 창알에

별 갱신이 안 되네요?

카메라 이동 명령을 내리면 카메라가 잘 이동하고, 밝은 별도 다시 잘 계산되며, 심지어 렌더링 명령도 새로운 별 리스트를 따라 내려집니다. 그런데 그것이 화면에 반영되지 않습니다! 이것 때문에 몇 시간을 내리 태워가며 해결 방법을 탐색했지만, 해결은 고사하고 문제의 원인조차 알 수 없었습니다. (GPT와 Claude는 디버깅에 적합하지 않습니다. 감사합니다.)

일단 무엇이 문제인지라도 알아야 합니다. 따라서 기존 문제를 어느 정도 해결할 겸 이 문제의 적용 방식도 함께 알아 보기로 하였습니다. 기존에는 별을 3차원 좌표에 따라 그대로 투영하였기 때문에 가까운 별이 지나치게 크게 보이는 문제가 있었습니다. 따라서 기준점 주위에 가상의 구를 만들고, 기준점에서 별을 향해 그은 반직선과 구가 만나는 교점에 별을 렌더링하기로 했습니다. (천구를 생각하면 편합니다) 이렇게 하면 모든 별의 크기가 같게 보일 것이고, 크기를 조절하여 밝기를 나타내는 데도 용이할 것입니다. 이렇게 해서 초기 위치에서는 만족할 만한 결과를 얻었습니다. 그리고 위치를 바꾸었을 때는... 됩니다...? 왜 되는지는 아직도 이해하지 못했지만, 아무튼 됩니다. 그럼 된 거죠.

완성

이 모든 작업은 별 25만 개의 테스트 데이터에서 이루어졌습니다. 이제 별이 더 많은 실제 데이터에서 해보면 됩니다. 그런데 O(NlogM)O(N \log M)이 생각보다 많이 느리더라고요? N=250000N=250000에서도 3~5초 정도 걸립니다. 그래서 NN이 1억 단위인 것은 시도하지 못했고, 200만 개의 별을 랜덤하게 골라서 개형만 보기로 했습니다. 그 결과 1회 로딩에 15~20초 정도 걸리게 되었습니다. (M=40000M=40000입니다) 지구 부근에서 은하수를 투영해 보면 아래 그림과 같습니다.

결론

은하 스케일은 건드리는 게 아닙니다. 감사합니다.
(Special Thanks To. gs25091, gs25026 [프라이버시를 위해 이름은 가립니다])

profile
경기과고 43rd

2개의 댓글

comment-user-thumbnail
2025년 11월 11일

우선순위 큐 직접 만드는게 1557배는 빠를듯

1개의 답글