在高德地图上实现城市扫光效果

4,176 阅读6分钟

在高德地图上实现城市扫光效果

前言

在设计和实现数据可视化大屏时,有一个常见的需求就是要实现一个周期性脉冲效果,让光墙定时掠过城市的每一个建筑。这种效果不仅能够吸引人们的眼球,还能够增强可视化大屏的视觉冲击力和信息传递效果。实现这个效果需要运用到WebGL和数学知识,下面我们浅谈一下实现过程。

Honeycam_2023-04-28_15-59-25.gif

需求说明

  1. 支持将GeoJSON格式的建筑面数据,转换为地图上的简易建筑模型,数据量10万以内能够流畅运行
  2. 建筑外观可配置,即支持在模型面上贴相同的纹理,顶部与侧面分开处理
  3. 扫光效果为平面环状,可以配置初始位置、最大尺寸、环带宽度、颜色、扫光速度

实现思路

1.GeoJSON格式的平面转换成挤压几何体是常规操作了,很多可视化图层都会用到,实现原理就是通过面的多边形坐标和已设定高度,生成对应的侧面和顶部面,相关代码可以看之前写的文章在高德地图实现可定制立体区块。如下图所示,每个建筑都是这样处理,需要注意的是有些建筑可以能是个组合型的几何体,建筑面积由多个平面构成,需要与单体建筑区分处理。 %E6%9C%AA%E6%A0%87%E9%A2%98-0.jpg

2.生成白模后贴纹理图,可以侧面和顶面分开使用材质;也可以仅使用一个材质,在着色器集体实现时对顶面和侧面分开贴图。 %E6%9C%AA%E6%A0%87%E9%A2%98-1.jpg

3.处理扫光效果,在片元着色器上实现,针对每个片元,判断它是否处于圆环波动带上。如果是,则取圆环和建筑纹理的叠加状态;如果不是,则取建筑本身的纹理状态。

Honeycam_2023-04-28_13-32-24.gif

4.逐帧变化圆环的半径,就形成了动画效果 Honeycam_2023-04-28_13-58-51.gif

名词解释

法线:在计算机图形学中,法线(Normal)通常指的是表面法线,是指垂直于表面的单位向量,用于描述一个几何体或几何形状的表面方向。在渲染图形时,表面法线对于光照和阴影的计算非常重要,因为它确定了每个点表面的朝向,从而影响表面的亮度和颜色。

顶点着色器:顶点着色器(Vertex Shader)是在图形渲染管线中的一个可编程的阶段,用于处理输入的顶点数据并输出变换后的顶点位置和其他属性信息。顶点着色器通常用于执行一些基本的几何变换,如旋转、缩放、平移和投影,以及计算每个顶点的法向量、纹理坐标和颜色等信息。

片元着色器:片元着色器(Fragment Shader)是在图形渲染管线中的一个可编程的阶段,用于计算每个像素的最终颜色值。它接收由顶点着色器传递过来的数据,如纹理坐标、法向量、顶点颜色等,并根据这些数据计算每个像素的颜色值。

代码实现

1.创建几何体

createPolygon () {
    let sideGeometryArr = []
    let topGeometryArr = []

    this._data.forEach((item, index) => {
      const { geometry, properties } = item
      const { type, coordinates } = geometry

      // 对象为单体多边形
      if (type === 'Polygon') {
        const { sides, tops } = this._createPolygon(coordinates, properties)
        sideGeometryArr = sides
        topGeometryArr = tops
      }
			//...独享为多体多边形
    })

    // 合并侧边
    const mesh0 = new THREE.Mesh(mergeBufferGeometries(sideGeometryArr, false), this._mt)
    this.scene.add(mesh0)

    // 合并顶部
    const mesh1 = new THREE.Mesh(mergeBufferGeometries(topGeometryArr, false), this._mt)
    this.scene.add(mesh1)
  }

/**
   * 根据路径绘制单个多边几何体
   * @param paths
   * @param properties
   * @returns {{tops: Array, sides: Array}}
   * @private
   */
  _createPolygon (paths = [], properties) {
    const sides = []; const tops = []
    paths.forEach(path => {
      if (this._limitCount >= this._limitMax) {
        return
      }
      // 绘制侧边几何体,这个之前文章讲过了不展开
      const side = this.drawSide(path, properties)
      sides.push(side)

      // 绘制顶部几何体,这个之前文章讲过了不展开
      const top = this.drawTop(path, properties)
      tops.push(top)

      this._limitCount++
    })
    return { sides, tops }
  }

2.创建自定义材质,把需要配置的参数设定好

