🐈

p5.js(processing)でイルミネーションをつくるプログラムを書いた。

2022/12/30に公開

https://openprocessing.org/sketch/1780127

こういうの作りました。ほんとはクリスマスに合わせようと思ったけど、記事書く暇がなかっので...

ほんとはもっとかけるなーとか思っているんだけど、

昔、https://note.com/nobkz/n/ne4fdbd97a162 で書いた記事が褒められていたので、ちょっと書くモチベが出てきたので書きます。

さて、作品に関して、技術的な解説よりも発想的なところからまた書いていこうと思っています。

(注意)途中につたない図解をしていますが、ちゃんと書くとかったるい手抜きです。すみません..

(未完成ですが公開します)

制作の流れ

だいたい流れを図にするとこんな感じ。

なぜ、こういう流れになるかといえば、そもそもなぜ我々が作品を作るのは、現実を模倣のではなく、現実に基づきながら、現実を超えて理想を作ってその過程を体験することが作品作りの本質であるように感じますし、多かれ少なかれこのような流れになるんじゃねー?という感じです。

Input Work

まず一番最初にやるべきなのはこれ。ただ、普通にインプットするわけではない。理解と想像することが非常に重要なのであり、心の中の構築が重要なのですね。

Output Work

これは、作品を作っていく作業です。心の中の構築をどのように実現するか思考して、現実化させていく工程です。

View

まずは観察することが大事です。モノや現象などをいろいろ観察します。現実の写真を見たり、作品を見たりすることが非常に大事です。

WebであればPinterestや、photobash、art stationあたりを私はよく見てますね。

https://www.pinterest.jp/

https://photobash.co/

https://www.artstation.com

また、見たものを写真に撮ったり、一覧かしておくといいでしょう。Pure Refというツールが非常に使いやすくで便利ですね。

https://www.pureref.com/

とりあえず、ただ見るんじゃなくて、正確に想像しながら見るのが大事です。たとえば、縦と横の長さ、どこが丸みを帯びているか?などなど、いろいろなところを見るべきです。また、みる目的を決めておくといいでしょう。

あと、私は大雑把に見たり、細かく見たり、背景を見たりするのもよくやりますね。

Study

その、モノの形や仕組み、歴史を調べる行為です。ひとに話を聞くのもよいでしょう。このところは伝えにくいのですが、非常に大切な行為です。

Image

この作業がなければ、作品なんか作れません。「どんなのがいいかなー?」「何がいいかなー?」「何をつくるのかなー」を考えて、吟味、決心する作業です。

Think

どのように実現するか?どのように構成するか?を考える行為です。

Write

プログラムを書きましょう。ちなみになんとなく書いてみました。

https://openprocessing.org/sketch/1782254

まずは作品の調査しながら想像する

さて、今回はどのようにコードを書いていったか?説明していきましょう。

Input Work

さて、イルミネーションをつくりたいなーと思ったら、まずいろいろ調査しましょう。

調査しながら、イルミネーションって、何のためにやってるんだろう?という背景や、なんでイルミネーションってキレイなんだろうか?という仮説がどんどん立ってきます。(私の場合)

また、ぼんやりとした印象を私は抱くので、それを表現してみたいなーとか考えたり、あと案外、構図が大事だなーとか考えたりいろいろ考えます。

形をかるくスケッチする。

こんな感じかなーとスケッチしています。

すけっちするときは、このぐらいラフに書いています。というか、めんどうな細かいことは、コンピュータが書いてくれるはずです。

実際に構築していく。

さて、コードを書いていきましょう。考えたこととかいろいろ公開していきます。

考えたこと

大きく分けて2点あります。

  • 奥行きどうしようかね?
  • ぼんやり光る感じどうする?

奥行きをどうするか?

さてまず考えることとしては、「奥行き」がある作品なので、初めから、3Dで構築するか?それとも2Dで、奥行き感を出していくか考えました。

個人的に手で書いた感じが欲しかったので、2Dで奥行きを出す方法を使いました。

遠近法

さて、実際に絵画で奥行きはどのように実現されているか?といえば遠近法です。かるく紹介しておくと、消失点を決めて、その方向に形の奥が向かっていくように書いていくことです。

遠近法を実験する。

さて、遠近法をコードで実現してみましょう!

ちょっと軽く書いてみました。こんな感じ。

https://openprocessing.org/sketch/1780619

コードも解説しますか。

消失点を作成する

下記のコードで消失点のコードを作成しました。

let vanishingPoint;
let vanishingPointColor;

function myGreenColor(){
  return color(60, random(100, 255), 120);
}

function setupVanishingPoint() {
  vanishingPointColor = myGreenColor();
  vanishingPoint = createVector(width / 2, height / 2);
}

function updateVanishingPoint() {
  vanishingPoint = createVector(mouseX, mouseY);
}

