Cómo crear un campo de partículas 3D con React Three Fiber

Yaxito · 6 min de lectura

Por qué React Three Fiber

@react-three/fiber (R3F) es un renderer de React para Three.js: en vez de crear la escena a mano con new THREE.Scene(), new THREE.Mesh(), etc., la describes con componentes JSX (<mesh>, <points>, <bufferGeometry>…) y R3F se encarga de sincronizarlos con el árbol de Three.js por debajo. Ganas el modelo mental de React —props, hooks, composición— sin perder acceso a la API completa de Three.js cuando la necesitas.

Un campo de partículas es un buen primer proyecto porque toca las piezas clave: geometría por buffers, materiales, y animación por frame con useFrame.

La idea: puntos repartidos en una esfera

Un THREE.Points necesita un BufferGeometry con un atributo position: un Float32Array plano de [x0, y0, z0, x1, y1, z1, ...]. La tentación es generar x, y, z aleatorios dentro de un cubo, pero eso agrupa partículas visualmente en las esquinas y aristas del cubo — la densidad no es uniforme.

La solución es generar las posiciones en coordenadas esféricas y convertirlas a cartesianas:

const radius = 4 + Math.random() * 6       // entre 4 y 10 unidades del centro
const theta = Math.random() * Math.PI * 2  // ángulo horizontal, 0 a 2π
const phi = Math.acos(Math.random() * 2 - 1) // ángulo vertical, con acos para distribución uniforme

const x = radius * Math.sin(phi) * Math.cos(theta)
const y = radius * Math.sin(phi) * Math.sin(theta)
const z = radius * Math.cos(phi)

El detalle importante es Math.acos(Math.random() * 2 - 1) en vez de Math.random() * Math.PI: sin el acos, las partículas se amontonan en los polos de la esfera. Con él, la distribución es uniforme sobre toda la superficie.

El componente completo

import { useMemo, useRef } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import * as THREE from 'three'

const PARTICLE_COUNT = 900
const PALETTE = ['#fa87a8', '#ffad8a', '#ee81e9', '#b9f987']

const prefersReducedMotion = () =>
  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches

function ParticleField() {
  const pointsRef = useRef<THREE.Points>(null)

  const { positions, colors } = useMemo(() => {
    const pos = new Float32Array(PARTICLE_COUNT * 3)
    const col = new Float32Array(PARTICLE_COUNT * 3)
    const tmpColor = new THREE.Color()

    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const radius = 4 + Math.random() * 6
      const theta = Math.random() * Math.PI * 2
      const phi = Math.acos(Math.random() * 2 - 1)

      pos[i * 3] = radius * Math.sin(phi) * Math.cos(theta)
      pos[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta)
      pos[i * 3 + 2] = radius * Math.cos(phi)

      tmpColor.set(PALETTE[i % PALETTE.length])
      col[i * 3] = tmpColor.r
      col[i * 3 + 1] = tmpColor.g
      col[i * 3 + 2] = tmpColor.b
    }
    return { positions: pos, colors: col }
  }, [])

  useFrame((_, delta) => {
    if (!pointsRef.current || prefersReducedMotion()) return
    pointsRef.current.rotation.y += delta * 0.02
    pointsRef.current.rotation.x += delta * 0.005
  })

  return (
    <points ref={pointsRef}>
      <bufferGeometry>
        <bufferAttribute attach="attributes-position" args={[positions, 3]} />
        <bufferAttribute attach="attributes-color" args={[colors, 3]} />
      </bufferGeometry>
      <pointsMaterial
        size={0.05}
        vertexColors
        transparent
        opacity={0.8}
        sizeAttenuation
        depthWrite={false}
      />
    </points>
  )
}

export default function Hero3D() {
  return (
    <Canvas camera={{ position: [0, 0, 7], fov: 45 }} dpr={[1, 1.5]} gl={{ antialias: true, alpha: true }}>
      <ParticleField />
    </Canvas>
  )
}

Los detalles que marcan la diferencia

Cuándo cargar esto en una página real

Three.js y React Three Fiber no son gratis en peso ni en CPU/GPU. Si lo usas como fondo decorativo (un hero, por ejemplo), tiene sentido:

Posts relacionados