junkerstock
 vrbx6-5ーーまだ物理調整中 

<!DOCTYPE html>

<!-- バブリン 鈴木ヒナのスカートがめくれすぎて世に出せない。。。 -->
<!-- BabylonMMDシミュレータ quest3用 Ver0.3 -->
<!-- あきらめた機能 ・プロセスバー ・物理無効ボタン ・音ONOFFボタン・最初の音またはモーションの遅延 -->
<!-- 物理演算をフレームレート変動から 固定ステップ(常に1/60秒として計算)に変更 モデル読み込み時のオプション追加 -->



<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 (Config Optimized)</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>
window.addEventListener("DOMContentLoaded", function () {
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 + 'baburin/';


// --------------------------------------------------------------------------------
// 2. モデル1 の設定
// --------------------------------------------------------------------------------
const MODEL_1_PATH = assetsPath + 'mmd/SuzukiHina_ver1.30/';
const MODEL_1_PMX = MODEL_1_PATH + 'HINASUZUKI.pmx';
const MODEL_1_VMD = motionFolder + 'Bubblin_MMD-Motion_HinaSuzuki.vmd';
const MODEL_1_OFFSET_X = -1.0; // X軸オフセット (左端へ 1.0m)
const MODEL_1_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_1_VISIBLE = 1; // モデル1の表示 (true(1): ON, false(0): OFF)


// --------------------------------------------------------------------------------
// 3. モデル2 の設定
// --------------------------------------------------------------------------------
const MODEL_2_PATH = assetsPath + 'mmd/TanakaHime_ver1.30/';
const MODEL_2_PMX = MODEL_2_PATH + 'HIMETANAKA.pmx';
const MODEL_2_VMD = motionFolder + 'Bubblin_MMD-Motion_HimeTanaka.vmd';
const MODEL_2_OFFSET_X = 1.0; // X軸オフセット (右端へ 1.0m)
const MODEL_2_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_2_VISIBLE = 1; // モデル2の表示 (true(1): ON, false(0): OFF)


// --------------------------------------------------------------------------------
// 4. モデル3 の設定
// --------------------------------------------------------------------------------
const MODEL_3_PATH = assetsPath + 'mmd/arueteto/';
const MODEL_3_PMX = MODEL_3_PATH + 'arueteto.pmx';
const MODEL_3_VMD = motionFolder + 'RIN.vmd';
const MODEL_3_OFFSET_X = 5.0; // X軸オフセット (右端へ2.0m)
const MODEL_3_OFFSET_Z = 18.0; // Z軸オフセット
const IS_MODEL_3_VISIBLE = 0; // モデル3の表示 (true(1): ON, false(0): OFF)


// --------------------------------------------------------------------------------
// 5. モーション・オーディオ・カメラの設定 (3体共通)
// --------------------------------------------------------------------------------
const AUDIO_WAV = motionFolder + 'baburin.mp3';
const CAMERA_VMD = "";
// const CAMERA_VMD = motionFolder + 'babu-came.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, 12, -10); // 正面ボタンのカメラ位置 X:左右 Y:高さ Z:奥行き
const VR_FRONT_CAMERA_TARGET = new BABYLON.Vector3(0, 10, 0); // 正面ボタンのカメラ視線目標
const VR_BACK_CAMERA_POS = new BABYLON.Vector3(0, 12, 10); // 背面ボタンのカメラ位置


// --------------------------------------------------------------------------------
// 8. 初期動作設定
// --------------------------------------------------------------------------------
let isLooping = 1; // アニメーションの初期リピート設定
// --------------------------------------------------------------------------------


// --------------------------------------------------------------------------------
// 9. クレジットテキストの内容
// --------------------------------------------------------------------------------
const creditTextContent =
"曲: バブリン(HIMEHINA様) モーション: HIMEHINA様 \n" +
"モデル1:田中ヒメ ver1.30 (C) HimeTanaka \n" +
"モデル2:鈴木ヒナ ver1.30 (C) HinaSuzuki \n" +
"カメラ:みみみ様 ステージ: 体育館 (ムムム様) ";
// --------------------------------------------------------------------------------




// ★★★ 設定項目ここまで ★★★


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


//// --- 共通で環境マップを先に設定しておく(どちらの手にも必要) ---
// これを一度だけ実行しておけばOK(scene作成直後などで)
const envTex = BABYLON.CubeTexture.CreateFromPrefilteredData(
"https://assets.babylonjs.com/environments/environmentSpecular.env",
scene
);
scene.environmentTexture = envTex;



let adt; // VRコントローラー用GUI
let coordinateTextBlock;
let mmdModel_1 = null;
let mmdModel_2 = null;
let mmdModel_3 = null;
let mmdMesh_2 = null;
let mmdMesh_3 = null;
let modelMotion_2 = null;
let modelMotion_3 = null;

let latestBonePosition_1 = new BABYLON.Vector3(0, 0, 0);
let latestBonePosition_2 = new BABYLON.Vector3(0, 0, 0);
let latestBonePosition_3 = new BABYLON.Vector3(0, 0, 0);

// ★追加★ PC用オーバーレイGUIとクレジット表示用テキスト
let adtUI;
let creditsTextBlock;
let leftGlowStick = null;
let rightGlowStick = null;
let audioDurationLatch = 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;


// --- ステージのメッシュを読み込む ---
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);

// --- モデル1 のメッシュを読み込む ---
let mmdMesh_1 = null;
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.isVisible = IS_MODEL_1_VISIBLE;
}

// --- モデル2 のメッシュを読み込む ---
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.isVisible = IS_MODEL_2_VISIBLE;
}

// --- モデル3 のメッシュを読み込む ---
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.isVisible = IS_MODEL_3_VISIBLE;
}


