アンチエイリアス付きの自由線描画(4)・ブラシ画像

ブラシ画像
前回までは、円塗りつぶしを直接描画することで、一つの点を描画していましたが、もっと自由な形の点を描画したいと思う場合もあるでしょう。
自由な形状の線を描きたい場合は、形状が描かれた画像を読み込んで、それを点として描画すれば、実現できます。

実際には、円の代わりに、そこに拡大縮小させた画像を貼り付けることになります。

今回は、ブラシ画像を使って、自由な形状の線が描けるようにします。
ブラシ画像の用意
まず、形状の元となるブラシ画像を用意します。
今回は、白色が濃度0、黒色が濃度最大として、グレイスケールで濃淡を表現することにします。
また、画像は正方形であるものとして、幅と高さは同じサイズにします。
正方形であるなら、サイズは自由な大きさで構いません。

また、画像を拡大縮小して貼り付けることになるのですから、画像のサイズは、そのまま描画品質につながります。
画像サイズが小さすぎると、大きな点を描画する際に、ドットが拡大されたような状態になってしまいます。
大きな点でも品質を保つようにしたい場合は、なるべく大きな画像にしておく必要があります。

今回は、300x300 の星形の形状を使うことにします。

>> brush1.bmp
スクリーンショット&ソースファイル


半径 8.0、間隔 2.0 で描画しています。
ちゃんと点が星形になっていますね。

>> 028_aaline4.c.txt
解説
今回、実質的に変更された部分は、点の描画部分だけです。
円を描画する代わりに、ブラシ画像から描画濃度を得ています。

void drawpoint(double x,double y,double radius)
{
    int nx1,ny1,nx2,ny2,ix,iy,jx,jy,val,bx,by;
    double xx,yy,scale,brushhalf;
    SPTK_PIX_RGBA col,*pbrush;
    
    /* 描画先の範囲 */
    
    nx1 = (int)floor(x - radius);
    ny1 = (int)floor(y - radius);
    nx2 = (int)floor(x + radius);
    ny2 = (int)floor(y + radius);
    
    /* 描画先ループ */
    
    brushhalf = brushimg->w / 2.0;
    scale = brushhalf / radius;
    
    for(iy = ny1; iy <= ny2; iy++)
    {
        for(ix = nx1; ix <= nx2; ix++)
        {
            /* オーバーサンプリング */
            
            val = 0;
            
            for(jy = 0; jy < 8; jy++)
            {
                yy = iy - y + jy * (1.0 / 8);
            
                for(jx = 0; jx < 8; jx++)
                {
                    xx = ix - x + jx * (1.0 / 8);
                    
                    bx = (int)(xx * scale + brushhalf);
                    by = (int)(yy * scale + brushhalf);
                    
                    if(bx >= 0 && bx < brushimg->w && by >= 0 && by < brushimg->h)
                    {
                        pbrush = brushimg->pixbuf + by * brushimg->w + bx;
                        
                        val += 255 - pbrush->r;
                    }
                }
            }
            
            val >>= 6;
            
            /* 結果 */
            
            col = drawcol;
            col.a = drawcol.a * val / 255;
            
            sptk_image32_setpixel_rgba(layerimg, ix, iy, &col);
        }
    }
}

ここでは、描画する点の大きさに合わせて拡大縮小したブラシ画像の濃度を、実際の描画濃度に適用します。
拡大縮小のアルゴリズムには、「オーバーサンプリング」を使っています。

では、まずは処理前のブラシ画像の方を基準にして考えてみます。
ブラシ画像を、描画する点の大きさに拡大縮小し、点の中心である (x, y) の位置に、ブラシ画像の中央を原点として貼り付けるわけですから、その逆の処理を行って、描画位置からブラシ画像の位置を逆算します。
あとは、それを各サブピクセルの座標ごとに行って、濃度の平均値を取れば、そのピクセルの描画濃度を求めることができます。

では、実際のコードを見てみましょう。
ループ前の処理
まず、ループ前に以下の値を求めています。

brushhalf = brushimg->w / 2.0;
scale = brushhalf / radius;

brushhalf は、ブラシ画像のサイズの半分、つまりブラシ画像における半径幅です。
この値はループ内でも使用するので、ここで求めています。

scale は、描画先の座標をブラシ画像の座標に変換する際の、倍率です。

ここでは、ブラシ画像を、描画する点の大きさに拡大縮小するので、
ブラシ画像を元にして考えると、倍率は「点のサイズ / 画像のサイズ」という値になります。
しかし、点のサイズは半径で表されているので、画像のサイズもそれに合わせて、幅・高さの半分を半径とします。
(画像は正方形であると仮定されているので、ここでは幅の値を使っています)

また、実際の処理内では、描画先の座標を元に逆算していくことになるので、ここで欲しいのは逆算時の倍率です。
なので、描画先の座標に掛ける scale の値は、「画像の半径 / 点の半径」ということで、「brushhalf / radius」となります。
オーバーサンプリング部分
オーバーサンプリング部分では、以下のコードによって、現在のピクセルの描画濃度を求めています。

val = 0;

for(jy = 0; jy < 8; jy++)
{
    yy = iy - y + jy * (1.0 / 8);

    for(jx = 0; jx < 8; jx++)
    {
        xx = ix - x + jx * (1.0 / 8);
        
        bx = (int)(xx * scale + brushhalf);
        by = (int)(yy * scale + brushhalf);
        
        if(bx >= 0 && bx < brushimg->w && by >= 0 && by < brushimg->h)
        {
            pbrush = brushimg->pixbuf + by * brushimg->w + bx;
            
            val += 255 - pbrush->r;
        }
    }
}

val >>= 6;

xx, yy は、各サブピクセル位置における、描画先の座標です。

まず、描画先の座標をブラシ画像の座標に拡大縮小するため、scale を掛けています。
拡大縮小は、ブラシ画像の中央を基準として行うため、現在はブラシ画像の中央が原点 (0, 0) となっています。
それを、左上を (0, 0) とした画像座標にするため、画像の半径分 brushhalf を加算しています。

これで、ブラシ画像の座標が求められたので、あとはその位置が範囲内かどうかを判定して、範囲内であれば、その位置の色を濃度として加算します。
ここでは、画像は R = G = B のグレイスケールであるものとして、R の値を使います。
なお、白色は濃度0、黒色は濃度最大とするので、R = 255 の場合は濃度はです。
ですから、最大値 255 から R 値を引いて、濃度の値に直します。

※ここでは、ブラシ画像を RGBA イメージとして読み込んでいますが、ブラシ画像としては濃度のみがあれば良いので、実際に実装する場合は、ブラシ画像を 8bit イメージなどに変換して、あらかじめ濃度のみを抽出したものを用意しておくと良いです。

そして最後に、値の平均値を出すため、「8x8=64」で割って (ここでは >> 6)、0〜255 の範囲の濃度値を得ます。

あとは、その濃度値を、実際の描画濃度に適用すれば OK です。
まとめ
描画の品質は、サブピクセル数によって変化させることができます。
半径が小さい場合は、サブピクセル数が多いほうが良いですし、半径が大きい場合は、サブピクセル数を減らして描画速度を上げた方が良いです。

また、半径を大きくする場合は、ブラシ画像も大きくしなければ、品質を保てません。
ブラシ画像を使う場合、円塗りつぶしなどのように直接図形を描画する場合に比べると、ブラシ画像の大きさで品質が左右されるのは、一つの欠点とも言えます。

半径を大きくして描画したい場合は、その点だけ注意しておいてください。