UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

257 lines (252 loc) 8.3 kB
import { BufferAttribute, BufferGeometry } from 'three'; const normalBufferPool = new Map(); function getNormalBuffer(length) { let buffer = normalBufferPool.get(length); if (!buffer) { buffer = new Float32Array(length); for (let i = 0; i < buffer.length; i += 3) { // Z-up vector buffer[i + 0] = 0; buffer[i + 1] = 0; buffer[i + 2] = 1; } normalBufferPool.set(length, buffer); } return buffer; } /** * The TileGeometry provides a new buffer geometry for each * {@link TileMesh} of a * {@link Map} object. * * It is implemented for performance using a rolling approach. * The rolling approach is a special case of the sliding window algorithm with * a single value window where we iterate (roll, slide) over the data array to * compute everything in a single pass (complexity O(n)). * By default it produces square geometries but providing different width and height * allows for rectangular tiles creation. * * ```js * // Inspired from Map.requestNewTile * const extent = new Extent('EPSG:3857', -1000, -1000, 1000, 1000); * const paramsGeometry = { extent, segment: 8 }; * const geometry = new TileGeometry(paramsGeometry); * ``` */ class TileGeometry extends BufferGeometry { isMemoryUsage = true; getMemoryUsage(context) { let cpuMemory = 0; let gpuMemory = 0; for (const attribute of Object.values(this.attributes)) { const bytes = attribute.array.byteLength; cpuMemory += bytes; gpuMemory += bytes; } if (this.index) { const bytes = this.index.array.byteLength; cpuMemory += bytes; gpuMemory += bytes; } context.objects.set(this.id, { cpuMemory, gpuMemory }); } /** * @param params - Parameters to construct the grid. Should contain an extent * and a size, either a number of segment or a width and an height in pixels. */ constructor(params) { super(); // Still mandatory to have on the geometry ? this.dimensions = params.dimensions; // Compute properties of the grid, square or rectangular. this._segments = params.segments; this.props = this.updateProps(); this.computeBuffers(this.props); // Compute the Oriented Bounding Box for spatial operations this.computeBoundingBox(); } updateProps() { const width = this._segments + 1; const height = this._segments + 1; const dimension = this.dimensions; const uvStep = 1 / this._segments; const uvStepY = 1 / this._segments; const rowStep = uvStep * dimension.x; const columnStep = uvStepY * -dimension.y; this.props = { width, height, uvStepX: uvStep, uvStepY, rowStep, columnStep, translateX: -this._segments * 0.5 * rowStep, translateY: -this._segments * 0.5 * columnStep, triangles: this._segments * this._segments * 2, numVertices: width * height }; return this.props; } get segments() { return this._segments; } set segments(v) { if (this._segments !== v) { this._segments = v; this.updateProps(); this.computeBuffers(this.props); } } /** * Resets all elevations to zero. */ resetHeights() { const positions = this.getAttribute('position'); for (let i = 0; i < positions.count; i++) { positions.setZ(i, 0); } positions.needsUpdate = true; this.computeBoundingBox(); } /** * Applies the provided heightmap to vertices' z-coordinate. * @param heightMap - The heightmap buffer. * @param width - The width of the heightmap, in pixels. * @param height - The height of the heightmap, in pixels. * @param stride - The stride to use when sampling the heightmap buffer. * @param offsetScale - The offset/scale to apply to UV coordinate before sampling the heightmap. * @returns The min/max elevation values encountered while updating the mesh. */ applyHeightMap(heightMap) { /** * Returns the elevation by sampling the heightmap at the (u, v) coordinate. * Note: the sampling does not perform any interpolation. */ function getElevation(u, v) { if (heightMap == null) { return 0; } return heightMap.getValue(u, v, true) ?? 0; } const segments = this._segments; const step = 1 / segments; const positions = this.getAttribute('position'); let min = +Infinity; let max = -Infinity; const verticesPerRow = segments + 1; for (let i = 0; i < verticesPerRow; i++) { for (let j = 0; j < verticesPerRow; j++) { const u = i * step; const v = j * step; const z = getElevation(u, v); min = Math.min(z, min); max = Math.max(z, max); const idx = i + j * verticesPerRow; positions.setZ(idx, z); } } positions.needsUpdate = true; this.computeBoundingBox(); this.computeBoundingSphere(); return { min, max }; } /** * Construct a simple grid buffer geometry using a fast rolling approach. * * @param props - Properties of the TileGeometry grid, as prepared by this.prepare. */ computeBuffers(props) { const width = props.width; const height = props.height; const rowStep = props.rowStep; const columnStep = props.columnStep; const translateX = props.translateX; const translateY = props.translateY; const uvStepX = props.uvStepX; const uvStepY = props.uvStepY; const numVertices = props.numVertices; const uvs = new Float32Array(numVertices * 2); const positions = new Float32Array(numVertices * 3); const indexCount = props.triangles * 3; const indices = positions.length <= 65536 ? new Uint16Array(indexCount) : new Uint32Array(indexCount); let posX; let h = 0; let iPos = 0; let uvY = 0.0; let indicesNdx = 0; let posY = translateY; let posNdx; let uvNdx; // Top border // for (posX = 0; posX < width; posX++) { // Store xy position and and corresponding uv of a pixel data. posNdx = iPos * 3; positions[posNdx + 0] = posX * rowStep + translateX; positions[posNdx + 1] = -posY; positions[posNdx + 2] = 0.0; uvNdx = iPos * 2; uvs[uvNdx + 0] = posX * uvStepX; uvs[uvNdx + 1] = uvY; iPos += 1; } // Next rows // for (h = 1; h < height; h++) { posY = h * columnStep + translateY; uvY = h * uvStepY; // First cell, left border posX = 0; // Store xy position and and corresponding uv of a pixel data. posNdx = iPos * 3; positions[posNdx + 0] = posX * rowStep + translateX; positions[posNdx + 1] = -posY; positions[posNdx + 2] = 0.0; uvNdx = iPos * 2; uvs[uvNdx + 0] = posX * uvStepX; uvs[uvNdx + 1] = uvY; iPos += 1; // Next cells for (posX = 1; posX < width; posX++) { // Construct indices as two different triangles from a // particular vertex. Use previous and aboves while rolling // so discard first row (top border) and first data of each // row (left border). // x---x x . // \ | | \ // \ | | \ // . x x---x const above = iPos - width; const previousPos = iPos - 1; const previousAbove = above - 1; indices[indicesNdx + 0] = iPos; indices[indicesNdx + 1] = previousAbove; indices[indicesNdx + 2] = above; indices[indicesNdx + 3] = iPos; indices[indicesNdx + 4] = previousPos; indices[indicesNdx + 5] = previousAbove; indicesNdx += 6; // Store xy position and and corresponding uv of a pixel data. posNdx = iPos * 3; positions[posNdx + 0] = posX * rowStep + translateX; positions[posNdx + 1] = -posY; positions[posNdx + 2] = 0.0; uvNdx = iPos * 2; uvs[uvNdx + 0] = posX * uvStepX; uvs[uvNdx + 1] = uvY; iPos += 1; } } this.setAttribute('uv', new BufferAttribute(uvs, 2)); this.setAttribute('position', new BufferAttribute(positions, 3)); this.setAttribute('normal', new BufferAttribute(getNormalBuffer(positions.length), 3)); this.setIndex(new BufferAttribute(indices, 1)); } } export default TileGeometry;