Line, LineSegments y LineLoop: tres formas de unir vértices
Qué primitiva de WebGL usa cada una, cómo cambia la interpretación del mismo buffer de posiciones, y qué hace falta para que una línea discontinua funcione.
Las tres clases comparten geometría, material y jerarquía; lo único que cambia es cómo le dice Three.js a la GPU que conecte los vértices del buffer. Suena a detalle menor hasta que dibujas la misma lista de puntos con las tres y obtienes tres dibujos completamente distintos, o hasta que una línea discontinua se niega a mostrar guiones y no hay ningún error en la consola.
- Asociar cada clase con su primitiva de WebGL y con la forma en que consume el buffer.
- Elegir la clase correcta para un contorno, un conjunto de segmentos sueltos y un polígono cerrado.
- Generar el contorno de una malla con
EdgesGeometrysin escribir los vértices a mano. - Aplicar
LineDashedMaterialcorrectamente, incluida la llamada que casi nadie recuerda.
La misma geometría, tres lecturas
Toma un atributo position con seis vértices, A, B, C, D, E y F, en ese orden.
Line los interpreta como una polilínea abierta: dibuja A-B, B-C, C-D, D-E, E-F. Cinco segmentos con seis vértices. Es el modo LINE_STRIP de WebGL y es lo más eficiente en memoria para un trazo continuo, porque cada vértice intermedio sirve para dos segmentos.
LineSegments los interpreta por parejas: dibuja A-B, C-D, E-F. Tres segmentos independientes con seis vértices. Es el modo LINES. Cada segmento cuesta dos vértices aunque comparta extremo con otro, y a cambio puedes dibujar un conjunto de aristas inconexas en una sola llamada.
LineLoop es una polilínea que además cierra: A-B, B-C, C-D, D-E, E-F, F-A. Es el modo LINE_LOOP y ese último segmento de vuelta lo pone la GPU, no tu buffer.
Las tres aceptan índice, igual que una malla. Con índice, el buffer de posiciones se comparte y el de índices decide el recorrido, que es exactamente lo que necesitas para dibujar el esqueleto de una malla sin duplicar ningún vértice.
El contorno de una malla, gratis
Escribir a mano los doce segmentos de las aristas de un cubo es un ejercicio aburrido y propenso a erratas. Three.js trae dos generadores que producen geometrías pensadas para LineSegments.
WireframeGeometry devuelve todas las aristas de todos los triángulos. Es lo mismo que material.wireframe = true pero como objeto separado, así que puedes darle su propio color y su propio renderOrder.
EdgesGeometry devuelve solo las aristas donde el ángulo entre las dos caras adyacentes supera un umbral en grados. Con el valor por defecto de uno, en un cubo te da las doce aristas reales y ninguna diagonal interna. Es la herramienta correcta para el aspecto de plano técnico.
import * as THREE from 'three';
const scene = new THREE.Scene();
const caja = new THREE.BoxGeometry(2, 2, 2);
// Solo las aristas donde hay un cambio de orientacion mayor de 15 grados.
const aristas = new THREE.EdgesGeometry(caja, 15);
const contorno = new THREE.LineSegments(
aristas,
new THREE.LineBasicMaterial({ color: 0x89b4fa })
);
const solido = new THREE.Mesh(
caja,
new THREE.MeshBasicMaterial({ color: 0x1e1e2e })
);
// El solido rellena el z-buffer y evita que se vean las aristas traseras.
solido.material.polygonOffset = true;
solido.material.polygonOffsetFactor = 1;
solido.material.polygonOffsetUnits = 1;
scene.add(solido, contorno);
El polygonOffset de ahí resuelve un problema muy concreto: las aristas están exactamente en la superficie del sólido, así que sus fragmentos compiten con los del sólido por el mismo valor de profundidad y aparecen puntos de la línea que desaparecen y vuelven según el redondeo del interpolador. Empujando el sólido un pelo hacia atrás en profundidad, la línea gana siempre.
Líneas discontinuas y la llamada que falta
LineDashedMaterial no calcula la distancia recorrida por su cuenta: la lee de un atributo llamado lineDistance que tiene que existir en la geometría. Ese atributo lo genera el método computeLineDistances() del objeto, y hay que llamarlo a mano después de crear la línea.
const puntos = new THREE.CatmullRomCurve3([
new THREE.Vector3(-4, 0, 0),
new THREE.Vector3(-1, 2, 1),
new THREE.Vector3(2, -1, -1),
new THREE.Vector3(4, 1, 0)
]).getPoints(200);
const geometria = new THREE.BufferGeometry().setFromPoints(puntos);
const material = new THREE.LineDashedMaterial({
color: 0xf9e2af,
dashSize: 0.3,
gapSize: 0.15,
scale: 1
});
const trazo = new THREE.Line(geometria, material);
trazo.computeLineDistances(); // sin esto no hay guiones, y no hay error
scene.add(trazo);
dashSize y gapSize están en las mismas unidades que las posiciones, y scale divide a las dos: subirlo a 2 hace los guiones y los huecos la mitad de largos sin tocar los otros dos valores. Si mueves los vértices del trazo, hay que volver a llamar a computeLineDistances(), porque el atributo guarda longitudes absolutas que ya no corresponden.
El método asume geometría no indexada. Si la geometría tiene índice, en r184 no calcula nada: emite un aviso por consola diciendo que el cálculo solo es posible con BufferGeometry no indexada y devuelve sin crear el atributo. La línea se dibuja entera, sin guiones, y el aviso se pierde entre el resto de mensajes. Si vienes de un EdgesGeometry, no te afecta, porque genera geometría no indexada; si vienes de una geometría propia con índice, tienes que llamar antes a geometry.toNonIndexed().
LineSegments sobrescribe computeLineDistances() para recorrer el buffer de dos en dos. La sutileza está en cómo arranca cada pareja: el primer vértice de un segmento hereda la distancia acumulada del final del segmento anterior, no reinicia a cero. Es decir, el patrón de guiones continúa de un segmento al siguiente como si fueran uno solo, pero el salto en el espacio entre ellos no consume longitud de patrón. El efecto práctico es que dos conjuntos de aristas idénticos en forma pero generados en distinto orden producen patrones de guiones distintos, y no hay forma de controlarlo desde el material. Si necesitas que cada segmento empiece con un guion completo, tienes que escribir tú el atributo lineDistance reiniciándolo a cero en cada pareja.
Cuándo cada una
LineLoop parece la opción natural para un polígono cerrado, y lo es, pero tiene una limitación que conviene saber antes de elegirla: hereda el computeLineDistances() de Line, que recorre el buffer de principio a fin sin contemplar el segmento de cierre. Una LineLoop con material discontinuo tendrá el último tramo sin patrón definido. Para un polígono cerrado con guiones, repite el primer punto al final del array y usa Line.
LineSegments es la elección por defecto para todo lo que sea un conjunto de aristas: contornos, rejillas, ejes, cajas de depuración, esqueletos. De hecho, los ayudantes que trae Three.js, como GridHelper o Box3Helper, son LineSegments por dentro.
Line es para trazos: curvas, caminos, gráficas, trayectorias. Y en cuanto necesites que ese trazo tenga más de un píxel de grosor, ninguna de las tres sirve y hay que cambiar de enfoque, que es el tema de la lección siguiente.