// モーションのロード
const modelMotion_1 = IS_MODEL_1_VISIBLE ? await vmdLoader.loadAsync("model_motion_1", MODEL_1_VMD) : null;
modelMotion_2 = IS_MODEL_2_VISIBLE ? await vmdLoader.loadAsync("model_motion_2", MODEL_2_VMD) : null;
modelMotion_3 = IS_MODEL_3_VISIBLE ? await vmdLoader.loadAsync("model_motion_3", MODEL_3_VMD) : null;


// ★↓ここから変更↓★
let cameraMotion = null;
let cameraMotionHandle = null; // ハンドルをここで宣言
const hasCameraVmd = (CAMERA_VMD && CAMERA_VMD !== ""); // VMDの有無を判定

if (hasCameraVmd) {
cameraMotion = await vmdLoader.loadAsync("camera_motion", CAMERA_VMD);
}
// ★↑ここまで変更↑★


const havokInstance = await HavokPhysics();

// 第1引数を false に変更 (可変ステップを無効化)
const havokPlugin = new BABYLON.HavokPlugin(false, havokInstance);

// 物理演算を常に 1/60秒 (約16ms) 刻みで計算するよう強制設定
havokPlugin.setTimeStep(1 / 60);

scene.enablePhysics(new BABYLON.Vector3(0, -9.8 * 10, 0), havokPlugin); // 重力はMMD標準の10倍スケール付近推奨


// 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にモーションを適用
if (IS_MODEL_1_VISIBLE && mmdMesh_1) {

mmdModel_1 = mmdRuntime.createMmdModel(mmdMesh_1, {physicsOptions: {disableOffsetForConstraintFrame: true } });
// mmdModel_1 = mmdRuntime.createMmdModel(mmdMesh_1);

const modelAnimationHandle = mmdModel_1.createRuntimeAnimation(modelMotion_1);
mmdModel_1.setRuntimeAnimation(modelAnimationHandle);
}

// モデル2にモーションを適用
if (IS_MODEL_2_VISIBLE && mmdMesh_2) {

mmdModel_2 = mmdRuntime.createMmdModel(mmdMesh_2, {physicsOptions: {disableOffsetForConstraintFrame: true } });
// mmdModel_2 = mmdRuntime.createMmdModel(mmdMesh_2);

const modelAnimationHandle_2 = mmdModel_2.createRuntimeAnimation(modelMotion_2);
mmdModel_2.setRuntimeAnimation(modelAnimationHandle_2);
}

// モデル3にモーションを適用
if (IS_MODEL_3_VISIBLE && mmdMesh_3) {

mmdModel_3 = mmdRuntime.createMmdModel(mmdMesh_3, {physicsOptions: {disableOffsetForConstraintFrame: true } });
// mmdModel_3 = mmdRuntime.createMmdModel(mmdMesh_3);

const modelAnimationHandle_3 = mmdModel_3.createRuntimeAnimation(modelMotion_3);
mmdModel_3.setRuntimeAnimation(modelAnimationHandle_3);
}


// カメラにモーションを適用
// ★↓ここから変更↓★
if (hasCameraVmd && cameraMotion) {
mmdRuntime.addAnimatable(camera);
// (cameraMotionHandle はステップ1で宣言済)
cameraMotionHandle = camera.createRuntimeAnimation(cameraMotion);
camera.setRuntimeAnimation(cameraMotionHandle);
}
// ★↑ここまで変更↑★

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

// 音量を1 (100%)、ミュートを解除 (false) に設定 したが効かない。初めからミュートは修正無理 これはブラウザの仕様の問題
audioPlayer.volume = 1;
audioPlayer.muted = false;

// --- ★ここから修正★ ---

// 1. (保険) MMDの総時間を、音楽の総時間に合わせる
// (これが "onloadedmetadata" イベント。音楽の本当の長さが分かった瞬間に呼ばれる)
audioPlayer.onloadedmetadata = () => {
const audioDuration = audioPlayer.duration;
// MMD側の「総再生時間」を、音楽の長さに強制的に上書きする
// (isFinite で Infinity を除外)
if (audioDuration > 0 && isFinite(audioDuration) && audioDuration > mmdRuntime.animationDuration) {
mmdRuntime.animationDuration = audioDuration;
}
};

// 2. 音楽が「本当に終わった」時だけ、リピート処理を実行する
audioPlayer.onended = () => {
if (isLooping) {
// mmdRuntime を 0 に戻す(これに連動して audioPlayer も 0 に戻る)
mmdRuntime.seekAnimation(0);
mmdRuntime.playAnimation();
}
};
// --- ★修正ここまで★

mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();


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


// =================================================================
// PC用 クレジット表示UI
// =================================================================
adtUI = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");


creditsTextBlock = new BABYLON.GUI.TextBlock("creditsText", creditTextContent);
creditsTextBlock.color = "white";
creditsTextBlock.fontSize = 20;
creditsTextBlock.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; // 右寄せ
creditsTextBlock.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; // 下寄せ
creditsTextBlock.paddingRight = "20px"; // 右からのパディング
creditsTextBlock.paddingBottom = "70px"; // 下からのパディング
creditsTextBlock.shadowColor = "black"; // 影の色
creditsTextBlock.shadowBlur = 2;
creditsTextBlock.shadowOffsetX = 1;
creditsTextBlock.shadowOffsetY = 1;

// 全画面UIの右下に配置
creditsTextBlock.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
creditsTextBlock.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT;

creditsTextBlock.isVisible = false; // 初期状態は非表示
adtUI.addControl(creditsTextBlock);
// PC用 クレジット表示UI (ここまで)



// --- モデル1のボーン座標を取得 ---
scene.onAfterPhysicsObservable.add(() => {
try {
if (mmdModel_1 && mmdModel_1.skeleton && IS_MODEL_1_VISIBLE) {
const targetBone = mmdModel_1.skeleton.bones.find(b => b.name === '上半身');
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_1);
}
}
} catch (e) {
// console.error("onAfterPhysicsObservable (Model 1) でエラー:", e);
}
});

