UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

180 lines (167 loc) 5.2 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { MathUtils, RGBAFormat, UnsignedByteType, Vector2 } from 'three'; import TextureGenerator from '../utils/TextureGenerator'; const RGBA_OFFSET = 20000; const temp = { input: new Vector2(), output: new Vector2(), ij: new Vector2(), topLeft: new Vector2(), bottomRight: new Vector2() }; /** * Utility class to sample an elevation raster. */ export class HeightMap { /** * The heightmap data. */ /** * The width, in pixels, of the heightmap buffer. */ /** * The height, in pixels, of the heightmap buffer. */ /** * The transformation to apply to UV coordinates before sampling the buffer. */ /** * The distance between each elevation value in the buffer. * e.g If the buffer is an RGBA buffer, stride is 4. */ /** * The format of the underlying buffer pixels. */ /** * The data type of the underlying buffer pixels. */ /** * The vertical precision of the height values to apply during decoding. */ /** * The offset to apply to height values during decoding. */ /** * The vertical scaling to apply in order to get values in meter. */ constructor(buffer, width, height, offsetScale, format, type, precision, offset, verticalScaling) { const stride = TextureGenerator.getChannelCount(format); if (buffer.length < width * height * stride) { throw new Error('buffer is too small'); } this.buffer = buffer; this.width = width; this.height = height; this.offsetScale = offsetScale; this.stride = stride; this.format = format; this.type = type; this.precision = precision ?? 0.1; this.offset = offset ?? RGBA_OFFSET; this.verticalScaling = verticalScaling ?? 1; } readRGBA(index, ignoreNoData) { const { buffer, stride } = this; const r = buffer[index * stride + 0]; const g = buffer[index * stride + 1]; const b = buffer[index * stride + 2]; const alpha = buffer[index * stride + 3]; if (!ignoreNoData && alpha === 0) { return null; } return (r + g * 256.0 + b * 256.0 * 256.0) * this.precision - this.offset; } readRG(index, ignoreNoData) { const { buffer, stride } = this; const alpha = buffer[index * stride + 1]; if (!ignoreNoData && alpha === 0) { return null; } const value = buffer[index * stride + 0]; return value; } clone() { return new HeightMap(this.buffer, this.width, this.height, this.offsetScale.clone(), this.format, this.type, this.precision, this.offset, this.verticalScaling); } /** * Returns the elevation of the pixel that contains the UV coordinate. * No interpolation is performed. * @param u - The normalized U coordinate (along the horizontal axis). * @param v - The normalized V coordinate (along the vertical axis). * @param ignoreTransparentPixels - If `true`, then transparent pixels are returned. Otherwise * values that match transparent pixels return `null`. Default is `false`. */ getValue(u, v, ignoreTransparentPixels = false) { const ij = this.getPixelCoordinates(u, v, temp.ij); return this.getValueRaw(ij.x, ij.y, ignoreTransparentPixels); } /** * Computes the min/max elevation from the given normalized region. * @param uvRect - The normalized region to process. * @returns The min/max, if any, otherwise `null`. */ getMinMax(uvRect) { const left = uvRect.left; const top = uvRect.top; const bottom = uvRect.bottom; const right = uvRect.right; let min = +Infinity; let max = -Infinity; const topLeft = this.getPixelCoordinates(left, top, temp.topLeft); const bottomRight = this.getPixelCoordinates(right, bottom, temp.bottomRight); for (let i = topLeft.x; i <= bottomRight.x; i++) { for (let j = bottomRight.y; j <= topLeft.y; j++) { const z = this.getValueRaw(i, j, true); if (z != null) { min = Math.min(z, min); max = Math.max(z, max); } } } if (isFinite(min) && isFinite(max)) { return { min, max }; } return null; } getPixelCoordinates(u, v, target) { const { width, height, offsetScale } = this; temp.input.set(u, v); const transformed = offsetScale.transform(temp.input, temp.output); const uu = MathUtils.clamp(transformed.x, 0, 1); const vv = MathUtils.clamp(transformed.y, 0, 1); const i = MathUtils.clamp(Math.round(uu * width - 1), 0, width); const j = MathUtils.clamp(Math.round(vv * height - 1), 0, height); return target.set(i, j); } getValueRaw(i, j, ignoreTransparentPixels = false) { const index = i + j * this.width; let rawValue = null; if (this.format === RGBAFormat && this.type === UnsignedByteType) { rawValue = this.readRGBA(index, ignoreTransparentPixels); } else { rawValue = this.readRG(index, ignoreTransparentPixels); } if (rawValue !== null) { rawValue *= this.verticalScaling; } return rawValue; } } export default HeightMap;