Trails

Starter pack

让我们深入探索 trails 的世界!Trails 是为场景添加运动感的绝佳方式。它们可以用于创建各种效果,例如光 trails、烟 trails,甚至是移动物体的 trails。

这是我们将一起构建的最终项目:

我们将从使用自定义 trail 光标创建一个简单的 trail 效果开始。然后我们将探索 dreiTrail 组件 来制作您在预览中看到的彗星效果。

初始项目

初始项目包含许多我们在之前课程中已经介绍过的内容:

此外,我使用 Tailwind CSS 快速设计了 UI。如果您不熟悉 Tailwind CSS,可以跳过 UI 部分,专注于 Three.js 部分。

WawaCoinWawaCard 模型是我们内部制作的,并在初始项目中可用。我使用了 dreiMeshTransmissionMaterial 来创造这个未来感的外观。

随意根据您的喜好转换场景。您可以自由地在自己的项目中重用项目的任何部分。

我忘了提,但网站的内容纯属虚构。我并没有在推出一种新加密货币。(还没有?👀)

定制轨迹光标

我们先从创建一个简单的跟随光标的轨迹效果开始。

创建一个新的 components/Cursor.jsx 文件,并添加以下代码:

import { useFrame } from "@react-three/fiber";
import { useControls } from "leva";
import { useRef } from "react";
export const Cursor = () => {
  const { color, intensity, opacity, size } = useControls("Cursor", {
    size: { value: 0.2, min: 0.1, max: 3, step: 0.01 },
    color: "#dfbcff",
    intensity: { value: 4.6, min: 1, max: 10, step: 0.1 },
    opacity: { value: 0.5, min: 0, max: 1, step: 0.01 },
  });
  const target = useRef();
  useFrame(({ clock }) => {
    if (target.current) {
      const elapsed = clock.getElapsedTime();
      target.current.position.x = Math.sin(elapsed) * 5;
      target.current.position.y = Math.cos(elapsed * 2) * 4;
      target.current.position.z = Math.sin(elapsed * 4) * 10;
    }
  });
  return (
    <>
      <group ref={target}>
        <mesh>
          <sphereGeometry args={[size / 2, 32, 32]} />
          <meshStandardMaterial
            color={color}
            transparent
            opacity={opacity}
            emissive={color}
            emissiveIntensity={intensity}
          />
        </mesh>
      </group>
    </>
  );
};

这是一简单的跟随正弦波的球体。您可以使用 Leva controls 来调整光标的大小、颜色、强度和不透明度。

目前我们使用固定的运动,这将简化轨迹的可视化。稍后我们会将其替换为鼠标位置。

Cursor 组件添加到 Experience 组件中:

// ...
import { Cursor } from "./Cursor";

export const Experience = () => {
  // ...
  return (
    <>
      <Cursor />
      {/* ... */}
    </>
  );
};

// ...

我们可以看到一个移动的球体,它将成为我们轨迹的目标。

SimpleTrail 组件

group 是我们的 trail 将要跟随的目标。我们将创建一个新组件 components/SimpleTrail.jsx 来实现 trail 效果:

import { useRef } from "react";
import * as THREE from "three";

export function SimpleTrail({
  target = null,
  color = "#ffffff",
  intensity = 6,
  numPoints = 20,
  height = 0.42,
  minDistance = 0.1,
  opacity = 0.5,
  duration = 20,
}) {
  const mesh = useRef();

  return (
    <>
      <mesh ref={mesh}>
        <planeGeometry args={[1, 1, 1, numPoints - 1]} />
        <meshBasicMaterial
          color={color}
          side={THREE.DoubleSide}
          transparent={true}
          opacity={opacity}
          depthWrite={false}
        />
      </mesh>
    </>
  );
}

