junkerstock
 vrbx1-33 

<!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</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/babylon.js"></script>
<script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
<script src="https://cdn.babylonjs.com/havok/HavokPhysics_umd.js"></script>
<script src="https://www.unpkg.com/babylon-mmd/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


const assetsPath = 'assets/'
const pmxPath = assetsPath + 'mmd/rin/'
const pmxModel = pmxPath + 'Black.pmx'

const vmdPath = assetsPath + 'ElectricAngel/'
const vmdModel = vmdPath + 'mmd_ElectricAngel2022Remake_motion.vmd'

const wavPath = assetsPath + 'ElectricAngel/'
const wavModel = wavPath + 'pv_769.wav'

const camPath = assetsPath + 'ElectricAngel/';
const camModel = camPath + 'CAMERAMAIN.vmd'

const offsetY = -100


// 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;

const mmdMesh = await BABYLON.SceneLoader.ImportMeshAsync(undefined, pmxModel, undefined, scene).then((result) => result.meshes[0]);

shadowGenerator.addShadowCaster(mmdMesh);
mmdMesh.receiveShadows = true;

const vmdLoader = new BABYLONMMD.VmdLoader(scene);
const modelMotion = await vmdLoader.loadAsync("model_motion", vmdModel);


const havokInstance = await HavokPhysics();
const havokPlugin = new BABYLON.HavokPlugin(true, havokInstance);
scene.enablePhysics(new BABYLON.Vector3(0, -9.8 * 5, 0), havokPlugin);

const mmdRuntime = new BABYLONMMD.MmdRuntime(scene, new BABYLONMMD.MmdPhysics(scene));
mmdRuntime.register(scene);
const mmdModel = mmdRuntime.createMmdModel(mmdMesh);

mmdModel.addAnimation(modelMotion);
mmdModel.setAnimation("model_motion");

mmdRuntime.setCamera(camera);
const cameraMotion = await vmdLoader.loadAsync("camera_motion", camModel);
camera.addAnimation(cameraMotion);
camera.setAnimation("camera_motion");

const audioPlayer = new BABYLONMMD.StreamAudioPlayer(scene);
audioPlayer.preservesPitch = false;
audioPlayer.source = wavModel;

mmdRuntime.setAudioPlayer(audioPlayer);

mmdRuntime.playAnimation();

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

// =================================================================
// ▼▼▼ VR機能、UIパネル、ジョイスティック移動処理 ▼▼▼
// =================================================================
const xr = await scene.createDefaultXRExperienceAsync({});

// ▼▼▼ 追従モードの状態を管理する変数 ▼▼▼
let isFollowing = false;
let followOffset = new BABYLON.Vector3();


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

let uiParent = scene.getTransformNodeByName("leftUIParent");

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

// 2. UIを表示するための平面メッシュを作成
const uiPlane = BABYLON.MeshBuilder.CreatePlane("uiPlane", { width: 0.2, height: 0.42 }, scene); // 高さを少し広げます
uiPlane.parent = uiParent;
uiPlane.visibility = 0.9;

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

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

const buttonHeight = "70px"; // ボタンの高さを統一

// 5. 「再生」ボタン
const playButton = BABYLON.GUI.Button.CreateSimpleButton("playButton", "再生");
playButton.width = "100%"; playButton.height = buttonHeight; playButton.color = "white";
playButton.background = "#4CAF50"; playButton.fontSize = 24;
playButton.onPointerClickObservable.add(() => { mmdRuntime.playAnimation(); });
stackPanel.addControl(playButton);

// 6. 「停止」ボタン
const stopButton = BABYLON.GUI.Button.CreateSimpleButton("stopButton", "停止");
stopButton.width = "100%"; stopButton.height = buttonHeight; stopButton.color = "white";
stopButton.background = "#f44336"; stopButton.fontSize = 24;
stopButton.onPointerClickObservable.add(() => { mmdRuntime.pauseAnimation(); });
stackPanel.addControl(stopButton);

// 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; // 追従モードを解除
xr.baseExperience.camera.position.set(0, 16, -20);
xr.baseExperience.camera.setTarget(new BABYLON.Vector3(0, 10, 0));
});
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; // 追従モードを解除
xr.baseExperience.camera.position.set(0, 16, 20);
xr.baseExperience.camera.setTarget(new BABYLON.Vector3(0, 10, 0));
});
stackPanel.addControl(backButton);

