yoppa.org


第13回 次のステップへ – Libraries, p5.js, openFrameworks

この講義では、前半にProcessing、後半はRhinoceros+Grasshopperを使用してメディアアートのためのプログラミングの基礎を学んできました。今回は、これまでのまとめと、ここから先へ進むためのステップについて解説します。

Processingの基礎を身に付けてから次に進む様々な手段がありますが、ここでは次の3つの方法を紹介します。

  1. Processingの機能を拡張する – ライブラリー (Libraries)
  2. Webブラウザ上で表現する – p5.js
  3. さらに高速化を追い求める – openFrameworks

ライブラリー(Libraries)とは、汎用性の高い複数のプログラムを、再利用可能な形でひとまとまりにしたものです。Processingでもライブラリーを活用して機能自体を拡張していくことが可能です。Processingのライブラリーを紹介するWebページを参照すると、膨大な量のライブラリーが公開されています。今回はライブラリーのインストール方法と代表的なライブラリーをいくつか紹介します。

p5.jsは、Javascript版のProcessingです。Javascriptをベースにしている最大の利点はWebブラウザー上で実行可能という点です。作成したスケッチをWebサーバー上ですぐに世界に向けて公開可能です。文法はProcessingをベースにしているので、習得も容易です。ただし、ベースとなる言語が違うため若干オリジナルのProcessingと異なる部分もあります。この差異に注意しつつ、p5.jsでのプログラミングの導入方法を解説します。

openFrameworksは、クリエイティブコーディングのためのC++のオープンソースツールキットです。その最大の利点はスピードです。Processingでは処理が追いつかずコマ落ちしてしまうような表現も高速な演算で実現可能です。より本格的な作品制作を目指す方にはProcessingを習得した後、openFrameworksにステップアップすることをお勧めします。openFrameworksには、Processingのライブラリーにあたるアドオン(addons)という拡張機能があり、膨大な数のアドオンが公開されているのも魅力の一つです。

自分の作品の表現形態やテーマにあわせて、次のステップへ進んでいきましょう!

スライド資料


第9回 作品への応用2: oscP5 – OSCによるアプリケーション間通信

今回は、ネットワークを活用したサンプルを紹介します。Open Sound Control(OSC)というプロトコルを使用して、アプリケーション同士をネットワークを介して通信する方法について解説します。これにより、Processingのスケッチ間で通信したり、複数のユーザが1つのスケッチを遠隔操作することが可能となります。またProessingのスケッチを他のアプリケーション、例えば、Sonic Pi、Max/MSPやPd、SuperColliderなどの音楽アプリケーション、さらにはTouchOSCといったiPhoneアプリなどからコントロールすることが出来るようになります。

Sonic Piのダウンロードは下記のURLから行ってください。

スライド資料

サンプルプログラム

01 OSC基本、送信側

//OSC関連のライブラリーをインポート
import oscP5.*;
import netP5.*;

//OSCP5クラスのインスタンス
OscP5 oscP5;
//OSC送出先のネットアドレス
NetAddress myRemoteLocation;

void setup() {
  size(800,600);
  frameRate(60);
  //ポートを12000に設定して新規にOSCP5のインスタンスを生成
  oscP5 = new OscP5(this,12000);
  //OSC送信先のIPアドレスとポートを指定
  myRemoteLocation = new NetAddress("127.0.0.1",12000);
}

void draw() {
  if(mousePressed){
    background(255, 0, 0);
  } else {
    background(0);
  }

  //マウスの場所に円を描く
  noFill();
  stroke(255);
  ellipse(mouseX, mouseY, 10, 10);

  //現在のマウスの位置をOSCで送出
  //新規にメッセージ作成
  OscMessage msg = new OscMessage("/mouse/position");
  msg.add(mouseX); //X座標の位置を追加
  msg.add(mouseY); //Y座標の位置を追加
  //OSCメッセージ送信
  oscP5.send(msg, myRemoteLocation);
}

//マウスボタンを押した時にメッセージを送信
void mousePressed(){
  OscMessage msg = new OscMessage("/mouse/cliked");
  msg.add(1); //1を送信
  //OSCメッセージ送信
  oscP5.send(msg, myRemoteLocation);
}

//マウスボタンを離した時にメッセージを送信
void mouseReleased(){
  OscMessage msg = new OscMessage("/mouse/cliked");
  msg.add(0); //0を送信
  //OSCメッセージ送信
  oscP5.send(msg, myRemoteLocation);
}

02 OSC基本、受信側

import oscP5.*;
import netP5.*;

//OSCP5クラスのインスタンス
OscP5 oscP5;
//マウスの位置ベクトル
PVector mouseLoc;
//マウスのクリック検知
int clicked;

void setup() {
  size(800,600);
  frameRate(60);

  //ポートを12000に設定して新規にOSCP5のインスタンスを生成
  oscP5 = new OscP5(this,12000);
  //マウスの位置ベクトルを初期化
  mouseLoc = new PVector(width/2, height/2);
  //マウスのクリック状態を初期化
  clicked = 0;
}

void draw() {
  if(clicked == 1){
    background(255, 0, 0);
  } else {
    background(0);
  }
  //OSCで指定された座標に円を描く
  noFill();
  stroke(255);
  ellipse(mouseLoc.x, mouseLoc.y, 10, 10);
}

//OSCメッセージを受信した際に実行するイベント
void oscEvent(OscMessage msg) {
  //もしOSCメッセージが /mouse/position だったら
  if(msg.checkAddrPattern("/mouse/position")==true) {
    //最初の値をint方としてX座標に
    mouseLoc.x = msg.get(0).intValue();
    //次の値をint方としてY座標に
    mouseLoc.y = msg.get(1).intValue();
  }
  if(msg.checkAddrPattern("/mouse/cliked")==true) {
    //Bool値を読み込み
    clicked = msg.get(0).intValue();
    println("msg = " + clicked);
    print("*");
  }
}

03 OSC受信応用 – 拡がる波紋

import oscP5.*;
import netP5.*;

//OSCP5クラスのインスタンス
OscP5 oscP5;
//マウスの位置ベクトル
PVector mouseLoc;
//Ringクラスのリスト
ArrayList ringList = new ArrayList();

