Processing日記(1)
長嶋 洋一
(一部 SuperCollider日記かも)
2011年2月24日(木)
マルチメディア室のコンピュータが新しくなる節目の2011年2月23日、 こんな作業 をやっていて、あと42台も同じ作業があるのか・・・とウンザリしたが、ちゃんとその中に SuperCollider と Processing を入れていたのを思い出し、フト、翌日の2011年2月24日、思い立った。 そういえば、これらのソフトはほとんど手つかずだったのだ(^_^;)。Processing は、かつて Arduino をやった時に、関連してちょっと触って、なんだJavaのお手軽版じゃん、と見切ったままだった(^_^;)。 Max/MSP/jitter があれば、たいてい同じ事が出来るからである。
そこで今回、新学期には、最近レベルが上がってきた学生に、 Max/MSP/jitter だけでなく、 SuperCollider と Processing も紹介して、ごく一部の学生であっても、これらを活用できるようにしよう、と決意した。 明日は一般前期入試だし、いろいろあって時間は限られているが、せっかくなので、 春休みの期間を活用して、両方を同時に並行して勉強してみよう。
昼過ぎまで教授会や委員会があったが、翌日が入試ということで午後は入構制限となって静かになったので、 まずは環境設定から開始した。 まずは Processing のサイトに行き、最新版をダウンロードしてインストールした。 1本のdmgファイルをダウンして解凍して、アプリのアイコンをコピーすれば完了である。 所要2分ほど。なるほど、オープンソースは。素晴らしい。
いろいろなリファレンスは ここ にあり、お約束のチュートリアルは ここ にある。当面、これに従って順におさらいしていけばOKだろう。
同時進行のSuperColliderに関して、日本語のページがある、という情報を発見した。 たぶんタケコさんのところだろう・・・と辿っていくと、 SuperColliderとProcessingとの連携 というページがあった。 なるほど、確かにOSCとか使えばSuperColliderはサーバなので容易に連携できそうだ。 そうなると、OSCで全体をまとめれば、
というのも簡単そうである。これはいいカモ。(^_^)
- 音源はSuperCollider
- グラフィクスはProcessing
- プログラミングはMax5
さて、せっかくなので、 チュートリアル のページに行き、たくさん並んでいるいちばん冒頭の Getting Started を選んで、眺めてみた。さっそく出て来たイラストが可愛い(^_^)。
とりあえずProcessingのアプリのアイコンをダブルクリックすると、プロジェクトウインドウが出た。 これはArduinoとまったく同じである。 ここに、サンプルコードとして、「Example 1: Draw and Ellipse」にあった1行をコピペで
というソースコードを置き、Arduinoと同じと確信して「実行」ボタンを押すと、結果のウインドウが現れて、以下のようになった。 これが「初めてのProcessingブログラミング」の実行、ということである。ellipse(50, 50, 80, 80); 続いて、サンプルコードとして、「Example 2-2: Make Circles」にあったサンプルをコピペで
というソースコードを置き、Arduinoと同じと確信して「実行」ボタンを押すと、結果のウインドウが現れて、以下のようになった。 ウインドウ内で動かすマウスを追いかけて、どんどん円が上書きされる、インタラクティブなグラフィックソフトの完成である(^_^)。void setup() { size(480, 120); smooth(); } void draw() { if (mousePressed) { fill(0); } else { fill(255); } ellipse(mouseX, mouseY, 80, 80); } この先にあったのは、「ストップ」「保存」「Share」などであり、特にアプレットでの書き出しもスムースだった。 実際に、このように 簡単に結果がアプレットとして表示できた。これは素晴らしい。(^_^)
2011年2月26日(土)
引き続き、 チュートリアル のページに行き、 Getting Started に続いて、 Processing Overview を眺めてみることにした。これでだいたいProcessingの全貌が判るはずである。まず冒頭に、 Processing is a simple programming environment that was created to make it easier to develop visually oriented applications with an emphasis on animation and providing users with instant feedback through interaction. とあった。まぁ、これがProcessingのもっとも簡単な概要、ということである。 これに続いて「Sketching with Processing」があったが、まぁ、これは ここ と相当に重複するのでパス。(^_^;)
続いて「Hello world」である。 ここはとりあえず、記載されたサンプルをそれぞれスケッチしてみることにした。 最初の「Hello world」プログラムは以下である。
これをコンパイルさせると以下のようになった。line(15, 25, 70, 90); どんな処理系でもだいたい同じであるが、座標は左上が(0,0)であり、右がx、下がyである。 「line」は始点と終点の座標を指定して直線で結ぶのであろう。 プログラムの座標を入れ替えて
としてもまったく同じ結果が表示されることで、これを確認できた。line(70, 90, 15, 25); 続いてのサンプルは以下である。
「size」はウインドウサイズ、「background」は背景色(おそらくRGB)、と予想できたが、「stroke」は初めてである。 とりあえずこれをコンパイルさせると以下のようになった。size(400, 400); background(192, 64, 0); stroke(255); line(150, 25, 270, 350); 「stroke」については、続いて解説があった。以下である。つまりは、続く「line」の色指定である。 おそらく「lineの色を指定」でなく、「続く描画の色指定」なのでは。
stroke(255); // sets the stroke color to white stroke(255, 255, 255); // identical to the line above stroke(255, 128, 0); // bright orange (red 255, green 128, blue 0) stroke(#FF8000); // bright orange as a web color stroke(255, 128, 0, 128); // bright orange with 50% transparency これに続くのは「Hello mouse」である。 ソースは以下である。
実行結果は以下のようになった。 マウスの位置に、(150, 25)から刻々と白い直線が描画された。void setup() { size(400, 400); stroke(255); background(192, 64, 0); } void draw() { line(150, 25, mouseX, mouseY); } アプレットとして書き出してみると、このようになる。
「setup()」という関数は最初に一度だけ、そして「draw()」という関数はProcessingのシステムが自動で無限ループするらしい。 また、「size()」関数は、必ず「setup()」の1行目に置く必要があるという。 上記のプログラムではどんどん白線が上書きされていたが、これを以下のようにすると、マウス追従して1本の白線が動く。
void setup() { size(400, 400); stroke(255); } void draw() { background(192, 64, 0); line(150, 25, mouseX, mouseY); } アプレットとして書き出してみると、このようになる。 無限ループしている「draw()」の方に、背景塗り潰しを入れたので、これも当然の動作である。
もう一つのサンプルとして以下があった。マウスボタンを押すと画面内をクリアする、というものである。
void setup() { size(400, 400); stroke(255); } void draw() { line(150, 25, mouseX, mouseY); } void mousePressed() { background(192, 64, 0); } ProcessingはJavaなので、当然ながら、マウスボタンについては指定が無い。これは1ボタンのMacでも、2ボタンのWindowsでも、3ボタンのUnixでも動くJavaでの常識である。 アプレットとして書き出してみると、このようになった。
この続きには「Exporting and distributing your work」があったが、これは既にアプレットとして書き出しているのでパス。
続いて「Creating images from your work」である。 無限ループの続く「draw()」を実行したたびに、その結果を連番の画像ファイルとして書き出すのが「saveFrame()」だそうだ。 高画質のためにPDF書き出しまで出来るらしい。まぁ、これは必要になったらやってみる事にしよう。
「Examples and reference」に続いて、「More about size()」ということで、size()の詳しい解説があった。 良くない例として
size(400, 400); ellipse(200, 200, 50, 50); を紹介している。表示させると以下になる。400*400のスクリーンの中央に円が描画された。
そして、推奨する例として
size(400, 400); ellipse(width/2, height/2, 50, 50); を紹介している。表示させると同じであったが、予約語「width」「height」により、スクリーンサイズを変えても常に画面の中央に来る。 この方が美しいということである。 まぁこれはよくある話だが、この後は重要である。 「size()」関数は単にスクリーンサイズを指定するだけでなく、Javaの色々なレンダリングモードを指定するらしい。
単なる「size(400, 400);」は、defaultとして「size(400, 400, JAVA2D);」であるという。これは高品質の2次元ベクター・グラフィクス・レンダラーであるが、Javaを生で使っているので、遅いらしい(^_^;)。
これに対して、 「size(400, 400, P2D);」 および 「size(400, 400, P3D);」 は、Processingに特化した2次元/3次元グラフィクス・レンダラーなので高速であり、また画像やビデオに対する画素ごとの処理も容易だという。ここは重要だ。
さらに、 「size(400, 400, OPENGL);」 は、Sun's JOGL (Java for OpenGL)を使った高速3次元グラフィクス・レンダラーである。ライブラリをインポートする必要があるらしいが、アプレットに書き出すのであればこれを使うべきだろうか。
そして、 「size(400, 400, PDF, "output.pdf");」 としてやると、OpenGLと似た品質で、PDFの書き出しに対応したグラフィクス・レンダラーが指定できるという。 これはなかなかに優れている。
つづいて「Loading and displaying data」である。外部データが自在に取り込めるのは、まぁJava系であれば当然だろう。 ここでは、「loadImage()」「loadStrings()」という関数が例示されている。 サンプルとしては、
// Examples of loading a text file and a JPEG image // from the data folder of a sketch. String[] lines = loadStrings("something.txt"); PImage image = loadImage("picture.jpg"); となっている。この検索パスとしては、Sketch(.pde)が置かれた同じフォルダ内に置くべし、とある。 手作業でもいいが、メニューから「Add...」として加えるのが間違いなさそうである。 ここにはロードした画像を表示するための関数のサンプルは無かったので、実際には後でやってみよう。
次のトピックは「Libraries add new features」である。 Processingのメニューでも、
のように、何やら相当に「濃い」ライブラリが簡単にインポートできそうである。 これは、Javaコミュニティを考えてみれば当然なので、Javaの美味しいところを全て活用できるのだろう。
この「Processing Overview」では、あと2つのトピックが書かれているだけである。 その「Sketching and scripting」と「Don't start by trying to build a cathedral」とは、ある意味で同じことを言っている。 Processingは簡単なスケッチングのツールである、いきなり壮大なシステムを構築しようとするな、という事である。
とかく真面目な学生ほど、「まずは教科書を読破・理解してから取りかかる」という傾向がある。 しかし、教科書をたらたら読破しようとしている間にも、機能と仕様がどんどん変化し進化するのがIT時代である。 とりあえずサンプルを走らせてみて、ちょっと改造してみて、何か困ったらReferenceを見たりFAQを調べたり誰かに聞く、というのでOKなのだ。 セキュリティを考慮したJavaの上にあるので、間違ってファイルを壊したりすることは無い。エラーが出るだけである。 軽い気持ちでProcessingに取り組んでいこう。
2011年2月27日(日)
前夜の「めちゃイケ」の「ナイナイ20周年」にちょっと感動して快眠したので、この日も快調にスタート。 論文の査読とかのお仕事もあるが、やっぱり新しい事をする、というのは楽しい。 昨日も、午後は SuperCollider をやったので、今日も午前はProcessingから始めた。 チュートリアル のページの「Getting Started」と「Processing Overview」までやったので、その次の Coordinate System and Shapes からである。このチュートリアルはMITの学生に対する教科書でもあるので、かなり基本的なところから解説されている。 最初は一般論としての「座標空間」のお話である。 たとえば「line(1,0,4,5);」という関数は、一般的な座標平面(2次元の座標空間)であれば以下のようになる。
Processingの関数(コマンド)についても、たとえば「line(1,0,4,5);」の意味は以下である、と解説されている。 これはJavaでもFlashのActionScriptでも同じ文化なので簡単だろう。
数学での座標平面の定義と違って、コンピュータでは歴史的に以下のような座標系を定義するので注意が必要である。 左上が(0,0)の原点で、右方向にx、下方向にy、である。y軸の向きが数学とは逆になっている「第1象限」という事である。 これは、コンピュータのモニタ(スクリーン)のサイズが色々であるので仕方ない。 HTMLでもユーザの画面の大きさは不明なので、あらゆる起点を左上にとっている。
「座標空間」に続いて、「Simple Shapes」のお話である。 まずは以下の4種類の単純な図形を要素として解説している。他の処理系では、「point」を「長さ1のline」で表現できるとして定義しないものもあるが、やはり別途にあった方がシンプルだろう。「Rectangle」は正方形を含むあらゆる矩形を、「Ellipse」は円を含むあらゆる楕円を定義できる。
「point()」は、xとyの2つの引数を持つ「点」である。以下の図はとても判りやすい。
「line()」は、端点(x1,y1)と端点(x2,y2)の、計4つの引数を持つ「直線」である。以下の図はとても判りやすい。
「rect()」は、ちょっとややこしくなってくる。 defaultでは以下のように、Processingの「rect()」は、矩形の左上の点(x,y)と、矩形の幅(x方向のwidth)、矩形の高さ(y方向のheight)の、計4つの引数を持つ。
ところが「rect()」には、もう一つのモードがある。 以下のように、「rectMode(CENTER)」という命令を入れた後では、 Processingの「rect()」は、矩形の中心の点(x,y)と、矩形の幅(x方向のwidth)、矩形の高さ(y方向のheight)の、計4つの引数を持つ。 アニメーションで矩形を動かす場合には、この中心座標を動かす方が判りやすいので便利なモードだ。
さらに「rect()」には、もう一つのモードもある。 以下のように、「rectMode(CORNERS)」という命令を入れた後では、 Processingの「rect()」は、左上の点(x1,y1)と右下の点(x2,y2)の、計4つの引数を持つ。 アニメーションで矩形の大きさを端点として変化させたい場合などには、便利なこともあるモードだろう。
ここでフト思ったのは、「rectMode()」の指定は、MIDIのプログラムチェンジのように、一度指定したら、その後に新たに指定されるまで全ての「rect()」に有効なのか、それともdefauleでないモードで「rect()」を実行するためには、いちいちその前に「rectMode()」の指定が必要なのか、という疑問である。 そこで リファレンス のページの rectMode() を見てみた。 まず最初に判ったのは、defaultに戻すために必要な指定は「rectMode(CORNER)」である、という事である。 「CORNER」と「CORNERS」とは紛らわしいが仕方ない。
また、Processingは大文字と小文字を厳格に区別するので、「CENTER」と「CORNER」と「CORNERS」などの予約語は全て大文字でなければならない。 さらにもう1つのモードもあり、「rectMode(RADIUS)」という命令を入れた後では、 Processingの「rect()」は、矩形の中心の点(x,y)と、矩形の幅(x方向のwidth)の半分、矩形の高さ(y方向のheight)の半分、計4つの引数を持つ。 これもアニメーションで矩形を動かす場合に便利なモードだ。
さて、そこで上の疑問を確認するために、 rectMode() にあったサンプルをちょっと改造して、いろいろなモードを切り替えつつ描画する、という以下のプログラムを作ってみた。
rect(30, 30, 50, 50); rectMode(CENTER); rect(35, 35, 45, 45); rect(25, 25, 45, 45); rectMode(CORNER); fill(102); rect(40, 40, 50, 50); この結果は以下のようになった。 defaultでまずいちばん下の矩形が描画され、次にモードを「CENTER」とした矩形が描画され、その次の矩形にはモード「CENTER」が有効となったまま描画され、最後にモードを「CORNER」に戻した矩形が塗り潰しで描画されている。 これにより、予想通りに、「rectMode()」のモード指定は、MIDIのプログラムチェンジのように、一度指定したら、その後に新たに指定されるまで全ての「rect()」に有効である、と確認できた。
基本図形の最後の「ellipse()」は、基本的には「rect()」と同じようなパラメータを持つが、以下のように、 defaultでのモードが違っているところに注意が必要である。
リファレンス のページの ellipseMode() を見ると、モードは「rect()」と同じ「CENTER, RADIUS, CORNER, CORNERS」の4種類であるが、defaultは「CENTER」である。 まぁ、楕円や円では中心座標と長軸/短軸(あるいは半径)という指定がいちばん普通なので当然だろう。
最後のサンプルとして、
size(200,200); rectMode(CENTER); rect(100,100,20,100); ellipse(100,70,60,60); ellipse(81,70,16,32); ellipse(119,70,16,32); line(90,150,80,160); line(110,150,120,160); の描画結果として以下が示されていた。 あまり可愛いとも言えないが、このように描くことが可能なわけだ。
これで チュートリアル のページの「基本図形」が終わったので、その次は Color である。 まず最初は「Grayscale Color」である。 モノクロのグレースケールは、Processingでは8ビット、すなわち0から255までである。
既に「線の色」として「stroke()」と、「色の塗りつぶし」として「Fill()」が登場していたので、以下のサンプルは判りやすい。 なお、この指定は背景色「background()」でも同様である。
background(255); // Setting the background to white stroke(0); // Setting the outline (stroke) to black fill(150); // Setting the interior of a shape (fill) to grey rect(50,50,75,100); // Drawing the rectangle 図形のモードにあったように、この「線の色」「塗りつぶしの色」の指定は、いったん実行されるとずっと続く。 これをeliminate、すなわち「指定範囲はここまで」とする関数が「noStroke()」「noFill()」である。 なお、「noStroke()」と「noFill()」の両方を指定すると、何も描かれなくなる。 また、明示的に「stroke()」を指定しない場合のdefaultカラーは「0」すなわち黒、明示的に「fill()」を指定しない場合のdefaultカラーは「255」すなわち白である、と実験して確認した。そして次がいよいよ、「RGB Color」である。
図形の描画において、「stroke()」や「fill()」の色パラメータ(引数)が1個であればグレースケールになり、コンマで切って3つの色パラメータ(引数)が並んだ場合には、それぞれ8ビットのRGBだ、ということである。 Processingはつまり、24ビットカラー、すなわち「1600万色」ということだ。 ここでは詳しく書かれていないが、Processingには「COLOR SELECTOR」というツールがあり、以下のように簡単にRGBの値を取出せるようである。
ここまではMax/MSP/jitterでも何でも同じだったが、Processingではアルファチャンネル、つまり透明度も指定できる。これは強力である。 「Color Transparency」の定義は、図形の描画において、「stroke()」や「fill()」にコンマで切って3つの色パラメータ(引数)が並んだ場合には、それぞれ8ビットのRGBとなるが、さらにoptionalとして4つ目のパラメータがあると(値は0から255)、これが「不透明度(opacity)」を表す。 完全に透明なのは「opacity=0%」なのでデータ「0」、完全に不透明なのは「opacity=100%」なのでデータ「255」である。
以下の例では、背景色を黒にして、まず左半分を不透明で青に塗りつぶし、次に上から、不透明度100%、75%、55%、25%、として横長の赤の矩形を描いている。
size(200,200); background(0); noStroke(); // No fourth argument means 100% opacity. fill(0,0,255); rect(0,0,100,200); // 255 means 100% opacity. fill(255,0,0,255); rect(0,0,200,40); // 75% opacity. fill(255,0,0,191); rect(0,50,200,40); // 55% opacity. fill(255,0,0,127); rect(0,100,200,40); // 25% opacity. fill(255,0,0,63); rect(0,150,200,40); ここまではHTMLでもよくあった「24ビットカラー」、あるいはアルファチャンネルを加えた「32ビットカラー」のお話だったが、Processingでは、それぞれの色パラメータを「0から255」というコンピュータ寄りの範囲だけでなく、人間に自然なレンジでも扱える。 これが「Custom Color Ranges」ということで、「colorMode()」関数で指定する。 「colorMode(RGB,100);」とするとグレースケールになって、値は「0から100」とできる。
また「colorMode(RGB,100,500,10,255);」とすると、パラメータが4つあるのでフルカラー、さらに赤は「0から100」、緑は「0から500」、青は「0から10」、不透明度は「0から255」と、それぞれ別々のレンジでも指定できる。 colorModeリファレンス を見てみると、色指定のパラメータは「int or float」ということなので、「-1.0から1.0までの実数値」というのも可能らしい。 ちょっとしたパラメータ変換でも実現できるが、まぁProcessingはそれだけ親切なのだ、という事だろう。
Processingでは、ここまでのRGBカラーだけでなく、「HSB (hue, saturation, and brightness) mode」もあるらしい。 色相(Hue)、彩度(Saturation・Chroma)、明度(Brightness・Lightness・Value)ということだが、 Wikipediaでは HSV となっている。
以下のサンプルは、RGBモードとHSBモードでグラデーションを表示させたものである。
noStroke(); colorMode(RGB, 100); for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { stroke(i, j, 0); point(i, j); } } noStroke(); colorMode(HSB, 100); for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { stroke(i, j, 100); point(i, j); } } 2011年2月28日(月)
今日は午前中、ゼミの鈴木さんが手伝ってくれて こんな作業 をしていたが、午後には外出の予定もあって、少ししか進めないようだ。 いつものように チュートリアル からスタートなので、「色」の次の Object Oriented Programming からである。これは骨太なテーマだ。 もともと、JavaでもFlashのActionScriptでもXcodeでも、全てがObject Orientedなのだが、 初学者にはここが大きな壁なので、たぶんテキストを書く方でも力が入っているだろう。 並行している SuperCollider でも、ちょうど昨日、これをやったような気がする。
最初の例では、Processingのプログラミングでなく、日常の自分の行動を考えてみよう、とスタートした。
これがもう、既にObject Oriented Programmingである、というわけだ。 何でもObjectであり、Objectのプロバティが「変数」であり、Objectが出来ることが「関数」である。 Object Oriented Programmingでは、全てのObjectのデータと機能を記述する。 再び人間の例で言えば、以下のようになる。
- Wake up.
- Drink coffee (or tea).
- Eat breakfast: cereal, blueberries, and soy milk.
- Ride the subway.
- Human data
- Height.
- Weight.
- Gender.
- Eye color.
- Hair color.
- Human functions
- Sleep.
- Wake up.
- Eat.
- Ride some form of transportation.
Processingでは、「setup()」は、プログラムの最初に一度だけ呼ばれる初期化、「draw()」はその後に無限に繰り返し呼び出される基本ループ、という特殊なメソッドがある。 これをふまえて、まずはスケッチとして、「Car」という矩形がスクリーン上を動くグラフィクスを仮想的に考えると、およそ以下のようになる。
- 広域変数としてCarを定義(Data):
- Car color.
- Car x location.
- Car y location.
- Car x speed.
- Setup:
- Initialize car color.
- Initialize car location to starting point.
- Initialize car speed.
- Draw:
- Fill background.
- Display car at location with color.
- Increment car's location by speed.
これをそのまま単純にProcessingのプログラムとしてみると、以下のようになる。
実行結果は以下のようになった。color c = color(0); float x = 0; float y = 100; float speed = 1; void setup() { size(200,200); } void draw() { background(255); move(); display(); } void move() { x = x + speed; if (x > width) { x = 0; } } void display() { fill(c); rect(x,y,30,10); } アプレットとして書き出してみると、このようになる。
上の例のように、Object Oriented Programmingといっても、全ての変数を広域変数として、Object Orientedでない昔のスタイルで記述することも可能である。 しかし、せっかくObject OrientedなProcessingなので、これを以下のように記述したいのである。 以下はプログラムとして完成していないが、やりたい事が良くわかる。
両者に見たところの違いは無さそうに思えるのは、まだ対象のクルマが1台だからである。 これが2台、3台、・・・100台、と増えた場合には、単純に「昔のスタイル」で記述するためには、変数が3台分とか100台分とか増えるとともに、3台分あるいは100台分の処理を全て、記述しなければならなくなる。 これは以下の図の左側のスタイルである。 そして、Object Oriented Programmingでは、以下の図の右側のスタイルを推奨している。Car myCar; void setup() { myCar = new Car(); } void draw() { background(255); myCar.drive(); myCar.display(); } 「Car」というクラスの中に、プロバティとしてData、コンストラクタとして変数の処理、機能として「表示」「移動」などの関数(メソッド)を全て記述してしまうのである。 Processingのプログラムとしての構造は、予約されている「setup()」と「draw()」があるので、概略の構造としては以下のようになる。
そして、実際に人間から見てvoid setup() { } void draw() { } class Car { } という処理をObject Oriented ProgrammingらしくProcessingで記述したのが以下である。
- Make a new car.
- Make a new red car, at location (0,10) with a speed of 1.
- Make a new blue car, at location (0,100) with a speed of 2.
- 両方のクルマよ、動きなさい !
実行結果は以下のようになった。Car myCar1; Car myCar2; void setup() { size(200,200); myCar1 = new Car(color(255,0,0),0,100,2); myCar2 = new Car(color(0,0,255),0,10,1); } void draw() { background(255); myCar1.drive(); myCar1.display(); myCar2.drive(); myCar2.display(); } class Car { color c; float xpos; float ypos; float xspeed; Car(color tempC, float tempXpos, float tempYpos, float tempXspeed) { c = tempC; xpos = tempXpos; ypos = tempYpos; xspeed = tempXspeed; } void display() { stroke(0); fill(c); rectMode(CENTER); rect(xpos,ypos,20,10); } void drive() { xpos = xpos + xspeed; if (xpos > width) { xpos = 0; } } } アプレットとして書き出してみると、このようになった。 この後にもあとちょっと「Objects are data types too!」という説明があったが、とりあえず、今日はここまで。
2011年3月1日(火)
今日は先にSuperColliderを進めたので、Processingは、ちょっとだけ。 チュートリアル の「Object Oriented Programming」の次なので、 Two-Dimensional Arrays である。 これはいくらなんでも、簡単だろう。1次元の配列は
と定義するので、2次元の配列はint[] myArray = {0,1,2,3}; となり、これは以下のようにすると理解しやすい。int[][] myArray = { {0,1,2,3}, {3,2,1,0}, {3,5,6,1}, {3,8,3,4} }; これは、int[][] myArray = { {0, 1, 2, 3}, {3, 2, 1, 0}, {3, 5, 6, 1}, {3, 8, 3, 4} };
という図を
のように定義する、というイメージである。 1次元の配列をfor文でint[][] myArray = { {236, 189, 189, 0}, {236, 80, 189, 189}, {236, 0, 189, 80}, {236, 189, 189, 80} }; と埋めるというアナロジーから、2次元配列だと以下のようになる。int[] myArray = new int[10]; for (int i = 0; i < myArray.length; i++) { myArray[i] = 0; } 画素にドットを打つ、というのは「stroke()」だったので、int cols = 10; int rows = 10; int[][] myArray = new int[cols][rows]; // Two nested loops allow us to visit every spot in a 2D array. // For every column I, visit every row J. for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { myArray[i][j] = 0; } }
という画像を、ランダムな濃さのグレースケールでstrokeする、というプログラムは以下となる。
まだ、sine関数とかも何もやっていないが、次のサンプル// Example: 2D Array size(200,200); int cols = width; int rows = height; // Declare 2D array int[][] myArray = new int[cols][rows]; // Initialize 2D array values for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { myArray[i][j] = int(random(255)); } } // Draw points for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { stroke(myArray[i][j]); point(i,j); } }
を描画するProcessingプログラムは以下である。 サイン関数をとても粗く、濃さの変化関数として使用している。 ちょっと長いものの、とても判りやすいサンプルだ。(^_^)
// 2D Array of objects Cell[][] grid; // Number of columns and rows in the grid int cols = 10; int rows = 10; void setup() { size(200,200); grid = new Cell[cols][rows]; for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { // Initialize each object grid[i][j] = new Cell(i*20,j*20,20,20,i+j); } } } void draw() { background(0); // The counter variables i and j are also the column and row numbers and // are used as arguments to the constructor for each object in the grid. for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { // Oscillate and display each object grid[i][j].oscillate(); grid[i][j].display(); } } } // A Cell object class Cell { // A cell object knows about its location in the grid as well as its size with the variables x,y,w,h. float x,y; // x,y location float w,h; // width and height float angle; // angle for oscillating brightness // Cell Constructor Cell(float tempX, float tempY, float tempW, float tempH, float tempAngle) { x = tempX; y = tempY; w = tempW; h = tempH; angle = tempAngle; } // Oscillation means increase angle void oscillate() { angle += 0.02; } void display() { stroke(255); // Color calculated using sine wave fill(127+127*sin(angle)); rect(x,y,w,h); } } 2011年3月2日(水)
この日は午前中だけなので、引き続きProcessingをちょっとだけ進めることにした。 チュートリアル の「Two-Dimensional Arrays」の次なので、 Images and Pixels である。 グラフィクスをメインとするProcessingとしては、画像と画素というのは重要な要素である。 ちょうど、昨日の SuperCollider では、サウンドファイルのアクセスをしたところだが、こちらSuperColliderでもイメージファイルのアクセスからである。画像は「PImage」オブジェクトで扱い、読み込むのは「loadImage()」メソッドである。 以下のサンプルにより、画像ファイルが表示されると思ったが、エラーが出た。(^_^;)
// Declaring a variable of type PImage PImage img; void setup() { size(320,240); // Make a new instance of a PImage by loading an image file img = loadImage("sample.jpg"); } void draw() { background(0); // Draw the image to the screen at coordinate (0,0) image(img,0,0); } これは、画像データを読み込むディレクトリを指定していなかったからである。 画像ファイルを指定するのは2つの方法がある。 その1つは、以下のように「Sketch」メニューから「Show Sketch Folder」を選んで、出て来たテンポラリのスケッチフォルダ(名称未設定の場合)に画像ファイルを入れればよい。
画像ファイルを指定するもう1つの方法は、以下のように「Sketch」メニューから「Add File. . . 」を選んで、画像ファイルを選択する。 この場合、オリジナル画像ファイルは移動することなく、上述のテンポラリのスケッチフォルダのData内に画像ファイルがコピーされる。
これにより、以下のように、実行させるとエラーが出ずに、無事に画像が表示された。 ただしオリジナルは これなので、ウインドウサイズに対応することなく、左上に詰めた状態で切り取られた。 Processingの扱える画像形式は、GIF, JPG, TGA, PNGの4種類である。
上のサンプルでは、これまでであれば新しいインスタンスを生成する時に
などとやっていたのに、「loadImage()」ではSpaceship ss = new Spaceship(); Flower flr = new Flower(25); とやっている、つまり「new」が無いのがこれまでと違っている、と思われるかもしれない。 しかしこれは、「loadImage()」はまず、newした後にloadしているので、実は同じなのである。 以下のように、ロードせずに新しい画像を生成する「createImage()」を見るとよく判る。PImage img = loadImage("file.jpg"); 「image()」関数は、表示する画像の左上の座標を指定できるだけでなく、右下の座標まで指定すると、ちゃんとリサイズしてくれる。 さっきの切り取られた例では、第3・第4の引数を省略していたので「リサイズ無し」とされたわけだ。 これは、以下の例で確認できた。// Create a blank image, 200x200 pixels with RGB color PImage img = createImage(200,200,RGB); さて、チュートリアルはここから「Your very first image processing filter」となった。 photoshopでもFlashでもjitterでもそうだが、画像に対する各種のエフェクトには、もの凄い歴史があるので、たぶんProcessingはほとんどその全てを実装しているものと思われる。「いいとこ取り」できる後発技術のメリットである。 まず素材として、去年のゼミ旅行の写真から これと これを 「Add File. . . 」により追加した。
そして、背景にこれを配置し、 その上にこれを「image()」で描画する際に、 濃さを指定する「tint()」を使った。 「tint()」は、引数が1つだった場合には、描画する画像の濃さ(0-255)となり、いずれであっても背景画像は上描きされて見えない。 上の画像に透明色があれば、そこだけ背景色が透けて見える、というのはHTMLと同じである。 以下の例は、「tint(255)」の場合である。
PImage satou, ozen; void setup() { size(480,640); satou = loadImage("satou.jpg"); ozen = loadImage("ozen.jpg"); background(ozen); } void draw() { tint(255); image(satou,0,0); } 以下の例は、「tint(100)」の場合である。
「tint()」は、引数が2つになると、第1パラメータは上記と同じ描画する画像の濃さ(0-255)で、第2パラメータは「不透明度(0-255で、値が小さいほど透明)」となる。 実験してみると、今回のサンプルでは画像も背景画像も明るいので、相当に小さい値(高い透明度)にしないと、背景が透けて見えてこなかった。 以下の例は、「tint(255,8)」の場合である。
以下の例は、「tint(255,3)」の場合である。
「tint()」は、引数が3つになると、第1パラメータは画像の「赤」の濃さ(0-255)、第2パラメータは像の「緑」の濃さ(0-255)、第3パラメータは像の「青」の濃さ(0-255)、となる。 以下の例は、「tint(255,180,0)」の場合である。
「tint()」は、引数が4つになると、第1パラメータは画像の「赤」の濃さ(0-255)、第2パラメータは像の「緑」の濃さ(0-255)、第3パラメータは像の「青」の濃さ、第4パラメータは「不透明度(0-255で、値が小さいほど透明)」となる。 以下の例は、「tint(200,255,0,3)」の場合である。
次のトピックは「Pixels, pixels, and more pixels」である。 つまり、ラインでも矩形でも楕円でも何でも、要は画素(pixel)だよ、という事だろう。 2次元のように縦横となっている場合でも、コンピュータのメモリ内では、以下のように1次元的に画素データを扱っている。
以下の例では、スクリーン内の全画素について、ランダムな濃さで全ての画素を描画している。 ホワイトノイズの画像である。 「loadPixels()」は、画素単位のアクセスに先立って呼び出す関数である。 そして画素の処理が終わったところで、 「updatePixels()」を呼び出すことで、その画素が一気に描画される。 Processingが、このように「setup()」と「draw()」ナシでも実行できる、とここでようやく気付いた(^_^;)。
size(200, 200); // Before we deal with pixels loadPixels(); // Loop through every pixel for (int i = 0; i < pixels.length; i++) { // Pick a random number, 0 to 255 float rand = random(255); // Create a grayscale color based on random number color c = color(rand); // Set pixel at that location to random color pixels[i] = c; } // When we are finished dealing with pixels updatePixels(); 上の例では、スクリーンの大きさを最初に「size(200, 200);」で定義して、画素の演算では2次元でなく、メモリの並びの1次元として処理した。 これは「pixels.length」という定数が、スクリーンサイズの指定により、ここでは200*200=40000、と決まるからである。 しかし実際には、2次元の画像で、タテとヨコで画素を指定して処理したい。 そこで、以下のように「LOCATION = X + Y*WIDTH」という関係を使うことになる。 これは、既にやった「2次元配列」、そのものである。
以下の例では、2次元配列として画素を処理しているので、画素の位置を示す「int loc = x + y * width;」を使っている。 ループの中で毎回、いちいちこの座標計算をする、というのはちょっと非効率な気がするが、もしかるとコンパイル時に参照テーブルを作成して高速化しているのかもしれない。
size(200, 200); loadPixels(); // Loop through every pixel column for (int x = 0; x < width; x++) { // Loop through every pixel row for (int y = 0; y < height; y++) { // Use the formula to find the 1D location int loc = x + y * width; if (x % 2 == 0) { // If we are an even column pixels[loc] = color(255); } else { // If we are an odd column pixels[loc] = color(0); } } } updatePixels(); ここまでがごく簡単なイントロ中のイントロで、いよいよこの次に「Intro To Image Processing」として、画像処理のいろいろに入っていく。 しかし今日は午後には外出の予定があり、ここまでである。
2011年3月4日(金)
昨日は午後に SuperCollider を進めたので、今日は沖縄行きの準備のかたわら、教授会の他の午後の空き時間にProcessingを進めていく事にした。 チュートリアル の Images and Pixels の続き、「Intro To Image Processing」からである。 ここまでの例では、画像データを座標の計算で処理してきたが、ここからは 「PImage object」としてまとめて扱う。 これは、という手順で処理する、一種の疑似コードである。 ページには以下のサンプルが紹介されていた。実際には、対応する「とじカッコ」が欠けていたり、「pixels」が「pixedls」とミスタイプされていて、そのまま実行してもエラーが出る、という教育的なサンプルである(^_^;)。 正しく修正して実行させると以下のように、読み込み指定した画像がそのまま表示されたが、これは「静止画データをそのまま表示」しているのではなくて、画像ファイルから1画素ごとに色データを抽出して、それをスクリーンの対応する画素の色データとして転送して、最後に改訂されたスクリーンを描画しているのである。(あらかじめこれを「Add File..」で指定しておいた)
- Load the image file into a PImage object
- For each pixel in the PImage, retrieve the pixel's color and set the display pixel to that color
PImage img; void setup() { size(200, 200); img = loadImage("sunflower.jpg"); } void draw() { loadPixels(); // Since we are going to access the image's pixels too img.loadPixels(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int loc = x + y*width; // The functions red(), green(), and blue() pull out the 3 color components from a pixel. float r = red(img.pixels[loc]); float g = green(img.pixels[loc]); float b = blue(img.pixedls[loc]); // Image Processing would go here // If we were to change the RGB values, we would do it here, before setting the pixel in the display window. // Set the display pixel to the image pixel pixels[loc] = color(r,g,b); } } updatePixels(); } これは「静止画データをそのまま表示」するプログラムよりちょっと複雑だが、画像の画素ごとにRGBの色データをアクセスできることから、いろいろな画像処理のための道具としてとても重要である。 また、上の例では、読み込む画像と表示する画像とが同じサイズだったが、これは同じである必要はない。 一般的には、以下のような定義をすれば、それぞれ独立に扱える。
次のトピックのタイトルは「Our second image filter, making our own "tint"」である。 上の例は、たまたま「読み込み画像と同じような画像を表示」(ソフトスルー)というフィルタだったが、いよいよ、オリジナルの画像フィルタの制作である。 さきの例では「画像全体」に対して、色や透明度の操作を行ったが、これはフィルタとしてはあまりに簡単すぎる。 画素ごとの処理が出来れば、対応する画素ごとだけでなく。、例えば「近傍の画素からの処理アルゴリズム」など、可能性は飛躍的に拡大する。 以下の例では、マウスのX座標(左右)に対応して画像全体の明るさ(brightness)が変化する、というインタラクティブな画像処理を実現している。 アプレットとして書き出してみると、このようになった。 これはもはや、一種のインスタレーション作品である。(^_^)int imageLoc = x + y*img.width; int displayLoc = x + y*width; PImage img; void setup() { size(480, 640); img = loadImage("satou.jpg"); } void draw() { loadPixels(); // Since we are going to access the image's pixels too img.loadPixels(); for (int x = 0; x < img.width; x++) { for (int y = 0; y < img.height; y++ ) { // Calculate the 1D pixel location int loc = x + y*img.width; // Get the R,G,B values from image float r = red (img.pixels[loc]); float g = green (img.pixels[loc]); float b = blue (img.pixels[loc]); // Change brightness according to the mouse here float adjustBrightness = ((float) mouseX / width) * 8.0; r *= adjustBrightness; g *= adjustBrightness; b *= adjustBrightness; // Constrain RGB to between 0-255 r = constrain(r,0,255); g = constrain(g,0,255); b = constrain(b,0,255); // Make a new color and set pixel in the window color c = color(r,g,b); pixels[loc] = c; } } updatePixels(); } 以下の例では、マウスの座標に対応して画像全体の明るさ(brightness)が変化する、というインタラクティブな画像処理を実現している。 アプレットとして書き出してみると、このようになった。 こちらも立派に一種のインスタレーション作品である(^_^)。 かつて、ゼミの乗松クンが制作した作品に、「夜の風景の中で、マウスカーソル付近だけが昼の風景に変わる」動画、というものがあり、Mac/MSP/jitterで制作したがとても重かったのを思い出した。Processingでやったらどうなるか、そのうち試してみよう。
PImage img; void setup() { size(480, 640); img = loadImage("satou.jpg"); } void draw() { loadPixels(); // Since we are going to access the image's pixels too img.loadPixels(); for (int x = 0; x < img.width; x++) { for (int y = 0; y < img.height; y++ ) { // Calculate the 1D pixel location int loc = x + y*img.width; // Get the R,G,B values from image float r = red (img.pixels[loc]); float g = green (img.pixels[loc]); float b = blue (img.pixels[loc]); // Calculate an amount to change brightness // based on proximity to the mouse float distance = dist(x,y,mouseX,mouseY); float adjustBrightness = (100-distance)/100; r *= adjustBrightness; g *= adjustBrightness; b *= adjustBrightness; // Constrain RGB to between 0-255 r = constrain(r,0,255); g = constrain(g,0,255); b = constrain(b,0,255); // Make a new color and set pixel in the window color c = color(r,g,b); pixels[loc] = c; } } updatePixels(); } ここまでの例では、画像データを画素ごとに読み込んで処理した結果を、Processingのウインドウの画素に直接、表示させていた。 しかし画像処理でより有効なのは、画像データを画素ごとに読み込んで処理した結果を画素とする新しい画像データを書き出す、という事である。 画像全体を「スレショルド」のフィルタによって、brightnessが半分より上か下かで2値化する、という処理は、「filter」を使えば、以下である。
同じことを、ここで話題としているように、画素単位で行うと以下のようになる。 ちょっと長くなっているが、ソースとディスティネーションが定義され、完全に画像データ単位での「画像処理コンバータ」がこの枠組みで実現できるので、こちらの方が有効である。// Draw the image image(img,0,0); // Filter the window with a threshold effect // 0.5 means threshold is 50% brightness filter(THRESHOLD,0.5); PImage source; // Source image PImage destination; // Destination image void setup() { size(480, 640); source = loadImage("satou.jpg"); // The destination image is created as a blank image the same size as the source. destination = createImage(source.width, source.height, RGB); } void draw() { float threshold = 127; // We are going to look at both image's pixels source.loadPixels(); destination.loadPixels(); for (int x = 0; x < source.width; x++) { for (int y = 0; y < source.height; y++ ) { int loc = x + y*source.width; // Test the brightness against the threshold if (brightness(source.pixels[loc]) > threshold) { destination.pixels[loc] = color(255); // White } else { destination.pixels[loc] = color(0); // Black } } } // We changed the pixels in destination destination.updatePixels(); // Display the destination image(destination,0,0); } さていよいよ、次のトピックは「Level II: Pixel Group Processing」である。 対応する画素ごとに何かする、というのでは、いいところ色や明るさや透明度を変えるぐらいであるが、 ここでようやく、本当のグラフィック処理が始まることになる。 座標が(x,y)であるピクセルに対して、
であるとすれば、この画素の左隣りの画素はint loc = x + y*img.width; color pix = img.pixels[loc]; である。この2つの画素の明るさの差をとることで、明るさが激しく変化する「エッジ抽出」が出来ることになる。以下の演算である。int leftLoc = (x-1) + y*img.width; color leftPix = img.pixels[leftLoc]; プログラム全体としては、以下のようになる。画面の左端の画素の左は無いので、ループが0でなく1から始まっていることに注意。float diff = abs(brightness(pix) - brightness(leftPix)); pixels[loc] = color(diff); PImage img; void setup() { size(480, 640); img = loadImage("satou.jpg"); } void draw() { loadPixels(); // Since we are going to access the image's pixels too img.loadPixels(); // Since we are looking at left neighbors // We skip the first column for (int x = 1; x < width; x++) { for (int y = 0; y < height; y++ ) { // Pixel location and color int loc = x + y*img.width; color pix = img.pixels[loc]; // Pixel to the left location and color int leftLoc = (x-1) + y*img.width; color leftPix = img.pixels[leftLoc]; // New color is difference between pixel and left neighbor float diff = abs(brightness(pix) - brightness(leftPix)); pixels[loc] = color(diff); } } updatePixels(); } 上の例では、もっとも単純に「x方向の左側の点との差分」を計算したが、実際の2次元画像であれば、その画素の周囲の全ての画素と比較する必要がある。 この「neighbors」の概念は以下である。
画像処理アルゴリズムの多くは、このように、それぞれの画素について、その近傍点(neighbors)の画素のデータを用いた演算処理(空間的畳み込み : spatial convolution)を行う。画面の端点を除けば、これは上下左右と斜めの4点、つまりその画素を内部に持つ正方形で、その画素自身を入れれば3*3の9点からなる配列を扱うことになる。 例えば、3*3の配列で、周囲の画素のデータの重み付け平均を取るとか、さらに大きく周囲5*5の画素で、などの処理を行う。 例えば
という演算だと、周囲を落として対象の画素をシャープ化する処理となり、逆にSharpen: -1 -1 -1 -1 9 -1 -1 -1 -1 という演算だと、滲んでぼやけた画像処理となる。 以下の例では、3*3配列を使って、マウス指定された画像の一部(200*200の領域の内部)をシャープ化させている。 アプレットとして書き出してみると、このようになった。 これも一種のインスタレーション作品と言えるだろう。Blur: 1/9 1/9 1/9 1/9 1/9 1/9 1/9 1/9 1/9 PImage img; int w = 200; float[][] matrix = { { -1, -1, -1 }, { -1, 9, -1 }, { -1, -1, -1 } }; void setup() { size(480, 640); frameRate(30); img = loadImage("satou.jpg"); } void draw() { image(img,0,0); int xstart = constrain(mouseX-w/2,0,img.width); int ystart = constrain(mouseY-w/2,0,img.height); int xend = constrain(mouseX+w/2,0,img.width); int yend = constrain(mouseY+w/2,0,img.height); int matrixsize = 3; loadPixels(); for (int x = xstart; x < xend; x++) { for (int y = ystart; y < yend; y++ ) { color c = convolution(x,y,matrix,matrixsize,img); int loc = x + y*img.width; pixels[loc] = c; } } updatePixels(); stroke(0); noFill(); rect(xstart,ystart,w,w); } color convolution(int x, int y, float[][] matrix, int matrixsize, PImage img) { float rtotal = 0.0; float gtotal = 0.0; float btotal = 0.0; int offset = matrixsize / 2; for (int i = 0; i < matrixsize; i++){ for (int j= 0; j < matrixsize; j++){ int xloc = x+i-offset; int yloc = y+j-offset; int loc = xloc + img.width*yloc; loc = constrain(loc,0,img.pixels.length-1); rtotal += (red(img.pixels[loc]) * matrix[i][j]); gtotal += (green(img.pixels[loc]) * matrix[i][j]); btotal += (blue(img.pixels[loc]) * matrix[i][j]); } } rtotal = constrain(rtotal,0,255); gtotal = constrain(gtotal,0,255); btotal = constrain(btotal,0,255); return color(rtotal,gtotal,btotal); } だんだん面白くなってきたが、あと1つ、「Visualizing the Image」を残して、今日はここでおしまいである。
2011年3月5日(土)
昨日は Processing日記 を進めたが、今日は午前に沖縄ミーティングがあり、昼休みに SuperCollider をちょっとだけ、区切りまで進めた。 午後にはゼミのちひろちゃんがシステム返却のついでにiMacセットアップを手伝ってくれて、 14時からの3時間でだいぶ作業が進んだ。 こちらProcessingもちょっとだけ区切りまで残していたので、帰宅前にそこだけ進めることにした。 明日3/6は停電工事のSUACネット停止のためお休み、そして3/7-10は沖縄、3/11は終日学科会議、3/12は一般後期入試、と予定満杯で、次は卒業式の週からのスタートとなりそうだ。 残っているのは、 チュートリアル の Images and Pixels の最後の「Visualizing the Image」である。ここには2つの例があるが、その第一は「"pointillist-like" effect」というものである。 これは以下のように、画面内のランダムな場所に塗りつぶしの小さな円を描くが、その色として、元の画像のその付近の色情報を参照する、というものである。 多くの塗りつぶしが終わると、全体として滲んだ画像に変換されている、というもののようである。 無限ループが続いていて、時間とともにより正確な画像になっていく。 アプレットとして書き出してみると、このようになった。
PImage img; int pointillize = 16; void setup() { size(480, 640); img = loadImage("satou.jpg"); background(0); smooth(); } void draw() { // Pick a random point int x = int(random(img.width)); int y = int(random(img.height)); int loc = x + y*img.width; // Look up the RGB color in the source image loadPixels(); float r = red(img.pixels[loc]); float g = green(img.pixels[loc]); float b = blue(img.pixels[loc]); noStroke(); // Draw an ellipse at that location with that color fill(r,g,b,100); ellipse(x,y,pointillize,pointillize); } そして最後の例は、まだ登場していない「3次元変換」を使った例で、2次元の画像を、マウスのx座標に対応して、3次元空間にマッピングして拡大していく、というものである。 なかなかドキッとするインスタレーション作品とも言えるものである。 Processingプログラムとしては以下のようなものである。 アプレットとして書き出してみると、このようになった。
PImage img; // The source image int cellsize = 2; // Dimensions of each cell in the grid int cols, rows; // Number of columns and rows in our system void setup() { size(480, 640, P3D); img = loadImage("satou.jpg"); cols = width/cellsize; // Calculate # of columns rows = height/cellsize; // Calculate # of rows } void draw() { background(0); loadPixels(); // Begin loop for columns for ( int i = 0; i < cols;i++) { // Begin loop for rows for ( int j = 0; j < rows;j++) { int x = i*cellsize + cellsize/2; // x position int y = j*cellsize + cellsize/2; // y position int loc = x + y*width; // Pixel array location color c = img.pixels[loc]; // Grab the color // Calculate a z position as a function of mouseX and pixel brightness float z = (mouseX/(float)width) * brightness(img.pixels[loc]) - 100.0; // Translate to the location, set fill and stroke, and draw the rect pushMatrix(); translate(x,y,z); fill(c); noStroke(); rectMode(CENTER); rect(0,0,cellsize,cellsize); popMatrix(); } } } これでようやく、 チュートリアル の Images and Pixels が終わりとなった。 ここまでを「日記(1)」として区切りにして、次は「日記(2)」として Drawing Curves から始めてみたいと思う。
Processing日記(2)
「日記」シリーズ の記録