diff --git a/.gitignore b/.gitignore index 557d2fc..942c9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /node_modules /src.ts/ /src/gen/ +/tsconfig.tsbuildinfo diff --git a/config/common.pr b/config/common.pr index d3e29fb..2cec067 100644 --- a/config/common.pr +++ b/config/common.pr @@ -10,6 +10,7 @@ ; Create a dataspace entity, and register it with the gatekeeper with name `"syndicate"` and an ; empty secret key: let ?ds = dataspace + ? [ diff --git a/config/scene.pr b/config/scene.pr new file mode 100644 index 0000000..91140cb --- /dev/null +++ b/config/scene.pr @@ -0,0 +1,9 @@ +let ?sceneDs = dataspace + +> + +? [ + $ds [ + + ] +] diff --git a/package.json b/package.json index 800ffd9..33631f4 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ "devDependencies": { "@preserves/core": "*", "@preserves/schema": "*", + "@rollup/plugin-node-resolve": "15.0", "@syndicate-lang/ts-plugin": "*", "@syndicate-lang/tsc": "*", - "@rollup/plugin-node-resolve": "15.0", - "rollup-plugin-sourcemaps": "0.6", "rollup": "3.8", + "rollup-plugin-sourcemaps": "0.6", "tslib": "2.4", "typescript": "4.9", "typescript-language-server": "3.0" diff --git a/protocols/schemas/shapes.prs b/protocols/schemas/shapes.prs index a17a342..0b0f1fc 100644 --- a/protocols/schemas/shapes.prs +++ b/protocols/schemas/shapes.prs @@ -1 +1,32 @@ version 1 . + +Sprite = . + +Shape = Sphere / Box / Light / Ground / Scale / Move / Rotate / @many [Shape ...] / Texture / Color / Name / Floor . + +Sphere = . +Box = . +Light = . +Ground = . + +Vector2 = . +Vector3 = . +Quaternion = . + +Scale = . +Move = . +Rotate = @euler / @quaternion . + +Texture = +/ @simple +/ @uv +. + +Color = +/ @opaque +/ @transparent +. + +Name = . + +Floor = . diff --git a/scene/README.md b/scene/README.md new file mode 100644 index 0000000..13f787e --- /dev/null +++ b/scene/README.md @@ -0,0 +1 @@ +For scene files. diff --git a/scene/example.pr b/scene/example.pr new file mode 100644 index 0000000..aa31c2d --- /dev/null +++ b/scene/example.pr @@ -0,0 +1,19 @@ +>> + + + + >>>> + + + >>>> + + + >>>> + +[] diff --git a/src/engine.ts b/src/engine.ts index 5492131..60e01f9 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,11 +1,11 @@ import { - AbstractMesh, DualShockPad, Engine, FreeCamera, FreeCameraGamepadInput, Gamepad as b_Gamepad, Mesh, + Quaternion, Scene, Vector3, } from '@babylonjs/core/Legacy/legacy'; @@ -58,16 +58,17 @@ if ((navigator as any).oscpu?.startsWith('Linux')) { }; } -export type CreateScene = (canvas: HTMLCanvasElement, engine: Engine) => Promise<{ +export type CreateScene = (canvas: HTMLCanvasElement, engine: Engine) => Promise; +export type CreatedScene = { scene: Scene, - floorMeshes: Mesh[] -}>; + floorMeshes: () => 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 { +): Promise { const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement; const engine = new Engine(canvas, true); const { scene, floorMeshes } = await createScene(canvas, engine); @@ -99,12 +100,6 @@ export async function startEngine( camera.applyGravity = true; camera.ellipsoid = new Vector3(0.25, 0.8, 0.25); - scene.getNodes().forEach(n => { - if (n instanceof AbstractMesh) { - n.checkCollisions = true; - } - }); - const enableVR = () => { if (xrAvailable) { xr.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => { @@ -117,6 +112,7 @@ export async function startEngine( document.body.onclick = enableVR; let leanBase: { position: Vector3 } | null = null; + let recenterBase: { rotation: Quaternion } | null = null; engine.runRenderLoop(() => { @@ -142,9 +138,10 @@ export async function startEngine( if (sm && latch(gp, 0)) { const ray = xr.baseExperience.camera.getForwardRay(); - const hit = scene.pickWithRay(ray, m => floorMeshes.indexOf(m as any) !== -1); + const meshes = floorMeshes(); + const hit = scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); if (hit !== null) { - if (floorMeshes.indexOf(hit.pickedMesh as any) !== -1) { + 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)); @@ -172,8 +169,14 @@ export async function startEngine( xr.baseExperience.camera.rotationQuaternion.copyFrom(r.toQuaternion()); } } - if (sm && latch(gp, 4)) { - xr.baseExperience.camera.rotationQuaternion.copyFrom(initialRotation.toQuaternion()); + + if (sm) { + if (latch(gp, 4)) { + recenterBase = { rotation: xr.baseExperience.camera.rotationQuaternion.clone() }; + } + if (buttonDown[4] && recenterBase) { + xr.baseExperience.camera.rotationQuaternion.copyFrom(recenterBase.rotation); + } } } } @@ -181,4 +184,6 @@ export async function startEngine( scene.render(); }); window.addEventListener("resize", () => engine.resize()); + + return scene; } diff --git a/src/index.ts b/src/index.ts index 756fe5d..aa21fe2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,38 +2,38 @@ import { Dataspace, Embedded, Reader, Ref, Schemas, Sturdy } from "@syndicate-la import * as html from "@syndicate-lang/html"; import * as wsRelay from "@syndicate-lang/ws-relay"; import * as wakeDetector from './wake-detector.js'; +import * as Shapes from './gen/shapes.js'; import { Engine, - HemisphericLight, Mesh, - MeshBuilder, Scene, - Vector3, } from '@babylonjs/core/Legacy/legacy'; -import { box } from './shapes.js'; -import { attachTexture } from './interiors.js'; -import { startEngine } from './engine.js'; -import { uuid } from './uuid.js'; +import { activeFloorMeshes, ShapeTree } from './shapes.js'; +import { startEngine, CreatedScene } from './engine.js'; -async function createScene(_canvas: HTMLCanvasElement, engine: Engine): Promise<{ - scene: Scene, - floorMeshes: Mesh[] -}> { - const scene = new Scene(engine); +assertion type SceneHandle(ds: Embedded); - new HemisphericLight("light", new Vector3(0, 1, 0), scene); +function interpretScene(scene: Scene, sceneDs: Ref) { + at sceneDs { + during Shapes.Sprite({ "name": $name: string }) => spawn named `sprite:${name}` { + console.log('+shape', name); + on stop console.log('-shape', name); + spriteMain(name, scene, sceneDs); + } + } +} - const b = attachTexture(box('b', 1, 1, 1, 0, 2, 2), scene, 'papenweg-textures/individual/floor1.jpg'); - - return { - scene, - floorMeshes: [ - b, - MeshBuilder.CreateGround("ground", {width:30, height:30}), - ], - }; +function spriteMain(name: string, scene: Scene, sceneDs: Ref) { + at sceneDs { + let currentShape = ShapeTree.empty(name, scene); + on stop currentShape.remove(); + during Shapes.Sprite({ "name": name, "shape": $shape: Shapes.Shape }) => { + console.log('=shape', name, shape); + currentShape = currentShape.reconcile(name, shape); + } + } } function wsurl(): string { @@ -41,13 +41,12 @@ function wsurl(): string { return `${scheme}://${document.location.host}/ws`; } -function bootApp(ds: Ref) { +function bootApp(ds: Ref, scene: Scene) { spawn named 'app' { at ds { const url = wsurl(); const serverCap = Sturdy.asSturdyRef(new Reader( '').next()); - const this_instance = uuid(); const relayAddr = wsRelay.RelayAddress(Schemas.transportAddress.WebSocket(url)); during wsRelay.Resolved({ @@ -60,17 +59,28 @@ function bootApp(ds: Ref) { on message wakeDetector.WakeEvent() => { send message wsRelay.ForceRelayDisconnect(relayAddr); } + + at remoteDs { + during SceneHandle($sceneDs_e: Embedded) => { + const sceneDs = sceneDs_e.embeddedValue; + interpretScene(scene, sceneDs); + } + } } } } } window.addEventListener('load', async () => { - await startEngine(createScene); + const scene = await startEngine( + async (_canvas: HTMLCanvasElement, engine: Engine): Promise => ({ + scene: new Scene(engine), + floorMeshes: () => activeFloorMeshes, + })); Dataspace.boot(ds => { html.boot(ds); wsRelay.boot(ds, true); wakeDetector.boot(ds); - bootApp(ds); + bootApp(ds, scene); }); }); diff --git a/src/shapes.ts b/src/shapes.ts index ed03b97..06c0521 100644 --- a/src/shapes.ts +++ b/src/shapes.ts @@ -1,10 +1,25 @@ import { + AbstractMesh, + Color3, CSG, + HemisphericLight, Mesh, MeshBuilder, + Node, PhotoDome, + Quaternion, + Scene, + StandardMaterial, + Texture, + TransformNode, + Vector2, Vector3, } from '@babylonjs/core/Legacy/legacy'; +import { Value, is } from "@syndicate-lang/core"; + +import * as Shapes from './gen/shapes.js'; + +export const activeFloorMeshes: Array = []; export function adjust(f: (... csgs: CSG[]) => CSG, ... ms: Mesh[]): Mesh { const csgs = ms.map(m => CSG.FromMesh(m)); @@ -47,3 +62,182 @@ export function box(name: string, width: number, height: number, depth: number, b.position = new Vector3(x ?? 0, y ?? b.scaling.y/2, z ?? 0); return b; } + +//--------------------------------------------------------------------------- + +export class ShapeTree { + shapePreserve: Value; + cleanups: Array<() => void> = []; + + constructor ( + public scene: Scene, + public shape: Shapes.Shape, + public node: N, + ) { + this.shapePreserve = Shapes.fromShape(this.shape); + if (this.node instanceof AbstractMesh) { + this.node.checkCollisions = true; + } + } + + reconcile(name: string, shape: Shapes.Shape): ShapeTree { + if (is(Shapes.fromShape(shape), this.shapePreserve)) { + return this; + } else { + this.remove(); + return build(name, this.scene, shape, {}); + } + } + + remove() { + this.node?.dispose(false, true); + } + + static empty(name: string, scene: Scene): ShapeTree { + return ShapeTree.transform(name, scene, Shapes.Shape.many([])); + } + + static transform(name: string, scene: Scene, shape: Shapes.Shape): ShapeTree { + return new ShapeTree(scene, shape, new TransformNode(name, scene)); + } +} + +export function v2(v: Shapes.Vector2): Vector2 { + return new Vector2(v.x, v.y); +} + +export function v3(v: Shapes.Vector3): Vector3 { + return new Vector3(v.x, v.y, v.z); +} + +export function q(q: Shapes.Quaternion): Quaternion { + return new Quaternion(q.a, q.b, q.c, q.d); +} + +export type MeshCustomizer = { [key: string]: ((m: ShapeTree) => void) }; + +function applyCustomizer(m: ShapeTree, c: MeshCustomizer) { + Object.values(c).forEach(f => f(m)); +} + +export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree { + switch (shape._variant) { + case "Sphere": { + const t = new ShapeTree(scene, shape, MeshBuilder.CreateSphere(name, {}, scene)); + applyCustomizer(t, customize); + return t; + } + + case "Box": { + const t = new ShapeTree(scene, shape, MeshBuilder.CreateBox(name, {}, scene)); + applyCustomizer(t, customize); + return t; + } + + case "Light": + return new ShapeTree( + scene, + shape, + new HemisphericLight(name, v3(shape.value.v), scene)); + + case "Ground": { + const v = v2(shape.value.size); + const t = new ShapeTree( + scene, + shape, + MeshBuilder.CreateGround(name, { width: v.x, height: v.y }, scene)); + applyCustomizer(t, customize); + return t; + } + + case "Scale": { + const t = ShapeTree.transform(name, scene, shape); + t.node.scaling = v3(shape.value.v); + build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node; + return t; + } + + case "Move": { + const t = ShapeTree.transform(name, scene, shape); + t.node.position = v3(shape.value.v); + build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node; + return t; + } + + case "Rotate": { + const t = ShapeTree.transform(name, scene, shape); + switch (shape.value._variant) { + case "euler": + t.node.rotation = v3(shape.value.v); + break; + case "quaternion": + t.node.rotationQuaternion = q(shape.value.q); + break; + } + build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node; + return t; + } + + case "many": { + const t = ShapeTree.transform(name, scene, shape); + shape.value.forEach((s, i) => { + build(name + '[' + i + ']', scene, s, customize).node.parent = t.node; + }); + return t; + } + + case "Texture": { + const mat = new StandardMaterial(name + '.texture', scene); + const tex = new Texture(shape.value.path, scene); + mat.diffuseTexture = tex; + switch (shape.value._variant) { + case "simple": + break; + case "uv": { + const scale = v2(shape.value.scale); + const offset = v2(shape.value.offset); + tex.uScale = 1 / scale.x; + tex.vScale = 1 / scale.y; + tex.uOffset = offset.x; + tex.vOffset = offset.y; + break; + } + } + return build(name + '.inner', scene, shape.value.shape, { + ... customize, + material: m => m.node.material = mat, + }); + } + + case "Color": { + const mat = new StandardMaterial(name + '.texture', scene); + mat.diffuseColor = new Color3(shape.value.r, shape.value.g, shape.value.b); + if (shape.value._variant === "transparent") mat.alpha = shape.value.alpha; + return build(name + '.inner', scene, shape.value.shape, { + ... customize, + material: m => m.node.material = mat, + }); + } + + case "Name": + return build(name + '.' + shape.value.base, scene, shape.value.shape, customize); + + case "Floor": + return build(name, scene, shape.value.shape, { + ... customize, + floor: m => { + activeFloorMeshes.push(m.node); + m.cleanups.push(() => { + const i = activeFloorMeshes.indexOf(m.node); + if (i !== -1) activeFloorMeshes.splice(i, 1); + }); + }, + }); + + default: + ((_shape: never) => { + console.error('Unsupported shape variant', shape); + throw new Error("Unsupported shape variant"); + })(shape); + } +} diff --git a/textures/grass-256x256.jpg b/textures/grass-256x256.jpg new file mode 100644 index 0000000..f970888 Binary files /dev/null and b/textures/grass-256x256.jpg differ diff --git a/tsconfig.json b/tsconfig.json index 07bbdef..8232974 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "module": "es6", "sourceMap": true, "strict": true, + "incremental": true, "plugins": [ { "name": "@syndicate-lang/ts-plugin" } ] diff --git a/yarn.lock b/yarn.lock index b52b8eb..e203d91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,21 +3,21 @@ "@babylonjs/core@5": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@babylonjs/core/-/core-5.39.0.tgz#8343fdc2ef105005bc564fc98bd36008e4301d08" - integrity sha512-bGDsxFbIq9GeiwpojdnQB58s1EvDxJSzUK5C8hL3Wj8UqK6tCELNTEHonmJ8KruilLGQRpm2mrrPZWEzUvAKkA== + version "5.41.0" + resolved "https://registry.yarnpkg.com/@babylonjs/core/-/core-5.41.0.tgz#ebc98f9d338d5dcbb4e81fcd3b6d515419532bca" + integrity sha512-PrY12n9IOql+9P/bFhEI7WTUqneTI0W9+ROKkwallqtTYku3XV7O5E7BXpdLJwrB/VufKApu6ErNoUb9Zhj9Cg== -"@preserves/core@*", "@preserves/core@>=0.20.2", "@preserves/core@^0.20.4": - version "0.20.4" - resolved "https://registry.yarnpkg.com/@preserves/core/-/core-0.20.4.tgz#b964d31c291a489c2682b5cf35f29b6677885000" - integrity sha512-4XSQwcbn66LQWl8ympnhumWboZiMGW3bIQmvSx4Bt04v3m1vtiJz/Pw/mltTgPahWWs/f6ADAZoNywD4w6aY0Q== +"@preserves/core@*", "@preserves/core@>=0.20.2", "@preserves/core@^0.20.5": + version "0.20.5" + resolved "https://registry.yarnpkg.com/@preserves/core/-/core-0.20.5.tgz#3b34693b1f5aff659639690725a703a95689f822" + integrity sha512-hnywtmY30swKSuHix3MbEoheyCh6plCERzmhnCQllrkhdT0hFm66iSr2PVjNsXAUNIwd3Rh3Vu2btgdjMK+Q9Q== "@preserves/schema@*", "@preserves/schema@>=0.21.2": - version "0.21.6" - resolved "https://registry.yarnpkg.com/@preserves/schema/-/schema-0.21.6.tgz#572a2731712a6e503ad218639ef58099a61f2956" - integrity sha512-0lxqEN5qJDu/Ez84gpZxD3/XHF8LzrBcx2WMfRIqOGl4L4067l4N+xM01YNRTHjiY53weT3xyoWAizlqow5fBg== + version "0.21.7" + resolved "https://registry.yarnpkg.com/@preserves/schema/-/schema-0.21.7.tgz#09e388b8c582c3dc95018ebdfe990381dddabfa8" + integrity sha512-q2gncEOOY3qqs+i+Op5yZx89cckqN601vbEcBDfnQge5PB9HrOrfMk68Wq4w/rpdQpRyVqWhZNxLlKsANjgoOw== dependencies: - "@preserves/core" "^0.20.4" + "@preserves/core" "^0.20.5" "@types/glob" "^7.1" "@types/minimatch" "^3.0" chalk "^4.1"