1
0
hugo/content/blog/2024-03-17-vrm.md
2024-12-20 17:03:48 +09:00

332 lines
9.8 KiB
Markdown

+++
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
<html>
<head>
<script src="main.js"></script>
</head>
<body>
<div id="canvas" style="width:100%;height:640px;"></div>
</body>
</html>
```
```sh
$ npm run build
$ npm run dev
```
<div class="vrm-model">
<iframe src="https://vrm.syui.ai" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen scrolling="no"></iframe>
</div>
## 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ですべてのチェックを付けましょう。精度が高まります。あと、コリジョンは外しました。
unityでの操作は以下の通り。
- 1. Animation Type : Humanoid
- 2. Animation Clip(Unreal Take) -> 右クリック -> VRM -> Convert to VRM Animation -> .vrma
### [issue] fbxからunityを使ってvrmaを作成するときの罠
fbxをvrmaにする際に[malaybaku/AnimationClipToVrmaSample](https://github.com/malaybaku/AnimationClipToVrmaSample)を使うんだけど、univrmのvrm 1.0をインストールしないといけない。両方必要なのかもしれない。つまり、インストールするものは以下の3つ。また、AnimationClipToVrmaSampleはwindowsでは動きません。macでのみ動きます。今後は`vrm 1.0`を使っていったほうがいいですね。
- [malaybaku/AnimationClipToVrmaSample](https://github.com/malaybaku/AnimationClipToVrmaSample)
- [VRM 1.0 Import/Export](https://github.com/vrm-c/UniVRM/releases/download/v0.124.2/VRM-0.124.2_dd50.unitypackage)
- [VRM 0.x Import/Export](https://github.com/vrm-c/UniVRM/releases/download/v0.124.2/UniVRM-0.124.2_dd50.unitypackage)
## 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