tpc12-1

TOPIC 12|Three.jsで活用する[1/2]|Three.jsを使った3D都市モデルの読み込みと表示

Three.jsは、ブラウザで3D映像を描画する軽量なJavaScriptライブラリです。Three.jsで扱うファイルはglTF形式が推奨されています。glTF形式に変換した3D都市モデルを読み込み、ブラウザに表示する方法を説明します。

Share

TOPIC12:Three.jsで活用する

Three.jsは、ブラウザからPCに搭載されているGPUを使って、ウェブ上で高速に3D描画できるJavaScriptのライブラリです。CesiumやDeck.glなどに比べて、WebGLの機能を直接使える低レイヤーの機能が関数として提供されているのが特徴です。このトピックでは、Three.jsでPLATEAUを活用する方法を説明します。

【目次】

12.1   Three.jsとは

 12.1.1  Three.jsが扱えるファイル

12.2   glTF形式に変換する

12.3   3D都市モデルの基本的な読み込み方

 12.3.1  プロジェクトの構成

 12.3.2  3D都市モデルを表示するプログラム

12.1 _ Three.jsとは

Three.jsは、ブラウザからPCに搭載されているGPUを使って、ウェブ上で高速に3D描画できる高機能なJavaScriptのライブラリです。多様なツールやサービスとも連携して安定稼働することで知られており、JavaScriptのフレームワーク構造に制限されず、少ないオーバーヘッドで自由度の高い開発ができます。

12.1.1 _ Three.jsが扱えるファイル

Three.jsでは、扱うオブジェクトのデータ形式として、glTF(GL Transmission Format)形式を推奨しています(参照:Loading 3D models)。しかしローダーと呼ばれるいくつかのプラグインを使えば、FBX形式、OBJ形式、3D Tiles形式などのファイルも扱えます(参照:Three.jsのローダー)。

このトピックでは、まず、3D都市モデルをglTF形式に変換し、それを扱う基本的な処理方法を説明します。次に応用例として、Reactプロジェクトを作り、3D Tiles形式のデータを扱う方法を説明します。

12.2 _ glTF形式に変換する

まずは、glTF形式を扱う方法から説明します。

glTF形式は、3DモデルやシーンをJSON形式で表現するフォーマットです。Khronos Group 3D Formats Working Groupが仕様化したもので、3Dオブジェクトの相互交換の場面で幅広く使われています。

PLATEAUでは、glTF形式に変換したデータを直接配布していません。そこで、あらかじめ、他のデータ形式から変換します。ここでは3DソフトのBlenderを使って、FBX形式またはOBJ形式の3D都市モデルを読み込み、glTF形式に変換します。

このとき、以降の処理で扱いやすいよう、メッシュの中心が原点になるよう調整しておきます。

【メモ】

原点の移動は必須ではありません。原点位置はそのままにしておき、カメラやライトの位置を調整する方法もあります。

[1]FBX形式もしくはOBJ形式の3D都市モデルを読み込む

Blenderを起動し、[ファイル]―[インポート]メニューから、FBX形式もしくはOBJ形式の3D都市モデルを読み込みます。

例として、G空間情報センターから東京都23区の3D都市モデルの「FBX形式データ」をダウンロードし、東京都庁周辺の建築物が含まれる、3次メッシュコード「53394525」に該当する次のファイルを扱います。ここではLOD1のデータをインポートします(LOD2の場合も同様に扱えますが、後述するオブジェクトの移動操作の負荷が高いので注意してください)。

LOD1の場合:bldg/lod1/53394525_bldg_6677.fbx

LOD2の場合:bldg/lod2/53394525_bldg_6677.fbx

【3D都市モデル(Project PLATEAU)東京都23区】

 https://www.geospatial.jp/ckan/dataset/plateau-tokyo23ku

図12-1 FBXをダウンロードする

FBX形式ファイルを読み込むには、[ファイル]メニューから[インポート]―[FBX(fbx)]を選択します。

読み込む際のオプションは、次のようにします。

・[スケール]を「100」に設定(これで単位がメートルになります)

・[方向を手動で設定]にチェックを付け、[前方]を[Yが前方]、[上]を[Zが上]に設定

