<!DOCTYPE html>
<html>
<head>
<title>VRM with BVH Animation Player (Corrected)</title>
<meta charset="utf-8">
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/three@0.150.1/examples/js/loaders/BVHLoader.js"></script>
<script src="./js/aframe-vrm.js"></script> <script>
// ==================== コンポーネント定義エリア ====================
// 1. カメラコントロール用コンポーネント
AFRAME.registerComponent('camera-relative-controls', {
schema: {
targetSpeed: { type: 'number', default: 5 },
acceleration: { type: 'number', default: 10 },
damping: { type: 'number', default: 8 },
enabled: { type: 'boolean', default: true },
rotationSpeed: { type: 'number', default: 1.5 },
verticalSpeed: { type: 'number', default: 3 },
groundY: { type: 'number', default: 0 },
},
init: function () {
this.keys = {};
this.currentVelocity = new THREE.Vector3();
this.cameraDirection = new THREE.Vector3();
this.cameraRight = new THREE.Vector3();
this.moveDirection = new THREE.Vector3();
this.desiredVelocity = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.rigEl = this.el;
this.cameraEl = this.el.querySelector('[camera]');
window.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('keyup', this.onKeyUp.bind(this));
},
remove: function () {
window.removeEventListener('keydown', this.onKeyDown.bind(this));
window.removeEventListener('keyup', this.onKeyUp.bind(this));
},
tick: function (time, timeDelta) {
if (!this.data.enabled || !this.cameraEl) return;
const data = this.data;
const dt = timeDelta / 1000;
const position = this.rigEl.object3D.position;
const cameraObject = this.cameraEl.object3D;
cameraObject.getWorldQuaternion(this.cameraWorldQuaternion);
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion);
this.cameraRight.y = 0;
this.cameraRight.normalize();
this.moveDirection.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); }
const isInputting = this.moveDirection.lengthSq() > 0.0001;
if (isInputting) { this.moveDirection.normalize(); }
let lerpFactor = data.damping;
if (isInputting) {
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed);
lerpFactor = data.acceleration;
} else {
this.desiredVelocity.set(0,0,0);
}
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
if (this.currentVelocity.lengthSq() > 0.0001) {
position.add(this.currentVelocity.clone().multiplyScalar(dt));
}
position.y = Math.max(position.y, data.groundY);
},
onKeyDown: function (event) { if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// 2. BVH再生用コンポーネント(レースコンディション対策版)
AFRAME.registerComponent('bvh-player', {
schema: {
src: { type: 'string' },
loop: { type: 'boolean', default: true },
},
init: function () {
console.log('✅ bvh-player: コンポーネント初期化');
this.mixer = null;
this.vrmData = null;
// --- レースコンディション対策 ---
// 先にVRMコンポーネントの状態を確認する
const vrmComponent = this.el.components.vrm;
if (vrmComponent && vrmComponent.model) {
// VRMが既に読み込み済みの場合 (イベントを聞き逃した場合)
console.log('✅ bvh-player: VRMは既に読み込み済み。直接BVH処理を開始します。');
this.startBvh(vrmComponent.model);
} else {
// VRMがまだ読み込み中の場合 (通常のパターン)
console.log('⏳ bvh-player: VRMの読み込みを待機します...');
this.el.addEventListener('vrm-loaded', (e) => {
console.log('✅ vrm-loaded: イベントをキャッチ。BVH処理を開始します。');
this.startBvh(e.detail.vrm);
}, { once: true }); // イベントを一度だけ受け取る
}
// モデル読み込みエラーの監視
this.el.addEventListener('model-error', (e) => {
console.error('❌ bvh-player: モデルの読み込みでエラーを検知しました。', e.detail.error);
});
},
// BVH処理を開始するメインの関数
startBvh: function(vrmData) {
this.vrmData = vrmData;
const loader = new THREE.BVHLoader();
console.log(`⏳ bvh-player: BVHファイルの読み込みを開始... URL: ${this.data.src}`);
loader.load(this.data.src, (bvh) => {
console.log('✅ bvh-player: BVHファイルの読み込み成功', bvh);
const boneMap = {
'J_Bip_C_Hips': 'hips', 'J_Bip_C_Spine': 'spine', 'J_Bip_C_Chest': 'chest',
'J_Bip_C_UpperChest': 'upperChest', 'J_Bip_C_Neck': 'neck', 'J_Bip_C_Head': 'head',
'J_Bip_L_Shoulder': 'leftShoulder', 'J_Bip_L_UpperArm': 'leftUpperArm',
'J_Bip_L_LowerArm': 'leftLowerArm', 'J_Bip_L_Hand': 'leftHand',
'J_Bip_R_Shoulder': 'rightShoulder', 'J_Bip_R_UpperArm': 'rightUpperArm',
'J_Bip_R_LowerArm': 'rightLowerArm', 'J_Bip_R_Hand': 'rightHand',
'J_Bip_L_UpperLeg': 'leftUpperLeg', 'J_Bip_L_LowerLeg': 'leftLowerLeg',
'J_Bip_L_Foot': 'leftFoot', 'J_Bip_L_ToeBase': 'leftToes',
'J_Bip_R_UpperLeg': 'rightUpperLeg', 'J_Bip_R_LowerLeg': 'rightLowerLeg',
'J_Bip_R_Foot': 'rightFoot', 'J_Bip_R_ToeBase': 'rightToes'
};
const newTracks = [];
bvh.clip.tracks.forEach(track => {
const bvhNodeName = track.name.split('.')[0];
const vrmBoneName = boneMap[bvhNodeName];
if (vrmBoneName) {
const vrmBoneNode = this.vrmData.humanoid.getBoneNode(vrmBoneName);
if (vrmBoneNode) {
const newTrack = track.clone();
newTrack.name = `${vrmBoneNode.name}.${track.name.split('.')[1]}`;
newTracks.push(newTrack);
}
}
});
console.log(`✅ bvh-player: ${newTracks.length}個のトラックをマッピングしました。`);
if (newTracks.length === 0) {
console.error('❌ エラー: マッピングされたボーンがありません。boneMapのキーがBVHファイル内のボーン名と一致しません。');
return;
}
const filteredTracks = newTracks.filter(track => !track.name.endsWith('.position'));
const newClip = new THREE.AnimationClip('bvh_animation', bvh.clip.duration, filteredTracks);
this.mixer = new THREE.AnimationMixer(this.el.object3D);
const action = this.mixer.clipAction(newClip);
if(this.data.loop) {
action.setLoop(THREE.LoopRepeat);
}
action.play();
console.log('✅ bvh-player: アニメーション再生を開始しました!');
}, undefined, (error) => {
console.error('❌ bvh-player: BVHファイルの読み込みに失敗しました。', error);
});
},
tick: function (time, timeDelta) {
if (this.mixer) {
this.mixer.update(timeDelta / 1000);
}
}
});
</script>
</head>
<body>
<a-scene renderer="physicallyCorrectLights: true">
<a-entity
id="avatar"
vrm="src: ./vrm/tesA1_V0a.vrm"
bvh-player="src: https://p-bookmark.sakura.ne.jp/junkerstock/vrm/8.bvh"
position="0 0 0"
rotation="0 180 0">
</a-entity>
<a-entity camera position="0 1.6 2.5" look-controls wasd-controls></a-entity>
<a-sky color="#ECECEC"></a-sky>
<a-plane position="0 0 0" rotation="-90 0 0" width="100" height="100" color="#FFFFFF" shadow></a-plane>
<a-light type="directional" color="#FFF" intensity="0.6" position="-1 2 2"></a-light>
<a-light type="ambient" color="#FFF" intensity="0.6"></a-light>
</a-scene>
</body>
</html>
使用変数
| -------( Function ) | |
| action | |
| boneMap | |
| bvhNodeName | |
| cameraDirection | |
| cameraEl | |
| cameraObject | |
| cameraRight | |
| charset | |
| color | |
| currentVelocity | |
| data | |
| desiredVelocity | |
| dt | |
| eraWorldQuaternion | |
| ffectiveLerpFactor | |
| filteredTracks | |
| height | |
| id | |
| intensity | |
| isInputting | |
| keys | |
| length | |
| lerpFactor | |
| loader | |
| mixer | |
| moveDirection | |
| name | |
| newClip | |
| newTrack | |
| newTracks | |
| player | |
| position | |
| renderer | |
| rigEl | |
| rotation | |
| src | |
| track | |
| type | |
| vrm | |
| vrmBoneName | |
| vrmBoneNode | |
| vrmComponent | |
| vrmData) { this.vrmData = vrmData; const loader = new THREE.BVHLoader -------( Function ) | |
| vrmData | |
| width | |
| y | |
| コンポーネント定義エリア |