<!DOCTYPE html>
<!-- -->
<!-- BabylonMMDシミュレータ Ver0.21 機能整ったVersion -->
<!-- あきらめた機能 ・プロセスバー ・物理無効ボタン ・音ONOFFボタン・最初の音またはモーションの遅延 -->
<!-- ベースに使用可能のレベルに到達したので一時的な終了とする -->
<!-- v これから追加したい機能 ・UIパネル非表示 -->
<!-- v これから追加したい機能 ・光る棒で応援 -->
<!-- これから追加したい機能 ・紙吹雪 -->
<!-- v これから追加したい機能 ・カメラ無しのときの正面設置機能 -->
<!-- キラキラ棒の出現を左のYから右のBに変更 -->
<!-- VRカメラ初期値を変更 -->
<!-- 物理演算をフレームレート変動から 固定ステップ(常に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 + 'ColorfulxMelody/';
// --------------------------------------------------------------------------------
// 2. モデル1 の設定
// --------------------------------------------------------------------------------
const MODEL_1_PATH = assetsPath + 'mmd/miku/';
const MODEL_1_PMX = MODEL_1_PATH + 'White.pmx';
const MODEL_1_VMD = motionFolder + 'mmd_ColorfulxMelody_MIK.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/rin/';
const MODEL_2_PMX = MODEL_2_PATH + 'Black.pmx';
const MODEL_2_VMD = motionFolder + 'mmd_ColorfulxMelody_RIN.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 + 'pv_709.wav';
const CAMERA_VMD = "";
// const CAMERA_VMD = motionFolder + 'came2.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, 10, -7); // 正面ボタンのカメラ位置 X:左右 Y:高さ Z:奥行き
const VR_FRONT_CAMERA_TARGET = new BABYLON.Vector3(0, 10, 0); // 正面ボタンのカメラ視線目標
const VR_BACK_CAMERA_POS = new BABYLON.Vector3(0, 10, 7); // 背面ボタンのカメラ位置
// --------------------------------------------------------------------------------
// 8. 初期動作設定
// --------------------------------------------------------------------------------
let isLooping = 1; // アニメーションの初期リピート設定
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// 9. クレジットテキストの内容
// --------------------------------------------------------------------------------
const creditTextContent =
"曲: ColorfulxMelody モーション: ColorfulxMelody \n" +
"モデル1:Sour式初音ミクVer.1.02\n" +
"モデル2:Sour式鏡音リンVer.2.01\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 | |
| 静的オフセット |