この講義ではProcessingを使用してクリエイティブ・コーディングの基礎を学んできました。今回は、これまでのまとめと、ここから先へ進むためのステップについて解説します。
Processingの基礎を身に付けてから次に進む様々な手段がありますが、ここでは次の2つの方法を紹介します。
さらに高速化を追い求める – openFrameworks
ノードベースのビジュアルプログラミング – TouchDesigner
openFrameworks は、クリエイティブコーディングのためのC++のオープンソースツールキットです。その最大の利点はスピードです。Processingでは処理が追いつかずコマ落ちしてしまうような表現も高速な演算で実現可能です。より本格的な作品制作を目指す方にはProcessingを習得した後、openFrameworksにステップアップすることをお勧めします。openFrameworksには、Processingのライブラリーにあたるアドオン(addons)という拡張機能があり、膨大な数のアドオンが公開されている のも魅力の一つです。
TouchDesigner は、ノードベースのビジュアルプログラミング環境です。様々な機能が内包された箱(オペレーター)を画面上に配置して、それらをパッチケーブルで接続していくことで、多彩で高機能なプログラム開発が可能となります。ただし、TouchDesignerはDerivative社が開発販売している製品であり、ProcessingやopenFrameworksとは違いオープンソースのプロジェクトではありません。ただし、非商用の利用で、画像の最大サイズが1280×1280 Pixel以内であれば、無料で使用することが可能です。
自分の作品の表現形態やテーマにあわせて、次のステップへ進んでいきましょう!
スライド資料
前回に引き続きp5.jsについて取り上げます。p5.jsはWebブラウザー上で動作します。ですので、Webブラウザー上にテキストエディターの機能を加えることで編集と実行環境をオンラインで実現可能です。さらに、編集中のコードを自動的に読み込んで実行することで、コードを実行しながら編集を行うという「ライブコーディング (Live Coding)」が可能となります。オンラインで公開されているp5.jsのWebエディターでこの機能が実現されています。
今回は、このp5.jsライブコーディングの機能に着目して、その可能性を探っていきます。また、関連するプロジェクトとして過去のコンピューターアートの名作をProcessingで再現するReCode Project について紹介します。
SFPC short time lapse from poohlaga on Vimeo .
スライド資料
p5.js は、Javascript版のProcessingです。Javascriptをベースにしている最大の利点はWebブラウザー上で実行可能という点です。作成したスケッチをWebサーバー上ですぐに世界に向けて公開可能です。文法はProcessingをベースにしているので、習得も容易です。ただし、ベースとなる言語が違うため若干オリジナルのProcessingと異なる部分もあります。この差異に注意しつつ、p5.jsでのプログラミングの導入方法を解説します。
スライド資料
「MOTサテライト2018秋」展示プランのアンケート
Processingを使用してインタラクティブな機能を実現するための手段は、センサーを使う方法や、KinectやLeap Motionなどのデバイスを使用する方法などいろいろ考えられます。今回は、最もシンプルな機材構成で可能な方法として、カメラの映像を解析してそこから動きや物体の輪郭を取り出す手法について取り上げます。
コンピュータで、映像から実世界の情報を取得して認識するための研究で「コンピュータ・ビジョン (Conputer Vision)」という分野が存在します。わかりやすく言うなら「ロボットの目」をつくるような研究です。このコンピュータ・ビジョンの様々な成果をオープンソースで公開しているOpenCV というライブラリーがあります。今回は、このOpenCVをProcessingで使用できるようにした、OpenCV for Processingライブラリー を使用したプログラミングを体験します。
スライド資料
サンプルプログラム
カメラキャプチャー基本
import gab.opencv.*;
import processing.video.*;
Capture video; // ライブカメラ
void setup() {
//初期設定
size(640, 480); //画面サイズ
//キャプチャーするカメラのサイズ
video = new Capture(this, 640, 480);
//キャプチャー開始
video.start();
}
void draw() {
//カメラ画像を表示
image(video, 0, 0 );
}
//キャプチャーイベント
void captureEvent(Capture c) {
c.read();
}
OpenCV輪郭抽出
import gab.opencv.*;
import processing.video.*;
Capture video; // ライブカメラ
OpenCV opencv; // OpenCV
ArrayList contours; //輪郭の配列
void setup() {
//初期設定
size(640, 480); //画面サイズ
//キャプチャーするカメラのサイズ
video = new Capture(this, 640, 480);
//OpenCVの画面サイズ
opencv = new OpenCV(this, 640, 480);
//キャプチャー開始
video.start();
}
void draw() {
//カメラの画像をOpenCVに読み込み
opencv.loadImage(video);
//カメラ画像を表示
image(video, 0, 0 );
//閾値の設定(マウスのX座標で変化)
int threshold = int(map(mouseX, 0, width, 0, 100));
opencv.threshold(threshold);
//輪郭抽出
contours = opencv.findContours();
//描画設定
noFill();
strokeWeight(1);
//検出された輪郭の数だけ、輪郭線を描く
for (Contour contour : contours) {
stroke(0, 255, 0);
contour.draw();
}
}
//キャプチャーイベント
void captureEvent(Capture c) {
c.read();
}
OpenCVによる顔検出
import gab.opencv.*;
import processing.video.*;
import java.awt.*;
Capture video; //ビデオキャプチャー
OpenCV opencv; //OpenCV
void setup() {
size(640, 480);
//ビデオキャプチャー初期化
video = new Capture(this, 640/2, 480/2);
//OpenCV初期化(ビデオキャプチャーの半分のサイズ)
opencv = new OpenCV(this, 640/2, 480/2);
//顔の学習データを読み込み
opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
//ビデオキャプチャー開始
video.start();
}
void draw() {
//二倍サイズで表示
scale(2);
//画像を読み込み
opencv.loadImage(video);
//カメラ画像を描画
image(video, 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) {
c.read();
}
OpenCVによる顔検出 – 目線を入れてみる
import gab.opencv.*;
import processing.video.*;
import java.awt.*;
Capture video; //ビデオキャプチャー
OpenCV opencv; //OpenCV
void setup() {
size(640, 480);
//ビデオキャプチャー初期化
video = new Capture(this, 640/2, 480/2);
//OpenCV初期化(ビデオキャプチャーの半分のサイズ)
opencv = new OpenCV(this, 640/2, 480/2);
//顔の学習データを読み込み
opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
//ビデオキャプチャー開始
video.start();
}
void draw() {
//二倍サイズで表示
scale(2);
//画像を読み込み
opencv.loadImage(video);
//カメラ画像を描画
image(video, 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 video; // ライブカメラ
OpenCV opencv; // OpenCV
void setup() {
//初期設定
size(640, 480); //画面サイズ
//キャプチャーするカメラのサイズ
video = new Capture(this, 640/2, 480/2);
//OpenCVの画面サイズ
opencv = new OpenCV(this, 640/2, 480/2);
//キャプチャー開始
video.start();
}
void draw() {
//描画スケール設定
scale(2.0);
//カメラの画像をOpenCVに読み込み
opencv.loadImage(video);
//カメラ画像を表示
image(video, 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 video; // ライブカメラ
OpenCV opencv; // OpenCV
int NUM = 500;
ParticleVec3[] particles = new ParticleVec3[NUM];
void setup() {
//初期設定
size(640, 480, P3D); //画面サイズ
//キャプチャーするカメラのサイズ
video = new Capture(this, 640/2, 480/2);
//OpenCVの画面サイズ
opencv = new OpenCV(this, 640/2, 480/2);
//キャプチャー開始
video.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(video);
//カメラ画像を表示
//image(video, 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);
}
}
}
今回は、Processingで音を扱うためのSoundライブラリーを紹介します。
Soundライブラリーは、Contributionライブラリーではなく、Processing Foundationで開発されているライブラリーで、Processingのパッケージに最初から含まれています。サウンドファイルの再生や、マイクやライン入力のキャプチャーだけでなく、音にエフェクトをかけたり、波形自体を生成したりと、サウンドに関する様々な機能が活用できます。さらに、FFTクラスを使用すると、音に含まれる周波数成分をリアルタイムに解析することが可能となります。この機能を使うことで、音の周波数成分を可視化して「音を視る」ことが可能となります。今回は、Soundライブラリーを用いたサウンドファイルの再生から、FFTを使用した音のビジュアライズまでを順番に解説していきます。
スライド資料
サンプルプログラム
// 01 サウンドの再生
import processing.sound.*;
SoundFile soundfile;
void setup() {
soundfile = new SoundFile(this, "sound.aiff");
soundfile.loop();
}
void draw(){
}
// 02 音量とスピードをマウスで変化
import processing.sound.*;
SoundFile soundfile;
void setup() {
size(800, 600);
soundfile = new SoundFile(this, "sound.aiff");
soundfile.loop();
}
void draw() {
background(0);
float rate = map(mouseX, 0, width, 0.0, 2.0);
float amp = map(mouseY, 0, height, 1.0, 0.0);
soundfile.rate(rate);
soundfile.amp(amp);
}
// 03 ディレイ
import processing.sound.*;
SoundFile soundfile;
Delay delay;
void setup() {
size(800, 600);
soundfile = new SoundFile(this, "sound.aiff");
delay = new Delay(this);
soundfile.loop();
delay.process(soundfile, 5);
}
void draw() {
background(0);
float delayTime = map(mouseY, 0, height, 0.0001, 1.0);
delay.time(delayTime);
float feedback = map(mouseX, 0, width, 0.0, 0.9);
delay.feedback(feedback);
}
//04 音量を解析
import processing.sound.*;
SoundFile soundfile;
Amplitude rms;
void setup() {
size(800, 600);
fill(0, 127, 255);
noStroke();
soundfile = new SoundFile(this, "sound.aiff");
soundfile.loop();
rms = new Amplitude(this);
rms.input(soundfile);
}
void draw() {
background(0);
float diameter = map(rms.analyze(), 0.0, 1.0, 0.0, width);
ellipse(width/2, height/2, diameter, diameter);
}
//05 音量を解析 (サウンド入力)
import processing.sound.*;
AudioIn input;
Amplitude rms;
void setup() {
size(800, 600);
fill(0, 127, 255);
noStroke();
input = new AudioIn(this, 0);
input.start();
rms = new Amplitude(this);
rms.input(input);
}
void draw() {
background(0);
float diameter = map(rms.analyze(), 0.0, 1.0, 0.0, width);
ellipse(width/2, height/2, diameter, diameter);
}
//06 FFT基本
import processing.sound.*;
AudioIn in;
FFT fft;
int bands = 1024;
float scale = 20.0;
void setup() {
size(800, 600);
in = new AudioIn(this, 0);
in.start();
fft = new FFT(this, bands);
fft.input(in);
}
void draw() {
background(0);
fft.analyze();
noFill();
stroke(255);
beginShape();
for (int i = 0; i < bands; i++) {
vertex(i * width/float(bands), height - fft.spectrum[i] * height * scale);
}
endShape();
}
//06 FFT : 明度で表現
import processing.sound.*;
AudioIn in;
FFT fft;
int bands = 128;
float scale = 5000.0;
void setup() {
size(800, 600);
noStroke();
in = new AudioIn(this, 0);
in.start();
fft = new FFT(this, bands);
fft.input(in);
}
void draw() {
background(0);
fft.analyze();
float w = width/float(bands)/2.0;
for (int i = 0; i < bands; i++) {
fill(fft.spectrum[i] * scale);
rect(width/2.0 + i * w, 0, w, height);
rect(width/2.0 - i * w, 0, w, height);
}
}
//07 FFT : 明度と色相で表現
import processing.sound.*;
AudioIn in;
FFT fft;
int bands = 128;
float scale = 5000.0;
void setup() {
size(800, 600);
colorMode(HSB, 360, 100, 100, 100);
noStroke();
in = new AudioIn(this, 0);
in.start();
fft = new FFT(this, bands);
fft.input(in);
}
void draw() {
background(0);
fft.analyze();
float w = width/float(bands)/2.0;
for (int i = 0; i < bands; i++) {
float hue = 360/float(bands) * i;
fill(hue, 100, fft.spectrum[i] * scale);
rect(width/2.0 + i * w, 0, w, height);
rect(width/2.0 - i * w, 0, w, height);
}
}
//08 FFT : 円の大きさで表現
import processing.sound.*;
AudioIn in;
FFT fft;
int bands = 128;
float scale = 40000.0;
void setup() {
size(800, 600, P2D);
noStroke();
blendMode(ADD);
colorMode(HSB, 360, 100, 100, 100);
in = new AudioIn(this, 0);
in.start();
fft = new FFT(this, bands);
fft.input(in);
}
void draw() {
background(0);
fft.analyze();
float w = width/float(bands)/2.0;
for (int i = 0; i < bands; i++) {
float hue = 360/float(bands) * i;
fill(hue, 100, 6);
float diameter = fft.spectrum[i] * scale;
ellipse(width/2.0 + i * w, height/2.0, diameter, diameter);
ellipse(width/2.0 - i * w, height/2.0, diameter, diameter);
}
}
//09 立方体でビジュアライズ
import processing.sound.*;
AudioIn in;
FFT fft;
int bands = 128;
float scale = 4000.0;
void setup() {
size(800, 600, P3D);
noStroke();
colorMode(HSB, 360, 100, 100, 100);
in = new AudioIn(this, 0);
in.start();
fft = new FFT(this, bands);
fft.input(in);
}
void draw() {
blendMode(ADD);
background(0);
fft.analyze();
for (int i = 0; i < bands; i++) {
float hue = map(i, 0, bands/4.0, 0, 360);
fill(hue, 100, 100, 30);
float boxSize = map(fft.spectrum[i], 0, 1, 0, scale);
float rot = map(i, 0, bands, 0, PI*8);
pushMatrix();
translate(width/2, height/2);
rotateX(rot+frameCount/frameRate);
rotateY((rot+frameCount/frameRate)*1.2);
box(boxSize);
popMatrix();
}
}
この演習では、ここまで主に2次元平面上での描画による表現を扱ってきました。今回は2次元での表現からさらに(文字通り)次元を越えて、3次元空間での表現について考えていきたいと思います。
ProcessingにはOpenGLという3Dのための描画エンジンを用いて3D表示をするモードが用意されています。これによって、複雑な数学的な処理や演算をすることなく3Dの描画が直感的に行えます。
しかし、3Dの物体を扱うには、これまでとは2Dの描画とは違った様々な要素が加わります。カメラ(視点)、ライティング、光と影(シェーディング)、奥行の重なりなどといった2次元の平面には無かった様々な技術や概念の理解が必要となります。
スライド資料
サンプルプログラム
ProcessingのPImageクラスは、外部のビットマップ画像(Jpeg, GIF、PNGなど)をデータとしてプログラムに読み込むことができます。読み込んだ画像は単に画面に表示するだけでなく、色や明度やサイズを変更して表示することができます。さらには、画像に含まれる全てのピクセルの色情報を読み取り配列に格納することが可能です。そのデータをもとに別の画像を再生成することが可能となり、読み込んだ画像データの色情報をもとにした多彩な表現が可能となります。
今回はProcessingに画像を読み込んで、分析再合成することで、様々な表現の可能性について探っていきます。
スライド資料
サンプルファイル
今回は、より複雑なアニメーションに挑戦します。たくさんの図形を、一定の手続き(= アルゴリズム)にそって動かしてみます。動きのアルゴリズムはいろいろありますが、今回はその一例として、三角関数(sin, cos , tan)を使用した動きをとりあげます。三角関数の定義は、直角三角形の角度とそれを取り囲む辺の比率で定義されます。しかし、同時に円運動を座標に変換する仕組みとしても活用可能です。この仕組みを応用してアニメーションするとどうなるか、三角関数を用いたアニメーションで美しいパターン「ビジュアルハーモニー」を生成する手法を紹介します。
スライド資料
サンプルプログラム
今回からいよいよ動きのある表現(= アニメーション)について扱っていきます。アニメーションを実現するには、まず時間を扱う基本構造を知る必要があります。Processingでは、setup(), draw() という2つのブロックにわけて、初期化と更新を行うことでアニメーションを実現しています。まず始めはこの基本構造について理解します。次に、これから動きを扱う際に、向きと大きさをもった「ベクトル」という概念を理解します。ベクトルを理解することで、位置や運動を整理して記述することが可能となります。最後に、この基本構造をベクトルを活用して簡単なアニメーションを作成します。
スライド資料
サンプルプログラム
授業内で使用したサンプルプログラムは下記からダウンロードしてください。
プログラムをしていく際に、3つの重要な構造化の形式があります。それは、「順次 (Sequence)」、「反復 (Iteration)」、「分岐 (Selection)」 です。今回は、この3つの構成要素の中の反復 (Iteration) に注目して、Processingで形を描きながら反復について考えます。
また、一意的でないランダムな値である「乱数」と反復を組合せて、色彩と形態でどのような表現か可能となるか探求していきます。
スライド資料