junkerstock
 vrbx4-test1-4x 

<!DOCTYPE html>

<!-- 追従に失敗 -->

<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 (Manual Follow Logic)</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/v8.31.0/babylon.js"></script>
<script src="https://cdn.babylonjs.com/v8.31.0/gui/babylon.gui.min.js"></script>
<script src="https://cdn.babylonjs.com/v8.31.0/havok/HavokPhysics_umd.js"></script>
<script src="https://www.unpkg.com/babylon-mmd@1.0.0/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


// ★★★ 設定項目ここから ★★★
// --------------------------------------------------------------------------------
// 1. パス設定
// --------------------------------------------------------------------------------
const assetsPath = 'assets/';
const motionFolder = assetsPath + 'SummerIdol2020REMAKE/';


// --------------------------------------------------------------------------------
// 2. モデル1 (Rin) の設定
// --------------------------------------------------------------------------------
const MODEL_1_PATH = assetsPath + 'mmd/YYBrin/';
const MODEL_1_PMX = MODEL_1_PATH + 'YYB Kagamine Rin_10th_v1.0.pmx';
const MODEL_1_VMD = motionFolder + 'mmd_SummerIdol_RIN.vmd';
const MODEL_1_OFFSET_X = -2.0; // X軸オフセット (左端へ2.0m)
const MODEL_1_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_1_VISIBLE = true; // モデル1の表示 (true: ON, false: OFF)


// --------------------------------------------------------------------------------
// 3. モデル2 (Miku) の設定
// --------------------------------------------------------------------------------
const MODEL_2_PATH = assetsPath + 'mmd/pumiku/';
const MODEL_2_PMX = MODEL_2_PATH + 'pumiku.pmx';
const MODEL_2_VMD = motionFolder + 'mmd_SummerIdol_MIK.vmd';
const MODEL_2_OFFSET_X = 0.0; // X軸オフセット (中央)
const MODEL_2_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_2_VISIBLE = true; // モデル2の表示 (true: ON, false: OFF)


// --------------------------------------------------------------------------------
// 4. モデル3 (hanamiku) の設定
// --------------------------------------------------------------------------------
const MODEL_3_PATH = assetsPath + 'mmd/hanamiku/';
const MODEL_3_PMX = MODEL_3_PATH + 'hanamiku.pmx';
const MODEL_3_VMD = motionFolder + 'mmd_SummerIdol_RIN.vmd'; // Rinと同じモーションを使用
const MODEL_3_OFFSET_X = 5.0; // X軸オフセット
const MODEL_3_OFFSET_Z = 18.0; // Z軸オフセット
const IS_MODEL_3_VISIBLE = true; // モデル3の表示 (true: ON, false: OFF)


// --------------------------------------------------------------------------------
// 5. モーション・オーディオ・カメラの設定 (3体共通)
// --------------------------------------------------------------------------------
const AUDIO_WAV = motionFolder + 'pv_624.wav';
const CAMERA_VMD = motionFolder + 'Camera_MAIN.vmd';


// --------------------------------------------------------------------------------
// 6. ステージの設定
// --------------------------------------------------------------------------------
const STAGE_PATH = assetsPath + 'stage/taikukan/';
const STAGE_PMX = STAGE_PATH + 'taikukan.pmx';

// ステージの座標調整 (ステージを中央の原点から移動・回転させる場合に使う)
const STAGE_POSITION_Y = 0; // Y軸位置 (例: -1.0 で1m沈ませる)
const STAGE_SCALE = 1.0; // 拡大率 (例: 1.2 で1.2倍に)
const STAGE_ROTATION_Y = 0; // Y軸回転 (ラジアン, 例: Math.PI / 2 で90度回転)
const IS_STAGE_VISIBLE = true; // ステージの表示 (true: ON, false: OFF)


// --------------------------------------------------------------------------------
// 7. VRカメラの初期設定
// --------------------------------------------------------------------------------
const VR_FRONT_CAMERA_POS = new BABYLON.Vector3(0, 16, -20); // 正面ボタンのカメラ位置
const VR_FRONT_CAMERA_TARGET = new BABYLON.Vector3(0, 10, 0); // 正面ボタンのカメラ視線目標
const VR_BACK_CAMERA_POS = new BABYLON.Vector3(0, 16, 20); // 背面ボタンのカメラ位置


