Three.js中实现流动的管线效果的全过程

 更新时间:2025年09月06日 09:49:12   作者:阿琰a_  
最近使用threejs开发项目, 碰到需要在模型的管道上画出流动效果,下面这篇文章主要介绍了Three.js中实现流动的管线效果的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

最近在跟第三方对接工业流程的模型,发现建模的管线还挺好看的,给材质加上一个偏移,就会出现一直流动的效果,于是在想可不可以使用Three.js画出同等效果的管线。

Three.js文档:three.js docs

整体的实现思路是先绘制管线模型,然后在模型上添加一个动态的着色器材质,实现管线流动效果。

首先我想通过使用CatmullRomCurve3和TubeGeometry这俩个类API去创建平滑的 3D 样条曲线。

代码如下

/**
 * 绘制管线
 * @param THREE
 * @param scene
 * @param points
 */
function createPipeLine(THREE, scene ,points) {
  const cps = points.map(item=>{
    return new THREE.Vector3(...item)
  })
  console.log(cps,'cps')
  const curve = new THREE.CatmullRomCurve3(cps)

  let geometry = new THREE.TubeGeometry(curve, 100, 10, 32)
  const material = new THREE.MeshLambertMaterial({
    color: '#19bbd5',
    side: THREE.DoubleSide,
  })

  let length = curve.getLength()
  let uniforms = {
    totalLength: { value: length },
    stripeOffset: { value: 0 },        // 条纹偏移量
    stripeWidth: { value: 100 },       // 条纹宽度 (归一化值)
    stripeSpacing: { value: 100 },     // 条纹间距 (归一化值)
    stripeColor: { value: new THREE.Color('#096be3') }, // 条纹颜色
    speedFactor: { value: 50 },
  };

  material.onBeforeCompile = shader => {
    shader.uniforms.totalLength = uniforms.totalLength;
    shader.uniforms.stripeOffset = uniforms.stripeOffset;
    shader.uniforms.stripeWidth = uniforms.stripeWidth;
    shader.uniforms.stripeSpacing = uniforms.stripeSpacing;
    shader.uniforms.stripeColor = uniforms.stripeColor;

    shader.fragmentShader = `
        uniform float totalLength;
        uniform float stripeOffset;
        uniform float stripeWidth;
        uniform float stripeSpacing;
        uniform vec3 stripeColor;

        ${shader.fragmentShader}
        `.replace(
        `#include <color_fragment>`,
        `#include <color_fragment>

        // 计算条纹模式
        float pattern = mod((vUv.x - stripeOffset) * totalLength / (stripeWidth + stripeSpacing), 1.0);
        float isStripe = step(pattern, stripeWidth / (stripeWidth + stripeSpacing));

        // 平滑边缘
        float edge = fwidth(vUv.x) * 2.0;
        float smoothFactor = smoothstep(0.0, edge, abs(pattern - 0.5 * stripeWidth));

        // 混合颜色
        diffuseColor.rgb = mix(diffuseColor.rgb, stripeColor, isStripe * smoothFactor);
      `
    )
  }

  material.defines = { 'USE_UV': "" }
  let mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  let clock = new THREE.Clock()
  function animation() {
    const delta = clock.getDelta();

    // 计算归一化的流动速度 (速度系数 / 管线长度)
    const normalizedSpeed = uniforms.speedFactor.value / uniforms.totalLength.value*10;

    // 更新条纹偏移量
    uniforms.stripeOffset.value += delta * normalizedSpeed;

    // 重置偏移量以保持循环
    uniforms.stripeOffset.value = uniforms.stripeOffset.value % 1.0;

    requestAnimationFrame(animation)
  }
  animation()
}

const points = [[-1137,70,2850],[694,70,2810],[684,70,1550]]
createPipeLine(THREE,scene,points)

因为我加载了一个模型地板比较大,所以我把管线相关的参数设置的也比较大。

实现效果如下:

其实没有达到我的预期,我想的是如下箭头效果,但这个曲线它自动偏移圆滑了一些。

