講義[7]:ボロノイ図(2)

ボロノイ図(2次元)の描画のために3次元空間を考える方法があります.母点(生成元)たちを頂点とする円錐たち(互いに軸が平行)を考えます.その側面(曲面)たちが交差してできる曲線たちを円錐軸に平行な方向に投影(射影)すると考えてください.円錐軸と垂直な投影面に投影(射影)された曲線たちが,ボロノイ辺たちとなります.ボロノイ辺たちの交点たちがボロノイ点たちとなります.複数のボロノイ辺で囲まれたある一つの領域がボロノイ領域です.この一つの領域はある円錐の頂点に対応します.
 上に述べたことを次の動画で示します.

こんな感じです.

こんかいは,この方法でボロノイ図を作成することを目指して,3次元空間に図形を描画する方法を学びます.いくつかの方法がありますが,今回は,空間内に3つの頂点を指定して三角形を作る考え方を基本とする方法を学びます.

まずは,次に2次元空間内に3つの頂点を指定して三角形をつくる基本的方法を示します:


void setup(){
  size(600,600);
}

void draw(){
  background(170);
  
  beginShape(TRIANGLES);
  vertex(100, 50);
  vertex(300, 100);
  vertex(100, 250);
  vertex(200, 200);
  vertex(400, 200);
  vertex(200, 400);
  vertex(300, 250);
  vertex(500, 300);
  vertex(250, 500);
  endShape();
 
}

実行結果はこれです:

「beginShape(TRIANGLES)」と「endShape()」に囲まれた部分で頂点(vertex)たちの座標を指定します.順に3つずつ組になっていると考えてください.

さて,頂点たちの座標を素朴に書いていくのは大変です.繰り返し処理を利用しましょう.繰り返し処理の利用例を次に示します:


int N = 20;
float d;

void setup(){
  size(500,500);
  d = width/N;
}

void draw(){
  background(170);
  
  beginShape(TRIANGLES);
  for(int i = 0; i < N; i++){
    float x = i * d;
    float y0 = height/2;
    float y1 = 200 * sin(x/width * PI * 3);
    vertex(x,     y0);
    vertex(x + d, y0);
    vertex(x,     y0 + y1);
  }
  endShape();
}

実行結果はこれです:

Processingには三角形を敷き詰めていくための便利な方法がいくつか用意されています.そのひとつが「TRIANGLE_STRIP」です.今度はこのキーワードを「beginShape()」の引数として指定します.
「TRIANGLE_STRIP」の使用例を次に示します.帯状(ベルト状)のものを想像してください.帯の両方の縁上の頂点たちの座標を「ジグザグ」に指定していく様子をイメージするとわかりやすいかもしれません:


int N = 30;
float d;

void setup(){
  size(500,500);
  d = width/N;
}

void draw(){
  background(170);
  
  beginShape(TRIANGLE_STRIP);
  for(int i = 0; i < N; i++){
    float x = i * d;
    float y0 = height/2;
    float y1 = 20 * sin(x/width * PI * 6) + 50;
    vertex(x, y0 - y1);
    vertex(x, y0 + y1);
  }
  endShape();
}

実行結果はこれです:

三角形を敷き詰めていくための便利な方法をもうひとつ示します.「TRIANGLE_FAN」です.今度はこのキーワードを「beginShape()」の引数として指定します.「TRIANGLE_FAN」の使用例を次に示します.扇(おうぎ)を想像してください.扇の要(かなめ)の座標を指定してから周辺の座標を指定していきます:


int n = 20;
float r = 100;

void setup(){
  size(500,500);
}

void draw(){
  background(170);
  
  beginShape(TRIANGLE_FAN);
  vertex(width/2, height/2);
  for(int i = 0; i <= n; i++){
    float p = i/(float)n * TWO_PI;
    float x = r * cos(p) + width/2;
    float y = r * sin(p) + height/2;
    vertex(x, y);
  }
  endShape();
}

実行結果はこれです:

この「扇」の要を3つ目の次元の方向に引き上げて円錐をつくることができます.実は,この円錐をボロノイ図の作成に利用しようというわけです.Processingでは,3次元的なものを容易に実現できます.上記の「扇」のソースコーソを少しだけ改変して円錐の表示を実現したものを次に示します.動きを取り入れて面白みをつけくわえました.


int n = 20;
float r = 150;

float a = 0;

void setup(){
  size(500,500,P3D);
}

void draw(){
  background(170);
  lights();
  translate(width/2, height/2);
  rotateX(a);
  a += 0.01;
  
  beginShape(TRIANGLE_FAN);
  float h = 200 * cos(6*a);
  vertex(0, 0, h);
  for(int i = 0; i <= n; i++){
    float p = i/(float)n * TWO_PI;
    float x = r * cos(p);
    float y = r * sin(p);
    vertex(x, y);
  }
  endShape();
}

実行結果はこれです:

さて,ここで,課題です.ボロノイ図の作成に進む前に,せっかくの機会ですから,頂点を指定して図形を作る方法に慣れておきましょう.
なるべく課題3まで到達してください.余力がある人は課題4に挑戦してみてください.