// --- モデル2のボーン座標を取得 ---
scene.onAfterPhysicsObservable.add(() => {
try {
if (mmdModel_2 && mmdModel_2.skeleton && IS_MODEL_2_VISIBLE) {
const targetBone = mmdModel_2.skeleton.bones.find(b => b.name === '上半身');
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_2);
}
}
} catch (e) {
// console.error("onAfterPhysicsObservable (Model 2) でエラー:", e);
}
});

// --- モデル3のボーン座標を取得 ---
scene.onAfterPhysicsObservable.add(() => {
try {
if (mmdModel_3 && mmdModel_3.skeleton && IS_MODEL_3_VISIBLE) {
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 (Model 3) でエラー:", e);
}
});


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

let followingModelIndex = 0;
let followOffset = new BABYLON.Vector3();





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

adt = null; // 毎回初期化
let uiParent = scene.getTransformNodeByName("leftUIParent");
// 変数をifの外側スコープで宣言
let vrCreditsTextBlock = null;
let uiPlane = null;
let debugTextBlock = null;

let followButton_1, followButton_2, followButton_3;
let resetButton, playPauseButton, frontButton, backButton, repeatButton, exitButton, arButton;

if (!uiParent) {

const buttonPadding = "8%";
const buttonFontSize = 54;

const updateFollowButtonStyles = () => {
const defaultColor = "#607D8B"; // 追従
const activeColor = "#9C27B0"; // 解除

if (followButton_1) {
followButton_1.textBlock.text = (followingModelIndex === 1) ? "解除1" : "追従1";
followButton_1.background = (followingModelIndex === 1) ? activeColor : defaultColor;
followButton_1.textBlock.fontSize = buttonFontSize;
followButton_1.textBlock.fontWeight = "bold";
}
if (followButton_2) {
followButton_2.textBlock.text = (followingModelIndex === 2) ? "解除2" : "追従2";
followButton_2.background = (followingModelIndex === 2) ? activeColor : defaultColor;
followButton_2.textBlock.fontSize = buttonFontSize;
followButton_2.textBlock.fontWeight = "bold";
}
if (followButton_3) {
followButton_3.textBlock.text = (followingModelIndex === 3) ? "解除3" : "追従3";
followButton_3.background = (followingModelIndex === 3) ? activeColor : defaultColor;
followButton_3.textBlock.fontSize = buttonFontSize;
followButton_3.textBlock.fontWeight = "bold";
}
};


// 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平面のサイズ (0.25 : 0.20 = 5 : 4)
uiPlane = BABYLON.MeshBuilder.CreatePlane("uiPlane", { width: 0.25, height: 0.20 }, scene);
uiPlane.parent = uiParent;
uiPlane.visibility = 0.9;


// 平面メッシュにGUIテクスチャを適用 (アスペクト比 5:4)
adt = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(uiPlane, 1280, 1024, true);



// 1. クレジット表示部 (上半分)

// 背景用の「長方形(Rectangle)」を作成
const creditsContainer = new BABYLON.GUI.Rectangle("creditsContainer");
creditsContainer.width = "100%";
creditsContainer.height = "25%"; // UIの上 1/4 を占有
creditsContainer.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
creditsContainer.thickness = 0;
creditsContainer.background = "rgba(0, 0, 150, 0.5)";
adt.addControl(creditsContainer);

// 次に「テキスト」を作成
vrCreditsTextBlock = new BABYLON.GUI.TextBlock("vrCreditsText", creditTextContent);
vrCreditsTextBlock.width = "100%";
vrCreditsTextBlock.height = "100%";
vrCreditsTextBlock.color = "white";
vrCreditsTextBlock.fontSize = 35;
vrCreditsTextBlock.fontWeight = "bold";
vrCreditsTextBlock.textWrapping = true;
vrCreditsTextBlock.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
vrCreditsTextBlock.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
vrCreditsTextBlock.paddingLeft = "5px";
vrCreditsTextBlock.paddingTop = "5px";
vrCreditsTextBlock.outlineColor = "black";
vrCreditsTextBlock.outlineWidth = 2;
creditsContainer.addControl(vrCreditsTextBlock);

// 2. ボタン表示部 (下半分) - 5列x2行のGridを新規作成
const buttonsGrid = new BABYLON.GUI.Grid("buttonsGrid");
buttonsGrid.width = "100%";
buttonsGrid.height = "63%";
buttonsGrid.alpha = 0.7;
buttonsGrid.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
adt.addControl(buttonsGrid);


// --- ★ここから追加 (デバッグエリア)★ ---
// クレジットエリアと全く同じ方法で、背景(Rectangle)を作成
const debugContainer = new BABYLON.GUI.Rectangle("debugContainer");
debugContainer.width = "100%";
debugContainer.height = "12%"; // UIの下部 12% を占有
debugContainer.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; // ★一番下に配置
debugContainer.thickness = 1; // 枠線
debugContainer.color = "lime"; // 枠線の色
debugContainer.background = "rgba(0, 0, 0, 0.5)"; // 半透明の黒背景
adt.addControl(debugContainer);

// 次に「テキスト」を作成
debugTextBlock = new BABYLON.GUI.TextBlock("debugText", "Debug Area");
debugTextBlock.width = "100%";
debugTextBlock.height = "100%";
debugTextBlock.color = "lime";
debugTextBlock.fontSize = 40;
debugTextBlock.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
debugTextBlock.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
debugTextBlock.paddingLeft = "5px";
debugContainer.addControl(debugTextBlock);
// --- ★ここまで追加★ ---




// --- buttonsGrid の中身 (5列x2行) を定義 ---
// 5列定義 (各20%)
for (let i = 0; i < 5; i++) {
buttonsGrid.addColumnDefinition(0.2);
}
// 2行定義 (ピクセル指定)
buttonsGrid.addRowDefinition(256, true);
buttonsGrid.addRowDefinition(256, true);
buttonsGrid.addRowDefinition(1.0); // ダミー行



// --- 1行目のボタン (Row 0) --- (buttonsGrid に追加)

// 1. 「リセット」ボタン
const resetButton = BABYLON.GUI.Button.CreateSimpleButton("resetButton", "リセット");
resetButton.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
resetButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
resetButton.color = "white";
resetButton.background = "#4CAF50";
resetButton.textBlock.fontSize = buttonFontSize;
resetButton.textBlock.fontWeight = "bold";
resetButton.onPointerClickObservable.add(() => {
mmdRuntime.seekAnimation(0);
});
buttonsGrid.addControl(resetButton, 0, 0); // buttonsGrid, Row 0

// 2. 「再生/停止」トグルボタン
const playPauseButton = BABYLON.GUI.Button.CreateSimpleButton("playPauseButton", "停止");
playPauseButton.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
playPauseButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
playPauseButton.color = "white";
playPauseButton.background = "#f44336";
playPauseButton.textBlock.fontSize = buttonFontSize;
playPauseButton.textBlock.fontWeight = "bold";
playPauseButton.onPointerClickObservable.add(() => {
if (mmdRuntime.isAnimationPlaying) {
mmdRuntime.pauseAnimation();
playPauseButton.textBlock.text = "再生";
playPauseButton.background = "#2196F3";
} else {
mmdRuntime.playAnimation();
playPauseButton.textBlock.text = "停止";
playPauseButton.background = "#f44336";
}
});
buttonsGrid.addControl(playPauseButton, 0, 1);




// 3. 「正面」ボタン
const frontButton = BABYLON.GUI.Button.CreateSimpleButton("frontButton", "正面");
frontButton.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
frontButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
frontButton.color = "white";
frontButton.background = "#2196F3";
frontButton.textBlock.fontSize = buttonFontSize;
frontButton.textBlock.fontWeight = "bold";
frontButton.onPointerClickObservable.add(() => {
followingModelIndex = 0;
updateFollowButtonStyles();

// VMDローカル座標 + 静的オフセット = ワールド座標
const visibleX = [];
const visibleZ = [];
if (IS_MODEL_1_VISIBLE && mmdModel_1) {
visibleX.push(latestBonePosition_1.x + MODEL_1_OFFSET_X);
visibleZ.push(latestBonePosition_1.z + MODEL_1_OFFSET_Z);
}
if (IS_MODEL_2_VISIBLE && mmdModel_2) {
visibleX.push(latestBonePosition_2.x + MODEL_2_OFFSET_X);
visibleZ.push(latestBonePosition_2.z + MODEL_2_OFFSET_Z);
}
if (IS_MODEL_3_VISIBLE && mmdModel_3) {
visibleX.push(latestBonePosition_3.x + MODEL_3_OFFSET_X);
visibleZ.push(latestBonePosition_3.z + MODEL_3_OFFSET_Z);
}

let centerX = 0, centerZ = 0, minZ = 0;

if (visibleX.length > 0) {
const minX = Math.min(...visibleX);
const maxX = Math.max(...visibleX);
minZ = Math.min(...visibleZ);
const maxZ = Math.max(...visibleZ);

centerX = (minX + maxX) / 2;
centerZ = (minZ + maxZ) / 2;
}

const groupCenter = new BABYLON.Vector3(centerX, 0, centerZ);
const targetPosition = groupCenter.add(VR_FRONT_CAMERA_TARGET);

// カメラ位置のZ座標は「一番手前のモデル(minZ)」を基準にする
const frontPosition = new BABYLON.Vector3(
centerX + VR_FRONT_CAMERA_POS.x,
groupCenter.y + VR_FRONT_CAMERA_POS.y,
minZ + VR_FRONT_CAMERA_POS.z // minZ + (-20)
);

xr.baseExperience.camera.position.copyFrom(frontPosition);
xr.baseExperience.camera.setTarget(targetPosition);
});
buttonsGrid.addControl(frontButton, 0, 2);

// 4. 「背面」ボタン
const backButton = BABYLON.GUI.Button.CreateSimpleButton("backButton", "背面");
backButton.width = (100 - (parseFloat(buttonPadding)* 2)) + "%";
backButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
backButton.color = "white";
backButton.background = "#FF9800";
backButton.textBlock.fontSize = buttonFontSize;
backButton.textBlock.fontWeight = "bold";
backButton.onPointerClickObservable.add(() => {
followingModelIndex = 0;
updateFollowButtonStyles();

//VMDローカル座標 + 静的オフセット = ワールド座標
const visibleX = [];
const visibleZ = [];
if (IS_MODEL_1_VISIBLE && mmdModel_1) {
visibleX.push(latestBonePosition_1.x + MODEL_1_OFFSET_X);
visibleZ.push(latestBonePosition_1.z + MODEL_1_OFFSET_Z);
}
if (IS_MODEL_2_VISIBLE && mmdModel_2) {
visibleX.push(latestBonePosition_2.x + MODEL_2_OFFSET_X);
visibleZ.push(latestBonePosition_2.z + MODEL_2_OFFSET_Z);
}
if (IS_MODEL_3_VISIBLE && mmdModel_3) {
visibleX.push(latestBonePosition_3.x + MODEL_3_OFFSET_X);
visibleZ.push(latestBonePosition_3.z + MODEL_3_OFFSET_Z);
}

let centerX = 0, centerZ = 0, maxZ = 0;

if (visibleX.length > 0) {
const minX = Math.min(...visibleX);
const maxX = Math.max(...visibleX);
const minZ = Math.min(...visibleZ);
maxZ = Math.max(...visibleZ);

centerX = (minX + maxX) / 2;
centerZ = (minZ + maxZ) / 2;
}

const groupCenter = new BABYLON.Vector3(centerX, 0, centerZ);
const targetPosition = groupCenter.add(VR_FRONT_CAMERA_TARGET);

// カメラ位置のZ座標は「一番奥のモデル(maxZ)」を基準にする
const backPosition = new BABYLON.Vector3(
centerX + VR_BACK_CAMERA_POS.x,
groupCenter.y + VR_BACK_CAMERA_POS.y,
maxZ + VR_BACK_CAMERA_POS.z // maxZ + 20
);

xr.baseExperience.camera.position.copyFrom(backPosition);
xr.baseExperience.camera.setTarget(targetPosition);

// デバッグコード (もし不要ならこのifブロックを削除してください)
// if (vrCreditsTextBlock) {
// let debugText =
// `Z Values (World):\n` +
// `M1:${(latestBonePosition_1.z + MODEL_1_OFFSET_Z).toFixed(2)}, ` +
// `M2:${(latestBonePosition_2.z + MODEL_2_OFFSET_Z).toFixed(2)}, ` +
// `M3:${(latestBonePosition_3.z + MODEL_3_OFFSET_Z).toFixed(2)}`;
// if(visibleZ.length > 0) {
// debugText += `\nminZ: ${Math.min(...visibleZ).toFixed(2)}, maxZ: ${maxZ.toFixed(2)}`;
// }
// vrCreditsTextBlock.text = debugText +
// `\nCam Z: ${backPosition.z.toFixed(2)}`;
// }
});
buttonsGrid.addControl(backButton, 0, 3);






// 5. 「リピート」トグルボタン
const repeatButton = BABYLON.GUI.Button.CreateSimpleButton("repeatButton", isLooping ? "リピート\nオン" : "リピート\nオフ");
repeatButton.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
repeatButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
repeatButton.color = "white";
repeatButton.background = isLooping ? "#FFC107" : "#795548";
repeatButton.textBlock.fontSize = buttonFontSize;
repeatButton.textBlock.fontWeight = "bold";
repeatButton.onPointerClickObservable.add(() => {
isLooping = !isLooping;
if (isLooping) {
repeatButton.textBlock.text = "リピート\nオン";
repeatButton.background = "#FFC107";
} else {
repeatButton.textBlock.text = "リピート\nオフ";
repeatButton.background = "#795548";
}
});
buttonsGrid.addControl(repeatButton, 0, 4);


// --- 2行目のボタン (Row 1) --- (buttonsGrid に追加)

// 6. 「VR 終了」ボタン
const exitButton = BABYLON.GUI.Button.CreateSimpleButton("exitButton", "VR\n終了");
exitButton.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
exitButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
exitButton.color = "white";
exitButton.background = "#E91E63";
exitButton.textBlock.fontSize = buttonFontSize;
exitButton.textBlock.fontWeight = "bold";
exitButton.onPointerClickObservable.add(async () => {
if (xr.baseExperience.state === BABYLON.WebXRState.IN_XR) {
await xr.baseExperience.exitXRAsync();
}
});
buttonsGrid.addControl(exitButton, 1, 0); // buttonsGrid, Row 1

// 7. 「ARモード」ボタン
const arButton = BABYLON.GUI.Button.CreateSimpleButton("arButton", "AR");
arButton.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
arButton.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
arButton.color = "white";
arButton.background = "#9C27B0";
arButton.textBlock.fontSize = buttonFontSize;
arButton.textBlock.fontWeight = "bold";
arButton.onPointerClickObservable.add(async () => {
if (xr.baseExperience.state === BABYLON.WebXRState.IN_XR) {
await xr.baseExperience.exitXRAsync();
}
await xr.baseExperience.enterXRAsync("immersive-ar", "local-floor");
});
buttonsGrid.addControl(arButton, 1, 1);

// 8. 「追従 1」ボタン
if (IS_MODEL_1_VISIBLE) {
followButton_1 = BABYLON.GUI.Button.CreateSimpleButton("followButton1", "追従1");
followButton_1.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
followButton_1.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
followButton_1.color = "white";
followButton_1.onPointerClickObservable.add(() => {
if (followingModelIndex === 1) {
followingModelIndex = 0;
} else {
followingModelIndex = 1;
const xrCamera = xr.baseExperience.camera;
followOffset = xrCamera.position.subtract(latestBonePosition_1);
}
updateFollowButtonStyles();
});
if (!IS_MODEL_1_VISIBLE || !mmdModel_1) {
followButton_1.textBlock.text = "M1\nOFF";
followButton_1.background = "#D3D3D3";
followButton_1.isHitTestVisible = false;
followButton_1.textBlock.fontSize = buttonFontSize;
followButton_1.textBlock.fontWeight = "bold";
}
buttonsGrid.addControl(followButton_1, 1, 2);
}

// 9. 「追従 2」ボタン
if (IS_MODEL_2_VISIBLE) {
followButton_2 = BABYLON.GUI.Button.CreateSimpleButton("followButton2", "追従2");
followButton_2.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
followButton_2.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
followButton_2.color = "white";
followButton_2.onPointerClickObservable.add(() => {
if (followingModelIndex === 2) {
followingModelIndex = 0;
} else {
followingModelIndex = 2;
const xrCamera = xr.baseExperience.camera;
followOffset = xrCamera.position.subtract(latestBonePosition_2);
}
updateFollowButtonStyles();
});
if (!IS_MODEL_2_VISIBLE || !mmdModel_2) {
followButton_2.textBlock.text = "M2\nOFF";
followButton_2.background = "#D3D3D3";
followButton_2.isHitTestVisible = false;
followButton_2.textBlock.fontSize = buttonFontSize;
followButton_2.textBlock.fontWeight = "bold";
}
buttonsGrid.addControl(followButton_2, 1, 3);
}

// 10. 「追従 3」ボタン
if (IS_MODEL_3_VISIBLE) {
followButton_3 = BABYLON.GUI.Button.CreateSimpleButton("followButton3", "追従3");
followButton_3.width = (100 - (parseFloat(buttonPadding) * 2)) + "%";
followButton_3.height = (100 - (parseFloat(buttonPadding) * 2)) + "%";
followButton_3.color = "white";
followButton_3.onPointerClickObservable.add(() => {
if (followingModelIndex === 3) {
followingModelIndex = 0;
} else {
followingModelIndex = 3;
const xrCamera = xr.baseExperience.camera;
followOffset = xrCamera.position.subtract(latestBonePosition_3);
}
updateFollowButtonStyles();
});
if (!IS_MODEL_3_VISIBLE || !mmdModel_3) {
followButton_3.textBlock.text = "M3\nOFF";
followButton_3.background = "#D3D3D3";
followButton_3.isHitTestVisible = false;
followButton_3.textBlock.fontSize = buttonFontSize;
followButton_3.textBlock.fontWeight = "bold";
}
buttonsGrid.addControl(followButton_3, 1, 4);
}

// 初期状態のボタンスタイルを適用
updateFollowButtonStyles();
} else {

// --- ★ここから修正 (elseブロック)★ ---
// uiParentが既にある場合、子要素を名前で検索する
uiPlane = uiParent.getChildMeshes(false, (node) => node.name === "uiPlane")[0];
if (uiPlane) uiPlane.renderingGroupId = 1;
if (uiPlane && uiPlane.material && uiPlane.material.diffuseTexture) {

// ADTテクスチャを取得
adt = uiPlane.material.diffuseTexture;
if (adt && adt.getControlByName) {
// ADTからテキストブロックを名前で検索
vrCreditsTextBlock = adt.getControlByName("vrCreditsText");
if (vrCreditsTextBlock) vrCreditsTextBlock.text = creditTextContent;

debugTextBlock = adt.getControlByName("debugText");
if (debugTextBlock) {
debugTextBlock.text = "Debug Area (cached)";
// (※デバッグエリアの表示方法は、前の修正(cellColumnSpan)ではなく、
//  「クレジットエリアと同じ方法」に直したため、cellColumnSpanは不要です)
}

// ★ 全てのボタンを adt から見つける (これが抜けていた)
resetButton = adt.getControlByName("resetButton");
playPauseButton = adt.getControlByName("playPauseButton");
frontButton = adt.getControlByName("frontButton");
backButton = adt.getControlByName("backButton");
repeatButton = adt.getControlByName("repeatButton");
exitButton = adt.getControlByName("exitButton");
arButton = adt.getControlByName("arButton");
followButton_1 = adt.getControlByName("followButton1");
followButton_2 = adt.getControlByName("followButton2");
followButton_3 = adt.getControlByName("followButton3");
}
}
// --- ★ここまで修正★
}

uiParent.parent = controller.grip;



// (コントローラーにつき1回だけ作成)
if (!controller.grip.getChildMeshes(false, (node) => node.name === "leftGlowStick").length) {

leftGlowStick = BABYLON.MeshBuilder.CreateCylinder("leftGlowStick", {
height: 0.7,
diameter: 0.05,
tessellation: 32
}, scene);

// --- 赤金マテリアル ---
const leftMat = new BABYLON.PBRMetallicRoughnessMaterial("stickMatLeft", scene);
leftMat.baseColor = new BABYLON.Color3(1.0, 0.55, 0.25); // 金+銅=赤金
leftMat.metallic = 1.0;
leftMat.roughness = 0.05;
leftMat.environmentTexture = envTex;
leftMat.emissiveColor = new BABYLON.Color3(0.1, 0.05, 0.02); // 暗所でもほんのり赤金に光る

leftGlowStick.material = leftMat;

// --- コントローラーに装着 ---
leftGlowStick.parent = controller.grip;
leftGlowStick.position = new BABYLON.Vector3(0, 0.18, 0.35);
leftGlowStick.rotation = new BABYLON.Vector3(-Math.PI * 0.7, 0, 0);
leftGlowStick.isVisible = false;

}




// UI ONOFF
controller.onMotionControllerInitObservable.add((mc) => {
// mc が motionController オブジェクト
if (mc) {
const xButton = mc.getComponent("x-button");
if (xButton) {
if (debugTextBlock) debugTextBlock.text = "MC/X-Btn OK. Press X.";

xButton.onButtonStateChangedObservable.add((buttonState) => {
if (buttonState.pressed) {
// UIパネルの表示/非表示をトグル
if (uiPlane) {
uiPlane.isVisible = !uiPlane.isVisible;
}

// --- ★デバッグ表示を追加★ ---
if (debugTextBlock) {
// isLooping は必ず存在するのでそのまま表示
let debugString = `Loop: ${isLooping}\n`;

// audioPlayer の状態を安全に確認
if (audioPlayer) {
const audioDur = audioPlayer.duration;
const audioTime = audioPlayer.currentTime;

// Infinity (無限) や NaN (非数) でないかチェック
if (isFinite(audioDur) && audioDur > 0) {
debugString += `Audio: ${audioTime.toFixed(1)} / ${audioDur.toFixed(1)}`;
} else {
// 読み込み中、または失敗
debugString += `Audio: ${audioTime.toFixed(1)} / (Loading... ${audioDur})`;
}
} else {
debugString += `Audio: null`;
}

debugTextBlock.text = debugString;
}
// --- ★ここまで★
}
});

}


}
});



}


