【three-vrm】せっかくなのでTypeScript+webpack+three.jsに入門して、Web上でVRMモデルを表示してみる

どうも、にー兄さんです。
先月のVRoid公式ツイッターで、three.js用のVRMローダーが発表されましたね！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Webアプリケーションで3Dアバターモデルを簡単に扱える『pixiv three-vrm』をOSSとして公開しました！3Dモデルの専門知識がない開発者の方でも「VRM」ファイルをより簡単に扱うことができます！ <a href="https://twitter.com/hashtag/VRoid?src=hash&amp;ref_src=twsrc%5Etfw">#VRoid</a><br><br>詳細: <a href="https://t.co/psBwTYe3cy">https://t.co/psBwTYe3cy</a><br><br>GitHubリポジトリ: <a href="https://t.co/j9IU2h2Zax">https://t.co/j9IU2h2Zax</a> <a href="https://t.co/Fsp2VLbpkW">pic.twitter.com/Fsp2VLbpkW</a></p>&mdash; VRoid公式 (@vroid_pixiv) <a href="https://twitter.com/vroid_pixiv/status/1172018149266313217?ref_src=twsrc%5Etfw">September 12, 2019</a></blockquote> 

Web上でVRMモデルが動かせるということで、さっそく使ってみたいと思いました。

「あれ、そういえばthree.jsって使ったことないな」
「jsのプロジェクトだったら、最近TypeScriptちょっと気になるからやってみたいな」
「クライアントjsにコンパイルしてもimport文使えないのか、webpack？使わなきゃいけないらしい」

「 **じゃあまとめて入門するか😇😇😇** 」

ということで、**ノンフレームワーク** の **クライアントts** でVRMモデルをWebで表示してみます。

ちなみにこんな感じのものを簡単に作れました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Web上でうちの子をぐりぐり見回すことができるやつできた！<br>TypeScript+Webpack環境整えるところから始まって三日くらいか？<br>three.jsとpixivさんが最近出したthree-vrmで実装してある<br>ちゃんとこっちに顔を向けてくれるのが良いんだよなぁ(限界)<a href="https://t.co/DJdAL2mgB7">https://t.co/DJdAL2mgB7</a> で見れるのでぜひ！ <a href="https://t.co/2SngmdYUWc">pic.twitter.com/2SngmdYUWc</a></p>&mdash; にー兄さん (@ninisan_drumath) <a href="https://twitter.com/ninisan_drumath/status/1174705148280442883?ref_src=twsrc%5Etfw">September 19, 2019</a></blockquote> 

