Realice la capa de lluvia en el mapa de Gaode

prefacio

Un día, el jefe se acercó a mí y me dijo que recibimos un proyecto de la Oficina de Conservación del Agua y que necesitábamos hacer algunos efectos climáticos, como lluvia, temporada de inundaciones de ríos, impacto de desastres de inundaciones, etc. ¿Qué piensas? Oye, creo que es muy interesante, así que empezaré a trabajar en ello de inmediato.

Declaración de necesidades

Tenga en cuenta el efecto de la lluvia en el mapa, y la imagen debe ser lo más real posible, como los cambios en el cielo, el viento y las nubes y la escena de la lluvia;

En combinación con el pronóstico del tiempo local, la velocidad del viento, la dirección del viento, la lluvia y otros parámetros se pueden ajustar automáticamente.

análisis de la demanda

Escenario 1: lluvia global

Agrega una capa de plano de lluvia 2D frente a la ventana gráfica del usuario.

Ventajas: solo use la capa bidimensional, no es necesario sincronizar las coordenadas con el mapa, es relativamente simple de implementar y la interfaz es global de una vez por todas.

Contras: solo es bueno para ver desde ciertos ángulos, no más personalización.

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

Opción 2: Lluvia en áreas locales

Especifique el rango de lluvia, es decir, un espacio tridimensional, las coordenadas se sincronizan con el mapa base del mapa y la lluvia solo se realiza en el espacio.

Ventajas: Las gotas de lluvia que caen tienen una relación de distancia, que está más en línea con la escena real, se puede aplicar a varios niveles de zoom del mapa.

Desventajas: Hay muchos parámetros a considerar, por ejemplo, el elemento rango de lluvia debe considerar la forma del espacio tridimensional, que puede ser un cubo, un cilindro o una extrusión poligonal, necesita la cooperación de capas externas, por ejemplo, si llueve, entonces si la capa de nubes de la caja del cielo y el brillo de la capa del edificio se ajustan en consecuencia.

Honeycam_2023-06-16_11-20-08.gif

Ideas de implementación

Con base en las compensaciones anteriores, elegí la segunda opción para el desarrollo y minimicé los parámetros de entrada. El rango de influencia de la lluvia se establece inicialmente como un cubo con el centro del mapa como centro de coordenadas, ignorando la influencia de la fuerza del viento. , y las gotas de lluvia se mueven en caída libre.

La lluvia se realiza mediante sombreadores personalizados, aprovechando al máximo el poder de cómputo paralelo de la GPU. Acabo de encontrar el código de demostración tres escrito por un tipo grande en Internet , y cambié el eje de coordenadas (el eje y del espacio threejs está mirando hacia arriba, y el espacio GLCustomLayer de Gaode GLCustomLayer Coordinate eje z hacia arriba) puede lograr directamente el efecto más básico. Aquí, por conveniencia de demostración, se agregan las líneas auxiliares del eje de coordenadas y el rango de influencia.

1. Cree un rango de influencia y cree una Geometría de la capa de lluvia dentro del rango. La geometría se compone de 1000 planos en posiciones aleatorias dentro del rango de influencia, y estos planos son perpendiculares a la superficie inferior del mapa;

Cámara de miel_2023-06-24_15-40-31.gif

2. Crea un material de gota de lluvia. Las gotas de lluvia no se ven afectadas por la luz. Aquí puedes usar el material MeshBasicMaterial más básico, translúcido y agregar una imagen como textura;

Cámara de miel_2023-06-24_15-50-32.gif

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

Honeycam_2023-06-24_16-01-39.gif

  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;

Honeycam_2023-06-24_16-16-52.gif

  1. 将图层叠加到地图3D场景中

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.bound
  const 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()
  // 设置几何体的顶点、法线、UV
  const 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.x
    pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y
    pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z

    const height = (box.max.z - box.min.z) * scale / 15
    const 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 = material

  return 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 % 1

  if (_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()
  })
}

优化调整

修改场景效果

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

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, // 直线降落速度倍率,默认值1
      scale: 0.2, // 粒子尺寸倍率,默认1
      opacity: 0.5, // 粒子透明度,默认0.5
      count: 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轴的位移距离是200
      float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200
			...
			// 生成变换矩阵
      vec3 transformed = vec3( foot.x, y, z );
  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。

Sin título.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;
}

待改进的地方

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

Cámara de miel_2023-06-24_21-06-51.gif

El motivo del problema es que el método de "mantener todas las caras siempre mirando a la cámara" en el sombreador de materiales mantendrá el estado de inclinación de la partícula sin cambios. La solución a este problema debería ser ajustar este método. Sin embargo, como escoria, todavía no lo he descubierto, y resulta que el final del proyecto de visualización son todas las matemáticas Orz.

enlaces relacionados

1. Versión avanzada de THREE.JS Rain, la superficie solo gira el eje Y hacia la cámara

www.wjceo.com/blog/tresj…

2. Código de demostración en línea DEMO

jsfiddle.net/gyratesky/5…

Supongo que te gusta

Origin juejin.im/post/7248180884766015546
Recomendado
Clasificación