コンテンツにスキップ

Interior Explore

実行結果

@navaramap/three-pluginsPersonViewPlugin を使って、3D Tiles の建物内をキャラクター操作で探索するアプリケーション開発を学びます。キャラクター操作処理をプラグインを使用することで簡単に実装できます。

このチュートリアルで学べること:

  • 3D Tiles 建物モデルを読み込む
  • PersonViewPlugin で GLTF キャラクターを操作する
  • 地下や建物内をキャラクターに歩かせる
  • 三人称視点と一人称視点を切り替える
  • シーン変更時にキャラクターをテレポートさせる

基本シーンをセットアップする

Section titled “基本シーンをセットアップする”

影は ThreeView の構築時に有効化する必要があります。shadow はコンストラクタで一度だけ読まれ、後から有効にはできません。

import ThreeView, { Color } from "@navaramap/three";
import {
DefaultPlugin,
type DefaultDescriptions,
} from "@navaramap/three-default-plugin";
import { PersonViewPlugin } from "@navaramap/three-plugins";
const plugin = new DefaultPlugin();
const view = new ThreeView<DefaultDescriptions>({
shadow: true,
backgroundColor: new Color().setStyle("#475668"),
});
view.addPlugin(plugin);

PersonViewPlugin も他のプラグインと同様、view.init() の前に登録します。

プラグインがキャラクター(モデル・アニメーション・移動)とカメラを自動的に扱います。minAlt を負の値にすると地下にも降りられます。建物内に収まるよう、cameraDistance は小さめに設定します。

const startLat = 35.6341630282;
const startLng = 139.7420527162;
const startHeight = 59.05;
const startHeading = 288;
const personView = new PersonViewPlugin({
character: {
modelUrl: "/Soldier.glb",
animation: {
idleClip: "Idle",
walkClip: "Walk",
dashClip: "Run",
speed: 1.0,
crossfadeDuration: 0.3,
},
modelRotationOffset: { x: Math.PI / 2, y: 0, z: 0 },
modelScale: 1,
castShadow: true,
receiveShadow: true,
},
moveSpeed: 5,
altSpeed: 5,
rotationSpeed: 2,
cameraDistance: 8,
cameraPitch: 3.4,
cameraLerpSpeed: 4,
minAlt: -1000,
maxAlt: 5000,
startLat,
startLng,
startHeight,
startHeading,
allowCameraControl: true,
});
view.addPlugin(personView);
await view.init();
view.atmosphere.date.setHours(8);
view.toneMappingExposure = 10;
const layers = plugin.addDefaultPhotorealScene();
layers.sun.update({
sun: { castShadow: true, shadowFar: 1000, shadowLambda: 1 },
});

sun.shadowFar の既定値は 50km で、シャドウカスケードが広がりすぎて人物サイズの影は解像されません。shadowFarをキャラクターが歩く範囲まで寄せると、キャラクターの影が見えるようになります。

探索エリアの地形と衛星写真タイルを追加します。建物の地下部分を可視化するため、地形のスカートは無効化しておきます。

const terrainSource = view.addSource({
type: "quantized-mesh",
url: "https://terrain.reearth.land/cesium-mesh/ellipsoid/{z}/{x}/{y}.terrain",
requestVertexNormals: true,
maxZoom: 18,
});
view.addLayer({
type: "terrain",
source: terrainSource,
terrain: {
castShadow: true,
receiveShadow: true,
skirt: false,
},
});
const photoSource = view.addSource({
type: "raster-tile",
// Credit:
// - Geospatial Information Authority of Japan Tiles - Latest Nationwide Photo (Seamless)
// https://maps.gsi.go.jp/development/ichiran.html
url: "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg",
maxZoom: 18,
});
view.addLayer({
type: "raster",
source: photoSource,
});

PLATEAU などの Cesium 3D Tiles 建物モデルを読み込みます。室内の空気感を表現するため、影の設定を有効化しておきます。

const buildingSource = view.addSource({
type: "3d-tiles",
// Credit:
// - [UC23-11] Advanced Area Management Using Storytelling GIS - MLIT PLATEAU
// https://www.geospatial.jp/ckan/dataset/plateau-uc23-11
url: "https://assets.cms.plateau.reearth.io/assets/c1/28f9ff-e9d0-44df-b092-88ac7ebdfa42/tngw_4gaiku/tileset.json",
});
view.addLayer({
type: "3d-tiles",
source: buildingSource,
model: {
show: true,
castShadow: true,
receiveShadow: true,
},
});

これで GLTF モデルがロードされ、キャラクターとカメラを駆動する毎フレームの更新ループが走り始めます。

