文系エンジニア悪戦苦闘記

広告代理店の営業を辞め、未経験でフロントエンジニアになった僕の日常です。

Web上で動くスライム的な流体物を実装する

終わっちゃいましたね、GW。

 

僕は2日間外出して残りの3日間家に篭ってたので、

出来心でUnityをいじってました。

 

 

今までなんとなく敬遠してたんですUnity。

 

簡単にそれっぽいものが出来ると聞いていたので、

基礎もできていない僕がそんなものを触ってしまったら

プログラミングの勉強しなくなっちゃうんじゃないかって。

 

あれです。科学忍具的な。

便利すぎてそれに頼ってると強くなれないよ的な。

http://soz.matchupp.com/wp-content/uploads/2016/06/9.jpg

 

でも僕には木の葉の里守る使命とかないんだった

と気づいたので、初めて手を出してみました。

んでこれがもうびっくり。

  

噂通りというか、めちゃ簡単なんですね。

こんな感じの謎のゲームつくって遊んでました。

f:id:kurovetica:20170508021941p:plain

 

科学忍具万歳。

 

 

 

それはさておき、

Unityって物理エンジンの側面が強いんですね。

いじってる間に僕はなんだか"自然的な物"をつくりたくなりました。

 

プログラミングで自然的な物とか有機的な物とかを生み出すのって

魅力的じゃないですか。

ちょっとした神様気分味わえますし。

 

 

有機物の最たるものは何かと言えば誰がなんと言おうが「流体」な訳で。

 

そんなこんなでGW最終日は

流体物を書いてみることにしました。

 

 

 (「待てよ...手軽に流体がwebで実装出来るライブラリとかあったら楽だな...」

とかつくる前に邪なことを考えて「web fluid」とかでググッてたら、

「liquidfun」というjavascriptで利用出来る

2Dの流体演算エンジンを見っけました。

 

http://google.github.io/liquidfun/

 

openFrameworksでもお馴染みのBox2D(C++)が基になって作られてるみたいですね。

emscriptenしてjavascriptでも使えるよー的なものらしいです。

 

でもなんだかなー。

 

動きは確かに流体シミュレーションのそれなんだけど、

パーティクルの集合感がなんとも...。

 

サンプルを改造すればもっといい感じの流体にできるのかもですけど、

コード読み解いてから色々パラメーターいじったりするの結局面倒臭いし、

もっとこう愛でたくなる流体物を欲していたので

やっぱり自分で書くことにしました。)

 

 

せっかくフロントエンジニアという肩書きもいただいてるのでWeb上で軽量に動く

2DのものをProcessing.jsで書くことにしました。

コーディングにあたってはクリエイティブコーディディングのよろず屋こと

田所さんのサイトを参考にしています。本当有難い泣。

http://yoppa.org/

 

本当は、画面全体が流体!って感じのがやりたいのですが、

ベクトル場に10万ほどのパーティクルをぶち込む方法を取るとすると

いくらシェーダーを使えどWebだと限界があるし、

(実際openFrameworksでつくった粒子法の流体シミュレーションをemscriptenして試したのですがfps落ちってレベルじゃねーぞ状態でした泣)、

他にやり方も分からなかったんで、丸っこい流体物をつくります。

スライムのもっとごにょごにょしたやつみたいなの。可愛いの。

液体物言った方が正しいかもです。

 

http://livedoor.blogimg.jp/onecall_dazeee/imgs/1/3/13aa671b.png

 

 

 

ただごにょごにょ形が変わる円だと流体感が出ないので、

パーティクル同士をバネで数珠繋ぎにし、

それを円状にしていきます。

 

まず円の頂点となるparticleクラス。

