# Processing 설치

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

## 내 컴퓨터 확인

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

## 내려받기

[processing.org/download](https://processing.org/download) 에서 내 컴퓨터에 맞는 파일을 받습니다. 지금 버전은 **4.5.6** 입니다.

| 컴퓨터 | 받을 파일 |
| --- | --- |
| Windows | **Windows (Intel 64-bit)** 의 설치 파일(`.msi`) |
| Mac, Apple Silicon | **macOS (Apple Silicon)** 의 `.dmg` |
| Mac, Intel | **macOS (Intel 64-bit)** 의 `.dmg` |

> [!NOTE]
> 다운로드 페이지가 열리지 않으면 [GitHub 릴리스](https://github.com/processing/processing4/releases/latest)에서 같은 파일을 받을 수 있습니다. 파일 이름에 `windows-x64.msi`, `macos-aarch64.dmg`(Apple Silicon), `macos-x64.dmg`(Intel)가 들어 있습니다.

## 설치

### Windows

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

> [!WARNING]
> 설치 파일 대신 `portable.zip` 을 받았다면 압축을 푼 폴더 안의 `Processing.exe` 를 실행합니다. 압축을 풀지 않고 zip 안에서 바로 실행하면 열리지 않습니다.

### Mac

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

> [!WARNING]
> 처음 열 때 「확인되지 않은 개발자」 또는 「손상되었기 때문에 열 수 없습니다」 가 나오면, 아이콘을 **오른쪽 클릭 → 열기**를 한 번 해 주세요. 그래도 안 되면 **시스템 설정 → 개인정보 보호 및 보안** 맨 아래의 **그래도 열기**. 한 번만 하면 다음부터는 그냥 열립니다.

## 확인

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

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

[https://youtu.be/CalSPlfwwfo](https://youtu.be/CalSPlfwwfo)

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

> [!NOTE]
> 바탕이 된 작품은 OpenProcessing 의 [Procedural Tree in 3D](https://openprocessing.org/sketch/31272) (Peter Jacobs, CC BY-SA)로, [공간 점유 알고리즘](https://algorithmicbotany.org/papers/colonization.egwnp2007.large.pdf)(Runions 외, 2007)을 Processing 으로 옮긴 것입니다. 여기에 「빛은 관객 쪽에 생긴다」는 규칙 하나를 얹었습니다.

```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 본체와 따로, 프로그램 안에서 받습니다.

| 검색어 | 목록에서 고를 것 | 만든 곳 | 크기 |
| --- | --- | --- | --- |
| `video` | **Video Library for Processing 4** | The Processing Foundation | 약 310MB |
| `opencv` | **OpenCV for Processing** | Greg Borenstein, Florian Bruggisser | 약 250MB |
| `blob` | **BlobDetection** | Julien 'v3ga' Gachadoat | 0.1MB 미만 |
| `sound` | **Sound** | The Processing Foundation | 약 7MB |

1. Processing을 열고 메뉴 **Sketch → Import Library… → Manage Libraries…** 를 누릅니다. 메뉴가 한글로 나오면 **스케치 → 내부 라이브러리… → Manage Libraries…** 입니다.

![1. 스케치 → 내부 라이브러리… → Manage Libraries…. 한글판에서도 마지막 항목만 영어로 나옵니다](/wiki/tools/lib-menu.png)

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

![2. Libraries 탭. 왼쪽 위 Filter 칸에 검색어를 칩니다. 목록의 오른쪽 열이 만든 곳입니다](/wiki/tools/lib-manager.png)

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

![3. 이름을 누르면 아래에 자세한 정보가 나옵니다. 표의 이름·만든 곳과 맞는지 보고 오른쪽 아래 Install](/wiki/tools/lib-video.png)

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

![4. 받는 중. Video와 OpenCV는 여기서 몇 분씩 걸립니다](/wiki/tools/lib-installing.png)

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

> [!WARNING]
> 검색하면 이름이 비슷한 것이 여럿 나옵니다. `video` 에는 GL Video와 Video Export가, `sound` 에는 SoundCloud가 함께 나옵니다. 표의 이름과 만든 곳을 확인하고 고르세요. 강사가 알려 준 「Blob」은 목록에 **BlobDetection** 이라는 이름으로 있습니다.

> [!NOTE]
> 진행 막대가 멈춘 것처럼 보여도 창을 닫지 말고 기다려 주세요. 네 개 모두 Mac(Apple Silicon·Intel)과 Windows에서 그대로 씁니다. 위 화면은 Windows이고 Mac도 같습니다.

### 네 개가 다 됐는지 보기

**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);
}
```

> [!NOTE]
> Mac에서 처음 실행하면 「카메라에 접근하도록 허용하시겠습니까」가 뜹니다. 수업에서 카메라를 쓰니 **허용**을 누르세요. 카메라가 0대로 나오면 다른 프로그램(줌·팀즈 등)이 카메라를 잡고 있는 경우가 많습니다. 그 프로그램을 끄고 다시 실행해 보세요.

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

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

## 더 둘러보기

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

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

## 수업이 끝나면

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

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

## 막힐 때

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