トップAndroid Java > PorterDuff演算よる穴あきポリゴンの描画

PorterDuff演算よる穴あきポリゴンの描画

はじめに

Android Java の PorterDuff.Mode演算に関するネット記事はそこそこあるが、具体事例がみつからないため、 取りつきにくかったが、ネット記事に何度か目を通すうちに、少しずつ敷居が下がってきた。 パターンによる塗りつぶしに成功したので、次に、マルチポリゴン(穴あきポリゴン polygon with holes)の レンダリング(描画)に取り掛かる。

穴あきポリゴン

下図(右下が横浜ズーラシア)のほぼ中央には3か所空白部分がある森林ポリゴンが描画されている。 このような穴あきポリゴンを OSM(OpenStreetMap) ではマルチポリゴンと呼んでいる。 外側(全体)を outer polygon、内側を inner polygon と呼ぶ。

下の画像データは自前の画素単位のプログラムで作成したものである。

PorterDuff演算を使って同じ結果を得たい。

プログラム

ポリゴンの描画メソッド

ポリゴンはノード(頂点)の座標値列データである。

自作地図アプリでは、ポリゴンのノード数の配列に続いて、 outer polygon のノード座標値データ、inner polygon のノード座標値データ を隙間や仕切りはなしに続けている。

ノード座標値データはX、Y座標がそれぞれ2バイト整数で表されるケース と4バイト整数で表されるケースがある。

OSMデータでは座標値は極座標であるが、メルカトル図法で平面的に描画するために、 予め、XY平面座標に変換している。

レンダリングはタイル単位であるため、 タイルの左上を原点とする相対座標に変換する必要がある。 この相対座標はタイル座標(zoom, x, y) によって算出される。

この Google Mapスタイルの OpenStreetMap に初めて触れたときは、 戸惑いもあったが、なれれば、この座標変換は難しいものではない。

Android Javaでポリゴンを描画するには、このノード座標値データ(fx,fy)を Path に登録する。最初が path.moveTo(fx,fy) で、以降は path.lineTo(fx,fy) となる。

全ての座標値の登録が終われば、path.close() により、path を完成させる。

canvas.drawPath(path, paint); で描画が実行される。

ポリゴンの境界線だけを描画することと、ポリゴンの内部を塗りつぶすこともできる。 この指示は Paintインスタンス引数で行う。

    private void fillPolygon(Canvas canvas, TileToRender tile, Paint paint, int offset, int length) {
        if (length == 0) return;
        Path path = tile.path;
        path.rewind();
        double fact = tile.fact;
        double xpx = tile.xpx;
        double ypx = tile.ypx;
        int INC = (slim ? 1 : 2);
        int end = offset + INC * length;
        float fx=0, fy=0, headX=0, headY=0;
        for (int ix = offset; ix < end; ) {
            if (slim) {
                int xy = buf[ix++];
                fx = (float) ((x0 + (short)(xy>>16)) * fact - xpx);
                fy = (float) ((y0 + (short)(xy&0xffff)) * fact - ypx);
            } else {
                fx = (float) (buf[ix++] * fact - xpx);
                fy = (float) (buf[ix++] * fact - ypx);
            }
            if (ix == offset + INC) {
                path.moveTo(fx, fy);
                headX = fx;
                headY = fy;
            } else {
                path.lineTo(fx, fy);
            }
        }
        if (fx != headX || fy != headY) {
            System.out.printf("閉ループでない (%.1f, %.1f) ≠ (%.1f, %.1f)",
                    headX, headY, fx, fy);
        }
        path.close();
        canvas.drawPath(path, paint);
    }

穴あきポリゴンの描画は上のプログラムを呼び出すことにより、 outer polygon や inner polygon を描画して、outer polygon のBitmapと inner polygon の Bitmap で PorterDuff演算を行い、所望の一つの穴あきポリゴン Bitmap を得ることになる。

穴あきポリゴンの描画

outer polygonだけを描画する

最初に、outer polygonだけを描画して、問題がないことを確認した。 これは、multi[0] の値に問題がないことをチェックした程度のことである。

    private void fillMultiPolygon(Canvas canvas, TileToRender tile, Paint paint) {
        if (type != 3) return;    // multipolygonではない
        int[] multi = getMulti(); // polygonのノード数配列
        fillPolygon(canvas, tile, paint, bgnData, multi[0]); // outer polygon
    }

inner polygonだけを描画する

今度は inner polygon を森林と見立ててみた。結果を下に示す。 場所は上の図と概ね対応している。 穴の部分が森林となっているのが確認できた。ここでは、意外と inner polygon が多く、また、大きい。

パターン塗りつぶしは最後と考えていたが、現在のプログラムではもう終わっている。