参数说明如下:

  • target: 要跟随的目标的 ref
  • color: trail 的颜色。
  • intensity: trail 的发光强度。
  • numPoints: 将存储在 trail 中的位置数量。(数量越多,trail 越长)。
  • height: trail 的高度。
  • minDistance: 两个点之间的最小距离。
  • opacity: trail 的不透明度。
  • duration: trail 结束后开始消退的时间。

如果您还不理解所有参数,请不用担心。我们将在实现 trail 时进行解释。

Cursor 组件中导入 SimpleTrail 组件:

// ...
import { SimpleTrail } from "./SimpleTrail";

export const Cursor = () => {
  // ...

  return (
    <>
      <group ref={target}>{/* ... */}</group>
      <SimpleTrail
        target={target}
        color={color}
        intensity={intensity}
        opacity={opacity}
        height={size}
      />
    </>
  );
};

mesh 由一个 <planeGeometry /> 组成,其段数等于 numPoints。我们将更新每个段的位置以跟随目标。

SimpleTrail

从视觉上看,由于我们的平面尺寸是 1x1,我们可以看到一个正方形,但是因为段数,我们可以操作顶点以创建 trail 效果。

让我们来看看一个有一个段的 plane 和一个有 20 个段的 plane

<group position-x={5}>
  <mesh position-x={4} scale-y={5}>
    <planeGeometry args={[1, 1, 1, numPoints - 1]} />
    <meshBasicMaterial color={"red"} wireframe />
  </mesh>
  <mesh position-x={2} scale-y={5}>
    <planeGeometry args={[1, 1, 1, 1]} />
    <meshBasicMaterial color={"red"} wireframe />
  </mesh>
</group>

这段代码仅用于视觉化理解。理解概念后可以将其移除。

我们在 y 轴 上缩放它们,以便看到段数的区别。

Representation of the segments

您可以看到左侧平面只有 4 个顶点,而右侧平面有更多。我们将操控这些顶点来构建 trail 效果。

我们可以使用 line 来代替 plane 创建 trail,但使用 plane 可以创造出更有趣的效果(例如在模拟风的时候效果更好)。

drei 中的 Trail 组件使用的是 line,我们不想重复编写相同的东西。

操控顶点

我们将更新平面的顶点位置,以便让它随时间跟随目标。

首先,我们需要在数组中存储目标的所有位置。我们将使用一个 ref 来存储这些位置。

// ...
import * as THREE from "three";

export function SimpleTrail(
  {
    // ...
  }
) {
  const mesh = useRef();
  const positions = useRef(
    new Array(numPoints).fill(new THREE.Vector3(0, 0, 0))
  );
  // ...
}

这个数组的长度总是保持为 numPoints,用于存储目标的位置。

当目标移动时,我们会将新的位置添加到数组的前面,并将其他位置向后推。

图表解释位置数组

为了实现这个功能,我们将使用 useFrame 钩子来更新顶点的位置。

// ...
import { useFrame } from "@react-three/fiber";

export function SimpleTrail(
  {
    // ...
  }
) {
  // ...

  useFrame(() => {
    if (!mesh.current || !target?.current) {
      return;
    }

    const curPoint = target.current.position;
    const lastPoint = positions.current[0];

    const distanceToLastPoint = lastPoint.distanceTo(target.current.position);

    if (distanceToLastPoint > minDistance) {
      positions.current.unshift(curPoint.clone());
      positions.current.pop();
    }
  });

  // ...
}

首先,我们计算最后一个点和当前点之间的距离。如果距离大于 minDistance,我们就使用 unshift 将当前点添加到数组前面,并使用 pop 移除最后一个点。

现在我们需要更新平面的顶点位置以跟随目标的位置。

Three.js logoReact logo

React Three Fiber: The Ultimate Guide to 3D Web Development

✨ You have reached the end of the preview ✨

Go to the next level with Three.js and React Three Fiber!

Get full access to this lesson and the complete course when you enroll:

  • 🔓 Full lesson videos with no limits
  • 💻 Access to the final source code
  • 🎓 Course progress tracking & completion
  • 💬 Invite to our private Discord community
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.