**追記**
サンプルプロジェクトのURLを[こちら](https://drumath2237.github.io/three-vrm-test/)に変えました。

## 対象の読者層

今回の記事は突発的に試したことをまとめるので、情報が錯綜しがちになると思います。
とりあえずこの記事を読むにあたってどの程度の知識量が必要か、そしてこの記事を読むと何ができるのかをまとめます。
「自分はちょっと違うな」とか「記事のレベル低いな」など思われた方はブラウザバックをお願いいたします。
実際「入門してみた」記事なので入門者の知識量で書きますが、知識不足により理解しにくい文章になってしまうかもしれません。
以上のことを踏まえて、最後まで読んでいただけると幸いです。

### 要求する知識/経験

- npm/yarnを触ったことがある
- クライアントjs書いたことがある
- VRMというものに興味がある(VRMはいいぞ)
- Chromeが最高のブラウザだと思っている(異論は認める)
- 型は至高、null安全に命を懸けている
- 肩にちっちゃいGPU載せてんのかーい(任意)
- VRM形式の”うちの子”がいる(任意)

### 得られる知見など

- typescriptが書けるようになった気になれる
- webpackがわかった気になれる
- ES2015とかCommonJSとかがワカランくなる
- three.jsがちょっとわかったりわからなかったりする
- **three-vrmでvrmモデルの表示/制御ができるようになる**
- pixivを神と崇めるようになる

# 環境構築/設定

今回開発に使った環境を書き下します。

### デバッグ環境

- windows 10
- CPU: Core-i7 7700
- GPU: GeForce GTX1060
- Visual Studio Code
- Google Chrome

### npmパッケージ

- typescript
- webpack
- webpack-cli
- webpack-dev-server
- three.js
- @type/three
- @pixiv/vrm-three

### packageインストール

```bash
yarn add typescript

yarn add --dev ts-loader webpack webpack-cli webpack-dev-server

yarn add three @types/three @pixiv/three-vrm
```

これによって得られる `package.json` は以下の通りです。
この記事では基本的にこれ以上のパッケージはインストしませんのでこれが最終形態です。

```json:package.json
{
  "name": "qiita-writingthree-vrm",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "ts-loader": "^6.1.2",
    "webpack": "^4.41.0",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "@pixiv/three-vrm": "^0.2.0",
    "@types/three": "^0.103.2",
    "three": "^0.108.0",
    "typescript": "^3.6.3"
  },
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server"
  }
}
```

### ディレクトリ

以下のようなディレクトリ構造で開発をしていきます。

```
/
├─ dist/
│  ├─ models/
│  │  └─ shino.vrm
│  ├─ index.html
│  └─ main.js
├─ src/
│  └─ index.ts
├─ node_modules/
├─ .gitignore
├─ package.json
├─ tsconfig.json
├─ webpack.config.js
└─ yarn.lock
```

- `src/`配下の`index.ts`にプログラムを書いていきます。
- webpackでビルドされたjsファイルは`dist/`配下の`main.js`として生成されます
- `dist/index.html`で`main.js`を読み込み、WebGLでレンダリングします
- `dist/models/`配下にはvrmモデルを配置して置き、`GLTFLoader`でファイル名を指定します。
    - 今回は[千駄ヶ谷篠ちゃんのモデル](https://hub.vroid.com/characters/5860098757548846785/models/6567311261748429976)を使っています。名前が長いので`shino`という名前にしていますが

### config系

今回のプロジェクトでは`tsconfig.json`と`webpack.config.js`という二つのconfigファイルを扱います。
それぞれ

```bash
# tsconfigの生成
tsc --init

# webpack.configの生成
webpack init
```
のように自動生成できますが、私が作ったやつのコピペでいいと思います。

以下にまとめます。

#### tsconfig.js

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "es6",
    "lib": [
      "dom",
      "es2019"
    ],
    "sourceMap": true,
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "allowJs": true
  }
}
```

three.jsが`es2019`の文法を採用しているところがあるらしいので、`lib`属性に追加してあげましょう。

#### webpack.config.js

```js:webpack.config.js
const path = require('path');

module.exports = {
	mode: 'development',
	entry: './src/index.ts',

	output: {
		filename: 'main.js',
		path: path.resolve(__dirname, 'dist')
	},

	module: {
		rules: [
			{
				test: /.(ts|tsx)?$/,
				loader: 'ts-loader',
				include: [path.resolve(__dirname, 'src')],
				exclude: [/node_modules/]
			}
		]
	},

	devServer: {
		open: true,
		openPage: "index.html",
		contentBase: path.join(__dirname, "dist"),
		watchContentBase: true,
		port: 8080
	},

	resolve: {
		extensions: ['.tsx', '.ts', '.js'],
		modules: ["node_modules"]
	}
};
```

これらのconfig系は[こちらのサイト](https://ics.media/entry/16329/)を参考にさせていただきました。

#### <font color="red"> ※もしコマンドからconfigファイルをinitするときの注意</font>

あくまで筆者の環境で起きたことなのですが、もしかすると同じことが起きる人がいるかもしれないので一応書いておきます。

##### `tsc --init`でなぜか`init`オプションがないといわれる

グローバルインストールされたtsでなぜか`init`が動かない場合は、`C:\Program Files (x86)\Microsoft SDKs\TypeScript\xx\`が環境パスに設定されているか確認してみてください。
これなんなんですかね？Visual StudioのコンポーネントとしてTypeScriptを入れたりすると設定されるのでしょうか？とにかくコイツにはinitオプションがアクティベートされてないらしいので、パスの設定を外してしまって構わないでしょう。
別の記事を見ていると、普通にtsをインストしなおすと実行できる例もあるみたいです。

##### `webpack init`でけられる

`webpack init`を実行すると、`@webpack/init`という別のpackageが実行されるみたいで、このpackageがグローバルインストールされていないと実行されないみたいです。
実際`webpack init`すると`yarn global add`コマンドが走るのですがこいつが **罠** ですね（）。
そもそもyarnとnpmではpackageがグローバルインストールされる場所が違うので、実行時にnpmの場所を参照すると当然そこに`@webpack/init`はないためエラーになるみたいです。
結論としては、 **事前に`@webpack/init`をnpmでグローバルインストールしておく** なんですかね...
もっとスマートな解決方法があったら教えていただきたいです。助けて任意のプロ...。
jsに詳しい友人に聞いたところ、「グローバルインストールするときはyarnじゃなくてnpm使ってる、遅いけどね」だそうです。

# れっつ　こーでぃんっ

さて前置きが長くなりましたが、ここからコードを書いていきます。

### Hello,Worldを書いてビルドしてみる

前述したディレクトリ構造に基づいてファイルを作成していきます。
まずは `/src/index.ts` を作成して、以下のようなコードを書きましょう。

```ts:index.ts
window.addEventListener("DOMContentLoaded", ()=> {
  console.log("Hello, World!");
})
```

そうしたら、次に

```bash
yarn build
```

とコマンドを打って、 `/dist/main.js` をビルドします。
正しくmain.jsが作成されていることを確認したら、`/dist/index.html`を作成し、以下のように記述します。

```html:index.html
<html>
  <head>
    <title>three-vrm test</title>
    <script src="./main.js"></script>
  </head>
  <body>
    
  </body>