function drawVanishingPoint(){
  noStroke();
  fill(vanishingPointColor);
  circle(vanishingPoint.x, vanishingPoint.y, 10);
}

消失点もマウスで動かせると楽しいと思うので、こんな感じに動かせるようにしました。
そうそう、Processingもこんな感じで、一つの構成物がちゃんと一つの場所にまとまるようにコードを書くとキレイですよ。作品は綺麗でも、コードも汚いとちょっと残念な気分になるので、こんな感じでスッキリかくと、仕組みも伝わりやすくてよいですよ。

消失点に向く四角形を書く。

四角形を書くコードを解説しますね。一旦全貌を見せておきます。

function quadObj() {
  const x = random(width);
  const y = random(height);
  const h = random(height/30);

  this.pos = createVector(x, y);
  this.underPos = createVector(x, y + h);
  this.deepenSize = random(-1,1);
  this.color = myGreenColor();
}

const objProtoType = {
  updateObj() {
    this.deepenPos = p5.Vector.lerp(this.pos, vanishingPoint, this.deepenSize);
    this.deepenUnderPos = p5.Vector.lerp(
      this.underPos,
      vanishingPoint,
      this.deepenSize
    );
  },
  drawObj() {
    noStroke();
    fill(this.color);
    quad(
      this.pos.x,
      this.pos.y,
      this.underPos.x,
      this.underPos.y,
      this.deepenUnderPos.x,
      this.deepenUnderPos.y,
      this.deepenPos.x,
      this.deepenPos.y,
    );
  },
};

Object.assign(quadObj.prototype, objProtoType);

こんな感じで、やっぱりJavaScriptのプロトタイプベースは書きやすいですね(余談)。わざわざ、クラスを書くまでもないです。

さて、重要な点だけ、ご紹介しておきます。この部分が一番重要です。

  updateObj() {
    this.deepenPos = p5.Vector.lerp(this.pos, vanishingPoint, this.deepenSize);
    this.deepenUnderPos = p5.Vector.lerp(
      this.underPos,
      vanishingPoint,
      this.deepenSize
    );
  },

ここで、そもそもLerpって何?って人も居そうなので解説しておきましょう。以下の図、で、A地点と、B地点の中間のC地点を求めたいとしましょう。

このとき、A地点と、B地点の位置は、こんな感じで設定されてました。ある点OからAまでの距離が50、OからBまでの距離が180です。

この時のCの位置を知りたいのです。

この時の登場するのがLerpです!

適当にコードも書いてみました。

https://editor.p5js.org/tone.nobukazu/sketches/u_Bm3PDdG

先ほどは数値のlerpですが、p5.jsには、p5.Vector.lerpという、Vectorのためのlerpがあり、今回はそれを使っています。

 p5.Vector.lerp(this.pos, vanishingPoint, this.deepenSize);

このコードで消失点までの途中の点を探しているんですね。

さてこれを応用すれば、奥行きがある表現ができるでしょう。

ぼんやり光る表現

ぼんやり光る表現を考えてみましょう。こんな感じの

観察してみると、中心の方が明るくて、遠方に行くと暗くなります。ですので、明るい色から暗い色へのグラデーションが重要になってくると考えました。

実験!

なんとなくシミュレーションしてみましょう。こういうの作りました。

https://openprocessing.org/sketch/1779641

かるく説明しましょう。まず、グラデーションについて考えてみましょう。

図はグラデーションです。そしてグラフがグラデーションの遷移具合を表しています。
そして、そのグラデーションで、光を表現したものも容易しました。

ここで、sizeを動かすと、グラデーションの矩形のサイズが大きくなります。試しに100にすると...

このようになります。サイズが小さいほど、処理すべき計算が増えるので、処理が重くなります。

HSBのHとSをコントロールして色を変化しています。(ここでBrightnessを単純に光の明るさとして計算しているので、コントロールできません。)

二次関数的な変化を楽しんだり、

折れ線グラフで楽しんだりすることができます。

こんな感じで適当にぼわっとした関数がいいかなと思いました。

イルミネーションを書く

さて光を書いていきましょうですがここで問題があります。

一つの光を書くプログラム

まず、ふつうに光を書くプログラムを書いてみましょう。

  colorMode(HSB,100);
  const cx = width/2; // 中心のx座標
  const cy = height/2; // 中心のy座標
  const lightLimit = 80; // 光りが0になる距離
  const drawSize = 200; // 描画サイズ
  const leftX = cx - drawSize/2; // 描画サイズの左端
  const topY = cy - drawSize/2; // 描画サイズの上端
  const rightX = cx + drawSize/2; // 描画サイズの右端
  const bottomY = cy + drawSize/2; // 描画サイズの下端
  for(let x = leftX; x <= rightX ; x++ ){
      for(let y = topY; y <= bottomY; y++ ){
        const d = dist(x,y,cx,cy); // 中心からの距離
        // 中心からの距離から減衰をどうするか記述。
        const lightD = map(d,0,lightLimit,0,100);
        let power;
        if( lightD <= 10 ){
          power = 100;
        }else if( lightD <= 60 ){
          power = map(lightD, 10, 60, 100, 90);
        }else if( lightD <= 80 ){
          power = map(lightD, 60, 80, 90,20);
        }else{
          power = map(lightD, 80, 100, 20,0);
        }
        // 位置と光の強さをもとに描画
        noStroke();
        fill(100, 0, power);
        square(x,y,1);
      }
  }

