今回の記事ではオブジェクト指向の解説をしてみます。具体的なコードをしっかりお見せすることを意識しました。
また、最初からオブジェクト指向でプログラミングするのは初心者の方には難しいと思うので、
まず普通のプログラミングをした後に、それをオブジェクト指向に改造する
という流れになっています。
オブジェクト指向をなんとなく知ってはいるけど使いみちがわからない、という方の参考になれば幸いです。
使用言語はJavaベースの開発環境であるProcessingです。
前回の、【ジェネラティブアート】プログラミング初心者がProcessingでちょっとしたアートを作れるようになる記事 の続編です。
そちらを先に見ていただいたほうがわかりやすいとは思いますが、この記事から読み始めても問題はないと思います。
前回の記事では、こんな画像を作りました。
ただ点と点をつないで図形を描くだけというシンプルなものですが、ここからいろんな応用ができてなかなか奥が深いです。
うまく応用すれば、こんなのも作れたりします。
無限にうねうねし続けるカラフルな螺旋です。けっこうおもしろいでしょ?
そこで今回は、前回のアルゴリズムを、より応用が効くようにオブジェクト指向プログラミングで書き直してみようと思います。
以下コード全文です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
Spiral[] spirals; void setup() { colorMode(HSB,360,100,100,100); size(900, 600); background(0,0,0); strokeWeight(0.5); smooth(); spirals = new Spiral[100]; for (int i = 0; i < spirals.length; i++){ spirals[i] = new Spiral(); } } void draw() { background(0,0,0); for (int i = 0; i < spirals.length; i++){ spirals[i].render(); } } class Particle { int centx = width/2; int centy = height/2; float radius; float thisRadius; float angle; float x; float y; Particle(float r, float a, float noise) { radius = r; angle = a; thisRadius = radius + noise; } void render() { x = centx + thisRadius*cos(angle); y = centy + thisRadius*sin(angle); //circle(x,y,10); } } class Spiral { int initialRadius; int nrotate; int nparticles; float incr; float incNoise; int col; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; incNoise = 0.01; col = int(random(360)); particles = new Particle[nparticles]; float noiseSeed = random(100); float radius = initialRadius; float angleStart = radians(random(nrotate*360)); for (int i = 0; i<particles.length; i++) { float noise = noise(noiseSeed)*200-100; float angle = i*nrotate*TWO_PI/particles.length + angleStart; particles[i] = new Particle(radius, angle, noise); radius += incr; noiseSeed += incNoise; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { stroke(col,100,100,30); line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
できる画像も基本的には前回と一緒なのですが、全く同じだとつまらないので少しだけアレンジしてあります。
どうしてオブジェクト指向が必要なの?
図形に動きを与えようと思うと、描画する点それぞれに自分の位置を覚えさせないといけません。
そこで配列を作って位置の情報を保持する、というのも悪くないのですが、それだとあまり応用が効かないんですよね。
情報量が増えたときにわかりにくくなってしまうんです。
たとえば動き以外にも、さきほどのうねうねカラフル螺旋のように色やその時間変化なんかを覚えさせたりしていくと、ものすごく多次元の配列になってしまいます。とても見にくいし扱いにくいです。
しかしオブジェクト指向プログラミングを使えば、いろんな情報を見やすく保持しつつ、独自の動きを与えることができます。
情報量を増やしたいけど、瀕雑になるのは避けたい。
そんな悩みを解決してくれるのがオブジェクト指向なんです。
オブジェクト指向でアルゴリズムをクラスにすると、情報量を増やせるだけでなく、メソッドというクラス固有の関数を持たせることだってできます。
更に、インスタンス化するだけでいくらでも複製することができるんです。
コンピュータの得意技である複製を簡単に使えるという点でも、オブジェクト指向は便利です。
1.Particleクラスを作って、全ての点に自分の位置を覚えさせる
まずは画像を構成する点を描くための、Particleクラスを定義します。
コードと、得られる画像はこちらです。
コード↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
Particle particle; void setup(){ size(900, 600); background(255); particle = new Particle(100,0); } void draw(){ particle.render(); } class Particle { int centx = width/2; int centy = height/2; float radius; float thisRadius; float angle; float x; float y; Particle(float r, float a) { radius = r; angle = a; thisRadius = radius; } void render() { x = centx + radius*cos(angle); y = centy + radius*sin(angle); circle(x,y,10); } } |
画像↓
まずは、それぞれの点が自分の位置を覚えられるように、Particleクラスを作るところからです。
クラスを作るというのは、いわば設計図を書くようなことです。
また、あとでこの設計図をもとにオブジェクトを作るのですが、それはインスタンス化といいます。
属性
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Particle { int centx = width/2; int centy = height/2; float radius; float thisRadius; float angle; float x; float y; Particle(float r, float a) { radius = r; angle = a; thisRadius = radius; } void render() { x = centx + thisRadius*cos(angle); y = centy + thisRadius*sin(angle); circle(x,y,10); } } |
黄色くマークしてある部分が、クラスの「属性」を宣言する部分です。
「属性」とは、クラスに覚えさせたい情報のことだと思っていただければ大丈夫です。
ここに、覚えさせたい情報、今回の場合は自分の座標などを書いていきます。
属性には、最初から値が決まっているものと、後で値が変わるものがあります。
今回は、centXやcentYなどは最初から値が決まっているものとして定義するので、
int centx = width/2;
int centy = height/2;
として、宣言と同時に値を与えてあります。
残りは後で値が変わるものなので、この時点では値は決まっていません。
今回は螺旋を描画する予定なので、位置情報は極座標形式で与えておきましょう。
centx、centyは中心となる点=原点で、radiusとangleがそれぞれ半径と角度になります。
描画する際には極座標からデカルト座標に変換しないといけないので、デカルト座標のx,yも用意してあります。
コンストラクタ
次はコンストラクタです。黄色くマークしてあるところを見てください。
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Particle { int centx = width/2; int centy = height/2; float radius; float thisRadius; float angle; float x; float y; Particle(float r, float a) { radius = r; angle = a; thisRadius = radius; } void render() { x = centx + radius*cos(angle); y = centy + radius*sin(angle); circle(x,y,10); } } |
コンストラクタでは、変数の初期化を行います。
初期化、つまりコンストラクタの内容は最初に一回だけ実行されます。
インスタンス化するときに受け取る値もここで指定します。
前述のように、インスタンス化というのはクラス設計図を元にオブジェクトを作る操作のことです。
ここではParticleクラスをインスタンス化する際に、
Particle(r, a)という形で、半径rと角度aを与えるようにしてあります。
メソッド
最後にメソッドです。
メソッドはクラスに固有の関数のようなものです。
今回は自分を表示する関数、renderメソッドを定義します。
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Particle { int centx = width/2; int centy = height/2; float radius; float thisRadius; float angle; float x; float y; Particle(float r, float a) { radius = r; angle = a; thisRadius = radius; } void render() { x = centx + radius*cos(angle); y = centy + radius*sin(angle); circle(x,y,10); } } |
与えられた半径と角度から、自分のXY座標を計算します。
そして計算できたら、自分の座標を中心に半径10の円を表示するようにしてあります。
これで、設計図となるParticleクラスは完成です。
Particleクラスを定義できたら、さっそく動かしてみましょう。クラスの外側に、以下のように書き込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Particle particle; void setup(){ size(900, 600); background(255); particle = new Particle(100,0); } void draw(){ particle.render(); } |
まずsetup関数の中でParticleクラスをparticleとしてインスタンス化します。
インスタンス化というのは、クラス設計図をもとにしてオブジェクトを作り出すことでした。
この時与える位置情報は適当でいいです。とりあえず半径100、角度0を与えておきました。
続いてdraw関数の中で、先程インスタンス化したparticleのrenderメソッドを実行してみます。
particlesのrenderメソッドを実行するときは、
particle.render()を使います。
以下のようにparticleが表示されたら成功です。
2.Particleをたくさんインスタンス化して、螺旋を描かせてみる
たった1つparticleを表示するだけではつまらないですよね。
せっかくクラスを作ったので、インスタンス化しまくって大量のParticleを表示してやりましょう。
とりあえず2000個のParticleを使って螺旋を描いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Particle[] particles; void setup() { size(900, 600); background(255); int initialRadius = 10; int nrotate = 4; int nparticles = 2000; float incr = 0.1; particles = new Particle[nparticles]; float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle); radius += incr; } } void draw() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } saveFrame("output.png"); } class Particle{ //省略 } |
ここではParticleクラスはそのままに、setup関数とdraw関数のみを変更します。
基本は前回の記事と同じで、角度が大きくなるにつれて半径を増やしていくようなアルゴリズムです。
ただ前回はデカルト座標(x,y)を使っていたのに対して、今回は極座標(半径radius, 角度angle)を使います。
また最終的にはこのアルゴリズムもクラスにしたいので、使う数字は全部、最初に宣言しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Particle[] particles; void setup() { size(900, 600); background(255); int initialRadius = 10; int nrotate = 4; int nparticles = 2000; float incr = 0.1; particles = new Particle[nparticles]; float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle); radius += incr; } } void draw() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } saveFrame("output.png"); } |
initialRadiusは半径の初期値で、最初の点がここから始まります。
nrotateは回転する回数です。今回は4回転させたいので4にしています。
nparticlesはインスタンスparticlesの数で、螺旋を構成する点の数です。今回は2000個のparticlesを使って螺旋を描いていきます。
incrは半径の増え幅です。次の点を描くときにどれだけ半径を増やすかを表しています。今回は0.1にしてあります。
そしてParticleクラスを、nparticles個使うことを宣言しています。このときに配列の形でインスタンス化します。
2000個のインスタンスを集めたものなので、particles(particleの複数形)と名付けて、これが配列であることがわかるようにしましょう。
以下からはparticlesのそれぞれに属性を与えて、インスタンス化していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Particle[] particles; void setup() { size(900, 600); background(255); int initialRadius = 10; int nrotate = 4; int nparticles = 2000; float incr = 0.1; particles = new Particle[nparticles]; float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle); radius += incr; } } void draw() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } saveFrame("output.png"); } |
まず半径radiusに初期値initialRadiusをセットします。
そしてiを0からparticles.length(particlesの全要素数)まで1ずつ増やしながらループ処理を行います。
ここは半径と角度を計算してparticles[i]に与えるだけです。
角度はiに連動して計算するようにして、半径はループが進むごとに0.1ずつ増やすようにします。
これでインスタンス化は完了です。
あとはdraw関数の中で、particlesの全ての要素のrenderメソッドを実行すれば、螺旋が表示されるはずです。
23 24 25 26 27 28 |
void draw() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } saveFrame("output.png"); } |
こんなかんじで表示されたら成功です。
3.螺旋を描いたアルゴリズムをクラスにする
先程は普通にdraw関数の中で螺旋を描きましたね。しかしそれだと後で螺旋を増やしたいときに面倒です。
そこで、この螺旋を描くアルゴリズムもクラスにしてしまいましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
Spiral spiral; void setup() { size(900, 600); background(255); spiral = new Spiral(); } void draw() { background(255); spiral.render(); } class Spiral { int initialRadius; int nrotate; int nparticles; float incr; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; particles = new Particle[nparticles]; float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle); radius += incr; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
今回も先程と同様、Particleクラスは変更しないので省略しています。(丸を消して線だけを残す時は、cicle(x, y,10)を消すかコメントアウトしてください。)
Particleクラスを作った時と同じ要領で、Spiralクラスを作っていきます。
まずは属性の宣言と、コンストラクタの設定です。
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
class Spiral { int initialRadius; int nrotate; int nparticles; float incr; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; particles = new Particle[nparticles]; float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle); radius += incr; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
Spiralクラスには、ステップ3で宣言した変数達を属性としてもたせます。
また、インスタンス配列particlesも宣言しておきます。
コンストラクタでは、先ほど宣言した属性達に具体的な値を入れていってあげます。
ここに、先程螺旋を描く際に作ったアルゴリズムをコピペします。
最後に、自分自身を表示するメソッド、renderメソッドを定義して完成です。
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
class Spiral { int initialRadius; int nrotate; int nparticles; float incr; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; particles = new Particle[nparticles]; float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle); radius += incr; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
まず全てのparticlesのrenderメソッドを実行して、自分の座標を計算させます。
その次に、particles同士を線で結び、螺旋を描くような命令を書きます。
renderメソッドを定義できたら、実際にSpiralクラスをインスタンス化して、renderメソッドを実行してみましょう。
Particleクラスのrenderメソッドからcircle(x,y,10)を取り除けば、丸が消えて線だけが残り、こんな画像が表示されます。
4.クラスSpiralにランダム性を加えてみる
ステップ3までで、螺旋をクラス化して、ノーマルな螺旋を作れるようになりました。
ステップ4では、螺旋にノイズを加えてグニャグニャさせてみます。
以下コードです。先程のコードから変更した部分を黄色くハイライト表示してあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class Spiral { int initialRadius; int nrotate; int nparticles; float incr; float incNoise; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; incNoise = 0.005; particles = new Particle[nparticles]; float noiseSeed = random(100); float radius = initialRadius; for (int i = 0; i<particles.length; i++) { float noise = noise(noiseSeed)*200-100; float angle = i*nrotate*TWO_PI/particles.length; particles[i] = new Particle(radius, angle, noise); radius += incr; noiseSeed += incNoise; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
今回変更するのはSpiralクラスのみです。
ここのアルゴリズムは前回の記事とほとんど同じなので、そちらを参照してみてください。
一応コピペを貼っておきます。↓
螺旋をグニャグニャさせるには、半径の大きさにパーリンノイズを加えるという方法をとります。
パーリンノイズというのは、完全なランダムノイズではなく、前の値に近い値をランダムに生成するもの。
滑らかなランダムさ、という感じです。
ノイズ関数を使うと、0から1までの範囲でパーリンノイズを生成することができます。
これを使って、半径になめらかなランダムさを加えます。
noise関数は0から1までの値しか取らないので、200倍して100を引くことで−100から100までの値を取るようにしてあります。
新しく出てきたnoiseSeedという変数は、パーリンノイズの種のようなものです。
noise関数は、noiseSeedを動かすことで値を変化させるので、ループごとに0.05だけ増やしています。
これでもうグニャグニャ螺旋の完成です。
5.クラスSpiralをたくさんインスタンス化してみる
やっと最後のステップです。
無事クラスとなった螺旋を量産していくのですが、それだけだと前回と同じになって面白くありません。
そこで今回はSpiralクラスに「色」属性も定義して、カラフルな螺旋を量産していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
Spiral[] spirals; void setup() { colorMode(HSB,360,100,100,100); size(900, 600); background(0,0,0); strokeWeight(0.5); smooth(); spirals = new Spiral[100]; for (int i = 0; i < spirals.length; i++){ spirals[i] = new Spiral(); } } void draw() { background(0,0,100); for (int i = 0; i < spirals.length; i++){ spirals[i].render(); } } class Spiral { int initialRadius; int nrotate; int nparticles; float incr; float incNoise; int col; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; incNoise = 0.005; col = int(random(360)); particles = new Particle[nparticles]; float noiseSeed = random(100); float radius = initialRadius; float angleStart = radians(random(nrotate*360)); for (int i = 0; i<particles.length; i++) { float noise = noise(noiseSeed)*200-100; float angle = i*nrotate*TWO_PI/particles.length + angleStart; particles[i] = new Particle(radius, angle, noise); radius += incr; noiseSeed += incNoise; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { stroke(col,100,100,30); line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
Particleクラス以外を変更しています。
変更点を順番に説明していきます。
まずはsetupとdrawから見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Spiral[] spirals; void setup() { colorMode(HSB,360,100,100,100); size(900, 600); background(0,0,0); strokeWeight(0.5); smooth(); spirals = new Spiral[100]; for (int i = 0; i < spirals.length; i++){ spirals[i] = new Spiral(); } } void draw() { background(0,0,100); for (int i = 0; i < spirals.length; i++){ spirals[i].render(); } } |
鮮やかな色だけを使いたいので、カラーモードはHSBにしてあります。
HSBは色相と彩度と明度で色を表現するモードで、彩度と明度をマックスにした状態で色相だけを変化させると、鮮やかな色だけが得られます。
HSBカラーモードでは、色は(色相、彩度、明度、透明度)で指定できます。(透明度は省略可)
また螺旋の色が映えるように、背景は黒にしてあります。
draw関数の中では、Spiralクラスをspiralsとして100本インスタンス化してます。
次にSpiralクラスの変更点です。新たな属性colを加えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class Spiral { int initialRadius; int nrotate; int nparticles; float incr; float incNoise; int col; Particle[] particles; Spiral() { initialRadius = 10; nrotate = 4; nparticles = 2000; incr = 0.1; incNoise = 0.005; col = int(random(360)); particles = new Particle[nparticles]; float noiseSeed = random(100); float radius = initialRadius; float angleStart = radians(random(nrotate*360)); for (int i = 0; i<particles.length; i++) { float noise = noise(noiseSeed)*200-100; float angle = i*nrotate*TWO_PI/particles.length + angleStart; particles[i] = new Particle(radius, angle, noise); radius += incr; noiseSeed += incNoise; } } void render() { for (int i = 0; i<particles.length; i++) { particles[i].render(); } for (int i = 1; i<particles.length; i++) { stroke(col,100,100,30); line(particles[i].x,particles[i].y,particles[i-1].x,particles[i-1].y); } } } |
また、29行目でangleにangleStartを加えることで、生成される全ての螺旋が異なる位置からスタートするように設定してます。
stroke(col,100,100,30);で、線に色を付けていきます。
鮮やかな色だけを使いたいので、属性colは色相を変化させるのに使い、彩度と明度はマックスの100に固定しておきます。
あとなんとなく透明度を30に設定しておきました。
これで完成です。お疲れ様でした。
参考書
どうでしょうか。オブジェクト指向の便利さが少しでも伝わっていたら嬉しいです。
ジェネラティブアートに関しては『[普及版]ジェネラティブ・アート―Processingによる実践ガイド』という本がとてもおすすめです。下にリンクを貼っておくので、興味があったら是非買ってみてください。
もちろんオブジェクト指向の解説もありますよ。
また、私のジェネラティブアート作品はここにまとめてあるので、もしよければ見ていってください。
おしまい。
コメント