void setup() {
  size(800, 600);
  frameRate(60);

  //ポートを12000に設定して新規にOSCP5のインスタンスを生成
  oscP5 = new OscP5(this,12000);
  //マウスの位置ベクトルを初期化
  mouseLoc = new PVector(width/2, height/2);
}

void draw() {
  background(0);
  noFill();
  stroke(255);
  strokeWeight(1);
  ellipse(mouseLoc.x, mouseLoc.y, 10, 10);
  //リストに格納されたマウスの位置を全て描画する
  strokeWeight(2);
  for (int i = 0; i < ringList.size(); i++) {
    ringList.get(i).draw();
  }
}

//OSCメッセージを受信した際に実行するイベント
void oscEvent(OscMessage msg) {
  //もしOSCメッセージが /mouse/position だったら
  if (msg.checkAddrPattern("/mouse/position")==true) {
    //最初の値をint方としてX座標に
    mouseLoc.x = msg.get(0).intValue();
    //次の値をint方としてY座標に
    mouseLoc.y = msg.get(1).intValue();
  }
  if (msg.checkAddrPattern("/mouse/cliked")==true) {
    //もしマウスがクリックされたメッセージを受けとったら
    if (msg.get(0).intValue() == 1) {
      //マウスの位置のリストに新規に現在の位置を追加する
      ringList.add(new Ring(mouseLoc.x, mouseLoc.y));
    }
  }
}

class Ring {
  PVector location;
  float radius;
  float speed;
  float alpha;
  float alphaSpeed;
  float release;

  Ring(PVector _location) {
    location = new PVector();
    location = _location;
    radius = 20;
    speed = 1.0;
    alpha = 255;
    alphaSpeed = 1.0;
  }

  void draw() {
    alphaSpeed = 255.0/(release*frameRate);
    fill(63, 127, 255, alpha);
    noStroke();
    pushMatrix();
    translate(location.x, location.y);
    ellipse(0, 0, radius, radius);
    popMatrix();
    radius += speed;
    alpha -= alphaSpeed;
    strokeWeight(1.0);
  }
}

04 Sonic PiへOSCを送信 - 基本

import oscP5.*;
import netP5.*;

OscP5 oscP5;
NetAddress location;

void setup() {
  size(400, 400);
  frameRate(60);
  //OSCのセットアップ
  oscP5 = new OscP5(this, 12000);
  //SonicPiの受信ポート4557に送信
  location = new NetAddress("127.0.0.1", 4557);
}

void draw() {
}

void mousePressed() {
  //ド(C)の音を演奏させる
  OscMessage msg = new OscMessage("/run-code");
  msg.add("fromP5");
  msg.add("play 60");
  oscP5.send(msg, location);
  println(msg);
}

05 Sonic PiへOSC送信 - マウスで音程とパンを操作

import oscP5.*;
import netP5.*;

OscP5 oscP5;
NetAddress location;

void setup() {
  size(640, 480);
  //OSCのセットアップ
  oscP5 = new OscP5(this, 12000);
  //SonicPiの受信ポート4557に送信
  location = new NetAddress("127.0.0.1", 4557);
  background(0);
}

void draw() {
}

void mousePressed() {
  //マウスのX座標で定位(パン)を決定
  float pan = map(mouseX, 0, width, -1, 1);
  //マウスのY座標でノートナンバーを決定
  float note = map(mouseY, 0, width, 80, 50);
  //OSCメッセージを送信
  OscMessage msg = new OscMessage("/run-code");
  msg.add("fromP5");
  msg.add("play "+ note + ", pan: "+ pan);
  oscP5.send(msg, location);
}

06 Sonic PiへOSC送信 - 表示の工夫

import oscP5.*;
import netP5.*;

OscP5 oscP5;
NetAddress location;

float note; //ノート番号
float pan;  //パン

void setup() {
  size(640, 480);
  oscP5 = new OscP5(this, 12000);
  location = new NetAddress("127.0.0.1", 4557);
}

void draw() {
  background(0);
  stroke(255);
  //マウスの位置を表示
  line(mouseX, 0, mouseX, height);
  line(0, mouseY, width, mouseY);
  ellipse(mouseX, mouseY, 10, 10);
  //マウスの位置から、ノート番号とパンを決定
  note = map(mouseY, 0, width, 80, 50);
  pan = map(mouseX, 0, width, -1, 1);


  //テキストで表示
  text("note = " + note + ", pan = " + pan, mouseX + 10, mouseY-10);
}

void mousePressed() {
  //OSCを送信
  OscMessage msg = new OscMessage("/run-code");
  msg.add("fromP5");
  msg.add("play "+ note + ", pan: "+ pan);
  oscP5.send(msg, location);
  println(msg);
}

07 Sonic PiへOSC送信 - 自動演奏

import oscP5.*;
import netP5.*;

OscP5 oscP5;
NetAddress location;

float note;
float pan;

void setup() {
  size(640, 480);
  frameRate(60);
  oscP5 = new OscP5(this, 12000);
  location = new NetAddress("127.0.0.1", 4557);
}

void draw() {
  background(0);
  int speed = 8;
  //8フレームに一度実行
  if (frameCount % speed == 0) {
    //ランダムなノート番号とパンを設定
    note = random(50, 80);
    pan = random(-1, 1);
    //OSCを送信
    OscMessage msg = new OscMessage("/run-code");
    msg.add("fromP5"+frameCount);
    msg.add("play "+ note + ", pan: "+ pan);
    oscP5.send(msg, location);
    println(msg);
  }
}

08 Sonic PiへOSC送信 - 自動演奏 + ビジュアライズ

import oscP5.*;
import netP5.*;

OscP5 oscP5;
NetAddress location;

float note;
float pan;

ArrayList< Ring > rings;
int MAX = 16;

void setup() {
  size(800, 600);
  frameRate(60);
  oscP5 = new OscP5(this, 12000);
  location = new NetAddress("127.0.0.1", 4557);
  rings = new ArrayList< Ring >();
}

