どうも、にー兄さんです。
先月のVRoid公式ツイッターで、three.js用のVRMローダーが発表されましたね!
Webアプリケーションで3Dアバターモデルを簡単に扱える『pixiv three-vrm』をOSSとして公開しました!3Dモデルの専門知識がない開発者の方でも「VRM」ファイルをより簡単に扱うことができます! #VRoid
— VRoid公式 (@vroid_pixiv) September 12, 2019
詳細: https://t.co/psBwTYe3cy
GitHubリポジトリ: https://t.co/j9IU2h2Zax pic.twitter.com/Fsp2VLbpkW
Web上でVRMモデルが動かせるということで、さっそく使ってみたいと思いました。
「あれ、そういえばthree.jsって使ったことないな」
「jsのプロジェクトだったら、最近TypeScriptちょっと気になるからやってみたいな」
「クライアントjsにコンパイルしてもimport文使えないのか、webpack?使わなきゃいけないらしい」
「 じゃあまとめて入門するか😇😇😇 」
ということで、ノンフレームワーク の クライアントts でVRMモデルをWebで表示してみます。
ちなみにこんな感じのものを簡単に作れました。
Web上でうちの子をぐりぐり見回すことができるやつできた!
— にー兄さん (@ninisan_drumath) September 19, 2019
TypeScript+Webpack環境整えるところから始まって三日くらいか?
three.jsとpixivさんが最近出したthree-vrmで実装してある
ちゃんとこっちに顔を向けてくれるのが良いんだよなぁ(限界)https://t.co/DJdAL2mgB7 で見れるのでぜひ! pic.twitter.com/2SngmdYUWc
追記
サンプルプロジェクトの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
は以下の通りです。
この記事では基本的にこれ以上のパッケージはインストしませんのでこれが最終形態です。
{
"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
でファイル名を指定します。- 今回は千駄ヶ谷篠ちゃんのモデルを使っています。名前が長いので
shino
という名前にしていますが
- 今回は千駄ヶ谷篠ちゃんのモデルを使っています。名前が長いので
config系
今回のプロジェクトではtsconfig.json
とwebpack.config.js
という二つのconfigファイルを扱います。
それぞれ
# tsconfigの生成
tsc --init
# webpack.configの生成
webpack init
のように自動生成できますが、私が作ったやつのコピペでいいと思います。
以下にまとめます。
tsconfig.js
{
"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
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
を作成して、以下のようなコードを書きましょう。
window.addEventListener("DOMContentLoaded", ()=> {
console.log("Hello, World!");
})
そうしたら、次に
yarn build
とコマンドを打って、 /dist/main.js
をビルドします。
正しくmain.jsが作成されていることを確認したら、/dist/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
キーで開発者ツールを開くと
ちゃんと出ているみたいですね。
three-vrmでVRMモデルを読み込んでレンダリングする
それでは、いよいよthree-vrmを使ってVRMモデルをレンダリングしていきましょう。
コードを記述する前に/dist/models
の中に任意のVRMモデルを配置しておくのを忘れないようにしてください。前述した通り、本記事ではshino.vrm
というVRMモデルを読み込みます。
VRMモデルが配置されているのを確認したら、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が有効なので、記述した内容をリアルタイムで反映してくれるので助かります)。
ちゃんと描画できているようですね。
解説
それでは今回のプログラムの主要な部分を解説していきたいと思います。
まず冒頭の3行のimport
について
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ファイルをインポートするために使うローダーです(詳しくは後述)。
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アバターを触ったことのある方ならなじみ深いかもしれません。
先ほど記述したコードに以下のように追加します。
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();
// ここまで追加
})
}
)
// (中略)
ここでやっていることは、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]で指定します)
例のごとくブレンドシェイプはVRMSchema内の列挙体で定義されているのでBlendShapeProxyのリファレンスページをご覧ください。
終わりに
とりあえず、three-vrmを使ってTypeScript+webpack+three.jsでVRMモデルを表示させることができました。
おそらくですが、Qiitaの記事では一番早くthree-vrmに触れた記事になったんじゃないかなーと思います。
記事の内容をまとめますと、
- VRMはいいぞ
- three-vrmはいいぞ
- TypeScriptの補完は人権付与
- インストール無しでVRMがプレビューできるだけで体験としての質が上がってよい(唐突に真面目な話題)
- WebXRと親和性が高そう、良き。
これでWeb上にうちの子を召喚して世界中に公開することができますね。やったぜ!
内容は以上になります。最後まで読んでいただきありがとうございました!