324 lines
8.9 KiB
Markdown
324 lines
8.9 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ですべてのチェックを付けましょう。精度が高まります。あと、コリジョンは外しました。
|
||
|
|
||
|
- 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
|
||
|
|