<!DOCTYPE html>
<!-- 追従に失敗 -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Babylon Template with VR Joystick Movement & UI (Manual Follow Logic)</title>
<style>
html,
body {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#renderCanvas {
width: 100%;
height: 100%;
touch-action: none;
}
</style>
</head>
<body>
<canvas id="renderCanvas" touch-action="none"></canvas>
<script src="https://cdn.babylonjs.com/v8.31.0/babylon.js"></script>
<script src="https://cdn.babylonjs.com/v8.31.0/gui/babylon.gui.min.js"></script>
<script src="https://cdn.babylonjs.com/v8.31.0/havok/HavokPhysics_umd.js"></script>
<script src="https://www.unpkg.com/babylon-mmd@1.0.0/umd/babylon.mmd.min.js"></script>
<script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>
<script>
const canvas = document.getElementById("renderCanvas"); // Get the canvas element
const engine = new BABYLON.Engine(canvas, true); // Generate the BABYLON 3D engine
// ★★★ 設定項目ここから ★★★
// --------------------------------------------------------------------------------
// 1. パス設定
// --------------------------------------------------------------------------------
const assetsPath = 'assets/';
const motionFolder = assetsPath + 'SummerIdol2020REMAKE/';
// --------------------------------------------------------------------------------
// 2. モデル1 (Rin) の設定
// --------------------------------------------------------------------------------
const MODEL_1_PATH = assetsPath + 'mmd/YYBrin/';
const MODEL_1_PMX = MODEL_1_PATH + 'YYB Kagamine Rin_10th_v1.0.pmx';
const MODEL_1_VMD = motionFolder + 'mmd_SummerIdol_RIN.vmd';
const MODEL_1_OFFSET_X = -2.0; // X軸オフセット (左端へ2.0m)
const MODEL_1_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_1_VISIBLE = true; // モデル1の表示 (true: ON, false: OFF)
// --------------------------------------------------------------------------------
// 3. モデル2 (Miku) の設定
// --------------------------------------------------------------------------------
const MODEL_2_PATH = assetsPath + 'mmd/pumiku/';
const MODEL_2_PMX = MODEL_2_PATH + 'pumiku.pmx';
const MODEL_2_VMD = motionFolder + 'mmd_SummerIdol_MIK.vmd';
const MODEL_2_OFFSET_X = 0.0; // X軸オフセット (中央)
const MODEL_2_OFFSET_Z = 0.0; // Z軸オフセット
const IS_MODEL_2_VISIBLE = true; // モデル2の表示 (true: ON, false: OFF)
// --------------------------------------------------------------------------------
// 4. モデル3 (hanamiku) の設定
// --------------------------------------------------------------------------------
const MODEL_3_PATH = assetsPath + 'mmd/hanamiku/';
const MODEL_3_PMX = MODEL_3_PATH + 'hanamiku.pmx';
const MODEL_3_VMD = motionFolder + 'mmd_SummerIdol_RIN.vmd'; // Rinと同じモーションを使用
const MODEL_3_OFFSET_X = 5.0; // X軸オフセット
const MODEL_3_OFFSET_Z = 18.0; // Z軸オフセット
const IS_MODEL_3_VISIBLE = true; // モデル3の表示 (true: ON, false: OFF)
// --------------------------------------------------------------------------------
// 5. モーション・オーディオ・カメラの設定 (3体共通)
// --------------------------------------------------------------------------------
const AUDIO_WAV = motionFolder + 'pv_624.wav';
const CAMERA_VMD = motionFolder + 'Camera_MAIN.vmd';
// --------------------------------------------------------------------------------
// 6. ステージの設定
// --------------------------------------------------------------------------------
const STAGE_PATH = assetsPath + 'stage/taikukan/';
const STAGE_PMX = STAGE_PATH + 'taikukan.pmx';
// ステージの座標調整 (ステージを中央の原点から移動・回転させる場合に使う)
const STAGE_POSITION_Y = 0; // Y軸位置 (例: -1.0 で1m沈ませる)
const STAGE_SCALE = 1.0; // 拡大率 (例: 1.2 で1.2倍に)
const STAGE_ROTATION_Y = 0; // Y軸回転 (ラジアン, 例: Math.PI / 2 で90度回転)
const IS_STAGE_VISIBLE = true; // ステージの表示 (true: ON, false: OFF)
// --------------------------------------------------------------------------------
// 7. VRカメラの初期設定
// --------------------------------------------------------------------------------
const VR_FRONT_CAMERA_POS = new BABYLON.Vector3(0, 16, -20); // 正面ボタンのカメラ位置
const VR_FRONT_CAMERA_TARGET = new BABYLON.Vector3(0, 10, 0); // 正面ボタンのカメラ視線目標
const VR_BACK_CAMERA_POS = new BABYLON.Vector3(0, 16, 20); // 背面ボタンのカメラ位置
// --------------------------------------------------------------------------------
// 8. 初期動作設定
// --------------------------------------------------------------------------------
let isLooping = true; // アニメーションの初期リピート設定
// --------------------------------------------------------------------------------
// ★★★ 設定項目ここまで ★★★
// --- グローバル変数 (動的機能用) ---
// モデルのメッシュとMMDモデルインスタンス (個別変数に戻す)
let mmdMesh_1 = null;
let mmdModel_1 = null;
let mmdMesh_2 = null;
let mmdModel_2 = null;
let mmdMesh_3 = null;
let mmdModel_3 = null;
// ★修正点1: モデルごとの最新ボーン座標を格納
let latestBonePosition_1 = new BABYLON.Vector3();
let latestBonePosition_2 = new BABYLON.Vector3();
let latestBonePosition_3 = new BABYLON.Vector3();
// 追従機能用変数
let trackedModelId = 0; // 追跡対象のID (1, 2, 3, 0: なし)
let isFollowing = false; // 追跡モードON/OFF
let followOffset = new BABYLON.Vector3(); // カメラとモデルのオフセット
// Add your code here matching the playground format
const createScene = async function (engine) {
const scene = new BABYLON.Scene(engine);
let adt;
let coordinateTextBlock;
const camera = new BABYLONMMD.MmdCamera("mmdCamera", new BABYLON.Vector3(0, 10, 0), scene);
// スカイボックス/グラウンドの代わりにステージを使用するため、デフォルトの地面を作成
const ground = BABYLON.MeshBuilder.CreateGround("Ground", { width: 200, height: 200, subdivisions: 2, updatable: false }, scene);
ground.receiveShadows = true;
const hemisphericLight = new BABYLON.HemisphericLight("HemisphericLight", new BABYLON.Vector3(0, 10, -10), scene);
hemisphericLight.intensity = 0.3;
hemisphericLight.specular = new BABYLON.Color3(0, 0, 0);
hemisphericLight.groundColor = new BABYLON.Color3(1.1, 1.1, 1.1);
const shadowLight = new BABYLON.DirectionalLight("shadowLight", new BABYLON.Vector3(-1, -2, 1), scene);
shadowLight.position = new BABYLON.Vector3(20, 100, 100);
const shadowGenerator = new BABYLON.ShadowGenerator(1024, shadowLight, true);
shadowGenerator.useBlurExponentialShadowMap = true;
shadowGenerator.blurKernel = 32;
// --- ステージのメッシュを読み込む ---
let stageMesh = null;
if (IS_STAGE_VISIBLE) {
stageMesh = await BABYLON.SceneLoader.ImportMeshAsync(undefined, STAGE_PMX, undefined, scene).then((result) => result.meshes[0]);
// ★★★ 設定項目反映: ステージの位置と大きさを調整 ★★★
stageMesh.position.y = STAGE_POSITION_Y;
stageMesh.scaling = new BABYLON.Vector3(STAGE_SCALE, STAGE_SCALE, STAGE_SCALE);
stageMesh.rotation.y = STAGE_ROTATION_Y;
stageMesh.isVisible = IS_STAGE_VISIBLE;
// ★★★ 設定項目反映ここまで ★★★
// 地面と影の調整
stageMesh.getChildMeshes().forEach(mesh => mesh.receiveShadows = true);
ground.dispose(); // 元の地面を削除
}
const vmdLoader = new BABYLONMMD.VmdLoader(scene);
// --- MMDランタイムと物理演算の初期化 ---
const havokInstance = await HavokPhysics();
const havokPlugin = new BABYLON.HavokPlugin(true, havokInstance);
scene.enablePhysics(new BABYLON.Vector3(0, -9.8 * 8, 0), havokPlugin);
const mmdRuntime = new BABYLONMMD.MmdRuntime(scene, new BABYLONMMD.MmdPhysics(scene));
mmdRuntime.register(scene);
// --- モデル1 (Rin) のロードと設定 ---
if (IS_MODEL_1_VISIBLE) {
mmdMesh_1 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, MODEL_1_PMX, undefined, scene).then((result) => result.meshes[0]);
shadowGenerator.addShadowCaster(mmdMesh_1);
mmdMesh_1.receiveShadows = true;
mmdMesh_1.position.x = MODEL_1_OFFSET_X;
mmdMesh_1.position.z = MODEL_1_OFFSET_Z;
mmdMesh_1.name = "Model 1 (Rin)"; // 名前設定
const modelMotion_1 = await vmdLoader.loadAsync("model_motion_1", MODEL_1_VMD);
mmdModel_1 = mmdRuntime.createMmdModel(mmdMesh_1);
const modelAnimationHandle_1 = mmdModel_1.createRuntimeAnimation(modelMotion_1);
mmdModel_1.setRuntimeAnimation(modelAnimationHandle_1);
// 最初の表示ONモデルをデフォルトの追跡対象に設定
if (trackedModelId === 0) {
trackedModelId = 1;
}
}
// --- モデル2 (Miku) のロードと設定 ---
if (IS_MODEL_2_VISIBLE) {
mmdMesh_2 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, MODEL_2_PMX, undefined, scene).then((result) => result.meshes[0]);
shadowGenerator.addShadowCaster(mmdMesh_2);
mmdMesh_2.receiveShadows = true;
mmdMesh_2.position.x = MODEL_2_OFFSET_X;
mmdMesh_2.position.z = MODEL_2_OFFSET_Z;
mmdMesh_2.name = "Model 2 (Miku)"; // 名前設定
const modelMotion_2 = await vmdLoader.loadAsync("model_motion_2", MODEL_2_VMD);
mmdModel_2 = mmdRuntime.createMmdModel(mmdMesh_2);
const modelAnimationHandle_2 = mmdModel_2.createRuntimeAnimation(modelMotion_2);
mmdModel_2.setRuntimeAnimation(modelAnimationHandle_2);
if (trackedModelId === 0) {
trackedModelId = 2;
}
}
// --- モデル3 (hanamiku) のロードと設定 ---
if (IS_MODEL_3_VISIBLE) {
mmdMesh_3 = await BABYLON.SceneLoader.ImportMeshAsync(undefined, MODEL_3_PMX, undefined, scene).then((result) => result.meshes[0]);
shadowGenerator.addShadowCaster(mmdMesh_3);
mmdMesh_3.receiveShadows = true;
mmdMesh_3.position.x = MODEL_3_OFFSET_X;
mmdMesh_3.position.z = MODEL_3_OFFSET_Z;
mmdMesh_3.name = "Model 3 (hanamiku)"; // 名前設定
const modelMotion_3 = await vmdLoader.loadAsync("model_motion_3", MODEL_3_VMD);
mmdModel_3 = mmdRuntime.createMmdModel(mmdMesh_3);
const modelAnimationHandle_3 = mmdModel_3.createRuntimeAnimation(modelMotion_3);
mmdModel_3.setRuntimeAnimation(modelAnimationHandle_3);
if (trackedModelId === 0) {
trackedModelId = 3;
}
}
// カメラにモーションを適用
mmdRuntime.addAnimatable(camera);
const cameraMotion = await vmdLoader.loadAsync("camera_motion", CAMERA_VMD);
const cameraMotionHandle = camera.createRuntimeAnimation(cameraMotion);
camera.setRuntimeAnimation(cameraMotionHandle);
// オーディオと再生
const audioPlayer = new BABYLONMMD.StreamAudioPlayer(scene);
audioPlayer.preservesPitch = false;
audioPlayer.source = AUDIO_WAV;
mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();
const mmdPlayerControl = new BABYLONMMD.MmdPlayerControl(scene, mmdRuntime, audioPlayer);
mmdPlayerControl.showPlayerControl();
// 物理演算完了後、ONになっている全モデルの上半身ボーン座標を格納
scene.onAfterPhysicsObservable.add(() => {
try {
// モデル1の座標を格納
if (mmdModel_1 && mmdModel_1.skeleton) {
const targetBone = mmdModel_1.skeleton.bones.find(b => b.name === '上半身');
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_1);
}
}
// モデル2の座標を格納
if (mmdModel_2 && mmdModel_2.skeleton) {
const targetBone = mmdModel_2.skeleton.bones.find(b => b.name === '上半身');
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_2);
}
}
// モデル3の座標を格納
if (mmdModel_3 && mmdModel_3.skeleton) {
const targetBone = mmdModel_3.skeleton.bones.find(b => b.name === '上半身');
if (targetBone) {
const finalMatrix = targetBone.getFinalMatrix();
finalMatrix.decompose(undefined, undefined, latestBonePosition_3);
}
}
} catch (e) {
console.error("onAfterPhysicsObservableでエラー:", e);
}
});
// =================================================================
// VR機能、UIパネル、ジョイスティック移動処理
// =================================================================
const xr = await scene.createDefaultXRExperienceAsync({
uiOptions: {
sessionMode: 'immersive-vr',
supportedSessionModes: ['immersive-vr', 'immersive-ar']
}
});
// --- 追従ボタンを動的に生成・更新する関数 ---
const updateFollowButtons = (stackPanel, xrCamera) => {
// 既存の動的な追従ボタンを全て削除
const controlsToRemove = stackPanel.controls.filter(c => c.name && c.name.startsWith("followButton_"));
controlsToRemove.forEach(c => stackPanel.removeControl(c));
const buttonHeight = "60px";
const models = [
{ id: 1, visible: IS_MODEL_1_VISIBLE, model: mmdModel_1, position: latestBonePosition_1, name: "Rin" },
{ id: 2, visible: IS_MODEL_2_VISIBLE, model: mmdModel_2, position: latestBonePosition_2, name: "Miku" },
{ id: 3, visible: IS_MODEL_3_VISIBLE, model: mmdModel_3, position: latestBonePosition_3, name: "Hanamiku" }
];
let insertionIndex = stackPanel.controls.findIndex(c => c.name === "arButton") + 1; // ARボタンの直後に挿入
// 表示ONのモデルごとにボタンを生成
models.forEach((config) => {
// 表示がONで、かつMMDモデルインスタンスが生成されているモデルのみボタンを表示
if (config.visible && config.model) {
const isCurrentlyTracking = trackedModelId === config.id && isFollowing;
const buttonText = isCurrentlyTracking ? `解除 ${config.id}` : `追従 ${config.id}`;
const buttonColor = isCurrentlyTracking ? "#9C27B0" : "#607D8B";
const followButton = BABYLON.GUI.Button.CreateSimpleButton(`followButton_${config.id}`, buttonText);
followButton.width = "100%";
followButton.height = buttonHeight;
followButton.color = "white";
followButton.background = buttonColor;
followButton.fontSize = 24;
followButton.name = `followButton_${config.id}`; // 名前を設定
followButton.onPointerClickObservable.add(() => {
// クリック時の追跡状態をチェック
const clickedIsCurrentlyTracking = trackedModelId === config.id && isFollowing;
// カメラのインスタンスを取得
const currentXrCamera = xr.baseExperience.camera;
if (clickedIsCurrentlyTracking) {
// 既に追跡中のモデルのボタンを押した場合 -> 追跡解除 (ジョイスティック移動に戻る)
isFollowing = false;
trackedModelId = 0; // 追跡対象なし
} else {
// 別のモデルのボタンを押した場合 -> そのモデルを追跡
isFollowing = true;
trackedModelId = config.id;
// 最新座標を取得してオフセットを計算
// config.position は onAfterPhysicsObservable で更新された最新の上半身座標
followOffset = currentXrCamera.position.subtract(config.position);
}
// UIの状態を全体的に再描画
updateFollowButtons(stackPanel, currentXrCamera);
});
// 指定位置にボタンを挿入
stackPanel.addControl(followButton);
stackPanel.controls.splice(insertionIndex, 0, stackPanel.controls.pop());
insertionIndex++;
}
});
// 座標表示テキストブロックの位置をスタックパネルの末尾に移動(UI更新時に再配置するため)
if (coordinateTextBlock) {
stackPanel.addControl(coordinateTextBlock);
}
};
// --- 左手コントローラーにUIパネルを追加 ---
xr.input.onControllerAddedObservable.add((controller) => {
if (controller.inputSource.handedness === 'left') {
let uiParent = scene.getTransformNodeByName("leftUIParent");
const xrCamera = xr.baseExperience.camera; // カメラへの参照
if (!uiParent) {
// UIの親となるTransformNodeを作成
uiParent = new BABYLON.TransformNode("leftUIParent", scene);
uiParent.position = new BABYLON.Vector3(0, 0.05, 0.08);
uiParent.rotation = new BABYLON.Vector3(Math.PI / 4, 0, 0);
// UIを表示するための平面メッシュを作成 (高さを調整)
const uiPlane = BABYLON.MeshBuilder.CreatePlane("uiPlane", { width: 0.25, height: 1.0 }, scene);
uiPlane.parent = uiParent;
uiPlane.visibility = 0.9;
// 平面メッシュにGUIテクスチャを適用
adt = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(uiPlane);
// ボタンを縦に並べるためのStackPanelを作成
const stackPanel = new BABYLON.GUI.StackPanel();
stackPanel.isVertical = true;
stackPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
adt.addControl(stackPanel);
const buttonHeight = "60px";
// --- 静的なボタン群 ---
// 5. 「最初から」ボタン
const resetButton = BABYLON.GUI.Button.CreateSimpleButton("resetButton", "最初から");
resetButton.width = "100%"; resetButton.height = buttonHeight; resetButton.color = "white";
resetButton.background = "#4CAF50"; resetButton.fontSize = 24;
resetButton.onPointerClickObservable.add(() => { mmdRuntime.seekAnimation(0); });
stackPanel.addControl(resetButton);
// 6. 「再生/停止」トグルボタン
const playPauseButton = BABYLON.GUI.Button.CreateSimpleButton("playPauseButton", "停止");
playPauseButton.width = "100%"; playPauseButton.height = buttonHeight; playPauseButton.color = "white";
playPauseButton.background = "#f44336"; playPauseButton.fontSize = 24;
playPauseButton.onPointerClickObservable.add(() => {
if (mmdRuntime.isAnimationPlaying) {
mmdRuntime.pauseAnimation(); playPauseButton.textBlock.text = "再生"; playPauseButton.background = "#2196F3";
} else {
mmdRuntime.playAnimation(); playPauseButton.textBlock.text = "停止"; playPauseButton.background = "#f44336";
}
});
stackPanel.addControl(playPauseButton);
// 7. 「正面」ボタン
const frontButton = BABYLON.GUI.Button.CreateSimpleButton("frontButton", "正面");
frontButton.width = "100%"; frontButton.height = buttonHeight; frontButton.color = "white";
frontButton.background = "#2196F3"; frontButton.fontSize = 24;
frontButton.onPointerClickObservable.add(() => {
isFollowing = false; // 追従モードを解除
trackedModelId = 0; // 追従対象なし
updateFollowButtons(stackPanel, xrCamera); // UI更新
xrCamera.position.copyFrom(VR_FRONT_CAMERA_POS);
xrCamera.setTarget(VR_FRONT_CAMERA_TARGET);
});
stackPanel.addControl(frontButton);
// 8. 「背面」ボタン
const backButton = BABYLON.GUI.Button.CreateSimpleButton("backButton", "背面");
backButton.width = "100%"; backButton.height = buttonHeight; backButton.color = "white";
backButton.background = "#FF9800"; backButton.fontSize = 24;
backButton.onPointerClickObservable.add(() => {
isFollowing = false; // 追従モードを解除
trackedModelId = 0; // 追従対象なし
updateFollowButtons(stackPanel, xrCamera); // UI更新
xrCamera.position.copyFrom(VR_BACK_CAMERA_POS);
xrCamera.setTarget(VR_FRONT_CAMERA_TARGET);
});
stackPanel.addControl(backButton);
// 9. 「リピート」トグルボタン
const repeatButton = BABYLON.GUI.Button.CreateSimpleButton("repeatButton", isLooping ? "リピート: オン" : "リピート: オフ");
repeatButton.width = "100%"; repeatButton.height = buttonHeight; repeatButton.color = "white";
repeatButton.background = isLooping ? "#FFC107" : "#795548";
repeatButton.fontSize = 24;
repeatButton.onPointerClickObservable.add(() => {
isLooping = !isLooping;
if (isLooping) {
repeatButton.textBlock.text = "リピート: オン"; repeatButton.background = "#FFC107";
} else {
repeatButton.textBlock.text = "リピート: オフ"; repeatButton.background = "#795548";
}
});
stackPanel.addControl(repeatButton);
// 10. 「ARモード」ボタン
const arButton = BABYLON.GUI.Button.CreateSimpleButton("arButton", "ARモード");
arButton.name = "arButton";
arButton.width = "100%"; arButton.height = buttonHeight; arButton.color = "white";
arButton.background = "#9C27B0"; arButton.fontSize = 24;
arButton.onPointerClickObservable.add(async () => {
if (xr.baseExperience.state === BABYLON.WebXRState.IN_XR) { await xr.baseExperience.exitXRAsync(); }
await xr.baseExperience.enterXRAsync("immersive-ar", "local-floor");
});
stackPanel.addControl(arButton);
// --- 動的な追従ボタンの追加 ---
updateFollowButtons(stackPanel, xrCamera);
// 座標表示用のテキストブロック
coordinateTextBlock = new BABYLON.GUI.TextBlock("coordinateTextBlock");
coordinateTextBlock.text = "追従対象 (未選択)";
coordinateTextBlock.color = "white";
coordinateTextBlock.fontSize = 20;
coordinateTextBlock.height = "50px";
stackPanel.addControl(coordinateTextBlock);
// ボーン名表示ボタンとテキストブロック (デバッグ用)
const showBonesButton = BABYLON.GUI.Button.CreateSimpleButton("showBonesButton", "ボーン名表示");
showBonesButton.width = "100%"; showBonesButton.height = buttonHeight; showBonesButton.color = "white";
showBonesButton.background = "#3F51B5"; showBonesButton.fontSize = 24;
stackPanel.addControl(showBonesButton);
const boneListTextBlock = new BABYLON.GUI.TextBlock("boneListTextBlock", "↑ボタンを押してボーン名を表示");
boneListTextBlock.color = "white"; boneListTextBlock.fontSize = 16;
boneListTextBlock.textWrapping = true;
boneListTextBlock.resizeToFit = true;
boneListTextBlock.paddingTop = "10px";
stackPanel.addControl(boneListTextBlock);
showBonesButton.onPointerClickObservable.add(() => {
let debugMessage = "";
let model = null;
if (trackedModelId === 1) model = mmdModel_1;
else if (trackedModelId === 2) model = mmdModel_2;
else if (trackedModelId === 3) model = mmdModel_3;
if (model && model.mesh) {
debugMessage += `現在追跡対象: ${model.mesh.name}\n`;
}
if (model && model.skeleton) {
const bones = model.skeleton.bones;
debugMessage += `ボーンの数: ${bones.length}個\n`;
const boneNames = bones.slice(0, 15).map(bone => bone.name).join(', ') + (bones.length > 15 ? ', ...' : '');
debugMessage += "名前リスト(一部): " + boneNames;
} else {
debugMessage += "追跡対象モデルが未選択または未ロードです。";
}
boneListTextBlock.text = debugMessage;
});
}
uiParent.parent = controller.grip;
}
});
// VRモード切替時のアニメーション制御
xr.baseExperience.onStateChangedObservable.add((state) => {
const stackPanel = adt ? adt.getChildren()[0] : null;
const xrCamera = xr.baseExperience.camera;
if (state === BABYLON.WebXRState.IN_XR) {
camera.setRuntimeAnimation(null);
if (xr.baseExperience.sessionManager.sessionMode === 'immersive-ar') {
ground.visibility = false;
if (stageMesh) {
stageMesh.isVisible = false;
stageMesh.getChildMeshes().forEach(mesh => {mesh.isVisible = false;});
}
}
} else if (state === BABYLON.WebXRState.NOT_IN_XR) {
camera.setRuntimeAnimation(cameraMotionHandle);
isFollowing = false;
trackedModelId = 0; // 追従解除
if (stackPanel) {
updateFollowButtons(stackPanel, xrCamera); // UIリセット
}
ground.visibility = true;
if (stageMesh) {
stageMesh.isVisible = IS_STAGE_VISIBLE;
stageMesh.getChildMeshes().forEach(mesh => {mesh.isVisible = IS_STAGE_VISIBLE;});
}
}
});
// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {
let currentBonePos = null;
let currentModelName = "未選択";
// 手動リピートロジック
if (isLooping && mmdRuntime.isAnimationPlaying) {
const currentTime = mmdRuntime.currentTime;
const duration = mmdRuntime.animationDuration;
if (duration > 0 && currentTime >= duration * 0.999) {
mmdRuntime.seekAnimation(0);
mmdRuntime.playAnimation();
}
}
if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;
// 追従対象のモデルと座標を取得
if (trackedModelId === 1 && mmdModel_1) {
currentBonePos = latestBonePosition_1;
currentModelName = mmdModel_1.mesh.name;
} else if (trackedModelId === 2 && mmdModel_2) {
currentBonePos = latestBonePosition_2;
currentModelName = mmdModel_2.mesh.name;
} else if (trackedModelId === 3 && mmdModel_3) {
currentBonePos = latestBonePosition_3;
currentModelName = mmdModel_3.mesh.name;
}
// --- 追従モードの処理 ---
if (coordinateTextBlock) {
if (currentBonePos && trackedModelId !== 0) {
coordinateTextBlock.text = `追従対象 ${currentModelName} X: ${currentBonePos.x.toFixed(2)} Y: ${currentBonePos.y.toFixed(2)} Z: ${currentBonePos.z.toFixed(2)}`;
} else {
coordinateTextBlock.text = `追従なし (ジョイスティック移動)`;
}
}
if (isFollowing && currentBonePos) {
const desiredPosition = currentBonePos.clone().add(followOffset);
xr.baseExperience.camera.position.copyFrom(desiredPosition);
}
// --- ジョイスティック操作の処理 ---
// isFollowingがfalseの時だけジョイスティック操作を許可
if (!isFollowing) {
const deltaMillis = engine.getDeltaTime();
const xrCamera = xr.baseExperience.camera;
// 左コントローラー: 平行移動
const leftController = xr.input.controllers.find(c => c.inputSource.handedness === 'left');
if (leftController?.motionController) {
const thumbstick = leftController.motionController.getComponent("xr-standard-thumbstick");
if (thumbstick?.axes) {
const moveSpeed = 6.0 * deltaMillis / 1000;
const forward = xrCamera.getDirection(BABYLON.Vector3.Forward());
forward.y = 0;
xrCamera.position.addInPlace(forward.normalize().scale(-thumbstick.axes.y * moveSpeed));
const right = xrCamera.getDirection(BABYLON.Vector3.Right());
xrCamera.position.addInPlace(right.scale(thumbstick.axes.x * moveSpeed));
}
}
// 右コントローラー: 回転と上下移動
const rightController = xr.input.controllers.find(c => c.inputSource.handedness === 'right');
if (rightController?.motionController) {
const thumbstick = rightController.motionController.getComponent("xr-standard-thumbstick");
if (thumbstick?.axes) {
const rotationThreshold = 0.2;
if (Math.abs(thumbstick.axes.x) > rotationThreshold) {
const rotSpeed = 0.4 * deltaMillis / 1000;
if (xrCamera.parent) {
xrCamera.parent.rotate(BABYLON.Vector3.Up(), thumbstick.axes.x * rotSpeed, BABYLON.Space.WORLD);
}
}
if (Math.abs(thumbstick.axes.y) > rotationThreshold) {
const verticalSpeed = 2.0 * deltaMillis / 1000;
xrCamera.position.y += -thumbstick.axes.y * verticalSpeed;
}
}
}
}
});
return scene;
};
(async function () {
canvas.focus();
const scene = await createScene(engine); //Call the createScene function
// Register a render loop to repeatedly render the scene
engine.runRenderLoop(async function () {
scene.render();
});
// Watch for browser/canvas resize events
window.addEventListener("resize", function () {
engine.resize();
});
})()
</script>
</body>
</html>
使用変数
| -------( Function ) | |
| action | |
| adt | |
| arButton | |
| assetsPath | |
| audioPlayer | |
| AUDIO_WAV | |
| b | |
| backButton | |
| background | |
| blurKernel | |
| bone | |
| boneListTextBlock | |
| boneNames | |
| bones | |
| br> // Register a render loop to repeatedly render the scene engine.runRenderLoop -------( Function ) | |
| buttonColor | |
| buttonHeight | |
| buttonText | |
| c | |
| camera | |
| cameraMotion | |
| cameraMotionHandle | |
| CAMERA_VMD | |
| canvas | |
| charset | |
| color | |
| content | |
| controlsToRemove | |
| createScene | |
| currentBonePos | |
| currentModelName | |
| currentTime | |
| currentXrCamera | |
| debugMessage | |
| deltaMillis | |
| desiredPosition | |
| duration | |
| engine | |
| equiv | |
| finalMatrix | |
| followButton | |
| followOffset | |
| fontSize | |
| forward | |
| frontButton | |
| ground | |
| groundColor | |
| handedness | |
| havokInstance | |
| havokPlugin | |
| height | |
| hemisphericLight | |
| id | |
| insertionIndex | |
| intensity | |
| isFollowing | |
| isLooping | |
| isVertical | |
| isVisible | |
| IS_MODEL_1_VISIBLE | |
| IS_MODEL_2_VISIBLE | |
| IS_MODEL_3_VISIBLE | |
| IS_STAGE_VISIBLE | |
| lAnimationHandle_1 | |
| lAnimationHandle_2 | |
| lAnimationHandle_3 | |
| leftController | |
| mesh | |
| mmdMesh_1 | |
| mmdMesh_2 | |
| mmdMesh_3 | |
| mmdModel_1 | |
| mmdModel_2 | |
| mmdModel_3 | |
| mmdPlayerControl | |
| mmdRuntime | |
| model | |
| modelMotion_1 | |
| modelMotion_2 | |
| modelMotion_3 | |
| models | |
| MODEL_1_OFFSET_X | |
| MODEL_1_OFFSET_Z | |
| MODEL_1_PATH | |
| MODEL_1_PMX | |
| MODEL_1_VMD | |
| MODEL_2_OFFSET_X | |
| MODEL_2_OFFSET_Z | |
| MODEL_2_PATH | |
| MODEL_2_PMX | |
| MODEL_2_VMD | |
| MODEL_3_OFFSET_X | |
| MODEL_3_OFFSET_Z | |
| MODEL_3_PATH | |
| MODEL_3_PMX | |
| MODEL_3_VMD | |
| motionFolder | |
| moveSpeed | |
| name | |
| oordinateTextBlock | |
| paddingTop | |
| parent | |
| pdateFollowButtons | |
| playPauseButton | |
| ponentialShadowMap | |
| position | |
| preservesPitch | |
| receiveShadows | |
| repeatButton | |
| resetButton | |
| resizeToFit | |
| right | |
| rightController | |
| RONT_CAMERA_TARGET | |
| rotation | |
| rotationThreshold | |
| rotSpeed | |
| R_FRONT_CAMERA_POS | |
| scaling | |
| scene | |
| sCurrentlyTracking | |
| sessionMode | |
| shadowGenerator | |
| shadowLight | |
| showBonesButton | |
| source | |
| specular | |
| src | |
| stackPanel | |
| stageMesh | |
| STAGE_PATH | |
| STAGE_PMX | |
| STAGE_POSITION_Y | |
| STAGE_ROTATION_Y | |
| STAGE_SCALE | |
| state | |
| targetBone | |
| testBonePosition_1 | |
| testBonePosition_2 | |
| testBonePosition_3 | |
| text | |
| textWrapping | |
| thumbstick | |
| trackedModelId | |
| uiParent | |
| uiPlane | |
| verticalAlignment | |
| verticalSpeed | |
| visibility | |
| vmdLoader | |
| VR_BACK_CAMERA_POS | |
| width | |
| x | |
| xmlns | |
| xr | |
| xrCamera | |
| y | |
| z |