import { createHash } from "node:crypto"; import { Accessor } from "@gltf-transform/core"; // Reviewed browser surface fixes live in rom-surface-quality-policies.mjs. // This registry remains for future UV-only adaptations whose projection is // safe to express as one source-UV transform. const POLICIES = Object.freeze({}); const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); const accessorHash = (accessor) => { const array = accessor.getArray(); return sha256(Buffer.from(array.buffer, array.byteOffset, array.byteLength)); }; const normalizeResource = (value) => String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "").toLowerCase(); function sourceProof(policy, dungeonId, sourceReport) { if (dungeonId !== policy.dungeonId) throw new Error(`UV policy ${policy.id} cannot run for ${dungeonId}.`); if (normalizeResource(sourceReport?.sourceModel) !== policy.sourceModel.toLowerCase()) { throw new Error(`UV policy ${policy.id} source ROS path does not match the pinned model.`); } if (String(sourceReport?.sourceResourceSha256 ?? "").toLowerCase() !== policy.sourceModelSha256) { throw new Error(`UV policy ${policy.id} source ROS SHA-256 does not match the reviewed source.`); } const matches = (sourceReport.materials ?? []).filter((material) => material.name === policy.sourceMaterial); if (matches.length !== 1) throw new Error(`UV policy ${policy.id} expected one exact native material record; found ${matches.length}.`); const sourceMaterial = matches[0]; if (normalizeResource(sourceMaterial.sourceTexture) !== policy.sourceTexture.toLowerCase()) { throw new Error(`UV policy ${policy.id} native diffuse texture path does not match the reviewed source.`); } if (normalizeResource(sourceMaterial.stagedTexture).split("/").at(-1) !== policy.stagedTexture.toLowerCase()) { throw new Error(`UV policy ${policy.id} staged texture basename does not match the reviewed source.`); } return { sourceModel: policy.sourceModel, sourceModelSha256: policy.sourceModelSha256, sourceMaterial: policy.sourceMaterial, sourceTexture: policy.sourceTexture, stagedTexture: policy.stagedTexture, }; } function policyMarker(policy) { return { id: policy.id, version: 1, repeat: [...policy.repeat], sourceAttribute: "TEXCOORD_0", adaptedAttribute: "TEXCOORD_1", sourceMaterial: policy.sourceMaterial, sourceTexture: policy.gltfTexture, }; } function validateAdaptedPrimitive(primitive, policy) { const info = primitive.getMaterial()?.getBaseColorTextureInfo(); const uv0 = primitive.getAttribute("TEXCOORD_0"); const uv1 = primitive.getAttribute("TEXCOORD_1"); if (!uv0 || !uv1 || info?.getTexCoord() !== 1) throw new Error(`Previously adapted primitive is incomplete for ${policy.id}.`); if (primitive.getExtras()?.romUvAdaptation?.id !== policy.id) throw new Error(`Previously adapted primitive has a mismatched marker for ${policy.id}.`); if (uv0.getCount() !== uv1.getCount()) throw new Error(`Previously adapted UV accessor counts differ for ${policy.id}.`); for (let index = 0; index < uv0.getCount(); index += 1) { const source = uv0.getElement(index, [0, 0]); const adapted = uv1.getElement(index, [0, 0]); if (Math.abs(adapted[0] - source[0] * policy.repeat[0]) > 1e-6 || Math.abs(adapted[1] - source[1] * policy.repeat[1]) > 1e-6) { throw new Error(`Previously adapted UV values are invalid for ${policy.id}.`); } } } /** * Applies explicitly opted-in, source-pinned RoM UV corrections to a canonical * unoptimized visual document. The source UV0 and all geometry attributes stay * intact; the cloned base-color material alone reads repeated UV1. */ export function applyRunewakerVisualUvAdaptations(document, { dungeonId, policyIds = [], sourceReport } = {}) { if (new Set(policyIds).size !== policyIds.length) throw new Error("RoM visual UV adaptation policy IDs must be unique."); const root = document.getRoot(); const result = { schemaVersion: 1, dungeonId, status: policyIds.length ? "applied" : "no-policy", policies: [], materialLightingMetadataChanged: false, geometryChanged: false, sourceUv0Changed: false, }; for (const policyId of policyIds) { const policy = POLICIES[policyId]; if (!policy) throw new Error(`Unknown RoM visual UV adaptation policy: ${policyId}.`); const proof = sourceProof(policy, dungeonId, sourceReport); const marker = policyMarker(policy); const originalMaterial = root.listMaterials().find((material) => material.getName() === policy.sourceMaterial); if (!originalMaterial) throw new Error(`UV policy ${policy.id} source material is absent from the canonical GLB.`); const originalTexture = originalMaterial.getBaseColorTexture(); if (originalTexture?.getName() !== policy.gltfTexture || !originalMaterial.getBaseColorTextureInfo()) { throw new Error(`UV policy ${policy.id} base-color image does not match the source report.`); } if (originalMaterial.getNormalTexture() || originalMaterial.getMetallicRoughnessTexture() || originalMaterial.getEmissiveTexture()) { throw new Error(`UV policy ${policy.id} has unexpected secondary texture slots.`); } const primitives = []; const alreadyAdapted = []; for (const node of root.listNodes()) { if (!node.getMesh()) continue; for (const [primitiveIndex, primitive] of node.getMesh().listPrimitives().entries()) { if (primitive.getMaterial()?.getName() === policy.sourceMaterial) { if (primitive.getMaterial().getBaseColorTexture()?.getName() !== policy.gltfTexture) { throw new Error(`UV policy ${policy.id} material use maps to an unexpected image.`); } primitives.push({ node, primitive, primitiveIndex }); } else if (primitive.getExtras()?.romUvAdaptation?.id === policy.id) { alreadyAdapted.push({ node, primitive, primitiveIndex }); } } } if (alreadyAdapted.length) { if (primitives.length) throw new Error(`UV policy ${policy.id} document contains a partial adaptation.`); for (const { primitive } of alreadyAdapted) validateAdaptedPrimitive(primitive, policy); result.status = "already-applied"; result.policies.push({ ...proof, ...marker, status: "already-applied", primitiveCount: alreadyAdapted.length, triangleCount: alreadyAdapted.reduce((sum, { primitive }) => sum + (primitive.getIndices()?.getCount() ?? primitive.getAttribute("POSITION")?.getCount() ?? 0) / 3, 0), nodes: alreadyAdapted.map(({ node, primitive }) => ({ node: node.getName(), sourceUvHash: accessorHash(primitive.getAttribute("TEXCOORD_0")), adaptedUvHash: accessorHash(primitive.getAttribute("TEXCOORD_1")) })), lightingMetadataPreserved: true, }); continue; } if (!primitives.length) throw new Error(`UV policy ${policy.id} found no matching canonical GLB primitives.`); const buffer = root.listBuffers()[0]; if (!buffer) throw new Error(`UV policy ${policy.id} document has no buffer for new accessors.`); const preflight = primitives.map(({ node, primitive, primitiveIndex }) => { if (primitive.getExtras()?.romUvAdaptation) throw new Error(`UV policy ${policy.id} target is marked by another adaptation.`); if (primitive.getAttribute("TEXCOORD_1")) throw new Error(`UV policy ${policy.id} refuses to overwrite existing TEXCOORD_1.`); const position = primitive.getAttribute("POSITION"); const normal = primitive.getAttribute("NORMAL"); const indices = primitive.getIndices(); const uv0 = primitive.getAttribute("TEXCOORD_0"); if (!position || !normal || !indices || !uv0 || uv0.getType() !== Accessor.Type.VEC2) { throw new Error(`UV policy ${policy.id} target is missing a required POSITION, NORMAL, index, or VEC2 TEXCOORD_0 accessor.`); } return { node, primitive, primitiveIndex, position, normal, indices, uv0 }; }); const adaptedMaterial = originalMaterial.clone().setName(`${policy.sourceMaterial}_repeat${policy.repeat[0]}x${policy.repeat[1]}_adapted`); adaptedMaterial.getBaseColorTextureInfo().setTexCoord(1); const policyReport = { ...proof, ...marker, status: "applied", primitiveCount: primitives.length, triangleCount: 0, nodes: [], lightingMetadataPreserved: true, noGeometryChange: true, noOtherUvSetChanged: true, }; for (const { node, primitive, primitiveIndex, position, normal, indices, uv0 } of preflight) { const sourceHashes = { position: accessorHash(position), normal: accessorHash(normal), indices: accessorHash(indices), uv0: accessorHash(uv0), }; const values = new Float32Array(uv0.getCount() * 2); for (let index = 0; index < uv0.getCount(); index += 1) { const uv = uv0.getElement(index, [0, 0]); values[index * 2] = uv[0] * policy.repeat[0]; values[index * 2 + 1] = uv[1] * policy.repeat[1]; } const uv1 = document.createAccessor(`${node.getName()}:${policy.id}:TEXCOORD_1`) .setType(Accessor.Type.VEC2) .setArray(values) .setBuffer(buffer); primitive.setAttribute("TEXCOORD_1", uv1); primitive.setMaterial(adaptedMaterial); primitive.setExtras({ ...(primitive.getExtras() ?? {}), romUvAdaptation: marker }); policyReport.triangleCount += indices.getCount() / 3; policyReport.nodes.push({ node: node.getName(), primitiveIndex, triangles: indices.getCount() / 3, vertices: position.getCount(), sourcePositionHash: sourceHashes.position, sourceNormalHash: sourceHashes.normal, sourceIndexHash: sourceHashes.indices, sourceUvHash: sourceHashes.uv0, adaptedUvHash: sha256(Buffer.from(values.buffer, values.byteOffset, values.byteLength)), }); } result.policies.push(policyReport); } return result; } export function listRunewakerVisualUvAdaptationPolicies() { return Object.values(POLICIES).map((policy) => ({ ...policy, repeat: [...policy.repeat] })); }