Skip to Content
ExamplesCustom Fires Shader

MapsGL - Custom fire symbols using a shader

This example customizes the symbols displayed by the fires-obs layer using a custom fragment shader to create an animated fire effect.

Custom shaders can be used for symbol or circle layer types to create a unique animated visual effect for each symbol based on the feature’s data and properties.

custom-fires-shader.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MapsGL SDK - Custom fire symbols using a shader</title> <meta name="description" content="Use a custom WebGL fragment shader to apply a realistic fire effect to wildfire 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/light-v9', center: [-94, 42], 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('fires-obs', { type: 'symbol', paint: { symbol: { shader: ` // https://www.shadertoy.com/view/Xtf3DX #extension GL_OES_standard_derivatives : enable precision mediump float; uniform vec2 resolution; uniform float dpr; uniform float time; uniform sampler2D tDiffuse; varying vec2 vUv; varying vec2 vPosition; varying float vFactor; varying float vRandom; #include <fbm> float rand(const vec2 co) { float t = dot(vec2(12.9898, 78.233), co); return fract(sin(t) * (4375.85453 + t)); } void main() { vec2 uv = vec2(vUv.x, 1.0 - vUv.y); //vec2 uv = vUv; float t = (time + vRandom * 10.) * 0.5; vec2 q = uv; q.x *= 1.; q.y *= 2.; float strength = floor(q.x + 2.0); float T3 = max(3., 1.25 * strength) * t; q.x = mod(q.x, 1.) - 0.5; q.y -= 0.25; float n = fbm(strength * q - vec2(0, T3)); float c = 1. - 16. * pow(max(0., length(q * vec2(1.8 + q.y * 1.5, .75)) - n * max(0., q.y + .25)), 1.2); float c1 = n * c * (1.5 - pow(1.25 * uv.y, 2.)); c1 = clamp(c1, 0., 1.); // flame color vec3 col = vec3(3.5*c1, 1.5*c1*c1*c1, c1*c1*c1*c1*c1*c1); float alpha = 1.0; // adjust color and alpha based on vFactor (percent contained) if (vFactor == 1.) { col *= vec3(0.); alpha = 0.7; } // smoke intensity: lower number gives more smoke float a = c * (1. - pow(uv.y, 1.0)); col = mix(vec3(0.3), col, a); if (col.r == col.g && col.g == col.b) { alpha *= 1.0 - col.r; } if (uv.y > 0.5) { float dist = length(2.0 * uv - 1.0); float delta = fwidth(dist); alpha *= 1.0 - smoothstep(1.0 - delta - 0.5, 1.0, dist); } gl_FragColor = vec4(col, alpha); gl_FragColor.a = alpha; gl_FragColor.rgb *= gl_FragColor.a; } `, animated: true, anchor: 'bottom', size: (data) => { const area = Math.max(1, data.report?.areaAC); const contained = data.report?.perContained || 0; const factor = (area * (contained === 100 ? 0.75 : 1)) / 10000; const size = { width: Math.min(80, Math.round(30 + 40 * factor)), height: Math.min(130, Math.round(40 + 80 * factor)) }; return size; }, factor: (data) => { const contained = data.report?.perContained || 0; return contained / 100; }, pitchWithMap: false, rotateWithMap: false } } }); }); }); </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)