Files
healer-man/scripts/runewaker-pipeline/sand-vortex/terrain-geometry.test.mjs
T
2026-10-06 22:25:34 -04:00

107 lines
5.1 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { Matrix3, Matrix4, Vector3 } from "three";
import { descriptorSourceMatrix } from "../lib/environment-assembly.mjs";
import { convertNativeTerrainChunks, sourceTerrainToThreeMatrix } from "./terrain-geometry.mjs";
function makeChunk(id, coordinateX, xOffset, normalX = 0) {
const vertices = [
[xOffset, 0, 0], [xOffset + 1, 0, 0],
[xOffset, 0, 1], [xOffset + 1, 0, 1],
];
const normals = Array.from({ length: 4 }, () => [normalX, 1, 0]).flat();
return {
id,
coordinate: { x: coordinateX, z: 0 },
dimension: 1,
positions: Float32Array.from(vertices.flat()),
normals: Float32Array.from(normals),
uvs: Float32Array.from([0, 0, 1, 0, 0, 1, 1, 1]),
// Source topology is upward facing in left-handed Runewaker coordinates.
indices: Uint16Array.from([0, 2, 3, 0, 0, 0]),
triangleVisibilityFlags: Uint8Array.from([1, 0]),
visibleSourceTriangles: 1,
maskedSourceTriangles: 1,
layers: [{ layerId: 4, tileScale: 12, sourceDDS: "textures/tile.dds" }],
splatMaps: [{ mapIndex: 0, splatDDS: "weights/a.dds", linearPngFile: "weights/a.png", channelLayerIds: [4] }],
material: { variantNames: [`${id}-layer-4`], weightVariantNames: [`${id}-weight-0`] },
};
}
test("native terrain conversion applies primary inverse and reflected meter bridge", () => {
const primary = {
translation: [10, 5, -2],
rotation: [0, Math.sin(Math.PI / 8), 0, Math.cos(Math.PI / 8)],
scale: [2, 1, 3],
};
const transform = sourceTerrainToThreeMatrix(primary, 0.1);
const sourceWorldPoint = new Vector3(10, 5, -2).applyMatrix4(descriptorSourceMatrix(primary));
const expected = new Vector3(1, 0.5, 0.2);
const actual = sourceWorldPoint.clone().applyMatrix4(transform);
assert.ok(actual.distanceTo(expected) < 1e-6);
const normalMatrix = new Matrix3().getNormalMatrix(transform);
const normal = new Vector3(1, 1, 0).applyMatrix3(normalMatrix).normalize();
assert.ok(normal.x > 0 && normal.y > 0);
});
test("visual indices preserve each source slot with reflected-Z winding; collision omits masked faces", () => {
const transform = new Matrix4().makeScale(0.1, 0.1, -0.1);
const chunks = [makeChunk("left", 0, 0), makeChunk("right", 1, 1, 0.01)];
const converted = convertNativeTerrainChunks(chunks, transform);
assert.equal(converted.vertexCount, 8, "chunk grid vertices remain distinct at borders to retain native normals");
assert.deepEqual([...converted.chunks[0].visualIndices], [0, 3, 2, 0, 0, 0]);
assert.deepEqual([...converted.chunks[1].visualIndices], [4, 7, 6, 4, 4, 4]);
assert.deepEqual([...converted.chunks[0].collisionIndices], [0, 3, 2]);
assert.deepEqual([...converted.chunks[1].collisionIndices], [4, 7, 6]);
assert.deepEqual([...converted.chunks[0].uvs], [0, 0, 1, 0, 0, 1, 1, 1]);
assert.equal(converted.report.sourceTriangleSlots, 4);
assert.equal(converted.report.visibleSourceTriangles, 2);
assert.equal(converted.report.maskedSourceTriangles, 2);
assert.equal(converted.report.collisionTriangles, 2);
assert.equal(converted.report.positionWelding.performed, false);
assert.equal(converted.report.positionWelding.sharedGridVertexPairs, 2);
assert.ok(converted.report.positionWelding.maxSharedNativeNormalDelta > 0);
assert.equal(converted.report.windingValidation.trianglesAgainstNativeNormal, 0);
assert.equal(converted.report.windingValidation.trianglesWithNativeNormalAgreement, 2);
});
test("chunk borders that disagree in transformed position fail instead of silently welding", () => {
const left = makeChunk("left", 0, 0);
const right = makeChunk("right", 1, 1);
right.positions[0] += 1;
assert.throws(
() => convertNativeTerrainChunks([left, right], new Matrix4().makeScale(0.1, 0.1, -0.1)),
/disagrees with a neighbor at terrain grid vertex/,
);
});
test("combined native grids retain indices beyond 65535 without wrapping or dropping masked slots", () => {
const chunks = Array.from({ length: 16 }, (_, x) => {
const dimension = 64;
const positions = [];
const normals = [];
const uvs = [];
for (let z = 0; z <= dimension; z++) for (let column = 0; column <= dimension; column++) {
positions.push(x * dimension + column, 0, z);
normals.push(0, 1, 0);
uvs.push(column / dimension, z / dimension);
}
return {
...makeChunk(`chunk-${x}`, x, x * dimension), dimension,
positions: Float32Array.from(positions), normals: Float32Array.from(normals),
uvs: Float32Array.from(uvs), indices: Uint16Array.from([0, 4160, 4224, 0, 0, 0]),
};
});
const converted = convertNativeTerrainChunks(chunks, new Matrix4().makeScale(0.1, 0.1, -0.1));
const last = converted.chunks.at(-1);
assert.equal(converted.vertexCount, 67600);
assert.ok(last.visualIndices instanceof Uint32Array);
assert.ok(last.collisionIndices instanceof Uint32Array);
assert.deepEqual([...last.visualIndices], [63375, 67599, 67535, 63375, 63375, 63375]);
assert.deepEqual([...last.collisionIndices], [63375, 67599, 67535]);
assert.equal(converted.report.maskedSourceTriangles, 16);
assert.equal(converted.report.collisionTriangles, 16);
});