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次ベジェ曲線での近似を検討する価値があるかもしれません。