Add Chromium-only Blender WebEngine parity work
This commit is contained in:
233
web/protocol/nonmesh-export.ts
Normal file
233
web/protocol/nonmesh-export.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import type { DepsgraphEvaluationIR, DepsgraphNonMeshGeometryIR } from "./depsgraph";
|
||||
import type { MeshSummaryIR, NonMeshDataIR, SceneSnapshotIR } from "./scene-ir";
|
||||
import type { MeshGeometryBuffer } from "./web-engine";
|
||||
import { reassembleNonMeshGeometry, type NonMeshGeometryChunk, type ReassembledNonMeshGeometry } from "./nonmesh-binary";
|
||||
|
||||
export type NonMeshExportStatus = "MAPPED" | "LOSSY" | "BLOCKED";
|
||||
|
||||
export interface NonMeshExportMapping {
|
||||
dataId: string;
|
||||
type: NonMeshDataIR["type"];
|
||||
glbTarget: "MESH" | "NONE";
|
||||
usdTarget: "UsdGeomMesh" | "UsdGeomPoints" | "UsdGeomBasisCurves" | "OpenVDBAsset" | "NONE";
|
||||
status: NonMeshExportStatus;
|
||||
losses: string[];
|
||||
errorCode?: "NON_MESH_EVALUATION_REQUIRED" | "GLB_NON_MESH_UNMAPPED" | "GLB_VOLUME_UNSUPPORTED" | "NON_MESH_RESOURCE_MISSING";
|
||||
}
|
||||
|
||||
export interface NonMeshExportReport {
|
||||
schemaVersion: 1;
|
||||
mappings: NonMeshExportMapping[];
|
||||
canExportGLB: boolean;
|
||||
canExportUSD: boolean;
|
||||
}
|
||||
|
||||
export interface EvaluatedNonMeshExportScene {
|
||||
snapshot: SceneSnapshotIR;
|
||||
geometryBuffers: MeshGeometryBuffer[];
|
||||
report: NonMeshExportReport;
|
||||
}
|
||||
|
||||
export interface BinaryNonMeshExportScene {
|
||||
snapshot: SceneSnapshotIR;
|
||||
geometryBuffers: MeshGeometryBuffer[];
|
||||
geometryByMeshId: Map<string, ReassembledNonMeshGeometry>;
|
||||
losses: Array<{ dataId: string; message: string }>;
|
||||
}
|
||||
|
||||
function evaluatedByData(depsgraph?: DepsgraphEvaluationIR): Map<string, DepsgraphNonMeshGeometryIR[]> {
|
||||
const result = new Map<string, DepsgraphNonMeshGeometryIR[]>();
|
||||
for (const geometry of depsgraph?.nonMeshGeometries ?? []) {
|
||||
const list = result.get(geometry.sourceDataId) ?? [];
|
||||
list.push(geometry);
|
||||
result.set(geometry.sourceDataId, list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function hasBinaryGeometry(data: NonMeshDataIR): boolean {
|
||||
return data.geometryStatus === "binary" && Boolean(data.geometryBufferId);
|
||||
}
|
||||
|
||||
function mappingFor(data: NonMeshDataIR, evaluations: readonly DepsgraphNonMeshGeometryIR[]): NonMeshExportMapping {
|
||||
const evaluated = evaluations.filter((geometry) => geometry.status === "EVALUATED");
|
||||
if (["CURVE", "SURFACE", "FONT", "METABALL"].includes(data.type)) {
|
||||
const hasTriangles = evaluated.some((geometry) => geometry.triangleCount > 0);
|
||||
const hasEdges = evaluated.some((geometry) => geometry.edgeCount > 0);
|
||||
if (hasTriangles) return { dataId: data.id, type: data.type, glbTarget: "MESH", usdTarget: "UsdGeomMesh", status: "LOSSY", losses: ["source control topology is baked to the evaluated triangle mesh"] };
|
||||
if (hasEdges) return { dataId: data.id, type: data.type, glbTarget: "MESH", usdTarget: "UsdGeomBasisCurves", status: "LOSSY", losses: ["source control topology is baked to evaluated line segments", "surface faces and curve width are not represented"] };
|
||||
return { dataId: data.id, type: data.type, glbTarget: "NONE", usdTarget: "NONE", status: "BLOCKED", losses: [], errorCode: evaluated.length > 0 ? "GLB_NON_MESH_UNMAPPED" : "NON_MESH_EVALUATION_REQUIRED" };
|
||||
}
|
||||
if (data.type === "POINT_CLOUD") {
|
||||
return hasBinaryGeometry(data)
|
||||
? { dataId: data.id, type: data.type, glbTarget: "MESH", usdTarget: "UsdGeomPoints", status: "LOSSY", losses: ["GLB uses a POINTS primitive and omits radius and arbitrary WNM attributes"] }
|
||||
: { dataId: data.id, type: data.type, glbTarget: "NONE", usdTarget: "NONE", status: "BLOCKED", losses: [], errorCode: "NON_MESH_EVALUATION_REQUIRED" };
|
||||
}
|
||||
if (data.type === "CURVES" || data.type === "HAIR") {
|
||||
return hasBinaryGeometry(data)
|
||||
? { dataId: data.id, type: data.type, glbTarget: "MESH", usdTarget: "UsdGeomBasisCurves", status: "LOSSY", losses: ["GLB uses line segments and omits radius and arbitrary WNM attributes", "USD maps typed WNM attributes to primvars but Blender-only semantics may be lost"] }
|
||||
: { dataId: data.id, type: data.type, glbTarget: "NONE", usdTarget: "NONE", status: "BLOCKED", losses: [], errorCode: "NON_MESH_EVALUATION_REQUIRED" };
|
||||
}
|
||||
const volumeReady = data.type === "VOLUME" && data.resourceKind === "OPENVDB" && Boolean(data.sourcePath) && (data.volumeGrids?.length ?? 0) > 0;
|
||||
return volumeReady
|
||||
? { dataId: data.id, type: data.type, glbTarget: "NONE", usdTarget: "OpenVDBAsset", status: "LOSSY", losses: ["GLB cannot represent OpenVDB volumes"], errorCode: "GLB_VOLUME_UNSUPPORTED" }
|
||||
: { dataId: data.id, type: data.type, glbTarget: "NONE", usdTarget: "NONE", status: "BLOCKED", losses: [], errorCode: "NON_MESH_RESOURCE_MISSING" };
|
||||
}
|
||||
|
||||
export function analyzeNonMeshExport(snapshot: SceneSnapshotIR, depsgraph?: DepsgraphEvaluationIR): NonMeshExportReport {
|
||||
const byData = evaluatedByData(depsgraph);
|
||||
const mappings = (snapshot.nonMeshData ?? []).map((data) => mappingFor(data, byData.get(data.id) ?? []));
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
mappings,
|
||||
canExportGLB: mappings.every((mapping) => mapping.glbTarget !== "NONE"),
|
||||
canExportUSD: mappings.every((mapping) => mapping.usdTarget !== "NONE"),
|
||||
};
|
||||
}
|
||||
|
||||
function geometryBuffer(evaluation: DepsgraphNonMeshGeometryIR): MeshGeometryBuffer {
|
||||
const positions = Float32Array.from(evaluation.positions ?? []).buffer;
|
||||
const indices = Uint32Array.from(evaluation.indices ?? []).buffer;
|
||||
const edgeVertexIndices = evaluation.edgeVertexIndices?.length ? Uint32Array.from(evaluation.edgeVertexIndices).buffer : undefined;
|
||||
const normals = evaluation.normals?.length ? Float32Array.from(evaluation.normals).buffer : undefined;
|
||||
const uvs = evaluation.uvs?.length ? Float32Array.from(evaluation.uvs).buffer : undefined;
|
||||
const triangleMaterialIndices = evaluation.triangleMaterialIndices?.length ? Uint32Array.from(evaluation.triangleMaterialIndices).buffer : undefined;
|
||||
const triangleFaceIndices = evaluation.sourceElementIndices?.length ? Uint32Array.from(evaluation.sourceElementIndices).buffer : undefined;
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
meshId: evaluation.meshId,
|
||||
byteLength: positions.byteLength + indices.byteLength + (edgeVertexIndices?.byteLength ?? 0) + (normals?.byteLength ?? 0) + (uvs?.byteLength ?? 0) + (triangleMaterialIndices?.byteLength ?? 0) + (triangleFaceIndices?.byteLength ?? 0),
|
||||
positions,
|
||||
indices,
|
||||
...(edgeVertexIndices ? { edgeVertexIndices } : {}),
|
||||
normals,
|
||||
uvs,
|
||||
...(triangleMaterialIndices ? { triangleMaterialIndices } : {}),
|
||||
...(triangleFaceIndices ? { triangleFaceIndices } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function meshSummary(evaluation: DepsgraphNonMeshGeometryIR, source?: NonMeshDataIR): MeshSummaryIR {
|
||||
const topology = evaluation.triangleCount > 0 ? "triangles" : "lines";
|
||||
return {
|
||||
id: evaluation.meshId,
|
||||
name: source?.name ?? evaluation.meshId,
|
||||
vertexCount: evaluation.vertexCount,
|
||||
edgeCount: evaluation.edgeCount,
|
||||
faceCount: evaluation.triangleCount,
|
||||
cornerCount: evaluation.triangleCount * 3,
|
||||
triangleCount: evaluation.triangleCount,
|
||||
geometryStatus: "binary",
|
||||
geometryBufferId: evaluation.meshId,
|
||||
topology,
|
||||
materialSlotIds: evaluation.materialSlotIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapEvaluatedNonMeshForExport(
|
||||
snapshot: SceneSnapshotIR,
|
||||
geometryBuffers: readonly MeshGeometryBuffer[],
|
||||
depsgraph: DepsgraphEvaluationIR,
|
||||
): EvaluatedNonMeshExportScene {
|
||||
const sourceById = new Map((snapshot.nonMeshData ?? []).map((data) => [data.id, data]));
|
||||
const successful = (depsgraph.nonMeshGeometries ?? []).filter((geometry) => geometry.status === "EVALUATED" && sourceById.has(geometry.sourceDataId));
|
||||
const byObject = new Map(successful.map((geometry) => [geometry.objectId, geometry]));
|
||||
const evaluatedBySource = evaluatedByData(depsgraph);
|
||||
const nonMeshData = (snapshot.nonMeshData ?? []).map((data): NonMeshDataIR => ({
|
||||
...data,
|
||||
evaluatedGeometry: (evaluatedBySource.get(data.id) ?? []).map((geometry) => ({
|
||||
objectId: geometry.objectId,
|
||||
meshId: geometry.meshId,
|
||||
vertexCount: geometry.vertexCount,
|
||||
edgeCount: geometry.edgeCount,
|
||||
triangleCount: geometry.triangleCount,
|
||||
status: geometry.status,
|
||||
...(geometry.errorCode ? { errorCode: geometry.errorCode } : {}),
|
||||
})),
|
||||
}));
|
||||
return {
|
||||
snapshot: {
|
||||
...snapshot,
|
||||
nodes: snapshot.nodes.map((node) => {
|
||||
const evaluation = byObject.get(node.id);
|
||||
return evaluation ? { ...node, dataId: evaluation.meshId } : node;
|
||||
}),
|
||||
meshes: [...snapshot.meshes, ...successful.map((geometry) => meshSummary(geometry, sourceById.get(geometry.sourceDataId)))],
|
||||
nonMeshData,
|
||||
},
|
||||
geometryBuffers: [...geometryBuffers, ...successful.map(geometryBuffer)],
|
||||
report: analyzeNonMeshExport(snapshot, depsgraph),
|
||||
};
|
||||
}
|
||||
|
||||
function lineEdges(offsets: Uint32Array): Uint32Array {
|
||||
let edgeCount = 0;
|
||||
for (let curve = 0; curve < offsets.length - 1; curve++) edgeCount += Math.max(0, offsets[curve + 1] - offsets[curve] - 1);
|
||||
const edges = new Uint32Array(edgeCount * 2);
|
||||
let cursor = 0;
|
||||
for (let curve = 0; curve < offsets.length - 1; curve++) {
|
||||
for (let point = offsets[curve]; point + 1 < offsets[curve + 1]; point++) {
|
||||
edges[cursor++] = point;
|
||||
edges[cursor++] = point + 1;
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
export function mapBinaryNonMeshForExport(
|
||||
snapshot: SceneSnapshotIR,
|
||||
geometryBuffers: readonly MeshGeometryBuffer[],
|
||||
chunks: readonly NonMeshGeometryChunk[],
|
||||
): BinaryNonMeshExportScene {
|
||||
const mappedData = new Map<string, { mesh: MeshSummaryIR; buffer: MeshGeometryBuffer; geometry: ReassembledNonMeshGeometry }>();
|
||||
const losses: BinaryNonMeshExportScene["losses"] = [];
|
||||
for (const data of snapshot.nonMeshData ?? []) {
|
||||
if (!["POINT_CLOUD", "CURVES", "HAIR"].includes(data.type) || data.geometryStatus !== "binary") continue;
|
||||
const geometry = reassembleNonMeshGeometry(data.id, chunks);
|
||||
if (geometry.positions.length !== data.pointCount * 3) throw new Error(`NON_MESH_BINARY_INVALID: ${data.id} point count does not match SceneIR`);
|
||||
const meshId = `wnm:${data.id}`;
|
||||
const pointTopology = data.type === "POINT_CLOUD";
|
||||
const offsets = geometry.curveOffsets;
|
||||
if (!pointTopology && !offsets) throw new Error(`NON_MESH_BINARY_INVALID: ${data.id} has no curve offsets`);
|
||||
const indices = pointTopology ? Uint32Array.from({ length: data.pointCount }, (_, index) => index) : lineEdges(offsets!);
|
||||
if (!pointTopology && indices.length === 0) throw new Error(`NON_MESH_BINARY_INVALID: ${data.id} contains no exportable curve segments`);
|
||||
const positions = geometry.positions.slice().buffer;
|
||||
const indexBuffer = indices.buffer.slice(indices.byteOffset, indices.byteOffset + indices.byteLength) as ArrayBuffer;
|
||||
const buffer: MeshGeometryBuffer = {
|
||||
schemaVersion: 1,
|
||||
meshId,
|
||||
byteLength: positions.byteLength + indexBuffer.byteLength,
|
||||
positions,
|
||||
indices: pointTopology ? indexBuffer : new ArrayBuffer(0),
|
||||
...(pointTopology ? {} : { edgeVertexIndices: indexBuffer }),
|
||||
};
|
||||
const mesh: MeshSummaryIR = {
|
||||
id: meshId,
|
||||
name: data.name,
|
||||
vertexCount: data.pointCount,
|
||||
edgeCount: pointTopology ? 0 : indices.length / 2,
|
||||
faceCount: 0,
|
||||
cornerCount: 0,
|
||||
triangleCount: 0,
|
||||
geometryStatus: "binary",
|
||||
geometryBufferId: meshId,
|
||||
topology: pointTopology ? "points" : "lines",
|
||||
};
|
||||
mappedData.set(data.id, { mesh, buffer, geometry });
|
||||
const lost = [geometry.radii ? "radius" : "", ...geometry.attributes.map((attribute) => attribute.name)].filter(Boolean);
|
||||
if (lost.length > 0) losses.push({ dataId: data.id, message: `GLB primitive preserves topology but omits WNM attributes: ${lost.join(", ")}` });
|
||||
}
|
||||
const geometryByMeshId = new Map<string, ReassembledNonMeshGeometry>();
|
||||
for (const { mesh, geometry } of mappedData.values()) geometryByMeshId.set(mesh.id, geometry);
|
||||
return {
|
||||
snapshot: {
|
||||
...snapshot,
|
||||
nodes: snapshot.nodes.map((node) => mappedData.has(node.dataId ?? "") ? { ...node, dataId: mappedData.get(node.dataId!)!.mesh.id } : node),
|
||||
meshes: [...snapshot.meshes, ...[...mappedData.values()].map((entry) => entry.mesh)],
|
||||
nonMeshData: (snapshot.nonMeshData ?? []).filter((data) => !mappedData.has(data.id)),
|
||||
},
|
||||
geometryBuffers: [...geometryBuffers, ...[...mappedData.values()].map((entry) => entry.buffer)],
|
||||
geometryByMeshId,
|
||||
losses,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user