远算前端团队,主要分享一些WEBGL、Three.js等三维技术,团队主要做数字孪生相关的数字大屏,项目一般涉及仿真、后处理,以及三维可视化。
欢迎关注公众号 远算前端团队
项目价值
去年郑州暴雨导致整个城市被淹,地铁倒灌、汽车被淹,多少人因此而牺牲。城市内涝预警系统,是根据天气预报,以及真实天气,通过仿真系统推演出哪里会发生内涝,从而通知人民转移。
视频效果:哔哩哔哩
技术实现
三维地图效果实现
首先获取所需城市建筑高程数据,通过Mapbox Studio先创建Tileset,点击Select a file 按钮选择文件(支持文件格式及文件大小可参照这里)
再通过Mapbox Studio创建Styles, 因为原图层建筑高程数据不可用,接下来创建新的图层将刚才创建的Tileset导入;新图层有Select data,Style连个选项,下面着重介绍这两项所需配置: Select data页签中分别设置Source、Type、Filter:
- Source选择刚才上传的 Tileset;
- Type数据展示类型Fill extrusion;
- 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');// 加入自定义图层
复制代码
注意点
- Mapbox不同图层调整可通过moveLayer API 来自定义图层顺序;
- 如果想要调整water图层至非顶层外的其他图层,可以参考这个issus;
- 为了减小几何模型文件大小,目前position数据只有x,y两个方向数据,z方向前端自己组装;
欢迎关注公众号 远算前端团队