2023-01-03 13:18:34 +00:00
|
|
|
import {
|
2023-01-06 14:09:46 +00:00
|
|
|
AbstractMesh,
|
2023-01-03 13:18:34 +00:00
|
|
|
DualShockPad,
|
|
|
|
Engine,
|
|
|
|
FreeCamera,
|
2023-01-05 09:10:50 +00:00
|
|
|
FreeCameraGamepadInput,
|
2023-01-03 13:18:34 +00:00
|
|
|
Gamepad as b_Gamepad,
|
|
|
|
Mesh,
|
2023-01-05 14:47:27 +00:00
|
|
|
Quaternion,
|
2023-01-03 13:18:34 +00:00
|
|
|
Scene,
|
|
|
|
Vector3,
|
2023-01-09 10:27:55 +00:00
|
|
|
WebXRDefaultExperience,
|
|
|
|
WebXRSessionManager,
|
2023-01-03 13:18:34 +00:00
|
|
|
} 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;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-09 13:40:05 +00:00
|
|
|
export type Interactivity = {
|
2023-01-06 14:09:46 +00:00
|
|
|
floorMeshes: () => Mesh[],
|
|
|
|
touchableMeshes: () => Mesh[],
|
2023-01-05 14:47:27 +00:00
|
|
|
};
|
2023-01-03 13:18:34 +00:00
|
|
|
|
2023-01-09 10:27:55 +00:00
|
|
|
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;
|
2023-01-03 13:18:34 +00:00
|
|
|
}
|
2023-01-09 10:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class RunningEngine {
|
2023-01-09 13:40:05 +00:00
|
|
|
camera!: FreeCamera;
|
|
|
|
xrSessionManager: WebXRSessionManager | null = null;
|
|
|
|
gamepadInput!: FreeCameraGamepadInput;
|
2023-01-09 10:27:55 +00:00
|
|
|
|
|
|
|
leanBase: { position: Vector3 } | null = null;
|
|
|
|
recenterBase: { rotation: Quaternion } | null = null;
|
|
|
|
|
|
|
|
padStates: Map<Gamepad, GamepadState> = new Map();
|
|
|
|
|
|
|
|
static async start(
|
2023-01-09 13:40:05 +00:00
|
|
|
interactivity: Interactivity,
|
2023-01-09 10:27:55 +00:00
|
|
|
options0: Partial<EngineOptions> = {},
|
|
|
|
): Promise<RunningEngine> {
|
|
|
|
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);
|
2023-01-09 13:40:05 +00:00
|
|
|
const scene = new Scene(engine);
|
|
|
|
const xr = await scene.createDefaultXRExperienceAsync({});
|
|
|
|
return new RunningEngine(options, engine, interactivity, scene, xr);
|
2023-01-09 10:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private constructor (
|
|
|
|
public options: EngineOptions,
|
|
|
|
public engine: Engine,
|
2023-01-09 13:40:05 +00:00
|
|
|
public interactivity: Interactivity,
|
|
|
|
public scene: Scene,
|
2023-01-09 10:27:55 +00:00
|
|
|
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",
|
2023-01-09 13:47:38 +00:00
|
|
|
this.options.initialPos.clone(),
|
2023-01-09 13:40:05 +00:00
|
|
|
this.scene);
|
2023-01-09 10:27:55 +00:00
|
|
|
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();
|
|
|
|
|
2023-01-09 13:40:05 +00:00
|
|
|
this.scene.gravity = new Vector3(0, -9.81 / 90, 0);
|
|
|
|
this.scene.collisionsEnabled = true;
|
2023-01-09 10:27:55 +00:00
|
|
|
|
|
|
|
this.camera.checkCollisions = true;
|
2023-01-09 13:40:05 +00:00
|
|
|
this.camera.applyGravity = false;
|
|
|
|
(this.camera as any)._needMoveForGravity = true;
|
|
|
|
this.camera.ellipsoid = new Vector3(0.25, this.options.initialPos.y / 2, 0.25);
|
2023-01-09 10:27:55 +00:00
|
|
|
|
|
|
|
if (this.options.canvas) {
|
|
|
|
const canvas = this.options.canvas;
|
|
|
|
if (this.xrSessionManager) {
|
|
|
|
canvas.onclick = () => this.xrEnable();
|
|
|
|
} else {
|
|
|
|
canvas.onclick = () => canvas.requestPointerLock?.();
|
2023-01-06 15:02:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-09 10:27:55 +00:00
|
|
|
this.engine.runRenderLoop(() => {
|
|
|
|
Array.from(navigator.getGamepads()).forEach(gp => {
|
|
|
|
if (gp !== null) this.checkGamepadInput(gp);
|
2023-01-05 10:24:52 +00:00
|
|
|
});
|
2023-01-09 13:40:05 +00:00
|
|
|
this.scene.render();
|
2023-01-09 10:27:55 +00:00
|
|
|
});
|
2023-01-09 13:40:05 +00:00
|
|
|
|
2023-01-09 10:27:55 +00:00
|
|
|
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);
|
|
|
|
}
|
2023-01-05 10:24:52 +00:00
|
|
|
}
|
2023-01-09 10:27:55 +00:00
|
|
|
}
|
2023-01-03 13:18:34 +00:00
|
|
|
|
2023-01-09 10:27:55 +00:00
|
|
|
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;
|
2023-01-03 13:18:34 +00:00
|
|
|
}
|
2023-01-09 10:27:55 +00:00
|
|
|
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());
|
2023-01-03 13:18:34 +00:00
|
|
|
}
|
2023-01-09 10:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
xrTouch() {
|
|
|
|
const ray = this.xr.baseExperience.camera.getForwardRay();
|
2023-01-09 13:40:05 +00:00
|
|
|
const meshes = this.interactivity.touchableMeshes();
|
|
|
|
const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);
|
2023-01-09 10:27:55 +00:00
|
|
|
|
|
|
|
if (hit === null) return;
|
|
|
|
if (meshes.indexOf(hit.pickedMesh as any) === -1) return;
|
2023-01-03 13:18:34 +00:00
|
|
|
|
2023-01-09 10:27:55 +00:00
|
|
|
this.camera.onCollide?.(hit.pickedMesh!);
|
|
|
|
}
|
|
|
|
|
|
|
|
xrTeleport() {
|
|
|
|
if (!this.xrSessionManager) return;
|
|
|
|
|
|
|
|
const ray = this.xr.baseExperience.camera.getForwardRay();
|
2023-01-09 13:40:05 +00:00
|
|
|
const meshes = this.interactivity.floorMeshes();
|
|
|
|
const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);
|
2023-01-09 10:27:55 +00:00
|
|
|
|
|
|
|
if (hit === null) return;
|
|
|
|
if (meshes.indexOf(hit.pickedMesh as any) === -1) return;
|
|
|
|
if (hit.pickedPoint === null) return;
|
2023-01-05 14:47:27 +00:00
|
|
|
|
2023-01-09 10:27:55 +00:00
|
|
|
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(() => {
|
2023-01-09 13:47:38 +00:00
|
|
|
this.xr.baseExperience.camera.position = this.options.initialPos.clone();
|
2023-01-09 10:27:55 +00:00
|
|
|
this.xr.baseExperience.camera.rotation = this.options.initialRotation;
|
|
|
|
this.xr.baseExperience.sessionManager.session.onselect = () => this.xrTeleport();
|
|
|
|
});
|
|
|
|
}
|
2023-01-03 13:18:34 +00:00
|
|
|
}
|