Block Break 〜ボールの反射と球体の描画〜

Block Break(ブロック崩し)に必要なボールの反射と, ブロックを立体的に見せるためのレンダリング(色付け)の説明です。

ボールの反射

ボールの反射 ブロックが普通の長方形だと,ボールは対称的に反射するので面白くありません。 そこでブロックを円(球)にしたときのボールの反射を考えます。 左の図を見てください。ブロック(黒い円)にボールが反射したときのボールの軌跡を描いています。 赤い,矢印のついた線がボールの軌跡,θ は水平軸に対するボールの入射角, θ' は水平軸に対するボールの反射角,原点 (0, 0) はブロックの中心, (x, y) はボールの反射点(ボールとブロックが接した点)の座標, α は (0, 0) と (x, y) のなす角とします。 また,青の線は (0, 0) と (x, y) を通る直線,緑の線は (x, y) における円の接線を表しています。

物理的現象から

それでは,まず物理的な現象から説明します。摩擦などの抵抗はないものとし,反発係数は1とします。
円にボールが当たって反射するのは,平面にボールが当たるのと同じように考えることができます。 ボールがブロックに当たるとき,ブロックとボールは一瞬ただ1点で接した後,すぐに反射します (実際は違いますが,ここでは簡単のため理想的な物体を考えます)。 ブロック,ボール共にごく小さな点のみが当たるので,その領域(接した球面)は平面と考えることができます。 ということは,ブロックとボールの接点における接平面(今は2次元(平面)のブロック崩しを考えているので接線) にボールが反射すると考えることができます。 そうすると後は簡単。青と緑の直線は共にブロックとボールの接点を通り直交します。 したがって,ボールは青線を対称軸にして反射します。 よって,θ' は次式で与えられます。

θ' = - ((θ - π) - α) + α = π + 2α - θ

また,α は次式で与えられます。

α = arctan(y / x)

arctan は tan の逆関数で,-π/2 〜 π/2 を返すものとします。

シミュレーション

次に,実際にシミュレーションする場合にはどうなるかを説明します。
シミュレーションする場合,どうしてもボールを連続的に動かすことはできないので, 時刻 t にブロックの手前にあって, それから Δt 秒後にはブロックの内部に入ってしまう,ということがあります。 よって,ボールがブロックに当たったかどうかを判断するには, ブロックの内部にボールが入ったかどうかを調べればよいことになります。ボールの位置を (i, j) , ブロックの半径を R ,ボールの半径を r とすると, 判定式は次のようになります。

i 2 + j 2 ≦ (R + r)2

これが成り立ったとき,ボールはブロックに当たったと判定します。
次に反射処理ですが,ボールがブロックの内部に入っていては正確な反射ができません。 ボールの位置をブロック表面に戻す必要があります。 ボールの速さを v , その x, y 成分をそれぞれ vxvy とします (v 2 = vx 2 + vy 2)。 ボールがブロックの表面に当たってから現在の位置 (i, j) に来るまでの時間を t , ボールがブロックに当たった瞬間の位置を (i', j') とすると,

t = (R + r - sqr(i 2 + j 2)) / v
i' = i - t * vx
j' = j - t * vy

sqr は平方根(正の値)を返すものとします。
以上をふまえて,ボールの反射アルゴリズム(Java)を以下に示すことにします。


double fBallRadius;             // ボールの半径r
double fBlockRadius;            // ブロックの半径R
double fVelocity;               // ボールの速さv
double fArg;                    // ボールの進行方向(ラジアン)
double fBallVX, fBallVY;        // ボールの速度成分vx,vy(fVelocity,fArgから決まる)
double fBallX, fBallY;          // ボールの中心の座標
double fBlockX, fBlockY;        // ブロックの中心の座標

double x, y;
double r2;                      // ブロックの中心から現在のボールの位置までの距離の2乗
double t;

x = fBallX - fBlockX;
y = fBallY - fBlockY;
r2 = x * x + y * y;
if (r2 < (double)((fBlockRadius + fBallRadius) * (fBlockRadius + fBallRadius))) {
    // ボールをブロックの表面まで戻す.
    t = (fBlockRadius + fBallRadius - Math.sqrt(r2)) / fVelocity;
    fBallX -= t * fBallVX;
    fBallY -= t * fBallVY;
    x = fBallX - x;
    y = fBallY - y;
    // (x, y)は,ブロックの中心から,ブロックとボールの接点(詳しくはボールの中心)へのベクトル.
    fArg = Math.PI + Math.atan2(y, x) * 2 - fArg;
    fBallVX = fVelocity * Math.cos(fArg);
    fBallVY = fVelocity * Math.sin(fArg);
    // ボールをブロックの表面まで戻した分,反射後のボールを先に進める.
    fBallX += t * fBallVX;
    fBallY += t * fBallVY;
}

球体の描画

単に円を塗りつぶしたブロックを描画するのも何なので(何って何だ?), 少し立体感を出すように色を付けます。またアンチエイリアシングも行います。

球体っぽく見せる努力をする

