在高德地图中实现降雨图层

2023-11-11 15:11

本文主要是介绍在高德地图中实现降雨图层,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。

需求说明

在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合;

可以结合当地天气预报情况,自动调节风速、风向、降雨量等参数。

需求分析

方案一:全局降雨

在用户视口面前加一层二维的降雨平面层。

优点:只管二维图层就行了,不需要与地图同步坐标,实现起来比较简单,界面是全局的一劳永逸。

缺点:只适合从某些角度观看,没法再做更多定制了。

a25e314973ee788bdad6431577fa284c.jpeg
Honeycam_2023-06-16_11-10-37.gif

方案二:局部地区降雨

指定降雨范围,即一个三维空间,坐标与地图底图同步,仅在空间内实现降雨。

优点:降落的雨滴有远近关系,比较符合现实场景;可适用各种地图缩放程度。

缺点:需要考虑的参数比较多,比如降雨范围一项就必须考虑这个三维空间是什么形状,可能是立方体、圆柱体或者多边形挤压体;需要外部图层的配合,比如说下雨了,那么天空盒子的云层、建筑图层的明度是否跟着调整。

8f576bd59a0195a19a752f0cc2632ae1.jpeg
Honeycam_2023-06-16_11-20-08.gif

实现思路

根据上面利弊权衡,我选择了方案二进行开发,并尽量减少输入参数,降雨影响范围初步定为以地图中心为坐标中心的立方体,忽略风力影响,雨滴采用自由落体方式运动。

降雨采用自定义着色器的方式实现,充分利用GPU并行计算能力,刚好在网上搜到一位大佬写的three演示代码[1],改一下坐标轴(threejs空间坐标轴y轴朝上,高德GLCustomLayer空间坐标z轴朝上)就可以直接实现最基础的效果。这里为了演示方便增加坐标轴和影响范围的辅助线。

1.创建影响范围,并在该范围内创建降雨层的几何体Geometry,该几何体的构成就是在影响范围内随机位置的1000个平面,这些平面与地图底面垂直;

33c7980662ef821be95daba4a9948ce8.jpeg
Honeycam_2023-06-24_15-40-31.gif

2.创建雨滴材质,雨滴不受光照影响,这里使用最基础的MeshBasicMaterial材质即可,半透明化且加上一张图片作为纹理;

932bfe2ede9fb43f85df06b341804628.jpeg
Honeycam_2023-06-24_15-50-32.gif

3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;

6cd53a9e112ac9cb7526e413b60f44d9.jpeg
Honeycam_2023-06-24_16-01-39.gif
  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;

7ae68686467ff50dd4c990cfef117035.jpeg
Honeycam_2023-06-24_16-16-52.gif
  1. 将图层叠加到地图3D场景中

1db478cf6b358bb9f00a32b89cc77814.jpeg
Honeycam_2023-06-24_16-28-46.gif

基础代码实现

为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即z轴朝上,three.js默认空间坐标系是y轴朝上。我把three.js示例代码演示放到文末链接中。

1.创建影响范围,并在该范围内创建降雨层的几何体Geometry

