Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,746 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { Accessor } from "@gltf-transform/core";
|
||||
import { KHRMaterialsVariants } from "@gltf-transform/extensions";
|
||||
import sharp from "sharp";
|
||||
|
||||
const BARREN_POLICY_ID = "rom-barren-caves-wretched-tunnel-surface-quality-v2";
|
||||
const HOTO_POLICY_ID = "rom-hoto-source-rock-detail-v2";
|
||||
const HOTO_LEGACY_POLICY_ID = "rom-hoto-source-rock-detail-v1";
|
||||
const BARREN_SOURCE = Object.freeze({
|
||||
model: "model/dungeon/wretched_grotto/act_world_wretched_grotto.ros",
|
||||
sha256: "d2de5b83f63248b9b6f6d17e13104ee3cddbc4e77903d4eb561ca5731556bab7",
|
||||
material: "runewaker_material_12",
|
||||
texture: "texture/dungeon/wretched_grotto/wretched_tunnel",
|
||||
image: "a6d9c718_wretched_tunnel",
|
||||
imageSha256: "0f05ae62b2486ae90a9836873fac287fd89169abcef962ac7dfacd81fb469a7c",
|
||||
});
|
||||
const HOTO_SOURCE = Object.freeze({
|
||||
model: "model/dungeon/echoes_of_the_sea/act_echoes_of_the_sea.ros",
|
||||
sha256: "a0bd77c09045f8bfa1507484bbb2c2ff0cc72d397df4f769fc4e6d5aab14f8a2",
|
||||
});
|
||||
const HOTO_TARGETS = Object.freeze({
|
||||
runewaker_material_96: Object.freeze({
|
||||
nativeMaterial: "runewaker_material_96", texture: "texture/dungeon/echoes_of_the_sea/echoes_of_the_sea_01",
|
||||
image: "122cc01b_echoes_of_the_sea_01", imageSha256: "ea395afa1d0c65aa57ed7e675a352ca47aab0b1f7a629a3bebdf36e03621a3b1",
|
||||
floorStrength: 0.85, floorBaseStrength: 0, wallStrength: 0.85, legacyWallStrength: 0.45,
|
||||
wallBaseContrast: 0.6, wallDetailVariation: 1, useOwnBaseMean: true,
|
||||
}),
|
||||
runewaker_material_53: Object.freeze({
|
||||
nativeMaterial: "runewaker_material_53", texture: "texture/dungeon/echoes_of_the_sea/echoes_of_the_sea_04",
|
||||
image: "5c863840_echoes_of_the_sea_04", imageSha256: "fbb0eb3ac74689cf2e7b6bc043bc15f1c3cbc4638fc15675ff0710037e9666f9",
|
||||
floorStrength: 0.85, floorBaseStrength: 0, wallStrength: 0.85, legacyWallStrength: 0.45,
|
||||
wallBaseContrast: 0.6, wallDetailVariation: 1, useOwnBaseMean: true,
|
||||
}),
|
||||
runewaker_material_26: Object.freeze({
|
||||
nativeMaterial: "runewaker_material_94", texture: "texture/dungeon/echoes_of_the_sea/echoes_of_the_sea_02",
|
||||
image: "5672f062_echoes_of_the_sea_02", imageSha256: "bc32a6751da4db246d50f66d2c9fb625f640be652956d7022a4feb649d62a1d2",
|
||||
floorVariantRole: "floor", floorStrength: 0, floorBaseStrength: 1, wallStrength: 0, legacyWallStrength: 0,
|
||||
wallBaseContrast: 1, wallDetailVariation: 0,
|
||||
}),
|
||||
runewaker_material_18: Object.freeze({
|
||||
nativeMaterial: "runewaker_material_95", texture: "texture/dungeon/echoes_of_the_sea/echoes_of_the_sea_03",
|
||||
image: "b2ababb9_echoes_of_the_sea_03", imageSha256: "7e20e5f22744d638fa9bd983c13844800bc316e3502ab092bf6d8b6f68a1d018",
|
||||
floorVariantRole: "wall", floorStrength: 0, floorBaseStrength: 1, wallStrength: 0.45, legacyWallStrength: 0.45,
|
||||
wallBaseContrast: 1, wallDetailVariation: 0,
|
||||
}),
|
||||
});
|
||||
const HOTO_DETAIL = Object.freeze({
|
||||
floor: Object.freeze({
|
||||
nativeMaterial: "runewaker_material_94", canonicalMaterial: "runewaker_material_26",
|
||||
texture: "texture/dungeon/echoes_of_the_sea/echoes_of_the_sea_02",
|
||||
image: "5672f062_echoes_of_the_sea_02", imageSha256: "bc32a6751da4db246d50f66d2c9fb625f640be652956d7022a4feb649d62a1d2",
|
||||
}),
|
||||
wall: Object.freeze({
|
||||
nativeMaterial: "runewaker_material_95", canonicalMaterial: "runewaker_material_18",
|
||||
texture: "texture/dungeon/echoes_of_the_sea/echoes_of_the_sea_03",
|
||||
image: "b2ababb9_echoes_of_the_sea_03", imageSha256: "7e20e5f22744d638fa9bd983c13844800bc316e3502ab092bf6d8b6f68a1d018",
|
||||
}),
|
||||
});
|
||||
const HOTO_VARIANTS = Object.freeze({ floor: "rom-detail-hoto-floor", wall: "rom-detail-hoto-wall" });
|
||||
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const normalizeResource = (value) => String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "").toLowerCase();
|
||||
const accessorHash = (accessor) => {
|
||||
const array = accessor?.getArray();
|
||||
return array ? sha256(Buffer.from(array.buffer, array.byteOffset, array.byteLength)) : null;
|
||||
};
|
||||
|
||||
function verifySourceReport(sourceReport, source) {
|
||||
if (normalizeResource(sourceReport?.sourceModel) !== source.model.toLowerCase()) {
|
||||
throw new Error(`Surface policy source ROS path does not match ${source.model}.`);
|
||||
}
|
||||
if (String(sourceReport?.sourceResourceSha256 ?? "").toLowerCase() !== source.sha256) {
|
||||
throw new Error(`Surface policy source ROS SHA-256 does not match ${source.model}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyNativeMaterial(sourceReport, nativeName, texturePath) {
|
||||
const rows = (sourceReport?.materials ?? []).filter((row) => row.name === nativeName);
|
||||
if (rows.length !== 1 || normalizeResource(rows[0].sourceTexture) !== texturePath.toLowerCase()) {
|
||||
throw new Error(`Expected exact native material/texture binding ${nativeName} -> ${texturePath}.`);
|
||||
}
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
function listVariants(document) {
|
||||
const root = document.getRoot();
|
||||
const extension = root.listExtensionsUsed().find((item) => item.extensionName === KHRMaterialsVariants.EXTENSION_NAME)
|
||||
?? document.createExtension(KHRMaterialsVariants).setRequired(false);
|
||||
const byName = new Map(extension.listVariants().map((variant) => [variant.getName(), variant]));
|
||||
const getOrCreate = (name) => byName.get(name) ?? (byName.set(name, extension.createVariant(name)), byName.get(name));
|
||||
return { extension, getOrCreate };
|
||||
}
|
||||
|
||||
function addSurfaceMappings(document, primitive, carrierByName, variantByName, names = [HOTO_VARIANTS.floor, HOTO_VARIANTS.wall]) {
|
||||
if (primitive.getExtension(KHRMaterialsVariants.EXTENSION_NAME)) {
|
||||
throw new Error("Surface-quality policy will not replace an existing KHR_materials_variants mapping.");
|
||||
}
|
||||
const variants = document.getRoot().listExtensionsUsed().find((item) => item.extensionName === KHRMaterialsVariants.EXTENSION_NAME);
|
||||
const mappingList = variants.createMappingList();
|
||||
for (const name of names) {
|
||||
const carrier = carrierByName.get(name);
|
||||
const variant = variantByName.get(name);
|
||||
if (!carrier || !variant) throw new Error(`Missing surface-detail carrier or variant ${name}.`);
|
||||
mappingList.addMapping(variants.createMapping().setMaterial(carrier).addVariant(variant));
|
||||
}
|
||||
primitive.setExtension(KHRMaterialsVariants.EXTENSION_NAME, mappingList);
|
||||
}
|
||||
|
||||
function getOrCreateCarrier(document, sourceTexture, name, extras) {
|
||||
const root = document.getRoot();
|
||||
const found = root.listMaterials().find((material) => material.getName() === `${name}:source-tile-carrier`);
|
||||
if (found) {
|
||||
if (found.getBaseColorTexture() !== sourceTexture || found.getExtras()?.runewakerSurfaceDetailCarrier?.policyId !== extras.policyId) {
|
||||
throw new Error(`Existing surface-detail carrier ${name} does not match its pinned source.`);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
return document.createMaterial(`${name}:source-tile-carrier`)
|
||||
.setBaseColorTexture(sourceTexture)
|
||||
.setMetallicFactor(0)
|
||||
.setRoughnessFactor(1)
|
||||
.setExtras({ runewakerSurfaceDetailCarrier: extras });
|
||||
}
|
||||
|
||||
async function meanLinearFromPng(bytes) {
|
||||
const { data, info } = await sharp(bytes).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||
if (info.channels < 3 || info.width < 1 || info.height < 1) throw new Error("Surface-detail source image must contain RGB pixels.");
|
||||
const sums = [0, 0, 0];
|
||||
for (let offset = 0; offset < data.length; offset += info.channels) {
|
||||
for (let channel = 0; channel < 3; channel += 1) {
|
||||
const srgb = data[offset + channel] / 255;
|
||||
sums[channel] += srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
|
||||
}
|
||||
}
|
||||
const count = info.width * info.height;
|
||||
return sums.map((value) => value / count);
|
||||
}
|
||||
|
||||
function trianglePositions(matrix, position, indices) {
|
||||
return indices.map((index) => {
|
||||
const value = position.getElement(index, [0, 0, 0]);
|
||||
return [
|
||||
matrix[0] * value[0] + matrix[4] * value[1] + matrix[8] * value[2] + matrix[12],
|
||||
matrix[1] * value[0] + matrix[5] * value[1] + matrix[9] * value[2] + matrix[13],
|
||||
matrix[2] * value[0] + matrix[6] * value[1] + matrix[10] * value[2] + matrix[14],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function sub3(left, right) { return [left[0] - right[0], left[1] - right[1], left[2] - right[2]]; }
|
||||
function cross3(left, right) {
|
||||
return [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]];
|
||||
}
|
||||
function length3(value) { return Math.hypot(value[0], value[1], value[2]); }
|
||||
|
||||
const BARREN_PATCHES = Object.freeze([
|
||||
{ key: "u0-v0", col: 0, row: 0, rect: { left: 0, top: 0, width: 256, height: 256 } },
|
||||
{ key: "u3-v0", col: 3, row: 0, rect: { left: 768, top: 0, width: 256, height: 256 } },
|
||||
{ key: "u0-v1", col: 0, row: 1, rect: { left: 0, top: 256, width: 256, height: 256 } },
|
||||
{ key: "u3-v1", col: 3, row: 1, rect: { left: 768, top: 256, width: 256, height: 256 } },
|
||||
].map((patch) => Object.freeze({
|
||||
...patch,
|
||||
uvRect: { u0: patch.col / 4, u1: (patch.col + 1) / 4, v0: patch.row / 2, v1: (patch.row + 1) / 2 },
|
||||
})));
|
||||
|
||||
const BARREN_WALL_DETAIL_PATCH = Object.freeze({
|
||||
key: "wall-u1-v0",
|
||||
rect: Object.freeze({ left: 256, top: 0, width: 256, height: 256 }),
|
||||
textureName: "rom-barren-wretched-tunnel-wall-detail-u1-v0",
|
||||
});
|
||||
|
||||
function barrenPatchForTriangle(uvs, worldNormalY) {
|
||||
if (worldNormalY < 0.7) return null;
|
||||
const tolerance = 0.0025;
|
||||
const minU = Math.min(...uvs.map((uv) => uv[0]));
|
||||
const maxU = Math.max(...uvs.map((uv) => uv[0]));
|
||||
const minV = Math.min(...uvs.map((uv) => uv[1]));
|
||||
const maxV = Math.max(...uvs.map((uv) => uv[1]));
|
||||
return BARREN_PATCHES.find((patch) => minU >= patch.uvRect.u0 - tolerance && maxU <= patch.uvRect.u1 + tolerance
|
||||
&& minV >= patch.uvRect.v0 - tolerance && maxV <= patch.uvRect.v1 + tolerance)?.key ?? null;
|
||||
}
|
||||
|
||||
async function applyBarrenFloorPolicy(document, sourceReport) {
|
||||
verifySourceReport(sourceReport, BARREN_SOURCE);
|
||||
verifyNativeMaterial(sourceReport, BARREN_SOURCE.material, BARREN_SOURCE.texture);
|
||||
const root = document.getRoot();
|
||||
const sourceMaterial = root.listMaterials().find((material) => material.getName() === BARREN_SOURCE.material);
|
||||
if (!sourceMaterial) throw new Error("Barren atlas-safe policy cannot find native material 12.");
|
||||
const sourceTexture = sourceMaterial.getBaseColorTexture();
|
||||
const sourcePng = Buffer.from(sourceTexture?.getImage() ?? []);
|
||||
if (sourceTexture?.getName() !== BARREN_SOURCE.image || sha256(sourcePng) !== BARREN_SOURCE.imageSha256) {
|
||||
throw new Error("Barren material 12 embedded atlas differs from the pinned native prepared PNG.");
|
||||
}
|
||||
const imageMeta = await sharp(sourcePng).metadata();
|
||||
if (imageMeta.width !== 1024 || imageMeta.height !== 512) throw new Error("Barren atlas dimensions differ from the reviewed 1024×512 source.");
|
||||
const baseMeanLinear = await meanLinearFromPng(sourcePng);
|
||||
|
||||
// Migrate the previous whole-atlas x4 experiment back to authentic UV0
|
||||
// before splitting only faces proven to stay inside a native floor island.
|
||||
let legacyRepeatPrimitivesRestored = 0;
|
||||
for (const node of root.listNodes()) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) continue;
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
if (primitive.getExtras()?.romUvAdaptation?.id !== "rom-barren-caves-wretched-tunnel-repeat-x4") continue;
|
||||
const material = primitive.getMaterial();
|
||||
const uv0 = primitive.getAttribute("TEXCOORD_0");
|
||||
const uv1 = primitive.getAttribute("TEXCOORD_1");
|
||||
if (material?.getBaseColorTexture()?.getName() !== BARREN_SOURCE.image || !uv0 || !uv1
|
||||
|| uv0.getCount() !== uv1.getCount()) {
|
||||
throw new Error("Cannot safely restore an earlier Barren whole-atlas x4 primitive.");
|
||||
}
|
||||
for (let index = 0; index < uv0.getCount(); index += 1) {
|
||||
const original = uv0.getElement(index, [0, 0]);
|
||||
const adapted = uv1.getElement(index, [0, 0]);
|
||||
if (Math.abs(adapted[0] - original[0] * 4) > 1e-6 || Math.abs(adapted[1] - original[1] * 4) > 1e-6) {
|
||||
throw new Error("Earlier Barren repeat marker does not contain the expected x4 UV1 values.");
|
||||
}
|
||||
}
|
||||
const extras = { ...(primitive.getExtras() ?? {}) };
|
||||
delete extras.romUvAdaptation;
|
||||
primitive.setExtras(extras);
|
||||
primitive.setAttribute("TEXCOORD_1", null);
|
||||
primitive.setMaterial(sourceMaterial);
|
||||
legacyRepeatPrimitivesRestored += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const alreadyApplied = sourceMaterial.getExtras()?.runewakerSurfaceDetail?.policyId === BARREN_POLICY_ID;
|
||||
if (alreadyApplied) {
|
||||
const surfaceDetail = sourceMaterial.getExtras()?.runewakerSurfaceDetail;
|
||||
const cropTextures = root.listTextures().filter((texture) => texture.getExtras()?.romSourcePixelCrop?.policyId === BARREN_POLICY_ID);
|
||||
const cropMaterials = root.listMaterials().filter((material) => material.getExtras()?.romUvAdaptation?.id === BARREN_POLICY_ID);
|
||||
const expectedCropMaterialNames = new Set(BARREN_PATCHES.map((patch) => `${BARREN_SOURCE.material}_${patch.key}_crop_repeat4`));
|
||||
const actualCropMaterialNames = new Set(cropMaterials.map((material) => material.getName()));
|
||||
const wallTexture = root.listTextures().find((texture) => texture.getName() === BARREN_WALL_DETAIL_PATCH.textureName);
|
||||
const wallCarrier = root.listMaterials().find((material) => material.getName() === "rom-detail-barren-caves-wall:source-tile-carrier");
|
||||
const wallInfo = wallCarrier?.getBaseColorTextureInfo();
|
||||
if (surfaceDetail.wallBaseContrast !== 0.25 || surfaceDetail.wallStrength !== 0.85 || surfaceDetail.wallDetailVariation !== 1
|
||||
|| JSON.stringify(surfaceDetail.baseMeanLinear) !== JSON.stringify(baseMeanLinear)
|
||||
|| cropTextures.length !== BARREN_PATCHES.length + 1 || !wallTexture || wallCarrier?.getBaseColorTexture() !== wallTexture
|
||||
|| wallInfo?.getWrapS() !== 33648 || wallInfo?.getWrapT() !== 33648
|
||||
|| actualCropMaterialNames.size !== BARREN_PATCHES.length
|
||||
|| [...expectedCropMaterialNames].some((name) => !actualCropMaterialNames.has(name))) {
|
||||
throw new Error("Barren atlas-safe policy marker is present but its exact floor/wall crop carriers are incomplete.");
|
||||
}
|
||||
return {
|
||||
id: BARREN_POLICY_ID, status: "already-applied", cropTextureCount: cropTextures.length,
|
||||
cropMaterialCount: actualCropMaterialNames.size, cropMaterialInstanceCount: cropMaterials.length,
|
||||
wallDetailTextureName: wallTexture.getName(), wallDetailWrap: "mirrored-repeat",
|
||||
repeatFactor: 4, trianglePartitionChanged: true,
|
||||
sourceModel: BARREN_SOURCE.model, sourceModelSha256: BARREN_SOURCE.sha256,
|
||||
};
|
||||
}
|
||||
if (root.listTextures().some((texture) => texture.getExtras()?.romSourcePixelCrop?.policyId === BARREN_POLICY_ID)) {
|
||||
throw new Error("Barren source GLB already contains partial atlas-safe floor crops.");
|
||||
}
|
||||
const buffer = root.listBuffers()[0];
|
||||
if (!buffer) throw new Error("Barren canonical GLB has no binary buffer for UV1.");
|
||||
const cropped = new Map();
|
||||
for (const patch of BARREN_PATCHES) {
|
||||
const png = await sharp(sourcePng).extract(patch.rect).png().toBuffer();
|
||||
const rgba = await sharp(png).ensureAlpha().raw().toBuffer();
|
||||
const meanLinear = await meanLinearFromPng(png);
|
||||
const sourcePixelCrop = {
|
||||
version: 1,
|
||||
sourceImageName: BARREN_SOURCE.image,
|
||||
sourcePreparedPngSha256: BARREN_SOURCE.imageSha256,
|
||||
sourceWidth: 1024,
|
||||
sourceHeight: 512,
|
||||
rect: patch.rect,
|
||||
croppedPngSha256: sha256(png),
|
||||
croppedRgbaSha256: sha256(rgba),
|
||||
policyId: BARREN_POLICY_ID,
|
||||
};
|
||||
const texture = document.createTexture(`rom-barren-wretched-tunnel-${patch.key}`)
|
||||
.setMimeType("image/png")
|
||||
.setImage(png)
|
||||
.setExtras({ romSourcePixelCrop: sourcePixelCrop });
|
||||
cropped.set(patch.key, { patch, png, meanLinear, sourcePixelCrop, texture });
|
||||
}
|
||||
const wallDetailPng = await sharp(sourcePng).extract(BARREN_WALL_DETAIL_PATCH.rect).png().toBuffer();
|
||||
const wallDetailRgba = await sharp(wallDetailPng).ensureAlpha().raw().toBuffer();
|
||||
const wallDetailCrop = {
|
||||
version: 1,
|
||||
sourceImageName: BARREN_SOURCE.image,
|
||||
sourcePreparedPngSha256: BARREN_SOURCE.imageSha256,
|
||||
sourceWidth: 1024,
|
||||
sourceHeight: 512,
|
||||
rect: BARREN_WALL_DETAIL_PATCH.rect,
|
||||
croppedPngSha256: sha256(wallDetailPng),
|
||||
croppedRgbaSha256: sha256(wallDetailRgba),
|
||||
policyId: BARREN_POLICY_ID,
|
||||
};
|
||||
const wallDetailTexture = document.createTexture(BARREN_WALL_DETAIL_PATCH.textureName)
|
||||
.setMimeType("image/png")
|
||||
.setImage(wallDetailPng)
|
||||
.setExtras({ romSourcePixelCrop: wallDetailCrop });
|
||||
const wallDetailMeanLinear = await meanLinearFromPng(wallDetailPng);
|
||||
|
||||
const variantInfo = listVariants(document);
|
||||
const variantNames = ["rom-detail-barren-caves-floor", "rom-detail-barren-caves-wall"];
|
||||
const variantByName = new Map(variantNames.map((name) => [name, variantInfo.getOrCreate(name)]));
|
||||
const baseExtras = sourceMaterial.getExtras() ?? {};
|
||||
const cropForDetail = cropped.get("u0-v0");
|
||||
const sourceProof = {
|
||||
sourceModel: BARREN_SOURCE.model,
|
||||
sourceModelSha256: BARREN_SOURCE.sha256,
|
||||
sourceMaterial: BARREN_SOURCE.material,
|
||||
sourceTexture: BARREN_SOURCE.texture,
|
||||
sourceImageName: BARREN_SOURCE.image,
|
||||
sourcePreparedPngSha256: BARREN_SOURCE.imageSha256,
|
||||
sourcePixelCrop: cropForDetail.sourcePixelCrop,
|
||||
baseMeanLinear,
|
||||
wallBaseContrast: 0.25,
|
||||
wallDetailVariation: 1,
|
||||
wallDetailImageName: BARREN_WALL_DETAIL_PATCH.textureName,
|
||||
wallDetailSourcePixelCrop: wallDetailCrop,
|
||||
wallDetailMeanLinear,
|
||||
wallDetailWrap: "mirrored-repeat",
|
||||
};
|
||||
sourceMaterial.setExtras({
|
||||
...baseExtras,
|
||||
runewakerSurfaceDetail: {
|
||||
version: 1,
|
||||
floorVariant: variantNames[0],
|
||||
wallVariant: variantNames[1],
|
||||
periodMeters: 2,
|
||||
floorStrength: 0,
|
||||
wallStrength: 0.85,
|
||||
baseMeanLinear,
|
||||
wallBaseContrast: 0.25,
|
||||
wallDetailVariation: 1,
|
||||
floorMeanLinear: cropForDetail.meanLinear,
|
||||
wallMeanLinear: wallDetailMeanLinear,
|
||||
policyId: BARREN_POLICY_ID,
|
||||
sourceProof,
|
||||
},
|
||||
});
|
||||
const carrierExtras = { version: 1, policyId: BARREN_POLICY_ID, sourceImageName: BARREN_SOURCE.image, sourcePixelCrop: cropForDetail.sourcePixelCrop };
|
||||
const carriers = new Map([
|
||||
[variantNames[0], getOrCreateCarrier(document, cropForDetail.texture, variantNames[0], { ...carrierExtras, variantName: variantNames[0] })],
|
||||
[variantNames[1], getOrCreateCarrier(document, wallDetailTexture, variantNames[1], {
|
||||
...carrierExtras,
|
||||
variantName: variantNames[1],
|
||||
sourcePixelCrop: wallDetailCrop,
|
||||
sourceImageName: BARREN_WALL_DETAIL_PATCH.textureName,
|
||||
wrap: "mirrored-repeat",
|
||||
})],
|
||||
]);
|
||||
carriers.get(variantNames[1]).getBaseColorTextureInfo().setWrapS(33648).setWrapT(33648);
|
||||
|
||||
const reports = [];
|
||||
let adaptedTriangles = 0;
|
||||
let untouchedTriangles = 0;
|
||||
for (const node of root.listNodes()) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) continue;
|
||||
const matrix = node.getWorldMatrix();
|
||||
const originalPrimitives = [...mesh.listPrimitives()];
|
||||
for (const [primitiveIndex, primitive] of originalPrimitives.entries()) {
|
||||
if (primitive.getMaterial() !== sourceMaterial) continue;
|
||||
if (primitive.getExtension(KHRMaterialsVariants.EXTENSION_NAME)) throw new Error("Barren material 12 already has variant mappings.");
|
||||
const position = primitive.getAttribute("POSITION");
|
||||
const normal = primitive.getAttribute("NORMAL");
|
||||
const uv0 = primitive.getAttribute("TEXCOORD_0");
|
||||
const indices = primitive.getIndices();
|
||||
if (!position || !normal || !uv0 || !indices || uv0.getType() !== Accessor.Type.VEC2) {
|
||||
throw new Error(`${node.getName()} material 12 primitive ${primitiveIndex} lacks position/normal/index/UV0 data.`);
|
||||
}
|
||||
const indexArray = indices.getArray();
|
||||
const indicesByCrop = new Map(BARREN_PATCHES.map((patch) => [patch.key, []]));
|
||||
const untouched = [];
|
||||
for (let offset = 0; offset < indexArray.length; offset += 3) {
|
||||
const triangle = [indexArray[offset], indexArray[offset + 1], indexArray[offset + 2]];
|
||||
const points = trianglePositions(matrix, position, triangle);
|
||||
const cross = cross3(sub3(points[1], points[0]), sub3(points[2], points[0]));
|
||||
const magnitude = length3(cross);
|
||||
const normalY = magnitude > 1e-12 ? Math.abs(cross[1]) / magnitude : 0;
|
||||
const triangleUvs = triangle.map((index) => uv0.getElement(index, [0, 0]));
|
||||
const key = barrenPatchForTriangle(triangleUvs, normalY);
|
||||
(key ? indicesByCrop.get(key) : untouched).push(...triangle);
|
||||
}
|
||||
const activeCrops = [...indicesByCrop].filter(([, group]) => group.length > 0);
|
||||
if (!activeCrops.length) continue;
|
||||
const originalTriangleCount = indexArray.length / 3;
|
||||
const before = accessorHash(indices);
|
||||
const primitiveOutputs = [];
|
||||
if (untouched.length) {
|
||||
primitive.setIndices(document.createAccessor(`${node.getName()}:${BARREN_POLICY_ID}:untouched-indices`)
|
||||
.setType(Accessor.Type.SCALAR).setArray(new indexArray.constructor(untouched)).setBuffer(buffer));
|
||||
primitive.setExtension(KHRMaterialsVariants.EXTENSION_NAME, (() => {
|
||||
const current = document.getRoot().listExtensionsUsed().find((item) => item.extensionName === KHRMaterialsVariants.EXTENSION_NAME);
|
||||
const mappingList = current.createMappingList();
|
||||
for (const name of variantNames) mappingList.addMapping(current.createMapping().setMaterial(carriers.get(name)).addVariant(variantByName.get(name)));
|
||||
return mappingList;
|
||||
})());
|
||||
primitiveOutputs.push({ primitive, triangles: untouched.length / 3, category: "untouched-atlas" });
|
||||
} else mesh.removePrimitive(primitive);
|
||||
for (const [key, group] of activeCrops) {
|
||||
const { patch, texture, sourcePixelCrop } = cropped.get(key);
|
||||
const adaptedMaterial = sourceMaterial.clone()
|
||||
.setName(`${BARREN_SOURCE.material}_${key}_crop_repeat4`)
|
||||
.setBaseColorTexture(texture);
|
||||
adaptedMaterial.getBaseColorTextureInfo().setTexCoord(1).setWrapS(10497).setWrapT(10497);
|
||||
adaptedMaterial.setExtras({
|
||||
...baseExtras,
|
||||
romUvAdaptation: {
|
||||
version: 1, id: BARREN_POLICY_ID, atlasRect: patch.rect, uvRect: patch.uvRect,
|
||||
repeatFactor: 4, nativeBaseTexture: BARREN_SOURCE.image, nativeBaseTexturePngSha256: BARREN_SOURCE.imageSha256,
|
||||
sourcePixelCrop,
|
||||
},
|
||||
});
|
||||
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] - patch.uvRect.u0) / (patch.uvRect.u1 - patch.uvRect.u0)) * 4;
|
||||
values[index * 2 + 1] = ((uv[1] - patch.uvRect.v0) / (patch.uvRect.v1 - patch.uvRect.v0)) * 4;
|
||||
}
|
||||
const uv1 = document.createAccessor(`${node.getName()}:${BARREN_POLICY_ID}:${key}:TEXCOORD_1`)
|
||||
.setType(Accessor.Type.VEC2).setArray(values).setBuffer(buffer);
|
||||
const adapted = primitive.clone()
|
||||
.setIndices(document.createAccessor(`${node.getName()}:${BARREN_POLICY_ID}:${key}:indices`)
|
||||
.setType(Accessor.Type.SCALAR).setArray(new indexArray.constructor(group)).setBuffer(buffer))
|
||||
.setAttribute("TEXCOORD_1", uv1)
|
||||
.setMaterial(adaptedMaterial)
|
||||
.setExtras({
|
||||
...(primitive.getExtras() ?? {}),
|
||||
romUvAdaptation: {
|
||||
version: 1, id: BARREN_POLICY_ID, atlasRect: patch.rect, uvRect: patch.uvRect, repeatFactor: 4,
|
||||
sourceMaterial: BARREN_SOURCE.material, sourceTexture: BARREN_SOURCE.image,
|
||||
sourcePngSha256: BARREN_SOURCE.imageSha256, sourcePixelCrop,
|
||||
},
|
||||
});
|
||||
const variants = document.getRoot().listExtensionsUsed().find((item) => item.extensionName === KHRMaterialsVariants.EXTENSION_NAME);
|
||||
const mappings = variants.createMappingList();
|
||||
for (const name of variantNames) mappings.addMapping(variants.createMapping().setMaterial(carriers.get(name)).addVariant(variantByName.get(name)));
|
||||
adapted.setExtension(KHRMaterialsVariants.EXTENSION_NAME, mappings);
|
||||
mesh.addPrimitive(adapted);
|
||||
primitiveOutputs.push({ primitive: adapted, triangles: group.length / 3, category: key });
|
||||
adaptedTriangles += group.length / 3;
|
||||
}
|
||||
untouchedTriangles += untouched.length / 3;
|
||||
const outputTriangles = primitiveOutputs.reduce((sum, item) => sum + item.triangles, 0);
|
||||
if (outputTriangles !== originalTriangleCount) throw new Error(`Barren triangle partition failed on ${node.getName()} primitive ${primitiveIndex}.`);
|
||||
for (const { primitive: outputPrimitive } of primitiveOutputs) {
|
||||
if (outputPrimitive.getAttribute("POSITION") !== position || outputPrimitive.getAttribute("NORMAL") !== normal
|
||||
|| outputPrimitive.getAttribute("TEXCOORD_0") !== uv0) {
|
||||
throw new Error(`Barren UV policy changed source position/normal/UV0 accessors on ${node.getName()}.`);
|
||||
}
|
||||
}
|
||||
reports.push({
|
||||
node: node.getName(), sourcePrimitiveIndex: primitiveIndex, sourceTriangles: originalTriangleCount,
|
||||
sourceIndexHash: before, sourcePositionHash: accessorHash(position), sourceNormalHash: accessorHash(normal),
|
||||
sourceUv0Hash: accessorHash(uv0),
|
||||
outputGroups: primitiveOutputs.map(({ category, triangles }) => ({ category, triangles })),
|
||||
triangleCoverageExact: true, positionNormalUv0AccessorsShared: true, indexStreamLayoutChanged: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!reports.length) throw new Error("Barren atlas-safe policy found no floor triangles in the four reviewed source-image quadrants.");
|
||||
const patchReports = [...cropped.values()].map(({ patch, png, sourcePixelCrop, meanLinear }) => ({
|
||||
key: patch.key, sourcePixelRect: patch.rect, sourceUvRect: patch.uvRect, repeatFactor: 4,
|
||||
croppedPngSha256: sourcePixelCrop.croppedPngSha256, croppedRgbaSha256: sourcePixelCrop.croppedRgbaSha256,
|
||||
bytes: png.length, meanLinear,
|
||||
}));
|
||||
patchReports.push({
|
||||
key: BARREN_WALL_DETAIL_PATCH.key,
|
||||
role: "wall-detail-carrier",
|
||||
sourcePixelRect: BARREN_WALL_DETAIL_PATCH.rect,
|
||||
repeatMode: "mirrored-repeat",
|
||||
croppedPngSha256: wallDetailCrop.croppedPngSha256,
|
||||
croppedRgbaSha256: wallDetailCrop.croppedRgbaSha256,
|
||||
bytes: wallDetailPng.length,
|
||||
meanLinear: wallDetailMeanLinear,
|
||||
});
|
||||
return {
|
||||
id: BARREN_POLICY_ID,
|
||||
status: "applied",
|
||||
sourceModel: BARREN_SOURCE.model,
|
||||
sourceModelSha256: BARREN_SOURCE.sha256,
|
||||
sourceMaterial: BARREN_SOURCE.material,
|
||||
sourceTexture: BARREN_SOURCE.texture,
|
||||
sourceImage: BARREN_SOURCE.image,
|
||||
sourceImageSha256: BARREN_SOURCE.imageSha256,
|
||||
sourceImageDimensions: [imageMeta.width, imageMeta.height],
|
||||
repeatFactor: 4,
|
||||
worldNormalYThreshold: 0.7,
|
||||
atlasSeamToleranceUv: 0.0025,
|
||||
trianglePolicy: "floor triangles whose UV0 corners remain inside a reviewed 256x256 floor island receive crop-localized repeat UV1; other faces keep the full native atlas",
|
||||
trianglePartitionChanged: true,
|
||||
legacyRepeatPrimitivesRestored,
|
||||
sourcePositionNormalUv0Preserved: true,
|
||||
untouchedTriangles,
|
||||
adaptedTriangles,
|
||||
primitiveReports: reports,
|
||||
crops: patchReports,
|
||||
wallDetail: {
|
||||
textureName: BARREN_WALL_DETAIL_PATCH.textureName,
|
||||
sourcePixelCrop: wallDetailCrop,
|
||||
meanLinear: wallDetailMeanLinear,
|
||||
samplerWrap: "mirrored-repeat",
|
||||
},
|
||||
wallBaseContrast: 0.25,
|
||||
wallDetailVariation: 1,
|
||||
baseMeanLinear,
|
||||
variants: variantNames,
|
||||
};
|
||||
}
|
||||
|
||||
async function applyHotoDetailPolicy(document, sourceReport) {
|
||||
verifySourceReport(sourceReport, HOTO_SOURCE);
|
||||
for (const target of Object.values(HOTO_TARGETS)) verifyNativeMaterial(sourceReport, target.nativeMaterial, target.texture);
|
||||
const root = document.getRoot();
|
||||
const sourceTextures = new Map();
|
||||
const targetMaterials = new Map();
|
||||
for (const [materialName, target] of Object.entries(HOTO_TARGETS)) {
|
||||
const material = root.listMaterials().find((item) => item.getName() === materialName);
|
||||
if (!material) throw new Error(`HOTO surface policy cannot find canonical material ${materialName}.`);
|
||||
const texture = material.getBaseColorTexture();
|
||||
const image = Buffer.from(texture?.getImage() ?? []);
|
||||
if (texture?.getName() !== target.image || sha256(image) !== target.imageSha256) {
|
||||
throw new Error(`HOTO ${materialName} base atlas/tile is not the pinned source PNG.`);
|
||||
}
|
||||
if (material.getNormalTexture() || material.getMetallicRoughnessTexture() || material.getEmissiveTexture()) {
|
||||
throw new Error(`HOTO surface policy refuses unexpected auxiliary native texture slots on ${materialName}.`);
|
||||
}
|
||||
targetMaterials.set(materialName, material);
|
||||
sourceTextures.set(materialName, texture);
|
||||
}
|
||||
const detailSources = new Map();
|
||||
for (const [role, detail] of Object.entries(HOTO_DETAIL)) {
|
||||
const material = targetMaterials.get(detail.canonicalMaterial);
|
||||
const texture = sourceTextures.get(detail.canonicalMaterial);
|
||||
if (texture?.getName() !== detail.image || sha256(texture.getImage() ?? Buffer.alloc(0)) !== detail.imageSha256) {
|
||||
throw new Error(`HOTO ${role} detail tile is not the native, byte-identical image.`);
|
||||
}
|
||||
detailSources.set(role, { ...detail, sourceTexturePath: detail.texture, textureObject: texture, meanLinear: await meanLinearFromPng(Buffer.from(texture.getImage())) });
|
||||
}
|
||||
const expectedByMaterial = new Map();
|
||||
const detailProofs = {};
|
||||
for (const [role, detail] of detailSources) {
|
||||
detailProofs[role] = {
|
||||
nativeMaterial: detail.nativeMaterial,
|
||||
canonicalMaterialName: detail.canonicalMaterial,
|
||||
sourceTexture: detail.sourceTexturePath,
|
||||
sourceImage: detail.image,
|
||||
sourcePreparedPngSha256: detail.imageSha256,
|
||||
meanLinear: detail.meanLinear,
|
||||
};
|
||||
}
|
||||
for (const [materialName, target] of Object.entries(HOTO_TARGETS)) {
|
||||
const material = targetMaterials.get(materialName);
|
||||
const baseTexture = sourceTextures.get(materialName);
|
||||
const baseMeanLinear = target.useOwnBaseMean ? await meanLinearFromPng(Buffer.from(baseTexture.getImage())) : undefined;
|
||||
const floorRole = target.floorVariantRole ?? "floor";
|
||||
const sourceProof = {
|
||||
sourceModel: HOTO_SOURCE.model,
|
||||
sourceModelSha256: HOTO_SOURCE.sha256,
|
||||
sourceMaterial: target.nativeMaterial,
|
||||
canonicalMaterialName: materialName,
|
||||
sourceTexture: target.texture,
|
||||
sourceTextureName: target.image,
|
||||
sourcePreparedPngSha256: target.imageSha256,
|
||||
floorDetailSource: detailProofs[floorRole],
|
||||
wallDetailSource: detailProofs.wall,
|
||||
floorVariantRole: floorRole,
|
||||
floorBaseStrength: target.floorBaseStrength,
|
||||
floorStrength: target.floorStrength,
|
||||
wallStrength: target.wallStrength,
|
||||
wallBaseContrast: target.wallBaseContrast,
|
||||
wallDetailVariation: target.wallDetailVariation,
|
||||
};
|
||||
const metadata = {
|
||||
version: 1,
|
||||
floorVariant: HOTO_VARIANTS[floorRole],
|
||||
wallVariant: HOTO_VARIANTS.wall,
|
||||
periodMeters: 2,
|
||||
floorBaseStrength: target.floorBaseStrength,
|
||||
floorStrength: target.floorStrength,
|
||||
wallStrength: target.wallStrength,
|
||||
floorMeanLinear: detailSources.get(floorRole).meanLinear,
|
||||
wallMeanLinear: detailSources.get("wall").meanLinear,
|
||||
wallBaseContrast: target.wallBaseContrast,
|
||||
wallDetailVariation: target.wallDetailVariation,
|
||||
policyId: HOTO_POLICY_ID,
|
||||
sourceProof,
|
||||
};
|
||||
if (baseMeanLinear) metadata.baseMeanLinear = baseMeanLinear;
|
||||
expectedByMaterial.set(materialName, metadata);
|
||||
}
|
||||
|
||||
const carrierNames = new Map(Object.entries(HOTO_VARIANTS).map(([role, variantName]) => [variantName, role]));
|
||||
const existingMaterialPolicies = [...targetMaterials.values()].map((material) => material.getExtras()?.runewakerSurfaceDetail?.policyId ?? null);
|
||||
const hasExistingPolicy = existingMaterialPolicies.some(Boolean);
|
||||
if (hasExistingPolicy) {
|
||||
if (existingMaterialPolicies.some((id) => id !== HOTO_POLICY_ID && id !== HOTO_LEGACY_POLICY_ID)) {
|
||||
throw new Error("HOTO surface metadata contains an unknown or partial policy marker.");
|
||||
}
|
||||
const existingPolicy = existingMaterialPolicies[0];
|
||||
if (!existingPolicy || existingMaterialPolicies.some((id) => id !== existingPolicy)) {
|
||||
throw new Error("HOTO source-rock detail policy is only partially applied.");
|
||||
}
|
||||
const extension = root.listExtensionsUsed().find((item) => item.extensionName === KHRMaterialsVariants.EXTENSION_NAME);
|
||||
const variantByName = new Map((extension?.listVariants() ?? []).map((variant) => [variant.getName(), variant]));
|
||||
if (!extension || [...carrierNames.keys()].some((name) => !variantByName.has(name))) {
|
||||
throw new Error("HOTO source-rock detail policy marker exists but its material variants are absent.");
|
||||
}
|
||||
const carrierByName = new Map();
|
||||
for (const [variantName, role] of carrierNames) {
|
||||
const detail = detailSources.get(role);
|
||||
const carrier = root.listMaterials().find((material) => material.getName() === `${variantName}:source-tile-carrier`);
|
||||
const carrierProof = carrier?.getExtras()?.runewakerSurfaceDetailCarrier;
|
||||
if (!carrier || carrier.getBaseColorTexture() !== detail.textureObject
|
||||
|| carrierProof?.policyId !== existingPolicy
|
||||
|| carrierProof?.sourceResourceSha256 !== HOTO_SOURCE.sha256
|
||||
|| carrierProof?.sourceTextureName !== detail.image
|
||||
|| carrierProof?.sourcePreparedPngSha256 !== detail.imageSha256) {
|
||||
throw new Error(`HOTO ${variantName} carrier does not match the pinned source tile/policy.`);
|
||||
}
|
||||
carrierByName.set(variantName, carrier);
|
||||
}
|
||||
let mappedPrimitiveCount = 0;
|
||||
for (const node of root.listNodes()) for (const primitive of node.getMesh()?.listPrimitives() ?? []) {
|
||||
const materialName = primitive.getMaterial()?.getName();
|
||||
if (!targetMaterials.has(materialName)) continue;
|
||||
const mappingList = primitive.getExtension(KHRMaterialsVariants.EXTENSION_NAME)?.listMappings() ?? [];
|
||||
const mappedNames = new Set();
|
||||
for (const mapping of mappingList) {
|
||||
if (mapping.listVariants().length !== 1) throw new Error(`HOTO ${materialName} has a bundled or ambiguous surface-detail mapping.`);
|
||||
const variantName = mapping.listVariants()[0].getName();
|
||||
if (!carrierByName.has(variantName) || mapping.getMaterial() !== carrierByName.get(variantName) || mappedNames.has(variantName)) {
|
||||
throw new Error(`HOTO ${materialName} surface-detail mapping does not point to its verified source carrier.`);
|
||||
}
|
||||
mappedNames.add(variantName);
|
||||
}
|
||||
if (mappedNames.size !== 2 || [...carrierNames.keys()].some((name) => !mappedNames.has(name))) {
|
||||
throw new Error(`HOTO ${materialName} is missing one of the pinned source-detail mappings.`);
|
||||
}
|
||||
mappedPrimitiveCount += 1;
|
||||
}
|
||||
if (mappedPrimitiveCount !== 36) throw new Error(`HOTO source-rock detail mappings are incomplete: expected 36, found ${mappedPrimitiveCount}.`);
|
||||
|
||||
if (existingPolicy === HOTO_LEGACY_POLICY_ID) {
|
||||
for (const [materialName, target] of Object.entries(HOTO_TARGETS)) {
|
||||
const old = targetMaterials.get(materialName).getExtras()?.runewakerSurfaceDetail;
|
||||
const proof = old?.sourceProof;
|
||||
if (old?.version !== 1 || old.floorVariant !== HOTO_VARIANTS.floor || old.wallVariant !== HOTO_VARIANTS.wall
|
||||
|| old.periodMeters !== 2 || old.floorStrength !== 0.65 || old.floorBaseStrength !== undefined && old.floorBaseStrength !== 0
|
||||
|| old.wallStrength !== (target.legacyWallStrength ?? target.wallStrength)
|
||||
|| old.wallBaseContrast !== undefined && old.wallBaseContrast !== 1
|
||||
|| old.wallDetailVariation !== undefined && old.wallDetailVariation !== 0
|
||||
|| JSON.stringify(old.floorMeanLinear) !== JSON.stringify(detailSources.get("floor").meanLinear)
|
||||
|| JSON.stringify(old.wallMeanLinear) !== JSON.stringify(detailSources.get("wall").meanLinear)
|
||||
|| proof?.sourceModelSha256 !== HOTO_SOURCE.sha256 || proof?.canonicalMaterialName !== materialName
|
||||
|| proof?.sourceMaterial !== target.nativeMaterial || proof?.sourceTexture !== target.texture
|
||||
|| proof?.sourceTextureName !== target.image || proof?.sourcePreparedPngSha256 !== target.imageSha256) {
|
||||
throw new Error(`HOTO legacy v1 metadata on ${materialName} is not the reviewed migration source.`);
|
||||
}
|
||||
}
|
||||
for (const carrier of carrierByName.values()) {
|
||||
const extras = carrier.getExtras() ?? {};
|
||||
carrier.setExtras({ ...extras, runewakerSurfaceDetailCarrier: { ...extras.runewakerSurfaceDetailCarrier, policyId: HOTO_POLICY_ID } });
|
||||
}
|
||||
for (const [materialName, metadata] of expectedByMaterial) {
|
||||
const material = targetMaterials.get(materialName);
|
||||
material.setExtras({ ...(material.getExtras() ?? {}), runewakerSurfaceDetail: metadata });
|
||||
}
|
||||
return { id: HOTO_POLICY_ID, status: "migrated-v1", migratedFrom: HOTO_LEGACY_POLICY_ID, mappedPrimitiveCount, materials: Object.keys(HOTO_TARGETS) };
|
||||
}
|
||||
|
||||
for (const [variantName, role] of carrierNames) {
|
||||
const detail = detailSources.get(role);
|
||||
const proof = carrierByName.get(variantName).getExtras()?.runewakerSurfaceDetailCarrier;
|
||||
if (proof?.variantName !== variantName || proof?.sourceMaterial !== detail.nativeMaterial
|
||||
|| proof?.canonicalMaterialName !== detail.canonicalMaterial || proof?.sourceTexturePath !== detail.texture) {
|
||||
throw new Error(`HOTO ${variantName} carrier provenance does not match the pinned source.`);
|
||||
}
|
||||
}
|
||||
for (const [materialName, metadata] of expectedByMaterial) {
|
||||
const existing = targetMaterials.get(materialName).getExtras()?.runewakerSurfaceDetail;
|
||||
if (JSON.stringify(existing) !== JSON.stringify(metadata)) throw new Error(`HOTO v2 surface metadata on ${materialName} differs from the reviewed policy.`);
|
||||
}
|
||||
return { id: HOTO_POLICY_ID, status: "already-applied", mappedPrimitiveCount, materials: Object.keys(HOTO_TARGETS) };
|
||||
}
|
||||
|
||||
if (root.listMaterials().some((material) => material.getExtras()?.runewakerSurfaceDetailCarrier?.policyId === HOTO_LEGACY_POLICY_ID
|
||||
|| material.getExtras()?.runewakerSurfaceDetailCarrier?.policyId === HOTO_POLICY_ID)) {
|
||||
throw new Error("HOTO source-tile carrier markers exist without the complete source-detail policy.");
|
||||
}
|
||||
const { getOrCreate } = listVariants(document);
|
||||
const variantByName = new Map(Object.values(HOTO_VARIANTS).map((name) => [name, getOrCreate(name)]));
|
||||
const carrierByName = new Map();
|
||||
for (const [role, detail] of detailSources) {
|
||||
const variantName = HOTO_VARIANTS[role];
|
||||
const extras = {
|
||||
version: 1, variantName, policyId: HOTO_POLICY_ID, sourceResourceSha256: HOTO_SOURCE.sha256,
|
||||
sourceMaterial: detail.nativeMaterial, canonicalMaterialName: detail.canonicalMaterial,
|
||||
sourceTexturePath: detail.sourceTexturePath, sourceTextureName: detail.image, sourcePreparedPngSha256: detail.imageSha256,
|
||||
};
|
||||
carrierByName.set(variantName, getOrCreateCarrier(document, detail.textureObject, variantName, extras));
|
||||
}
|
||||
let mappedPrimitiveCount = 0;
|
||||
for (const node of root.listNodes()) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) continue;
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
const materialName = primitive.getMaterial()?.getName();
|
||||
if (targetMaterials.get(materialName) !== primitive.getMaterial()) continue;
|
||||
addSurfaceMappings(document, primitive, carrierByName, variantByName);
|
||||
mappedPrimitiveCount += 1;
|
||||
}
|
||||
}
|
||||
if (mappedPrimitiveCount !== 36) throw new Error(`HOTO source-rock detail target count changed: expected 36, found ${mappedPrimitiveCount}.`);
|
||||
for (const [materialName, metadata] of expectedByMaterial) {
|
||||
const material = targetMaterials.get(materialName);
|
||||
material.setExtras({ ...(material.getExtras() ?? {}), runewakerSurfaceDetail: metadata });
|
||||
}
|
||||
return {
|
||||
id: HOTO_POLICY_ID,
|
||||
status: "applied",
|
||||
sourceModel: HOTO_SOURCE.model,
|
||||
sourceModelSha256: HOTO_SOURCE.sha256,
|
||||
detail: { periodMeters: 2, sourceSpace: "browser-adapted world-space detail; native base UV0 retained" },
|
||||
detailSources: detailProofs,
|
||||
materials: Object.keys(HOTO_TARGETS).map((name) => ({ name, metadata: expectedByMaterial.get(name) })),
|
||||
mappedPrimitiveCount,
|
||||
sourceGeometryChanged: false,
|
||||
nativeBaseTexturesAndMaterialSemanticsPreserved: true,
|
||||
migration: "v1 metadata migrates only when every target material, tile carrier and variant mapping matches the reviewed legacy configuration",
|
||||
};
|
||||
}
|
||||
|
||||
/** Source-pinned atlas-safe crops and native source-rock detail carriers. */
|
||||
export async function applyRunewakerSurfaceQualityPolicies(document, { dungeonId, policyIds = [], sourceReport } = {}) {
|
||||
if (new Set(policyIds).size !== policyIds.length) throw new Error("RoM surface-quality policy IDs must be unique.");
|
||||
const results = [];
|
||||
for (const policyId of policyIds) {
|
||||
if (dungeonId === "barren-caves" && policyId === BARREN_POLICY_ID) results.push(await applyBarrenFloorPolicy(document, sourceReport));
|
||||
else if (dungeonId === "hoto" && policyId === HOTO_POLICY_ID) results.push(await applyHotoDetailPolicy(document, sourceReport));
|
||||
else throw new Error(`Unknown or mismatched RoM surface-quality policy ${policyId} for ${dungeonId}.`);
|
||||
}
|
||||
return { schemaVersion: 1, dungeonId, status: policyIds.length ? "applied" : "no-policy", policies: results };
|
||||
}
|
||||
|
||||
export const runewakerSurfaceQualityPolicyIds = Object.freeze({ barrenCaves: BARREN_POLICY_ID, hoto: HOTO_POLICY_ID });
|
||||
Reference in new issue
Block a user