maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
234 lines (216 loc) • 8.65 kB
text/typescript
import {StyleLayer} from '../style_layer';
import type {Map} from '../../ui/map';
import {mat4} from 'gl-matrix';
import {LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
/**
* Input arguments exposed by custom render function.
*/
type CustomRenderMethodInput = {
/**
* This value represents the distance from the camera to the far clipping plane.
* It is used in the calculation of the projection matrix to determine which objects are visible.
* farz should be larger than nearZ.
*/
farZ: number;
/**
* This value represents the distance from the camera to the near clipping plane.
* It is used in the calculation of the projection matrix to determine which objects are visible.
* nearZ should be smaller than farZ.
*/
nearZ: number;
/** field of view of camera **/
fov: number;
/**
* model view projection matrix
* represents the matrix converting from world space to clip space
* https://learnopengl.com/Getting-started/Coordinate-Systems
* **/
modelViewProjectionMatrix: mat4;
/**
* projection matrix
* represents the matrix converting from view space to clip space
* https://learnopengl.com/Getting-started/Coordinate-Systems
*/
projectionMatrix: mat4;
}
/**
* @param gl - The map's gl context.
* @param matrix - The map's camera matrix. It projects spherical mercator
* coordinates to gl clip space coordinates. The spherical mercator coordinate `[0, 0]` represents the
* top left corner of the mercator world and `[1, 1]` represents the bottom right corner. When
* the `renderingMode` is `"3d"`, the z coordinate is conformal. A box with identical x, y, and z
* lengths in mercator units would be rendered as a cube. {@link MercatorCoordinate.fromLngLat}
* can be used to project a `LngLat` to a mercator coordinate.
* @param options - Argument object with additional render inputs like camera properties.
*/
type CustomRenderMethod = (gl: WebGLRenderingContext|WebGL2RenderingContext, matrix: mat4, options: CustomRenderMethodInput) => void;
/**
* Interface for custom style layers. This is a specification for
* implementers to model: it is not an exported method or class.
*
* Custom layers allow a user to render directly into the map's GL context using the map's camera.
* These layers can be added between any regular layers using {@link Map#addLayer}.
*
* Custom layers must have a unique `id` and must have the `type` of `"custom"`.
* They must implement `render` and may implement `prerender`, `onAdd` and `onRemove`.
* They can trigger rendering using {@link Map#triggerRepaint}
* and they should appropriately handle {@link MapContextEvent} with `webglcontextlost` and `webglcontextrestored`.
*
* The `renderingMode` property controls whether the layer is treated as a `"2d"` or `"3d"` map layer. Use:
*
* - `"renderingMode": "3d"` to use the depth buffer and share it with other layers
* - `"renderingMode": "2d"` to add a layer with no depth. If you need to use the depth buffer for a `"2d"` layer you must use an offscreen
* framebuffer and {@link CustomLayerInterface#prerender}
*
* @example
* Custom layer implemented as ES6 class
* ```ts
* class NullIslandLayer {
* constructor() {
* this.id = 'null-island';
* this.type = 'custom';
* this.renderingMode = '2d';
* }
*
* onAdd(map, gl) {
* const vertexSource = `
* uniform mat4 u_matrix;
* void main() {
* gl_Position = u_matrix * vec4(0.5, 0.5, 0.0, 1.0);
* gl_PointSize = 20.0;
* }`;
*
* const fragmentSource = `
* void main() {
* fragColor = vec4(1.0, 0.0, 0.0, 1.0);
* }`;
*
* const vertexShader = gl.createShader(gl.VERTEX_SHADER);
* gl.shaderSource(vertexShader, vertexSource);
* gl.compileShader(vertexShader);
* const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
* gl.shaderSource(fragmentShader, fragmentSource);
* gl.compileShader(fragmentShader);
*
* this.program = gl.createProgram();
* gl.attachShader(this.program, vertexShader);
* gl.attachShader(this.program, fragmentShader);
* gl.linkProgram(this.program);
* }
*
* render(gl, matrix) {
* gl.useProgram(this.program);
* gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
* gl.drawArrays(gl.POINTS, 0, 1);
* }
* }
*
* map.on('load', () => {
* map.addLayer(new NullIslandLayer());
* });
* ```
*/
export interface CustomLayerInterface {
/**
* A unique layer id.
*/
id: string;
/**
* The layer's type. Must be `"custom"`.
*/
type: 'custom';
/**
* Either `"2d"` or `"3d"`. Defaults to `"2d"`.
*/
renderingMode?: '2d' | '3d';
/**
* Called during a render frame allowing the layer to draw into the GL context.
*
* The layer can assume blending and depth state is set to allow the layer to properly
* blend and clip other layers. The layer cannot make any other assumptions about the
* current GL state.
*
* If the layer needs to render to a texture, it should implement the `prerender` method
* to do this and only use the `render` method for drawing directly into the main framebuffer.
*
* The blend function is set to `gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`. This expects
* colors to be provided in premultiplied alpha form where the `r`, `g` and `b` values are already
* multiplied by the `a` value. If you are unable to provide colors in premultiplied form you
* may want to change the blend function to
* `gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`.
*/
render: CustomRenderMethod;
/**
* Optional method called during a render frame to allow a layer to prepare resources or render into a texture.
*
* The layer cannot make any assumptions about the current GL state and must bind a framebuffer before rendering.
*/
prerender?: CustomRenderMethod;
/**
* Optional method called when the layer has been added to the Map with {@link Map#addLayer}. This
* gives the layer a chance to initialize gl resources and register event listeners.
*
* @param map - The Map this custom layer was just added to.
* @param gl - The gl context for the map.
*/
onAdd?(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
/**
* Optional method called when the layer has been removed from the Map with {@link Map#removeLayer}. This
* gives the layer a chance to clean up gl resources and event listeners.
*
* @param map - The Map this custom layer was just added to.
* @param gl - The gl context for the map.
*/
onRemove?(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
}
export function validateCustomStyleLayer(layerObject: CustomLayerInterface) {
const errors = [];
const id = layerObject.id;
if (id === undefined) {
errors.push({
message: `layers.${id}: missing required property "id"`
});
}
if (layerObject.render === undefined) {
errors.push({
message: `layers.${id}: missing required method "render"`
});
}
if (layerObject.renderingMode &&
layerObject.renderingMode !== '2d' &&
layerObject.renderingMode !== '3d') {
errors.push({
message: `layers.${id}: property "renderingMode" must be either "2d" or "3d"`
});
}
return errors;
}
export class CustomStyleLayer extends StyleLayer {
implementation: CustomLayerInterface;
constructor(implementation: CustomLayerInterface) {
super(implementation, {});
this.implementation = implementation;
}
is3D() {
return this.implementation.renderingMode === '3d';
}
hasOffscreenPass() {
return this.implementation.prerender !== undefined;
}
recalculate() {}
updateTransitions() {}
hasTransition() { return false; }
serialize(): LayerSpecification {
throw new Error('Custom layers cannot be serialized');
}
onAdd = (map: Map) => {
if (this.implementation.onAdd) {
this.implementation.onAdd(map, map.painter.context.gl);
}
};
onRemove = (map: Map) => {
if (this.implementation.onRemove) {
this.implementation.onRemove(map, map.painter.context.gl);
}
};
}