Add Chromium-only Blender WebEngine parity work
This commit is contained in:
138
web/protocol/mesh-cache.ts
Normal file
138
web/protocol/mesh-cache.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user