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);
}
}
これを繰り返し重ね塗りした様子をアニメーションにすると、このように焼き付くような表現になります。
光の三原色に灼かれる悦びを噛み締めよ!🌞#p5js #creativecoding pic.twitter.com/wwuHpSGYAU
— deconbatch (@deconbatch) January 10, 2022
もっと光を!
重ね塗りするときに色相、彩度、明度に変化をつけると光り方の印象が変わります。
※色相、彩度、明度
色相は赤や青などの色味、彩度は色の濃さ鮮やかさ、明度は明るさです。
Processing / p5.js 標準の RGB 形式のカラーモードだと、色相や彩度をそれぞれ個別にコントロールするのは難しいです。こういう場合は HSB 形式を使いましょう。
colorMode(HSB, 360, 100, 100, 100);
一例として、彩度を変えるだけでも光り具合が違ってきます。
こちらは、この特性を利用して、青と青紫のぼぅっとしたやつを横に並べ、青紫側の彩度を変化させていったアニメーション例です。
これはアニメーションではありません。
— deconbatch (@deconbatch) January 10, 2022
今、あなたのモニターが焼きついているところです。😈#p5js #creativecoding pic.twitter.com/lOLcCCgrHw
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);
}
時は来たれり👼#p5js #creativecoding pic.twitter.com/sMJV909RYo
— deconbatch (@deconbatch) January 10, 2022
ADD と SCREEN
blendMode(ADD) と似た効果を出すものに blendMode(SCREEN) があります。
SCREEN は ADD よりマイルドというか、白飛びしにくく、ADD がギラギラ・ビカビカなら 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/>
*/