Three.js实现最简管道流动效果

7,632 阅读1分钟

背景

最近项目开发中遇到一个需求,需要画个流程图,由管道连接组成,且管道内有水流效果,于是想到了用Three.js。但是咱也不会呀,于是开始学习,通过看B站,查文档,以及参考别人的文章,实现了一个简单的效果。如下图,在此记录一下实现方法,大家以后遇到同样的需求可以参考这篇文章。

效果

屏幕录制2023-07-13-10.34.27.gif

实现

我使用的是html中引入three.js的方式进行开发的,通过设置 type="importmap" 的方式实现了在vue和react环境中通用的写法,这种方式需要手动引入three源码,我使用的是vscode,需要按一个Live Server插件,模拟本地环境,写好代码后,通过鼠标右键open with live server打开。vue和react中通过npm即可。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <style>
    body { 
      margin: 0;
      overflow: hidden;
    }
  </style>
  <script type="importmap">
    {
      "imports": {
        "three": "../three.js/build/three.module.js",
        "three/addons/": "../three.js/examples/jsm/"
      }
    }
  </script>
  <script src="./TubeGeometry.js" type="module">
  </script>
</body>
</html>

TubeGeometry.js文件,详细步骤及每段代码的含义,我标记了注释

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

// 1. 创建一个场景
const scene = new THREE.Scene();

const width = window.innerWidth;
const height = window.innerHeight;

function createPath(pointsArr) {
  // 将参数数组转换成点数组的形式
  pointsArr = pointsArr.map((point) => new THREE.Vector3(...point));
  // 自定义三维路径 curvePath
  const path = new THREE.CurvePath();
  for (let i = 0; i < pointsArr.length - 1; i++) {
    // 每两个点之间形成一条三维直线
    const lineCurve = new THREE.LineCurve3(pointsArr[i], pointsArr[i + 1]); 
    // curvePath有一个curves属性,里面存放组成该三维路径的各个子路径
    path.curves.push(lineCurve); 
  }
  return path;
}

const pointsArr = [
  [42, 0, 10],
  [21, 0, 10],
  [21, 0, 1],
  [-3, 0, 1],
  [-3, 0, -18],
  [-10, 0, -18],
  [-10, 0, 5],
  [1, 0, 5],
  [1, 0, 24],
  [-27, 0, 24],
  [-27, 0, 18],
  [-46, 0, 19],
  [-46, 0, -4],
  [-25, 0, -6],
  [-25, 0, -19],
  [-35, 0, -20],
  [-35, 0, -26],
  [-30, 0, -30],
  [3, 0, -30]
];

const curve = createPath(pointsArr);

// 2. 创建管道体
const tubeGeometry = new THREE.TubeGeometry(curve, 1000, 0.5, 10, false);
// 纹理贴图:一定要使用透明背景的图片,否则贴图会全部叠在一起,看不出来效果
const texLoader = new THREE.TextureLoader();
// 图片可以用这张:http://pic.yupoo.com/mazhenghjj/e546038d/9610773f.jpg
const texture = texLoader.load('./WechatIMG110.jpg'); 
// 允许横纵设置矩阵(人话就是可以平铺)
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
texture.repeat.x = 110;
texture.repeat.y = 1;
texture.offset.y = 0.5;

// 3. 创建管道材质
const tubeMaterial = new THREE.MeshPhongMaterial({
  map: texture, // 颜色贴图
  transparent: true,
  color: 0x47d8fa,
  side: THREE.DoubleSide,
});

// 底部网格(可以不设置)
const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x004444);
scene.add(gridHelper);

const mesh = new THREE.Mesh( tubeGeometry, tubeMaterial );
mesh.position.y = 35;
mesh.position.x = 0;
mesh.rotateZ(3.14);
mesh.scale.set(2, 2, 2);
// 4. 把几何体(管道)和 材质 生成的网格物体添加到场景中
scene.add( mesh );

// 5. 设置点光源(关键的一步,没有这一步,没法照亮材质)
const pointLight = new THREE.PointLight(0xffffff, 1);
// 点光源位置
pointLight.position.set(100, 100, 100);
scene.add(pointLight);

// 6. 点光源辅助对象(可有可无,辅助观察光源位置,需要时放开 add 方法)
const sphereSize = 10;
const pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
// scene.add( pointLightHelper );

// 7. 环境光(整个场景的光,添加后整个管道都能更明亮)
const light = new THREE.AmbientLight( 0xffffff, 1 );
scene.add( light );

// 8. 相机位置(拍照片,关键,Three的每一个帧需要拍下来才能显示)
const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 2000);
// 超出视锥体的远裁截面的范围会被剪掉
camera.position.set(200, 200, 200);

// 定义相机的视线
camera.lookAt(100, 100, 100); 

// 9. 渲染方法
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
// 用相机(camera)渲染一个场景(scene)。类比相机的拍照动作
renderer.render(scene, camera); 
document.body.appendChild(renderer.domElement);

// 10. 循环
function render() {
  // 周期性渲染,更新 canvas 画布的内容
  renderer.render(scene, camera); 
  requestAnimationFrame(render);
  // 关键一步,循环改变贴图的位置,css的属性
  texture.offset.x -= 0.04;
}
render();

// 11. 拖动相机位置(场景能放大、缩小、移动,其实通过更改相机位置做到的)
const controls = new OrbitControls( camera, renderer.domElement );

// 12. 这一步是优化,在窗口改变时,canvas 位置也跟着改变
window.onresize = function() {
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 相机宽高比
  camera.aspect = window.innerWidth/window.innerHeight;
  // 更新摄像机投影矩阵
  camera.updateProjectionMatrix();
}

以上就是全部代码及步骤详解,大家可以复制代码到本地慢慢调试效果。

参考链接

B站郭隆邦老师的视频
Three.js中文网
管道流动

小建议

前段时间找工作(找了三个月,人麻了),看到很多岗位需要了解或数据WebGL,Three.js是一种不错的实现方式,大家可以学习学习,最后祝大家变得更强