本気でレイトレーシング,ラジオシティなど,3DCGの技術を駆使してレンダリングするのもいいですが, たかがブロック,しかも1個の球体を描画するのにそんなことをするのは明らかに無駄です (大体1個の球体に対して上記のアルゴリズムは使えるのでしょうか・・・)。 簡単に立体感を出すために,単なるグラデーションをつける方法をとります。 球体の表面に平行光線が当たっているとします。 球体の最も明るい点は,球体表面上の点の法線ベクトルと平行光線のベクトル(光線ベクトル) が平行になる点です。ただし,光の当たらない点は除きます (最も明るい点の真裏でも法線ベクトルと光線ベクトルが平行になります)。 その最も明るい点を中心にして,同心円上に同じ明るさの点が並びます。 ということで,最も明るい点からの距離で色を少しずつ変えていけばよいですね (光が当たっていない球体の裏側にも,複雑な環境光の影響により色が見えます)。 RGB(赤,緑,青)ではなくHSB(色相,彩度,明度)を使うと楽にできます。
以下に色付け(レンダリングと言うには貧相すぎる(苦笑))のアルゴリズム(Java)を示します。


int r, g, b;              // それぞれ球体の赤・緑・青成分(0〜255)
int iRadius;              // 球体の半径
int iRightX, iRightY;     // 最も明るい点の座標

double d;                 // 最も明るい点からの距離
int[] image;              // イメージ配列
float[] hsbvals;          // HSBの値
// imageには,0xAARRGGBB(AA:アルファ,RR:赤成分,GG:緑成分,BB:青成分)で格納する

hsbvals = new float[3];
image = new int[iRadius * iRadius * 4];
for (int j = 0; j < iRadius * 2; j++) for (int i = 0; i < iRadius * 2; i++) {
    int x = i - iRadius;
    int y = j - iRadius;
    // (x,y)は,中心を原点としたときの現在処理中のピクセルの座標.
    if (x * x + y * y > (iRadius - 1) * (iRadius - 1)) {
        // アンチエイリアシングのため(iRadius-1)にしてある.
        image[i + j * iRadius * 2] = 0x00FFFFFF;            // 球体の外側は透明(白)
    } else {
        d = Math.sqrt((iRightX - i) * (iRightX - i) + (iRightY - j) * (iRightY - j));
        Color.RGBtoHSB(r, g, b, hsbvals);
        hsbvals[2] -= d / (iRadius * 2) - 0.05;    // hsbvals[2]:明度(0.0〜1.0)
        // この (iRadius*2)-0.05 の部分を変えることによってグラデーション(陰)のつき方が変わる.
        if (hsbvals[2] < 0) hsbvals[2] = 0;
        if (hsbvals[2] > 1) hsbvals[2] = 1;
        image[i + j * iRadius * 2] = 0xFF000000
                | Color.HSBtoRGB(hsbvals[0], hsbvals[1], hsbvals[2]);
    }
}

アンチエイリアシング

上記のコードにも少し書いてありますが,色付けした球体アンチエイリアシング処理を実行します。 アンチエイリアシングとは,図形の境界をなめらかにする処理で,ドット絵のようなギザギザ感が緩和されます (アンチエイリアシングはCG専門用語らしいです。英和辞書には載っていません)。
それでは実際にどのようにするかを説明します。 アンチエイリアシングにもいろいろな方法がありますが, 球体の陰を簡単につけたので,ここでも簡単な方法でギザギザ感を緩和させます。 上記のコードで,色を付けたのは中心から (iRadius - 1) の範囲にある点だけでした。 今度は,(iRadius - 1)iRadius の範囲にある点を色付けします。 ギザギザ感を緩和させるには,隣り合うピクセルの変化が少なくすればよいでしょう。 そのために,現在色を付けようとしているピクセルと, その周りの8ピクセルのR,G,Bをそれぞれ平均したものを現在処理中のピクセルの色とします。
以下にそのアルゴリズム(Java)を示します。


// ここでのimage配列は,先ほどのimage配列と同じ.
int[] image2 = new int[image.length];
System.arraycopy(image, 0, image2, 0, image.length);
for (int j = 0; j < iRadius * 2; j++) for (int i = 0; i < iRadius * 2; i++) {
    int x = i - iRadius;
    int y = j - iRadius;
    if (x * x + y * y <= iRadius * iRadius && x * x + y * y > (iRadius - 1) * (iRadius - 1)) {
        int r = 0, g = 0, b = 0;
        for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) {
            if (i + dx >= 0 && i + dx < iRadius * 2 && j + dy >= 0 && j + dy < iRadius * 2) {
                // image配列からはみ出していなければ,その色を足す.
                r += (image[i + dx + (j + dy) * iRadius * 2] >> 16) & 0xFF;
                g += (image[i + dx + (j + dy) * iRadius * 2] >> 8) & 0xFF;
                b += image[i + dx + (j + dy) * iRadius * 2] & 0xFF;
            } else {
                // 画像の外は白とする.
                r += 255;
                g += 255;
                b += 255;
            }
        }
        // それぞれ平均する.
        r /= 9;
        g /= 9;
        b /= 9;
        image2[i + j * iRadius * 2] = 0xFF000000 | (r << 16) | (g << 8) | b;
    }
}
image = image2;

では実際にどれだけ変化するのか見てみましょう。

何もしない アンチエイリアシング
何もしない アンチエイリアシング
何もしない(拡大) アンチエイリアシング(拡大)

上段は普通のサイズ,下段は一部を拡大した画像です。 アンチエイリアシング処理をした方がぼやけてソフトな感じになります。

Sundry Street ひと休み