Cómo crear un icosaedro low-poly con Three.js

Yaxito · 5 min de lectura

Qué es “low-poly” en la práctica

El estilo low-poly no es más que usar geometrías con pocas caras y hacer que esas caras se vean como caras planas —en vez de suavizarlas para simular una superficie curva—. En Three.js, la pieza más simple para conseguirlo es un icosaedro: un poliedro de 20 caras triangulares que, sin ninguna subdivisión, ya tiene ese aspecto geométrico y anguloso tan característico.

La geometría: IcosahedronGeometry sin subdivisiones

new THREE.IcosahedronGeometry(radius, detail)

El segundo parámetro, detail, es la clave: en 0 obtienes el icosaedro original de 20 caras. Cada valor por encima subdivide las caras en triángulos más pequeños, acercándose a una esfera perfecta a medida que sube. Para el efecto low-poly, quieres detail={0} (o dejarlo sin especificar, que es el valor por defecto).

Caras planas: flatShading

Por defecto, Three.js interpola las normales entre vértices compartidos para simular una superficie suave (Gouraud/Phong shading). Eso es exactamente lo que no queremos aquí: para que cada triángulo se vea como una cara plana con su propio tono de luz, hay que decírselo al material:

<mesh geometry={new THREE.IcosahedronGeometry(1.6, 0)}>
  <meshStandardMaterial color="#fa87a8" flatShading />
</mesh>

Con flatShading activado, el material calcula la normal de cada cara en lugar de interpolar entre vértices, y el resultado son facetas visualmente diferenciadas — el aspecto low-poly real, no solo la geometría angulosa.

El contorno: un segundo mesh wireframe

Para reforzar la silueta (muy útil en un hero con fondo oscuro), un truco habitual es superponer un segundo mesh con la misma geometría, ligeramente más grande, en modo wireframe:

function LowPolyIcosahedron() {
  const geometry = useMemo(() => new THREE.IcosahedronGeometry(1.6, 0), [])

  return (
    <group>
      <mesh geometry={geometry}>
        <meshStandardMaterial color="#fa87a8" flatShading />
      </mesh>
      <mesh geometry={geometry} scale={1.02}>
        <meshBasicMaterial color="#ffffff" wireframe transparent opacity={0.15} />
      </mesh>
    </group>
  )
}

El scale={1.02} es lo que evita el z-fighting (parpadeo por solapamiento de dos superficies a la misma profundidad): al ser un 2% más grande, el wireframe queda justo por delante de las caras sólidas, dibujando el contorno de cada arista sin competir por los mismos píxeles.

Animarlo con useFrame

Igual que con un campo de partículas, conviene rotar usando el delta de useFrame en vez de un contador manual, para que la velocidad no dependa del framerate del dispositivo:

const groupRef = useRef<THREE.Group>(null)

useFrame((_, delta) => {
  if (!groupRef.current) return
  groupRef.current.rotation.y += delta * 0.15
  groupRef.current.rotation.x += delta * 0.05
})

Y respetar prefers-reduced-motion igual que en cualquier animación decorativa: comprobarlo dentro de useFrame y saltarte la actualización de rotación si el usuario lo ha pedido.

Combinándolo con un fondo de partículas

El icosaedro low-poly y un campo de partículas esférico comparten cámara y Canvas sin problema — solo hay que montarlos como hermanos dentro de la misma escena:

<Canvas camera={{ position: [0, 0, 7], fov: 45 }}>
  <ambientLight intensity={0.6} />
  <pointLight position={[5, 5, 5]} intensity={1} />
  <LowPolyIcosahedron />
  <ParticleField />
</Canvas>

Un detalle a tener en cuenta: a diferencia de pointsMaterial (que no necesita luces), meshStandardMaterial sí reacciona a la iluminación de la escena. Sin al menos una luz, el icosaedro se verá completamente negro — un ambientLight suave más una pointLight direccional suele bastar para que las facetas planas se distingan unas de otras.

Posts relacionados