prefácio
Um dia, o chefe veio até mim e me disse que recebemos um projeto do Water Conservancy Bureau e precisávamos fazer alguns efeitos climáticos, como chuvas, temporada de enchentes de rios, impacto de desastres de enchentes, etc. O que você acha? Ei, acho que é muito interessante, então vou começar a trabalhar nisso imediatamente.
Declaração de necessidades
Perceba o efeito da chuva no mapa, e a imagem deve ser a mais real possível, como as mudanças de céu, vento e nuvens e o cenário de chuva;
Combinado com a previsão do tempo local, a velocidade do vento, direção do vento, precipitação e outros parâmetros podem ser ajustados automaticamente.
análise de demanda
Cenário 1: Precipitação Global
Adiciona uma camada de plano de chuva 2D na frente da janela de visualização do usuário.
Vantagens: Basta utilizar a camada bidimensional, não há necessidade de sincronizar as coordenadas com o mapa, é relativamente simples de implementar, e a interface é global de vez.
Contras: bom apenas para visualização de determinados ângulos, sem mais personalização.
Opção 2: Chuva nas áreas locais
Especifique a faixa pluviométrica, ou seja, um espaço tridimensional, as coordenadas são sincronizadas com o mapa base do mapa, e a pluviosidade é realizada apenas no espaço.
Vantagens: As gotas de chuva que caem têm uma relação de distância, que está mais de acordo com a cena real; pode ser aplicada a vários níveis de zoom do mapa.
Desvantagens: São muitos os parâmetros a serem considerados. Por exemplo, o item faixa pluviométrica deve considerar a forma do espaço tridimensional, que pode ser cubo, cilindro ou extrusão poligonal, necessita da colaboração de camadas externas. Por exemplo, se chover, então Se a camada de nuvem da caixa do céu e o brilho da camada de construção são ajustados de acordo.
Ideias de implementação
Com base nos prós e contras acima, escolhi a segunda opção de desenvolvimento e minimizei os parâmetros de entrada. A faixa de influência da chuva foi inicialmente determinada como um cubo com o centro do mapa como centro de coordenadas, ignorando a influência da força do vento , e as gotas de chuva se moviam em queda livre.
A chuva é realizada por shaders personalizados, fazendo pleno uso dos recursos de computação paralela da GPU, acabei de encontrar um código de três demo escrito por um cara grande na Internet , altere o eixo de coordenadas (o eixo de coordenadas de espaço threejs, o eixo y é para cima, coordenada de espaço Gaode GLCustomLayer eixo z para cima) pode atingir diretamente o efeito mais básico. Aqui, para conveniência de demonstração, são adicionadas as linhas auxiliares do eixo de coordenadas e a faixa de influência.
1. Crie um intervalo de influência e crie uma Geometria da camada de chuva dentro do intervalo. A geometria é composta por 1000 planos em posições aleatórias dentro do intervalo de influência e esses planos são perpendiculares à superfície inferior do mapa;
2. Crie um material de gota de chuva. Pingos de chuva não são afetados pela luz. Aqui você pode usar o material MeshBasicMaterial mais básico, translúcido e adicionar uma imagem como textura;
3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;
- 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;
- 将图层叠加到地图3D场景中
基础代码实现
为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即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()
})
}
优化调整
修改场景效果
通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。
以下是配置数据结构,可供参考
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个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。
- 首先调整一下代码实际那一节步骤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的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转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;
}
待改进的地方
本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。
A razão para o problema é que o método "fazer com que todos os rostos fiquem sempre voltados para a câmera" no sombreador de material manterá o estado de inclinação da partícula inalterado. A solução para esse problema deve ser ajustar esse método. No entanto, como um canalha, ainda não descobri e descobri que o final do projeto de visualização é todo matemática Orz.
Links Relacionados
1. Versão avançada do THREE.JS Rain, a superfície gira apenas o eixo Y em direção à câmera
2. Código de demonstração online DEMO