Refactor engine setup in preparation for scene changes
This commit is contained in:
parent
4f4c793c42
commit
4d59654eef
5
TODO
5
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"
|
||||
|
|
|
@ -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>>>>>
|
||||
|
||||
[]
|
||||
|
|
362
src/engine.ts
362
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<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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
56
src/index.ts
56
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<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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue