Files
workinf_Blender_Wasm/tools/web/check-glb-blender-roundtrip.mjs
2026-08-12 04:47:48 -04:00

125 lines
8.4 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { pathToFileURL } from "node:url";
import ts from "../../web/node_modules/typescript/lib/typescript.js";
const root = path.resolve(new URL("../../", import.meta.url).pathname);
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "web-glb-blender-"));
function identity(translation = [0, 0, 0]) {
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, translation[0], translation[1], translation[2], 1];
}
function geometry(meshId, positions) {
const positionBuffer = Float32Array.from(positions).buffer;
const indexBuffer = Uint32Array.from([0, 1, 2]).buffer;
return {
schemaVersion: 1,
meshId,
byteLength: positionBuffer.byteLength + indexBuffer.byteLength,
positions: positionBuffer,
indices: indexBuffer,
};
}
try {
const exporterSource = fs.readFileSync(path.join(root, "web/protocol/glb-export.ts"), "utf8");
const transpiled = ts.transpileModule(exporterSource, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
fileName: "glb-export.ts",
});
const exporterPath = path.join(temporary, "glb-export.cjs");
fs.writeFileSync(exporterPath, transpiled.outputText);
const { exportGLB } = await import(pathToFileURL(exporterPath).href);
const roundTripPositions = [0, 0, 0, 1, 0, 0, 0, 1, 0];
const skinPositions = [0, 0, 0, 0.5, 0, 0, 0, 0, 1];
const snapshot = {
schemaVersion: 1,
revision: 7,
sceneId: "scene:BlenderRoundTrip",
source: { kind: "mock" },
coordinateSystem: { upAxis: "Z", forwardAxis: "-Y", handedness: "RIGHT", unitSystem: 0, unitScale: 1 },
nodes: [
{ id: "object:Armature", name: "Armature", type: "ARMATURE", parentId: null, dataId: "armature:Rig", visible: true, selectable: true, localMatrix: identity(), worldMatrix: identity(), transform: { translation: [0, 0, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1], rotationMode: 1 } },
{ id: "object:RoundTripMesh", name: "RoundTripMesh", type: "MESH", parentId: null, dataId: "mesh:RoundTripMesh", visible: true, selectable: true, localMatrix: identity([2, -3, 4]), worldMatrix: identity([2, -3, 4]), transform: { translation: [2, -3, 4], rotationEuler: [0, 0, 0], scale: [1, 1, 1], rotationMode: 1 } },
{ id: "object:SkinnedMesh", name: "SkinnedMesh", type: "MESH", parentId: null, dataId: "mesh:SkinnedMesh", visible: true, selectable: true, localMatrix: identity(), worldMatrix: identity(), transform: { translation: [0, 0, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1], rotationMode: 1 } },
],
meshes: [
{ id: "mesh:RoundTripMesh", name: "RoundTripMesh", vertexCount: 3, edgeCount: 3, faceCount: 1, cornerCount: 3, triangleCount: 1, geometryStatus: "binary", geometryBufferId: "mesh:RoundTripMesh", topology: "triangles", materialSlotIds: ["material:RoundTrip"], shapeKeys: [{ name: "Lift", positions: [0, 0, 0, 1, 0, 0, 0, 1, 1] }] },
{ id: "mesh:SkinnedMesh", name: "SkinnedMesh", vertexCount: 3, edgeCount: 3, faceCount: 1, cornerCount: 3, triangleCount: 1, geometryStatus: "binary", geometryBufferId: "mesh:SkinnedMesh", topology: "triangles", materialSlotIds: ["material:Texture"], skinWeights: { boneNames: ["Root", "Tip"], indices: [0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0], weights: [0.75, 0.25, 0, 0, 0.5, 0.5, 0, 0, 0.25, 0.75, 0, 0], bindMatrix: identity(), armatureId: "armature:Rig", jointIds: ["bone:Root", "bone:Tip"] } },
],
armatures: [{ id: "armature:Rig", name: "Rig", objectId: "object:Armature", bones: [
{ id: "bone:Root", name: "Root", parentId: null, head: [0, 0, 0], tail: [0, 0, 1], restMatrix: identity(), poseMatrix: identity() },
{ id: "bone:Tip", name: "Tip", parentId: "bone:Root", head: [0, 0, 1], tail: [0, 0, 2], restMatrix: identity([0, 0, 1]), poseMatrix: identity([0, 0, 1]) },
] }],
materials: [
{ id: "material:RoundTrip", name: "RoundTripMaterial", baseColor: [0.25, 0.5, 0.75, 0.8], roughness: 0.6, metallic: 0.35, emissionColor: [0.05, 0.1, 0.2, 1], alpha: 0.8, ior: 1.6, specularIORLevel: 0.35, transmissionWeight: 0.27, coatWeight: 0.64, coatRoughness: 0.12, emissionStrength: 3.5 },
{ id: "material:Texture", name: "TextureMaterial", baseColor: [1, 1, 1, 1], roughness: 0.4, metallic: 0, emissionColor: [0, 0, 0, 1], alpha: 1, ior: 1.45, imageIds: ["image:RoundTripTexture"] },
],
images: [{ id: "image:RoundTripTexture", name: "RoundTripTexture", assetId: "asset:RoundTripTexture", mimeType: "image/png", packed: true }],
animations: [{ id: "action:Move", name: "Move", targetId: "object:RoundTripMesh", frameStart: 1, frameEnd: 25, channels: [
{ path: "location[0]", keyframes: [{ frame: 1, value: [2] }, { frame: 25, value: [4] }] },
{ path: "location[1]", keyframes: [{ frame: 1, value: [-3] }, { frame: 25, value: [-3] }] },
{ path: "location[2]", keyframes: [{ frame: 1, value: [4] }, { frame: 25, value: [4] }] },
] }],
cameras: [], lights: [], worlds: [], collections: [], scenes: [], activeObjectId: "object:RoundTripMesh", frame: { current: 1, start: 1, end: 25 },
};
const texture = fs.readFileSync(path.join(root, "tests/files/web/resources/udim_1001.png"));
const exported = exportGLB(
snapshot,
[geometry("mesh:RoundTripMesh", roundTripPositions), geometry("mesh:SkinnedMesh", skinPositions)],
[{ assetId: "asset:RoundTripTexture", mimeType: "image/png", data: texture.buffer.slice(texture.byteOffset, texture.byteOffset + texture.byteLength) }],
);
assert.equal(exported.report.canExport, true, JSON.stringify(exported.report.warnings));
assert.ok(exported.glb?.byteLength > 1000, "GLB output is unexpectedly small");
const glbPath = path.join(temporary, "roundtrip.glb");
const reportPath = path.join(temporary, "blender-report.json");
fs.writeFileSync(glbPath, new Uint8Array(exported.glb));
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
const imported = spawnSync(blender, ["-b", "--python", path.join(root, "tools/web/blender-check-glb-roundtrip.py"), "--", glbPath, reportPath], { cwd: root, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
assert.equal(imported.status, 0, `${imported.stdout}\n${imported.stderr}`);
assert.ok(fs.existsSync(reportPath), `Blender produced no round-trip report\n${imported.stdout}\n${imported.stderr}`);
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
if (process.env.GLB_ROUNDTRIP_DEBUG === "1") process.stderr.write(`${JSON.stringify(report.material, null, 2)}\n`);
assert.equal(report.mesh.vertexCount, 3);
assert.equal(report.mesh.triangleCount, 1);
assert.deepEqual(report.mesh.positions, [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);
assert.deepEqual(report.mesh.frame1WorldTranslation, [2, -3, 4]);
assert.deepEqual(report.mesh.frame25WorldTranslation, [4, -3, 4]);
assert.deepEqual(report.mesh.shapeKeys.Lift, [0, 0, 1]);
assert.deepEqual(report.material.principled.baseColor, [0.25, 0.5, 0.75, 1]);
assert.equal(report.material.principled.alpha, 0.8);
assert.equal(report.material.principled.metallic, 0.35);
assert.equal(report.material.principled.roughness, 0.6);
assert.equal(report.material.principled.ior, 1.6);
assert.equal(report.material.principled.specularIORLevel, 0.35);
assert.equal(report.material.principled.transmissionWeight, 0.27);
assert.equal(report.material.principled.coatWeight, 0.64);
assert.equal(report.material.principled.coatRoughness, 0.12);
assert.equal(report.material.principled.emissionStrength, 3.5);
assert.equal(report.material.texture.present, true);
assert.equal(report.material.texture.interpolation, "Linear");
assert.equal(report.material.texture.extension, "REPEAT");
assert.equal(report.material.texture.imageWidth, 8);
assert.equal(report.material.texture.imageHeight, 8);
assert.equal(report.material.texture.colorSpace, "sRGB");
assert.ok(report.animation.actionCount >= 1);
assert.equal(report.skin.armatureCount, 1);
assert.deepEqual(report.skin.bones, ["Root", "Tip"]);
assert.deepEqual(report.skin.boneParents, { Root: null, Tip: "Root" });
assert.ok(report.skin.modifierCount >= 1);
assert.equal(report.skin.vertexGroupCount, 2);
assert.deepEqual(report.skin.weightSums, [1, 1, 1]);
process.stdout.write(`glb-blender-roundtrip-ok bytes=${exported.glb.byteLength} vertices=3 morphs=1 bones=2 actions=${report.animation.actionCount} texture=8x8\n`);
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}