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);