junkerstock
 vrm-text-bvh3 

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