문서 / 지도 뷰 / 카메라

카메라 ― 추상화의 가장 어려운 부분

MapConductor에서 가장 어렵고, 가장 가치 있는 것이 카메라의 통일입니다. position・zoom・bearing・tilt・visibleRegion으로 이루어진 하나의 카메라 모델을, 10종류 이상의 지도 엔진에서 같은 의미로 맞춰 놓았습니다. 같은 MapCameraPosition을 넘기면, 어느 프로바이더에서도 같은 풍경이 나옵니다.

POSITION
lat / lng
ZOOM
0 – 22
BEARING
0 – 360°
TILT
−60 – 60°

01 · 왜 통일에 의미가 있는가

지도 엔진은 저마다 다른 카메라 모델을 가지고 있습니다. 줌으로 말하는 것, 카메라 고도로 말하는 것, 지상의 주시점을 중심으로 도는 것, 시점 자체를 움직이는 것. 네이티브 SDK를 그대로 쓰면, 프로바이더를 교체한 순간에 「같은 수치인데 다른 풍경」이 되고, 화면의 로직까지 다시 쓰게 됩니다.

MapConductor는 이 차이를 어댑터 층에 가두고, 앱에서는 하나의 카메라 모델만을 보여 줍니다. 그 결과, 카메라 위치는 프로바이더를 넘어 들고 다닐 수 있는 값이 됩니다. 샘플 앱의 Camera Sync 페이지에서는, 하나의 카메라 위치를 모든 프로바이더에 흘려 넣어, 같은 지점・같은 축척・같은 기울기로 늘어서는 것을 실제로 확인할 수 있습니다.

DIFFERENCE 01

줌인가, 고도인가

2D 타일 계열은 줌 레벨, 3D 계열은 카메라 고도나 시거리로 축척을 나타냅니다. 단위도 기준도 다릅니다.

DIFFERENCE 02

주시점인가, 시점인가

지상의 한 점을 중심으로 도는(오빗) 모델과, 카메라 자신의 위치와 방향으로 말하는 모델이 섞여 있습니다.

DIFFERENCE 03

기울기의 허용 범위

최대 경사각도, 위쪽으로 기울일 수 있는지도, 엔진마다 다릅니다. 음의 기울기를 가지는 것은 하나도 없습니다.

그림 · 한 번의 moveCameraTo가 각 엔진에서 같은 시점이 되기까지
val camera = MapCameraPosition(
    position = geoPoint,
    zoom = 18.0,
    tilt = 70.0,
)
moveCameraTo(camera)
MapConductor
통합 카메라 모델
position / zoom · bearing / tilt
각 사 지도 SDK
지도 SDK 어댑터
각 사 지도 SDK
지도 SDK 어댑터
어댑터가 네이티브 카메라 표현으로 번역합니다. 줌 기준·고도 기준·위쪽으로 기울일 수 없음 같은 차이는 여기서 흡수됩니다.
줌 기준 엔진(Google Maps · Mapbox · MapLibre …)
고도 기준 엔진(ArcGIS · Cesium · MapKit …)
입구는 하나. 출구는 엔진마다 다른 표현이지만, 화면에 나오는 풍경은 같은 지점·같은 축척·같은 기울기입니다.
샘플 영상 · Camera Sync(하나의 카메라 위치를 모든 프로바이더에 동기)

02 · 통일 카메라 모델

카메라는 MapCameraPosition 하나로 나타냅니다. 프로바이더마다의 타입은 없고, 이 값을 만들어 moveCameraTo에 넘기기만 하면 됩니다.

프로퍼티
설명
position
카메라가 보고 있는 지상의 중심 좌표.
zoom
축척. Google Maps의 줌 레벨을 기준으로 모든 프로바이더에서 정규화하고 있습니다.
bearing
방위(진북으로부터의 회전각).
tilt
기울기. 0이 바로 위에서, 양수가 앞으로 엎드린 비스듬한 시점, 음수가 수평선보다 위를 보는 앙각 뷰.
paddings
UI에 가려지는 영역. 중심이나 피팅 계산은 이것을 빼고 이루어집니다.
visibleRegion
실제로 화면에 비치고 있는 지상의 범위. 사각형의 bounds와 네 모서리의 좌표를 가집니다.