【メモ】

さらに別のFBX形式ファイルをインポートすれば、glTF形式にエクスポートする際、それらもひとまとめにできます。

図12-2 FBX形式ファイルをインポートする

[2]メッシュのおおむねの中心を原点に移動する

このままglTF形式にエクスポートしてもかまいませんが、ここでは扱いやすさを考えて、メッシュの中心が原点になるように移動しておきます。

基本的な操作としては、配置した3D都市モデルのオブジェクト全体をマウスでドラッグ操作して、原点近くに移動すればよいのですが、3D都市モデルは複雑で処理に時間がかかり、ドラッグ操作が追いつかず、思うように操作できないこともあるため、以下では、数値入力で移動する方法をとります。

① より遠くまで見えるようにする

左側の3Dビューポートをクリックして選択状態にし、[N]キーを押します。右側にアイテムやツール、ビューを選択できる画面が表示されたら、[ビュー]をクリックし、[範囲の終了]の値を「100000m」に変更します。設定変更を終えたら、もう一度[N]キーを押して閉じます。

図12-3 遠くまで見えるように設定する

② 建物に視点を向ける

[ビュー]メニューから[全てを表示]をクリックします。すると、配置した建物が表示されます。それらをマウスでドラッグして範囲選択して選択し、[ビュー]―[選択をフレームイン](ショートカットはテンキーの[.])をクリックすると、その範囲がフレームいっぱいに収まるよう、視点が調整されます。

図12-4 建物に視点を向ける(1)
図12-5 建物に視点を向ける(2)
図12-6 建物に視点を向ける(3)
図12-7 建物に視点を向ける(4)

③ 中心点を確認する

図12-7で表示した建物をドラッグしてすべて選択します。左上の[オブジェクト]と書かれているドロップダウンリストから[編集モード]を選択して、編集モードに切り替えます。

【メモ】

オブジェクトモードは、それぞれのオブジェクトの位置や回転、スケールを編集するモード、編集モードは、オブジェクトの頂点など、オブジェクト自身を編集するモードです。左上からドロップダウンリストで切り替える以外に、[Tab]キーでも切り替えられます。

図12-8 編集モードに切り替える

そして、おおむね全体の中心にある建築物をクリックして、[N]キーを押します。[アイテム]タブをクリックし、[ローカル]をクリックして、頂点のX座標とY座標を確認します。例えば、(-12535, -34700)などです。確認を終えたら、左上のドロップダウンリストを[オブジェクトモード]に戻しておきます。

【メモ】

ここでは話を簡単にするため、移動すべき座標をBlenderで確認していますが、経緯度をもとに、平面直角座標に変換して計算で求める方法でもかまいません。

図12-9 おおむねの中心座標を確認する

④ X軸方向に移動する

オブジェクトを移動します。[アウトライナー]で、読み込んだ3D都市モデルをすべて選択します。

キーボードの[G]キーを押すと、オブジェクトの移動モードになります。そのままマウスを移動すると、そのぶんだけ移動しますが、ここでは、座標を手入力します。

まずは、X座標を移動します。[X]キーを押し、確認したX座標の数値をマイナスにしたもの(例えば「12535」)を入力します。入力中のコマンドは、画面左上に表示されます。入力中にその座標に移動するので建築物は一時的に見えなくなりますが問題ありません。入力し終えたら、[Enter]キーを押すとその量だけ移動します。

【メモ】

間違えたときは[ESC]キーを押すと、取り消せます。

図12-10 X軸方向に移動する

⑤ Y軸方向に移動する

同様に操作して、Y軸方向に移動します。移動後、ビューから見えなくなってしまっているので、手順②と同じ方法で視点を調整して、見えるようにしておきます。

その状態で、先ほどと同じように、[G]キーを押します。今度は、Y座標を移動したいので、[Y]キーを押し、先ほど確認した中心付近のY座標をマイナスにしたもの(例えば「34700」)を入力し、最後に[Enter]キーを押します。

図12-11 Y軸方向に移動する

⑥ 原点近くに移動したことを確認する

手順②と同様の手順で、全体を確認します。座標軸がおおむね3D都市モデルの中心を通っていることを確認します(図12-12)。

