2012年9月26日水曜日

FMX.Types.TCanvas.DrawLine のアンチエイリアスと DDA




twitter で LandscapeSketch (WorkToolSmith) さんから、アンチエイリアスの付いていない線を引きたい!というお話しがありました。

FireMonkey で普通に線を描こうと思ったら、↓こんなコードになります。

// FBmp は TBitmap のインスタンス
FBmp.Canvas.BeginScene;
try
  FBmp.Canvas.DrawLine(
    TPointF.Create(0, 0),
    TPointF.Create(FBmp.Width, FBmp.Height),
    1);
finally
  FBmp.Canvas.EndScene;
end;

そうすると描ける線は↓こんな風にアンチエイリアスがかかって非常にキレイな線になります。



↓線を拡大したところ


大体のケースでは線がキレイになるし問題ないんだろうと思いますが、アンチエイリアスが不必要な場合もあります。
たとえば、領域指定でアンチエイリアスの扱いをどうするのかとか……?

では、アンチエイリアスを外そう!と思っても、一筋縄ではいきません。
というのも、FireMonkey の TCanvas は複数の実装があり、TCanvas は、それらの Abstract クラスでしかないからです。

Direct2D が使える環境では TCanvasD2D が実装で、それ以外の GDI+ が使える環境では TCanvasGdiPlus が実装です。
もちろん Mac であれば、また違って TCanvasQuartz が実装になっています。

そうなると、アンチエイリアスを外そうにも実装によって異なっているため、非常に面倒な作業になります。
そもそも、アンチエイリアスを解除できない場合も想定されます。

TCanvasGdiPlus  だけアンチエイリアスを外そうと思えば外せそうです。
それは GDI+に SetSmoothingMode という API があるからです。
ただし、TCanvasGdiPlus が持っている TGPGraphics のインスタンスは private 変数& pirvate メソッドでしか取得できないため、リフレクションを使うなど手荒な技が必要になります。

なお、実際にアンチエイリアスをかける実体は TStrokeBrush のインスタンスである TCanvas.Stroke です。

そういったわけで、FireMonkey でアンチエイリアスを外すくらいなら、自分で描いちゃおうぜっていうお話しです。

自分で線を描くためには DDA を使います。
DDA は Digital Differential Analyzer の略で日本語では「デジタル微分解析器」とかそんな意味です。

線を描くに当たって問題なのは「ディスプレイは画素の集まり」であって連続的な線を表せるわけでは無い事です。
そのため、始点から終点まで、どの画素を通るのかということを知る必要があります。
その計算をするのが(ここでの)DDA です。
その名の通り微分するので傾きが求まり、それをデジタル値として返します。

また、DDA が優れているのは、加算・減算・論理演算、のみで実装可能なことです。乗算や除算、小数演算などが必要無いため、非常に高速に実行できます。
ここでは Delphi 言語で実装していますが、機械語での実装も容易でしょう(有効かどうかはともあれ……)。

ということで、DDA で線の描画を実装したサンプルが↓です。


サンプルソースはここから(GitHub)


詳しい説明は、昔のデブキャンの資料(PDF)をどうぞ……と思ったら、そんなに詳しく書いていなかった件……。

大まかに説明すると、4つに場合分けします。

  • 幅の方が大きい場合
    • 始点の方が小さい
    • 終点の方が小さい
  • 高さの方が大きい場合
    • 始点の方が小さい
    • 終点の方が小さい

つまり、必ず小さい方から大きい方に動くように正規化してやります。
コードでは↓こんな感じです。

procedure DDA(
  iX1, iY1, iX2, iY2: Integer;
  const iOnDDAEvent: TDDAEvent;
  const iData: Pointer);
var
  (略)
begin
  XSize := abs(iX2 - iX1);
  YSize := abs(iY2 - iY1);

  if (XSize > YSize) then begin
    // 水平方向の方が広い場合
    Flag := XSize shr 1;

    if (iX1 < iX2) then begin
      // 左の方が小さい場合
      (略)
    end
    else begin
      // 右の方が小さい場合
      (略)
    end;
  end
  else begin
    // 垂直方向の方が広い場合
    Flag := YSize shr 1;

    if (iY1 < iY2) then begin
      // 上の方が小さい場合
       (略)
    end
    else begin
      // 下の方が小さい場合
      (略)
    end;
  end;
end;

それぞれの場合において、地道に X(n) に +1 をしていって、X(n+1) の値になったら Y(n)→Y(n+1) とするだけです。

全文ソースは GitHub に上がっているので、ご覧ください。

そんなこんなで DDA を使って書いた線がこちら。
赤がDDA で、青が通常の方法です。





赤の線はガックガクなのが判りますね

2 件のコメント:

  1. 厳密に言うと、この実装は本当はDDAじゃなくって、Bresenhamのアルゴリズムなんですけどね。
    http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
    DDAの直線は、最初に割り算で傾きを求めて、残りは固定小数点で座標を計算します。
    有名どころではMacのQuickDrawのLineかしら。

    返信削除
  2. そうですね。
    円アルゴリズムとともに有名なアルゴリズムですね。
    失礼しました。

    返信削除