목차

Wiki/도구

Processing 설치

  1. 1내 컴퓨터 확인
  2. 2내려받기
  3. 3설치
  4. 4확인
  5. 5라이브러리 설치
  6. 6수업에서 한 실습을 다시 하려면
  7. 7더 둘러보기
  8. 8수업이 끝나면
  9. 9막힐 때

Processing 설치

Processing은 그림을 그리는 코드를 바로 실행해 보는 프로그램입니다. 3회차(9월 5일, 이정섭 강사)에서 관객의 몸과 움직임에 반응하는 작품을 만들 때 씁니다. 수업 전에 집에서 설치해 오세요. 프로그램 500MB에 아래 라이브러리 570MB까지, 1GB가 넘게 받아야 해서 현장 와이파이로는 오래 걸립니다.

내 컴퓨터 확인

  • Windows — 요즘 노트북은 거의 다 64비트입니다. 그대로 진행하면 됩니다.
  • Mac — 왼쪽 위 사과 메뉴 → 이 Mac에 관하여. 「칩」에 Apple M1/M2/M3/M4 가 보이면 Apple Silicon, 「프로세서」에 Intel 이 보이면 Intel입니다. 둘 중 어느 쪽인지 기억해 두세요.

내려받기

processing.org/download 에서 내 컴퓨터에 맞는 파일을 받습니다. 지금 버전은 4.5.6 입니다.

컴퓨터받을 파일
WindowsWindows (Intel 64-bit) 의 설치 파일(.msi)
Mac, Apple SiliconmacOS (Apple Silicon).dmg
Mac, IntelmacOS (Intel 64-bit).dmg

설치

Windows

  1. 받은 .msi 파일을 더블클릭합니다.
  2. 옵션은 전부 기본값 그대로 Next, 마지막에 Install.
  3. 「Windows의 PC 보호」 파란 창이 뜨면 추가 정보실행.
  4. 시작 메뉴에서 Processing 을 찾아 실행합니다.

Mac

  1. 받은 .dmg 파일을 더블클릭합니다.
  2. 열린 창에서 Processing 아이콘을 Applications 폴더로 끌어다 놓습니다.
  3. 응용 프로그램 폴더에서 Processing을 실행합니다.

확인

Processing을 열면 흰 편집 창이 하나 뜹니다. 아래 코드를 복사해 붙여 넣고 왼쪽 위 ▶ (Run) 을 누르세요. 끌 때는 ■ (Stop) 이나 창 닫기.

이 코드가 만드는 정원입니다:

영상처럼 나무가 자라고 가지 끝에 전구가 켜지면 설치가 잘 된 것입니다. 마우스를 움직이면 정원을 빙 둘러볼 수 있고, 나무는 천천히 보는 사람 쪽을 향해 자랍니다. R은 새 나무, M은 소리 켜고 끄기, S는 화면 저장, V는 녹화입니다.

java
// 회로 정원.
// 나무 한 그루가 빛을 찾아 자랍니다. 공중의 빛 알갱이를 향해 가지를 뻗고, 닿은 빛은 사라집니다.
// 새 빛은 관객(카메라) 쪽에 더 많이 생기므로 나무는 보는 사람을 향해 자랍니다.
// 더 자랄 곳이 없는 가지 끝에는 전구가 켜집니다. 다 자란 나무는 전구가 꺼지고 잎이 지고 먹이 마릅니다.
//
// 공간 점유 알고리즘(Runions, Lane, Prusinkiewicz 2007).
// OpenProcessing 의 Peter Jacobs, "Procedural Tree in 3D"(CC BY-SA) 를 바탕으로 다시 썼습니다.
//
// 소리: 자바에 내장된 신시사이저(Gervill)가 data/ 폴더의 사운드폰트로 연주합니다.
// 전구가 켜질 때 피아노 한 음, 계절마다 바뀌는 화음의 패드, 빛이 닿을 때 첼레스타 한 알, 나무가 질 때 낮은 피아노.
//
// 마우스: 정원 둘레를 돕니다. 휠: 다가가거나 물러납니다. 클릭: 관객 쪽에 빛을 한 줌.
// 키: R 새 나무, V 녹화 시작/멈춤, S 한 장 저장, M 소리 끄기/켜기.

