Refactor engine setup in preparation for scene changes

This commit is contained in:
Tony Garnock-Jones 2023-01-09 11:27:55 +01:00
parent 4f4c793c42
commit 4d59654eef
4 changed files with 255 additions and 184 deletions

5
TODO
View File

@ -2,3 +2,8 @@ portals to other scenes
✓ collision events
✓ touch events/interactions
✓ motion with xr without a controller
make texture scaling divide instead of multiply. multiply is more sensible for thinking about
scaling, but divide makes sense in terms of scaling the *object* the texture is applied to. a
1mx1m texture on a 10mx10m surface is easier to scale if you say "10x10", like "10 repeats x 10
repeats" rather than "0.1x0.1" i.e. "make one repeat a tenth of the size of the surface"

View File

@ -1,4 +1,4 @@
<sprite "light" <hemispheric-light <v 0.0 1.0 0.0>>>
<sprite "light" <hemispheric-light <v 0.1 1.0 0.0>>>
<sprite "ground"
<texture ["textures/grass-256x256.jpg"
@ -32,10 +32,13 @@
<move <v 2.0 -0.25 0.0> <box>>
]>>>>>
let ?ballDs = dataspace
<portal "ball" $ballDs>
<sprite "ball"
<move <v 0.0 2.0 3.0>
<scale <v 1.0 3.0 1.0>
<color 1.0 1.0 0.0
<move <v 0.0 1.5 2.0>
<scale <v 1.0 1.0 1.0>
<color 0.0 1.0 0.0
<floor
<touchable
<sphere>>>>>>>
@ -55,4 +58,9 @@
<color 0.5 0.5 0.0
<box>>>>>>>
<sprite "house"
<scale <v 10.0 4.0 15.0>
<floor
<move <v 1.5 0.5 -0.5> <box>>>>>
[]

View File

