TypeScript + three.jsでWeb上で表示したVRMモデルにジャグリングさせる

VRMという3Dモデルのファイルフォーマットがあるが、
今回Web上で表示した上にジャグリングをさせるという依頼を受けたので、
依頼主様の許可を得て備忘録として記事を投稿する。

#VRMとは
>「VRM」はVRアプリケーション向けの人型3Dアバター（3Dモデル）データを扱うためのファイルフォーマットです。glTF2.0をベースとしており、誰でも自由に利用することができます。
>また、Unity向けのVRMファイルの読み書きを行うC#による標準実装（UniVRM）がオープンソースで提供されます。
>[公式サイト](https://vrm.dev/)より引用

用途はアバターとして利用できて、VRゲームや配信なんかで使うものらしい。
昨今のVtuberみたいなやつかな？
どちらにしてもWebで利用するものではなさそう・・・。

#でもWebで利用する
webで表示ってことはThree.jsを使うんだろうなぁ・・・。
VRMのベースになっているglTF2.0のローダーもあるし。
んで調べたら`@pixiv/three-vrm`なんてものがあるじゃないですか。
実際使っている人もいる。
>https://qiita.com/drumath2237/items/2d43d7e3beb7024285ae
>https://qiita.com/Akatsuki1910/items/8c18858d5a8807660008
>https://qiita.com/blachocolat/items/2e16eb78328b7997de3c

##すでにある記事を参考にコードを書いてみる

```ts
    const container = document.querySelector('セレクタ');
    const width = container.clientWidth;
    const height = container.clientHeight;

    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(width, height);
    renderer.setClearColor(0x000000, 0.0);
    container.appendChild(renderer.domElement);

    const camera = new THREE.PerspectiveCamera(45, this.width / this.height, 0.1, 1000);
    camera.position.set(0, 0.75, -3);
    camera.rotation.set(0, Math.PI, 0);

    const scene = new THREE.Scene();

    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x080820, 1);
    scene.add(hemisphereLight);

    const pointLight = new THREE.PointLight(0xffffff, 1, 100);
    pointLight.position.set(0, 0, -2);
    scene.add(pointLight);


    const loader = new GLTFLoader();
    loader.load('VRMモデルのパス', async (gltf: any) => {
      VRM.from(gltf).then((vrm) => {
        scene.add(vrm.scene);

        // 以下VRMの操作

      });
    });

```
割と簡単に表示**は**できる。
`@pixiv/three-vrm`を使えば自分の持っている3Dモデル、いわゆる「うちの子」Web上で表示して遊ぶことができるわけか。
私にはうちの子をいないので今回は[養子](https://3d.nicovideo.jp/works/td32797)をとることにしました。
![FireShot Capture 003 - Document - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266668/65172867-ff74-d30d-620e-8736a8e8ef47.png)

#ジャグリングさせる
依頼内容は[gunswap](http://www.gunswap.co/about)というサイトにジャグリングシミュレーターを流用してVRMにジャグリングをさせるというもの。
[ソース](https://github.com/yDgunz/gunswap)を確認するとジャグリングの小道具のキャッチするときと投げるときの肘と手の座標を算出しているっぽい。
（こういうのサイトスワップっていうのね。初めて知った。）

`@pixiv/three-vrm`は各部位のボーンを回転させることができるけどこれを利用して算出した座標に肘と手持っていくのはどう考えてもしんどいので、
`three-ik`というのを利用する。

## IKとは
>https://codelabo.com/posts/20200229090229

## three-ikの参考記事
>https://qiita.com/naotaro0123/items/13f297fa52a92af36d37
>https://note.com/npaka/n/n477c15fedaef

# 結果
ソースの公開はできないのでデモページだけ公開する。
https://torabe.github.io/juggling/
Escキーを押すことでGUIが開きパラメータの調整を可能にしている。
（パラメータによってはボールが手から離れたり破綻するけど・・・。）

## 工夫・苦労した点
### three-ikによるボーンのジョイント
IKでボーンをつなぐとき二の腕、腕、手までにするとボールを手首で受けるようになってしまったので、中指の第一関節までジョイントした。
おかげで、手首が動くようになりボールのキャッチやトスの動きがより自然な形になった。

### 指の形の調整
何もしないとパーの形のままなので少し指を曲げるように調整するが、

```ts
    //右人差し指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightIndexDistal).rotation.z = -Math.PI / 18;
    //右人差し指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightIndexIntermediate).rotation.z = -Math.PI / 3.6;
    //右人差し指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightIndexProximal).rotation.z = -Math.PI / 6;
    //右小指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightLittleDistal).rotation.z = -Math.PI / 18;
    //右小指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightLittleIntermediate).rotation.z = -Math.PI / 3.6;
    //右小指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightLittleProximal).rotation.z = -Math.PI / 6;
    //右中指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightMiddleDistal).rotation.z = -Math.PI / 18;
    //右中指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightMiddleIntermediate).rotation.z = -Math.PI / 3.6;
    //右中指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightMiddleProximal).rotation.z = -Math.PI / 6;
    //右薬指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightRingDistal).rotation.z = -Math.PI / 18;
    //右薬指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightRingIntermediate).rotation.z = -Math.PI / 3.6;
    //右薬指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightRingProximal).rotation.z = -Math.PI / 6;
    //右親指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightThumbDistal).rotation.y = -Math.PI / 3;
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightThumbDistal).rotation.y = -Math.PI / 3;
    //右親指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightThumbDistal).rotation.y = -Math.PI / 3;
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.RightThumbIntermediate).rotation.z = -Math.PI / 3.6;
    //右親指第一指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbProximal).rotation.y = Math.PI / 12;
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbProximal).rotation.z = Math.PI / 12;

    //左人差し指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftIndexDistal).rotation.z = Math.PI / 18;
    //左人差し指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftIndexIntermediate).rotation.z = Math.PI / 3.6;
    //左人差し指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftIndexProximal).rotation.z = Math.PI / 6;
    //左小指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftLittleDistal).rotation.z = Math.PI / 18;
    //左小指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftLittleIntermediate).rotation.z = Math.PI / 3.6;
    //左小指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftLittleProximal).rotation.z = Math.PI / 6;
    //左中指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftMiddleDistal).rotation.z = Math.PI / 18;
    //左中指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftMiddleIntermediate).rotation.z = Math.PI / 3.6;
    //左中指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftMiddleProximal).rotation.z = Math.PI / 6;
    //左薬指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftRingDistal).rotation.z = Math.PI / 18;
    //左薬指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftRingIntermediate).rotation.z = Math.PI / 3.6;
    //左薬指第一指骨
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftRingProximal).rotation.z = Math.PI / 6;
    //左親指第三指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbDistal).rotation.y = Math.PI / 3;
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbDistal).rotation.y = Math.PI / 3;
    //左親指第二指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbDistal).rotation.y = Math.PI / 3;
    //vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbIntermediate).rotation.z = Math.PI / 3.6;
    //左親指第一指骨
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbProximal).rotation.y = Math.PI / 12;
    vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.LeftThumbProximal).rotation.z = Math.PI / 12;
```
長い。

### 首の角度
GUIでボールの高さの調整したとき首上に曲げてボールの頂点を見るようにしたいが、
各部位のボーンもつ`position`プロパティはおそらく親のボーンからの相対的な座標で`scene`上の絶対的な座標をもとに角度を算出する方法がわからなかった。
（WebGLの知識不足かな？）
あと、
![FireShot Capture 006 - Document - torabe.github.io.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266668/3c2808f1-7abb-1e9a-5451-2d7a511483b7.png)
髪の毛が重力を無視する・・・。

# 感想
実はTypeScriptは今回が初めてなので「型がanyだらけでTypeScriptの意味ないじゃん」ってコードになっている。要学習。

VRMがもともと人が中に入って動かすことを想定しているためか、
Web上で表示できるといっても、動かすのはひと手間も二手間も必要って印象。
髪の毛が重力に逆らうのは`@pixiv/three-vrm`でどうにかできるのか？
それともVRMモデル制作時に何か設定があるのか？

でもやってみるととても面白かった。
3Dモデル作ってみようかな？