junkerstock
 vrbx5-4sxxああ2 

<!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 (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/yyb-miku-CrudeHair/';
const MODEL_1_PMX = MODEL_1_PATH + 'yybmiku-sodenasi-suashi.pmx';
const MODEL_1_VMD = motionFolder + 'Bubblin_MMD-Motion_HinaSuzuki.vmd';
const MODEL_1_OFFSET_X = -1.0; // X軸オフセット (左端へ2.0m)
const MODEL_1_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_1_VISIBLE = true; // モデル1の表示 (true(1): ON, false(0): OFF)


// --------------------------------------------------------------------------------
// 3. モデル2 の設定
// --------------------------------------------------------------------------------
const MODEL_2_PATH = assetsPath + 'mmd/yyb-rin-suashi/';
const MODEL_2_PMX = MODEL_2_PATH + 'yyb-rin-sodenashi-suashi.pmx';
const MODEL_2_VMD = motionFolder + 'Bubblin_MMD-Motion_HimeTanaka.vmd';
const MODEL_2_OFFSET_X = 1.0; // X軸オフセット (中央)
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 + 'mmd_SummerIdol_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 = 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, 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 = false; // アニメーションの初期リピート設定
// --------------------------------------------------------------------------------


// --------------------------------------------------------------------------------
// 9. クレジットテキストの内容
// --------------------------------------------------------------------------------
const creditTextContent =
"曲: バブリン(HIMEHINA様) モーション: HIMEHINA様 \n" +
"モデル1:YYB式鏡音リン袖なし素足(三目YYB様,改変:あくうぁ様)\n" +
"モデル2:YYB式初音ミク袖なし素足(三目YYB様,改変:あくうぁ様) \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;
const cameraMotion = await vmdLoader.loadAsync("camera_motion", CAMERA_VMD);


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にモーションを適用
if (IS_MODEL_1_VISIBLE && mmdMesh_1) {
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);
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);
const modelAnimationHandle_3 = mmdModel_3.createRuntimeAnimation(modelMotion_3);
mmdModel_3.setRuntimeAnimation(modelAnimationHandle_3);
}


// カメラにモーションを適用
mmdRuntime.addAnimatable(camera);
const 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 (Rin)」ボタン
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 (Miku)」ボタン
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 (Teto)」ボタン
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;
}
// --- ★ここまで★
}
});

}


// --- ★ここから追加 (Yボタン)★ ---
const yButton = mc.getComponent("y-button");
if (yButton) {
// Yボタンが見つかったことをデバッグ表示(Xボタンの表示に追記)
if (debugTextBlock) debugTextBlock.text += "\nY-Btn OK.";

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

if (leftGlowStick) {
// 現在の状態の「逆」を新しい状態(newState)とする
const newState = !leftGlowStick.isVisible;

// 両方のスティックを新しい状態にする
leftGlowStick.isVisible = newState;

// rightGlowStick が存在すれば、それも同じ状態にする
if (rightGlowStick) {
rightGlowStick.isVisible = newState;
}

if (debugTextBlock) debugTextBlock.text = "Glow Sticks: " + newState;
}
}
});
}

// --- ★


}
});



}


// --- ★ここから追加 (右手コントローラー)★ ---
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;

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

});




// 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) {
camera.setRuntimeAnimation(cameraMotionHandle);
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(() => {



// 手動リピートロジック (ユーザー指定:最初に取得した 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>


使用変数

adt
arButton
backButton
debugTextBlock
exitButton
followButton_1
followButton_2
followButton_3
frontButton
playPauseButton
repeatButton
resetButton
uiPlane
-------( Function )
20
action
activeColor
adt
adtUI
alpha
animationDuration
arButton
assetsPath
audioDur
audioDuration
audioDurationLatch
audioPlayer
audioTime
AUDIO_WAV
b
backButton
background
backPosition
baseColor
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
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
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
yButton
z
静的オフセット