← Back to Blog
Media Art Article · 2026.11.08

AI와 만드는 흐름장, 방향을 따라 선을 쌓기

AI와 함께 p5.js의 노이즈로 흐름장을 만들고, 방향을 따라 이동하는 선을 쌓아 추상 이미지를 그립니다. 실행 코드와 결과를 비교하며 선의 밀도와 굽이, 재현성을 조절합니다.

media-artgenerative-artcreative-codingp5jsAI

밝은 종이 위 청록색 실선이 큰 굽이와 소용돌이를 이루는 AI 생성 흐름장 콘셉트 표지

가는 선 수백 개가 한 방향으로 휘어지고, 몇 군데에서는 겹쳐 짙은 띠를 만드는 그림을 그려봅니다. 선의 모양을 하나씩 지정하는 대신, 화면의 각 위치에 이동 방향을 정합니다. 선은 자기 위치의 방향을 읽고 조금 움직인 뒤, 새 위치에서 다시 방향을 읽습니다. 이 과정을 반복하면 짧은 직선 조각들이 연결되어 휘어진 선처럼 보입니다.

이 글에서는 p5.js로 960×540 크기의 정지 이미지를 만듭니다. AI에는 방향 계산과 반복 구조의 초안을 요청하고, 실제 코드에서 바꿀 값을 정리하게 합니다. 최종 그림을 그리는 것은 노이즈와 좌표 계산입니다. 생성형 AI의 이미지 생성과 알고리즘 기반 제너러티브 드로잉은 여기서 서로 다른 역할을 맡습니다.

표지는 AI로 만든 흐름장 콘셉트 이미지입니다. 아래 본문 이미지는 제공한 코드를 브라우저에서 실행한 결과입니다.

선이 따라갈 방향부터 정하기

흐름장은 공간의 각 위치에 방향과 크기를 대응시킨 것으로 생각하면 됩니다. 이번 예제에서는 이동 크기를 항상 2픽셀로 두고 방향만 바꿉니다. 시작점은 일정한 간격의 격자에 놓습니다. 각 선이 기억할 것은 현재 위치인 x, y뿐입니다.

방향을 매번 독립적인 난수로 뽑으면 선이 급하게 꺾이기 쉽습니다. 이 예제는 위치가 가까우면 대체로 가까운 값을 돌려주는 noise()를 사용합니다. p5.js의 noise() 문서는 입력 좌표의 간격으로 값의 변화 정도를 조절하는 방법을 설명합니다. 화면 좌표에 작은 수를 곱해 넣으면 이웃한 지점에서 읽는 값도 천천히 바뀝니다.

AI에 처음부터 "멋진 흐름장 작품"만 요청하면 움직임, 색상, 입자 수가 한꺼번에 늘어날 수 있습니다. 먼저 다음처럼 결과와 계산 규칙을 좁혀 요청합니다. 특정 AI 제품에 종속된 명령이 아니라, 코드를 작성하고 검토할 때 쓸 작업 명세입니다.

p5.js 2.3.1으로 960×540 정지 드로잉을 만들어줘.
24픽셀 여백 안에 24픽셀 간격으로 시작점을 놓아줘.
각 시작점에서 noise(x * scale, y * scale)로 방향을 정하고,
2픽셀씩 최대 100번 이동하며 선을 이어줘.
다음 점이 여백 밖이면 그 선을 끝내줘.
seed, scale, step, steps, gap을 cfg 객체에 모아줘.
noiseSeed로 재현성을 고정하고 noiseDetail은 4, 0.5로 지정해줘.
애니메이션과 외부 이미지 없이 밝은 배경에 반투명한 짙은 청록 선을 그려줘.
각 변수의 역할과 경계 검사 위치를 설명해줘.

AI가 답한 코드에서 먼저 확인할 부분은 두 가지입니다. 시작점 사이의 간격과 선이 이동하는 거리가 다른 값인지, 경계를 벗어나는 점을 그리기 전에 멈추는지 살펴봅니다. gap은 선의 개수에, step은 선 한 가닥을 구성하는 조각의 길이에 영향을 줍니다. 둘을 같은 변수로 묶으면 밀도와 선의 굽이를 따로 비교하기 어렵습니다.

