これは MIERUNE AdventCalendar 2023 24日目の記事です! 昨日は@northprintさんによるSvelteKitでURLクエリパラメーターの操作をするでした。
はじめに
この記事では新宿駅の屋内地図データを使用して、Three.jsで3Dによる可視化をします。
DEMOはこちら
サンプルコードはこちら
使用するデータ
今回は、G空間情報センターで公開されている「新宿駅屋内地図オープンデータ」の統合版(ShapeFile)を使用します。
データについての詳細は製品仕様書に記載されています。
この記事のように、データの加工利用には以下の出典が必要となります。
コンテンツを編集・加工等して利用する場合は、上記出典とは別に、編集・加工等を行ったことを記載してください。なお、編集・加工した情報を、あたかも国(又は府省等)が作成したかのような態様で公表・利用してはいけません。(コンテンツを編集・加工等して利用する場合の記載例)「新宿駅周辺屋内地図データ」(国土交通省)(https://www.geospatial.jp/ckan/dataset/mlit-indoor-shinjuku-r2)を加工して作成
統合版(ShapeFile)のデータからShinjukuTerminal
ディレクトリに入っている以下の地物データを使用します。
地物データ
ファイル名 | データ名称 | 地物タイプ |
---|---|---|
ShinjukuTerminal_<階層名>_Floor.shp | 階層データ | Polygon |
ShinjukuTerminal_<階層名>_Space.shp | 物理的な空間データ | Polygon |
ShinjukuTerminal_<階層名>_Fixture.shp | 固定設置物データ | Polygon |
QGISでの表示(※半透明にしてます)
また、nw
ディレクトリに入っているネットワークデータも使用します。
ネットワークデータ
ファイル名 | データ名称 | 地物タイプ |
---|---|---|
Shinjuku_node.shp | ノードデータ | Point |
Shinjuku_link.shp | リンクデータ | LineString |
データ変換
これらのデータはすべてShapeFileとなっています。JavaScriptで扱いやすいように
GDALコマンドでShapeFileをGeoJsonに変換します。また、座標系はEPSG6677に変換します。
mkdir geojson
for f in *.shp; do
ogr2ogr -f GeoJSON -t_srs EPSG:6677 "geojson/${f%.*}.geojson" $f
done
Three.jsで描画する
データの変換後にThree.js上で表示させます。
シーンの作成
シーンを作成し、カメラとコントロールを追加します。コントロールの種類はお好みで構いませんが、この記事では移動操作をMapControls、ズーム操作をTrackballControlsに割り当ててます。
npm install three
import * as THREE from 'three';
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js';
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
// キャンバスの作成
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
// シーンの作成
const scene = new THREE.Scene();
// カメラ
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100000);
camera.position.set(-190, 280, -350);
scene.add(camera);
// コントロール
const mapControls = new MapControls(camera, canvas);
mapControls.enableDamping = true;
mapControls.enableZoom = false;
mapControls.maxDistance = 1000;
const zoomControls = new TrackballControls(camera, canvas);
zoomControls.noPan = true;
zoomControls.noRotate = true;
zoomControls.noZoom = false;
zoomControls.zoomSpeed = 0.5;
// レンダラー
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
alpha: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 画面リサイズ時にキャンバスもリサイズ
const onResize = () => {
// サイズを取得
const width = window.innerWidth;
const height = window.innerHeight;
// レンダラーのサイズを調整する
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
// カメラのアスペクト比を正す
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', onResize);
// アニメーション
const animate = () => {
requestAnimationFrame(animate);
const target = mapControls.target;
mapControls.update();
zoomControls.target.set(target.x, target.y, target.z);
zoomControls.update();
renderer.render(scene, camera);
};
animate();
また、WebGLRendererのオプションでalpha: true
にすることで背景を透過させ、CSSで背景にグラデーションを適用させます。
canvas {
background-image: radial-gradient(#382c6e, #000000);
}
GUIとグループの作成
シーンの中に地物データを描画させます。今回は地物を階層ごとにグループ分けしたいと思うので、シーンのにあらかじめGroupを作成しておき、地物データによって作成したオブジェクトを各階層ごとのグループに追加することにします。そして、各階層の表示切り替えができるようにチェックボックスのGUIも追加します。
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
// dat.GUIのインスタンスを作成
const gui = new GUI({ width: 150 });
// グループの作成
const groupList = [4, 3, 2, 1, 0, -1, -2, -3];
const layers = ['4F', '3F', '2F', '1F', '0', 'B1', 'B2', 'B3'];
groupList.forEach((num, i) => {
const group = new THREE.Group();
group.name = `group${num}`;
scene.add(group);
const key = `group${num}`;
// GUIにチェックボックスを追加
gui.add(
{
[key]: true,
},
key,
)
.onChange((isVisible) => {
scene.getObjectByName(key).visible = isVisible;
})
.name(layers[i]);
});
地物データの追加
作成した階層グループごとに地物データを追加していきます。地物データは2次元のポリゴンデータなので、ExtrudeGeometryを使用して立ち上げることで立体化します。階層情報はファイル名にありますが、高さ情報はないため、地物データの種類で高さ分けをすることにします。
また、地物データは前述の変換処理によりEPSG:6677になっていますがThree.jsのワールド座標にそのままの座標値で乗っけてしまうと、原点0地点から遠くは慣れた場所に描画されてしまうので、地物にオフセットをかける必要があります。
まずは、シーンの原点0をEPSG:6677上のどの座標点にするかを決めます。今回は-12035.29, -34261.85(x,y)
の地点をワールド座標の原点0に合わせます。この座標は新宿駅の中心あたりになります。
地物データのポリゴンの座標値からExtrudeGeometryを作成するときに、各座標点から中心とする点([-12035.29, -34261.85])を減算することでオフセットをかけます。これにより地物がシーンの原点0の近くに描画されます。
もう一つ注意点として、Three.js上(ワールド座標)のY、Zのベクトル方向とGIS上(地理座標)のY、Zのベクトル方向が90度違うため、ExtrudeGeometryを作成後にx軸を起点に前方向(Z軸を正面として)に90度倒す必要があります(そうしないと地物が縦向きになってしまいます)。
これらを踏まえた上でGeoJsonからExtrudeGeometryを作成します。
Space、Floor、Fixtureのデータを各配列に記述してループ処理をし、getFloorNumber
という関数で階層情報を取得し、loadAndAddToScene
という関数で、引数にGeoJsonの情報と、階層情報、高さにする数値を渡します。
// Spaceの配列
const SpaceLists = [
'./ShinjukuTerminal/ShinjukuTerminal_B3_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_B2_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_B1_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_0_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_1_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_2_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_2out_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_3_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_3out_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_4_Space.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_4out_Space.geojson',
];
// Spaceの読み込み
SpaceLists.forEach((geojson) => {
const floorNumber = getFloorNumber(geojson, 'Space');
if (floorNumber !== null) {
loadAndAddToScene(geojson, floorNumber, 5);
}
});
// Spaceの配列
const FloorLists = [
'./ShinjukuTerminal/ShinjukuTerminal_B3_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_B2_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_B1_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_0_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_1_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_2_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_2out_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_3_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_3out_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_4_Floor.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_4out_Floor.geojson',
];
// Floorの読み込み
FloorLists.forEach((geojson) => {
const floorNumber = getFloorNumber(geojson, 'Floor');
if (floorNumber !== null) {
loadAndAddToScene(geojson, floorNumber, 0.5);
}
});
// Fixtureの配列
const FixtureLists = [
'./ShinjukuTerminal/ShinjukuTerminal_B3_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_B2_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_B1_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_0_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_2_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_2out_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_3_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_3out_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_4_Fixture.geojson',
'./ShinjukuTerminal/ShinjukuTerminal_4out_Fixture.geojson',
];
// Fixtureの読み込み
FixtureLists.forEach((geojson) => {
const floorNumber = getFloorNumber(geojson, 'Fixture');
if (floorNumber !== null) {
loadAndAddToScene(geojson, floorNumber, 5);
}
});
getFloorNumber
関数は以下になります。正規表現を使ってファイル名の数字の部分を抜き出し、地下である「B」が含まれている場合は負の数を返すようにしています。
// 正規表現を用いて階層番号を取得
const getFloorNumber = (geojson, type) => {
const regex = new RegExp(`ShinjukuTerminal_([-B\\d]+)(out)?_${type}`);
const match = geojson.match(regex);
if (!match) return null;
let floor = match[1].replace('B', '-');
return parseInt(match[2] === 'out' ? floor.replace('out', '') : floor, 10);
};
loadAndAddToScene
関数は以下になります。FileLoaderを使用してGeoJsonを読み込み、createExtrudedGeometry
という関数でExtrudeGeometryを生成し、ExtrudeGeometryからEdgesGeometryを作成することでアウトラインの立方体ポリゴンを作成します。このとき、立方体ポリゴンは縦向きになっているのでapplyMatrix4で90度回転させています。
そして、階層情報ごとにY軸のどのあたりに地物を置くかを決め、各階層のGroupに追加しています。
// 階ごとに離すY軸方向の距離
const verticalOffset = 30;
// FileLoaderをインスタンス化。JSON形式でデータを取得する
const loader = new THREE.FileLoader().setResponseType('json');
// ファイルを読み込んで、シーンに追加。geometryの情報がないものは除外
const loadAndAddToScene = (geojson, floorNumber, depth) => {
loader.load(geojson, (data) => {
// Lineのマテリアル
const lineMaterial = new THREE.LineBasicMaterial({ color: 'rgb(255, 255, 255)' });
// geometryの情報がないものは除外
data.features
.filter((feature) => feature.geometry)
.forEach((feature) => {
// ExtrudeGeometryを作成
const geometry = createExtrudedGeometry(feature.geometry.coordinates, depth);
// 90度回転させる
const matrix = new THREE.Matrix4().makeRotationX(Math.PI / -2);
geometry.applyMatrix4(matrix);
// ExtrudeGeometryからLineを作成
const edges = new THREE.EdgesGeometry(geometry);
const line = new THREE.LineSegments(edges, lineMaterial);
line.position.y += floorNumber * verticalOffset - 1;
// Groupに追加
const group = scene.getObjectByName(`group${floorNumber}`);
group.add(line);
});
});
};
createExtrudedGeometry
は以下になります。前述に説明した通り、ここでGeoJsonのcoordinates
(座標点)からExtrudedGeometryの元となるShapeGeometryを作成しています。各座標点はあらかじめ中心にする地理座標を減算しています。
また、地理空間データのポリゴンデータはポリゴンの閉じるための頂点が必要なので、頂点配列の最初と最後に同じ頂点座標を持っています(四角形のポリゴンは5つの頂点を持っている)。ShapeGeometryはこの最後の頂点は不要なので、最後の頂点は処理を飛ばしています。ExtrudeGeometryのオプションのdepth
は押し出す高さを示してます。
// シーンの中心にする地理座標(EPSG:6677)
const center = [-12035.29, -34261.85];
// ポリゴンからExtrudeGeometryを返す関数
const createExtrudedGeometry = (coordinates, depth) => {
const shape = new THREE.Shape();
// ポリゴンの座標からShapeを作成
coordinates[0].forEach((point, index) => {
const [x, y] = point.map((coord, idx) => coord - center[idx]);
if (index === 0) {
// 最初の点のみmoveTo
shape.moveTo(x, y);
} else if (index + 1 === coordinates[0].length) {
// 最後の点のみclosePathで閉じる
shape.closePath();
} else {
// それ以外はlineTo
shape.lineTo(x, y);
}
});
return new THREE.ExtrudeGeometry(shape, {
steps: 1,
depth: depth,
bevelEnabled: false,
});
};
以上のような処理を記述することで、地物データがシーンに追加されました。要塞みたいですね。
ネットワークデータの追加
続いて、ネットワークデータ(歩行者ネットワーク)をシーンに追加します。前述で変換したノードデータのGeoJsonを先ほどインスタンス化したFileLoaderで読み込み、nodeId
とordinal
(階層情報)を取得して配列を作成し、creatingLink
という関数に渡します。
// ノードデータからnode_idと階層(ordinal)を取得
loader.load('./nw/Shinjuku_node.geojson', (data) => {
const nodeIds = data.features.map((feature) => {
return {
node_id: feature.properties.node_id,
ordinal: feature.properties.ordinal,
};
});
// 歩行者ネットワークの作成
creatingLink(nodeIds);
});
creatingLink
関数は下記になります。リンクデータを読み込んで先ほど作成した配列から、リンクデータの始点と終点の階層を取得します。これはリンクデータに階層情報が含まれていないためです(事前にQGISのテーブル結合で階層情報を付与するのも良いかもしれません)。これで、リンクデータの始点と終点の階層はわかりますが、その途中のラインの階層がどこに当てはまるかがわかりません。そこで、条件分岐を用意して始点のノードデータしか見つからない場合は始点の階高さ、終点のノードデータしか見つからない場合は終点の階の高さにリンクデータのラインを作成します。もし始点と終点両方のノードデータが見つかる場合は、始点と終点の階が同じの場合は、その階にしかリンクデータのラインが存在しないことがわかりますが、始点と終点の階が違う場合は、その中間の階層にラインを仮で引くようにしました。
また、リンクデータのラインは幅のあるラインを描画できるMeshLineを使用します。コードの後半で、ラインを2点間の頂点を持つラインにわざわざ分割して、BufferGeometryUtilsで、マージしてからシーンに追加していますが、その理由はこの後説明します。
npm install three.meshline
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { MeshLine, MeshLineMaterial } from 'three.meshline';
// マテリアル
const linkMaterial = new MeshLineMaterial({
transparent: true,
lineWidth: 1,
color: new THREE.Color('rgb(0, 255, 255)'),
});
// MeshLineの配列
const meshLines = [];
// 歩行者ネットワークの作成
const creatingLink = (nodeId) => {
loader.load('./nw/Shinjuku_link.geojson', (data) => {
data.features.forEach((feature) => {
const coordinates = feature.geometry.coordinates;
// ノードデータからstart_idとend_idの取得
const start_id = nodeId.find((node) => node.node_id === feature.properties.start_id);
const end_id = nodeId.find((node) => node.node_id === feature.properties.end_id);
// 3次元のpointの配列を作成
const points = coordinates.map((point, index) => {
let y;
if (!start_id && !end_id) {
// start_idとend_idがない場合は、0階層に配置
y = 0;
} else if (start_id && !end_id) {
// start_idのみある場合は、start_idの階層に配置
y = start_id.ordinal;
} else if (!start_id && end_id) {
// end_idのみある場合は、end_idの階層に配置
y = end_id.ordinal;
} else {
// start_idとend_idがある場合
if (index === 0) {
// 最初の点の場合はstart_idの階層に配置
y = start_id.ordinal;
} else if (index === coordinates.length - 1) {
// 最後の点の場合はend_idの階層に配置
y = end_id.ordinal;
} else if (start_id.ordinal === end_id.ordinal) {
// start_idとend_idの階層が同じ場合は、その階層に配置
y = end_id.ordinal;
} else {
// start_idとend_idの階層が異なる場合は、中間の階層に配置
y = Math.round((start_id.ordinal + end_id.ordinal) / 2);
}
}
return new THREE.Vector3(point[0] - center[0], y * verticalOffset + 1, -(point[1] - center[1]));
});
// pointの配列からMeshLineを作成
points.forEach((point, index) => {
// 最後の点の場合は処理を終了
if (index + 1 === points.length) return;
// MeshLineを作成。2点間のMeshLineを別々に作成する
const geometry = new THREE.BufferGeometry().setFromPoints([point, points[index + 1]]);
const line = new MeshLine();
line.setGeometry(geometry);
// MeshLineの配列に追加
const mesh = new THREE.Mesh(line, linkMaterial);
meshLines.push(mesh.geometry);
});
});
// MeshLineをマージ
const linkGeometry = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(meshLines), linkMaterial);
linkGeometry.name = 'link';
// シーンに追加
scene.add(linkGeometry);
});
};
歩行者ネットワークをうまく3次元化できました。
ですが、このままでは面白くないので、歩行者ネットワーク感?を表現するためにシェーダーでアニメーションをつけることにします。
シェーダーを書く
リンクデータは製品仕様書によると進行方向を示す情報(direction
)をもっているので、これに合わせて違うアニメーションを適用させることにします。
今回使用したデータは両方向(1)と起点より終点方向(2)の2パターンしかなかったので2パターンの簡易的なシェーダーを用意しました。
「両方向」のシェーダー
「起点より終点方向」のシェーダー
MeshLineにシェーダーを適用させます。まず、先ほど定義したMeshLineMaterial
からカラー情報を削除します。
// マテリアル
const linkMaterial = new MeshLineMaterial({
transparent: true,
lineWidth: 1,
- color: new THREE.Color('rgb(0, 255, 255)'),
});
そして、MeshLineの既存のシェーダーを上書きするためにonBeforeCompileを使用してシェーダーを追記していきます。
こちらの記事を参考にさせていただきました。
// Shaderを追加
linkMaterial.onBeforeCompile = (shader) => {
// userDataにuniformsを追加
Object.assign(shader.uniforms, linkMaterial.userData.uniforms);
const keyword1 = 'void main() {';
shader.vertexShader = shader.vertexShader.replace(
keyword1,
/* GLSL */ `
varying vec2 vUv;
attribute float uDistance;
attribute float uDirection;
varying float vDistance;
varying float vDirection;
${keyword1}`,
);
// 置換してシェーダーに追記する
const keyword2 = 'vUV = uv;';
shader.vertexShader = shader.vertexShader.replace(
keyword2,
/* GLSL */ `
${keyword2}
vUv = uv;
vDistance = uDistance;
vDirection = uDirection;
`,
);
const keyword3 = 'void main() {';
shader.fragmentShader = shader.fragmentShader.replace(
keyword3,
/* GLSL */ `
uniform float uTime;
varying float vDirection;
varying float vDistance;
varying vec2 vUv;
${keyword3}`,
);
// 置換してシェーダーに追記する
const keyword4 = 'gl_FragColor.a *= step(vCounters, visibility);';
shader.fragmentShader = shader.fragmentShader.replace(
keyword4,
/* GLSL */ `${keyword4}
vec2 p;
p.x = vUv.x * vDistance;
p.y = vUv.y * 1.0 - 0.5;
float centerDistY = p.y; // 中心からのY距離
float offset = abs(centerDistY) * 0.5; // 斜めの角度を制御
float time = uTime;
// 中心より上と下で斜めの方向を変える
if(centerDistY < 0.0) {
if(vDirection == 1.0){
time = -uTime;
offset = -offset;
}else if(vDirection == 2.0) {
offset = offset;
}
}
// mod関数と中心からのy距離に基づくオフセットを使用して線を生成
float line = mod(p.x - time + offset, 1.9) < 0.9 ? 1.0 : 0.0;
vec3 mainColor;
// 方向によって色を変える
if(vDirection == 1.0) {
mainColor = vec3(0.0, 1.0, 1.0);
} else if(vDirection == 2.0) {
mainColor = vec3(1.0, 1.0, 0.0);
}
vec3 color = mix(mainColor, mainColor, line);
gl_FragColor = vec4(color, line * 0.7);
`,
);
};
そしてcreatingLink
関数に追記します。各MeshLineのUV座標のアスペクト比を合わせるためにdistanceToで分割したラインの2点間の距離を取得してuDirection
というattribute変数をシェーダーに渡します。さらにリンクデータの方向の情報もuDirection
というattribute変数でシェーダーに渡します。アニメーションをさせるためにuTime
というuniforms変数を追加してます。
// メッシュラインの配列
const meshLines = [];
// 歩行者ネットワークの作成
const creatingLink = (nodeId) => {
loader.load('./nw/Shinjuku_link.geojson', (data) => {
data.features.forEach((feature) => {
/* 省略 */
// pointの配列からMeshLineを作成
points.forEach((point, index) => {
// 最後の点の場合は処理を終了
if (index + 1 === points.length) return;
// MeshLineを作成。2点間のMeshLineを別々に作成する
const geometry = new THREE.BufferGeometry().setFromPoints([point, points[index + 1]]);
const line = new MeshLine();
line.setGeometry(geometry);
+ // 2点間の距離を計算
+ const distance = point.distanceTo(points[index + 1]);
+
+ // MeshLineの頂点数を取得
+ const numVerticesAfter = line.geometry.getAttribute('position').count;
+
+ // 頂点数に基づいて distances 配列を生成しsetAttributeで頂点属性を追加。UV座標のアスペクト比の計算に使用
+ const distances = new Float32Array(numVerticesAfter).fill(distance);
+ line.setAttribute('uDistance', new THREE.BufferAttribute(distances, 1));
+
+ // 頂点数に基づいて directions 配列を生成しsetAttributeで頂点属性を追加。リンクデータの方向を表す
+ const directions = new Float32Array(numVerticesAfter).fill(feature.properties.direction);
+ line.setAttribute('uDirection', new THREE.BufferAttribute(directions, 1));
+ // uniforms変数にuTime(時間)を追加。アニメーションに使用
+ Object.assign(linkMaterial.userData, {
+ uniforms: {
+ uTime: { value: 0 },
+ },
+ });
// MeshLineの配列に追加
const mesh = new THREE.Mesh(line, linkMaterial);
meshLines.push(mesh.geometry);
});
});
/* 省略 */
あとはanimate関数内でuTime
に加算する処理を記述します。
// アニメーション
const animate = () => {
requestAnimationFrame(animate);
/* 省略 */
+ // 歩行者ネットワークのアニメーション
+ if (linkMaterial.uniforms.uTime) {
+ linkMaterial.uniforms.uTime.value += 0.1;
+ }
renderer.render(scene, camera);
};
animate();
地上の可視化
仕上げに地上の部分も可視化します。基盤地図情報の基本項目から新宿駅が含まれている「533945」のメッシュを選択し、地物データをダウンロードします。
ダウンロードした地物データから「道路縁(Road Edge)」と「道路構成線(Road Component)」をGDALコマンドでGeoJsonに変換し、座標系をEPSG:6677に変換します。
ogr2ogr -f GeoJSON -t_srs EPSG:6677 FG-GML-533945-RdEdg-20231001-0001.geojson FG-GML-533945-RdEdg-20231001-0001.xml
ogr2ogr -f GeoJSON -t_srs EPSG:6677 FG-GML-533945-RdCompt-20231001-0001.geojson FG-GML-533945-RdCompt-20231001-0001.xml
変換後にQGISに読み込んで加工します。プロセシングツールで「ベクタレイヤをマージ」で二つのデータをマージし、「バッファ」で中心点([-12035.29, -34261.85])から円ポリゴンを作成します。そのあと「切り抜く」でマージした道路データに対して円ポリゴンをオーバレイレイヤとして切り抜き処理をすることで、円形の道路ラインデータを作成します。
この加工したデータをThree.jsで読み込み、LINEで生成してシーンに追加することで完成となります。
// 基盤地図情報道路データの読み込み;
loader.load('./road.geojson', (data) => {
const material = new THREE.LineBasicMaterial({
color: new THREE.Color('rgb(209, 102, 255)'),
});
data.features.forEach((feature) => {
const coordinates = feature.geometry.coordinates;
const points = coordinates[0].map((point) => {
return new THREE.Vector3(point[0] - center[0], point[1] - center[1], 0);
});
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const matrix = new THREE.Matrix4().makeRotationX(Math.PI / -2);
geometry.applyMatrix4(matrix);
const line = new THREE.Line(geometry, material);
scene.getObjectByName(`group0`).add(line);
});
});
地上面を追加したことにより、歩行者ネットワークがより見やすくなったかと思います。
おわりに
今回は2次元のGISデータを無理やり3次元化したものなので、高さ情報の正確性がないのが難点でした。正確性をもとめた3次元の可視化はPLATEAUのような3次元モデルのデータの使用を推奨します。
G空間情報センターの「3D都市モデル(Project PLATEAU)新宿区(2023年度)」のデータの中に地下街モデルが公開されているようです。
明日は@Kanahiroさんによる記事です!お楽しみに!!