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 ✓ collision events
✓ touch events/interactions ✓ touch events/interactions
✓ motion with xr without a controller ✓ 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" <sprite "ground"
<texture ["textures/grass-256x256.jpg" <texture ["textures/grass-256x256.jpg"
@ -32,10 +32,13 @@
<move <v 2.0 -0.25 0.0> <box>> <move <v 2.0 -0.25 0.0> <box>>
]>>>>> ]>>>>>
let ?ballDs = dataspace
<portal "ball" $ballDs>
<sprite "ball" <sprite "ball"
<move <v 0.0 2.0 3.0> <move <v 0.0 1.5 2.0>
<scale <v 1.0 3.0 1.0> <scale <v 1.0 1.0 1.0>
<color 1.0 1.0 0.0 <color 0.0 1.0 0.0
<floor <floor
<touchable <touchable
<sphere>>>>>>> <sphere>>>>>>>
@ -55,4 +58,9 @@
<color 0.5 0.5 0.0 <color 0.5 0.5 0.0
<box>>>>>>> <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, Quaternion,
Scene, Scene,
Vector3, Vector3,
WebXRDefaultExperience,
WebXRSessionManager,
} from '@babylonjs/core/Legacy/legacy'; } 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')) { if ((navigator as any).oscpu?.startsWith('Linux')) {
// ^ oscpu is undefined on chrome on Android, at least... // ^ 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 = { export type CreatedScene = {
scene: Scene, scene: Scene,
floorMeshes: () => Mesh[], floorMeshes: () => Mesh[],
touchableMeshes: () => Mesh[], touchableMeshes: () => Mesh[],
}; };
export async function startEngine( export type EngineOptions = {
createScene: CreateScene, initialPos: Vector3,
initialPos = new Vector3(0, 1.6, 0), initialRotation: Vector3,
initialRotation = new Vector3(0, 0, 0).scaleInPlace(2 * Math.PI), canvas: HTMLCanvasElement | null,
): Promise<Scene> { };
const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
const engine = new Engine(canvas, true);
const { scene, floorMeshes, touchableMeshes } = await createScene(canvas, engine);
const xr = await scene.createDefaultXRExperienceAsync({}); export type ButtonState = { [button: number]: boolean };
const xrAvailable = xr?.baseExperience !== void 0;
let camera: FreeCamera; export class GamepadState {
if (xrAvailable) { buttons: ButtonState = {};
camera = xr.baseExperience.camera;
} else { constructor (
camera = new FreeCamera("camera", initialPos, scene); public gp: Gamepad, // NB. browser's Gamepad class, not Babylon's Gamepad class.
camera.minZ = 0.1; ) {}
camera.rotation = initialRotation;
camera.inertia = 0.75; latch(button: number): boolean {
camera.speed = 0.5; let result = false;
camera.attachControl(canvas, true); 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(); isDown(button: number): boolean {
gamepadInput.gamepadMoveSensibility = 320; return this.buttons[button] ?? false;
gamepadInput.gamepadAngularSensibility = 100; }
camera.inputs.add(gamepadInput); }
gamepadInput.attachControl();
export class RunningEngine {
scene.gravity = new Vector3(0, -9.81 / 90, 0); camera: FreeCamera;
scene.collisionsEnabled = true; xrSessionManager: WebXRSessionManager | null;
gamepadInput: FreeCameraGamepadInput;
camera.checkCollisions = true;
camera.applyGravity = true; leanBase: { position: Vector3 } | null = null;
camera.ellipsoid = new Vector3(0.25, 0.8, 0.25); recenterBase: { rotation: Quaternion } | null = null;
const teleport = () => { padStates: Map<Gamepad, GamepadState> = new Map();
const ray = xr.baseExperience.camera.getForwardRay();
const meshes = floorMeshes(); static async start(
const hit = scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); createScene: CreateScene,
if (hit !== null) { options0: Partial<EngineOptions> = {},
if (meshes.indexOf(hit.pickedMesh as any) !== -1) { ): Promise<RunningEngine> {
if (hit.pickedPoint !== null) { const options = Object.assign({
xr.baseExperience.camera.position = initialPos: new Vector3(0, 1.6, 0),
hit.pickedPoint.add(new Vector3(0, 1.6, 0)); initialRotation: new Vector3(0, 0, 0).scaleInPlace(2 * Math.PI),
xr.baseExperience.camera.rotation = Vector3.Zero(); canvas: document.getElementById("renderCanvas") as HTMLCanvasElement,
if (leanBase !== null) { }, options0);
leanBase.position = xr.baseExperience.camera.position; 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 (
const enableVR = () => { public options: EngineOptions,
if (xrAvailable) { public engine: Engine,
xr.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => { public createdScene: CreatedScene,
xr.baseExperience.camera.position = initialPos; public xr: WebXRDefaultExperience,
xr.baseExperience.camera.rotation = initialRotation; ) {
xr.baseExperience.sessionManager.session.onselect = teleport; this.xrSessionManager = this.xr.baseExperience?.sessionManager ?? null;
});
} else { if (this.xrSessionManager) {
canvas.requestPointerLock?.(); this.camera = this.xr.baseExperience.camera;
} } else {
}; this.camera = new FreeCamera("camera",
this.options.initialPos,
canvas.onclick = enableVR; this.createdScene.scene);
this.camera.rotation = this.options.initialRotation;
let leanBase: { position: Vector3 } | null = null; this.camera.minZ = 0.1;
let recenterBase: { rotation: Quaternion } | null = null; this.camera.inertia = 0.75;
this.camera.speed = 0.5;
engine.runRenderLoop(() => { this.camera.attachControl(true);
}
for (const gp of Array.from(navigator.getGamepads())) {
if (gp !== null) { this.gamepadInput = new FreeCameraGamepadInput();
if (sm) { this.gamepadInput.gamepadMoveSensibility = 320;
const pos = new Vector3((gp.axes[0]), (-gp.axes[1]), (-gp.axes[3])); this.gamepadInput.gamepadAngularSensibility = 100;
if (pos.length() > 0.0625) { this.camera.inputs.add(this.gamepadInput);
if (leanBase === null) { this.gamepadInput.attachControl();
leanBase = { position: xr.baseExperience.camera.position };
} this.createdScene.scene.gravity = new Vector3(0, -9.81 / 90, 0);
xr.baseExperience.camera.position = this.createdScene.scene.collisionsEnabled = true;
pos.applyRotationQuaternion(xr.baseExperience.camera.absoluteRotation)
.scale(0.25) this.camera.checkCollisions = true;
.add(leanBase.position); this.camera.applyGravity = true;
} else { this.camera.ellipsoid = new Vector3(0.25, 0.8, 0.25);
if (leanBase !== null) {
xr.baseExperience.camera.position = leanBase.position; if (this.options.canvas) {
} const canvas = this.options.canvas;
leanBase = null; if (this.xrSessionManager) {
} canvas.onclick = () => this.xrEnable();
} } else {
canvas.onclick = () => canvas.requestPointerLock?.();
if (sm && latch(gp, 0)) { }
teleport(); }
}
if (latch(gp, 2)) { this.engine.runRenderLoop(() => {
location.reload(); Array.from(navigator.getGamepads()).forEach(gp => {
} if (gp !== null) this.checkGamepadInput(gp);
if (sm && latch(gp, 3)) { });
enableVR(); this.createdScene.scene.render();
} });
if (latch(gp, 5)) { window.addEventListener("resize", () => this.engine.resize());
const r = sm }
? xr.baseExperience.camera.rotationQuaternion.toEulerAngles()
: camera.rotation; padStateFor(gp: Gamepad): GamepadState {
r.y += Math.PI; const state = this.padStates.get(gp);
r.y %= 2 * Math.PI; if (state) return state;
if (sm) { const newState = new GamepadState(gp);
xr.baseExperience.camera.rotationQuaternion.copyFrom(r.toQuaternion()); this.padStates.set(gp, newState);
} return newState;
} }
if (sm && latch(gp, 1)) { checkGamepadInput(gp: Gamepad) {
const ray = xr.baseExperience.camera.getForwardRay(); const state = this.padStateFor(gp);
const meshes = touchableMeshes();
const hit = scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); if (state.latch(2)) location.reload();
if (hit !== null) { if (state.latch(5)) this.turn180();
if (meshes.indexOf(hit.pickedMesh as any) !== -1) {
camera.onCollide?.(hit.pickedMesh!); 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 (sm) {
if (latch(gp, 4)) { if (state.latch(4)) {
recenterBase = { rotation: xr.baseExperience.camera.rotationQuaternion.clone() }; this.recenterBase = {
} rotation: this.xr.baseExperience.camera.rotationQuaternion.clone(),
if (buttonDown[4] && recenterBase) { };
xr.baseExperience.camera.rotationQuaternion.copyFrom(recenterBase.rotation); }
} if (state.isDown(4) && this.recenterBase) {
} this.xr.baseExperience.camera.rotationQuaternion.copyFrom(
} this.recenterBase.rotation);
} }
}
scene.render(); }
});
window.addEventListener("resize", () => engine.resize()); updateLean(gp: Gamepad) {
const pos = new Vector3((gp.axes[0]), (-gp.axes[1]), (-gp.axes[3]));
return scene; 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'; } from '@babylonjs/core/Legacy/legacy';
import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, builder as B } from './shapes.js'; 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'; import { uuid } from './uuid.js';
assertion type SceneHandle(ds: Embedded<Ref>); assertion type SceneHandle(ds: Embedded<Ref>);
@ -52,7 +52,7 @@ function wsurl(): string {
return `${scheme}://${document.location.host}/ws`; return `${scheme}://${document.location.host}/ws`;
} }
function bootApp(ds: Ref, scene: Scene) { function bootApp(ds: Ref, runningEngine: RunningEngine) {
spawn named 'app' { spawn named 'app' {
at ds { at ds {
const id = uuid(); const id = uuid();
@ -86,24 +86,21 @@ function bootApp(ds: Ref, scene: Scene) {
during SceneHandle($sceneDs_e: Embedded) => { during SceneHandle($sceneDs_e: Embedded) => {
const thisFacet = Turn.activeFacet; const thisFacet = Turn.activeFacet;
const sceneDs = sceneDs_e.embeddedValue; const sceneDs = sceneDs_e.embeddedValue;
interpretScene(id, scene, sceneDs); interpretScene(id, runningEngine.createdScene.scene, sceneDs);
const camera = scene.cameras[0]; const camera = runningEngine.camera;
camera.onCollide = (other: AbstractMesh) => {
if (camera instanceof FreeCamera) { if (other.metadata?.touchable) {
camera.onCollide = (other: AbstractMesh) => { thisFacet.turn(() => {
if (other.metadata?.touchable) { at sceneDs {
thisFacet.turn(() => { send message SceneProtocol.Touch({
at sceneDs { subject: id,
send message SceneProtocol.Touch({ object: other.metadata?.spriteName,
subject: id, });
object: other.metadata?.spriteName, }
}); });
} }
}); };
}
};
}
field position: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 }); field position: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 });
field rotation: 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); console.log('touch!', o);
react { react {
on stop console.log('portal check ending', o); on stop console.log('portal check ending', o);
during SceneProtocol.Portal({ stop on asserted SceneProtocol.Portal({
"name": o, "name": o,
"destination": $dest: SceneProtocol.PortalDestination "destination": $dest: SceneProtocol.PortalDestination
}) => { }) => react {
console.log('portal!', dest); console.log('portal!', dest);
switch (dest._variant) {
case "local":
at dest.value {
assert 909909909;
}
break;
default:
break;
}
} }
const checkFacet = Turn.activeFacet; const checkFacet = Turn.activeFacet;
Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => { Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => {
console.log('synced'); console.log('synced');
stop; stop {}
})); }));
} }
} }
@ -178,8 +184,8 @@ function bootApp(ds: Ref, scene: Scene) {
} }
window.addEventListener('load', async () => { window.addEventListener('load', async () => {
const scene = await startEngine( const runningEngine = await RunningEngine.start(
async (_canvas: HTMLCanvasElement, engine: Engine): Promise<CreatedScene> => ({ async (engine: Engine): Promise<CreatedScene> => ({
scene: new Scene(engine), scene: new Scene(engine),
floorMeshes: () => activeFloorMeshes, floorMeshes: () => activeFloorMeshes,
touchableMeshes: () => activeTouchableMeshes, touchableMeshes: () => activeTouchableMeshes,
@ -189,6 +195,6 @@ window.addEventListener('load', async () => {
timer.boot(ds); timer.boot(ds);
wsRelay.boot(ds, false); wsRelay.boot(ds, false);
wakeDetector.boot(ds); wakeDetector.boot(ds);
bootApp(ds, scene); bootApp(ds, runningEngine);
}); });
}); });