</html>
```

まぁ普通に`main.js`を読み込んでいるだけなんですけどねw
このまま`index.html`を開いてもいいのですが、せっかくなので`webpack-dev-server`を使っていきましょう。以下のようにコマンドを打ってください。

```bash
yarn start
```

すると、ブラウザが立ち上がり真っ白なページが出てくると思います。ちゃんとログが出ているか確かめたいので`F12`キーで開発者ツールを開くと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86191/c16be525-ee5b-8707-b752-97835816353d.png)

ちゃんと出ているみたいですね。

### three-vrmでVRMモデルを読み込んでレンダリングする

それでは、いよいよthree-vrmを使ってVRMモデルをレンダリングしていきましょう。
コードを記述する前に`/dist/models`の中に任意のVRMモデルを配置しておくのを忘れないようにしてください。前述した通り、本記事では`shino.vrm`というVRMモデルを読み込みます。

VRMモデルが配置されているのを確認したら、index.tsに以下のようにごりっと書いていきます（）

```ts:index.ts
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM } from '@pixiv/three-vrm'

window.addEventListener("DOMContentLoaded", ()=> {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth/window.innerHeight,
    0.1,
    1000
  );
  camera.position.set(0,1,3);

  const renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(0x000000);
  document.body.appendChild(renderer.domElement);

  const light = new THREE.DirectionalLight(0xffffff);
  light.position.set(1,1,1).normalize();
  scene.add(light);

  const loader = new GLTFLoader();
  loader.load(
    './models/shino.vrm',

    (gltf) => {
      VRM.from(gltf).then( (vrm) => {
        scene.add(vrm.scene);
        vrm.scene.rotation.y = Math.PI;
      })
    }
  )

  const update = () => {
    requestAnimationFrame(update);    
    renderer.render(scene, camera);
  };
  update();
})
```

結果を見てみましょう(ちなみにwebpack-dev-serverはbrowser-syncが有効なので、記述した内容をリアルタイムで反映してくれるので助かります)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86191/5f094189-eb4d-b9c7-edce-a1ecc1ee64a9.png)

ちゃんと描画できているようですね。

#### 解説
それでは今回のプログラムの主要な部分を解説していきたいと思います。

まず冒頭の3行の`import`について

```ts
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM } from '@pixiv/three-vrm'
```
まぁ見ればわかる通りなのですが、`THREE`、`GLTFLoader`、`VRM`という名前でそれぞれthree.jsやthree-vrmのAPIにアクセスできるようにインポートしています。`GLTFLoader`というのはVRMファイルをインポートするために使うローダーです(詳しくは後述)。

```ts
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth/window.innerHeight,
    0.1,
    1000
  );
  camera.position.set(0,1,3);

  const renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(0x000000);
  document.body.appendChild(renderer.domElement);

  const light = new THREE.DirectionalLight(0xffffff);
  light.position.set(1,1,1).normalize();
  scene.add(light);