// --- ★ここから追加 (右手コントローラー)★ ---
else if (controller.inputSource.handedness === 'right') {

// (コントローラーにつき1回だけ作成)
if (!controller.grip.getChildMeshes(false, (node) => node.name === "rightGlowStick").length) {

rightGlowStick = BABYLON.MeshBuilder.CreateCylinder("rightGlowStick", {
height: 0.7,
diameter: 0.05,
tessellation: 32
}, scene);

// --- 赤金マテリアル ---
const rightMat = new BABYLON.PBRMetallicRoughnessMaterial("stickMatRight", scene);
rightMat.baseColor = new BABYLON.Color3(1.0, 0.55, 0.25); // 金より赤みを強く
rightMat.metallic = 1.0;
rightMat.roughness = 0.05;
rightMat.environmentTexture = envTex;
rightMat.emissiveColor = new BABYLON.Color3(0.1, 0.05, 0.02);

rightGlowStick.material = rightMat;

// --- コントローラーに装着 ---
rightGlowStick.parent = controller.grip;
rightGlowStick.position = new BABYLON.Vector3(0, 0.18, 0.35);
rightGlowStick.rotation = new BABYLON.Vector3(-Math.PI * 0.7, 0, 0);
rightGlowStick.isVisible = false;


// --- ★ここから追加 (BボタンでスティックON/OFF)★ ---
controller.onMotionControllerInitObservable.add((mc) => {
if (mc) {
const bButton = mc.getComponent("b-button"); // ★ YからBに変更
if (bButton) {

bButton.onButtonStateChangedObservable.add((buttonState) => {
if (buttonState.pressed) {

// Yボタンからコピーしたロジック
if (leftGlowStick) {
const newState = !leftGlowStick.isVisible;
leftGlowStick.isVisible = newState;

if (rightGlowStick) {
rightGlowStick.isVisible = newState;
}
}
}
});
}
}
});
// --- ★追加ここまで★


}
}
// --- ★右手コントローラーここまで★ ---

});




