[플라네타리움 구현기] 4. 투영 구현

  1. 1. 목차
  2. 2. 투영(Projection)
    1. 2.1. Stereographic Projection
    2. 2.2. Cylindrical Projection
  3. 3. 구현
    1. 3.1. Stereographic Projection
    2. 3.2. Cylindrical Projection
  4. 4. 결과
    1. 4.1. Stereographic Projection
    2. 4.2. Cylindrical Projection

목차

지금까지 한 일들은 다음과 같습니다.

이제는 별의 (방위각, 고도)값을 이용하여 브라우저에서 표시될 위치를 계산해야합니다.

투영(Projection)

모니터는 직사각형이고 천구는 반구의 형태입니다. 그렇기 때문에 반구를 평면에 투영하는 과정이 필요합니다. 이 때, 투영을 어떤 식으로 하냐에 따라서 실제 투영된 평면에 맺힌 상이 다릅니다. 다양한 투영방식이 있고, 그에 따른 다양한 왜곡이 생겨납니다.

이 글에서는 Stereographic Projection과 Cylindrical Projection을 활용합니다.

Stereographic Projection

아래 동영상을 봅시다.

동영상 중간(혹은 썸네일)에 보면 땅에 비치는 그림자가 직교하는 격자가 될 때가 있는데, 그 장면이 Stereographic projection이라고 생각하면 됩니다. 위에서 빛을 쏴서 구(동영상에서의 물체)의 각 부분을 평면(동영상에서의 바닥)에 대응시키는 방식입니다. 간략하게 도식화하면 아래 그림과 같습니다.

Stereographic Projection, Wikipedia

이 방식을 선택한 이유는 해당 방식이 conformal하면서 구현하기 쉽기 때문입니다. 투영이 conformal 하다는 것은 투영 전의 두 곡선이 이루는 각의 크기를 투영한 후에도 보존할 수 있다는 것이라 생각하면 됩니다.

각의 크기를 보존해야 할 이유는 별자리 때문입니다. 곡선들이 이루는 각의 크기를 보존해야 실제 우리가 보는 밤하늘의 별자리와 비슷한 모양이 나옵니다.

Cylindrical Projection

Cylindrical projection은 말 그대로 원통에 투영하는 방식입니다. 천구에 접하는 원통을 만든 후, 천구 안에서 빛을 쏴서 원통에 천구가 투영되게 하는 방식입니다.

Stereographic Projection, geography.hunter.cuny.edu

이 글에서는 위의 그림의 왼쪽 방식을 사용합니다. 왼쪽 방식을 사용할 경우 다음과 같은 식을 이용하여 투영이 가능합니다.

$$ x = Az \\
y = Alt $$

구현

Stereographic Projection

Stereographic Projection, Wikipedia

위에서 아래로 내리쬐는 경우에는 위키백과에 투영 공식이 나와있습니다.

$$(X, Y) = (\frac{x}{1 - z}, \frac{y}{1 - z})$$

해당 식은 북극에서 빛을 쏴서 남반구를 평면에 투영시키는 식이기 때문에, 식을 바꿔서 접속한 곳의 지구 반대편 적도에서 빛을 쏴서 접속한 곳 앞의 평면으로 투영되도록 수정할 필요가 있습니다.

가장 쉬운 방법은 x, y, z 변수 위치를 바꾸는 방법인데, 해당 방법을 사용하기 위해서는 현재 Az-Alt로 표현되어있는 구면좌표계(Spherical coordinate)를 직교좌표계(Cartesian coordinate)로 바꿔줄 필요가 있습니다.

구면좌표를 직교좌표로 바꾸는 식은 다음과 같습니다.
$$
x = sin(\theta)cos(\phi) \\
y = sin(\theta)sin(\phi) \\
z = cos(\theta)
$$

이 때, $$\theta, \phi$$와 Az, Alt는 출발하는 점과 방향이 다르기 때문에, Az를 반시계방향으로 바꿔주고, Alt는 위에서 아래 방향으로 값이 증가하게 바꿔줄 필요가 있습니다. 해당 내용에 유의하여 구현합시다.

