maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
343 lines (302 loc) • 15.8 kB
text/typescript
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
];
}
}
}