<!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);
//MMDのカメラモーション
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({});
// --- 左手コントローラーにUIパネルを追加 ---
xr.input.onControllerAddedObservable.add((controller) => {
if (controller.inputSource.handedness === 'left') {
// 1. UIの親となるTransformNodeを作成し、コントローラーのグリップに追従させる
const uiParent = new BABYLON.TransformNode("leftUIParent", scene);
uiParent.parent = controller.grip;
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.36 }, scene);
uiPlane.parent = uiParent; // 親に追従させる
uiPlane.visibility = 0.9;
// 3. 平面メッシュにGUIテクスチャを適用
const 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);
// 5. 「再生」ボタンを作成
const playButton = BABYLON.GUI.Button.CreateSimpleButton("playButton", "再生");
playButton.width = "100%";
playButton.height = "150px";
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 = "150px";
stopButton.color = "white";
stopButton.background = "#f44336"; // 赤色
stopButton.fontSize = 24;
stopButton.onPointerClickObservable.add(() => {
mmdRuntime.pauseAnimation(); // MMDRuntimeではpauseAnimation()で停止します
});
stackPanel.addControl(stopButton);
}
});
// VRモードの出入りを監視して、MMDカメラのアニメーションを制御
xr.baseExperience.onStateChangedObservable.add((state) => {
if (state === BABYLON.WebXRState.IN_XR) {
// VRに入ったらMMDカメラアニメーションを停止
camera.setAnimation(null);
} else if (state === BABYLON.WebXRState.NOT_IN_XR) {
// VRから抜けたらMMDカメラアニメーションを再開
camera.setAnimation("camera_motion");
}
});
// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {
// VRモードでなければ何もしない
if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;
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) {
// ★★★ 移動速度を5倍に変更 (1.2 * 5 = 6.0) ★★★
const moveSpeed = 6.0 * deltaMillis / 1000;
// カメラの前方ベクトルを取得(Y軸は無視して水平移動)
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) {
// XRカメラの親(XR体験のルート)をY軸周りに回転させる
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 ) | |
5 | |
action | |
adt | |
assetsPath | |
audioPlayer | |
background | |
blurKernel | |
br> // Register a render loop to repeatedly render the scene engine.runRenderLoop -------( Function ) | |
c | |
camera | |
cameraMotion | |
camModel | |
camPath | |
canvas | |
charset | |
color | |
content | |
createScene | |
deltaMillis | |
engine | |
equiv | |
fontSize | |
forward | |
ground | |
groundColor | |
handedness | |
havokInstance | |
havokPlugin | |
height | |
hemisphericLight | |
id | |
intensity | |
isVertical | |
leftController | |
mmdMesh | |
mmdModel | |
mmdPlayerControl | |
mmdRuntime | |
modelMotion | |
moveSpeed | |
offsetY | |
parent | |
playButton | |
pmxModel | |
pmxPath | |
ponentialShadowMap | |
position | |
preservesPitch | |
receiveShadows | |
right | |
rightController | |
rotation | |
rotationThreshold | |
rotSpeed | |
scene | |
shadowGenerator | |
shadowLight | |
source | |
specular | |
src | |
stackPanel | |
state | |
stopButton | |
thumbstick | |
uiParent | |
uiPlane | |
verticalAlignment | |
verticalSpeed | |
visibility | |
vmdLoader | |
vmdModel | |
vmdPath | |
wavModel | |
wavPath | |
width | |
xmlns | |
xr | |
xrCamera | |
y |