From 684bb92d969f41a99163c3b3524f05d228d6657b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Duhen?= Date: Wed, 9 Oct 2024 13:35:42 +0200 Subject: [PATCH 1/2] refactor(TileBuilder): convert to TypeScript Improve performance by using statically-sized ArrayBuffers. Reorganize code to get rid of some of the params/builder mess. Cleanup computeBuffers function. Squashed commit history (oldest to youngest): - fix: UV_1 generation - refacto(wip): cleanup and optimize computeBuffers - refacto(wip): improve index generation - wip: correct offset, off-by-one still at large - fix: change calls to allow camera debug - fix: found the error, off by a power of 2 actually - fix(uv): correct indices passed to UV buffering - fix(index): only generate buffer when needed - wip: enable cache - fix(computeBuffers): group tile and skirt together - fix(wip): squash rogue private field access - refacto: convert TileGeometry to TypeScript - style(builders): make method visibility explicit - refactor(exports): remove default exports - feat(TileGeometry): add OBB type - refactor: rename BuilderEllipsoidTile, fix imports - fix(tileGeometry): update tests to use public api - fix(TileBuilder): remove dead code comments --- src/Converter/convertToTile.js | 6 +- src/Core/Prefab/Globe/BuilderEllipsoidTile.js | 126 ------- src/Core/Prefab/Globe/GlobeLayer.js | 5 +- src/Core/Prefab/Globe/GlobeTileBuilder.ts | 170 +++++++++ src/Core/Prefab/Planar/PlanarLayer.js | 2 +- src/Core/Prefab/Planar/PlanarTileBuilder.js | 75 ---- src/Core/Prefab/Planar/PlanarTileBuilder.ts | 112 ++++++ src/Core/Prefab/TileBuilder.js | 74 ---- src/Core/Prefab/TileBuilder.ts | 146 ++++++++ src/Core/Prefab/computeBufferTileGeometry.js | 227 ------------ src/Core/Prefab/computeBufferTileGeometry.ts | 338 ++++++++++++++++++ src/Core/TileGeometry.js | 44 --- src/Core/TileGeometry.ts | 157 ++++++++ src/Renderer/OBB.js | 17 +- test/unit/obb.js | 8 +- test/unit/tilemesh.js | 10 +- 16 files changed, 947 insertions(+), 570 deletions(-) delete mode 100644 src/Core/Prefab/Globe/BuilderEllipsoidTile.js create mode 100644 src/Core/Prefab/Globe/GlobeTileBuilder.ts delete mode 100644 src/Core/Prefab/Planar/PlanarTileBuilder.js create mode 100644 src/Core/Prefab/Planar/PlanarTileBuilder.ts delete mode 100644 src/Core/Prefab/TileBuilder.js create mode 100644 src/Core/Prefab/TileBuilder.ts delete mode 100644 src/Core/Prefab/computeBufferTileGeometry.js create mode 100644 src/Core/Prefab/computeBufferTileGeometry.ts delete mode 100644 src/Core/TileGeometry.js create mode 100644 src/Core/TileGeometry.ts diff --git a/src/Converter/convertToTile.js b/src/Converter/convertToTile.js index c22d89154c..56fd6420a6 100644 --- a/src/Converter/convertToTile.js +++ b/src/Converter/convertToTile.js @@ -6,7 +6,7 @@ import * as THREE from 'three'; import TileMesh from 'Core/TileMesh'; import LayeredMaterial from 'Renderer/LayeredMaterial'; -import newTileGeometry from 'Core/Prefab/TileBuilder'; +import { newTileGeometry } from 'Core/Prefab/TileBuilder'; import ReferLayerProperties from 'Layer/ReferencingLayerProperties'; import { geoidLayerIsVisible } from 'Layer/GeoidLayer'; @@ -52,7 +52,7 @@ export default { return newTileGeometry(builder, paramsGeometry).then((result) => { // build tile mesh - result.geometry._count++; + result.geometry.increaseRefCount(); const crsCount = layer.tileMatrixSets.length; const material = new LayeredMaterial(layer.materialOptions, crsCount); ReferLayerProperties(material, layer); @@ -61,7 +61,7 @@ export default { if (parent && parent.isTileMesh) { // get parent extent transformation - const pTrans = builder.computeSharableExtent(parent.extent); + const pTrans = builder.computeShareableExtent(parent.extent); // place relative to his parent result.position.sub(pTrans.position).applyQuaternion(pTrans.quaternion.invert()); result.quaternion.premultiply(pTrans.quaternion); diff --git a/src/Core/Prefab/Globe/BuilderEllipsoidTile.js b/src/Core/Prefab/Globe/BuilderEllipsoidTile.js deleted file mode 100644 index f872a7eeb9..0000000000 --- a/src/Core/Prefab/Globe/BuilderEllipsoidTile.js +++ /dev/null @@ -1,126 +0,0 @@ -import * as THREE from 'three'; -import Coordinates from 'Core/Geographic/Coordinates'; -import Extent from 'Core/Geographic/Extent'; - -const PI_OV_FOUR = Math.PI / 4; -const INV_TWO_PI = 1.0 / (Math.PI * 2); -const axisZ = new THREE.Vector3(0, 0, 1); -const axisY = new THREE.Vector3(0, 1, 0); -const quatToAlignLongitude = new THREE.Quaternion(); -const quatToAlignLatitude = new THREE.Quaternion(); -const quatNormalToZ = new THREE.Quaternion(); - -function WGS84ToOneSubY(latitude) { - return 1.0 - (0.5 - Math.log(Math.tan(PI_OV_FOUR + THREE.MathUtils.degToRad(latitude) * 0.5)) * INV_TWO_PI); -} - -class BuilderEllipsoidTile { - constructor(options = {}) { - this.tmp = { - coords: [ - new Coordinates('EPSG:4326', 0, 0), - new Coordinates('EPSG:4326', 0, 0)], - position: new THREE.Vector3(), - dimension: new THREE.Vector2(), - }; - - this.crs = options.crs; - // Order crs projection on tiles - this.uvCount = options.uvCount; - - this.computeUvs = [ - // Normalized coordinates (from degree) on the entire tile - // EPSG:4326 - () => {}, - // Float row coordinate from Pseudo mercator coordinates - // EPSG:3857 - (params) => { - const t = WGS84ToOneSubY(params.projected.latitude) * params.nbRow; - return (!isFinite(t) ? 0 : t) - params.deltaUV1; - }, - ]; - } - // prepare params - // init projected object -> params.projected - prepare(params) { - params.nbRow = 2 ** (params.level + 1.0); - - let st1 = WGS84ToOneSubY(params.extent.south); - - if (!isFinite(st1)) { st1 = 0; } - - const sizeTexture = 1.0 / params.nbRow; - - const start = (st1 % (sizeTexture)); - - params.deltaUV1 = (st1 - start) * params.nbRow; - - // transformation to align tile's normal to z axis - params.quatNormalToZ = quatNormalToZ.setFromAxisAngle( - axisY, - -(Math.PI * 0.5 - THREE.MathUtils.degToRad(params.extent.center().latitude))); - - // let's avoid building too much temp objects - params.projected = { longitude: 0, latitude: 0 }; - params.extent.planarDimensions(this.tmp.dimension); - } - - // get center tile in cartesian 3D - center(extent) { - return extent.center(this.tmp.coords[0]) - .as(this.crs, this.tmp.coords[1]).toVector3(); - } - - // get position 3D cartesian - vertexPosition(params) { - this.tmp.coords[0].setFromValues( - params.projected.longitude, - params.projected.latitude); - - this.tmp.coords[0].as(this.crs, this.tmp.coords[1]).toVector3(this.tmp.position); - return this.tmp.position; - } - - // get normal for last vertex - vertexNormal() { - return this.tmp.coords[1].geodesicNormal; - } - - // coord u tile to projected - uProjecte(u, params) { - params.projected.longitude = params.extent.west + u * this.tmp.dimension.x; - } - - // coord v tile to projected - vProjecte(v, params) { - params.projected.latitude = params.extent.south + v * this.tmp.dimension.y; - } - - computeSharableExtent(extent) { - // Compute sharable extent to pool the geometries - // the geometry in common extent is identical to the existing input - // with a transformation (translation, rotation) - - // TODO: It should be possible to use equatorial plan symetrie, - // but we should be reverse UV on tile - // Common geometry is looking for only on longitude - const sizeLongitude = Math.abs(extent.west - extent.east) / 2; - const sharableExtent = new Extent(extent.crs, -sizeLongitude, sizeLongitude, extent.south, extent.north); - - // compute rotation to transform tile to position it on ellipsoid - // this transformation take into account the transformation of the parents - const rotLon = THREE.MathUtils.degToRad(extent.west - sharableExtent.west); - const rotLat = THREE.MathUtils.degToRad(90 - extent.center(this.tmp.coords[0]).latitude); - quatToAlignLongitude.setFromAxisAngle(axisZ, rotLon); - quatToAlignLatitude.setFromAxisAngle(axisY, rotLat); - quatToAlignLongitude.multiply(quatToAlignLatitude); - - return { - sharableExtent, - quaternion: quatToAlignLongitude.clone(), - position: this.center(extent), - }; - } -} - -export default BuilderEllipsoidTile; diff --git a/src/Core/Prefab/Globe/GlobeLayer.js b/src/Core/Prefab/Globe/GlobeLayer.js index a72a29db17..45b03f3cdf 100644 --- a/src/Core/Prefab/Globe/GlobeLayer.js +++ b/src/Core/Prefab/Globe/GlobeLayer.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import TiledGeometryLayer from 'Layer/TiledGeometryLayer'; import { ellipsoidSizes } from 'Core/Math/Ellipsoid'; import { globalExtentTMS, schemeTiles } from 'Core/Tile/TileGrid'; -import BuilderEllipsoidTile from 'Core/Prefab/Globe/BuilderEllipsoidTile'; +import { GlobeTileBuilder } from 'Core/Prefab/Globe/GlobeTileBuilder'; // matrix to convert sphere to ellipsoid const worldToScaledEllipsoid = new THREE.Matrix4(); @@ -61,8 +61,9 @@ class GlobeLayer extends TiledGeometryLayer { 'EPSG:4326', 'EPSG:3857', ]; + const uvCount = tileMatrixSets.length; - const builder = new BuilderEllipsoidTile({ crs: 'EPSG:4978', uvCount }); + const builder = new GlobeTileBuilder({ crs: 'EPSG:4978', uvCount }); super(id, object3d || new THREE.Group(), schemeTile, builder, { tileMatrixSets, diff --git a/src/Core/Prefab/Globe/GlobeTileBuilder.ts b/src/Core/Prefab/Globe/GlobeTileBuilder.ts new file mode 100644 index 0000000000..b03c0626c9 --- /dev/null +++ b/src/Core/Prefab/Globe/GlobeTileBuilder.ts @@ -0,0 +1,170 @@ +import * as THREE from 'three'; +import Coordinates from 'Core/Geographic/Coordinates'; +import Extent from 'Core/Geographic/Extent'; +import { + Projected, + ShareableExtent, + TileBuilder, + TileBuilderParams, +} from '../TileBuilder'; + +const PI_OV_FOUR = Math.PI / 4; +const INV_TWO_PI = 1.0 / (Math.PI * 2); +const axisZ = new THREE.Vector3(0, 0, 1); +const axisY = new THREE.Vector3(0, 1, 0); +const quatToAlignLongitude = new THREE.Quaternion(); +const quatToAlignLatitude = new THREE.Quaternion(); +const quatNormalToZ = new THREE.Quaternion(); + +function WGS84ToOneSubY(latitude: number) { + return 1.0 - (0.5 - Math.log(Math.tan( + PI_OV_FOUR + THREE.MathUtils.degToRad(latitude) * 0.5, + )) * INV_TWO_PI); +} + +export interface GlobeTileBuilderParams extends TileBuilderParams { + nbRow: number; + deltaUV1: number; + quatNormalToZ: THREE.Quaternion; +} + +export class GlobeTileBuilder +implements TileBuilder { + private _crs: string; + private _transform: { + coords: Coordinates[]; + position: THREE.Vector3; + dimension: THREE.Vector2; + }; + + public computeExtraOffset?: (params: GlobeTileBuilderParams) => number; + + public get crs(): string { + return this._crs; + } + + public constructor(options: { + crs: string, + uvCount: number, + }) { + this._transform = { + coords: [ + new Coordinates('EPSG:4326', 0, 0), + new Coordinates('EPSG:4326', 0, 0), + ], + position: new THREE.Vector3(), + dimension: new THREE.Vector2(), + }; + + this._crs = options.crs; + // Order crs projection on tiles + + // UV: Normalized coordinates (from degree) on the entire tile + // EPSG:4326 + // Offset: Float row coordinate from Pseudo mercator coordinates + // EPSG:3857 + if (options.uvCount > 1) { + this.computeExtraOffset = GlobeTileBuilder._computeExtraOffset; + } + } + + private static _computeExtraOffset(params: GlobeTileBuilderParams): number { + const t = WGS84ToOneSubY(params.projected.latitude) * params.nbRow; + return (!isFinite(t) ? 0 : t) - params.deltaUV1; + } + + // prepare params + // init projected object -> params.projected + public prepare(params: TileBuilderParams): GlobeTileBuilderParams { + const nbRow = 2 ** (params.level + 1.0); + let st1 = WGS84ToOneSubY(params.extent.south); + + if (!isFinite(st1)) { st1 = 0; } + + const sizeTexture = 1.0 / nbRow; + + const start = (st1 % (sizeTexture)); + + const newParams = { + nbRow, + deltaUV1: (st1 - start) * nbRow, + // transformation to align tile's normal to z axis + quatNormalToZ: quatNormalToZ.setFromAxisAngle( + axisY, + -(Math.PI * 0.5 - THREE.MathUtils.degToRad( + params.extent.center().latitude, + ))), + // let's avoid building too much temp objects + projected: new Projected(), + }; + + params.extent.planarDimensions(this._transform.dimension); + + return { ...params, ...newParams }; + } + + // get center tile in cartesian 3D + public center(extent: Extent) { + return extent.center(this._transform.coords[0]) + .as(this.crs, this._transform.coords[1]) + .toVector3(); + } + + // get position 3D cartesian + public vertexPosition(position: THREE.Vector2): THREE.Vector3 { + return this._transform.coords[0] + .setFromValues(position.x, position.y) + .as(this.crs, this._transform.coords[1]) + .toVector3(this._transform.position); + } + + // get normal for last vertex + public vertexNormal() { + return this._transform.coords[1].geodesicNormal; + } + + // coord u tile to projected + public uProject(u: number, extent: Extent): number { + return extent.west + u * this._transform.dimension.x; + } + + // coord v tile to projected + public vProject(v: number, extent: Extent): number { + return extent.south + v * this._transform.dimension.y; + } + + public computeShareableExtent(extent: Extent): ShareableExtent { + // Compute shareable extent to pool the geometries + // the geometry in common extent is identical to the existing input + // with a transformation (translation, rotation) + + // TODO: It should be possible to use equatorial plan symetrie, + // but we should be reverse UV on tile + // Common geometry is looking for only on longitude + const sizeLongitude = Math.abs(extent.west - extent.east) / 2; + const shareableExtent = new Extent( + extent.crs, + -sizeLongitude, sizeLongitude, + extent.south, extent.north, + ); + + // compute rotation to transform tile to position on ellipsoid + // this transformation takes into account the transformation of the + // parents + const rotLon = THREE.MathUtils.degToRad( + extent.west - shareableExtent.west, + ); + const rotLat = THREE.MathUtils.degToRad( + 90 - extent.center(this._transform.coords[0]).latitude, + ); + quatToAlignLongitude.setFromAxisAngle(axisZ, rotLon); + quatToAlignLatitude.setFromAxisAngle(axisY, rotLat); + quatToAlignLongitude.multiply(quatToAlignLatitude); + + return { + shareableExtent, + quaternion: quatToAlignLongitude.clone(), + position: this.center(extent), + }; + } +} diff --git a/src/Core/Prefab/Planar/PlanarLayer.js b/src/Core/Prefab/Planar/PlanarLayer.js index dbc154074f..97d620d4bb 100644 --- a/src/Core/Prefab/Planar/PlanarLayer.js +++ b/src/Core/Prefab/Planar/PlanarLayer.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import TiledGeometryLayer from 'Layer/TiledGeometryLayer'; import { globalExtentTMS } from 'Core/Tile/TileGrid'; -import PlanarTileBuilder from './PlanarTileBuilder'; +import { PlanarTileBuilder } from './PlanarTileBuilder'; /** * @property {boolean} isPlanarLayer - Used to checkout whether this layer is a diff --git a/src/Core/Prefab/Planar/PlanarTileBuilder.js b/src/Core/Prefab/Planar/PlanarTileBuilder.js deleted file mode 100644 index 68a981a219..0000000000 --- a/src/Core/Prefab/Planar/PlanarTileBuilder.js +++ /dev/null @@ -1,75 +0,0 @@ -import * as THREE from 'three'; -import Coordinates from 'Core/Geographic/Coordinates'; -import Extent from 'Core/Geographic/Extent'; - -const quaternion = new THREE.Quaternion(); -const center = new THREE.Vector3(); - -class PlanarTileBuilder { - constructor(options = {}) { - /* istanbul ignore next */ - if (options.projection) { - console.warn('PlanarTileBuilder projection parameter is deprecated, use crs instead.'); - options.crs = options.crs || options.projection; - } - if (options.crs) { - this.crs = options.crs; - } else { - throw new Error('options.crs is mandatory for PlanarTileBuilder'); - } - this.tmp = { - coords: new Coordinates('EPSG:4326', 0, 0), - position: new THREE.Vector3(), - normal: new THREE.Vector3(0, 0, 1), - }; - this.uvCount = options.uvCount || 1; - } - // prepare params - // init projected object -> params.projected - prepare(params) { - params.nbRow = 2 ** (params.zoom + 1.0); - params.projected = new THREE.Vector3(); - } - - // get center tile in cartesian 3D - center(extent) { - extent.center(this.tmp.coords); - center.set(this.tmp.coords.x, this.tmp.coords.y, 0); - return center; - } - - // get position 3D cartesian - vertexPosition(params) { - this.tmp.position.set(params.projected.x, params.projected.y, 0); - return this.tmp.position; - } - - // get normal for last vertex - vertexNormal() { - return this.tmp.normal; - } - - // coord u tile to projected - uProjecte(u, params) { - params.projected.x = params.extent.west + u * (params.extent.east - params.extent.west); - } - - // coord v tile to projected - vProjecte(v, params) { - params.projected.y = params.extent.south + v * (params.extent.north - params.extent.south); - } - - computeSharableExtent(extent) { - // compute sharable extent to pool the geometries - // the geometry in common extent is identical to the existing input - // with a translation - const sharableExtent = new Extent(extent.crs, 0, Math.abs(extent.west - extent.east), 0, Math.abs(extent.north - extent.south)); - return { - sharableExtent, - quaternion, - position: this.center(extent).clone(), - }; - } -} - -export default PlanarTileBuilder; diff --git a/src/Core/Prefab/Planar/PlanarTileBuilder.ts b/src/Core/Prefab/Planar/PlanarTileBuilder.ts new file mode 100644 index 0000000000..81466ba55f --- /dev/null +++ b/src/Core/Prefab/Planar/PlanarTileBuilder.ts @@ -0,0 +1,112 @@ +import * as THREE from 'three'; +import Coordinates from 'Core/Geographic/Coordinates'; +import Extent from 'Core/Geographic/Extent'; +import { + Projected, + ShareableExtent, + TileBuilder, + TileBuilderParams, +} from '../TileBuilder'; + +const quaternion = new THREE.Quaternion(); +const center = new THREE.Vector3(); + +type Transform = { + coords: Coordinates, + position: THREE.Vector3, + normal: THREE.Vector3, +}; + +export interface PlanarTileBuilderParams extends TileBuilderParams { + crs: string; + uvCount?: number; + nbRow: number; +} + +export class PlanarTileBuilder implements TileBuilder { + private _uvCount: number; + private _transform: Transform; + private _crs: string; + + public constructor(options: { + projection?: string, + crs: string, + uvCount?: number, + }) { + if (options.projection) { + console.warn('PlanarTileBuilder projection parameter is deprecated,' + + ' use crs instead.'); + options.crs ??= options.projection; + } + + this._crs = options.crs; + + this._transform = { + coords: new Coordinates('EPSG:4326', 0, 0), + position: new THREE.Vector3(), + normal: new THREE.Vector3(0, 0, 1), + }; + + this._uvCount = options.uvCount ?? 1; + } + + public get uvCount(): number { + return this._uvCount; + } + + public get crs(): string { + return this._crs; + } + + // prepare params + // init projected object -> params.projected + public prepare(params: TileBuilderParams): PlanarTileBuilderParams { + const newParams = params as PlanarTileBuilderParams; + newParams.nbRow = 2 ** (params.zoom + 1.0); + newParams.projected = new Projected(); + return newParams; + } + + public center(extent: Extent): THREE.Vector3 { + extent.center(this._transform.coords); + center.set(this._transform.coords.x, this._transform.coords.y, 0); + return center; + } + + // set position 3D cartesian + public vertexPosition(position: THREE.Vector2): THREE.Vector3 { + this._transform.position.set(position.x, position.y, 0); + return this._transform.position; + } + + // get normal for last vertex + public vertexNormal(): THREE.Vector3 { + return this._transform.normal; + } + + // coord u tile to projected + public uProject(u: number, extent: Extent): number { + return extent.west + u * (extent.east - extent.west); + } + + // coord v tile to projected + public vProject(v: number, extent: Extent): number { + return extent.south + v * (extent.north - extent.south); + } + + public computeShareableExtent(extent: Extent): ShareableExtent { + // compute shareable extent to pool the geometries + // the geometry in common extent is identical to the existing input + // with a translation + return { + shareableExtent: new Extent(extent.crs, { + west: 0, + east: Math.abs(extent.west - extent.east), + south: 0, + north: Math.abs(extent.north - extent.south), + }), + quaternion, + position: this.center(extent).clone(), + }; + } +} diff --git a/src/Core/Prefab/TileBuilder.js b/src/Core/Prefab/TileBuilder.js deleted file mode 100644 index 3b898a024c..0000000000 --- a/src/Core/Prefab/TileBuilder.js +++ /dev/null @@ -1,74 +0,0 @@ -import * as THREE from 'three'; -import TileGeometry from 'Core/TileGeometry'; -import Cache from 'Core/Scheduler/Cache'; -import computeBuffers from 'Core/Prefab/computeBufferTileGeometry'; -import OBB from 'Renderer/OBB'; - -const cacheBuffer = new Map(); -const cacheTile = new Cache(); - -export default function newTileGeometry(builder, params) { - const { sharableExtent, quaternion, position } = builder.computeSharableExtent(params.extent); - const south = sharableExtent.south.toFixed(6); - const bufferKey = `${builder.crs}_${params.disableSkirt ? 0 : 1}_${params.segments}`; - let promiseGeometry = cacheTile.get(south, params.level, bufferKey); - - // build geometry if doesn't exist - if (!promiseGeometry) { - let resolve; - promiseGeometry = new Promise((r) => { resolve = r; }); - cacheTile.set(promiseGeometry, south, params.level, bufferKey); - - params.extent = sharableExtent; - params.center = builder.center(params.extent).clone(); - // Read previously cached values (index and uv.wgs84 only depend on the # of triangles) - let cachedBuffers = cacheBuffer.get(bufferKey); - params.buildIndexAndUv_0 = !cachedBuffers; - params.builder = builder; - let buffers; - try { - buffers = computeBuffers(params); - } catch (e) { - return Promise.reject(e); - } - - if (!cachedBuffers) { - cachedBuffers = {}; - cachedBuffers.index = new THREE.BufferAttribute(buffers.index, 1); - cachedBuffers.uv = new THREE.BufferAttribute(buffers.uvs[0], 2); - - // Update cacheBuffer - cacheBuffer.set(bufferKey, cachedBuffers); - } - - buffers.index = cachedBuffers.index; - buffers.uvs[0] = cachedBuffers.uv; - buffers.position = new THREE.BufferAttribute(buffers.position, 3); - buffers.normal = new THREE.BufferAttribute(buffers.normal, 3); - if (params.builder.uvCount > 1) { - buffers.uvs[1] = new THREE.BufferAttribute(buffers.uvs[1], 1); - } - - const geometry = new TileGeometry(params, buffers); - geometry.OBB = new OBB(geometry.boundingBox.min, geometry.boundingBox.max); - - geometry._count = 0; - geometry.dispose = () => { - geometry._count--; - if (geometry._count <= 0) { - // To avoid remove index buffer and attribute buffer uv - // error un-bound buffer in webgl with VAO rendering. - // Could be removed if the attribute buffer deleting is - // taken into account in the buffer binding state (in THREE.WebGLBindingStates code). - geometry.index = null; - delete geometry.attributes.uv; - THREE.BufferGeometry.prototype.dispose.call(geometry); - cacheTile.delete(south, params.level, bufferKey); - } - }; - resolve(geometry); - return Promise.resolve({ geometry, quaternion, position }); - } - - return promiseGeometry.then(geometry => ({ geometry, quaternion, position })); -} diff --git a/src/Core/Prefab/TileBuilder.ts b/src/Core/Prefab/TileBuilder.ts new file mode 100644 index 0000000000..b7fa510b17 --- /dev/null +++ b/src/Core/Prefab/TileBuilder.ts @@ -0,0 +1,146 @@ +import * as THREE from 'three'; +import { TileGeometry } from 'Core/TileGeometry'; +import Cache from 'Core/Scheduler/Cache'; +import { computeBuffers } from 'Core/Prefab/computeBufferTileGeometry'; +import OBB from 'Renderer/OBB'; +import type Extent from 'Core/Geographic/Extent'; + +const cacheBuffer = new Map(); +const cacheTile = new Cache(); + +export type GpuBufferAttributes = { + index: THREE.BufferAttribute | null; + position: THREE.BufferAttribute; + normal: THREE.BufferAttribute; + uvs: THREE.BufferAttribute[]; +}; + +export type ShareableExtent = { + shareableExtent: Extent; + quaternion: THREE.Quaternion; + position: THREE.Vector3; +}; + +// TODO: Check if this order is right +// Ideally we split this into Vec2 and a simpler LatLon type +// Somewhat equivalent to a light Coordinates class +export class Projected extends THREE.Vector2 { + public get longitude(): number { + return this.x; + } + + public set longitude(longitude: number) { + this.x = longitude; + } + + public get latitude(): number { + return this.y; + } + + public set latitude(latitude: number) { + this.y = latitude; + } +} + +export interface TileBuilderParams { + /** Whether to build the skirt. */ + disableSkirt: boolean; + /** Whether to render the skirt. */ + hideSkirt: boolean; + buildIndexAndUv_0: boolean; + /** Number of segments (edge loops) inside tiles. */ + segments: number; + /** Buffer for projected points. */ + projected: Projected; + extent: Extent; + level: number; + zoom: number; + center: THREE.Vector3; +} + +export interface TileBuilder { + crs: string; + + /** Convert builder-agnostic params to specialized ones. */ + prepare(params: TileBuilderParams): SpecializedParams; + computeExtraOffset?: (params: SpecializedParams) => number; + /** Get the center of the tile in 3D cartesian coordinates. */ + center(extent: Extent): THREE.Vector3; + vertexPosition(position: THREE.Vector2): THREE.Vector3; + vertexNormal(): THREE.Vector3; + uProject(u: number, extent: Extent): number; + vProject(v: number, extent: Extent): number; + computeShareableExtent(extent: Extent): ShareableExtent; +} + +export function newTileGeometry( + builder: TileBuilder, + params: TileBuilderParams, +) { + const { shareableExtent, quaternion, position } = + builder.computeShareableExtent(params.extent); + + const south = shareableExtent.south.toFixed(6); + + const bufferKey = + `${builder.crs}_${params.disableSkirt ? 0 : 1}_${params.segments}`; + + let promiseGeometry = cacheTile.get(south, params.level, bufferKey); + // let promiseGeometry; + + // build geometry if doesn't exist + if (!promiseGeometry) { + let resolve; + promiseGeometry = new Promise((r) => { resolve = r; }); + cacheTile.set(promiseGeometry, south, params.level, bufferKey); + + params.extent = shareableExtent; + params.center = builder.center(params.extent).clone(); + // Read previously cached values (index and uv.wgs84 only + // depend on the # of triangles) + let cachedBuffers = cacheBuffer.get(bufferKey); + params.buildIndexAndUv_0 = !cachedBuffers; + let buffers; + try { + buffers = computeBuffers(builder, params); + } catch (e) { + return Promise.reject(e); + } + + if (!cachedBuffers) { + cachedBuffers = {}; + // We know the fields will exist due to the condition + // matching with the one for buildIndexAndUv_0. + // TODO: Make this brain-based check compiler-based. + cachedBuffers.index = new THREE.BufferAttribute(buffers.index!, 1); + cachedBuffers.uv = new THREE.BufferAttribute(buffers.uvs[0]!, 2); + + // Update cacheBuffer + cacheBuffer.set(bufferKey, cachedBuffers); + } + + const gpuBuffers: GpuBufferAttributes = { + index: cachedBuffers.index, + uvs: [ + cachedBuffers.uv, + ...(buffers.uvs[1] !== undefined + ? [new THREE.BufferAttribute(buffers.uvs[1], 1)] + : [] + ), + ], + position: new THREE.BufferAttribute(buffers.position, 3), + normal: new THREE.BufferAttribute(buffers.normal, 3), + }; + + const geometry = new TileGeometry(builder, params, gpuBuffers); + geometry.OBB = + new OBB(geometry.boundingBox!.min, geometry.boundingBox!.max); + geometry.initRefCount(cacheTile, [south, params.level, bufferKey]); + resolve!(geometry); + + return Promise.resolve({ geometry, quaternion, position }); + } + + return (promiseGeometry as Promise) + .then(geometry => ({ geometry, quaternion, position })); +} diff --git a/src/Core/Prefab/computeBufferTileGeometry.js b/src/Core/Prefab/computeBufferTileGeometry.js deleted file mode 100644 index e53ca351d4..0000000000 --- a/src/Core/Prefab/computeBufferTileGeometry.js +++ /dev/null @@ -1,227 +0,0 @@ -import * as THREE from 'three'; - -export function getBufferIndexSize(segments, noSkirt) { - const triangles = (segments) * (segments) * 2 + (noSkirt ? 0 : 4 * segments * 2); - return triangles * 3; -} - -export default function computeBuffers(params) { - // Create output buffers. - const outBuffers = { - index: null, - position: null, - normal: null, - // 2 UV set per tile: wgs84 (uv[0]) and pm (uv[1]) - // - wgs84: 1 texture per tile because tiles are using wgs84 projection - // - pm: use multiple textures per tile. - // +-------------------------+ - // | | - // | Texture 0 | - // +-------------------------+ - // | | - // | Texture 1 | - // +-------------------------+ - // | | - // | Texture 2 | - // +-------------------------+ - // * u = wgs84.u - // * v = textureid + v in builder texture - uvs: [], - }; - const computeUvs = []; - - const builder = params.builder; - const nSeg = params.segments; - // segments count : - // Tile : (nSeg + 1) * (nSeg + 1) - // Skirt : 8 * (nSeg - 1) - const nVertex = (nSeg + 1) * (nSeg + 1) + (params.disableSkirt ? 0 : 4 * nSeg); - if (nVertex > 2 ** 32) { - throw new Error('Tile segments count is too big'); - } - - outBuffers.position = new Float32Array(nVertex * 3); - outBuffers.normal = new Float32Array(nVertex * 3); - - const uvCount = params.builder.uvCount; - if (uvCount > 1) { - outBuffers.uvs[1] = new Float32Array(nVertex); - } - - computeUvs[0] = () => {}; - const bufferIndexSize = getBufferIndexSize(nSeg, params.disableSkirt); - if (params.buildIndexAndUv_0) { - if (nVertex < 2 ** 8) { - outBuffers.index = new Uint8Array(bufferIndexSize); - } else if (nVertex < 2 ** 16) { - outBuffers.index = new Uint16Array(bufferIndexSize); - } else if (nVertex < 2 ** 32) { - outBuffers.index = new Uint32Array(bufferIndexSize); - } - outBuffers.uvs[0] = new Float32Array(nVertex * 2); - computeUvs[0] = (id, u, v) => { - outBuffers.uvs[0][id * 2 + 0] = u; - outBuffers.uvs[0][id * 2 + 1] = v; - }; - } - const widthSegments = Math.max(2, Math.floor(nSeg) || 2); - const heightSegments = Math.max(2, Math.floor(nSeg) || 2); - - let idVertex = 0; - const vertices = []; - let skirt = []; - const skirtEnd = []; - - builder.prepare(params); - - for (let y = 0; y <= heightSegments; y++) { - const verticesRow = []; - const v = y / heightSegments; - - builder.vProjecte(v, params); - if (uvCount > 1) { - const u = builder.computeUvs[1](params); - computeUvs[1] = (id) => { - outBuffers.uvs[1][id] = u; - }; - } - - for (let x = 0; x <= widthSegments; x++) { - const u = x / widthSegments; - const id_m3 = idVertex * 3; - - builder.uProjecte(u, params); - - const vertex = builder.vertexPosition(params, params.projected); - const normal = builder.vertexNormal(params); - - // move geometry to center world - vertex.sub(params.center); - - // align normal to z axis - if (params.quatNormalToZ) { - vertex.applyQuaternion(params.quatNormalToZ); - normal.applyQuaternion(params.quatNormalToZ); - } - - vertex.toArray(outBuffers.position, id_m3); - normal.toArray(outBuffers.normal, id_m3); - - for (const computeUv of computeUvs) { - computeUv(idVertex, u, v); - } - - if (!params.disableSkirt) { - if (y !== 0 && y !== heightSegments) { - if (x === widthSegments) { - skirt.push(idVertex); - } else if (x === 0) { - skirtEnd.push(idVertex); - } - } - } - - verticesRow.push(idVertex); - - idVertex++; - } - - vertices.push(verticesRow); - - if (y === 0) { - skirt = skirt.concat(verticesRow); - } else if (y === heightSegments) { - skirt = skirt.concat(verticesRow.slice().reverse()); - } - } - - if (!params.disableSkirt) { - skirt = skirt.concat(skirtEnd.reverse()); - } - - function bufferize(va, vb, vc, idVertex) { - outBuffers.index[idVertex + 0] = va; - outBuffers.index[idVertex + 1] = vb; - outBuffers.index[idVertex + 2] = vc; - return idVertex + 3; - } - - let idVertex2 = 0; - - if (params.buildIndexAndUv_0) { - for (let y = 0; y < heightSegments; y++) { - for (let x = 0; x < widthSegments; x++) { - const v1 = vertices[y][x + 1]; - const v2 = vertices[y][x]; - const v3 = vertices[y + 1][x]; - const v4 = vertices[y + 1][x + 1]; - - idVertex2 = bufferize(v4, v2, v1, idVertex2); - idVertex2 = bufferize(v4, v3, v2, idVertex2); - } - } - } - - const iStart = idVertex; - - // TODO: WARNING beware skirt's size influences performance - // The size of the skirt is now a ratio of the size of the tile. - // To be perfect it should depend on the real elevation delta but too heavy to compute - if (!params.disableSkirt) { - // We compute the actual size of tile segment to use later for the skirt. - const segmentSize = new THREE.Vector3().fromArray(outBuffers.position).distanceTo( - new THREE.Vector3().fromArray(outBuffers.position, 3)); - - let buildIndexSkirt = function buildIndexSkirt() { }; - let buildUVSkirt = function buildUVSkirt() { }; - - if (params.buildIndexAndUv_0) { - buildIndexSkirt = function buildIndexSkirt(id, v1, v2, v3, v4) { - id = bufferize(v1, v2, v3, id); - id = bufferize(v1, v3, v4, id); - return id; - }; - - buildUVSkirt = function buildUVSkirt(id) { - outBuffers.uvs[0][idVertex * 2 + 0] = outBuffers.uvs[0][id * 2 + 0]; - outBuffers.uvs[0][idVertex * 2 + 1] = outBuffers.uvs[0][id * 2 + 1]; - }; - } - - for (let i = 0; i < skirt.length; i++) { - const id = skirt[i]; - const id_m3 = idVertex * 3; - const id2_m3 = id * 3; - - outBuffers.position[id_m3 + 0] = outBuffers.position[id2_m3 + 0] - - outBuffers.normal[id2_m3 + 0] * segmentSize; - outBuffers.position[id_m3 + 1] = outBuffers.position[id2_m3 + 1] - - outBuffers.normal[id2_m3 + 1] * segmentSize; - outBuffers.position[id_m3 + 2] = outBuffers.position[id2_m3 + 2] - - outBuffers.normal[id2_m3 + 2] * segmentSize; - - outBuffers.normal[id_m3 + 0] = outBuffers.normal[id2_m3 + 0]; - outBuffers.normal[id_m3 + 1] = outBuffers.normal[id2_m3 + 1]; - outBuffers.normal[id_m3 + 2] = outBuffers.normal[id2_m3 + 2]; - - buildUVSkirt(id); - - if (uvCount > 1) { - outBuffers.uvs[1][idVertex] = outBuffers.uvs[1][id]; - } - - const idf = (i + 1) % skirt.length; - - const v1 = id; - const v2 = idVertex; - const v3 = (idf === 0) ? iStart : idVertex + 1; - const v4 = skirt[idf]; - - idVertex2 = buildIndexSkirt(idVertex2, v1, v2, v3, v4); - - idVertex++; - } - } - - return outBuffers; -} diff --git a/src/Core/Prefab/computeBufferTileGeometry.ts b/src/Core/Prefab/computeBufferTileGeometry.ts new file mode 100644 index 0000000000..54bae0df92 --- /dev/null +++ b/src/Core/Prefab/computeBufferTileGeometry.ts @@ -0,0 +1,338 @@ +import type { TileBuilder, TileBuilderParams } from 'Core/Prefab/TileBuilder'; +import * as THREE from 'three'; + +export function getBufferIndexSize(segments: number, noSkirt: boolean): number { + const triangles = (segments) * (segments) * 2 + + (noSkirt ? 0 : 4 * segments * 2); + return triangles * 3; +} + +type Option = T | undefined; + +type IndexArray = Option; + +export type Buffers = { + index: IndexArray, + position: Float32Array, + normal: Float32Array, + uvs: [Option, Option], +}; + +type TmpBuffers = Buffers & { + skirt: IndexArray, +}; + +function pickUintArraySize( + highestValue: number, +): Uint8ArrayConstructor | Uint16ArrayConstructor | Uint32ArrayConstructor { + let picked = null; + + if (highestValue < 2 ** 8) { + picked = Uint8Array; + } else if (highestValue < 2 ** 16) { + picked = Uint16Array; + } else if (highestValue < 2 ** 32) { + picked = Uint32Array; + } else { + throw new Error('Value is too high'); + } + + return picked; +} + +function allocateIndexBuffer( + nVertex: number, + nSeg: number, + params: TileBuilderParams, +): Option<{ index: IndexArray, skirt: IndexArray }> { + if (!params.buildIndexAndUv_0) { + return undefined; + } + + const indexBufferSize = getBufferIndexSize(nSeg, params.disableSkirt); + const indexConstructor = pickUintArraySize(nVertex); + + const tileLen = indexBufferSize; + const skirtLen = 4 * nSeg; + const indexBuffer = new ArrayBuffer(( + // Tile + tileLen + // Skirt + + (params.disableSkirt ? 0 : skirtLen) + ) * indexConstructor!.BYTES_PER_ELEMENT); + + const index = new indexConstructor(indexBuffer); + const skirt = !params.disableSkirt + ? index.subarray(tileLen, tileLen + skirtLen) + : undefined; + + return { + index, + skirt, + }; +} + +function allocateBuffers( + nVertex: number, + nSeg: number, + builder: TileBuilder, + params: TileBuilderParams, +): TmpBuffers { + const { + index, + skirt, + } = allocateIndexBuffer(nVertex, nSeg, params) ?? {}; + + return { + index, + skirt, + position: new Float32Array(nVertex * 3), + normal: new Float32Array(nVertex * 3), + // 2 UV set per tile: wgs84 (uv[0]) and pseudo-mercator (pm, uv[1]) + // - wgs84: 1 texture per tile because tiles are using wgs84 + // projection + // - pm: use multiple textures per tile. + // +-------------------------+ + // | | + // | Texture 0 | + // +-------------------------+ + // | | + // | Texture 1 | + // +-------------------------+ + // | | + // | Texture 2 | + // +-------------------------+ + // * u = wgs84.u + // * v = textureid + v in builder texture + uvs: [ + params.buildIndexAndUv_0 + ? new Float32Array(nVertex * 2) + : undefined, + builder.computeExtraOffset !== undefined + ? new Float32Array(nVertex) + : undefined, + ], + }; +} + +function computeUv0(uv: Float32Array, id: number, u: number, v: number): void { + uv[id * 2 + 0] = u; + uv[id * 2 + 1] = v; +} + +function initComputeUv1(value: number): (uv: Float32Array, id: number) => void { + return (uv: Float32Array, id: number): void => { uv[id] = value; }; +} + +type ComputeUvs = + [typeof computeUv0 | (() => void), ReturnType?]; + +// TODO: Split this even further into subfunctions +export function computeBuffers( + builder: TileBuilder, + params: TileBuilderParams, +) { + // n seg, n+1 vert + <- skirt, n verts per side + // <---------------> / | + // +---+---+---+---+ | + // | / | / | / | / | | Vertices: + // +---+---+---+---+ - + tile = (n + 1)^2 + // | / | / | / | / | | skirt = 4n + // +---+---+---+---+ - + + // | / | / | / | / | | Segments: + // +---+---+---+---+ - + tile = 2 * n * (n + 1) + n^2 + // | / | / | / | / | | skirt = 2n * 4 + // +---+---+---+---+ | + const nSeg: number = Math.max(2, params.segments); + const nVertex: number = nSeg + 1; + const nTileVertex: number = nVertex ** 2; + const nSkirtVertex: number = params.disableSkirt ? 0 : 4 * nSeg; + const nTotalVertex: number = nTileVertex + nSkirtVertex; + + // Computer should combust before this happens + if (nTotalVertex > 2 ** 32) { + throw new Error('Tile segments count is too big'); + } + + const outBuffers: TmpBuffers = allocateBuffers( + nTotalVertex, nSeg, + builder, params, + ); + + const computeUvs: ComputeUvs = + [params.buildIndexAndUv_0 ? computeUv0 : () => { }]; + + params = builder.prepare(params); + + for (let y = 0; y <= nSeg; y++) { + const v = y / nSeg; + + params.projected.y = builder.vProject(v, params.extent); + + if (builder.computeExtraOffset !== undefined) { + computeUvs[1] = initComputeUv1( + builder.computeExtraOffset(params) as number, + ); + } + + for (let x = 0; x <= nSeg; x++) { + const u = x / nSeg; + const id_m3 = (y * nVertex + x) * 3; + + params.projected.x = builder.uProject(u, params.extent); + + const vertex = builder.vertexPosition(params.projected); + const normal = builder.vertexNormal(); + + // move geometry to center world + vertex.sub(params.center); + + // align normal to z axis + // HACK: this check style is not great + if ('quatNormalToZ' in params) { + const quat = + params.quatNormalToZ as THREE.Quaternion; + vertex.applyQuaternion(quat); + normal.applyQuaternion(quat); + } + + vertex.toArray(outBuffers.position, id_m3); + normal.toArray(outBuffers.normal, id_m3); + + for (const [index, computeUv] of computeUvs.entries()) { + if (computeUv !== undefined) { + computeUv(outBuffers.uvs[index]!, y * nVertex + x, u, v); + } + } + } + } + + // Fill skirt index buffer + if (params.buildIndexAndUv_0 && !params.disableSkirt) { + for (let x = 0; x < nVertex; x++) { + // --------> + // 0---1---2 + // | / | / | [0-9] = assign order + // +---+---+ + // | / | / | + // +---+---+ + outBuffers.skirt![x] = x; + // +---+---+ + // | / | / | [0-9] = assign order + // +---+---x x = skipped for now + // | / | / | + // 0---1---2 + // <-------- + outBuffers.skirt![2 * nVertex - 2 + x] = nVertex ** 2 - (x + 1); + } + + for (let y = 1; y < nVertex - 1; y++) { + // +---+---s | + // | / | / | | o = stored vertices + // +---+---o | s = already stored + // | / | / | | + // +---+---s v + outBuffers.skirt![nVertex - 1 + y] = y * nVertex + (nVertex - 1); + // ^ s---+---+ + // | | / | / | o = stored vertices + // | o---+---+ s = already stored + // | | / | / | + // | s---+---+ + outBuffers.skirt![3 * nVertex - 3 + y] = + nVertex * (nVertex - 1 - y); + } + } + + function bufferizeTri(id: number, va: number, vb: number, vc: number) { + outBuffers.index![id + 0] = va; + outBuffers.index![id + 1] = vb; + outBuffers.index![id + 2] = vc; + } + + if (params.buildIndexAndUv_0) { + for (let y = 0; y < nSeg; y++) { + for (let x = 0; x < nSeg; x++) { + const v1 = y * nVertex + (x + 1); + const v2 = y * nVertex + x; + const v3 = (y + 1) * nVertex + x; + const v4 = (y + 1) * nVertex + (x + 1); + + const id = (y * nSeg + x) * 6; + bufferizeTri(id, /**/v4, v2, v1); + bufferizeTri(id + 3, v4, v3, v2); + } + } + } + + // PERF: Beware skirt's size influences performance + // INFO: The size of the skirt is now a ratio of the size of the tile. + // To be perfect it should depend on the real elevation delta but too heavy + // to compute + if (params.buildIndexAndUv_0 && !params.disableSkirt) { + // We compute the actual size of tile segment to use later for + // the skirt. + const segmentSize = new THREE.Vector3() + .fromArray(outBuffers.position) + .distanceTo(new THREE.Vector3() + .fromArray(outBuffers.position, 3)); + + const buildSkirt = { + index: ( + id: number, + v1: number, v2: number, v3: number, v4: number, + ) => { + bufferizeTri(id, v1, v2, v3); + bufferizeTri(id + 3, v1, v3, v4); + return id + 6; + }, + uv: (buf: Option, idTo: number, idFrom: number) => { + buf![idTo * 2 + 0] = buf![idFrom * 2 + 0]; + buf![idTo * 2 + 1] = buf![idFrom * 2 + 1]; + }, + }; + + // Alias for readability + const start = nTileVertex; + const indexBufStart = 6 * nSeg ** 2; + + for (let i = 0; i < outBuffers.skirt!.length; i++) { + const id = outBuffers.skirt![i]; + const id_m3 = (start + i) * 3; + const id2_m3 = id * 3; + + outBuffers.position[id_m3 + 0] = outBuffers.position[id2_m3 + 0] + - outBuffers.normal[id2_m3 + 0] * segmentSize; + outBuffers.position[id_m3 + 1] = outBuffers.position[id2_m3 + 1] + - outBuffers.normal[id2_m3 + 1] * segmentSize; + outBuffers.position[id_m3 + 2] = outBuffers.position[id2_m3 + 2] + - outBuffers.normal[id2_m3 + 2] * segmentSize; + + outBuffers.normal[id_m3 + 0] = outBuffers.normal[id2_m3 + 0]; + outBuffers.normal[id_m3 + 1] = outBuffers.normal[id2_m3 + 1]; + outBuffers.normal[id_m3 + 2] = outBuffers.normal[id2_m3 + 2]; + + buildSkirt.uv(outBuffers.uvs[0], start + i, id); + + if (outBuffers.uvs[1] !== undefined) { + outBuffers.uvs[1][start + i] = outBuffers.uvs[1][id]; + } + + const idf = (i + 1) % outBuffers.skirt!.length; + + const v1 = id; + const v2 = start + i; + const v3 = (idf === 0) ? start : start + i + 1; + const v4 = outBuffers.skirt![idf]; + + buildSkirt.index(indexBufStart + i * 6, v1, v2, v3, v4); + } + } + + // Dropping skirt view + return { + index: outBuffers.index, + position: outBuffers.position, + uvs: outBuffers.uvs, + normal: outBuffers.normal, + }; +} diff --git a/src/Core/TileGeometry.js b/src/Core/TileGeometry.js deleted file mode 100644 index 8c5b7beecb..0000000000 --- a/src/Core/TileGeometry.js +++ /dev/null @@ -1,44 +0,0 @@ -import * as THREE from 'three'; -import computeBuffers, { getBufferIndexSize } from 'Core/Prefab/computeBufferTileGeometry'; - -function defaultBuffers(params) { - params.buildIndexAndUv_0 = true; - params.center = params.builder.center(params.extent).clone(); - const buffers = computeBuffers(params); - buffers.index = new THREE.BufferAttribute(buffers.index, 1); - buffers.uvs[0] = new THREE.BufferAttribute(buffers.uvs[0], 2); - buffers.position = new THREE.BufferAttribute(buffers.position, 3); - buffers.normal = new THREE.BufferAttribute(buffers.normal, 3); - for (let i = 1; i < params.builder.uvCount; i++) { - buffers.uvs[1] = new THREE.BufferAttribute(buffers.uvs[1], 1); - } - return buffers; -} - -class TileGeometry extends THREE.BufferGeometry { - constructor(params, buffers = defaultBuffers(params)) { - super(); - this.center = params.center; - this.extent = params.extent; - this.segments = params.segments; - this.setIndex(buffers.index); - this.setAttribute('position', buffers.position); - this.setAttribute('normal', buffers.normal); - this.setAttribute('uv', buffers.uvs[0]); - - for (let i = 1; i < buffers.uvs.length; i++) { - this.setAttribute(`uv_${i}`, buffers.uvs[i]); - } - - this.computeBoundingBox(); - this.OBB = {}; - if (params.hideSkirt) { - this.hideSkirt = params.hideSkirt; - } - } - set hideSkirt(value) { - this.setDrawRange(0, getBufferIndexSize(this.segments, value)); - } -} - -export default TileGeometry; diff --git a/src/Core/TileGeometry.ts b/src/Core/TileGeometry.ts new file mode 100644 index 0000000000..696d82d135 --- /dev/null +++ b/src/Core/TileGeometry.ts @@ -0,0 +1,157 @@ +import * as THREE from 'three'; + +import { computeBuffers, getBufferIndexSize } + from 'Core/Prefab/computeBufferTileGeometry'; +import { GpuBufferAttributes, Projected, TileBuilder, TileBuilderParams } + from 'Core/Prefab/TileBuilder'; +import Extent from 'Core/Geographic/Extent'; +import Cache from 'Core/Scheduler/Cache'; + +import OBB from 'Renderer/OBB'; + +type PartialTileBuilderParams = + Pick + & Partial; + +function defaultBuffers( + builder: TileBuilder, + params: PartialTileBuilderParams, +): GpuBufferAttributes { + const fullParams = { + disableSkirt: false, + hideSkirt: false, + buildIndexAndUv_0: true, + segments: 16, + projected: new Projected(0, 0), + center: builder.center(params.extent!).clone(), + ...params, + }; + + const buffers = computeBuffers(builder, fullParams); + const bufferAttributes = { + index: buffers.index + ? new THREE.BufferAttribute(buffers.index, 1) + : null, + uvs: [ + ...(buffers.uvs[0] + ? [new THREE.BufferAttribute(buffers.uvs[0], 2)] + : [] + ), + ...(buffers.uvs[1] + ? [new THREE.BufferAttribute(buffers.uvs[1], 1)] + : []), + ], + position: new THREE.BufferAttribute(buffers.position, 3), + normal: new THREE.BufferAttribute(buffers.normal, 3), + }; + + return bufferAttributes; +} + +export class TileGeometry extends THREE.BufferGeometry { + public OBB: OBB | null; + public extent: Extent; + public segments: number; + public tileCenter: THREE.Vector3; + + private _refCount: { + count: number, + fn: () => void, + } | null; + + public constructor( + builder: TileBuilder, + params: TileBuilderParams, + bufferAttributes: GpuBufferAttributes = defaultBuffers(builder, params), + ) { + super(); + this.tileCenter = params.center; + this.extent = params.extent; + this.segments = params.segments; + this.setIndex(bufferAttributes.index); + this.setAttribute('position', bufferAttributes.position); + this.setAttribute('normal', bufferAttributes.normal); + this.setAttribute('uv', bufferAttributes.uvs[0]); + + for (let i = 1; i < bufferAttributes.uvs.length; i++) { + this.setAttribute(`uv_${i}`, bufferAttributes.uvs[i]); + } + + this.computeBoundingBox(); + this.OBB = null; + if (params.hideSkirt) { + this.hideSkirt = params.hideSkirt; + } + + this._refCount = null; + } + + /** + * Enables or disables skirt rendering. + * + * @param toggle - Whether to hide the skirt; true hides, false shows. + */ + public set hideSkirt(toggle: boolean) { + this.setDrawRange(0, getBufferIndexSize(this.segments, toggle)); + } + + /** + * Initialize reference count for this geometry. + * Idempotent operation. + * + * @param cacheTile - The [Cache] used to store this geometry. + * @param keys - The [south, level, epsg] key of this geometry. + */ + public initRefCount( + cacheTile: Cache, + keys: [string, number, string], + ): void { + if (this._refCount !== null) { + return; + } + + this._refCount = { + count: 0, + fn: () => { + this._refCount!.count--; + if (this._refCount!.count <= 0) { + // To avoid remove index buffer and attribute buffer uv + // error un-bound buffer in webgl with VAO rendering. + // Could be removed if the attribute buffer deleting is + // taken into account in the buffer binding state + // (in THREE.WebGLBindingStates code). + this.index = null; + delete this.attributes.uv; + cacheTile.delete(...keys); + super.dispose(); + // THREE.BufferGeometry.prototype.dispose.call(this); + } + }, + }; + } + + /** + * Increase reference count. + * + * @throws If reference count has not been initialized. + */ + public increaseRefCount(): void { + if (this._refCount === null) { + throw new Error('[TileGeometry::increaseRefCount] ' + + 'Tried to increment an unitialized reference count.'); + } + this._refCount.count++; + } + + public get refCount(): number | undefined { + return this._refCount?.count; + } + + public override dispose(): void { + if (this._refCount == null) { + super.dispose(); + } else { + this._refCount.fn(); + } + } +} diff --git a/src/Renderer/OBB.js b/src/Renderer/OBB.js index 567cce69f6..ea5738e3fe 100644 --- a/src/Renderer/OBB.js +++ b/src/Renderer/OBB.js @@ -1,11 +1,11 @@ import * as THREE from 'three'; import * as CRS from 'Core/Geographic/Crs'; -import TileGeometry from 'Core/TileGeometry'; -import BuilderEllipsoidTile from 'Core/Prefab/Globe/BuilderEllipsoidTile'; +import { TileGeometry } from 'Core/TileGeometry'; +import { GlobeTileBuilder } from 'Core/Prefab/Globe/GlobeTileBuilder'; import Coordinates from 'Core/Geographic/Coordinates'; // get oriented bounding box of tile -const builder = new BuilderEllipsoidTile({ crs: 'EPSG:4978', uvCount: 1 }); +const builder = new GlobeTileBuilder({ crs: 'EPSG:4978', uvCount: 1 }); const size = new THREE.Vector3(); const dimension = new THREE.Vector2(); const center = new THREE.Vector3(); @@ -90,7 +90,7 @@ class OBB extends THREE.Object3D { // this is the same as isPointInsideSphere.position const distance = Math.sqrt((x - localSpherePosition.x) * (x - localSpherePosition.x) + - (y - localSpherePosition.y) * (y - localSpherePosition.y)); + (y - localSpherePosition.y) * (y - localSpherePosition.y)); return distance < sphere.radius; } @@ -106,18 +106,17 @@ class OBB extends THREE.Object3D { */ setFromExtent(extent, minHeight = extent.min || 0, maxHeight = extent.max || 0) { if (extent.crs == 'EPSG:4326') { - const { sharableExtent, quaternion, position } = builder.computeSharableExtent(extent); + const { shareableExtent, quaternion, position } = builder.computeShareableExtent(extent); // Compute the minimum count of segment to build tile - const segments = Math.max(Math.floor(sharableExtent.planarDimensions(dimension).x / 90 + 1), 2); + const segments = Math.max(Math.floor(shareableExtent.planarDimensions(dimension).x / 90 + 1), 2); const paramsGeometry = { - extent: sharableExtent, + extent: shareableExtent, level: 0, segments, disableSkirt: true, - builder, }; - const geometry = new TileGeometry(paramsGeometry); + const geometry = new TileGeometry(builder, paramsGeometry); obb.box3D.copy(geometry.boundingBox); obb.natBox.copy(geometry.boundingBox); this.copy(obb); diff --git a/test/unit/obb.js b/test/unit/obb.js index b2e4dd8f01..05de9565cd 100644 --- a/test/unit/obb.js +++ b/test/unit/obb.js @@ -2,9 +2,9 @@ import * as THREE from 'three'; import proj4 from 'proj4'; import assert from 'assert'; import Extent from 'Core/Geographic/Extent'; -import PlanarTileBuilder from 'Core/Prefab/Planar/PlanarTileBuilder'; -import BuilderEllipsoidTile from 'Core/Prefab/Globe/BuilderEllipsoidTile'; -import newTileGeometry from 'Core/Prefab/TileBuilder'; +import { PlanarTileBuilder } from 'Core/Prefab/Planar/PlanarTileBuilder'; +import { GlobeTileBuilder } from 'Core/Prefab/Globe/GlobeTileBuilder'; +import { newTileGeometry } from 'Core/Prefab/TileBuilder'; import OBB from 'Renderer/OBB'; describe('OBB', function () { @@ -83,7 +83,7 @@ describe('Planar tiles OBB computation', function () { }); }); describe('Ellipsoid tiles OBB computation', function () { - const builder = new BuilderEllipsoidTile({ crs: 'EPSG:4978', uvCount: 1 }); + const builder = new GlobeTileBuilder({ crs: 'EPSG:4978', uvCount: 1 }); it('should compute globe-level 0 OBB correctly', function (done) { const extent = new Extent('EPSG:4326', -180, 0, -90, 90); diff --git a/test/unit/tilemesh.js b/test/unit/tilemesh.js index bcdca3e261..aea00caef4 100644 --- a/test/unit/tilemesh.js +++ b/test/unit/tilemesh.js @@ -6,7 +6,7 @@ import PlanarLayer from 'Core/Prefab/Planar/PlanarLayer'; import Tile from 'Core/Tile/Tile'; import { globalExtentTMS } from 'Core/Tile/TileGrid'; import TileProvider from 'Provider/TileProvider'; -import newTileGeometry from 'Core/Prefab/TileBuilder'; +import { newTileGeometry } from 'Core/Prefab/TileBuilder'; import OBB from 'Renderer/OBB'; import ElevationLayer from 'Layer/ElevationLayer'; import Source from 'Source/Source'; @@ -136,9 +136,9 @@ describe('TileMesh', function () { }; newTileGeometry(planarlayer.builder, paramsGeometry).then((r) => { - r.geometry._count++; + r.geometry.increaseRefCount(); return newTileGeometry(planarlayer.builder, paramsGeometry).then((r) => { - assert.equal(r.geometry._count, 1); + assert.equal(r.geometry.refCount, 1); done(); }); }); @@ -175,8 +175,8 @@ describe('TileMesh', function () { elevationLayer.parent = planarlayer; const material = new THREE.Material(); - material.addLayer = () => {}; - material.setSequenceElevation = () => {}; + material.addLayer = () => { }; + material.setSequenceElevation = () => { }; it('event rasterElevationLevelChanged RasterElevationTile sets TileMesh bounding box ', () => { const tileMesh = new TileMesh(geom, material, planarlayer, tile.toExtent('EPSG:3857'), 0); From 4fc027e81c225bc2b23d919f518a1bc5105c81f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Duhen?= Date: Wed, 27 Nov 2024 17:40:03 +0100 Subject: [PATCH 2/2] fix: apply some of the suggested changes feat(tile): add doc comments, rm unused tileCenter --- src/Core/Prefab/Globe/GlobeLayer.js | 2 +- src/Core/Prefab/Globe/GlobeTileBuilder.ts | 78 ++++++++++---------- src/Core/Prefab/Planar/PlanarTileBuilder.ts | 20 +++-- src/Core/Prefab/TileBuilder.ts | 52 ++++++------- src/Core/Prefab/computeBufferTileGeometry.ts | 20 ++--- src/Core/TileGeometry.ts | 27 +++++-- src/Renderer/OBB.js | 2 +- test/unit/obb.js | 2 +- 8 files changed, 109 insertions(+), 94 deletions(-) diff --git a/src/Core/Prefab/Globe/GlobeLayer.js b/src/Core/Prefab/Globe/GlobeLayer.js index 45b03f3cdf..3aa9f5a03f 100644 --- a/src/Core/Prefab/Globe/GlobeLayer.js +++ b/src/Core/Prefab/Globe/GlobeLayer.js @@ -63,7 +63,7 @@ class GlobeLayer extends TiledGeometryLayer { ]; const uvCount = tileMatrixSets.length; - const builder = new GlobeTileBuilder({ crs: 'EPSG:4978', uvCount }); + const builder = new GlobeTileBuilder({ uvCount }); super(id, object3d || new THREE.Group(), schemeTile, builder, { tileMatrixSets, diff --git a/src/Core/Prefab/Globe/GlobeTileBuilder.ts b/src/Core/Prefab/Globe/GlobeTileBuilder.ts index b03c0626c9..39bc384ae2 100644 --- a/src/Core/Prefab/Globe/GlobeTileBuilder.ts +++ b/src/Core/Prefab/Globe/GlobeTileBuilder.ts @@ -2,7 +2,6 @@ import * as THREE from 'three'; import Coordinates from 'Core/Geographic/Coordinates'; import Extent from 'Core/Geographic/Extent'; import { - Projected, ShareableExtent, TileBuilder, TileBuilderParams, @@ -16,35 +15,56 @@ const quatToAlignLongitude = new THREE.Quaternion(); const quatToAlignLatitude = new THREE.Quaternion(); const quatNormalToZ = new THREE.Quaternion(); -function WGS84ToOneSubY(latitude: number) { +/** Transforms a WGS84 latitude into a usable texture offset. */ +function WGS84ToOneSubY(latitude: number): number { return 1.0 - (0.5 - Math.log(Math.tan( PI_OV_FOUR + THREE.MathUtils.degToRad(latitude) * 0.5, )) * INV_TWO_PI); } +type Transform = { + /** Buffers for 2-part coordinate mapping operations. */ + coords: [Coordinates, Coordinates]; + position: THREE.Vector3; + dimension: THREE.Vector2; +}; + +/** Specialized parameters for the [GlobeTileBuilder]. */ export interface GlobeTileBuilderParams extends TileBuilderParams { + /** Number of rows of tiles, essentially the resolution of the globe. */ nbRow: number; + /** Offset of the second texture set. */ deltaUV1: number; + /** Transformation to align a tile's normal to the Z axis. */ quatNormalToZ: THREE.Quaternion; } +/** + * TileBuilder implementation for the purpose of generating globe (or more + * precisely ellipsoidal) tile arrangements. + */ export class GlobeTileBuilder implements TileBuilder { - private _crs: string; - private _transform: { - coords: Coordinates[]; - position: THREE.Vector3; - dimension: THREE.Vector2; - }; + private static _crs: string = 'EPSG:4978'; + private static _computeExtraOffset(params: GlobeTileBuilderParams): number { + const t = WGS84ToOneSubY(params.coordinates.latitude) * params.nbRow; + return (!isFinite(t) ? 0 : t) - params.deltaUV1; + } + + /** + * Buffer holding information about the tile/vertex currently being + * processed. + */ + private _transform: Transform; public computeExtraOffset?: (params: GlobeTileBuilderParams) => number; public get crs(): string { - return this._crs; + return GlobeTileBuilder._crs; } public constructor(options: { - crs: string, + /** Number of unaligned texture sets. */ uvCount: number, }) { this._transform = { @@ -56,9 +76,6 @@ implements TileBuilder { dimension: new THREE.Vector2(), }; - this._crs = options.crs; - // Order crs projection on tiles - // UV: Normalized coordinates (from degree) on the entire tile // EPSG:4326 // Offset: Float row coordinate from Pseudo mercator coordinates @@ -68,13 +85,6 @@ implements TileBuilder { } } - private static _computeExtraOffset(params: GlobeTileBuilderParams): number { - const t = WGS84ToOneSubY(params.projected.latitude) * params.nbRow; - return (!isFinite(t) ? 0 : t) - params.deltaUV1; - } - - // prepare params - // init projected object -> params.projected public prepare(params: TileBuilderParams): GlobeTileBuilderParams { const nbRow = 2 ** (params.level + 1.0); let st1 = WGS84ToOneSubY(params.extent.south); @@ -95,7 +105,7 @@ implements TileBuilder { params.extent.center().latitude, ))), // let's avoid building too much temp objects - projected: new Projected(), + coordinates: new Coordinates(this.crs), }; params.extent.planarDimensions(this._transform.dimension); @@ -103,44 +113,37 @@ implements TileBuilder { return { ...params, ...newParams }; } - // get center tile in cartesian 3D public center(extent: Extent) { return extent.center(this._transform.coords[0]) .as(this.crs, this._transform.coords[1]) .toVector3(); } - // get position 3D cartesian - public vertexPosition(position: THREE.Vector2): THREE.Vector3 { + public vertexPosition(coordinates: Coordinates): THREE.Vector3 { return this._transform.coords[0] - .setFromValues(position.x, position.y) + .setFromValues(coordinates.x, coordinates.y) .as(this.crs, this._transform.coords[1]) .toVector3(this._transform.position); } - // get normal for last vertex public vertexNormal() { return this._transform.coords[1].geodesicNormal; } - // coord u tile to projected public uProject(u: number, extent: Extent): number { return extent.west + u * this._transform.dimension.x; } - // coord v tile to projected public vProject(v: number, extent: Extent): number { return extent.south + v * this._transform.dimension.y; } public computeShareableExtent(extent: Extent): ShareableExtent { - // Compute shareable extent to pool the geometries - // the geometry in common extent is identical to the existing input - // with a transformation (translation, rotation) - - // TODO: It should be possible to use equatorial plan symetrie, - // but we should be reverse UV on tile - // Common geometry is looking for only on longitude + // NOTE: It should be possible to take advantage of equatorial plane + // symmetry, for which we'd have to reverse the tile's UVs. + // This would halve the memory requirement when viewing a full globe, + // but that case is not that relevant for iTowns' usual use cases and + // the globe mesh memory usage is already inconsequential. const sizeLongitude = Math.abs(extent.west - extent.east) / 2; const shareableExtent = new Extent( extent.crs, @@ -148,9 +151,8 @@ implements TileBuilder { extent.south, extent.north, ); - // compute rotation to transform tile to position on ellipsoid - // this transformation takes into account the transformation of the - // parents + // Compute rotation to transform the tile to position on the ellispoid. + // This transformation takes the parents' transformation into account. const rotLon = THREE.MathUtils.degToRad( extent.west - shareableExtent.west, ); diff --git a/src/Core/Prefab/Planar/PlanarTileBuilder.ts b/src/Core/Prefab/Planar/PlanarTileBuilder.ts index 81466ba55f..30a18e7650 100644 --- a/src/Core/Prefab/Planar/PlanarTileBuilder.ts +++ b/src/Core/Prefab/Planar/PlanarTileBuilder.ts @@ -2,7 +2,6 @@ import * as THREE from 'three'; import Coordinates from 'Core/Geographic/Coordinates'; import Extent from 'Core/Geographic/Extent'; import { - Projected, ShareableExtent, TileBuilder, TileBuilderParams, @@ -17,12 +16,17 @@ type Transform = { normal: THREE.Vector3, }; +/** Specialized parameters for the [PlanarTileBuilder]. */ export interface PlanarTileBuilderParams extends TileBuilderParams { crs: string; uvCount?: number; nbRow: number; } +/** + * TileBuilder implementation for the purpose of generating planar + * tile arrangements. + */ export class PlanarTileBuilder implements TileBuilder { private _uvCount: number; private _transform: Transform; @@ -58,12 +62,10 @@ export class PlanarTileBuilder implements TileBuilder { return this._crs; } - // prepare params - // init projected object -> params.projected public prepare(params: TileBuilderParams): PlanarTileBuilderParams { const newParams = params as PlanarTileBuilderParams; - newParams.nbRow = 2 ** (params.zoom + 1.0); - newParams.projected = new Projected(); + newParams.nbRow = 2 ** (params.level + 1.0); + newParams.coordinates = new Coordinates(this.crs); return newParams; } @@ -73,23 +75,19 @@ export class PlanarTileBuilder implements TileBuilder { return center; } - // set position 3D cartesian - public vertexPosition(position: THREE.Vector2): THREE.Vector3 { - this._transform.position.set(position.x, position.y, 0); + public vertexPosition(coordinates: Coordinates): THREE.Vector3 { + this._transform.position.set(coordinates.x, coordinates.y, 0); return this._transform.position; } - // get normal for last vertex public vertexNormal(): THREE.Vector3 { return this._transform.normal; } - // coord u tile to projected public uProject(u: number, extent: Extent): number { return extent.west + u * (extent.east - extent.west); } - // coord v tile to projected public vProject(v: number, extent: Extent): number { return extent.south + v * (extent.north - extent.south); } diff --git a/src/Core/Prefab/TileBuilder.ts b/src/Core/Prefab/TileBuilder.ts index b7fa510b17..35e1a2d97f 100644 --- a/src/Core/Prefab/TileBuilder.ts +++ b/src/Core/Prefab/TileBuilder.ts @@ -4,6 +4,7 @@ import Cache from 'Core/Scheduler/Cache'; import { computeBuffers } from 'Core/Prefab/computeBufferTileGeometry'; import OBB from 'Renderer/OBB'; import type Extent from 'Core/Geographic/Extent'; +import Coordinates from 'Core/Geographic/Coordinates'; const cacheBuffer = new Map(); const cacheTile = new Cache(); @@ -15,46 +16,33 @@ export type GpuBufferAttributes = { uvs: THREE.BufferAttribute[]; }; +/** + * Reference to a tile's extent with rigid transformations. + * Enables reuse of geometry, saving a bit of memory. + */ export type ShareableExtent = { shareableExtent: Extent; quaternion: THREE.Quaternion; position: THREE.Vector3; }; -// TODO: Check if this order is right -// Ideally we split this into Vec2 and a simpler LatLon type -// Somewhat equivalent to a light Coordinates class -export class Projected extends THREE.Vector2 { - public get longitude(): number { - return this.x; - } - - public set longitude(longitude: number) { - this.x = longitude; - } - - public get latitude(): number { - return this.y; - } - - public set latitude(latitude: number) { - this.y = latitude; - } -} - export interface TileBuilderParams { /** Whether to build the skirt. */ disableSkirt: boolean; /** Whether to render the skirt. */ hideSkirt: boolean; + /** + * Cache-related. + * Tells the function whether to build or skip the index and uv buffers. + */ buildIndexAndUv_0: boolean; /** Number of segments (edge loops) inside tiles. */ segments: number; + // TODO: Move this out of the interface /** Buffer for projected points. */ - projected: Projected; + coordinates: Coordinates; extent: Extent; level: number; - zoom: number; center: THREE.Vector3; } @@ -63,13 +51,26 @@ export interface TileBuilder { /** Convert builder-agnostic params to specialized ones. */ prepare(params: TileBuilderParams): SpecializedParams; + /** + * Computes final offset of the second texture set. + * Only relevant in the case of more than one texture sets. + */ computeExtraOffset?: (params: SpecializedParams) => number; - /** Get the center of the tile in 3D cartesian coordinates. */ + /** Get the center of the current tile as a 3D vector. */ center(extent: Extent): THREE.Vector3; - vertexPosition(position: THREE.Vector2): THREE.Vector3; + /** Converts an x/y tile-space position to its equivalent in 3D space. */ + vertexPosition(coordinates: Coordinates): THREE.Vector3; + /** Gets the geodesic normal of the last processed vertex. */ vertexNormal(): THREE.Vector3; + /** Project horizontal texture coordinate to world space. */ uProject(u: number, extent: Extent): number; + /** Project vertical texture coordinate to world space. */ vProject(v: number, extent: Extent): number; + /** + * Compute shareable extent to pool geometries together. + * The geometry of tiles on the same latitude is the same with an added + * rigid transform. + */ computeShareableExtent(extent: Extent): ShareableExtent; } @@ -86,7 +87,6 @@ export function newTileGeometry( `${builder.crs}_${params.disableSkirt ? 0 : 1}_${params.segments}`; let promiseGeometry = cacheTile.get(south, params.level, bufferKey); - // let promiseGeometry; // build geometry if doesn't exist if (!promiseGeometry) { diff --git a/src/Core/Prefab/computeBufferTileGeometry.ts b/src/Core/Prefab/computeBufferTileGeometry.ts index 54bae0df92..9176aaa064 100644 --- a/src/Core/Prefab/computeBufferTileGeometry.ts +++ b/src/Core/Prefab/computeBufferTileGeometry.ts @@ -18,11 +18,11 @@ export type Buffers = { uvs: [Option, Option], }; -type TmpBuffers = Buffers & { +type BuffersAndSkirt = Buffers & { skirt: IndexArray, }; -function pickUintArraySize( +function getUintArrayConstructor( highestValue: number, ): Uint8ArrayConstructor | Uint16ArrayConstructor | Uint32ArrayConstructor { let picked = null; @@ -50,7 +50,7 @@ function allocateIndexBuffer( } const indexBufferSize = getBufferIndexSize(nSeg, params.disableSkirt); - const indexConstructor = pickUintArraySize(nVertex); + const indexConstructor = getUintArrayConstructor(nVertex); const tileLen = indexBufferSize; const skirtLen = 4 * nSeg; @@ -77,7 +77,7 @@ function allocateBuffers( nSeg: number, builder: TileBuilder, params: TileBuilderParams, -): TmpBuffers { +): BuffersAndSkirt { const { index, skirt, @@ -127,11 +127,12 @@ function initComputeUv1(value: number): (uv: Float32Array, id: number) => void { type ComputeUvs = [typeof computeUv0 | (() => void), ReturnType?]; +/** Compute buffers describing a tile according to a builder and its params. */ // TODO: Split this even further into subfunctions export function computeBuffers( builder: TileBuilder, params: TileBuilderParams, -) { +): Buffers { // n seg, n+1 vert + <- skirt, n verts per side // <---------------> / | // +---+---+---+---+ | @@ -154,7 +155,7 @@ export function computeBuffers( throw new Error('Tile segments count is too big'); } - const outBuffers: TmpBuffers = allocateBuffers( + const outBuffers: BuffersAndSkirt = allocateBuffers( nTotalVertex, nSeg, builder, params, ); @@ -167,7 +168,7 @@ export function computeBuffers( for (let y = 0; y <= nSeg; y++) { const v = y / nSeg; - params.projected.y = builder.vProject(v, params.extent); + params.coordinates.y = builder.vProject(v, params.extent); if (builder.computeExtraOffset !== undefined) { computeUvs[1] = initComputeUv1( @@ -179,9 +180,9 @@ export function computeBuffers( const u = x / nSeg; const id_m3 = (y * nVertex + x) * 3; - params.projected.x = builder.uProject(u, params.extent); + params.coordinates.x = builder.uProject(u, params.extent); - const vertex = builder.vertexPosition(params.projected); + const vertex = builder.vertexPosition(params.coordinates); const normal = builder.vertexNormal(); // move geometry to center world @@ -243,6 +244,7 @@ export function computeBuffers( } } + /** Copy passed indices at the desired index of the output index buffer. */ function bufferizeTri(id: number, va: number, vb: number, vc: number) { outBuffers.index![id + 0] = va; outBuffers.index![id + 1] = vb; diff --git a/src/Core/TileGeometry.ts b/src/Core/TileGeometry.ts index 696d82d135..e63f5a1ac7 100644 --- a/src/Core/TileGeometry.ts +++ b/src/Core/TileGeometry.ts @@ -2,15 +2,16 @@ import * as THREE from 'three'; import { computeBuffers, getBufferIndexSize } from 'Core/Prefab/computeBufferTileGeometry'; -import { GpuBufferAttributes, Projected, TileBuilder, TileBuilderParams } +import { GpuBufferAttributes, TileBuilder, TileBuilderParams } from 'Core/Prefab/TileBuilder'; import Extent from 'Core/Geographic/Extent'; import Cache from 'Core/Scheduler/Cache'; import OBB from 'Renderer/OBB'; +import Coordinates from './Geographic/Coordinates'; type PartialTileBuilderParams = - Pick + Pick & Partial; function defaultBuffers( @@ -22,7 +23,7 @@ function defaultBuffers( hideSkirt: false, buildIndexAndUv_0: true, segments: 16, - projected: new Projected(0, 0), + coordinates: new Coordinates(builder.crs), center: builder.center(params.extent!).clone(), ...params, }; @@ -49,11 +50,21 @@ function defaultBuffers( } export class TileGeometry extends THREE.BufferGeometry { + /** Oriented Bounding Box of the tile geometry. */ public OBB: OBB | null; + /** Ground area covered by this tile geometry. */ public extent: Extent; + /** Resolution of the tile geometry in segments per side. */ public segments: number; - public tileCenter: THREE.Vector3; + /** + * [TileGeometry] instances are shared between tiles. Since a geometry + * handles its own GPU resource, it needs a reference counter to dispose of + * that resource only when it is discarded by every single owner of a + * reference to the geometry. + */ + // https://github.com/iTowns/itowns/pull/2440#discussion_r1860743294 + // TODO: Remove nullability by reworking OBB:setFromExtent private _refCount: { count: number, fn: () => void, @@ -65,7 +76,6 @@ export class TileGeometry extends THREE.BufferGeometry { bufferAttributes: GpuBufferAttributes = defaultBuffers(builder, params), ) { super(); - this.tileCenter = params.center; this.extent = params.extent; this.segments = params.segments; this.setIndex(bufferAttributes.index); @@ -96,8 +106,7 @@ export class TileGeometry extends THREE.BufferGeometry { } /** - * Initialize reference count for this geometry. - * Idempotent operation. + * Initialize reference count for this geometry if it is currently null. * * @param cacheTile - The [Cache] used to store this geometry. * @param keys - The [south, level, epsg] key of this geometry. @@ -143,6 +152,10 @@ export class TileGeometry extends THREE.BufferGeometry { this._refCount.count++; } + /** + * The current reference count of this [TileGeometry] if it has been + * initialized. + */ public get refCount(): number | undefined { return this._refCount?.count; } diff --git a/src/Renderer/OBB.js b/src/Renderer/OBB.js index ea5738e3fe..5b763b6b00 100644 --- a/src/Renderer/OBB.js +++ b/src/Renderer/OBB.js @@ -5,7 +5,7 @@ import { GlobeTileBuilder } from 'Core/Prefab/Globe/GlobeTileBuilder'; import Coordinates from 'Core/Geographic/Coordinates'; // get oriented bounding box of tile -const builder = new GlobeTileBuilder({ crs: 'EPSG:4978', uvCount: 1 }); +const builder = new GlobeTileBuilder({ uvCount: 1 }); const size = new THREE.Vector3(); const dimension = new THREE.Vector2(); const center = new THREE.Vector3(); diff --git a/test/unit/obb.js b/test/unit/obb.js index 05de9565cd..706e0929c9 100644 --- a/test/unit/obb.js +++ b/test/unit/obb.js @@ -83,7 +83,7 @@ describe('Planar tiles OBB computation', function () { }); }); describe('Ellipsoid tiles OBB computation', function () { - const builder = new GlobeTileBuilder({ crs: 'EPSG:4978', uvCount: 1 }); + const builder = new GlobeTileBuilder({ uvCount: 1 }); it('should compute globe-level 0 OBB correctly', function (done) { const extent = new Extent('EPSG:4326', -180, 0, -90, 90);