UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

215 lines (197 loc) 7.79 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Box3, BufferAttribute, BufferGeometry, Float32BufferAttribute, Sphere, Vector2, Vector3 } from 'three'; import { getGeometryMemoryUsage } from '../../core/MemoryUsage'; import { getGridBuffers, iterateBottomVertices, iterateSkirtVertices } from './GridBuilder'; const tmpVec3 = new Vector3(); const tmpVec2 = new Vector2(); /** * Geometry for map tiles in a planar coordinate system (where the up axis is the same everywhere). */ class PlanarTileGeometry extends BufferGeometry { isMemoryUsage = true; _heightMap = null; _skirtDepth = null; getMemoryUsage(context) { getGeometryMemoryUsage(context, this); } get vertexCount() { return this.getAttribute('position').count; } get origin() { return this._origin; } get raycastGeometry() { // No distinction between the raycast geometry and the rendered geometry. return this; } /** * @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(); this._extent = params.extent; this._origin = this._extent.center().toVector3(); this._dimensions = params.extent.dimensions(); this._skirtDepth = params.skirtDepth ?? null; this._segments = params.segments; this.buildBuffers(this); } get segments() { return this._segments; } set segments(v) { if (this._segments !== v) { this._segments = v; this.buildBuffers(this); } } resetHeights() { const positions = this.getAttribute('position'); let end = positions.count; if (this._skirtDepth != null) { end -= 4; } for (let i = 0; i < end; i++) { positions.setZ(i, 0); } if (this._skirtDepth != null) { for (let i = end; i < end + 4; i++) { positions.setZ(i, this._skirtDepth); } } positions.needsUpdate = true; this.computeBoundingBox(); } applyHeightMap(heightMap) { this._heightMap = heightMap; return this.buildBuffers(this); } buildBuffers(geometry) { this.dispose(); const rowVertices = this._segments + 1; const dims = this._dimensions; const width = dims.width; const height = dims.height; // Positions are relative to the origin of the tile const origin = this._origin; const buffers = getGridBuffers(this._segments, this._skirtDepth != null); const heightMap = this._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; } let min = +Infinity; let max = -Infinity; const boundingBox = new Box3().makeEmpty(); // Those buffers need to be cloned because they are unique per-tile const positionBuffer = buffers.positionBuffer.clone(); const normalBuffer = buffers.normalBuffer.clone(); // But these one can be reused as they are never modified const uvBuffer = buffers.uvBuffer; const indexBuffer = buffers.indexBuffer; const position = new Vector3(); const up = new Vector3(0, 0, 1); const north = new Vector3(0, 1, 0); const east = new Vector3(1, 0, 0); const south = new Vector3(0, -1, 0); const west = new Vector3(-1, 0, 0); const down = new Vector3(0, 0, -1); for (let j = 0; j < rowVertices; j++) { for (let i = 0; i < rowVertices; i++) { const idx = j * rowVertices + i; const u = i / this.segments; const v = j / this.segments; const altitude = getElevation(u, 1 - v); min = Math.min(min, altitude); max = Math.max(max, altitude); const x = origin.x - width / 2 + u * width; const y = origin.y + height / 2 - v * height; position.set(x, y, altitude); const pos = position.sub(origin); boundingBox.expandByPoint(pos); positionBuffer.set(idx, pos.x, pos.y, altitude); normalBuffer.set(idx, up.x, up.y, up.z); } } if (this._skirtDepth != null) { const skirtDepth = this._skirtDepth; const skirtStart = rowVertices * rowVertices; // Let's set the skirt vertices position to match the XY position of their top // edge counterpart, but with the Z coordinate set to the desired depth. iterateSkirtVertices(this.segments, positionBuffer, (side, top, skirtTop, skirtBottom) => { const x = positionBuffer.getX(top); const y = positionBuffer.getY(top); const z = positionBuffer.getZ(top); positionBuffer.set(skirtTop, x, y, z); positionBuffer.set(skirtBottom, x, y, skirtDepth); }); const normals = [north, east, south, west]; // Let's set the normal of each skirt side to point to its cardinal direction (in local space) iterateSkirtVertices(this.segments, normalBuffer, (side, top, skirtTop, skirtBottom) => { const normal = normals[side]; normalBuffer.setVector(skirtTop, normal); normalBuffer.setVector(skirtBottom, normal); }); // Finally, set the UV coordinates of the side to match the UV coordinates // of the original mesh's corresponding side. // We don't want the shader to deform the vertices on the bottom of the skirts, // so we use a special UV value to flag them. const bottomUv = new Vector2(-999, -999); iterateSkirtVertices(this.segments, normalBuffer, (side, top, skirtTop, skirtBottom) => { const uv = uvBuffer.get(top, tmpVec2); uvBuffer.setVector(skirtTop, uv); uvBuffer.setVector(skirtBottom, bottomUv); }); // Let's set the vertex positions for the bottom side const last = positionBuffer.length; const bboxMin = boundingBox.min; const bboxMax = boundingBox.max; positionBuffer.set(last - 4, bboxMin.x, bboxMax.y, skirtDepth); positionBuffer.set(last - 3, bboxMax.x, bboxMax.y, skirtDepth); positionBuffer.set(last - 2, bboxMax.x, bboxMin.y, skirtDepth); positionBuffer.set(last - 1, bboxMin.x, bboxMin.y, skirtDepth); iterateBottomVertices(uvBuffer, idx => { uvBuffer.setVector(idx, bottomUv); }); iterateBottomVertices(normalBuffer, idx => { normalBuffer.setVector(idx, down); }); // Let's include the skirt vertices into the bounding box boundingBox.expandByPoint(positionBuffer.get(skirtStart + 0, tmpVec3)); boundingBox.expandByPoint(positionBuffer.get(skirtStart + 1, tmpVec3)); boundingBox.expandByPoint(positionBuffer.get(skirtStart + 2, tmpVec3)); boundingBox.expandByPoint(positionBuffer.get(skirtStart + 3, tmpVec3)); } // Per-tile buffers geometry.setAttribute('position', new Float32BufferAttribute(positionBuffer.array, 3)); geometry.setAttribute('normal', new Float32BufferAttribute(normalBuffer.array, 3)); // Shared buffers geometry.setAttribute('uv', new Float32BufferAttribute(uvBuffer.array, 2)); geometry.setIndex(new BufferAttribute(indexBuffer, 1)); this.boundingBox = boundingBox; this.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); if (this._skirtDepth != null) { const topVertexCount = rowVertices * rowVertices; // Let's use distinct material groups for the top side // and the skirts, so that we can use different materials this.addGroup(0, topVertexCount, 0); this.addGroup(topVertexCount, 4, 1); } return { min, max }; } } export default PlanarTileGeometry;