于是我换一个曲线API,使用LineCurve3去实现管线。

效果如下,管线画出来了,但是拐角又不是很好看。解决办法就是把拐角的点设置多一些,就看起来比较圆滑了。

拐角点设置多后的效果

代码如下

/**
 * 绘制管线
 * @param THREE
 * @param scene
 * @param points
 */
function createPipeLine(THREE, scene ,points) {
  const cps = points.map(item=>{
    return new THREE.Vector3(...item)
  })
  console.log(cps,'cpscps')
  const curve = new THREE.CurvePath();
  for (let i = 0; i < cps.length - 1; i++) {
    // 每两个点之间形成一条三维直线
    const lineCurve = new THREE.LineCurve3(cps[i], cps[i + 1]);
    // curvePath有一个curves属性,里面存放组成该三维路径的各个子路径
    curve.curves.push(lineCurve);
  }
  
  let geometry = new THREE.TubeGeometry(curve, 100, 10, 32)
  const material = new THREE.MeshLambertMaterial({
    color: '#19bbd5',
    side: THREE.DoubleSide,
  })

  let length = curve.getLength()
  let uniforms = {
    totalLength: { value: length },
    stripeOffset: { value: 0 },        // 条纹偏移量
    stripeWidth: { value: 100 },       // 条纹宽度 (归一化值)
    stripeSpacing: { value: 100 },     // 条纹间距 (归一化值)
    stripeColor: { value: new THREE.Color('#096be3') }, // 条纹颜色
    speedFactor: { value: 50 },
  };

  material.onBeforeCompile = shader => {
    shader.uniforms.totalLength = uniforms.totalLength;
    shader.uniforms.stripeOffset = uniforms.stripeOffset;
    shader.uniforms.stripeWidth = uniforms.stripeWidth;
    shader.uniforms.stripeSpacing = uniforms.stripeSpacing;
    shader.uniforms.stripeColor = uniforms.stripeColor;

    shader.fragmentShader = `
        uniform float totalLength;
        uniform float stripeOffset;
        uniform float stripeWidth;
        uniform float stripeSpacing;
        uniform vec3 stripeColor;

        ${shader.fragmentShader}
        `.replace(
        `#include <color_fragment>`,
        `#include <color_fragment>

        // 计算条纹模式
        float pattern = mod((vUv.x - stripeOffset) * totalLength / (stripeWidth + stripeSpacing), 1.0);
        float isStripe = step(pattern, stripeWidth / (stripeWidth + stripeSpacing));

        // 平滑边缘
        float edge = fwidth(vUv.x) * 2.0;
        float smoothFactor = smoothstep(0.0, edge, abs(pattern - 0.5 * stripeWidth));

        // 混合颜色
        diffuseColor.rgb = mix(diffuseColor.rgb, stripeColor, isStripe * smoothFactor);
      `
    )
  }

  material.defines = { 'USE_UV': "" }
  let mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  let clock = new THREE.Clock()
  function animation() {
    const delta = clock.getDelta();

    // 计算归一化的流动速度 (速度系数 / 管线长度)
    const normalizedSpeed = uniforms.speedFactor.value / uniforms.totalLength.value*10;

    // 更新条纹偏移量
    uniforms.stripeOffset.value += delta * normalizedSpeed;

    // 重置偏移量以保持循环
    uniforms.stripeOffset.value = uniforms.stripeOffset.value % 1.0;

    requestAnimationFrame(animation)
  }
  animation()
}

const points = [[-1137,70,2850],[654, 70, 2850], [664, 70, 2840], [674, 70, 2830],[684,70,2820],[694,70,2810],[684,70,1550]]

createPipeLine(THREE,scene,points)

上面的方案能实现流动的管线,但是拐角的点位很难处理,还是打算再换一种方案。

最终方案:直线使用LineCurve3+拐角曲线CatmullRomCurve3组合使用。

效果如下

源代码

/**
 * 绘制管线
 * @param THREE
 * @param scene
 * @param points
 */
