Better XR support

This commit is contained in:
Tony Garnock-Jones 2023-01-11 14:28:34 +01:00
parent 24a7d2a55f
commit d59de6f641
3 changed files with 180 additions and 83 deletions

View File

@ -79,17 +79,19 @@
>
>>>
<Exit "y" "lobby">
<sprite "y"
<move <v 12.0 0.75 -5.0>
<texture ["textures/oak-herringbone-5e80fb40b00c9-1200.jpg"
<v 1.0 1.0 1.0>
<v 0.0 0.0 0.0>]
<touchable
<csg
<intersect [
<scale <v 0.5 1.0 0.5> <mesh <box>>>
<mesh <sphere>>
]>
>>
>>>
; <Exit "y" "lobby">
; <sprite "y"
; <move <v 12.0 0.75 -5.0>
; <texture ["textures/oak-herringbone-5e80fb40b00c9-1200.jpg"
; <v 1.0 1.0 1.0>
; <v 0.0 0.0 0.0>]
; <touchable
; <csg
; <intersect [
; <scale <v 0.5 1.0 0.5> <mesh <box>>>
; <mesh <sphere>>
; ]>
; >>
; >>>
[]

View File

@ -1,15 +1,19 @@
import {
AbstractMesh,
DualShockPad,
Engine,
FreeCamera,
FreeCameraGamepadInput,
Gamepad as b_Gamepad,
KeyboardEventTypes,
Mesh,
Quaternion,
Scene,
Vector3,
WebXRCamera,
WebXRDefaultExperience,
WebXRSessionManager,
WebXRState,
} from '@babylonjs/core/Legacy/legacy';
import { log } from './log.js';
@ -86,14 +90,20 @@ export class GamepadState {
}
export class RunningEngine {
camera!: FreeCamera;
xrSessionManager: WebXRSessionManager | null = null;
gamepadInput!: FreeCameraGamepadInput;
// The active camera - plainCamera or xrCamera, depending.
camera: FreeCamera;
plainCamera: FreeCamera;
gamepadInput: FreeCameraGamepadInput;
xrCamera: WebXRCamera | null = null;
leanBase: { position: Vector3 } | null = null;
recenterBase: { rotation: Quaternion } | null = null;
padStates: Map<number, GamepadState> = new Map();
padStates = new Map<number, GamepadState>();
keysDown = new Set<number>();
keysChanged = new Map<number, boolean>();
static async start(
interactivity: Interactivity,
@ -106,8 +116,14 @@ export class RunningEngine {
}, 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);
return new RunningEngine(
options,
engine,
interactivity,
scene,
(await WebXRSessionManager.IsSessionSupportedAsync('immersive-vr')
? await scene.createDefaultXRExperienceAsync({})
: null));
}
private constructor (
@ -115,41 +131,49 @@ export class RunningEngine {
public engine: Engine,
public interactivity: Interactivity,
public scene: Scene,
public xr: WebXRDefaultExperience,
public xr: WebXRDefaultExperience | null,
) {
this.xrSessionManager = this.xr.baseExperience?.sessionManager ?? null;
this.plainCamera = new FreeCamera("camera", this.options.initialPos.clone(), this.scene);
this.plainCamera.rotation = this.options.initialRotation;
this.plainCamera.minZ = 0.1;
this.plainCamera.inertia = 0.75;
this.plainCamera.speed = 0.5;
this.plainCamera.keysUp.push(87 /* W */);
this.plainCamera.keysLeft.push(65 /* A */);
this.plainCamera.keysDown.push(83 /* S */);
this.plainCamera.keysRight.push(68 /* D */);
this.plainCamera.keysUpward.push(69 /* E */);
this.plainCamera.keysDownward.push(81 /* Q */);
this.plainCamera.attachControl(true);
if (this.xrSessionManager) {
this.camera = this.xr.baseExperience.camera;
} else {
this.camera = new FreeCamera("camera",
this.options.initialPos.clone(),
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);
scene.onKeyboardObservable.add(info => {
if (info.event.metaKey) return;
this.keysChanged.set(info.event.keyCode, info.type === KeyboardEventTypes.KEYDOWN);
});
this._setupCamera(this.plainCamera);
if (this.xr) {
this.xr.baseExperience.onStateChangedObservable.add(state => this.xrStateChanged(state));
this.xrCamera = this.xr.baseExperience.camera;
this._setupCamera(this.xrCamera);
}
this.camera = this.plainCamera;
this.gamepadInput = new FreeCameraGamepadInput();
this.gamepadInput.gamepadMoveSensibility = 320;
this.gamepadInput.gamepadAngularSensibility = 100;
this.camera.inputs.add(this.gamepadInput);
this.plainCamera.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();
if (this.xrAvailable) {
canvas.onclick = () => this.xrToggle();
} else {
canvas.onclick = () => canvas.requestPointerLock?.();
}
@ -160,6 +184,7 @@ export class RunningEngine {
Array.from(navigator.getGamepads()).forEach(gp => {
if (gp !== null) this.checkGamepadInput(gp);
});
this.checkKeys();
this.scene.render();
} catch (e) {
console.error('Error in render loop', e);
@ -170,6 +195,36 @@ export class RunningEngine {
window.addEventListener("resize", () => this.engine.resize());
}
_setupCamera(c: FreeCamera) {
c.checkCollisions = true;
c.applyGravity = false;
(c as any)._needMoveForGravity = true;
c.ellipsoid = new Vector3(0.25, this.options.initialPos.y / 2, 0.25);
}
get inXR(): boolean {
return (this.xr !== null) && (this.xr.baseExperience.state === WebXRState.IN_XR);
}
get xrAvailable(): boolean {
return this.xr !== null;
}
set onCollide(c: (m: AbstractMesh) => void) {
this.plainCamera.onCollide = c;
if (this.xrCamera) this.xrCamera.onCollide = c;
}
get position(): Vector3 {
return this.camera.position;
}
get rotation(): Vector3 {
return this.inXR
? this.camera.rotationQuaternion.toEulerAngles()
: this.camera.rotation;
}
padStateFor(gp: Gamepad): GamepadState {
const state = this.padStates.get(gp.index);
if (state) {
@ -186,58 +241,82 @@ export class RunningEngine {
checkGamepadInput(gp: Gamepad) {
const state = this.padStateFor(gp);
if (state.latch(2)) location.reload();
if (state.latch(5)) this.turn180();
if (state.latch(9 /* options */)) location.reload();
if (state.latch(3 /* triangle */)) 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 (this.xrAvailable) {
if (state.latch(16 /* ps */)) this.xrToggle();
}
if (state.latch(4)) {
if (this.inXR) {
this.xrUpdateLean(gp);
if (state.latch(0 /* cross */)) this.xrTeleport();
if (state.latch(1 /* circle */)) this.xrTouch();
if (state.latch(8 /* share */)) {
this.recenterBase = {
rotation: this.xr.baseExperience.camera.rotationQuaternion.clone(),
rotation: this.xrCamera!.rotationQuaternion.clone(),
};
}
if (state.isDown(4) && this.recenterBase) {
this.xr.baseExperience.camera.rotationQuaternion.copyFrom(
if (state.isDown(8 /* share */) && this.recenterBase) {
this.xrCamera!.rotationQuaternion.copyFrom(
this.recenterBase.rotation);
}
}
}
updateLean(gp: Gamepad) {
checkKeys() {
for (const [keyCode, state] of this.keysChanged.entries()) {
if (state) {
this.keysDown.add(keyCode);
switch (keyCode) {
case 32 /* space */: this.jump(); break;
default: break;
}
} else {
this.keysDown.delete(keyCode);
}
}
this.keysChanged.clear();
}
jump() {
if (Math.abs(this.camera.cameraDirection.y) < 0.1) {
this.camera.cameraDirection.y += 1;
}
}
xrUpdateLean(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.leanBase = { position: this.xrCamera!.position };
}
this.xr.baseExperience.camera.position =
pos.applyRotationQuaternion(this.xr.baseExperience.camera.absoluteRotation)
this.xrCamera!.position =
pos.applyRotationQuaternion(this.xrCamera!.absoluteRotation)
.scale(0.25)
.add(this.leanBase.position);
} else {
if (this.leanBase !== null) {
this.xr.baseExperience.camera.position = this.leanBase.position;
this.xrCamera!.position = this.leanBase.position;
}
this.leanBase = null;
}
}
turn180() {
const r = this.xrSessionManager
? this.xr.baseExperience.camera.rotationQuaternion.toEulerAngles()
const r = this.inXR
? this.xrCamera!.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());
if (this.inXR) {
this.xrCamera!.rotationQuaternion.copyFrom(r.toQuaternion());
}
}
xrTouch() {
const ray = this.xr.baseExperience.camera.getForwardRay();
const ray = this.xrCamera!.getForwardRay();
const meshes = this.interactivity.touchableMeshes();
const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);
@ -248,9 +327,9 @@ export class RunningEngine {
}
xrTeleport() {
if (!this.xrSessionManager) return;
if (!this.inXR) return;
const ray = this.xr.baseExperience.camera.getForwardRay();
const ray = this.xrCamera!.getForwardRay();
const meshes = this.interactivity.floorMeshes();
const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);
@ -259,17 +338,37 @@ export class RunningEngine {
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();
this.xrCamera!.position = pos;
this.xrCamera!.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.clone();
this.xr.baseExperience.camera.rotation = this.options.initialRotation;
this.xr.baseExperience.sessionManager.session.onselect = () => this.xrTeleport();
});
xrStateChanged(state: WebXRState) {
switch (state) {
case WebXRState.IN_XR:
break;
case WebXRState.ENTERING_XR:
this.camera = this.xrCamera!;
this.camera.position = this.plainCamera.position.clone();
this.camera.rotationQuaternion = this.options.initialRotation.toQuaternion();
break;
case WebXRState.EXITING_XR:
this.camera = this.plainCamera;
this.camera.position = this.xrCamera!.position.clone();
this.camera.rotation = this.xrCamera!.rotationQuaternion.toEulerAngles();
break;
case WebXRState.NOT_IN_XR:
break;
}
}
xrToggle() {
if (this.inXR) {
this.xr!.baseExperience.exitXRAsync();
} else {
this.xr!.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => {
this.xr!.baseExperience.sessionManager.session.onselect = () => this.xrTeleport();
});
}
}
}

View File

@ -70,15 +70,14 @@ async function enterScene(
const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene);
const camera = runningEngine.camera;
camera.applyGravity = false;
camera.position = initialPosition.clone();
runningEngine.camera.applyGravity = false;
runningEngine.camera.position = initialPosition.clone();
interpretScene(id, runningEngine, rootMesh, sceneDs);
let lastTouchTime = 0;
let lastTouchSpriteName = "";
camera.onCollide = (other: AbstractMesh) => {
runningEngine.onCollide = (other: AbstractMesh) => {
if (other.metadata?.touchable) {
const now = +new Date();
const touched = other.metadata?.spriteName ?? "";
@ -97,11 +96,8 @@ async function enterScene(
}
};
const currentPosition = () => Shapes.Vector3(camera.position);
const currentRotation = () => Shapes.Vector3(
camera instanceof WebXRCamera
? camera.rotationQuaternion.toEulerAngles()
: camera.rotation);
const currentPosition = () => Shapes.Vector3(runningEngine.position);
const currentRotation = () => Shapes.Vector3(runningEngine.rotation);
field position: Shapes.Vector3 = currentPosition();
field rotation: Shapes.Vector3 = currentRotation();
@ -137,7 +133,7 @@ async function enterScene(
switch (dest._variant) {
case "local":
if (dest.value === sceneDs) {
camera.position = newPos;
runningEngine.camera.position = newPos;
} else {
runningEngine.scene.removeMesh(rootMesh, true);
Turn.active.stop(currentSceneFacet, () => {