課題1.上記「TRIANGLES」の例題のソースコードの内容をよく理解して次のようにインタラクティブにしてみてください.

課題2.上記「TRIANGLE_STRIP」の例題のソースコードの内容をよく理解して次のようにインタラクティブにしてみてください.

課題3.上記「TRIANGLE_FAN」(2次元)の例題のソースコードの内容をよく理解して次のようにインタラクティブにしてみてください.

課題4.これは,余力がある人が取り組んでみてください.上記「TRIANGLE_FAN」(3次元)の例題のソースコードの内容をよく理解して次のように,にぎやかにしてみてください.オブジェクト指向の考え方を用いましょう!

講義[6]:ボロノイ図(1)

今回からは,ボロノイ図(Voronoi diagram)を取り扱います.
ここでは2次元ユークリッド空間(簡単のため,以後,空間)を考えましょう.

空間内に特に指定されたいくつかの点(母点)があるとします.
ボロノイ図とは,それぞれの母点が他より自分に近い場所を自分の勢力圏(なわばり)として囲い込むことによってできる図形です.
(「なわばりの数理モデル — ボロノイ図からの数理工学入門」杉原著(共立出版2009)より)

下図にボロノイ図に関する用語を示します.

また,次の図にボロノイ図の性質を示します.

次の動画は母点を動かしながらボロノイ図を描画させたものです.

さて,早速ですが,課題です.
課題1(レポートです!)
 (a)ボロノイ図の理学工学における応用例をなるべくたくさん調べてリストを作ってください.必要に応じて簡単な説明をつけましょう.
 (b)余力があれば応用例を分類整理しましょう.
 (c)さらに余力があれば,ボロノイ図として解釈できそうな,自然界にみられる造形物を探してみてください.
 (d)さらに余力があれば,ボロノイ図を作成するためのアルゴリズムにはどのようなものがあるのか調べましょう.
 以上の「調査結果」をレポートとして提出してください.
締め切りなどの必要事項や詳細は授業時間に説明します.

参考文献:
(1)「なわばりの数理モデル — ボロノイ図からの数理工学入門」杉原著(共立出版2009)
(2)「迷路の中のウシ」イアン・スチュアート(川辺訳)(共立出版2015)

講義[5]:迷路をつくる(3)

今回は,迷路をつくるアルゴリズムを理解しましょう.
迷路をつくるための様々なアルゴリズムは既にいくつも提案されています( Wikipedia:「Maze generation algorithm」参照).
 ここでは「depth-first search by recursive backtracker」を実装することを目標にしましょう.実装の前に,まずアルゴリズムを理解しましょう.
 このアルゴリズムは次のように表されます.

------------------------------------------------------
1. Make the initial cell the current cell and mark it 
   as visited
2. While there are unvisited cells
  1. If the current cell has any neighbors 
     which have not been visited
        1. Choose randomly one of the unvisited 
           neighbors
        2. Push the current cell to the stack
        3. Remove the wall between the current cell 
           and the chosen (next) cell
        4. Make the chosen (next) cell the current 
           cell and mark it as visited
  2. Else if stack is not empty
        1. Pop a cell from the stack
        2. Make it the current cell
------------------------------------------------------

ここで活用されている重要なデータ構造は「スタック(stack)」です.
データを格納する操作をpush,データを取り出す操作をpopといいます.
スタックは,最後に格納(push)したデータが最初に取り出される(pop)という特徴をもちます.
つまり「LIFO (Last In First Out)」あるいは「FILO (First In Last Out)」です.

課題1(レポートです!).スタックの特徴に注意しながら,上記アルゴリズムを解読(理解)してください.紙と鉛筆を用いて上記アルゴリズムをシミュレートしてみるとよいかもしれません.そのシミュレーションをわかりやすく図示してまだ理解していない人に説明してあげましょう.
 例えば図示の方法として紙に迷路の図としての4×4ぐらいのマス目を描き,そこに番号を振っておきます.またマス目の左脇にはスタックを描いておく方法が考えられます.各自なるべく「わかりやすい解説方法」を考えてください.
 この「わかりやすい解説方法」をレポートとして提出してください.締め切りなどの必要事項や詳細は授業時間に説明します.

参考動画:

講義[4]:迷路をつくる(2)

今回も前回に引き続き,オブジェクト指向の考え方を用いて迷路を自動生成するプログラムを組んでいきます.少しづつ進めましょう.
今度のプログラムでは,「マーカー」が中心から出発して動き回ります.そのためにCellクラスのオブジェクト「 marker 」を導入しました.
「訪問された」(Cellクラスの)オブジェクト grid[][]には色をつけるようにしました.そのためにCellクラスに「 visited 」というフィールド変数を追加しています.
さて早速ソースコードを示しますが,これを実行してしばらくするとエラーが出ます.何回か実行させてみて,どのようなときにどのようなエラーがでるか確認してください.
「マーカー」が端っこに移動しようとするとエラーがでることがわかるでしょう.
まずはソースコードの意味を理解してください.前回のソースコードとほぼ同じ内容です.
異なる部分(追加した部分)は「 //// <<----(*) 」で示してあります.


