threejs 记录风的样子
风的样子还是非常漂亮的,很多网站都有用风力图来展示风向和风速,一直觉得还挺有意思的,之前研究过一阵子,不过后来放下就忘记了,最近又开始捣鼓threejs,又想起来弄这个来了,老样子,先上效果图。
项目基于vue+threejs。
思路
大体思路如下:
- 在屏幕上随机生成很多的点。
- 根据风力场数据更新点的位置。
- 风速快的地方标成红色,要是弄成颜色映射就更好了,下期再优化一下。
- 新建一个纹理显示在后边记录风的行迹。
实现
1. 随机生成点
首先,我们需要生成很多的点,以便于后续计算风力。
js
var colorData = []
var particleCount = 10000;
for (var i = 0; i < particleCount; i++) {
var x = THREE.MathUtils.randFloatSpread(window.innerWidth );
var y = THREE.MathUtils.randFloatSpread(window.innerHeight );
var z = 0;
positionVertices.push(x, y, z);
colorData.push(1,1,1)
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionVertices, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorData, 3));
var material = new THREE.PointsMaterial({vertexColors: true})
var points = new THREE.Points(geometry, material);
scene.add(points);
其中的 vertexColors 是必须要设置的,意思是使用顶点的颜色,如果不设置的话就无法修改点的颜色。
2. 根据风力场数据更新点的位置
接下来,我们需要根据风力场数据更新点的位置。并且根据风速来标记成红色。
风场数据用的 Echart 例子的,不过我也不太了解风场的坐标关系,就一股脑给画上去了,以后再研究一下数据代表什么意思吧,那样才能把点画到正确的地方。
js
function animate() {
requestAnimationFrame(animate);
var positions = points.geometry.attributes.position.array;
var colors = points.geometry.attributes.color.array;
for (var i = 0; i < positions.length; i += 3) {
// 随机删除一些点,避免都扎到一堆里边去
if (Math.random() < 0.01) {
let x = THREE.MathUtils.randFloatSpread(window.innerWidth );
let y = THREE.MathUtils.randFloatSpread(window.innerHeight );
positions[i] = x
positions[i + 1] = y
continue
}
let xratio = wind.nx / window.innerWidth;
let yration = wind.ny / window.innerHeight;
let x = parseInt((positions[i] + window.innerWidth/2) * xratio)
let y = parseInt((positions[i + 1] + window.innerHeight/2) * yration)
if (wind.data[y*wind.nx + x]) {
let deltax = wind.data[y*wind.nx + x][0] / wind.max
let deltay = wind.data[y*wind.nx + x][1] / wind.max
positions[i] += deltax
positions[i + 1] += deltay
// 风速快的地方标成红色
let alpha = Math.sqrt(Math.pow(deltax, 2) + Math.pow(deltay, 2))
if (alpha > 0.3) {
colors[i] = 1
colors[i + 1] = 0
colors[i + 2] = 0
} else {
colors[i] = alpha
colors[i + 1] = alpha
colors[i + 2] = alpha
}
}
}
points.geometry.attributes.position.needsUpdate = true;
points.geometry.attributes.color.needsUpdate = true;
renderer.render(scene, camera);
renderer.copyFramebufferToTexture(new THREE.Vector2(0, 0), tx)
}
animate();
风速数据如下:
javascript
export const wind = {
"nx": 360, // 经度
"ny": 181, // 纬度
"max": 28.700000762939453,
"data": [
[
-2.9,
4.2
],
...
]
}
目前就是直接从左上角开始映射,看起来跟echart的图有点不一样,可能还是经纬度的计算上有点区别,回头好好研究一下这个数据。
3.新建一个纹理显示在后边记录风的行迹
主要就是把点的数据重复的画到一个纹理上,每次都往上画,并且设置一个透明度,透明度设置次数多了,基本上就消失了。
javascript
var tx = new THREE.FramebufferTexture(window.innerWidth, window.innerHeight);
var planeGeo = new THREE.PlaneGeometry(window.innerWidth, window.innerHeight);
var planeMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0.0 },
tx: { value: tx }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform sampler2D tx;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(texture2D(tx, vUv).xyz, 0.975);
}
`,
blending: THREE.AdditiveBlending,
transparent: true,
});
var plane = new THREE.Mesh(planeGeo, planeMaterial);
scene.add(plane)
结束语
看起来效果还挺好的,虽说也不一定对,哈哈。针对于这个还有很大的改进空间。这个人的思路还是挺好的,把计算都扔个gpu来做,下次试试这种情况,不过她这个需要先处理一下风速的数据。但是他说这种情况就能把粒子数提升到一百万,还是挺厉害的。
再次那个风速还是弄个颜色映射比较好,当然了也是用shader来写效果会比较好,毕竟用shader做颜色映射这个方式比较简单,避免了中间计算。
欢迎大家交流,谢谢。
完整代码
javascript
<script setup>
import * as THREE from 'three';
import { onMounted } from 'vue';
import { wind } from './wind'
const scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
var geometry = new THREE.BufferGeometry();
var positionVertices = [];
// Create particles
var colorData = []
var particleCount = 10000;
for (var i = 0; i < particleCount; i++) {
var x = THREE.MathUtils.randFloatSpread(window.innerWidth );
var y = THREE.MathUtils.randFloatSpread(window.innerHeight );
var z = 0;
positionVertices.push(x, y, z);
colorData.push(1,1,1)
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionVertices, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorData, 3));
var material = new THREE.PointsMaterial({vertexColors: true})
var points = new THREE.Points(geometry, material);
scene.add(points);
var tx = new THREE.FramebufferTexture(window.innerWidth, window.innerHeight);
var planeGeo = new THREE.PlaneGeometry(window.innerWidth, window.innerHeight);
var planeMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0.0 },
tx: { value: tx }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform sampler2D tx;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(texture2D(tx, vUv).xyz, 0.975);
}
`,
blending: THREE.AdditiveBlending,
transparent: true,
});
var plane = new THREE.Mesh(planeGeo, planeMaterial);
scene.add(plane)
function animate() {
requestAnimationFrame(animate);
var positions = points.geometry.attributes.position.array;
var colors = points.geometry.attributes.color.array;
for (var i = 0; i < positions.length; i += 3) {
if (Math.random() < 0.01) {
let x = THREE.MathUtils.randFloatSpread(window.innerWidth );
let y = THREE.MathUtils.randFloatSpread(window.innerHeight );
positions[i] = x
positions[i + 1] = y
continue
}
let xratio = wind.nx / window.innerWidth;
let yration = wind.ny / window.innerHeight;
let x = parseInt((positions[i] + window.innerWidth/2) * xratio)
let y = parseInt((positions[i + 1] + window.innerHeight/2) * yration)
if (wind.data[y*wind.nx + x]) {
let deltax = wind.data[y*wind.nx + x][0] / wind.max
let deltay = wind.data[y*wind.nx + x][1] / wind.max
positions[i] += deltax
positions[i + 1] += deltay
let alpha = Math.sqrt(Math.pow(deltax, 2) + Math.pow(deltay, 2))
if (alpha > 0.3) {
colors[i] = 1
colors[i + 1] = 0
colors[i + 2] = 0
} else {
colors[i] = alpha
colors[i + 1] = alpha
colors[i + 2] = alpha
}
}
}
points.geometry.attributes.position.needsUpdate = true;
points.geometry.attributes.color.needsUpdate = true;
// material.uniforms.time.value += 0.1
renderer.render(scene, camera);
renderer.copyFramebufferToTexture(new THREE.Vector2(0, 0), tx)
}
animate();
onMounted(() => {
let canvas = document.getElementById('can');
renderer = new THREE.WebGLRenderer({canvas: canvas});
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
<template>
<div id="content">
<canvas id="can"></canvas>
</div>
</template>
<style scoped>
#content {
width: 100%;
height: 100%;
overflow: hidden;
}
#can {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>