class particle{
PVector position;
PVector velocity;
PVector force;
float friction;
float radius;
boolean bFixed;
float mass;

particle(){
radius = 5.0;
friction = 0.01;
mass = 1.0;
bFixed = false;
}

void setup(float positionX, float positionY, float velocityX, float velocityY){
position = new PVector(positionX,positionY);
velocity = new PVector(velocityX,velocityY);
}
void resetForce(){
force = new PVector(0,0);
}

void addForce(float forceX, float forceY){
force.x += forceX;
force.y += forceY;
}
void updateForce(){
force.x -= velocity.x * friction;
force.y -= velocity.y * friction;

}

void updatePos(){
if(!bFixed){
velocity.x += force.x;
velocity.y += force.y;
position.x += velocity.x;
position.y += velocity.y;
}
}
void update(){
updateForce();
updatePos();
}

void bounceOffWalls(){
boolean bDampedOnCollision = false;
boolean bDidICollide = false;

float minx = 0;
float miny = 0;
float maxx = width;
float maxy = height;

if (position.x > maxx){
position.x = maxx;
velocity.x *= -1;
bDidICollide = true;
} else if (position.x < minx){
position.x = minx;
velocity.x *= -1;
bDidICollide = true;
}

if (position.y > maxy){
position.y = maxy;
velocity.y *= -1;
bDidICollide = true;
} else if (position.y < miny){
position.y = miny;
velocity.y *= -1;
bDidICollide = true;
}

if (bDidICollide == true && bDampedOnCollision == true){
velocity.x *= 0.3;
velocity.y *= 0.3;
}
}
void draw(){
fill(255);
noStroke();
ellipse(position.x,position.y,radius,radius);
}
void addRepulsionForce(float x, float y, float radius, float scale){
PVector posOfForce;
posOfForce = new PVector(x,y);
PVector diff = new PVector(position.x - posOfForce.x, position.y - posOfForce.y);
float length = dist(position.x,position.y,posOfForce.x,posOfForce.y);
boolean bAmCloseEnough = true;
if (radius > 0){
if (length > radius){
bAmCloseEnough = false;
}
}
if (bAmCloseEnough == true){
float pct = 1 - (length / radius);
diff.normalize();
force.x = force.x + diff.x * scale * pct;
force.y = force.y + diff.y * scale * pct;
}
}
void addRepulsionForce(particle p, float radius, float scale){
p = new particle();
PVector posOfForce;
posOfForce = new PVector(p.position.x, p.position.y);
PVector diff = new PVector(position.x - posOfForce.x, position.y - posOfForce.y);
float length = dist(position.x,position.y,posOfForce.x,posOfForce.y);
boolean bAmCloseEnough = true;
if (radius > 0){
if (length > radius){
bAmCloseEnough = false;
}
}
if (bAmCloseEnough == true){
float pct = 1 - (length / radius);
diff.normalize();
force.x = force.x + diff.x * scale * pct;
force.y = force.y + diff.y * scale * pct;
p.force.x = p.force.x - diff.x * scale * pct;
p.force.y = p.force.y - diff.y * scale * pct;
}
}

void addAttractionForce(float x, float y, float radius, float scale){
PVector posOfForce;
posOfForce = new PVector(x,y);
PVector diff = new PVector(position.x - posOfForce.x, position.y - posOfForce.y);
float length = dist(position.x,position.y,posOfForce.x,posOfForce.y);
boolean bAmCloseEnough = true;
if (radius > 0){
if (length > radius){
bAmCloseEnough = false;
}
}
if (bAmCloseEnough == true){
float pct = 1 - (length / radius);
diff.normalize();
force.x = force.x - diff.x * scale * pct;
force.y = force.y - diff.y * scale * pct;
}
}

}

 

PVectorでポジションやら速度やら摩擦やら基本的な部分を定義して、

さらにパーティクル同士が反発しあう斥力を書いてやります。

 

これをしないとバネで繋いだ時にぺちゃんこになっちゃうのです。

 

続いてパーティクル同士をつなぐspringクラス。

 

class spring{
particle particleA = new particle();
particle particleB = new particle();


float distance;
float springiness;

spring(){
particleA = null;
particleB = null;
}

void update(){

PVector pta = new PVector(particleA.position.x, particleA.position.y);
PVector ptb = new PVector(particleB.position.x, particleB.position.y);

float thirDistance = dist(pta.x,pta.y,ptb.x,ptb.y);
float springForce = (springiness * (distance - thirDistance));
PVector frcToAddVec = new PVector(pta.x-ptb.x, pta.y-ptb.y);
frcToAddVec.normalize();
PVector frcToAdd = new PVector(frcToAddVec.x * springForce,frcToAddVec.y * springForce);


particleA.addForce(frcToAdd.x,frcToAdd.y);
particleB.addForce(-frcToAdd.x,-frcToAdd.y);
}

void draw(){

noFill();
stroke(255);

line(particleA.position.x,particleA.position.y,particleB.position.x,particleB.position.y);
}
}


