Unity에서 SUMO 도로 네트워크(.net.xml) 시각화하기
SUMO로 도로네트워크 시뮬레이션을 위해 도로 정보를 lane(차선) 단위로 분석·시각화하려고 합니다. 하지만 SUMO에서 자체적으로 변환한 GeoJSON 파일은 edge(도로 중심선)만 추출해주고, 실제 각 차선(lane) 단위의 라인은 표현되지 않습니다. 그래서 net.xml을 직접 파싱해서 lane별 GeoJSON을 만드려고 합니다.
이전 글 보기
[디지털 트윈] Unity에 지형을 3D로 시각화하기 - 수치 지형도/정사 영상 편
Unity에 지형을 3D로 시각화하기 - 수치 지형도/정사 영상 편 디지털 트윈 프로젝트를 준비하며, 웹 브라우저 상에서 3D 지형을 구현해보려 합니다. 이를 위해 지형, 건물, 도로 등 다양한 공간 정보
dachaes-devlogs.tistory.com
[디지털 트윈] Unity에 건물을 3D로 시각화하기
Unity에 건물을 3D로 시각화하기 디지털 트윈 프로젝트를 준비하며, 웹 브라우저 상에서 3D 지도를 구현해보려 합니다. 이를 위해 지형, 건물, 도로 등 다양한 공간 정보를 시각화할 계획입니다. 우
dachaes-devlogs.tistory.com
개발 환경
- Node v22.16.0
- React v19.1.0 (TypeScript) + Vite
- QGIS Desktop 3.42.3 → 3.44.0
- Unity 6000.0.50f1 (LTS)
개발 순서
- SUMO net.xml 파일 파싱하기
- QGIS에서 필요한 도로 구획만 남기고 자르기
- 도로 레이어 꾸미기
- glTF 파일로 내보내기 (export)
- 유니티에 불러오기
1. SUMO net.xml 파일 파싱하기
SUMO net.xml에는 lane(차선) 정보와 각 차선의 shape 좌표가 들어 있습니다. 이 좌표를 각각 LineString으로 변환하여GeoJSON 파일로 저장할 수 있습니다. SUMO에서 GeoJSON 파일로 변환해 주는 기능이 있으나, 이 파일은 edge(도로 중심선)만 추출해주고, 실제 각 차선(lane) 단위의 라인은 표현되지 않습니다.
a. net.xml의 좌표계 정보 읽기
net.xml에는 보통 아래와 같은 location 정보가 있습니다.
<location netOffset="-295519.28,-4128274.41" projParameter="+proj=utm +zone=52 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"/>
- projParameter를 보면 이 도로네트워크의 좌표계는 UTM 52N (EPSG:32652)임을 알 수 있습니다.
- netOffset은 모든 shape 좌표에 더하거나 빼서 실제 UTM 52N 좌표계로 변환해주는 값입니다.
b. pyproj를 활용한 좌표계 변환
Python의 pyproj 라이브러리를 활용하여 UTM 52N(32652) → WGS84(4326) 또는 UTM 52N(32652) → TM서부(5186) 등으로 변환합니다.
c. 파싱 코드 (임시)
import xml.etree.ElementTree as ET
import json
from pyproj import CRS, Transformer
SRC_EPSG = 32652 # UTM 52N
DST_EPSG = 4326 # WGS84(경위도)
NET_XML = 'network_file.net.xml'
LANE_GEOJSON = 'network_lanes_parsed.geojson'
src_crs = CRS.from_epsg(SRC_EPSG)
dst_crs = CRS.from_epsg(DST_EPSG)
transformer = Transformer.from_crs(src_crs, dst_crs, always_xy=True)
tree = ET.parse(NET_XML)
root = tree.getroot()
loc_tag = root.find('.//location')
netOffset_x, netOffset_y = 0.0, 0.0
if loc_tag is not None and 'netOffset' in loc_tag.attrib:
netOffset_x, netOffset_y = map(float, loc_tag.get('netOffset').split(','))
lane_features = []
for lane in root.findall('.//lane'):
lane_id = lane.get('id')
shape = lane.get('shape')
if not shape:
continue
coords = []
for pt in shape.strip().split(' '):
x, y = map(float, pt.split(','))
# netOffset 적용
real_x = x - netOffset_x
real_y = y - netOffset_y
lon, lat = transformer.transform(real_x, real_y)
coords.append([lon, lat])
lane_features.append({
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": coords},
"properties": {"id": lane_id}
})
with open(LANE_GEOJSON, 'w', encoding='utf-8') as f:
json.dump({
"type": "FeatureCollection",
"features": lane_features
}, f, ensure_ascii=False, indent=2)
print(f"lane 파싱 완료: {LANE_GEOJSON}")
SUMO의 netconvert로 만든 GeoJSON은 보통 EPSG:4326(경위도) 좌표계를 사용하기 때문에 QGIS 같은 GIS 소프트웨어에 바로 올려도 위치가 잘 맞습니다. 하지만 net.xml을 직접 파싱해서 GeoJSON을 만들면, 결과가 엉뚱한 위치로 나오는 경우가 많습니다.
이런 문제가 발생하는 이유는 net.xml에서 사용하는 좌표계가 UTM이나 TM 같은 지역 좌표계인 데다, 각 도로 shape 좌표가 netOffset만큼 평행이동되어 저장되기 때문입니다. SUMO의 내부 변환툴(netconvert)은 이 오프셋(netOffset)과 좌표계 변환을 자동으로 처리해주지만, 직접 파싱할 때는 오프셋 보정이 빠져서 위치가 어긋나는 일이 흔하게 생깁니다. 따라서 net.xml을 직접 파싱해서 GeoJSON을 만들 때는 반드시 netOffset 값을 빼준 뒤, pyproj 같은 도구로 EPSG:4326(경위도)나 EPSG:5186(한국 TM서부) 등 원하는 좌표계로 변환해줘야 합니다.
2. QGIS에서 필요한 도로 구획만 남기고 자르기
상단 탭에 레이어 > 벡터 레이어 추가 를 눌러 아웃풋 파일(파싱한 파일)을 넣거나 레이어 창에 드래그 앤 드롭합니다. 그러면 도로가 현재 노란색(색상 랜덤)으로 표현된 것을 볼 수 있습니다.
필요한 구획만 남기기 위해 레이어 > 레이어 생성 > 새 임시 스크래치 레이어를 선택하면 새 임시 스크래치 레이어 창이 열립니다. 레이어 이름을 작성하고, 도형 유형을 선택합니다. 좌표계는 프로젝트와 동일한 좌표계를 선택합니다.
그러면 레이어 탭에 방금 만든 임시 스크래치 데이터가 생성된 것을 볼 수 있습니다.
새로 만든 레이어를 우클릭하여 편집 모드 켜고 끄기를 누릅니다. 편집 > 폴리곤 피처 추가를 선택하고, 원하는 영역을 그리고, 저장합니다. 또는 캡처 사진에 체크한 아이콘을 통해 편집 모드에 들어가서 원하는 영역을 그릴 수 있습니다
벡터 > Geoprocessing Tools > 자르기 를 선택하면 벡터 중첩 - 자르기 창이 열립니다. 입력 레이어에는 도로가 있는 레이어를, 중첩 레이어에는 원하는 위치를 고른 레이어를 선택한 후 실행합니다.
a. 유효성 체크하기
하지만 자르기를 실행하면, 위 사진과 같이 에러가 발생합니다. 실제 잘린 산출물 레이어를 봐도 도로가 생성이 되지 않았음을 볼 수 있습니다. 이런 경우에는 상단 탭에서 벡터 > 지오메트리 도구(Geometry Tools) > 유효성 체크(Check Validity) 를 눌러 벡터 지오메트리 창을 엽니다. 그리고 입력 레이어에 도로 레이어를 넣고 실행을 누릅니다. 그리고 산출된 Valid output을 사용합니다. 에러가 뜨지 않는다면 유효성 체크를 하지 않아도 됩니다.
(하지만 성공적으로 잘라버려도, 어차피 Z값이 없기 때문에 아래의 작업을 진행한 후 다시 잘라야 함)
b. 자르기 전에, Z 값부터 넣기 (제대로 실행안돼서 수정 예정 - 파싱 과정 문제일 듯)
생성된 도로 라인에는 고도를 의미하는 값이 없으므로 해당 Z 값을 DEM에서 추출하여 넣어야 합니다. 그러기 위해 상단 탭의 플러그인 > 플러그인 관리 및 설치 를 눌러 Point Sampling Tool 을 설치합니다.
Point Sampling Tool은 포인트에만 값을 붙일 수 있으므로 도로 라인에 포인트를 만들어야 합니다. 우선 상단 탭의 공간 처리 > 툴박스 에서 라인을 따라 포인트 생성 창을 엽니다. 벡터 지리 정보 처리 - 라인을 따라 포인트 생성 창에 원하는 도로 라인을 넣고, 포인트를 생성하고 싶은 간격을 기입합니다.
이렇게 생성된 도로 라인 포인트와 DEM의 좌표계가 현재 다릅니다. (DEM은 5179, 도로는 4326임)
포인트 레이어를 우클릭하여 내보내기 > 피처를 다른 이름으로 저장 을 눌러서 좌표계를 5179로 변경합니다.
이제 플러그인 > Analysis > Point Sampling Tool 을 눌러 Point Sampling Tool을 실행합니다. Layer containing sampling points는 방금 만든 '라인을 따라 포인트 생성_5179' 레이어를, Layers with fields/bands to get values from은 이전에 작업했던 '라인을 따라 포인트 생성_5179:fid', '라인을 따라 포인트 생성_5179:id', 'DEM' 레이어를 선택합니다.
다음은 공간 처리 툴박스 > 포인트를 경로로(Point to Path) 를 선택합니다.
.
.
.
c. 자르기
다시 벡터 > Geoprocessing Tools > 자르기 를 통해 레이어를 원하는 구획만 남기고 잘라줍니다.
성공적으로 원하는 구획의 도로를 잘라낸 것을 볼 수 있습니다. (확인을 위해 지형과 건물 레이어를 잠시 껐습니다.)
3. 도로 레이어 꾸미기
도로 레이어를 정말 도로로 보이도록 속성 창에 들어가서 심볼의 색상과 투명도를 조절하고 너비와 단위를 변경해줍니다. 수치는 각자 프로젝트의 정사 영상 사진(또는 위성 사진)에 맞추어 조정합니다.
도로의 윤곽에 흰색 라인을 표현해주기 위해, 심볼에 라인을 두 개 더 추가합니다.
4. glTF 파일로 내보내기 (export)
상단 탭의 플러그인 > 플러그인 관리 및 설치 창을 엽니다. 플러그인 창에서 Qgis2threejs을 설치합니다. 설치가 완료되면 상단 탭의 웹 > Qgis2threejs > Qgis2threejs Exporter 를 열고, DEM을 선택합니다.
원하는 구획만 export하고 싶다면, Scene > Scene Settings 를 누르고, Base Extent > Fixed extent > Select > Use Layer Extent 에서 원하는 크기의 레이어를 선택합니다. 또는 Base Extent > Fixed extent > Select > Select Extent on Canvas 를 선택하여 원하는 크기를 직접 드래그합니다.
DEM을 우클릭하여 properties 창을 엽니다. 배경으로 쓸 DEM과 정사 영상 레이어를 선택합니다.
도로 레이어를 우클릭하여 poroperties 창을 엽니다.
(작성 중....)
'개발 기록 > [CTSG] 25.06.01-' 카테고리의 다른 글
[디지털 트윈] Unity에서 SUMO 차량(fcd.xml) 시뮬레이션하기 (2) | 2025.07.01 |
---|---|
[디지털 트윈] Unity에서 건물을 3D로 시각화하기 (0) | 2025.06.27 |
[디지털 트윈] Unity에서 지형을 3D로 시각화하기 - DEM 편 (0) | 2025.06.24 |
[디지털 트윈] Unity에서 지형을 3D로 시각화하기 - 수치 지형도/정사 영상 편 (0) | 2025.06.24 |
[디지털 트윈] React와 CesiumJS로 3D 건물 시각화하기 (0) | 2025.06.11 |