diff --git a/TODO b/TODO index 8dddc4d..5584c6a 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,4 @@ portals to other scenes -collision events -touch events/interactions +✓ collision events +✓ touch events/interactions motion with xr without a controller diff --git a/protocols/schemas/scene.prs b/protocols/schemas/scene.prs new file mode 100644 index 0000000..61993e4 --- /dev/null +++ b/protocols/schemas/scene.prs @@ -0,0 +1,4 @@ +version 1 . + +; Message +Touch = . diff --git a/protocols/schemas/shapes.prs b/protocols/schemas/shapes.prs index b66db2d..a6a8a2c 100644 --- a/protocols/schemas/shapes.prs +++ b/protocols/schemas/shapes.prs @@ -2,7 +2,7 @@ version 1 . Sprite = . -Shape = Mesh / Light / Scale / Move / Rotate / @many [Shape ...] / Texture / Color / Name / Floor / Nonphysical / CSG . +Shape = Mesh / Light / Scale / Move / Rotate / @many [Shape ...] / Texture / Color / Name / Floor / Nonphysical / Touchable / CSG . Mesh = Sphere / Box / Ground / Plane . @@ -36,8 +36,8 @@ Color = Name = . Floor = . - Nonphysical = . +Touchable = . CSG = . diff --git a/scene/example.pr b/scene/example.pr index 81a3f19..17a88e3 100644 --- a/scene/example.pr +++ b/scene/example.pr @@ -37,7 +37,8 @@ >>>>> + >>>>>> >>>>> - - >>>> + + + + >>>>>> [] diff --git a/src/engine.ts b/src/engine.ts index f22a0e6..509a5bd 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,4 +1,5 @@ import { + AbstractMesh, DualShockPad, Engine, FreeCamera, @@ -61,7 +62,8 @@ if ((navigator as any).oscpu?.startsWith('Linux')) { export type CreateScene = (canvas: HTMLCanvasElement, engine: Engine) => Promise; export type CreatedScene = { scene: Scene, - floorMeshes: () => Mesh[] + floorMeshes: () => Mesh[], + touchableMeshes: () => Mesh[], }; export async function startEngine( @@ -71,7 +73,7 @@ export async function startEngine( ): Promise { const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement; const engine = new Engine(canvas, true); - const { scene, floorMeshes } = await createScene(canvas, engine); + const { scene, floorMeshes, touchableMeshes } = await createScene(canvas, engine); const xr = await scene.createDefaultXRExperienceAsync({}); const xrAvailable = xr?.baseExperience !== void 0; @@ -171,6 +173,17 @@ export async function startEngine( } } + 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() }; diff --git a/src/index.ts b/src/index.ts index 57ba2ef..c7fbd22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,17 +4,20 @@ import * as timer from "@syndicate-lang/timer"; import * as wsRelay from "@syndicate-lang/ws-relay"; import * as wakeDetector from './wake-detector.js'; import * as Shapes from './gen/shapes.js'; +import * as SceneProtocol from './gen/scene.js'; import { md5 } from './md5.js'; import { + AbstractMesh, Engine, + FreeCamera, Mesh, Scene, TargetCamera, WebXRCamera, } from '@babylonjs/core/Legacy/legacy'; -import { activeFloorMeshes, ShapeTree, builder as B } from './shapes.js'; +import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, builder as B } from './shapes.js'; import { startEngine, CreatedScene } from './engine.js'; import { uuid } from './uuid.js'; @@ -34,12 +37,12 @@ function interpretScene(myId: string, scene: Scene, sceneDs: Ref) { } } -function spriteMain(name: string, scene: Scene, sceneDs: Ref) { +function spriteMain(spriteName: string, scene: Scene, sceneDs: Ref) { at sceneDs { - let currentShape = ShapeTree.empty(name, scene); + let currentShape = ShapeTree.empty(spriteName, scene); on stop currentShape.remove(); - during Shapes.Sprite({ "name": name, "shape": $shape: Shapes.Shape }) => { - currentShape = currentShape.reconcile(name, shape); + during Shapes.Sprite({ "name": spriteName, "shape": $shape: Shapes.Shape }) => { + currentShape = currentShape.reconcile(spriteName, spriteName, shape); } } } @@ -52,11 +55,22 @@ function wsurl(): string { function bootApp(ds: Ref, scene: Scene) { spawn named 'app' { at ds { + const id = uuid(); + const url = wsurl(); const serverCap = Sturdy.asSturdyRef(new Reader( '').next()); const relayAddr = wsRelay.RelayAddress(Schemas.transportAddress.WebSocket(url)); + field email: string = localStorage.getItem('userEmail') ?? id; + const outerFacet = Turn.activeFacet; + const emailInput = document.getElementById('emailInput') as HTMLInputElement; + emailInput.value = email.value; + emailInput.addEventListener('keyup', () => outerFacet.turn(() => { + email.value = emailInput.value; + localStorage.setItem('userEmail', emailInput.value); + })); + during wsRelay.Resolved({ "addr": relayAddr, "sturdyref": serverCap, @@ -70,19 +84,34 @@ function bootApp(ds: Ref, scene: Scene) { at remoteDs { during SceneHandle($sceneDs_e: Embedded) => { - const id = uuid(); const thisFacet = Turn.activeFacet; const sceneDs = sceneDs_e.embeddedValue; interpretScene(id, 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, + }); + } + }); + } + }; + } + field position: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 }); field rotation: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 }); at ds { const refreshPeriod = Math.floor(1000 / 10); on message timer.PeriodicTick(refreshPeriod) => { - const camera = scene.cameras[0]; - if (camera && camera instanceof TargetCamera) { + if (camera instanceof TargetCamera) { const newPosition = Shapes.Vector3(camera.position); const newRotation = Shapes.Vector3(camera instanceof WebXRCamera ? camera.rotationQuaternion.toEulerAngles() @@ -97,15 +126,11 @@ function bootApp(ds: Ref, scene: Scene) { } } - field email: string = localStorage.getItem('userEmail') ?? id; - const emailInput = document.getElementById('emailInput') as HTMLInputElement; - emailInput.value = email.value; - emailInput.addEventListener('keyup', () => thisFacet.turn(() => { - email.value = emailInput.value; - localStorage.setItem('userEmail', emailInput.value); - })); - at sceneDs { + on message SceneProtocol.Touch({ "subject": $subject, "object": $object }) => { + console.log('touch!', subject, object); + } + assert Shapes.Sprite({ name: id, shape: B.nonphysical( @@ -143,6 +168,7 @@ window.addEventListener('load', async () => { async (_canvas: HTMLCanvasElement, engine: Engine): Promise => ({ scene: new Scene(engine), floorMeshes: () => activeFloorMeshes, + touchableMeshes: () => activeTouchableMeshes, })); Dataspace.boot(ds => { html.boot(ds); diff --git a/src/shapes.ts b/src/shapes.ts index 4153046..da618a9 100644 --- a/src/shapes.ts +++ b/src/shapes.ts @@ -20,6 +20,7 @@ import { KeyedDictionary, Value, is } from "@syndicate-lang/core"; import * as Shapes from './gen/shapes.js'; export const activeFloorMeshes: Array = []; +export const activeTouchableMeshes: Array = []; export function adjust(f: (... csgs: CSG[]) => CSG, ... ms: Mesh[]): Mesh { const csgs = ms.map(m => CSG.FromMesh(m)); @@ -75,17 +76,17 @@ export class ShapeTree { public node: N, ) { this.shapePreserve = Shapes.fromShape(this.shape); + this.node.metadata = {}; } - reconcile(name: string, shape: Shapes.Shape): ShapeTree { + reconcile(spriteName: string, name: string, shape: Shapes.Shape): ShapeTree { if (is(Shapes.fromShape(shape), this.shapePreserve)) { return this; } else { this.remove(); return build(name, this.scene, shape, { - collisions: m => { - m.node.checkCollisions = true; - }, + spriteName: m => m.node.metadata.spriteName = spriteName, + collisions: m => m.node.checkCollisions = true, }); } } @@ -286,6 +287,19 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize }, }); + case "Touchable": + return build(name, scene, shape.value.shape, { + ... customize, + touchable: m => { + m.node.metadata.touchable = true; + activeTouchableMeshes.push(m.node); + m.cleanups.push(() => { + const i = activeTouchableMeshes.indexOf(m.node); + if (i !== -1) activeTouchableMeshes.splice(i, 1); + }); + } + }); + case "CSG": { throw new Error("unimplemented"); }