図12-12 原点近くに移動した

[3]glTF形式にエクスポートする

原点の移動設定が終わったら、glTF形式にエクスポートします。エクスポートするには、[ファイル]メニューから[エクスポート]―[glTF2.0(.glb/.gltf))]を選択します。

エクスポートの設定では、フォーマットを[glTFバイナリ(.glb)]とします。どのようなファイル名でもよいですが、ここでは「53394525_bldg_6677.glb」というファイル名にします。

図12-13 glTF形式にエクスポートする
図12-14 名前を付けて保存する

12.3 _ 3D都市モデルの基本的な読み込み方

glTF形式に変換してしまえば、Three.jsの処理は、汎用的な3Dオブジェクトを読み込んで表示するやり方と変わりません。

以下に、glTF形式に変換した3D都市モデルを読み込んで、ブラウザに表示する基本的な方法を示します。

12.3.1 _ プロジェクトの構成

ここでは、(例えばNode.jsなどの)ビルド環境やサーバー側のプログラミングを必要とせず、HTMLファイルといくつかのJavaScriptのライブラリ、そして、glTFファイルをWebサーバーに置くだけで動かせる、シンプルな構成のプロジェクトを考えます。

次のフォルダ構成とします。

  example 
   ├─index.html(以下で作成するプログラムです) 
   ├─js 
   │  ├─three.module.js(Three.jsのサイトからダウンロードしたものを配置します) 
   ├─jsm 
   │  └─loaders(Three.jsのサイトからダウンロードしたものを配置します) 
   │      ├─itf 
   │      ├─lwo 
   │      ├─3DMLoaders.js 
   │      ├─…略… 
   │      ├─GLTFLoader.js 
   │      └─…略… 
   └─gltf 
      └─53394525_bldg_6677.glb(先に変換しておいたglTF形式のファイルです)

[1]プロジェクトのフォルダを作り、Webサーバーを構成する

プロジェクトのフォルダを作ります。例えば「example」とします。

何らかのWebサーバーを構成し、ブラウザでアクセスできるようにします。ApacheやnginxのようなWebサーバーを構築するのでもよいですし、何らかのプログラミング言語のランタイムによる、簡易なWebサーバーでもかまいません。例えば、Node.js環境であるなら、http-serverというモジュールで簡易Webサーバーを作れます(「-g」はグローバル環境にインストールするオプションです)。

npm install http-server -g

