そもそも光を発してるモニタ上で、あえて「光ってる」ように見せるには?

2022年1月16日日曜日

p5.js Processing テクニック 初心者向け

t f B! P L

Processing や p5.js で、まるで光っているように見せる「発光効果」を出すにはコツがあります。

Processing や p5.js で「発光効果」

そのコツを、モニタは始めから光ってるというそもそも論から始め、コードを交えて具体的にやさしく説明していきます。説明のコードは p5.js で進め、 Processing で作った作例コードも最後に掲載します。

👉 Read this article in English.

 

 

モニタって、そもそも光ってるんだけど…

普段クリエイティブ・コーディングをするとき、コンピュータのモニタ(ディスプレイとも)上に描画していると思います。モニタは自ら光を発して、文字なり形なりを表示しています。つまり、クリエイティブ・コーディングで描画した作品は、どれもそもそも光っています。


モニタ上の色は、赤・緑・青の光の三原色から作られます。

赤 (Red)、緑 (Green)、青 (Blue) = RGB
https://ja.wikipedia.org/wiki/RGB

光の三原色

モニタは、この赤・緑・青色の光を放ってるわけですが、「発光してる!」って感じは受けないですよね。

 

「光」の見せ方

じゃあ光ってるように見せるにはどうしたらいいのか?

漫画やイラスト的に放射や照りを描くとか、いろいろ見せ方はありますが、Processing や p5.js で簡単にできる方法の一つとして、 blendMode(ADD) を使う方法があります

blendMode(ADD) はその名の通り、重ね塗りの際に光の「足し算」を行います。

例えばこのコードで赤、緑、青の順に円を描くとこうなります。


const w = 640;
const h = w;

function setup() {
    createCanvas(w, h);
    colorMode(HSB, 360, 100, 100, 100);
    noLoop();

    background(0, 0, 0, 100);
    noStroke();

    // Red
    fill(0, 90, 30, 100);
    circle(w * 0.4, h * 0.4, w * 0.5);
    // Green
    fill(120, 90, 30, 100);
    circle(w * 0.6, h * 0.4, w * 0.5);
    // Blue
    fill(240, 90, 30, 100);
    circle(w * 0.5, h * 0.6, w * 0.5);
}

赤、緑、青の順に円を描いた図

普通の円の重なりですね。

ここで描画の前に blendMode(ADD) を置くと、加算が行われて、先程の光の三原色の図になります。


    background(0, 0, 0, 100);
    noStroke();

    blendMode(ADD);
    // Red

光の三原色

赤+緑=黄色、赤+緑+青=白 という具合です。

blendMode(ADD) を使った上で、次のコードのように明度暗めで重ね塗りをすると、「光ってるな!」という感じを出せます。暗い原色の円を半径を変えて複数重ね塗りしていて、このように「ぼぅっ」と光ってる感じが出ます。


const w = 640;
const h = w;

function setup() {
    createCanvas(w, h);
    colorMode(HSB, 360, 100, 100, 100);
    
    noLoop();
    frameRate(15);

    background(0, 0, 0, 100);
    noStroke();

    blendMode(ADD);
    for (let r = 0.0; r < 0.5; r += 0.01) {
	// Red
	fill(0, 90, 5, 100);
	circle(w * 0.4, h * 0.4, w * r);
	// Green
	fill(120, 90, 5, 100);
	circle(w * 0.6, h * 0.4, w * r);
	// Blue
	fill(240, 90, 5, 100);
	circle(w * 0.5, h * 0.6, w * r);
    }
}

「光ってるな!」という効果

これを繰り返し重ね塗りした様子をアニメーションにすると、このように焼き付くような表現になります。

 

もっと光を!

重ね塗りするときに色相、彩度、明度に変化をつけると光り方の印象が変わります。

※色相、彩度、明度
色相は赤や青などの色味、彩度は色の濃さ鮮やかさ、明度は明るさです。
Processing / p5.js 標準の RGB 形式のカラーモードだと、色相や彩度をそれぞれ個別にコントロールするのは難しいです。こういう場合は HSB 形式を使いましょう。
colorMode(HSB, 360, 100, 100, 100);

一例として、彩度を変えるだけでも光り具合が違ってきます。

