junkerstock
 vrbx2-1 

<!DOCTYPE html>

<!-- ColorfulxMelody Sour式初音ミクVer.1.02 & Sour式鏡音リンVer.2.01 -->

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Babylon Template with VR Joystick Movement & UI</title>

<style>
html,
body {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}

#renderCanvas {
width: 100%;
height: 100%;
touch-action: none;
}
</style>

</head>

<body>
<canvas id="renderCanvas" touch-action="none"></canvas>

<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
<script src="https://cdn.babylonjs.com/havok/HavokPhysics_umd.js"></script>
<script src="https://www.unpkg.com/babylon-mmd/umd/babylon.mmd.min.js"></script>
<script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>

<script>
const canvas = document.getElementById("renderCanvas"); // Get the canvas element
const engine = new BABYLON.Engine(canvas, true); // Generate the BABYLON 3D engine


const assetsPath = 'assets/'
const pmxPath = assetsPath + 'mmd/miku/'
const pmxModel = pmxPath + 'White.pmx'

const vmdPath = assetsPath + 'ColorfulxMelody/'
const vmdModel = vmdPath + 'mmd_ColorfulxMelody_MIK.vmd'

const wavPath = assetsPath + 'ColorfulxMelody/'
const wavModel = wavPath + 'pv_709.wav'

const camPath = assetsPath + 'ColorfulxMelody/';
const camModel = camPath + 'came2.vmd'

// ▼▼▼ ここから追加 ▼▼▼
// --- 2体目のモデル ---
const pmxPath_2 = assetsPath + 'mmd/rin/';
const pmxModel_2 = pmxPath_2 + 'Black.pmx';

// --- 2体目のモーション ---
const vmdPath_2 = assetsPath + 'ColorfulxMelody/'
const vmdModel_2 = vmdPath_2 + 'mmd_ColorfulxMelody_RIN.vmd'; // 2体目のモーションファイル
// ▲▲▲ ここまで追加 ▲▲▲

const offsetY = -100


// Add your code here matching the playground format
const createScene = async function (engine) {


const scene = new BABYLON.Scene(engine);
let adt;
let coordinateTextBlock;
let mmdModel;
let latestBonePosition = new BABYLON.Vector3(0, 0, 0); // ボーン座標を格納するグローバル変数

const camera = new BABYLONMMD.MmdCamera("mmdCamera", new BABYLON.Vector3(0, 10, 0), scene);

const ground = BABYLON.MeshBuilder.CreateGround("Ground", { width: 200, height: 200, subdivisions: 2, updatable: false }, scene);
ground.receiveShadows = true;

const hemisphericLight = new BABYLON.HemisphericLight("HemisphericLight", new BABYLON.Vector3(0, 10, -10), scene);
hemisphericLight.intensity = 0.3;
hemisphericLight.specular = new BABYLON.Color3(0, 0, 0);
hemisphericLight.groundColor = new BABYLON.Color3(1.1, 1.1, 1.1);


const shadowLight = new BABYLON.DirectionalLight("shadowLight", new BABYLON.Vector3(-1, -2, 1), scene);
shadowLight.position = new BABYLON.Vector3(20, 100, 100);

const shadowGenerator = new BABYLON.ShadowGenerator(1024, shadowLight, true);
shadowGenerator.useBlurExponentialShadowMap = true;
shadowGenerator.blurKernel = 32;

const mmdMesh = await BABYLON.SceneLoader.ImportMeshAsync(undefined, pmxModel, undefined, scene).then((result) => result.meshes[0]);

shadowGenerator.addShadowCaster(mmdMesh);
mmdMesh.receiveShadows = true;


// ▼▼▼ ここから追加 ▼▼▼
// --- 2体目のメッシュを読み込む ---
const mmdMesh_2 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, pmxModel_2, undefined, scene).then((result) => result.meshes[0]);

// 1体目と重ならないように、X軸方向に2ずらす
mmdMesh_2.position.x = 2;

// 2体目も影を落とすように設定
shadowGenerator.addShadowCaster(mmdMesh_2);
mmdMesh_2.receiveShadows = true;
// ▲▲▲ ここまで追加 ▲▲▲


