88 lines
3.7 KiB
JavaScript
88 lines
3.7 KiB
JavaScript
import { NodeIO } from "@gltf-transform/core";
|
|
import { ALL_EXTENSIONS, KHRMaterialsUnlit } from "@gltf-transform/extensions";
|
|
import { writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const SHA256 = /^[\da-f]{64}$/i;
|
|
|
|
export function hasVerifiedNativeMaterialIdentity(extras) {
|
|
return typeof extras?.nativeSourceModel === "string"
|
|
&& extras.nativeSourceModel.toLowerCase().endsWith(".ros")
|
|
&& typeof extras.nativeSourceResourceSha256 === "string"
|
|
&& SHA256.test(extras.nativeSourceResourceSha256)
|
|
&& Number.isInteger(extras.nativeMaterialIndex)
|
|
&& extras.nativeMaterialIndex >= 0
|
|
&& typeof extras.nativeSourceTexture === "string"
|
|
&& extras.nativeSourceTexture.trim().length > 0;
|
|
}
|
|
|
|
/** Preserve only source-proven lighting modes on native ROS materials. */
|
|
export async function applyNativeMaterialSemantics(inputFile, outputFile = inputFile) {
|
|
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
const document = await io.read(inputFile);
|
|
const materials = document.getRoot().listMaterials();
|
|
const prelitRows = [];
|
|
const additiveRows = [];
|
|
|
|
for (const material of materials) {
|
|
const extras = material.getExtras() ?? {};
|
|
if (!hasVerifiedNativeMaterialIdentity(extras)) {
|
|
if (extras.nativeLightingType === "prelit" || extras.nativeBlendMode === "additive") {
|
|
throw new Error(`Native lighting/blend metadata lacks verified material provenance: ${material.getName()}`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (extras.nativeLightingType === "prelit") prelitRows.push(material);
|
|
if (extras.nativeBlendMode === "additive"
|
|
&& extras.nativeAlphaPolicy === "native-srcalpha-one-additive") additiveRows.push(material);
|
|
}
|
|
|
|
if (prelitRows.length > 0) {
|
|
const unlit = document.createExtension(KHRMaterialsUnlit).setRequired(false).createUnlit();
|
|
for (const material of prelitRows) {
|
|
// Blender exports specular metadata even for source-prelit surfaces.
|
|
// glTF forbids its combination with unlit; it has no effect on prelit RGB.
|
|
material.setExtension("KHR_materials_specular", null);
|
|
material.setExtension("KHR_materials_unlit", unlit);
|
|
}
|
|
}
|
|
|
|
await io.write(outputFile, document);
|
|
const report = {
|
|
schemaVersion: 1,
|
|
input: path.resolve(inputFile),
|
|
output: path.resolve(outputFile),
|
|
khrMaterialsUnlit: prelitRows.map(material => ({
|
|
material: material.getName(),
|
|
sourceModel: material.getExtras().nativeSourceModel,
|
|
sourceResourceSha256: material.getExtras().nativeSourceResourceSha256,
|
|
nativeMaterialIndex: material.getExtras().nativeMaterialIndex,
|
|
sourceTexture: material.getExtras().nativeSourceTexture,
|
|
})),
|
|
nativeAdditiveMaterials: additiveRows.map(material => ({
|
|
material: material.getName(),
|
|
sourceModel: material.getExtras().nativeSourceModel,
|
|
sourceResourceSha256: material.getExtras().nativeSourceResourceSha256,
|
|
nativeMaterialIndex: material.getExtras().nativeMaterialIndex,
|
|
sourceTexture: material.getExtras().nativeSourceTexture,
|
|
alphaMode: material.getAlphaMode(),
|
|
})),
|
|
};
|
|
return report;
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
const inputFile = process.argv[2];
|
|
if (!inputFile) throw new Error("Usage: node apply-native-material-semantics.mjs <input.glb> [report.json]");
|
|
const report = await applyNativeMaterialSemantics(inputFile);
|
|
const reportFile = process.argv[3];
|
|
if (reportFile) await writeFile(reportFile, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
console.log(JSON.stringify({
|
|
output: report.output,
|
|
unlitMaterialCount: report.khrMaterialsUnlit.length,
|
|
additiveMaterialCount: report.nativeAdditiveMaterials.length,
|
|
}));
|
|
}
|