UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

233 lines (176 loc) 7.81 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Box3, BufferAttribute, BufferGeometry, Vector2, Vector3 } from 'three'; import type Ellipsoid from '../../core/geographic/Ellipsoid'; import type Extent from '../../core/geographic/Extent'; import type HeightMap from '../../core/HeightMap'; import type MemoryUsage from '../../core/MemoryUsage'; import type { GetMemoryUsageContext } from '../../core/MemoryUsage'; import type { VectorArray } from '../../core/VectorArray'; import type TileGeometry from './TileGeometry'; import { getGeometryMemoryUsage } from '../../core/MemoryUsage'; import { getGridBuffers } from './GridBuilder'; const tmpVec2 = new Vector2(); const tmpVec3 = new Vector3(); const tmpNormal = new Vector3(); const tmpNW = new Vector3(); const tmpNE = new Vector3(); const tmpSW = new Vector3(); const tmpSE = new Vector3(); enum Usage { Rendering, Raycasting, } function copySkirtValues(array: VectorArray, segments: number): void { const rowSize = segments + 1; const length = rowSize * rowSize; const end = length; const UL = 0; const UR = rowSize; const LL = end - rowSize; const LR = end; array.copyItem(UL, end + 0); array.copyItem(UR, end + 1); array.copyItem(LL, end + 2); array.copyItem(LR, end + 3); } export class EllipsoidTileGeometry extends BufferGeometry implements MemoryUsage, TileGeometry { public readonly isMemoryUsage = true as const; private readonly _extent: Extent; private readonly _origin: Vector3; private readonly _ellipsoid: Ellipsoid; private readonly _raycastGeometry: BufferGeometry; private _segments = 32; private _heightMap: HeightMap | null = null; private _skirtDepth: number | null = null; public get vertexCount(): number { return this.getAttribute('position').count; } public get segments(): number { return this._segments; } public set segments(v: number) { if (this._segments !== v) { this._segments = v; this.buildBuffers(this, Usage.Rendering); this.buildBuffers(this._raycastGeometry, Usage.Raycasting); } } public get origin(): Vector3 { return this._origin; } public get raycastGeometry(): BufferGeometry { return this._raycastGeometry; } public constructor(params: { extent: Extent; segments: number; ellipsoid: Ellipsoid; skirtDepth: number | null; }) { super(); this._segments = params.segments; this._extent = params.extent; this._skirtDepth = params.skirtDepth; this._ellipsoid = params.ellipsoid; this._origin = this._ellipsoid.toCartesian(this._extent.maxY, this._extent.minX, 0); if (!this._extent.crs.isEpsg(4326)) { throw new Error(`invalid CRS. Expected EPSG:4326, got: ${this._extent.crs.id}`); } this._raycastGeometry = new BufferGeometry(); this.buildBuffers(this, Usage.Rendering); this.buildBuffers(this._raycastGeometry, Usage.Raycasting); } public resetHeights(): void { this.buildBuffers(this.raycastGeometry, Usage.Raycasting); } public applyHeightMap(heightMap: HeightMap): { min: number; max: number } { this._heightMap = heightMap; return this.buildBuffers(this.raycastGeometry, Usage.Raycasting); } public getMemoryUsage(context: GetMemoryUsageContext): void { getGeometryMemoryUsage(context, this); getGeometryMemoryUsage(context, this.raycastGeometry); } private buildBuffers(geometry: BufferGeometry, usage: Usage): { min: number; max: number } { this.dispose(); const rowVertices = this._segments + 1; const dims = this._extent.dimensions(tmpVec2); const width = dims.width; const height = dims.height; const west = this._extent.minX; const north = this._extent.maxY; const south = this._extent.minY; const east = this._extent.maxX; // Positions are relative to the origin of the tile const origin = this._origin; // A shortcut to get ready to use buffers 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: number, v: number): number { if (!heightMap) { 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; 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 lon = west + u * width; const lat = north - v * height; const altitude = usage === Usage.Raycasting ? getElevation(u, 1 - v) : 0; min = Math.min(min, altitude); max = Math.max(max, altitude); const cartesian = this._ellipsoid.toCartesian(lat, lon, altitude, tmpVec3); const normal = this._ellipsoid.getNormalFromCartesian(cartesian, tmpNormal); const pos = cartesian.sub(origin); // Note that the bounding box ignores the skirts, which are purely // graphical and should not count towards the actual volume of the tile. boundingBox.expandByPoint(pos); positionBuffer.set(idx, pos.x, pos.y, pos.z); normalBuffer.set(idx, normal.x, normal.y, normal.z); } } if (this._skirtDepth != null) { const skirtDepth = this._skirtDepth; const skirtStart = rowVertices * rowVertices; const nw = this._ellipsoid.toCartesian(north, west, skirtDepth, tmpNW).sub(origin); const ne = this._ellipsoid.toCartesian(north, east, skirtDepth, tmpNE).sub(origin); const sw = this._ellipsoid.toCartesian(south, west, skirtDepth, tmpSW).sub(origin); const se = this._ellipsoid.toCartesian(south, east, skirtDepth, tmpSE).sub(origin); positionBuffer.set(skirtStart + 0, nw.x, nw.y, nw.z); positionBuffer.set(skirtStart + 1, ne.x, ne.y, ne.z); positionBuffer.set(skirtStart + 2, sw.x, sw.y, sw.z); positionBuffer.set(skirtStart + 3, se.x, se.y, se.z); // Skirt normals are the same as their non-skirt counterpart copySkirtValues(normalBuffer, this.segments); } // Per-tile buffers geometry.setAttribute('position', new BufferAttribute(positionBuffer.array, 3)); geometry.setAttribute('normal', new BufferAttribute(normalBuffer.array, 3)); // Shared buffers geometry.setAttribute('uv', new BufferAttribute(uvBuffer.array, 2)); geometry.setIndex(new BufferAttribute(indexBuffer, 1)); this.boundingBox = boundingBox; return { min, max }; } } export default EllipsoidTileGeometry;