import javax.sound.midi.*;

color PAPER = #FAF8F3;
color INK = #14141A;
color LEAF = #8DB34A;
color LIGHT = #E0A020;
color[] palette = { #2F6BFF, #FFD23F, #E68BC5, #8DB34A };

float STEP = 10;       // 가지가 한 번에 자라는 길이
float REACH = 150;     // 이 거리 안의 빛만 가지를 끌어당긴다
float KILL = 22;       // 이 거리 안에 들어온 빛은 닿은 것으로 치고 사라진다
int MAX_NODES = 3200;  // 나무의 최대 크기. 넘으면 한 철이 끝난다
int MAX_LIGHTS = 600;  // 공중에 떠 있는 빛의 최대 수

ArrayList<Node> nodes = new ArrayList<Node>();
ArrayList<PVector> lights = new ArrayList<PVector>();
int stall = 0;         // 자라지 못한 프레임 수
float fade = 0;        // 0 선명, 1 사라짐
boolean dying = false;

PVector eye = new PVector();
float camYaw = 0, camPitch = 0.5, camDist = 1000, zoom = 1000, orbit = 0, idle = 0;
boolean recording = false;

// 소리. 계절(나무 한 그루)마다 화음과 음계가 바뀐다
String SOUNDFONT = "MuseScore_General.sf2";
int[] ROOTS = { 60, 57, 53, 55 };                                           // C, A, F, G
int[][] CHORDS = { {0, 4, 7, 11}, {0, 3, 7, 10}, {0, 4, 7, 11}, {0, 4, 7, 9} };
int[][] SCALES = { {0, 2, 4, 7, 9}, {0, 3, 5, 7, 10}, {0, 2, 4, 7, 9}, {0, 2, 4, 7, 9} };
Synthesizer synth;
MidiChannel piano, pad, bell;
boolean sound = false, muted = false;
int season = -1, pending = -1, lastBell = -100;
ArrayList<int[]> ringing = new ArrayList<int[]>();  // {채널, 음, 끌 프레임}

void setup() {
  size(1280, 720, P3D);
  frameRate(30);
  smooth(8);
  surface.setTitle("회로 정원 · OPEN CIRCUIT BUSAN");
  soundSetup();
  plant();
}

// 새 나무. 줄기 한 뼘과 빛 구름
void plant() {
  nodes.clear();
  lights.clear();
  nodes.add(new Node(new PVector(0, 0, 0), null));
  for (int i = 1; i <= 6; i++) {
    nodes.add(new Node(new PVector(0, -STEP * i, 0), nodes.get(i - 1)));
  }
  for (int i = 0; i < 350; i++) lights.add(lightAt(false));
  stall = 0;
  fade = 0;
  dying = false;
  newSeason();
}

// 공중 어딘가의 빛 한 알. toEye 면 관객이 있는 쪽에 치우친다
PVector lightAt(boolean toEye) {
  while (true) {
    PVector p = PVector.random3D().mult(pow(random(1), 1 / 3.0) * 300);
    p.y = p.y * 0.8 - 270;
    if (p.y > -50) continue;
    if (toEye) {
      float side = (p.x * eye.x + p.z * eye.z) / (sqrt(p.x * p.x + p.z * p.z) * sqrt(eye.x * eye.x + eye.z * eye.z) + 1);
      if (random(1) > 0.5 + 0.5 * side) continue;
    }
    return p;
  }
}

void keyPressed() {
  if (key == 'r' || key == 'R') {
    restart();
  } else if (key == 'v' || key == 'V') {
    recording = !recording;
    surface.setTitle(recording ? "회로 정원 ● REC" : "회로 정원 · OPEN CIRCUIT BUSAN");
  } else if (key == 's' || key == 'S') {
    saveFrame("garden-######.png");
  } else if (key == 'm' || key == 'M') {
    muted = !muted;
    if (sound) for (MidiChannel ch : synth.getChannels()) ch.controlChange(7, muted ? 0 : 100);
  }
}

void mousePressed() {
  for (int i = 0; i < 40 && lights.size() < MAX_LIGHTS; i++) lights.add(lightAt(true));
}

void mouseWheel(MouseEvent event) {
  zoom = constrain(zoom + event.getCount() * 60, 450, 1800);
}

void draw() {
  background(PAPER);

  // 관객. 마우스를 부드럽게 따라 돌고, 가만두면 저절로 천천히 돈다
  idle = (mouseX == pmouseX && mouseY == pmouseY) ? idle + 1 : 0;
  if (idle > 90) orbit += 0.0025;
  camYaw = lerp(camYaw, map(mouseX, 0, width, -PI, PI) + orbit, 0.04);
  camPitch = lerp(camPitch, map(mouseY, 0, height, 1.0, 0.1), 0.04);
  camDist = lerp(camDist, zoom, 0.08);
  eye.set(cos(camYaw) * cos(camPitch) * camDist, -sin(camPitch) * camDist, sin(camYaw) * cos(camPitch) * camDist);
  camera(eye.x, eye.y, eye.z, 0, -230, 0, 0, 1, 0);

  // 땅. 가운데가 아주 조금 어둡다
  noStroke();
  for (int i = 5; i >= 1; i--) {
    pushMatrix();
    translate(0, -0.1 * (6 - i), 0);
    rotateX(HALF_PI);
    fill(lerpColor(PAPER, INK, 0.012 * (6 - i)));
    circle(0, 0, 640 * i / 5.0);
    popMatrix();
  }

  // 빛이 조금씩 새로 생긴다. 관객 쪽에 더 많이
  if (!dying && frameCount % 2 == 0 && lights.size() < MAX_LIGHTS) lights.add(lightAt(true));

  if (!dying) grow();
  if (!dying && (nodes.size() > MAX_NODES || stall > 240)) {
    dying = true;
    play(piano, ROOTS[season] - 24, 34, 240);   // 나무가 질 때 낮은 피아노 한 음
  }
  if (dying) {
    fade += 1 / 240.0;
    if (fade >= 1) plant();
  }

  weigh();
  for (Node n : nodes) n.showShadow();
  for (Node n : nodes) n.showBranch();
  for (Node n : nodes) n.showLeaf();
  showLights();

  // 전구는 깊이 검사를 끄고 맨 위에 그린다. 빛무리와 전구가 겹쳐 떨리지 않도록
  hint(DISABLE_DEPTH_TEST);
  for (Node n : nodes) n.showLamp();
  hint(ENABLE_DEPTH_TEST);

  soundUpdate();
  if (recording) saveFrame("frames/######.png");
}

// 공간 점유 알고리즘의 한 걸음
void grow() {
  // 1. 빛마다 가장 가까운 가지를 찾아 끌어당긴다. 충분히 가까우면 빛은 닿은 것
  for (int i = lights.size() - 1; i >= 0; i--) {
    PVector l = lights.get(i);
    Node near = null;
    float best = REACH;
    for (Node n : nodes) {
      float d = PVector.dist(l, n.pos);
      if (d < best) {
        best = d;
        near = n;
      }
    }
    if (near == null) continue;
    if (best < KILL) {
      lights.remove(i);
      if (frameCount - lastBell > 14 && random(1) < 0.5) {   // 빛이 닿으면 가끔 첼레스타 한 알
        lastBell = frameCount;
        play(bell, ROOTS[season] + 24 + SCALES[season][int(random(5))] + (random(1) < 0.5 ? 12 : 0), int(random(16, 26)), 60);
      }
      continue;
    }
    near.pull.add(PVector.sub(l, near.pos).normalize());
    near.pulls++;
  }

  // 2. 끌린 가지는 그쪽으로 한 걸음 자란다. 조금 흔들리고, 조금은 위로
  int grew = 0;
  int count = nodes.size();
  for (int i = 0; i < count; i++) {
    Node n = nodes.get(i);
    if (n.pulls == 0) continue;
    n.pull.div(n.pulls).add(PVector.random3D().mult(0.25)).add(0, -0.08, 0).normalize().mult(STEP);
    nodes.add(new Node(PVector.add(n.pos, n.pull), n));
    n.kids++;
    n.pull.set(0, 0, 0);
    n.pulls = 0;
    grew++;
  }
  stall = grew == 0 ? stall + 1 : 0;
}

// 파이프 규칙. 가지의 단면적은 그 위에 달린 가지 끝의 수
void weigh() {
  for (Node n : nodes) n.area = 0;
  for (int i = nodes.size() - 1; i >= 0; i--) {
    Node n = nodes.get(i);
    if (n.area == 0) n.area = 1;
    if (n.parent != null) n.parent.area += n.area;
  }
}

// 소리 준비. 사운드폰트가 없으면 자바 기본 음색으로, 소리 장치가 없으면 소리 없이 간다
void soundSetup() {
  try {
    synth = MidiSystem.getSynthesizer();
    synth.open();
    File sf = new File(dataPath(SOUNDFONT));
    if (sf.exists()) {
      Soundbank bank = MidiSystem.getSoundbank(sf);
      if (synth.getDefaultSoundbank() != null) synth.unloadAllInstruments(synth.getDefaultSoundbank());
      synth.loadAllInstruments(bank);
      println("사운드폰트: " + bank.getName());
    } else {
      println("data/" + SOUNDFONT + " 가 없어 자바 기본 음색으로 연주합니다");
    }
    MidiChannel[] ch = synth.getChannels();
    piano = ch[0];
    pad = ch[1];
    bell = ch[2];
    piano.programChange(0);    // 그랜드 피아노
    pad.programChange(89);     // 따뜻한 패드
    bell.programChange(8);     // 첼레스타
    piano.controlChange(91, 110);   // 리버브
    pad.controlChange(91, 127);
    bell.controlChange(91, 120);
    pad.controlChange(73, 110);     // 느린 어택
    pad.controlChange(72, 110);     // 긴 릴리즈
    sound = true;
  } catch (Exception e) {
    println("소리 없이 진행합니다: " + e);
  }
}

// 처음부터. 나무도 음악도 첫 계절로 돌아간다
void restart() {
  if (sound) {
    for (MidiChannel ch : synth.getChannels()) ch.allNotesOff();
    ringing.clear();
    pending = -1;
  }
  season = -1;
  plant();
}

// 새 계절. 패드의 화음이 바뀐다
void newSeason() {
  season = (season + 1) % ROOTS.length;
  if (!sound) return;
  pad.allNotesOff();
  for (int i : CHORDS[season]) pad.noteOn(ROOTS[season] - 12 + i, 40);
}

// 한 음. 정해진 프레임 뒤에 끈다
void play(MidiChannel ch, int note, int vel, int frames) {
  if (!sound) return;
  ch.noteOn(note, vel);
  ringing.add(new int[] { ch == piano ? 0 : ch == pad ? 1 : 2, note, frameCount + frames });
}

// 전구가 켜지면 피아노 한 음을 예약한다. 음높이는 전구의 색과 높이에서
void ask(Node n) {
  int oct = int(map(n.pos.y, -50, -570, 0, 2.99));
  pending = ROOTS[season] + SCALES[season][n.ci] + 12 * oct;
}

// 매 프레임. 예약된 음은 8 프레임 격자에 맞춰 하나씩만 치고, 다 울린 음은 끈다
void soundUpdate() {
  if (!sound) return;
  if (pending >= 0 && frameCount % 8 == 0) {
    play(piano, pending, int(random(38, 58)), 120);
    pending = -1;
  }
  if (frameCount % 10 == 0) {
    pad.controlChange(11, int(map(nodes.size(), 0, MAX_NODES, 45, 100) * (1 - lampFade())));
  }
  MidiChannel[] chs = { piano, pad, bell };
  for (int i = ringing.size() - 1; i >= 0; i--) {
    int[] r = ringing.get(i);
    if (frameCount >= r[2]) {
      chs[r[0]].noteOff(r[1]);
      ringing.remove(i);
    }
  }
}

void exit() {
  if (synth != null) synth.close();
  super.exit();
}

// 나무가 아직 닿지 못한 빛. 반딧불처럼 조금씩 떠다닌다
void showLights() {
  strokeWeight(3);
  stroke(LIGHT, 150 * (1 - fade));
  for (PVector l : lights) {
    point(l.x + 2 * sin(frameCount * 0.05 + l.z), l.y + 2 * cos(frameCount * 0.04 + l.x), l.z);
  }
}

// 멀면 옅게. 종이 위의 공기
float depthOf(PVector p) {
  return map(constrain(PVector.dist(eye, p), camDist - 500, camDist + 500), camDist - 500, camDist + 500, 1, 0.45);
}

// 바람. 높을수록 크게 흔들린다. 그리기에만 쓰고 자라는 계산에는 쓰지 않는다
PVector swayed(PVector p) {
  float s = sin(frameCount * 0.03 + p.y * 0.004) * p.y * -0.02;
  return new PVector(p.x + s, p.y, p.z + s * 0.4);
}

// 나무가 지는 순서. 전구가 먼저 꺼지고, 잎이 지고, 마지막에 먹이 마른다
float lampFade() {
  return constrain(fade / 0.3, 0, 1);
}
float leafFall() {
  return constrain((fade - 0.2) / 0.5, 0, 1);
}
float inkFade() {
  return constrain((fade - 0.4) / 0.6, 0, 1);
}

class Node {
  PVector pos, pull = new PVector();
  Node parent;
  int pulls = 0, kids = 0, born, depth;
  float area = 0, phase;
  int ci;
  color c;
  boolean leafy, lampy;

  Node(PVector pos, Node parent) {
    this.pos = pos;
    this.parent = parent;
    depth = parent == null ? 0 : parent.depth + 1;
    born = frameCount;
    ci = int(random(palette.length));
    c = palette[ci];
    phase = random(TWO_PI);
    leafy = random(1) < 0.3;
    lampy = random(1) < 0.5;
  }

  float depth() {
    return depthOf(pos);
  }

  // 갓 난 가지는 며칠에 걸쳐 돋는다
  float grown() {
    return constrain((frameCount - born) / 6.0, 0, 1);
  }

  float weight() {
    return min(11, 0.6 + sqrt(area) * 0.32);
  }

  // 잔가지 끝인가. 줄기나 굵은 가지에 바로 붙은 짧은 곁가지는 아니다
  boolean onTwig() {
    if (depth < 12) return false;
    Node p = parent;
    for (int i = 0; i < 4 && p != null; i++) {
      if (p.area > 10) return false;
      p = p.parent;
    }
    return true;
  }

  void showShadow() {
    if (parent == null) return;
    PVector a = swayed(parent.pos);
    PVector b = PVector.lerp(a, swayed(pos), grown());
    stroke(INK, 14 * (1 - inkFade()));
    strokeWeight(weight());
    line(a.x + a.y * 0.45, -0.7, a.z + a.y * 0.25, b.x + b.y * 0.45, -0.7, b.z + b.y * 0.25);
  }

  void showBranch() {
    if (parent == null) return;
    PVector a = swayed(parent.pos);
    PVector b = PVector.lerp(a, swayed(pos), grown());
    stroke(INK, 255 * depth() * (1 - inkFade()));
    strokeWeight(weight());
    line(a.x, a.y, a.z, b.x, b.y, b.z);
  }

  // 잎. 나무가 질 때는 흔들리며 떨어진다
  void showLeaf() {
    if (!leafy || area > 3 || !onTwig() || grown() < 1) return;
    float fall = leafFall();
    PVector p = swayed(pos);
    PVector d = PVector.sub(pos, parent.pos);
    pushMatrix();
    translate(p.x + 30 * fall * sin(frameCount * 0.08 + phase), p.y + fall * fall * 320, p.z);
    rotateY(atan2(d.x, d.z) + fall * 4 * sin(frameCount * 0.05 + phase));
    rotateX(phase < PI ? 1.2 : -1.2);
    noStroke();
    fill(LEAF, 255 * depth() * (1 - constrain((fall - 0.7) / 0.3, 0, 1)));
    ellipse(6, 0, 12, 6);
    popMatrix();
  }

  // 전구. 더 자라지 않은 잔가지 끝에만 켜지고, 항상 관객을 향한 납작한 원이라 그림처럼 보인다
  void showLamp() {
    if (!lampy || kids > 0 || !onTwig()) return;
    float age = frameCount - born;
    if (age < 30) return;
    if (age == 30 && !dying) ask(this);
    float open = constrain((age - 30) / 40.0, 0, 1) * (1 - lampFade());
    if (open <= 0) return;
    float d = 14 * open * (1 + 0.06 * sin(frameCount * 0.05 + phase));
    float a = 255 * depth();
    PVector p = swayed(pos);
    pushMatrix();
    translate(p.x, p.y, p.z);
    rotateY(HALF_PI - camYaw);
    rotateX(camPitch);
    noStroke();
    fill(c, a * 0.10);
    circle(0, 0, d * 3.4);
    fill(c, a * 0.22);
    circle(0, 0, d * 2.0);
    stroke(INK, a);
    strokeWeight(1.6);
    fill(c, a);
    circle(0, 0, d);
    popMatrix();
  }
}

라이브러리 설치

3회차에서 쓰는 라이브러리 네 개입니다. 이정섭 강사가 미리 설치를 권했습니다. Processing 본체와 따로, 프로그램 안에서 받습니다.

검색어목록에서 고를 것만든 곳크기
videoVideo Library for Processing 4The Processing Foundation약 310MB
opencvOpenCV for ProcessingGreg Borenstein, Florian Bruggisser약 250MB
blobBlobDetectionJulien 'v3ga' Gachadoat0.1MB 미만
soundSoundThe Processing Foundation약 7MB
  1. Processing을 열고 메뉴 Sketch → Import Library… → Manage Libraries… 를 누릅니다. 메뉴가 한글로 나오면 스케치 → 내부 라이브러리… → Manage Libraries… 입니다.

1. 스케치 → 내부 라이브러리… → Manage Libraries…. 한글판에서도 마지막 항목만 영어로 나옵니다
1. 스케치 → 내부 라이브러리… → Manage Libraries…. 한글판에서도 마지막 항목만 영어로 나옵니다

  1. 「Contribution Manager」 창이 Libraries 탭으로 열립니다. 왼쪽 위 Filter 칸에 표의 검색어를 칩니다.

2. Libraries 탭. 왼쪽 위 Filter 칸에 검색어를 칩니다. 목록의 오른쪽 열이 만든 곳입니다
2. Libraries 탭. 왼쪽 위 Filter 칸에 검색어를 칩니다. 목록의 오른쪽 열이 만든 곳입니다

  1. 남은 목록에서 이름을 한 번 누릅니다. 창 아래에 이름·버전·만든 곳·설명이 나옵니다. 표와 맞으면 오른쪽 아래 Install.

3. 이름을 누르면 아래에 자세한 정보가 나옵니다. 표의 이름·만든 곳과 맞는지 보고 오른쪽 아래 Install
3. 이름을 누르면 아래에 자세한 정보가 나옵니다. 표의 이름·만든 곳과 맞는지 보고 오른쪽 아래 Install

  1. 받는 동안에는 Install 밑에 파란 진행 막대와 「다운로드 중」이 나옵니다. 다 되면 왼쪽 Status 칸에 표시가 남습니다.

4. 받는 중. Video와 OpenCV는 여기서 몇 분씩 걸립니다
4. 받는 중. Video와 OpenCV는 여기서 몇 분씩 걸립니다

  1. 네 개를 차례로 설치한 뒤 창을 닫고 Processing을 껐다 켭니다.

네 개가 다 됐는지 보기

File → New 로 새 스케치를 열고 아래를 붙여 넣은 뒤 ▶ 를 누릅니다. 작은 창에 전구 네 개가 뜹니다. 넷 다 초록이면 끝입니다. 빨간 전구가 있으면 그 라이브러리가 아직 안 된 것이고, 까닭은 아래 검은 칸(콘솔)에 한글로 찍힙니다. 소리는 나지 않습니다.

java
// 라이브러리 확인. 네 개를 하나씩 실제로 열어 보고 전구를 켠다.
// 초록 전구는 준비된 것, 빨간 전구는 아직 안 된 것. 까닭은 아래 콘솔에 찍힌다.
// 창에 보이는 글이 영어인 것은 Processing 기본 글꼴에 한글이 없어서다. 소리는 나지 않는다.

import processing.video.*;
import gab.opencv.*;
import blobDetection.*;
import processing.sound.*;

color PAPER = #FAF8F3;
color INK = #14141A;
color GREEN = #8DB34A;
color RED = #E0533F;

String[] names = { "Video", "OpenCV", "Blob", "Sound" };
String[] said = new String[4];    // 라이브러리가 대답한 값
boolean[] ready = new boolean[4];

void setup() {
  size(520, 320);
  smooth(8);
  surface.setTitle("Library check · OPEN CIRCUIT BUSAN");

  for (int i = 0; i < names.length; i++) {
    try {
      said[i] = open(i);
      ready[i] = true;
      println(names[i] + " 준비됨 · " + said[i]);
    } catch (Throwable e) {
      said[i] = "not ready";
      ready[i] = false;
      println(names[i] + " 안 됨 · " + e);
    }
  }
  println(lit() == names.length
    ? "네 개 모두 준비됐습니다"
    : "빨간 전구가 켜진 라이브러리를 단톡방에 알려 주세요");
  noLoop();
}

// 라이브러리를 하나씩 실제로 열어 본다. 설치가 덜 됐으면 여기서 걸린다
String open(int i) {
  if (i == 0) {
    return "cameras: " + Capture.list().length;
  }
  if (i == 1) {
    OpenCV cv = new OpenCV(this, 64, 64);
    return org.opencv.core.Core.VERSION + ", " + cv.width + "x" + cv.height;
  }
  if (i == 2) {
    BlobDetection blobs = new BlobDetection(64, 64);
    blobs.setThreshold(0.5);
    return "blobs: " + blobs.getBlobNb();
  }
  SinOsc osc = new SinOsc(this);
  osc.freq(440);
  return "sine 440 Hz";
}

int lit() {
  int n = 0;
  for (boolean b : ready) if (b) n++;
  return n;
}

void draw() {
  background(PAPER);
  textAlign(LEFT, BASELINE);
  textSize(11);
  fill(INK, 120);
  text("LIBRARY CHECK", 44, 58);
  textAlign(RIGHT, BASELINE);
  text("class 3  ·  Processing 4", width - 44, 58);
  stroke(INK, 45);
  strokeWeight(1);
  line(44, 72, width - 44, 72);

  for (int i = 0; i < names.length; i++) {
    float y = 112 + i * 40;
    lamp(62, y, ready[i] ? GREEN : RED);
    textAlign(LEFT, CENTER);
    textSize(15);
    fill(INK);
    text(names[i], 92, y);
    fill(INK, 130);
    text(said[i], 210, y);
  }

  textAlign(CENTER, CENTER);
  textSize(13);
  fill(INK, 190);
  text(lit() + " / " + names.length + " ready", width / 2, height - 40);
}

// 정원 스케치의 전구와 같은 그림
void lamp(float x, float y, color c) {
  noStroke();
  fill(c, 45);
  circle(x, y, 30);
  fill(c, 80);
  circle(x, y, 20);
  stroke(INK, 220);
  strokeWeight(1.4);
  fill(c);
  circle(x, y, 11);
}

수업에서 한 실습을 다시 하려면

3회차에서 손전등을 찾아 그 자리에 패턴을 입힌 실습은 손전등 찾아 패턴 입히기에 단계별 파일로 있습니다. 1단계 웹캠 열기부터 5단계 내 마음대로 바꾸기까지 하나씩 열어 보면 됩니다.

더 둘러보기

Processing으로 무엇까지 되는지 감을 잡으려면 공식 예제를 훑어보는 게 가장 빠릅니다.

  • processing.org/examples — 그리기, 움직임, 소리, 카메라, 3D까지 분야별 예제. 각 예제는 코드와 실행 화면이 함께 있어 눌러 보기만 해도 됩니다.
  • Processing 안에서도 같은 예제를 바로 열 수 있습니다. File → Examples 를 누르면 목록이 뜹니다. 하나 열어 ▶ 를 누르고, 숫자를 바꿔 보는 식으로 놀아 보세요.
  • processing.org/reference — 함수 사전. 위 스케치에 나온 noise, atan2, pushMatrix 같은 이름이 궁금할 때 찾아봅니다.

수업이 끝나면

이정섭 작가의 수업을 듣고 Processing이 내 손에 맞는다 싶으면, 그 감을 놓치지 말고 작업으로 밀고 나가 보세요. 그렇게 쌓인 작업을 들고 Processing Foundation 펠로우십에 지원해 보시길 권합니다. 2013년부터 예술가·개발자·교육자의 프로젝트를 지원해 온 프로그램이고, p5.js와 p5.js 에디터도 10여 년 전 펠로우의 제안에서 나왔습니다. 2025년까지 해마다 열 명 안팎을 뽑았고, 여러 나라의 작업자가 섞여 있습니다.

지금은 프로그램을 다시 짜는 중이라 2026년에는 뽑지 않습니다. 2027년 펠로우십 소식은 2026년 10월에 나온다고 공지돼 있으니, 그 사이 메일링 리스트에 이름을 올려 두세요. 3회차에서 만든 것이 그때 지원서의 첫 재료가 됩니다.

막힐 때

  • Windows에서 설치 후 실행이 안 되면 → 다시 시작한 뒤 한 번 더. .msi 설치 직후에는 가끔 재부팅이 필요합니다.
  • 정원 실행 시 창이 검게만 뜨거나 「OpenGL」 오류가 나오면 → 그래픽 드라이버 문제입니다. 화면을 캡처해서 단톡방에 올려 주세요.
  • Mac에서 「Processing을 열 수 없습니다」 만 뜨고 아무 설명이 없으면 → 받은 파일이 내 칩(Apple Silicon/Intel)에 맞는지 다시 확인.
  • 라이브러리 설치가 「다운로드 중 에러」로 끝나면 → 와이파이를 바꾸거나 잠시 뒤 다시 Install. 받다 만 파일이 남아도 다시 설치하면 덮어씁니다.
  • 확인 스케치에서 The package "gab" does not exist 같은 빨간 줄이 나오면 → 그 라이브러리가 아직 설치되지 않은 것입니다. Contribution Manager에서 이름을 다시 확인하세요.
  • 그 밖의 문제는 화면을 캡처해서 단톡방에 올리세요. → 막힐 때

《2026 오픈서킷 부산: 아트앤테크 프랙티스》 수업 자료 · 글·기획 배준형