GLTFモデルをNode.js上のヘッドレスなthree.jsで読み込み3Dの計算を行う(レンダリングは行わない)


概要

  • Node.jsで3Dの計算だけしたい
  • 画のレンダリングは不要
  • Raycasterによる当たり判定程度まではできることがわかった、それ以上は未検証

時間がない人向けの内容ざっくり(tl;dr)

  • GLTFLoaderをNode.js上で動くように改変することが必要
    • BufferをUint8Arrayに変換するメソッドの追加
    • それに伴うparseメソッド内でのmagic周辺の改変
  • 改造したGLTFLoader.parseにfs.readFileSyncしたBufferを食わせる
  • callbackの引数にオブジェクトが返るので、通常のGLTFLoaderのときのように、THREE.Sceneにロードできる

動機

シンプルなWeb上で動くオンライン3Dゲームを作りたい。

オンラインなのでサーバが必要。

オンラインかつ3Dなので、3D位置情報が同期的である必要があるだろうと考えた。

またサーバ側でマスタ3D位置情報を持つ必要もあるだろうと考えた。

前提

Web上で動く3Dゲームである以上フロントはThree.jsが楽だろうと考えた。Godot, Unity, Cocos2D などの選択肢もあるがjsのほうが慣れている。個人プロジェクトのため選定は自由。

一方でサーバ側でも3D情報を持つ必要がある。このためサーバ側でもThree.jsを使うと楽だろうと考えた。サーバ側でグラフィックを出す必要はないのでレンダリング等は不要だが、3Dの計算はサーバ側で行える必要がある。

幸いjavascriptは実行時に変数等が評価され解決されるため、windowオブジェクトやXHR、WebGL Rendererなどを呼び出すメソッドにさえ触れなければ、Three.jsのうち単なるjsで書かれている部分は実行環境非依存で動くはずであり、3Dの計算だけを行うことができるはずである。

また、サーバサイドでもゲームの情報である3Dモデルを読み込む必要がある。Three.jsにおいてはGLTFLoaderを用いることが多いのでこちらを用いることにした。

結果

少なくともモデルの読み込みと、Raycasterによる当たり判定などができる。

メインソースファイル(index.js)


const THREE = require('three');
const GLTFLoader = require('./gltf-loader');
const fs = require('fs');

const map = fs.readFileSync('map.glb', {encoding: null});

function init() {
  const scene = new THREE.Scene();

  var loader = new GLTFLoader();
  loader.parse(map, 'map.glb', (gltf)=>{
    scene.add(gltf.scene)
    const raycaster = new THREE.Raycaster(new THREE.Vector3(parseFloat(process.argv[2]), 500, parseFloat(process.argv[3])), new THREE.Vector3(0, -1, 0), 1, 2000);
    const intersects = raycaster.intersectObjects(scene.children, true);
    for ( var i = 0; i < intersects.length; i++ ) {
      console.log(intersects[ i ].distance)
    }
  })
}

init();

map.glb というGLTFファイルを読み込んでいる。Blenderでテストモデルとして作成した。PlaneをSubdivision Surface→Apply→適当に形状変更→Triangulateにて作成している。Export設定はSelected Objectsにしたこと以外デフォルト。以下の画像のような形状をしている。

本index.jsファイルは実行時の第一引数と第二引数をRay位置の水平方向座標に割り当てることで、端的に言えば空中からマップ地形までの距離を測定するサンプルコードとなっている。

GLTFLoaderの改変

GLTFLoaderのソースはここにある。

そのまま用いると動かない。Node.jsに対応させるためにいくつか改変が要る。改変したGLTFLoaderを含めたプロジェクト/プロジェクトソースを配布する場合はライセンスに注意すること。

require構文への変更

import / export構文を前提としたコードになっているので、require構文に変更する。

diff抜粋は以下の通り


1c1
< import {
---
> var {
65c65
< } from "../../../build/three.module.js";
---
> } = require('three');
3665c3677
< export { GLTFLoader };
---
> module.exports = GLTFLoader;

toArrayBufferメソッドの追加

以下を追加する。

こちらを参考にして、len引数だけ追加した。


toArrayBuffer: function(buf, len) {
  len = len || buf.length
  var ab = new ArrayBuffer(len);
  var view = new Uint8Array(ab);
  for (var i = 0; i < len; ++i) {
      view[i] = buf[i];
  }
  return ab;
},

これを追加する理由は、fs.readFileSyncで以下のように読み込んでいるが、こちらがBufferであり、GLTFLoaderがインスタンスタイプが違うとエラーを出すため。

const map = fs.readFileSync('map.glb', {encoding: null});

parseメソッド中でtoArrayBufferメソッドを使用するように変更

以下のdiff抜粋のように変更する。以下の変更によってindex.jsコードは動くようになるはずである。

なお、なるべくdiffが少なくなるようにコードを書いてしまったので、実際にはparseメソッドのdata引数をbufなどにリネームしたりしてdata変数の再宣言を行わない方がコードの治安が良いと思う。そちらは読者の方々の方で適宜やっていただければ幸いである。


234,235c244,247
< 
<                               var magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
---
> 
>                               var magicSrc = this.toArrayBuffer(data, 4);
>                               var data = this.toArrayBuffer(data);
>                               var magic = LoaderUtils.decodeText(magicSrc);
254c266
<                                       content = LoaderUtils.decodeText( new Uint8Array( data ) );
---
>                                       content = LoaderUtils.decodeText( data );