Files
yawn/addons/handles/src/Mesh.ts
T

175 lines
5.7 KiB
TypeScript

import { Node, type NodeOptions } from "./Node";
import type { Scene } from "./Scene";
import type { PBRMaterial } from "./materials/PBRMaterial";
export const VertexKinds = Object.freeze([
"positions",
"normals",
"tangents",
"uvs",
"uvs2",
"colors",
"indices",
] as const);
export type VertexKind = (typeof VertexKinds)[number];
export type MeshOptions = NodeOptions & {
geometryId?: number;
material?: PBRMaterial;
vertexData?: Partial<Record<VertexKind, ArrayLike<number>>>;
visible?: boolean;
};
/** A renderable Node; clones share geometry until either clone mutates vertex data. */
export class Mesh extends Node {
geometryId: number;
vertexCount = 0;
indexCount = 0;
instanceOf = -1;
readonly faceMaterials = new Map<number, number>();
#registered = false;
constructor(scene: Scene, options: MeshOptions = {}) {
super(scene, options);
this.geometryId = options.geometryId ?? scene.createGeometry();
const nodeReady = this.ready;
this.ready = nodeReady.then(async () => {
if (options.material) await options.material.ready;
await scene.ensureRows(`mesh.${this.id}.faceMaterials`, 1, 16, "u32");
this.vertexCount =
Number(scene.geometryData(this.geometryId, "positions")?.length ?? 0) /
3;
this.indexCount = Number(
scene.geometryData(this.geometryId, "indices")?.length ?? 0,
);
this.instanceOf =
options.geometryId === undefined ? this.id : this.geometryId;
scene
.array("meshInfo")
.write(this.id, [
this.geometryId,
options.material?.id ?? 0,
options.visible === false ? 0 : 1,
this.instanceOf,
]);
for (const [kind, data] of Object.entries(options.vertexData ?? {}))
await this.#setVertexData(kind as VertexKind, data!, false);
this.#writeBounds();
this.#registered = true;
await scene.registerMesh(this);
return this;
});
}
get isVisible() {
if (this.id < 0) throw new Error("Await mesh.ready before reading it");
return this.scene.array("meshInfo").row(this.id)[2] !== 0;
}
set isVisible(value: boolean) {
if (this.id < 0) throw new Error("Await mesh.ready before writing it");
this.scene.array("meshInfo").row(this.id)[2] = value ? 1 : 0;
}
get materialId() {
return this.scene.array("meshInfo").row(this.id)[1];
}
set material(value: PBRMaterial) {
if (value.scene !== this.scene || value.id < 0)
throw new Error("Await a material from the same Scene");
this.scene.markDirty(true);
this.scene.array("meshInfo").row(this.id)[1] = value.id;
void this.scene.updateRenderGraph().catch(() => undefined);
}
clone(options: Omit<MeshOptions, "geometryId" | "vertexData"> = {}) {
if (this.id < 0) throw new Error("Await mesh.ready before cloning it");
return new Mesh(this.scene, { ...options, geometryId: this.geometryId });
}
async setVertexData(kind: VertexKind, data: ArrayLike<number>) {
if (!VertexKinds.includes(kind))
throw new TypeError(`Unknown vertex kind: ${kind}`);
await this.ready;
await this.#setVertexData(kind, data, true);
return this;
}
async #setVertexData(
kind: VertexKind,
data: ArrayLike<number>,
makeUnique: boolean,
) {
if (makeUnique && this.scene.geometryReferences(this.geometryId) > 1) {
const original = this.geometryId;
this.geometryId = await this.scene.cloneGeometry(original);
this.scene.releaseGeometry(original);
this.scene.referenceGeometry(this.geometryId);
this.instanceOf = this.id;
this.scene.markDirty(true);
this.scene
.array("meshInfo")
.row(this.id)
.set([
this.geometryId,
this.materialId,
this.isVisible ? 1 : 0,
this.id,
]);
}
if (kind === "positions") this.vertexCount = data.length / 3;
if (kind === "indices") this.indexCount = data.length;
await this.scene.setVertexData(this.geometryId, kind, data, makeUnique);
if (kind === "positions") this.#writeBounds();
}
async setMaterialForFaces(material: PBRMaterial, faces: number | number[]) {
await Promise.all([this.ready, material.ready]);
if (material.scene !== this.scene)
throw new Error("Material must belong to the same Scene");
const list = Array.isArray(faces) ? faces : [faces];
const maximum = Math.max(...list);
const array = await this.scene.ensureRows(
`mesh.${this.id}.faceMaterials`,
maximum + 1,
16,
"u32",
);
this.scene.markDirty(true);
for (const face of list) {
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
array.row(face)[0] = material.id + 1;
this.faceMaterials.set(face, material.id);
}
await this.scene.updateRenderGraph();
return this;
}
#writeBounds() {
const positions = this.scene.geometryData(this.geometryId, "positions");
if (!positions?.length || this.id < 0) return;
const minimum = [Infinity, Infinity, Infinity];
const maximum = [-Infinity, -Infinity, -Infinity];
for (let index = 0; index < positions.length; index += 3)
for (let lane = 0; lane < 3; lane++) {
minimum[lane] = Math.min(minimum[lane], positions[index + lane]);
maximum[lane] = Math.max(maximum[lane], positions[index + lane]);
}
this.scene
.array("bounds")
.row(this.id)
.set([...minimum, 0, ...maximum, 0]);
}
override async dispose() {
await this.ready;
if (this.disposed) return;
if (this.#registered) {
this.#registered = false;
await this.scene.unregisterMesh(this);
}
await this.scene.core.deleteRows(`mesh.${this.id}.faceMaterials`);
await super.dispose();
}
}