_uniforms = {
  // 建筑顶部贴图
  topMap: {
    value: null
  },
  // 建筑侧边贴图
  sideMap: {
    value: null
  },
  innerCircleWidth: {
    value: 0
  },
  // 波动环宽度
  circleWidth: {
    value: null
  },
  // 圆环颜色
  color: {
    value: null
  },
  // 圆环透明度
  opacity: {
    value: 0.8
  },
  // 环圆心位置
  center: {
    value: null
  }
}

this._mt = new THREE.ShaderMaterial({
  uniforms:_uniforms,
  vertexShader,
  fragmentShader,
  side: THREE.DoubleSide,
  depthTest: true,
  transparent: true
})

3.本文的重点来了!着色器的实现。

在片元着色器主函数中,首先计算当前片元与中心点center的距离,判断是否在圆环范围内,如果是则取圆环和纹理的叠加态,否则取纹理;

其次判断这里的纹理是取顶面纹理还是侧面纹理,则根据当前点的法线方向做判断,法线朝上表示当前所在面为顶面,否则为侧面。

const shader ={
	//顶点着色器
	vertexShader: `
      varying vec2 vUv;
      varying vec3 v_position;
      varying vec3 vNormal; //点法线向量
      void main() {
          vUv = uv;
          vNormal = normal;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          v_position = vec3(modelMatrix * vec4(position, 1.0));
      }
  `,
   //片元着色器
  fragmentShader: `   
     varying vec2 vUv;
     varying vec3 v_position;
     varying vec3 vNormal;

     uniform float innerCircleWidth;
     uniform float circleWidth;
     uniform float opacity;
     uniform vec3 center;

     uniform vec3 color;
     uniform sampler2D topMap;
     uniform sampler2D sideMap;

     void main() {
       float dis = length(v_position - center);

       // 与波动有交集
       if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
          float r = (dis - innerCircleWidth) / circleWidth;
          if (vNormal.z > 0.0) {
					  // 位于顶部
            gl_FragColor = mix(texture2D(topMap, vUv), vec4(color, opacity), r); 
          }else{
					  // 位于侧面
            gl_FragColor = mix(texture2D(sideMap, vUv), vec4(color, opacity), r); 
          }          
       }else {         
          if (vNormal.z > 0.0) {
            gl_FragColor = texture2D( topMap, vUv);
          }else{
            gl_FragColor = texture2D( sideMap, vUv);
          }
       }  
     }
  `
}

4.在逐帧函数中修改圆环宽度


animate () {
  if (this.update) {
    this.update()
  }
  requestAnimationFrame(() => {
    this.animate()
  })
}

update () {
    if (!this._isAnimate) {
      return
    }

    const { innerCircleWidth, circleWidth, opacity } = this._uniforms
    // 每帧半径增加20
    innerCircleWidth.value += 20
    // 内半径超过最大值时重置
    **if (innerCircleWidth.value > this._conf.maxRadius) {
      innerCircleWidth.value = 0
    }
}

注意点

  1. 这里有个非常重要的技巧,生成模型后需要将所有的几何体做一次合并,这样做的好处是极大提升性能,如果不合并几万个几何体会卡成狗,注意看下图左上角的帧率性能指标。合并后的模型会丢失独立信息,不过可以通其他方法补偿。

Honeycam_2023-04-28_14-03-49.gif

合并前卡成狗

Honeycam_2023-04-28_11-56-47.gif

合并后纵享丝滑

2.从上面代码实现第二步可以看到事实上我把所有几何体合并成了2个,一个是总体侧边,一个是总体顶部,这样做只是为了方便以后直接替换顶部材质。具体实现时也可以有其他合并方法。

还可以做哪些改进

1.增加鼠标交互,选中的建筑高亮。

这一步还没实现,鼠标选中对象是单个几何体,有自己的材质,则很简单,将材质中pick属性设置为true,则表示鼠标选中,材质变成高亮状态;然而为了性能考虑我已经将几万个几何体合并了,需要另外想办法,基本原理我猜想是把鼠标坐标EventPoint传入到着色其中,判断几何体顶点和事件点EventPoint的距离,如果在约定范围内则将对应的所有片元颜色高亮,还在调整中。有没有大佬分享下经验。

2.建筑楼面的材质,可以使用着色器写得更真实,比如antvL7中的图层,以阵列的方式绘制了矩形并赋予指定的颜色作为纹理,需要更多的GLSL和数学知识,不过有了chatGPT的加持这一块难度会极大降低。

Honeycam_2023-04-28_14-51-29.gif

相关链接

在高德地图实现可定制立体区块

juejin.cn/post/718180…

用three.js实现炫酷的城市扫光效果

blog.csdn.net/zhgu1992/ar…

three.js实战-shader实现antv L7城市扫光效果

blog.csdn.net/weixin_4677…