diff --git a/docs/specifications/category-mesh.md b/docs/specifications/category-mesh.md index b0599e9f9f..83cbae179e 100644 --- a/docs/specifications/category-mesh.md +++ b/docs/specifications/category-mesh.md @@ -4,14 +4,15 @@ The _mesh and pointcloud_ loader category is intended for simpler mesh and point ## Mesh/PointCloud Category Loaders -| Loader | Notes | -| -------------------------------------------------------------------- | ----- | -| [`DracoLoader`](modules/draco/docs/api-reference/draco-loader) | | -| [`LASLoader`](modules/las/docs/api-reference/las-loader) | | -| [`OBJLoader`](modules/obj/docs/api-reference/obj-loader) | | -| [`PCDLoader`](modules/pcd/docs/api-reference/pcd-loader) | | -| [`PLYLoader`](modules/ply/docs/api-reference/ply-loader) | | -| [`TerrainLoader`](modules/terrain/docs/api-reference/terrain-loader) | | +| Loader | Notes | +| --------------------------------------------------------------------------------- | ----- | +| [`DracoLoader`](modules/draco/docs/api-reference/draco-loader) | | +| [`LASLoader`](modules/las/docs/api-reference/las-loader) | | +| [`OBJLoader`](modules/obj/docs/api-reference/obj-loader) | | +| [`PCDLoader`](modules/pcd/docs/api-reference/pcd-loader) | | +| [`PLYLoader`](modules/ply/docs/api-reference/ply-loader) | | +| [`QuantizedMeshLoader`](modules/terrain/docs/api-reference/quantized-mesh-loader) | | +| [`TerrainLoader`](modules/terrain/docs/api-reference/terrain-loader) | | ## Data Format diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json index 593e5430ff..f8d7941886 100644 --- a/docs/table-of-contents.json +++ b/docs/table-of-contents.json @@ -76,6 +76,8 @@ {"entry": "modules/obj/docs/api-reference/obj-loader"}, {"entry": "modules/pcd/docs/api-reference/pcd-loader"}, {"entry": "modules/ply/docs/api-reference/ply-loader"}, + {"entry": "modules/terrain/docs/api-reference/quantized-mesh-loader"}, + {"entry": "modules/terrain/docs/api-reference/terrain-loader"}, {"entry": "modules/video/docs/api-reference/video-loader"}, {"entry": "modules/wkt/docs/api-reference/wkt-loader"}, {"entry": "modules/wkt/docs/api-reference/wkt-writer"}, diff --git a/docs/whats-new.md b/docs/whats-new.md index 08a884d090..82b4244071 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -18,6 +18,10 @@ The `ImageLoader` now loads images as `Imagebitmap` by default on browsers that Addresses a number of compatibility issues with different tilesets that have been reported by users. See the git log or issues for details. +**@loaders.gl/terrain** + +A new `QuantizedMeshLoader` has been added to the `terrain` module to decode the [Quantized Mesh](https://github.com/CesiumGS/quantized-mesh) format. + **@loaders.gl/video** (new loader module) An experimental new module with video loading and GIF generation support. diff --git a/modules/terrain/docs/README.md b/modules/terrain/docs/README.md index 8ae915e649..35d9560659 100644 --- a/modules/terrain/docs/README.md +++ b/modules/terrain/docs/README.md @@ -1,6 +1,11 @@ # Overview -The `@loaders.gl/terrain` module reconstructs mesh surfaces from height map images, e.g. [Mapzen Terrain Tiles](https://github.com/tilezen/joerd/blob/master/docs/formats.md), which encodes elevation into R,G,B values. +The `@loaders.gl/terrain` module reconstructs mesh surfaces from either height +map images--e.g. [Mapzen Terrain Tiles][mapzen_terrain_tiles]--which encode +elevation into R,G,B values or the [quantized mesh][quantized_mesh] format. + +[mapzen_terrain_tiles]: https://github.com/tilezen/joerd/blob/master/docs/formats.md +[quantized_mesh]: https://github.com/CesiumGS/quantized-mesh ## Installation @@ -11,20 +16,9 @@ npm install @loaders.gl/core ## Attribution -`@loaders.gl/terrain` uses [MARTINI](https://github.com/mapbox/martini) for mesh reconstruction. - -ISC License - -Copyright (c) 2019, Mapbox - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. +The `QuantizedMeshLoader` is a fork of +[`quantized-mesh-decoder`](https://github.com/heremaps/quantized-mesh-decoder) +from HERE under the MIT license to decode quantized mesh. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. +The `TerrainLoader` uses [MARTINI](https://github.com/mapbox/martini) for mesh +reconstruction under the ISC License. diff --git a/modules/terrain/docs/api-reference/quantized-mesh-loader.md b/modules/terrain/docs/api-reference/quantized-mesh-loader.md new file mode 100644 index 0000000000..7a65f3f10e --- /dev/null +++ b/modules/terrain/docs/api-reference/quantized-mesh-loader.md @@ -0,0 +1,46 @@ +# QuantizedMeshLoader + +The `QuantizedMeshLoader` module reconstructs mesh surfaces from the [quantized +mesh][quantized_mesh] format. + +[quantized_mesh]: https://github.com/CesiumGS/quantized-mesh + +| Loader | Characteristic | +| --------------------- | --------------------------------------------- | +| File Extension | `.terrain` | +| File Type | Binary | +| File Format | Encoded mesh | +| Data Format | [Mesh](/docs/specifications/category-mesh.md) | +| Supported APIs | `load`, `parse`, `parseSync` | +| Decoder Type | Synchronous | +| Worker Thread Support | Yes | +| Streaming Support | No | + +## Usage + +```js +import {QuantizedMeshLoader} from '@loaders.gl/terrain'; +import {load} from '@loaders.gl/core'; + +const options = { + 'quantized-mesh': { + bounds: [0, 0, 1, 1] + } +}; +const data = await load(url, QuantizedMeshLoader, options); +``` + +## Options + +| Option | Type | Default | Description | +| -------------------------- | ------------- | -------------- | ------------------------------------------------------------------------------- | +| `quantized-mesh.bounds` | array | `[0, 0, 1, 1]` | Bounds of the image to fit x,y coordinates into. In `[minX, minY, maxX, maxY]`. | +| `quantized-mesh.workerUrl` | string | | Custom worker url. Defaults to the unpkg CDN. | + +## Remarks + +### Future Work + +- Skirting. The Quantized Mesh format includes data on which vertices are on each edge, which should assist in creating a skirt. +- Use optional Quantized Mesh extensions, such as vertex normals. +- Closer integration into tile culling. Quantized Mesh headers, the first 88 bytes, describe a tile's bounding volume and min/max elevations. Just the headers could be parsed while deciding whether the tile is in view. Upon verifying visibility, the rest of the tile's data can be parsed. diff --git a/modules/terrain/docs/api-reference/terrain-loader.md b/modules/terrain/docs/api-reference/terrain-loader.md index e11adf0279..fde81165e7 100644 --- a/modules/terrain/docs/api-reference/terrain-loader.md +++ b/modules/terrain/docs/api-reference/terrain-loader.md @@ -8,6 +8,7 @@ The `TerrainLoader` reconstructs mesh surfaces from height map images, e.g. [Map | File Type | Binary | | File Format | Encoded height map | | Data Format | [Mesh](/docs/specifications/category-mesh.md) | +| Supported APIs | `load`, `parse` | | Decoder Type | Asynchronous | | Worker Thread Support | Yes | | Streaming Support | No | diff --git a/modules/terrain/package.json b/modules/terrain/package.json index fe8e68b88a..50e1ff49ec 100644 --- a/modules/terrain/package.json +++ b/modules/terrain/package.json @@ -30,7 +30,7 @@ "scripts": { "pre-build": "npm run build-worker && npm run build-bundle && npm run build-bundle -- --env.dev", "build-bundle": "webpack --display=minimal --config ../../scripts/bundle.config.js", - "build-worker": "webpack --entry ./src/terrain-loader.worker.js --output ./dist/terrain-loader.worker.js --config ../../scripts/worker-webpack-config.js" + "build-worker": "webpack --entry ./src/terrain-loader.worker.js --output ./dist/terrain-loader.worker.js --config ../../scripts/worker-webpack-config.js && webpack --entry ./src/quantized-mesh-loader.worker.js --output ./dist/quantized-mesh-loader.worker.js --config ../../scripts/worker-webpack-config.js" }, "dependencies": { "@babel/runtime": "^7.3.1", diff --git a/modules/terrain/src/index.js b/modules/terrain/src/index.js index 8031794b84..d873b89100 100644 --- a/modules/terrain/src/index.js +++ b/modules/terrain/src/index.js @@ -1 +1,2 @@ export {TerrainLoader, TerrainWorkerLoader} from './terrain-loader'; +export {QuantizedMeshLoader, QuantizedMeshWorkerLoader} from './quantized-mesh-loader'; diff --git a/modules/terrain/src/lib/decode-quantized-mesh.js b/modules/terrain/src/lib/decode-quantized-mesh.js new file mode 100644 index 0000000000..57d79f1759 --- /dev/null +++ b/modules/terrain/src/lib/decode-quantized-mesh.js @@ -0,0 +1,325 @@ +// Copyright (C) 2018-2019 HERE Europe B.V. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +const QUANTIZED_MESH_HEADER = new Map([ + ['centerX', Float64Array.BYTES_PER_ELEMENT], + ['centerY', Float64Array.BYTES_PER_ELEMENT], + ['centerZ', Float64Array.BYTES_PER_ELEMENT], + + ['minHeight', Float32Array.BYTES_PER_ELEMENT], + ['maxHeight', Float32Array.BYTES_PER_ELEMENT], + + ['boundingSphereCenterX', Float64Array.BYTES_PER_ELEMENT], + ['boundingSphereCenterY', Float64Array.BYTES_PER_ELEMENT], + ['boundingSphereCenterZ', Float64Array.BYTES_PER_ELEMENT], + ['boundingSphereRadius', Float64Array.BYTES_PER_ELEMENT], + + ['horizonOcclusionPointX', Float64Array.BYTES_PER_ELEMENT], + ['horizonOcclusionPointY', Float64Array.BYTES_PER_ELEMENT], + ['horizonOcclusionPointZ', Float64Array.BYTES_PER_ELEMENT] +]); + +function decodeZigZag(value) { + return (value >> 1) ^ -(value & 1); +} + +function decodeHeader(dataView) { + let position = 0; + const header = {}; + + for (const [key, bytesCount] of QUANTIZED_MESH_HEADER) { + const getter = bytesCount === 8 ? dataView.getFloat64 : dataView.getFloat32; + + header[key] = getter.call(dataView, position, true); + position += bytesCount; + } + + return {header, headerEndPosition: position}; +} + +function decodeVertexData(dataView, headerEndPosition) { + let position = headerEndPosition; + const elementsPerVertex = 3; + const vertexCount = dataView.getUint32(position, true); + const vertexData = new Uint16Array(vertexCount * elementsPerVertex); + + position += Uint32Array.BYTES_PER_ELEMENT; + + const bytesPerArrayElement = Uint16Array.BYTES_PER_ELEMENT; + const elementArrayLength = vertexCount * bytesPerArrayElement; + const uArrayStartPosition = position; + const vArrayStartPosition = uArrayStartPosition + elementArrayLength; + const heightArrayStartPosition = vArrayStartPosition + elementArrayLength; + + let u = 0; + let v = 0; + let height = 0; + + for (let i = 0; i < vertexCount; i++) { + u += decodeZigZag(dataView.getUint16(uArrayStartPosition + bytesPerArrayElement * i, true)); + v += decodeZigZag(dataView.getUint16(vArrayStartPosition + bytesPerArrayElement * i, true)); + height += decodeZigZag( + dataView.getUint16(heightArrayStartPosition + bytesPerArrayElement * i, true) + ); + + vertexData[i] = u; + vertexData[i + vertexCount] = v; + vertexData[i + vertexCount * 2] = height; + } + + position += elementArrayLength * 3; + + return {vertexData, vertexDataEndPosition: position}; +} + +function decodeIndex(buffer, position, indicesCount, bytesPerIndex, encoded = true) { + let indices; + + if (bytesPerIndex === 2) { + indices = new Uint16Array(buffer, position, indicesCount); + } else { + indices = new Uint32Array(buffer, position, indicesCount); + } + + if (!encoded) { + return indices; + } + + let highest = 0; + + for (let i = 0; i < indices.length; ++i) { + const code = indices[i]; + + indices[i] = highest - code; + + if (code === 0) { + ++highest; + } + } + + return indices; +} + +function decodeTriangleIndices(dataView, vertexData, vertexDataEndPosition) { + let position = vertexDataEndPosition; + const elementsPerVertex = 3; + const vertexCount = vertexData.length / elementsPerVertex; + const bytesPerIndex = + vertexCount > 65536 ? Uint32Array.BYTES_PER_ELEMENT : Uint16Array.BYTES_PER_ELEMENT; + + if (position % bytesPerIndex !== 0) { + position += bytesPerIndex - (position % bytesPerIndex); + } + + const triangleCount = dataView.getUint32(position, true); + position += Uint32Array.BYTES_PER_ELEMENT; + + const triangleIndicesCount = triangleCount * 3; + const triangleIndices = decodeIndex( + dataView.buffer, + position, + triangleIndicesCount, + bytesPerIndex + ); + position += triangleIndicesCount * bytesPerIndex; + + return { + triangleIndicesEndPosition: position, + triangleIndices + }; +} + +function decodeEdgeIndices(dataView, vertexData, triangleIndicesEndPosition) { + let position = triangleIndicesEndPosition; + const elementsPerVertex = 3; + const vertexCount = vertexData.length / elementsPerVertex; + const bytesPerIndex = + vertexCount > 65536 ? Uint32Array.BYTES_PER_ELEMENT : Uint16Array.BYTES_PER_ELEMENT; + + const westVertexCount = dataView.getUint32(position, true); + position += Uint32Array.BYTES_PER_ELEMENT; + + const westIndices = decodeIndex(dataView.buffer, position, westVertexCount, bytesPerIndex, false); + position += westVertexCount * bytesPerIndex; + + const southVertexCount = dataView.getUint32(position, true); + position += Uint32Array.BYTES_PER_ELEMENT; + + const southIndices = decodeIndex( + dataView.buffer, + position, + southVertexCount, + bytesPerIndex, + false + ); + position += southVertexCount * bytesPerIndex; + + const eastVertexCount = dataView.getUint32(position, true); + position += Uint32Array.BYTES_PER_ELEMENT; + + const eastIndices = decodeIndex(dataView.buffer, position, eastVertexCount, bytesPerIndex, false); + position += eastVertexCount * bytesPerIndex; + + const northVertexCount = dataView.getUint32(position, true); + position += Uint32Array.BYTES_PER_ELEMENT; + + const northIndices = decodeIndex( + dataView.buffer, + position, + northVertexCount, + bytesPerIndex, + false + ); + position += northVertexCount * bytesPerIndex; + + return { + edgeIndicesEndPosition: position, + westIndices, + southIndices, + eastIndices, + northIndices + }; +} + +function decodeVertexNormalsExtension(extensionDataView) { + return new Uint8Array( + extensionDataView.buffer, + extensionDataView.byteOffset, + extensionDataView.byteLength + ); +} + +function decodeWaterMaskExtension(extensionDataView) { + return extensionDataView.buffer.slice( + extensionDataView.byteOffset, + extensionDataView.byteOffset + extensionDataView.byteLength + ); +} + +function decodeExtensions(dataView, indicesEndPosition) { + const extensions = {}; + + if (dataView.byteLength <= indicesEndPosition) { + return {extensions, extensionsEndPosition: indicesEndPosition}; + } + + let position = indicesEndPosition; + + while (position < dataView.byteLength) { + const extensionId = dataView.getUint8(position, true); + position += Uint8Array.BYTES_PER_ELEMENT; + + const extensionLength = dataView.getUint32(position, true); + position += Uint32Array.BYTES_PER_ELEMENT; + + const extensionView = new DataView(dataView.buffer, position, extensionLength); + + switch (extensionId) { + case 1: { + extensions.vertexNormals = decodeVertexNormalsExtension(extensionView); + + break; + } + case 2: { + extensions.waterMask = decodeWaterMaskExtension(extensionView); + + break; + } + default: { + // console.warn(`Unknown extension with id ${extensionId}`) + } + } + + position += extensionLength; + } + + return {extensions, extensionsEndPosition: position}; +} + +export const DECODING_STEPS = { + header: 0, + vertices: 1, + triangleIndices: 2, + edgeIndices: 3, + extensions: 4 +}; + +const DEFAULT_OPTIONS = { + maxDecodingStep: DECODING_STEPS.extensions +}; + +export default function decode(data, userOptions) { + const options = Object.assign({}, DEFAULT_OPTIONS, userOptions); + const view = new DataView(data); + const {header, headerEndPosition} = decodeHeader(view); + + if (options.maxDecodingStep < DECODING_STEPS.vertices) { + return {header}; + } + + const {vertexData, vertexDataEndPosition} = decodeVertexData(view, headerEndPosition); + + if (options.maxDecodingStep < DECODING_STEPS.triangleIndices) { + return {header, vertexData}; + } + + const {triangleIndices, triangleIndicesEndPosition} = decodeTriangleIndices( + view, + vertexData, + vertexDataEndPosition + ); + + if (options.maxDecodingStep < DECODING_STEPS.edgeIndices) { + return {header, vertexData, triangleIndices}; + } + + const { + westIndices, + southIndices, + eastIndices, + northIndices, + edgeIndicesEndPosition + } = decodeEdgeIndices(view, vertexData, triangleIndicesEndPosition); + + if (options.maxDecodingStep < DECODING_STEPS.extensions) { + return { + header, + vertexData, + triangleIndices, + westIndices, + northIndices, + eastIndices, + southIndices + }; + } + + const {extensions} = decodeExtensions(view, edgeIndicesEndPosition); + + return { + header, + vertexData, + triangleIndices, + westIndices, + northIndices, + eastIndices, + southIndices, + extensions + }; +} diff --git a/modules/terrain/src/lib/parse-quantized-mesh.js b/modules/terrain/src/lib/parse-quantized-mesh.js new file mode 100644 index 0000000000..267b720cb9 --- /dev/null +++ b/modules/terrain/src/lib/parse-quantized-mesh.js @@ -0,0 +1,69 @@ +import {getMeshBoundingBox} from '@loaders.gl/loader-utils'; +import decode, {DECODING_STEPS} from './decode-quantized-mesh'; + +function getMeshAttributes(vertexData, header, bounds) { + const {minHeight, maxHeight} = header; + const [minX, minY, maxX, maxY] = bounds || [0, 0, 1, 1]; + const xScale = maxX - minX; + const yScale = maxY - minY; + const zScale = maxHeight - minHeight; + + const nCoords = vertexData.length / 3; + // vec3. x, y defined by bounds, z in meters + const positions = new Float32Array(nCoords * 3); + + // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1. + const texCoords = new Float32Array(nCoords * 2); + + // Data is not interleaved; all u, then all v, then all heights + for (let i = 0; i < nCoords; i++) { + const x = vertexData[i] / 32767; + const y = vertexData[i + nCoords] / 32767; + const z = vertexData[i + nCoords * 2] / 32767; + + positions[3 * i + 0] = x * xScale + minX; + positions[3 * i + 1] = y * yScale + minY; + positions[3 * i + 2] = z * zScale + minHeight; + + texCoords[2 * i + 0] = x; + texCoords[2 * i + 1] = y; + } + + return { + POSITION: {value: positions, size: 3}, + TEXCOORD_0: {value: texCoords, size: 2} + // TODO: Parse normals if they exist in the file + // NORMAL: {}, - optional, but creates the high poly look with lighting + }; +} + +function getTileMesh(arrayBuffer, options) { + if (!arrayBuffer) { + return null; + } + const {bounds} = options; + // Don't parse edge indices or format extensions + const {header, vertexData, triangleIndices} = decode(arrayBuffer, DECODING_STEPS.triangleIndices); + // TODO: use skirt information from file + const attributes = getMeshAttributes(vertexData, header, bounds); + + return { + // Data return by this loader implementation + loaderData: { + header: {} + }, + header: { + vertexCount: triangleIndices.length, + // TODO: Find bounding box from header, instead of doing extra pass over + // vertices. + boundingBox: getMeshBoundingBox(attributes) + }, + mode: 4, // TRIANGLES + indices: {value: triangleIndices, size: 1}, + attributes + }; +} + +export default function loadQuantizedMesh(arrayBuffer, options) { + return getTileMesh(arrayBuffer, options['quantized-mesh']); +} diff --git a/modules/terrain/src/quantized-mesh-loader.js b/modules/terrain/src/quantized-mesh-loader.js new file mode 100644 index 0000000000..64cc0b811a --- /dev/null +++ b/modules/terrain/src/quantized-mesh-loader.js @@ -0,0 +1,26 @@ +// __VERSION__ is injected by babel-plugin-version-inline +/* global __VERSION__ */ +import parseQuantizedMesh from './lib/parse-quantized-mesh'; + +// @ts-ignore TS2304: Cannot find name '__VERSION__'. +const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest'; + +export const QuantizedMeshWorkerLoader = { + id: 'quantized-mesh', + name: 'Quantized Mesh', + version: VERSION, + extensions: ['terrain'], + mimeType: 'application/vnd.quantized-mesh', + options: { + 'quantized-mesh': { + workerUrl: `https://unpkg.com/@loaders.gl/terrain@${VERSION}/dist/quantized-mesh-loader.worker.js`, + bounds: [0, 0, 1, 1] + } + } +}; + +export const QuantizedMeshLoader = { + ...QuantizedMeshWorkerLoader, + parseSync: parseQuantizedMesh, + parse: async (arrayBuffer, options) => parseQuantizedMesh(arrayBuffer, options) +}; diff --git a/modules/terrain/src/quantized-mesh-loader.worker.js b/modules/terrain/src/quantized-mesh-loader.worker.js new file mode 100644 index 0000000000..ab09af0669 --- /dev/null +++ b/modules/terrain/src/quantized-mesh-loader.worker.js @@ -0,0 +1,4 @@ +import {QuantizedMeshLoader} from './quantized-mesh-loader'; +import {createWorker} from '@loaders.gl/loader-utils'; + +createWorker(QuantizedMeshLoader); diff --git a/modules/terrain/test/data/README.md b/modules/terrain/test/data/README.md index d142ac216e..f143135265 100644 --- a/modules/terrain/test/data/README.md +++ b/modules/terrain/test/data/README.md @@ -1,3 +1,6 @@ mapbox.png is from Mapbox's [Terrain RGB service](https://docs.mapbox.com/help/troubleshooting/access-elevation-data/#mapbox-terrain-rgb) terrarium.png is from [Mapzen Terrain Tiles](https://registry.opendata.aws/terrain-tiles/). + +`tile-with-extensions.terrain` is from [Cesium World Terrain from Cesium +ion](https://cesiumjs.org/Cesium/Build/Apps/Sandcastle/index.html?src=Terrain.html) diff --git a/modules/terrain/test/data/maptiler_10_1070_778.terrain b/modules/terrain/test/data/maptiler_10_1070_778.terrain new file mode 100644 index 0000000000..f3bd3fcfa3 Binary files /dev/null and b/modules/terrain/test/data/maptiler_10_1070_778.terrain differ diff --git a/modules/terrain/test/data/tile-with-extensions.terrain b/modules/terrain/test/data/tile-with-extensions.terrain new file mode 100644 index 0000000000..0825d8f998 Binary files /dev/null and b/modules/terrain/test/data/tile-with-extensions.terrain differ diff --git a/modules/terrain/test/index.js b/modules/terrain/test/index.js index 09c882be00..2333bcca18 100644 --- a/modules/terrain/test/index.js +++ b/modules/terrain/test/index.js @@ -1 +1,2 @@ import './terrain-loader.spec'; +import './quantized-mesh-loader.spec'; diff --git a/modules/terrain/test/quantized-mesh-loader.spec.js b/modules/terrain/test/quantized-mesh-loader.spec.js new file mode 100644 index 0000000000..d7f640eaf1 --- /dev/null +++ b/modules/terrain/test/quantized-mesh-loader.spec.js @@ -0,0 +1,64 @@ +/* eslint-disable max-len */ +import test from 'tape-promise/tape'; +import {validateLoader, validateMeshCategoryData} from 'test/common/conformance'; + +import {QuantizedMeshLoader, QuantizedMeshWorkerLoader} from '@loaders.gl/terrain'; +import {setLoaderOptions, load} from '@loaders.gl/core'; + +const TILE_WITH_EXTENSIONS_URL = '@loaders.gl/terrain/test/data/tile-with-extensions.terrain'; + +setLoaderOptions({ + 'quantized-mesh': { + workerUrl: 'modules/terrain/dist/quantized-mesh-loader.worker.js' + } +}); + +test('QuantizedMeshLoader#loader objects', async t => { + validateLoader(t, QuantizedMeshLoader, 'QuantizedMeshLoader'); + validateLoader(t, QuantizedMeshWorkerLoader, 'QuantizedMeshWorkerLoader'); + t.end(); +}); + +test('QuantizedMeshLoader#parse tile-with-extensions', async t => { + const options = {}; + const data = await load(TILE_WITH_EXTENSIONS_URL, QuantizedMeshLoader, options); + validateMeshCategoryData(t, data); // TODO: should there be a validateMeshCategoryData? + + t.equal(data.mode, 4, 'mode is TRIANGLES (4)'); + + t.equal(data.indices.value.length, 1175 * 3, 'indices was found'); + t.equal(data.indices.size, 1, 'indices was found'); + + t.equal(data.attributes.TEXCOORD_0.value.length, 627 * 2, 'TEXCOORD_0 attribute was found'); + t.equal(data.attributes.TEXCOORD_0.size, 2, 'TEXCOORD_0 attribute was found'); + + t.equal(data.attributes.POSITION.value.length, 627 * 3, 'POSITION attribute was found'); + t.equal(data.attributes.POSITION.size, 3, 'POSITION attribute was found'); + + t.end(); +}); + +test('QuantizedMeshWorkerLoader#tile-with-extensions', async t => { + if (typeof Worker === 'undefined') { + t.comment('Worker is not usable in non-browser environments'); + t.end(); + return; + } + + const options = {}; + const data = await load(TILE_WITH_EXTENSIONS_URL, QuantizedMeshWorkerLoader, options); + validateMeshCategoryData(t, data); // TODO: should there be a validateMeshCategoryData? + + t.equal(data.mode, 4, 'mode is TRIANGLES (4)'); + + t.equal(data.indices.value.length, 1175 * 3, 'indices was found'); + t.equal(data.indices.size, 1, 'indices was found'); + + t.equal(data.attributes.TEXCOORD_0.value.length, 627 * 2, 'TEXCOORD_0 attribute was found'); + t.equal(data.attributes.TEXCOORD_0.size, 2, 'TEXCOORD_0 attribute was found'); + + t.equal(data.attributes.POSITION.value.length, 627 * 3, 'POSITION attribute was found'); + t.equal(data.attributes.POSITION.size, 3, 'POSITION attribute was found'); + + t.end(); +}); diff --git a/test/aliases.js b/test/aliases.js index f656e06d5d..76481b3488 100644 --- a/test/aliases.js +++ b/test/aliases.js @@ -31,12 +31,12 @@ function makeAliases(basename = __dirname) { '@loaders.gl/core/test': path.resolve(basename, '../modules/core/test'), '@loaders.gl/csv/test': path.resolve(basename, '../modules/csv/test'), '@loaders.gl/draco/test': path.resolve(basename, '../modules/draco/test'), - '@loaders.gl/images/test': path.resolve(basename, '../modules/images/test'), '@loaders.gl/gis/test': path.resolve(basename, '../modules/gis/test'), '@loaders.gl/gltf/test': path.resolve(basename, '../modules/gltf/test'), + '@loaders.gl/i3s/test': path.resolve(basename, '../modules/i3s/test'), + '@loaders.gl/images/test': path.resolve(basename, '../modules/images/test'), '@loaders.gl/json/test': path.resolve(basename, '../modules/json/test'), '@loaders.gl/kml/test': path.resolve(basename, '../modules/kml/test'), - '@loaders.gl/i3s/test': path.resolve(basename, '../modules/i3s/test'), '@loaders.gl/las/test': path.resolve(basename, '../modules/las/test'), '@loaders.gl/mvt/test': path.resolve(basename, '../modules/mvt/test'), '@loaders.gl/obj/test': path.resolve(basename, '../modules/obj/test'), @@ -49,7 +49,7 @@ function makeAliases(basename = __dirname) { '@loaders.gl/video/test': path.resolve(basename, '../modules/video/test'), '@loaders.gl/wkt/test': path.resolve(basename, '../modules/wkt/test'), '@loaders.gl/zip/test': path.resolve(basename, '../modules/zip/test') - } + }; } module.exports = makeAliases();