//// Maze generation 

int cols, rows;
float w = 20;
Cell[][] grid;

Cell marker; //// <<----(*)

int margin = 50;
//////////////////////////
void setup() {
  size(600, 600);
  rectMode(CENTER);
  colorMode(HSB);
  cols = floor((width - margin * 2)/w);
  rows = floor((height - margin * 2)/w);
  grid  = new Cell[cols][rows];
  
  for(int j = 0; j < rows; j++){
    for(int i = 0; i < cols; i++){
      grid[i][j] = new Cell(i,j);
    }
  } 
  
  int startCol = int(cols/2);         //// <<----(*)
  int startRow = int(rows/2);         //// <<----(*)
  marker = grid[startCol][startRow];  //// <<----(*)
  
}
//////////////////////////
void draw() {
  background(120);
  for(int j = 0; j < rows; j++){
    for(int i = 0; i < cols; i++){
      grid[i][j].show();
    }
  }
  
  marker.visited = true;  //// <<----(*)
  marker.highlight();     //// <<----(*)
  marker = marker.move(); //// <<----(*)
  
}
////////////////////////////////////////////////////
class Cell{
  int i, j;
  boolean[] walls = {true, true, true, true};
  boolean visited = false; //// <<----(*)
  
  Cell(int i0, int j0){
    i = i0;
    j = j0;
  }
  
  Cell move(){                     //// <<----(*)
    Cell[] ngb = new Cell[4];      //// <<----(*)
    ngb[0] = grid[i][j-1];         //// <<----(*)
    ngb[1] = grid[i+1][j];         //// <<----(*)
    ngb[2] = grid[i][j+1];         //// <<----(*)
    ngb[3] = grid[i-1][j];         //// <<----(*)
    return ngb[floor(random(4))];  //// <<----(*)
  }
  
  void highlight(){                //// <<----(*)
    float x = i*w + margin + w/2;  //// <<----(*)
    float y = j*w + margin + w/2;  //// <<----(*)
    noFill();                      //// <<----(*)
    strokeWeight(4);               //// <<----(*)
    stroke(50,255,255);            //// <<----(*)
    rect(x,y,w*1.3,w*1.3);         //// <<----(*)
  }                                //// <<----(*)
  
  void show(){
    float x = i*w + margin + w/2;
    float y = j*w + margin + w/2;
    
    if(visited){          //// <<----(*)
      noStroke();         //// <<----(*)
      fill(170,140,255);  //// <<----(*)
      rect(x,y,w,w);      //// <<----(*)
    }                     //// <<----(*)
    
    strokeWeight(2);
    stroke(0);
    if(walls[0]){
      line(x-w/2, y-w/2, x+w/2, y-w/2); // 0:top
    }
    if(walls[1]){
      line(x+w/2, y-w/2, x+w/2, y+w/2); // 1:right
    }
    if(walls[2]){
      line(x+w/2, y+w/2, x-w/2, y+w/2); // 2:bottom
    }
    if(walls[3]){
      line(x-w/2, y+w/2, x-w/2, y-w/2); // 3:left
    }
    strokeWeight(4);
    stroke(255);
    if(!walls[0]){
      line(x, y, x, y-w/2); // 0:top
    }
    if(!walls[1]){
      line(x, y, x+w/2, y); // 1:right
    }
    if(!walls[2]){
      line(x, y, x, y+w/2); // 2:bottom
    }
    if(!walls[3]){
      line(x, y, x-w/2, y); // 3:left
    }
  }
}

課題1.上記ソースコードを実行させてエラーが出る理由を理解してください.次にエラーが出ないように修正してください.
ここでは,「マーカー」の移動方向に制限を加えて「端っこ」から出ないようにしてください.
課題2.上記ソースコードを修正して「マーカー」の移動にともなって移動した方向の「壁」を取り除くようにしてください.
課題3.上記ソースコードを修正して一度「訪問した」セルには2度と訪れないようにしてください.

参考図(今回のプログラム):

参考図(課題1):

参考図(課題2):

参考図(課題3):

講義[3]:迷路をつくる(1)

今回から,オブジェクト指向の考え方を用いて迷路を自動生成するプログラムを組んでいきます.少しづつ進めましょう.


//// Maze generation 

int cols, rows;
float w = 50;
Cell[][] grid;

int margin = 50;
//////////////////////////
void setup() {
  size(600, 600);
  rectMode(CENTER);
  colorMode(HSB);
  cols = floor((width - margin * 2)/w);
  rows = floor((height - margin * 2)/w);
  grid  = new Cell[cols][rows];
  
  for(int j = 0; j < rows; j++){
    for(int i = 0; i < cols; i++){
      grid[i][j] = new Cell(i,j);
    }
  } 
}
//////////////////////////
void draw() {
  background(120);
  for(int j = 0; j < rows; j++){
    for(int i = 0; i < cols; i++){
      grid[i][j].show();
    }
  }
}
//////////////////////////////////////////////
class Cell{
  int i, j;
  boolean[] walls = {true, true, true, true};
  
