139 lines
6.1 KiB
TypeScript
139 lines
6.1 KiB
TypeScript
import type { MeshGeometryBuffer, WebEngineLODLevelResult } from "./web-engine";
|
|
|
|
const bufferFields = ["positions", "indices", "normals", "triangleCornerIndices", "uvs", "colors", "triangleMaterialIndices"] as const;
|
|
type BufferField = typeof bufferFields[number];
|
|
|
|
interface BufferDescriptor {
|
|
field: BufferField;
|
|
offset: number;
|
|
byteLength: number;
|
|
}
|
|
|
|
interface GeometryDescriptor {
|
|
meshId: string;
|
|
byteLength: number;
|
|
buffers: BufferDescriptor[];
|
|
}
|
|
|
|
interface LevelDescriptor {
|
|
level: number;
|
|
meshId: string;
|
|
triangleBudget: number;
|
|
outputTriangleCount: number;
|
|
outputVertexCount: number;
|
|
geometry: GeometryDescriptor[];
|
|
}
|
|
|
|
interface CacheHeader {
|
|
schemaVersion: 1;
|
|
levels: LevelDescriptor[];
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
export function encodeLODGeometry(levels: readonly WebEngineLODLevelResult[]): ArrayBuffer {
|
|
if (levels.length === 0) throw new Error("LOD geometry cache requires at least one level");
|
|
const chunks: ArrayBuffer[] = [];
|
|
let offset = 0;
|
|
const header: CacheHeader = {
|
|
schemaVersion: 1,
|
|
levels: levels.map((level, expectedLevel) => {
|
|
if (level.level !== expectedLevel || level.geometryBuffers.length === 0) throw new Error("LOD cache levels must be contiguous and contain geometry");
|
|
return {
|
|
level: level.level,
|
|
meshId: level.meshId,
|
|
triangleBudget: level.triangleBudget,
|
|
outputTriangleCount: level.outputTriangleCount,
|
|
outputVertexCount: level.outputVertexCount,
|
|
geometry: level.geometryBuffers.map((geometry) => {
|
|
const descriptors: BufferDescriptor[] = [];
|
|
for (const field of bufferFields) {
|
|
const value = geometry[field];
|
|
if (!(value instanceof ArrayBuffer)) continue;
|
|
descriptors.push({ field, offset, byteLength: value.byteLength });
|
|
chunks.push(value);
|
|
offset += value.byteLength;
|
|
}
|
|
if (!descriptors.some((descriptor) => descriptor.field === "positions") || !descriptors.some((descriptor) => descriptor.field === "indices")) {
|
|
throw new Error("LOD cache geometry requires positions and indices");
|
|
}
|
|
return { meshId: geometry.meshId, byteLength: geometry.byteLength, buffers: descriptors };
|
|
}),
|
|
};
|
|
}),
|
|
};
|
|
const headerBytes = new TextEncoder().encode(JSON.stringify(header));
|
|
const output = new ArrayBuffer(4 + headerBytes.byteLength + offset);
|
|
const view = new DataView(output);
|
|
view.setUint32(0, headerBytes.byteLength, true);
|
|
const bytes = new Uint8Array(output);
|
|
bytes.set(headerBytes, 4);
|
|
let writeOffset = 4 + headerBytes.byteLength;
|
|
for (const chunk of chunks) {
|
|
bytes.set(new Uint8Array(chunk), writeOffset);
|
|
writeOffset += chunk.byteLength;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
export function decodeLODGeometry(data: ArrayBuffer): WebEngineLODLevelResult[] {
|
|
if (data.byteLength < 5) throw new Error("LOD geometry cache is truncated");
|
|
const headerLength = new DataView(data).getUint32(0, true);
|
|
if (headerLength === 0 || headerLength > data.byteLength - 4 || headerLength > 16 * 1024 * 1024) throw new Error("LOD geometry cache header is invalid");
|
|
const parsed = JSON.parse(new TextDecoder().decode(new Uint8Array(data, 4, headerLength))) as unknown;
|
|
if (!isRecord(parsed) || parsed.schemaVersion !== 1 || !Array.isArray(parsed.levels) || parsed.levels.length === 0) {
|
|
throw new Error("LOD geometry cache schema is invalid");
|
|
}
|
|
const payloadOffset = 4 + headerLength;
|
|
return parsed.levels.map((rawLevel, expectedLevel) => {
|
|
if (!isRecord(rawLevel) || rawLevel.level !== expectedLevel || typeof rawLevel.meshId !== "string" || !Array.isArray(rawLevel.geometry)) {
|
|
throw new Error("LOD geometry cache level is invalid");
|
|
}
|
|
const geometryBuffers = rawLevel.geometry.map((rawGeometry): MeshGeometryBuffer => {
|
|
if (!isRecord(rawGeometry) || typeof rawGeometry.meshId !== "string" || !Array.isArray(rawGeometry.buffers)) {
|
|
throw new Error("LOD geometry cache entry is invalid");
|
|
}
|
|
const decoded = new Map<BufferField, ArrayBuffer>();
|
|
for (const rawBuffer of rawGeometry.buffers) {
|
|
if (!isRecord(rawBuffer) || !bufferFields.includes(rawBuffer.field as BufferField) || !Number.isSafeInteger(rawBuffer.offset) || !Number.isSafeInteger(rawBuffer.byteLength)) {
|
|
throw new Error("LOD geometry cache buffer descriptor is invalid");
|
|
}
|
|
const offset = rawBuffer.offset as number;
|
|
const byteLength = rawBuffer.byteLength as number;
|
|
if (offset < 0 || byteLength < 0 || payloadOffset + offset + byteLength > data.byteLength || decoded.has(rawBuffer.field as BufferField)) {
|
|
throw new Error("LOD geometry cache buffer range is invalid");
|
|
}
|
|
decoded.set(rawBuffer.field as BufferField, data.slice(payloadOffset + offset, payloadOffset + offset + byteLength));
|
|
}
|
|
const positions = decoded.get("positions");
|
|
const indices = decoded.get("indices");
|
|
if (!positions || !indices) throw new Error("LOD geometry cache is missing topology buffers");
|
|
return {
|
|
schemaVersion: 1,
|
|
meshId: rawGeometry.meshId,
|
|
byteLength: typeof rawGeometry.byteLength === "number" ? rawGeometry.byteLength : [...decoded.values()].reduce((sum, value) => sum + value.byteLength, 0),
|
|
positions,
|
|
indices,
|
|
normals: decoded.get("normals"),
|
|
triangleCornerIndices: decoded.get("triangleCornerIndices"),
|
|
uvs: decoded.get("uvs"),
|
|
colors: decoded.get("colors"),
|
|
triangleMaterialIndices: decoded.get("triangleMaterialIndices"),
|
|
};
|
|
});
|
|
if (typeof rawLevel.triangleBudget !== "number" || typeof rawLevel.outputTriangleCount !== "number" || typeof rawLevel.outputVertexCount !== "number") {
|
|
throw new Error("LOD geometry cache statistics are invalid");
|
|
}
|
|
return {
|
|
level: expectedLevel,
|
|
meshId: rawLevel.meshId,
|
|
triangleBudget: rawLevel.triangleBudget,
|
|
outputTriangleCount: rawLevel.outputTriangleCount,
|
|
outputVertexCount: rawLevel.outputVertexCount,
|
|
geometryBuffers,
|
|
};
|
|
});
|
|
}
|