沢山の光をかく

素朴にたくさんの光を書くと遅い

上記のプログラムにおいて、描画サイズによりますが、サイズが200程度の光でも、200 * 200のループが必要となり、相当計算量が必要だってことが分かるでしょう。

そして、もし光の数が100個必要であるならば、100 * 200 * 200のループが必要となり、大変になります。つまり、drawSize * drawSize * 光の数ということになり、計算が爆発します。

つまり以下のコードは描画するまでに時間がかかりまくります。

function setup() {
  createCanvas(400, 400);
  noLoop();
}

function draw() {
  background(0);
  
  colorMode(HSB,100);
  blendMode(SCREEN);
  
  for(let i=0; i < 100; i++){
    drawLight(random(width), random(height));
  }
}

function drawLight(cx,cy){
  const lightLimit = 20; // 光りが0になる距離
  const drawSize = 100; // 描画サイズ
  const leftX = cx - drawSize/2; // 描画サイズの左端
  const topY = cy - drawSize/2; // 描画サイズの上端
  const rightX = cx + drawSize/2; // 描画サイズの右端
  const bottomY = cy + drawSize/2; // 描画サイズの下端
  for(let x = leftX; x <= rightX ; x++ ){
      for(let y = topY; y <= bottomY; y++ ){
        const d = dist(x,y,cx,cy); // 中心からの距離
        // 中心からの距離から減衰をどうするか記述。
        const lightD = map(d,0,lightLimit,0,100);
        let power;
        if( lightD <= 10 ){
          power = 100;
        }else if( lightD <= 60 ){
          power = map(lightD, 10, 60, 100, 90);
        }else if( lightD <= 80 ){
          power = map(lightD, 60, 80, 90,20);
        }else{
          power = map(lightD, 80, 100, 20,0);
        }
        // 位置と光の強さをもとに描画
        noStroke();
        fill(100, 0, power);
        square(x,y,1);
      }
  }
}

p5.Imageを使う

なので、p5.Imageを用いて、沢山光を描画することを考えてみましょう。そうすると、p5.Imageに一回光を描画するだけでよく、あとはp5.Imageを沢山描画すればよいのです。

function setup() {
  createCanvas(400, 400);
  noLoop();
}

function draw() {
  background(0);
  
  colorMode(HSB,100);
  blendMode(SCREEN);
  
  const img = drawLightImg();


  for(let i=0; i < 100; i++){
    image(img, random(width) - img.width/2, random(height) - img.height/2 );
  }
  
  
}

function drawLightImg(){
  const lightLimit = 20; // 光りが0になる距離
  const drawSize = 100; // 描画サイズ
  const img = createImage(drawSize, drawSize);
  const leftX = 0;
  const rightX = drawSize;
  const topY = 0;
  const bottomY = drawSize;
  const cx = drawSize/2;
  const cy = drawSize/2;
  img.loadPixels();
  for(let x = leftX; x <= rightX ; x++ ){
      for(let y = topY; y <= bottomY; y++ ){
        const d = dist(x,y,cx,cy); // 中心からの距離
        // 中心からの距離から減衰をどうするか記述。
        const lightD = map(d,0,lightLimit,0,100);
        let power;
        if( lightD <= 10 ){
          power = 100;
        }else if( lightD <= 60 ){
          power = map(lightD, 10, 60, 100, 90);
        }else if( lightD <= 80 ){
          power = map(lightD, 60, 80, 90,20);
        }else{
          power = map(lightD, 80, 100, 20,0);
        }
        // 位置と光の強さをもとに描画
        img.set(x,y, color(100,0,power))
      }
  }
  img.updatePixels();
  return img;

もちろん、Imageを描画するためにp5.jsの内部の実装が遅かったりする可能性がありますが、ブラウザのキャンバスのdrawImageを使っていると思いますので、それなりに高速なはずです。少なくとも、いちいち光の距離からの、色の計算をしていない分、高速な筈です。

最終的にはGLSLシェーダを使うのが一番高速になるとは思いますがここでは別の機会にしましょう。

## 作品を完成させよう

さてここまで来たら、後は風景を自由に書いて、適当に光を配置するだけでいいのです。

あと気を付けるべきことや技術的な要素として

  • blendModeを切り替え
  • 描画する順番
  • Graphicsをつかう

という観点に注意しましょう。

Discussion