キャンバス描画(2)・ニアレストネイバー高速化

ニアレストネイバー高速化
今回は、前回やったニアレストネイバーの処理を高速化する方法を説明します。

ニアレストネイバーは普通に高速なので、わざわざ高速化する必要はないと思うかもしれませんが、ペイントソフトとしては、より速いに越したことはありません。
それに、この高速化方法は他の高品質な拡大縮小時にも使えるので、先にここで説明しておきます。
座標の変化する値は一定
まずは、前回のソースの、以下の部分を見てみてください。

sx = (ix + scrx) * 100 / zoom;
sy = (iy + scry) * 100 / zoom;

キャンバス座標 ix, iy から拡大縮小前のソース画像座標 sx, sy を求めています。

ix, iy はループで +1 ずつ加算されていきますが、sx, sy のソース座標を浮動小数点として見てみると、ix, iy が +1 した時に変化する値というのは常に一定となります。

倍率が 200% の場合で検証してみます。

ix = 0 なら、sx は 0。
ix = 1 なら、sx は 0.5。
ix = 2 なら、sx は 1.0。
ix = 3 なら、sx は 1.5。

ix が +1 するごとに、sx は 0.5 ずつ増えていますね。

だとすれば、毎回計算を行わなくても、ix = 0 の時の値を初期値として、それに変化値分を加算していけば、ソースの座標は得られそうです。

しかし、それをやるためには、ソースの座標に小数点が必要なので、まずは浮動小数点を使って試してみます。
浮動小数点を使った場合
void draw_canvas()
{
    int ix,iy,pitchd;
    uint8_t *pdst;
    SPTK_PIX_RGBA *psrc;
    double sdx,sdy,incdxy;
        
    pdst = winimg->pixbuftop;
    pitchd = winimg->pitch_dir - CANVAS_W * winimg->bpp;
    
    incdxy = 100 / (double)zoom;
    
    sdy = scry * incdxy;
    
    for(iy = 0; iy < CANVAS_H; iy++)
    {
        sdx = scrx * incdxy;

        for(ix = 0; ix < CANVAS_W; ix++)
        {
            psrc = sptk_image32_getptbuf(srcimg, (int)sdx, (int)sdy);
            
            if(psrc)
                sptk_image_setpixel_buf_rgb(winimg, pdst, psrc->r, psrc->g, psrc->b);
            else
                sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc);
        
            sdx += incdxy;
            pdst += winimg->bpp;
        }
        
        sdy += incdxy;
        pdst += pitchd;
    }
}

まずは、キャンバス座標が +1 されるごとに変化するソース座標の値を incdxy に計算しておきます。
ここでは、「100 / zoom」です。
ix, iy が +1 されるごとに、これをソース座標に加算していきます。

ix, iy のループの開始前には、ソース座標の初期値を計算しておきます。
初期値は、以下の計算式において、ix,iy がループの開始値 (ここでは 0) の場合の値なので、

sx = (ix + scrx) * 100 / zoom;
sy = (iy + scry) * 100 / zoom;

以下のようになります。

sx = (0 + scrx) * 100 / zoom
   = scrx * 100 / zoom

さらにここでは、100 / zoomincdxy と同じなので、scrx * incdxy としています。

sdx, sdy には浮動小数点としてすでに結果のソース座標が入っているので、後はこれを整数に変換して、実際のソース座標を取得するだけです。
誤差について
ここで、一つ注意しなければいけないのは、浮動小数点を使っても、倍率によっては多少誤差が出るということです。

試しに、draw_canvas() を上記のコードに置き換えて、300% の倍率で表示してみてください。
右または下端までスクロールすると、右/下端が 1px 足りないように見えると思います。

浮動小数点も万能ではないので、結果として誤差が出るのは仕方がありません。
今回はキャンバス描画用に高速化するということなので、1px 程度の誤差はこの際許容範囲ということにしておきます。
固定小数点数を使う
浮動小数点を使っただけでは、高速化としては不十分です。
次に、浮動小数点を固定小数点数にして、整数演算で行うようにしてみます。

固定小数点演算は、直線描画の時に説明しました。
忘れてしまった場合は、見直しておきましょう。

void draw_canvas()
{
    int ix,iy,pitchd,sfx,sfy,sfx_left,incfxy;
    uint8_t *pdst;
    SPTK_PIX_RGBA *psrc;
        
    pdst = winimg->pixbuftop;
    pitchd = winimg->pitch_dir - CANVAS_W * winimg->bpp;
    
    incfxy = (1 << 18) * 100 / zoom;
    sfx_left = scrx * incfxy;
    
    sfy = scry * incfxy;
    
    for(iy = 0; iy < CANVAS_H; iy++)
    {
        sfx = sfx_left;

        for(ix = 0; ix < CANVAS_W; ix++)
        {
            psrc = sptk_image32_getptbuf(srcimg, sfx >> 18, sfy >> 18);
            
            if(psrc)
                sptk_image_setpixel_buf_rgb(winimg, pdst, psrc->r, psrc->g, psrc->b);
            else
                sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc);
        
            sfx += incfxy;
            pdst += winimg->bpp;
        }
        
        sfy += incfxy;
        pdst += pitchd;
    }
}

今回は、「整数:14bit、小数:18bit」の固定小数点数にしました。
整数部分が 14bit ということは、座標値は -8191〜8192 までの範囲しか表現できません。

精度を上げるためには小数部分のビット幅を多くしなければいけませんが、座標である整数部分もそれなりに幅がないと、正しい座標が得られません。
というわけで、32bit 整数で扱う場合は、ビット幅の振り分けがかなり重要になってきます。

また、今回も誤差の問題が付いてきます。
倍率によっては 1〜2px ずれる場合があるので、気をつけてください。

計算上、多少の誤差が出るのは仕方ありませんが、ループ内では加算とシフト演算しか使っていませんので、高速化としてはかなりの成果が出せました。