void draw() {
  background(0);
  int speed = 8;
  if (frameCount % speed == 0) {
    note = int(random(50, 80));
    pan = random(-1, 1);
    OscMessage msg
             = new OscMessage("/run-code");
    msg.add("fromP5"+frameCount);
    msg.add("play "+ note + ", pan: "+ pan);
    oscP5.send(msg, location);
    println(msg);

    float x = map(pan, -1, 1,
                  width/4, width/4*3);
    float y = map(note, 40, 90, height, 0);
    PVector location = new PVector(x, y);
    Ring r = new Ring(location);
    r.release = 3.0;
    rings.add(r);
  }

  for (int i = 0; i < rings.size(); i++) {
    rings.get(i).draw();
  }
  if (rings.size() > MAX) {
    rings.remove(0);
  }
}

class Ring {
  PVector location;
  float radius;
  float speed;
  float alpha;
  float alphaSpeed;
  float release;

  Ring(PVector _location) {
    location = new PVector();
    location = _location;
    radius = 20;
    speed = 1.0;
    alpha = 255;
    alphaSpeed = 1.0;
  }

  void draw() {
    alphaSpeed = 255.0/(release*frameRate);
    fill(63, 127, 255, alpha);
    noStroke();
    pushMatrix();
    translate(location.x, location.y);
    ellipse(0, 0, radius, radius);
    popMatrix();
    radius += speed;
    alpha -= alphaSpeed;
    strokeWeight(1.0);
  }
}


第8回 作品への応用1 : OpenCV for Processing コンピュータ・ビジョン、映像を使ったインタラクション

これまでは、主にProcessingの基礎をステップバイステップで取り上げてきましたが、ここから数回は作品制作に直接役に立ちそうな実践的なプログラミングテクニックを紹介します。今回は、その第一回目として、映像を使ったインタラクションについて取り上げます。

Processingを使用してインタラクティブな機能を実現するための手段は、センサーを使う方法や、KinectやLeap Motionなどのデバイスを使用する方法などいろいろ考えられます。今回は、最もシンプルな機材構成で可能な方法として、カメラの映像を解析してそこから動きや物体の輪郭を取り出す手法について取り上げます。

コンピュータで、映像から実世界の情報を取得して認識するための研究で「コンピュータ・ビジョン (Conputer Vision)」という分野が存在します。わかりやすく言うなら「ロボットの目」をつくるような研究です。このコンピュータ・ビジョンの様々な成果をオープンソースで公開しているOpenCVというライブラリーがあります。今回は、このOpenCVをProcessingで使用できるようにした、OpenCV for Processingライブラリーを使用したプログラミングを体験します。

スライド資料

サンプルプログラム

カメラキャプチャー基本

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

Capture cam; // ライブカメラ

void setup() {
  //初期設定
  size(640, 480); //画面サイズ
  //キャプチャーするカメラのサイズ
  cam = new Capture(this, 640, 480);
  //キャプチャー開始
  cam.start();
}

void draw() {
  //カメラ画像を表示
  image(cam, 0, 0 );
}

//キャプチャーイベント
void captureEvent(Capture c) {
  cam.read();
}

ビデオ映像をピクセレイト

// ビデオライブラリのインポート
import processing.video.*; 
Capture cam; // ムービープレイヤの宣言
boolean playing; // ムービーを再生しているか否か

void setup() {
  // 画面初期設定
  size(853, 480);
  frameRate(60);
  noStroke();
  cam = new Capture(this); // カメラを初期化する
  cam.start(); // キャプチャを開始する
}

void draw() {
  background(0);
  int skip = 20; // 色をピックアップする間隔を設定する
  // 設定した間隔で画面をスキャン
  for (int j = skip/2; j < height; j += skip) {
    for (int i = skip/2; i < width; i += skip) {
      // 指定した座標の色を読み込む
      color col = cam.get(i, j); 
      // 明るさを抽出する
      float br = brightness(col);
      fill(col); // 塗りの色を設定する
      // 明るさをサイズにして円を描く
      ellipse(i, j, skip * br / 255.0, skip * br / 255.0);
    }
  }
}

// カメラのフレームが更新されたらイベントを実行する
void captureEvent(Capture c) {
  cam.read();
}

OpenCV輪郭抽出

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

Capture cam; // ライブカメラ
OpenCV opencv; // OpenCV
ArrayList < Contour > contours; //輪郭の配列

void setup() {
  //初期設定
  size(640, 480); //画面サイズ
  //キャプチャーするカメラのサイズ
  cam = new Capture(this, 640, 480);
  //OpenCVの画面サイズ
  opencv = new OpenCV(this, 640, 480);
  //キャプチャー開始
  cam.start();
}

void draw() {
  //カメラの画像をOpenCVに読み込み
  opencv.loadImage(cam);
  //カメラ画像を表示
  image(cam, 0, 0 );
  //閾値の設定(マウスのX座標で変化)
  int threshold = int(map(mouseX, 0, width, 0, 100));
  opencv.threshold(threshold);
  //輪郭抽出
  contours = opencv.findContours();
  //描画設定
  noFill();
  strokeWeight(1);
  //検出された輪郭の数だけ、輪郭線を描く
  for (int i = 0; i < contours.size(); i++) {
    stroke(0, 255, 0);
    contours.get(i).draw();
  }
}

//キャプチャーイベント
void captureEvent(Capture c) {
  cam.read();
}

OpenCVによる顔検出

import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture cam; //ビデオキャプチャー
OpenCV opencv; //OpenCV

void setup() {
  size(640, 480);
  //ビデオキャプチャー初期化
  cam = new Capture(this, 640/2, 480/2);
  //OpenCV初期化(ビデオキャプチャーの半分のサイズ)
  opencv = new OpenCV(this, 640/2, 480/2);
  //顔の学習データを読み込み
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
  //ビデオキャプチャー開始
  cam.start();
}

void draw() {
  //二倍サイズで表示
  scale(2);
  //画像を読み込み
  opencv.loadImage(cam);
  //カメラ画像を描画
  image(cam, 0, 0 );

  //顔を検出
  Rectangle[] faces = opencv.detect();
  //検出した顔の周囲を四角形で描画
  noFill();
  stroke(0, 255, 0);
  strokeWeight(3);
  for (int i = 0; i < faces.length; i++) {
    rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height);
  }
}

//キャプチャーイベント
void captureEvent(Capture c) {
  cam.read();
}