彩度90での発光効果
彩度60での発光効果
彩度30での発光効果

こちらは、この特性を利用して、青と青紫のぼぅっとしたやつを横に並べ、青紫側の彩度を変化させていったアニメーション例です。

 

100 + 0 = 100 に注意

blendMode(ADD) は足し算なので、黒、つまり明度=0 で塗りつぶすことはできません。正確には明度=0 で塗りつぶしてはいるんですけど、ゼロを足しても結果は変わらないので、暗くなりません。

※もともとの明るさが 100 だとしたら、そこで blendMode(ADD) で明るさゼロで塗りつぶしても 100 + 0 = 100 で元のままの明るさです。

もし、アニメーションとかで毎フレーム background() で塗りつぶしたいときなどは、blendMode(BLEND) に切り替えてから塗りつぶす必要があります。私はこれを忘れて、画面真っ白にしちゃうことがちょいちょいあります。

先程の彩度の変化と、blendMode の切り替えの合わせ技での作例を載せます。このコードで、blendMode(BLEND) を無くすとどうなるか実験してみると、効果がわかると思います。


const w = 640;
const h = w;

function setup() {
    createCanvas(w, h);
    colorMode(HSB, 360, 100, 100, 100);
}

function draw() {

    let frmRatio = map(frameCount % 120, 0, 120, 1.0, 0.0);

    blendMode(BLEND);
    background(240, 100, 30, 100);
    noStroke();

    // sun
    blendMode(ADD);
    for (let r = 0.0; r < 1.0; r += 0.01) {
	fill(280, frmRatio * 100, (1.0 - r) * 5, 100);
	circle(w * 0.6, h * 0.5, w * r);
    }

    // planet
    blendMode(BLEND);
    fill(240, 100, frmRatio * 80, 100);
    circle(w * 0.5, h, w * 0.8);

}

 

ADD と SCREEN

blendMode(ADD) と似た効果を出すものに blendMode(SCREEN) があります。

SCREEN は ADD よりマイルドというか、白飛びしにくく、ADD がギラギラ・ビカビカなら SCREEN はボンヤリ光る感じです。

blendMode(ADD)での発光効果
blendMode(SCREEN)での発光効果

 

「光ってる」ように見せるポイントのまとめ

