That's better

This commit is contained in:
Tony Garnock-Jones 2023-01-12 15:22:03 +01:00
parent df1f42ea81
commit c319bb460a
4 changed files with 111 additions and 218 deletions

View File

@ -51,8 +51,8 @@ if ((navigator as any).oscpu?.startsWith('Linux')) {
} }
export type Interactivity = { export type Interactivity = {
floorMeshes: () => Mesh[], floorMeshes: () => AbstractMesh[],
touchableMeshes: () => Mesh[], touchableMeshes: () => AbstractMesh[],
}; };
export type EngineOptions = { export type EngineOptions = {

View File

@ -38,11 +38,11 @@ function interpretScene(myId: string, runningEngine: RunningEngine, rootMesh: Me
function spriteMain(spriteName: string, runningEngine: RunningEngine, rootMesh: Mesh, sceneDs: Ref) { function spriteMain(spriteName: string, runningEngine: RunningEngine, rootMesh: Mesh, sceneDs: Ref) {
at sceneDs { at sceneDs {
let currentShape = ShapeTree.empty(spriteName, runningEngine.scene); let currentShape = ShapeTree.empty(spriteName, runningEngine.scene);
currentShape.node.parent = rootMesh; currentShape.rootnode.parent = rootMesh;
on stop currentShape.remove(); on stop currentShape.remove();
during Shapes.Sprite({ "name": spriteName, "shape": $shape: Shapes.Shape }) => { during Shapes.Sprite({ "name": spriteName, "shape": $shape: Shapes.Shape }) => {
currentShape = currentShape.reconcile(spriteName, spriteName, shape); currentShape = currentShape.reconcile(spriteName, spriteName, shape);
currentShape.node.parent = rootMesh; currentShape.rootnode.parent = rootMesh;
} }
during SceneProtocol.Gravity($direction: Shapes.Vector3) => { during SceneProtocol.Gravity($direction: Shapes.Vector3) => {

View File

@ -1,100 +0,0 @@
import {
Mesh,
Nullable,
Scene,
StandardMaterial,
Texture,
} from '@babylonjs/core/Legacy/legacy';
import { adjust, box, subtractMany } from './shapes.js';
export interface SurfaceOptions {
height?: number,
thickness?: number,
texturePath?: string,
};
export function attachTexture(b: Mesh, scene: Scene, texturePath: string | undefined): Mesh {
if (texturePath !== void 0) {
const mat = new StandardMaterial(b.name + "_mat", scene);
mat.diffuseTexture = new Texture(texturePath, scene);
b.material = mat;
}
return b;
}
export const DEFAULT_THICKNESS = 0.125;
export async function wall(name: string, length: number, scene: Scene, options: SurfaceOptions = {}): Promise<Mesh> {
return attachTexture(box(name, length, options.height ?? 2.90, options.thickness ?? DEFAULT_THICKNESS),
scene,
options.texturePath);
}
export async function floor(name: string, width: number, depth: number, scene: Scene, options: SurfaceOptions = {}): Promise<Mesh> {
return attachTexture(box(name, width, options.thickness ?? DEFAULT_THICKNESS, depth),
scene,
options.texturePath);
}
export type Wall = {
texture: string,
thickness?: number,
holes: Array<Mesh>,
};
export async function room(name: string, width: number, depth: number, scene: Scene,
floorTexture: string,
ceilingTexture: string,
wall0: Nullable<Wall>,
wall1: Nullable<Wall>,
wall2: Nullable<Wall>,
wall3: Nullable<Wall>): Promise<Mesh> {
const th = (wallDetails: Wall): number => wallDetails.thickness ?? DEFAULT_THICKNESS;
const mkw = async (n: number, width: number, wallDetails: Wall): Promise<Mesh> => {
return adjust(subtractMany,
await wall(`${name}_${n}`, width, scene, {
texturePath: wallDetails.texture,
thickness: wallDetails.thickness,
}),
... wallDetails.holes);
};
const f = await floor(name +"_f", width, depth, scene, { texturePath: floorTexture });
f.position.y = -DEFAULT_THICKNESS / 2;
const c = await floor(name + "_c", width, depth, scene, { texturePath: ceilingTexture });
c.position.y = 2.90 + DEFAULT_THICKNESS / 2;
c.setParent(f);
if (wall0 !== null) {
const w0 = await mkw(0, width, wall0);
w0.position.z = f.scaling.z / 2 + th(wall0) / 2;
w0.setParent(f);
}
if (wall1 !== null) {
const w1 = await mkw(1, depth, wall1);
w1.position.x = f.scaling.x / 2 + th(wall1) / 2;
w1.rotation.y = Math.PI / 2;
w1.setParent(f);
}
if (wall2 !== null) {
const w2 = await mkw(2, width, wall2);
w2.position.z = -(f.scaling.z / 2 + th(wall2) / 2);
w2.rotation.y = Math.PI;
w2.setParent(f);
}
if (wall3 !== null) {
const w3 = await mkw(3, depth, wall3);
w3.position.x = -(f.scaling.x / 2 + th(wall3) / 2);
w3.rotation.y = -Math.PI / 2;
w3.setParent(f);
}
f.position.y = DEFAULT_THICKNESS / 2;
return f;
}

View File

@ -1,4 +1,5 @@
import { import {
AbstractMesh,
Color3, Color3,
CubeTexture, CubeTexture,
CSG, CSG,
@ -8,7 +9,6 @@ import {
Mesh, Mesh,
MeshBuilder, MeshBuilder,
Node, Node,
PhotoDome,
Quaternion, Quaternion,
Scene, Scene,
SceneLoader, SceneLoader,
@ -22,64 +22,29 @@ import { KeyedDictionary, Value, is } from "@syndicate-lang/core";
import * as Shapes from './gen/shapes.js'; import * as Shapes from './gen/shapes.js';
export const activeFloorMeshes: Array<Mesh> = []; export const activeFloorMeshes: Array<AbstractMesh> = [];
export const activeTouchableMeshes: Array<Mesh> = []; export const activeTouchableMeshes: Array<AbstractMesh> = [];
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> { export class ShapeTree<N extends Node = Node> {
shapePreserve: Value; shapePreserve: Value;
cleanups: Array<() => void> = []; cleanups: Array<() => void> = [];
subnodes: Promise<N[]>;
constructor ( constructor (
public scene: Scene, public scene: Scene,
public shape: Shapes.Shape, public shape: Shapes.Shape,
public node: N, public rootnode: N,
subnodes?: Promise<N[]>,
) { ) {
this.shapePreserve = Shapes.fromShape(this.shape); this.shapePreserve = Shapes.fromShape(this.shape);
this.node.metadata = {}; 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 { reconcile(spriteName: string, name: string, shape: Shapes.Shape): ShapeTree {
@ -88,14 +53,14 @@ export class ShapeTree<N extends Node = Node> {
} else { } else {
this.remove(); this.remove();
return build(name, this.scene, shape, { return build(name, this.scene, shape, {
spriteName: m => m.node.metadata.spriteName = spriteName, spriteName: async m => m.rootnode.metadata.spriteName = spriteName,
collisions: m => m.node.checkCollisions = true, collisions: async m => (await m.allnodes).forEach(n => n.checkCollisions = true),
}); });
} }
} }
remove() { remove() {
this.node?.dispose(); this.allnodes.then(ns => ns.forEach(n => n.dispose()));
} }
static empty(name: string, scene: Scene): ShapeTree { static empty(name: string, scene: Scene): ShapeTree {
@ -123,9 +88,9 @@ export function q(q: Shapes.Quaternion): Quaternion {
return new Quaternion(q.a, q.b, q.c, q.d); return new Quaternion(q.a, q.b, q.c, q.d);
} }
export type MeshCustomizer = { [key: string]: ((m: ShapeTree<Mesh>) => void) }; export type MeshCustomizer = { [key: string]: ((m: ShapeTree<AbstractMesh>) => void) };
function applyCustomizer(m: ShapeTree<Mesh>, c: MeshCustomizer) { function applyCustomizer(m: ShapeTree<AbstractMesh>, c: MeshCustomizer) {
Object.values(c).forEach(f => f(m)); Object.values(c).forEach(f => f(m));
} }
@ -179,32 +144,38 @@ function releaseTexture(entry: CachedTexture) {
} }
} }
export type BuiltMesh = {
rootnode: AbstractMesh,
subnodes?: Promise<AbstractMesh[]>,
};
export function buildMesh( export function buildMesh(
name: string, name: string,
scene: Scene | null, scene: Scene | null,
meshSpec: Shapes.Mesh, meshSpec: Shapes.Mesh,
): Mesh { ): BuiltMesh {
switch (meshSpec._variant) { switch (meshSpec._variant) {
case "Sphere": return MeshBuilder.CreateSphere(name, {}, scene); case "Sphere": return { rootnode: MeshBuilder.CreateSphere(name, {}, scene) };
case "Box": return MeshBuilder.CreateBox(name, {}, scene); case "Box": return { rootnode: MeshBuilder.CreateBox(name, {}, scene) };
case "Ground": { case "Ground": {
const v = v2(meshSpec.value.size); const v = v2(meshSpec.value.size);
return MeshBuilder.CreateGround(name, { width: v.x, height: v.y }, scene ?? void 0); return {
rootnode: MeshBuilder.CreateGround(
name, { width: v.x, height: v.y }, scene ?? void 0),
};
} }
case "Plane": return MeshBuilder.CreatePlane(name, {}, scene); case "Plane": return { rootnode: MeshBuilder.CreatePlane(name, {}, scene) };
case "External": { case "External": {
const primary = new Mesh(name, scene); const rootnode = new Mesh(name, scene);
SceneLoader.ImportMesh( return {
"", rootnode,
meshSpec.value.path, subnodes: new Promise(async resolve => {
void 0, const r =
scene, await SceneLoader.ImportMeshAsync("", meshSpec.value.path, void 0, scene);
meshes => meshes.forEach(m => { r.meshes.forEach(m => m.parent = rootnode);
m.parent = primary; resolve(r.meshes);
console.log('adding submesh'); }),
})); };
console.log('returning primary');
return primary;
} }
} }
} }
@ -212,7 +183,8 @@ export function buildMesh(
export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree { export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree {
switch (shape._variant) { switch (shape._variant) {
case "Mesh": { case "Mesh": {
const t = new ShapeTree<Mesh>(scene, shape, buildMesh(name, scene, shape.value)); const m = buildMesh(name, scene, shape.value);
const t = new ShapeTree<AbstractMesh>(scene, shape, m.rootnode, m.subnodes);
applyCustomizer(t, customize); applyCustomizer(t, customize);
return t; return t;
} }
@ -225,15 +197,15 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
case "Scale": { case "Scale": {
const t = ShapeTree.transform(name, scene, shape); const t = ShapeTree.transform(name, scene, shape);
t.node.scaling = v3(shape.value.v); t.rootnode.scaling = v3(shape.value.v);
build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node; build(name + '.inner', scene, shape.value.shape, customize).rootnode.parent = t.rootnode;
return t; return t;
} }
case "Move": { case "Move": {
const t = ShapeTree.transform(name, scene, shape); const t = ShapeTree.transform(name, scene, shape);
t.node.position = v3(shape.value.v); t.rootnode.position = v3(shape.value.v);
build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node; build(name + '.inner', scene, shape.value.shape, customize).rootnode.parent = t.rootnode;
return t; return t;
} }
@ -241,21 +213,21 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
const t = ShapeTree.transform(name, scene, shape); const t = ShapeTree.transform(name, scene, shape);
switch (shape.value._variant) { switch (shape.value._variant) {
case "euler": case "euler":
t.node.rotation = v3(shape.value.v); t.rootnode.rotation = v3(shape.value.v);
t.node.rotation.scaleInPlace(2 * Math.PI); t.rootnode.rotation.scaleInPlace(2 * Math.PI);
break; break;
case "quaternion": case "quaternion":
t.node.rotationQuaternion = q(shape.value.q); t.rootnode.rotationQuaternion = q(shape.value.q);
break; break;
} }
build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node; build(name + '.inner', scene, shape.value.shape, customize).rootnode.parent = t.rootnode;
return t; return t;
} }
case "many": { case "many": {
const t = ShapeTree.transform(name, scene, shape); const t = ShapeTree.transform(name, scene, shape);
shape.value.forEach((s, i) => { shape.value.forEach((s, i) => {
build(name + '[' + i + ']', scene, s, customize).node.parent = t.node; build(name + '[' + i + ']', scene, s, customize).rootnode.parent = t.rootnode;
}); });
return t; return t;
} }
@ -264,7 +236,7 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
const entry = buildTexture(name + '.texture', scene, shape.value.spec); const entry = buildTexture(name + '.texture', scene, shape.value.spec);
const t = build(name + '.inner', scene, shape.value.shape, { const t = build(name + '.inner', scene, shape.value.shape, {
... customize, ... customize,
material: m => m.node.material = entry.material, material: async m => (await m.allnodes).forEach(n => n.material = entry.material),
}); });
t.cleanups.push(() => releaseTexture(entry)); t.cleanups.push(() => releaseTexture(entry));
return t; return t;
@ -276,7 +248,7 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
if (shape.value._variant === "transparent") mat.alpha = shape.value.alpha; if (shape.value._variant === "transparent") mat.alpha = shape.value.alpha;
return build(name + '.inner', scene, shape.value.shape, { return build(name + '.inner', scene, shape.value.shape, {
... customize, ... customize,
material: m => m.node.material = mat, material: async m => (await m.allnodes).forEach(n => n.material = mat),
}); });
} }
@ -286,11 +258,12 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
case "Floor": case "Floor":
return build(name, scene, shape.value.shape, { return build(name, scene, shape.value.shape, {
... customize, ... customize,
floor: m => { floor: async m => {
activeFloorMeshes.push(m.node); const nodes = await m.allnodes;
activeFloorMeshes.push(... nodes);
m.cleanups.push(() => { m.cleanups.push(() => {
const i = activeFloorMeshes.indexOf(m.node); const i = activeFloorMeshes.indexOf(nodes[0]);
if (i !== -1) activeFloorMeshes.splice(i, 1); if (i !== -1) activeFloorMeshes.splice(i, nodes.length);
}); });
}, },
}); });
@ -298,40 +271,39 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
case "Nonphysical": case "Nonphysical":
return build(name, scene, shape.value.shape, { return build(name, scene, shape.value.shape, {
... customize, ... customize,
collisions: m => { collisions: async m => (await m.allnodes).forEach(n => n.checkCollisions = false),
m.node.checkCollisions = false;
},
}); });
case "Touchable": case "Touchable":
return build(name, scene, shape.value.shape, { return build(name, scene, shape.value.shape, {
... customize, ... customize,
touchable: m => { touchable: async m => {
m.node.metadata.touchable = true; const nodes = await m.allnodes;
activeTouchableMeshes.push(m.node); m.rootnode.metadata.touchable = true;
activeTouchableMeshes.push(... nodes);
m.cleanups.push(() => { m.cleanups.push(() => {
const i = activeTouchableMeshes.indexOf(m.node); const i = activeTouchableMeshes.indexOf(nodes[0]);
if (i !== -1) activeTouchableMeshes.splice(i, 1); if (i !== -1) activeTouchableMeshes.splice(i, nodes.length);
}); });
} }
}); });
case "CSG": { case "CSG": {
const t = new ShapeTree<Mesh>( const m = buildCSG(name, scene, shape.value.expr);
scene, shape, buildCSG(shape.value.expr).toMesh(name, null, scene)); const t = new ShapeTree<AbstractMesh>(scene, shape, m.rootnode, m.subnodes);
applyCustomizer(t, customize); applyCustomizer(t, customize);
return t; return t;
} }
case "Skybox": { case "Skybox": {
const t = new ShapeTree<Mesh>(scene, shape, MeshBuilder.CreateBox(name, { size: 2000 }, scene)); const t = new ShapeTree<AbstractMesh>(scene, shape, MeshBuilder.CreateBox(name, { size: 2000 }, scene));
const mat = new StandardMaterial(name, scene); const mat = new StandardMaterial(name, scene);
mat.backFaceCulling = false; mat.backFaceCulling = false;
mat.reflectionTexture = new CubeTexture(shape.value.path, scene); mat.reflectionTexture = new CubeTexture(shape.value.path, scene);
mat.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE; mat.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE;
mat.diffuseColor = new Color3(0, 0, 0); mat.diffuseColor = new Color3(0, 0, 0);
mat.specularColor = new Color3(0, 0, 0); mat.specularColor = new Color3(0, 0, 0);
t.node.material = mat; t.rootnode.material = mat;
applyCustomizer(t, customize); applyCustomizer(t, customize);
return t; return t;
} }
@ -344,15 +316,21 @@ export function build(name: string, scene: Scene, shape: Shapes.Shape, customize
} }
} }
export function buildCSG(expr: Shapes.CSGExpr): CSG { export function buildCSG(name: string, scene: Scene, expr: Shapes.CSGExpr): BuiltMesh {
function walk(expr: Shapes.CSGExpr, matrix: Matrix): CSG { async function walk(expr: Shapes.CSGExpr, matrix: Matrix): Promise<CSG> {
switch (expr._variant) { switch (expr._variant) {
case "mesh": { case "mesh": {
const mesh = buildMesh("", null, expr.shape); const m = buildMesh("", null, expr.shape);
mesh.freezeWorldMatrix(matrix, true); const nodes: Mesh[] =
mesh.unfreezeWorldMatrix(); ([m.rootnode, ... (m.subnodes ? await m.subnodes : [])]
const c = CSG.FromMesh(mesh); .filter(n => n instanceof Mesh)) as Mesh[];
mesh.dispose(); 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; return c;
} }
case "scale": case "scale":
@ -367,28 +345,43 @@ export function buildCSG(expr: Shapes.CSGExpr): CSG {
expr.v.x * 2 * Math.PI, expr.v.x * 2 * Math.PI,
expr.v.z * 2 * Math.PI))); expr.v.z * 2 * Math.PI)));
case "subtract": { case "subtract": {
let c = walk(expr.base, matrix); const c = await walk(expr.base, matrix);
expr.more.forEach(d => c.subtractInPlace(walk(d, matrix))); for (const d of expr.more) {
c.subtractInPlace(await walk(d, matrix));
}
return c; return c;
} }
case "union": { case "union": {
let c = walk(expr.base, matrix); const c = await walk(expr.base, matrix);
expr.more.forEach(d => c.unionInPlace(walk(d, matrix))); for (const d of expr.more) {
c.unionInPlace(await walk(d, matrix));
}
return c; return c;
} }
case "intersect": { case "intersect": {
let c = walk(expr.base, matrix); const c = await walk(expr.base, matrix);
expr.more.forEach(d => c.intersectInPlace(walk(d, matrix))); for (const d of expr.more) {
c.intersectInPlace(await walk(d, matrix));
}
return c; return c;
} }
case "invert": { case "invert": {
let c = walk(expr.shape, matrix); const c = await walk(expr.shape, matrix);
c.inverseInPlace(); c.inverseInPlace();
return c; return c;
} }
} }
} }
return walk(expr, Matrix.Identity()); 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 } = { export const builder: { [key: string]: (... args: any[]) => Shapes.Shape } = {