50
32

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-07

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

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

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

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

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

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

追記
サンプルプロジェクトのURLをこちらに変えました。

対象の読者層

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

要求する知識/経験

  • 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インストール

yarn add typescript

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

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

これによって得られる package.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.htmlmain.jsを読み込み、WebGLでレンダリングします
  • dist/models/配下にはvrmモデルを配置して置き、GLTFLoaderでファイル名を指定します。

config系

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

# tsconfigの生成
tsc --init

# webpack.configの生成
webpack init

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

以下にまとめます。

tsconfig.js

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

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系はこちらのサイトを参考にさせていただきました。

※もしコマンドからconfigファイルをinitするときの注意

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

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 を作成して、以下のようなコードを書きましょう。

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

そうしたら、次に

yarn build

とコマンドを打って、 /dist/main.js をビルドします。
正しくmain.jsが作成されていることを確認したら、/dist/index.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を使っていきましょう。以下のようにコマンドを打ってください。

yarn start

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

image.png

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

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

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

VRMモデルが配置されているのを確認したら、index.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

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

解説

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

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

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM } from '@pixiv/three-vrm'

まぁ見ればわかる通りなのですが、THREEGLTFLoaderVRMという名前でそれぞれthree.jsやthree-vrmのAPIにアクセスできるようにインポートしています。GLTFLoaderというのはVRMファイルをインポートするために使うローダーです(詳しくは後述)。

  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をロードしてシーンに追加するまでの機構を見ていきます。

  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とは?

VRMとは

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

である、という風にVRMの公式リファレンスページに書いてあります。
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アバターを触ったことのある方ならなじみ深いかもしれません。

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

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

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

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

例えばこの行では、LeftUppperArmというボーンを指定してx軸ローカル回転を0.6(弧度法)に設定しています。
ボーンはVRMSchema.HumanoidBoneNameの中の列挙体に定義されています。詳しくはHumanoidBoneNameのリファレンスページをご覧ください。

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

またこの行では、ブレンドシェイププロキシを使って表情を変えています。具体的にはFun、つまり喜怒哀楽の「楽」の表情のウェイトを0.7に指定しています。VRoid Studioでモデルを作ったことがある人は、表情を設定する際に間接的にBlendShapeProxyからウェイトを設定しています(VRoid Studioでは[0,100]で指定します)
image.png

例のごとくブレンドシェイプはVRMSchema内の列挙体で定義されているのでBlendShapeProxyのリファレンスページをご覧ください。

終わりに

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

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

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

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

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

50
32
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
32