top of page

Processing 球に影をつける

執筆者の写真: NUMNUM

こんにちは。NUMです。


最近は立体的な作品作りにはまっています。

Processingには元々3D表現が簡単に実装できる機能が備わっていますが

使い方がよく分からないので今回は別の方法で実装します(笑)。


絵の立体感は様々な手法で表現できます。

  1. 陰影をつける

  2. 近くにある物は色が濃く、遠くにある物は色が薄い

  3. 近くにある物は大きく、遠くにある物は小さい


今回は1の手法で球に立体感を出していこうと思います。


陰影の観察

図1を観察してみると左上が白く、右下に行くほど暗くなっているように見えます。

(厳密には地面からの光の反射がありますが今回は考慮しないです。)


これを手順化すると以下のようになると思います。

  1. 円の範囲内で一番明るい点を決める

  2. 1からの距離が近いほど白く、遠いほど黒くする

  3. 2で設定した色を使用して円の範囲内に点を描画していく


手順3で点描を選択した理由は、点で描くと自然な仕上がりになります。

点の集まりが線、線を均等に並べれば面になるので、点は形の最小要素と言えます。

僕はProcessingで絵を描くときに全てのモチーフを点で描いています。

図1



実装

以下が先ほどの手順をプログラムにしたものになります。

分かりにくいのでプログラムを分割して解説していきます。


//光源を指定して球の陰影をつける
//球の中心座標(x,y)、光源(lightSourceX,Y)、半径(r)、球の色(c)
void shadowBall(float x, float y, float lightSourceX, float lightSourceY, float r, color c) {

  //影の色
  color shadowc = color(0);

  //変数.lightAngleはsincosListの添字を計算している
  float lightAngle = degrees(atan2(lightSourceY-y, lightSourceX-x));
  
  //181°以降は-π~0を180~360に変換する必要があるため
  //lightAngle変数に360°を加算して調整している
  if (lightAngle<0) {
    lightAngle = 360+lightAngle;
  }
  
  //lightSourceX,Yから一番近い座標を計算
  float lightPointX = x+(r*sincosList.get(0)[int(lightAngle)]);
  float lightPointY = y+(r*sincosList.get(1)[int(lightAngle)]);

  for (int i = 0; i<30000; i++) {
    int index;
    if (i >= 360) {
      index = i%360;
    } else {
      index = i;
    }
    //描画する点の位置の計算
    float randomNum = random(1);
    float cosx = x+((r*randomNum)*sincosList.get(0)[index]);
    float siny = y+((r*randomNum)*sincosList.get(1)[index]);
    
    //一番明るい点と描画する点の距離を計算して距離に応じてグラデーションをかける
    float d = dist(cosx, siny, lightPointX, lightPointY);
    float mapd = map(d, 0, 2*r, -0.3, 1.2);
    color l_c = lerpColor(c, shadowc, mapd);
    
    //描画
    strokeWeight(random(1,5));
    stroke(l_c);
    point(cosx,siny);
  }
}
  

shadowBall関数ではcos関数とsin関数の計算を大量に行います。

cos関数とsin関数は0~360°以降は同じ値を返すので(多少誤差はありますが)

配列に計算結果を格納しておき、必要に応じて値を取り出せば処理速度が速くなります。

以下がプログラムになります。


//sin関数とcos関数の計算結果をListに格納する
ArrayList<float[]> createSinCosList() {
  
  //List、配列の宣言
  ArrayList<float[]> sincosList = new ArrayList<float[]>();
  float[] cosArr = new float[360];
  float[] sinArr = new float[360];
  
  //0~359°までのcos,sinの計算結果を配列に格納
  for (int i = 0; i<360; i++) {
    cosArr[i] = cos(radians(i));
    sinArr[i] = sin(radians(i));
  }
  //Listにcos配列とsin配列を格納
  sincosList.add(cosArr);
  sincosList.add(sinArr);
  return sincosList;
}
  

createSinCosList関数の使い方を説明します。

変数.indexはsincosListに格納されているcos配列、sin配列の添字となります。

配列の添字は要素数が360なので0~359までとなっています。


カウンタ変数.iが360より小さい時は変数.indexにそのまま代入します。

360以上の場合、iを360で割った余りを使用します。

これでiがどれだけ大きくなっても計算結果を再利用できます。


void setup() {
  ArrayList<float[]> sincosList = createSinCosList();
  for (int i = 0; i<1080; i++) {
    int index;
    if (i >= 360) {
      index = i%360;
    } else {
      index = i;
    }
    float cosx = sincosList.get(0)[index];
    float siny = sincosList.get(0)[index];
  }
}

それではshadowBall関数について説明します。


以下の処理は描画する球の一番明るい点を求めています。

今回の記事で一番重要な処理です。


円の範囲内で、光源(引数.lightSoruceX,Y)から一番近い点(lightPointX,Y)の位置を計算します。(イメージは図2です。)


