forked from phenom/healer-man
133 lines
5.9 KiB
JavaScript
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 });
|
|
}
|
|
});
|