const vmdLoader = new BABYLONMMD.VmdLoader(scene);
const modelMotion = await vmdLoader.loadAsync("model_motion", vmdModel);


const havokInstance = await HavokPhysics();
const havokPlugin = new BABYLON.HavokPlugin(true, havokInstance);
scene.enablePhysics(new BABYLON.Vector3(0, -9.8 * 8, 0), havokPlugin);

const mmdRuntime = new BABYLONMMD.MmdRuntime(scene, new BABYLONMMD.MmdPhysics(scene));
mmdRuntime.register(scene);
mmdModel = mmdRuntime.createMmdModel(mmdMesh);


// ▼▼▼ このコードを追加してボーン名を調べる ▼▼▼
if (mmdMesh.skeleton) {
console.log("--- 利用可能なボーン名リスト ---");
mmdMesh.skeleton.bones.forEach(bone => {
console.log(bone.name);
});
console.log("---------------------------------");
}
// ▲▲▲ ここまで追加 ▲▲▲


mmdModel.addAnimation(modelMotion);
mmdModel.setAnimation("model_motion");



// ▼▼▼ ここから追加 ▼▼▼
// --- 2体目のモーションを読み込んで適用 ---
const modelMotion_2 = await vmdLoader.loadAsync("model_motion_2", vmdModel_2); // 1つ目と違う名前を付ける

const mmdModel_2 = mmdRuntime.createMmdModel(mmdMesh_2); // 2体目のMMDモデルを作成
mmdModel_2.addAnimation(modelMotion_2);
mmdModel_2.setAnimation("model_motion_2");
// ▲▲▲ ここまで追加 ▲▲▲



mmdRuntime.setCamera(camera);
const cameraMotion = await vmdLoader.loadAsync("camera_motion", camModel);
camera.addAnimation(cameraMotion);
camera.setAnimation("camera_motion");

const audioPlayer = new BABYLONMMD.StreamAudioPlayer(scene);
audioPlayer.preservesPitch = false;
audioPlayer.source = wavModel;

mmdRuntime.setAudioPlayer(audioPlayer);

mmdRuntime.playAnimation();

const mmdPlayerControl = new BABYLONMMD.MmdPlayerControl(scene, mmdRuntime, audioPlayer);
mmdPlayerControl.showPlayerControl();


// ▼▼▼ こちらが正しいコードです ▼▼▼
// sceneオブジェクトの「物理演算完了後」イベントに処理を追加する
scene.onAfterPhysicsObservable.add(() => {
try {
if (mmdModel && mmdModel.skeleton) {
const targetBone = mmdModel.skeleton.bones.find(b => b.name === '上半身');
if (targetBone) {
// グローバル変数に最新の座標をコピー

const finalMatrix = targetBone.getFinalMatrix(); // ボーンの最終的なワールド行列を取得
finalMatrix.decompose(undefined, undefined, latestBonePosition); // 行列から位置情報だけを抽出する


}
}
} catch (e) {
// エラーが発生してもコンソールに表示するだけで、プログラムは止めない
console.error("onAfterPhysicsObservableでエラー:", e);
}
});


// =================================================================
// ▼▼▼ VR機能、UIパネル、ジョイスティック移動処理 ▼▼▼
// =================================================================
const xr = await scene.createDefaultXRExperienceAsync({});

// ▼▼▼ 追従モードの状態を管理する変数 ▼▼▼
let isFollowing = false;
let followOffset = new BABYLON.Vector3();