copy()로 일부만 교체할 수 있고, 비교는 줌 0.01・기울기 0.01의 허용 오차를 두고 이루어집니다. 프로바이더에서 돌아오는 값의 미세한 흔들림으로 「움직였다」고 오판정하지 않기 위해서입니다.

플랫폼

03 · 줌 레벨=높이의 통일

줌은, 내부에서는 「카메라의 높이」와 같은 것으로 다뤄집니다. 2D 타일 계열의 줌, 3D 계열의 고도・시거리를 상호 변환할 수 있는 컨버터가 core에 있고, 어느 프로바이더의 값이든 일단 공통의 줌(Google Maps 기준)으로 고친 뒤에 공개됩니다.

이 기준은 이론만으로 정한 것이 아닙니다. 샘플 앱의 캘리브레이션 도구로, 같은 줌을 지정했을 때 실제로 보이는 범위를 각 엔진에서 실측하고, 면적이 일치하도록 기준 고도를 교정하고 있습니다. 그래서 zoom 14는, 어느 프로바이더에서도 같은 넓이입니다.

그림 · 줌 레벨과 축척의 기준(숫자는 화면 가로폭이 대략 몇 km에 해당하는지)
0
≈ 40,000 km
지구 전체
전 세계가 한 화면에 들어옵니다. 대륙의 윤곽만 보입니다.
3
≈ 5,000 km
대륙
대륙과 바다의 윤곽. 국경은 아직 의미를 갖지 않습니다.
6
≈ 600 km
국가
나라 전체와 주요 도시의 위치 관계.
10
≈ 40 km
도시권
시가지와 간선도로. 지도 앱의 초기 표시에 자주 씁니다.
14
≈ 2.5 km
시가
도로망이 읽힙니다. 매장 목록의 핀을 놓기에 알맞습니다.
17
≈ 300 m
블록
교차로와 건물의 형태. 도보 내비게이션의 기준.
20
≈ 40 m
부지・건물
건물 입구나 정확한 지점의 위치.
한 단계 올릴 때마다 축척은 2배. 표시된 값은 적도 부근·화면 폭 256px 기준의 환산이며, 위도에 따라 실제 거리는 달라집니다.
LATITUDE

같은 줌이라도 위도에 따라 필요한 높이가 다릅니다

웹 메르카토르는 극지방으로 갈수록 지도가 확대되므로, 같은 줌 레벨이라도 적도 부근과 고위도에서는 필요한 카메라 고도가 몇 배 차이 납니다. 줌 ⇄ 고도 변환에는 이 보정이 들어 있습니다.

ZOOM 10 · 적도 부근
≈ 5,200 m
ZOOM 10 · 북위 70도 부근
≈ 1,700 m
NATIVE
네이티브 값을 이 눈금에 맞추기
±0.0
Google Maps · Mapbox 2D — 그대로 기준으로 삼았습니다. 통합 줌은 이 계열에 맞춰져 있습니다.
+1.0
MapLibre · MapTiler · TomTom — 네이티브 줌이 한 단계 낮으므로 +1.0 오프셋을 적용해 맞춥니다.
m
MapKit · ArcGIS · Cesium · Google 3D — 줌 ⇄ 고도(시거리)를 서로 변환합니다. 위도와 기울기도 함께 고려됩니다.
ZOOM RANGE
0.0 – 22.0
ZOOM FACTOR
2.0
ZOOM 0 ALTITUDE
171,319,879 m
MAPLIBRE OFFSET
+1.0

2배마다 1단이라는 관계와, 기준이 되는 zoom 0의 고도만이 상수입니다. 위도가 높을수록 같은 줌에서의 지상 거리가 줄어드는 점도 변환에 들어 있기 때문에, 도쿄와 호놀룰루에서 축척이 어긋나는 일은 없습니다.

04 · tilt < 0의 의사 지원

tilt는 −60〜60도를 취합니다. 양의 값은 익숙한 비스듬한 내려다보기이지만, 음의 값은 수평선보다 위를 보는 앙각 뷰입니다. 지도 엔진에는, 이것을 직접 표현할 수 있는 것이 하나도 없습니다. 어느 SDK도 pitch는 0 이상만 받아들입니다.

그래서 MapConductor는, 카메라의 눈 위치와 방향을 유지한 채로, 엔진에는 양의 기울기로 주는 형태로 바꿔 놓습니다. 앱에서 보면 음의 tilt는 그대로 쓸 수 있고, 취득한 카메라 위치도 음의 tilt로 돌아옵니다.

