1 | import StyleLayer from '../style_layer';
|
2 | import type Map from '../../ui/map';
|
3 | import assert from 'assert';
|
4 | import {mat4} from 'gl-matrix';
|
5 | import {LayerSpecification} from '../../style-spec/types.g';
|
6 |
|
7 | type CustomRenderMethod = (gl: WebGLRenderingContext, matrix: mat4) => void;
|
8 |
|
9 | /**
|
10 | * Interface for custom style layers. This is a specification for
|
11 | * implementers to model: it is not an exported method or class.
|
12 | *
|
13 | * Custom layers allow a user to render directly into the map's GL context using the map's camera.
|
14 | * These layers can be added between any regular layers using {@link Map#addLayer}.
|
15 | *
|
16 | * Custom layers must have a unique `id` and must have the `type` of `"custom"`.
|
17 | * They must implement `render` and may implement `prerender`, `onAdd` and `onRemove`.
|
18 | * They can trigger rendering using {@link Map#triggerRepaint}
|
19 | * and they should appropriately handle {@link Map.event:webglcontextlost} and
|
20 | * {@link Map.event:webglcontextrestored}.
|
21 | *
|
22 | * The `renderingMode` property controls whether the layer is treated as a `"2d"` or `"3d"` map layer. Use:
|
23 | * - `"renderingMode": "3d"` to use the depth buffer and share it with other layers
|
24 | * - `"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
|
25 | * framebuffer and {@link CustomLayerInterface#prerender}
|
26 | *
|
27 | * @interface CustomLayerInterface
|
28 | * @example
|
29 | * // Custom layer implemented as ES6 class
|
30 | * class NullIslandLayer {
|
31 | * constructor() {
|
32 | * this.id = 'null-island';
|
33 | * this.type = 'custom';
|
34 | * this.renderingMode = '2d';
|
35 | * }
|
36 | *
|
37 | * onAdd(map, gl) {
|
38 | * const vertexSource = `
|
39 | * uniform mat4 u_matrix;
|
40 | * void main() {
|
41 | * gl_Position = u_matrix * vec4(0.5, 0.5, 0.0, 1.0);
|
42 | * gl_PointSize = 20.0;
|
43 | * }`;
|
44 | *
|
45 | * const fragmentSource = `
|
46 | * void main() {
|
47 | * gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
48 | * }`;
|
49 | *
|
50 | * const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
51 | * gl.shaderSource(vertexShader, vertexSource);
|
52 | * gl.compileShader(vertexShader);
|
53 | * const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
54 | * gl.shaderSource(fragmentShader, fragmentSource);
|
55 | * gl.compileShader(fragmentShader);
|
56 | *
|
57 | * this.program = gl.createProgram();
|
58 | * gl.attachShader(this.program, vertexShader);
|
59 | * gl.attachShader(this.program, fragmentShader);
|
60 | * gl.linkProgram(this.program);
|
61 | * }
|
62 | *
|
63 | * render(gl, matrix) {
|
64 | * gl.useProgram(this.program);
|
65 | * gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
|
66 | * gl.drawArrays(gl.POINTS, 0, 1);
|
67 | * }
|
68 | * }
|
69 | *
|
70 | * map.on('load', function() {
|
71 | * map.addLayer(new NullIslandLayer());
|
72 | * });
|
73 | */
|
74 | export interface CustomLayerInterface {
|
75 | /**
|
76 | * @property {string} id A unique layer id.
|
77 | */
|
78 | id: string;
|
79 | /**
|
80 | * @property {string} type The layer's type. Must be `"custom"`.
|
81 | */
|
82 | type: 'custom';
|
83 | /**
|
84 | * @property {string} renderingMode Either `"2d"` or `"3d"`. Defaults to `"2d"`.
|
85 | */
|
86 | renderingMode?: '2d' | '3d';
|
87 | /**
|
88 | * Called during a render frame allowing the layer to draw into the GL context.
|
89 | *
|
90 | * The layer can assume blending and depth state is set to allow the layer to properly
|
91 | * blend and clip other layers. The layer cannot make any other assumptions about the
|
92 | * current GL state.
|
93 | *
|
94 | * If the layer needs to render to a texture, it should implement the `prerender` method
|
95 | * to do this and only use the `render` method for drawing directly into the main framebuffer.
|
96 | *
|
97 | * The blend function is set to `gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`. This expects
|
98 | * colors to be provided in premultiplied alpha form where the `r`, `g` and `b` values are already
|
99 | * multiplied by the `a` value. If you are unable to provide colors in premultiplied form you
|
100 | * may want to change the blend function to
|
101 | * `gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`.
|
102 | *
|
103 | * @function
|
104 | * @memberof CustomLayerInterface
|
105 | * @instance
|
106 | * @name render
|
107 | * @param {WebGLRenderingContext} gl The map's gl context.
|
108 | * @param {Array<number>} matrix The map's camera matrix. It projects spherical mercator
|
109 | * coordinates to gl coordinates. The spherical mercator coordinate `[0, 0]` represents the
|
110 | * top left corner of the mercator world and `[1, 1]` represents the bottom right corner. When
|
111 | * the `renderingMode` is `"3d"`, the z coordinate is conformal. A box with identical x, y, and z
|
112 | * lengths in mercator units would be rendered as a cube. {@link MercatorCoordinate}.fromLngLat
|
113 | * can be used to project a `LngLat` to a mercator coordinate.
|
114 | */
|
115 | render: CustomRenderMethod;
|
116 | /**
|
117 | * Optional method called during a render frame to allow a layer to prepare resources or render into a texture.
|
118 | *
|
119 | * The layer cannot make any assumptions about the current GL state and must bind a framebuffer before rendering.
|
120 | *
|
121 | * @function
|
122 | * @memberof CustomLayerInterface
|
123 | * @instance
|
124 | * @name prerender
|
125 | * @param {WebGLRenderingContext} gl The map's gl context.
|
126 | * @param {mat4} matrix The map's camera matrix. It projects spherical mercator
|
127 | * coordinates to gl coordinates. The mercator coordinate `[0, 0]` represents the
|
128 | * top left corner of the mercator world and `[1, 1]` represents the bottom right corner. When
|
129 | * the `renderingMode` is `"3d"`, the z coordinate is conformal. A box with identical x, y, and z
|
130 | * lengths in mercator units would be rendered as a cube. {@link MercatorCoordinate}.fromLngLat
|
131 | * can be used to project a `LngLat` to a mercator coordinate.
|
132 | */
|
133 | prerender?: CustomRenderMethod;
|
134 | /**
|
135 | * Optional method called when the layer has been added to the Map with {@link Map#addLayer}. This
|
136 | * gives the layer a chance to initialize gl resources and register event listeners.
|
137 | *
|
138 | * @function
|
139 | * @memberof CustomLayerInterface
|
140 | * @instance
|
141 | * @name onAdd
|
142 | * @param {Map} map The Map this custom layer was just added to.
|
143 | * @param {WebGLRenderingContext} gl The gl context for the map.
|
144 | */
|
145 | onAdd?(map: Map, gl: WebGLRenderingContext): void;
|
146 | /**
|
147 | * Optional method called when the layer has been removed from the Map with {@link Map#removeLayer}. This
|
148 | * gives the layer a chance to clean up gl resources and event listeners.
|
149 | *
|
150 | * @function
|
151 | * @memberof CustomLayerInterface
|
152 | * @instance
|
153 | * @name onRemove
|
154 | * @param {Map} map The Map this custom layer was just added to.
|
155 | * @param {WebGLRenderingContext} gl The gl context for the map.
|
156 | */
|
157 | onRemove?(map: Map, gl: WebGLRenderingContext): void;
|
158 | }
|
159 |
|
160 | export function validateCustomStyleLayer(layerObject: CustomLayerInterface) {
|
161 | const errors = [];
|
162 | const id = layerObject.id;
|
163 |
|
164 | if (id === undefined) {
|
165 | errors.push({
|
166 | message: `layers.${id}: missing required property "id"`
|
167 | });
|
168 | }
|
169 |
|
170 | if (layerObject.render === undefined) {
|
171 | errors.push({
|
172 | message: `layers.${id}: missing required method "render"`
|
173 | });
|
174 | }
|
175 |
|
176 | if (layerObject.renderingMode &&
|
177 | layerObject.renderingMode !== '2d' &&
|
178 | layerObject.renderingMode !== '3d') {
|
179 | errors.push({
|
180 | message: `layers.${id}: property "renderingMode" must be either "2d" or "3d"`
|
181 | });
|
182 | }
|
183 |
|
184 | return errors;
|
185 | }
|
186 |
|
187 | class CustomStyleLayer extends StyleLayer {
|
188 |
|
189 | implementation: CustomLayerInterface;
|
190 |
|
191 | constructor(implementation: CustomLayerInterface) {
|
192 | super(implementation, {});
|
193 | this.implementation = implementation;
|
194 | }
|
195 |
|
196 | is3D() {
|
197 | return this.implementation.renderingMode === '3d';
|
198 | }
|
199 |
|
200 | hasOffscreenPass() {
|
201 | return this.implementation.prerender !== undefined;
|
202 | }
|
203 |
|
204 | recalculate() {}
|
205 | updateTransitions() {}
|
206 | hasTransition() { return false; }
|
207 |
|
208 | serialize(): LayerSpecification {
|
209 | assert(false, 'Custom layers cannot be serialized');
|
210 | }
|
211 |
|
212 | onAdd = (map: Map) => {
|
213 | if (this.implementation.onAdd) {
|
214 | this.implementation.onAdd(map, map.painter.context.gl);
|
215 | }
|
216 | };
|
217 |
|
218 | onRemove = (map: Map) => {
|
219 | if (this.implementation.onRemove) {
|
220 | this.implementation.onRemove(map, map.painter.context.gl);
|
221 | }
|
222 | };
|
223 | }
|
224 |
|
225 | export default CustomStyleLayer;
|