// --------------------------------------------------------------------------------
// 8. 初期動作設定
// --------------------------------------------------------------------------------
let isLooping = true; // アニメーションの初期リピート設定
// --------------------------------------------------------------------------------
// ★★★ 設定項目ここまで ★★★


// --- グローバル変数 (動的機能用) ---
// モデルのメッシュとMMDモデルインスタンス (個別変数に戻す)
let mmdMesh_1 = null;
let mmdModel_1 = null;
let mmdMesh_2 = null;
let mmdModel_2 = null;
let mmdMesh_3 = null;
let mmdModel_3 = null;

// ★修正点1: モデルごとの最新ボーン座標を格納
let latestBonePosition_1 = new BABYLON.Vector3();
let latestBonePosition_2 = new BABYLON.Vector3();
let latestBonePosition_3 = new BABYLON.Vector3();

// 追従機能用変数
let trackedModelId = 0; // 追跡対象のID (1, 2, 3, 0: なし)
let isFollowing = false; // 追跡モードON/OFF
let followOffset = new BABYLON.Vector3(); // カメラとモデルのオフセット


// Add your code here matching the playground format
const createScene = async function (engine) {
const scene = new BABYLON.Scene(engine);

let adt;
let coordinateTextBlock;

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;


// --- ステージのメッシュを読み込む ---
let stageMesh = null;
if (IS_STAGE_VISIBLE) {
stageMesh = await BABYLON.SceneLoader.ImportMeshAsync(undefined, STAGE_PMX, undefined, scene).then((result) => result.meshes[0]);

// ★★★ 設定項目反映: ステージの位置と大きさを調整 ★★★
stageMesh.position.y = STAGE_POSITION_Y;
stageMesh.scaling = new BABYLON.Vector3(STAGE_SCALE, STAGE_SCALE, STAGE_SCALE);
stageMesh.rotation.y = STAGE_ROTATION_Y;
stageMesh.isVisible = IS_STAGE_VISIBLE;
// ★★★ 設定項目反映ここまで ★★★

// 地面と影の調整
stageMesh.getChildMeshes().forEach(mesh => mesh.receiveShadows = true);
ground.dispose(); // 元の地面を削除
}


const vmdLoader = new BABYLONMMD.VmdLoader(scene);

// --- MMDランタイムと物理演算の初期化 ---
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);


// --- モデル1 (Rin) のロードと設定 ---
if (IS_MODEL_1_VISIBLE) {
mmdMesh_1 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, MODEL_1_PMX, undefined, scene).then((result) => result.meshes[0]);
shadowGenerator.addShadowCaster(mmdMesh_1);
mmdMesh_1.receiveShadows = true;
mmdMesh_1.position.x = MODEL_1_OFFSET_X;
mmdMesh_1.position.z = MODEL_1_OFFSET_Z;
mmdMesh_1.name = "Model 1 (Rin)"; // 名前設定

const modelMotion_1 = await vmdLoader.loadAsync("model_motion_1", MODEL_1_VMD);
mmdModel_1 = mmdRuntime.createMmdModel(mmdMesh_1);
const modelAnimationHandle_1 = mmdModel_1.createRuntimeAnimation(modelMotion_1);
mmdModel_1.setRuntimeAnimation(modelAnimationHandle_1);

// 最初の表示ONモデルをデフォルトの追跡対象に設定
if (trackedModelId === 0) {
trackedModelId = 1;
}
}

// --- モデル2 (Miku) のロードと設定 ---
if (IS_MODEL_2_VISIBLE) {
mmdMesh_2 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, MODEL_2_PMX, undefined, scene).then((result) => result.meshes[0]);
shadowGenerator.addShadowCaster(mmdMesh_2);
mmdMesh_2.receiveShadows = true;
mmdMesh_2.position.x = MODEL_2_OFFSET_X;
mmdMesh_2.position.z = MODEL_2_OFFSET_Z;
mmdMesh_2.name = "Model 2 (Miku)"; // 名前設定