createGeometry () {// 影响范围:只需要设定好立方体的size [width/2, depth/2, height/2]// const { count, scale, ratio } = this._conf.particleStyle// 立方体的size [width/2, depth/2, height/2]const { size } = this._conf.boundconst box = new THREE.Box3(new THREE.Vector3(-size[0], -size[1], 0),new THREE.Vector3(size[0], size[1], size[2]))const geometry = new THREE.BufferGeometry()// 设置几何体的顶点、法线、UVconst vertices = []const normals = []const uvs = []const indices = []// 在影响范围内随机位置创建粒子for (let i = 0; i < count; i++) {const pos = new THREE.Vector3()pos.x = Math.random() * (box.max.x - box.min.x) + box.min.xpos.y = Math.random() * (box.max.y - box.min.y) + box.min.ypos.z = Math.random() * (box.max.z - box.min.z) + box.min.zconst height = (box.max.z - box.min.z) * scale / 15const width = height * ratio// 创建当前粒子的顶点坐标const rect = [pos.x + width,pos.y,pos.z + height / 2,pos.x - width,pos.y,pos.z + height / 2,pos.x - width,pos.y,pos.z - height / 2,pos.x + width,pos.y,pos.z - height / 2]vertices.push(...rect)normals.push(pos.x,pos.y,pos.z,pos.x,pos.y,pos.z,pos.x,pos.y,pos.z,pos.x,pos.y,pos.z)uvs.push(1, 1, 0, 1, 0, 0, 1, 0)indices.push(i * 4 + 0,i * 4 + 1,i * 4 + 2,i * 4 + 0,i * 4 + 2,i * 4 + 3)}// 所有顶点的位置geometry.setAttribute('position',new THREE.BufferAttribute(new Float32Array(vertices), 3))// 法线信息geometry.setAttribute('normal',new THREE.BufferAttribute(new Float32Array(normals), 3))// 设置UV属性与顶点顺序一致geometry.setAttribute('uv',new THREE.BufferAttribute(new Float32Array(uvs), 2))// 设置基本单元的顶点顺序geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1))return geometry
}

2.创建材质

createMaterial () {// 粒子透明度、贴图地址const { opacity, textureUrl } = this._conf.particleStyle// 实例化基础材质const material = new THREE.MeshBasicMaterial({transparent: true,opacity,alphaMap: new THREE.TextureLoader().load(textureUrl),map: new THREE.TextureLoader().load(textureUrl),depthWrite: false,side: THREE.DoubleSide})// 降落起点高度const top = this._conf.bound.size[2]material.onBeforeCompile = function (shader, renderer) {const getFoot = `uniform float top; // 天花板高度uniform float bottom; // 地面高度uniform float time; // 时间轴进度[0,1]#include <common>float angle(float x, float y){return atan(y, x);}// 让所有面始终朝向相机vec2 getFoot(vec2 camera,vec2 normal,vec2 pos){           vec2 position;//  计算法向量到点的距离float distanceLen = distance(pos, normal);// 计算相机位置与法向量之间的夹角float a = angle(camera.x - normal.x, camera.y - normal.y);// 根据点的位置和法向量的位置调整90度 pos.x > normal.x ? a -= 0.785 : a += 0.785; // 计算投影值position.x = cos(a) * distanceLen;position.y = sin(a) * distanceLen;return position + normal;}`const begin_vertex = `vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.y),  vec2(normal.x, normal.y), vec2(position.x, position.y));float height = top - bottom;// 计算目标当前高度float z = normal.z - bottom - height * time;// 落地后重新开始,保持运动循环z = z + (z < 0.0 ? height : 0.0);// 利用自由落体公式计算目标高度float ratio = (1.0 - z / height) * (1.0 - z / height);z = height * (1.0 - ratio);        // 调整坐标参考值z += bottom;z += position.z - normal.z;// 生成变换矩阵vec3 transformed = vec3( foot.x, foot.y, z );`shader.vertexShader = shader.vertexShader.replace('#include <common>',getFoot)shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',begin_vertex)// 设置着色器参数的初始值shader.uniforms.cameraPosition = { value: new THREE.Vector3(0, 0, 0) }shader.uniforms.top = { value: top }shader.uniforms.bottom = { value: 0 }shader.uniforms.time = { value: 0 }material.uniforms = shader.uniforms}this._material = materialreturn material
}

3.创建模型

