three.js+shader 制作粒子飞线,小蝌蚪那种。

本文分享了使用three.js和shader技术实现独特蝌蚪状粒子飞线效果的过程,包括粒子束制作、粒子运动控制及使用tween.js进行动画处理的详细步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习three.js四个月了,最近终于利用shader粒子做出了我心心念念的蝌蚪状飞线,这个网上也是查不到什么资料的,2Dcanvas的可以查得到,3D的都藏着掖着很难找的到相关案例,最近我通过腾讯一个小姐姐发布几个three.js 案例受到了一点启发,终于花了一个晚上做了出来,也是第一次在three.js中使用shader。

下面是效果图:

先说说思路,第一步是制作一个蝌蚪状的粒子束,其实很简单就是让粒子一个一个从小到大排列就好,这部分主要利用shader处理,代码如下:

const vs:string = `

      attribute float size; // 顶点尺寸

      attribute vec4 colors; //顶点颜色

      varying float opacity; // 控制透明度

      varying vec3 vexColor; // 顶点颜色

      void main(){

          vexColor.x = colors.r;

          vexColor.y = colors.g;

          vexColor.z = colors.b;

          //w分量为透明度

          opacity = colors.w;

          vec4 mvPosition = modelViewMatrix * vec4(position,1.0); //?这里模型矩阵,坐标向量,和投影矩阵都是three给你注入的好像。

          gl_PointSize = size;

          gl_Position = projectionMatrix * mvPosition;

      }

`;

然后就是配置数据源,数据源可以利用three.js 给的curve3组件中的getPoints取得,代码如下

this.spline = new THREE.CatmullRomCurve3(vecs);

    this.pointNum = num;

    this.distance = this.spline.getLength();

    //初始化粒子

    this.points = this.spline.getPoints(num);

    const colorsLen = this.points.length * 4;

    const sizeLen = this.points.length;

    const colors:Float32Array = new Float32Array(colorsLen);

    const sizes:Float32Array = new Float32Array(sizeLen);

    this.geometry = new THREE.BufferGeometry().setFromPoints(this.points);

    for(let i=0,z=0;i<colorsLen;i+=4,z++){

      //color

      colors[i] = color.r;

      colors[i+1] = color.g;

      colors[i+2] = color.b;

      // opacity

      colors[i+3] = (i+3)/sizeLen;

      // size从小到大

      sizes[z] =size*(z/sizeLen);

    };

    this.geometry.addAttribute('colors',new THREE.BufferAttribute(colors,4));

    this.geometry.addAttribute('size',new THREE.BufferAttribute(sizes,1));

 

第二部分就是如何让粒子运动起来,这里我借助了tween.js,整体思路就是取一段固定的粒子数目+加上Curve3的getPointAt取点函数,不断的取出固定数目并不断前移的坐标,直接修改bufferGeometry的position数据即可。

 

下面贴出完整代码:

import * as THREE from "three";

import TWEEN from "@tweenjs/tween.js";

const fs:string = `

      uniform sampler2D texture;

      varying float opacity;

      varying vec3 vexColor;

      void main(){

          gl_FragColor = vec4(vexColor,opacity);

          gl_FragColor = gl_FragColor * texture2D(texture,gl_PointCoord);

      }     

`;

const vs:string = `

      attribute float size;

      attribute vec4 colors;

      varying float opacity;

      varying vec3 vexColor;

      void main(){

          vexColor.x = colors.r;

          vexColor.y = colors.g;

          vexColor.z = colors.b;

          //w分量为透明度

          opacity = colors.w;

          vec4 mvPosition = modelViewMatrix * vec4(position,1.0);

          gl_PointSize = size;

          gl_Position = projectionMatrix * mvPosition;

      }

`;

/**

* 粒子飞线

*/

export default class PointsFlyLine{

  //粒子位置

  geometry: THREE.BufferGeometry;

  //曲线

  spline: THREE.CatmullRomCurve3;

  //粒子系统

  particleSystem: THREE.Points;

  //粒子数目

  pointNum:number;

  //粒子间的总距离

  distance: number;

  points: THREE.Vector3[];

  tween: any;

  /**

  * 创建粒子系统

  * @param points 粒子

  * @param size 粒子大小

  * @param num 粒子数目

  * @param color 粒子颜色

  */

