321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
import {
|
|
Color3,
|
|
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<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);
|
|
}
|
|
|
|
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, {
|
|
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 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 = 1 / scale.x;
|
|
tex.vScale = 1 / 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<Mesh>;
|
|
switch (mesh._variant) {
|
|
case "Sphere":
|
|
t = new ShapeTree<Mesh>(scene, shape, MeshBuilder.CreateSphere(name, {}, scene));
|
|
break;
|
|
case "Box":
|
|
t = new ShapeTree<Mesh>(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<Mesh>(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);
|
|
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 "CSG": {
|
|
throw new Error("unimplemented");
|
|
}
|
|
|
|
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)),
|
|
};
|