브라우저에서 첫 그림 그리기

이 예제는 p5.js 2.3.1으로 실행했습니다. p5.js 다운로드 페이지에서 설치 방식을 확인할 수 있습니다. 아래 예제에서는 CDN 주소에 버전을 고정합니다. 빈 폴더에 index.html과 sketch.js를 만들고, 먼저 index.html에 아래 내용을 저장합니다. 인터넷에 연결된 상태에서 이 파일을 브라우저로 열면 라이브러리를 불러옵니다.

<!doctype html>
<html lang="ko">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>흐름장 드로잉</title>
<script src="https://cdn.jsdelivr.net/npm/p5@2.3.1/lib/p5.min.js"></script>
<script src="sketch.js"></script>
</html>

다음 코드를 sketch.js에 저장합니다. 이미 열어둔 index.html을 새로고침하면 그림이 나타납니다.

const cfg = { seed: 7, scale: 0.004, step: 2, steps: 100, gap: 24 };

function setup() {
  createCanvas(960, 540);
  pixelDensity(1);
  angleMode(RADIANS);
  noLoop();
  noiseSeed(cfg.seed);
  noiseDetail(4, 0.5);
  background(245, 241, 232);
  noFill();
  stroke(18, 80, 88, 55);
  strokeWeight(0.8);

  for (let sy = 24; sy < height - 24; sy += cfg.gap) {
    for (let sx = 24; sx < width - 24; sx += cfg.gap) {
      let x = sx;
      let y = sy;
      beginShape();
      vertex(x, y);
      for (let i = 0; i < cfg.steps; i++) {
        const angle = noise(x * cfg.scale, y * cfg.scale) * TWO_PI * 2;
        const nx = x + cos(angle) * cfg.step;
        const ny = y + sin(angle) * cfg.step;
        if (nx < 24 || nx > width - 24 || ny < 24 || ny > height - 24) break;
        vertex(nx, ny);
        x = nx;
        y = ny;
      }
      endShape();
    }
  }
}

p5.js 예제에서 실행한 흐름장 드로잉. 밝은 배경 위 청록색 선들이 휘어지고 겹쳐 짙은 띠를 만든다.

기준 설정의 실제 실행 결과입니다. 격자 위 798개 시작점에서 선을 그렸습니다. 같은 방향을 따라 모인 선은 짙어지고, 선 사이가 벌어진 곳은 밝게 남습니다. stroke()의 마지막 값인 55가 투명도를 정하므로, 겹침 자체가 농도 차이를 만듭니다. 선은 서로 밀어내거나 충돌을 피하지 않습니다.

방향 계산에서 TWO_PI는 라디안 단위의 한 바퀴입니다. 여기에 2를 더 곱해 노이즈 값의 변화를 각도에 크게 반영했습니다. cos(angle)과 sin(angle)은 그 방향으로 움직일 가로·세로 성분을 만듭니다. 새 점을 이으면 직선 조각 하나가 생기고, 그 점에서 다음 이동 방향을 계산합니다. 부드러운 곡선 함수를 호출하는 방식은 아닙니다.

각 선은 최대 100번 이동하므로 경계에 닿지 않았다면 경로 길이가 200픽셀입니다. 이것은 시작점과 끝점 사이의 직선거리가 아닙니다. 선이 여백을 넘으려 하면 더 짧게 끝납니다. 화면 오른쪽이나 아래쪽에서 선이 끊겨 보여도 먼저 경계 조건에 따른 결과인지 확인합니다.

한 번에 하나의 값만 바꾸기

첫 비교에서는 cfg의 scale만 0.004에서 0.008로 바꾸고 새로고침합니다. 화면 좌표가 노이즈 공간에서 두 배 넓은 범위를 읽습니다. 이 설정으로 실행한 결과에서는 큰 흐름 안의 굽이가 더 작고 여러 군데로 나뉘었습니다. 값을 키우는 일이 모든 그림에서 같은 곡률이나 균형을 보장하지는 않으므로, 같은 시드에서 두 결과를 나란히 보며 선택합니다.

