分享
three-meshline

three-meshline

在使用 Three.js 绘制线条时,我们常常会遇到“线条总是 1 像素宽”的困扰。Three.js 默认的 THREE.Line 并不支持在 3D 场景中灵活控制线条的宽度,这就使得在某些需求场景中(例如可随相机变化保持可见的线条、较粗的线条表现等)略显不足。

为了解决这一限制,社区中出现了多种方案:

  1. three-meshline:基于 Three.js 进行“宽线”渲染的辅助库。
  2. Line2:Three.js 官方后续提供的 Line2 相关类,也能实现对宽线的支持。

简单介绍下介绍下 three-meshline,并展示了它如何与 React Three Fiber 相结合来实现 3D 线条效果。借鉴这个库实现的案例,可以将尝试着自己使用 Line2 来实现。

仓库地址

https://github.com/lume/three-meshline (opens in a new tab)

官网案例演示

下面是三方库(或其衍生改版)在项目中的一些案例效果

birds

graph

shape

spinner

svg

React Three Fiber 结合

three-meshline 可以很方便地与 React Three Fiber 结合使用。下面是官方示例代码,附带中文注释,展示了如何:

  • 生成多条 3D 线条
  • 动态控制线条的宽度、颜色、虚线等属性
  • 使用 React Hooks 优化性能
  • 将后期处理(如 Bloom 泛光效果)应用到线条上
  • 通过指针移动,实现相机平滑跟随 示例动图如下:

代码:

// 导入必要的库和组件
import * as THREE from "three"; // 导入Three.js库,用于3D图形渲染
import { useMemo, useRef } from "react"; // 导入React钩子,用于性能优化和引用DOM元素
import { MeshLineGeometry, MeshLineMaterial } from "meshline"; // 导入MeshLine库,用于创建高级线条效果
import { extend, Canvas, useFrame } from "@react-three/fiber"; // 导入React Three Fiber库,用于在React中使用Three.js
import { EffectComposer, Bloom } from "@react-three/postprocessing"; // 导入后期处理效果
import { easing } from "maath"; // 导入缓动函数,用于平滑动画
import { useControls } from "leva"; // 导入控制面板,用于交互式调整参数
 
// 将MeshLine组件注册到React Three Fiber
extend({ MeshLineGeometry, MeshLineMaterial });
 
/**
 * Lines组件 - 创建多条3D线条
 * @param {number} dash - 线条的虚线比例
 * @param {number} count - 要创建的线条数量
 * @param {array} colors - 可用颜色数组
 * @param {number} radius - 线条分布的半径范围
 * @param {function} rand - 随机数生成函数,默认使用Three.js的随机浮点数函数
 */
function Lines({ dash, count, colors, radius = 50, rand = THREE.MathUtils.randFloatSpread }) {
  // 使用useMemo优化性能,只有当依赖项改变时才重新计算
  const lines = useMemo(() => {
    // 创建指定数量的线条数组
    return Array.from({ length: count }, () => {
      // 创建一个随机起始位置
      const pos = new THREE.Vector3(rand(radius), rand(radius), rand(radius));
      // 创建10个随机点,每个点都是在前一个点的基础上添加随机偏移
      const points = Array.from({ length: 10 }, () =>
        pos.add(new THREE.Vector3(rand(radius), rand(radius), rand(radius))).clone()
      );
      // 使用CatmullRom曲线算法创建平滑的曲线,并获取300个点
      const curve = new THREE.CatmullRomCurve3(points).getPoints(300);
      // 返回线条的属性
      return {
        color: colors[parseInt(colors.length * Math.random())], // 随机选择一种颜色
        width: Math.max(radius / 100, (radius / 50) * Math.random()), // 计算线条宽度
        speed: Math.max(0.1, 1 * Math.random()), // 计算线条动画速度
        curve: curve.flatMap((point) => point.toArray()), // 将曲线点转换为一维数组
      };
    });
  }, [colors, count, radius]); // 依赖项:颜色、数量和半径
 
  // 为每条线渲染一个Fatline组件
  return lines.map((props, index) => <Fatline key={index} dash={dash} {...props} />);
}
 
