125 lines
8.4 KiB
JavaScript
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 });
|
|
}
|