import { AbstractMesh, Color3, CubeTexture, CSG, HemisphericLight, ISoundOptions, Material, Matrix, Mesh, MeshBuilder, Node, Quaternion, Scene, SceneLoader, Sound, 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'; import * as SceneProtocol from './gen/scene.js'; export const activeFloorMeshes: Array = []; export const activeTouchableMeshes: Array = []; export class ShapeTree { shapePreserve: Value; cleanups: Array<() => void> = []; subnodes: Promise; subtrees: ShapeTree[] = []; constructor ( public scene: Scene, public shape: Shapes.Shape, public rootnode: N, subnodes?: Promise, ) { this.shapePreserve = Shapes.fromShape(this.shape); const metadata = {}; this.rootnode.metadata = metadata; this.subnodes = subnodes ?? Promise.resolve([]); this.subnodes.then(ns => ns.forEach(n => n.metadata = metadata)); } get allnodes(): Promise { return this.subnodes.then(ns => [this.rootnode, ... ns]); } 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: async m => m.rootnode.metadata.spriteName = spriteName, collisions: async m => (await m.allnodes).forEach(n => n.checkCollisions = true), }); } } set parent(t: ShapeTree) { t.subtrees.push(this); this.rootnode.parent = t.rootnode; } remove() { this.subtrees.forEach(t => t.remove()); this.allnodes.then(ns => ns.forEach(n => n.dispose())); this.cleanups.forEach(c => c()); } 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 type BuiltMesh = { rootnode: AbstractMesh, subnodes?: Promise, }; export function buildMesh( name: string, scene: Scene | null, meshSpec: Shapes.Mesh, ): BuiltMesh { switch (meshSpec._variant) { case "Sphere": return { rootnode: MeshBuilder.CreateSphere(name, {}, scene) }; case "Box": return { rootnode: MeshBuilder.CreateBox(name, {}, scene) }; case "Ground": { const v = v2(meshSpec.value.size); return { rootnode: MeshBuilder.CreateGround( name, { width: v.x, height: v.y }, scene ?? void 0), }; } case "Plane": return { rootnode: MeshBuilder.CreatePlane(name, {}, scene) }; case "External": { const rootnode = new Mesh(name, scene); return { rootnode, subnodes: new Promise(async resolve => { const r = await SceneLoader.ImportMeshAsync("", meshSpec.value.path, void 0, scene); r.meshes.forEach(m => m.parent = rootnode); resolve(r.meshes); }), }; } } } export function buildSound(name: string, scene: Scene, spec: SceneProtocol.SoundSpec, spatial: boolean): Sound { const options: ISoundOptions = { loop: true, autoplay: true, skipCodecCheck: true, }; if (spatial) { options.distanceModel = "inverse"; options.rolloffFactor = 0.25; } switch (spec._variant) { case "stream": options.streaming = true; break; case "loop": break; } return new Sound(name, spec.url, scene, null, options); } export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree { switch (shape._variant) { case "Mesh": { const m = buildMesh(name, scene, shape.value); const t = new ShapeTree(scene, shape, m.rootnode, m.subnodes); 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.rootnode.scaling = v3(shape.value.v); build(name + '.inner', scene, shape.value.shape, customize).parent = t; return t; } case "Move": { const t = ShapeTree.transform(name, scene, shape); t.rootnode.position = v3(shape.value.v); build(name + '.inner', scene, shape.value.shape, customize).parent = t; return t; } case "Rotate": { const t = ShapeTree.transform(name, scene, shape); switch (shape.value._variant) { case "euler": t.rootnode.rotation = v3(shape.value.v); t.rootnode.rotation.scaleInPlace(2 * Math.PI); break; case "quaternion": t.rootnode.rotationQuaternion = q(shape.value.q); break; } build(name + '.inner', scene, shape.value.shape, customize).parent = t; return t; } case "many": { const t = ShapeTree.transform(name, scene, shape); shape.value.forEach((s, i) => { build(name + '[' + i + ']', scene, s, customize).parent = t; }); return t; } case "Texture": { const entry = buildTexture(name + '.texture', scene, shape.value.spec); const t = build(name + '.inner', scene, shape.value.shape, { ... customize, material: async m => (await m.allnodes).forEach(n => n.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: async m => (await m.allnodes).forEach(n => n.material = mat), }); } case "Sound": { const sound = buildSound(name + ".sound", scene, shape.value.spec, true); const t = build(name + ".inner", scene, shape.value.shape, { ... customize, sound: async m => sound.attachToMesh(m.rootnode), }); t.cleanups.push(() => sound.dispose()); return t; } case "Name": return build(name + '.' + shape.value.base, scene, shape.value.shape, customize); case "Floor": return build(name, scene, shape.value.shape, { ... customize, floor: async m => { const nodes = await m.allnodes; activeFloorMeshes.push(... nodes); m.cleanups.push(() => { const i = activeFloorMeshes.indexOf(nodes[0]); if (i !== -1) activeFloorMeshes.splice(i, nodes.length); }); }, }); case "Nonphysical": return build(name, scene, shape.value.shape, { ... customize, collisions: async m => (await m.allnodes).forEach(n => n.checkCollisions = false), }); case "Touchable": return build(name, scene, shape.value.shape, { ... customize, touchable: async m => { const nodes = await m.allnodes; m.rootnode.metadata.touchable = true; activeTouchableMeshes.push(... nodes); m.cleanups.push(() => { const i = activeTouchableMeshes.indexOf(nodes[0]); if (i !== -1) activeTouchableMeshes.splice(i, nodes.length); }); } }); case "CSG": { const m = buildCSG(name, scene, shape.value.expr); const t = new ShapeTree(scene, shape, m.rootnode, m.subnodes); applyCustomizer(t, customize); return t; } 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.rootnode.material = mat; applyCustomizer(t, customize); return t; } default: ((_shape: never) => { console.error('Unsupported shape variant', shape); throw new Error("Unsupported shape variant"); })(shape); } } export function buildCSG(name: string, scene: Scene, expr: Shapes.CSGExpr): BuiltMesh { async function walk(expr: Shapes.CSGExpr, matrix: Matrix): Promise { switch (expr._variant) { case "mesh": { const m = buildMesh("", null, expr.shape); const nodes: Mesh[] = ([m.rootnode, ... (m.subnodes ? await m.subnodes : [])] .filter(n => n instanceof Mesh)) as Mesh[]; nodes.forEach(n => { n.freezeWorldMatrix(matrix, true); n.unfreezeWorldMatrix(); }); const c = CSG.FromMesh(nodes[0]); nodes.slice(1).forEach(n => c.unionInPlace(CSG.FromMesh(n))); nodes.forEach(n => n.dispose()); return c; } case "scale": return walk(expr.shape, matrix.multiply( Matrix.Scaling(expr.v.x, expr.v.y, expr.v.z))); case "move": return walk(expr.shape, matrix.multiply( Matrix.Translation(expr.v.x, expr.v.y, expr.v.z))); case "rotate": return walk(expr.shape, matrix.multiply( Matrix.RotationYawPitchRoll(expr.v.y * 2 * Math.PI, expr.v.x * 2 * Math.PI, expr.v.z * 2 * Math.PI))); case "subtract": { const c = await walk(expr.base, matrix); for (const d of expr.more) { c.subtractInPlace(await walk(d, matrix)); } return c; } case "union": { const c = await walk(expr.base, matrix); for (const d of expr.more) { c.unionInPlace(await walk(d, matrix)); } return c; } case "intersect": { const c = await walk(expr.base, matrix); for (const d of expr.more) { c.intersectInPlace(await walk(d, matrix)); } return c; } case "invert": { const c = await walk(expr.shape, matrix); c.inverseInPlace(); return c; } } } const rootnode = new Mesh(name, scene); return { rootnode, subnodes: new Promise(async resolve => { const c = await walk(expr, Matrix.Identity()); const m = c.toMesh(name + ".csg", null, scene); m.parent = rootnode; resolve([m]); }), }; } 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)), };