createScope () {const material = this.createMaterial()const geometry = this.createGeometry()const mesh = new THREE.Mesh(geometry, material)this.scene.add(mesh)// 便于调试,显示轮廓// const box1 = new THREE.BoxHelper(mesh, 0xffff00)// this.scene.add(box1)}

4.更新参数

// 该对象用于跟踪时间
_clock = new THREE.Clock()update () {const { _conf, _time, _clock, _material, camera } = this//  调整时间轴进度,_time都值在[0,1]内不断递增循环//  particleStyle.speed为降落速度倍率,默认值1//  _clock.getElapsedTime() 为获取自时钟启动后的秒数this._time = _clock.getElapsedTime() * _conf.particleStyle.speed / 2 % 1if (_material.uniforms) {// 更新镜头位置_material.uniforms.cameraPosition.value = camera.position// 更新进度_material.uniforms.time.value = _time}
}animate (time) {if (this.update) {this.update(time)}if (this.map) {// 叠加地图时才需要this.map.render()}requestAnimationFrame(() => {this.animate()})
}

优化调整

修改场景效果

通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。

285c852fd1bb818066051c450f016a3e.jpeg
Honeycam_2023-06-24_17-00-11.gif

以下是配置数据结构,可供参考

const layer = new ParticleLayer({map: getMap(),center: mapConf.center,zooms: [4, 30],bound: {type: 'cube',size: [500, 500, 500]},particleStyle: {textureUrl: './static/texture/snowflake.png', //粒子贴图ratio: 0.9, //粒子宽高比,雨滴是长条形,雪花接近方形speed: 0.04, // 直线降落速度倍率,默认值1scale: 0.2, // 粒子尺寸倍率,默认1opacity: 0.5, // 粒子透明度,默认0.5count: 1000 // 粒子数量,默认值10000}})

添加风力影响

要实现该效果需要添加2个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。

  1. 首先调整一下代码实际那一节步骤2运动的相关代码

const begin_vertex = `...// 利用自由落体公式计算目标高度float ratio = (1.0 - z / height) * (1.0 - z / height);z = height * (1.0 - ratio);// 增加了下面这几行float x = foot.x+ 200.0 * ratio; // 粒子最终在x轴的位移距离是200float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200...// 生成变换矩阵vec3 transformed = vec3( foot.x, y, z );
  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。

468064a48f8ec668ca567777f43519df.jpeg
Untitled.png

我们调整调整一下代码实际那一节步骤1的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转a角度。

本示例旋转轴(x, y, 1)与z轴(0,0,1)平行,这里有个技巧,我们在做平面绕轴旋转的时候先把平面从初始位置orgPos移到坐标原点,绕着z轴旋转后再移回orgPos,会让计算过程简单很多。

// 创建当前粒子的顶点坐标
const rect = [pos.x + width,pos.y,pos.z + height / 2,pos.x - width,pos.y,pos.z + height / 2,pos.x - width,pos.y,pos.z - height / 2,pos.x + width,pos.y,pos.z - height / 2
]// 定义旋转轴
const axis = new THREE.Vector3(0, 0, 1).normalize();
//定义旋转角度
const angle = Math.PI / 6;
// 创建旋转矩阵
const rotationMatrix = new THREE.Matrix4().makeRotationAxis(axis, angle);for(let index =0; index< rect.length; index +=3 ){const vec = new THREE.Vector3(rect[index], rect[index + 1], rect[index + 2]);//移动到中心点vec.sub(new THREE.Vector3(pos.x, pos.y,pos.z))//绕轴旋转vec.applyMatrix4(rotationMatrix);//移动到原位vec.add(new THREE.Vector3(pos.x, pos.y, pos.z))rect[index] = vec.x;rect[index + 1] = vec.y;rect[index + 2] = vec.z;
}

待改进的地方

本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。

3bcd6784efd4828f4c66e0d6d1b9b965.jpeg
Honeycam_2023-06-24_21-06-51.gif

问题的原因是材质着色器中的“让所有面始终朝向相机”方法会一直维持粒子的倾斜状态不变,解决这个问题应该是调整这个方法就可以了。然而作为学渣的我还没摸索出来,果然可视化工程的尽头全是数学Orz。

相关链接

1.THREE.JS下雨进阶版,面只旋转Y轴朝向相机

www.wjceo.com/blog/threej…[2]

2.演示代码在线DEMO

jsfiddle.net/gyratesky/5…[3]

参考资料

[1]

https://www.wjceo.com/blog/threejs2/2019-02-28/185.html

[2]

https://www.wjceo.com/blog/threejs2/2019-02-28/185.html

[3]

https://jsfiddle.net/gyratesky/5em3rckq/17/

关于本文

作者:gyratesky

https://juejin.cn/post/7248180884766015546

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持!

 
最后不要忘了点赞呦!

这篇关于在高德地图中实现降雨图层的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/390940

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import