  constructor({ vecs, num, size, color }: { vecs: THREE.Vector3[]; num: number; size: number; color: THREE.Color; }){

    this.spline = new THREE.CatmullRomCurve3(vecs);

    this.pointNum = num;

    this.distance = this.spline.getLength();

    //初始化粒子

    this.points = this.spline.getPoints(num);

    const colorsLen = this.points.length * 4;

    const sizeLen = this.points.length;

    const colors:Float32Array = new Float32Array(colorsLen);

    const sizes:Float32Array = new Float32Array(sizeLen);

    this.geometry = new THREE.BufferGeometry().setFromPoints(this.points);

    for(let i=0,z=0;i<colorsLen;i+=4,z++){

      //color

      colors[i] = color.r;

      colors[i+1] = color.g;

      colors[i+2] = color.b;

      // opacity

      colors[i+3] = (i+3)/sizeLen;

      // size从小到大

      sizes[z] =size*(z/sizeLen);

    };

    this.geometry.addAttribute('colors',new THREE.BufferAttribute(colors,4));

    this.geometry.addAttribute('size',new THREE.BufferAttribute(sizes,1));

    const uniforms:object = {

      texture: {

        value: new THREE.CanvasTexture(this.createSpriteCanvas(size)),

      }

    };

    const shaderMaterial:THREE.ShaderMaterial = new THREE.ShaderMaterial({

      uniforms,

      vertexShader:vs,

      fragmentShader:fs,

      transparent:true,

      depthTest:false

    });

    this.particleSystem = new THREE.Points(this.geometry,shaderMaterial);

  }

  //飞线开始

  start(){

    const max = this.distance*10;

    const end:number = this.pointNum;

    const m = {start:0,end};

    this.tween = this.tweenAnimate(m,{start:max-end,end:max},2000,null,()=>{

      let pointArr:number[] = [];

      let s = Math.round(m.start),e = Math.floor(m.end);

      for (let i = s; i <= e && i<=max; i++) {

        pointArr = pointArr.concat(this.spline.getPointAt(i/max).toArray());

      }

      this.geometry.attributes.position = new THREE.BufferAttribute(new Float32Array(pointArr),3);

    });

    this.tween.repeat(Infinity).start();

  }

  stop(){

    this.tween.stop();

  }

  tweenAnimate(current:object, target:object, interval:number, animation?:TWEEN.Easing, onUpdate?:Function, complete?:Function) {

    var animate = animation ? animation : TWEEN.Easing.Linear.None;

    let tween = new TWEEN.Tween(current).to(target, interval).easing(animate);

    onUpdate && tween.onUpdate(() => onUpdate());

    complete && tween.onComplete(() => complete());

    return tween;

  }

  //创建圆形精灵贴图

  createSpriteCanvas(size:number){

    const canvas = document.createElement('canvas');

    canvas.width = canvas.height = size;

    const context = canvas.getContext('2d');

    if(context!=null){

      context.fillStyle='rgba(255,255,255,.0)';

      context.beginPath();

      context.arc(size/2,size/2,size/2,0,Math.PI*2);

      context.fillStyle = 'white';

      context.fill();

      context.closePath();

    }

    return canvas;

  }

}