これで確認したのは、inner polygon の描画も正しく行えるということである。

    private void fillMultiPolygon(Canvas canvas, TileToRender tile, Paint paint) {
        if (type != 3) return;    // multipolygonではない
        int[] multi = getMulti();
        int offset = bgnData + multi[0] * (slim ? 1 : 2);  // outer polygon のノード数
        for (int n = 1; n < multi.length; n++) {
            fillPolygon(canvas, tile, paint, offset, multi[n]);
            offset += multi[n] * (slim ? 1 : 2);
        }
    }

PorterDuff演算

outer polygon を bmpWork(DST)、inner polygon を bmpHoles(SRC) に描画して、 DST から SRC の部分をくりぬくような PorterDuff演算を行えばよい。

その前にまず、現在の下のプログラムを吟味する必要がありそうである。

fillPolygon の Canvas 引数は tile.cvWork になっている。 マルチポリゴンでもこのままとすれば、fillPolygonメソッドでは tile.cvWork は使えない。

もう一つワークを追加することは簡単である。 しかし、ワークが多くなればその分、演算やコピーが増えることになる。 cvWork と cvHoles でマルチポリゴン対応とパターン塗りつぶしが行えればその方が効率がよい。

    void fillPolygon(Canvas canvas, TileToRender tile, Paint paint, Bitmap pattern) {
        if (!fIntersect) return;
        tile.cvWork.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);    // DST
        fillPolygon(tile.cvWork, tile, paint);
        tile.cvWork.drawBitmap(pattern, rect, rect, tile.pPorterDuffSRC_ATOP);
        canvas.drawBitmap(tile.bmpWork, rect, rect, null);
    }

fillMultiPolygonというメソッドを作るのではなく、void fillPolygon(Canvas canvas, TileToRender tile, Paint paint, Bitmap pattern) という一つのメソッドで、マルチポリゴン(穴あきポリゴン)も対応すべきであろう。 また、pattern == null のときは、単色塗りつぶしとなるようにして、全ての描画に対応すべきであろう。

マルチポリゴンの場合、最初に、cvWork と cvHoles を使って、穴あきポリゴンを cvWork に置く。 そのあと、パターン塗りつぶしのときは、cvWork にパターンを上書きして、最終的に bmpWork を canvas に描画する形とする。

最終プログラム

PorterDuff演算としては DET_OUT を使うと、inner polygon の部分が透明になることが分かった。 これで穴あきポリゴンの描画が完成した。

横浜ズーラシア付近の描画結果を下に示す。

    final void fillPolygon(Canvas canvas, TileToRender tile, Paint paint) {
        fillPolygon(canvas, tile, paint, null);
    }

    final private static Paint paintHoles = SuperRenderer.getPaintFill(0xff000000);
    void fillPolygon(Canvas canvas, TileToRender tile, Paint paint, Bitmap pattern) {
        if (!fIntersect) return;
        if (type == 2 && pattern == null) {
            fillPolygon(canvas, tile, paint, bgnData, nodes);
            return;
        }
        tile.cvWork.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);    // DST
        if (type == 3) {  // マルチポリゴン(穴あきポリゴン)
            int[] multi = getMulti();
            fillPolygon(tile.cvWork, tile, paint, bgnData, multi[0]); // outer polygon
            tile.cvHoles.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); // inners
            int offset = bgnData + multi[0] * (slim ? 1 : 2);  // outer polygon のノード数
            for (int n = 1; n < multi.length; n++) {
                fillPolygon(tile.cvHoles, tile, paintHoles, offset, multi[n]);
                offset += multi[n] * (slim ? 1 : 2);
            }
            tile.cvWork.drawBitmap(tile.bmpHoles, rect, rect, tile.pPorterDuffDST_OUT);
        } else {    // type==2, pattern != null
            fillPolygon(tile.cvWork, tile, paint, bgnData, nodes);
        }
        if (pattern != null) {
            tile.cvWork.drawBitmap(pattern, rect, rect, tile.pPorterDuffSRC_ATOP);
        }
        canvas.drawBitmap(tile.bmpWork, rect, rect, null);
    }

リファレンス

[1] AndroidのCanvasを使いこなす! - PorterDuff
[2] その7 PorterDuff.Modeとは何なのか
[3] PorterDuff.Mode
[4] 何かの時にスッと使える力技 - PorterDuffXfermode 編
[5] android.support.v4.graphics.ColorUtils Java Examples
[6] 2015-06-12 Android の Canvas#saveLayer メソッドと xfermode について
[7] Android での実際の画像 PorterDuff モードの使用法[Kotlin]
[8] 矩形の重なりを求めるプログラム