  Cell(int i0, int j0){
    i = i0;
    j = j0;
  }
  
  void show(){
    float x = i*w + margin + w/2;
    float y = j*w + margin + w/2;
    
    strokeWeight(2);
    stroke(0);
    if(walls[0]){
      line(x-w/2, y-w/2, x+w/2, y-w/2); // 0:top
    }
    if(walls[1]){
      line(x+w/2, y-w/2, x+w/2, y+w/2); // 1:right
    }
    if(walls[2]){
      line(x+w/2, y+w/2, x-w/2, y+w/2); // 2:bottom
    }
    if(walls[3]){
      line(x-w/2, y+w/2, x-w/2, y-w/2); // 3:left
    }
    strokeWeight(4);
    stroke(255);
    if(!walls[0]){
      line(x, y, x, y-w/2); // 0:top
    }
    if(!walls[1]){
      line(x, y, x+w/2, y); // 1:right
    }
    if(!walls[2]){
      line(x, y, x, y+w/2); // 2:bottom
    }
    if(!walls[3]){
      line(x, y, x-w/2, y); // 3:left
    }
  }
}

課題1.上記コードを修正して,ある一つの方向(乱数で決めましょう)の「壁」が無くなった状況を実現しましょう.
課題2.上記コードをさらに修正して,ある特定の「セル」をそれをハイライトさせてみましょう.
課題3.上記コードをさらに修正して,ハイライトさせるセルを変えていきましょう.

参考図(課題1):

参考図(課題2):

講義[12]:曲面(「TRIANGLE_STRIP」を使う)

曲面を描いてみましょう.
「TRIANGLE_STRIP」というキーワードを用いれば,簡単に曲面を描画できます.

細長い布(strip)あるいは帯をイメージしてください.
それにジグザグに線を入れていくと,1列につながった三角形ができますね.そうやって「三角形がつらなった帯」ができます.それを並行にならべていくような感じで曲面を構成します.

次にこれを利用して球面を描くコードサンプルを示します.

////////////////////////////////////////////////
// surface
////////////////////////////////////////////////

int Nu = 20;
int Nv = 40;

PVector[][] surface = new PVector[Nu + 1][Nv + 1];

float Lu = PI;
float Lv = PI;

float a = 250;

float angle = 0;
////////////////////////////////////////////////
void setup(){
  size(500,500,P3D);
  setSurface();
}
////////////////////////////////////////////////
void draw(){
  background(0);
  translate(width/2, height/2, -width/2);
  lights();
  rotateX(angle);
  rotateY(angle*0.3);
  angle += 0.02;
  
  for(int j = 0; j < Nv; j++){
    beginShape(TRIANGLE_STRIP);
    for(int i = 0; i < Nu + 1; i++){
      PVector v1 = surface[i][j];
      PVector v2 = surface[i][j+1];
      fill(255);
      stroke(0);
      vertex(v1.x, v1.y, v1.z);
      vertex(v2.x, v2.y, v2.z);
    }
    endShape();
  }
  
}
////////////////////////////////////////////////
void setSurface(){
  for(int j = 0; j < Nv + 1; j++){
    for(int i = 0; i < Nu + 1; i++){
      float u = map(i,0,Nu,0,Lu);
      float v = map(j,0,Nv,-Lv,Lv);
      PVector p = fp(u, v);
      p.mult(a);
      surface[i][j] = p;
    }
  }
}
////////////////////////////////////////////////
PVector fp(float u, float v){
  float x = sin(u) * cos(v);
  float y = sin(u) * sin(v);
  float z = cos(u);
  return new PVector(x, y, z);
}

課題1.上記コードを少々修正して色々な曲面を描いてみなさい.
例を次に示します.

課題2.曲面の上を物体が動き回るような芸術作品を作りなさい.
例を次に示します.

これは,気合いをいれてつくった例です(^^).

講義[11]:再帰(座標変換,スタック)

再帰的定義による構造記述は面白いです.フラクタル的な構造が短いコードで実現できます.

//////////////////////////////////////////
// 3D tree
//////////////////////////////////////////
float theta = PI/6;
float a = 0;
//////////////////////////////////////////
void setup(){
  size(500,500,P3D);
  colorMode(HSB);
}
//////////////////////////////////////////
void draw(){
  background(0);
  lights();
  translate(width/2, height/2, width/2.5);
  rotateX(HALF_PI);
  rotateX(a);
  rotateZ(a*3);
  if( mousePressed == true){
    theta = map(mouseX, 0, width, 0, PI);
    a     = map(mouseY, 0, height, PI, TWO_PI);
  }else{
    theta = HALF_PI * sin(a/4);
  }
  lights();
  branch(70,40);
  a += 0.03;
}
//////////////////////////////////////////
void branch(float len, float c){ 
  noStroke();
  fill(c,255,255);
  c += 13;
  translate(0,0,len/2);
  box(len/5, len/5, len);
  translate(0, 0, len/2);
  len *= 0.6;
  if(len > 2){
    for(int i = 0; i < 3; i++){
      pushMatrix();
      rotateZ(TWO_PI/3 * i);
      rotateY(theta);
      branch(len, c);
      popMatrix();
    }
  }
}

