From 4d59654eef155a14cd829eab5d78f2b387bb54a3 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Mon, 9 Jan 2023 11:27:55 +0100 Subject: [PATCH] Refactor engine setup in preparation for scene changes --- TODO | 5 + scene/example.pr | 16 ++- src/engine.ts | 362 +++++++++++++++++++++++++++-------------------- src/index.ts | 56 ++++---- 4 files changed, 255 insertions(+), 184 deletions(-) diff --git a/TODO b/TODO index 2cba119..ab6343f 100644 --- a/TODO +++ b/TODO @@ -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" diff --git a/scene/example.pr b/scene/example.pr index 17a88e3..0a1130f 100644 --- a/scene/example.pr +++ b/scene/example.pr @@ -1,4 +1,4 @@ ->> +>> > ]>>>>> +let ?ballDs = dataspace + + - - + + >>>>>> @@ -55,4 +58,9 @@ >>>>>> + + >>>> + [] diff --git a/src/engine.ts b/src/engine.ts index e929550..3b9a372 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -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; +export type CreateScene = (engine: Engine) => Promise; 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 { - 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 = new Map(); + + static async start( + createScene: CreateScene, + 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 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(); + }); + } } diff --git a/src/index.ts b/src/index.ts index e8cd9fa..32893e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); @@ -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 => ({ + const runningEngine = await RunningEngine.start( + async (engine: Engine): Promise => ({ 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); }); });