chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ export type GLBWarningCode =
|
||||
| "GLB_NON_MESH_UNMAPPED"
|
||||
| "GLB_VOLUME_UNSUPPORTED"
|
||||
| "NON_MESH_ATTRIBUTE_LOSS"
|
||||
| "MESH_ATTRIBUTE_LOSS"
|
||||
| "MESH_NORMAL_MAPPING_INVALID"
|
||||
| "NO_EXPORTABLE_GEOMETRY";
|
||||
|
||||
export interface GLBExportWarning {
|
||||
@@ -141,8 +143,9 @@ function matrixInverse(matrix: readonly number[]): number[] {
|
||||
}
|
||||
|
||||
function convertMatrix(matrix: readonly number[]): number[] {
|
||||
const basis = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 1];
|
||||
return matrixMultiply(matrixMultiply(basis, Array.from(matrix)), basis);
|
||||
const basis = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1];
|
||||
const inverse = [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1];
|
||||
return matrixMultiply(matrixMultiply(basis, Array.from(matrix)), inverse);
|
||||
}
|
||||
|
||||
function convertPosition(values: ArrayLike<number>): Float32Array {
|
||||
@@ -180,7 +183,7 @@ function convertQuaternion(values: readonly number[], source: "EULER" | "QUATERN
|
||||
return [blender[0], blender[2], -blender[1], blender[3]];
|
||||
}
|
||||
|
||||
function geometryArray<T extends Float32Array | Uint32Array>(payload: MeshGeometryBuffer | undefined, summary: MeshSummaryIR, key: "positions" | "indices" | "normals" | "uvs" | "colors" | "triangleMaterialIndices"): T | undefined {
|
||||
function geometryArray<T extends Float32Array | Uint32Array>(payload: MeshGeometryBuffer | undefined, summary: MeshSummaryIR, key: "positions" | "indices" | "normals" | "splitNormals" | "uvs" | "colors" | "triangleMaterialIndices"): T | undefined {
|
||||
if (payload) {
|
||||
const data = payload[key];
|
||||
if (data) return new (key === "indices" || key === "triangleMaterialIndices" ? Uint32Array : Float32Array)(data) as T;
|
||||
@@ -317,6 +320,7 @@ function materialJSON(material: MaterialIR, textureIndexByImageId: ReadonlyMap<s
|
||||
|
||||
function meshWarnings(snapshot: SceneSnapshotIR, geometryBuffers: readonly MeshGeometryBuffer[], assetBuffers: readonly GLBAssetBuffer[]): GLBExportWarning[] {
|
||||
const warnings: GLBExportWarning[] = [];
|
||||
for (const node of snapshot.nodes) if (node.type === 'CAMERA' || node.type === 'LIGHT') warnings.push({ code: 'GLB_NON_MESH_UNMAPPED', severity: 'warning', id: node.id, message: `${node.name}: ${node.type} data is exported as an empty hierarchy node; keep .blend to retain camera and light settings` });
|
||||
const bufferIds = new Set(geometryBuffers.map((geometry) => geometry.meshId));
|
||||
const assetIds = new Set(assetBuffers.map((asset) => asset.assetId));
|
||||
let exportableGeometry = 0;
|
||||
@@ -324,7 +328,15 @@ function meshWarnings(snapshot: SceneSnapshotIR, geometryBuffers: readonly MeshG
|
||||
if (mesh.geometryStatus === "summary-only") warnings.push({ code: "SUMMARY_ONLY_MESH", severity: "error", message: `Mesh ${mesh.name} has summary-only geometry`, id: mesh.id });
|
||||
const geometryId = mesh.geometryBufferId ?? mesh.id;
|
||||
if (mesh.geometryStatus === "binary" && !bufferIds.has(geometryId)) warnings.push({ code: "MISSING_GEOMETRY_BUFFER", severity: "error", message: `Mesh ${mesh.name} has no transferable geometry buffer`, id: mesh.id });
|
||||
const normalPayload = geometryBuffers.find(buffer => buffer.meshId === geometryId), split = geometryArray<Float32Array>(normalPayload, mesh, 'splitNormals');
|
||||
const corners = normalPayload?.triangleCornerIndices ? new Uint32Array(normalPayload.triangleCornerIndices) : mesh.triangleCornerIndices;
|
||||
if (split && (!corners || corners.length !== (mesh.triangleCount ?? 0) * 3 || split.length % 3 !== 0 || split.some(value => !Number.isFinite(value)) || Array.from(corners).some(corner => !Number.isInteger(corner) || corner < 0 || corner * 3 + 2 >= split.length))) warnings.push({code:'MESH_NORMAL_MAPPING_INVALID',severity:'error',id:mesh.id,message:`Mesh ${mesh.name} has invalid corner-normal mapping`});
|
||||
if (mesh.geometryStatus === "available" || bufferIds.has(geometryId)) exportableGeometry++;
|
||||
const mappedAttributes = new Set(['position', 'material_index', 'sharp_face', 'sharp_edge', 'custom_normal', mesh.activeUVMap ?? mesh.uvLayers?.[0]?.name ?? 'UVMap']);
|
||||
const colorLayers = (mesh.attributes ?? []).filter(attribute => ['FLOAT_COLOR', 'BYTE_COLOR'].includes(attribute.dataType));
|
||||
if (colorLayers.length === 1 && geometryArray<Float32Array>(geometryBuffers.find(buffer => buffer.meshId === geometryId), mesh, 'colors')) mappedAttributes.add(colorLayers[0].name);
|
||||
const extraAttributes = (mesh.attributes ?? []).filter(attribute => !attribute.name.startsWith('.') && !mappedAttributes.has(attribute.name));
|
||||
if (extraAttributes.length) warnings.push({ code: 'MESH_ATTRIBUTE_LOSS', severity: 'warning', id: mesh.id, message: `Mesh ${mesh.name}: GLB exports standard vertex channels; keep .blend to retain named attribute layers ${extraAttributes.map(attribute => attribute.name).join(', ')}` });
|
||||
if (mesh.modifierStack?.some((modifier) => modifier.enabled)) warnings.push({ code: "MODIFIER_STACK_NOT_BAKED", severity: "warning", message: `Mesh ${mesh.name} has enabled modifiers that are not baked for export`, id: mesh.id });
|
||||
if (mesh.skinWeights) {
|
||||
const armature = mesh.skinWeights.armatureId ? snapshot.armatures?.find((candidate) => candidate.id === mesh.skinWeights?.armatureId) : undefined;
|
||||
@@ -431,15 +443,18 @@ function buildGLB(snapshot: SceneSnapshotIR, geometryBuffers: readonly MeshGeome
|
||||
if (indicesSource.some((index) => index >= positionsSource.length / 3)) continue;
|
||||
const normalsSource = linePrimitive || pointPrimitive ? undefined : geometryArray<Float32Array>(payload, summary, "normals");
|
||||
const cornerSource = payload?.triangleCornerIndices ? new Uint32Array(payload.triangleCornerIndices) : summary.triangleCornerIndices ? new Uint32Array(summary.triangleCornerIndices) : undefined;
|
||||
const splitNormals = linePrimitive || pointPrimitive ? undefined : geometryArray<Float32Array>(payload, summary, "splitNormals");
|
||||
const hasSplitNormals = Boolean(splitNormals && cornerSource && cornerSource.length === indicesSource.length && cornerSource.every(corner => corner * 3 + 2 < splitNormals.length));
|
||||
const exportNormals = hasSplitNormals ? splitNormals : normalsSource;
|
||||
const uvsSource = geometryArray<Float32Array>(payload, summary, "uvs");
|
||||
const colorsSource = geometryArray<Float32Array>(payload, summary, "colors");
|
||||
const materialSource = linePrimitive || pointPrimitive ? undefined : geometryArray<Uint32Array>(payload, summary, "triangleMaterialIndices");
|
||||
const vertexCount = positionsSource.length / 3;
|
||||
const hasCornerUVs = Boolean(uvsSource && uvsSource.length === indicesSource.length * 2 && uvsSource.length !== vertexCount * 2);
|
||||
const hasCornerColors = Boolean(colorsSource && colorsSource.length === indicesSource.length * 4 && colorsSource.length !== vertexCount * 4);
|
||||
const deindexed = Boolean((cornerSource && (uvsSource || colorsSource)) || hasCornerUVs || hasCornerColors);
|
||||
const deindexed = Boolean(hasSplitNormals || (cornerSource && (uvsSource || colorsSource)) || hasCornerUVs || hasCornerColors);
|
||||
const positions = deindexed ? new Float32Array(indicesSource.length * 3) : convertPosition(positionsSource);
|
||||
const normals = normalsSource ? (deindexed ? new Float32Array(indicesSource.length * 3) : convertNormal(normalsSource)) : undefined;
|
||||
const normals = exportNormals ? (deindexed ? new Float32Array(indicesSource.length * 3) : convertNormal(exportNormals)) : undefined;
|
||||
const uvs = uvsSource ? new Float32Array((deindexed ? indicesSource.length : positionsSource.length / 3) * 2) : undefined;
|
||||
const colors = colorsSource ? new Float32Array((deindexed ? indicesSource.length : positionsSource.length / 3) * 4) : undefined;
|
||||
const hasSkin = Boolean(summary.skinWeights && summary.skinWeights.indices.length === vertexCount * 4 && summary.skinWeights.weights.length === vertexCount * 4 && summary.skinWeights.armatureId);
|
||||
@@ -452,10 +467,11 @@ function buildGLB(snapshot: SceneSnapshotIR, geometryBuffers: readonly MeshGeome
|
||||
positions[corner * 3] = positionsSource[vertex * 3];
|
||||
positions[corner * 3 + 1] = positionsSource[vertex * 3 + 2];
|
||||
positions[corner * 3 + 2] = -positionsSource[vertex * 3 + 1];
|
||||
if (normals && normalsSource) {
|
||||
normals[corner * 3] = normalsSource[vertex * 3];
|
||||
normals[corner * 3 + 1] = normalsSource[vertex * 3 + 2];
|
||||
normals[corner * 3 + 2] = -normalsSource[vertex * 3 + 1];
|
||||
if (normals && exportNormals) {
|
||||
const normalIndex = hasSplitNormals ? cornerSource![corner] : vertex;
|
||||
normals[corner * 3] = exportNormals[normalIndex * 3];
|
||||
normals[corner * 3 + 1] = exportNormals[normalIndex * 3 + 2];
|
||||
normals[corner * 3 + 2] = -exportNormals[normalIndex * 3 + 1];
|
||||
}
|
||||
const sourceCorner = cornerSource?.[corner] ?? vertex;
|
||||
if (uvs && uvsSource && sourceCorner * 2 + 1 < uvsSource.length) uvs.set(uvsSource.subarray(sourceCorner * 2, sourceCorner * 2 + 2), corner * 2);
|
||||
@@ -484,6 +500,7 @@ function buildGLB(snapshot: SceneSnapshotIR, geometryBuffers: readonly MeshGeome
|
||||
}
|
||||
const positionAccessor = appendAccessor(parts, binaryLength, bufferViews, accessors, positions, "VEC3", TARGET_ARRAY_BUFFER); binaryLength = positionAccessor.length;
|
||||
const normalAccessor = normals ? appendAccessor(parts, binaryLength, bufferViews, accessors, normals, "VEC3", TARGET_ARRAY_BUFFER) : undefined; if (normalAccessor) binaryLength = normalAccessor.length;
|
||||
if (uvs) for (let component = 1; component < uvs.length; component += 2) uvs[component] = 1 - uvs[component];
|
||||
const uvAccessor = uvs ? appendAccessor(parts, binaryLength, bufferViews, accessors, uvs, "VEC2", TARGET_ARRAY_BUFFER) : undefined; if (uvAccessor) binaryLength = uvAccessor.length;
|
||||
const colorAccessor = colors ? appendAccessor(parts, binaryLength, bufferViews, accessors, colors, "VEC4", TARGET_ARRAY_BUFFER) : undefined; if (colorAccessor) binaryLength = colorAccessor.length;
|
||||
const jointAccessor = joints ? appendAccessor(parts, binaryLength, bufferViews, accessors, joints, "VEC4", TARGET_ARRAY_BUFFER) : undefined; if (jointAccessor) binaryLength = jointAccessor.length;
|
||||
|
||||
Reference in New Issue
Block a user