アルファ値付きの描画

アルファ値付きの描画
今までは、線などの描画時にはイメージ上の色を新しい色に置き換えるだけでしたが、ペイントソフトでアルファ値を有効にして描画する場合は、「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] = {
    {{255,0,0,128}}, {{0,255,0,128}}, {{0,0,255,128}}
};

/** 点を打つ */

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) が右下になるようにセットされます。
点の描画
drawlayer_fillbox() で、レイヤイメージに四角形塗りつぶしを描画します。
アルファ値を有効にした点描画関数が、drawlayer_pixel() です。
RGBA + RGBA
RGBA と RGBA を重ねて RGBA の色を得る場合の計算は、以下のようになります。
アルファ値計算
まずは、新しいアルファ値を計算します。
結果の 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 値を計算します。

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() は、指定範囲を通常合成する関数です。

そして、合成結果のイメージをウィンドウイメージに転送し、ウィンドウ内容を更新します。