@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
167 lines (157 loc) • 6.06 kB
JavaScript
/*
* 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 { 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();
var Usage = /*#__PURE__*/function (Usage) {
Usage[Usage["Rendering"] = 0] = "Rendering";
Usage[Usage["Raycasting"] = 1] = "Raycasting";
return Usage;
}(Usage || {});
function copySkirtValues(array, segments) {
const rowSize = segments + 1;
const end = rowSize * rowSize;
array.copyItem(0, end + 0);
array.copyItem(rowSize, end + 1);
array.copyItem(end - rowSize, end + 2);
array.copyItem(end, end + 3);
}
export class EllipsoidTileGeometry extends BufferGeometry {
isMemoryUsage = true;
_segments = 32;
_heightMap = null;
_skirtDepth = null;
get vertexCount() {
return this.getAttribute('position').count;
}
get segments() {
return this._segments;
}
set segments(v) {
if (this._segments !== v) {
this._segments = v;
this.buildBuffers(this, Usage.Rendering);
this.buildBuffers(this._raycastGeometry, Usage.Raycasting);
}
}
get origin() {
return this._origin;
}
get raycastGeometry() {
return this._raycastGeometry;
}
constructor(params) {
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);
}
resetHeights() {
this.buildBuffers(this.raycastGeometry, Usage.Raycasting);
}
applyHeightMap(heightMap) {
this._heightMap = heightMap;
return this.buildBuffers(this.raycastGeometry, Usage.Raycasting);
}
getMemoryUsage(context) {
getGeometryMemoryUsage(context, this);
getGeometryMemoryUsage(context, this.raycastGeometry);
}
buildBuffers(geometry, usage) {
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, v) {
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 altitude = usage === Usage.Raycasting ? getElevation(u, 1 - v) : 0;
min = Math.min(min, altitude);
max = Math.max(max, altitude);
const cartesian = this._ellipsoid.toCartesian(north - v * height, west + u * width, 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;