2023-10-24 /
@syui
vrm
, 3d
three-vrm v2.0 update
three-vrm
関連のブログを調べていると、ほとんどのコードが動かないのでthree-vrm v2.0
の対応をまとめます。
唯一、動くコードを発行していたのがpixivのexampleでした。参考にしてみてください。
$ 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で動かしてみてください。
"dependencies": {"@pixiv/three-vrm": "^2.0.6", "three": "^0.157.0"}
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を回転させる
vrm.scene.rotation.y = 3;
vrm.scene.rotation.y = Math.PI * Math.sin(clock.getElapsedTime());
blink
まばたきを制御します。
vrm.expressionManager.setValue('blink', 1);
pose
左手を動かします。
vrm.humanoid.getNormalizedBoneNode('leftUpperArm').rotation.z = 1;
lookat
視線をカメラに合わせます。
// 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
を使用します。
// 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でしているするときは小文字から始めます。
currentVrm.humanoid.getNormalizedBoneNode('head').rotation.z = 1;
currentVrm.humanoid.getNormalizedBoneNode('leftHand').rotation.z = 1;
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で使うといいです。
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