// --- 左手コントローラーにUIパネルを追加 ---
xr.input.onControllerAddedObservable.add((controller) => {
if (controller.inputSource.handedness === 'left') {

let uiParent = scene.getTransformNodeByName("leftUIParent");

if (!uiParent) {
// 1. UIの親となるTransformNodeを作成
uiParent = new BABYLON.TransformNode("leftUIParent", scene);
uiParent.position = new BABYLON.Vector3(0, 0.05, 0.08);
uiParent.rotation = new BABYLON.Vector3(Math.PI / 4, 0, 0);

// 2. UIを表示するための平面メッシュを作成(高さを広げる)
const uiPlane = BABYLON.MeshBuilder.CreatePlane("uiPlane", { width: 0.25, height: 0.6 }, scene);
uiPlane.parent = uiParent;
uiPlane.visibility = 0.9;

// 3. 平面メッシュにGUIテクスチャを適用
adt = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(uiPlane);

// 4. ボタンを縦に並べるためのStackPanelを作成
const stackPanel = new BABYLON.GUI.StackPanel();
stackPanel.isVertical = true;
stackPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGN_MENT_TOP; // 上揃えに変更
adt.addControl(stackPanel);

const buttonHeight = "60px";


// 5. 「再生」ボタン
const playButton = BABYLON.GUI.Button.CreateSimpleButton("playButton", "再生");
playButton.width = "100%"; playButton.height = buttonHeight; playButton.color = "white";
playButton.background = "#4CAF50"; playButton.fontSize = 24;

// ▼▼▼ このブロックに置き換える ▼▼▼
playButton.onPointerClickObservable.add(() => {
// アニメーションが再生中でなく、かつ最終フレームに達しているかを確認
// if (!mmdRuntime.isAnimationPlaying && mmdRuntime.currentFrame >= mmdRuntime.endFrame) {
if (mmdRuntime.isAnimationPlaying || mmdRuntime.currentFrame >= mmdRuntime.endFrame) {
// 条件を満たす場合、アニメーションをフレーム0(先頭)に戻す
mmdRuntime.seekAnimation(0);
}

// アニメーションを再生する
mmdRuntime.playAnimation();
});
// ▲▲▲ ここまで ▲▲▲

stackPanel.addControl(playButton);


// 6. 「停止」ボタン
const stopButton = BABYLON.GUI.Button.CreateSimpleButton("stopButton", "停止");
stopButton.width = "100%"; stopButton.height = buttonHeight; stopButton.color = "white";
stopButton.background = "#f44336"; stopButton.fontSize = 24;
stopButton.onPointerClickObservable.add(() => { mmdRuntime.pauseAnimation(); });
stackPanel.addControl(stopButton);

// 7. 「正面」ボタン
const frontButton = BABYLON.GUI.Button.CreateSimpleButton("frontButton", "正面");
frontButton.width = "100%"; frontButton.height = buttonHeight; frontButton.color = "white";
frontButton.background = "#2196F3"; frontButton.fontSize = 24;
frontButton.onPointerClickObservable.add(() => {
isFollowing = false; // 追従モードを解除
xr.baseExperience.camera.position.set(0, 16, -20);
xr.baseExperience.camera.setTarget(new BABYLON.Vector3(0, 10, 0));
});
stackPanel.addControl(frontButton);

// 8. 「背面」ボタン
const backButton = BABYLON.GUI.Button.CreateSimpleButton("backButton", "背面");
backButton.width = "100%"; backButton.height = buttonHeight; backButton.color = "white";
backButton.background = "#FF9800"; backButton.fontSize = 24;
backButton.onPointerClickObservable.add(() => {
isFollowing = false; // 追従モードを解除
xr.baseExperience.camera.position.set(0, 16, 20);
xr.baseExperience.camera.setTarget(new BABYLON.Vector3(0, 10, 0));
});
stackPanel.addControl(backButton);

// ▼▼▼ 9. 「追従/解除」ボタンを作成 ▼▼▼
const followButton = BABYLON.GUI.Button.CreateSimpleButton("followButton", "追従");
followButton.width = "100%";
followButton.height = buttonHeight;
followButton.color = "white";
followButton.background = "#607D8B"; // グレー系の色
followButton.fontSize = 24;

followButton.onPointerClickObservable.add(() => {
isFollowing = !isFollowing; // 追従モードをトグル
if (isFollowing) {
// 追従開始:現在のカメラとモデルの「真の中心」との相対位置を計算して保存
const xrCamera = xr.baseExperience.camera;

followOffset = xrCamera.position.subtract(latestBonePosition);

followButton.textBlock.text = "解除";
followButton.background = "#9C27B0"; // 紫色に変更
} else {
// 追従解除
followButton.textBlock.text = "追従";
followButton.background = "#607D8B"; // 元の色に戻す
}
});
stackPanel.addControl(followButton);




// ▼▼▼ 追加するコード ▼▼▼
// 座標表示用のテキストブロック
coordinateTextBlock = new BABYLON.GUI.TextBlock("coordinateTextBlock");
coordinateTextBlock.text = "X: 0.00 Y: 0.00 Z: 0.00";
coordinateTextBlock.color = "white";
coordinateTextBlock.fontSize = 20;
coordinateTextBlock.height = "50px";
stackPanel.addControl(coordinateTextBlock);







// ▼▼▼ ここからが今回の追加機能 ▼▼▼
// 5. ボーン名を表示するためのボタン
const showBonesButton = BABYLON.GUI.Button.CreateSimpleButton("showBonesButton", "ボーン名表示");
showBonesButton.width = "100%";
showBonesButton.height = buttonHeight;
showBonesButton.color = "white";
showBonesButton.background = "#3F51B5"; // 藍色
showBonesButton.fontSize = 24;
stackPanel.addControl(showBonesButton);

// 6. ボーン名リストを表示するためのテキストブロック
const boneListTextBlock = new BABYLON.GUI.TextBlock("boneListTextBlock", "↑ボタンを押してボーン名を表示");
boneListTextBlock.color = "white";
boneListTextBlock.fontSize = 16;
boneListTextBlock.textWrapping = true; // 折り返しを有効にする
boneListTextBlock.resizeToFit = true; // テキスト量に合わせて高さを自動調整
boneListTextBlock.paddingTop = "10px";
stackPanel.addControl(boneListTextBlock);

// ボタンが押された時の処理
showBonesButton.onPointerClickObservable.add(() => {
let debugMessage = ""; // 表示するメッセージを格納する変数

if (mmdModel && mmdModel.skeleton) {
debugMessage += "スケルトンを発見。\n"; // \nは改行

const bones = mmdModel.skeleton.bones;
const boneCount = bones.length;

debugMessage += `ボーンの数: ${boneCount}個\n`;

if (boneCount > 0) {
// bone.nameの配列を作成し、カンマ区切りの文字列に変換
const boneNames = bones.map(bone => bone.name).join(', ');
debugMessage += "名前リスト: " + boneNames;
} else {
debugMessage += "ボーン配列は空です。";
}

} else if (mmdModel) {
debugMessage = "モデルはありますが、スケルトンが見つかりません。";
} else {
debugMessage = "MMDモデル自体が見つかりません。";
}

// 最終的なデバッグメッセージをテキストブロックに表示
boneListTextBlock.text = debugMessage;
});
// ▲▲▲ ここまで追加 ▲▲▲












}

uiParent.parent = controller.grip;
}
});