1
2
3
4
5
6
7
8
9
10
11
// azaltToCatesian : az -> alt -> {x, y, z}
const azaltToCatesian = (az, alt) => {
const pi = (-az + 360) % 360;
const theta = -alt + 90;

const x = sin(d2r(theta)) * cos(d2r(pi));
const y = sin(d2r(theta)) * sin(d2r(pi));
const z = cos(d2r(theta));

return {x, y, z}
};

직교좌표를 얻은 후에 변형된 투영 식을 이용하여 Stereographic Projection을 구현해줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Thx to Daehyun Pyo for nice reference.

...

// setGroundMap : fov -> star* -> star*
const setGroundMap = async (star, fov="N") => {
const az = star.getAttribute('az');
const alt = star.getAttribute('alt');
const mag = star.getAttribute('mag');

const {x, y, z} = azaltToCatesian(az, alt);

let start, end;

let Y, Z;

if (fov === "N") {
start = 90;
end = 270;
factor = 1 + x;

Y = y / factor;
Z = z / factor;
}
else {
start = 270;
end = 90;
factor = 1 - x;

Y = - y / factor;
Z = z / factor;
}

const W = innerWidth / 2;
const H = innerHeight;
const scale = Math.sqrt(W*W + H*H);

// Filter out left half sphere
if ((start <= az && az <= end)) {
star.style.height = star.style.width = `0px`;
return;
}

star.style.left = `${innerWidth / 2 - Y * scale}px`;
star.style.top = `${innerHeight - Z * scale}px`;
star.style.height = star.style.width = `${brightness(mag)}px`;

return;
};

식을 잘 수정할 경우 남반구에 해당하는 별도 투영할 수 있습니다. 해당 코드에서는 FOV가 N이 아닌 경우에 남반구를 투영합니다.

Cylindrical Projection

Cylindrical Projection은 아까 언급한 것과 같이 간단히 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
// setCelestialMap : star* -> star*
const setCelestialMap = async (star) => {
const az = star.getAttribute('az');
const alt = star.getAttribute('alt');
const mag = star.getAttribute('mag');

star.style.left = `${((az / 360) + 0.5) % 1.0 * innerWidth}px`;
star.style.top = `${(-alt + 90) / 180 * innerHeight}px`;
star.style.height = star.style.width = `${brightness(mag)}px`;

return;
};

결과

Stereographic Projection

Stereographic projection
제대로 구현이 되어 별자리를 찾을 수 있습니다. 하늘에서 볼 수 있는 별자리 모양과 같습니다.

위의 투영공식을 사용하면
$$
-1 \leq Y \leq 1 \\
-1 \leq Z \leq 1
$$
``
이기 때문에 적절한 크기로 X, Y를 scale해줄 필요가 있습니다. 위의 사진에서는 X, Y에 임의의 값(400.0)을 곱했습니다. 어떤 값을 곱해야 가장 적절하게 표현될지는 아래 그림을 보면 알 수 있습니다.

Scaling to get maximum FOV

위 사진의 원은 투영된 밤하늘이고, 직사각형은 브라우저의 창입니다. 직사각형이 지평선과 천구 위쪽과 접하게 만드려면 투영면의 가운데(0, 0)이 브라우저의 (innerWidth / 2, innerHeight)에 위치하도록 원점을 옮겨줄 필요가 있습니다.

따라서 Math.sqrt(W * W + H * H), W = innerWidth/2, H = innerHeight를 곱한 후 적절하게 투영면을 이동시켜주면 최종적으로 위 그림과 같은 직사각형 영역을 투영할 수 있게됩니다.

실제 결과는 아래와 같습니다.

Stereographic projection, scaled

실제 우리가 보는 밤하늘과 같아졌습니다. 화면 가운데에 북극성이 있고, 오른쪽에 북두칠성을 확인할 수 있습니다.

Cylindrical Projection

Cylindrical projection도 잘 구현된 것을 확인할 수 있습니다. 별이 많은 영역을 보면 sin 함수 곡선처럼 생겼는데, 해당 영역이 우리 은하의 은하수 부분입니다. 자세히 들여다본다면 왼쪽 위 부분에 있는 오리온자리를 확인할 수 있습니다.

Cylindrical projection
다음 챕터에서는 부가적인 요소를 구현합니다.