Update Healer Man server
This commit is contained in:
1 parent
69809f7228
commit
ff20ab8779
101 files changed
+16713
-945
No files matched your search
@@ -302,7 +302,9 @@ function textureSizeByIndex(json, images) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function auditGeometry(file, textureSizes, label = path.basename(file)) {
|
||||
export async function auditGeometry(file, textureSizes, label = path.basename(file), options = {}) {
|
||||
const hotspotCellSizeM = Number(options.hotspotCellSizeM);
|
||||
const collectHotspots = Number.isFinite(hotspotCellSizeM) && hotspotCellSizeM > 0;
|
||||
const io = new NodeIO()
|
||||
.registerExtensions(ALL_EXTENSIONS)
|
||||
.registerDependencies({ "meshopt.decoder": MeshoptDecoder });
|
||||
@@ -325,6 +327,7 @@ export async function auditGeometry(file, textureSizes, label = path.basename(fi
|
||||
untexturedMaterials: {},
|
||||
};
|
||||
const bySurface = { floor: emptySurfaceMetrics(), wall: emptySurfaceMetrics(), other: emptySurfaceMetrics() };
|
||||
const hotspotCells = new Map();
|
||||
for (const scene of document.getRoot().listScenes()) {
|
||||
const sceneNodes = [];
|
||||
const visit = (node) => {
|
||||
@@ -400,19 +403,59 @@ export async function auditGeometry(file, textureSizes, label = path.basename(fi
|
||||
info.getExtension("KHR_texture_transform"),
|
||||
));
|
||||
const mapping = mappingMetrics(points, uvValues, size[0], size[1]);
|
||||
if (!mapping || mapping.uvDoubleArea <= 1e-12) {
|
||||
const uvDegenerate = !mapping || mapping.uvDoubleArea <= 1e-12;
|
||||
const rankCollapsed = uvDegenerate || mapping.smallestStretchPxPerMeter <= 0.01;
|
||||
if (uvDegenerate) {
|
||||
for (const metric of [totals, surface]) {
|
||||
metric.uvDegenerateTriangles += 1;
|
||||
metric.rankCollapsedTriangles += 1;
|
||||
metric.uvDegenerateAreaM2 += area;
|
||||
metric.rankCollapsedAreaM2 += area;
|
||||
}
|
||||
} else if (mapping.smallestStretchPxPerMeter <= 0.01) {
|
||||
} else if (rankCollapsed) {
|
||||
for (const metric of [totals, surface]) {
|
||||
metric.rankCollapsedTriangles += 1;
|
||||
metric.rankCollapsedAreaM2 += area;
|
||||
}
|
||||
}
|
||||
if (collectHotspots && rankCollapsed) {
|
||||
const centroid = [0, 1, 2].map((axis) => points.reduce((sum, point) => sum + point[axis], 0) / 3);
|
||||
const cellX = Math.floor(centroid[0] / hotspotCellSizeM);
|
||||
const cellZ = Math.floor(centroid[2] / hotspotCellSizeM);
|
||||
const key = `${cellX}:${cellZ}`;
|
||||
const cell = hotspotCells.get(key) ?? {
|
||||
cellX,
|
||||
cellZ,
|
||||
cellSizeM: hotspotCellSizeM,
|
||||
uvDegenerateTriangles: 0,
|
||||
rankCollapsedTriangles: 0,
|
||||
uvDegenerateAreaM2: 0,
|
||||
rankCollapsedAreaM2: 0,
|
||||
centroidAreaWeight: 0,
|
||||
centroidAreaSum: [0, 0, 0],
|
||||
bySurface: {},
|
||||
};
|
||||
const surfaceCell = cell.bySurface[surfaceKey] ?? {
|
||||
uvDegenerateTriangles: 0,
|
||||
rankCollapsedTriangles: 0,
|
||||
uvDegenerateAreaM2: 0,
|
||||
rankCollapsedAreaM2: 0,
|
||||
};
|
||||
cell.rankCollapsedTriangles += 1;
|
||||
cell.rankCollapsedAreaM2 += area;
|
||||
surfaceCell.rankCollapsedTriangles += 1;
|
||||
surfaceCell.rankCollapsedAreaM2 += area;
|
||||
if (uvDegenerate) {
|
||||
cell.uvDegenerateTriangles += 1;
|
||||
cell.uvDegenerateAreaM2 += area;
|
||||
surfaceCell.uvDegenerateTriangles += 1;
|
||||
surfaceCell.uvDegenerateAreaM2 += area;
|
||||
}
|
||||
cell.centroidAreaWeight += area;
|
||||
for (let axis = 0; axis < 3; axis += 1) cell.centroidAreaSum[axis] += centroid[axis] * area;
|
||||
cell.bySurface[surfaceKey] = surfaceCell;
|
||||
hotspotCells.set(key, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,7 +471,31 @@ export async function auditGeometry(file, textureSizes, label = path.basename(fi
|
||||
for (const material of Object.values(materialCoverage.untexturedMaterials)) {
|
||||
material.worldAreaM2 = round(material.worldAreaM2, 2);
|
||||
}
|
||||
return { ...totals, bySurface, materialCoverage };
|
||||
const uvHotspots = collectHotspots
|
||||
? [...hotspotCells.values()]
|
||||
.map((cell) => ({
|
||||
cellX: cell.cellX,
|
||||
cellZ: cell.cellZ,
|
||||
cellSizeM: cell.cellSizeM,
|
||||
center: [
|
||||
round((cell.cellX + 0.5) * cell.cellSizeM, 2),
|
||||
round(cell.centroidAreaSum[1] / cell.centroidAreaWeight, 2),
|
||||
round((cell.cellZ + 0.5) * cell.cellSizeM, 2),
|
||||
],
|
||||
uvDegenerateTriangles: cell.uvDegenerateTriangles,
|
||||
rankCollapsedTriangles: cell.rankCollapsedTriangles,
|
||||
uvDegenerateAreaM2: round(cell.uvDegenerateAreaM2, 2),
|
||||
rankCollapsedAreaM2: round(cell.rankCollapsedAreaM2, 2),
|
||||
bySurface: Object.fromEntries(Object.entries(cell.bySurface).map(([surface, metric]) => [surface, {
|
||||
uvDegenerateTriangles: metric.uvDegenerateTriangles,
|
||||
rankCollapsedTriangles: metric.rankCollapsedTriangles,
|
||||
uvDegenerateAreaM2: round(metric.uvDegenerateAreaM2, 2),
|
||||
rankCollapsedAreaM2: round(metric.rankCollapsedAreaM2, 2),
|
||||
}])),
|
||||
}))
|
||||
.sort((left, right) => right.rankCollapsedAreaM2 - left.rankCollapsedAreaM2 || left.cellX - right.cellX || left.cellZ - right.cellZ)
|
||||
: undefined;
|
||||
return { ...totals, bySurface, materialCoverage, ...(uvHotspots ? { uvHotspots } : {}) };
|
||||
}
|
||||
|
||||
function compareImages(source, shipped) {
|
||||
@@ -479,6 +546,39 @@ function delta(after, before, key) {
|
||||
return round(after[key] - before[key], 2);
|
||||
}
|
||||
|
||||
function compareUvHotspots(sourceCells = [], runtimeCells = []) {
|
||||
const sourceByCell = new Map(sourceCells.map((cell) => [`${cell.cellX}:${cell.cellZ}`, cell]));
|
||||
const runtimeByCell = new Map(runtimeCells.map((cell) => [`${cell.cellX}:${cell.cellZ}`, cell]));
|
||||
const keys = new Set([...sourceByCell.keys(), ...runtimeByCell.keys()]);
|
||||
return [...keys].map((key) => {
|
||||
const source = sourceByCell.get(key);
|
||||
const runtime = runtimeByCell.get(key);
|
||||
const center = runtime?.center ?? source?.center;
|
||||
const sourceArea = source?.rankCollapsedAreaM2 ?? 0;
|
||||
const runtimeArea = runtime?.rankCollapsedAreaM2 ?? 0;
|
||||
const sourceUvArea = source?.uvDegenerateAreaM2 ?? 0;
|
||||
const runtimeUvArea = runtime?.uvDegenerateAreaM2 ?? 0;
|
||||
return {
|
||||
cellX: runtime?.cellX ?? source.cellX,
|
||||
cellZ: runtime?.cellZ ?? source.cellZ,
|
||||
cellSizeM: runtime?.cellSizeM ?? source.cellSizeM,
|
||||
center,
|
||||
rankCollapsedAreaM2Delta: round(runtimeArea - sourceArea, 2),
|
||||
uvDegenerateAreaM2Delta: round(runtimeUvArea - sourceUvArea, 2),
|
||||
rankCollapsedTrianglesDelta: (runtime?.rankCollapsedTriangles ?? 0) - (source?.rankCollapsedTriangles ?? 0),
|
||||
uvDegenerateTrianglesDelta: (runtime?.uvDegenerateTriangles ?? 0) - (source?.uvDegenerateTriangles ?? 0),
|
||||
sourceRankCollapsedAreaM2: sourceArea,
|
||||
runtimeRankCollapsedAreaM2: runtimeArea,
|
||||
sourceUvDegenerateAreaM2: sourceUvArea,
|
||||
runtimeUvDegenerateAreaM2: runtimeUvArea,
|
||||
};
|
||||
})
|
||||
.filter((cell) => cell.rankCollapsedAreaM2Delta > 0 || cell.uvDegenerateAreaM2Delta > 0)
|
||||
.sort((left, right) => right.rankCollapsedAreaM2Delta - left.rankCollapsedAreaM2Delta
|
||||
|| right.uvDegenerateAreaM2Delta - left.uvDegenerateAreaM2Delta)
|
||||
.slice(0, 50);
|
||||
}
|
||||
|
||||
function resolvePublishedPath(url) {
|
||||
if (typeof url !== "string" || !url.startsWith("/assets/game/")) return null;
|
||||
const file = path.resolve(publicRoot, url.replace(/^\//u, ""));
|
||||
@@ -511,7 +611,7 @@ async function readWowDefinitions() {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
async function auditDungeon(definition, io, visualOverride = null) {
|
||||
async function auditDungeon(definition, io, visualOverride = null, hotspotCellSizeM = null) {
|
||||
const visualEntry = definition.assets?.visual?.[0];
|
||||
const authoredRuntimePath = definition.id === "wailing-caverns"
|
||||
? path.join(projectRoot, "src/assets/game/dungeons/wailing-caverns/wailing-caverns-visual.glb")
|
||||
@@ -542,12 +642,12 @@ async function auditDungeon(definition, io, visualOverride = null) {
|
||||
row.shipped = await auditGlb(publishedPath, `${definition.id} shipped`);
|
||||
row.shippedGeometry = await auditGeometry(publishedPath,
|
||||
textureSizeByIndex(parseGlb(await readFile(publishedPath), `${definition.id} shipped`).json, row.shipped.images),
|
||||
`${definition.id} shipped`);
|
||||
`${definition.id} shipped`, { hotspotCellSizeM });
|
||||
if (source.path) {
|
||||
const sourceAudit = await auditGlb(source.path, `${definition.id} source`);
|
||||
const sourceParsed = parseGlb(await readFile(source.path), `${definition.id} source`);
|
||||
row.source = sourceAudit;
|
||||
row.sourceGeometry = await auditGeometry(source.path, textureSizeByIndex(sourceParsed.json, sourceAudit.images), `${definition.id} source`);
|
||||
row.sourceGeometry = await auditGeometry(source.path, textureSizeByIndex(sourceParsed.json, sourceAudit.images), `${definition.id} source`, { hotspotCellSizeM });
|
||||
row.textureComparison = compareImages(sourceAudit, row.shipped);
|
||||
row.geometryComparison = {
|
||||
triangleCountDelta: row.shippedGeometry.triangleCount - row.sourceGeometry.triangleCount,
|
||||
@@ -577,6 +677,7 @@ async function auditDungeon(definition, io, visualOverride = null) {
|
||||
rankCollapsedAreaM2Delta: round(shippedMetric.rankCollapsedAreaM2 - sourceMetric.rankCollapsedAreaM2, 2),
|
||||
}];
|
||||
})),
|
||||
...(hotspotCellSizeM ? { uvHotspots: compareUvHotspots(row.sourceGeometry.uvHotspots, row.shippedGeometry.uvHotspots) } : {}),
|
||||
};
|
||||
if (row.textureComparison.downsizedImages) {
|
||||
row.issues.push(`${row.textureComparison.downsizedImages} shipped images are smaller than their matched source dimensions.`);
|
||||
@@ -680,6 +781,11 @@ async function main() {
|
||||
const overrideId = overrideSeparator > 0 ? overrideText.slice(0, overrideSeparator) : null;
|
||||
const visualOverride = overrideId ? path.resolve(projectRoot, overrideText.slice(overrideSeparator + 1)) : null;
|
||||
if (overrideText && !visualOverride) throw new Error("--visual-override must be <dungeon-id>=<glb-path>.");
|
||||
const hotspotArg = process.argv.indexOf("--hotspot-cell-m");
|
||||
const hotspotCellSizeM = hotspotArg >= 0 ? Number(process.argv[hotspotArg + 1]) : null;
|
||||
if (hotspotArg >= 0 && (!Number.isFinite(hotspotCellSizeM) || hotspotCellSizeM <= 0)) {
|
||||
throw new Error("--hotspot-cell-m must be a positive world-space cell size in meters.");
|
||||
}
|
||||
const selectedIds = new Set([...(onlyIds ?? []), ...(overrideId ? [overrideId] : [])]);
|
||||
const unknownIds = [...selectedIds].filter((id) => !allDefinitions.some((definition) => definition.id === id));
|
||||
if (unknownIds.length) throw new Error(`Unknown WoW dungeon id(s): ${unknownIds.join(", ")}.`);
|
||||
@@ -690,7 +796,7 @@ async function main() {
|
||||
const dungeons = [];
|
||||
for (const [index, definition] of definitions.entries()) {
|
||||
console.log(`[${index + 1}/${definitions.length}] auditing ${definition.id}`);
|
||||
dungeons.push(await auditDungeon(definition, io, definition.id === overrideId ? visualOverride : null));
|
||||
dungeons.push(await auditDungeon(definition, io, definition.id === overrideId ? visualOverride : null, hotspotCellSizeM));
|
||||
}
|
||||
const compared = dungeons.filter((dungeon) => dungeon.source && dungeon.shipped);
|
||||
const shipped = dungeons.filter((dungeon) => dungeon.shipped);
|
||||
|
||||
Reference in new issue
Block a user