<think>我们正在处理用户关于Three.js实现3D地图线动画的请求。根据用户要求,我们需要提供教程式的指导。同时,我们注意到用户引用了两篇关于Three.js线可视化和3D地图可视化的文章,这些将作为参考。 目标:指导用户使用Three.js创建3D地图线效果。 步骤: 1. 准备3D地图底图(可以使用省份或国家地图的3D模型)。 2. 创建线(从起点到终点的弧线)。 3. 实现线的动画效果(通常是通过流动的光点或渐变的颜色来表示方向)。 4.线与地图结合,并确保视觉效果良好。 根据引用[1]和[2],我们可以借鉴以下内容: - 引用[1]提到可以实现复杂的线可视化效果,包括简单数据展示和地理信息系统。 - 引用[2]提到结合Three.js的3D渲染能力和GSAP动画系统,实现数据可视化、交互反馈和动态效果。 因此,我们将按照以下步骤进行: 步骤1:创建3D地图 - 使用Three.js创建一个场景,并添加地图的3D模型。地图模型可以通过加载GeoJSON数据转换为3D形状,或者使用预先制作好的模型。 步骤2:创建线 - 线通常使用曲线(如CatmullRomCurve3)来定义路径,然后使用TubeGeometry或Line来可视化。 - 为了美观,我们可以使用带有纹理的管状体,并让纹理流动以产生动画效果。 步骤3:线动画 - 通过修改纹理的偏移量来实现流动效果。 - 也可以使用粒子沿着路径移动来模拟线。 步骤4:整合与优化 - 将线添加到地图场景中,调整位置和大小。 - 使用GSAP(一个动画库)来创建更复杂的动画效果(如引用[2]所述)。 下面是一个简化的代码示例,展示如何创建一条线并实现动画:</think>### Three.js 实现 3D 地图线动画教程 #### 一、基础准备 1. **场景搭建** 创建基础3D场景,包含地图模型(推荐使用GeoJSON数据生成省份轮廓): ```javascript const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true }); // 添加光源 const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); scene.add(directionalLight); ``` 2. **地图模型生成** 使用`d3-geo`+`Three.js`转换地理数据: ```javascript import * as d3 from 'd3-geo'; const projection = d3.geoMercator().fitSize([800, 600], geojsonData); const shape = new THREE.Shape(); geojsonData.coordinates[0].forEach(([lng, lat]) => { const [x, y] = projection([lng, lat]); shape.lineTo(x, y); }); const geometry = new THREE.ExtrudeGeometry(shape, { depth: 2 }); const material = new THREE.MeshPhongMaterial({ color: 0x3498db }); const mapMesh = new THREE.Mesh(geometry, material); scene.add(mapMesh); ``` #### 二、线实现 1. **线路径生成** 使用三维曲线连接起点(如北京)和终点(如上海): ```javascript const start = projection([116.4, 39.9]); // 北京坐标 const end = projection([121.47, 31.23]); // 上海坐标 // 创建弧线路径 const curve = new THREE.CatmullRomCurve3([ new THREE.Vector3(start[0], start[1], 5), new THREE.Vector3((start[0]+end[0])/2, (start[1]+end[1])/2, 20), // 中间高点 new THREE.Vector3(end[0], end[1], 5) ]); ``` 2. **线材质与动画** 通过流动纹理实现动态效果: ```javascript // 1. 创建管状几何体 const tubeGeometry = new THREE.TubeGeometry(curve, 64, 0.3, 8, false); // 2. 加载流动纹理 const textureLoader = new THREE.TextureLoader(); const flowTexture = textureLoader.load('path/glow_texture.png'); flowTexture.wrapS = THREE.RepeatWrapping; // 3. 创建着色器材质 const material = new THREE.MeshBasicMaterial({ map: flowTexture, transparent: true, side: THREE.DoubleSide }); // 4. 动画循环更新纹理偏移 function animate() { requestAnimationFrame(animate); flowTexture.offset.x -= 0.01; // 控制流动速度 renderer.render(scene, camera); } ``` #### 三、进阶优化 1. **性能优化技巧** - 使用`InstancedMesh`批量渲染多条线 - 实现视锥体裁剪(Frustum Culling) - 添加LOD(Level of Detail)控制 2. **视觉效果增强** ```javascript // 添加光晕效果 const glowMaterial = new THREE.ShaderMaterial({ uniforms: { glowColor: { value: new THREE.Color(0x00ffff) }, viewVector: { value: camera.position } }, vertexShader: `...`, // 自定义着色器代码 fragmentShader: `...`, blending: THREE.AdditiveBlending }); ``` 3. **交互实现** ```javascript // 鼠标悬停高亮 const raycaster = new THREE.Raycaster(); window.addEventListener('mousemove', (event) => { raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(flyLines); if (intersects.length > 0) { intersects[0].object.material.color.set(0xff0000); // 变红色 } }); ``` > 完整项目示例: > [GitHub - Three.js 线地图示例](https://github.com/example/threejs-flyline-map) > 建议结合[GSAP动画库](https://greensock.com/gsap/)实现更流畅的过渡效果[^1] #### 四、调试建议 1. 使用`dat.GUI`快速调整参数: ```javascript const gui = new dat.GUI(); gui.add(flowTexture.offset, 'x', -1, 1).name('流动速度'); gui.addColor({ color: 0x00ff00 }, 'color').onChange(v => material.color.set(v)); ``` 2. 性能监控: ```javascript import Stats from 'stats.js'; const stats = new Stats(); document.body.appendChild(stats.dom); function animate() { stats.update(); // ...其他动画 } ```
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值