+++ date = "2024-03-17" tags = ["vrm", "threejs"] title = "three-vrmでvrmaを読み込む" slug = "vrm" +++ [three-vrm](https://github.com/pixiv/three-vrm)でvrmaを読み込むことができるようになりました。そこで今回は色々なtipsを紹介します。 `three-vrm`は、私がよく3d-modelの読み込みに使っている`three.js`を`.vrm`に対応させたものです。three.jsは`.gltf(v2.0)`を読み込めますので、その拡張である`.vrm`を`.gltf`や`.glb`に変換して読み込めばいいのですが、色々と問題があります。そのため`three-vrm`を使ったほうが見栄えが良くなります。 ## three-vrm -> vrma 使用するのは、`npm`, `webpack`, `ts`あたりです。 nodeは`v18.14.1`です。場合によっては`nvm`を使用してください。 ```sh . ├── dist │   ├── index.html │   ├── vrm/ai.vrm │   └── vrma/VRMA_01.vrma ├── package.json ├── src │   └── index.ts ├── tsconfig.json └── webpack.config.js ``` `./dist/vrm/`, `./dist/vrma/`にファイルを置いてください。 - vrm : [download](https://hub.vroid.com/characters/675572020956181239/models/7175071267176594918) - vrma : [download](https://vroid.booth.pm/items/5512385) 後述しますが、`src/index.ts`の以下の部分で読み込みます。 ```js:src/index.ts load("/vrm/ai.vrm"); load("/vrma/VRMA_01.vrma"); ``` ```json:package.json { "name": "model", "version": "1.0.0", "private": true, "scripts": { "build": "webpack", "dev": "webpack-dev-server --open" }, "devDependencies": { "ts-loader": "^9.5.1", "typescript": "^5.4.2", "webpack": "^5.90.3", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.3" }, "dependencies": { "@pixiv/three-vrm": "^2.1.1", "@pixiv/three-vrm-animation": "^2.1.1", "three": "^0.162.0" } } ``` ```json:tsconfig.json { "compilerOptions": { "target": "es2016", "module": "commonjs", "skipLibCheck": true } } ``` ```js:webpack.config.js const path = require('path'); module.exports = { mode: 'development', entry: './src/index.ts', module: { rules: [ { test: /\.ts$/, loader: 'ts-loader' } ] }, resolve: { extensions: ['.ts', '.js'] }, output: { filename: 'main.js', path: path.join(__dirname, "dist") }, devServer: { static: { directory: path.join(__dirname, "dist"), } } } ``` ```sh $ npm i ``` ```js:src/index.ts import * as THREE from "three" import { Vector3 } from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader" import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { VRMLoaderPlugin } from "@pixiv/three-vrm"; import { createVRMAnimationClip, VRMAnimationLoaderPlugin } from "@pixiv/three-vrm-animation"; window.addEventListener("DOMContentLoaded", () => { const canvas = document.getElementById("canvas"); if (canvas == null) return; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 30, canvas.clientWidth/canvas.clientHeight, 0.1, 20); camera.position.set(0.0, 0, -4.0) camera.rotation.set(0.0, Math.PI, 0.0) camera.lookAt(new THREE.Vector3(0, 0, 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, Math.PI); light.position.set(1.0, 1.0, 1.0); scene.add(light); let currentVrm: any = undefined; let currentVrmAnimation: any = undefined; let currentMixer:any = undefined; function load(url: string) { loader.load( url, (gltf) => { tryInitVRM(gltf); tryInitVRMA(gltf); }, (progress) => console.log( "Loading model...", 100.0 * (progress.loaded / progress.total), "%" ), (error) => console.error(error) ); } function tryInitVRM(gltf: any) { const vrm = gltf.userData.vrm; if ( vrm == null ) { return; } currentVrm = vrm; scene.add(vrm.scene); initAnimationClip(); } function tryInitVRMA(gltf: any) { const vrmAnimations = gltf.userData.vrmAnimations; if (vrmAnimations == null) { return; } currentVrmAnimation = vrmAnimations[0] ?? null; initAnimationClip(); } function initAnimationClip() { if (currentVrm && currentVrmAnimation) { currentMixer = new THREE.AnimationMixer(currentVrm.scene); const clip = createVRMAnimationClip(currentVrmAnimation, currentVrm); currentMixer.clipAction(clip).play(); } } const loader = new GLTFLoader(); loader.register((parser) => { return new VRMLoaderPlugin(parser); }); loader.register((parser) => { return new VRMAnimationLoaderPlugin(parser); }); // ここで読み込む load("/vrm/ai.vrm"); load("/vrma/VRMA_01.vrma"); const clock = new THREE.Clock(); clock.start(); scene.background = new THREE.Color( 0x404040 ); const directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(1, 1, 1); scene.add(directionalLight); const ambientLight = new THREE.AmbientLight(0x333333); scene.add(ambientLight); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.2; controls.enableRotate = true; controls.target.set( 0.0, 1.0, 0.0 ); const update = () => { controls.update(); requestAnimationFrame(update); const deltaTime = clock.getDelta(); if (currentMixer) { currentMixer.update(deltaTime); } if (currentVrm) { currentVrm.update(deltaTime); } renderer.render(scene, camera); } update(); }) ``` ```html:dist/index.html
``` ```sh $ npm run build $ npm run dev ```
## random vrma 例えば、random(ランダム)で`.vrma`を切り替えてみましょう。 ```js:src/index.ts //VRMA_01 全身を見せる //VRMA_02 挨拶 //VRMA_03 Vサイン //VRMA_04 撃つ //VRMA_05 回る //VRMA_06 モデルポーズ //VRMA_07 屈伸運動 setInterval(() => { load("./vrma/VRMA_0" + Math.floor(Math.random() * 7 + 1) + ".vrma"); }, 10000); ``` 自然に見せるには`idle`状態の`.vrma`を用意して`setInterval`をすれば良さそうですね。 ## make vrma bvhを作成してそれを`vrma`に変換することができます。基本的に`.vrma`は`.gltf`でいくつかの宣言を行うことで有効になります。それを`glb`に変換して`vrma`にリネームします。 しかし、めんどくさすぎてそんなことはやってられませんので、[UniVRM](https://github.com/vrm-c/UniVRM/releases)を使用すると良いでしょう。 - docs : https://github.com/malaybaku/AnimationClipToVrmaSample - bvh : [download](https://github.com/BandaiNamcoResearchInc/Bandai-Namco-Research-Motiondataset) 例えば、最新版の`UniVRM`をinstallして、`AnimationClipToVrmaSample/Assets`をunity(project)にコピーすれば`SampleMotion/Wave.anim`を`.vrma`でexportできます。 基本的な手順としては、まず`ue5`や`.bvh`から`.fbx`を用意し、それをunityで読み込みます。 読み込むと`Animation Clip`ができます。これはunity独自のmodel motionのようなものです。まずはfbxの`Animation Type`を`Humanoid`にします。 > ue5からfbxをexportする際は、animationですべてのチェックを付けましょう。精度が高まります。あと、コリジョンは外しました。 - 1. Animation Type : Humanoid - 2. Animation Clip -> .vrma ref : https://note.com/hibit_at/n/nf05ac76ecc3c ## sRGBEncoding 見た目を変えます。 ```js:src/index.ts // https://threejs.org/docs/#api/en/constants/Renderer const renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); renderer.shadowMap.enabled = true; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; ``` ```js:src/index.ts //renderer.outputEncoding = THREE.sRGBEncoding; renderer.outputColorSpace = THREE.SRGBColorSpace; ``` ref : https://koro-koro.com/threejs-no4/ ## fvp -> glb `.fvp`というのは3d-printの拡張子です。 これはポーズを決めて出力できますが、それをglbに変換することでポーズ付きのglbができます。 ポーズも`fvp`の出力も`vroid studio`で行います。 - vroid studio : https://vroid.com/studio - fvp -> glb : https://booth.pm/ja/items/2755440