Mapbox GL Custom WebGL Layer
Implement custom Mapbox GL layers with deterministic WebGL lifecycle, stable geometry generation, and precision-safe rendering. Follow the workflow, rules, and checklists below.
CustomLayerInterface Contract
A custom layer object must conform to the Mapbox CustomLayerInterface:
| Property / Method | Required | Description |
|---|---|---|
id | ✅ | Unique layer identifier |
type | ✅ | Must be 'custom' |
renderingMode | ❌ | '2d' (default, no depth) or '3d' (shares depth buffer) |
onAdd(map, gl) | ❌ | Called once when layer is added. Initialize shaders, buffers, uniforms here. |
render(gl, matrix) | ✅ | Called every frame. Draw geometry here. Do not assume GL state except blend/depth. |
prerender(gl, matrix) | ❌ | Called before render if the layer needs to draw to a texture/FBO first. |
onRemove(map, gl) | ❌ | Called when layer is removed. Delete buffers, programs, textures here. |
Critical API Notes
- Mapbox uses premultiplied alpha blending. The expected blend state is
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA). Fragment shaders must output premultiplied colors:gl_FragColor = vec4(color.rgb * color.a, color.a). - Do NOT assume any GL state in
renderexcept that blending and depth are configured for compositing. Set all other state explicitly. - Call
map.triggerRepaint()only when continuous animation is needed. - Handle
webglcontextlost/webglcontextrestoredevents to recreate GPU resources gracefully.
Workflow
- Normalize input data into explicit
LineString/MultiLineString/Polygon/MultiPolygon/Pointprimitives. - Construct a custom layer object with
id,type: 'custom',renderingMode,onAdd,render, andonRemove. - In
onAdd: compile/link shaders, cache attribute/uniform locations, allocate buffers, register context-loss listeners. - Build geometry on CPU: convert coordinates → Mercator → local offsets. Upload typed arrays with
gl.bufferData. - In
render: computeoriginClipon CPU, set uniforms (u_matrix,u_originClip, time, colors), bind buffers, set GL state, draw. - Trigger repaint only when animation is active.
- In
onRemove: delete buffers, program, textures; remove event listeners.
Data Preprocessing
- Parse GeoJSON features, flatten
Multi*geometries into individual primitives. - Convert
[lng, lat]to Mercator usingmapboxgl.MercatorCoordinate.fromLngLat(lngLat). - Use
MercatorCoordinate.meterInMercatorCoordinateUnits()to convert metric widths/offsets to Mercator space. - Remove consecutive duplicate points (
dist < epsilon). - Optionally simplify dense polylines (Douglas-Peucker) for performance.
Precision Rules (RTC Pipeline)
- Keep geographic coordinates (
lng/lat) on CPU only. - Convert to
MercatorCoordinateon CPU — values range [0, 1]. - Pick one local origin (
originMercator) per geometry batch (e.g. centroid or first point). - Store vertex positions as local offsets:
local = mercator - originMercator. - Per frame on CPU:
originClip = matrix * vec4(originMercator.x, originMercator.y, 0, 1). - In vertex shader:
offsetClip = matrix * vec4(a_pos, 0, 0)(notew=0), thengl_Position = originClip + offsetClip. - Never add large world coordinates and small offsets in GPU math.
- Use
highpin vertex shader. Use fragment precision fallback guard:
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
Reference: precision-playbook.md
Geometry Rules
Lines / Path Ribbons
- Sample centerline in input order (do not reorder points).
- Compute direction from neighboring samples and derive perpendicular normals.
- Use miter join with clamp (
miterLimit ≈ 2.0) and bevel fallback on sharp turns. - Build triangle strip or indexed triangle list deterministically.
- For animated patterns, compute cumulative
lineDistanceon CPU and pass as attribute. - Rebuild buffers only when geometry-affecting props change (data, width, offset, join style).
Polygons
- Flatten rings from GeoJSON: outer ring + holes.
- Triangulate using earcut (or equivalent):
earcut(flatCoords, holeIndices, dimensions). - Convert resulting vertex positions to Mercator local offsets (same RTC pipeline).
- Use
gl.drawElements(gl.TRIANGLES, ...)with the earcut index buffer. - For extruded polygons (3D), add height attribute and adjust
renderingMode: '3d'.
Points / Icons / Symbols
- For each point, emit a billboard quad (4 vertices, 2 triangles) centered at the point.
- Pass quad corner offsets as attributes (e.g.
[-1,-1], [1,-1], [1,1], [-1,1]). - In vertex shader, scale quad offsets by desired pixel size / zoom factor.
- Use texture atlas or SDF (Signed Distance Field) for icon/glyph rendering.
- For large point counts, consider instancing (
ANGLE_instanced_arraysextension).
Reference: common-patterns.md
Render-State Rules
- Set blend state explicitly:
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)(premultiplied alpha). - Disable depth test for pure 2D overlay layers unless 3D ordering is required.
- Rebind attribute pointers explicitly every frame — do not rely on preserved state.
- Avoid hidden global state assumptions between custom layers.
- Restore any modified GL state if the layer modifies non-standard state (stencil, scissor, etc.).
- Keep
renderingModealigned with intent:'2d'for overlays,'3d'for depth interaction.
Performance Rules
- Use
gl.STATIC_DRAWfor geometry that rarely changes;gl.DYNAMIC_DRAWfor frequently updated data. - Avoid rebuilding geometry or re-uploading buffers every frame.
- Batch multiple features into a single draw call when possible.
- Minimize shader uniform uploads — cache values and skip if unchanged.
- Use
Uint16Arrayfor index buffers when vertex count ≤ 65535;Uint32ArraywithOES_element_index_uintextension otherwise. - Consider using VAO (
OES_vertex_array_object) to reduce per-frame attribute setup cost.
Debug Sequence
- Check layer is actually added:
map.getLayer(id). - Check shader compile/link logs before geometry debugging.
- Log
indexCount/vertexCount; if zero, fix data normalization or geometry build. - If geometry exists but nothing renders, verify attribute locations are not
-1and draw count > 0. - If geometry renders but flickers/jitters, verify RTC pipeline and
w=0local transform. - If colors appear too bright or too dark, check premultiplied alpha output.
- If spikes or self-intersections appear, tune join strategy (reduce
miterLimit, use bevel fallback). - If visuals disappear on some GPUs, check precision declarations and fragment fallback.
- If layer disappears after tab switch, handle
webglcontextrestoredevent.
Reference: troubleshooting-checklist.md
Output Contract
- Return implementation changes with concrete file paths and line-level rationale.
- Explain precision decisions explicitly when touching vertex transforms.
- Explain blending mode choice (premultiplied alpha).
- Add a short validation checklist: render visible, zoom/pan stability, no shader errors, context-loss recovery.