house/src/shapes.ts

414 lines
14 KiB
TypeScript

import {
Color3,
CubeTexture,
CSG,
HemisphericLight,
Material,
Matrix,
Mesh,
MeshBuilder,
Node,
PhotoDome,
Quaternion,
Scene,
SceneLoader,
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<Mesh> = [];
export const activeTouchableMeshes: Array<Mesh> = [];
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<typeof PhotoDome>) {
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<N extends Node = Node> {
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<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<Mesh>) => void) };
function applyCustomizer(m: ShapeTree<Mesh>, 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 function buildMesh(
name: string,
scene: Scene | null,
meshSpec: Shapes.Mesh,
): Mesh {
switch (meshSpec._variant) {
case "Sphere": return MeshBuilder.CreateSphere(name, {}, scene);
case "Box": return MeshBuilder.CreateBox(name, {}, scene);
case "Ground": {
const v = v2(meshSpec.value.size);
return MeshBuilder.CreateGround(name, { width: v.x, height: v.y }, scene ?? void 0);
}
case "Plane": return MeshBuilder.CreatePlane(name, {}, scene);
case "External": {
const primary = new Mesh(name, scene);
SceneLoader.ImportMesh(
"",
meshSpec.value.path,
void 0,
scene,
meshes => meshes.forEach(m => {
m.parent = primary;
console.log('adding submesh');
}));
console.log('returning primary');
return primary;
}
}
}
export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree {
switch (shape._variant) {
case "Mesh": {
const t = new ShapeTree<Mesh>(scene, shape, buildMesh(name, scene, shape.value));
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": {
const t = new ShapeTree<Mesh>(
scene, shape, buildCSG(shape.value.expr).toMesh(name, null, scene));
applyCustomizer(t, customize);
return t;
}
case "Skybox": {
const t = new ShapeTree<Mesh>(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 function buildCSG(expr: Shapes.CSGExpr): CSG {
function walk(expr: Shapes.CSGExpr, matrix: Matrix): CSG {
switch (expr._variant) {
case "mesh": {
const mesh = buildMesh("", null, expr.shape);
mesh.freezeWorldMatrix(matrix, true);
mesh.unfreezeWorldMatrix();
const c = CSG.FromMesh(mesh);
mesh.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": {
let c = walk(expr.base, matrix);
expr.more.forEach(d => c.subtractInPlace(walk(d, matrix)));
return c;
}
case "union": {
let c = walk(expr.base, matrix);
expr.more.forEach(d => c.unionInPlace(walk(d, matrix)));
return c;
}
case "intersect": {
let c = walk(expr.base, matrix);
expr.more.forEach(d => c.intersectInPlace(walk(d, matrix)));
return c;
}
case "invert": {
let c = walk(expr.shape, matrix);
c.inverseInPlace();
return c;
}
}
}
return walk(expr, Matrix.Identity());
}
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)),
};