babylon-vrm-loader を作る際に参考にしたこと、注意したことをまとめておきます。
そもそも babylon.js のサードパーティパッケージ自体がほぼなく、参考資料なしで作る必要がありましたので、この手法を参考にして babylon.js の拡張パッケージを作ってくれる方がいれば嬉しい限りです。
webpack の設定は three.js など他のライブラリでも参考になるかもしれません。
ゴール
-
npm install @babylonjs/core babylon-vrm-loader
で npm 経由で読み込めること - npm, webpack を利用せずブラウザだけでも読み込めること
二種類の出力を用意する必要がありますね。
前提
- TypeScript で作ります
- webpack でビルドします
これは元の babylon.js を踏襲するためなので、拡張パッケージは別の方法で作成しても構いません。あくまで一例です。
デバッグ用のページを作る
何はともあれブラウザで表示が確認出来ないと拡張のしようがありません。最初にテスト用ページを作成することにしました。
# 依存関係を用意する
$ yarn init -y
$ yarn add --dev @babylonjs/core@^4.0.0 @babylonjs/loaders@^4.0.0 @babylonjs/inspector@^4.0.0 ts-loader typescript webpack webpack-cli webpack-dev-server webpack-merge
dependencies
と peerDependencies
@babylonjs/core
パッケージが babylon.js のものですが、ここでは dependencies
ではなく devDependencies
に記載しています。
今回作る新しいパッケージは @babylonjs/core
を依存関係として要求しますが、 dependencies
に記載してしまうと npm install @babylonjs/core babylon-vrm-loader
した時に、 下記のような構成になってしまいます。
- new-package/
- node_modules/
- @babylonjs/
- core/
- ...
- babylon-vrm-loader/
- node_modules/
- @babylonjs/
- core/
- ...
この状態になると、 new-package
のビルド時に TypeScript で型エラーが発生する場合があります。
babylon-vrm-loader
は 自身を必要とするパッケージ(ここでは new-package
)と同じ(peer)依存関係が必要である だけなので、 dependencies
ではなく peerDependencies
に記述します。
$ yarn add --peer @babylonjs/core @babylonjs/loaders
こうすることで依存関係ツリーが正常になります。
- new-package/
- node_modules/
- @babylonjs/
- core/
- ...
- babylon-vrm-loader/
- node_modules/
- ここには何も入らない!
peerDependencies
に記述すると、 npm install babylon-vrm-loader
しただけでは WARNING 警告が出るだけで @babylonjs/core
はインストールされません。ビルド時に依存関係が見つからないエラーになります。
開発中はインストールされていてほしいので、 devDependencies
にも追加する必要があります。
webpack-dev-server で開発環境構築
webpack-dev-server パッケージを利用すれば、 webpack による開発環境を簡単に構築することが出来ます。
まず適当に test/index.html を作ります。
<!DOCTYPE html>
<html>
<head>
<title>Babylon.js VRM Loader Test Sandbox</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
body {
overflow: hidden;
}
# wrapper {
width: 100%;
height: 100%;
}
# main-canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<main id="wrapper">
<canvas id="main-canvas"></canvas>
</main>
<script src="vendors~main.js"></script><!-- 後述・ほとんど変わらない依存関係部分を別にして、リビルドを早くします -->
<script src="main.js"></script>
</body>
</html>
エントリポイントとなる ts ファイルを書きます。
import { Engine } from '@babylonjs/core/Engines/engine';
import { Scene } from '@babylonjs/core/scene';
// scene.createDefaultEnvironment を呼ぶために import する必要がある
import '@babylonjs/core/Helpers/sceneHelpers';
async function main() {
const canvas = document.getElementById('main-canvas') as HTMLCanvasElement;
const engine = new Engine(
canvas,
true,
);
const scene = new Scene(engine);
scene.createDefaultEnvironment({
createGround: true,
createSkybox: true,
});
engine.runRenderLoop(() => {
scene.render();
});
window.addEventListener('resize', () => {
engine.resize();
});
// デバッグ用にグローバル変数に露出させる
(window as any).currentScene = scene;
}
main().catch((reason) => {
console.error(reason);
});
デバッグ用の webpack コンフィグを書きます。
const resolve = require('path').resolve;
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: resolve(__dirname, 'src', 'test', 'index'), // エントリポイントを src/test/index.ts にします
output: {
library: 'babylon-vrm-loader',
libraryTarget: 'umd', // 一般的な umd 形式で出力します
filename: '[name].js',
path: resolve(__dirname, 'test'), // 出力先を test ディレクトリにします
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
},
],
},
resolve: {
modules: [resolve(__dirname, 'node_modules')],
extensions: ['.js', '.ts'],
},
devServer: {
contentBase: resolve(__dirname, 'test'), // test 以下を開発HTTPサーバとします
port: 8080,
},
optimization: {
splitChunks: {
chunks: 'all', // この設定で `vendors~main.js` が生成されます
},
},
target: 'web',
};
この状態で $ yarn webpack-dev-server --config webpack.test.config.js
を実行すると、 http://localhost:8080 にシーンが表示されるようになります。
後は、エントリポイントを起点に新しい Material を作ったり、自分なりの拡張をしていけば OK です。
拡張した新しいクラスは src/
直下において、 src/index.ts
で export * from './some-material.ts';
のような形で全てのファイルを export するようにすると、ビルドが楽になります。
公開用の webpack コンフィグを作る
babylon.js は Ver.4 から ESModules 形式に対応したので、 Tree Shaking を意識して書くことが出来ます。必要な依存関係だけを含められるという webpack の機能です。
自分がビルドしたファイルの中に、依存である @babylonjs
のコードは入れたくないので、 webpack の externals 機能を使って依存部分をビルドに含めないようにします。
const resolve = require('path').resolve;
const merge = require('webpack-merge');
// Web版・npm版両方同じの基本設定
const baseConfig = {
mode: 'production',
entry: resolve(__dirname, 'src', 'index'), // エントリポイントを src/index.ts に指定
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
},
],
},
resolve: {
modules: [resolve(__dirname, 'node_modules')],
extensions: ['.js', '.ts'],
},
target: 'web',
};
// 配列を export して二種類ビルドする
module.exports = [
/**
* UMD 形式で npm 用のビルド
*/
merge(baseConfig, {
output: {
library: 'babylon-vrm-loader', // パッケージ名
libraryTarget: 'umd',
filename: 'index.module.js', // 出力するファイル名
path: resolve(__dirname, 'dist'), // 出力先(gitignore しておきます)
},
externals: [
/^@babylonjs.*$/, // この正規表現に引っかかる依存は全てビルドの対象外になります
],
}),
/**
* ブラウザ用のビルド
*/
merge(baseConfig, {
output: {
library: 'VRMLoader', // `window.VRMLoader` としてアクセス出来るようになります
libraryTarget: 'window', // `window` グローバル変数に代入されるタイプです。
filename: 'index.js',
path: resolve(__dirname, 'dist'),
},
externals: [
// ここが一番難しい所です。 externals に関数を渡し、場合に応じて「どのグローバル変数を利用するか」をわけています。
function (context, request, callback) {
// `import ... from '@babylonjs/core`; している所は `BABYLON` グローバル変数から取得する
if (/^@babylonjs\/core.*$/.test(request)) {
return callback(null, 'var BABYLON');
}
// `import ... from '@babylonjs/loaders/glTF/2.0`; している所は `LOADERS.GLTF2` グローバル変数から取得する
// @see https://github.com/BabylonJS/Babylon.js/blob/master/Tools/Config/config.json#L415
if (/^@babylonjs\/loaders\/glTF\/2\.0.*$/.test(request)) {
return callback(null, 'var LOADERS.GLTF2');
}
// `import ... from '@babylonjs/loaders`; している所は `LOADERS` グローバル変数から取得する
if (/^@babylonjs\/loaders.*$/.test(request)) {
return callback(null, 'var LOADERS');
}
callback();
},
],
}),
];
ブラウザ用依存解決設定
注意したいのが最後の「ブラウザ用のビルド」時の externals
の設定です。
ブラウザで babylon-vrm-loader
を利用する場合は、下記のように script
タグを構築します。
<script src="https://preview.babylonjs.com/babylon.max.js"></script>
<script src="https://unpkg.com/babylon-vrm-loader/dist/index.js"></script>
つまり、 require
だとか import
だとかいった構文は通常のビルドだけでは利用できません。
そこで、 babylon.js がグローバル変数として出力する値で依存を解決するように書き換えてやる必要があります。
上記コードによって、例えば import ... from '@babylonjs/core';
と書いた部分は、 '@babylonjs/core'
の代わりに BABYLON
グローバル変数から依存クラスを取得します。
このあたりは Externals | webpack に詳細が書かれているのですが、いまいちパッとしない説明だったので色々試行錯誤して結果上記になりました。パクってください。
パッケージング
後は npm にビルドしたファイルを登録して終わりです。
今回は semantic-release/npm パッケージを利用して自動化しています。
このパッケージに関しては、 semantic-release + git-cz でライブラリを良い感じに Semantic Versioning する | ryonkmr.com を参考にしました。
一度構築してしまえばあとは自動で CHANGE LOG の発行、 GitHub/npm 連携のリリースなどが出来るので便利です。
コミットメッセージを元にバージョンアップするかどうか判定しているので、 git commit
ではなく git cz
を使うという所だけ気を付ける必要があります。
実際のパブリッシュは CircleCI で行っています。これも 1 ファイルで構築でき、無償で利用を始められるので便利に使わせてもらっています。