Files
healer-man/scripts/runewaker-pipeline/lib/native-material-semantics.mjs
T
2026-10-06 22:25:34 -04:00

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,
}));
}