+++ 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