// VRモードの出入りを監視
xr.baseExperience.onStateChangedObservable.add((state) => {
if (state === BABYLON.WebXRState.IN_XR) {
camera.setAnimation(null);
} else if (state === BABYLON.WebXRState.NOT_IN_XR) {
camera.setAnimation("camera_motion");
isFollowing = false;
}
});






// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {
if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;



// ▼▼▼ ここから修正 ▼▼▼
// --- VRカメラ自身の座標をUIに表示する ---
// if (coordinateTextBlock) {const cameraPosition = xr.baseExperience.camera.position;
// coordinateTextBlock.text = `CAMERA X: ${cameraPosition.x.toFixed(2)} Y: ${cameraPosition.y.toFixed(2)} Z: ${cameraPosition.z.toFixed(2)}`;}
// ▲▲▲ ここまで修正 ▲▲▲






// ▼▼▼ ここからが最終修正 ▼▼▼
// mmdMeshと、最も重要なmmdMesh.skeletonが準備完了しているか、最初に確認する
if (mmdModel && mmdModel.skeleton) {

// グローバル変数に格納された最新のボーン座標をUIに表示する
if (coordinateTextBlock) {
coordinateTextBlock.text = `上半身 X: ${latestBonePosition.x.toFixed(2)} Y: ${latestBonePosition.y.toFixed(2)} Z: ${latestBonePosition.z.toFixed(2)}`;
}

// --- 追従モードの処理 ---


// --- 追従モードの処理 ---
if (isFollowing) {
// グローバル変数(上半身の現在位置)をコピーし、それにオフセットを足してカメラの目標位置を決めます
// ※ .clone() は、元の latestBonePosition の値を誤って変更しないための「おまじない」です
const desiredPosition = latestBonePosition.clone().add(followOffset);

// カメラの位置を、計算した目標位置へ更新します
xr.baseExperience.camera.position.copyFrom(desiredPosition);

// カメラの視線を、モデルの現在位置(上半身)に向けます
xr.baseExperience.camera.setTarget(latestBonePosition);
}



}
// ▲▲▲ ここまでが最終修正 ▲▲▲