```

この部分に関してはthree.jsの基本的な使い方になるのであまり詳しくは解説しません。Web上に3D空間をレンダリングするためには、

- レンダラー
- カメラ
- シーン
    - ライト
    - 3Dオブジェクトなど

が必要です。
カメラとは、3D空間に光がどのように届くのかを記述した撮影機オブジェクトのことで、これによって記述されたようにcanvasにシーンが映ることになります。
シーンとは、3Dあるオブジェクトなどをまとめたものです。`scene.add(obj)`というメソッドによって光源や3Dオブジェクトなどがシーンに含まれるようになります。
最後にレンダラーとはシーンとカメラを用いてシーンの情報を描画するための機構です。`renderer.render(scene,camera)`というメソッドによって一気に光の挙動が計算され、canvasにレンダリングされます。

さて最後にVRMをロードしてシーンに追加するまでの機構を見ていきます。

```ts
  const loader = new GLTFLoader();
  loader.load(
    './models/shino.vrm',

    (gltf) => {
      VRM.from(gltf).then( (vrm) => {
        scene.add(vrm.scene);
        vrm.scene.rotation.y = Math.PI;
      })
    }
  )
```

loaderはGLTFLoaderオブジェクトをインスタンス化したものになります。
loaderは`load`メソッドによってGLTFファイルを読み込むための機構です。loadメソッドは第一引数に`参照するファイル`、第二引数に`gltfファイルを渡したクロージャ`をとります。
この時点で帰ってくるのはGLTFファイルなのですが、three-vrmによってVRMファイルとして扱うためにgltf->vrmという変換を行います。


#### コラム：webpack-dev-serverを使う理由

余談なのですが、このGLTFLoaderではファイルをロードするときにXMLHttpRequestが発生しますね、そうなるとローカル環境で普通に実行するとCORSでモデルがうまくロードできません。Chromeであればセキュリティを無効にすれば解決しますが、開発時だけ設定するのも面倒ですので、ローカルサーバーを立てるのがベストです。
webpack-dev-serverを使うことで新たにサーバを立てるためにバックエンドを記述したり新たなコンフィグでプロジェクトが汚れることなく、コマンド一つでモックサーバーが建ちます。便利ですね。

#### コラム：three-vrm登場以前の描画方法

three-vrmが登場以前は、three-vrm同様、GLTFLoaderを拡張することでVRMインポータを自作する例がいろいろな記事で散見されています。
Qiitaでもthree.jsでモデルを動かすための記事が結構ありますね。

- [VRMのモデルをthree.jsで動かしてみた](https://qiita.com/TakenokoTech/items/b3395d8fb26cf3237f15)
- [ブラウザだけでVtuber収録 - Teraconnectを支える技術](https://qiita.com/superdog/items/ca8ac65d414bde6cbb99)
- [虚無なXmasをARで彩る](https://qiita.com/JPNYKW/items/82b965465622e149a875)

などなど

#### コラム：そもそもVRMとは？

VRMとは

> VR向け3Dアバターファイルフォーマット

である、という風にVRMの公式[リファレンスページ](https://vrm.dev/)に書いてあります。
3Dデータのフォーマットといえば、例えばシンプルな構造のOBJファイルやUnityで使うことが多いFXBファイル、3Dモデリングツールなどで多く使われるCOLLADA(.dae)ファイル、3Dキャラクターを躍らせるためのMMDファイルなどなど...いろいろなフォーマットがあり、それぞれに用途があります。
その中でもVRMというフォーマットは既存のGLTFという、JSONで記述された3Dファイルフォーマットを拡張したものになります。
GLTFがJSON構造であるため、Webとの親和性が高いのが特徴の一つですが、ほかにもGLTFから拡張された特徴がいくつかあります。

- ライセンスを記述できる
- Unity用のインポータ、UniVRMがある
- 独自のシェーダやスプリングボーンの実装がある
- 複数のVR/配信プラットフォームで扱える3Dアバターの共通規格である

などが代表的なところでしょうか？ほかにもたくさんありますが(筆者が勉強不足のため)省略します。

VRMはドワンゴやピクシブなどが参加するVRMコンソーシアムという団体が規定しており、これから需要が高まるであろう(Humanoid)アバターシステムの共通規格として盛んに議論がなされている状態です。
将来的には一人1アバター時代に、「VRMファイルさえ持っていればどこでも配信したりVRSNSで交流できたりする未来」が実現されると思います。アツい！

#### three-vrmのAPIを使ってブレンドシェイプやボーンを制御する

さてただ表示するだけでもアレなので(アレってなんだ)、VRMモデルのポーズを変えたり表情を変えたりしてみます。
これもthree-vrmのおかげで簡単に操作できるようになっています。それこそUnityでHumanoidアバターを触ったことのある方ならなじみ深いかもしれません。

先ほど記述したコードに以下のように追加します。

```ts:index.ts
import * as THREE from 'three'
import {GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// VRMSchemaを追加
import { VRM, VRMSchema } from '@pixiv/three-vrm'

window.addEventListener("DOMContentLoaded", ()=> {

// (中略)

    (gltf) => {
      VRM.from(gltf).then( (vrm) => {
        scene.add(vrm.scene);
        vrm.scene.rotation.y = Math.PI;

        // ここから追加
        vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.LeftUpperArm)!.rotation.x = 0.6;
        vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.LeftLowerArm)!.rotation.x = 0.8;
        vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.LeftLowerArm)!.rotation.y = -1.;
        vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.LeftHand)!.rotation.y = -0.5;
        vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.RightUpperArm)!.rotation.z = -1.3;

        vrm.blendShapeProxy!.setValue(VRMSchema.BlendShapePresetName.Fun, .7);
        vrm.blendShapeProxy!.setValue(VRMSchema.BlendShapePresetName.Sorrow, .2);
        vrm.blendShapeProxy!.update();
        // ここまで追加
        
      })
    }
  )