今回もダニエル・シフマンのこの動画を大いに参考にしました:

この動画では座標変換とスタックという概念を用いて定番例題の「木構造」を実現しています.

講義[10]:ArrayListによる軌跡の表現

今回は軌跡(軌道)の表現を試してみます.
ただし,ここではリアルタイムに様々な角度から眺める事ができる軌跡を考えます.
したがって描画面に単に点や線分を描けばおしまい,ではありません.
軌跡を有限個数の点の集まりとして取り扱いますが,
ここではArrayListを用いて点の座標を保持します.
軌跡の例としては,ここでは,とある有名な3変数の微分方程式を用います.

実は今回の話題は下に示すダニエル・シフマンの動画を大いに参考にしました(^^).

実に楽しい動画ですね.

下にこの動画で示されたコードに少々手を加えたものを示します.
ここでは,ArrayListの使用例として考えています.
 しかし,ついでに,微分方程式とは位相空間においてベクトル場(流れの場)を定めるものであり,ある初期値から「流れ」にのって初期値を始点とする曲線を描いたもの(軌跡あるいは軌道)がその微分方程式の解であるという認識を持ってもらえると,なおよろしいです.
 このコードは数値積分によって微分方程式を解くという例題でもあります.

課題1.なにか面白い(芸術的な)軌跡を描くような微分方程式をみつけてきて実装しなさい.
課題2.ArrayListを用いて空間中に軌跡を描くテクニックを用いた芸術的なプログラムを組みなさい.

///////////////////////////////////////////////////////
// P3D sketch
// differential equation
///////////////////////////////////////////////////////
int N = 3000;
float L; // phase space size [-L,L],[-L,L],[-L,L]
float MouseX, MouseY;
float Zoom, ZoomFlag;

float x = 0.1;
float y = 0;
float z = 0;
float a = 10;
float b = 28;
float c = 8.0/3.0;

ArrayList<PVector> points = new ArrayList<PVector>();

///////////////////////////////////////////////////////
void setup(){
  size(500,500,P3D);
  L = 60;  // simulation area: [-L,L][-L,L][-L,L]
  MouseX = width/2;
  MouseY = height/2;
  Zoom = width/1.5;
}
///////////////////////////////////////////////////////
void draw(){
  lights();
  background(0);
  viewRotationsZoom(Zoom);
  axis();
  boundaryBox();
  //////////
  float dt = 0.01;
  float dx = (a * (y - x)) * dt;
  float dy = (x * (b - z) - y) * dt;
  float dz = (x * y - c * z) * dt;
  x = x + dx;
  y = y + dy;
  z = z + dz;
  points.add(new PVector(x, y, z));
  
  if(points.size() > N){
    points.remove(0);
  }
  
  stroke(255);
  strokeWeight(2);
  noFill();
  
  colorMode(HSB);
  float hu = 0;
  beginShape();
  for(PVector v: points){ 
    stroke(hu, 255, 255);
    vertex(v.x, v.y, v.z);
    hu = (hu + 0.1)%256;
  }
  endShape();
  
  
  PVector v = points.get(points.size()-1);
  strokeWeight(1);
  stroke(255);
  fill(255,100);
  pushMatrix();
  translate(v.x, v.y, v.z);
  box(3,3,3);
  popMatrix();
  
  textSize(12);
  fill(255);
  text("Lorentz", -L,L/2,-L);
  text("Attractor", -L,L/2 + 12,-L);
  text("The Chaotic Orbit", -L,-L/2,-L);
  
  MouseX -= 0.4;
  MouseY -= 0.03;
  if(ZoomFlag ==  1){ Zoom += 3; }
  if(ZoomFlag == -1){ Zoom -= 3; }
}
////////////////////////////////
void viewRotationsZoom(float zoom){
  translate(width/2, height/2, zoom);
  if(mousePressed == true){
    MouseX = mouseX;
    MouseY = mouseY;
  }
  rotateY(PI - (float)MouseX/width * TWO_PI);
  rotateX(PI + (float)MouseY/width * TWO_PI + HALF_PI);
}
////////////////////////////////
void axis(){
  colorMode(RGB);
  strokeWeight(1);
  stroke(255, 0, 0);       // red
  line(0, 0, 0, L, 0, 0);  // red axis
  stroke(0, 255, 0);       // green
  line(0, 0, 0, 0, L, 0);  // green axis
  stroke(0, 0, 255);       // blue
  line(0, 0, 0, 0, 0, L);  // blue axis
  fill(0);
}
////////////////////////////////
void boundaryBox(){
  strokeWeight(1);
  stroke(255);
  noFill();
  box(L*2,L*2,L*2);
}
////////////////////////////////
void keyPressed(){
  if(key == 'i' || key == 'I' ){ ZoomFlag = 1; }
  if(key == 'o' || key == 'O' ){ ZoomFlag = -1; }
}
////////////////////////////////
void keyReleased(){
  ZoomFlag = 0;
}