// ▼▼▼ 9. 「追従/解除」ボタンを作成 ▼▼▼
const followButton = BABYLON.GUI.Button.CreateSimpleButton("followButton", "追従");
followButton.width = "100%";
followButton.height = buttonHeight;
followButton.color = "white";
followButton.background = "#607D8B"; // グレー系の色
followButton.fontSize = 24;

followButton.onPointerClickObservable.add(() => {
isFollowing = !isFollowing; // 追従モードをトグル
if (isFollowing) {
// 追従開始:現在のカメラとモデルの「真の中心」との相対位置を計算して保存
const xrCamera = xr.baseExperience.camera;
// ★★★ 修正点1: 素体の正確な中心位置を取得 ★★★
const modelCenter = mmdMesh.getBoundingInfo().boundingBox.centerWorld;
followOffset = xrCamera.position.subtract(modelCenter);

followButton.textBlock.text = "解除";
followButton.background = "#9C27B0"; // 紫色に変更
} else {
// 追従解除
followButton.textBlock.text = "追従";
followButton.background = "#607D8B"; // 元の色に戻す
}
});
stackPanel.addControl(followButton);




// ▼▼▼ 追加するコード ▼▼▼
// 座標表示用のテキストブロック
coordinateTextBlock = new BABYLON.GUI.TextBlock("coordinateTextBlock");
coordinateTextBlock.text = "X: 0.00 Y: 0.00 Z: 0.00";
coordinateTextBlock.color = "white";
coordinateTextBlock.fontSize = 20;
coordinateTextBlock.height = "50px";
stackPanel.addControl(coordinateTextBlock);






}

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




// VRモードの出入りを監視
xr.baseExperience.onStateChangedObservable.add((state) => {
if (state === BABYLON.WebXRState.IN_XR) {
camera.setAnimation(null);
} else if (state === BABYLON.WebXRState.NOT_IN_XR) {
camera.setAnimation("camera_motion");
isFollowing = false;
}
});






// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {
if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;

// ▼▼▼ ここからが最終的な修正コード ▼▼▼
// MMDモデルとスケルトンが利用可能か、毎フレーム最初に確認する
if (mmdMesh && mmdMesh.skeleton) {
// --- ボーンの座標をUIに表示 ---
const skeleton = mmdMesh.skeleton;
const targetBone = skeleton.bones.find(b => b.name === 'センター');

if (targetBone && coordinateTextBlock) {
const currentModelCenter = targetBone.getAbsolutePosition(mmdMesh);
coordinateTextBlock.text = `X: ${currentModelCenter.x.toFixed(2)} Y: ${currentModelCenter.y.toFixed(2)} Z: ${currentModelCenter.z.toFixed(2)}`;
}

// --- 追従モードの処理 ---
if (isFollowing) {
// isFollowingがtrueの時だけ追従処理を実行
const followBone = skeleton.bones.find(b => b.name === 'センター'); // 追従の基準ボーン
if (followBone) {
const currentModelCenter = followBone.getAbsolutePosition(mmdMesh);
const desiredPosition = currentModelCenter.add(followOffset);
xr.baseExperience.camera.position.copyFrom(desiredPosition);
xr.baseExperience.camera.setTarget(currentModelCenter);
}
}
}
// ▲▲▲ ここまでが最終的な修正コード ▲▲▲

// --- ジョイスティック操作の処理 ---
// 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
assetsPath
audioPlayer
b
backButton
background
blurKernel
br> // Register a render loop to repeatedly render the scene engine.runRenderLoop -------( Function )
buttonHeight
c
camera
cameraMotion
camModel
camPath
canvas
charset
color
content
createScene
currentModelCenter
deltaMillis
desiredPosition
engine
equiv
followBone
followButton
followOffset
fontSize
forward
frontButton
ground
groundColor
handedness
havokInstance
havokPlugin
height
hemisphericLight
id
intensity
isFollowing
isVertical
leftController
mmdMesh
mmdModel
mmdPlayerControl
mmdRuntime
modelCenter
modelMotion
moveSpeed
name
offsetY
oordinateTextBlock
parent
playButton
pmxModel
pmxPath
ponentialShadowMap
position
preservesPitch
receiveShadows
right
rightController
rotation
rotationThreshold
rotSpeed
scene
shadowGenerator
shadowLight
skeleton
source
specular
src
stackPanel
state
stopButton
targetBone
text
thumbstick
uiParent
uiPlane
verticalAlignment
verticalSpeed
visibility
vmdLoader
vmdModel
vmdPath
wavModel
wavPath
width
xmlns
xr
xrCamera
y