見出し画像

three.js で VRM を表示する (1) - 事始め

npaka

three.js を使ってブラウザ上で VRM を表示する方法をまとめました。

・three.js@0.125.1
・typescript@4.1.3
・webpack@5.18.0

前回

1. 開発環境の準備

開発環境の準備手順は、次のとおり。

(1) Node.js と live-server のインストール。
(2) プロジェクトの作成。

$ mkdir helloworld
$ cd helloworld
$ npm init -y

(3) モジュールのインストール。

$ npm i -D webpack webpack-cli typescript ts-loader
$ npm i -S three @pixiv/three-vrm

(4) プロジェクトの設定ファイル「package.json」の「scripts」を以下のように変更。

・package.json

  "scripts": {
    "start": "live-server dist",
    "build": "webpack",
    "watch": "webpack -w"
  },

(5) TypeScriptの設定ファイル「tsconfig.json」をプロジェクトフォルダ直下に作成し、以下のように編集。

・tsconfig.json

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "lib": [
      "es2020",
      "dom"
    ]
  }
}

(6) webpackの設定ファイル「webpack.config.js」をプロジェクトフォルダ直下に作成し、以下のように編集。

・webpack.config.js

module.exports = {
  // development or production
  mode: "development",

  entry: "./src/index.ts",
  output: {
    path: `${__dirname}/dist`,
    filename: "main.js"
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader"
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".js"]
  }
};

2.  VRM の表示

VRMの表示手順は次のとおり。

(1) プロジェクトフォルダ直下に src フォルダを生成し、srcフォルダ直下に 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", () => {
  // canvasの取得
  const canvas = document.getElementById('canvas')

  // シーンの生成
  const scene = new THREE.Scene()

  // カメラの生成
  const camera = new THREE.PerspectiveCamera(
    45, canvas.clientWidth/canvas.clientHeight, 0.1, 1000)
  camera.position.set(0, 1.3, -1)
  camera.rotation.set(0, Math.PI, 0)

  // レンダラーの生成
  const renderer = new THREE.WebGLRenderer()
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(canvas.clientWidth, canvas.clientHeight)
  renderer.setClearColor(0x7fbfff, 1.0)
  canvas.appendChild(renderer.domElement)

  // ライトの生成
  const light = new THREE.DirectionalLight(0xffffff)
  light.position.set(-1, 1, -1).normalize()
  scene.add(light)

  // VRMの読み込み
  const loader = new GLTFLoader()
  loader.load('./alicia.vrm',
    (gltf) => {
      VRM.from(gltf).then( (vrm) => {
        // シーンへの追加
        scene.add(vrm.scene)
      })
    }
  )

  // フレーム毎に呼ばれる
  const update = () => {
    requestAnimationFrame(update)
    renderer.render(scene, camera)
  }
  update()
})

(2) プロジェクトフォルダ直下に dist フォルダを生成し、distフォルダ直下に index.html と VRMファイル alicia.vrm を配置。

 ・index.html

<html>
  <head>
    <script type="text/javascript" src="main.js"></script>
  </head>
  <body>
    <div id="canvas" style="width:640px;height:480px;"></div>
  </body>
</html>

・alicia.vrm
ニコニ立体ちゃん (VRM)」からダウンロードしてファイル名変更。

(3) ビルドと実行

$ npm run build
$ npm run start

画像2

3. ボーンで関節を回転

ボーンで関節を回転させるには、vrm.humanoid.getBoneNode() でボーンを取得し、rotationで回転させます。

頭を見上げる(頭ボーンのX軸をπ/6回転)コードは、次のとおり。
VRMSchemaを使ってるので、インポートも必要になります。

import { VRM, VRMSchema } from '@pixiv/three-vrm'


const head = vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.Head)
head.rotation.x = Math.PI /6

画像1

HumanoidBoneName 定数は、次のとおり。

・ Chest
・ Head
・ Hips
・ Jaw
・ LeftEye
・ LeftFoot
・ LeftHand
・ LeftIndexDistal
・ LeftIndexIntermediate
・ LeftIndexProximal
・ LeftLittleDistal
・ LeftLittleIntermediate
・ LeftLittleProximal
・ LeftLowerArm
・ LeftLowerLeg
・ LeftMiddleDistal
・ LeftMiddleIntermediate
・ LeftMiddleProximal
・ LeftRingDistal
・ LeftRingIntermediate
・ LeftRingProximal
・ LeftShoulder
・ LeftThumbDistal
・ LeftThumbIntermediate
・ LeftThumbProximal
・ LeftToes
・ LeftUpperArm
・ LeftUpperLeg
・ Neck
・ RightEye
・ RightFoot
・ RightHand
・ RightIndexDistal
・ RightIndexIntermediate
・ RightIndexProximal
・ RightLittleDistal
・ RightLittleIntermediate
・ RightLittleProximal
・ RightLowerArm
・ RightLowerLeg
・ RightMiddleDistal
・ RightMiddleIntermediate
・ RightMiddleProximal
・ RightRingDistal
・ RightRingIntermediate
・ RightRingProximal
・ RightShoulder
・ RightThumbDistal
・ RightThumbIntermediate
・ RightThumbProximal
・ RightToes
・ RightUpperArm
・ RightUpperLeg
・ Spine
・ UpperChest

4. ブレンドシェイプで表情を変更

ブレンドシェイプで表情を変更するには、vrm.blendShapeProxy.setValue() を使って、ブレンドシェイプの種別と値(0.0 〜 1.0)を指定します。vrm.blendShapeProxy.update() で反映されます。

「楽しい」表情で「お」の口にするコードは次のとおり。

vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Fun, 1.0)
vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.O, 1.0)
vrm.blendShapeProxy.update()