// (中略)
```

実行するとこんな感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86191/08e27085-54f2-cb93-f0b3-cd369f508fe5.png)


ここでやっていることは、vrmオブジェクトのhumanoidプロパティからボーン情報をいじったり、blendShapeProxyプロパティからブレンドシェイプで表情を変えていますね。(ここまで実装されてるの流石だと思う)


```ts
vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.LeftUpperArm)!.rotation.x = 0.6
```

例えばこの行では、LeftUppperArmというボーンを指定してx軸ローカル回転を0.6(弧度法)に設定しています。
ボーンは`VRMSchema.HumanoidBoneName`の中の列挙体に定義されています。詳しくは[HumanoidBoneNameのリファレンスページ](https://pixiv.github.io/three-vrm/docs/enums/vrmschema.humanoidbonename.html)をご覧ください。

```ts
vrm.blendShapeProxy!.setValue(VRMSchema.BlendShapePresetName.Fun, .7);
```

またこの行では、ブレンドシェイププロキシを使って表情を変えています。具体的にはFun、つまり喜怒哀楽の「楽」の表情のウェイトを0.7に指定しています。VRoid Studioでモデルを作ったことがある人は、表情を設定する際に間接的にBlendShapeProxyからウェイトを設定しています(VRoid Studioでは[0,100]で指定します)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86191/29d3089d-ae0f-998d-963b-a8f41c1f2d75.png)

例のごとくブレンドシェイプはVRMSchema内の列挙体で定義されているので[BlendShapeProxyのリファレンスページ](https://pixiv.github.io/three-vrm/docs/enums/vrmschema.blendshapepresetname.html)をご覧ください。


# 終わりに

とりあえず、three-vrmを使ってTypeScript＋webpack＋three.jsでVRMモデルを表示させることができました。
おそらくですが、Qiitaの記事では一番早くthree-vrmに触れた記事になったんじゃないかなーと思います。

記事の内容をまとめますと、

- VRMはいいぞ
- three-vrmはいいぞ
- TypeScriptの補完は人権付与
- インストール無しでVRMがプレビューできるだけで体験としての質が上がってよい(唐突に真面目な話題)
- WebXRと親和性が高そう、良き。

これでWeb上にうちの子を召喚して世界中に公開することができますね。やったぜ！

内容は以上になります。最後まで読んでいただきありがとうございました！