講義[9]:オブジェクト指向入門(3)

「オブジェクト指向入門(1)」では,オブジェクトとは何か,クラスとは,フィールド,メソッド,コンストラクタ,ポリモーフィズムなどの基礎概念を学びました.「オブジェクト指向入門(2)」では,オブジェクト配列を学びました.そのあとしばらくは,3次元空間におけるオブジェクトの動作表現に関することを学びました.
 さて,今回の「オブジェクト指向入門(3)」においては,「継承」を学びます.
 たとえば,魚(Fishオブジェクト)とエサ(Targetオブジェクト)には共通することがあります.どちらも動き回ることです.どちらのオブジェクトにも位置と速度という属性があります.動き方は異なりますが,水槽の壁で反射するということは共通しています.実際,ソースコードを眺めてみると,同じような記述が複数個所に存在します.このようなときには「継承」という考え方を用いることによって,コードをすっきりさせる,すなわち可読性を上げることができます.
 具体的には,共通するものごとを「先祖クラス(superclass)」にまとめて,そのくらすから「子孫クラス(subclass)」を派生させるような考え方を用います.その際,子孫は先祖の特性(フィールドやメソッド)を継承することができ,なおかつ新たな特性(フィールドやメソッド)を付け加えることができます.
 というわけで今回は,キーワード「extends」と「super」の使い方やオーバーライド(上書き)という概念を理解しましょう.
 まずは,継承を用いて前回のコードを書き直したものを例示します.詳細は講義にて...(^^)

///////////////////////////////////////////////////////
// P3D sketch
// using PVector
// targets
// separation
///////////////////////////////////////////////////////
int N = 400;
float L; // simulation area
Fish[] fish;
float bodySize = 20;
PVector rt1 = new PVector(0, 0, 0);
PVector rt2 = new PVector(0, 0, 0);
PVector vt1 = new PVector(2, 0, 1.7);
PVector vt2 = new PVector(1, 1.3, 0);
Target target1 = new Target(rt1, vt1,20,100);
Target target2 = new Target(rt2, vt2,20,140);
float MouseX, MouseY;
///////////////////////////////////////////////////////
void setup(){
  size(600,600,P3D);
  L = width/2;  // simulation area: [-L,L][-L,L][-L,L]
  fish = new Fish[N];
  for(int i = 0; i < N; i++){
    fish[i] = new Fish(PVector.random3D(), PVector.random3D(),
                       bodySize);
  }
  MouseX = width/2;
  MouseY = height/2;
}
///////////////////////////////////////////////////////
void draw(){
  lights();
  background(170,160,220);
  viewRotations();
  axis();
  boundaryBox();
  target1.display();
  target2.display();
  target1.move();
  target2.move();
  for(int i = 0; i < N; i++){
    fish[i].display();
    fish[i].seek(target1);
    fish[i].seek(target2);
    fish[i].separate();
    fish[i].move();
  }
}
///////////////////////////////////////////////////////
class Particle{
  PVector r, v, a; //location, velocity, acceleration
  float s; //size
  Particle(PVector r0, PVector v0, float s0){
    r = r0;
    v = v0;
    s = s0;
    a = new PVector(0,0,0);
  }
  PVector getPos(){
    return r;
  }
  void reflection(){
    if(r.x > L || r.x < -L){ v.x *= -1; }
    if(r.y > L || r.y < -L){ v.y *= -1; }
    if(r.z > L || r.z < -L){ v.z *= -1; }
  }
}
///////////////////////////////////////////////////////
class Fish extends Particle{
  float eye;
  float vmax = 8.0;
  float fmax = 0.01;
  Fish(PVector r0, PVector v0, float s0){
    super(r0, v0, s0);
    r.mult(L*0.9);
    v.mult(random(1.0, vmax));
  }
  void display(){
    colorMode(HSB);
    pushMatrix();
    stroke(255);
    fill(255,100);
    translate(r.x, r.y, r.z);
    ////
    rotateZ(-atan2(v.x, v.y)); 
    rotateX(-atan2(sqrt(v.x * v.x + v.y * v.y), v.z));
    ////
    box(s/8, s/2, s);   //body
    translate(0,0,s/4);
    eye = v.mag()/vmax*255;
    noStroke();
    fill(eye,255,255);
    sphereDetail(10);      //important for the speed
    sphere(s/8+2);        //eye
    popMatrix();
  }
  void move(){ //// update state
    reflection();
    v.add(a);
    v.limit(vmax);
    r.add(v);
    a.mult(0);
  }
  void applyForce(PVector force) {
    a.add(force);
  }
  void seek(Target tg) {
    PVector desired = PVector.sub(tg.getPos(),r);
    desired.normalize(); // unit vector to the target
    desired.mult(vmax);
    PVector steer = PVector.sub(desired,v);
    steer.limit(fmax);
    applyForce(steer);
  }
  void separate(){
    float desiredseparation = bodySize * 2;
    PVector sum = new PVector(0,0,0);
    int count = 0;
    for (int i = 0; i < N; i++) {
      float d = PVector.dist(r, fish[i].r);
      if ((d > 0) && (d < desiredseparation)) {
        PVector diff = PVector.sub(r, fish[i].r);
        diff.normalize();
        diff.div(d);
        sum.add(diff);
        count++;
      }
    }
    if (count > 0) {
      sum.normalize();
      sum.div(count);
      sum.mult(vmax);
      PVector steer = PVector.sub(sum, v);
      steer.limit(fmax*10);
      applyForce(steer);
    }
  }
}
////////////////////////////////
class Target extends Particle{
  float c;
  Target(PVector r0, PVector v0, float s0, float c0){
    super(r0, v0, s0);
    c = c0;
  }
  void display(){
    colorMode(HSB);
    pushMatrix();
    translate(r.x, r.y, r.z);
    noFill();
    stroke(c,255,255);
    sphereDetail(10);
    sphere(s);
    popMatrix();
  }
  void move(){
    reflection();
    r.add(v);
  }
}
////////////////////////////////
void viewRotations(){
  float zoom = -width/2;
  if(keyPressed == true){ zoom = 0; }
  translate(width/2, height/2, zoom);
  if(mousePressed == true){
    MouseX = mouseX;
    MouseY = mouseY;
  }
  rotateY(PI - (float)MouseX/width * TWO_PI);
  rotateX(PI + (float)MouseY/width * TWO_PI);
}
////////////////////////////////
void axis(){
  colorMode(RGB);
  stroke(255, 0, 0);       // red
  line(0, 0, 0, L, 0, 0);  // red axis
  stroke(0, 255, 0);       // green
  line(0, 0, 0, 0, L, 0);  // green axis
  stroke(0, 0, 255);       // blue
  line(0, 0, 0, 0, 0, L);  // blue axis
  fill(0);
}
////////////////////////////////
void boundaryBox(){
  stroke(255);
  noFill();
  box(L*2,L*2,L*2);
}