OpenCVによる顔検出 – 目線を入れてみる

import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture cam; //ビデオキャプチャー
OpenCV opencv; //OpenCV

void setup() {
  size(640, 480);
  //ビデオキャプチャー初期化
  cam = new Capture(this, 640/2, 480/2);
  //OpenCV初期化(ビデオキャプチャーの半分のサイズ)
  opencv = new OpenCV(this, 640/2, 480/2);
  //顔の学習データを読み込み
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
  //ビデオキャプチャー開始
  cam.start();
}

void draw() {
  //二倍サイズで表示
  scale(2);
  //画像を読み込み
  opencv.loadImage(cam);
  //カメラ画像を描画
  image(cam, 0, 0 );

  //顔を検出
  Rectangle[] faces = opencv.detect();
  //検出した顔の周囲を四角形で描画
  fill(0);
  for (int i = 0; i < faces.length; i++) {
    //ちょうど目の場所にくるよう、場所とサイズを調整
    float x = faces[i].x + faces[i].width * 0.15;
    float y = faces[i].y + faces[i].height * 0.3;
    float width = faces[i].width * 0.7;
    float height = faces[i].height * 0.15;
    //目線を描画
    rect(x, y, width, height);
  }
}

//キャプチャーイベント
void captureEvent(Capture c) {
  c.read();
}

Optical Flowの描画

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

Capture cam; // ライブカメラ
OpenCV opencv; // OpenCV

void setup() {
  //初期設定
  size(640, 480); //画面サイズ
  //キャプチャーするカメラのサイズ
  cam = new Capture(this, 640/2, 480/2);
  //OpenCVの画面サイズ
  opencv = new OpenCV(this, 640/2, 480/2);
  //キャプチャー開始
  cam.start();
}

void draw() {
  //描画スケール設定
  scale(2.0);
  //カメラの画像をOpenCVに読み込み
  opencv.loadImage(cam);
  //カメラ画像を表示
  image(cam, 0, 0 );
  //OpticalFlowを計算
  opencv.calculateOpticalFlow();
  //描画設定
  stroke(255,0,0);
  //OpticalFlowを描画
  opencv.drawOpticalFlow();
}

//キャプチャーイベント
void captureEvent(Capture c) {
  c.read();
}

OpticalFlow + Particle

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

Capture cam; // ライブカメラ
OpenCV opencv; // OpenCV

int NUM = 500;
ParticleVec3[] particles = new ParticleVec3[NUM];

void setup() {
  //初期設定
  size(640, 480, P3D); //画面サイズ
  //キャプチャーするカメラのサイズ
  cam = new Capture(this, 640/2, 480/2);
  //OpenCVの画面サイズ
  opencv = new OpenCV(this, 640/2, 480/2);
  //キャプチャー開始
  cam.start();

  for (int i = 0; i < NUM; i++) {
    particles[i] = new ParticleVec3();
    particles[i].radius = 4.0;
    particles[i].position.set(random(width/2), random(height/2), 0);
    particles[i].minx = 0;
    particles[i].miny = 0;
    particles[i].maxx = width/2;
    particles[i].maxy = height/2;
  }
}

void draw() {
  background(0);
  blendMode(ADD);
  //描画スケール設定
  scale(2.0);
  //カメラの画像をOpenCVに読み込み
  opencv.loadImage(cam);
  //カメラ画像を表示
  //image(cam, 0, 0 );
  //OpticalFlowを計算
  opencv.calculateOpticalFlow();
  //描画設定
  stroke(255, 0, 0);
  //OpticalFlowを描画
  opencv.drawOpticalFlow();

  noStroke();
  fill(0, 127, 255);
  for (int i = 0; i < NUM; i++) {
    //パーティクルの位置を更新
    particles[i].update();
    //パーティクルを描画
    particles[i].draw();
    //画面の端で反対側から出現するように
    particles[i].throughOffWalls();
    //OpticalFlowから力を算出してパーティクルに反映する
    if (particles[i].position.x > 0
        && particles[i].position.x < width/2
        && particles[i].position.y > 0
        && particles[i].position.y < height/2 ) {
      PVector vec = opencv.getFlowAt(int(particles[i].position.x),
                                     int(particles[i].position.y));
      particles[i].addForce(vec.mult(0.1));
    }
  }
}

//キャプチャーイベント
void captureEvent(Capture c) {
  c.read();
}

class ParticleVec3 {
  PVector position;
  PVector velocity;
  PVector acceleration;
  float friction;
  float radius;
  float mass;
  float minx, miny, minz;
  float maxx, maxy, maxz;

  ParticleVec3() {
    radius = 4.0;
    friction = 0.01;
    mass = 1.0;
    position = new PVector(width/2.0, height/2.0, 0);
    velocity = new PVector(0, 0, 0);
    acceleration = new PVector(0, 0, 0);
    minx = 0;
    miny = 0;
    minz = -height;
    maxx = width;
    maxy = height;
    maxz = height;
  }

  void update() {
    velocity.add(acceleration);
    velocity.mult(1.0 - friction);
    position.add(velocity);
    acceleration.set(0, 0, 0);
  }

  void draw() {
    pushMatrix();
    translate(position.x, position.y, position.z);
    ellipse(0, 0, radius * 2, radius * 2);
    popMatrix();
  }

  void addForce(PVector force) {
    force.div(mass);
    acceleration.add(force);
  }

  void bounceOffWalls() {
    if (position.x > maxx) {
      position.x = maxx;
      velocity.x *= -1;
    }
    if (position.x < minx) {
      position.x = minx;
      velocity.x *= -1;
    }
    if (position.y > maxy) {
      position.y = maxy;
      velocity.y *= -1;
    }
    if (position.y < miny) {
      position.y = miny;
      velocity.y *= -1;
    }
    if (position.z > maxz) {
      position.z = maxz;
      velocity.z *= -1;
    }
    if (position.z < minz) {
      position.z = minz;
      velocity.z *= -1;
    }
  }

  void throughOffWalls() {
    if (position.x < minx) {
      position.x = maxx;
    }
    if (position.y < miny) {
      position.y = maxy;
    }
    if (position.z < minz) {
      position.z = maxz;
    }
    if (position.x > maxx) {
      position.x = minx;
    }
    if (position.y > maxy) {
      position.y = miny;
    }
    if (position.z > maxz) {
      position.z = minz;
    }
  }

