UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

439 lines (396 loc) 15.1 kB
import {describe, expect, test} from 'vitest'; import {FillLayoutArray, LineIndexArray, TriangleIndexArray} from '../data/array_types.g'; import {SegmentVector} from '../data/segment'; import {fillLargeMeshArrays} from './fill_large_mesh_arrays'; import {type SimpleMesh, getGridMesh, getGridMeshRandom} from '../../test/unit/lib/mesh_utils'; describe('fillArrays', () => { test('Mesh comparison works', () => { const meshA: SimpleMesh = { vertices: [ 0, 0, // 0 0 ---- 1 1, 0, // 1 | | 1, 1, // 2 | | 0, 1 // 3 3 ---- 2 ], indicesTriangles: [ 0, 3, 1, 3, 2, 1 ], indicesLines: [ 0, 1, 1, 2, 2, 3, 3, 0 ], segmentsTriangles: [ { vertexOffset: 0, primitiveLength: 2, primitiveOffset: 0, } ], segmentsLines: [ { vertexOffset: 0, primitiveLength: 4, primitiveOffset: 0, } ] }; // Check string representation const stringsA = getRenderedGeometryRepresentation(meshA); expect(stringsA.stringsTriangles).toEqual(['(0 0) (0 1) (1 0)', '(0 1) (1 1) (1 0)']); expect(stringsA.stringsLines).toEqual(['(0 0) (1 0)', '(1 0) (1 1)', '(1 1) (0 1)', '(0 1) (0 0)']); const meshB: SimpleMesh = { vertices: [ 0, 0, // 0 0, 1, // 1 1, 0, // 2 0, 1, // 3 1, 1, // 4 1, 0, // 5 0, 0, 1, 0, 1, 1, 0, 1, ], indicesTriangles: [ 0, 1, 2, 0, 1, 2 ], indicesLines: [ 0, 1, 1, 2, 2, 3, 3, 0 ], segmentsTriangles: [ { vertexOffset: 0, primitiveLength: 1, primitiveOffset: 0, }, { vertexOffset: 3, primitiveLength: 1, primitiveOffset: 1, } ], segmentsLines: [ { vertexOffset: 6, primitiveLength: 4, primitiveOffset: 0, } ] }; testMeshesEqual(meshA, meshB); // same as mesh A, but contains one error const meshC: SimpleMesh = { vertices: [ 0, 0, // 0 0 ---- 1 1, 0, // 1 | | 1, 1, // 2 | | 0, 1 // 3 3 ---- 2 ], indicesTriangles: [ 0, 3, 1, 1, 2, 3 // flip vertex order ], indicesLines: [ 0, 1, 1, 2, 2, 3, 3, 0 ], segmentsTriangles: [ { vertexOffset: 0, primitiveLength: 2, primitiveOffset: 0, } ], segmentsLines: [ { vertexOffset: 0, primitiveLength: 4, primitiveOffset: 0, } ] }; const stringsC = getRenderedGeometryRepresentation(meshC); // String representations should be different expect(stringsC.stringsTriangles).not.toEqual(stringsA.stringsTriangles); }); test('Mesh grid generation', () => { const mesh = getGridMesh(2); const strings = getRenderedGeometryRepresentation(mesh); // Note that this forms a correct 2x2 quad mesh. expect(strings.stringsTriangles).toEqual([ '(0 0) (1 1) (1 0)', '(0 0) (0 1) (1 1)', '(1 0) (2 1) (2 0)', '(1 0) (1 1) (2 1)', '(0 1) (1 2) (1 1)', '(0 1) (0 2) (1 2)', '(1 1) (2 2) (2 1)', '(1 1) (1 2) (2 2)' ]); expect(strings.stringsLines).toEqual([ '(0 0) (1 0)', '(1 0) (2 0)', '(0 2) (1 2)', '(1 2) (2 2)', '(0 0) (0 1)', '(0 1) (0 2)', '(2 0) (2 1)', '(2 1) (2 2)' ]); }); test('Tiny mesh is unchanged.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16; const mesh = getGridMesh(1); const split = createSegmentsAndSplitMesh(mesh); expect(split.segmentsTriangles).toHaveLength(1); testMeshesEqual(mesh, split); }); test('Small mesh is unchanged.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16; const mesh = getGridMesh(2); const split = createSegmentsAndSplitMesh(mesh); expect(split.segmentsTriangles).toHaveLength(1); testMeshesEqual(mesh, split); }); test('Large mesh is correctly split into multiple segments.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16; const mesh = getGridMesh(4); const split = createSegmentsAndSplitMesh(mesh); expect(split.segmentsTriangles.length).toBeGreaterThan(1); testMeshesEqual(mesh, split); }); test('Very large mesh is correctly split into multiple segments.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 1024; const mesh = getGridMesh(64); const split = createSegmentsAndSplitMesh(mesh); expect(split.segmentsTriangles.length).toBeGreaterThan(1); testMeshesEqual(mesh, split); }); test('Very large random mesh is correctly split into multiple segments.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 1024; const mesh = getGridMeshRandom(64, 8192, 1024); const split = createSegmentsAndSplitMesh(mesh); expect(split.segmentsTriangles.length).toBeGreaterThan(1); testMeshesEqual(mesh, split); }); test('Several small meshes are correctly placed into a single segment.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16; const buffers = createMeshBuffers(); const smallMesh = getGridMesh(1); // 4 vertices fillMesh(buffers, smallMesh); fillMesh(buffers, smallMesh); const result = convertBuffersToMesh(buffers); expect(result.vertices).toEqual([ 0, 0, // 0 1, 0, // 1 0, 1, // 2 1, 1, // 3 0, 0, 1, 0, 0, 1, 1, 1 ]); expect(result.indicesTriangles).toEqual([ 0, 3, 1, 0, 2, 3, 4, 7, 5, 4, 6, 7 ]); expect(result.indicesLines).toEqual([ 0, 1, 2, 3, 0, 2, 1, 3, 4, 5, 6, 7, 4, 6, 5, 7 ]); expect(result.segmentsTriangles).toHaveLength(1); expect(result.segmentsLines).toHaveLength(1); }); test('Several small and large meshes are correctly split into multiple segments.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16; const buffers = createMeshBuffers(); const smallMesh = getGridMesh(1); // 4 vertices const largeMesh = getGridMesh(2); // 9 vertices const meshList = [ smallMesh, largeMesh, // Previous mesh still fits into first segment: 9+4 = 13 largeMesh, // Only the first triangle fits, usage is second segment is 8 vertices smallMesh, // This last one brings up second segment usage to 12 vertices ]; for (const mesh of meshList) { fillMesh(buffers, mesh); } const result = convertBuffersToMesh(buffers); const merge = mergeMeshes(meshList); expect(result.segmentsTriangles).toHaveLength(2); expect(result.segmentsTriangles[0].primitiveLength).toBe(10); // 2 + 8 triangles expect(result.segmentsTriangles[1].primitiveLength).toBe(10); // 8 + 2 triangles testMeshesEqual(merge, result); }); test('Many small and large meshes are correctly split into multiple segments.', () => { SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16; const buffers = createMeshBuffers(); const smallMesh = getGridMesh(1); // 4 vertices const largeMesh = getGridMesh(2); // 9 vertices const meshList = [ smallMesh, largeMesh, largeMesh, smallMesh, smallMesh, smallMesh, largeMesh, largeMesh, largeMesh, largeMesh, largeMesh, ]; for (const mesh of meshList) { fillMesh(buffers, mesh); } const result = convertBuffersToMesh(buffers); const merge = mergeMeshes(meshList); testMeshesEqual(merge, result); expect(result.segmentsTriangles.length).toBeGreaterThan(merge.vertices.length / 2 / SegmentVector.MAX_VERTEX_ARRAY_LENGTH); expect(result.segmentsTriangles.length).toBeLessThan(meshList.length); }); }); type MeshBuffers = { segmentsTriangles: SegmentVector; segmentsLines: SegmentVector; vertices: FillLayoutArray; indicesTriangles: TriangleIndexArray; indicesLines: LineIndexArray; }; function createMeshBuffers(): MeshBuffers { return { segmentsTriangles: new SegmentVector(), segmentsLines: new SegmentVector(), vertices: new FillLayoutArray(), indicesTriangles: new TriangleIndexArray(), indicesLines: new LineIndexArray(), }; } /** * Creates a mesh the geometry of which is a merge of the specified input meshes, * useful for comparing the result of using {@link fillLargeMeshArrays} on several meshes. */ function mergeMeshes(meshes: Array<SimpleMesh>): SimpleMesh { const result: SimpleMesh = { vertices: [], indicesTriangles: [], indicesLines: [], segmentsTriangles: [], segmentsLines: [], }; for (const mesh of meshes) { const baseVertex = result.vertices.length / 2; result.vertices.push(...mesh.vertices); result.indicesTriangles.push(...(mesh.indicesTriangles.map(x => x + baseVertex))); result.indicesLines.push(...(mesh.indicesLines.map(x => x + baseVertex))); } result.segmentsTriangles.push({ vertexOffset: 0, primitiveOffset: 0, primitiveLength: result.indicesTriangles.length / 3, }); result.segmentsLines.push({ vertexOffset: 0, primitiveOffset: 0, primitiveLength: result.indicesLines.length / 2, }); return result; } /** * Creates a mesh that is equal to the actual rendered output of a single * {@link fillLargeMeshArrays} call that is run in isolation. */ function createSegmentsAndSplitMesh(mesh: SimpleMesh): SimpleMesh { const buffers = createMeshBuffers(); fillMesh(buffers, mesh); return convertBuffersToMesh(buffers); } function fillMesh(buffers: MeshBuffers, mesh: SimpleMesh): void { fillLargeMeshArrays( (x, y) => { buffers.vertices.emplaceBack(x, y); }, buffers.segmentsTriangles, buffers.vertices, buffers.indicesTriangles, mesh.vertices, mesh.indicesTriangles, buffers.segmentsLines, buffers.indicesLines, [mesh.indicesLines]); } function convertBuffersToMesh(buffers: MeshBuffers): SimpleMesh { return { segmentsTriangles: buffers.segmentsTriangles.segments, segmentsLines: buffers.segmentsLines.segments, vertices: Array.from(buffers.vertices.int16).slice(0, buffers.vertices.length * 2), indicesTriangles: Array.from(buffers.indicesTriangles.uint16).slice(0, buffers.indicesTriangles.length * 3), indicesLines: Array.from(buffers.indicesLines.uint16).slice(0, buffers.indicesLines.length * 2) }; } /** * Our goal is to check that a mesh (in this context, a mesh is a vertex buffer, index buffer and segment vector) * with potentially more than `SegmentVector.MAX_VERTEX_ARRAY_LENGTH` vertices results in the same rendered geometry * as the result of passing that mesh through `fillLargeMeshArrays`, which creates a mesh that respects the vertex count limit. * @param expected - The original mesh that might overflow the vertex count limit. * @param actual - The result of passing the original mesh through `fillLargeMeshArrays`. */ function testMeshesEqual(expected: SimpleMesh, actual: SimpleMesh) { const stringsExpected = getRenderedGeometryRepresentation(expected); const stringsActual = getRenderedGeometryRepresentation(actual); expect(stringsActual.stringsTriangles).toEqual(stringsExpected.stringsTriangles); expect(stringsActual.stringsLines).toEqual(stringsExpected.stringsLines); } /** * Returns an ordered string representation of the geometry that would be fetched by the GPU's vertex fetch * if it were to draw the specified mesh segments, respecting `vertexOffset` and `primitiveOffset`. */ function getRenderedGeometryRepresentation(mesh: SimpleMesh) { const stringsTriangles = []; const stringsLines = []; for (const s of mesh.segmentsTriangles) { for (let i = 0; i < s.primitiveLength; i++) { const i0 = s.vertexOffset + mesh.indicesTriangles[(s.primitiveOffset + i) * 3]; const i1 = s.vertexOffset + mesh.indicesTriangles[(s.primitiveOffset + i) * 3 + 1]; const i2 = s.vertexOffset + mesh.indicesTriangles[(s.primitiveOffset + i) * 3 + 2]; const v0x = mesh.vertices[i0 * 2]; const v0y = mesh.vertices[i0 * 2 + 1]; const v1x = mesh.vertices[i1 * 2]; const v1y = mesh.vertices[i1 * 2 + 1]; const v2x = mesh.vertices[i2 * 2]; const v2y = mesh.vertices[i2 * 2 + 1]; const str = `(${v0x} ${v0y}) (${v1x} ${v1y}) (${v2x} ${v2y})`; stringsTriangles.push(str); } } for (const s of mesh.segmentsLines) { for (let i = 0; i < s.primitiveLength; i++) { const i0 = s.vertexOffset + mesh.indicesLines[(s.primitiveOffset + i) * 2]; const i1 = s.vertexOffset + mesh.indicesLines[(s.primitiveOffset + i) * 2 + 1]; const v0x = mesh.vertices[i0 * 2]; const v0y = mesh.vertices[i0 * 2 + 1]; const v1x = mesh.vertices[i1 * 2]; const v1y = mesh.vertices[i1 * 2 + 1]; const str = `(${v0x} ${v0y}) (${v1x} ${v1y})`; stringsLines.push(str); } } return { stringsTriangles, stringsLines }; }