406 lines
11 KiB
Markdown
406 lines
11 KiB
Markdown
|
+++
|
||
|
date = "2023-10-24"
|
||
|
tags = ["vrm","3d"]
|
||
|
title = "three-vrm v2.0 update"
|
||
|
slug = "vrm"
|
||
|
+++
|
||
|
|
||
|
`three-vrm`関連のブログを調べていると、ほとんどのコードが動かないので`three-vrm v2.0`の対応をまとめます。
|
||
|
|
||
|
唯一、動くコードを発行していたのがpixivの[example](https://pixiv.github.io/three-vrm/packages/three-vrm/examples/)でした。参考にしてみてください。
|
||
|
|
||
|
```sh
|
||
|
$ git clone https://github.com/pixiv/three-vrm/
|
||
|
$ cd three-vrm
|
||
|
$ git checkout gh-pages
|
||
|
$ cd ./packages/three-vrm/examples/
|
||
|
|
||
|
$ vim lookat-advanced.html
|
||
|
```
|
||
|
|
||
|
### example
|
||
|
|
||
|
以下は私が作った`example`です。基本的には`.js`なので好きなframeworkで動かしてみてください。
|
||
|
|
||
|
```json:pacakge.json
|
||
|
"dependencies": {"@pixiv/three-vrm": "^2.0.6", "three": "^0.157.0"}
|
||
|
```
|
||
|
|
||
|
```js:main.js
|
||
|
import * as THREE from 'three';
|
||
|
import { GridHelper, Mesh, MeshLambertMaterial, PlaneGeometry, Vector3, Color, DirectionalLight, Fog, HemisphereLight, AnimationAction, AnimationClip, AnimationMixer, MathUtils, Matrix4, Quaternion } from 'three';
|
||
|
import { VRMLoaderPlugin, VRMUtils, VRMLookAt, VRMSchema } from '@pixiv/three-vrm';
|
||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||
|
|
||
|
// model
|
||
|
const defaultModelUrl = 'https://pixiv.github.io/three-vrm/packages/three-vrm/examples/models/VRM1_Constraint_Twist_Sample.vrm';
|
||
|
|
||
|
// lookat
|
||
|
const _v3A = new THREE.Vector3();
|
||
|
class VRMSmoothLookAt extends VRMLookAt {
|
||
|
constructor(humanoid, applier) {
|
||
|
super(humanoid, applier);
|
||
|
this.smoothFactor = 10.0;
|
||
|
this.yawLimit = 45.0;
|
||
|
this.pitchLimit = 45.0;
|
||
|
this._yawDamped = 0.0;
|
||
|
this._pitchDamped = 0.0;
|
||
|
}
|
||
|
update(delta) {
|
||
|
if ( this.target && this.autoUpdate ) {
|
||
|
this.lookAt( this.target.getWorldPosition( _v3A ) );
|
||
|
if (
|
||
|
this.yawLimit < Math.abs( this._yaw ) ||
|
||
|
this.pitchLimit < Math.abs( this._pitch )
|
||
|
) {
|
||
|
this._yaw = 0.0;
|
||
|
this._pitch = 0.0;
|
||
|
}
|
||
|
const k = 1.0 - Math.exp( - this.smoothFactor * delta );
|
||
|
this._yawDamped += ( this._yaw - this._yawDamped ) * k;
|
||
|
this._pitchDamped += ( this._pitch - this._pitchDamped ) * k;
|
||
|
this.applier.applyYawPitch( this._yawDamped, this._pitchDamped );
|
||
|
this._needsUpdate = false;
|
||
|
}
|
||
|
if ( this._needsUpdate ) {
|
||
|
this._needsUpdate = false;
|
||
|
this.applier.applyYawPitch( this._yaw, this._pitch );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// renderer
|
||
|
const renderer = new THREE.WebGLRenderer({alpha: true, antialias: true});
|
||
|
renderer.outputEncoding = THREE.sRGBEncoding;
|
||
|
renderer.shadowMap.enabled = true;
|
||
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||
|
renderer.setPixelRatio( window.devicePixelRatio );
|
||
|
document.body.appendChild( renderer.domElement );
|
||
|
|
||
|
// camera
|
||
|
const camera = new THREE.PerspectiveCamera( 30.0, window.innerWidth / window.innerHeight, 0.1, 20.0 );
|
||
|
camera.position.set( 0.0, 1.0, 5.0 );
|
||
|
|
||
|
// camera controls
|
||
|
const controls = new OrbitControls( camera, renderer.domElement );
|
||
|
controls.screenSpacePanning = false;
|
||
|
controls.target.set( 0.0, 1.0, 0.0 );
|
||
|
controls.update();
|
||
|
|
||
|
// scene
|
||
|
const scene = new THREE.Scene();
|
||
|
|
||
|
// add color
|
||
|
const bgColor = new Color(0xffffff);
|
||
|
scene.background = new Color(bgColor);
|
||
|
scene.fog = new Fog(bgColor, 3, 10);
|
||
|
const ambiantLight = new HemisphereLight(0xffffff, 0x444444);
|
||
|
ambiantLight.position.set(0, 20, 0);
|
||
|
scene.add(ambiantLight);
|
||
|
|
||
|
// add mesh
|
||
|
const floor = new Mesh(
|
||
|
new PlaneGeometry(100, 100),
|
||
|
new MeshLambertMaterial({
|
||
|
color: 0xffffff,
|
||
|
depthWrite: true,
|
||
|
})
|
||
|
);
|
||
|
floor.position.y = -0.5;
|
||
|
floor.rotation.x = -Math.PI / 2;
|
||
|
scene.add(floor);
|
||
|
const grid = new GridHelper(50, 100, 0xffffff, 0xffffff);
|
||
|
scene.add(grid);
|
||
|
|
||
|
// light
|
||
|
const light = new THREE.DirectionalLight(0xffffff);
|
||
|
light.position.set( 1.0, 1.0, 1.0 ).normalize();
|
||
|
scene.add( light );
|
||
|
|
||
|
// gltf and vrm
|
||
|
let currentVrm = undefined;
|
||
|
let currentAnimationUrl = undefined;
|
||
|
let currentMixer = undefined;
|
||
|
const helperRoot = new THREE.Group();
|
||
|
helperRoot.renderOrder = 10000;
|
||
|
scene.add( helperRoot );
|
||
|
|
||
|
function loadVRM( modelUrl ) {
|
||
|
const loader = new GLTFLoader();
|
||
|
loader.crossOrigin = 'anonymous';
|
||
|
helperRoot.clear();
|
||
|
loader.register((parser) => {
|
||
|
return new VRMLoaderPlugin(parser, { autoUpdateHumanBones: true } );
|
||
|
});
|
||
|
loader.load(
|
||
|
modelUrl,
|
||
|
(gltf) => {
|
||
|
const vrm = gltf.userData.vrm;
|
||
|
VRMUtils.removeUnnecessaryVertices(gltf.scene);
|
||
|
VRMUtils.removeUnnecessaryJoints(gltf.scene);
|
||
|
//VRMUtils.rotateVRM0(vrm);
|
||
|
|
||
|
vrm.scene.traverse((obj) => {
|
||
|
obj.frustumCulled = false;
|
||
|
});
|
||
|
|
||
|
// replace the lookAt to our extended one
|
||
|
const smoothLookAt = new VRMSmoothLookAt(vrm.humanoid, vrm.lookAt.applier);
|
||
|
smoothLookAt.copy(vrm.lookAt);
|
||
|
vrm.lookAt = smoothLookAt;
|
||
|
scene.add(vrm.scene);
|
||
|
currentVrm = vrm;
|
||
|
vrm.lookAt.target = camera;
|
||
|
currentVrm.humanoid.getNormalizedBoneNode('leftUpperArm').rotation.z = 1.3;
|
||
|
currentVrm.humanoid.getNormalizedBoneNode('rightUpperArm').rotation.z = -1.3;
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
loadVRM( defaultModelUrl );
|
||
|
|
||
|
function blink(){
|
||
|
var rand = Math.random()
|
||
|
if (rand > .9) {
|
||
|
currentVrm.expressionManager.setValue('blink', 1);
|
||
|
} else {
|
||
|
currentVrm.expressionManager.setValue('blink', 0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// animate
|
||
|
const clock = new THREE.Clock();
|
||
|
function animate() {
|
||
|
requestAnimationFrame(animate);
|
||
|
const delta = clock.getDelta();
|
||
|
if (currentMixer) {
|
||
|
currentMixer.update(delta);
|
||
|
}
|
||
|
if (currentVrm) {
|
||
|
const s = 0.01 * Math.PI * Math.sin(Math.PI * clock.elapsedTime);
|
||
|
blink();
|
||
|
currentVrm.humanoid.getNormalizedBoneNode('neck').rotation.y = s;
|
||
|
//currentVrm.humanoid.getNormalizedBoneNode('leftUpperArm').rotation.z = s;
|
||
|
//currentVrm.humanoid.getNormalizedBoneNode('rightUpperArm').rotation.x = s;
|
||
|
currentVrm.update(delta);
|
||
|
}
|
||
|
renderer.render(scene, camera);
|
||
|
}
|
||
|
animate();
|
||
|
```
|
||
|
|
||
|
### scene rotation
|
||
|
|
||
|
modelを回転させる
|
||
|
|
||
|
```js
|
||
|
vrm.scene.rotation.y = 3;
|
||
|
```
|
||
|
|
||
|
```js
|
||
|
vrm.scene.rotation.y = Math.PI * Math.sin(clock.getElapsedTime());
|
||
|
```
|
||
|
|
||
|
### blink
|
||
|
|
||
|
まばたきを制御します。
|
||
|
|
||
|
```js
|
||
|
vrm.expressionManager.setValue('blink', 1);
|
||
|
```
|
||
|
|
||
|
### pose
|
||
|
|
||
|
左手を動かします。
|
||
|
|
||
|
```js
|
||
|
vrm.humanoid.getNormalizedBoneNode('leftUpperArm').rotation.z = 1;
|
||
|
```
|
||
|
|
||
|
### lookat
|
||
|
|
||
|
視線をカメラに合わせます。
|
||
|
|
||
|
```js
|
||
|
// lookat
|
||
|
const _v3A = new THREE.Vector3();
|
||
|
class VRMSmoothLookAt extends VRMLookAt {
|
||
|
constructor(humanoid, applier) {
|
||
|
super(humanoid, applier);
|
||
|
this.smoothFactor = 10.0;
|
||
|
this.yawLimit = 45.0;
|
||
|
this.pitchLimit = 45.0;
|
||
|
this._yawDamped = 0.0;
|
||
|
this._pitchDamped = 0.0;
|
||
|
}
|
||
|
update(delta) {
|
||
|
if ( this.target && this.autoUpdate ) {
|
||
|
this.lookAt( this.target.getWorldPosition( _v3A ) );
|
||
|
if (
|
||
|
this.yawLimit < Math.abs( this._yaw ) ||
|
||
|
this.pitchLimit < Math.abs( this._pitch )
|
||
|
) {
|
||
|
this._yaw = 0.0;
|
||
|
this._pitch = 0.0;
|
||
|
}
|
||
|
const k = 1.0 - Math.exp( - this.smoothFactor * delta );
|
||
|
this._yawDamped += ( this._yaw - this._yawDamped ) * k;
|
||
|
this._pitchDamped += ( this._pitch - this._pitchDamped ) * k;
|
||
|
this.applier.applyYawPitch( this._yawDamped, this._pitchDamped );
|
||
|
this._needsUpdate = false;
|
||
|
}
|
||
|
if ( this._needsUpdate ) {
|
||
|
this._needsUpdate = false;
|
||
|
this.applier.applyYawPitch( this._yaw, this._pitch );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
loader.load(
|
||
|
modelUrl,
|
||
|
(gltf) => {
|
||
|
const vrm = gltf.userData.vrm;
|
||
|
|
||
|
// replace the lookAt to our extended one
|
||
|
const smoothLookAt = new VRMSmoothLookAt(vrm.humanoid, vrm.lookAt.applier);
|
||
|
smoothLookAt.copy(vrm.lookAt);
|
||
|
vrm.lookAt = smoothLookAt;
|
||
|
scene.add(vrm.scene);
|
||
|
currentVrm = vrm;
|
||
|
vrm.lookAt.target = camera;
|
||
|
},
|
||
|
)
|
||
|
```
|
||
|
|
||
|
### animation
|
||
|
|
||
|
アニメーションは基本的に以下の構文になります。
|
||
|
|
||
|
`vrm`ではなく`currentVrm`を使用します。
|
||
|
|
||
|
```js
|
||
|
// animate
|
||
|
const clock = new THREE.Clock();
|
||
|
function animate() {
|
||
|
requestAnimationFrame(animate);
|
||
|
const delta = clock.getDelta();
|
||
|
if (currentMixer) {
|
||
|
currentMixer.update(delta);
|
||
|
}
|
||
|
if (currentVrm) {
|
||
|
// ここに追加
|
||
|
currentVrm.humanoid.getNormalizedBoneNode('leftUpperArm').rotation.z = 1;
|
||
|
currentVrm.update(delta);
|
||
|
}
|
||
|
renderer.render(scene, camera);
|
||
|
}
|
||
|
animate();
|
||
|
```
|
||
|
|
||
|
### bone name
|
||
|
|
||
|
BoneNodeでしているするときは小文字から始めます。
|
||
|
|
||
|
```js
|
||
|
currentVrm.humanoid.getNormalizedBoneNode('head').rotation.z = 1;
|
||
|
currentVrm.humanoid.getNormalizedBoneNode('leftHand').rotation.z = 1;
|
||
|
```
|
||
|
|
||
|
```js
|
||
|
const enum VMDBoneNames {
|
||
|
Root = '全ての親',
|
||
|
Center = 'センター',
|
||
|
Hips = '下半身',
|
||
|
Spine = '上半身',
|
||
|
Chest = '上半身2',
|
||
|
Neck = '首',
|
||
|
Head = '頭',
|
||
|
LeftEye = '左目',
|
||
|
LeftShoulder = '左肩',
|
||
|
LeftUpperArm = '左腕',
|
||
|
LeftLowerArm = '左ひじ',
|
||
|
LeftHand = '左手首',
|
||
|
LeftThumbProximal = '左親指0',
|
||
|
LeftThumbIntermediate = '左親指1',
|
||
|
LeftThumbDistal = '左親指2',
|
||
|
LeftIndexProximal = '左人指1',
|
||
|
LeftIndexIntermediate = '左人指2',
|
||
|
LeftIndexDistal = '左人指3',
|
||
|
LeftMiddleProximal = '左中指1',
|
||
|
LeftMiddleIntermediate = '左中指2',
|
||
|
LeftMiddleDistal = '左中指3',
|
||
|
LeftRingProximal = '左薬指1',
|
||
|
LeftRingIntermediate = '左薬指2',
|
||
|
LeftRingDistal = '左薬指3',
|
||
|
LeftLittleProximal = '左小指1',
|
||
|
LeftLittleIntermediate = '左小指2',
|
||
|
LeftLittleDistal = '左小指3',
|
||
|
LeftUpperLeg = '左足',
|
||
|
LeftLowerLeg = '左ひざ',
|
||
|
LeftFoot = '左足首',
|
||
|
LeftFootIK = '左足IK',
|
||
|
LeftToes = '左つま先',
|
||
|
LeftToeIK = '左つま先IK',
|
||
|
RightEye = '右目',
|
||
|
RightShoulder = '右肩',
|
||
|
RightUpperArm = '右腕',
|
||
|
RightLowerArm = '右ひじ',
|
||
|
RightHand = '右手首',
|
||
|
RightThumbProximal = '右親指0',
|
||
|
RightThumbIntermediate = '右親指1',
|
||
|
RightThumbDistal = '右親指2',
|
||
|
RightIndexProximal = '右人指1',
|
||
|
RightIndexIntermediate = '右人指2',
|
||
|
RightIndexDistal = '右人指3',
|
||
|
RightMiddleProximal = '右中指1',
|
||
|
RightMiddleIntermediate = '右中指2',
|
||
|
RightMiddleDistal = '右中指3',
|
||
|
RightRingProximal = '右薬指1',
|
||
|
RightRingIntermediate = '右薬指2',
|
||
|
RightRingDistal = '右薬指3',
|
||
|
RightLittleProximal = '右小指1',
|
||
|
RightLittleIntermediate = '右小指2',
|
||
|
RightLittleDistal = '右小指3',
|
||
|
RightUpperLeg = '右足',
|
||
|
RightLowerLeg = '右ひざ',
|
||
|
RightFoot = '右足首',
|
||
|
RightFootIK = '右足IK',
|
||
|
RightToes = '右つま先',
|
||
|
RightToeIK = '右つま先IK',
|
||
|
}
|
||
|
|
||
|
const enum VMDMorphNames {
|
||
|
Blink = 'まばたき',
|
||
|
BlinkR = 'ウィンク',
|
||
|
BlinkL = 'ウィンク右',
|
||
|
A = 'あ',
|
||
|
I = 'い',
|
||
|
U = 'う',
|
||
|
E = 'え',
|
||
|
O = 'お',
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### カメラ移動
|
||
|
|
||
|
animationで使うといいです。
|
||
|
|
||
|
```js
|
||
|
camera.translateZ(0.01);
|
||
|
camera.translateY(0.01);
|
||
|
camera.translateX(0.01);
|
||
|
|
||
|
// カメラ目線で移動
|
||
|
camera.lookAt(new THREE.Vector3(0, 0, 0));
|
||
|
```
|
||
|
|
||
|
### ref
|
||
|
|
||
|
https://pixiv.github.io/three-vrm/packages/three-vrm/examples/
|
||
|
|
||
|
https://pixiv.github.io/three-vrm/packages/three-vrm-materials-mtoon/examples/
|
||
|
|
||
|
https://gist.github.com/ahuglajbclajep/6ea07f6feb250aa776afa141a35e725b
|
||
|
|