プログラミングの備忘録

プログラムをつくる過程を残すもの

processingの備忘録 -反射-

f:id:taq2777:20220203202330p:plain こんにちは。
今回はprocessingに物体の反射を導入する方法についてです。

軸に垂直(平行)な向きの壁での反射とそれ以外の向きの壁での反射の2種類について書いてみます。
以降の説明では、簡単のために物体を円形とします。


軸に垂直(平行)な壁での反射

垂直な壁での反射は簡単、壁がある場所で速度の一方の成分の向きを逆にすれば良いだけです。

例えば壁がウィンドウの端であるとしたとき、円を動かすコードは、

float x, y, vx, vy, d;

void setup(){
    size(500, 500);
    
    x = width/2;
    y = height/2;
    vx = random(1, 3);
    vy = random(1, 3);
    d = 50;
}

void draw(){
    background(255);
    
    ellipse(x, y, d, d);
    
    x += vx;
    y += vy;
}

draw()内に以下の処理を追加することで、ウィンドウの端を壁として反射させることができます。

if(x < d/2){ //左の端
    x = d/2;
    vx *= -1;
}
if(x > width-d/2){ //右の端
    x = width-d/2;
    vx *= -1;
}
if(y < d/2){ //上の端
    y = d/2;
    vy *= -1;
}
if(y > height-d/2){ //下の端
    y = height-d/2;
    vy *= -1;
}

if文の処理によって位置の調整と向きの変更を行っています。
位置の調整をしないとめり込んだまま出てこなくなることがあります。


せっかくベクトルを扱い始めたので、ベクトルで書いたものも載せておきます。

PVector position, velocity;
float v, d;

void setup(){
    size(500, 500);
    
    position = new PVector(width/2, height/2);
    velocity = PVector.random2D();
    velocity.mult(5);
    d = 50;
}

void draw(){
    background(255);
    
    ellipse(position.x, position.y, d, d);
    
    position.add(velocity);
    
    if(position.x < d/2){
        position.x = d/2;
        velocity.x *= -1;
    }
    if(position.x > width-d/2){
        position.x = width-d/2;
        velocity.x *= -1;
    }
    if(position.y < d/2){
        position.y = d/2;
        velocity.y *= -1;
    }
    if(position.y > height-d/2){
        position.y = height-d/2;
        velocity.y *= -1;
    }
}


軸に垂直(平行)でない壁での反射

前回ベクトルの基礎をまとめましたが、そこで書いていたやってみたいことというのがこれです。
基本はレファレンスにあったものに沿っていますが、咀嚼した後に書いているので一部表現が違うところもあります。


まずはウィンドウの端を壁とした円を動かすコードから。
(面倒だったのでpositionはpos、velocityはvelと省略しています。)

PVector pos, vel;
float v, d;

void setup(){
    size(500, 500);
    
    pos = new PVector(width/2, height/4);
    vel = PVector.random2D();
    v = 5;
    vel.mult(v);
    d = 50;
}

void draw(){
    background(255);
    
    ellipse(pos.x, pos.y, d, d);
    
    pos.add(vel);
    
    if(pos.x < d/2){
        pos.x = d/2;
        vel.x *= -1;
    }
    if(pos.x > width-d/2){
        pos.x = width-d/2;
        vel.x *= -1;
    }
    if(pos.y < d/2){
        pos.y = d/2;
        vel.y *= -1;
    }
    if(pos.y > height-d/2){
        pos.y = height-d/2;
        vel.y *= -1;
    }
}


続いて、壁をつくります。(位置的には壁というより床ですが)

PVector pos, vel;
float v, d;
PVector g_left, g_right; //壁の左端、右端それぞれの位置を指定するベクトル

void setup(){
    size(500, 500);
    noStroke();
    
    pos = new PVector(width/2, height/4);
    vel = PVector.random2D();
    v = 5;
    vel.mult(v);
    d = 50;

    makeGround(); //関数の呼び出し
}

void draw(){
    background(0);
    
    fill(100);
    quad(g_left.x, g_left.y, 
         g_left.x, height, 
         g_right.x, height, 
         g_right.x, g_right.y); //壁の描画
    
    fill(255);
    ellipse(pos.x, pos.y, d, d);
    
    pos.add(vel);
    
    if(pos.x < d/2){
        pos.x = d/2;
        vel.x *= -1;
    }
    if(pos.x > width-d/2){
        pos.x = width-d/2;
        vel.x *= -1;
    }
    if(pos.y < d/2){
        pos.y = d/2;
        vel.y *= -1;
    }
    if(pos.y > height-d/2){
        pos.y = height-d/2;
        vel.y *= -1;
    }
}

void makeGround(){ //壁をつくる関数
    g_left = new PVector(0, random(height/2, height));
    g_right = new PVector(width, random(height/2, height));
}