// VRモード切替時のアニメーション制御
xr.baseExperience.onStateChangedObservable.add((state) => {
if (state === BABYLON.WebXRState.IN_XR) {
camera.setRuntimeAnimation(null);

if (adtUI) adtUI.rootContainer.isVisible = false; // PC用UIを非表示

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) {
// ★↓ここから変更↓★
if (hasCameraVmd && cameraMotionHandle) {
camera.setRuntimeAnimation(cameraMotionHandle);
} else {
camera.setRuntimeAnimation(null); // VMDがなければ固定
}
// ★↑ここまで変更↑★
followingModelIndex = 0;



ground.visibility = true;

if (stageMesh) {
stageMesh.isVisible = IS_STAGE_VISIBLE;
stageMesh.getChildMeshes().forEach(mesh => {mesh.isVisible = IS_STAGE_VISIBLE;});
}

if (adtUI) adtUI.rootContainer.isVisible = true; // PC用UIを再表示
}
});




// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {

// ★★★ ここからが修正ブロック (カメラVMDがない場合のPCモード固定カメラ) ★★★
if (xr.baseExperience.state === BABYLON.WebXRState.NOT_IN_XR && !hasCameraVmd) {

// VR「正面」ボタンのロジックを流用 (対象は 'camera')
const visibleX = [];
const visibleZ = [];
if (IS_MODEL_1_VISIBLE && mmdModel_1) {
visibleX.push(latestBonePosition_1.x + MODEL_1_OFFSET_X);
visibleZ.push(latestBonePosition_1.z + MODEL_1_OFFSET_Z);
}
if (IS_MODEL_2_VISIBLE && mmdModel_2) {
visibleX.push(latestBonePosition_2.x + MODEL_2_OFFSET_X);
visibleZ.push(latestBonePosition_2.z + MODEL_2_OFFSET_Z);
}
if (IS_MODEL_3_VISIBLE && mmdModel_3) {
visibleX.push(latestBonePosition_3.x + MODEL_3_OFFSET_X);
visibleZ.push(latestBonePosition_3.z + MODEL_3_OFFSET_Z);
}

let centerX = 0, centerZ = 0, minZ = 0;

if (visibleX.length > 0) {
const minX = Math.min(...visibleX);
const maxX = Math.max(...visibleX);
minZ = Math.min(...visibleZ);
const maxZ = Math.max(...visibleZ);

centerX = (minX + maxX) / 2;
centerZ = (minZ + maxZ) / 2;
}

const groupCenter = new BABYLON.Vector3(centerX, 0, centerZ);
const targetPosition = groupCenter.add(VR_FRONT_CAMERA_TARGET);

const frontPosition = new BABYLON.Vector3(
centerX + VR_FRONT_CAMERA_POS.x,
groupCenter.y + VR_FRONT_CAMERA_POS.y,
minZ + VR_FRONT_CAMERA_POS.z
);

// PCモードのカメラ(mmdCamera)に適用
camera.position.copyFrom(frontPosition);

// ★★★ ここを修正しました ★★★
// 誤: camera.setTarget(targetPosition);
// 正:
camera.target = targetPosition;
// ★★★ 修正ここまで ★★★
}
// ★★★ 修正ブロックここまで ★★★