ところで,
上記コードは複雑です.
「継承」とは何かを知るには,もっと簡単なコードが適切でしょう.
というわけで,シンプルなコードを用意しました(^^):

int N = 100;
Particle[] particle = new Particle[N];
Mover[] mover = new Mover[N];
/////////////////////////////////////////////
void setup(){
  size(500,500);
  colorMode(HSB);
  for(int i = 0; i < N; i++){
    particle[i] = new Particle(random(0,width),random(0,height),
                               random(5,40));
    mover[i] = new Mover(random(0,width),random(0,height),
                         random(5,40),random(255));
  }
}
/////////////////////////////////////////////
void draw(){
  background(170);
  for(Particle p: particle){
    p.display();
  }
  for(Mover m: mover){
    m.display();
    m.update();
  }
}
/////////////////////////////////////////////
class Mover extends Particle{
  float c;
  Mover(float x0, float y0, float d0, float c0){
    super(x0,y0,d0);
    c = c0;
  }
  void display(){
    fill(c,255,255);
    rect(x, y, d, d);
    fill(140,255,255);
    ellipse(x+d*2/3,y+d/2,d/3,d/3);
  }
  void update(){
    if(x > width){ x = 0; }
    x = x + 1;
  }
}
/////////////////////////////////////////////
class Particle{
  float x, y, d;
  Particle(float x0, float y0, float d0){
    x = x0;
    y = y0;
    d = d0;
  }
  void display(){
    fill(255);
    ellipse(x, y, d, d);
  }
}

これの動作の様子は次の通り:

丸くてじっとしているものが,四角い動くものに「進化」(派生)しました(^^).

講義[8]:3次元空間で遊ぶ:力学方程式の実装

前回「3次元空間で遊ぶ:PVectorクラス」の課題1と2に同時に実現した例は次の通りです.
ここで示したコード例は,ある力学方程式の実装例ともなっています.
「Taiyaki」オブジェクトにターゲットを追跡させるための力に応じて各オブジェクトの速度が更新され,位置が更新されます.さらに,各オブジェクト同士が互いに反発しようとする力も速度更新に加味されます.

課題1.「Taiyaki」(Fish)オブジェクトたちが寄り集まってくるようにしください.
課題2.ちかくの「Taiyaki」(Fish)オブジェクトどうしが進行する方向をそろえようとするようにしてください.

※参考:
生物の群れの数理モデルとして古くから研究されている「ボイド」モデルでは,システムを構成する各個体が次の3つのルールに従って運動します.
(1)寄り集まる(cohesion)
(2)進行方向をそろえる(alignment)
(3)互いに反発する(separation)
上記それぞれのルールにおいて各個体が他の個体を認識できる範囲を適切に設定する必要があります.通常は(1)>(2)>(3)の順番のようです.これらの「認識範囲パラメータ」をいろいろな値の組み合わせにすることによって生物の群れにおける様々なふるまいを再現できることが知られています.