function createPipeLine(THREE, scene ,points) {
  if(points.length<=1){
    return false
  }
  const curvePath = new THREE.CurvePath();
  let prevPoint = null; // 用于记录上一个点

  // 遍历点数组
  for (let i = 0; i < points.length; i++) {
    const current = points[i];

    // 处理普通点(一维数组)
    if (Array.isArray(current) && current.length === 3 && typeof current[0] === 'number') {
      const point = new THREE.Vector3(...current);

      if (prevPoint === null) {
        // 第一个点,只记录不创建曲线
        prevPoint = point;
      } else {
        // 创建直线连接到当前点
        curvePath.add(new THREE.LineCurve3(prevPoint, point));
        prevPoint = point;
      }
    }
    // 处理拐角点(二维数组)
    else if (Array.isArray(current) && current.length >= 3) {
      // 将拐角点数组转换为Vector3
      const curvePoints = current.map(p => new THREE.Vector3(...p));

      // 确保与上一个点连接
      if (prevPoint !== null) {
        // 检查拐角曲线的起点是否与上一个点相同
        if (!prevPoint.equals(curvePoints[0])) {
          // 如果不相同,添加一条连接线
          curvePath.add(new THREE.LineCurve3(prevPoint, curvePoints[0]));
        }
      }

      // 创建CatmullRom曲线
      const cornerCurve = new THREE.CatmullRomCurve3(curvePoints);
      curvePath.add(cornerCurve);

      // 更新上一个点为拐角曲线的最后一个点
      prevPoint = curvePoints[curvePoints.length - 1];
    } else {
      console.warn('无效的点格式:', current);
    }
  }

// 创建管道几何体
  const segments = Math.max(100, Math.floor(curvePath.getLength() / 20));
  const geometry = new THREE.TubeGeometry(
      curvePath,
      segments, // 动态计算分段数
      10,      // 管道半径
      16,      // 径向分段数
      false    // 是否闭合
  );
  const material = new THREE.MeshLambertMaterial({
    color: '#19bbd5',
    side: THREE.DoubleSide,
  })

  let length = curvePath.getLength()
  let uniforms = {
    totalLength: { value: length },
    stripeOffset: { value: 0 },        // 条纹偏移量
    stripeWidth: { value: 100 },       // 条纹宽度 (归一化值)
    stripeSpacing: { value: 100 },     // 条纹间距 (归一化值)
    stripeColor: { value: new THREE.Color('#096be3') }, // 条纹颜色
    speedFactor: { value: 50 },
  };

  material.onBeforeCompile = shader => {
    shader.uniforms.totalLength = uniforms.totalLength;
    shader.uniforms.stripeOffset = uniforms.stripeOffset;
    shader.uniforms.stripeWidth = uniforms.stripeWidth;
    shader.uniforms.stripeSpacing = uniforms.stripeSpacing;
    shader.uniforms.stripeColor = uniforms.stripeColor;

    shader.fragmentShader = `
        uniform float totalLength;
        uniform float stripeOffset;
        uniform float stripeWidth;
        uniform float stripeSpacing;
        uniform vec3 stripeColor;

        ${shader.fragmentShader}
        `.replace(
        `#include <color_fragment>`,
        `#include <color_fragment>

        // 计算条纹模式
        float pattern = mod((vUv.x - stripeOffset) * totalLength / (stripeWidth + stripeSpacing), 1.0);
        float isStripe = step(pattern, stripeWidth / (stripeWidth + stripeSpacing));

        // 平滑边缘
        float edge = fwidth(vUv.x) * 2.0;
        float smoothFactor = smoothstep(0.0, edge, abs(pattern - 0.5 * stripeWidth));

        // 混合颜色
        diffuseColor.rgb = mix(diffuseColor.rgb, stripeColor, isStripe * smoothFactor);
      `
    )
  }

  material.defines = { 'USE_UV': "" }
  let mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  let clock = new THREE.Clock()
  function animation() {
    const delta = clock.getDelta();

    // 计算归一化的流动速度 (速度系数 / 管线长度)
    const normalizedSpeed = uniforms.speedFactor.value / uniforms.totalLength.value*10;

    // 更新条纹偏移量
    uniforms.stripeOffset.value += delta * normalizedSpeed;

    // 重置偏移量以保持循环
    uniforms.stripeOffset.value = uniforms.stripeOffset.value % 1.0;

    requestAnimationFrame(animation)
  }
  animation()
}