const modelMotion_2 = await vmdLoader.loadAsync("model_motion_2", MODEL_2_VMD);
mmdModel_2 = mmdRuntime.createMmdModel(mmdMesh_2)
const modelAnimationHandle_2 = mmdModel_2.createRuntimeAnimation(modelMotion_2);
mmdModel_2.setRuntimeAnimation(modelAnimationHandle_2);

if (trackedModelId === 0) {
trackedModelId = 2;
}
}

// --- モデル3 (hanamiku) のロードと設定 ---
if (IS_MODEL_3_VISIBLE) {
mmdMesh_3 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, MODEL_3_PMX, undefined, scene).then((result) => result.meshes[0]);
shadowGenerator.addShadowCaster(mmdMesh_3);
mmdMesh_3.receiveShadows = true;
mmdMesh_3.position.x = MODEL_3_OFFSET_X;
mmdMesh_3.position.z = MODEL_3_OFFSET_Z;
mmdMesh_3.name = "Model 3 (hanamiku)"; // 名前設定

const modelMotion_3 = await vmdLoader.loadAsync("model_motion_3", MODEL_3_VMD);
mmdModel_3 = mmdRuntime.createMmdModel(mmdMesh_3)
const modelAnimationHandle_3 = mmdModel_3.createRuntimeAnimation(modelMotion_3);
mmdModel_3.setRuntimeAnimation(modelAnimationHandle_3);

if (trackedModelId === 0) {
trackedModelId = 3;
}
}


// カメラにモーションを適用
mmdRuntime.addAnimatable(camera);
const cameraMotion = await vmdLoader.loadAsync("camera_motion", CAMERA_VMD);
const cameraMotionHandle = camera.createRuntimeAnimation(cameraMotion);
camera.setRuntimeAnimation(cameraMotionHandle);


// オーディオと再生
const audioPlayer = new BABYLONMMD.StreamAudioPlayer(scene);
audioPlayer.preservesPitch = false;
audioPlayer.source = AUDIO_WAV;
mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();


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


// 物理演算完了後、ONになっている全モデルの上半身ボーン座標を格納
scene.onAfterPhysicsObservable.add(() => {
try {
// モデル1の座標を格納
if (mmdModel_1 && mmdModel_1.skeleton) {
const targetBone = mmdModel_1.skeleton.bones.find(b => b.name === '上半身')
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_1)
}
}
// モデル2の座標を格納
if (mmdModel_2 && mmdModel_2.skeleton) {
const targetBone = mmdModel_2.skeleton.bones.find(b => b.name === '上半身')
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_2)
}
}
// モデル3の座標を格納
if (mmdModel_3 && mmdModel_3.skeleton) {
const targetBone = mmdModel_3.skeleton.bones.find(b => b.name === '上半身')
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_3)
}
}
} catch (e) {
console.error("onAfterPhysicsObservableでエラー:", e);
}
});

// =================================================================
// VR機能、UIパネル、ジョイスティック移動処理
// =================================================================
const xr = await scene.createDefaultXRExperienceAsync({
uiOptions: {
sessionMode: 'immersive-vr',
supportedSessionModes: ['immersive-vr', 'immersive-ar']
}
});