lightPointはlightSourceから一直線に線を引いて、円と被った部分です。

lightPointの位置は少し面倒な計算をしないといけないです。


atan2関数は二点からx軸を起点としてラジアンで返します。

(第一引数が,y座標なのは仕様です。)

戻り値の範囲は0~180°が0~π、180~360°が -π~0です。


円の中心座標(引数.x,y)と光源(引数.lightSoruceX,Y)からラジアンを取得します。


この時、戻り値はラジアンなのでsincosListの計算値を使用するにはラジアンを角度に変換する必要があり、degrees関数を使用しています。 これはsincosListの配列の添字は角度と一致しているためです。


atan2関数の戻り値が0~πの時はdegrees関数の戻り値が0~180°なので問題ないですが

180°以降になるとatan2関数の戻り値が-π~0になってしまいます。

これではdegrees関数の戻り値が-180~0°になってしまい、配列の要素にアクセスする際に

エラーが出てしまいます。


そのためdegrees関数の戻り値が0°より小さい時に360を加算しています。

こうする事で配列の要素数の帳尻を合わせます。

  
  //変数.lightAngleはsincosListの添字を計算している
  float lightAngle = degrees(atan2(lightSourceY-y, lightSourceX-x));
  
  //181°以降は-π~0を180~360に変換する必要があるため
  //lightAngle変数に360°を加算して調整している
  if (lightAngle<0) {
    lightAngle = 360+lightAngle;
  }
  
  //lightSourceX,Yから一番近い座標を計算
  float lightPointX = x+(r*sincosList.get(0)[int(lightAngle)]);
  float lightPointY = y+(r*sincosList.get(1)[int(lightAngle)]);
  

図2



以下処理は円の範囲内に点の描画をひたすら行います。

その際に点の色を変数.lightPointの距離に応じてlerpColor関数でグラデーションをかけます。そうする事で陰影ができ立体感ができます。


円の範囲内に点を打つ方法は変数.randomNumの値が重要です。

変数.randomNumの値は0~1の値がランダムで代入されます。

以下の式で半径(変数.r)に0~1の値が掛けられるとどうなるでしょう?

1の時に半径が最大、0で最小となります。つまり必ず円の範囲内に点が描画さrます。

    float randomNum = random(1);
    float cosx = x+((r*randomNum)*sincosList.get(0)[index]);
    float siny = y+((r*randomNum)*sincosList.get(1)[index]);

グラデーションの処理は最初に変数.lightPointと描画点(cosx,siny)の距離をdist関数で求めます。

dist関数の戻り値が0に近い場合は-0.3、2×rに近い場合は1.2にmap関数で変換します。

(-0.3~1.2は僕が一番立体的だと感じた変化の値です。)


map関数の戻り値はlerpColor関数で使用され、引数.cから変数.shadowcの色へとグラデーションがかかります。


あとは描画です。strokeWeight関数は1~5を引数としていますが、好みに応じて変えても良いです。値が大きいならfor文の変数.iの最大値を小さくしても良いかもしれないです。

(点描のサイズが大きいとそれだけ被るので計算効率が無駄になるためです。)

  
  for (int i = 0; i<30000; i++) {
    int index;
    if (i >= 360) {
      index = i%360;
    } else {
      index = i;
    }
    //描画する点の位置の計算
    float randomNum = random(1);
    float cosx = x+((r*randomNum)*sincosList.get(0)[index]);
    float siny = y+((r*randomNum)*sincosList.get(1)[index]);
    
    //一番明るい点と描画する点の距離を計算して距離に応じてグラデーションをかける
    float d = dist(cosx, siny, lightPointX, lightPointY);
    float mapd = map(d, 0, 2*r, -0.3, 1.2);
    color l_c = lerpColor(c, shadowc, mapd);
    
    //点描画
    strokeWeight(random(1,5));
    stroke(l_c);
    point(cosx,siny);
  }
}


shadowBall関数の説明は以上です。

個人的な感想としてdist関数とlerpColor関数の相性は抜群だと思っています。

Processingで作品を作るならこの二つの使い方を知っていれば色んなバリエーションの作品ができると思うぐらいです。


以下の作品はshadowBall関数を使って作った作品です。

空気遠近法を背景に使って空間に奥行きを持たせ、地面を砂漠のような色使いをしました。

こうする事で砂漠の広大さ、球のちっぽけさの対比を表現しました。

ちなみにこの球はフンコロガシの糞をカラフルにイメージしています。笑


shadowBall関数の光源は(0,0)に設定しているのがわかると思います。

右下に行くにつれて暗くしているのも個人的に好きな表現です。



以上が陰影をプログラム化して球に影をつける方法でした。

僕はまだ球しか陰影をつけられないので他にも挑戦していきたいです!

最後まで読んでいただきありがとうございます。

コメント


bottom of page