name: mapboxgl-custom-webgl-layer
description: "Build, debug, and optimize mapboxgl type: 'custom' layers using raw WebGL. Covers shader setup, buffer pipelines, matrix usage, animation uniforms, precision-safe coordinate handling (RTC/local origin), and multiple geometry types (lines, polygons, points/icons). Use for custom GIS overlays and for fixing jitter, tearing, flicker, or empty rendering issues."
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.