プログラムの事とか

お約束ですが「掲載内容は私個人の見解です」

WPFで速い描画方法が知りたい!

WPFで描画処理書いていてもっと速く描きたい!(ていうか自分の書いた処理遅い!)ということで実験を兼ねてプロジェクト作ってあわよくばすごい人に速い方法教えてもらおう!

プロジェクトはこちら

github.com

簡単な説明

class CustomDrawControl : FrameworkElement
{
    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        switch (DrawType)
        {
        case DrawType.NotFreeze:
        case DrawType.Freeze:
        case DrawType.Grouping:
        case DrawType.BackingStore:
        }

        _counter++;
        Task.Run(async () =>
        {
            await Task.Delay(1);
            await Dispatcher.BeginInvoke((Action)this.InvalidateVisual);
        });
    }
}

こんな感じでOnRender内でいろんな方法で描画してみてFPSを確認してみます

FPSの算出方法間違ってるとかそもそもOnRender使っちゃダメとかそういう突っ込みあったら教えてください)

描画はフレーム毎に色が変わるペン(パレットは8色)で点(短い線)をたくさん描きます

計測

最大FPS

何も描かないで上の処理のようにOnRenderTask.Delay(1) -> InvalidateVisual()ってやるだけで大体64fpsでした

f:id:puni-o:20180411131559j:plain

もっと速くなると思っていたのでこの再描画方法がそもそも間違っているんじゃないか疑惑が・・・

FreezableをFreezeした場合としなかった場合

遅い → Freeze()シロ っていう流れはググるとよく出てくるので実際に比較してみました

Freezeしない

f:id:puni-o:20180411131908j:plain

最大で 20 fps くらい

Freezableな奴を片っ端からFreezeする

f:id:puni-o:20180411131923j:plain

最大 64 fps

Freezeシロ

点の数は3000個で実験したのですがここまで差が出るとは

ちゃんとFreezeしようね


3パターンで計測

本番

ですがネット調べたけど私には3パターンしか見つけられませんでした

ここからは点の数を10000個で実験です


2018/4/12

デバッグビルドで計測していたり、一つは完全に間違った処理だったりなので以下はスクショ入れ替えてます


1点ずつ描画

private void FreezeRender(DrawingContext drawingContext)
{
    foreach (var p in _particles)
    {
        drawingContext.DrawLine(penList[p.Pallete], new Point(p.X1, p.Y1), new Point(p.X2, p.Y2));
    }
}

こんな感じで描きます(余計なの消しているので全部見たければGitHubからどうぞ)

f:id:puni-o:20180412103100j:plain

結果は50 fpsちょいでした

パレット毎にまとめて描画

パスはGeometryにまとめて描画した方が速いみたいな書き込みをみたので、パレット毎に一気に描画してみます

private void GroupingRender(DrawingContext drawingContext)
{
    foreach (var g in _particles.GroupBy(p => p.Pallete))
    {
        var geometry = new StreamGeometry();
        using (var context = geometry.Open())
        {
            foreach (var p in g)
            {
                context.BeginFigure(new Point(p.X1, p.Y1), false, false);
                context.LineTo(new Point(p.X2, p.Y2), true, false);
            }
        }
        drawingContext.DrawGeometry(null, penList[g.Key], geometry);
    }
}

余計なの消していますが(以下略

f:id:puni-o:20180412103124j:plain

60 fps以上出ているのでまだ余裕がありそうです(UIの操作が普通にできるので)

ダブルバッファリング?

DrawingGroup作ってそっちに描いてOnRender時はそれを描け的なことを見かけたので実験

昔懐かしのダブルバッファリングなのだろうか?

readonly DrawingGroup _backingStore = new DrawingGroup();
private void BackingStoreRender(DrawingContext drawingContext)
{
    var context = _backingStore.Open();
    foreach (var p in _particles)
    {
        context.DrawLine(penList[p.Pallete], new Point(p.X1, p.Y1), new Point(p.X2, p.Y2));
    }
    context.Close();
    drawingContext.DrawDrawing(_backingStore);
}

f:id:puni-o:20180412103226j:plain

10 fpsくらい

backingStore使って~っていうのは多分こういう使い方じゃないんだよね(OnRenderで事前に描画しておいたのを表示とかそういう使い方かな?)

まとめて描画できるならまとめよう

とりあえずまとめて描けるならまとめた方が速そう


2018/4/16 追記

  • FPSCompositionTarget.Renderingで測るみたいなのでそっちに移しました

  • DrawType.WriteableBitmapを追加しました

スクショとかないけど一番速かったです

雑な測定結果はGitHubの方にまとめてあるのでそちらを参照ということで・・・


まとめ

WPFの便利なもろもろを使いつつ速い描画を求めるならWriteableBitmapになるんですかね?

これより速そうなのだとDirectXくらいしか思いつかないんですが、それだと自由度が無くなりすぎだと思うんですよね(透過できないのが痛い)

WPFのDrawingContextでアニメーション

DrawingContextの謎メソッド(自分の中で)の実験

WPFでちょっと変わった表現をしたい時とか結局自分で描きますよね(WPFに限らずですが)

例えば

public class DrawTest : FrameworkElement
{
    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        var center = new Point(this.ActualWidth / 2, this.ActualHeight / 2);

        drawingContext.DrawEllipse(Brushes.Orange, null, center, 20, 20);
    }
}