particle myParticle = new particle[100];
spring mySpring = new spring[100];

void setup(){


size(innerWidth,innerHeight);

for(int i = 0; i<100; i++){
myParticle[i] = new particle();
myParticle[i].friction = 0.04;
myParticle[i].radius = 2;
float x = width/2 + 100 * cos ( (i / 200.0) * TWO_PI);
float y = height/2 + 100 * sin ( (i / 200.0) * TWO_PI);
myParticle[i].setup(x,y,0,0);
}

for (int i = 0; i < 100; i++){
mySpring[i] = new spring();
mySpring[i].distance = 0;
mySpring[i].springiness = 0.6;
mySpring[i].particleA = myParticle[i];
mySpring[i].particleB = myParticle[(i+1)0];
}
}

 

バネの両端を取得するためのparticleクラスのインスタンスparticleAとparticleBとを用意して、

それぞれをつなぐバネをつくっていきます。

springForceという変数んとこがバネの式です。

 

バネの伸び(このコード上ではparticleAからparticleBまでの距離)がxのとき、

それによって生じる力をFとすると、

F= -kx

という式になります。

kがバネの定数です。

 

フックの法則です。懐かしいですね。

 

 

んでメインの関数はこんな感じです。

 

particle myParticle = new particle[100];
spring mySpring = new spring[100];

void setup(){


size(innerWidth,innerHeight);

for(int i = 0; i<100; i++){
myParticle[i] = new particle();
myParticle[i].friction = 0.04;
myParticle[i].radius = 2;
float x = width/2 + 100 * cos ( (i / 200.0) * TWO_PI);
float y = height/2 + 100 * sin ( (i / 200.0) * TWO_PI);
myParticle[i].setup(x,y,0,0);
}

for (int i = 0; i < 100; i++){
mySpring[i] = new spring();
mySpring[i].distance = 0;
mySpring[i].springiness = 0.6;
mySpring[i].particleA = myParticle[i];
mySpring[i].particleB = myParticle[(i+1)0];
}
}

void draw(){
fill(200,200,255);
rect(0,0,width,height);
for (int i = 0; i < 100; i++){
myParticle[i].resetForce();
}


for (int i = 0; i < 100; i++){
if(mousePressed){

myParticle[i].addAttractionForce(mouseX, mouseY, 200, 4.0);
} else {

myParticle[i].addRepulsionForce(mouseX, mouseY, 200, 4.0);
}

for (int j = 0; j < 100; j++){
myParticle[i].addRepulsionForce(myParticle[j].position.x,myParticle[j].position.y, 100, 1.0);
}
}


for (int i = 0; i < 100; i++){
mySpring[i].update();
}


for (int i = 0; i < 100; i++){
myParticle[i].updateForce();
myParticle[i].update();
myParticle[i].bounceOffWalls();
}

fill(170,170,255);
noStroke();
beginShape();
for(int i = 0; i<100;i++){
vertex(myParticle[i].position.x,myParticle[i].position.y);
}
endShape();

}

 particleクラスとspringクラスを100個用意してそれを円状に繋いでいます。

 

ちょっと変わってるのが描画の方法です。

 

普通にparticleとspringのdraw()を呼び出すのではなく、

頂点となっているparticleに囲まれた図形を

beginShape()とendShape()で塗りつぶしています。

 

 

 

これで流体物改めスライムができました。

完成したものがこちらです。

マウスで撫でてやったりドラッグしてやったりしてください。

 

http://eikikurokawa.com/fluid/fluidWeb01/index.html

 

うん、可愛いですね。

 

さっき定数きたバネ定数(springiness)など、

ここで各種パラメータをいじるといろんな動きを見せてくれます。

 

こういう流体的なポヨポヨの動き、

意外と応用が効きそうな気がしてるのは僕だけでしょうか。

そうでもないかな。

 

それはともかく有機物ちっくなものつくるの、楽しいですね。

こういうのどんどん生み出して魔王になりたいです。

 

 

ーーーおまけーーー

 

今回のコーディングしてるうちに偶然できた、

キモ美しいジェネラティブアートです。

マウスに絶妙に反応します。

 

http://eikikurokawa.com/gene/gene01/index.html