junkerstock
 vrbx1-32 

<!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({});

// ▼▼▼ 追従モードの状態を管理する変数 ▼▼▼
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テクスチャを適用
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);

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;
followOffset = xrCamera.position.subtract(mmdMesh.position);
followButton.textBlock.text = "解除";
followButton.background = "#9C27B0"; // 紫色に変更
} else {
// 追従解除
followButton.textBlock.text = "追従";
followButton.background = "#607D8B"; // 元の色に戻す
}
});
stackPanel.addControl(followButton);

}

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




// 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");
isFollowing = false; // VRを抜けたら追従モードもリセット
}
});

// 毎フレームごとの処理
scene.onBeforeRenderObservable.add(() => {
// VRモードでなければ何もしない
if (xr.baseExperience.state !== BABYLON.WebXRState.IN_XR) return;

const deltaMillis = engine.getDeltaTime();
const xrCamera = xr.baseExperience.camera;

// ▼▼▼ 追従モードの処理 ▼▼▼
if (isFollowing) {
// モデルの位置にオフセットを加えた位置にカメラを移動させる
const desiredPosition = mmdMesh.position.add(followOffset);
xrCamera.position.copyFrom(desiredPosition);

// 常にモデルの中心を見るようにターゲットを設定
xrCamera.setTarget(mmdMesh.position.add(new BABYLON.Vector3(0, 10, 0)));

} else {
// ▼▼▼ 追従モードでない時だけジョイスティック操作を許可 ▼▼▼

// 左コントローラー: 平行移動
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
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
deltaMillis
desiredPosition
engine
equiv
followButton
followOffset
fontSize
forward
frontButton
ground
groundColor
handedness
havokInstance
havokPlugin
height
hemisphericLight
id
intensity
isFollowing
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
text
thumbstick
uiParent
uiPlane
verticalAlignment
verticalSpeed
visibility
vmdLoader
vmdModel
vmdPath
wavModel
wavPath
width
xmlns
xr
xrCamera
y