// 手動リピートロジック (ユーザー指定:最初に取得した duration を使う)
if (isLooping) {

const currentAudioTime = audioPlayer.currentTime;

// 1. 「仮の総時間」 (audioDurationLatch) がまだ保存されていないか?
if (audioDurationLatch === 0) {
// 2. audioPlayer から総時間を取得しようと試みる
const durationNow = audioPlayer.duration;

// 3. 取得した値が「有効」か (0より大きく、Infinityでない)
if (durationNow > 0 && isFinite(durationNow)) {
// 4. 最初の有効な値(例: 203.0)を「仮の総時間」として保存
audioDurationLatch = durationNow;

// 5. 保存したことをデバッグ表示
if (debugTextBlock) debugTextBlock.text = `Audio Duration Latched: ${audioDurationLatch.toFixed(1)}`;
}

// 6. 「仮の総時間」が既に保存されている場合
} else {

// 7. 保存された「仮の総時間」を使ってリピート判定
// (203.3で止まる対策として、閾値は 99% (0.99) にします)
if (currentAudioTime >= (audioDurationLatch * 0.99)) {
mmdRuntime.seekAnimation(0);
mmdRuntime.playAnimation();

// (※ラッチはリセットしません。常に最初に取得した値を使います)
}
}
}







// クレジット表示タイミング制御
// VRモードでない場合(PCモード)のみ処理
if (mmdRuntime && creditsTextBlock && xr.baseExperience.state === BABYLON.WebXRState.NOT_IN_XR) {
const currentTime = mmdRuntime.currentTime;
const duration = mmdRuntime.animationDuration;

if (duration > 0) {
const triggerTime = duration * 0.9;

// 9/10 ~ 10/10 の間だけ表示
if (currentTime >= triggerTime && currentTime <= duration) {
creditsTextBlock.isVisible = true;
} else {
creditsTextBlock.isVisible = false; // それ以外の時間は非表示
}
} else {
creditsTextBlock.isVisible = false;
}
} else if (creditsTextBlock) {
// VR中、またはMMDランタイムが存在しない場合は非表示
creditsTextBlock.isVisible = false;
}



