junkerstock
 vrm-text-bvh2 

<!DOCTYPE html>
<html>
<head>
<title>VRM with BVH Animation Player</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 },
brakingDeceleration: { type: 'number', default: 20 },
enabled: { type: 'boolean', default: true },
rotationSpeed: { type: 'number', default: 1.5 },
verticalSpeed: { type: 'number', default: 3 },
groundY: { type: 'number', default: 0 },
ceilingY: { type: 'number', default: 50 }
},
init: function () {
this.keys = {};
this.leftThumbstickInput = { x: 0, y: 0 };
this.rightThumbstickInput = { x: 0, y: 0 };
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]');

this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand');
if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); }
this.rightHand = document.getElementById('rightHand');
if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); }
});

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));
if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } catch(e){} }
if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} }
},
onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; },
onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.y = evt.detail.y; },
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;

if (this.rigEl.sceneEl.is('vr-mode')) {
if (Math.abs(this.rightThumbstickInput.x) > 0.1) { this.rigEl.object3D.rotation.y -= this.rightThumbstickInput.x * data.rotationSpeed * dt; }
}

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); }
if (Math.abs(this.leftThumbstickInput.y) > 0.1) { this.moveDirection.add(this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y)); }
if (Math.abs(this.leftThumbstickInput.x) > 0.1) { this.moveDirection.add(this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.x)); }

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);
lerpFactor = data.damping;
}
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));
}

let verticalMovement = 0;
if (this.rigEl.sceneEl.is('vr-mode') && Math.abs(this.rightThumbstickInput.y) > 0.1) {
verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt;
}

const nextY = position.y + verticalMovement;
position.y = THREE.MathUtils.clamp(nextY, data.groundY, data.ceilingY);
},
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' }, // BVHファイルのURL
loop: { type: 'boolean', default: true }, // アニメーションをループさせるか
},

init: function () {
console.log('✅ bvh-player: コンポーネント初期化');
this.mixer = null; // three.jsのアニメーションミキサー
this.vrmData = null; // ロードされたVRMのデータ

// VRMモデルのロード完了を待ってから処理を開始
this.el.addEventListener('vrm-loaded', (e) => {
console.log('✅ vrm-loaded: VRMモデルの読み込み完了', e.detail.vrm);
this.vrmData = e.detail.vrm;
this.loadBVH(this.data.src);
});
},

// BVHファイルを読み込む関数
loadBVH: function(url) {
console.log(`⏳ bvh-player: BVHファイルの読み込みを開始... URL: ${url}`);
const loader = new THREE.BVHLoader();
loader.load(url, (bvh) => {
console.log('✅ bvh-player: BVHファイルの読み込み成功', bvh);

// --- BVHボーン名とVRMヒューマノイドボーン名の対応表 ---
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'
};

console.log('⏳ bvh-player: ボーンのマッピングを開始...');
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) {
console.log(` 👍 マッピング成功: ${bvhNodeName} -> ${vrmBoneName}`);
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'));
console.log(`✅ bvh-player: 位置情報を除外。${filteredTracks.length}個のトラックが残りました。`);

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: アニメーション再生を開始しました。', this.mixer);

},
undefined, // onProgress コールバック
(error) => {
console.error('❌ bvh-player: BVHファイルの読み込みに失敗しました。', error);
console.error('CORSポリシーエラーの可能性があります。ローカルサーバーで実行しているか、BVHファイルのURLが正しいか確認してください。');
});
},

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 id="rig" position="0 0 2.5" camera-relative-controls="groundY: -1.5">
<a-entity id="player">
<a-entity id="camera" camera look-controls position="0 1.6 0"></a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right"></a-entity>
</a-entity>
</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
controls
currentVelocity
data
desiredVelocity
dt
eftThumbstickInput
eraWorldQuaternion
ffectiveLerpFactor
filteredTracks
ghtThumbstickInput
height
id
intensity
isInputting
keys
leftHand
length
lerpFactor
loader
mixer
moveDirection
name
newClip
newTrack
newTracks
nextY
player
position
renderer
rigEl
rightHand
rotation
src
track
type
url) { console.log -------( Function )
verticalMovement
vrm
vrmBoneName
vrmBoneNode
vrmData
width
x
y
コンポーネント定義エリア