Estelas

Starter pack

¡Vamos a sumergirnos en el mundo de las estelas! Las estelas son una excelente manera de agregar un sentido de movimiento a tu escena. Pueden usarse para crear una variedad de efectos, como estelas de luz, estelas de humo, o incluso la estela de un objeto en movimiento.

Aquí está el proyecto final que construiremos juntos:

Comenzaremos creando un efecto de estela simple usando un cursor de estela personalizado. Luego exploraremos el componente Trail de drei para hacer los cometas que viste en la vista previa.

Proyecto inicial

El proyecto inicial contiene muchas cosas que ya hemos cubierto en lecciones anteriores:

Además, utilicé Tailwind CSS para diseñar rápidamente la interfaz de usuario. Si no estás familiarizado con Tailwind CSS, puedes omitir la parte de UI y concentrarte en la parte de Threejs.

Los modelos WawaCoin y WawaCard están hechos internamente y están disponibles en el proyecto inicial. Usé el MeshTransmissionMaterial de drei para crear este aspecto futurista.

Siéntete libre de transformar la escena a tu gusto. Puedes reutilizar libremente cualquier parte del proyecto en tus propios proyectos.

Se me olvidó mencionar, pero el contenido del sitio web es puramente ficticio. No estoy lanzando una nueva criptomoneda. (¿Todavía? 👀)

Cursor personalizado con rastro

Comencemos creando un efecto simple de rastro que sigue al cursor.

Crea un nuevo archivo components/Cursor.jsx y añade el siguiente código:

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>
    </>
  );
};

Es una esfera sencilla que sigue una onda senoidal. Puedes ajustar el tamaño, color, intensidad y opacidad del cursor usando los controles de Leva.

Por ahora utilizamos un movimiento fijo, lo que simplificará la visualización del rastro. Más adelante lo reemplazaremos con la posición del ratón.

Añade el componente Cursor al componente Experience:

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

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

// ...

Podemos ver una esfera en movimiento, que será el objetivo de nuestro rastro.

Componente SimpleTrail

El group es el objetivo que nuestro rastro seguirá. Crearemos un nuevo componente components/SimpleTrail.jsx para crear el efecto de rastro:

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>
    </>
  );
}

Los parámetros son los siguientes:

  • target: el ref del objetivo a seguir.
  • color: el color del rastro.
  • intensity: la intensidad emisiva del rastro.
  • numPoints: el número de posiciones que almacenaremos en el rastro. (A mayor número, más largo el rastro).
  • height: la altura del rastro.
  • minDistance: la distancia mínima entre dos puntos.
  • opacity: la opacidad del rastro.
  • duration: el tiempo antes de que el rastro comience a desvanecerse desde su final.

No te preocupes si no entiendes todos los parámetros aún. Los explicaremos mientras implementamos el rastro.

Importa el componente SimpleTrail en el componente Cursor:

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

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

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

El mesh está compuesto por un <planeGeometry /> con un número de segmentos igual a numPoints. Actualizaremos la posición de cada segmento para seguir al objetivo.

SimpleTrail

Visualmente, como nuestro plano es de tamaño 1x1, podemos ver un cuadrado, pero debido a la cantidad de segmentos, podremos manipular los vértices para crear el efecto de rastro.

Veamos lado a lado un plane con un segmento y un plane con 20 segmentos:

<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>

Este código es solo para propósitos de visualización. Puedes eliminarlo después de entender el concepto.

Los escalamos en el eje y para ver la diferencia en el número de segmentos.

Representación de los segmentos

Puedes ver que el plano de la izquierda solo tiene 4 vértices mientras que el plano de la derecha tiene muchos más. Manipularemos estos vértices para construir el efecto de rastro.

Podríamos usar una line en lugar de un plane para crear el rastro, pero usar un plane nos permite crear un efecto interesante (Funciona mejor para el viento, por ejemplo).

El componente Trail de drei usa una line, no queremos reescribir lo mismo.

Manipulando los vértices

Actualizaremos la posición de los vértices del plano para seguir el objetivo a lo largo del tiempo.

Primero necesitaremos almacenar todas las posiciones del objetivo en un arreglo. Usaremos un ref para almacenar las posiciones.

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

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

Este arreglo siempre tendrá una longitud de numPoints y almacenará las posiciones del objetivo.

Cuando el objetivo se mueva, añadiremos la nueva posición al frente del arreglo, empujando las otras posiciones hacia atrás.

Gráfico explicando el arreglo de posiciones

Para implementar esto, utilizaremos el hook useFrame para actualizar la posición de los vértices.

// ...
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();
    }
  });

  // ...
}

Primero, calculamos la distancia entre el último punto y el punto actual. Si la distancia es mayor que minDistance, añadimos el punto actual al frente del arreglo con unshift y eliminamos el último punto con pop.

Ahora necesitamos actualizar la posición de los vértices del plano para seguir las posiciones del objetivo.

End of lesson preview

To get access to the entire lesson, you need to purchase the course.