  void addAttractionForce(PVector force, float radius, float scale) {
    float length = PVector.dist(position, force);
    PVector diff = new PVector();
    diff = position.get();
    diff.sub(force);
    boolean bAmCloseEnough = true;
    if (radius > 0) {
      if (length > radius) {
        bAmCloseEnough = false;
      }
    }
    if (bAmCloseEnough == true) {
      float pct = 1 - (length / radius);
      diff.normalize();
      diff.mult(scale);
      diff.mult(pct);
      acceleration.sub(diff);
    }
  }
}

第7回 Processing実習 3 : Processingで画像データを扱う – 画像の分析・再合成

ProcessingのPImageクラスは、外部のビットマップ画像(Jpeg, GIF、PNGなど)をデータとしてプログラムに読み込むことができます。読み込んだ画像は単に画面に表示するだけでなく、色や明度やサイズを変更して表示することができます。さらには、画像に含まれる全てのピクセルの色情報を読み取り配列に格納することが可能です。そのデータをもとに別の画像を再生成することが可能となり、読み込んだ画像データの色情報をもとにした多彩な表現が可能となります。

今回はProcessingに画像を読み込んで、分析再合成することで、様々な表現の可能性について探っていきます。

スライド資料

サンプルプログラム

画像を表示

PImage img; //画像データ
void setup() {
  size(1280, 720);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  //画面に画像データを表示
  image(img, 0, 0);
}

位置とサイズを指定して、画像を表示

PImage img; //画像データ
void setup() {
  size(1280, 720);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  background(0); //背景
  //位置と大きさを指定して、画像データを表示
  image(img, width/4, height/4, width/2, height/2);
}

画像の明度を変更 (マウスX座標で明度が変化)

PImage img; //画像データ
void setup() {
  size(1280, 720);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  background(0);
  //マウスのX座標で明度を変化させる
  tint(map(mouseX, 0, width, 0, 255));
  //色を変更した画像を表示
  image(img, 0, 0);
}

画像の色を変更 (マウスの位置で色が変化)

PImage img; //画像データ
void setup() {
  size(800, 600);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  background(0);
  //色補正をリセット
  noTint();
  //元画像を左に表示
  image(img, 0, height/4, width/2, height/2);
  //画像のRGB値をマウスのY座標で変更
  float r = mouseX/float(width) * 255;
  float g = mouseY/float(height) * 255;
  float b = 255;
  tint(r, g, b);
  //色を変更した画像を表示
  image(img, width/2, height/4, width/2, height/2);
}

マウスの位置の色を取得

PImage img; //画像データ
void setup() {
  size(1280, 720);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  background(0);
  image(img, 0, 0);
  //マウスの位置のピクセルの色を取得
  color col = img.get(mouseX, mouseY);
  //取得した色で円を描画
  fill(col);
  ellipse(50, 50, 80, 80);
}

マウスの位置でモザイクの大きさが変化

PImage img; //画像データ
void setup() {
  size(1280, 720);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  background(0);
  noStroke();
  //モザイクの粒度をマウスのX座標で決定
  float step = map(mouseX, 0, width, 1, 800);
  //画面の幅と高さでループ
  for (int j = 0; j < height; j += step) {
    for (int i = 0; i < width; i += step) {
      //座標を指定して色を取得
      color c = img.get(i, j);
      //取得した色を塗りの色にして四角形を描画
      fill(c);
      rect(i, j, step, step);
    }
  }
}

マウスの位置でスキャンする高さを変更

PImage img; //画像データ
void setup() {
  size(1280, 720);
  //画像を読み込み
  img = loadImage("photo.jpg");
}
void draw() {
  background(0);
  noStroke();
  //画面の幅だけループ
  for (int i = 0; i < width; i++) {
    //現在のマウスのY座標の色を取得
    color c = img.get(i, mouseY);
    //取得した色を塗りの色に
    fill(c);
    //画面の高さいっぱいに幅1pixの長方形を描画
    rect(i, 0, 1, height);
  }
}

円の大きさと色で画像を再構成する、明度がサイズに対応

PImage img; //画像データ
void setup() {
  size(1280, 720);
  img = loadImage("photo.jpg");
  background(0); //背景
  noStroke(); //枠線は無し
}
void draw() {
  //色を取得する位置をランダムに決定
  int x = int(random(width));
  int y = int(random(height));
  //指定した場所の色を取得
  color col = img.get(x, y);
  fill(col, 127);
  //色の明度を円の直径にする
  float ellipseSize = map(brightness(col), 0, 255, 2, 50);
  //円を描画
  ellipse(x, y, ellipseSize, ellipseSize);
}

円の大きさと色で画像を再構成する、彩度がサイズに対応

PImage img; //画像データ
void setup() {
  size(1280, 720);
  img = loadImage("photo.jpg");
  background(0); //背景
  noStroke(); //枠線は無し
}
void draw() {
  for (int i = 0; i < 10; i++) {
    //色を取得する位置をランダムに決定
    int x = int(random(width));
    int y = int(random(height));
    //指定した場所の色を取得
    color col = img.get(x, y);
    fill(col, 127);
    //色の明度を円の直径にする
    float ellipseSize = map(saturation(col), 0, 255, 2, 50);
    //円を描画
    ellipse(x, y, ellipseSize, ellipseSize);
  }
}

四角形の大きさと角度で、画像を再構成する

PImage img; //画像データ
void setup() {
  size(1280, 720);
  img = loadImage("photo.jpg");
  background(0); //背景
  noStroke(); //枠線は無し
  rectMode(CENTER); //中心を原点に四角形を描画
}
void draw() {
  for (int i=0; i<100; i++) {
    //場所をランダムに決定
    int x = int(random(width));
    int y = int(random(height));
    //色を取得
    color col = img.get(x, y);
    fill(col, 127);
    pushMatrix();
    translate(x, y);
    //色の色相を回転角度に
    rotate(map(hue(col), 0, 255, 0, PI));
    //色の再度を四角形の幅に
    float w = map(saturation(col), 0, 255, 1, 40);
    rect(0, 0, w, 1.5);
    popMatrix();
  }
}

