라인 아트 셰이더

Line art shader

두께가 살아있는 3D 선으로 리사주 곡선을 여러 겹 그려, 손으로 그린 듯한 생성적 라인 아트를 만드는 기법.

다른 이름: 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의 고질적인 한계는 선 두께(linewidth)가 대부분의 플랫폼에서 무시되고 항상 1px로만 그려진다는 것입니다(브라우저 WebGL 구현체 대부분이 굵은 선을 지원하지 않기 때문입니다). three/addons/lines/의 Line2 · LineGeometry · LineMaterial 세트는 이 한계를 우회합니다 — 실제로는 얇은 선이 아니라, 각 선분을 카메라를 향하는 납작한 사각형(billboard quad)으로 확장해서 그리는 것입니다. 그래서 진짜 두께를 가진 선이 나옵니다.

이 방식 때문에 LineMaterial은 resolution 유니폼(뷰포트의 픽셀 크기)을 알아야 사각형을 올바른 두께로 펼칠 수 있습니다 — 그래서 mat.resolution.set(innerWidth, innerHeight)를 리사이즈 때마다 반드시 다시 호출해야 합니다. worldUnits: false(기본값)면 linewidth는 CSS 픽셀 단위로 고정되고, true면 3D 공간 단위가 되어 카메라가 멀어질수록 선도 가늘어 보입니다.

데모는 a·b 값이 다른 리사주 곡선(x = sin(aθ), y = sin(bθ + phase)) 6개를 색상환을 돌려가며 겹쳐 그립니다. geometry.setPositions(flatArray)와 setColors(flatArray)로 정점과 정점별 색을 한 번에 넣고, vertexColors: true로 그 색을 보간해서 씁니다 — line.computeLineDistances()는 점선(dashed) 재질을 쓸 때 필요한 누적 거리 정보를 미리 계산해 둡니다.

언제 쓰나

데이터 아트, 생성적 브랜드 패턴, 3D 공간에서 두께가 살아있는 다이어그램/궤적을 그려야 할 때.