UNPKG

maplibre-gl

Version:

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

343 lines (302 loc) • 15.8 kB
import {Color} from '@maplibre/maplibre-gl-style-spec'; import {TileManager} from '../tile/tile_manager.ts'; import {DepthMode} from '../webgl/depth_mode.ts'; import {ColorMode} from '../webgl/color_mode.ts'; import {StencilMode} from '../webgl/stencil_mode.ts'; import {CullFaceMode} from '../webgl/cull_face_mode.ts'; import {shaders} from '../shaders/shaders.ts'; import {MercatorShaderDefine, MercatorShaderVariantKey} from '../geo/projection/mercator_projection.ts'; import type {IReadonlyTransform} from '../geo/transform_interface.ts'; import type {Terrain, TerrainData} from './terrain.ts'; import type {RendererProjectionData} from '../geo/projection/projection_data.ts'; import type {CanonicalTileID, OverscaledTileID} from '../tile/tile_id.ts'; import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../webgl/types.ts'; import type {Context} from '../webgl/context.ts'; import type {Program} from '../webgl/program.ts'; import type {ProgramCache} from '../webgl/program_cache.ts'; import type {ProgramConfiguration} from '../data/program_configuration.ts'; import type {PreparedShader} from '../shaders/shaders.ts'; import type {Mesh} from './mesh.ts'; import type {StyleLayer} from '../style/style_layer.ts'; import type {LightPropsPossiblyEvaluated} from '../style/light_properties.g.ts'; import type {SkyPropsPossiblyEvaluated} from '../style/sky_properties.g.ts'; export type RenderPass = 'offscreen' | 'opaque' | 'translucent'; /** The code a projection adds to every shader, and the name that tells its programs apart. */ export type ProjectionShaderVariant = { readonly name: string; readonly define: string; readonly prelude: PreparedShader; }; /** Plain values that describe one frame. The map builds them once per frame. */ export type FrameRenderData = { readonly showOverdrawInspector: boolean; readonly showTileBoundaries: boolean; readonly showPadding: boolean; readonly rotating: boolean; readonly zooming: boolean; readonly moving: boolean; readonly fadeDuration: number; /** Progress of the symbol fade since the last placement. */ readonly symbolFadeChange: number; readonly anisotropicFilterPitch: number; readonly projectionTransition: number; readonly isRenderingGlobe: boolean; /** The projection's shader variant for this frame, undefined until the style has a projection. Globe switches it during its transition. */ readonly projectionShaderVariant: ProjectionShaderVariant | undefined; /** Whether the projection subdivides tile meshes this frame. */ readonly useSubdivision: boolean; /** Drawing buffer pixels per CSS pixel, after the map clamps it to the largest canvas the GPU supports. */ readonly pixelRatio: number; /** The style's light, evaluated for this frame, undefined until the style has loaded. */ readonly light: Readonly<LightPropsPossiblyEvaluated> | undefined; /** The style's sky, evaluated for this frame, undefined until the style has loaded. */ readonly sky: Readonly<SkyPropsPossiblyEvaluated> | undefined; }; type FrameRenderContextOptions = { transform: IReadonlyTransform; terrain: Terrain | null; data: FrameRenderData; context: Context; programCache: ProgramCache; currentPass: RenderPass; /** Returns the mesh a tile's clipping mask is drawn with. */ getStencilMesh: (tileID: CanonicalTileID, hasBorder: boolean) => Mesh; }; /** Distinct z-planes within each layer that can be drawn to, implemented with the WebGL depth buffer. */ const NUM_SUBLAYERS: number = TileManager.maxOverzooming + TileManager.maxUnderzooming + 1; /** Depth distance between two neighboring sublayers. */ const DEPTH_EPSILON: number = 1 / Math.pow(2, 16); /** The mercator variant, for draws that force the simple projection, like fullscreen quads. */ const MERCATOR_SHADER_VARIANT: ProjectionShaderVariant = { name: MercatorShaderVariantKey, define: MercatorShaderDefine, prelude: shaders.projectionMercator }; /** * @internal * The state of one frame, created per render and updated as rendering proceeds. * Corresponds to part of MapLibre Native's `PaintParameters`. */ export class FrameRenderContext { currentPass: RenderPass; currentLayer: number = 0; opaquePassCutoff: number = Infinity; depthRangeFor3D: DepthRangeType = [0, 1]; isRenderingToTexture: boolean = false; readonly transform: IReadonlyTransform; readonly terrain: Terrain | null; readonly data: FrameRenderData; readonly context: Context; readonly programCache: ProgramCache; /** The source whose clipping masks are in the stencil buffer. */ private currentStencilSource: string; private nextStencilID: number = 1; private tileClippingMaskIDs: Record<string, number> = {}; private readonly getStencilMesh: (tileID: CanonicalTileID, hasBorder: boolean) => Mesh; constructor(options: FrameRenderContextOptions) { this.transform = options.transform; this.terrain = options.terrain; this.data = options.data; this.context = options.context; this.programCache = options.programCache; this.currentPass = options.currentPass; this.getStencilMesh = options.getStencilMesh; } getProjectionDataForTile(tileID: OverscaledTileID, options: {aligned?: boolean; applyTerrainMatrix?: boolean} = {}): RendererProjectionData { const projectionData = this.transform.getProjectionData({ overscaledTileID: tileID, aligned: options.aligned, applyGlobeMatrix: !this.isRenderingToTexture, applyTerrainMatrix: options.applyTerrainMatrix ?? true }); if (this.isRenderingToTexture) return projectionData; projectionData.uniformBufferKey = options.aligned ? `${tileID.key}/aligned` : tileID.key; return projectionData; } /** * Returns terrain data for a tile. * Returns null if terrain is not configured or tiles are being rendered to a texture. */ getTerrainDataForTile(tileID: OverscaledTileID): TerrainData | null { if (this.isRenderingToTexture) return null; return this.terrain?.getTerrainData(tileID) ?? null; } /** * Finds the required shader and its variant (base/terrain/globe, etc.) and binds it, compiling a new shader if required. * @param name - Name of the desired shader. * @param programConfiguration - Configuration of shader's inputs. * @param forceSimpleProjection - Whether to force the use of a shader variant with simple mercator projection vertex shader. * False by default. Use true when drawing with a simple projection matrix is desired, eg. when drawing a fullscreen quad. * @param defines - Additional macros to be injected at the beginning of the shader. Expected format is `['#define XYZ']`, etc. */ useProgram(name: string, programConfiguration?: ProgramConfiguration | null, forceSimpleProjection: boolean = false, defines: string[] = []): Program<any> { return this.programCache.getProgram({ name, programConfiguration, projectionShaderVariant: forceSimpleProjection ? MERCATOR_SHADER_VARIANT : this.data.projectionShaderVariant, showOverdrawInspector: this.data.showOverdrawInspector, useTerrain: this.terrain !== null, defines }); } colorModeForRenderPass(): Readonly<ColorMode> { const gl = this.context.gl; if (this.data.showOverdrawInspector) { const numOverdrawSteps = 8; const a = 1 / numOverdrawSteps; return new ColorMode([gl.CONSTANT_COLOR, gl.ONE], new Color(a, a, a, 0), [true, true, true, true]); } else if (this.currentPass === 'opaque') { return ColorMode.unblended; } else { return ColorMode.alphaBlended; } } getDepthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly<DepthMode> { if (!this.opaquePassEnabledForLayer()) return DepthMode.disabled; const depth = 1 - ((1 + this.currentLayer) * NUM_SUBLAYERS + n) * DEPTH_EPSILON; return new DepthMode(func || this.context.gl.LEQUAL, mask, [depth, depth]); } getDepthModeFor3D(): Readonly<DepthMode> { return new DepthMode(this.context.gl.LEQUAL, DepthMode.ReadWrite, this.depthRangeFor3D); } /** Sets the depth range that 3D layers draw into, which lies below the depth values of every sublayer of the `layerCount` layers. */ setDepthRangeFor3D(layerCount: number): void { this.depthRangeFor3D = [0, 1 - ((layerCount + 2) * NUM_SUBLAYERS * DEPTH_EPSILON)]; } /** * Returns whether the current layer can be drawn in the opaque pass. The opaque pass and 3D layers both use the depth buffer, * so layers drawn above 3D layers use the painter's algorithm to appear above 3D features. */ opaquePassEnabledForLayer(): boolean { return this.currentLayer < this.opaquePassCutoff; } /** * Reset the drawing canvas by clearing the stencil buffer so that we can draw * new tiles at the same location, while retaining previously drawn pixels. */ clearStencil(): void { this.nextStencilID = 1; this.currentStencilSource = undefined; this.context.clear({stencil: 0}); } /** Makes the next tile-clipped layer draw its clipping masks again. */ invalidateTileClippingMasks(): void { this.currentStencilSource = undefined; } /** Draws the clipping masks of a layer's tiles into the stencil buffer, unless its source's masks are already there. */ renderTileClippingMasks(layer: StyleLayer, tileIDs: OverscaledTileID[]): void { if (this.currentStencilSource === layer.source || !layer.isTileClipped() || !tileIDs?.length) { return; } this.currentStencilSource = layer.source; if (this.nextStencilID + tileIDs.length > 256) { this.clearStencil(); } const context = this.context; context.setColorMode(ColorMode.disabled); context.setDepthMode(DepthMode.disabled); const stencilRefs: Record<string, number> = {}; for (const tileID of tileIDs) { stencilRefs[tileID.key] = this.nextStencilID++; } if (this.data.useSubdivision) { this.renderTileMasks(stencilRefs, tileIDs, true); } this.renderTileMasks(stencilRefs, tileIDs, false); this.tileClippingMaskIDs = stencilRefs; } private renderTileMasks(tileStencilRefs: Record<string, number>, tileIDs: OverscaledTileID[], useBorders: boolean): void { const context = this.context; const gl = context.gl; const program = this.useProgram('clippingMask'); for (const tileID of tileIDs) { const stencilRef = tileStencilRefs[tileID.key]; const terrainData = this.getTerrainDataForTile(tileID); const mesh = this.getStencilMesh(tileID.canonical, useBorders); const projectionData = this.getProjectionDataForTile(tileID); program.draw(context, gl.TRIANGLES, DepthMode.disabled, new StencilMode({func: gl.ALWAYS, mask: 0}, stencilRef, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE), ColorMode.disabled, this.isRenderingToTexture ? CullFaceMode.disabled : CullFaceMode.backCCW, null, terrainData, projectionData, '$clipping', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); } } /** Returns a stencil mode that draws each pixel of a 3D layer only once. */ stencilModeFor3D(): StencilMode { this.currentStencilSource = undefined; if (this.nextStencilID + 1 > 256) { this.clearStencil(); } const id = this.nextStencilID++; const gl = this.context.gl; return new StencilMode({func: gl.NOTEQUAL, mask: 0xFF}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); } stencilModeForClipping(tileID: OverscaledTileID): StencilMode { const gl = this.context.gl; return new StencilMode({func: gl.EQUAL, mask: 0xFF}, this.tileClippingMaskIDs[tileID.key], 0x00, gl.KEEP, gl.KEEP, gl.REPLACE); } /** * Sort coordinates by Z as drawing tiles is done in Z-descending order. * All children with the same Z write the same stencil value. Children * stencil values are greater than parent's. This is used only for raster * and raster-dem tiles, which are already clipped to tile boundaries, to * mask area of tile overlapped by children tiles. * Stencil ref values continue the range used by the tile clipping masks. * * Attention: This function changes the next stencil ID even if the result of it * is not used, which might cause problems when rendering due to invalid stencil * values. * Returns [StencilMode for tile overscaleZ map, sortedCoords]. */ getStencilConfigForOverlapAndUpdateStencilID(tileIDs: OverscaledTileID[]): [Record<number, Readonly<StencilMode>>, OverscaledTileID[]] { const gl = this.context.gl; const coords = tileIDs.sort((a, b) => b.overscaledZ - a.overscaledZ); const minTileZ = coords[coords.length - 1].overscaledZ; const stencilValues = coords[0].overscaledZ - minTileZ + 1; if (stencilValues > 1) { this.currentStencilSource = undefined; if (this.nextStencilID + stencilValues > 256) { this.clearStencil(); } const zToStencilMode: Record<number, Readonly<StencilMode>> = {}; for (let i = 0; i < stencilValues; i++) { zToStencilMode[i + minTileZ] = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, i + this.nextStencilID, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); } this.nextStencilID += stencilValues; return [zToStencilMode, coords]; } return [{[minTileZ]: StencilMode.disabled}, coords]; } stencilConfigForOverlapTwoPass(tileIDs: OverscaledTileID[]): [ Record<number, Readonly<StencilMode>>, // borderless tiles - high priority & high stencil values Record<number, Readonly<StencilMode>>, // tiles with border - low priority OverscaledTileID[] ] { const gl = this.context.gl; const coords = tileIDs.sort((a, b) => b.overscaledZ - a.overscaledZ); const minTileZ = coords[coords.length - 1].overscaledZ; const stencilValues = coords[0].overscaledZ - minTileZ + 1; this.clearStencil(); if (stencilValues > 1) { const zToStencilModeHigh: Record<number, Readonly<StencilMode>> = {}; const zToStencilModeLow: Record<number, Readonly<StencilMode>> = {}; for (let i = 0; i < stencilValues; i++) { zToStencilModeHigh[i + minTileZ] = new StencilMode({func: gl.GREATER, mask: 0xFF}, stencilValues + 1 + i, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); zToStencilModeLow[i + minTileZ] = new StencilMode({func: gl.GREATER, mask: 0xFF}, 1 + i, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); } this.nextStencilID = stencilValues * 2 + 1; return [ zToStencilModeHigh, zToStencilModeLow, coords ]; } else { this.nextStencilID = 3; return [ {[minTileZ]: new StencilMode({func: gl.GREATER, mask: 0xFF}, 2, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE)}, {[minTileZ]: new StencilMode({func: gl.GREATER, mask: 0xFF}, 1, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE)}, coords ]; } } }