膨張するピクセル

ArrayList bubbles; //Bubbleクラスを格納するArrayList
PImage img; //色をピックアップするイメージ
int maxSize = 60; //円の最大サイズ(直径)
void setup() {
  //画面初期設定
  size(800, 600);
  frameRate(60);
  noStroke();
  //ArrayListの初期化
  bubbles = new ArrayList();
  //画像を読み込んで、画面の大きさにリサイズ
  //画像の名前は読み込んだ画像に変更する
  img = loadImage("photo.jpg");
  img.resize(width, height);
  //最初のきっかけの円を描画
  for (int i = 0; i < 10; i++) {
    PVector loc = new PVector(random(width), random(height));
    bubbles.add(new Bubble(loc));
  }
}
void draw() {
  background(0);
  //ArrayListに格納された数だけ、Bubbleを描画
  for (int i = 0; i < bubbles.size(); i++) {
    bubbles.get(i).draw();
  }
  //Bubbleの状態を更新
  for (int i = 0; i < bubbles.size(); i++) {
    //もしアクティブな状態だったら
    if (bubbles.get(i).isDead == false) {
      //円の周囲のピクセルの色を確認
      boolean expand = bubbles.get(i).checkPixel();
      //もしこれ以上膨張できない場合
      if (expand == false) {
        //新規にBubbleを生成
        PVector loc;
        //余白がみつかるまで繰り返し
        while (true) {
          loc = new PVector(random(width), random(height));
          color c = get(int(loc.x), int(loc.y));
          if ((red(c) + blue(c) + green(c)) == 0) break;
        }
        //余白に新規Bubbleを生成
        bubbles.add(new Bubble(loc));
        bubbles.get(i).isDead = true;
      }
    }
  }
}
//マウスクリックで初期化
void mouseClicked() {
  //ArrayListをクリア
  bubbles.clear();
  //きっかけの円を描画
  for (int i = 0; i < 100; i++) {
    PVector loc = new PVector(random(width), random(height));
    bubbles.add(new Bubble(loc));
  }
}
//Bubbleクラス
//円が膨張しながら空間を充填していく
class Bubble {
  float size; //円のサイズ(直径)
  float expandSpeed; //膨張スピード
  color circleColor; //円の色
  PVector location; //中心の位置
  boolean expand; //膨張中か否か
  boolean isDead; //活動している状態か否か
  //コンスタラクター
  Bubble(PVector _location) {
    location = _location; //位置を引数から取得
    //パラメータの初期値設定
    size = 0;
    expandSpeed = 4.0;
    expand = true;
    isDead = false;
    //読み込んだ画像から中心位置と同じピクセルの色を取得
    circleColor = img.get(int(location.x), int(location.y));
  }
  //円の描画
  void draw() {
    //もし膨張中なら
    if (expand == true) {
      //指定した速度で膨張
      size += expandSpeed;
    }
    //円を描画
    fill(circleColor);
    ellipse(location.x, location.y, size, size);
  }
  //円の周囲のピクセルの色を取得して、まだ膨張する余地があるかを判断する
  boolean checkPixel() {
    //次のフレームでのサイズを計算
    float nextSize = size + expandSpeed;
    for (float i = 0; i < TWO_PI; i += 0.01) {
      //円の周囲の座標を取得
      int x = int(cos(i) * nextSize / 2.0 + location.x);
      int y = int(sin(i) * nextSize / 2.0 + location.y);
      //取得した座標の直下のピクセルの色を取得
      color c = get(x, y);
      //色が黒意外、もしくは最大サイズを超えていたら、膨張を中止
      if ((red(c) + blue(c) + green(c)) > 0 || size > maxSize) {
        expand = false;
      }
    }
    //現在の膨張の状態を返す
    return expand;
  }
}

第6回 Processing実習 2 : Visual Harmony – アルゴリズムによるアニメーション表現

今回は、より複雑なアニメーションに挑戦します。たくさんの図形を、一定の手続き(= アルゴリズム)にそって動かしてみます。動きのアルゴリズムはいろいろありますが、今回はその一例として、三角関数(sin, cos , tan)を使用した動きをとりあげます。三角関数の定義は、直角三角形の角度とそれを取り囲む辺の比率で定義されます。しかし、同時に円運動を座標に変換する仕組みとしても活用可能です。この仕組みを応用してアニメーションするとどうなるか、三角関数を用いたアニメーションで美しいパターン「ビジュアルハーモニー」を生成する手法を紹介します。

スライド資料

サンプルプログラム

sin関数による動き

PVector location;
PVector velocity;

void setup() {
  size(800, 800);
  frameRate(60);
  background(0);
  noStroke();
  velocity = new PVector(4, 0);
  location = new PVector(0, 0);
}

void draw() {
  background(0);
  translate(width / 2.0, height / 2.0);
  location.add(velocity);
  location.y = sin(frameCount/20.0)*height/4.0;
  fill(0, 127, 255);
  ellipse(location.x, location.y, 10, 10);
  if (location.x > width/2) {
    location.x = -width/2;
  }
}

円運動

PVector location;
float angle, velocity, radius;

void setup() {
    size(800, 800);
    frameRate(60);
    background(0);
    noStroke();
    angle = 0.0; //現在の角度
    velocity = 2.0; //角速度
    radius = height / 4.0; //半径
    location = new PVector(0, 0);
}

void draw() {
    background(0);
    translate(width / 2.0, height / 2.0);
    location.x = cos(radians(angle)) * radius;
    location.y = sin(radians(angle)) * radius;
    fill(0, 127, 255);
    ellipse(location.x, location.y, 10, 10);
    angle += velocity; //角速度から角度を更新
}

配列をつかった沢山の円運動

int NUM = 32;
PVector[] location = new PVector[NUM];
float angle, velocity, radius;

void setup() {
    size(800, 800);
    frameRate(60);
    background(0);
    noStroke();
    angle = 0.0;
    velocity = 2.0;
    radius = height / 4.0;
    for (int i = 0; i < NUM; i++) {
        location[i] = new PVector(0, 0);
    }
}

