house/src/shapes.ts

448 lines
16 KiB
TypeScript

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<AbstractMesh> = [];
export const activeTouchableMeshes: Array<AbstractMesh> = [];
export class ShapeTree<N extends Node = Node> {
shapePreserve: Value;
cleanups: Array<() => void> = [];
subnodes: Promise<N[]>;
subtrees: ShapeTree[] = [];
constructor (
public scene: Scene,
public shape: Shapes.Shape,
public rootnode: N,
subnodes?: Promise<N[]>,
) {
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<N[]> {
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<TransformNode> {
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<AbstractMesh>) => void) };
function applyCustomizer(m: ShapeTree<AbstractMesh>, c: MeshCustomizer) {
Object.values(c).forEach(f => f(m));
}
type CachedTexture = {
key: Value,
referenceCount: number,
material: Material,
};
const textureCache = new KeyedDictionary<Value, CachedTexture>();
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<AbstractMesh[]>,
};
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<AbstractMesh>(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<AbstractMesh>(scene, shape, m.rootnode, m.subnodes);
applyCustomizer(t, customize);
return t;
}
case "Skybox": {
const t = new ShapeTree<AbstractMesh>(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<CSG> {
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)),
};