之前搞了个3D的涟漪效果。使用场景,可以拿来当页面背景。
一、基础
1、html引入threejs; (.html文件)
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>粒子化</title>
<style>
*{
margin: 0;
padding: 0;
}
html,body{
width: 100%;
height: 100%;
}
#container{
color:red;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.150.1/three.js"></script>
<script src="./index.js"></script>
</body>
</html>
2、搭建threejs基础环境
和threejs基本一致: scene、camera、WebGLRenderer等
function init() {
// 创建透视相机
camera = new THREE.PerspectiveCamera();
camera.position.y = -250;
camera.position.z = 300; // 创建场景,并设置雾效果
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x000000, 1, 550);
camera.lookAt(scene.position);
// 创建 WebGL 渲染器
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// 渲染场景并将结果添加到 DOM 中
document.getElementById('container').appendChild(renderer.domElement);
}
// 初始化函数
init();
3、用于显示涟漪的平台(线、点)
这里用镂空的平面 PlaneGeometry,来显示。感觉会好看一点,如果加入了颜色啥的,反而落入俗套。
geometry0 = new THREE.PlaneGeometry(500, 500, 150, 150);
// 使用 MeshBasicMaterial 来显示网格
var material = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
plane = new THREE.Mesh(geometry0, material);
scene.add(plane);
4、悬浮的粒子背景
单纯只有一个平台,太空了,,,来点悬浮的粒子,让画面更丰富一点。
const size = 500;
// 创建粒子的几何体
const geometry = new THREE.BufferGeometry();
var vertices = [];
// 随机生成大量粒子的坐标
for (let i = 0; i < 1000; i++) {
const x = (Math.random() * size + Math.random() * size) / 2 - size / 2;
const y = (Math.random() * size + Math.random() * size) / 2 - size / 2;
const z = (Math.random() * size + Math.random() * size) / 2 - size / 2;
vertices.push(x, y, z);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
// 创建粒子的材质
material = new THREE.PointsMaterial({
size: 1,
color: 0xffffff
});
// 使用几何体和材质创建粒子系统
particles = new THREE.Points(geometry, material);
// 将粒子系统添加到场景中
scene.add(particles);
二、涟漪动画
基础静态的东西已经搞上去了;现在开始搞动态的。
1、鼠标点击、存储涟漪起点
要有交互噻!没交互那和录屏有什么区别?!
这里使用threejs的**raycaster**射线,详细官方文档:threejs.org/docs/index.…
反正逻辑就是,鼠标点击一下,发送个射线,然后穿过3D模型,这样就取到了交点!将交点的位置、触发时间存入points内,后面 animate时,使用;
// 起始点的信息
const points = [
//{ id: 1, position: {x: 0, y: 0 }, time: performance.now()},
];
const canvas = document.querySelector('#container')
// 计算 以画布 开始为(0,0)点 的鼠标坐标
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect()
return {
x: ((event.clientX - rect.left) * window.innerWidth) / rect.width,
y: ((event.clientY - rect.top) * window.innerHeight) / rect.height
}
}
/** * 获取鼠标在three.js 中归一化坐标 * */
function setPickPosition(event) {
let pickPosition = { x: 0, y: 0 }
// 计算后 以画布 开始为 (0,0)点
const pos = getCanvasRelativePosition(event)
// 数据归一化
pickPosition.x = (pos.x / window.innerWidth) * 2 - 1
pickPosition.y = (pos.y / window.innerHeight) * -2 + 1
return pickPosition
}
window.addEventListener('mouseup', onRay)
// 全局对象
function onRay(event) {
let pickPosition = setPickPosition(event)
const raycaster = new THREE.Raycaster()
raycaster.setFromCamera(pickPosition, camera)
// 计算物体和射线的交点
const intersects = raycaster.intersectObjects(scene.children, true)
// 数组大于0 表示有相交对象
if (intersects.length > 0) {
intersects[0].point;
points.unshift({
id: performance.now(),
position: {x: intersects[0].point?.x, y: intersects[0].point?.y },
time: performance.now()
})
}
}
2、涟漪动画
有了涟漪中点后,在循环动画中,不停的计算点的波动就行了。
具体逻辑:
(1)先获取波动平面的所有顶点pos;
(2)根据(当前时间 - points触发时间)* 速度 ,计算涟漪波动的距离。
(3)顶点pos循环,如果点在涟漪范围内,然后使用 正弦函数 计算当期涟漪的 z 坐标;(同时要循环points, 多个点对同一个pos的点,需要叠加z坐标);
(4)顶点更新设置 pos.needsUpdate = true;
// 启动动画循环
animate();
function animate() {
const pos = plane.geometry.attributes.position;
for (var i = 0; i < pos.count; i++) {
var vertex = new THREE.Vector3(); // 创建一个三维向量来保存顶点位置
vertex.fromBufferAttribute(pos, i);
// 从顶点属性中获取顶点位置
vertex.z = 0;
for(let index = points.length - 1; index >= 0; index--) {
const { time, position } = points[index] || {};
if (time) {
const timeS = (performance.now() - time) * 0.001; // 除以1000,就是秒数
const sDis = timeS * speed;
if (sDis < 500) {
const olDis0 = timeS * speed - amplitude * 4 * Math.PI;
const olDis = timeS * speed - amplitude * 2 * Math.PI;
// 计算顶点的新位置,这里使用正弦函数来模拟涟漪效果
var distance = Math.sqrt(Math.pow(vertex.x - position.x, 2) + Math.pow(vertex.y - position.y , 2));
// 计算顶点到 point 点的距离
if (distance < sDis && distance > olDis) {
const distanceNow = (distance - olDis) / amplitude;
vertex.z += (distanceNow < 0 ? 0 : Math.sin(distanceNow) * amplitude); // 计算顶点的新高度
}
}
}
}
pos.setXYZ(i, vertex.x, vertex.y, vertex.z);// 将顶点的新位置应用到顶点属性中
}
pos.needsUpdate = true;
requestAnimationFrame(animate);
render();
}
function render() {
// 渲染场景 renderer.render(scene, camera);
}
3、粒子悬浮动画
粒子肯定也需要动嘛。如果每次都随机计算位置,没必要,太浪费。我们简单写个假的随机浮动。
(1)最开始放置点的时候位置是随机的。
(2)先生成一份儿随机速度,存着。velocities
(3)每次更新,它的x,y,z 都加 随机速度。
(4)如果点飞出了屏幕外,则把它随机挪到中心点附近的一定区域。
const maxWid= 230;// 随机生成速度
const positions = particles.geometry.attributes.position.array;
const velocities = new Float32Array(positions.length);
// 存储粒子的速度
for (let i = 0; i < velocities.length; i++) {
velocities[i] = Math.random() * 0.04 - 0.02; // 随机生成速度
}
// 启动动画循环
animate();
function animate() {
// 点的随机飘动
for (let i = 0; i < positions.length; i += 3) {
positions[i] += velocities[i]; // 更新 x 坐标
positions[i + 1] += velocities[i + 1]; // 更新 y 坐标
positions[i + 2] += velocities[i + 2]; // 更新 z 坐标
// 如果它的xyz 出现在视野之外,那么重置它的位置
if (
positions[i] < -maxWid || positions[i] > maxWid
|| positions[i + 1] < -maxWid || positions[i + 1] > maxWid
|| positions[i + 2] < -maxWid || positions[i + 2] > maxWid
) {
positions[i] = Math.random() * 200 - 100;
positions[i + 1] = Math.random() * 200 - 100;
positions[i + 2] = Math.random() * 200 - 100;
}
}
particles.geometry.attributes.position.needsUpdate = true;
requestAnimationFrame(animate);
render();
}
四、源码
.js文件
let scene;let camera;let renderer;let material;let plane;let particles;let mouseX = 0;let mouseY = 0;// 起始点的信息const points = [ { id: 1, position: {x: 0, y: 0 }, time: performance.now()},];// 随机点的范围const size = 500;const maxWid= 230;const amplitude = 25.0; // 涟漪的振幅// const frequency = 0.05; // 涟漪的频率const speed = 100; // 距离 / time function init() { // 创建透视相机 camera = new THREE.PerspectiveCamera(); camera.position.y = -250; camera.position.z = 300; // 创建场景,并设置雾效果 scene = new THREE.Scene(); scene.fog = new THREE.Fog(0x000000, 1, 550); camera.lookAt(scene.position); // 创建粒子的几何体 const geometry = new THREE.BufferGeometry(); var vertices = []; // 随机生成大量粒子的坐标 for (let i = 0; i < 1000; i++) { const x = (Math.random() * size + Math.random() * size) / 2 - size / 2; const y = (Math.random() * size + Math.random() * size) / 2 - size / 2; const z = (Math.random() * size + Math.random() * size) / 2 - size / 2; vertices.push(x, y, z); } geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); // 创建粒子的材质 material = new THREE.PointsMaterial({ size: 1, color: 0xffffff }); // 使用几何体和材质创建粒子系统 particles = new THREE.Points(geometry, material); // 将粒子系统添加到场景中 scene.add(particles); // 创建 WebGL 渲染器 renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // 渲染场景并将结果添加到 DOM 中 document.getElementById('container').appendChild(renderer.domElement); // 监听鼠标移动事件 // document.getElementById('container').addEventListener('pointermove', onPointermove); geometry0 = new THREE.PlaneGeometry(500, 500, 150, 150); var material = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }); // 使用 MeshBasicMaterial 来显示网格 plane = new THREE.Mesh(geometry0, material); scene.add(plane); // 添加OrbitControls}// 初始化函数init();const canvas = document.querySelector('#container')// 计算 以画布 开始为(0,0)点 的鼠标坐标function getCanvasRelativePosition(event) { const rect = canvas.getBoundingClientRect() return { x: ((event.clientX - rect.left) * window.innerWidth) / rect.width, y: ((event.clientY - rect.top) * window.innerHeight) / rect.height }} /** * 获取鼠标在three.js 中归一化坐标 * */ function setPickPosition(event) { let pickPosition = { x: 0, y: 0 } // 计算后 以画布 开始为 (0,0)点 const pos = getCanvasRelativePosition(event) // 数据归一化 pickPosition.x = (pos.x / window.innerWidth) * 2 - 1 pickPosition.y = (pos.y / window.innerHeight) * -2 + 1 return pickPosition}window.addEventListener('mouseup', onRay)// 全局对象function onRay(event) { let pickPosition = setPickPosition(event) const raycaster = new THREE.Raycaster() raycaster.setFromCamera(pickPosition, camera) // 计算物体和射线的交点 const intersects = raycaster.intersectObjects(scene.children, true) // 数组大于0 表示有相交对象 if (intersects.length > 0) { intersects[0].point; points.unshift({ id: performance.now(), position: {x: intersects[0].point?.x, y: intersects[0].point?.y }, time: performance.now() }) }}const positions = particles.geometry.attributes.position.array;const velocities = new Float32Array(positions.length); // 存储粒子的速度for (let i = 0; i < velocities.length; i++) { velocities[i] = Math.random() * 0.04 - 0.02; // 随机生成速度}// 启动动画循环animate();// render();function animate() { // let index = points.length; const pos = plane.geometry.attributes.position; for (var i = 0; i < pos.count; i++) { var vertex = new THREE.Vector3(); // 创建一个三维向量来保存顶点位置 vertex.fromBufferAttribute(pos, i); // 从顶点属性中获取顶点位置 vertex.z = 0; for(let index = points.length - 1; index >= 0; index--) { const { time, position } = points[index] || {}; if (time) { const timeS = (performance.now() - time) * 0.001; // 除以1000,就是秒数 const sDis = timeS * speed; if (sDis < 500) { const olDis0 = timeS * speed - amplitude * 4 * Math.PI; const olDis = timeS * speed - amplitude * 2 * Math.PI; // 计算顶点的新位置,这里使用正弦函数来模拟涟漪效果 var distance = Math.sqrt(Math.pow(vertex.x - position.x, 2) + Math.pow(vertex.y - position.y , 2)); // 计算顶点到 point 点的距离 // console.log( i, index, 'distance'); if (distance < sDis && distance > olDis) { const distanceNow = (distance - olDis) / amplitude; vertex.z += (distanceNow < 0 ? 0 : Math.sin(distanceNow) * amplitude); // 计算顶点的新高度 } } } } pos.setXYZ(i, vertex.x, vertex.y, vertex.z);// 将顶点的新位置应用到顶点属性中 } pos.needsUpdate = true; // 点的随机飘动 for (let i = 0; i < positions.length; i += 3) { positions[i] += velocities[i]; // 更新 x 坐标 positions[i + 1] += velocities[i + 1]; // 更新 y 坐标 positions[i + 2] += velocities[i + 2]; // 更新 z 坐标 // 限制粒子在一定范围内飘动 positions[i] = Math.min(Math.max(positions[i], -size / 2), size / 2); positions[i + 1] = Math.min(Math.max(positions[i + 1], -size / 2), size / 2); positions[i + 2] = Math.min(Math.max(positions[i + 2], -size / 2), size / 2); // 如果它的xyz 出现在视野之外,那么重置它的位置 if ( positions[i] < -maxWid || positions[i] > maxWid || positions[i + 1] < -maxWid || positions[i + 1] > maxWid || positions[i + 2] < -maxWid || positions[i + 2] > maxWid ) { positions[i] = Math.random() * 200 - 100; positions[i + 1] = Math.random() * 200 - 100; positions[i + 2] = Math.random() * 200 - 100; } } particles.geometry.attributes.position.needsUpdate = true; requestAnimationFrame(animate); render();}function render() { // 控制相机位置和朝向 // camera.position.x += (mouseX * 2 - camera.position.x) * 0.02; // camera.position.y += (-mouseY * 2 - camera.position.y) * 0.02; // camera.lookAt(scene.position); // 渲染场景 renderer.render(scene, camera); // 使场景自旋 // scene.rotation.x += 0.001; // scene.rotation.y += 0.002;}function onPointermove(event) { // 更新鼠标位置 mouseX = event.clientX - (window.innerWidth / 2); mouseY = event.clientY - (window.innerHeight / 2);}