Line art shader

라인 아트 셰이더

Drawing layered Lissajous curves with true-width 3D lines to build generative, hand-drawn-looking line art.

Also known as: Line2LineMaterialFat lines
···
js
import * as THREE from 'three';
import { Line2 } from 'three/addons/lines/Line2.js';
import { LineGeometry } from 'three/addons/lines/LineGeometry.js';
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05060c);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0, 5.4);
camera.lookAt(0, 0, 0);

const group = new THREE.Group();
const lines = [];
const CURVES = 6;
for (let c = 0; c < CURVES; c++) {
  const points = [];
  const colors = [];
  const a = 3 + c, b = 2 + (c % 3);
  const col = new THREE.Color().setHSL(c / CURVES, 0.85, 0.6);
  const N = 220;
  for (let i = 0; i <= N; i++) {
    const th = (i / N) * Math.PI * 2;
    const r = 1.5 + 0.15 * c;
    const x = r * Math.sin(a * th);
    const y = r * Math.sin(b * th + c);
    const z = 0.2 * Math.cos(a * th - b * th);
    points.push(x, y, z);
    colors.push(col.r, col.g, col.b);
  }
  const geo = new LineGeometry();
  geo.setPositions(points);
  geo.setColors(colors);
  const mat = new LineMaterial({ linewidth: 2.4, vertexColors: true, transparent: true, opacity: 0.85 });
  mat.resolution.set(innerWidth, innerHeight);
  const line = new Line2(geo, mat);
  line.computeLineDistances();
  group.add(line);
  lines.push({ line, mat });
}
scene.add(group);

function resize() {
  renderer.setSize(innerWidth, innerHeight);
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  lines.forEach(({ mat }) => mat.resolution.set(innerWidth, innerHeight));
}
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  group.rotation.y = time * 0.25;
  group.rotation.x = Math.sin(time * 0.18) * 0.3;
  renderer.render(scene, camera);
});

THREE.LineBasicMaterial has a longstanding limitation: linewidth is ignored on most platforms and lines always render at 1px, because most browser WebGL implementations don’t support thick lines. The Line2 / LineGeometry / LineMaterial trio in three/addons/lines/ works around this — under the hood it doesn’t draw a thin line at all, it expands each segment into a flat, camera-facing quad (a billboard). That’s what produces genuinely thick lines.

Because of that trick, LineMaterial needs a resolution uniform (the viewport’s pixel size) to expand those quads to the correct thickness — so mat.resolution.set(innerWidth, innerHeight) must be called again on every resize. With worldUnits: false (the default), linewidth is fixed in CSS pixels; set it true and it becomes a 3D-space unit, so lines get visually thinner as the camera moves away.

The demo layers six Lissajous curves (x = sin(aθ), y = sin(bθ + phase)) with different a/b values, cycling color across them. geometry.setPositions(flatArray) and setColors(flatArray) load vertices and per-vertex colors in one shot, and vertexColors: true interpolates that color; line.computeLineDistances() precomputes the cumulative-distance data a dashed-line material would need.

When to use

Data art, generative brand patterns, or any diagram or trajectory in 3D space that needs a true line thickness.