// --- ジョイスティック操作の処理 ---
// isFollowingがfalseの時だけジョイスティック操作を許可
if (!isFollowing) {
const deltaMillis = engine.getDeltaTime();
const xrCamera = xr.baseExperience.camera;
// 左コントローラー: 平行移動
const leftController = xr.input.controllers.find(c => c.inputSource.handedness === 'left');
if (leftController?.motionController) {
const thumbstick = leftController.motionController.getComponent("xr-standard-thumbstick");
if (thumbstick?.axes) {
const moveSpeed = 6.0 * deltaMillis / 1000;
const forward = xrCamera.getDirection(BABYLON.Vector3.Forward());
forward.y = 0;
xrCamera.position.addInPlace(forward.normalize().scale(-thumbstick.axes.y * moveSpeed));
const right = xrCamera.getDirection(BABYLON.Vector3.Right());
xrCamera.position.addInPlace(right.scale(thumbstick.axes.x * moveSpeed));
}
}

// 右コントローラー: 回転と上下移動
const rightController = xr.input.controllers.find(c => c.inputSource.handedness === 'right');
if (rightController?.motionController) {
const thumbstick = rightController.motionController.getComponent("xr-standard-thumbstick");
if (thumbstick?.axes) {
const rotationThreshold = 0.2;
if (Math.abs(thumbstick.axes.x) > rotationThreshold) {
const rotSpeed = 0.4 * deltaMillis / 1000;
if (xrCamera.parent) {
xrCamera.parent.rotate(BABYLON.Vector3.Up(), thumbstick.axes.x * rotSpeed, BABYLON.Space.WORLD);
}
}
if (Math.abs(thumbstick.axes.y) > rotationThreshold) {
const verticalSpeed = 2.0 * deltaMillis / 1000;
xrCamera.position.y += -thumbstick.axes.y * verticalSpeed;
}
}
}
}
});








return scene;
};

(async function () {

canvas.focus();
const scene = await createScene(engine); //Call the createScene function


// Register a render loop to repeatedly render the scene
engine.runRenderLoop(async function () {
scene.render();
});

// Watch for browser/canvas resize events
window.addEventListener("resize", function () {
engine.resize();
});
})()

</script>
</body>

</html>


使用変数

-------( Function )
action
adt
assetsPath
audioPlayer
b
backButton
background
blurKernel
bone
boneCount
boneListTextBlock
boneNames
bones
br> // Register a render loop to repeatedly render the scene engine.runRenderLoop -------( Function )
buttonHeight
c
camera
cameraMotion
cameraPosition
camModel
camPath
canvas
charset
color
content
createScene
debugMessage
deltaMillis
desiredPosition
engine
equiv
finalMatrix
followButton
followOffset
fontSize
forward
frontButton
ground
groundColor
handedness
havokInstance
havokPlugin
height
hemisphericLight
id
intensity
isFollowing
isVertical
latestBonePosition
leftController
mmdMesh
mmdMesh_2
mmdModel
mmdModel_2
mmdPlayerControl
mmdRuntime
modelMotion
modelMotion_2
moveSpeed
name
offsetY
oordinateTextBlock
paddingTop
parent
playButton
pmxModel
pmxModel_2
pmxPath
pmxPath_2
ponentialShadowMap
position
preservesPitch
receiveShadows
resizeToFit
right
rightController
rotation
rotationThreshold
rotSpeed
scene
shadowGenerator
shadowLight
showBonesButton
source
specular
src
stackPanel
state
stopButton
targetBone
text
textWrapping
thumbstick
uiParent
uiPlane
verticalAlignment
verticalSpeed
visibility
vmdLoader
vmdModel
vmdModel_2
vmdPath
vmdPath_2
wavModel
wavPath
width
x
xmlns
xr
xrCamera
y