import { AbstractMesh, DualShockPad, Engine, FreeCamera, FreeCameraGamepadInput, Gamepad as b_Gamepad, Mesh, Quaternion, Scene, Vector3, WebXRDefaultExperience, WebXRSessionManager, } from '@babylonjs/core/Legacy/legacy'; if ((navigator as any).oscpu?.startsWith('Linux')) { // ^ oscpu is undefined on chrome on Android, at least... DualShockPad.prototype.update = function () { b_Gamepad.prototype.update.call(this); (window as any).G = this; (this as any)._rightStickAxisX = 3; (this as any)._rightStickAxisY = 4; this.buttonCross = this.browserGamepad.buttons[0].value; this.buttonCircle = this.browserGamepad.buttons[1].value; this.buttonTriangle = this.browserGamepad.buttons[2].value; this.buttonSquare = this.browserGamepad.buttons[3].value; this.buttonL1 = this.browserGamepad.buttons[4].value; this.buttonR1 = this.browserGamepad.buttons[5].value; this.leftTrigger = this.browserGamepad.buttons[6].value; this.rightTrigger = this.browserGamepad.buttons[7].value; this.buttonShare = this.browserGamepad.buttons[8].value; this.buttonOptions = this.browserGamepad.buttons[9].value; this.buttonLeftStick = this.browserGamepad.buttons[11].value; this.buttonRightStick = this.browserGamepad.buttons[12].value; this.dPadUp = this.browserGamepad.axes[7].value < 0 ? 1 : 0; this.dPadDown = this.browserGamepad.axes[7].value > 0 ? 1 : 0; this.dPadLeft = this.browserGamepad.axes[6].value < 0 ? 1 : 0; this.dPadRight = this.browserGamepad.axes[6].value > 0 ? 1 : 0; }; } export type Interactivity = { floorMeshes: () => Mesh[], touchableMeshes: () => Mesh[], }; export type EngineOptions = { initialPos: Vector3, initialRotation: Vector3, canvas: HTMLCanvasElement | null, }; export type ButtonState = { [button: number]: boolean }; export class GamepadState { buttons: ButtonState = {}; constructor ( public gp: Gamepad, // NB. browser's Gamepad class, not Babylon's Gamepad class. ) {} latch(button: number): boolean { let result = false; const b = this.gp.buttons[button]; if (b) { if (b.pressed) { if (!this.isDown(button)) result = true; this.buttons[button] = true; } else { this.buttons[button] = false; } } return result; } isDown(button: number): boolean { return this.buttons[button] ?? false; } } export class RunningEngine { camera!: FreeCamera; xrSessionManager: WebXRSessionManager | null = null; gamepadInput!: FreeCameraGamepadInput; leanBase: { position: Vector3 } | null = null; recenterBase: { rotation: Quaternion } | null = null; padStates: Map = new Map(); static async start( interactivity: Interactivity, options0: Partial = {}, ): Promise { const options = Object.assign({ initialPos: new Vector3(0, 1.6, 0), initialRotation: new Vector3(0, 0, 0).scaleInPlace(2 * Math.PI), canvas: document.getElementById("renderCanvas") as HTMLCanvasElement, }, options0); const engine = new Engine(options.canvas, true); const scene = new Scene(engine); const xr = await scene.createDefaultXRExperienceAsync({}); return new RunningEngine(options, engine, interactivity, scene, xr); } private constructor ( public options: EngineOptions, public engine: Engine, public interactivity: Interactivity, public scene: Scene, public xr: WebXRDefaultExperience, ) { this.xrSessionManager = this.xr.baseExperience?.sessionManager ?? null; if (this.xrSessionManager) { this.camera = this.xr.baseExperience.camera; } else { this.camera = new FreeCamera("camera", this.options.initialPos, this.scene); this.camera.rotation = this.options.initialRotation; this.camera.minZ = 0.1; this.camera.inertia = 0.75; this.camera.speed = 0.5; this.camera.attachControl(true); } this.gamepadInput = new FreeCameraGamepadInput(); this.gamepadInput.gamepadMoveSensibility = 320; this.gamepadInput.gamepadAngularSensibility = 100; this.camera.inputs.add(this.gamepadInput); this.gamepadInput.attachControl(); this.scene.gravity = new Vector3(0, -9.81 / 90, 0); this.scene.collisionsEnabled = true; this.camera.checkCollisions = true; this.camera.applyGravity = false; (this.camera as any)._needMoveForGravity = true; this.camera.ellipsoid = new Vector3(0.25, this.options.initialPos.y / 2, 0.25); if (this.options.canvas) { const canvas = this.options.canvas; if (this.xrSessionManager) { canvas.onclick = () => this.xrEnable(); } else { canvas.onclick = () => canvas.requestPointerLock?.(); } } this.engine.runRenderLoop(() => { Array.from(navigator.getGamepads()).forEach(gp => { if (gp !== null) this.checkGamepadInput(gp); }); this.scene.render(); }); window.addEventListener("resize", () => this.engine.resize()); } padStateFor(gp: Gamepad): GamepadState { const state = this.padStates.get(gp); if (state) return state; const newState = new GamepadState(gp); this.padStates.set(gp, newState); return newState; } checkGamepadInput(gp: Gamepad) { const state = this.padStateFor(gp); if (state.latch(2)) location.reload(); if (state.latch(5)) this.turn180(); if (this.xrSessionManager) { this.updateLean(gp); if (state.latch(0)) this.xrTeleport(); if (state.latch(1)) this.xrTouch(); if (state.latch(3)) this.xrEnable(); if (state.latch(4)) { this.recenterBase = { rotation: this.xr.baseExperience.camera.rotationQuaternion.clone(), }; } if (state.isDown(4) && this.recenterBase) { this.xr.baseExperience.camera.rotationQuaternion.copyFrom( this.recenterBase.rotation); } } } updateLean(gp: Gamepad) { const pos = new Vector3((gp.axes[0]), (-gp.axes[1]), (-gp.axes[3])); if (pos.length() > 0.0625) { if (this.leanBase === null) { this.leanBase = { position: this.xr.baseExperience.camera.position }; } this.xr.baseExperience.camera.position = pos.applyRotationQuaternion(this.xr.baseExperience.camera.absoluteRotation) .scale(0.25) .add(this.leanBase.position); } else { if (this.leanBase !== null) { this.xr.baseExperience.camera.position = this.leanBase.position; } this.leanBase = null; } } turn180() { const r = this.xrSessionManager ? this.xr.baseExperience.camera.rotationQuaternion.toEulerAngles() : this.camera.rotation; r.y += Math.PI; r.y %= 2 * Math.PI; if (this.xrSessionManager) { this.xr.baseExperience.camera.rotationQuaternion.copyFrom(r.toQuaternion()); } } xrTouch() { const ray = this.xr.baseExperience.camera.getForwardRay(); const meshes = this.interactivity.touchableMeshes(); const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); if (hit === null) return; if (meshes.indexOf(hit.pickedMesh as any) === -1) return; this.camera.onCollide?.(hit.pickedMesh!); } xrTeleport() { if (!this.xrSessionManager) return; const ray = this.xr.baseExperience.camera.getForwardRay(); const meshes = this.interactivity.floorMeshes(); const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); if (hit === null) return; if (meshes.indexOf(hit.pickedMesh as any) === -1) return; if (hit.pickedPoint === null) return; const pos = hit.pickedPoint.add(new Vector3(0, 1.6, 0)); this.xr.baseExperience.camera.position = pos; this.xr.baseExperience.camera.rotation = Vector3.Zero(); if (this.leanBase !== null) this.leanBase.position = pos; } xrEnable() { if (!this.xrSessionManager) return; this.xr.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => { this.xr.baseExperience.camera.position = this.options.initialPos; this.xr.baseExperience.camera.rotation = this.options.initialRotation; this.xr.baseExperience.sessionManager.session.onselect = () => this.xrTeleport(); }); } }