150 lines
5.8 KiB
JavaScript
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`);
|