// --- 追従ボタンを動的に生成・更新する関数 ---
const updateFollowButtons = (stackPanel, xrCamera) => {
// 既存の動的な追従ボタンを全て削除
const controlsToRemove = stackPanel.controls.filter(c => c.name && c.name.startsWith("followButton_"));
controlsToRemove.forEach(c => stackPanel.removeControl(c));

const buttonHeight = "60px";
const models = [
{ id: 1, visible: IS_MODEL_1_VISIBLE, model: mmdModel_1, position: latestBonePosition_1, name: "Rin" },
{ id: 2, visible: IS_MODEL_2_VISIBLE, model: mmdModel_2, position: latestBonePosition_2, name: "Miku" },
{ id: 3, visible: IS_MODEL_3_VISIBLE, model: mmdModel_3, position: latestBonePosition_3, name: "Hanamiku" }
];

let insertionIndex = stackPanel.controls.findIndex(c => c.name === "arButton") + 1; // ARボタンの直後に挿入

// 表示ONのモデルごとにボタンを生成
models.forEach((config) => {
// 表示がONで、かつMMDモデルインスタンスが生成されているモデルのみボタンを表示
if (config.visible && config.model) {

const isCurrentlyTracking = trackedModelId === config.id && isFollowing;
const buttonText = isCurrentlyTracking ? `解除 ${config.id}` : `追従 ${config.id}`;
const buttonColor = isCurrentlyTracking ? "#9C27B0" : "#607D8B";

const followButton = BABYLON.GUI.Button.CreateSimpleButton(`followButton_${config.id}`, buttonText);
followButton.width = "100%";
followButton.height = buttonHeight;
followButton.color = "white";
followButton.background = buttonColor;
followButton.fontSize = 24;
followButton.name = `followButton_${config.id}`; // 名前を設定

followButton.onPointerClickObservable.add(() => {
// クリック時の追跡状態をチェック
const clickedIsCurrentlyTracking = trackedModelId === config.id && isFollowing;

// カメラのインスタンスを取得
const currentXrCamera = xr.baseExperience.camera;

if (clickedIsCurrentlyTracking) {
// 既に追跡中のモデルのボタンを押した場合 -> 追跡解除 (ジョイスティック移動に戻る)
isFollowing = false;
trackedModelId = 0; // 追跡対象なし
} else {
// 別のモデルのボタンを押した場合 -> そのモデルを追跡
isFollowing = true;
trackedModelId = config.id;

// 最新座標を取得してオフセットを計算
// config.position は onAfterPhysicsObservable で更新された最新の上半身座標
followOffset = currentXrCamera.position.subtract(config.position);
}

// UIの状態を全体的に再描画
updateFollowButtons(stackPanel, currentXrCamera);
});

// 指定位置にボタンを挿入
stackPanel.addControl(followButton);
stackPanel.controls.splice(insertionIndex, 0, stackPanel.controls.pop());
insertionIndex++;
}
});

// 座標表示テキストブロックの位置をスタックパネルの末尾に移動(UI更新時に再配置するため)
if (coordinateTextBlock) {
stackPanel.addControl(coordinateTextBlock);
}
};


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

let uiParent = scene.getTransformNodeByName("leftUIParent");
const xrCamera = xr.baseExperience.camera; // カメラへの参照

if (!uiParent) {
// 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);

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

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

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

const buttonHeight = "60px";

// --- 静的なボタン群 ---
// 5. 「最初から」ボタン
const resetButton = BABYLON.GUI.Button.CreateSimpleButton("resetButton", "最初から");
resetButton.width = "100%"; resetButton.height = buttonHeight; resetButton.color = "white";
resetButton.background = "#4CAF50"; resetButton.fontSize = 24;
resetButton.onPointerClickObservable.add(() => { mmdRuntime.seekAnimation(0); });
stackPanel.addControl(resetButton);

// 6. 「再生/停止」トグルボタン
const playPauseButton = BABYLON.GUI.Button.CreateSimpleButton("playPauseButton", "停止")
playPauseButton.width = "100%"; playPauseButton.height = buttonHeight; playPauseButton.color = "white";
playPauseButton.background = "#f44336"; playPauseButton.fontSize = 24;
playPauseButton.onPointerClickObservable.add(() => {
if (mmdRuntime.isAnimationPlaying) {
mmdRuntime.pauseAnimation(); playPauseButton.textBlock.text = "再生"; playPauseButton.background = "#2196F3";
} else { 
mmdRuntime.playAnimation(); playPauseButton.textBlock.text = "停止"; playPauseButton.background = "#f44336";
}
});
stackPanel.addControl(playPauseButton);

// 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; // 追従モードを解除
trackedModelId = 0; // 追従対象なし
updateFollowButtons(stackPanel, xrCamera); // UI更新
xrCamera.position.copyFrom(VR_FRONT_CAMERA_POS);
xrCamera.setTarget(VR_FRONT_CAMERA_TARGET);
});
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; // 追従モードを解除
trackedModelId = 0; // 追従対象なし
updateFollowButtons(stackPanel, xrCamera); // UI更新
xrCamera.position.copyFrom(VR_BACK_CAMERA_POS);
xrCamera.setTarget(VR_FRONT_CAMERA_TARGET);
});
stackPanel.addControl(backButton);


