@loaders.gl/terrain
Version:
Framework-independent loader for terrain raster formats
1,226 lines (1,210 loc) • 42.5 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd) define([], factory);
else if (typeof exports === 'object') exports['loaders'] = factory();
else root['loaders'] = factory();})(globalThis, function () {
"use strict";
var __exports__ = (() => {
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// external-global-plugin:@loaders.gl/core
var require_core = __commonJS({
"external-global-plugin:@loaders.gl/core"(exports, module) {
module.exports = globalThis.loaders;
}
});
// bundle.ts
var bundle_exports = {};
__export(bundle_exports, {
QuantizedMeshLoader: () => QuantizedMeshLoader2,
QuantizedMeshWorkerLoader: () => QuantizedMeshLoader,
TerrainLoader: () => TerrainLoader2,
TerrainWorkerLoader: () => TerrainLoader,
parseTerrain: () => parseTerrain
});
__reExport(bundle_exports, __toESM(require_core(), 1));
// ../loader-utils/src/loader-types.ts
async function parseFromContext(data, loaders, options, context) {
return context._parse(data, loaders, options, context);
}
// ../loader-utils/src/lib/binary-utils/array-buffer-utils.ts
function concatenateTypedArrays(...typedArrays) {
const arrays = typedArrays;
const TypedArrayConstructor = arrays && arrays.length > 1 && arrays[0].constructor || null;
if (!TypedArrayConstructor) {
throw new Error(
'"concatenateTypedArrays" - incorrect quantity of arguments or arguments have incompatible data types'
);
}
const sumLength = arrays.reduce((acc, value) => acc + value.length, 0);
const result = new TypedArrayConstructor(sumLength);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
// ../schema/src/lib/mesh/mesh-utils.ts
function getMeshBoundingBox(attributes) {
let minX = Infinity;
let minY = Infinity;
let minZ = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let maxZ = -Infinity;
const positions = attributes.POSITION ? attributes.POSITION.value : [];
const len = positions && positions.length;
for (let i = 0; i < len; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
minX = x < minX ? x : minX;
minY = y < minY ? y : minY;
minZ = z < minZ ? z : minZ;
maxX = x > maxX ? x : maxX;
maxY = y > maxY ? y : maxY;
maxZ = z > maxZ ? z : maxZ;
}
return [
[minX, minY, minZ],
[maxX, maxY, maxZ]
];
}
// src/lib/decode-quantized-mesh.ts
var QUANTIZED_MESH_HEADER = /* @__PURE__ */ 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: {
}
}
position += extensionLength;
}
return { extensions, extensionsEndPosition: position };
}
var DECODING_STEPS = {
header: 0,
vertices: 1,
triangleIndices: 2,
edgeIndices: 3,
extensions: 4
};
var DEFAULT_OPTIONS = {
maxDecodingStep: DECODING_STEPS.extensions
};
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
};
}
// src/lib/helpers/skirt.ts
function addSkirt(attributes, triangles, skirtHeight, outsideIndices) {
const outsideEdges = outsideIndices ? getOutsideEdgesFromIndices(outsideIndices, attributes.POSITION.value) : getOutsideEdgesFromTriangles(triangles);
const newPosition = new attributes.POSITION.value.constructor(outsideEdges.length * 6);
const newTexcoord0 = new attributes.TEXCOORD_0.value.constructor(outsideEdges.length * 4);
const newTriangles = new triangles.constructor(outsideEdges.length * 6);
for (let i = 0; i < outsideEdges.length; i++) {
const edge = outsideEdges[i];
updateAttributesForNewEdge({
edge,
edgeIndex: i,
attributes,
skirtHeight,
newPosition,
newTexcoord0,
newTriangles
});
}
attributes.POSITION.value = concatenateTypedArrays(attributes.POSITION.value, newPosition);
attributes.TEXCOORD_0.value = concatenateTypedArrays(attributes.TEXCOORD_0.value, newTexcoord0);
const resultTriangles = triangles instanceof Array ? triangles.concat(newTriangles) : concatenateTypedArrays(triangles, newTriangles);
return {
attributes,
triangles: resultTriangles
};
}
function getOutsideEdgesFromTriangles(triangles) {
const edges = [];
for (let i = 0; i < triangles.length; i += 3) {
edges.push([triangles[i], triangles[i + 1]]);
edges.push([triangles[i + 1], triangles[i + 2]]);
edges.push([triangles[i + 2], triangles[i]]);
}
edges.sort((a, b) => Math.min(...a) - Math.min(...b) || Math.max(...a) - Math.max(...b));
const outsideEdges = [];
let index = 0;
while (index < edges.length) {
if (edges[index][0] === edges[index + 1]?.[1] && edges[index][1] === edges[index + 1]?.[0]) {
index += 2;
} else {
outsideEdges.push(edges[index]);
index++;
}
}
return outsideEdges;
}
function getOutsideEdgesFromIndices(indices, position) {
indices.westIndices.sort((a, b) => position[3 * a + 1] - position[3 * b + 1]);
indices.eastIndices.sort((a, b) => position[3 * b + 1] - position[3 * a + 1]);
indices.southIndices.sort((a, b) => position[3 * b] - position[3 * a]);
indices.northIndices.sort((a, b) => position[3 * a] - position[3 * b]);
const edges = [];
for (const index in indices) {
const indexGroup = indices[index];
for (let i = 0; i < indexGroup.length - 1; i++) {
edges.push([indexGroup[i], indexGroup[i + 1]]);
}
}
return edges;
}
function updateAttributesForNewEdge({
edge,
edgeIndex,
attributes,
skirtHeight,
newPosition,
newTexcoord0,
newTriangles
}) {
const positionsLength = attributes.POSITION.value.length;
const vertex1Offset = edgeIndex * 2;
const vertex2Offset = edgeIndex * 2 + 1;
newPosition.set(
attributes.POSITION.value.subarray(edge[0] * 3, edge[0] * 3 + 3),
vertex1Offset * 3
);
newPosition[vertex1Offset * 3 + 2] = newPosition[vertex1Offset * 3 + 2] - skirtHeight;
newPosition.set(
attributes.POSITION.value.subarray(edge[1] * 3, edge[1] * 3 + 3),
vertex2Offset * 3
);
newPosition[vertex2Offset * 3 + 2] = newPosition[vertex2Offset * 3 + 2] - skirtHeight;
newTexcoord0.set(
attributes.TEXCOORD_0.value.subarray(edge[0] * 2, edge[0] * 2 + 2),
vertex1Offset * 2
);
newTexcoord0.set(
attributes.TEXCOORD_0.value.subarray(edge[1] * 2, edge[1] * 2 + 2),
vertex2Offset * 2
);
const triangle1Offset = edgeIndex * 2 * 3;
newTriangles[triangle1Offset] = edge[0];
newTriangles[triangle1Offset + 1] = positionsLength / 3 + vertex2Offset;
newTriangles[triangle1Offset + 2] = edge[1];
newTriangles[triangle1Offset + 3] = positionsLength / 3 + vertex2Offset;
newTriangles[triangle1Offset + 4] = edge[0];
newTriangles[triangle1Offset + 5] = positionsLength / 3 + vertex1Offset;
}
// src/lib/parse-quantized-mesh.ts
function parseQuantizedMesh(arrayBuffer, options = {}) {
const { bounds } = options;
const {
header,
vertexData,
triangleIndices: originalTriangleIndices,
westIndices,
northIndices,
eastIndices,
southIndices
} = decode(arrayBuffer, DECODING_STEPS.triangleIndices);
let triangleIndices = originalTriangleIndices;
let attributes = getMeshAttributes(vertexData, header, bounds);
const boundingBox = getMeshBoundingBox(attributes);
if (options?.skirtHeight) {
const { attributes: newAttributes, triangles: newTriangles } = addSkirt(
attributes,
triangleIndices,
options.skirtHeight,
{
westIndices,
northIndices,
eastIndices,
southIndices
}
);
attributes = newAttributes;
triangleIndices = newTriangles;
}
return {
// Data return by this loader implementation
loaderData: {
header: {}
},
header: {
// @ts-ignore
vertexCount: triangleIndices.length,
boundingBox
},
// TODO
schema: void 0,
topology: "triangle-list",
mode: 4,
// TRIANGLES
indices: { value: triangleIndices, size: 1 },
attributes
};
}
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;
const positions = new Float32Array(nCoords * 3);
const texCoords = new Float32Array(nCoords * 2);
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
};
}
// ../../node_modules/@mapbox/martini/index.js
var Martini = class {
constructor(gridSize = 257) {
this.gridSize = gridSize;
const tileSize = gridSize - 1;
if (tileSize & tileSize - 1)
throw new Error(
`Expected grid size to be 2^n+1, got ${gridSize}.`
);
this.numTriangles = tileSize * tileSize * 2 - 2;
this.numParentTriangles = this.numTriangles - tileSize * tileSize;
this.indices = new Uint32Array(this.gridSize * this.gridSize);
this.coords = new Uint16Array(this.numTriangles * 4);
for (let i = 0; i < this.numTriangles; i++) {
let id = i + 2;
let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0;
if (id & 1) {
bx = by = cx = tileSize;
} else {
ax = ay = cy = tileSize;
}
while ((id >>= 1) > 1) {
const mx = ax + bx >> 1;
const my = ay + by >> 1;
if (id & 1) {
bx = ax;
by = ay;
ax = cx;
ay = cy;
} else {
ax = bx;
ay = by;
bx = cx;
by = cy;
}
cx = mx;
cy = my;
}
const k = i * 4;
this.coords[k + 0] = ax;
this.coords[k + 1] = ay;
this.coords[k + 2] = bx;
this.coords[k + 3] = by;
}
}
createTile(terrain) {
return new Tile(terrain, this);
}
};
var Tile = class {
constructor(terrain, martini) {
const size = martini.gridSize;
if (terrain.length !== size * size)
throw new Error(
`Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`
);
this.terrain = terrain;
this.martini = martini;
this.errors = new Float32Array(terrain.length);
this.update();
}
update() {
const { numTriangles, numParentTriangles, coords, gridSize: size } = this.martini;
const { terrain, errors } = this;
for (let i = numTriangles - 1; i >= 0; i--) {
const k = i * 4;
const ax = coords[k + 0];
const ay = coords[k + 1];
const bx = coords[k + 2];
const by = coords[k + 3];
const mx = ax + bx >> 1;
const my = ay + by >> 1;
const cx = mx + my - ay;
const cy = my + ax - mx;
const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2;
const middleIndex = my * size + mx;
const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]);
errors[middleIndex] = Math.max(errors[middleIndex], middleError);
if (i < numParentTriangles) {
const leftChildIndex = (ay + cy >> 1) * size + (ax + cx >> 1);
const rightChildIndex = (by + cy >> 1) * size + (bx + cx >> 1);
errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]);
}
}
}
getMesh(maxError = 0) {
const { gridSize: size, indices } = this.martini;
const { errors } = this;
let numVertices = 0;
let numTriangles = 0;
const max = size - 1;
indices.fill(0);
function countElements(ax, ay, bx, by, cx, cy) {
const mx = ax + bx >> 1;
const my = ay + by >> 1;
if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
countElements(cx, cy, ax, ay, mx, my);
countElements(bx, by, cx, cy, mx, my);
} else {
indices[ay * size + ax] = indices[ay * size + ax] || ++numVertices;
indices[by * size + bx] = indices[by * size + bx] || ++numVertices;
indices[cy * size + cx] = indices[cy * size + cx] || ++numVertices;
numTriangles++;
}
}
countElements(0, 0, max, max, max, 0);
countElements(max, max, 0, 0, 0, max);
const vertices = new Uint16Array(numVertices * 2);
const triangles = new Uint32Array(numTriangles * 3);
let triIndex = 0;
function processTriangle(ax, ay, bx, by, cx, cy) {
const mx = ax + bx >> 1;
const my = ay + by >> 1;
if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
processTriangle(cx, cy, ax, ay, mx, my);
processTriangle(bx, by, cx, cy, mx, my);
} else {
const a = indices[ay * size + ax] - 1;
const b = indices[by * size + bx] - 1;
const c = indices[cy * size + cx] - 1;
vertices[2 * a] = ax;
vertices[2 * a + 1] = ay;
vertices[2 * b] = bx;
vertices[2 * b + 1] = by;
vertices[2 * c] = cx;
vertices[2 * c + 1] = cy;
triangles[triIndex++] = a;
triangles[triIndex++] = b;
triangles[triIndex++] = c;
}
}
processTriangle(0, 0, max, max, max, 0);
processTriangle(max, max, 0, 0, 0, max);
return { vertices, triangles };
}
};
// src/lib/delatin/index.ts
var Delatin = class {
constructor(data, width, height = width) {
this.data = data;
this.width = width;
this.height = height;
this.coords = [];
this.triangles = [];
this._halfedges = [];
this._candidates = [];
this._queueIndices = [];
this._queue = [];
this._errors = [];
this._rms = [];
this._pending = [];
this._pendingLen = 0;
this._rmsSum = 0;
const x1 = width - 1;
const y1 = height - 1;
const p0 = this._addPoint(0, 0);
const p1 = this._addPoint(x1, 0);
const p2 = this._addPoint(0, y1);
const p3 = this._addPoint(x1, y1);
const t0 = this._addTriangle(p3, p0, p2, -1, -1, -1);
this._addTriangle(p0, p3, p1, t0, -1, -1);
this._flush();
}
// refine the mesh until its maximum error gets below the given one
run(maxError = 1) {
while (this.getMaxError() > maxError) {
this.refine();
}
}
// refine the mesh with a single point
refine() {
this._step();
this._flush();
}
// max error of the current mesh
getMaxError() {
return this._errors[0];
}
// root-mean-square deviation of the current mesh
getRMSD() {
return this._rmsSum > 0 ? Math.sqrt(this._rmsSum / (this.width * this.height)) : 0;
}
// height value at a given position
heightAt(x, y) {
return this.data[this.width * y + x];
}
// rasterize and queue all triangles that got added or updated in _step
_flush() {
const coords = this.coords;
for (let i = 0; i < this._pendingLen; i++) {
const t = this._pending[i];
const a = 2 * this.triangles[t * 3 + 0];
const b = 2 * this.triangles[t * 3 + 1];
const c = 2 * this.triangles[t * 3 + 2];
this._findCandidate(
coords[a],
coords[a + 1],
coords[b],
coords[b + 1],
coords[c],
coords[c + 1],
t
);
}
this._pendingLen = 0;
}
// rasterize a triangle, find its max error, and queue it for processing
_findCandidate(p0x, p0y, p1x, p1y, p2x, p2y, t) {
const minX = Math.min(p0x, p1x, p2x);
const minY = Math.min(p0y, p1y, p2y);
const maxX = Math.max(p0x, p1x, p2x);
const maxY = Math.max(p0y, p1y, p2y);
let w00 = orient(p1x, p1y, p2x, p2y, minX, minY);
let w01 = orient(p2x, p2y, p0x, p0y, minX, minY);
let w02 = orient(p0x, p0y, p1x, p1y, minX, minY);
const a01 = p1y - p0y;
const b01 = p0x - p1x;
const a12 = p2y - p1y;
const b12 = p1x - p2x;
const a20 = p0y - p2y;
const b20 = p2x - p0x;
const a = orient(p0x, p0y, p1x, p1y, p2x, p2y);
const z0 = this.heightAt(p0x, p0y) / a;
const z1 = this.heightAt(p1x, p1y) / a;
const z2 = this.heightAt(p2x, p2y) / a;
let maxError = 0;
let mx = 0;
let my = 0;
let rms = 0;
for (let y = minY; y <= maxY; y++) {
let dx = 0;
if (w00 < 0 && a12 !== 0) {
dx = Math.max(dx, Math.floor(-w00 / a12));
}
if (w01 < 0 && a20 !== 0) {
dx = Math.max(dx, Math.floor(-w01 / a20));
}
if (w02 < 0 && a01 !== 0) {
dx = Math.max(dx, Math.floor(-w02 / a01));
}
let w0 = w00 + a12 * dx;
let w1 = w01 + a20 * dx;
let w2 = w02 + a01 * dx;
let wasInside = false;
for (let x = minX + dx; x <= maxX; x++) {
if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
wasInside = true;
const z = z0 * w0 + z1 * w1 + z2 * w2;
const dz = Math.abs(z - this.heightAt(x, y));
rms += dz * dz;
if (dz > maxError) {
maxError = dz;
mx = x;
my = y;
}
} else if (wasInside) {
break;
}
w0 += a12;
w1 += a20;
w2 += a01;
}
w00 += b12;
w01 += b20;
w02 += b01;
}
if (mx === p0x && my === p0y || mx === p1x && my === p1y || mx === p2x && my === p2y) {
maxError = 0;
}
this._candidates[2 * t] = mx;
this._candidates[2 * t + 1] = my;
this._rms[t] = rms;
this._queuePush(t, maxError, rms);
}
// process the next triangle in the queue, splitting it with a new point
_step() {
const t = this._queuePop();
const e0 = t * 3 + 0;
const e1 = t * 3 + 1;
const e2 = t * 3 + 2;
const p0 = this.triangles[e0];
const p1 = this.triangles[e1];
const p2 = this.triangles[e2];
const ax = this.coords[2 * p0];
const ay = this.coords[2 * p0 + 1];
const bx = this.coords[2 * p1];
const by = this.coords[2 * p1 + 1];
const cx = this.coords[2 * p2];
const cy = this.coords[2 * p2 + 1];
const px = this._candidates[2 * t];
const py = this._candidates[2 * t + 1];
const pn = this._addPoint(px, py);
if (orient(ax, ay, bx, by, px, py) === 0) {
this._handleCollinear(pn, e0);
} else if (orient(bx, by, cx, cy, px, py) === 0) {
this._handleCollinear(pn, e1);
} else if (orient(cx, cy, ax, ay, px, py) === 0) {
this._handleCollinear(pn, e2);
} else {
const h0 = this._halfedges[e0];
const h1 = this._halfedges[e1];
const h2 = this._halfedges[e2];
const t0 = this._addTriangle(p0, p1, pn, h0, -1, -1, e0);
const t1 = this._addTriangle(p1, p2, pn, h1, -1, t0 + 1);
const t2 = this._addTriangle(p2, p0, pn, h2, t0 + 2, t1 + 1);
this._legalize(t0);
this._legalize(t1);
this._legalize(t2);
}
}
// add coordinates for a new vertex
_addPoint(x, y) {
const i = this.coords.length >> 1;
this.coords.push(x, y);
return i;
}
// add or update a triangle in the mesh
_addTriangle(a, b, c, ab, bc, ca, e = this.triangles.length) {
const t = e / 3;
this.triangles[e + 0] = a;
this.triangles[e + 1] = b;
this.triangles[e + 2] = c;
this._halfedges[e + 0] = ab;
this._halfedges[e + 1] = bc;
this._halfedges[e + 2] = ca;
if (ab >= 0) {
this._halfedges[ab] = e + 0;
}
if (bc >= 0) {
this._halfedges[bc] = e + 1;
}
if (ca >= 0) {
this._halfedges[ca] = e + 2;
}
this._candidates[2 * t + 0] = 0;
this._candidates[2 * t + 1] = 0;
this._queueIndices[t] = -1;
this._rms[t] = 0;
this._pending[this._pendingLen++] = t;
return e;
}
_legalize(a) {
const b = this._halfedges[a];
if (b < 0) {
return;
}
const a0 = a - a % 3;
const b0 = b - b % 3;
const al = a0 + (a + 1) % 3;
const ar = a0 + (a + 2) % 3;
const bl = b0 + (b + 2) % 3;
const br = b0 + (b + 1) % 3;
const p0 = this.triangles[ar];
const pr = this.triangles[a];
const pl = this.triangles[al];
const p1 = this.triangles[bl];
const coords = this.coords;
if (!inCircle(
coords[2 * p0],
coords[2 * p0 + 1],
coords[2 * pr],
coords[2 * pr + 1],
coords[2 * pl],
coords[2 * pl + 1],
coords[2 * p1],
coords[2 * p1 + 1]
)) {
return;
}
const hal = this._halfedges[al];
const har = this._halfedges[ar];
const hbl = this._halfedges[bl];
const hbr = this._halfedges[br];
this._queueRemove(a0 / 3);
this._queueRemove(b0 / 3);
const t0 = this._addTriangle(p0, p1, pl, -1, hbl, hal, a0);
const t1 = this._addTriangle(p1, p0, pr, t0, har, hbr, b0);
this._legalize(t0 + 1);
this._legalize(t1 + 2);
}
// handle a case where new vertex is on the edge of a triangle
_handleCollinear(pn, a) {
const a0 = a - a % 3;
const al = a0 + (a + 1) % 3;
const ar = a0 + (a + 2) % 3;
const p0 = this.triangles[ar];
const pr = this.triangles[a];
const pl = this.triangles[al];
const hal = this._halfedges[al];
const har = this._halfedges[ar];
const b = this._halfedges[a];
if (b < 0) {
const t02 = this._addTriangle(pn, p0, pr, -1, har, -1, a0);
const t12 = this._addTriangle(p0, pn, pl, t02, -1, hal);
this._legalize(t02 + 1);
this._legalize(t12 + 2);
return;
}
const b0 = b - b % 3;
const bl = b0 + (b + 2) % 3;
const br = b0 + (b + 1) % 3;
const p1 = this.triangles[bl];
const hbl = this._halfedges[bl];
const hbr = this._halfedges[br];
this._queueRemove(b0 / 3);
const t0 = this._addTriangle(p0, pr, pn, har, -1, -1, a0);
const t1 = this._addTriangle(pr, p1, pn, hbr, -1, t0 + 1, b0);
const t2 = this._addTriangle(p1, pl, pn, hbl, -1, t1 + 1);
const t3 = this._addTriangle(pl, p0, pn, hal, t0 + 2, t2 + 1);
this._legalize(t0);
this._legalize(t1);
this._legalize(t2);
this._legalize(t3);
}
// priority queue methods
_queuePush(t, error, rms) {
const i = this._queue.length;
this._queueIndices[t] = i;
this._queue.push(t);
this._errors.push(error);
this._rmsSum += rms;
this._queueUp(i);
}
_queuePop() {
const n = this._queue.length - 1;
this._queueSwap(0, n);
this._queueDown(0, n);
return this._queuePopBack();
}
_queuePopBack() {
const t = this._queue.pop();
this._errors.pop();
this._rmsSum -= this._rms[t];
this._queueIndices[t] = -1;
return t;
}
_queueRemove(t) {
const i = this._queueIndices[t];
if (i < 0) {
const it = this._pending.indexOf(t);
if (it !== -1) {
this._pending[it] = this._pending[--this._pendingLen];
} else {
throw new Error("Broken triangulation (something went wrong).");
}
return;
}
const n = this._queue.length - 1;
if (n !== i) {
this._queueSwap(i, n);
if (!this._queueDown(i, n)) {
this._queueUp(i);
}
}
this._queuePopBack();
}
_queueLess(i, j) {
return this._errors[i] > this._errors[j];
}
_queueSwap(i, j) {
const pi = this._queue[i];
const pj = this._queue[j];
this._queue[i] = pj;
this._queue[j] = pi;
this._queueIndices[pi] = j;
this._queueIndices[pj] = i;
const e = this._errors[i];
this._errors[i] = this._errors[j];
this._errors[j] = e;
}
_queueUp(j0) {
let j = j0;
while (true) {
const i = j - 1 >> 1;
if (i === j || !this._queueLess(j, i)) {
break;
}
this._queueSwap(i, j);
j = i;
}
}
_queueDown(i0, n) {
let i = i0;
while (true) {
const j1 = 2 * i + 1;
if (j1 >= n || j1 < 0) {
break;
}
const j2 = j1 + 1;
let j = j1;
if (j2 < n && this._queueLess(j2, j1)) {
j = j2;
}
if (!this._queueLess(j, i)) {
break;
}
this._queueSwap(i, j);
i = j;
}
return i > i0;
}
};
function orient(ax, ay, bx, by, cx, cy) {
return (bx - cx) * (ay - cy) - (by - cy) * (ax - cx);
}
function inCircle(ax, ay, bx, by, cx, cy, px, py) {
const dx = ax - px;
const dy = ay - py;
const ex = bx - px;
const ey = by - py;
const fx = cx - px;
const fy = cy - py;
const ap = dx * dx + dy * dy;
const bp = ex * ex + ey * ey;
const cp = fx * fx + fy * fy;
return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0;
}
// src/lib/parse-terrain.ts
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;
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 = getMeshAttributes2(vertices, terrain, width, height, bounds);
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
};
}
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 };
}
function getDelatinTileMesh(meshMaxError, width, height, terrain) {
const tin = new Delatin(terrain, width + 1, height + 1);
tin.run(meshMaxError);
const { coords, triangles } = tin;
const vertices = coords;
return { vertices, triangles };
}
function getTerrain(imageData, width, height, elevationDecoder, tesselator) {
const { rScaler, bScaler, gScaler, offset } = elevationDecoder;
const terrain = new Float32Array((width + 1) * (height + 1));
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") {
for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
terrain[i] = terrain[i - width - 1];
}
for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
terrain[i] = terrain[i - 1];
}
}
return terrain;
}
function getMeshAttributes2(vertices, terrain, width, height, bounds) {
const gridSize = width + 1;
const numOfVerticies = vertices.length / 2;
const positions = new Float32Array(numOfVerticies * 3);
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
};
}
// src/lib/utils/version.ts
var VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "latest";
// src/terrain-loader.ts
var TerrainLoader = {
dataType: null,
batchType: null,
name: "Terrain",
id: "terrain",
module: "terrain",
version: VERSION,
worker: true,
extensions: ["png", "pngraw", "jpg", "jpeg", "gif", "webp", "bmp"],
mimeTypes: ["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp"],
options: {
terrain: {
tesselator: "auto",
bounds: void 0,
meshMaxError: 10,
elevationDecoder: {
rScaler: 1,
gScaler: 0,
bScaler: 0,
offset: 0
},
skirtHeight: void 0
}
}
};
// src/quantized-mesh-loader.ts
var QuantizedMeshLoader = {
dataType: null,
// Mesh,
batchType: null,
name: "Quantized Mesh",
id: "quantized-mesh",
module: "terrain",
version: VERSION,
worker: true,
extensions: ["terrain"],
mimeTypes: ["application/vnd.quantized-mesh"],
options: {
"quantized-mesh": {
bounds: [0, 0, 1, 1],
skirtHeight: null
}
}
};
// src/index.ts
var TerrainLoader2 = {
...TerrainLoader,
parse: parseTerrain
};
async function parseTerrain(arrayBuffer, options, context) {
const loadImageOptions = {
...options,
mimeType: "application/x.image",
image: { ...options?.image, type: "data" }
};
const image = await parseFromContext(arrayBuffer, [], loadImageOptions, context);
const terrainOptions = { ...TerrainLoader2.options.terrain, ...options?.terrain };
return makeTerrainMeshFromImage(image, terrainOptions);
}
var QuantizedMeshLoader2 = {
...QuantizedMeshLoader,
parseSync: (arrayBuffer, options) => parseQuantizedMesh(arrayBuffer, options?.["quantized-mesh"]),
parse: async (arrayBuffer, options) => parseQuantizedMesh(arrayBuffer, options?.["quantized-mesh"])
};
return __toCommonJS(bundle_exports);
})();
return __exports__;
});