2024-03-17 /
@syui
vrm
, threejs
three-vrmでvrmaを読み込む
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
を使用してください。
.
├── dist
│ ├── index.html
│ ├── vrm/ai.vrm
│ └── vrma/VRMA_01.vrma
├── package.json
├── src
│ └── index.ts
├── tsconfig.json
└── webpack.config.js
./dist/vrm/
, ./dist/vrma/
にファイルを置いてください。
後述しますが、src/index.ts
の以下の部分で読み込みます。
load("/vrm/ai.vrm");
load("/vrma/VRMA_01.vrma");
{
"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"
}
}
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"skipLibCheck": true
}
}
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"),
}
}
}
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>
<head>
<script src="main.js"></script>
</head>
<body>
<div id="canvas" style="width:100%;height:640px;"></div>
</body>
</html>
$ npm run build
$ npm run dev
random vrma
例えば、random(ランダム)で.vrma
を切り替えてみましょう。
//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を使用すると良いでしょう。
例えば、最新版の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での操作は以下の通り。
-
- Animation Type : Humanoid
-
- Animation Clip(Unreal Take) -> 右クリック -> VRM -> Convert to VRM Animation -> .vrma
[issue] fbxからunityを使ってvrmaを作成するときの罠
fbxをvrmaにする際にmalaybaku/AnimationClipToVrmaSampleを使うんだけど、univrmのvrm 1.0をインストールしないといけない。両方必要なのかもしれない。つまり、インストールするものは以下の3つ。また、AnimationClipToVrmaSampleはwindowsでは動きません。macでのみ動きます。今後はvrm 1.0
を使っていったほうがいいですね。
sRGBEncoding
見た目を変えます。
// 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;
//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
で行います。