基于Three.js城市内涝实时预警系统

远算前端团队,主要分享一些WEBGL、Three.js等三维技术,团队主要做数字孪生相关的数字大屏,项目一般涉及仿真、后处理,以及三维可视化。

欢迎关注公众号 远算前端团队

项目价值

去年郑州暴雨导致整个城市被淹,地铁倒灌、汽车被淹,多少人因此而牺牲。城市内涝预警系统,是根据天气预报,以及真实天气,通过仿真系统推演出哪里会发生内涝,从而通知人民转移。

视频效果:哔哩哔哩

技术实现

三维地图效果实现

首先获取所需城市建筑高程数据,通过Mapbox Studio先创建Tileset,点击Select a file 按钮选择文件(支持文件格式及文件大小可参照这里)

再通过Mapbox Studio创建Styles, 因为原图层建筑高程数据不可用,接下来创建新的图层将刚才创建的Tileset导入;新图层有Select data,Style连个选项,下面着重介绍这两项所需配置: Select data页签中分别设置Source、Type、Filter:

  1. Source选择刚才上传的 Tileset;
  2. Type数据展示类型Fill extrusion;
  3. Filter会展示选取数据源下的所有物理量,我们选择height及其所对应的值即可。

Style页签中主要介绍一下Height和Base height其他可自行尝试,一般Base height设置为0,这样紧贴地面,Height可设置根据不同的缩放层级展示建筑高度;如果想改变颜色和建筑透明度更改对应值即可。

设置后展示效果如下:

最终效果如下:

ThreeJs 云图效果实现

云图实现效果主要依托于着色器材质(ShaderMaterial);使用自定义shader渲染的材质将将许多对象组合成单个BufferGeometry,依次执行渲染。 下面介绍重点代码:

初始参数

  objWaterParams: {
    positionX: 852.5, // 849,
    positionY: 0,
    positionZ: -480,
    scale: 0.82,
    amplitude: 1,
    amplitudeAfterLoaded: 0, // 15,
    normalAmplitude: 15,
    // 播放控制
    bPlay: true,
    bLoop: true,
    index: 0, // GUI控制当前帧
    interval: 0.2, // 0.1, // 播放时帧之间的间隔(秒)
    bVisible: true,
    nFrames: 20, // 加载帧数 
    renderDataType: 'WATER_DEPTH',
    // 最大水深(对应色带最大值颜色)
    maxValue: 3.0,
    // 水系渲染成单色的话采用的颜色 
    waterFixColor: 0x63cf29,
    waterColorC1: 0xe0ffff,
    waterColorOpacity1: 0.1,
    waterColorC2: 0xe0ffff,
    waterColorOpacity2: 0.5,
    waterColorC3: 0x87ceeb,
    waterColorOpacity3: 0.8,
    waterColorC4: 0x1e90ff,
    waterColorOpacity4: 0.85,
    waterColorC5: 0x0000cd,
    waterColorOpacity5: 0.9,
    // 光,影响水的着色
    light1PosX: 48,
    light1PosY: 35,
    light1PosZ: 50,
    light1Color: 0xffffff, 
  },
复制代码

加载几何数据

  /**
   * 设置仿真结果的几何体
   * @param geometry THREE.BufferGeometry,包含几何数据(position和index)
   */
  setGeometry: function (geometry) {
    // 加载"平面"网格 + shaderMaterial,动画时会逐帧更新
    this.geo = geometry;
    // geo.computeBoundingBox()
    this.geo.center();

    console.log('setGeometry, this.geo =', this.geo);
    this.mesh = new THREE.Mesh(this.geo, this.shaderMaterial);
    this.mesh.rotation.x = -Math.PI / 2;
    this.mesh.position.set(
      this.objWaterParams.positionX,
      this.objWaterParams.positionY,
      this.objWaterParams.positionZ,
    );
    this.mesh.scale.set(
      this.objWaterParams.scale,
      this.objWaterParams.scale,
      this.objWaterParams.scale,
    );

    // console.log('', geometry.getAttribute('minValue'))
    this.scene.add(this.mesh);
  }