後々の処理の関係上、関数としてまとめた方が楽なのでそうします。

実行してみると下側に四角形が書かれます。これを壁として反射の処理を追加していきます。

f:id:taq2777:20220203184154p:plain

反射の処理の中身としては、壁の表面にある点と円との間の距離が半径よりも小さくなったら、速度のベクトルの向きを反射後のものに変えるという感じです。

PVector pos, vel;
float v, d;
PVector g_left, g_right, ground;
float l;
PVector[] surface;

void setup(){
    size(500, 500);
    noStroke();
    
    pos = new PVector(width/2, height/4);
    vel = PVector.random2D();
    v = 5;
    vel.mult(v);
    d = 50;
    
    makeGround();
}

void draw(){
    background(0);
    
    fill(100);
    quad(g_left.x, g_left.y, g_left.x, height, g_right.x, height, g_right.x, g_right.y);
    
    fill(255);
    ellipse(pos.x, pos.y, d, d);
    
    pos.add(vel);
    
    for(int i = 0; i < surface.length; i ++){ //壁の表面の各点について
        if(PVector.dist(pos, surface[i]) < d/2){ //距離が円の半径より小さくなったら
            float angle = PI/2-PVector.angleBetween(vel, ground) + ground.heading() - (PI/2-vel.heading()); //反射後の速度ベクトルの角度を計算
            vel.y *= -1; //y成分の向きを逆に
            vel.rotate(angle); //計算した角度だけ回転させる
        }
    }
    
    if(pos.x < d/2){
        pos.x = d/2;
        vel.x *= -1;
    }
    if(pos.x > width-d/2){
        pos.x = width-d/2;
        vel.x *= -1;
    }
    if(pos.y < d/2){
        pos.y = d/2;
        vel.y *= -1;
    }
    if(pos.y > height-d/2){
        pos.y = height-d/2;
        vel.y *= -1;
    }
}

void makeGround(){
    g_left = new PVector(0, random(height/2, height));
    g_right = new PVector(width, random(height/2, height));
    ground = PVector.sub(g_right, g_left); //壁と平行なベクトル
    
    float g_length = ground.mag(); //壁の長さ
    surface = new PVector[ceil(g_length)];
    for(int i = 0; i < surface.length; i ++){ //壁の表面の各点について
        surface[i] = new PVector();
        //点の座標を指定
        surface[i].x = g_left.x + ground.x*i/g_length;
        surface[i].y = g_left.y + ground.y*i/g_length;
    }
}

反射後の速度ベクトルは、角度を計算してその角度の分だけ速度ベクトルを回転させるという形で求めています。
ここでの「角度」は垂直な壁での反射ベクトルと斜めの壁での反射ベクトルとのなす角度です。
少しややこしいですが図を描いて各値との対応を考えれば理解できると思います。


追記(2022/02/05)
さすがにこれだけだとわかりづらいなと感じたので図を加えておきます。
f:id:taq2777:20220205143824p:plain f:id:taq2777:20220205144021p:plain f:id:taq2777:20220205144432p:plain 垂直な壁での反射ベクトルは入射ベクトルのy成分を-1倍すれば求められるので、これを求めた角度の分だけ回転させれば斜めの壁での反射ベクトルが求められるというわけです。
以上


レファレンスはレファレンスでややこしい計算をして求めていますが、これでも理解できれば数学力は上がるかもしれませんが自分ではよく分からなかったので今回のような求め方を考えました。

最後のmakeGround()では、壁の長さを求め、その単位長さごとにベクトルsurface[i]を割り当て、各ベクトルの座標を指定する、という形で壁の表面に点をおいています。
ceil()は引数の小数点以下を切り上げる関数で、配列の数は整数で指定するので壁の長さを整数にするために使っています。


これを実行してみると、無事斜めの壁での反射を導入できました。

f:id:taq2777:20220203200004p:plain


これでも十分動きますが、位置の調整が無いのでたまに壁にめり込みます。
一応、調整するための処理も考えてみましたが、ここはまだ改善の余地がありそうです。

PVector a = PVector.sub(pos, surface[i]);
float b = d/2-a.mag();
a.setMag(b);
pos.add(a);


加えて、反射ベクトルを計算するfor文にprint(i);を加えてみると、一回の衝突でたくさんのiとの計算がされているようなので、ここを1点のみにするために処理の最後にreturn;を加えた方が良いのだろうかと思ったりしました。


まとめ

ここまでで、壁での反射を導入する方法について書いてきました。
これを応用すればより複雑な面をもった壁でも反射をさせることができるので、やれることが増えると思います。
ベクトルの扱い方にもなんとなく慣れてきたのではないでしょうか。

今回はここまで。また次回。


参考