// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {getMeshBoundingBox} from '@loaders.gl/schema'; import Martini from '@mapbox/martini'; import Delatin from './delatin/index'; import {addSkirt} from './helpers/skirt'; export type TerrainOptions = { meshMaxError: number; bounds: number[]; elevationDecoder: ElevationDecoder; tesselator: 'martini' | 'delatin' | 'auto'; skirtHeight?: number; }; type TerrainImage = { data: Uint8Array; width: number; height: number; }; type ElevationDecoder = { rScaler: any; bScaler: any; gScaler: any; offset: number; }; /** * Returns generated mesh object from image data * * @param terrainImage terrain image data * @param terrainOptions terrain options * @returns mesh object */ export function makeTerrainMeshFromImage( terrainImage: TerrainImage, terrainOptions: TerrainOptions ) { const {meshMaxError, bounds, elevationDecoder} = terrainOptions; const {data, width, height} = terrainImage; let terrain; let mesh; switch (terrainOptions.tesselator) { case 'martini': terrain = getTerrain(data, width, height, elevationDecoder, terrainOptions.tesselator); mesh = getMartiniTileMesh(meshMaxError, width, terrain); break; case 'delatin': terrain = getTerrain(data, width, height, elevationDecoder, terrainOptions.tesselator); mesh = getDelatinTileMesh(meshMaxError, width, height, terrain); break; // auto default: if (width === height && !(height & (width - 1))) { terrain = getTerrain(data, width, height, elevationDecoder, 'martini'); mesh = getMartiniTileMesh(meshMaxError, width, terrain); } else { terrain = getTerrain(data, width, height, elevationDecoder, 'delatin'); mesh = getDelatinTileMesh(meshMaxError, width, height, terrain); } break; } const {vertices} = mesh; let {triangles} = mesh; let attributes = getMeshAttributes(vertices, terrain, width, height, bounds); // Compute bounding box before adding skirt so that z values are not skewed const boundingBox = getMeshBoundingBox(attributes); if (terrainOptions.skirtHeight) { const {attributes: newAttributes, triangles: newTriangles} = addSkirt( attributes, triangles, terrainOptions.skirtHeight ); attributes = newAttributes; triangles = newTriangles; } return { // Data return by this loader implementation loaderData: { header: {} }, header: { vertexCount: triangles.length, boundingBox }, mode: 4, // TRIANGLES indices: {value: Uint32Array.from(triangles), size: 1}, attributes }; } /** * Get Martini generated vertices and triangles * * @param {number} meshMaxError threshold for simplifying mesh * @param {number} width width of the input data * @param {number[] | Float32Array} terrain elevation data * @returns {{vertices: Uint16Array, triangles: Uint32Array}} vertices and triangles data */ function getMartiniTileMesh(meshMaxError, width, terrain) { const gridSize = width + 1; const martini = new Martini(gridSize); const tile = martini.createTile(terrain); const {vertices, triangles} = tile.getMesh(meshMaxError); return {vertices, triangles}; } /** * Get Delatin generated vertices and triangles * * @param {number} meshMaxError threshold for simplifying mesh * @param {number} width width of the input data array * @param {number} height height of the input data array * @param {number[] | Float32Array} terrain elevation data * @returns {{vertices: number[], triangles: number[]}} vertices and triangles data */ function getDelatinTileMesh(meshMaxError, width, height, terrain) { const tin = new Delatin(terrain, width + 1, height + 1); tin.run(meshMaxError); // @ts-expect-error const {coords, triangles} = tin; const vertices = coords; return {vertices, triangles}; } function getTerrain( imageData: Uint8Array, width: number, height: number, elevationDecoder: ElevationDecoder, tesselator: 'martini' | 'delatin' ) { const {rScaler, bScaler, gScaler, offset} = elevationDecoder; // From Martini demo // https://observablehq.com/@mourner/martin-real-time-rtin-terrain-mesh const terrain = new Float32Array((width + 1) * (height + 1)); // decode terrain values for (let i = 0, y = 0; y < height; y++) { for (let x = 0; x < width; x++, i++) { const k = i * 4; const r = imageData[k + 0]; const g = imageData[k + 1]; const b = imageData[k + 2]; terrain[i + y] = r * rScaler + g * gScaler + b * bScaler + offset; } } if (tesselator === 'martini') { // backfill bottom border for (let i = (width + 1) * width, x = 0; x < width; x++, i++) { terrain[i] = terrain[i - width - 1]; } // backfill right border for (let i = height, y = 0; y < height + 1; y++, i += height + 1) { terrain[i] = terrain[i - 1]; } } return terrain; } function getMeshAttributes( vertices, terrain: Uint8Array, width: number, height: number, bounds: number[] ) { const gridSize = width + 1; const numOfVerticies = vertices.length / 2; // vec3. x, y in pixels, z in meters const positions = new Float32Array(numOfVerticies * 3); // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1. const texCoords = new Float32Array(numOfVerticies * 2); const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height]; const xScale = (maxX - minX) / width; const yScale = (maxY - minY) / height; for (let i = 0; i < numOfVerticies; i++) { const x = vertices[i * 2]; const y = vertices[i * 2 + 1]; const pixelIdx = y * gridSize + x; positions[3 * i + 0] = x * xScale + minX; positions[3 * i + 1] = -y * yScale + maxY; positions[3 * i + 2] = terrain[pixelIdx]; texCoords[2 * i + 0] = x / width; texCoords[2 * i + 1] = y / height; } return { POSITION: {value: positions, size: 3}, TEXCOORD_0: {value: texCoords, size: 2} // NORMAL: {}, - optional, but creates the high poly look with lighting }; }