@ -9,24 +9,10 @@ import {
Quaternion,
Scene,
Vector3,
WebXRDefaultExperience,
WebXRSessionManager,
} from '@babylonjs/core/Legacy/legacy';
let buttonDown : { [button: number]: boolean } = {};
function latch(gp: Gamepad, button: number): boolean {
let result = false;
const b = gp.buttons[button];
if (b) {
if (b.pressed) {
if (!(buttonDown[button] ?? false)) result = true;
buttonDown[button] = true;
} else {
buttonDown[button] = false;
}
}
return result;
}
if ((navigator as any).oscpu?.startsWith('Linux')) {
// ^ oscpu is undefined on chrome on Android, at least...
@ -59,153 +45,219 @@ if ((navigator as any).oscpu?.startsWith('Linux')) {
};
}
export type CreateScene = (canvas: HTMLCanvasElement, engine: Engine) => Promise<CreatedScene>;
export type CreateScene = (engine: Engine) => Promise<CreatedScene>;
export type CreatedScene = {
scene: Scene,
floorMeshes: () => Mesh[],
touchableMeshes: () => Mesh[],
};
export async function startEngine(
createScene: CreateScene,
initialPos = new Vector3(0, 1.6, 0),
initialRotation = new Vector3(0, 0, 0).scaleInPlace(2 * Math.PI),
): Promise<Scene> {
const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
const engine = new Engine(canvas, true);
const { scene, floorMeshes, touchableMeshes } = await createScene(canvas, engine);
export type EngineOptions = {
initialPos: Vector3,
initialRotation: Vector3,
canvas: HTMLCanvasElement | null,
};
const xr = await scene.createDefaultXRExperienceAsync({});
const xrAvailable = xr?.baseExperience !== void 0;
export type ButtonState = { [button: number]: boolean };
let camera: FreeCamera;
if (xrAvailable) {
camera = xr.baseExperience.camera;
} else {
camera = new FreeCamera("camera", initialPos, scene);
camera.minZ = 0.1;
camera.rotation = initialRotation;
camera.inertia = 0.75;
camera.speed = 0.5;
camera.attachControl(canvas, true);
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;
}
const sm = xrAvailable ? xr.baseExperience.sessionManager : null;
const gamepadInput = new FreeCameraGamepadInput();
gamepadInput.gamepadMoveSensibility = 320;
gamepadInput.gamepadAngularSensibility = 100;
camera.inputs.add(gamepadInput);
gamepadInput.attachControl();
scene.gravity = new Vector3(0, -9.81 / 90, 0);
scene.collisionsEnabled = true;
camera.checkCollisions = true;
camera.applyGravity = true;
camera.ellipsoid = new Vector3(0.25, 0.8, 0.25);
const teleport = () => {
const ray = xr.baseExperience.camera.getForwardRay();
const meshes = floorMeshes();
const hit = scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);
if (hit !== null) {
if (meshes.indexOf(hit.pickedMesh as any) !== -1) {
if (hit.pickedPoint !== null) {
xr.baseExperience.camera.position =
hit.pickedPoint.add(new Vector3(0, 1.6, 0));
xr.baseExperience.camera.rotation = Vector3.Zero();
if (leanBase !== null) {
leanBase.position = xr.baseExperience.camera.position;
}
}
}
}
};
const enableVR = () => {
if (xrAvailable) {
xr.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => {
xr.baseExperience.camera.position = initialPos;
xr.baseExperience.camera.rotation = initialRotation;
xr.baseExperience.sessionManager.session.onselect = teleport;
});
} else {
canvas.requestPointerLock?.();
}
};
canvas.onclick = enableVR;
let leanBase: { position: Vector3 } | null = null;
let recenterBase: { rotation: Quaternion } | null = null;
engine.runRenderLoop(() => {
for (const gp of Array.from(navigator.getGamepads())) {
if (gp !== null) {
if (sm) {
const pos = new Vector3((gp.axes[0]), (-gp.axes[1]), (-gp.axes[3]));
if (pos.length() > 0.0625) {
if (leanBase === null) {
leanBase = { position: xr.baseExperience.camera.position };
}
xr.baseExperience.camera.position =
pos.applyRotationQuaternion(xr.baseExperience.camera.absoluteRotation)
.scale(0.25)
.add(leanBase.position);
} else {
if (leanBase !== null) {
xr.baseExperience.camera.position = leanBase.position;
}
leanBase = null;
}
}
if (sm && latch(gp, 0)) {
teleport();
}
if (latch(gp, 2)) {
location.reload();
}
if (sm && latch(gp, 3)) {
enableVR();
}
if (latch(gp, 5)) {
const r = sm
? xr.baseExperience.camera.rotationQuaternion.toEulerAngles()
: camera.rotation;
r.y += Math.PI;
r.y %= 2 * Math.PI;
if (sm) {
xr.baseExperience.camera.rotationQuaternion.copyFrom(r.toQuaternion());
}
}
if (sm && latch(gp, 1)) {
const ray = xr.baseExperience.camera.getForwardRay();
const meshes = touchableMeshes();
const hit = scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);
if (hit !== null) {
if (meshes.indexOf(hit.pickedMesh as any) !== -1) {
camera.onCollide?.(hit.pickedMesh!);
}
}
}
if (sm) {
if (latch(gp, 4)) {
recenterBase = { rotation: xr.baseExperience.camera.rotationQuaternion.clone() };
}
if (buttonDown[4] && recenterBase) {
xr.baseExperience.camera.rotationQuaternion.copyFrom(recenterBase.rotation);
}
}
}
}
scene.render();
});
window.addEventListener("resize", () => engine.resize());
return scene;
isDown(button: number): boolean {
return this.buttons[button] ?? false;
}
}
export class RunningEngine {
camera: FreeCamera;
xrSessionManager: WebXRSessionManager | null;
gamepadInput: FreeCameraGamepadInput;
leanBase: { position: Vector3 } | null = null;
recenterBase: { rotation: Quaternion } | null = null;
padStates: Map<Gamepad, GamepadState> = new Map();
static async start(
createScene: CreateScene,
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);
const createdScene = await createScene(engine);
const xr = await createdScene.scene.createDefaultXRExperienceAsync({});
return new RunningEngine(options, engine, createdScene, xr);
}
private constructor (
public options: EngineOptions,
public engine: Engine,
public createdScene: CreatedScene,
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.createdScene.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.createdScene.scene.gravity = new Vector3(0, -9.81 / 90, 0);
this.createdScene.scene.collisionsEnabled = true;
this.camera.checkCollisions = true;
this.camera.applyGravity = true;
this.camera.ellipsoid = new Vector3(0.25, 0.8, 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.createdScene.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.createdScene.touchableMeshes();
const hit = this.createdScene.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.createdScene.floorMeshes();
const hit = this.createdScene.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();
});
}
}