/**
 * Fatline组件 - 渲染单条线
 * @param {array} curve - 线条的点坐标数组
 * @param {number} width - 线条宽度
 * @param {string|array} color - 线条颜色
 * @param {number} speed - 动画速度
 * @param {number} dash - 虚线比例
 */
function Fatline({ curve, width, color, speed, dash }) {
  const ref = useRef(); // 创建对mesh的引用
 
  // 每帧更新线条的虚线偏移,创建动画效果
  useFrame((state, delta) => (ref.current.material.dashOffset -= (delta * speed) / 10));
 
  return (
    <mesh ref={ref}>
      {/* 使用曲线点创建线条几何体 */}
      <meshLineGeometry points={curve} />
      {/* 设置线条材质属性 */}
      <meshLineMaterial
        transparent // 启用透明度
        lineWidth={width} // 设置线宽
        color={color} // 设置颜色
        depthWrite={false} // 禁用深度写入,避免z-fighting问题
        dashArray={0.25} // 虚线的间隔长度
        dashRatio={dash} // 虚线的比例
        toneMapped={false} // 禁用色调映射,保持颜色鲜艳
      />
    </mesh>
  );
}
 
/**
 * Rig组件 - 控制相机移动,实现交互式视角
 * @param {number} radius - 相机移动的半径
 */
function Rig({ radius = 20 }) {
  // 每帧更新相机位置,根据鼠标位置调整视角
  useFrame((state, dt) => {
    // 使用缓动函数平滑地更新相机位置
    easing.damp3(
      state.camera.position, // 目标:相机位置
      [
        Math.sin(state.pointer.x) * radius, // X坐标:基于鼠标X位置的正弦函数
        Math.atan(state.pointer.y) * radius, // Y坐标:基于鼠标Y位置的反正切函数
        Math.cos(state.pointer.x) * radius, // Z坐标:基于鼠标X位置的余弦函数
      ],
      0.25, // 缓动系数,值越小移动越平滑
      dt // 时间增量
    );
    // 确保相机始终看向场景中心
    state.camera.lookAt(0, 0, 0);
  });
}
 
/**
 * App组件 - 主应用组件
 * 创建3D场景并设置交互控制
 */
export default function App() {
  // 使用Leva库创建交互式控制面板
  const { dash, count, radius } = useControls({
    dash: { value: 0.9, min: 0, max: 0.99, step: 0.01 }, // 虚线比例滑块
    count: { value: 50, min: 0, max: 200, step: 1 }, // 线条数量滑块
    radius: { value: 50, min: 1, max: 100, step: 1 }, // 半径范围滑块
  });
 
  return (
    // 创建全屏容器
    <div style={{ width: "100vw", height: "100vh" }}>
      {/* 创建3D画布,设置相机初始位置和视场角 */}
      <Canvas camera={{ position: [0, 0, 5], fov: 90 }}>
        {/* 设置背景颜色为深蓝色 */}
        <color attach="background" args={["#101020"]} />
        {/* 添加线条组件,传入控制面板的参数和颜色数组 */}
        <Lines
          dash={dash}
          count={count}
          radius={radius}
          colors={[
            [10, 0.5, 2], // 紫红色
            [1, 2, 10], // 蓝色
            "#A2CCB6", // 淡绿色
            "#FCEEB5", // 淡黄色
            "#EE786E", // 粉红色
            "#e0feff", // 淡蓝色
          ]}
        />
        {/* 添加相机控制组件 */}
        <Rig />
        {/* 添加后期处理效果 */}
        <EffectComposer>
          {/* 添加泛光效果,使亮部发光 */}
          <Bloom mipmapBlur luminanceThreshold={1} radius={0.6} />
        </EffectComposer>
      </Canvas>
    </div>
  );
}

结语

  • three-meshline:适用于需要快速绘制较粗的线条,同时对性能、实现细节有所考量的场景。它在社区沉淀已久,适配到 React Three Fiber 也非常容易。
  • Line2:若追求与最新版本的 Three.js 生态深度结合,或需要官方后续维护,可尝试查看 Line2 实现。