import { Color3, CubeTexture, CSG, HemisphericLight, Material, Mesh, MeshBuilder, Node, PhotoDome, Quaternion, Scene, StandardMaterial, Texture, TransformNode, Vector2, Vector3, } from '@babylonjs/core/Legacy/legacy'; 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)); const c = f(... csgs); if (ms.length > 0) { ms.forEach(m => m.dispose()); const scene = ms[0].getScene(); const ans = c.toMesh(ms[0].name, null, scene, true); ans.material = ms[0].material; return ans; } else { return c.toMesh("adjusted"); } } export const subtractMany = (a: CSG, ... bs: CSG[]) => bs.reduce((a, b) => a.subtract(b), a); export class PhotoSemiDome extends PhotoDome { readonly _size: number; constructor(... args: ConstructorParameters) { super(... args); this._size = args[2].size!; this._chopMesh(); } _chopMesh() { const d = this._size; this._mesh.getChildMeshes()[0].dispose(); // remove the covering half-sphere this._mesh = adjust(subtractMany, this._mesh, box(this._mesh.name + "_chop", d, d, d, 0, 0, -d/2)); this._mesh.parent = this; } } export function box(name: string, width: number, height: number, depth: number, x?: number, y?: number, z?: number): Mesh { const b = MeshBuilder.CreateBox(name, {}); b.scaling = new Vector3(width, height, depth); 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); this.node.metadata = {}; } 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, { spriteName: m => m.node.metadata.spriteName = spriteName, collisions: m => m.node.checkCollisions = true, }); } } remove() { this.node?.dispose(); } 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 scale3(v: Shapes.Vector3, scale: number): Shapes.Vector3 { return Shapes.Vector3({ x: v.x * scale, y: v.y * scale, z: v.z * scale }); } 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)); } type CachedTexture = { key: Value, referenceCount: number, material: Material, }; const textureCache = new KeyedDictionary(); function buildTexture(name: string, scene: Scene, spec: Shapes.TextureSpec): CachedTexture { const cacheKey = Shapes.fromTextureSpec(spec); const entry = textureCache.get(cacheKey); if (entry !== void 0) { entry.referenceCount++; return entry; } const mat = new StandardMaterial(name, scene); const tex = new Texture(spec.path, scene); mat.diffuseTexture = tex; switch (spec._variant) { case "simple": break; case "uvAlpha": mat.alpha = spec.alpha; tex.hasAlpha = true; /* FALL THROUGH */ case "uv": { const scale = v2(spec.scale); const offset = v2(spec.offset); tex.uScale = scale.x; tex.vScale = scale.y; tex.uOffset = offset.x; tex.vOffset = offset.y; break; } } const newEntry = { key: cacheKey, referenceCount: 1, material: mat, }; textureCache.set(cacheKey, newEntry); return newEntry; } function releaseTexture(entry: CachedTexture) { if (--entry.referenceCount === 0) { textureCache.delete(entry.key); entry.material.dispose(); } } export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree { switch (shape._variant) { case "Mesh": { const mesh = shape.value; let t: ShapeTree; switch (mesh._variant) { case "Sphere": t = new ShapeTree(scene, shape, MeshBuilder.CreateSphere(name, {}, scene)); break; case "Box": t = new ShapeTree(scene, shape, MeshBuilder.CreateBox(name, {}, scene)); break; case "Ground": { const v = v2(mesh.value.size); t = new ShapeTree( scene, shape, MeshBuilder.CreateGround(name, { width: v.x, height: v.y }, scene)); break; } case "Plane": t = new ShapeTree(scene, shape, MeshBuilder.CreatePlane(name, {}, scene)); break; } applyCustomizer(t, customize); return t; } case "Light": return new ShapeTree( scene, shape, new HemisphericLight(name, v3(shape.value.v), scene)); 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); t.node.rotation.scaleInPlace(2 * Math.PI); 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 entry = buildTexture(name + '.texture', scene, shape.value.spec); const t = build(name + '.inner', scene, shape.value.shape, { ... customize, material: m => m.node.material = entry.material, }); t.cleanups.push(() => releaseTexture(entry)); return t; } 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); }); }, }); case "Nonphysical": return build(name, scene, shape.value.shape, { ... customize, collisions: m => { m.node.checkCollisions = false; }, }); 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"); } case "Skybox": { const t = new ShapeTree(scene, shape, MeshBuilder.CreateBox(name, { size: 2000 }, scene)); const mat = new StandardMaterial(name, scene); mat.backFaceCulling = false; mat.reflectionTexture = new CubeTexture(shape.value.path, scene); mat.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE; mat.diffuseColor = new Color3(0, 0, 0); mat.specularColor = new Color3(0, 0, 0); t.node.material = mat; applyCustomizer(t, customize); return t; } default: ((_shape: never) => { console.error('Unsupported shape variant', shape); throw new Error("Unsupported shape variant"); })(shape); } } export const builder: { [key: string]: (... args: any[]) => Shapes.Shape } = { sphere: () => Shapes.Shape.Mesh(Shapes.Mesh.Sphere(Shapes.Sphere())), box: () => Shapes.Shape.Mesh(Shapes.Mesh.Box(Shapes.Box())), plane: () => Shapes.Shape.Mesh(Shapes.Mesh.Plane(Shapes.Plane())), ground: (v: Shapes.Vector2) => Shapes.Shape.Mesh(Shapes.Mesh.Ground(Shapes.Ground(Shapes.Vector2(v)))), light: (v: Shapes.Vector3) => Shapes.Shape.Light(Shapes.Light(Shapes.Vector3(v))), scale: (v: Shapes.Vector3, shape: Shapes.Shape) => Shapes.Shape.Scale(Shapes.Scale({ v: Shapes.Vector3(v), shape })), move: (v: Shapes.Vector3, shape: Shapes.Shape) => Shapes.Shape.Move(Shapes.Move({ v: Shapes.Vector3(v), shape })), rotate: (v: Shapes.Vector3, shape: Shapes.Shape) => Shapes.Shape.Rotate(Shapes.Rotate.euler({ v: Shapes.Vector3(v), shape })), many: (shapes: Shapes.Shape[]) => Shapes.Shape.many(shapes), texture: (spec: Shapes.TextureSpec, shape: Shapes.Shape) => Shapes.Shape.Texture(Shapes.Texture({ spec, shape })), color: (r: number, g: number, b: number, shape: Shapes.Shape, alpha = 1.0) => { return Shapes.Shape.Color((alpha === 1.0) ? Shapes.Color.opaque({ r, g, b, shape }) : Shapes.Color.transparent({ r, g, b, alpha, shape })); }, name: (base: string, shape: Shapes.Shape) => Shapes.Shape.Name(Shapes.Name({ base, shape })), floor: (shape: Shapes.Shape) => Shapes.Shape.Floor(Shapes.Floor(shape)), nonphysical: (shape: Shapes.Shape) => Shapes.Shape.Nonphysical(Shapes.Nonphysical(shape)), };