当内置的点、线、面图层无法满足需求时,可以通过 BMap.WebGLCustomLayer 直接在地图的 WebGL 上下文中,使用自定义 GLSL 着色器绘制内容,实现粒子系统、流场、光效等高度个性化的可视化效果。图层会负责着色器编译、GL 资源管理与逐帧调度,你只需关注着色器与顶点数据。
创建图层时,vertexShader 与 fragmentShader 为必填的着色器源码。顶点使用图层局部坐标,强烈建议传入 referCenter 作为坐标原点,避免墨卡托坐标量级过大造成 float32 精度丢失。顶点属性在 attributes 中声明,其中 type: 'lnglat' 是语法糖,图层会自动把经纬度投影成局部坐标。
下面通过一组发光点演示基础用法:在 attributes 中声明逐顶点的位置与颜色,配合内置的 u_time 实现呼吸动画。
var CENTER = new BMap.Point(116.404, 39.915);var map = new BMap.Map('map');map.centerAndZoom(CENTER, 13);// 顶点着色器:u_ 打头的多为内置 uniform,声明即由图层自动注入var vertexShader = `attribute vec2 a_pos;attribute vec4 a_color;uniform mat4 u_mvpMatrix; // 图层局部坐标 -> 裁剪空间uniform float u_pixelRatio; // 设备像素比uniform float u_size;varying vec4 v_color;void main() {v_color = a_color;gl_Position = u_mvpMatrix * vec4(a_pos, 0.0, 1.0);gl_PointSize = u_size * u_pixelRatio;}`;// 片元着色器:用内置 u_time 做呼吸动画,并把方点裁成圆形光斑var fragmentShader = `precision mediump float;uniform float u_opacity;uniform float u_time;varying vec4 v_color;void main() {float d = distance(gl_PointCoord, vec2(0.5)) * 2.0;if (d > 1.0) { discard; }float breath = 0.5 + 0.5 * sin(u_time * 2.0);float core = smoothstep(1.0, 0.0, d);gl_FragColor = vec4(v_color.rgb, v_color.a * mix(0.35, 1.0, breath) * core * core * u_opacity);}`;var layer = new BMap.WebGLCustomLayer({referCenter: CENTER, // 图层局部坐标原点,强烈建议传入animation: true, // 每帧自动重绘,u_time 随之推进vertexShader: vertexShader,fragmentShader: fragmentShader,attributes: {// type:'lnglat' 会自动把经纬度投影成局部坐标,无需再写 sizea_pos: {data: [[116.40282, 39.93227], [116.40396, 39.91512], [116.38909, 39.92451]],type: 'lnglat'},// Uint8Array + normalized:着色器里读到 0~1,省显存a_color: {data: new Uint8Array([82, 196, 26, 255, 255, 77, 79, 255, 19, 194, 194, 255]),size: 4,normalized: true}},uniforms: {u_size: 56},draw: {mode: 'POINTS'}});map.addLayer(layer);
图层支持两种组织绘制的方式,二者等价,也可混用:
声明式:几何固定时,把顶点数据、uniform 与绘制指令直接写在构造参数里,无需提供 onRender,图层会按 draw 描述自动绘制一次(上面的发光点即是)。
命令式:几何逐帧变化,或需要在一帧内多次提交绘制时,构造参数只给着色器,改在 onRender 回调里通过绘制上下文 ctx 逐帧组织数据并调用 ctx.draw()。
在着色器中声明以下 uniform 后,图层会逐帧自动赋值;未声明则不产生任何开销:
uniform mat4 u_mvpMatrix; // 图层局部坐标 -> 裁剪空间uniform float u_opacity; // 图层透明度uniform float u_zoom; // 当前缩放级别uniform float u_time; // 动画时间(秒)uniform vec2 u_resolution; // 画布尺寸(像素)uniform float u_pixelRatio; // 设备像素比uniform float u_unitsPerPixel; // 一个屏幕像素对应的局部坐标单位uniform vec2 u_center; // 地图中心uniform float u_heading; // 相机朝向(度)uniform float u_tilt; // 相机倾斜角(度)
给构造参数传入 indices 后,图层会自动改用 drawElements 索引绘制。下面用两个三角形拼出一个矩形,四个角各给一种颜色,片元阶段插值出渐变覆盖面。
// 逐顶点色的通用着色器,后续示例复用var vsVColor = `attribute vec2 a_pos;attribute vec4 a_color;uniform mat4 u_mvpMatrix;varying vec4 v_color;void main() {v_color = a_color;gl_Position = u_mvpMatrix * vec4(a_pos, 0.0, 1.0);}`;var fsVColor = `precision mediump float;uniform float u_opacity;varying vec4 v_color;void main() {gl_FragColor = vec4(v_color.rgb, v_color.a * u_opacity);}`;var corners = [[116.36, 39.90], [116.45, 39.90], [116.45, 39.94], [116.36, 39.94]];var layer = new BMap.WebGLCustomLayer({referCenter: CENTER,vertexShader: vsVColor,fragmentShader: fsVColor,attributes: {a_pos: {data: corners, type: 'lnglat'},a_color: {data: new Uint8Array([79, 140, 255, 170,107, 225, 255, 170,124, 255, 178, 170,255, 209, 102, 170]),size: 4,normalized: true}},indices: [0, 1, 2, 0, 2, 3], // 两个三角形拼成矩形,传了 indices 即走 drawElementsdraw: {mode: 'TRIANGLES'}});map.addLayer(layer);
对于每帧都在变化的几何,可在 onRender 回调中借助绘制上下文实时计算顶点。下面用 ctx.getUnitsPerPixel() 让扫描半径在屏幕上恒定为 150 像素,用 ctx.getTime() 驱动旋转,实现雷达扫描效果。
var layer = new BMap.WebGLCustomLayer({referCenter: CENTER,animation: true,vertexShader: vsVColor, // 沿用上文定义的着色器fragmentShader: fsVColor,onRender: function (ctx) {var radius = 150 * ctx.getUnitsPerPixel(); // 屏幕上恒定 150pxvar sweep = ctx.getTime() * 1.4; // 随时间旋转var n = 64;var pos = [0, 0]; // 扇心var col = [80, 230, 170, 0]; // 扇心透明for (var i = 0; i <= n; i++) {var a = sweep - i / n * 1.2;pos.push(Math.cos(a) * radius, Math.sin(a) * radius);col.push(80, 230, 170, Math.round(210 * (1 - i / n)));}ctx.setAttribute('a_pos', {data: new Float32Array(pos), size: 2});ctx.setAttribute('a_color', {data: new Uint8Array(col), size: 4, normalized: true});ctx.draw({mode: 'TRIANGLE_FAN'});}});map.addLayer(layer);
深度测试、混合、面剔除等全局 GL 状态必须通过 renderState 声明,切勿直接调用 gl.enable()、gl.blendFunc(),否则会破坏地图对状态的缓存,导致底图文字、图标、marker 出现难以定位的渲染异常。
blend 支持 normal(标准 alpha 混合,默认)、additive(颜色相加,仅在深色背景且图元互相重叠时才明显)、screen(滤色,浅色底图也能提亮)等。下面通过一层深色底衬叠加 additive 发光点演示效果:
// additive 需深色背景才看得出叠加,先铺一层深色底衬(zIndex 小,先画)var backdrop = new BMap.WebGLCustomLayer({referCenter: CENTER,zIndex: 1,vertexShader: vsVColor,fragmentShader: fsVColor,attributes: {a_pos: {data: [[116.34, 39.86], [116.47, 39.86], [116.47, 39.97], [116.34, 39.97]],type: 'lnglat'},a_color: {data: new Uint8Array([10, 14, 24, 200, 10, 14, 24, 200, 10, 14, 24, 200, 10, 14, 24, 200]),size: 4,normalized: true}},indices: [0, 1, 2, 0, 2, 3],renderState: {blend: 'normal'},draw: {mode: 'TRIANGLES'}});map.addLayer(backdrop);// 一圈互相重叠的发光点,additive 混合,重叠处颜色累积过曝var glow = new BMap.WebGLCustomLayer({referCenter: CENTER,zIndex: 2,animation: true,vertexShader: vertexShader, // 复用示例一的发光点着色器fragmentShader: fragmentShader,attributes: {a_pos: {data: spots, type: 'lnglat'},a_color: {data: new Uint8Array(colors), size: 4, normalized: true}},uniforms: {u_size: 130},renderState: {blend: 'additive'}, // 颜色相加draw: {mode: 'POINTS'}});map.addLayer(glow);
图层加入地图后,可通过 setVisible 控制显隐,通过 map.removeLayer 移除。
layer.setVisible(false); // 隐藏图层layer.setVisible(true); // 显示图层map.removeLayer(layer); // 从地图移除
上一篇
下一篇