UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

167 lines (139 loc) 6.84 kB
import type {Context} from '../../gl/context'; import type {CanonicalTileID} from '../../source/tile_id'; import {type Mesh} from '../../render/mesh'; import {browser} from '../../util/browser'; import {easeCubicInOut, lerp} from '../../util/util'; import {mercatorYfromLat} from '../mercator_coordinate'; import {SubdivisionGranularityExpression, SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings'; import type {Projection, ProjectionGPUContext, TileMeshUsage} from './projection'; import {type PreparedShader, shaders} from '../../shaders/shaders'; import {ProjectionErrorMeasurement} from './globe_projection_error_measurement'; import {createTileMeshWithBuffers, type CreateTileMeshOptions} from '../../util/create_tile_mesh'; import {type EvaluationParameters} from '../../style/evaluation_parameters'; export const VerticalPerspectiveShaderDefine = '#define GLOBE'; export const VerticalPerspectiveShaderVariantKey = 'globe'; export const globeConstants = { errorTransitionTimeSeconds: 0.5 }; const granularitySettingsGlobe: SubdivisionGranularitySetting = new SubdivisionGranularitySetting({ fill: new SubdivisionGranularityExpression(128, 2), line: new SubdivisionGranularityExpression(512, 0), // Always keep at least some subdivision on raster tiles, etc, // otherwise they will be visibly warped at high zooms (before mercator transition). // This si not needed on fill, because fill geometry tends to already be // highly tessellated and granular at high zooms. tile: new SubdivisionGranularityExpression(128, 32), // Stencil granularity must never be higher than fill granularity, // otherwise we would get seams in the oceans at zoom levels where // stencil has higher granularity than fill. stencil: new SubdivisionGranularityExpression(128, 1), circle: 3 }); export class VerticalPerspectiveProjection implements Projection { private _tileMeshCache: {[_: string]: Mesh} = {}; // GPU atan() error correction private _errorMeasurement: ProjectionErrorMeasurement; private _errorQueryLatitudeDegrees: number; private _errorCorrectionUsable: number = 0.0; private _errorMeasurementLastValue: number = 0.0; private _errorCorrectionPreviousValue: number = 0.0; private _errorMeasurementLastChangeTime: number = -1000.0; get name(): 'vertical-perspective' { return 'vertical-perspective'; } get transitionState(): number { return 1; } get useSubdivision(): boolean { return true; } get shaderVariantName(): string { return VerticalPerspectiveShaderVariantKey; } get shaderDefine(): string { return VerticalPerspectiveShaderDefine; } get shaderPreludeCode(): PreparedShader { return shaders.projectionGlobe; } get vertexShaderPreludeCode(): string { return shaders.projectionMercator.vertexSource; } get subdivisionGranularity(): SubdivisionGranularitySetting { return granularitySettingsGlobe; } get useGlobeControls(): boolean { return true; } /** * @internal * Globe projection periodically measures the error of the GPU's * projection from mercator to globe and computes how much to correct * the globe's latitude alignment. * This stores the correction that should be applied to the projection matrix. */ get latitudeErrorCorrectionRadians(): number { return this._errorCorrectionUsable; } public destroy() { if (this._errorMeasurement) { this._errorMeasurement.destroy(); } } public updateGPUdependent(renderContext: ProjectionGPUContext): void { if (!this._errorMeasurement) { this._errorMeasurement = new ProjectionErrorMeasurement(renderContext); } const mercatorY = mercatorYfromLat(this._errorQueryLatitudeDegrees); const expectedResult = 2.0 * Math.atan(Math.exp(Math.PI - (mercatorY * Math.PI * 2.0))) - Math.PI * 0.5; const newValue = this._errorMeasurement.updateErrorLoop(mercatorY, expectedResult); const now = browser.now(); if (newValue !== this._errorMeasurementLastValue) { this._errorCorrectionPreviousValue = this._errorCorrectionUsable; // store the interpolated value this._errorMeasurementLastValue = newValue; this._errorMeasurementLastChangeTime = now; } const sinceUpdateSeconds = (now - this._errorMeasurementLastChangeTime) / 1000.0; const mix = Math.min(Math.max(sinceUpdateSeconds / globeConstants.errorTransitionTimeSeconds, 0.0), 1.0); const newCorrection = -this._errorMeasurementLastValue; // Note the negation this._errorCorrectionUsable = lerp(this._errorCorrectionPreviousValue, newCorrection, easeCubicInOut(mix)); } private _getMeshKey(options: CreateTileMeshOptions): string { return `${options.granularity.toString(36)}_${options.generateBorders ? 'b' : ''}${options.extendToNorthPole ? 'n' : ''}${options.extendToSouthPole ? 's' : ''}`; } public getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean, allowPoles: boolean, usage: TileMeshUsage): Mesh { // Stencil granularity must match fill granularity const granularityConfig = usage === 'stencil' ? granularitySettingsGlobe.stencil : granularitySettingsGlobe.tile; const granularity = granularityConfig.getGranularityForZoomLevel(canonical.z); const north = (canonical.y === 0) && allowPoles; const south = (canonical.y === (1 << canonical.z) - 1) && allowPoles; return this._getMesh(context, { granularity, generateBorders: hasBorder, extendToNorthPole: north, extendToSouthPole: south, }); } private _getMesh(context: Context, options: CreateTileMeshOptions): Mesh { const key = this._getMeshKey(options); if (key in this._tileMeshCache) { return this._tileMeshCache[key]; } const mesh = createTileMeshWithBuffers(context, options); this._tileMeshCache[key] = mesh; return mesh; } recalculate(_params: EvaluationParameters): void { // Do nothing. } hasTransition(): boolean { const now = browser.now(); let dirty = false; // Error correction transition dirty = dirty || (now - this._errorMeasurementLastChangeTime) / 1000.0 < (globeConstants.errorTransitionTimeSeconds + 0.2); // Error correction query in flight dirty = dirty || (this._errorMeasurement && this._errorMeasurement.awaitingQuery); return dirty; } setErrorQueryLatitudeDegrees(value: number) { this._errorQueryLatitudeDegrees = value; } }