const points = [
        [-1137,70,2850],
        [440,70,2850],
        [[470,70,2838],[480,70,2825],[490,70,2812]],//拐角点二维数组且至少三个点
        [500,70,2800],
        [500,70,1850]
    ]

createPipeLine(THREE,scene,points)

总结 

到此这篇关于Three.js中实现流动的管线效果的文章就介绍到这了,更多相关Three.js流动管线效果内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 调整小数的格式保留小数点后两位

    调整小数的格式保留小数点后两位

    调整小数的格式,如保留小数点后两位等等在开发过程中经常会遇到,下面本文搜集了一些不错的实现方法与分享
    2014-05-05
  • IE8 中使用加速器(Activities)

    IE8 中使用加速器(Activities)

    在IE8中使用Activities感觉就是让自己使用IE更方便了,懒人就是得懒得底。
    2010-05-05
  • JavaScript中的for...of和for...in循环容易遇到的问题及解决方法总结

    JavaScript中的for...of和for...in循环容易遇到的问题及解决方法总结

    在 JavaScript 编程中,for...of 和 for...in 是常用的循环语法,但它们在使用时可能会引发一些意想不到的问题,本文将分享我在使用这两种循环时所遇到的坑和经验,需要的朋友可以参考下
    2023-08-08
  • 关于JavaScript的面向对象和继承有利新手学习

    关于JavaScript的面向对象和继承有利新手学习

    这是一篇关于JavaScript的面向对象和继承的文章,对想学习JavaScript中面向对象的同学来说是很有帮助,虽然一些Javascript用户可能永远也不需要知道原型或面向对象语言的性质,但是那些来自传统面向对象的语言的开发者使用的时候会发现JavaScript的继承模型非常的奇怪
    2013-01-01
  • Javascript中的for in循环和hasOwnProperty结合使用

    Javascript中的for in循环和hasOwnProperty结合使用

    当检测某个对象是否拥有某个属性时,hasOwnProperty 是唯一可以完成这一任务的方法,在 for in 循环时,建议增加 hasOwnProperty 进行判断,可以有效避免扩展本地原型而引起的错误
    2013-06-06
  • 微信小程序对接七牛云存储的方法

    微信小程序对接七牛云存储的方法

    本篇文章主要介绍了小程序对接七牛云存储的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • 基于JS实现带动画效果的流程进度条

    基于JS实现带动画效果的流程进度条

    当在使用流程的时候,比如有一个审核流程,有三个阶段:开始,审核中,审核成功。当在不同的阶段,做相应的进度显示,当显示时,是以动画的形式显示的。下面通过代码给大家介绍JS实现带动画效果的流程进度条,感兴趣的朋友一起看看吧
    2018-06-06
  • 微信小程序本地存储增加有效期的方法

    微信小程序本地存储增加有效期的方法

    这篇文章主要介绍了微信小程序本地存储增加有效期的方法,这里通过一个简单示例,展示如何设置有效期为1小时的本地存储,首先将storage.js引入到app.js中,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2024-07-07
  • JS实现向iframe中表单传值的方法

    JS实现向iframe中表单传值的方法

    这篇文章主要介绍了JS实现向iframe中表单传值的方法,涉及js针对页面元素及表单属性操作相关实现技巧,需要的朋友可以参考下
    2017-03-03
  • js实现抽奖效果

    js实现抽奖效果

    本文主要介绍了js实现抽奖效果的方法实例。具有很好的参考价值。下面跟着小编一起来看下吧
    2017-03-03

最新评论