2次ベジェ曲線で円を近似

カスタム形状に最適化されたShapeとPathIteratorを自作することになり、参考のためにJDKのソースを眺めていたところ、Ellipse2Dが3次ベジェ曲線で近似描画されていることを知りました。

半径1の円の場合、第1象限の円弧を近似する3次ベジェ曲線の制御点は、c = 0.5522847498307933 としたとき、(1, c)および(c, 1)で表されます。この1/4円弧を全象限で描けば円になる訳ですね。0.55228...という数字の求め方は、こちらのページが参考になります。

3次の代わりに、2次ベジェ曲線で円を近似したらどんな見た目になるんでしょうか?2次ベジェ版のPathIteratorを以下のように書いて検証してみました。

import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.RectangularShape;
import java.util.NoSuchElementException;

/**
 * @author Kaisei Hamamoto
 */

public class QuadCurveEllipseIterator implements PathIterator {
    private static final double CTRL = Math.sqrt(2.0) - 0.5;
    private static double[][] POINTS = {
        {  CTRL,  CTRL,  0.0,  1.0 },
        { -CTRL,  CTRL, -1.0,  0.0 },
        { -CTRL, -CTRL,  0.0, -1.0 },
        {  CTRL, -CTRL,  1.0,  0.0 }
    };

    static {
        for(int i = 0; i < POINTS.length; i++) {
            for(int j = 0; j < POINTS[i].length; j++) {
                POINTS[i][j] = 0.5 * POINTS[i][j] + 0.5;
            }
        }
    }

    private double x;
    private double y;
    private double w;
    private double h;
    private AffineTransform transform;
    private int index;

    public QuadCurveEllipseIterator(RectangularShape shape, AffineTransform transform) {
        this.x = shape.getX();
        this.y = shape.getY();
        this.w = shape.getWidth();
        this.h = shape.getHeight();
        this.transform = transform;
    }

    public int currentSegment(double[] coords) {
        return innerCurrentSegment(coords, null);
    }

    public int currentSegment(float[] coords) {
        return innerCurrentSegment(null, coords);
    }

    private int innerCurrentSegment(double[] dCoords, float[] fCoords) {
        if(index == 0) {
            store(dCoords, fCoords, x + POINTS[3][2] * w, y + POINTS[3][3] * h, 0, 0);
            return SEG_MOVETO;
        }
        else if(index <= 4) {
            double[] p = POINTS[index - 1];
            store(dCoords, fCoords, x + p[0] * w, y + p[1] * h, x + p[2] * w, y + p[3] * h);
            return SEG_QUADTO;
        }
        else if(index == 5) {
            return SEG_CLOSE;
        }
        else {
            throw new NoSuchElementException();
        }
    }

    private void store(double[] dCoords, float[] fCoords,
            double x0, double y0, double x1, double y1) {
        if(dCoords != null) {
            dCoords[0] = x0;
            dCoords[1] = y0;
            if(dCoords.length > 2) {
                dCoords[2] = x1;
                dCoords[3] = y1;
            }
            if(transform != null) {
                transform.transform(dCoords, 0, dCoords, 0, dCoords.length / 2);
            }
        }
        else if(fCoords != null) {
            fCoords[0] = (float)x0;
            fCoords[1] = (float)y0;
            if(fCoords.length > 2) {
                fCoords[2] = (float)x1;
                fCoords[3] = (float)y1;
            }
            if(transform != null) {
                transform.transform(fCoords, 0, fCoords, 0, fCoords.length / 2);
            }
        }
    }

    public int getWindingRule() {
        return WIND_NON_ZERO;
    }

    public boolean isDone() {
        return index > 5;
    }

    public void next() {
        index++;
    }
}

上がJava2Dによるデフォルトの描画結果、下が2次ベジェ曲線による描画結果です。ツンツンした感じに違和感がありますが、小さい円なら問題にならないレベルだと思います。

パフォーマンスですが、draw()の場合、僕の環境ではデフォルトに比べて3割ほど速くなりました。fill()の速度は、ほとんどデフォルトの描画と一緒でした。

円の描画を少しでも高速化したいなら、2次ベジェ曲線での近似を検討する価値があるかもしれません。