Files
workinf_Blender_Wasm/tools/vdb/build-nanovdb-manifest.mjs
2026-08-14 18:08:29 -04:00

150 lines
5.8 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const args = new Map();
for (let index = 2; index < process.argv.length; index += 2) {
args.set(process.argv[index], process.argv[index + 1]);
}
function required(name) {
const value = args.get(name);
if (!value) throw new Error(`VDB_MANIFEST_ARGUMENT_MISSING: ${name}`);
return value;
}
function sha256(file) {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
function semantic(name) {
const known = new Map([
["density", "DENSITY"],
["temperature", "TEMPERATURE"],
["color", "COLOR"],
["emission", "EMISSION"],
["flame", "EMISSION"],
["velocity", "VELOCITY"],
]);
return known.get(name.toLowerCase()) ?? "CUSTOM";
}
const sourceFile = path.resolve(required("--source"));
const bundleFile = path.resolve(required("--bundle"));
const reportFile = path.resolve(required("--report"));
const outputFile = path.resolve(required("--output"));
const converterFile = path.resolve(required("--converter"));
const projectId = args.get("--project-id") ?? "vdb-fixtures";
const sourcePath = args.get("--source-path") ?? `//volumes/${path.basename(sourceFile)}`;
const bundlePath = args.get("--bundle-path") ?? `//volumes/${path.basename(bundleFile)}`;
const blenderVersion = args.get("--blender-version") ?? "5.2.0";
const converterTarget = args.get("--converter-target") ?? "DESKTOP";
const chunkByteLength = Number(args.get("--chunk-bytes") ?? 4 * 1024 * 1024);
if (converterTarget !== "DESKTOP" && converterTarget !== "SERVER") {
throw new Error("VDB_MANIFEST_ARGUMENT_INVALID: --converter-target");
}
if (!Number.isSafeInteger(chunkByteLength) || chunkByteLength < 64 * 1024 || chunkByteLength > 16 * 1024 * 1024 || chunkByteLength % 32 !== 0) {
throw new Error("VDB_MANIFEST_ARGUMENT_INVALID: --chunk-bytes");
}
for (const file of [sourceFile, bundleFile, reportFile, converterFile]) {
if (!fs.statSync(file).isFile()) throw new Error(`VDB_MANIFEST_RESOURCE_MISSING: ${file}`);
}
const report = JSON.parse(fs.readFileSync(reportFile, "utf8"));
if (report.schemaVersion !== 1 || !Array.isArray(report.grids) || report.grids.length === 0) throw new Error("VDB_CONVERSION_INVALID: native report");
if (path.resolve(report.input) !== sourceFile || path.resolve(report.output) !== bundleFile) throw new Error("VDB_CONVERSION_INVALID: report paths do not match artifacts");
const sourceSha256 = sha256(sourceFile);
const converter = {
target: converterTarget,
blenderVersion,
openVDBVersion: report.openVDBVersion,
nanoVDBVersion: report.nanoVDBVersion,
executableSha256: sha256(converterFile),
};
const sourceGrids = report.grids.map((grid) => ({
name: grid.name,
valueType: grid.sourceType.toUpperCase(),
voxelCount: grid.activeVoxelCount,
activeVoxelCount: grid.activeVoxelCount,
bounds: grid.indexBounds,
}));
const conversionRequest = {
schemaVersion: 1,
source: {
byteLength: fs.statSync(sourceFile).size,
sha256: sourceSha256,
grids: sourceGrids,
},
selectedGrids: report.grids.map((grid) => grid.name),
quantization: report.quantization,
chunkByteLength,
converter,
};
const conversionRequestSha256 = crypto.createHash("sha256").update(JSON.stringify(conversionRequest)).digest("hex");
const bundleBytes = fs.readFileSync(bundleFile);
const chunks = [];
for (let byteOffset = 0, index = 0; byteOffset < bundleBytes.length; byteOffset += chunkByteLength, index += 1) {
const data = bundleBytes.subarray(byteOffset, Math.min(bundleBytes.length, byteOffset + chunkByteLength));
chunks.push({ index, byteOffset, byteLength: data.length, sha256: crypto.createHash("sha256").update(data).digest("hex") });
}
const grids = report.grids.map((grid) => ({
name: grid.name,
valueType: grid.valueType,
gridClass: grid.gridClass,
semantic: semantic(grid.name),
activeVoxelCount: grid.activeVoxelCount,
segmentByteOffset: grid.segmentByteOffset,
segmentByteLength: grid.segmentByteLength,
byteOffset: grid.byteOffset,
byteLength: grid.byteLength,
indexBounds: grid.indexBounds,
worldBounds: grid.worldBounds,
voxelSize: grid.voxelSize,
indexToWorld: grid.indexToWorld,
}));
const gridBySemantic = new Map(grids.map((grid) => [grid.semantic, grid.name]));
if (!gridBySemantic.has("DENSITY")) throw new Error("NANOVDB_GRID_UNSUPPORTED: a browser volume bundle requires a density grid");
const manifest = {
schemaVersion: 1,
projectId,
sourcePath,
sourceSha256,
conversionRequestSha256,
bundlePath,
bundleByteLength: bundleBytes.length,
bundleSha256: crypto.createHash("sha256").update(bundleBytes).digest("hex"),
converter,
grids,
chunks,
material: {
densityGrid: gridBySemantic.get("DENSITY"),
...(gridBySemantic.has("TEMPERATURE") ? { temperatureGrid: gridBySemantic.get("TEMPERATURE") } : {}),
...(gridBySemantic.has("COLOR") ? { colorGrid: gridBySemantic.get("COLOR") } : {}),
...(gridBySemantic.has("EMISSION") ? { emissionGrid: gridBySemantic.get("EMISSION") } : {}),
...(gridBySemantic.has("VELOCITY") ? { velocityGrid: gridBySemantic.get("VELOCITY") } : {}),
densityScale: 1,
emissionScale: 0,
temperatureScale: 1,
anisotropy: 0,
interpolation: "LINEAR",
},
gpu: {
representation: "NANOVDB_STORAGE_BUFFER",
byteAlignment: 32,
pageByteLength: chunkByteLength,
maxResidentBytes: 256 * 1024 * 1024,
shaderSemanticVersion: "volume-wgsl-v1",
...(report.float32TreeLayout ? { float32TreeLayout: report.float32TreeLayout } : {}),
...(report.vec3fTreeLayout ? { vec3fTreeLayout: report.vec3fTreeLayout } : {}),
},
};
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
fs.writeFileSync(outputFile, `${JSON.stringify(manifest, null, 2)}\n`);
process.stdout.write(`nanovdb-manifest-ok grids=${grids.length} chunks=${chunks.length} bytes=${bundleBytes.length} sha256=${manifest.bundleSha256}\n`);