以前制作した油絵の質感表現をだいぶ使いこなせるようになってきました。
油絵の流体のようなタッチを描いてみたい時にFlowFieldが思いつきました。
FlowFieldの仕組みを勉強して使いこなしていこうと思います。
今回の記事、ソースはダニエル・シフマン先生のFlowFieldの動画を参考としています。
いつも有益な情報をありがとうございます!
目次
参考文献
FlowFieldとは
FlowFieldとは格子状に区切られた任意の位置にベクトル(PVector)を設定し、
オブジェクト(Particle)の位置、移動方向に対して影響を与えるアルゴリズムです。
以下のキャプチャだと白い点(Particle)が格子状に設定されたベクトルの影響を受けながら移動しています。FlowFieldは流れ場と呼ばれます。
概要
FlowFieldの概要としては以下3点となります。
任意の位置にParticleオブジェクトを生成
格子状にベクトルを配置する
2のベクトルを1のParticleオブジェクトのベクトル対して加算する
noise関数について
実装に入る前にnoise関数について少し確認していきます。
FlowFieldはnoise関数で取得できる値を使用して流体のような形を生成します。
noise関数:引数に応じて0~1の範囲で値を返します。
noise(x,y,z);//x,y,zは1~3次元ノイズに対応する
引数が同じ値の場合、返す値は同じとなります。
noise関数の引数に2を指定してコンソール出力すると値は全て0.58916605となっています。
noise関数の特徴を視覚化したのが図1です。
図1
vertex関数の引数にx座標を0~widthまで増加させて、y座標にnoise関数を使用しました。
y座標の変化がなめらかですね。
変数.tは時間を表し、時間の変化(インクリメントの値)が微小なほど、戻り値の変化が
小さいので、なめらかに見えます。
図1の赤文字のようにtが50ずつ増加していく場合、y軸変化量も大きいため
時間の変化が大きいとなめらかでは無くなります。
(x座標で切り取る断面のようなイメージ)
//図1:
void setup() {
float t = 0;//時間
float inc = 0.01;//インクリメント
size(500, 500);
background(240);
pixelDensity(2);
//vertex関数で線を作成
noFill();
beginShape();
for (int i = 0; i<width; i++) {
vertex(i, 100+200*noise(t));
t+=inc;
}
endShape();
}
noise関数をrandom関数に変更してみると形がジグザグしています。
random関数は引数に指定した範囲内の値をランダムで返す(今回だと0~200の範囲)ので
戻り値の前後に関連はなく、y座標の変化はなめらかではないです。
vertex(i, 100+random(200));
図2
noise関数についてもっと詳しく知りたい方はダニエル・シフマン先生の
パーリンノイズチュートリアル を見るとより仕組みがわかると思います。
次にFlowFieldのベースとなる、noise関数の戻り値を角度に変換し、
ベクトルを生成する方法について記載します。
概要.2はnoise関数の戻り値をベースとしたベクトルを格子状に配置しています。図3は線の角度が流れのように変化しています。
この流れを表すベクトルがFlowFieldで重要なポイントとなってきます。
Particleオブジェクトにこのベクトルの影響を与えられれば良いというわけですね。
図3
ソースコードの概要は以下となります。
2次元noiseの戻り値は0~1の範囲なので、0~2πにmap関数で変換し、fromAngle関数で角度に対応する単位ベクトルを取得。
単位ベクトルを任意の値で乗算し、グリッドの位置(pos)に加算したベクトル(pos2)を取得します。
line関数でposとpos2を指定して描画するという処理になっています。
//図3:
void setup() {
int scl = 25;//グリッドの分割数(分割数までfor文を実施する)
int w = width/scl;//四角形の横幅
int h = height/scl;//四角形の縦幅
size(850, 850);
pixelDensity(2);
background(255);
float ystart = 0;//noise関数の第2引数で使用
for (int y = 0; y<scl; y++) {
float xstart = 0;//noise関数の第1引数で使用
for (int x = 0; x<scl; x++) {
//2次元noiseの値を取得
float n = noise(xstart, ystart);
//noiseの値0~1を0~2πの範囲に変換
float angle = map(n, 0, 1, 0, TWO_PI);
//noiseの影響を受けた角度からベクトルを生成して20倍
PVector p = PVector.fromAngle(angle);
p.mult(20);
//グリッドの位置ベクトル
PVector pos = new PVector(x*w, y*h);
//グリッドの位置ベクトルをコピーしてpを加算
PVector pos2 = pos.copy();
pos2.add(p);
//グリッドを可視化
fill(255);
stroke(0);
strokeWeight(1);
rect(pos.x, pos.y, w, h);
//流れを可視化
strokeWeight(2);
line(pos.x, pos.y, pos2.x, pos2.y);
xstart+=0.1;
}
ystart+=0.1;
}
}
noiseの値を角度に変換し、ベクトルを生成する手順が分かったらFlowFieldの実装に入っていきます。
実装
それでは概要に記載した内容を元にFlowFieldの実装を行なっていきます。
Particleクラス:
public class Particle {
PVector pos;//位置
PVector vel;//速度
boolean finished = false;//停止フラグ
//コンストラクタ
Particle(float x, float y) {
pos = new PVector(x, y);
vel = new PVector(0, 0);//最初の速度は0
}
//位置に速度を加算
void update() {
pos.add(vel);
}
//点を描画
void show() {
stroke(0);
strokeWeight(5);
point(pos.x, pos.y);
}
//位置がキャンバスの端に到達したら停止フラグをtrueにする
void edges() {
if (pos.x < 0 || pos.x > width-1 || pos.y < 0 || pos.y > height-1) {
this.finished = true;
}
}
//FlowFieldオブジェクトからベクトルを取得し、自身の速度に設定する
void follow(FlowField flowfield) {
int x = floor(pos.x / flowfield.scl);
int y = floor(pos.y / flowfield.scl);
int index = x + y * flowfield.cols;
this.vel = flowfield.vectors[index];
}
}
Particleクラスで一番理解に苦しんだのがfollow関数でした。
なぜ変数.index = x+y×列数という計算式になるかが理解できませんでした。
FlowFieldクラスの実装内容を理解してからの方が頭に入ってきやすいです。
follow関数はParticleオブジェクトの位置がグリッドのどの位置に存在しているかを計算します。計算結果(配列の添字)に対応する配列要素を取得して、Particleオブジェクトの速度に設定します。
この処理をする理由としてはキャンバスのwidth,heightは配列の添字に対応していないので、変数.resでwidth,heightを割ってあげて、Particleオブジェクトの位置をスケールダウンしてあげる必要があるからです。
FlowFieldクラス:
public class FlowField {
PVector[] vectors;//ベクトルを格納する配列
int cols, rows;//列数,行数
float inc = 0.01;//インクリメント
int scl;//キャンバスをグリッドにするための分割値
//コンストラクタ
FlowField(int res) {
scl = res;
cols = floor(width / res) + 1;//widthをresで割った商が列数となる
rows = floor(height / res) + 1;//heightをresで割った商が行数となる
vectors = new PVector[cols * rows];//要素数(行×列)を設定し配列を作成
}
//vectors配列にベクトルを設定
void update() {
float yoff = 0;
for (int y = 0; y < rows; y++) {
float xoff = 0;
for (int x = 0; x < cols; x++) {
//2次元noiseを取得して、取得結果を-1~1の範囲から0~2πに変換
float n = (float) noise.eval(xoff, yoff);
float angle = map(n, -1, 1, 0, TWO_PI);
//変換した角度から単位ベクトルを取得
PVector v = PVector.fromAngle(angle);
//2次元の配列の値から1次元の配列の添字を計算する
int index = x + y * cols;
vectors[index] = v;
xoff += 0.01;
}
yoff += 0.01;
}
}
}
FlowFieldクラスは一言で説明すると、Particleクラスの動きに影響を与えるベクトルを、グリッド状に設定するクラスです。
図3を例にすると、キャンバスが10×10のグリッドに分割されている場合
1マスずつベクトルが配置されていて、Particleオブジェクトが任意のマスに移動した瞬間に
配置されていたベクトルがParticleオブジェクトの速度に加算され、別のマスに移動する
ような感じです。
図4
Processingでグリッドを作りたいならfor分をネストさせると扱いやすいです。
図4のグリッドでは10×10マスなので、以下のソースのとおり一つ目のfor文はy座標、二つ目のfor文はx座標を表します。
図4を例にすると以下の処理の順番になります。
y=0の時、V1-1 ~ V1-10まで処理を実行
y=1の時、V2-1 ~ V2-10まで処理を実行
yが10まで処理を繰り返す
for(int y = 1;y<11;y++){
for(int x = 1;x<11;x++){
println("V" + y + "-" + x);
}
}
FlowFieldクラスのupdate関数ではグリッド状にベクトルを配列に設定していく関数となります。
update関数の以下のソースはネストしたfor文から1次元配列の添字を計算する処理となります。なぜこのようなことが計算式になるのかを説明していきます。
int index = x + y * cols;
要素数が100の1次元配列の場合、添字と要素は以下となります。 添字:0,1,2,3,4,5,6,7,8,9,10.......99 要素:1,2,3,4,5,6,7,8,9,10.......100
for文をネストして変数.i,jで2次元配列にすると、添字は以下のようになります。
最初の添字を確認すると0,10,20,30,40,50....となっており列の数の倍数(cols)となる
{0,1,2,3,4,5,6,7,8,9}
{10,11,12,13,14,15,16,17,18,19}
{20,21,22,23,24,25,26,27,28,29}
{30,31,32,33,34,35,36,37,38,39}
{40,41,42,43,44,45,46,47,48,49} {50,51,52,53,54,55,56,57,58,59}
,,,,,,,,99}
この法則を一般化すると「列の添字+行の添字×列の数」は2次元配列の添字を1次元配列の添字に変換できるようになります。
noise関数の値を角度に変換し、ベクトルを生成する手順は図3のソースコード で
記載したので省略します。
update関数で使用しているnoise関数はOpenSimplexNoiseのeval関数を使用しています。
Processingのデフォルトのnoise関数で実装しても問題ないです。
(僕的にはeval関数の方が形が好きでした笑)
OpenSimplexNoiseについて詳しく知りたい方はシフマン先生の動画を参照してください。
ソースはこちらからダウンロードできます。
Main:
//変数宣言
FlowField flowfield;
ArrayList<Particle> particles;
//OpenSimplexNoiseオブジェクト生成
OpenSimplexNoise noise = new OpenSimplexNoise();
void setup() {
//いつもの
size(850, 850);
background(255);
pixelDensity(2);
smooth();
int res = 3;//分割値
flowfield = new FlowField(res);//FlowFieldオブジェクト生成
flowfield.update();//ベクトル配列の更新
//Particleオブジェクトの位置をランダムに設定し、リストに格納
particles = new ArrayList<Particle>();
for (int i = 0; i < 100; i++) {
particles.add(new Particle(random(width), random(height)));
}
}
//全てのParticleオブジェクトの各処理を呼び出す
void draw() {
for (Particle p : particles) {
p.edges();
//停止フラグがfalseのみparticleオブジェクトを移動させる
if (!p.finished) {
p.follow(flowfield);
p.update();
p.show();
}
}
}
Mainでは以下の処理を行なっています。
FlowFieldオブジェクト生成、ベクトル配列を更新
リストにParticleオブジェクトを格納
リストに格納されているParticleオブジェクトに対してベクトル配列のベクトルを加算させて移動、描画
まとめ
以上がFlowFieldについて学んだ内容です。
自分の頭をこねくり回して勉強したので、間違っている点があると思います。
何かご指摘あればコメントで記載していただけると幸いです🙇
FlowFieldは少し値を変えるだけでも様々な形を作ることができます。
またnoise関数の値を使用しているので、形の変化がとてもなめらかで
見ていても気持ち良いです。
こちらは油絵ソースコードを使用してFlowFieldで形を生成してみました。
自分がやりたかった油絵の流体のような表現できました。
青色の曲線を主に使用し、赤と黄色を少し使用することで色が引き立ちます。
色の使い方次第で印象が全く違ってきます。
液体にも気体にも見える表現は自然を描く時にとても役立ちそうだなと思いました。
最後まで記事を読んでいただきありがとうございました!
参考文献
Comments