@loaders.gl/terrain
Version:
Framework-independent loader for terrain raster formats
150 lines (149 loc) • 6.01 kB
JavaScript
// 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.js";
import { addSkirt } from "./helpers/skirt.js";
/**
* Returns generated mesh object from image data
*
* @param terrainImage terrain image data
* @param terrainOptions terrain options
* @returns mesh object
*/
export function makeTerrainMeshFromImage(terrainImage, 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, width, height, elevationDecoder, tesselator) {
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, width, height, bounds) {
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
};
}