<!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 | |
コンポーネント定義エリア |