Skip to Content
ExamplesCustom Lightning Shader

MapsGL - Custom lightning symbols using a shader

This example demonstrates how to customized the appearance of lightning symbols using a custom WebGL fragment shader using the symbol.shader paint property. The shader is used to create a custom lightning effect that is animated and changes based on the age of the lightning strike.

custom-lightning-shader.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MapsGL SDK - Custom lightning symbols using a shader</title> <meta name="description" content="Use a custom WebGL fragment shader to create a custom effect for lightning strike data." /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="https://api.mapbox.com/mapbox-gl-js/v3.12.0/mapbox-gl.css" rel="stylesheet" /> <script defer src="https://api.mapbox.com/mapbox-gl-js/v3.12.0/mapbox-gl.js"></script> <link href="https://cdn.aerisapi.com/sdk/js/mapsgl/1.9.1/aerisweather.mapsgl.css" rel="stylesheet" /> <script defer src="https://cdn.aerisapi.com/sdk/js/mapsgl/1.9.1/aerisweather.mapsgl.js"></script> <style> body, html { margin: 0; padding: 0; } #map { height: 100vh; width: 100%; } </style> </head> <body> <div id="map"></div> <script> window.addEventListener('load', () => { mapboxgl.accessToken = 'MAPBOX_TOKEN'; const map = new mapboxgl.Map({ container: document.getElementById('map'), style: 'mapbox://styles/mapbox/dark-v9', center: [-73.961, 31.984], zoom: 2 }); const account = new aerisweather.mapsgl.Account('CLIENT_ID', 'CLIENT_SECRET'); const controller = new aerisweather.mapsgl.MapboxMapController(map, { account }); controller.on('load', () => { controller.addWeatherLayer('lightning-strikes', { type: 'symbol', paint: { symbol: { shader: ` #extension GL_OES_standard_derivatives : enable precision mediump float; uniform vec2 resolution; uniform float dpr; uniform float time; varying vec2 vUv; varying vec2 vPosition; varying float vFactor; varying float vRandom; float rand(float x) { return fract(sin(x)*75154.32912); } float rand3d(vec3 x) { return fract(375.10297 * sin(dot(x, vec3(103.0139,227.0595,31.05914)))); } float noise(float x) { float i = floor(x); float a = rand(i), b = rand(i+1.); float f = x - i; return mix(a,b,f); } float perlin(float x) { float r=0.,s=1.,w=1.; for (int i=0; i<6; i++) { s *= 2.0; w *= 0.5; r += w * noise(s*x); } return r; } float noise3d(vec3 x) { vec3 i = floor(x); float i000 = rand3d(i+vec3(0.,0.,0.)), i001 = rand3d(i+vec3(0.,0.,1.)); float i010 = rand3d(i+vec3(0.,1.,0.)), i011 = rand3d(i+vec3(0.,1.,1.)); float i100 = rand3d(i+vec3(1.,0.,0.)), i101 = rand3d(i+vec3(1.,0.,1.)); float i110 = rand3d(i+vec3(1.,1.,0.)), i111 = rand3d(i+vec3(1.,1.,1.)); vec3 f = x - i; return mix(mix(mix(i000,i001,f.z), mix(i010,i011,f.z), f.y), mix(mix(i100,i101,f.z), mix(i110,i111,f.z), f.y), f.x); } float perlin3d(vec3 x) { float r = 0.0; float w = 1.0, s = 1.0; for (int i=0; i<5; i++) { w *= 0.5; s *= 2.0; r += w * noise3d(s * x); } return r; } #define COL1 vec4(0, 0, 0, 0) / 255.0 #define COL2 vec4(235, 241, 245, 255) / 255.0 #define SIZE 100 #define FLASH_POWER .8 #define RADIUS .001 #define SPEED .0018 #define SEED void main() { vec2 pos = vUv; float dist = length(2.0 * pos - 1.0) * 2.0; float x = time + 0.1; float m = 0.2 + 0.2 * vFactor; // max duration of strike float i = floor(x/m); float f = x/m - i; float k = vFactor; // frequency of strikes float n = noise(i); float t = ceil(n-k); // occurrence float d = max(0., n-k) / (1.-k); // duration float o = ceil(t - f - (1. - d)); // occurrence with duration float fx = 4.; if (o == 1.) { fx += 10. * vFactor; } fx = max(4., fx); float g = fx / (dist * (10. + 20.)) * FLASH_POWER; // smooth out edges to avoid fading extending beyond the symbol's bounds float edgeFadeFactor = smoothstep(0.5, 1.0, dist); float invertedEdgeFadeFactor = 1.0 - edgeFadeFactor; vec4 color = mix(COL1, COL2, g); color.a *= min(1.0, 0.5 + vFactor) * invertedEdgeFadeFactor; gl_FragColor = color; gl_FragColor.rgb *= gl_FragColor.a; } `, size: { width: 60, height: 60 }, animated: true, blending: 2, pitchWithMap: true, allowOverlap: true, factor: (data) => { const max = 200; return (200 - data.age) / 200; } } } }); }); }); </script> </body> </html>
© 2026 Xweather (opens in a new tab)Terms of Service (opens in a new tab)Privacy Policy (opens in a new tab)