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/v2.8.0/mapbox-gl.css" rel="stylesheet" />
<script defer src="https://api.mapbox.com/mapbox-gl-js/v2.8.0/mapbox-gl.js"></script>
<link href="https://cdn.aerisapi.com/sdk/js/mapsgl/latest/aerisweather.mapsgl.css" rel="stylesheet" />
<script defer src="https://cdn.aerisapi.com/sdk/js/mapsgl/latest/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>