発光のポイント

  • blendMode(ADD) を使う
  • 明度暗めで重ね塗りする
  • 黒の background() 等で塗りつぶしたいときは blendMode(BLEND) に切り替えるのを忘れずに

  • 発光モノ制作時工夫のポイント

  • 色を変えながら重ね塗りしてみる
  • blendMode(SCREEN) にしてみる
  • 重なり具合、色、彩度、明度の変化具合の調整
  •  

     

    "p5.js" での作例コード

    blendMode(SCREEN)を使って、ぼぅっと光るアニメーション作例を p5.js (JavaScript) で作成しました。どんな動きになるのか、ぜひ動かしてみてください。タイトルは「街の灯」です。

    コードは GPL で公開します。ライセンス条項にのっとってご自由にお使いください。

    
    // City Lights
    
    const w = 720;
    const h = 480;
    const blobNum = 20;
    
    function setup() {
        createCanvas(w, h);
        colorMode(HSB, 360, 100, 100, 100);
        frameRate(15);
    }
    
    function draw() {
    
        let frmRatio = map(frameCount % 120, 0, 120, 0, 1);
    
        blendMode(BLEND);
        background(240, 100, 20, 100);
    
        for (let i = 0; i < blobNum; i++) {
    	let bTime = sin(PI * ((frmRatio + noise(10, i)) % 1));
    	let bHue  = (360 * frmRatio + noise(20, i) * 240) % 360;
    	blob(i / blobNum, noise(30, i), bTime, bHue);
        }
    
    }
    
    function blob(_x, _y, _t, _hue) {
    
        blendMode(SCREEN);
        noStroke();
        for (let r = 0.0; r < 0.2; r += 0.002) {
    	fill(_hue, 100, r * 3, 100);
    	circle(_x * w, _y * h, w * r * 0.5);
    	fill(_hue, _t * 100, (1.0 - r) * 3, 100);
    	circle(_x * w, _y * h, w * r);
        }
    
    }
    
    /*
    Copyright (C) 2022- deconbatch
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>
    */
    
    
    

     

    "Processing" での作例コード


    こちらはぼぅっと光るというより、キラキラ光る作例です。コードは GPL で公開します。ライセンス条項にのっとってご自由にお使いください。

    
    /**
     * Light Years.
     * simple animation using Node-Garden technique.
     *
     * @author @deconbatch
     * @version 0.1
     * @license GPL Version 3 http://www.gnu.org/licenses/
     * Processing 3.5.3
     * 2022.01.15
     */
    
    void setup() {
      size(720, 480, P2D);
      colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
      smooth();
      noLoop();
    }
    
    void draw() {
    
      int frmRate  = 30;
      int cycleSec = 8;
      int cycleMax = 3;
      int nodeNum  = 15;
      float orbitBase = min(width, height) * 0.25;
      float rangeS    = orbitBase * 0.2;
      float rangeL    = orbitBase * 0.4;
      float hueBase   = random(360.0);
    
      // nodes clusters
      ArrayList<Cluster> clusters = getClusters(nodeNum, orbitBase);
      int clusterMax = clusters.size();
    
      // nodes
      ArrayList<Node> nodes = getNodes(clusterMax * nodeNum, orbitBase);
      int nodeMax = nodes.size();
    
      // easing functions
      ArrayList<Ease> easing = new ArrayList<Ease>();
      easing.add(new Four());
      easing.add(new Quadratic());
      easing.add(new Cos());
      easing.add(new Pow());
      easing.add(new Cubic());
      int easeMax = easing.size();
    
      int frmMax = frmRate * cycleSec * cycleMax;
      int frmCycleMax = frmRate * cycleSec;
    
      for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
    
        int   cycleCnt = frmCnt / frmCycleMax;
        float frmRatio = map(frmCnt % frmCycleMax, 0, frmCycleMax, 0.0, 1.0);
          
        // animate calculation
        for (int clusterCnt = 0; clusterCnt < clusterMax; clusterCnt++) {
          float waveRatio = easing.get((cycleCnt + clusterCnt) % easeMax).ease(frmRatio);
          Cluster cluster = clusters.get(clusterCnt);
          for (int idx = cluster.nodeFrom; idx < cluster.nodeTo; idx++) {
            nodes.get(idx).animate(waveRatio, frmRatio, cluster.x, cluster.y);
          }
        }
      
        blendMode(BLEND);
        background(0.0, 0.0, 0.0, 100.0);
          
        // draw
        blendMode(ADD);
        for (int clusterCnt = 0; clusterCnt < clusterMax; clusterCnt++) {
    
          float clusterRatio = map(clusterCnt, 0, clusterMax, 0.0, 1.0);
          float rangeWave = abs(sin(PI * (frmRatio + clusterRatio)));
    
          // lines between clusters
          Cluster cluster = clusters.get(clusterCnt);
          stroke(cluster.hueVal % 360.0, 90.0, 30.0, 100.0 * rangeWave);
          for (Cluster c : clusters) {
            float d = dist(cluster.x, cluster.y, c.x, c.y);
            strokeWeight(d / orbitBase);
            if (d < orbitBase * 1.5) {
              line(cluster.x, cluster.y, c.x, c.y);
            }
          }
    
          for (int i = cluster.nodeFrom; i < cluster.nodeTo; i++) {
            Node n = nodes.get(i);
    
            // nodes
            noStroke();
            fill(cluster.hueVal % 360.0, 80.0, 70.0, 100.0);
            ellipse(n.x, n.y, 3.0, 3.0);
    
            // lines between nodes
            for (int j = i + 1; j < nodeMax; j++) {
              Node m = nodes.get(j);
              float d = dist(n.x, n.y, m.x, m.y);
              if (d < rangeL * rangeWave && d > rangeS * rangeWave) {
                stroke(cluster.hueVal % 360.0, 80.0, 40.0, 100.0);
                strokeWeight(2);
                line(n.x, n.y, m.x, m.y);
    
                noStroke();
                fill((cluster.hueVal + 300.0) % 360.0, 40.0, 10.0, 100.0);
                ellipse(n.x, n.y, 8.0, 8.0);
                fill((cluster.hueVal + 30.0 + 60.0 * (d - rangeS) / (rangeL - rangeS)) % 360.0, 40.0, 3.0, 100.0);
                ellipse(m.x, m.y, 15.0, 15.0);
              }
            }
          }
        }
        saveFrame("frames/" + String.format("%04d", frmCnt) + ".png");
      }
      exit();
    }
    
    
    /**
     * getClusters : returns whole clusters.
     */
    ArrayList<Cluster> getClusters(int _nodeNum, float _radius) {
      int   tryMax  = 100;
      float spacing = _radius * 0.75;
      float hueBase = random(360.0);
      ArrayList<Cluster> clusters = new ArrayList<Cluster>();
    
      // circle packing
      int cnt   = 0;
      for (int i = 0; i < tryMax; i++) {
        int x = floor(random(spacing, width) - spacing * 0.5);
        int y = floor(random(spacing, height) - spacing * 0.5);
    
        boolean hit = false;
        for (Cluster c : clusters) {
          float d = dist(x, y, c.x, c.y);
          if (d < spacing) {
            hit = true;
            break;
          }
        }
    
        if (!hit) {
          clusters.add(new Cluster(cnt++, _nodeNum, x, y, hueBase + cnt * 90.0));
        }
      }
      
      return clusters;
    }
    
    
    /**
     * getNodes : returns whole nodes.
     */
    ArrayList<Node> getNodes(int _cnt, float _radius) {
      ArrayList<Node> nodes = new ArrayList<Node>();
      for (int i = 0; i < _cnt; i++) {
        float r = random(_radius);
        float t = random(TWO_PI);
    		nodes.add(new Node(
                           r,
                           t
                           ));    
      }
      return nodes;
    }
    
    
    /**
     * Node : hold node.
     */
    public class Node {
    
      public  float x, y;   // coordinate of node
      private float r, t;   // radius and theta to calculate the x, y
      private float oR, oT; // original radius and theta
      private float tPhase; // random phase of theta
    
      Node(float _oR, float _oT) {
        oR = _oR;
        oT = _oT;
        tPhase = random(PI);
      }
    
      public void animate(float _rRatio, float _tRatio, float _oX, float _oY) {
        r = oR * abs(sin(TWO_PI * _rRatio + tPhase));
        t = oT + TWO_PI * ((_tRatio + sin(tPhase)) % 1.0);
        x = _oX + r * cos(t);
        y = _oY + r * sin(t);
      }
    
    }
    
    /**
     * Cluster : hold cluster.
     */
    public class Cluster {
    
      public int   nodeFrom, nodeTo; // number of nodes belong to the cluster
      public float x, y;   // coordinate of node
      public float hueVal; // hue value of node
    
      Cluster(int _no, int _nodeNum, float _x, float _y, float _hue) {
        nodeFrom = _no * _nodeNum;
        nodeTo = nodeFrom + _nodeNum - 1;
        x = _x;
        y = _y;
        hueVal = _hue;
      }
    }
    
    /**
     * Ease : hold easing functions.
     */
    public interface Ease {
      public float ease(float _t);
    }
    
    public class Cos implements Ease {
      public float ease(float _t) {
        return 1.0 - cos(HALF_PI * _t);
      }
    }
    
    public class Quadratic implements Ease {
      public float ease(float _t) {
        _t *= 2.0;
        if (_t < 1.0) {
          return pow(_t, 2) / 2.0;
        }
        _t -= 1.0;
        return -(_t * (_t - 2) - 1.0) / 2.0;
      }
    }
    
    public class Cubic implements Ease {
      public float ease(float _t) {
        _t *= 2.0;
        if (_t < 1.0) {
          return pow(_t, 3) / 2.0;
        }
        _t -= 2.0;
        return (pow(_t, 3) + 2.0) / 2.0;
      }
    }
    
    public class Four implements Ease {
      public float ease(float _t) {
        return 1.0 - pow(1.0 - _t, 4);
      }
    }
    
    public class Pow implements Ease {
      public float ease(float _t) {
        return pow(_t, 2);
      }
    }
    
    /*
    Copyright (C) 2022- deconbatch
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>
    */
    
    
    

     

    QooQ