View File

@ -18,7 +18,7 @@ import {
} from '@babylonjs/core/Legacy/legacy';
import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, builder as B } from './shapes.js';
import { startEngine, CreatedScene } from './engine.js';
import { RunningEngine, CreatedScene } from './engine.js';
import { uuid } from './uuid.js';
assertion type SceneHandle(ds: Embedded<Ref>);
@ -52,7 +52,7 @@ function wsurl(): string {
return `${scheme}://${document.location.host}/ws`;
}
function bootApp(ds: Ref, scene: Scene) {
function bootApp(ds: Ref, runningEngine: RunningEngine) {
spawn named 'app' {
at ds {
const id = uuid();
@ -86,24 +86,21 @@ function bootApp(ds: Ref, scene: Scene) {
during SceneHandle($sceneDs_e: Embedded) => {
const thisFacet = Turn.activeFacet;
const sceneDs = sceneDs_e.embeddedValue;
interpretScene(id, scene, sceneDs);
interpretScene(id, runningEngine.createdScene.scene, sceneDs);
const camera = scene.cameras[0];
if (camera instanceof FreeCamera) {
camera.onCollide = (other: AbstractMesh) => {
if (other.metadata?.touchable) {
thisFacet.turn(() => {
at sceneDs {
send message SceneProtocol.Touch({
subject: id,
object: other.metadata?.spriteName,
});
}
});
}
};
}
const camera = runningEngine.camera;
camera.onCollide = (other: AbstractMesh) => {
if (other.metadata?.touchable) {
thisFacet.turn(() => {
at sceneDs {
send message SceneProtocol.Touch({
subject: id,
object: other.metadata?.spriteName,
});
}
});
}
};
field position: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 });
field rotation: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 });
@ -131,16 +128,25 @@ function bootApp(ds: Ref, scene: Scene) {
console.log('touch!', o);
react {
on stop console.log('portal check ending', o);
during SceneProtocol.Portal({
stop on asserted SceneProtocol.Portal({
"name": o,
"destination": $dest: SceneProtocol.PortalDestination
}) => {
}) => react {
console.log('portal!', dest);
switch (dest._variant) {
case "local":
at dest.value {
assert 909909909;
}
break;
default:
break;
}
}
const checkFacet = Turn.activeFacet;
Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => {
console.log('synced');
stop;
stop {}
}));
}
}
@ -178,8 +184,8 @@ function bootApp(ds: Ref, scene: Scene) {
}
window.addEventListener('load', async () => {
const scene = await startEngine(
async (_canvas: HTMLCanvasElement, engine: Engine): Promise<CreatedScene> => ({
const runningEngine = await RunningEngine.start(
async (engine: Engine): Promise<CreatedScene> => ({
scene: new Scene(engine),
floorMeshes: () => activeFloorMeshes,
touchableMeshes: () => activeTouchableMeshes,
@ -189,6 +195,6 @@ window.addEventListener('load', async () => {
timer.boot(ds);
wsRelay.boot(ds, false);
wakeDetector.boot(ds);
bootApp(ds, scene);
bootApp(ds, runningEngine);
});
});