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

133 lines
5.9 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import sharp from "sharp";
import { Matrix4 } from "three";
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { convertNativeTerrainChunks } from "./terrain-geometry.mjs";
import { writeNativeTerrainGlbs } from "./native-terrain-glb.mjs";
function makeChunk(id, coordinateX, xOffset) {
return {
id,
coordinate: { x: coordinateX, z: 0 },
dimension: 1,
positions: Float32Array.from([
xOffset, 0, 0, xOffset + 1, 0, 0,
xOffset, 0, 1, xOffset + 1, 0, 1,
]),
normals: Float32Array.from(Array.from({ length: 4 }, () => [0, 1, 0]).flat()),
uvs: Float32Array.from([0, 0, 1, 0, 0, 1, 1, 1]),
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`] },
};
}
function bounds3(array) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
for (let offset = 0; offset < array.length; offset += 3) {
for (let axis = 0; axis < 3; axis += 1) {
min[axis] = Math.min(min[axis], array[offset + axis]);
max[axis] = Math.max(max[axis], array[offset + axis]);
}
}
return { min, max };
}
test("partitioned native terrain GLBs serialize local chunk bounds and indices for visuals and collision", async () => {
const converted = convertNativeTerrainChunks(
[makeChunk("left", 0, 0), makeChunk("right", 1, 1)],
// Source Z reflection and meter scale match the serializer's production path.
new Matrix4().makeScale(0.1, 0.1, -0.1),
);
const png = await sharp(Buffer.from([
255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 255, 255, 255, 255, 255, 255,
]), { raw: { width: 2, height: 2, channels: 4 } }).png().toBuffer();
const textureBySource = new Map([
["textures/tile.dds", { imageName: "tile.png", image: png, sourceDDS: "textures/tile.dds", colorSpace: "srgb" }],
["weights/a.png", { imageName: "a.png", image: png, sourceDDS: "weights/a.dds", colorSpace: "linear", channelLayerIds: [4] }],
]);
const directory = await mkdtemp(path.join(os.tmpdir(), "native-terrain-partition-"));
const visualFile = path.join(directory, "terrain-visual.glb");
const collisionFile = path.join(directory, "terrain-collision.glb");
const provenance = {
dungeonId: "fixture",
dungeonTitle: "Fixture",
partitionVisualChunks: true,
sourceManifestSha256: "a".repeat(64),
sourceWdbSha256: "b".repeat(64),
primaryPlacementGuid: "fixture-primary",
metersPerSourceUnit: 0.1,
textureSourceDDS: ["textures/tile.dds", "weights/a.dds"],
};
try {
await writeNativeTerrainGlbs({
converted,
textureSources: { textureBySource },
provenance,
visualFile,
collisionFile,
});
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const visual = await io.read(visualFile);
const collision = await io.read(collisionFile);
const visualPrimitives = visual.getRoot().listMeshes().flatMap((mesh) => mesh.listPrimitives())
.filter((primitive) => primitive.getExtras()?.runewakerTerrainChunk);
const collisionPrimitives = collision.getRoot().listMeshes().flatMap((mesh) => mesh.listPrimitives())
.filter((primitive) => primitive.getExtras()?.runewakerTerrainCollisionChunk);
assert.equal(visualPrimitives.length, 2);
assert.equal(collisionPrimitives.length, 2);
for (const [primitives, extraKey, indexKey] of [
[visualPrimitives, "runewakerTerrainChunk", "visualIndices"],
[collisionPrimitives, "runewakerTerrainCollisionChunk", "collisionIndices"],
]) {
for (const primitive of primitives) {
const metadata = primitive.getExtras()[extraKey];
const chunk = converted.chunks.find((row) => row.id === metadata.id);
assert.ok(chunk, `chunk metadata ${metadata.id} resolves to source geometry`);
const positionAccessor = primitive.getAttribute("POSITION");
const indexAccessor = primitive.getIndices();
assert.ok(positionAccessor);
assert.ok(indexAccessor);
assert.deepEqual([...positionAccessor.getArray()], [...chunk.positions]);
assert.deepEqual([...indexAccessor.getArray()], [...chunk[indexKey]].map((index) => index - chunk.vertexOffset));
assert.deepEqual(bounds3(positionAccessor.getArray()), bounds3(chunk.positions));
}
}
const visualByChunk = new Map(visualPrimitives.map((primitive) => [
primitive.getExtras().runewakerTerrainChunk.id,
[...primitive.getIndices().getArray()],
]));
const collisionByChunk = new Map(collisionPrimitives.map((primitive) => [
primitive.getExtras().runewakerTerrainCollisionChunk.id,
[...primitive.getIndices().getArray()],
]));
assert.deepEqual(visualByChunk.get("left"), [0, 3, 2, 0, 0, 0]);
assert.deepEqual(visualByChunk.get("right"), [0, 3, 2, 0, 0, 0]);
assert.deepEqual(collisionByChunk.get("left"), [0, 3, 2]);
assert.deepEqual(collisionByChunk.get("right"), [0, 3, 2]);
const close = (actual, expected) => assert.ok(
actual.every((value, index) => Math.abs(value - expected[index]) < 1e-6),
`${actual} should be within 1e-6 of ${expected}`,
);
close(bounds3(converted.chunks[0].positions).max, [0.1, 0, 0]);
close(bounds3(converted.chunks[1].positions).min, [0.1, 0, -0.1]);
close(bounds3(converted.chunks[1].positions).max, [0.2, 0, 0]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});