다음에는 scale을 0.004로 돌리고 gap만 24에서 48로 바꿉니다. 실제 시작점은 798개에서 209개로 줄었습니다. 방향을 만드는 노이즈는 그대로지만 선 사이의 빈 공간이 넓어지고 짙은 띠가 약해집니다. 격자 간격을 두 배로 했을 때 선 개수가 정확히 4분의 1이 아닌 것은 캔버스 크기와 여백 안에 들어가는 격자점 개수를 세기 때문입니다.

구도 자체를 바꾸려면 gap을 24로 돌린 뒤 seed를 7에서 8로 바꿉니다. 시작점 격자는 유지되고 각 위치의 노이즈 값이 바뀝니다. noiseSeed() 문서에 따라 시드를 다시 7로 지정하면 같은 노이즈를 사용할 수 있습니다. 이 글의 브라우저 실행에서는 기준 설정으로 복원했을 때 좌표와 출력 이미지가 처음 결과와 일치했습니다. 재현용으로 코드와 함께 p5.js 버전도 기록합니다.

여러 값을 동시에 바꾸고 싶다면 AI에 수정 전에 비교 계획을 쓰게 합니다. "시드와 선의 색은 유지하고 시작점 간격만 바꿔줘. 바뀌어야 하는 줄과 그대로 둘 줄을 구분해줘"라고 요청합니다. 출력이 마음에 들지 않을 때도 "더 유기적으로"라는 말만 덧붙이기보다, 큰 굽이를 유지할지 선을 더 드물게 놓을지 결정하면 수정 전후를 비교하기 쉽습니다.

noiseDetail(4, 0.5)는 노이즈를 구성하는 옥타브 수와 각 단계의 영향력을 고정합니다. noiseDetail() 문서가 설명하는 별도의 조절값입니다. 처음에는 이 값을 유지하고 scale의 효과부터 확인합니다. 서로 다른 설정을 한꺼번에 바꾸면 그림이 달라진 원인을 찾기 어렵습니다.

선의 밀도를 작품의 선택으로 만들기

이 예제에서는 짙은 띠가 생기는 위치를 직접 그리지 않았습니다. 위치별 방향, 시작점 배치, 반투명 선의 겹침이 함께 그 위치를 만듭니다. 더 조밀한 결과를 원할 때는 먼저 gap을 줄여 봅니다. 같은 방향 구조를 더 많은 선으로 읽게 됩니다. 반대로 빈 공간을 드러내려면 선을 굵게 하기 전에 gap을 넓힌 결과부터 비교합니다. 선의 밀도와 굵기는 화면에 다른 흔적을 남깁니다.

실제로 검증한 범위는 기준 그림, scale 변경, gap 변경, seed 변경과 기준 설정 복원입니다. 각 선의 점이 여백 안에 있고, 연결한 조각의 길이가 2픽셀인지도 확인했습니다. 방향 값을 일정하게 주는 별도 검사에서는 모든 선이 오른쪽 수평으로 이어지는지 확인했습니다. 애니메이션, 인쇄 색상, 펜 플로터 출력은 이 예제의 검증 범위에 포함하지 않았습니다.

움직이는 작품으로 확장할 때는 무엇을 움직일지 먼저 정합니다. 시작점을 움직이면 같은 방향 분포를 선들이 지나가고, 노이즈에 시간 입력을 더하면 방향 분포 자체가 변합니다. 현재 코드의 noLoop()를 지우는 것만으로 이 변화가 생기지는 않습니다. 그림을 매번 그릴 draw()와 시간에 따라 바뀌는 입력을 별도로 설계해야 합니다.

화면이 바람이나 물결을 연상시키더라도 이 코드는 유체의 압력이나 질량 보존을 계산하지 않습니다. 노이즈에서 만든 방향을 따라가는 추상 드로잉입니다. 작품 설명에서도 이 차이를 밝혀두면 시각적 인상과 계산 원리를 구분할 수 있습니다.

마음에 드는 결과를 고른 뒤에는 시드와 다섯 설정값, 라이브러리 버전, 출력 이미지를 함께 보관합니다. 다음 수정에서는 그 기준으로 돌아와 한 가지 선택만 바꿉니다. AI가 코드를 늘리는 속도보다, 어떤 변화가 선의 형태를 바꾸었는지 확인하는 과정이 다음 작품의 판단 기준이 됩니다.