复制代码

加载物理量数据

  /**
   * 设置仿真结果的物理量数据。
   * 确保先调用 setGeometry() 创建几何体。
   * @param geometry THREE.BufferGeometry,包含物理量数据,及每个物理量的最大最小值
   * @param index number,指定该geometry是哪一帧
   */
  setAttributes: function (geometry, index) {
    let attr_WaterDepth = geometry.getAttribute('WATER_DEPTH');
    let strIndex = this.padding4(index, 8);
    this.aryAttribute_WaterDepth.set(strIndex, attr_WaterDepth); 
    // 统计物理量的最大最小值
    for (let attributeName in geometry.attributes) {
      if (
        Object.prototype.hasOwnProperty.call(geometry.attributes, attributeName)
      ) {
        let attribute = geometry.attributes[attributeName];
        if (attribute.itemSize === 1) {
          let objScalar = this.mapMinMax.get(attributeName);
          if (!objScalar) {
            objScalar = {};
            objScalar.minValue = geometry.userData[attributeName].minValue;
            objScalar.maxValue = geometry.userData[attributeName].maxValue;
            this.mapMinMax.set(attributeName, objScalar);
          }
          if (geometry.userData[attributeName].minValue < objScalar.minValue) {
            objScalar.minValue = geometry.userData[attributeName].minValue;
          }

          if (geometry.userData[attributeName].maxValue > objScalar.maxValue) {
            objScalar.maxValue = geometry.userData[attributeName].maxValue;
          }
        }
      }
    }
复制代码

图层绘制到GL上下文期间被调用

  render: function (gl, matrix) { 
    if (this.objWaterParams.bVisible && this.objWaterParams.bPlay) {
      this.geo.setAttribute('normal', this.aryAttribute_Normal.get(strIndex));
    }
    const rotationX = new THREE.Matrix4().makeRotationAxis(
      new THREE.Vector3(1, 0, 0),
      this.modelTransform.rotateX,
    );
    const rotationY = new THREE.Matrix4().makeRotationAxis(
      new THREE.Vector3(0, 1, 0),
      this.modelTransform.rotateY,
    );
    const rotationZ = new THREE.Matrix4().makeRotationAxis(
      new THREE.Vector3(0, 0, 1),
      this.modelTransform.rotateZ,
    );

    const m = new THREE.Matrix4().fromArray(matrix);
    const l = new THREE.Matrix4()
      .makeTranslation(
        this.modelTransform.translateX,
        this.modelTransform.translateY,
        this.modelTransform.translateZ,
      )
      .scale(
        new THREE.Vector3(
          this.modelTransform.scale,
          -this.modelTransform.scale,
          this.modelTransform.scale,
        ),
      )
      .multiply(rotationX)
      .multiply(rotationY)
      .multiply(rotationZ);

    this.camera.projectionMatrix = m.multiply(l);
    this.renderer.resetState();
    this.renderer.render(this.scene, this.camera);
    this.map.triggerRepaint();
  }
复制代码

三维地图和ThreeJs效果融合

不同时刻水深效果通过render函数来切换不同的物理量数据来实现

   SimAnimateLayer.init(); 
   SimAnimateLayer.setGeometry(geometry); //设置几何模型
   SimAnimateLayer.setAttributes(geometryData, key); //设置物理量数据
   state.map.addLayer(store.geoemtry, 'water');// 加入自定义图层
复制代码

飞书20220618-152615.png

注意点

  1. Mapbox不同图层调整可通过moveLayer API 来自定义图层顺序;
  2. 如果想要调整water图层至非顶层外的其他图层,可以参考这个issus;
  3. 为了减小几何模型文件大小,目前position数据只有x,y两个方向数据,z方向前端自己组装;

欢迎关注公众号 远算前端团队

猜你喜欢

转载自juejin.im/post/7111090363627995143
今日推荐