WebGPU-accelerated snow particle layer for MapLibre GL JS.
Renders 100,000+ animated snow particles using Three.js WebGPU renderer with TSL compute shaders. Particles are georeferenced in Mercator coordinates — snow always fills the viewport regardless of zoom level. Supports wind direction, intensity, fog overlay, and more.
# npm
npm install @geoql/maplibre-gl-snow maplibre-gl three
# pnpm
pnpm add @geoql/maplibre-gl-snow maplibre-gl three
# yarn
yarn add @geoql/maplibre-gl-snow maplibre-gl three
# bun
bun add @geoql/maplibre-gl-snow maplibre-gl threeimport maplibregl from 'maplibre-gl';
import { MaplibreSnowLayer } from '@geoql/maplibre-gl-snow';
import 'maplibre-gl/dist/maplibre-gl.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
center: [-74.006, 40.7128],
zoom: 13,
pitch: 55,
maxPitch: 85,
});
map.on('load', () => {
const snow = new MaplibreSnowLayer({
density: 0.5,
intensity: 0.5,
flakeSize: 4,
opacity: 0.8,
direction: [0, 50], // [azimuth degrees, horizontal speed px/s]
fog: true,
fogOpacity: 0.08,
});
map.addLayer(snow);
});interface MaplibreSnowOptions {
id?: string;
density?: number;
intensity?: number;
flakeSize?: number;
opacity?: number;
direction?: [number, number];
fog?: boolean;
fogOpacity?: number;
}| Option | Type | Default | Description |
|---|---|---|---|
id |
string |
'snow' |
Unique layer ID |
density |
number (0–1) |
0.5 |
Particle density — maps to 10k–200k particles |
intensity |
number (0–1) |
0.5 |
Fall speed multiplier |
flakeSize |
number |
4 |
Base flake size in CSS pixels |
opacity |
number (0–1) |
0.8 |
Global opacity multiplier |
direction |
[number, number] |
[0, 50] |
Wind as [azimuth degrees, horizontal speed px/s] |
fog |
boolean |
true |
Enable atmospheric fog overlay |
fogOpacity |
number (0–1) |
0.08 |
Fog opacity |
const snow = new MaplibreSnowLayer(options);
// Update settings at runtime
snow.setDensity(0.8);
snow.setIntensity(0.7);
snow.setFlakeSize(6);
snow.setOpacity(0.6);
snow.setDirection([45, 80]); // wind from NE at 80 px/s
snow.setFog(false);
snow.setFogOpacity(0.12);The layer implements MapLibre's CustomLayerInterface with a two-canvas architecture:
- WebGPU overlay — Three.js
WebGPURendereron a separate<canvas>positioned absolutely over the MapLibre canvas.pointer-events: noneensures clicks pass through to the map. - TSL compute shaders — 100k particles stored in GPU
instancedArraybuffers. Compute shaders handle:computeInit— spawns particles in a zoom-adaptive volume centered on the viewportcomputeUpdate— applies gravity, wind drift, and respawns particles that fall below ground
- Georeferenced particles — positions stored as
(mercX, mercY, mercAlt)in Mercator [0,1] space. Spawn volume adapts to zoom level so snow always fills the viewport. - Camera sync — uses MapLibre's projection matrix directly. A
PerspectiveCamerawithupdateProjectionMatrixno-op'd prevents Three.js from overwriting the matrix. - Animation — MapLibre drives the frame loop via
triggerRepaint(), calling ourrender()callback which runs compute + render each frame.
Requires a WebGPU-capable browser (Chrome 113+, Edge 113+, Firefox Nightly with dom.webgpu.enabled, or Safari 17.4+ with WebGPU enabled).
Note: This library is WebGPU-only. There is no WebGL fallback. If WebGPU is unavailable, the layer silently does nothing.
// Main class
export { MaplibreSnowLayer } from '@geoql/maplibre-gl-snow';
// Default export (same class)
export { default } from '@geoql/maplibre-gl-snow';
// Types
export type { MaplibreSnowOptions } from '@geoql/maplibre-gl-snow';- MapLibre GL JS >= 3.0.0
- Three.js >= 0.183.0 (WebGPU-enabled build)
- Node.js >= 24.0.0
- Fork and create a feature branch from
main - Make changes following conventional commits
- Ensure commits are signed (why?)
- Submit a PR
bun install
bun run build
bun run lint
bun run typecheck