画像3

BlendShapePresetName 定数は、次のとおり。

・ A
・ Angry
・ Blink
・ BlinkL
・ BlinkR
・ E
・ Fun
・ I
・ Joy
・ Lookdown
・ Lookleft
・ Lookright
・ Lookup
・ Neutral
・ O
・ Sorrow
・ U
・ Unknown

5. IKで手足を動かす

IKで手足を動かすには、THREE.IK を使います。

インストールコマンドは、次のとおり。

$ npm i -S three-ik

適当に動くターゲット(赤い丸)に手を近づけるコードは次のとおり。

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM, VRMSchema } from '@pixiv/three-vrm'
import { IK, IKChain, IKJoint, IKHelper } from 'three-ik'

window.addEventListener("DOMContentLoaded", () => {
  // canvasの取得
  const canvas = document.getElementById('canvas')

  // シーンの生成
  const scene = new THREE.Scene()

  // カメラの生成
  const camera = new THREE.PerspectiveCamera(
    45, canvas.clientWidth/canvas.clientHeight, 0.1, 1000)
  camera.position.set(0, 1.3, -1)
  camera.rotation.set(0, Math.PI, 0)

  // レンダラーの生成
  const renderer = new THREE.WebGLRenderer()
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(canvas.clientWidth, canvas.clientHeight)
  renderer.setClearColor(0x7fbfff, 1.0)
  canvas.appendChild(renderer.domElement)

  // ライトの生成
  const light = new THREE.DirectionalLight(0xffffff)
  light.position.set(-1, 1, -1).normalize()
  scene.add(light)

  // VRMの読み込み
  const loader = new GLTFLoader()
  loader.load('./alicia.vrm',
    (gltf) => {
      VRM.from(gltf).then((vrm) => {
        // シーンへの追加
        scene.add(vrm.scene)

        // IKの準備
        const ikList = [new IK(), new IK()] // IKシステム
        const chainList = [new IKChain(), new IKChain()] // チェーン
        const pivotList = [] // ピボット
        const bonesList = [] // ボーン
        const nodesList = [] // ノード

        // ボーン名
        let boneName = [
          [VRMSchema.HumanoidBoneName.LeftUpperArm,
            VRMSchema.HumanoidBoneName.LeftLowerArm,
            VRMSchema.HumanoidBoneName.LeftHand],
          [VRMSchema.HumanoidBoneName.RightUpperArm,
           VRMSchema.HumanoidBoneName.RightLowerArm,
           VRMSchema.HumanoidBoneName.RightHand]]

        for (let j = 0; j < 2; j++) {
          // ターゲットの生成
          const movingTarget = new THREE.Mesh(
            new THREE.SphereGeometry(0.05),
            new THREE.MeshBasicMaterial({color: 0xff0000}))
          movingTarget.position.x = -0.2
          let pivot = new THREE.Object3D()
          pivot.add(movingTarget)
          pivot.position.x =  j == 0 ? -0.3 : 0.3
          pivot.position.y = 1.2
          pivot.position.z = -0.3
          scene.add(pivot)
          pivotList.push(pivot)

          // チェーンの生成
          const bones = [] // ボーン
          const nodes = [] // ノード
          for (let i = 0; i < 3; i++) {
            // ボーンとノードの生成
            const bone = new THREE.Bone()
            let node = vrm.humanoid.getBoneNode(boneName[j][i])
 
            if (i == 0) {
              node.getWorldPosition(bone.position)
            } else {
              bone.position.set(node.position.x, node.position.y, node.position.z)
              bones[i - 1].add(bone)
            }
            bones.push(bone)
            nodes.push(node)
 
            // チェーンに追加
            const target = i === 2 ? movingTarget : null
            chainList[j].add(new IKJoint(bone, {}), {target})
          }

          // IKシステムにチェーン追加
          ikList[j].add(chainList[j])

          // リストに追加
          bonesList.push(bones)
          nodesList.push(nodes)

          // ルートボーンの追加
          scene.add(ikList[j].getRootBone())

          // ヘルパーの追加
          //const helper = new IKHelper(ikList[j])
          //scene.add(helper)
        }

        // 更新の開始
        update(vrm, ikList, pivotList, bonesList, nodesList)
      })
    }
  )

  // 腕の更新
  const updateArm = (bones, nodes, offset) => {
    const q = new THREE.Quaternion()
    q.setFromAxisAngle( new THREE.Vector3(0, 1, 0), offset)
    nodes[0].setRotationFromQuaternion(bones[0].quaternion.multiply(q))
    nodes[1].setRotationFromQuaternion(bones[1].quaternion)
    nodes[2].setRotationFromQuaternion(bones[2].quaternion)
  }

  // フレーム毎回に呼ばれる
  const update = (vrm, ikList, pivotList, bonesList, nodesList) => {
    // ターゲットの移動
    pivotList[0].rotation.z -= 0.01
    pivotList[1].rotation.z += 0.01

    // IKの更新
    ikList[0].solve()
    ikList[1].solve()

    // 腕の更新
    updateArm(bonesList[0], nodesList[0], Math.PI / 2)
    updateArm(bonesList[1], nodesList[1], -Math.PI / 2)

    // フレーム更新
    requestAnimationFrame(() => update(vrm, ikList, pivotList, bonesList, nodesList))
    renderer.render(scene, camera)
  }
})

画像4

【おまけ】 不必要な警告表示の対策

次回



この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
npaka
プログラマー。iPhone / Android / Unity / ROS / AI / AR / VR / RasPi / Jetson / ロボット / ガジェット。年2冊ペースで技術書を執筆。アニソン / カラオケ / ギター twitter : @npaka123