上記のようにモジュールをインストールしておき、このフォルダをカレントフォルダにして、次のコマンドを実行すると、Webサーバーとして実行されます(-pはポート番号を指定するオプションです。この場合、http://localhost:8000/で接続できます)。

http-server . -p 8000

Pythonの環境にも同様の機能があり、次のようにします(-mはPythonのモジュールを実行するオプションです)。

python -m http.server

【メモ】

Webサーバーの構成方法についての詳細は、「How to run things locally」を参照してください。自分のPCではなく、ホスティングサービスに置くのでもかまいません。

[2]Three.jsのライブラリをダウンロードする

Three.jsのGitHubのサイトから、Three.jsファイル一式をダウンロードします。下記のリンクをクリックすると、まとめてZIP形式でダウンロードできます。

ダウンロードしたら、適当なフォルダに展開します。

【Three.jsのGitHub】

https://github.com/mrdoob/three.js/

【まとめてダウンロード】

https://github.com/mrdoob/three.js/archive/master.zip

[3]ライブラリのファイルを配置する

[1]で作成したフォルダのなかに、[2]の必要なファイルを配置します。

・build/three.module.jsをjsにコピー

・example/jsm/loadersをjsmにコピー

[4]glTFファイルを配置する

「12.2 glTF形式に変換する」で変換済みのglTF形式ファイルを、gltfフォルダにコピーします。

12.3.2 _ 3D都市モデルを表示するプログラム

リスト12-1に、3D都市モデルを表示するプログラムを示します。ブラウザで参照すると、図12-12のように3D都市モデルが表示されます。マウスのドラッグで見る向きを変えられるようにしています。

<!DOCTYPE html> 
<html> 
  <body> 
    <!-- Three.js本体とGLTFローダー --> 
    <script type="importmap">
    {
      "imports": {
        "three" : "./js/three.module.js",
        "three/loaders/": "./jsm/loaders/"
      }
    }
    </script>
     
    <script  type="module"> 
      import * as THREE from 'three';
      import { GLTFLoader} from 'three/loaders/GLTFLoader.js';
      let mouseX = 0, mouseY = 0; 
      let windowHalfX = window.innerWidth / 2; 
      let windowHalfY = window.innerHeight / 2;  
     
      // シーンを追加 
      const scene = new THREE.Scene(); 
      scene.background = new THREE.Color(0x333333);  
      
      // ライトを追加 
      // 環境光 
      const ambientLight = new THREE.AmbientLight(0xcccccc, 0.5); 
      scene.add(ambientLight); 
      // 太陽光 
      const light = new THREE.DirectionalLight(0xFFFFFF, 1); 
      scene.add(light); 
      // ポイントライト 
      const pointLight = new THREE.PointLight(0xffffff, 0.8); 
      scene.add(pointLight); 
 
      // 1km四方の地面を追加 
      const geometry = new THREE.PlaneGeometry( 1000, 1000 ); 
      const material = new THREE.MeshBasicMaterial( 
        {color: 0x666666, side: THREE.DoubleSide} ); 
      const plane = new THREE.Mesh( geometry, material ); 
      plane.rotation.x = 90 * Math.PI / 180; 
      scene.add( plane ); 
       
      // カメラの追加 
      const camera = new THREE.PerspectiveCamera( 
        45, window.innerWidth / window.innerHeight, 1, 2000); 
      camera.position.z = 500; 
       
      // レンダラーを追加 
      const renderer = new THREE.WebGLRenderer(); 
      renderer.setPixelRatio(window.devicePixelRatio); 
      renderer.setSize(window.innerWidth, window.innerHeight); 
      document.body.appendChild(renderer.domElement);  
       
      // 描画開始 
      animate(); 
       
      // GLTFファイルを読み込む 
      const GLTFFILE = 'gltf/53394525_bldg_6677.glb'; 
      const gltfLoader = new GLTFLoader() 
      gltfLoader.load(GLTFFILE, (gltf) => { 
        scene.add(gltf.scene); 
      }, undefined, (error) => { 
        console.error(error); 
      }); 
       
      // マウスイベントなどを設定 
      document.addEventListener('mousemove', onDocumentMouseMove); 
      window.addEventListener('resize', onWindowResize); 
 
      // 描画処理 
      function animate() { 
        requestAnimationFrame(animate); 
        render(); 
      } 
        
      function render() { 
        camera.position.x += (mouseX - camera.position.x) * .05; 
        camera.position.y += (-mouseY - camera.position.y) * .05; 
        camera.lookAt(new THREE.Vector3(0, 10, 0)); 
        renderer.render(scene, camera); 
      } 
 
      // リサイズ時のカメラ調整 
      function onWindowResize() { 
        windowHalfX = window.innerWidth / 2; 
        windowHalfY = window.innerHeight / 2; 
        camera.aspect = window.innerWidth / window.innerHeight; 
        camera.updateProjectionMatrix(); 
        renderer.setSize(window.innerWidth, window.innerHeight); 
      } 
 
      // マウスが動いたときの座標記録 
      function onDocumentMouseMove(event) { 
        mouseX = (event.clientX - windowHalfX) / 2; 
        mouseY = (event.clientY - windowHalfY) / 2; 
      } 
    </script> 
  </body> 
</html>

リスト12-1 index.html

図12-15 リスト12-1の実行例

■ ライブラリのインポート

まずは冒頭でライブラリをインポートします。three.module.jsが本体ですが、ここではglTF形式の読み込みをするため、追加でGLTFLoader.jsも読み込んでいます。

【メモ】

Three.jsでは、Webpackのようなビルドツールを使った開発を推奨しています。それに伴い、r148版からはモジュール形式ではないサンプルが削除されました。本来は、Webpackのようなビルドツールを使うべきですが、PLATEAUの解説の本質ではないnpmを使った環境構築を避けるため、リスト12-1では、ビルドツールを使わない環境でも使えるES Modulesのimport構文の機能を使った書き方をしています。この書き方については、Three.jsのIntroductionの「Install from CDN or static hosting」の項に記述されています。

<script type="importmap">
{
    "imports": {
        "three" : "./js/three.module.js",
        "three/loaders/": "./jsm/loaders/"
    }
}
</script>
     
<script type="module">
  import * as THREE from 'three';
  import { GLTFLoader} from 'three/loaders/GLTFLoader.js';
・・・略・・・

■ シーンの構築

Three.jsでは「シーン(Scene)」と呼ばれるオブジェクトに、3Dオブジェクトやライト、カメラなどを配置します。まずはシーンを作ります。ここでは背景色も設定しました。

const scene = new THREE.Scene(); 
scene.background = new THREE.Color(0x333333);

次にライトを追加します。環境光、太陽光、ポイントライトを追加しました。すべてが必要というわけではないので、どのように表示したいかに応じて調整してください。

// ライトを追加 
// 環境光 
const ambientLight = new THREE.AmbientLight(0xcccccc, 0.5); 
scene.add(ambientLight); 
// 太陽光 
const light = new THREE.DirectionalLight(0xFFFFFF, 1); 
scene.add(light); 
// ポイントライト 
const pointLight = new THREE.PointLight(0xffffff, 0.8); 
scene.add(pointLight);

これも必須ではありませんが、地面を追加しました。FBX形式は3次メッシュでの分割なので、おおむね1km単位です。そこで1km(1000メートル)の地面を作成して配置しています。

// 1km四方の地面を追加 
const geometry = new THREE.PlaneGeometry( 1000, 1000 ); 
const material = new THREE.MeshBasicMaterial( 
  {color: 0x666666, side: THREE.DoubleSide} ); 
const plane = new THREE.Mesh( geometry, material ); 
plane.rotation.x = 90 * Math.PI / 180; 
scene.add( plane );

そして最後にカメラを配置します。

// カメラの追加 
const camera = new THREE.PerspectiveCamera( 
  45, window.innerWidth / window.innerHeight, 1, 2000); 
camera.position.z = 500;

■ 描画処理

シーンの登録が終わったら、レンダラーを追加して描画できるようにします。次のように、WebGLRendererオブジェクトを作り、その要素をHTMLドキュメントに配置します。すると、この領域に描画されるようになります。

// レンダラーを追加 
const renderer = new THREE.WebGLRenderer(); 
renderer.setPixelRatio(window.devicePixelRatio); 
renderer.setSize(window.innerWidth, window.innerHeight); 
document.body.appendChild(renderer.domElement);

実際の描画処理は、animate関数以下に記述しています。

animate();

animate関数では、フレームが更新されるときに、自身を呼び出すように構成しています。

function animate() { 
  requestAnimationFrame(animate); 
  render(); 
}

このrender関数では、マウスの現在位置に応じてカメラの位置を調整し、レンダリングするようにしました。ここでは解説を省きますが、変数mouseXと変数mouseYは、マウスの移動(mousemoveイベント)でのマウス座標を設定するようにしています。

function render() { 
  camera.position.x += (mouseX - camera.position.x) * .05; 
  camera.position.y += (-mouseY - camera.position.y) * .05; 
  camera.lookAt(new THREE.Vector3(0, 10, 0)); 
  renderer.render(scene, camera); 
}

■ glTFの読み込み

glTF形式に変換した3D都市モデルは、次のように読み込んで、シーンに配置しています。

// GLTFファイルを読み込む 
const GLTFFILE = 'gltf/53394525_bldg_6677.glb'; 
const gltfLoader = new GLTFLoader() 
gltfLoader.load(GLTFFILE, (gltf) => { 
  scene.add(gltf.scene); 
}, undefined, (error) => { 
  console.error(error); 
});

loadメソッドを呼び出すと、読み込みが完了したときに、第1引数に指定した関数が呼び出されるので、ここで読み込んだ3D都市モデルをシーンへと追加しています。

【文】

大澤文孝

【監修】

林 久純(ベースドラム株式会社)