Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

@@ -0,0 +1,324 @@
import { Matrix3, Matrix4, Vector3 } from "three";
import { descriptorSourceMatrix } from "../lib/environment-assembly.mjs";
const NORMAL_EPSILON = 1e-12;
export function sourceTerrainToThreeMatrix(primaryPlacement, metersPerSourceUnit) {
const meters = Number(metersPerSourceUnit);
if (!Number.isFinite(meters) || meters <= 0) {
throw new Error("metersPerSourceUnit must be a positive finite number.");
}
const sourceToThree = new Matrix4().makeScale(meters, meters, -meters);
return sourceToThree.multiply(descriptorSourceMatrix(primaryPlacement).invert());
}
function checkArrayLength(array, expectedLength, label) {
if (!ArrayBuffer.isView(array) || array.length !== expectedLength) {
throw new Error(`${label} has ${array?.length ?? "no"} values; expected ${expectedLength}.`);
}
}
function transformPosition(sourcePosition, matrix) {
const position = new Vector3().fromArray(sourcePosition).applyMatrix4(matrix);
if (![position.x, position.y, position.z].every(Number.isFinite)) {
throw new Error("Terrain position transform produced a non-finite coordinate.");
}
return position.toArray();
}
function transformNormal(sourceNormal, normalMatrix) {
const normal = new Vector3().fromArray(sourceNormal).applyMatrix3(normalMatrix);
const lengthSq = normal.lengthSq();
if (!Number.isFinite(lengthSq) || lengthSq < NORMAL_EPSILON) {
throw new Error("Terrain source contains an invalid native normal.");
}
return normal.normalize().toArray();
}
function triangleAreaSquared(positions, a, b, c) {
const ax = positions[a * 3];
const ay = positions[a * 3 + 1];
const az = positions[a * 3 + 2];
const abx = positions[b * 3] - ax;
const aby = positions[b * 3 + 1] - ay;
const abz = positions[b * 3 + 2] - az;
const acx = positions[c * 3] - ax;
const acy = positions[c * 3 + 1] - ay;
const acz = positions[c * 3 + 2] - az;
const nx = aby * acz - abz * acy;
const ny = abz * acx - abx * acz;
const nz = abx * acy - aby * acx;
return nx * nx + ny * ny + nz * nz;
}
function faceNormalDot(positions, normals, a, b, c) {
const ax = positions[a * 3];
const ay = positions[a * 3 + 1];
const az = positions[a * 3 + 2];
const abx = positions[b * 3] - ax;
const aby = positions[b * 3 + 1] - ay;
const abz = positions[b * 3 + 2] - az;
const acx = positions[c * 3] - ax;
const acy = positions[c * 3 + 1] - ay;
const acz = positions[c * 3 + 2] - az;
let nx = aby * acz - abz * acy;
let ny = abz * acx - abx * acz;
let nz = abx * acy - aby * acx;
const length = Math.hypot(nx, ny, nz);
if (length < NORMAL_EPSILON) return null;
nx /= length;
ny /= length;
nz /= length;
const sx = normals[a * 3] + normals[b * 3] + normals[c * 3];
const sy = normals[a * 3 + 1] + normals[b * 3 + 1] + normals[c * 3 + 1];
const sz = normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2];
const normalLength = Math.hypot(sx, sy, sz);
if (normalLength < NORMAL_EPSILON) return null;
return (nx * sx + ny * sy + nz * sz) / normalLength;
}
/**
* Converts the extracted native grid arrays without changing their vertex grid,
* texture coordinates, or source per-vertex lighting normals. Winding is reversed
* once because the Runewaker-to-Three bridge reflects Z.
*/
export function convertNativeTerrainChunks(chunks, sourceToThree, options = {}) {
if (!Array.isArray(chunks) || chunks.length === 0) {
throw new Error("Native terrain conversion requires at least one chunk.");
}
const positionMatrix = sourceToThree.clone();
const normalMatrix = new Matrix3().getNormalMatrix(positionMatrix);
const convertedChunks = [];
const perChunkSummaries = [];
const edgeVertices = new Map();
const seamPositionToleranceMeters = Number(options.seamPositionToleranceMeters ?? 0.02);
let vertexOffset = 0;
let sourceNormalLengthMin = Number.POSITIVE_INFINITY;
let sourceNormalLengthMax = 0;
let sourceNormalLengthSum = 0;
let sourceNormalLengthCount = 0;
let faceAlignedCount = 0;
let faceOpposedCount = 0;
let faceDegenerateCount = 0;
let sourceTriangleSlots = 0;
let visibleSourceTriangles = 0;
let maskedSourceTriangles = 0;
let collisionTriangles = 0;
let excludedCollisionTriangles = 0;
let maxSeamPositionDeltaMeters = 0;
let maxSeamNormalDelta = 0;
let seamNormalPairs = 0;
let seamNormalPairsWithinOneChannelStep = 0;
const dimensions = new Set(chunks.map((chunk) => Number(chunk.dimension)));
if (dimensions.size !== 1) throw new Error("All native terrain chunks must have the same grid dimension.");
const dimension = [...dimensions][0];
if (!Number.isInteger(dimension) || dimension < 1) throw new Error("Terrain chunk dimension is invalid.");
const side = dimension + 1;
const expectedVertices = side * side;
if (expectedVertices > 0x10000) {
throw new Error("A native terrain chunk exceeds its 16-bit source index contract.");
}
// Each source chunk uses 16-bit local indices. The combined GLB uses global
// offsets, which need 32 bits when more than fifteen 65×65 grids are present.
const IndexArray = expectedVertices * chunks.length > 0x10000 ? Uint32Array : Uint16Array;
for (const chunk of chunks) {
const chunkDimension = Number(chunk.dimension);
const vertexCount = (chunkDimension + 1) ** 2;
checkArrayLength(chunk.positions, vertexCount * 3, `${chunk.id} positions`);
checkArrayLength(chunk.uvs, vertexCount * 2, `${chunk.id} UVs`);
checkArrayLength(chunk.normals, vertexCount * 3, `${chunk.id} native normals`);
if (!Number.isInteger(Number(chunk.coordinate?.x)) || !Number.isInteger(Number(chunk.coordinate?.z))) {
throw new Error(`${chunk.id} requires integer chunk coordinates.`);
}
if (chunk.indices.length % 3 !== 0) throw new Error(`${chunk.id} indices are not triangular.`);
const triangleCount = chunk.indices.length / 3;
checkArrayLength(chunk.triangleVisibilityFlags, triangleCount, `${chunk.id} triangle visibility`);
const convertedPositions = new Float32Array(vertexCount * 3);
const convertedNormals = new Float32Array(vertexCount * 3);
const collisionIndices = [];
const visualIndices = new IndexArray(chunk.indices.length);
let chunkVisibleTriangles = 0;
let chunkMaskedTriangles = 0;
let chunkAlignedFaces = 0;
let chunkOpposedFaces = 0;
let chunkDegenerateFaces = 0;
for (let vertex = 0; vertex < vertexCount; vertex += 1) {
const positionOffset = vertex * 3;
const uvOffset = vertex * 2;
const sourcePosition = [
Number(chunk.positions[positionOffset]),
Number(chunk.positions[positionOffset + 1]),
Number(chunk.positions[positionOffset + 2]),
];
const sourceNormal = [
Number(chunk.normals[positionOffset]),
Number(chunk.normals[positionOffset + 1]),
Number(chunk.normals[positionOffset + 2]),
];
const outputPosition = transformPosition(sourcePosition, positionMatrix);
const outputNormal = transformNormal(sourceNormal, normalMatrix);
convertedPositions.set(outputPosition, positionOffset);
convertedNormals.set(outputNormal, positionOffset);
const sourceLength = Math.hypot(...sourceNormal);
sourceNormalLengthMin = Math.min(sourceNormalLengthMin, sourceLength);
sourceNormalLengthMax = Math.max(sourceNormalLengthMax, sourceLength);
sourceNormalLengthSum += sourceLength;
sourceNormalLengthCount += 1;
const localX = vertex % side;
const localZ = Math.floor(vertex / side);
const globalGridKey = `${Number(chunk.coordinate.x) * dimension + localX},${Number(chunk.coordinate.z) * dimension + localZ}`;
const previous = edgeVertices.get(globalGridKey);
if (previous) {
const dx = previous.position[0] - outputPosition[0];
const dy = previous.position[1] - outputPosition[1];
const dz = previous.position[2] - outputPosition[2];
const positionDelta = Math.hypot(dx, dy, dz);
maxSeamPositionDeltaMeters = Math.max(maxSeamPositionDeltaMeters, positionDelta);
if (positionDelta > seamPositionToleranceMeters) {
throw new Error(`${chunk.id} disagrees with a neighbor at terrain grid vertex ${globalGridKey} by ${positionDelta}m.`);
}
const normalDelta = Math.hypot(
previous.normal[0] - outputNormal[0],
previous.normal[1] - outputNormal[1],
previous.normal[2] - outputNormal[2],
);
seamNormalPairs += 1;
maxSeamNormalDelta = Math.max(maxSeamNormalDelta, normalDelta);
if (normalDelta <= (2 * Math.sqrt(3) / 255) + 1e-6) seamNormalPairsWithinOneChannelStep += 1;
} else {
edgeVertices.set(globalGridKey, { position: outputPosition, normal: outputNormal });
}
if (!Number.isFinite(chunk.uvs[uvOffset]) || !Number.isFinite(chunk.uvs[uvOffset + 1])) {
throw new Error(`${chunk.id} contains non-finite texture coordinates.`);
}
}
for (let triangle = 0; triangle < triangleCount; triangle += 1) {
const offset = triangle * 3;
const a = Number(chunk.indices[offset]);
const b = Number(chunk.indices[offset + 1]);
const c = Number(chunk.indices[offset + 2]);
if ([a, b, c].some((index) => !Number.isInteger(index) || index < 0 || index >= vertexCount)) {
throw new Error(`${chunk.id} triangle ${triangle} has an invalid source vertex index.`);
}
const visible = Number(chunk.triangleVisibilityFlags[triangle]);
if (visible !== 0 && visible !== 1) throw new Error(`${chunk.id} triangle ${triangle} has invalid visibility flag ${visible}.`);
const outA = vertexOffset + a;
const outB = vertexOffset + b;
const outC = vertexOffset + c;
// Keep the original masked degenerate slots, including [0,0,0], in visual geometry.
visualIndices[offset] = outA;
visualIndices[offset + 1] = outC;
visualIndices[offset + 2] = outB;
sourceTriangleSlots += 1;
if (visible === 0) {
chunkMaskedTriangles += 1;
maskedSourceTriangles += 1;
excludedCollisionTriangles += 1;
continue;
}
chunkVisibleTriangles += 1;
visibleSourceTriangles += 1;
if (a === b || b === c || c === a
|| triangleAreaSquared(convertedPositions, a, b, c) < 1e-24) {
excludedCollisionTriangles += 1;
chunkDegenerateFaces += 1;
continue;
}
collisionIndices.push(outA, outC, outB);
collisionTriangles += 1;
const dot = faceNormalDot(convertedPositions, convertedNormals, a, c, b);
if (dot === null) {
faceDegenerateCount += 1;
chunkDegenerateFaces += 1;
} else if (dot >= 0) {
faceAlignedCount += 1;
chunkAlignedFaces += 1;
} else {
faceOpposedCount += 1;
chunkOpposedFaces += 1;
}
}
convertedChunks.push({
...chunk,
vertexOffset,
positions: convertedPositions,
normals: convertedNormals,
uvs: new Float32Array(chunk.uvs),
visualIndices,
collisionIndices: IndexArray.from(collisionIndices),
});
perChunkSummaries.push({
id: chunk.id,
coordinate: chunk.coordinate,
vertexCount,
sourceTriangleSlots: triangleCount,
visibleSourceTriangles: chunkVisibleTriangles,
maskedSourceTriangles: chunkMaskedTriangles,
collisionTriangles: collisionIndices.length / 3,
excludedCollisionTriangles: chunkMaskedTriangles + chunkDegenerateFaces,
sourceTriangleVisibilitySha256: chunk.triangleVisibilitySha256 ?? null,
sourceLayers: chunk.layers?.map((layer) => ({
layerId: layer.layerId,
tileScale: layer.tileScale,
sourceDDS: layer.sourceDDS,
sourceClientPath: layer.sourceClientPath,
})) ?? [],
sourceWeights: chunk.splatMaps?.map((weight) => ({
mapIndex: weight.mapIndex,
sourceDDS: weight.splatDDS,
linearPngFile: weight.linearPngFile,
channelLayerIds: weight.channelLayerIds,
})) ?? [],
nativeNormalWinding: {
alignedTriangles: chunkAlignedFaces,
opposedTriangles: chunkOpposedFaces,
degenerateTriangles: chunkDegenerateFaces,
},
});
vertexOffset += expectedVertices;
}
return {
chunks: convertedChunks,
vertexCount: vertexOffset,
report: {
chunkCount: chunks.length,
verticesPerChunk: expectedVertices,
sourceVertices: vertexOffset,
sourceTriangleSlots,
visibleSourceTriangles,
maskedSourceTriangles,
collisionTriangles,
excludedCollisionTriangles,
positionWelding: {
performed: false,
reason: "Native normal records can differ at chunk boundaries; retain independent source vertices and normals.",
sharedGridVertexPairs: seamNormalPairs,
seamPositionToleranceMeters,
maxSharedPositionDeltaMeters: maxSeamPositionDeltaMeters,
maxSharedNativeNormalDelta: maxSeamNormalDelta,
normalPairsWithinOneEightBitChannelStep: seamNormalPairsWithinOneChannelStep,
normalPairCount: seamNormalPairs,
},
sourceNativeNormals: {
minLength: sourceNormalLengthMin,
meanLength: sourceNormalLengthSum / Math.max(1, sourceNormalLengthCount),
maxLength: sourceNormalLengthMax,
transform: "inverse-transpose of primary descriptor TRS and reflected-Z meter bridge; normalized",
},
windingValidation: {
rule: "swap triangle indices 1 and 2 after reflected-Z coordinate bridge",
trianglesWithNativeNormalAgreement: faceAlignedCount,
trianglesAgainstNativeNormal: faceOpposedCount,
degenerateTriangles: faceDegenerateCount,
},
chunks: perChunkSummaries,
},
};
}