// 9. 「リピート」トグルボタン
const repeatButton = BABYLON.GUI.Button.CreateSimpleButton("repeatButton", isLooping ? "リピート: オン" : "リピート: オフ");
repeatButton.width = "100%"; repeatButton.height = buttonHeight; repeatButton.color = "white";
repeatButton.background = isLooping ? "#FFC107" : "#795548";
repeatButton.fontSize = 24;
repeatButton.onPointerClickObservable.add(() => {
isLooping = !isLooping; 
if (isLooping) {
repeatButton.textBlock.text = "リピート: オン"; repeatButton.background = "#FFC107";
} else {
repeatButton.textBlock.text = "リピート: オフ"; repeatButton.background = "#795548";
}
});
stackPanel.addControl(repeatButton);

// 10. 「ARモード」ボタン
const arButton = BABYLON.GUI.Button.CreateSimpleButton("arButton", "ARモード");
arButton.name = "arButton";
arButton.width = "100%"; arButton.height = buttonHeight; arButton.color = "white";
arButton.background = "#9C27B0"; arButton.fontSize = 24;
arButton.onPointerClickObservable.add(async () => {
if (xr.baseExperience.state === BABYLON.WebXRState.IN_XR) { await xr.baseExperience.exitXRAsync(); }
await xr.baseExperience.enterXRAsync("immersive-ar", "local-floor");
});
stackPanel.addControl(arButton);

// --- 動的な追従ボタンの追加 ---
updateFollowButtons(stackPanel, xrCamera);


// 座標表示用のテキストブロック
coordinateTextBlock = new BABYLON.GUI.TextBlock("coordinateTextBlock");
coordinateTextBlock.text = "追従対象 (未選択)";
coordinateTextBlock.color = "white";
coordinateTextBlock.fontSize = 20;
coordinateTextBlock.height = "50px";
stackPanel.addControl(coordinateTextBlock);


// ボーン名表示ボタンとテキストブロック (デバッグ用)
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);

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 = "";
let model = null;
if (trackedModelId === 1) model = mmdModel_1;
else if (trackedModelId === 2) model = mmdModel_2;
else if (trackedModelId === 3) model = mmdModel_3;

if (model && model.mesh) {
debugMessage += `現在追跡対象: ${model.mesh.name}\n`;
}
if (model && model.skeleton) {
const bones = model.skeleton.bones;
debugMessage += `ボーンの数: ${bones.length}個\n`;
const boneNames = bones.slice(0, 15).map(bone => bone.name).join(', ') + (bones.length > 15 ? ', ...' : '');
debugMessage += "名前リスト(一部): " + boneNames;
} else {
debugMessage += "追跡対象モデルが未選択または未ロードです。";
}
boneListTextBlock.text = debugMessage;
});
}

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


// VRモード切替時のアニメーション制御
xr.baseExperience.onStateChangedObservable.add((state) => {
const stackPanel = adt ? adt.getChildren()[0] : null;
const xrCamera = xr.baseExperience.camera;

if (state === BABYLON.WebXRState.IN_XR) {
camera.setRuntimeAnimation(null);

if (xr.baseExperience.sessionManager.sessionMode === 'immersive-ar') {
ground.visibility = false;
if (stageMesh) {
stageMesh.isVisible = false;
stageMesh.getChildMeshes().forEach(mesh => {mesh.isVisible = false;});
}
}
} else if (state === BABYLON.WebXRState.NOT_IN_XR) {
camera.setRuntimeAnimation(cameraMotionHandle);
isFollowing = false;
trackedModelId = 0; // 追従解除
if (stackPanel) {
updateFollowButtons(stackPanel, xrCamera); // UIリセット
}
ground.visibility = true;
if (stageMesh) {
stageMesh.isVisible = IS_STAGE_VISIBLE;
stageMesh.getChildMeshes().forEach(mesh => {mesh.isVisible = IS_STAGE_VISIBLE;});
}
}
});


// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {
let currentBonePos = null;
let currentModelName = "未選択";

// 手動リピートロジック
if (isLooping && mmdRuntime.isAnimationPlaying) {
const currentTime = mmdRuntime.currentTime;
const duration = mmdRuntime.animationDuration;

if (duration > 0 && currentTime >= duration * 0.999) {
mmdRuntime.seekAnimation(0);
mmdRuntime.playAnimation();
}
}

