アルファ値付きの描画
今までは、線などの描画時にはイメージ上の色を新しい色に置き換えるだけでしたが、ペイントソフトでアルファ値を有効にして描画する場合は、「RGBA の色の上に RGBA の色を重ねる」という処理が必要です。
今回は、アルファ値付きでの描画を行います。
今回は、アルファ値付きでの描画を行います。
スクリーンショット
左ボタンドラッグで、四角形塗りつぶしを描画します。
描画色のアルファ値は 128 (0〜255) です。
R,G,B キーを押すと、それぞれ 赤、緑、青 の色に描画色を変更します。
ソースコード
013_drawrgba.c
#include "sptk.h" #define WIDTH 300 #define HEIGHT 300 SPTK_IMAGE *winimg; SPTK_IMAGE32 *layerimg,*blendimg; SPTK_POINT start,last; SPTK_RECT boxrc; int drawcol_no = 0; SPTK_PIX_RGBA drawcol[3] = { {b:255,0,0,128:b}, {b:0,255,0,128:b}, {b:0,0,255,128:b} }; /** 点を打つ */ void drawlayer_pixel(int x,int y,SPTK_PIX_RGBA *pix) { SPTK_PIX_RGBA *pd; double dstA,srcA,newA; pd = sptk_image32_getptbuf(layerimg, x, y); if(!pd) return; /* アルファ値 */ srcA = pix->a / 255.0; dstA = pd->a / 255.0; newA = srcA + dstA - srcA * dstA; pd->a = (uint8_t)(newA * 255 + 0.5); /* RGB */ if(pd->a) { pd->r = (uint8_t)((pix->r * srcA + pd->r * dstA * (1 - srcA)) / newA + 0.5); pd->g = (uint8_t)((pix->g * srcA + pd->g * dstA * (1 - srcA)) / newA + 0.5); pd->b = (uint8_t)((pix->b * srcA + pd->b * dstA * (1 - srcA)) / newA + 0.5); } } /** 四角形塗りつぶし */ void drawlayer_fillbox(SPTK_RECT *rc,SPTK_PIX_RGBA *pix) { int ix,iy; for(iy = rc->y1; iy <= rc->y2; iy++) { for(ix = rc->x1; ix <= rc->x2; ix++) drawlayer_pixel(ix, iy, pix); } } /** 画面を更新 */ void update_screen(SPTK_RECT *updaterc) { SPTK_RECT rc; int w,h; /* 更新範囲 */ if(updaterc) rc = *updaterc; else { rc.x1 = rc.y1 = 0; rc.x2 = WIDTH - 1; rc.y2 = HEIGHT - 1; } if(sptk_rect_clip_wh(&rc, WIDTH, HEIGHT)) return; /* 合成 */ w = rc.x2 - rc.x1 + 1; h = rc.y2 - rc.y1 + 1; sptk_image32_fill_plaid(blendimg, rc.x1, rc.y1, w, h); sptk_image32_blend_normal(blendimg, layerimg, rc.x1, rc.y1, w, h); /* ウィンドウ更新 */ sptk_image32_blt_image(blendimg, rc.x1, rc.y1, w, h, winimg, rc.x1, rc.y1); sptk_update_rect(NULL, &rc, 0); } /** boxrc に矩形範囲をセット */ void set_boxrc() { boxrc.x1 = start.x; boxrc.y1 = start.y; boxrc.x2 = last.x; boxrc.y2 = last.y; sptk_rect_swap(&boxrc); } void draw_xorbox(int time) { sptk_image_box(winimg, boxrc.x1, boxrc.y1, boxrc.x2 - boxrc.x1 + 1, boxrc.y2 - boxrc.y1 + 1, SPTK_COL_XOR); sptk_update_rect(NULL, &boxrc, time); } void winhandle(SPTK_EVENT *ev) { switch(ev->type) { case SPTK_EVENT_BTTDOWN: if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT) { start.x = ev->mouse.x; start.y = ev->mouse.y; last = start; set_boxrc(); draw_xorbox(2); sptk_grab(NULL); } break; case SPTK_EVENT_BTTUP: if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT && sptk_isgrab()) { sptk_ungrab(); drawlayer_fillbox(&boxrc, &drawcol[drawcol_no]); update_screen(&boxrc); } break; case SPTK_EVENT_MOUSEMOVE: if(sptk_isgrab()) { draw_xorbox(-1); last.x = ev->mouse.x; last.y = ev->mouse.y; set_boxrc(); draw_xorbox(2); } break; case SPTK_EVENT_WINDOW_KEYDOWN: if(ev->key.code == 'R') drawcol_no = 0; else if(ev->key.code == 'G') drawcol_no = 1; else if(ev->key.code == 'B') drawcol_no = 2; break; case SPTK_EVENT_WINDOW_CLOSE: sptk_quit(); break; } } int main() { sptk_init("test", WIDTH, HEIGHT); sptk_window_set_handle(winhandle); winimg = sptk_window_get_image(); blendimg = sptk_image32_create(WIDTH, HEIGHT); layerimg = sptk_image32_create(WIDTH, HEIGHT); sptk_image32_clear(layerimg, NULL); update_screen(NULL); sptk_run(); sptk_image32_free(blendimg); sptk_image32_free(layerimg); return 0; }
解説
描画を行うレイヤは layerimg、合成結果用のイメージは blendimg です。
左ボタンドラッグで四角形描画
直線ツールでやった時のように、左ボタンドラッグしている間は、ウィンドウ用イメージに直接 XOR で四角形枠を描画します。
ボタンが離された時点で、描画する四角形の範囲が確定するので、レイヤイメージに四角形塗りつぶしを描画して、ウィンドウ内容を更新します。
なお、現在の矩形範囲は boxrc に格納してあります。
boxrc は、(x1,y1) が常に左上、(x2,y2) が右下になるようにセットされます。
ボタンが離された時点で、描画する四角形の範囲が確定するので、レイヤイメージに四角形塗りつぶしを描画して、ウィンドウ内容を更新します。
なお、現在の矩形範囲は boxrc に格納してあります。
boxrc は、(x1,y1) が常に左上、(x2,y2) が右下になるようにセットされます。
点の描画
drawlayer_fillbox() で、レイヤイメージに四角形塗りつぶしを描画します。
アルファ値を有効にした点描画関数が、drawlayer_pixel() です。
アルファ値を有効にした点描画関数が、drawlayer_pixel() です。
RGBA + RGBA
RGBA と RGBA を重ねて RGBA の色を得る場合の計算は、以下のようになります。
アルファ値計算
まずは、新しいアルファ値を計算します。
結果の RGB 値を求める時に新しいアルファ値が必要なので、先に求めておく必要があります。
計算の精度を上げるため、描画する色のアルファ値と、描画先のアルファ値を、0.0〜1.0 の範囲の浮動小数点に変換します。
そして、その2つのアルファ値を「SRC + DST - SRC * DST」の計算式で計算し、2つのアルファ値を掛けあわせた新しいアルファ値を求めます。
これが描画先の新しいアルファ値になります。
ところで、「SRC + DST - SRC * DST」の式はどこかで見たことありませんか?
実は、これは、前回のレイヤ合成モードで扱った「スクリーン」の合成の計算式と同じです。
実際に値をはめ込んで試してみると、
で、どちらかが 1.0 なら必ず結果は 1.0、そうでなければ、両方の値を掛けあわせて値が大きくなります。
今はアルファ値を重ねさせたいのですから、この式が丁度良い結果を生んでくれます。
最後に、浮動小数点から 0〜255 の範囲に戻して、描画先のアルファ値を置き換えます。
+0.5 の四捨五入は必ず必要です。これがないと、誤差が出ます。
結果の RGB 値を求める時に新しいアルファ値が必要なので、先に求めておく必要があります。
srcA = pix->a / 255.0; dstA = pd->a / 255.0; newA = srcA + dstA - srcA * dstA; pd->a = (uint8_t)(newA * 255 + 0.5);
計算の精度を上げるため、描画する色のアルファ値と、描画先のアルファ値を、0.0〜1.0 の範囲の浮動小数点に変換します。
そして、その2つのアルファ値を「SRC + DST - SRC * DST」の計算式で計算し、2つのアルファ値を掛けあわせた新しいアルファ値を求めます。
これが描画先の新しいアルファ値になります。
ところで、「SRC + DST - SRC * DST」の式はどこかで見たことありませんか?
実は、これは、前回のレイヤ合成モードで扱った「スクリーン」の合成の計算式と同じです。
実際に値をはめ込んで試してみると、
SRC (0.0) DST (1.0) = 1.0 SRC (1.0) DST (0.0) = 1.0 SRC (0.5) DST (1.0) = 1.0 SRC (0.5) DST (0.5) = 0.75
で、どちらかが 1.0 なら必ず結果は 1.0、そうでなければ、両方の値を掛けあわせて値が大きくなります。
今はアルファ値を重ねさせたいのですから、この式が丁度良い結果を生んでくれます。
最後に、浮動小数点から 0〜255 の範囲に戻して、描画先のアルファ値を置き換えます。
+0.5 の四捨五入は必ず必要です。これがないと、誤差が出ます。
RGB 計算
新しいアルファ値が計算できたので、次に RGB 値を計算します。
まず、新しいアルファ値が 0 (透明) の場合は、RGB 値の計算をしてはいけません。
なぜなら、計算式の途中に / newA があり、新しいアルファ値で割っているからです。
ゼロ除算回避のため、アルファ値が 0 以外の場合のみ RGB 値を計算します。
計算式は、RGB でそれぞれ同じです。
また、アルファ値 srcA、dstA、newA も引き続き使用します。
まずは、大前提として、アルファ値が付いた色同士を計算して新しい色を得る場合、各 RGB 値には、それぞれ重みとして自身のアルファ値を掛けます。
そして、RGB 値を処理した後、最後に新しいアルファ値で割ります。
これは、点の合成時に限らず、拡大縮小やぼかしなど、アルファ値を考慮した画像処理を行う時すべてで必要になります。
…というわけで、描画元の RGB と描画先の RGB にはそれぞれアルファ値を掛けています。
描画元のアルファ値は srcA、描画先のアルファ値は dstA です。
アルファ値の値が元の 0〜255 の範囲だと、掛けた後 255 で割らなければなりませんが、値は浮動小数点に変換されて 0.0〜1.0 になっているので、そのままアルファ値を掛けるだけです。
浮動小数点を使うと処理速度が気になるかもしれませんが、計算の精度や計算式の単純化のことを考えると、アルファ値を浮動小数点で計算した方が簡単です。
ちなみに、RGB 値は 0.0〜1.0 の浮動小数点に変換する必要はありません。
さて、RGB 値にそれぞれアルファ値を掛けるのはわかりましたが、描画先の RGB 値にはさらに (1 - srcA) が掛けられています。
これはつまり、描画元のアルファ値を適用するということです。
アルファ合成の基本形を見てみると、
となっていましたが、
この (1 - ALPHA) の部分にあたります。
こうして求めた RGB の値を、最後に新しいアルファ値 newA で割り、+0.5 で四捨五入して整数値にしています。
ちなみに、結果の値は基本的に 0〜255 の範囲外にはなりませんので、わざわざ最小/最大値の調整を行う必要はありません。
if(pd->a) { pd->r = (uint8_t)((pix->r * srcA + pd->r * dstA * (1 - srcA)) / newA + 0.5); pd->g = (uint8_t)((pix->g * srcA + pd->g * dstA * (1 - srcA)) / newA + 0.5); pd->b = (uint8_t)((pix->b * srcA + pd->b * dstA * (1 - srcA)) / newA + 0.5); }
まず、新しいアルファ値が 0 (透明) の場合は、RGB 値の計算をしてはいけません。
なぜなら、計算式の途中に / newA があり、新しいアルファ値で割っているからです。
ゼロ除算回避のため、アルファ値が 0 以外の場合のみ RGB 値を計算します。
計算式は、RGB でそれぞれ同じです。
また、アルファ値 srcA、dstA、newA も引き続き使用します。
まずは、大前提として、アルファ値が付いた色同士を計算して新しい色を得る場合、各 RGB 値には、それぞれ重みとして自身のアルファ値を掛けます。
そして、RGB 値を処理した後、最後に新しいアルファ値で割ります。
SRC_R = SRC_R * SRC_A SRC_G = SRC_G * SRC_A SRC_B = SRC_B * SRC_A DST_R = DST_R * DST_R DST_G = DST_G * DST_R DST_B = DST_B * DST_R ...RGBを処理 NEW_R = R / NEW_A NEW_G = G / NEW_A NEW_B = B / NEW_A
これは、点の合成時に限らず、拡大縮小やぼかしなど、アルファ値を考慮した画像処理を行う時すべてで必要になります。
…というわけで、描画元の RGB と描画先の RGB にはそれぞれアルファ値を掛けています。
描画元のアルファ値は srcA、描画先のアルファ値は dstA です。
アルファ値の値が元の 0〜255 の範囲だと、掛けた後 255 で割らなければなりませんが、値は浮動小数点に変換されて 0.0〜1.0 になっているので、そのままアルファ値を掛けるだけです。
浮動小数点を使うと処理速度が気になるかもしれませんが、計算の精度や計算式の単純化のことを考えると、アルファ値を浮動小数点で計算した方が簡単です。
ちなみに、RGB 値は 0.0〜1.0 の浮動小数点に変換する必要はありません。
さて、RGB 値にそれぞれアルファ値を掛けるのはわかりましたが、描画先の RGB 値にはさらに (1 - srcA) が掛けられています。
これはつまり、描画元のアルファ値を適用するということです。
アルファ合成の基本形を見てみると、
SRC * ALPHA + DST * (1 - ALPHA)
となっていましたが、
この (1 - ALPHA) の部分にあたります。
こうして求めた RGB の値を、最後に新しいアルファ値 newA で割り、+0.5 で四捨五入して整数値にしています。
ちなみに、結果の値は基本的に 0〜255 の範囲外にはなりませんので、わざわざ最小/最大値の調整を行う必要はありません。
画面の更新
update_screen() が、画面を更新する関数です。
描画する四角形がイメージの範囲外にはみ出ている場合があるので、矩形範囲はクリッピングする必要があります。
sptk_rect_clip_wh() は、矩形範囲を (0,0)-(w-1,h-1) の範囲にクリッピングする関数です。
全体が範囲外にある場合は、戻り値として 1 が返ります。
更新する範囲が決まったら、合成結果用イメージに背景を描画してレイヤを合成します。
sptk_image32_blend_normal() は、指定範囲を通常合成する関数です。
そして、合成結果のイメージをウィンドウイメージに転送し、ウィンドウ内容を更新します。
描画する四角形がイメージの範囲外にはみ出ている場合があるので、矩形範囲はクリッピングする必要があります。
sptk_rect_clip_wh() は、矩形範囲を (0,0)-(w-1,h-1) の範囲にクリッピングする関数です。
全体が範囲外にある場合は、戻り値として 1 が返ります。
更新する範囲が決まったら、合成結果用イメージに背景を描画してレイヤを合成します。
sptk_image32_blend_normal() は、指定範囲を通常合成する関数です。
そして、合成結果のイメージをウィンドウイメージに転送し、ウィンドウ内容を更新します。