void draw() {
    background(0);
    translate(width / 2.0, height / 2.0);
    for (int i = 0; i < NUM; i++) {
        location[i].x = cos(radians(angle) / NUM * (i+1)) * radius;
        location[i].y = sin(radians(angle) / NUM * (i+1)) * radius;
        fill(0, 127, 255);
        ellipse(location[i].x, location[i].y, 10, 10);
    }
    angle += velocity;
}

ビジュアルハーモニー 01 - 基本

int NUM = 32;
PVector[] location = new PVector[NUM];
float angle, velocity, radius;

void setup() {
    size(800, 800);
    frameRate(60);
    background(0);
    noFill();
    angle = 0.0;
    velocity = 2.0;
    radius = height / 4.0;
    for (int i = 0; i < NUM; i++) {
        location[i] = new PVector(0, 0);
    }
}

void draw() {
    background(0);
    translate(width / 2.0, height / 2.0);
    for (int i = 0; i < NUM; i++) {
        location[i].x = cos(radians(angle) / NUM * (i+1)) * radius;
        location[i].y = sin(radians(angle) / NUM * (i+1)) * radius;
        stroke(0, 127, 255);
        ellipse(location[i].x, location[i].y, 4 * i, 4 * i);
    }
    angle += velocity;
}

ビジュアルハーモニー 02 - 円の数を増やしてみる

int NUM = 256;
PVector[] location = new PVector[NUM];
float angle, velocity, radius;

void setup() {
    size(800, 800, P3D);
    frameRate(60);
    background(0);
    noFill();
    angle = 0.0;
    velocity = 2.0;
    radius = height / 4.0;
    for (int i = 0; i < NUM; i++) {
        location[i] = new PVector(0, 0);
    }
}

void draw() {
    background(0);
    translate(width / 2.0, height / 2.0);
    for (int i = 0; i < NUM; i++) {
        location[i].x = cos(radians(angle) / NUM * (i+1)) * radius;
        location[i].y = sin(radians(angle) / NUM * (i+1)) * radius;
        stroke(0, 127, 255);
        float diameter = i * (height / 2) / float(NUM);
        ellipse(location[i].x, location[i].y, diameter, diameter);
    }
    angle += velocity;
}

ビジュアルハーモニー 03 - 周波数の比率を変化させる

int NUM = 256;
PVector[] location = new PVector[NUM];
float angle, velocity, radius;

void setup() {
    size(800, 800, P3D);
    frameRate(60);
    background(0);
    noFill();
    angle = 0.0;
    velocity = 2.0;
    radius = height / 4.0;
    for (int i = 0; i < NUM; i++) {
        location[i] = new PVector(0, 0);
    }
}

void draw() {
    background(0);
    translate(width / 2.0, height / 2.0);
    for (int i = 0; i < NUM; i++) {
        location[i].x = cos(radians(angle) / NUM * (i+1) * 1.2) * radius;
        location[i].y = sin(radians(angle) / NUM * (i+1) * 1.5) * radius;
        stroke(0, 127, 255);
        float diameter = i * (height / 2) / float(NUM);
        ellipse(location[i].x, location[i].y, diameter, diameter);
    }
    angle += velocity;
}

第5回 Processing実習 1 : 動きを生みだす – アニメーションとベクトル

今回からいよいよ動きのある表現(= アニメーション)について扱っていきます。アニメーションを実現するには、まず時間を扱う基本構造を知る必要があります。Processingでは、setup(), draw() という2つのブロックにわけて、初期化と更新を行うことでアニメーションを実現しています。まず始めはこの基本構造について理解します。次に、これから動きを扱う際に、向きと大きさをもった「ベクトル」という概念を理解します。ベクトルを理解することで、位置や運動を整理して記述することが可能となります。最後に、この基本構造をベクトルを活用して簡単なアニメーションを作成します。

スライド資料

サンプルコード

サンプルコード 1

//初期化関数
void setup() {
  size(800, 600);
  frameRate(12);   //書き換え頻度の設定
  background(0);
}
//メインループ
void draw() {
  float diameter = random(100);
  noStroke();
  fill(random(255), random(255), random(255));
  ellipse(random(width), random(height), diameter, diameter);
}

サンプルコード 2

float locationX, locationY; //円の中心位置を格納する変数
float velocityX, velocityY; //円の速度を格納する変数
void setup() {
    size(800, 600); //800x600 pixelの画面を生成
    frameRate(60); //フレームレート
    locationX = width/2; //円の初期位置X
    locationY = height/2; //円の初期位置Y
    velocityX = 3; //円の初期位置X
    velocityY = 2; //円の初期位置Y
}

void draw() {
    background(0); //背景を描画
    locationX = locationX + velocityX; //円のX座標を更新
    locationY = locationY + velocityY; //円のY座標を更新
    noStroke(); //枠線なし
    fill(0, 127, 255); //塗りの色
    ellipse(locationX, locationY, 20, 20); //指定した位置に円を描画
}

サンプルコード 3

float locationX, locationY; //円の中心位置を格納する変数
float velocityX, velocityY; //円の速度を格納する変数

void setup() {
    size(800, 600); //800x600 pixelの画面を生成
    frameRate(60);  //フレームレート
    locationX = width/2; //円の初期位置X
    locationY = height/2; //円の初期位置Y
    velocityX = 3;  //円の初期位置X
    velocityY = 2;  //円の初期位置Y
}

void draw() {
    background(0); //背景を描画
    locationX = locationX + velocityX; //円のX座標を更新
    locationY = locationY + velocityY; //円のY座標を更新
    noStroke(); //枠線なし
    fill(0, 127, 255); //塗りの色
    //指定した位置に円を描画
    ellipse(locationX, locationY, 20, 20); 

    if (locationX < 0 || locationX > width) {
        //もし画面の左端、または右端に到達したら
        //X方向のスピードを反転
        velocityX = velocityX * -1;
    }
    if (locationY < 0 || locationY > height) {
        //もし画面の下端、または上端に到達したら
        //Y方向のスピードを反転
        velocityY = velocityY * -1; 
    }
}

サンプルコード 4

PVector location; //円の中心位置を格納する変数
PVector velocity; //円の速度を格納する変数
void setup() {
    size(800, 600); //800x600 pixelの画面を生成
    frameRate(60); //フレームレート
    location = new PVector(random(width), random(height));
    velocity = new PVector(random(-10, 10), random(-10, 10));
}

