ブラシ画像
前回までは、円塗りつぶしを直接描画することで、一つの点を描画していましたが、もっと自由な形の点を描画したいと思う場合もあるでしょう。
自由な形状の線を描きたい場合は、形状が描かれた画像を読み込んで、それを点として描画すれば、実現できます。
実際には、円の代わりに、そこに拡大縮小させた画像を貼り付けることになります。
今回は、ブラシ画像を使って、自由な形状の線が描けるようにします。
自由な形状の線を描きたい場合は、形状が描かれた画像を読み込んで、それを点として描画すれば、実現できます。
実際には、円の代わりに、そこに拡大縮小させた画像を貼り付けることになります。
今回は、ブラシ画像を使って、自由な形状の線が描けるようにします。
ブラシ画像の用意
まず、形状の元となるブラシ画像を用意します。
今回は、白色が濃度0、黒色が濃度最大として、グレイスケールで濃淡を表現することにします。
また、画像は正方形であるものとして、幅と高さは同じサイズにします。
正方形であるなら、サイズは自由な大きさで構いません。
また、画像を拡大縮小して貼り付けることになるのですから、画像のサイズは、そのまま描画品質につながります。
画像サイズが小さすぎると、大きな点を描画する際に、ドットが拡大されたような状態になってしまいます。
大きな点でも品質を保つようにしたい場合は、なるべく大きな画像にしておく必要があります。
今回は、300x300 の星形の形状を使うことにします。
使用画像: brush1.bmp
今回は、白色が濃度0、黒色が濃度最大として、グレイスケールで濃淡を表現することにします。
また、画像は正方形であるものとして、幅と高さは同じサイズにします。
正方形であるなら、サイズは自由な大きさで構いません。
また、画像を拡大縮小して貼り付けることになるのですから、画像のサイズは、そのまま描画品質につながります。
画像サイズが小さすぎると、大きな点を描画する際に、ドットが拡大されたような状態になってしまいます。
大きな点でも品質を保つようにしたい場合は、なるべく大きな画像にしておく必要があります。
今回は、300x300 の星形の形状を使うことにします。
使用画像: brush1.bmp
スクリーンショット&ソースファイル
半径 8.0、間隔 2.0 で描画しています。
ちゃんと点が星形になっていますね。
>> 028_aaline4.c
解説
今回、実質的に変更された部分は、点の描画部分だけです。
円を描画する代わりに、ブラシ画像から描画濃度を得ています。
ここでは、描画する点の大きさに合わせて拡大縮小したブラシ画像の濃度を、実際の描画濃度に適用します。
拡大縮小のアルゴリズムには、「オーバーサンプリング」を使っています。
では、まずは処理前のブラシ画像の方を基準にして考えてみます。
ブラシ画像を、描画する点の大きさに拡大縮小し、点の中心である (x, y) の位置に、ブラシ画像の中央を原点として貼り付けるわけですから、その逆の処理を行って、描画位置からブラシ画像の位置を逆算します。
あとは、それを各サブピクセルの座標ごとに行って、濃度の平均値を取れば、そのピクセルの描画濃度を求めることができます。
では、実際のコードを見てみましょう。
円を描画する代わりに、ブラシ画像から描画濃度を得ています。
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 は、ブラシ画像のサイズの半分、つまりブラシ画像における半径幅です。
この値はループ内でも使用するので、ここで求めています。
scale は、描画先の座標をブラシ画像の座標に変換する際の、倍率です。
ここでは、ブラシ画像を、描画する点の大きさに拡大縮小するので、
ブラシ画像を元にして考えると、倍率は「点のサイズ / 画像のサイズ」という値になります。
しかし、点のサイズは半径で表されているので、画像のサイズもそれに合わせて、幅・高さの半分を半径とします。
(画像は正方形であると仮定されているので、ここでは幅の値を使っています)
また、実際の処理内では、描画先の座標を元に逆算していくことになるので、ここで欲しいのは逆算時の倍率です。
なので、描画先の座標に掛ける scale の値は、「画像の半径 / 点の半径」ということで、「brushhalf / radius」となります。
brushhalf = brushimg->w / 2.0; scale = brushhalf / radius;
brushhalf は、ブラシ画像のサイズの半分、つまりブラシ画像における半径幅です。
この値はループ内でも使用するので、ここで求めています。
scale は、描画先の座標をブラシ画像の座標に変換する際の、倍率です。
ここでは、ブラシ画像を、描画する点の大きさに拡大縮小するので、
ブラシ画像を元にして考えると、倍率は「点のサイズ / 画像のサイズ」という値になります。
しかし、点のサイズは半径で表されているので、画像のサイズもそれに合わせて、幅・高さの半分を半径とします。
(画像は正方形であると仮定されているので、ここでは幅の値を使っています)
また、実際の処理内では、描画先の座標を元に逆算していくことになるので、ここで欲しいのは逆算時の倍率です。
なので、描画先の座標に掛ける scale の値は、「画像の半径 / 点の半径」ということで、「brushhalf / radius」となります。
オーバーサンプリング部分
オーバーサンプリング部分では、以下のコードによって、現在のピクセルの描画濃度を求めています。
xx, yy は、各サブピクセル位置における、描画先の座標です。
まず、描画先の座標をブラシ画像の座標に拡大縮小するため、scale を掛けています。
拡大縮小は、ブラシ画像の中央を基準として行うため、現在はブラシ画像の中央が原点 (0, 0) となっています。
それを、左上を (0, 0) とした画像座標にするため、画像の半径分 brushhalf を加算しています。
これで、ブラシ画像の座標が求められたので、あとはその位置が範囲内かどうかを判定して、範囲内であれば、その位置の色を濃度として加算します。
ここでは、画像は R = G = B のグレイスケールであるものとして、R の値を使います。
なお、白色は濃度0、黒色は濃度最大とするので、R = 255 の場合は濃度は0です。
ですから、最大値 255 から R 値を引いて、濃度の値に直します。
※ここでは、ブラシ画像を RGBA イメージとして読み込んでいますが、ブラシ画像としては濃度のみがあれば良いので、実際に実装する場合は、ブラシ画像を 8bit イメージなどに変換して、あらかじめ濃度のみを抽出したものを用意しておくと良いです。
そして最後に、値の平均値を出すため、「8x8=64」で割って (ここでは >> 6)、0〜255 の範囲の濃度値を得ます。
あとは、その濃度値を、実際の描画濃度に適用すれば OK です。
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 の場合は濃度は0です。
ですから、最大値 255 から R 値を引いて、濃度の値に直します。
※ここでは、ブラシ画像を RGBA イメージとして読み込んでいますが、ブラシ画像としては濃度のみがあれば良いので、実際に実装する場合は、ブラシ画像を 8bit イメージなどに変換して、あらかじめ濃度のみを抽出したものを用意しておくと良いです。
そして最後に、値の平均値を出すため、「8x8=64」で割って (ここでは >> 6)、0〜255 の範囲の濃度値を得ます。
あとは、その濃度値を、実際の描画濃度に適用すれば OK です。
まとめ
描画の品質は、サブピクセル数によって変化させることができます。
半径が小さい場合は、サブピクセル数が多いほうが良いですし、半径が大きい場合は、サブピクセル数を減らして描画速度を上げた方が良いです。
また、半径を大きくする場合は、ブラシ画像も大きくしなければ、品質を保てません。
ブラシ画像を使う場合、円塗りつぶしなどのように直接図形を描画する場合に比べると、ブラシ画像の大きさで品質が左右されるのは、一つの欠点とも言えます。
半径を大きくして描画したい場合は、その点だけ注意しておいてください。
半径が小さい場合は、サブピクセル数が多いほうが良いですし、半径が大きい場合は、サブピクセル数を減らして描画速度を上げた方が良いです。
また、半径を大きくする場合は、ブラシ画像も大きくしなければ、品質を保てません。
ブラシ画像を使う場合、円塗りつぶしなどのように直接図形を描画する場合に比べると、ブラシ画像の大きさで品質が左右されるのは、一つの欠点とも言えます。
半径を大きくして描画したい場合は、その点だけ注意しておいてください。