Advance WebGPU volume and bounded workflows
This commit is contained in:
149
tools/vdb/build-nanovdb-manifest.mjs
Normal file
149
tools/vdb/build-nanovdb-manifest.mjs
Normal file
@@ -0,0 +1,149 @@
|
||||
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`);
|
||||
Reference in New Issue
Block a user