void draw() {
    background(0); //背景を描画
    //位置のベクトルに速度のベクトルを加算、次の位置になる
    location.add(velocity); 

    noStroke();
    fill(0, 127, 255);
    ellipse(location.x, location.y, 20, 20);
    if (location.x < 0 || location.x > width) {
        velocity.x = velocity.x * -1;
    }
    if (location.y < 0 || location.y > height) {
        velocity.y = velocity.y * -1;
    }
}

サンプルコード 5

int NUM = 100; //配列の数
//位置のベクトルの配列
PVector[] location = new PVector[NUM];
//速度のベクトルの配列
PVector[] velocity = new PVector[NUM];

void setup() {
  size(800, 600); //800x600pixelの画面を生成
  frameRate(60); //フレームレート
  noStroke(); //枠線なし
  fill(0, 127, 255); //塗りの色
  for (int i = 0; i < NUM; i++) { //配列の数だけ繰り返し
    //位置のベクトルの初期設定
    location[i] = new PVector(random(width), random(height));
    //速度のベクトルの初期設定
    velocity[i] = new PVector(random(-4, 4), random(-4, 4));
  }
}

void draw() {
  background(15); //背景を描画
  for (int i = 0; i < NUM; i++) { //配列の数だけ繰り返し
    //指定した位置に円を描画
    ellipse(location[i].x, location[i].y, 20, 20);
    //位置のベクトルに速度のベクトルを加算、次の位置になる
    location[i].add(velocity[i]);
    //もし画面の左端、または右端に到達したら
    if ((location[i].x > width) || (location[i].x < 0)) {
      velocity[i].x *= -1; //X方向のスピードを反転
    }
    //もし画面の下端、または上端に到達したら
    if ((location[i].y > height) || (location[i].y < 0)) {
      velocity[i].y *= -1; //Y方向のスピードを反転
    }
  }
}

サンプルコード 6

int NUM = 500; //配列の数
//位置のベクトルの配列
PVector[] location = new PVector[NUM];
//速度のベクトルの配列
PVector[] velocity = new PVector[NUM];
//塗りの色の配列
color[] col = new color[NUM];
//円の大きさ(直径)の配列
float[] diameter = new float[NUM];

void setup() {
  size(800, 600); //800x600pixelの画面を生成
  frameRate(60); //フレームレート
  noStroke();
  for (int i = 0; i < NUM; i++) { //配列の数だけ繰り返し
    //位置のベクトルの初期設定
    location[i] = new PVector(random(width), random(height));
    //速度のベクトルの初期設定
    velocity[i] = new PVector(random(-4, 4), random(-4, 4));
    //色の初期設定
    col[i] = color(random(255), random(255), random(255), 192);
    //大きさの初期設定
    diameter[i] = random(3, 40);
  }
}

void draw() {
  background(15); //背景を描画
  //配列の数だけ繰り返し
  for (int i = 0; i < NUM; i++) {
    fill(col[i]); //色を指定
    //指定した位置に円を描画
    ellipse(location[i].x, location[i].y, diameter[i], diameter[i]);
    //位置のベクトルに速度のベクトルを加算、次の位置になる
    location[i].add(velocity[i]);
    //もし画面の左端、または右端に到達したら
    if ((location[i].x > width) || (location[i].x < 0)) {
      velocity[i].x *= -1; //X方向のスピードを反転
    }
    //もし画面の下端、または上端に到達したら
    if ((location[i].y > height) || (location[i].y < 0)) {
      velocity[i].y *= -1; //Y方向のスピードを反転
    }
  }
}

第3回: メディアアート・プログラミング基礎2 – くりかえし、乱数

今回は「くりかえし」に焦点をあてて、プログラミングによる表現を試みます。くりかえし(反復)は、コンピューターが得意とする作業です。プログラミングに繰り返す回数を指定してその内容を指示することで、コンピューターは瞬時に大量の繰り返し作業を完了します。人間のように、途中で飽きたり作業にぶれが出たりすることはありません。くりかえし(反復)の操作を使用することで、手作業では困難であったり長時間を要するような表現が可能となります。

まず始めにくりかえしの操作の際に必要となる、プログラム内で任意のデータ(数、文字、色など)を格納するための仕組みである「変数」について解説します。その上で「for文」というくりかえしのための構文を用いてくりかえしの基本を学び、それを使用して形を描いていきます。

さらに「乱数 (random)」という規則性のない数を生成するための方法を取り入れて、指定した数、特定の範囲にランダムに形や位置や色を指定していくことで、どのような表現が可能となるのか、実際のプログラム例を通して探求します。

スライド資料


第2回 メディアアート・プログラミング基礎1 かたちとコード – Processingで図形を描くには?

実際に手(と頭)を動かしながら、実際にメディアアート作品の制作にむけたプログラミングを始めていきます。今回は、コンピューターのスクリーン上に図形を描くにはどうすれば良いのかについて考えていきます。

コンピューターで形を描くことは、手で図形で描くこととは大きく異なります。図形そのものを直接描くのではなく、図形を描くという行為を抽象化してそれをコンピュータに理解できる方法に翻訳して伝達する必要があります。コンピュータによって図形を描く方法は、過去多くの技術者やアーティスト、デザイナー達が試行錯誤を繰り返しながら発展してきました。まず始めに、コンピューターを用いて形を描いて表現する様々な試みについて紹介します。

後半は、このワークショップで主に使用するProcessingを用いて、その操作方法の基本を解説した上で、簡単な形を描く方法と色を塗る方法について基本から習得していきます。

スライド資料


第1回 オリエンテーション

初回となる今回は、まず、講義全体のオリエンテーションを行います。全体のスケジュール、目標、成績の評価基準、担当教員とその担当範囲など、講義に関する基本的な情報について説明します。次に、この講義の一環として行うメディアアートの企画展等と展示支援についての解説を行います。最後に、この講義で主に使用する開発環境であるProcessingについての導入的な解説をします。

最後に簡単なアンケートを行います。

【重要】履修選抜課題 (4/16締切)

履修希望者が多数のため、課題による履修選抜を行います。以下のフォームから回答してください。

スライド資料