///////////////////////////////////////////////////////
// P3D sketch
// using PVector
// targets
// separation
///////////////////////////////////////////////////////
int N = 400;
float L; // simulation area
Fish[] fish;
float bodySize = 20;
PVector vt1 = new PVector(2, 0, 1.7);
PVector vt2 = new PVector(1, 1.3, 0);
Target target1 = new Target(vt1,20,100);
Target target2 = new Target(vt2,20,140);
float MouseX, MouseY;
///////////////////////////////////////////////////////
void setup(){
  size(600,600,P3D);
  L = width/2;  // simulation area: [-L,L][-L,L][-L,L]
  fish = new Fish[N];
  for(int i = 0; i < N; i++){
    fish[i] = new Fish(PVector.random3D(),PVector.random3D(),
                       bodySize);
  }
  MouseX = width/2;
  MouseY = height/2;
}
///////////////////////////////////////////////////////
void draw(){
  lights();
  background(170,160,220);
  viewRotations();
  axis();
  boundaryBox();
  target1.display();
  target2.display();
  target1.move();
  target2.move();
  for(int i = 0; i < N; i++){
    fish[i].display();
    fish[i].seek(target1);
    fish[i].seek(target2);
    fish[i].separate();
    fish[i].move();
  }
}
///////////////////////////////////////////////////////
class Fish{
  PVector r, v, a; //location, velocity, acceleration
  float bs; // body size
  float vmax = 8;
  float fmax = 0.01;
  float eye;
  Fish(PVector r0, PVector v0, float bs0){
    r = r0;
    v = v0;
    bs = bs0;
    a = new PVector(0,0,0);
    r.mult(L * 0.9);
    v.mult(random(1.0, vmax));
  }
  void display(){
    colorMode(HSB);
    pushMatrix();
    stroke(255);
    fill(255,100);
    translate(r.x, r.y, r.z);
    ////
    rotateZ(-atan2(v.x, v.y)); 
    rotateX(-atan2(sqrt(v.x * v.x + v.y * v.y), v.z));
    ////
    box(bs/8, bs/2, bs);   //body
    translate(0,0,bs/4);
    eye = v.mag()/vmax*255;
    noStroke();
    fill(eye,255,255);
    sphereDetail(10);      //important for the speed
    sphere(bs/8+2);        //eye
    popMatrix();
  }
  void move(){ //// update state
    if(r.x > L || r.x < -L){ v.x *= -1; }
    if(r.y > L || r.y < -L){ v.y *= -1; }
    if(r.z > L || r.z < -L){ v.z *= -1; }
    v.add(a);
    v.limit(vmax);
    r.add(v);
    a.mult(0);
  }
  void applyForce(PVector force) {
    a.add(force);
  }
  void seek(Target tg) {
    PVector desired = PVector.sub(tg.getPos(),r);
    desired.normalize(); // unit vector to the target
    desired.mult(vmax);
    PVector steer = PVector.sub(desired,v);
    steer.limit(fmax);
    applyForce(steer);
  }
  void separate(){
    float desiredseparation = bodySize * 2;
    PVector sum = new PVector(0,0,0);
    int count = 0;
    for (int i = 0; i < N; i++) {
      float d = PVector.dist(r, fish[i].r);
      if ((d > 0) && (d < desiredseparation)) {
        PVector diff = PVector.sub(r, fish[i].r);
        diff.normalize();
        diff.div(d);
        sum.add(diff);
        count++;
      }
    }
    if (count > 0) {
      sum.normalize();
      sum.div(count);
      sum.mult(vmax);
      PVector steer = PVector.sub(sum, v);
      steer.limit(fmax*10);
      applyForce(steer);
    }
  }
}
////////////////////////////////
class Target{
  PVector r = new PVector(0,0,0);
  PVector v = new PVector();
  float s, c;;
  Target(PVector v0, float s0, float c0){
    s = s0;
    v = v0;
    c = c0;
  }
  void display(){
    colorMode(HSB);
    pushMatrix();
    translate(r.x, r.y, r.z);
    noFill();
    stroke(c,255,255);
    sphereDetail(10);
    sphere(s);
    popMatrix();
  }
  PVector getPos(){
    return r;
  }
  void move(){
    if(r.x > L || r.x < -L){ v.x *= -1; }
    if(r.y > L || r.y < -L){ v.y *= -1; }
    if(r.z > L || r.z < -L){ v.z *= -1; }
    r.add(v);
  }
}
////////////////////////////////
void viewRotations(){
  float zoom = -width/2;
  if(keyPressed == true){ zoom = 0; }
  translate(width/2, height/2, zoom);
  if(mousePressed == true){
    MouseX = mouseX;
    MouseY = mouseY;
  }
  rotateY(PI - (float)MouseX/width * TWO_PI);
  rotateX(PI + (float)MouseY/width * TWO_PI);
}
////////////////////////////////
void axis(){
  colorMode(RGB);
  stroke(255, 0, 0);       // red
  line(0, 0, 0, L, 0, 0);  // red axis
  stroke(0, 255, 0);       // green
  line(0, 0, 0, 0, L, 0);  // green axis
  stroke(0, 0, 255);       // blue
  line(0, 0, 0, 0, 0, L);  // blue axis
  fill(0);
}
////////////////////////////////
void boundaryBox(){
  stroke(255);
  noFill();
  box(L*2,L*2,L*2);
}