UNPKG

maplibre-gl

Version:

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

321 lines (296 loc) • 15.2 kB
import {Color} from '@maplibre/maplibre-gl-style-spec'; import {drawTerrain} from './draw/draw_terrain.ts'; import {setFrameUniformWorldSize} from './frame_uniform_buffer.ts'; import {ImageSource} from '../source/image_source.ts'; import {RTT_DIFFERENCES, RTTFingerprint, type RTTDifference} from './rtt_fingerprint.ts'; import {isCustomStyleLayer} from '../style/style_layer/custom_style_layer.ts'; import type {Tile} from '../tile/tile.ts'; import type {OverscaledTileID} from '../tile/tile_id.ts'; import type {Style} from '../style/style.ts'; import type {Terrain} from '../render/terrain.ts'; import type {Texture} from './texture.ts'; import type {StyleLayer} from '../style/style_layer.ts'; import type {Painter} from '../render/painter.ts'; import type {FrameRenderContext} from '../render/frame_render_context.ts'; /** * lookup table which layers should rendered to texture */ const LAYERS_TO_TEXTURES: { [keyof in StyleLayer['type']]?: boolean } = { background: true, fill: true, line: true, raster: true, hillshade: true, 'color-relief': true }; /** * Returns the key under which a tile keeps the fingerprint of the layer, which is the layer's source, or its id for a custom layer. */ function rttFingerprintKey(layer: StyleLayer): string | undefined { return isCustomStyleLayer(layer) ? `custom-layer:${layer.id}` : layer.source; } /** * Whether the layer is drawn into the terrain tiles' textures rather than onto the map: the layer types above, and * custom layers that implement `renderToTerrainTile`. */ function isRenderedToTexture(layer: StyleLayer): boolean { if (isCustomStyleLayer(layer)) return layer.implementation.renderToTerrainTile !== undefined; return LAYERS_TO_TEXTURES[layer.type] === true; } /** * Returns the ids of the layers in each render stack, a run of layers rendered to texture that ends at a layer * that is not. */ function getRenderStacks(style: Style, layerIds: string[]): string[][] { const stacks: string[][] = []; let prevRenderedToTexture = false; for (const layerId of layerIds) { const renderedToTexture = isRenderedToTexture(style._layers[layerId]); if (renderedToTexture && !prevRenderedToTexture) stacks.push([]); if (renderedToTexture) stacks[stacks.length - 1].push(layerId); prevRenderedToTexture = renderedToTexture; } return stacks; } /** * @internal * Renders RTT-eligible layers into per-tile cached textures, then drapes * them onto the terrain mesh. Slots live on each Tile so their lifetime * tracks the tile itself; the underlying FBO+texture handles are recycled * via the painter's RTT pool. */ export class RenderToTexture { painter: Painter; terrain: Terrain; /** RTT texture dimension in pixels (tile size × terrain quality factor). */ rttSize: number; /** * coordsAscending contains a list of all tiles which should be rendered for one render-to-texture tile * e.g. render 4 raster-tiles with size 256px to the 512px render-to-texture tile */ _coordsAscending: {[_: string]: {[_:string]: OverscaledTileID[]}}; /** * This frame's fingerprint for each render-to-texture tile, keyed by source id * then tile key. A tile whose stored fingerprint differs has stale textures. */ _rttFingerprints: Record<string, Record<string, RTTFingerprint>>; /** * store for render-stacks * a render stack is a set of layers which should be rendered into one texture * every stylesheet can have multiple stacks. A new stack is created if layers which should * not rendered to texture sit between layers which should rendered to texture. e.g. hillshading or symbols */ _stacks: string[][]; /** * whether the previous processed layer was rendered to texture, to check if a new stack is needed */ _prevRenderedToTexture: boolean; /** * a list of tiles that can potentially rendered */ _renderableTiles: Tile[]; /** * a list of tiles that should be rendered to screen in the next render-call */ _rttTiles: Tile[]; /** * a list of all layer-ids which should be rendered */ _renderableLayerIds: string[]; /** * the zoom of the previous prepareForRender call */ _lastPrepareZoom: number; /** * whether the render loop needs a follow-up frame to refresh cached textures that were kept this frame: * textures rendered at another zoom, and stale textures beyond the per-frame budget. */ needsFollowUpFrame: boolean = false; constructor(painter: Painter, terrain: Terrain) { this.painter = painter; this.terrain = terrain; this.rttSize = terrain.tileManager.tileSize * terrain.qualityFactor; } getTexture(tile: Tile): Texture { return tile.getRTT(this._stacks.length - 1).texture; } /** * Collects the frame's tiles, layers and source fingerprints and releases the textures that need * re-rendering. Textures that differ only by zoom are kept while the zoom is changing or the map is moving * and re-rendered one tile per frame once it has stopped, nearest to the camera first; textures with other * source tiles under them are re-rendered one per frame too, `needsFollowUpFrame` bringing in the rest. * Textures whose visible layer set changed (a layer entered or left its zoom range) are kept while the zoom * is changing and then all re-rendered in the same frame, since a tile-by-tile change would show; a source * data change re-renders immediately. */ prepareForRender(style: Style, zoom: number, isMoving: boolean): void { const zoomChanged = zoom !== this._lastPrepareZoom; this._lastPrepareZoom = zoom; this._stacks = []; this._prevRenderedToTexture = false; this._rttTiles = []; this._renderableTiles = this.terrain.tileManager.getRenderableTiles(); this._renderableLayerIds = style._order.filter(id => !style._layers[id].isHidden(zoom)); const stacks = getRenderStacks(style, this._renderableLayerIds); const visibleLayerIds = this._renderableLayerIds.join(); const rttSourceIds = new Set<string>(); for (const layerId of this._renderableLayerIds) { const layer = style._layers[layerId]; const source = layer.source; if (source && LAYERS_TO_TEXTURES[layer.type]) rttSourceIds.add(source); } this._coordsAscending = {}; this._rttFingerprints = {}; for (const sourceId of rttSourceIds) { const tileManager = style.tileManagers[sourceId]; if (!tileManager) continue; this._coordsAscending[sourceId] = {}; const coordsAscending = this._coordsAscending[sourceId]; const source = tileManager.getSource(); const terrainTileRanges = source instanceof ImageSource ? source.terrainTileRanges : null; for (const tileID of tileManager.getVisibleCoordinates()) { const keys = this.terrain.tileManager.getTerrainCoords(tileID, terrainTileRanges); for (const key in keys) { coordsAscending[key] ||= []; coordsAscending[key].push(keys[key]); } } this._rttFingerprints[sourceId] = {}; const fingerprints = this._rttFingerprints[sourceId]; const revision = tileManager.getState().revision; for (const key in coordsAscending) fingerprints[key] = new RTTFingerprint(coordsAscending[key], revision, zoom, visibleLayerIds); } for (const layerId of this._renderableLayerIds) { const layer = style._layers[layerId]; if (!isCustomStyleLayer(layer) || !isRenderedToTexture(layer)) continue; const revision = layer.implementation.terrainTileRevision ?? 0; const fingerprints: Record<string, RTTFingerprint> = {}; for (const tile of this._renderableTiles) { fingerprints[tile.tileID.key] = new RTTFingerprint([tile.tileID], revision, zoom, visibleLayerIds); } this._rttFingerprints[rttFingerprintKey(layer)] = fingerprints; } // check tiles to render this.needsFollowUpFrame = false; const moving = zoomChanged || isMoving; let staleTileReleased = false; for (const tile of this._renderableTiles) { const difference = this._textureDifference(tile); if (difference === 'none') continue; if ((difference === 'zoom' && moving) || (difference === 'visibleLayers' && zoomChanged)) { this.needsFollowUpFrame = true; } else if (difference === 'zoom' || difference === 'sourceTiles') { if (staleTileReleased) this.needsFollowUpFrame = true; else this._releaseChangedTextures(tile, style, stacks); staleTileReleased = true; } else { this._releaseChangedTextures(tile, style, stacks); } } } /** * Releases the tile's textures of the stacks whose layers changed, or all of its textures when every stack * changed, which also drops the textures of stacks that the layers no longer form. */ _releaseChangedTextures(tile: Tile, style: Style, stacks: string[][]): void { const changedStacks = stacks.map(layerIds => this._hasStackChanged(tile, style, layerIds)); if (!changedStacks.includes(false)) { tile.releaseRTT(this.painter); return; } for (const [stack, changed] of changedStacks.entries()) { if (changed) tile.releaseRTTStack(this.painter, stack); } } /** * Whether the tile's texture of the stack differs from what this frame would render into it. A stack whose * layers have no fingerprint, background layers only, counts as changed. */ _hasStackChanged(tile: Tile, style: Style, layerIds: string[]): boolean { let fingerprinted = false; for (const layerId of layerIds) { const fingerprintKey = rttFingerprintKey(style._layers[layerId]); const frameFingerprint = this._rttFingerprints[fingerprintKey]?.[tile.tileID.key]; const tileFingerprint = tile.rttFingerprint[fingerprintKey]; if (!frameFingerprint && !tileFingerprint) continue; if (frameFingerprint?.difference(tileFingerprint) !== 'none') return true; fingerprinted = true; } return !fingerprinted; } /** * The most severe difference, over the sources rendered to texture, between the tile's cached textures * and what this frame would render into them. */ _textureDifference(tile: Tile): RTTDifference { let worst: RTTDifference = 'none'; for (const source in this._rttFingerprints) { const frameFingerprint = this._rttFingerprints[source][tile.tileID.key]; if (!frameFingerprint) continue; const difference = frameFingerprint.difference(tile.rttFingerprint[source]); if (RTT_DIFFERENCES.indexOf(difference) > RTT_DIFFERENCES.indexOf(worst)) worst = difference; } return worst; } /** * due that switching textures is relatively slow, the render * layer-by-layer context is not practicable. To bypass this problem * this lines of code stack all layers and later render all at once. * Because of the stylesheet possibility to mixing render-to-texture layers * and 'live'-layers (f.e. symbols) it is necessary to create more stacks. For example * a symbol-layer is in between of fill-layers. * @param layer - the layer to render * @param style - the style the stacked layers and their tile managers come from * @param frameRenderContext - shared state for the current render * @returns if true layer is rendered to texture, otherwise false */ renderLayer(layer: StyleLayer, style: Style, frameRenderContext: FrameRenderContext): boolean { if (layer.isHidden(frameRenderContext.transform.zoom)) return false; const renderedToTexture = isRenderedToTexture(layer); const painter = this.painter; const isLastLayer = this._renderableLayerIds[this._renderableLayerIds.length - 1] === layer.id; // remember background, fill, line & raster layer to render into a stack if (renderedToTexture) { // create a new stack if previous layer was not rendered to texture (f.e. symbols) if (!this._prevRenderedToTexture) this._stacks.push([]); // push current render-to-texture layer to render-stack this._prevRenderedToTexture = true; this._stacks[this._stacks.length - 1].push(layer.id); // rendering is done later, all in once if (!isLastLayer) return true; } // in case a stack is finished render all collected stack-layers into a texture if (this._prevRenderedToTexture || (renderedToTexture && isLastLayer)) { this._prevRenderedToTexture = renderedToTexture; const stack = this._stacks.length - 1, layers = this._stacks[stack] || []; frameRenderContext.isRenderingToTexture = true; setFrameUniformWorldSize(painter.context.frameUniformBuffer, this.rttSize, this.rttSize); for (const tile of this._renderableTiles) { this._rttTiles.push(tile); // Cache hit: this tile already has a RTT object for this stack from a previous frame. if (tile.getRTT(stack)) continue; const obj = tile.acquireRTT(painter, stack, this.rttSize); painter.bindRTT(obj); painter.context.clear({color: Color.transparent, stencil: 0}); frameRenderContext.invalidateTileClippingMasks(); for (const layerId of layers) { const layer = style._layers[layerId]; const coords = layer.source ? this._coordsAscending[layer.source][tile.tileID.key] : [tile.tileID]; painter.context.viewport.set([0, 0, this.rttSize, this.rttSize]); frameRenderContext.renderTileClippingMasks(layer, coords); painter.renderLayer(painter, style.tileManagers[layer.source], layer, coords, frameRenderContext); const fingerprintKey = rttFingerprintKey(layer); if (fingerprintKey) tile.rttFingerprint[fingerprintKey] = this._rttFingerprints[fingerprintKey][tile.tileID.key]; } obj.texture.generateMipmap(); } frameRenderContext.isRenderingToTexture = false; setFrameUniformWorldSize(painter.context.frameUniformBuffer, painter.context.gl.drawingBufferWidth, painter.context.gl.drawingBufferHeight); drawTerrain(this.painter, this.terrain, this._rttTiles, frameRenderContext); this._rttTiles = []; return renderedToTexture; } return false; } }