if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;

// 追従対象のモデルと座標を取得
if (trackedModelId === 1 && mmdModel_1) {
currentBonePos = latestBonePosition_1;
currentModelName = mmdModel_1.mesh.name;
} else if (trackedModelId === 2 && mmdModel_2) {
currentBonePos = latestBonePosition_2;
currentModelName = mmdModel_2.mesh.name;
} else if (trackedModelId === 3 && mmdModel_3) {
currentBonePos = latestBonePosition_3;
currentModelName = mmdModel_3.mesh.name;
}


// --- 追従モードの処理 ---
if (coordinateTextBlock) {
if (currentBonePos && trackedModelId !== 0) {
coordinateTextBlock.text = `追従対象 ${currentModelName} X: ${currentBonePos.x.toFixed(2)} Y: ${currentBonePos.y.toFixed(2)} Z: ${currentBonePos.z.toFixed(2)}`;
} else {
coordinateTextBlock.text = `追従なし (ジョイスティック移動)`;
}
}

if (isFollowing && currentBonePos) {
const desiredPosition = currentBonePos.clone().add(followOffset);
xr.baseExperience.camera.position.copyFrom(desiredPosition);
}


// --- ジョイスティック操作の処理 ---
// 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
arButton
assetsPath
audioPlayer
AUDIO_WAV
b
backButton
background
blurKernel
bone
boneListTextBlock
boneNames
bones
br> // Register a render loop to repeatedly render the scene engine.runRenderLoop -------( Function )
buttonColor
buttonHeight
buttonText
c
camera
cameraMotion
cameraMotionHandle
CAMERA_VMD
canvas
charset
color
content
controlsToRemove
createScene
currentBonePos
currentModelName
currentTime
currentXrCamera
debugMessage
deltaMillis
desiredPosition
duration
engine
equiv
finalMatrix
followButton
followOffset
fontSize
forward
frontButton
ground
groundColor
handedness
havokInstance
havokPlugin
height
hemisphericLight
id
insertionIndex
intensity
isFollowing
isLooping
isVertical
isVisible
IS_MODEL_1_VISIBLE
IS_MODEL_2_VISIBLE
IS_MODEL_3_VISIBLE
IS_STAGE_VISIBLE
lAnimationHandle_1
lAnimationHandle_2
lAnimationHandle_3
leftController
mesh
mmdMesh_1
mmdMesh_2
mmdMesh_3
mmdModel_1
mmdModel_2
mmdModel_3
mmdPlayerControl
mmdRuntime
model
modelMotion_1
modelMotion_2
modelMotion_3
models
MODEL_1_OFFSET_X
MODEL_1_OFFSET_Z
MODEL_1_PATH
MODEL_1_PMX
MODEL_1_VMD
MODEL_2_OFFSET_X
MODEL_2_OFFSET_Z
MODEL_2_PATH
MODEL_2_PMX
MODEL_2_VMD
MODEL_3_OFFSET_X
MODEL_3_OFFSET_Z
MODEL_3_PATH
MODEL_3_PMX
MODEL_3_VMD
motionFolder
moveSpeed
name
oordinateTextBlock
paddingTop
parent
pdateFollowButtons
playPauseButton
ponentialShadowMap
position
preservesPitch
receiveShadows
repeatButton
resetButton
resizeToFit
right
rightController
RONT_CAMERA_TARGET
rotation
rotationThreshold
rotSpeed
R_FRONT_CAMERA_POS
scaling
scene
sCurrentlyTracking
sessionMode
shadowGenerator
shadowLight
showBonesButton
source
specular
src
stackPanel
stageMesh
STAGE_PATH
STAGE_PMX
STAGE_POSITION_Y
STAGE_ROTATION_Y
STAGE_SCALE
state
targetBone
testBonePosition_1
testBonePosition_2
testBonePosition_3
text
textWrapping
thumbstick
trackedModelId
uiParent
uiPlane
verticalAlignment
verticalSpeed
visibility
vmdLoader
VR_BACK_CAMERA_POS
width
x
xmlns
xr
xrCamera
y
z