こんなクラス作ってMainWindowに張り付けると

f:id:puni-o:20180409100051j:plain

こんな感じに中央にオレンジの円を描画します

上で使ったDrawingContext.DrawEllipseが円を描くメソッドなわけですがこいつには

public abstract void DrawEllipse(Brush brush, Pen pen, Point center, double radiusX, double radiusY);
public abstract void DrawEllipse(Brush brush, Pen pen, Point center, AnimationClock centerAnimations, double radiusX, AnimationClock radiusXAnimations, double radiusY, AnimationClock radiusYAnimations);

こんな感じにオーバーロードな奴がもう一つあります

AnimationClockを引数に持つんですが、AnimationClockってことはアニメーションしてくれんの?でもアニメーションってOnRenderで一コマずつ描くんじゃないの??どういうこと???といつも疑問に思って試していませんでした

ためしてみよう

簡単に試せるんだから試してみました

protected override void OnRender(DrawingContext drawingContext)
{
    base.OnRender(drawingContext);

    var center = new Point(this.ActualWidth / 2, this.ActualHeight / 2);

    var pointAnimation = new PointAnimation(new Point(this.ActualWidth, this.ActualHeight / 2), new Duration(TimeSpan.FromSeconds(3)));
    pointAnimation.AutoReverse = true;

    var doubleAnimation = new DoubleAnimation(20, 5, new Duration(TimeSpan.FromSeconds(5)));
    doubleAnimation.AutoReverse = true;

    drawingContext.DrawEllipse(Brushes.Orange, null, center, pointAnimation.CreateClock(), 20, doubleAnimation.CreateClock(), 20, doubleAnimation.CreateClock());
}

OnRenderを書き換え

centerAnimationsにはPointAnimationを、radiusAnimationsにはDoubleAnimationをつっこみます

これで実行すると

f:id:puni-o:20180409100748g:plain

うごいているよぉぉぉ~~~

OnRenderはちゃんと(?)1回しか呼ばれてません

予想通りだったんだけど予想通りでびっくりしました

また一つWPFに詳しくなったぞ

自己交差ポリゴンを分割する

どういうことかというとこういうことをやりたいんです

f:id:puni-o:20180330100108j:plain

WPFの画面ですが、左側は5点で構成されているポリゴンです

FillRuleがEvenOddになっているので偶数回重なっているところは塗りつぶさないようになってます

んで、これを右側のように5個のポリゴンに分けたいんです

自己交差ポリゴンに対応していないようなうんこソフトに対応するためしょーがない、やらなきゃいけない

Clipper

今更車輪の再発明してできの悪いわっか作ってもしょうがないのでググりますとでてきますよすごいライブラリ

Clipper - an open source freeware polygon clipping library

クリッピングの便利ライブラリらしいですよ。URLにdelphiとか入っていますがC#用もあります

このクラスの中に

//------------------------------------------------------------------------------
// SimplifyPolygon functions ...
// Convert self-intersecting polygons into simple polygons
//------------------------------------------------------------------------------
public static Paths SimplifyPolygon(Path poly, PolyFillType fillType = PolyFillType.pftEvenOdd)

まさにそのものがあります

呼ぶのも簡単で

var p = new List<IntPoint>();
foreach (var point in _clickPoints) p.Add(new IntPoint(point.X, point.Y));  // _clickPointsに元の座標が入っている
var sp = ClipperLib.Clipper.SimplifyPolygon(p);

SimplifyPolygonでポリゴン単位に分けてくれるのであとはそれを好き勝手すればいいんです

ということで

f:id:puni-o:20180330102048g:plain

確認するためにポリゴン毎に色を分けて表示するようにしてみましたがいい感じに分かれてくれてます

めでたしめでたし


追記

ClipperはNuGetででてきますね