handles: one interleaved vertices/indices arena
Geometry becomes a span in a single 80-byte interleaved `vertices` array plus a span in a single `indices` array, allocated through the core's span allocator instead of one arena array per mesh attribute. Draws are always indexed with baseVertex; face-material overrides emit one draw per run of same-material faces rather than one per face. Vertex data is validated as a whole before any span is touched, same-geometry mutations are serialised, and render signal 4 is held across a span change until the graph reflecting it is published. Sibling arrays grow to the primary array's capacity in one batch; reserve() takes vertex/index totals. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
+27
-36
@@ -12,18 +12,17 @@ export const VertexKinds = Object.freeze([
|
|||||||
"indices",
|
"indices",
|
||||||
] as const);
|
] as const);
|
||||||
export type VertexKind = (typeof VertexKinds)[number];
|
export type VertexKind = (typeof VertexKinds)[number];
|
||||||
|
export type VertexStreams = Partial<Record<VertexKind, ArrayLike<number>>>;
|
||||||
export type MeshOptions = NodeOptions & {
|
export type MeshOptions = NodeOptions & {
|
||||||
geometryId?: number;
|
geometryId?: number;
|
||||||
material?: PBRMaterial;
|
material?: PBRMaterial;
|
||||||
vertexData?: Partial<Record<VertexKind, ArrayLike<number>>>;
|
vertexData?: VertexStreams;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A renderable Node; clones share geometry until either clone mutates vertex data. */
|
/** A renderable Node; clones share geometry until either clone mutates vertex data. */
|
||||||
export class Mesh extends Node {
|
export class Mesh extends Node {
|
||||||
geometryId: number;
|
geometryId: number;
|
||||||
vertexCount = 0;
|
|
||||||
indexCount = 0;
|
|
||||||
instanceOf = -1;
|
instanceOf = -1;
|
||||||
readonly faceMaterials = new Map<number, number>();
|
readonly faceMaterials = new Map<number, number>();
|
||||||
#registered = false;
|
#registered = false;
|
||||||
@@ -34,13 +33,6 @@ export class Mesh extends Node {
|
|||||||
const nodeReady = this.ready;
|
const nodeReady = this.ready;
|
||||||
this.ready = nodeReady.then(async () => {
|
this.ready = nodeReady.then(async () => {
|
||||||
if (options.material) await options.material.ready;
|
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 =
|
this.instanceOf =
|
||||||
options.geometryId === undefined ? this.id : this.geometryId;
|
options.geometryId === undefined ? this.id : this.geometryId;
|
||||||
scene
|
scene
|
||||||
@@ -51,8 +43,8 @@ export class Mesh extends Node {
|
|||||||
options.visible === false ? 0 : 1,
|
options.visible === false ? 0 : 1,
|
||||||
this.instanceOf,
|
this.instanceOf,
|
||||||
]);
|
]);
|
||||||
for (const [kind, data] of Object.entries(options.vertexData ?? {}))
|
if (options.vertexData)
|
||||||
await this.#setVertexData(kind as VertexKind, data!, false);
|
await this.#setVertexData(options.vertexData, false);
|
||||||
this.#writeBounds();
|
this.#writeBounds();
|
||||||
this.#registered = true;
|
this.#registered = true;
|
||||||
await scene.registerMesh(this);
|
await scene.registerMesh(this);
|
||||||
@@ -60,6 +52,14 @@ export class Mesh extends Node {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get vertexCount() {
|
||||||
|
return this.scene.geometry(this.geometryId).vertexCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get indexCount() {
|
||||||
|
return this.scene.geometry(this.geometryId).indexCount;
|
||||||
|
}
|
||||||
|
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
if (this.id < 0) throw new Error("Await mesh.ready before reading it");
|
if (this.id < 0) throw new Error("Await mesh.ready before reading it");
|
||||||
return this.scene.array("meshInfo").row(this.id)[2] !== 0;
|
return this.scene.array("meshInfo").row(this.id)[2] !== 0;
|
||||||
@@ -87,20 +87,22 @@ export class Mesh extends Node {
|
|||||||
return new Mesh(this.scene, { ...options, geometryId: this.geometryId });
|
return new Mesh(this.scene, { ...options, geometryId: this.geometryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVertexData(kind: VertexKind, data: ArrayLike<number>) {
|
/** Replaces one stream, or several at once, repacking the geometry a single time. */
|
||||||
if (!VertexKinds.includes(kind))
|
async setVertexData(
|
||||||
throw new TypeError(`Unknown vertex kind: ${kind}`);
|
kind: VertexKind | VertexStreams,
|
||||||
|
data?: ArrayLike<number>,
|
||||||
|
) {
|
||||||
|
const streams = typeof kind === "string" ? { [kind]: data } : kind;
|
||||||
|
for (const name of Object.keys(streams))
|
||||||
|
if (!VertexKinds.includes(name as VertexKind))
|
||||||
|
throw new TypeError(`Unknown vertex kind: ${name}`);
|
||||||
await this.ready;
|
await this.ready;
|
||||||
await this.#setVertexData(kind, data, true);
|
await this.#setVertexData(streams, true);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #setVertexData(
|
async #setVertexData(streams: VertexStreams, makeUnique: boolean) {
|
||||||
kind: VertexKind,
|
if (makeUnique && this.scene.geometry(this.geometryId).references > 1) {
|
||||||
data: ArrayLike<number>,
|
|
||||||
makeUnique: boolean,
|
|
||||||
) {
|
|
||||||
if (makeUnique && this.scene.geometryReferences(this.geometryId) > 1) {
|
|
||||||
const original = this.geometryId;
|
const original = this.geometryId;
|
||||||
this.geometryId = await this.scene.cloneGeometry(original);
|
this.geometryId = await this.scene.cloneGeometry(original);
|
||||||
this.scene.releaseGeometry(original);
|
this.scene.releaseGeometry(original);
|
||||||
@@ -117,10 +119,8 @@ export class Mesh extends Node {
|
|||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (kind === "positions") this.vertexCount = data.length / 3;
|
await this.scene.setVertexData(this.geometryId, streams, makeUnique);
|
||||||
if (kind === "indices") this.indexCount = data.length;
|
if (streams.positions) this.#writeBounds();
|
||||||
await this.scene.setVertexData(this.geometryId, kind, data, makeUnique);
|
|
||||||
if (kind === "positions") this.#writeBounds();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMaterialForFaces(material: PBRMaterial, faces: number | number[]) {
|
async setMaterialForFaces(material: PBRMaterial, faces: number | number[]) {
|
||||||
@@ -128,19 +128,11 @@ export class Mesh extends Node {
|
|||||||
if (material.scene !== this.scene)
|
if (material.scene !== this.scene)
|
||||||
throw new Error("Material must belong to the same Scene");
|
throw new Error("Material must belong to the same Scene");
|
||||||
const list = Array.isArray(faces) ? faces : [faces];
|
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) {
|
for (const face of list) {
|
||||||
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
|
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
|
||||||
array.row(face)[0] = material.id + 1;
|
|
||||||
this.faceMaterials.set(face, material.id);
|
this.faceMaterials.set(face, material.id);
|
||||||
}
|
}
|
||||||
|
this.scene.markDirty(true);
|
||||||
await this.scene.updateRenderGraph();
|
await this.scene.updateRenderGraph();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -168,7 +160,6 @@ export class Mesh extends Node {
|
|||||||
this.#registered = false;
|
this.#registered = false;
|
||||||
await this.scene.unregisterMesh(this);
|
await this.scene.unregisterMesh(this);
|
||||||
}
|
}
|
||||||
await this.scene.core.deleteRows(`mesh.${this.id}.faceMaterials`);
|
|
||||||
await super.dispose();
|
await super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+347
-338
@@ -10,10 +10,39 @@ type RowFormat = "f32" | "u32" | "i32";
|
|||||||
type MeshLike = {
|
type MeshLike = {
|
||||||
id: number;
|
id: number;
|
||||||
geometryId: number;
|
geometryId: number;
|
||||||
indexCount: number;
|
|
||||||
vertexCount: number;
|
|
||||||
faceMaterials: ReadonlyMap<number, number>;
|
faceMaterials: ReadonlyMap<number, number>;
|
||||||
};
|
};
|
||||||
|
type VertexData = Float32Array | Uint32Array;
|
||||||
|
/** One geometry: a span of interleaved `vertices` rows plus a span of `indices` rows. */
|
||||||
|
type Geometry = {
|
||||||
|
data: Map<string, VertexData>;
|
||||||
|
/** Serialises published mutations of this geometry's spans. */
|
||||||
|
pending: Promise<unknown>;
|
||||||
|
references: number;
|
||||||
|
vertexRow: number;
|
||||||
|
vertexCount: number;
|
||||||
|
indexRow: number;
|
||||||
|
indexCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Interleaved vertex row: 20 floats = 80 bytes; lanes match the pipeline attribute offsets. */
|
||||||
|
const vertexLanes: Record<string, [offset: number, width: number]> = {
|
||||||
|
positions: [0, 3],
|
||||||
|
normals: [4, 3],
|
||||||
|
uvs: [8, 2],
|
||||||
|
uvs2: [10, 2],
|
||||||
|
tangents: [12, 4],
|
||||||
|
colors: [16, 4],
|
||||||
|
};
|
||||||
|
const vertexWidth = 20;
|
||||||
|
const vertexAttributes = [
|
||||||
|
{ format: "float32x3", offset: 0, shaderLocation: 0 },
|
||||||
|
{ format: "float32x3", offset: 16, shaderLocation: 1 },
|
||||||
|
{ format: "float32x2", offset: 32, shaderLocation: 2 },
|
||||||
|
{ format: "float32x4", offset: 48, shaderLocation: 3 },
|
||||||
|
{ format: "float32x4", offset: 64, shaderLocation: 4 },
|
||||||
|
{ format: "float32x2", offset: 40, shaderLocation: 5 },
|
||||||
|
];
|
||||||
type ShaderLike = {
|
type ShaderLike = {
|
||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -50,6 +79,8 @@ const rows = [
|
|||||||
["directionalLights", 32, "f32"],
|
["directionalLights", 32, "f32"],
|
||||||
["ambientLights", 16, "f32"],
|
["ambientLights", 16, "f32"],
|
||||||
["sceneAccent", 16, "f32"],
|
["sceneAccent", 16, "f32"],
|
||||||
|
["vertices", 80, "f32"],
|
||||||
|
["indices", 16, "u32"],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const compactLightLimit = 500;
|
const compactLightLimit = 500;
|
||||||
@@ -196,8 +227,6 @@ fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
function pbrShader(
|
function pbrShader(
|
||||||
mask: number,
|
mask: number,
|
||||||
flags: number,
|
flags: number,
|
||||||
hasColors: boolean,
|
|
||||||
hasUvs2: boolean,
|
|
||||||
transmission: boolean,
|
transmission: boolean,
|
||||||
textureInfo: ArrayLike<number>,
|
textureInfo: ArrayLike<number>,
|
||||||
extensionInfo: ArrayLike<number>,
|
extensionInfo: ArrayLike<number>,
|
||||||
@@ -293,9 +322,9 @@ fn vertex(
|
|||||||
@location(0) point: vec3<f32>,
|
@location(0) point: vec3<f32>,
|
||||||
@location(1) localNormal: vec3<f32>,
|
@location(1) localNormal: vec3<f32>,
|
||||||
@location(2) uv: vec2<f32>,
|
@location(2) uv: vec2<f32>,
|
||||||
${normalTexture ? "@location(3) localTangent: vec4<f32>," : ""}
|
@location(3) localTangent: vec4<f32>,
|
||||||
${hasColors ? "@location(4) color: vec4<f32>," : ""}
|
@location(4) color: vec4<f32>,
|
||||||
${hasUvs2 ? "@location(5) uv2: vec2<f32>," : ""}
|
@location(5) uv2: vec2<f32>,
|
||||||
@builtin(instance_index) packed: u32,
|
@builtin(instance_index) packed: u32,
|
||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
let instance = packed & 65535u;
|
let instance = packed & 65535u;
|
||||||
@@ -316,9 +345,9 @@ fn vertex(
|
|||||||
output.world = world;
|
output.world = world;
|
||||||
output.normal = normalize(rotate(rotors[instance], localNormal / scale));
|
output.normal = normalize(rotate(rotors[instance], localNormal / scale));
|
||||||
output.uv = uv;
|
output.uv = uv;
|
||||||
output.tangent = ${normalTexture ? "vec4(normalize(rotate(rotors[instance], localTangent.xyz * scale)), localTangent.w)" : "vec4(1.0, 0.0, 0.0, 1.0)"};
|
output.tangent = vec4(normalize(rotate(rotors[instance], localTangent.xyz * scale)), localTangent.w);
|
||||||
output.color = ${hasColors ? "color" : "vec4(1.0)"};
|
output.color = color;
|
||||||
output.uv2 = ${hasUvs2 ? "uv2" : "uv"};
|
output.uv2 = uv2;
|
||||||
output.material = (packed >> 16u) - 1u;
|
output.material = (packed >> 16u) - 1u;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -590,12 +619,13 @@ export class Scene {
|
|||||||
#shaders = new Map<number, ShaderLike>();
|
#shaders = new Map<number, ShaderLike>();
|
||||||
#effects = new Map<string, PostProcessState>();
|
#effects = new Map<string, PostProcessState>();
|
||||||
#textures = new Map<number, TextureState>();
|
#textures = new Map<number, TextureState>();
|
||||||
#geometry = new Map<number, Map<string, Float32Array | Uint32Array>>();
|
#geometry = new Map<number, Geometry>();
|
||||||
#geometryRefs = new Map<number, number>();
|
|
||||||
#nextGeometry = 1;
|
#nextGeometry = 1;
|
||||||
#nextTexture = 0;
|
#nextTexture = 0;
|
||||||
#graphBatchDepth = 0;
|
#graphBatchDepth = 0;
|
||||||
#graphBatchDirty = false;
|
#graphBatchDirty = false;
|
||||||
|
#inhibits = 0;
|
||||||
|
#batchInhibits = 0;
|
||||||
#writeBatchDepth = 0;
|
#writeBatchDepth = 0;
|
||||||
#writeBatchDirty = false;
|
#writeBatchDirty = false;
|
||||||
#writeBatchBundleDirty = false;
|
#writeBatchBundleDirty = false;
|
||||||
@@ -774,50 +804,47 @@ export class Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async reserve(additional: { nodes?: number; materials?: number }) {
|
/**
|
||||||
|
* Grows the shared arrays ahead of a bulk load so the objects created afterwards never
|
||||||
|
* relocate them: `nodes`/`materials` count objects, `vertices`/`indices` count elements.
|
||||||
|
*/
|
||||||
|
async reserve(additional: {
|
||||||
|
nodes?: number;
|
||||||
|
materials?: number;
|
||||||
|
vertices?: number;
|
||||||
|
indices?: number;
|
||||||
|
}) {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
const nodes = additional.nodes ?? 0;
|
const counts = {
|
||||||
const materials = additional.materials ?? 0;
|
nodes: additional.nodes ?? 0,
|
||||||
|
materials: additional.materials ?? 0,
|
||||||
|
vertices: additional.vertices ?? 0,
|
||||||
|
indices: Math.ceil((additional.indices ?? 0) / 4),
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
![nodes, materials].every(
|
!Object.values(counts).every(
|
||||||
(value) => Number.isInteger(value) && value >= 0,
|
(value) => Number.isInteger(value) && value >= 0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
throw new RangeError("additional");
|
throw new RangeError("additional");
|
||||||
const nodeCapacity = this.array("nodes").rows + nodes;
|
const families: [keyof typeof counts, readonly (typeof rows)[number][]][] =
|
||||||
const materialCapacity = this.array("materials").rows + materials;
|
[
|
||||||
const growth = [
|
["nodes", rows.slice(0, 6)],
|
||||||
...rows.slice(0, 6).map(([name, stride, format]) => ({
|
["materials", rows.slice(8, 12)],
|
||||||
|
["vertices", rows.slice(18, 19)],
|
||||||
|
["indices", rows.slice(19, 20)],
|
||||||
|
];
|
||||||
|
const growth = families
|
||||||
|
.flatMap(([primary, members]) => {
|
||||||
|
const capacity = this.core.array(primary).rows + counts[primary];
|
||||||
|
return members.map(([name, stride, format]) => ({
|
||||||
name,
|
name,
|
||||||
rows: nodeCapacity,
|
rows: capacity,
|
||||||
stride,
|
stride,
|
||||||
format,
|
format,
|
||||||
})),
|
}));
|
||||||
{
|
})
|
||||||
name: "materials",
|
.filter((request) => this.core.array(request.name).rows < request.rows);
|
||||||
rows: materialCapacity,
|
|
||||||
stride: 48,
|
|
||||||
format: "f32" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "materialTextures",
|
|
||||||
rows: materialCapacity,
|
|
||||||
stride: 64,
|
|
||||||
format: "u32" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "materialExtensions",
|
|
||||||
rows: materialCapacity,
|
|
||||||
stride: 112,
|
|
||||||
format: "f32" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "materialTextureInfo",
|
|
||||||
rows: materialCapacity,
|
|
||||||
stride: 416,
|
|
||||||
format: "f32" as const,
|
|
||||||
},
|
|
||||||
].filter((request) => this.array(request.name).rows < request.rows);
|
|
||||||
if (growth.length) await this.core.createRowsBatch(growth);
|
if (growth.length) await this.core.createRowsBatch(growth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,23 +855,24 @@ export class Scene {
|
|||||||
return await operation();
|
return await operation();
|
||||||
} finally {
|
} finally {
|
||||||
this.#graphBatchDepth--;
|
this.#graphBatchDepth--;
|
||||||
if (!this.#graphBatchDepth && this.#graphBatchDirty) {
|
if (!this.#graphBatchDepth) {
|
||||||
|
try {
|
||||||
|
if (this.#graphBatchDirty) {
|
||||||
this.#graphBatchDirty = false;
|
this.#graphBatchDirty = false;
|
||||||
await this.updateRenderGraph();
|
await this.updateRenderGraph();
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
for (; this.#batchInhibits; this.#batchInhibits--)
|
||||||
|
this.#releaseRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async allocateNode() {
|
async allocateNode() {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
const id = await this.core.allocateObject("nodes");
|
const id = await this.core.allocateObject("nodes");
|
||||||
const growth = rows.slice(1, 6).flatMap(([name, stride, format]) => {
|
await this.#growSiblings("nodes", rows.slice(1, 6));
|
||||||
const current = this.array(name);
|
|
||||||
return current.rows < id + 1
|
|
||||||
? [{ name, rows: id + 1, stride, format }]
|
|
||||||
: [];
|
|
||||||
});
|
|
||||||
if (growth.length) await this.core.createRowsBatch(growth);
|
|
||||||
this.array("nodeRotors").write(id, [0, 0, 0, 1]);
|
this.array("nodeRotors").write(id, [0, 0, 0, 1]);
|
||||||
this.array("nodeScales").write(id, [1, 1, 1, 0]);
|
this.array("nodeScales").write(id, [1, 1, 1, 0]);
|
||||||
this.array("nodes").write(id, [1, 0, 0, 0]);
|
this.array("nodes").write(id, [1, 0, 0, 0]);
|
||||||
@@ -867,14 +895,24 @@ export class Scene {
|
|||||||
async allocateMaterial() {
|
async allocateMaterial() {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
const id = await this.core.allocateObject("materials");
|
const id = await this.core.allocateObject("materials");
|
||||||
await Promise.all([
|
await this.#growSiblings("materials", rows.slice(9, 12));
|
||||||
this.ensureRows("materialTextures", id + 1, 64, "u32"),
|
|
||||||
this.ensureRows("materialExtensions", id + 1, 112, "f32"),
|
|
||||||
this.ensureRows("materialTextureInfo", id + 1, 416, "f32"),
|
|
||||||
]);
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Grows the arrays indexed by `primary`'s IDs to its (geometric) capacity in one batch. */
|
||||||
|
async #growSiblings(
|
||||||
|
primary: string,
|
||||||
|
siblings: readonly (readonly [string, number, RowFormat])[],
|
||||||
|
) {
|
||||||
|
const capacity = this.core.array(primary).rows;
|
||||||
|
const growth = siblings.flatMap(([name, stride, format]) =>
|
||||||
|
this.core.array(name).rows < capacity
|
||||||
|
? [{ name, rows: capacity, stride, format }]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
if (growth.length) await this.core.createRowsBatch(growth);
|
||||||
|
}
|
||||||
|
|
||||||
addComputePass(pass: ComputePass) {
|
addComputePass(pass: ComputePass) {
|
||||||
if (this.#computePasses.has(pass.id))
|
if (this.#computePasses.has(pass.id))
|
||||||
throw new Error(`COMPUTE_PASS_EXISTS: ${pass.id}`);
|
throw new Error(`COMPUTE_PASS_EXISTS: ${pass.id}`);
|
||||||
@@ -893,109 +931,210 @@ export class Scene {
|
|||||||
|
|
||||||
registerMesh(mesh: MeshLike) {
|
registerMesh(mesh: MeshLike) {
|
||||||
this.#meshes.set(mesh.id, mesh);
|
this.#meshes.set(mesh.id, mesh);
|
||||||
this.#geometryRefs.set(
|
this.referenceGeometry(mesh.geometryId);
|
||||||
mesh.geometryId,
|
|
||||||
(this.#geometryRefs.get(mesh.geometryId) ?? 0) + 1,
|
|
||||||
);
|
|
||||||
return this.updateRenderGraph();
|
return this.updateRenderGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
async unregisterMesh(mesh: MeshLike) {
|
async unregisterMesh(mesh: MeshLike) {
|
||||||
this.#meshes.delete(mesh.id);
|
this.#meshes.delete(mesh.id);
|
||||||
const references = Math.max(
|
const geometry = this.#geometry.get(mesh.geometryId)!;
|
||||||
0,
|
geometry.references = Math.max(0, geometry.references - 1);
|
||||||
(this.#geometryRefs.get(mesh.geometryId) ?? 1) - 1,
|
if (geometry.references) return this.updateRenderGraph();
|
||||||
);
|
|
||||||
this.#geometryRefs.set(mesh.geometryId, references);
|
|
||||||
await this.updateRenderGraph();
|
|
||||||
if (!references) {
|
|
||||||
for (const kind of this.#geometry.get(mesh.geometryId)?.keys() ?? [])
|
|
||||||
await this.core.deleteRows(`geometry.${mesh.geometryId}.${kind}`);
|
|
||||||
this.#geometry.delete(mesh.geometryId);
|
this.#geometry.delete(mesh.geometryId);
|
||||||
this.#geometryRefs.delete(mesh.geometryId);
|
await this.ready;
|
||||||
}
|
await this.#inhibitRender(async () => {
|
||||||
|
await this.updateRenderGraph();
|
||||||
|
await this.#allocateSpans(geometry, 0, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createGeometry() {
|
createGeometry() {
|
||||||
const id = this.#nextGeometry++;
|
const id = this.#nextGeometry++;
|
||||||
this.#geometry.set(id, new Map());
|
this.#geometry.set(id, {
|
||||||
this.#geometryRefs.set(id, 0);
|
data: new Map(),
|
||||||
|
pending: Promise.resolve(),
|
||||||
|
references: 0,
|
||||||
|
vertexRow: 0,
|
||||||
|
vertexCount: 0,
|
||||||
|
indexRow: 0,
|
||||||
|
indexCount: 0,
|
||||||
|
});
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
geometryReferences(id: number) {
|
geometry(id: number): Readonly<Geometry> {
|
||||||
return this.#geometryRefs.get(id) ?? 0;
|
const geometry = this.#geometry.get(id);
|
||||||
|
if (!geometry) throw new Error(`UNKNOWN_GEOMETRY: ${id}`);
|
||||||
|
return geometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
referenceGeometry(id: number) {
|
referenceGeometry(id: number) {
|
||||||
this.#geometryRefs.set(id, (this.#geometryRefs.get(id) ?? 0) + 1);
|
this.#geometry.get(id)!.references++;
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseGeometry(id: number) {
|
releaseGeometry(id: number) {
|
||||||
this.#geometryRefs.set(
|
const geometry = this.#geometry.get(id)!;
|
||||||
id,
|
geometry.references = Math.max(0, geometry.references - 1);
|
||||||
Math.max(0, (this.#geometryRefs.get(id) ?? 1) - 1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cloneGeometry(id: number) {
|
async cloneGeometry(id: number) {
|
||||||
const clone = this.createGeometry();
|
const clone = this.createGeometry();
|
||||||
for (const [kind, data] of this.#geometry.get(id) ?? [])
|
const copies: Record<string, VertexData> = {};
|
||||||
await this.setVertexData(
|
for (const [kind, data] of this.geometry(id).data)
|
||||||
clone,
|
copies[kind] = data.slice() as VertexData;
|
||||||
kind,
|
await this.setVertexData(clone, copies, false);
|
||||||
data.slice() as Float32Array | Uint32Array,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the given attribute streams of a geometry, repacks its interleaved `vertices` span
|
||||||
|
* and `indices` span (synthesised as `0..n` when absent), and uploads exactly those rows.
|
||||||
|
*/
|
||||||
async setVertexData(
|
async setVertexData(
|
||||||
geometry: number,
|
geometry: number,
|
||||||
kind: string,
|
streams: Partial<Record<string, ArrayLike<number>>>,
|
||||||
source: ArrayLike<number>,
|
|
||||||
updateGraph = true,
|
updateGraph = true,
|
||||||
) {
|
) {
|
||||||
const components: Record<string, number> = {
|
const record = this.#geometry.get(geometry);
|
||||||
positions: 3,
|
if (!record) throw new Error(`UNKNOWN_GEOMETRY: ${geometry}`);
|
||||||
normals: 3,
|
// Validate the whole assembled geometry before touching the record or its spans.
|
||||||
tangents: 4,
|
const data = new Map(record.data);
|
||||||
uvs: 2,
|
for (const [kind, source] of Object.entries(streams)) {
|
||||||
uvs2: 2,
|
if (!source) continue;
|
||||||
colors: 4,
|
if (kind !== "indices" && !vertexLanes[kind])
|
||||||
indices: 1,
|
|
||||||
};
|
|
||||||
const width = components[kind];
|
|
||||||
if (!width || source.length % width)
|
|
||||||
throw new RangeError(`VERTEX_DATA: ${kind}`);
|
throw new RangeError(`VERTEX_DATA: ${kind}`);
|
||||||
const integer = kind === "indices";
|
data.set(
|
||||||
const data = integer ? Uint32Array.from(source) : Float32Array.from(source);
|
kind,
|
||||||
const name = `geometry.${geometry}.${kind}`;
|
kind === "indices"
|
||||||
const rowCount = integer ? Math.ceil(data.length / 4) : data.length / width;
|
? Uint32Array.from(source)
|
||||||
const target = await this.ensureRows(
|
: Float32Array.from(source),
|
||||||
name,
|
|
||||||
rowCount,
|
|
||||||
16,
|
|
||||||
integer ? "u32" : "f32",
|
|
||||||
);
|
);
|
||||||
if (updateGraph) this.markDirty(true);
|
}
|
||||||
const view = target.view;
|
const vertexCount = (data.get("positions")?.length ?? 0) / 3;
|
||||||
view.fill(0);
|
for (const [kind, [, width]] of Object.entries(vertexLanes)) {
|
||||||
if (integer || width === 4) view.set(data);
|
const stream = data.get(kind);
|
||||||
else
|
if (stream && stream.length !== vertexCount * width)
|
||||||
for (let row = 0; row < rowCount; row++)
|
throw new RangeError(`VERTEX_DATA: ${kind}`);
|
||||||
|
}
|
||||||
|
const indices = data.get("indices");
|
||||||
|
const source = streams.indices;
|
||||||
|
if (indices && source)
|
||||||
|
for (let index = 0; index < indices.length; index++)
|
||||||
|
if (source[index] !== indices[index] || indices[index] >= vertexCount)
|
||||||
|
throw new RangeError(`VERTEX_DATA: indices[${index}]`);
|
||||||
|
const indexCount = indices?.length ?? vertexCount;
|
||||||
|
await this.ready;
|
||||||
|
if (!updateGraph) {
|
||||||
|
record.data = data;
|
||||||
|
await this.#allocateSpans(record, vertexCount, indexCount);
|
||||||
|
await this.#packGeometry(record);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pending = record.pending.then(async () => {
|
||||||
|
this.markDirty(true);
|
||||||
|
record.data = data;
|
||||||
|
await this.#allocateSpans(record, vertexCount, indexCount);
|
||||||
|
await this.#packGeometry(record);
|
||||||
|
await this.updateRenderGraph();
|
||||||
|
});
|
||||||
|
record.pending = pending.catch(() => undefined);
|
||||||
|
await this.#inhibitRender(() => pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds render signal 4 across a structural mutation until the graph reflecting it is
|
||||||
|
* published, so an older loadout switch clearing signal 6 cannot expose stale spans.
|
||||||
|
*/
|
||||||
|
async #inhibitRender<T>(operation: () => Promise<T>) {
|
||||||
|
if (!this.#inhibits++) this.#signals![4] = 1;
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} finally {
|
||||||
|
if (this.#graphBatchDepth) this.#batchInhibits++;
|
||||||
|
else this.#releaseRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#releaseRender() {
|
||||||
|
if (--this.#inhibits) return;
|
||||||
|
this.#signals![4] = 0;
|
||||||
|
this.#signals![5] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Packs the geometry's CPU streams into its `vertices`/`indices` spans and uploads them. */
|
||||||
|
async #packGeometry(record: Geometry) {
|
||||||
|
const { vertexCount, indexCount } = record;
|
||||||
|
const indices = record.data.get("indices");
|
||||||
|
if (vertexCount) {
|
||||||
|
const vertices = this.core.array("vertices").view as Float32Array;
|
||||||
|
const base = record.vertexRow * vertexWidth;
|
||||||
|
vertices.fill(0, base, base + vertexCount * vertexWidth);
|
||||||
|
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
||||||
|
const at = base + vertex * vertexWidth;
|
||||||
|
vertices[at + 12] = 1;
|
||||||
|
vertices[at + 15] = 1;
|
||||||
|
vertices.fill(1, at + 16, at + 20);
|
||||||
|
}
|
||||||
|
for (const [kind, [offset, width]] of Object.entries(vertexLanes)) {
|
||||||
|
const data = record.data.get(kind);
|
||||||
|
if (!data || data.length !== vertexCount * width) continue;
|
||||||
|
for (let vertex = 0; vertex < vertexCount; vertex++)
|
||||||
for (let lane = 0; lane < width; lane++)
|
for (let lane = 0; lane < width; lane++)
|
||||||
view[row * 4 + lane] = data[row * width + lane];
|
vertices[base + vertex * vertexWidth + offset + lane] =
|
||||||
(
|
data[vertex * width + lane];
|
||||||
this.#geometry.get(geometry) ??
|
}
|
||||||
this.#geometry.set(geometry, new Map()).get(geometry)!
|
if (!record.data.has("uvs2")) {
|
||||||
).set(kind, data);
|
const uvs = record.data.get("uvs");
|
||||||
await this.core.syncRows(name, 0, rowCount);
|
if (uvs)
|
||||||
if (updateGraph) await this.updateRenderGraph();
|
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
||||||
|
const at = base + vertex * vertexWidth;
|
||||||
|
vertices[at + 10] = uvs[vertex * 2];
|
||||||
|
vertices[at + 11] = uvs[vertex * 2 + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const indexView = this.core.array("indices").view as Uint32Array;
|
||||||
|
const indexBase = record.indexRow * 4;
|
||||||
|
if (indices) indexView.set(indices, indexBase);
|
||||||
|
else
|
||||||
|
for (let index = 0; index < indexCount; index++)
|
||||||
|
indexView[indexBase + index] = index;
|
||||||
|
await this.core.syncRows("vertices", record.vertexRow, vertexCount);
|
||||||
|
await this.core.syncRows(
|
||||||
|
"indices",
|
||||||
|
record.indexRow,
|
||||||
|
Math.ceil(indexCount / 4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-reserves the geometry's spans when their sizes change; zero counts free them. */
|
||||||
|
async #allocateSpans(
|
||||||
|
geometry: Geometry,
|
||||||
|
vertexCount: number,
|
||||||
|
indexCount: number,
|
||||||
|
) {
|
||||||
|
const indexRows = Math.ceil(indexCount / 4);
|
||||||
|
const currentIndexRows = Math.ceil(geometry.indexCount / 4);
|
||||||
|
if (vertexCount !== geometry.vertexCount) {
|
||||||
|
if (geometry.vertexCount)
|
||||||
|
await this.core.deleteObject("vertices", geometry.vertexRow);
|
||||||
|
geometry.vertexRow = vertexCount
|
||||||
|
? await this.core.allocateObject("vertices", vertexCount)
|
||||||
|
: 0;
|
||||||
|
geometry.vertexCount = vertexCount;
|
||||||
|
}
|
||||||
|
if (indexRows !== currentIndexRows) {
|
||||||
|
if (currentIndexRows)
|
||||||
|
await this.core.deleteObject("indices", geometry.indexRow);
|
||||||
|
geometry.indexRow = indexRows
|
||||||
|
? await this.core.allocateObject("indices", indexRows)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
geometry.indexCount = indexCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
geometryData(id: number, kind: string) {
|
geometryData(id: number, kind: string) {
|
||||||
return this.#geometry.get(id)?.get(kind);
|
return this.#geometry.get(id)?.data.get(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerShader(material: ShaderLike) {
|
registerShader(material: ShaderLike) {
|
||||||
@@ -1128,22 +1267,56 @@ export class Scene {
|
|||||||
const transmitted = this.array("materialExtensions").row(material)[0] > 0;
|
const transmitted = this.array("materialExtensions").row(material)[0] > 0;
|
||||||
return transmitted ? 1 : (flags & 3) === 2 ? 2 : 0;
|
return transmitted ? 1 : (flags & 3) === 2 ? 2 : 0;
|
||||||
};
|
};
|
||||||
const renderedMeshes = [...this.#meshes.values()]
|
// One draw per run of consecutive faces sharing a material, in category order:
|
||||||
.filter((mesh) => mesh.vertexCount > 0)
|
// opaque/mask first, then transmission, then blended, each in mesh ID order.
|
||||||
.sort(
|
type ForwardDraw = {
|
||||||
(left, right) =>
|
mesh: MeshLike;
|
||||||
renderCategory(Number(this.array("meshInfo").row(left.id)[1])) -
|
geometry: Readonly<Geometry>;
|
||||||
renderCategory(Number(this.array("meshInfo").row(right.id)[1])),
|
material: number;
|
||||||
);
|
firstIndex: number;
|
||||||
const hasTransmission = renderedMeshes.some((mesh) => {
|
indices: number;
|
||||||
const materials = new Set([
|
};
|
||||||
Number(this.array("meshInfo").row(mesh.id)[1]),
|
const forwardDraws: ForwardDraw[] = [];
|
||||||
...mesh.faceMaterials.values(),
|
for (const mesh of this.#meshes.values()) {
|
||||||
]);
|
const geometry = this.geometry(mesh.geometryId);
|
||||||
return [...materials].some(
|
if (!geometry.vertexCount) continue;
|
||||||
(material) => this.array("materialExtensions").row(material)[0] > 0,
|
const fallback = Number(this.array("meshInfo").row(mesh.id)[1]);
|
||||||
);
|
if (!mesh.faceMaterials.size) {
|
||||||
|
forwardDraws.push({
|
||||||
|
mesh,
|
||||||
|
geometry,
|
||||||
|
material: fallback,
|
||||||
|
firstIndex: geometry.indexRow * 4,
|
||||||
|
indices: geometry.indexCount,
|
||||||
});
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const faces = Math.floor(geometry.indexCount / 3);
|
||||||
|
let start = 0;
|
||||||
|
let material = mesh.faceMaterials.get(0) ?? fallback;
|
||||||
|
for (let face = 1; face <= faces; face++) {
|
||||||
|
const next =
|
||||||
|
face < faces ? (mesh.faceMaterials.get(face) ?? fallback) : -1;
|
||||||
|
if (next === material) continue;
|
||||||
|
forwardDraws.push({
|
||||||
|
mesh,
|
||||||
|
geometry,
|
||||||
|
material,
|
||||||
|
firstIndex: geometry.indexRow * 4 + start * 3,
|
||||||
|
indices: (face - start) * 3,
|
||||||
|
});
|
||||||
|
start = face;
|
||||||
|
material = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forwardDraws.sort(
|
||||||
|
(left, right) =>
|
||||||
|
renderCategory(left.material) - renderCategory(right.material) ||
|
||||||
|
left.mesh.id - right.mesh.id,
|
||||||
|
);
|
||||||
|
const hasTransmission = forwardDraws.some(
|
||||||
|
(draw) => this.array("materialExtensions").row(draw.material)[0] > 0,
|
||||||
|
);
|
||||||
const compositeSceneColor =
|
const compositeSceneColor =
|
||||||
hasTransmission && this.#effects.size === 0 && this.#shaders.size === 0;
|
hasTransmission && this.#effects.size === 0 && this.#shaders.size === 0;
|
||||||
const hdrFormat = this.hdr ? "rgba16float" : "rgba8unorm";
|
const hdrFormat = this.hdr ? "rgba16float" : "rgba8unorm";
|
||||||
@@ -1190,7 +1363,7 @@ export class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let previous = computeIds.length ? computeIds : ["cluster-lights"];
|
let previous = computeIds.length ? computeIds : ["cluster-lights"];
|
||||||
if (!renderedMeshes.length) {
|
if (!forwardDraws.length) {
|
||||||
renderPipelines.push({
|
renderPipelines.push({
|
||||||
id: "empty-forward",
|
id: "empty-forward",
|
||||||
code: clearShader,
|
code: clearShader,
|
||||||
@@ -1238,74 +1411,29 @@ export class Scene {
|
|||||||
});
|
});
|
||||||
previous = ["scene-color-clear"];
|
previous = ["scene-color-clear"];
|
||||||
}
|
}
|
||||||
|
addBuffer({
|
||||||
|
id: "vertices",
|
||||||
|
array: "vertices",
|
||||||
|
usage: ["vertex"],
|
||||||
|
sync: "manual",
|
||||||
|
});
|
||||||
|
addBuffer({
|
||||||
|
id: "indices",
|
||||||
|
array: "indices",
|
||||||
|
usage: ["index"],
|
||||||
|
sync: "manual",
|
||||||
|
});
|
||||||
const forwardPipelines = new Map<string, string>();
|
const forwardPipelines = new Map<string, string>();
|
||||||
let firstColor = true;
|
let firstColor = true;
|
||||||
let firstDepth = true;
|
let firstDepth = true;
|
||||||
let sceneColorReady = false;
|
let sceneColorReady = false;
|
||||||
let forwardTarget = compositeSceneColor ? "scene-color" : "hdr";
|
let forwardTarget = compositeSceneColor ? "scene-color" : "hdr";
|
||||||
for (const mesh of renderedMeshes) {
|
for (const draw of forwardDraws) {
|
||||||
const vertex = `geometry-${mesh.geometryId}-positions`;
|
const { mesh, geometry, material } = draw;
|
||||||
addBuffer({
|
const id = `forward-${mesh.id}-${draw.firstIndex}`;
|
||||||
id: vertex,
|
const hasNormals = geometry.data.has("normals");
|
||||||
array: `geometry.${mesh.geometryId}.positions`,
|
const hasUvs = geometry.data.has("uvs");
|
||||||
usage: ["vertex"],
|
const hasTangents = geometry.data.has("tangents");
|
||||||
sync: "manual",
|
|
||||||
});
|
|
||||||
const normal = `geometry-${mesh.geometryId}-normals`;
|
|
||||||
const uv = `geometry-${mesh.geometryId}-uvs`;
|
|
||||||
const uv2 = `geometry-${mesh.geometryId}-uvs2`;
|
|
||||||
const tangent = `geometry-${mesh.geometryId}-tangents`;
|
|
||||||
const color = `geometry-${mesh.geometryId}-colors`;
|
|
||||||
const hasNormals = !!this.geometryData(mesh.geometryId, "normals");
|
|
||||||
const hasUvs = !!this.geometryData(mesh.geometryId, "uvs");
|
|
||||||
const hasUvs2 = !!this.geometryData(mesh.geometryId, "uvs2");
|
|
||||||
const hasTangents = !!this.geometryData(mesh.geometryId, "tangents");
|
|
||||||
const hasColors = !!this.geometryData(mesh.geometryId, "colors");
|
|
||||||
for (const [present, id, kind] of [
|
|
||||||
[hasNormals, normal, "normals"],
|
|
||||||
[hasUvs, uv, "uvs"],
|
|
||||||
[hasUvs2, uv2, "uvs2"],
|
|
||||||
[hasTangents, tangent, "tangents"],
|
|
||||||
[hasColors, color, "colors"],
|
|
||||||
] as const)
|
|
||||||
if (present)
|
|
||||||
addBuffer({
|
|
||||||
id,
|
|
||||||
array: `geometry.${mesh.geometryId}.${kind}`,
|
|
||||||
usage: ["vertex"],
|
|
||||||
sync: "manual",
|
|
||||||
});
|
|
||||||
const indexed = mesh.indexCount > 0;
|
|
||||||
if (indexed)
|
|
||||||
addBuffer({
|
|
||||||
id: `geometry-${mesh.geometryId}-indices`,
|
|
||||||
array: `geometry.${mesh.geometryId}.indices`,
|
|
||||||
usage: ["index"],
|
|
||||||
sync: "manual",
|
|
||||||
});
|
|
||||||
const draws =
|
|
||||||
indexed && mesh.faceMaterials.size
|
|
||||||
? Array.from(
|
|
||||||
{ length: Math.floor(mesh.indexCount / 3) },
|
|
||||||
(_, face) => ({
|
|
||||||
face,
|
|
||||||
count: 3,
|
|
||||||
firstIndex: face * 3,
|
|
||||||
material: mesh.faceMaterials.get(face),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
face: -1,
|
|
||||||
count: indexed ? mesh.indexCount : mesh.vertexCount,
|
|
||||||
firstIndex: 0,
|
|
||||||
material: undefined,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const draw of draws) {
|
|
||||||
const id = `forward-${mesh.id}-${draw.face}`;
|
|
||||||
const material =
|
|
||||||
draw.material ?? Number(this.array("meshInfo").row(mesh.id)[1]);
|
|
||||||
if (mesh.id > 65535 || material > 65534)
|
if (mesh.id > 65535 || material > 65534)
|
||||||
throw new RangeError(
|
throw new RangeError(
|
||||||
`Scene handle limit: mesh ${mesh.id}, material ${material}`,
|
`Scene handle limit: mesh ${mesh.id}, material ${material}`,
|
||||||
@@ -1318,7 +1446,6 @@ export class Scene {
|
|||||||
const materialTextures = Array.from({ length: 13 }, (_, lane) =>
|
const materialTextures = Array.from({ length: 13 }, (_, lane) =>
|
||||||
lane === 2 && !hasTangents ? undefined : texture(lane),
|
lane === 2 && !hasTangents ? undefined : texture(lane),
|
||||||
);
|
);
|
||||||
const normalTexture = materialTextures[2];
|
|
||||||
const detailed = hasNormals && hasUvs;
|
const detailed = hasNormals && hasUvs;
|
||||||
const flags = Math.round(this.array("materials").row(material)[11]);
|
const flags = Math.round(this.array("materials").row(material)[11]);
|
||||||
const extensionInfo = this.array("materialExtensions").row(material);
|
const extensionInfo = this.array("materialExtensions").row(material);
|
||||||
@@ -1337,11 +1464,6 @@ export class Scene {
|
|||||||
if (extensionInfo[20] <= 0) mask &= ~(1 << 10);
|
if (extensionInfo[20] <= 0) mask &= ~(1 << 10);
|
||||||
if (extensionInfo[11] <= 0) mask &= ~(1 << 4);
|
if (extensionInfo[11] <= 0) mask &= ~(1 << 4);
|
||||||
if ((flags & 8) !== 0 && !transmitted) mask &= (1 << 0) | (1 << 3);
|
if ((flags & 8) !== 0 && !transmitted) mask &= (1 << 0) | (1 << 3);
|
||||||
const tangentSlot = normalTexture ? 3 : -1;
|
|
||||||
const colorSlot = hasColors ? 3 + Number(!!normalTexture) : -1;
|
|
||||||
const uv2Slot = hasUvs2
|
|
||||||
? 3 + Number(!!normalTexture) + Number(hasColors)
|
|
||||||
: -1;
|
|
||||||
const textureInfo = this.array("materialTextureInfo").row(material);
|
const textureInfo = this.array("materialTextureInfo").row(material);
|
||||||
let clearCompositeLayer = false;
|
let clearCompositeLayer = false;
|
||||||
if (transmitted && !sceneColorReady) {
|
if (transmitted && !sceneColorReady) {
|
||||||
@@ -1375,24 +1497,14 @@ export class Scene {
|
|||||||
{ group: 0, binding: 0, resource: "hdr" },
|
{ group: 0, binding: 0, resource: "hdr" },
|
||||||
{ group: 0, binding: 1, resource: "linear" },
|
{ group: 0, binding: 1, resource: "linear" },
|
||||||
],
|
],
|
||||||
color: [
|
color: [{ resource: "scene-color", clear: [...this.clearColor] }],
|
||||||
{ resource: "scene-color", clear: [...this.clearColor] },
|
|
||||||
],
|
|
||||||
draw: { vertices: 3 },
|
draw: { vertices: 3 },
|
||||||
});
|
});
|
||||||
previous = ["copy-scene-color"];
|
previous = ["copy-scene-color"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const code = detailed
|
const code = detailed
|
||||||
? pbrShader(
|
? pbrShader(mask, flags, transmitted, textureInfo, extensionInfo)
|
||||||
mask,
|
|
||||||
flags,
|
|
||||||
hasColors,
|
|
||||||
hasUvs2,
|
|
||||||
transmitted,
|
|
||||||
textureInfo,
|
|
||||||
extensionInfo,
|
|
||||||
)
|
|
||||||
: basicForwardShader;
|
: basicForwardShader;
|
||||||
const pipelineKey = `${flags}-${transmitted ? 1 : 0}\n${code}`;
|
const pipelineKey = `${flags}-${transmitted ? 1 : 0}\n${code}`;
|
||||||
let pipeline = forwardPipelines.get(pipelineKey);
|
let pipeline = forwardPipelines.get(pipelineKey);
|
||||||
@@ -1404,91 +1516,12 @@ export class Scene {
|
|||||||
code,
|
code,
|
||||||
vertex: {
|
vertex: {
|
||||||
entry: "vertex",
|
entry: "vertex",
|
||||||
buffers: detailed
|
buffers: [
|
||||||
? [
|
|
||||||
{
|
{
|
||||||
arrayStride: 16,
|
arrayStride: 80,
|
||||||
attributes: [
|
attributes: detailed
|
||||||
{
|
? vertexAttributes
|
||||||
format: "float32x3",
|
: vertexAttributes.slice(0, 1),
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
format: "float32x3",
|
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
format: "float32x2",
|
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...(normalTexture
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
format: "float32x4",
|
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(hasColors
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
format: "float32x4",
|
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(hasUvs2
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
format: "float32x2",
|
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
format: "float32x3",
|
|
||||||
offset: 0,
|
|
||||||
shaderLocation: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -1607,45 +1640,21 @@ export class Scene {
|
|||||||
resource: "depth",
|
resource: "depth",
|
||||||
...(firstDepth ? { clear: 1 } : { load: "load" }),
|
...(firstDepth ? { clear: 1 } : { load: "load" }),
|
||||||
},
|
},
|
||||||
vertexBuffers: [
|
vertexBuffers: [{ slot: 0, resource: "vertices" }],
|
||||||
{ slot: 0, resource: vertex },
|
indexBuffer: { resource: "indices", format: "uint32" },
|
||||||
...(detailed
|
draw: {
|
||||||
? [
|
indices: draw.indices,
|
||||||
{ slot: 1, resource: normal },
|
|
||||||
{ slot: 2, resource: uv },
|
|
||||||
...(normalTexture
|
|
||||||
? [{ slot: tangentSlot, resource: tangent }]
|
|
||||||
: []),
|
|
||||||
...(hasColors
|
|
||||||
? [{ slot: colorSlot, resource: color }]
|
|
||||||
: []),
|
|
||||||
...(hasUvs2 ? [{ slot: uv2Slot, resource: uv2 }] : []),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
...(indexed
|
|
||||||
? {
|
|
||||||
indexBuffer: {
|
|
||||||
resource: `geometry-${mesh.geometryId}-indices`,
|
|
||||||
format: "uint32",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
draw: indexed
|
|
||||||
? {
|
|
||||||
indices: draw.count,
|
|
||||||
firstIndex: draw.firstIndex,
|
firstIndex: draw.firstIndex,
|
||||||
|
baseVertex: geometry.vertexRow,
|
||||||
instances: 1,
|
instances: 1,
|
||||||
firstInstance: instance,
|
firstInstance: instance,
|
||||||
}
|
},
|
||||||
: { vertices: draw.count, instances: 1, firstInstance: instance },
|
|
||||||
});
|
});
|
||||||
firstColor = false;
|
firstColor = false;
|
||||||
firstDepth = false;
|
firstDepth = false;
|
||||||
previous = [id];
|
previous = [id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const material of this.#shaders.values()) {
|
for (const material of this.#shaders.values()) {
|
||||||
const id = `shader-${material.id}`;
|
const id = `shader-${material.id}`;
|
||||||
|
|||||||
@@ -45,6 +45,20 @@ export async function importGltf(scene: Scene, url: string | URL) {
|
|||||||
await scene.reserve({
|
await scene.reserve({
|
||||||
nodes: result.primitives.length,
|
nodes: result.primitives.length,
|
||||||
materials: result.materials.length,
|
materials: result.materials.length,
|
||||||
|
vertices: result.primitives.reduce(
|
||||||
|
(total: number, primitive: any) => total + primitive.positions.length / 3,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
// Each geometry's index span is padded to whole rows of four.
|
||||||
|
indices: result.primitives.reduce(
|
||||||
|
(total: number, primitive: any) =>
|
||||||
|
total +
|
||||||
|
Math.ceil(
|
||||||
|
(primitive.indices?.length ?? primitive.positions.length / 3) / 4,
|
||||||
|
) *
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
let textures: Texture[] = [];
|
let textures: Texture[] = [];
|
||||||
const imported = await scene.batchGraphUpdates(async () => {
|
const imported = await scene.batchGraphUpdates(async () => {
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import { ArcRotateCamera } from "../src/camera/ArcRotateCamera";
|
|||||||
import { PointLight } from "../src/lights/PointLight";
|
import { PointLight } from "../src/lights/PointLight";
|
||||||
import { PBRMaterial } from "../src/materials/PBRMaterial";
|
import { PBRMaterial } from "../src/materials/PBRMaterial";
|
||||||
import type { Texture } from "../src/materials/Texture";
|
import type { Texture } from "../src/materials/Texture";
|
||||||
import { createScene, testCore, triangle } from "./scene-helpers";
|
import {
|
||||||
|
createScene,
|
||||||
|
forwardPasses,
|
||||||
|
latestGraph,
|
||||||
|
testCore,
|
||||||
|
triangle,
|
||||||
|
} from "./scene-helpers";
|
||||||
|
|
||||||
function close(actual: ArrayLike<number>, expected: ArrayLike<number>) {
|
function close(actual: ArrayLike<number>, expected: ArrayLike<number>) {
|
||||||
assert.equal(actual.length, expected.length);
|
assert.equal(actual.length, expected.length);
|
||||||
@@ -139,6 +145,13 @@ test("Mesh packs geometry and bounds, then clones with copy-on-write", async ()
|
|||||||
assert.equal(mesh.indexCount, 3);
|
assert.equal(mesh.indexCount, 3);
|
||||||
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
||||||
close(scene.array("bounds").row(mesh.id), [-1, -1, 0, 0, 1, 1, 0, 0]);
|
close(scene.array("bounds").row(mesh.id), [-1, -1, 0, 0, 1, 1, 0, 0]);
|
||||||
|
// Vertex 1 lives in the shared interleaved array: position, normal, uv, uv2, tangent, colour.
|
||||||
|
const geometry = scene.geometry(mesh.geometryId);
|
||||||
|
close(
|
||||||
|
scene.array("vertices").row(geometry.vertexRow + 1),
|
||||||
|
[1, -1, 0, 0, 0, 0, 1, 0, 1, 0, 0.9, 0.1, 1, 0, 0, 1, 0, 1, 0, 1],
|
||||||
|
);
|
||||||
|
close(scene.array("indices").row(geometry.indexRow), [0, 1, 2, 0]);
|
||||||
|
|
||||||
const clone = mesh.clone({ position: [2, 0, 0] });
|
const clone = mesh.clone({ position: [2, 0, 0] });
|
||||||
await clone.ready;
|
await clone.ready;
|
||||||
@@ -155,9 +168,81 @@ test("Mesh packs geometry and bounds, then clones with copy-on-write", async ()
|
|||||||
await clone.setMaterialForFaces(firstMaterial, 0);
|
await clone.setMaterialForFaces(firstMaterial, 0);
|
||||||
assert.equal(clone.materialId, secondMaterial.id);
|
assert.equal(clone.materialId, secondMaterial.id);
|
||||||
assert.equal(clone.faceMaterials.get(0), firstMaterial.id);
|
assert.equal(clone.faceMaterials.get(0), firstMaterial.id);
|
||||||
assert.equal(
|
});
|
||||||
scene.array(`mesh.${clone.id}.faceMaterials`).row(0)[0],
|
|
||||||
firstMaterial.id + 1,
|
test("geometry spans validate before mutating, move intact, and inhibit rendering until published", async () => {
|
||||||
|
const scene = await createScene();
|
||||||
|
const material = new PBRMaterial(scene);
|
||||||
|
await material.ready;
|
||||||
|
const first = new Mesh(scene, { material, vertexData: triangle });
|
||||||
|
const second = new Mesh(scene, { material, vertexData: triangle });
|
||||||
|
await Promise.all([first.ready, second.ready]);
|
||||||
|
const core = testCore(scene);
|
||||||
|
const before = {
|
||||||
|
graphs: core.graphs.length,
|
||||||
|
first: { ...scene.geometry(first.geometryId) },
|
||||||
|
second: { ...scene.geometry(second.geometryId) },
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
first.setVertexData("indices", [0, 1, 3]),
|
||||||
|
/VERTEX_DATA: indices\[2\]/u,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
first.setVertexData("indices", [0, 1, 1.5]),
|
||||||
|
/VERTEX_DATA: indices\[2\]/u,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
first.setVertexData("normals", [0, 0, 1]),
|
||||||
|
/VERTEX_DATA: normals/u,
|
||||||
|
);
|
||||||
|
assert.deepEqual({ ...scene.geometry(first.geometryId) }, before.first);
|
||||||
|
close(scene.geometryData(first.geometryId, "indices")!, [0, 1, 2]);
|
||||||
|
assert.equal(core.graphs.length, before.graphs);
|
||||||
|
|
||||||
|
// Hold the older loadout switch open while the first mesh grows past its span.
|
||||||
|
let releaseSwitch!: () => void;
|
||||||
|
const switching = new Promise<void>((resolve) => (releaseSwitch = resolve));
|
||||||
|
const switchLoadout = core.switchLoadout.bind(core);
|
||||||
|
core.switchLoadout = (id) => switching.then(() => switchLoadout(id));
|
||||||
|
const older = scene.updateRenderGraph();
|
||||||
|
const grown = first.setVertexData({
|
||||||
|
positions: [0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0],
|
||||||
|
normals: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
|
||||||
|
tangents: Array(16).fill(0),
|
||||||
|
uvs: [0, 0, 1, 0, 0, 1, 1, 1],
|
||||||
|
uvs2: [0, 0, 1, 0, 0, 1, 1, 1],
|
||||||
|
colors: Array(16).fill(1),
|
||||||
|
indices: [0, 1, 2, 2, 1, 3],
|
||||||
|
});
|
||||||
|
const signals = core.array("signals").row(0);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
assert.equal(signals[4], 1);
|
||||||
|
assert.equal(core.loadouts.length, before.graphs);
|
||||||
|
releaseSwitch();
|
||||||
|
await older;
|
||||||
|
assert.equal(signals[4], 1);
|
||||||
|
core.switchLoadout = switchLoadout;
|
||||||
|
await grown;
|
||||||
|
assert.equal(signals[4], 0);
|
||||||
|
assert.equal(signals[5], 1);
|
||||||
|
|
||||||
|
const moved = scene.geometry(first.geometryId);
|
||||||
|
assert.equal(moved.vertexCount, 4);
|
||||||
|
assert.notEqual(moved.vertexRow, before.first.vertexRow);
|
||||||
|
assert.deepEqual({ ...scene.geometry(second.geometryId) }, before.second);
|
||||||
|
close(
|
||||||
|
scene.array("vertices").row(before.second.vertexRow + 1),
|
||||||
|
[1, -1, 0, 0, 0, 0, 1, 0, 1, 0, 0.9, 0.1, 1, 0, 0, 1, 0, 1, 0, 1],
|
||||||
|
);
|
||||||
|
close(scene.array("indices").row(moved.indexRow), [0, 1, 2, 2]);
|
||||||
|
const draws = forwardPasses(latestGraph(scene)).map((pass: any) => pass.draw);
|
||||||
|
assert.deepEqual(
|
||||||
|
draws.map((draw: any) => [draw.baseVertex, draw.firstIndex, draw.indices]),
|
||||||
|
[
|
||||||
|
[moved.vertexRow, moved.indexRow * 4, 6],
|
||||||
|
[before.second.vertexRow, before.second.indexRow * 4, 3],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Vendored
+14
-5
@@ -87,7 +87,12 @@ declare module "@yawn/handles" {
|
|||||||
stride: number,
|
stride: number,
|
||||||
format: "f32" | "u32" | "i32",
|
format: "f32" | "u32" | "i32",
|
||||||
): Promise<SharedRows>;
|
): Promise<SharedRows>;
|
||||||
reserve(additional: { nodes?: number; materials?: number }): Promise<void>;
|
reserve(additional: {
|
||||||
|
nodes?: number;
|
||||||
|
materials?: number;
|
||||||
|
vertices?: number;
|
||||||
|
indices?: number;
|
||||||
|
}): Promise<void>;
|
||||||
batchWrites<T>(operation: () => T): T;
|
batchWrites<T>(operation: () => T): T;
|
||||||
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
|
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
|
||||||
compileRenderGraph(serialized: string): Promise<string>;
|
compileRenderGraph(serialized: string): Promise<string>;
|
||||||
@@ -149,18 +154,22 @@ declare module "@yawn/handles" {
|
|||||||
export interface MeshOptions extends NodeOptions {
|
export interface MeshOptions extends NodeOptions {
|
||||||
geometryId?: number;
|
geometryId?: number;
|
||||||
material?: PBRMaterial;
|
material?: PBRMaterial;
|
||||||
vertexData?: Partial<Record<VertexKind, ArrayLike<number>>>;
|
vertexData?: VertexStreams;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
}
|
}
|
||||||
|
export type VertexStreams = Partial<Record<VertexKind, ArrayLike<number>>>;
|
||||||
export class Mesh extends Node {
|
export class Mesh extends Node {
|
||||||
constructor(scene: Scene, options?: MeshOptions);
|
constructor(scene: Scene, options?: MeshOptions);
|
||||||
geometryId: number;
|
geometryId: number;
|
||||||
vertexCount: number;
|
readonly vertexCount: number;
|
||||||
indexCount: number;
|
readonly indexCount: number;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
material: PBRMaterial;
|
material: PBRMaterial;
|
||||||
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
|
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
|
||||||
setVertexData(kind: VertexKind, data: ArrayLike<number>): Promise<this>;
|
setVertexData(
|
||||||
|
kind: VertexKind | VertexStreams,
|
||||||
|
data?: ArrayLike<number>,
|
||||||
|
): Promise<this>;
|
||||||
setMaterialForFaces(
|
setMaterialForFaces(
|
||||||
material: PBRMaterial,
|
material: PBRMaterial,
|
||||||
faces: number | number[],
|
faces: number | number[],
|
||||||
|
|||||||
Reference in New Issue
Block a user