tilt = 0

바로 위에서 내려다보는 평면도. 지상의 사각형이 그대로 화면의 사각형이 됩니다.

tilt > 0

앞으로 엎드린 비스듬한 시점. 지상에서는 앞이 좁고 뒤가 넓은 사다리꼴이 됩니다.

tilt < 0

수평선보다 위를 향하는 앙각 뷰. 하늘 쪽을 포함하는 시점으로, 네이티브에는 존재하지 않습니다.

의사 표현의 원리
01
카메라의 눈 위치와 높이는 그대로 두고, 지상의 주시점만 bearing 방향으로 전진시킵니다. 전진량은 카메라 고도와 기울기 각도로 정해집니다.
02
엔진에는 abs(tilt)의 아래 방향 피치로 전달합니다. 눈의 위치와 방향이 일치하므로 보이는 그림은 올려다보는 뷰와 같아집니다. MapLibre 계열 엔진에서는 화면을 맞추기 위해 전진량과 줌에 고정 보정 계수를 곱합니다.
03
카메라 위치를 읽을 때는 같은 계산을 거꾸로 되짚어, 전진한 주시점과 줌 보정을 상쇄하고 논리적인 음수 tilt와 원래 중심 좌표로 되돌립니다. 왕복해도 값이 변하지 않습니다.
음수 tilt 지원 현황
Android · 전체 프로바이더
OK
iOS · 전체 프로바이더
OK
React · Google Maps 2D 이외 전부
OK
React · Google Maps 2D
N/A

예외는 React의 Google Maps 2D(JavaScript API의 래스터 지도)뿐입니다. 기울기가 브라우저와 타일 쪽 조건에 좌우되어, 의사 표현이 전제로 하는 고도와 방향을 제어할 수 없기 때문입니다. Google Maps 3D(Map3DElement)에서는 음수 tilt를 쓸 수 있습니다.

TiltMapPageViewModel.kt
// −60〜60 슬라이더를 그대로 넘기기만 하면 된다. 음수 값이어도 같은 호출
currentPosition = currentPosition.copy(tilt = tilt)
mapViewState.value?.moveCameraTo(currentPosition)
샘플 영상 · Tilt(−60〜60을 연속으로 움직이기)

05 · VisibleRegion ― 실제로 보이고 있는 범위

카메라를 기울이면, 화면의 사각형은 지상에서는 사다리꼴이 됩니다. 남서와 북동만의 사각형(bounds)으로는, 앞과 뒤의 넓이의 차이를 나타낼 수 없습니다. VisibleRegion은 사각형에 더해 화면 네 모서리의 지상 좌표를 가지고, 기울어진 카메라에서도 「정말로 비치고 있는 범위」를 다룰 수 있게 합니다.

near는 화면의 아래 변, 즉 카메라에 가까운 쪽, far는 위 변으로 먼 쪽입니다. left / right는 화면 기준이므로, bearing으로 지도를 돌려도 의미는 달라지지 않습니다. 이 4점이 갖춰져 있는 덕분에, 기울어진 지도에서도 데이터 취득 범위나 출처 표기의 판정을 올바르게 할 수 있습니다.

farLeft
farRight
nearLeft
nearRight
화면의 사각형 → 지상에서는 사다리꼴
먼 쪽(far)일수록 넓고 가까운 쪽(near)일수록 좁아집니다. bounds는 이 사다리꼴을 감싸는 사각형입니다.
bounds
사다리꼴을 감싸는 남서·북동 사각형. 대략적인 범위 판정이나 기존 API와의 호환에 씁니다.
nearLeft / nearRight
화면 아래쪽 좌우의 지상 좌표. 카메라에 가까운 쪽.
farLeft / farRight
화면 위쪽 좌우의 지상 좌표. 먼 쪽이며, 기울일수록 크게 벌어집니다.
쓰임새
표시 범위의 데이터 취득, fitBounds, 타일 출처 표기(attributionRules) 전환 등.
VisibleRegionViewModel.kt
mapViewState.cameraPosition.visibleRegion?.let { region ->
    val sw = region.bounds.southWest
    val ne = region.bounds.northEast
    // 기울어진 화면의 네 모서리. 사다리꼴로 다룰 수 있다
    val corners = listOf(region.nearLeft, region.nearRight, region.farLeft, region.farRight)
}

관련 페이지