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 }); } });