// --- これ以降はVRモード中の処理 ---
if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;


// ボーン座標をUIに表示 (3モデルのY座標)
if (coordinateTextBlock) {
coordinateTextBlock.text = `Y | 1: ${latestBonePosition_1.y.toFixed(2)} | 2: ${latestBonePosition_2.y.toFixed(2)} | 3: ${latestBonePosition_3.y.toFixed(2)}`;
}

// --- 追従モードの処理 ---
if (followingModelIndex > 0) {

let targetPosition = new BABYLON.Vector3();

if (followingModelIndex === 1) {
targetPosition.copyFrom(latestBonePosition_1);
} else if (followingModelIndex === 2) {
targetPosition.copyFrom(latestBonePosition_2);
} else if (followingModelIndex === 3) {
targetPosition.copyFrom(latestBonePosition_3);
}

const desiredPosition = targetPosition.clone().add(followOffset);
xr.baseExperience.camera.position.copyFrom(desiredPosition);
}

// --- ジョイスティック操作の処理 ---
if (followingModelIndex === 0) {
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 () {

//物理演算の例外を握りつぶさないように try...catch を削除
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 )
20
action
activeColor
adt
adtUI
alpha
animationDuration
arButton
assetsPath
audioDur
audioDuration
audioDurationLatch
audioPlayer
audioTime
AUDIO_WAV
b
backButton
background
backPosition
baseColor
bButton
blurKernel
br> // Register a render loop to repeatedly render the scene engine.runRenderLoop -------( Function )
buttonFontSize
buttonPadding
buttonsGrid
c
camera
cameraMotion
cameraMotionHandle
CAMERA_VMD
canvas
centerX
centerZ
charset
color
content
createScene
creditsContainer
creditsTextBlock
creditTextContent
currentAudioTime
currentTime
debugContainer
debugString
debugText
debugTextBlock
defaultColor
delAnimationHandle
deltaMillis
desiredPosition
duration
durationNow
emissiveColor
engine
environmentTexture
envTex
equiv
exitButton
finalMatrix
FollowButtonStyles
followButton_1
followButton_2
followButton_3
followOffset
fontSize
fontWeight
forward
frontButton
frontPosition
ground
groundColor
groupCenter
handedness
hasCameraVmd
havokInstance
havokPlugin
height
hemisphericLight
i
id
intensity
isHitTestVisible
isLooping
isVisible
IS_MODEL_1_VISIBLE
IS_MODEL_2_VISIBLE
IS_MODEL_3_VISIBLE
IS_STAGE_VISIBLE
lAnimationHandle_2
lAnimationHandle_3
leftController
leftGlowStick
leftMat
material
maxX
maxZ
mesh
metallic
minX
minZ
mmdMesh_1
mmdMesh_2
mmdMesh_3
mmdModel_1
mmdModel_2
mmdModel_3
mmdPlayerControl
mmdRuntime
modelMotion_1
modelMotion_2
modelMotion_3
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
muted
name
newState
ollowingModelIndex
onended
onloadedmetadata
orizontalAlignment
outlineColor
outlineWidth
paddingBottom
paddingLeft
paddingRight
paddingTop
parent
playPauseButton
ponentialShadowMap
position
preservesPitch
receiveShadows
renderingGroupId
repeatButton
resetButton
right
rightController
rightGlowStick
rightMat
RONT_CAMERA_TARGET
rotation
rotationThreshold
rotSpeed
roughness
R_FRONT_CAMERA_POS
scaling
scene
sessionMode
shadowBlur
shadowColor
shadowGenerator
shadowLight
shadowOffsetX
shadowOffsetY
source
specular
src
stageMesh
STAGE_PATH
STAGE_PMX
STAGE_POSITION_Y
STAGE_ROTATION_Y
STAGE_SCALE
state
target
targetBone
targetPosition
testBonePosition_1
testBonePosition_2
testBonePosition_3
text
textWrapping
thickness
thumbstick
triggerTime
tVerticalAlignment
uiParent
uiPlane
verticalAlignment
verticalSpeed
visibility
visibleX
visibleZ
vmdLoader
volume
vrCreditsTextBlock
VR_BACK_CAMERA_POS
width
x
xButton
xmlns
xr
xrCamera
y
z
静的オフセット