threejs 涟漪效果

390 阅读3分钟

之前搞了个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);  // 渲染场景并将结果添加到 DOMdocument.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);}