
Three.js를 이용해서 Google Maps API Street View 모드에 관로를 구현하였습니다.
구글 지도와 연동하여 입체 관로를 표시하고자 하는 분들이 참고하실 수 있도록 글로 남깁니다.
구글 지도 API의 공식 문서를 찾아보았는데 불가능하였습니다.
자세한 내용은 아래 링크를 참고해주시면 됩니다.
현재 스트리트 뷰 파노라마에서 지원되는 오버레이 유형은
Marker,InfoWindow,맞춤 OverlayView로 제한됩니다.
참조: Google Maps API Document | 스트리트 뷰 서비스
구글 지도 레이어 상단에 WebGL을 활용한 레이어를 덮어씌운 형태로 구현하기로 하였습니다.
JS에서 WebGL로 유명한 라이브러리는 거의 다 아시다시피 Three.js 라는 훌륭한 라이브러리가 존재합니다.
Three.js를 통해서 구현하기 위해서는 크게 아래 단계들을 거쳐야 합니다.
단계별로 좀 더 자세하게 설명해보겠습니다.
관로 GIS 데이터는 처음에 EPSG:5179이라는 좌표 참조 시스템(CRS)으로 제공됩니다. 이는 주로 한국에서 사용됩니다. 이 데이터를 Google Maps와 Three.js에서 사용할 수 있도록 WGS84 좌표 체계(EPSG:4326, 경도와 위도)로 변환해야 합니다.
좌표 변환에는 proj4 라이브러리를 활용했습니다.
const processCollection = (collection: any) => {
collection.forEach((item: any) => {
// 좌표를 EPSG:5179에서 EPSG:4326으로 변환
item.geometry.coordinates = item.geometry.coordinates.map(
(coord: any) => {
return proj4('EPSG:5179', 'EPSG:4326', coord);
}
);
out.push(new PipeLine(item));
});
};
GIS 데이터를 위도와 경도로 변환한 좌표를 Three.js에서 사용할 수 있는 3D 공간으로 변환합니다.
export class MapPoint extends google.maps.LatLng {}
const linePath: MapPoint[] = staticData!.linePath;
const points = linePath.map(point => this.latLngToVector3(point.lng(), point.lat()));
/**
* 위도, 경도 좌표를 받아서 미터 단위 좌표로 변환합니다.
* @param lng 경도
* @param lat 위도
* @returns 변환된 미터 단위 좌표
*/
latLngToMeters(lng: number, lat: number): { x: number; y: number } {
const baseLat = 35.89672396898795;
const baseLng = 128.6076667475771;
const earthRadius = 6378137; // 지구 반지름, 미터 단위
const dLat = (lat - baseLat) * Math.PI / 180;
const dLng = (lng - baseLng) * Math.PI / 180;
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(baseLat * Math.PI / 180) * Math.cos(lat * Math.PI / 180) *
Math.sin(dLng/2) * Math.sin(dLng/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const d = earthRadius * c;
return {x: dLng * earthRadius * Math.cos(baseLat * Math.PI / 180), y: dLat * earthRadius, };
}
/**
* 위도, 경도 좌표를 받아서 THREE.js의 Vector3로 변환합니다.
* @param lng 경도
* @param lat 위도
* @returns 변환된 Vector3 객체
*/
latLngToVector3(lng: number, lat: number): THREE.Vector3 {
const m = this.latLngToMeters(lng, lat);
return new THREE.Vector3(m.x, 0, -m.y);
}
latLngToMeters 함수 세부 설명:
기준 좌표 설정:
baseLat와 baseLng는 변환 기준이 되는 위도와 경도입니다. 이 값을 기준으로 변환된 좌표계의 (0, 0) 지점이 결정됩니다.(= 여기서는 대구의 어떤 지역을 중심으로 설정했습니다.)
지구 반지름:
earthRadius는 지구의 반지름을 나타내며, WGS84 좌표계에서 자주 사용하는 6,378,137미터의 값을 사용합니다.
위도와 경도 차이 계산 (dLat, dLng):
dLat와 dLng는 기준 좌표와 입력된 좌표 간의 차이를 라디안 단위로 변환한 값입니다.
이 값들은 두 좌표 사이의 실제 거리 계산에 사용됩니다.
Haversine 공식 사용:
두 좌표 사이의 거리 계산을 위해 Haversine 공식을 사용합니다.
a: 두 점 사이의 대각선을 이용한 사인 값을 계산하여 두 점 간의 거리 비율을 계산합니다.c: 두 점 사이의 대각선 각도 값.d: Haversine 공식을 사용하여 두 점 간의 거리(단위: 미터)를 계산한 결과.x, y 좌표 변환:
x 좌표:
x는 경도 차이에 해당하는 dLng를 지구 반지름과 곱해 얻은 거리입니다.
여기에 기준 위도의 코사인 값을 곱해 경도에 따른 거리 변화를 보정합니다.
이는 기준 위도에서 위도가 클수록 경도 1도가 나타내는 거리가 줄어드는 특성 때문입니다.
y 좌표:
y는 위도 차이에 해당하는 dLat을 지구 반지름과 곱한 거리입니다. 위도는 경도에 비해 거리가 변하지 않기 때문에 별도의 보정 없이 곱합니다.
latLngToVector3 세부 설명:
latLngToMeters 호출:
latLngToVector3 함수는 먼저 latLngToMeters를 호출하여 위도와 경도를 변환한 미터 단위의 x, y 좌표를 얻습니다.
THREE.Vector3 객체 생성:
x: 변환된 미터 단위의 x 값을 그대로 사용하여 Three.js의 x 축에 설정합니다.y: y 값은 0으로 설정합니다. 이는 기본적으로 지면에 해당하는 y 좌표를 0으로 설정한 것입니다.z: -m.y로 설정하여 Three.js의 z 축에 y 값을 부여합니다. 여기서 -를 사용하는 이유는 Three.js와 위도 좌표계 간의 축 방향 차이 때문입니다. 일반적으로 위도는 북쪽으로 갈수록 값이 커지지만, Three.js의 z 축에서는 반대 방향으로 이동하기 때문에 부호를 반전합니다.변환된 좌표를 사용하여 Three.js에서 3D 관로 기하를 생성합니다.
/**
* 관로를 3D로 렌더링합니다.
* @param pipeData 관로 데이터
* @param color 관로 색상
* @param pipeId 관로 ID
* @returns void
*/
addPipeLine(pipeData: MoPipe, color: string, pipeId: string){
// 관로에 대한 정적 데이터를 가져옴
const staticData = this.staticPipeMgr.getObjsById(pipeData.id);
const minDip = 13;
const maxDip = 800;
const minWidth = 0.1; // 최소 굵기
const maxWidth = 2.0; // 최대 굵기
const normDip = (Number(staticData?.properties.std_dip) - minDip) / (maxDip - minDip);
let width = normDip * (maxWidth - minWidth) + minWidth;
width = Math.max(Math.min(width, maxWidth), minWidth); // 굵기를 maxWidth와 minWidth 사이로 제한
const linePath: MapPoint[] = staticData!.linePath;
const points = linePath.map(point => this.latLngToVector3(point.lng(), point.lat()));
const curve = new THREE.CatmullRomCurve3(points);
const geometry = new THREE.TubeGeometry(curve, 100, width, 20, false);
const material = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide, opacity: 0.7 });
const mesh = new THREE.Mesh(geometry, material);
this.addObjectToScene(mesh);
}
/**
* THREE.js 씬에 객체를 추가합니다.
* @param obj
*/
public addObjectToScene(obj: THREE.Object3D){
this.scene.add(obj);
}
세부 설명
const minDip = 13;
const maxDip = 800;
const minWidth = 0.1;
const maxWidth = 2.0;
const normDip = (Number('파이프굵기 ex) 20') - minDip) / (maxDip - minDip);
let width = normDip * (maxWidth - minWidth) + minWidth;
width = Math.max(Math.min(width, maxWidth), minWidth);
minDip과 maxDip: 파이프의 최소 및 최대 굵기 값을 정의합니다.minWidth와 maxWidth: 파이프의 최소 및 최대 굵기(직경)를 설정합니다.normDip 계산: 파이프의 굵기 값을 기준으로 0과 1 사이의 값으로 정규화(normalization)합니다. 예시로 사용된 '20'은 파이프의 굵기 값을 의미하며, 이를 minDip과 maxDip 사이의 비율로 변환합니다.width 계산: 정규화된 값을 이용해 파이프의 최종 굵기(width)를 계산하고, 이를 minWidth와 maxWidth 사이로 제한합니다. const linePath: MapPoint[] = staticData!.linePath;
const points = linePath.map(point => this.latLngToVector3(point.lng(), point.lat()));
const curve = new THREE.CatmullRomCurve3(points);
linePath에는 관로의 좌표들이 MapPoint 형태로 저장되어 있습니다.MapPoint의 경도와 위도를 latLngToVector3 메서드를 사용해 3D 좌표(THREE.Vector3)로 변환합니다.THREE.CatmullRomCurve3는 점들의 배열을 사용해 부드러운 3D 곡선을 생성합니다. 이 곡선은 관로의 경로를 표현합니다. const geometry = new THREE.TubeGeometry(curve, 100, width, 20, false);
const material = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide, opacity: 0.7 });
const mesh = new THREE.Mesh(geometry, material);
THREE.TubeGeometry: curve를 따라 3D 튜브 형태의 기하(geometry)를 생성합니다.100: 곡선의 세그먼트 수, 곡선을 얼마나 세밀하게 표현할지를 결정합니다.width: 튜브의 반지름으로, 관로의 굵기를 나타냅니다.20: 원형 단면의 세그먼트 수, 튜브의 둥근 정도를 결정합니다.false: 튜브를 닫지 않고, 양 끝이 열린 형태로 만듭니다.THREE.MeshBasicMaterial: 파이프에 사용할 재질(material)을 정의합니다.color: 파이프의 색상을 설정합니다.side: THREE.DoubleSide: 파이프의 내부와 외부 모두 렌더링합니다.opacity: 0.7: 반투명하게 설정하여 약간 투명하게 표시합니다.THREE.Mesh: 생성된 기하와 재질을 조합해 파이프의 메쉬(mesh)를 생성합니다. this.addObjectToScene(mesh);
addObjectToScene 메서드를 호출해 생성된 파이프 메쉬를 Three.js 씬에 추가합니다.addObjectToScene 함수기능:
Three.js 씬에 객체를 추가하여 렌더링할 수 있도록 합니다.
설명:
obj로 전달된 THREE.Object3D 객체를 this.scene에 추가합니다.this.scene은 Three.js에서 모든 3D 객체가 포함되는 공간입니다. 씬에 추가된 객체들은 Three.js 카메라로 렌더링되어 화면에 표시됩니다.THREE.Mesh와 같은 3D 메쉬뿐만 아니라 조명(THREE.Light), 그룹(THREE.Group), 카메라(THREE.Camera) 등의 다양한 객체를 추가할 수 있습니다.이를 통해 GIS 데이터로부터 얻은 관로 경로를 3D로 시각화하여, 특정 지역의 관로 네트워크를 효과적으로 표현할 수 있습니다.
이 글에서는 Three.js를 활용해 Google Maps API Street View에서 3D 관로를 표시하는 방법을 설명합니다. 구글 지도 API 자체적으로는 스트리트 뷰에 입체적인 관로를 직접 표시할 수 없기 때문에, WebGL을 활용해 커스텀 레이어를 구현했습니다.
GIS 데이터 로드 및 변환: 관로 데이터는 EPSG:5179 좌표계로 제공되며, 이를 proj4 라이브러리를 사용해 WGS84 좌표계로 변환합니다.
Three.js 공간으로 변환: 변환된 위도와 경도 좌표를 미터 단위로 변환하고, 이를 THREE.Vector3 객체로 변환합니다.
3D 관로 기하 생성: Three.js의 TubeGeometry를 사용해 파이프의 두께와 경로를 따라 3D 기하를 생성하고, 재질을 적용해 메쉬를 만듭니다.
이어서 4, 5 단계에 해당하는 Google Street View와 동기화와 카메라와 가시성 업데이트에 대한 내용을 작성해서 올리겠습니다.
읽어주셔서 감사합니다.