personView.start();

これだけでキーボード入力が効くようになり、カメラもキャラクターを追従します。

デフォルトのキー割り当て

キーアクション
W / S前進 / 後退
A / D左旋回 / 右旋回
Arrow Up / Space上昇
Arrow Down / Ctrl下降
Shiftダッシュ(dashClip に切り替わる)
Alt(押下中)カメラのオービット操作
V三人称 / 一人称の切り替え

デフォルトの追従カメラ(TPV)は三人称です。V キーで一人称(FPV)に切り替えると、キャラクターのモデルは自動的に非表示になります。Alt を押している間はカメラを手動操作できますが、注視点はキャラクターのままです。

プラグインは毎フレーム、現在の緯度・経度・高度・方位・速度・視点モードを通知します。HUD やミニマップなどの UI を駆動するときは onStateChange() を購読してください。

const unsubscribe = personView.onStateChange((state) => {
console.log(state.lat, state.lng, state.alt, state.heading, state.mode);
});
// 後で購読解除する
unsubscribe();

teleport({ lng, lat, alt, heading? }) を使うと、メニューから別の建物を選んだ時などにキャラクターを瞬時に移動させられます。その場で向きだけを変えるには setHeading() を、カメラのピッチを調整するには setCameraPitch() / setFpvPitch() を使用します。

personView.teleport({ lng: 139.7397, lat: 35.6352, alt: 84 });

追従カメラも新しい位置にスナップし、状態リスナーが新しい位置で 1 回発火します。

入力・キャラクター・アニメーション・カメラをすべてプラグインが担うため、アプリ側のコードは短く保てます。

import ThreeView, { Color } from "@navaramap/three";
import {
DefaultPlugin,
type DefaultDescriptions,
} from "@navaramap/three-default-plugin";
import { PersonViewPlugin } from "@navaramap/three-plugins";
const plugin = new DefaultPlugin();
const view = new ThreeView<DefaultDescriptions>({
shadow: true,
backgroundColor: new Color().setStyle("#475668"),
});
const startLat = 35.6341630282;
const startLng = 139.7420527162;
const startHeight = 59.05;
const startHeading = 288;
const personView = new PersonViewPlugin({
character: {
modelUrl: "/Soldier.glb",
animation: {
idleClip: "Idle",
walkClip: "Walk",
dashClip: "Run",
speed: 1.0,
crossfadeDuration: 0.3,
},
modelRotationOffset: { x: Math.PI / 2, y: 0, z: 0 },
modelScale: 1,
castShadow: true,
receiveShadow: true,
},
moveSpeed: 5,
altSpeed: 5,
rotationSpeed: 2,
cameraDistance: 8,
cameraPitch: 3.4,
cameraLerpSpeed: 4,
minAlt: -1000,
maxAlt: 5000,
startLat,
startLng,
startHeight,
startHeading,
allowCameraControl: true,
});
view.addPlugin(plugin);
view.addPlugin(personView);
await view.init();
view.atmosphere.date.setHours(8);
view.toneMappingExposure = 10;
const layers = plugin.addDefaultPhotorealScene();
layers.sun.update({
sun: { castShadow: true, shadowFar: 1000, shadowLambda: 1 },
});
const terrainSource = view.addSource({
type: "quantized-mesh",
url: "https://terrain.reearth.land/cesium-mesh/ellipsoid/{z}/{x}/{y}.terrain",
requestVertexNormals: true,
maxZoom: 18,
});
view.addLayer({
type: "terrain",
source: terrainSource,
terrain: {
castShadow: true,
receiveShadow: true,
skirt: false,
},
});
const photoSource = view.addSource({
type: "raster-tile",
// Credit:
// - Geospatial Information Authority of Japan Tiles - Latest Nationwide Photo (Seamless)
// https://maps.gsi.go.jp/development/ichiran.html
url: "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg",
maxZoom: 18,
});
view.addLayer({
type: "raster",
source: photoSource,
});
const buildingSource = view.addSource({
type: "3d-tiles",
// Credit:
// - [UC23-11] Advanced Area Management Using Storytelling GIS - MLIT PLATEAU
// https://www.geospatial.jp/ckan/dataset/plateau-uc23-11
url: "https://assets.cms.plateau.reearth.io/assets/c1/28f9ff-e9d0-44df-b092-88ac7ebdfa42/tngw_4gaiku/tileset.json",
});
view.addLayer({
type: "3d-tiles",
source: buildingSource,
model: {
show: true,
castShadow: true,
receiveShadow: true,
},
});
personView.start();