円塗りつぶし

円塗りつぶし
今回は、塗りつぶされた円を描画していきます。

アンチエイリアス無しの円塗りつぶし
まずは、アンチエイリアス無しでの円塗りつぶしを描画してみます。



画面の中心に、半径 80px の円を描画しています。

#include "sptk.h"

SPTK_IMAGE *img;

void draw_fillcircle()
{
    int ix,iy,x,y,r;
    
    r = 80;
    
    for(iy = 0; iy < 200; iy++)
    {
        for(ix = 0; ix < 200; ix++)
        {
            x = ix - 100;
            y = iy - 100;
            
            if(x * x + y * y < r * r)
                sptk_image_setpixel(img, ix, iy, 0);
        }
    }
}

int main()
{
    sptk_init("test", 200, 200);

    img = sptk_window_get_image();
    
    draw_fillcircle();
    
    sptk_run();

    return 0;
}

まず、(cx, cy) を中心とした半径 r の円があり、その中を塗りつぶしたいとします。

ここでは、描画先の任意の点 (x, y) を基準として考え、その点が円の内部にあるかどうかを判定して、点が円の内部にあれば、その位置に色を描画します。

点 (x, y) が円の内部にあるかどうかを判定するためには、「円の中心からの長さ」を使います。
「円の半径=円の中心からの長さ」なので、「円の中心からの長さが半径の値以下」であれば、その点は円の内部にあると判断できます。

では、実際のコードを見てみましょう。

ix, iy は描画先のイメージの座標です。
まずはこの座標を、円の中心が (0, 0) となるようにします。
今回の場合はイメージの (100, 100) の位置を円の中心とするので、その値を引きます。
そして、その状態の座標で、円の中心からの長さを計算します。

(0, 0) を原点とした時の点 (x, y) の長さは、数学的には、「x * x + y * y の平方根」を求めることで計算できます。
この長さが半径以下なら、円の内側にあると判定できます。

if(sqrt(x * x + y * y) < r) ...

ですが、描画したいすべての点で平方根を求めるのは処理速度的に厳しいので、ここでは、平方根は求めずに、

if(x * x + y * y < r * r) ...

とすることで、点の内部判定を行います。

長さそのものをきっちり求めなくても、ここでは値の範囲内か範囲外であるかを判定できれば良いので、この計算式で十分です。
アンチエイリアス付き
では、上記のコードを踏まえて、次はアンチエイリアス付きで描画してみます。

アルゴリズム的には、キャンバス描画の時にやった「オーバーサンプリング」を使います。
本来、オーバーサンプリングというのは、アンチエイリアスを表現する時に使われます。

オーバーサンプリングについて少し復習すると、1ピクセルを複数のサブピクセルに分解して処理する、というものでしたね。
ここでは、描画先の1ピクセルを分解して、それぞれのサブピクセルで点の内部判定を行い、その平均値を、実際の色の濃度とします。



#include "sptk.h"

#define SUBPIXEL  8

SPTK_IMAGE *img;

void draw_fillcircle_aa()
{
    int ix,iy,jx,jy,cnt,val;
    double dx,dy,r;
    
    r = 80;
    
    for(iy = 0; iy < 200; iy++)
    {
        for(ix = 0; ix < 200; ix++)
        {
            /* オーバーサンプリング */
            
            cnt = 0;
        
            for(jy = 0; jy < SUBPIXEL; jy++)
            {
                for(jx = 0; jx < SUBPIXEL; jx++)
                {
                    dx = ix - 100 + jx * (1.0 / SUBPIXEL);
                    dy = iy - 100 + jy * (1.0 / SUBPIXEL);
                    
                    if(dx * dx + dy * dy < r * r)
                        cnt++;
                }
            }
        
            val = 255 * cnt / (SUBPIXEL * SUBPIXEL);
            val = 255 - val;
        
            sptk_image_setpixel(img, ix, iy, SPTK_RGB(val, val, val));
        }
    }
}

int main()
{
    sptk_init("test", 200, 200);

    img = sptk_window_get_image();
    
    draw_fillcircle_aa();
    
    sptk_run();

    return 0;
}

サブピクセル数は、SUBPIXEL マクロで定義しています。

それぞれのサブピクセルの位置ごとに点の内部判定を行い、cnt で点の数をカウントします。

ピクセルの点の濃度は「cnt / (SUBPIXEL * SUBPIXEL)」となるので、RGB の最大値 255 を掛けて 0〜255 の値にしています。
しかし、この値をそのまま RGB 値にすると、背景が黒で円が白になってしまうので、最終的には値を反転させて、背景を白、円を黒として描画しています。

ここのコードはわかりやすさを重視して書いていますが、浮動小数点の代わりに固定小数点数を使えば、高速化することもできます。
ただし、固定小数点数同士の乗算を行うことになるので、桁溢れには注意してください。
楕円
ついでに、楕円での円塗りつぶしについても説明しておきます。

基本的には今までやったのと同じ方法になるのですが、「円の扁平率」という形で楕円のパラメータを指定することになります。

ここでの扁平率は、円の横・縦方向のいずれかを 1.0 とした場合、もう一方の方向がそれに対してどれだけの長さになるかという、相対的な値です。
1.0 なら正円、0 で潰れた円となります。



縦の扁平率を 0.6 にした場合。

#include "sptk.h"

#define SUBPIXEL  8

SPTK_IMAGE *img;

void draw_fillellipse_aa(double yscale)
{
    int ix,iy,jx,jy,cnt,val;
    double dx,dy,r;
    
    r = 80;
    
    for(iy = 0; iy < 200; iy++)
    {
        for(ix = 0; ix < 200; ix++)
        {
            /* オーバーサンプリング */
            
            cnt = 0;
        
            for(jy = 0; jy < SUBPIXEL; jy++)
            {
                for(jx = 0; jx < SUBPIXEL; jx++)
                {
                    dx = ix - 100 + jx * (1.0 / SUBPIXEL);
                    dy = iy - 100 + jy * (1.0 / SUBPIXEL);
                    
                    dy /= yscale;
                    
                    if(dx * dx + dy * dy < r * r)
                        cnt++;
                }
            }
        
            val = 255 * cnt / (SUBPIXEL * SUBPIXEL);
            val = 255 - val;
        
            sptk_image_setpixel(img, ix, iy, SPTK_RGB(val, val, val));
        }
    }
}

int main()
{
    sptk_init("test", 200, 200);

    img = sptk_window_get_image();
    
    draw_fillellipse_aa(0.6);
    
    sptk_run();

    return 0;
}

ここでは、横方向を 1.0 として、縦方向がそれに対してどれだけの長さになるかを、引数 yscale で指定します。

実質の変更点は、「dy /= yscale;」の部分だけです。

縦方向が横方向より yscale 分潰れている、または伸びている、ということは、逆に考えれば、楕円の縦方向を「1.0 / yscale」倍すれば、正円になるということです。
Y 座標を yscale で割ることで、座標を正円の状態に戻し、それを元に点の内部判定を行うことで、円の内外判定自体はそのまま使うことができます。

ちなみに、回転させた楕円の塗りつぶしを行うこともできますが、先に回転について説明する必要があるので、ここでは省略します。