Hi!
I’m a complete newbie when it comes to babylonjs and 3D in general.
I’m trying to build a simple walkthrough to be used in combination with an Oculus headset.
I have a very basic scene which is a single room with a door. Just something miniamlistic to be able to test movement with collisions and rotation.
What I’m trying to achieve is for the user to be able to move and rotate in the scene without phisically moving, just by moving left and right joysticks.
At the moment I have the following working fine:
- Controlling movement (left, right, forwards and backwards) with right joystick
- Movement does not depend on position of headset (where you look at). So if for example you look to the right and push the joystick forward you go forward while looking to the right. You do not move to the right.
- Collision detection
- Fixed y position to avoid “flying” and “diving” into the floor
The only thing I cannot get to work properly is rotation with the left joystick.
I’ve tried asking AI but it never gets it right. When it gives me the code to achieve rotation it messes everything up. I need the user to “spin in place” but all the code its giving me till now doesnt manage the rotation well and even messes up movement too.
I tried creating a Playground but couldnt get it to work. I was getting errors all the time. I guess I dont understand how it works. I’m so sorry about that.
This is my code at the moment (with no code managing rotation).
I would be so grateful if someone could help me out.
Thanks in advance!!
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
HemisphericLight,
MeshBuilder,
Color3,
StandardMaterial
} from "@babylonjs/core";
import "@babylonjs/loaders"; // In case we load assets later
import "@babylonjs/core/XR/features/WebXRControllerMovement";
const COLLIDER_HEIGHT = 1.7;
const FIXED_Y = COLLIDER_HEIGHT / 2;
const INITIAL_POSITION = new Vector3(0, FIXED_Y, 0);
const canvas = document.getElementById("renderCanvas");
const engine = new Engine(canvas, true);
const createScene = async () => {
const scene = new Scene(engine);
scene.clearColor = new Color3(0.8, 0.9, 1.0);
scene.collisionsEnabled = true;
// Light
const light = new HemisphericLight("light", new Vector3(0, 1, 0), scene);
light.intensity = 0.8;
// Optional: non-XR camera for testing on desktop
const camera = new ArcRotateCamera("camera", Math.PI / 2, Math.PI / 3, 10, new Vector3(0, 1, 0), scene);
camera.attachControl(canvas, true);
// Create room
await createRoom(scene);
// Add WebXR experience
const xrHelper = await scene.createDefaultXRExperienceAsync({
disableTeleportation: true,
});
const input = xrHelper.input;
// Create collider
const collider = getCollider(scene, {
height: COLLIDER_HEIGHT,
position: INITIAL_POSITION
});
scene.onBeforeRenderObservable.add(() => {
const cam = scene.activeCamera;
let move = new Vector3(0, 0, 0);
if (!input.controllers || input.controllers.length === 0) {
// No controllers detected
}
else {
input.controllers.forEach(controller => {
const handedness = controller.inputSource.handedness;
let axes = null;
// Find the thumbstick component (usually called "xr-standard-thumbstick")
if (controller.motionController && controller.motionController.components) {
for (const compId in controller.motionController.components) {
const comp = controller.motionController.components[compId];
if (comp.type === "thumbstick" && comp.axes) {
axes = comp.axes;
break;
}
}
}
if (!axes || typeof axes.x !== "number" || typeof axes.y !== "number") {
return;
};
// Left controller: rotation
if (handedness === "left") {
const leftX = axes.x;
}
// Right controller: movement
if (handedness === "right") {
const rightX = axes.x;
const rightY = axes.y;
if (Math.abs(rightX) > 0.01 || Math.abs(rightY) > 0.01) {
const forward = new Vector3(0, 0, 1);
const right = new Vector3(1, 0, 0);
move.addInPlace(right.scale(-rightX * 0.1)); // left/right
move.addInPlace(forward.scale(rightY * 0.1)); // forward/backward
}
}
});
}
move.y = 0; // Only move horizontally
collider.moveWithCollisions(move);
collider.position.y = FIXED_Y;
cam.position.copyFrom(collider.position);
});
return scene;
};
createScene().then(scene => {
engine.runRenderLoop(() => {
scene.render();
});
});
window.addEventListener("resize", () => {
engine.resize();
});
async function createRoom(scene) {
// Floor
const floor = MeshBuilder.CreateGround("floor", { width: 6, height: 6 }, scene);
const floorMat = new StandardMaterial("floorMat", scene);
floorMat.diffuseColor = new Color3(0.6, 0.8, 0.9);
floor.material = floorMat;
// Door parameters
const doorWidth = 1.5;
const doorHeight = 2.2;
const wallHeight = 3;
const wallThickness = 0.2;
const roomSize = 6;
const wallMat = new StandardMaterial("wallMat", scene);
wallMat.diffuseColor = new Color3(1, 1, 1);
const wall1 = MeshBuilder.CreateBox("wall1", {
width: roomSize,
height: wallHeight,
depth: wallThickness
}, scene);
wall1.position.z = -roomSize / 2;
wall1.position.y = wallHeight / 2;
wall1.material = wallMat;
// Wall 2 (left)
const wall2 = MeshBuilder.CreateBox("wall2", {
width: wallThickness,
height: wallHeight,
depth: roomSize
}, scene);
wall2.position.x = -roomSize / 2;
wall2.position.y = wallHeight / 2;
wall2.material = wallMat;
// Wall 3 (right)
const wall3 = MeshBuilder.CreateBox("wall3", {
width: wallThickness,
height: wallHeight,
depth: roomSize
}, scene);
wall3.position.x = roomSize / 2;
wall3.position.y = wallHeight / 2;
wall3.material = wallMat;
// Left part of front wall
const frontWallLeft = MeshBuilder.CreateBox("frontWallLeft", {
width: (roomSize - doorWidth) / 2,
height: wallHeight,
depth: wallThickness
}, scene);
frontWallLeft.position.z = roomSize / 2;
frontWallLeft.position.y = wallHeight / 2;
frontWallLeft.position.x = -((roomSize - doorWidth) / 4 + doorWidth / 2);
frontWallLeft.material = wallMat;
// Right part of front wall
const frontWallRight = MeshBuilder.CreateBox("frontWallRight", {
width: (roomSize - doorWidth) / 2,
height: wallHeight,
depth: wallThickness
}, scene);
frontWallRight.position.z = roomSize / 2;
frontWallRight.position.y = wallHeight / 2;
frontWallRight.position.x = ((roomSize - doorWidth) / 4 + doorWidth / 2);
frontWallRight.material = wallMat;
// Top part above the door
const frontWallTop = MeshBuilder.CreateBox("frontWallTop", {
width: doorWidth,
height: wallHeight - doorHeight,
depth: wallThickness
}, scene);
frontWallTop.position.z = roomSize / 2;
frontWallTop.position.y = doorHeight + (wallHeight - doorHeight) / 2;
frontWallTop.position.x = 0;
frontWallTop.material = wallMat;
// Enable collisions
floor.checkCollisions = true;
wall1.checkCollisions = true;
wall2.checkCollisions = true;
wall3.checkCollisions = true;
frontWallLeft.checkCollisions = true;
frontWallRight.checkCollisions = true;
frontWallTop.checkCollisions = true;
}
function getCollider(scene, options) {
const collider = MeshBuilder.CreateBox("xrCollider", {
width: 0.25,
height: options.height || 1.7,
depth: 0.25
},
scene);
collider.isVisible = false;
collider.checkCollisions = true;
const colliderMat = new StandardMaterial("colliderMat", scene);
colliderMat.diffuseColor = new Color3(1, 0, 0);
collider.material = colliderMat;
collider.position = options.position || new Vector3(0, options.height / 2, 0);
return collider;
}