Ускорение рендерера L-System в C#/WPF

lsys работает молниеносно L-System визуализатор, написанный на CoffeeScript.

Ниже представлен простой рендерер на C# и WPF. Он жестко закодирован для рендеринга в этом примере. Результат при запуске выглядит следующим образом:

введите здесь описание изображения

Щелчок мышью в окне отрегулирует переменную angleGrowth. Повторный расчет GeometryGroup, а также построение Canvas обычно занимают гораздо меньше десятой доли секунды. Однако фактическое обновление экрана, похоже, занимает гораздо больше времени.

Любые предложения о том, как сделать это быстрее или эффективнее? В настоящее время он намного медленнее, чем версия CoffeeScript/JavaScript... :-)

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Diagnostics;

namespace WpfLsysRender
{
    class DrawingVisualElement : FrameworkElement
    {
        public DrawingVisual visual;

        public DrawingVisualElement() { visual = new DrawingVisual(); }

        protected override int VisualChildrenCount { get { return 1; } }

        protected override Visual GetVisualChild(int index) { return visual; }
    }

    class State
    {
        public double size;

        public double angle;

        public double x;

        public double y;

        public double dir;

        public State Clone() { return (State) this.MemberwiseClone(); }
    }

    public partial class MainWindow : Window
    {
        static string Rewrite(Dictionary<char, string> tbl, string str)
        {
            var sb = new StringBuilder();

            foreach (var elt in str)
            {
                if (tbl.ContainsKey(elt))
                    sb.Append(tbl[elt]);
                else
                    sb.Append(elt);
            }

            return sb.ToString();
        }

        public MainWindow()
        {
            InitializeComponent();

            Width = 800;
            Height = 800;

            var states = new Stack<State>();

            var str = "L";

            {
                var tbl = new Dictionary<char, string>();

                tbl.Add('L', "|-S!L!Y");
                tbl.Add('S', "[F[FF-YS]F)G]+");
                tbl.Add('Y', "--[F-)<F-FG]-");
                tbl.Add('G', "FGF[Y+>F]+Y");

                for (var i = 0; i < 12; i++) str = Rewrite(tbl, str);
            }

            var canvas = new Canvas();

            Content = canvas;

            var sizeGrowth = -1.359672;
            var angleGrowth = -0.138235;

            State state;

            var pen = new Pen(new SolidColorBrush(Colors.Black), 0.25);

            var geometryGroup = new GeometryGroup();

            Action buildGeometry = () => 
            {
                state = new State()
                {
                    x = 0,
                    y = 0,
                    dir = 0,
                    size = 14.11,
                    angle = -3963.7485
                };

                geometryGroup = new GeometryGroup();

                foreach (var elt in str)
                {
                    if (elt == 'F')
                    {
                        var new_x = state.x + state.size * Math.Cos(state.dir * Math.PI / 180.0);
                        var new_y = state.y + state.size * Math.Sin(state.dir * Math.PI / 180.0);

                        geometryGroup.Children.Add(
                            new LineGeometry(
                                new Point(state.x, state.y),
                                new Point(new_x, new_y)));

                        state.x = new_x;
                        state.y = new_y;
                    }
                    else if (elt == '+') state.dir += state.angle;

                    else if (elt == '-') state.dir -= state.angle;

                    else if (elt == '>') state.size *= (1.0 - sizeGrowth);

                    else if (elt == '<') state.size *= (1.0 + sizeGrowth);

                    else if (elt == ')') state.angle *= (1 + angleGrowth);

                    else if (elt == '(') state.angle *= (1 - angleGrowth);

                    else if (elt == '[') states.Push(state.Clone());

                    else if (elt == ']') state = states.Pop();

                    else if (elt == '!') state.angle *= -1.0;

                    else if (elt == '|') state.dir += 180.0;
                }
            };

            Action populateCanvas = () =>
            {
                var drawingVisualElement = new DrawingVisualElement();

                Console.WriteLine(".");

                canvas.Children.Clear();

                canvas.RenderTransform = new TranslateTransform(400.0, 400.0);

                using (var dc = drawingVisualElement.visual.RenderOpen())
                    dc.DrawGeometry(null, pen, geometryGroup);

                canvas.Children.Add(drawingVisualElement);
            };

            MouseDown += (s, e) =>
                {
                    angleGrowth += 0.001;
                    Console.WriteLine("angleGrowth: {0}", angleGrowth);

                    var sw = Stopwatch.StartNew();

                    buildGeometry();
                    populateCanvas();

                    sw.Stop();

                    Console.WriteLine(sw.Elapsed);
                };

            buildGeometry();

            populateCanvas();
        }
    }
}

person dharmatech    schedule 24.03.2014    source источник


Ответы (4)


Рендеринг геометрии WPF просто медленно. Если вы хотите быстро, выполните рендеринг с использованием другой технологии и разместите результат в WPF. Например, вы можете выполнить рендеринг с помощью Direct3D и разместить цель рендеринга внутри D3DИзображение. Вот пример использования Direct2D. Или вы можете рисовать вручную, задав значения байтов в буфере RGB и скопировав их в WriteableBitmap.

РЕДАКТИРОВАТЬ: как выяснил ОП, есть также бесплатная библиотека, помогающая рисовать внутри WriteableBitmap, которая называется WriteableBitmapEx.

person Asik    schedule 24.03.2014
comment
Асик! Спасибо за совет! Я опубликовал ответ, включающий версию кода, в которой используется WritableBitmap. Сейчас очень-очень быстро... :-) - person dharmatech; 29.03.2014
comment
Принимая во внимание ваше другое предложение, я также сделал версию DirectX с использованием SlimDX: github.com/dharmatech/LSysSlimDx - person dharmatech; 01.06.2014

Ниже приведена версия, в которой используется WritableBitmap, как было предложено Asik. Я использовал библиотеку методов расширения WriteableBitmapEx для метода DrawLine.

Сейчас это невероятно быстро. Спасибо Асик!

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Diagnostics;

namespace WpfLsysRender
{
    class DrawingVisualElement : FrameworkElement
    {
        public DrawingVisual visual;

        public DrawingVisualElement() { visual = new DrawingVisual(); }

        protected override int VisualChildrenCount { get { return 1; } }

        protected override Visual GetVisualChild(int index) { return visual; }
    }

    class State
    {
        public double size;

        public double angle;

        public double x;

        public double y;

        public double dir;

        public State Clone() { return (State) this.MemberwiseClone(); }
    }

    public partial class MainWindow : Window
    {
        static string Rewrite(Dictionary<char, string> tbl, string str)
        {
            var sb = new StringBuilder();

            foreach (var elt in str)
            {
                if (tbl.ContainsKey(elt))
                    sb.Append(tbl[elt]);
                else
                    sb.Append(elt);
            }

            return sb.ToString();
        }

        public MainWindow()
        {
            InitializeComponent();

            Width = 800;
            Height = 800;

            var bitmap = BitmapFactory.New(800, 800);

            Content = new Image() { Source = bitmap };

            var states = new Stack<State>();

            var str = "L";

            {
                var tbl = new Dictionary<char, string>();

                tbl.Add('L', "|-S!L!Y");
                tbl.Add('S', "[F[FF-YS]F)G]+");
                tbl.Add('Y', "--[F-)<F-FG]-");
                tbl.Add('G', "FGF[Y+>F]+Y");

                for (var i = 0; i < 12; i++) str = Rewrite(tbl, str);
            }

            var sizeGrowth = -1.359672;
            var angleGrowth = -0.138235;

            State state;

            var lines = new List<Point>();

            var pen = new Pen(new SolidColorBrush(Colors.Black), 0.25);

            var geometryGroup = new GeometryGroup();

            Action buildLines = () =>
                {
                    lines.Clear();

                    state = new State()
                    {
                        x = 400,
                        y = 400,
                        dir = 0,
                        size = 14.11,
                        angle = -3963.7485
                    };

                    foreach (var elt in str)
                    {
                        if (elt == 'F')
                        {
                            var new_x = state.x + state.size * Math.Cos(state.dir * Math.PI / 180.0);
                            var new_y = state.y + state.size * Math.Sin(state.dir * Math.PI / 180.0);

                            lines.Add(new Point(state.x, state.y));
                            lines.Add(new Point(new_x, new_y));

                            state.x = new_x;
                            state.y = new_y;
                        }
                        else if (elt == '+') state.dir += state.angle;

                        else if (elt == '-') state.dir -= state.angle;

                        else if (elt == '>') state.size *= (1.0 - sizeGrowth);

                        else if (elt == '<') state.size *= (1.0 + sizeGrowth);

                        else if (elt == ')') state.angle *= (1 + angleGrowth);

                        else if (elt == '(') state.angle *= (1 - angleGrowth);

                        else if (elt == '[') states.Push(state.Clone());

                        else if (elt == ']') state = states.Pop();

                        else if (elt == '!') state.angle *= -1.0;

                        else if (elt == '|') state.dir += 180.0;
                    }
                };

            Action updateBitmap = () =>
                {
                    using (bitmap.GetBitmapContext())
                    {
                        bitmap.Clear();

                        for (var i = 0; i < lines.Count; i += 2)
                        {
                            var a = lines[i];
                            var b = lines[i+1];

                            bitmap.DrawLine(
                                (int) a.X, (int) a.Y, (int) b.X, (int) b.Y, 
                                Colors.Black);
                        }
                    }
                };

            MouseDown += (s, e) =>
                {
                    angleGrowth += 0.001;
                    Console.WriteLine("angleGrowth: {0}", angleGrowth);

                    var sw = Stopwatch.StartNew();

                    buildLines();
                    updateBitmap();

                    sw.Stop();

                    Console.WriteLine(sw.Elapsed);
                };

            buildLines();

            updateBitmap();
        }
    }
}
person dharmatech    schedule 29.03.2014
comment
Милая, я не знал об этой библиотеке. Рад видеть, что вы смогли разобраться! - person Asik; 29.03.2014
comment
@Asik, одним недостатком является то, что DrawLine не имеет параметра толщины, как многие методы WPF. Линии в растровой версии не такие тонкие, как в версии WPF. - person dharmatech; 29.03.2014

Я не тестировал версию WriteableBitmapEx, поэтому не знаю, как это сравнить, но мне удалось существенно ускорить собственную версию WPF с помощью StreamGeometry и Freeze(), что является способом оптимизации при отсутствии анимации. (Хотя это все еще не так быстро, как версия javascript)

  • Тайминг опубликованной версии составляет ~ 0,15 с.
  • Время версии StreamGeometry составляет ~ 0,029 с.

Я не думаю, что таймер включает фактическое время рендеринга, а только время для заполнения команд рендеринга. Тем не менее, он также чувствует себя намного более быстрым. Этот тест производительности WPF демонстрирует способ получения фактического времени рендеринга.

Я также удалил Canvas и FrameworkElement, но переключение на StreamGeometry сделало ускорение.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Diagnostics;

using System.Windows.Media.Imaging;

// https://stackoverflow.com/q/22599806/519568

namespace WpfLsysRender
{

    class UpdatableUIElement : UIElement {        
        DrawingGroup backingStore = new DrawingGroup();
        public UpdatableUIElement() {

        }

        protected override void OnRender(DrawingContext drawingContext) {
            base.OnRender(drawingContext);                    
            drawingContext.DrawDrawing(backingStore);            
        }
        public void Redraw(Action<DrawingContext> fn) {
            var vis = backingStore.Open();            
            fn(vis);
            vis.Close();
        }
    }    

    class State
    {
        public double size;

        public double angle;

        public double x;

        public double y;

        public double dir;

        public State Clone() { return (State)this.MemberwiseClone(); }
    }

    public partial class MainWindow : Window
    {
        static string Rewrite(Dictionary<char, string> tbl, string str) {
            var sb = new StringBuilder();

            foreach (var elt in str) {
                if (tbl.ContainsKey(elt))
                    sb.Append(tbl[elt]);
                else
                    sb.Append(elt);
            }

            return sb.ToString();
        }

        public MainWindow() {
            // InitializeComponent();

            Width = 800;
            Height = 800;

            var states = new Stack<State>();

            var str = "L";

            {
                var tbl = new Dictionary<char, string>();

                tbl.Add('L', "|-S!L!Y");
                tbl.Add('S', "[F[FF-YS]F)G]+");
                tbl.Add('Y', "--[F-)<F-FG]-");
                tbl.Add('G', "FGF[Y+>F]+Y");

                for (var i = 0; i < 12; i++) str = Rewrite(tbl, str);
            }

            var lsystem_view = new UpdatableUIElement();
            Content = lsystem_view;


            var sizeGrowth = -1.359672;
            var angleGrowth = -0.138235;

            State state;

            var pen = new Pen(new SolidColorBrush(Colors.Black), 0.25);

            var geometry = new StreamGeometry();

            Action buildGeometry = () => {
                state = new State() {
                    x = 0,
                    y = 0,
                    dir = 0,
                    size = 14.11,
                    angle = -3963.7485
                };

                geometry = new StreamGeometry();
                var gc = geometry.Open();

                foreach (var elt in str) {
                    if (elt == 'F') {
                        var new_x = state.x + state.size * Math.Cos(state.dir * Math.PI / 180.0);
                        var new_y = state.y + state.size * Math.Sin(state.dir * Math.PI / 180.0);
                        var p1 = new Point(state.x, state.y);
                        var p2 = new Point(new_x, new_y); 
                        gc.BeginFigure(p1,false,false);
                        gc.LineTo(p2,true,true);


                        state.x = new_x;
                        state.y = new_y;
                    }
                    else if (elt == '+') state.dir += state.angle;

                    else if (elt == '-') state.dir -= state.angle;

                    else if (elt == '>') state.size *= (1.0 - sizeGrowth);

                    else if (elt == '<') state.size *= (1.0 + sizeGrowth);

                    else if (elt == ')') state.angle *= (1 + angleGrowth);

                    else if (elt == '(') state.angle *= (1 - angleGrowth);

                    else if (elt == '[') states.Push(state.Clone());

                    else if (elt == ']') state = states.Pop();

                    else if (elt == '!') state.angle *= -1.0;

                    else if (elt == '|') state.dir += 180.0;
                }
                gc.Close();
                geometry.Freeze();
            };

            Action populateCanvas = () => {
                Console.WriteLine(".");

                lsystem_view.RenderTransform = new TranslateTransform(400,400);

                lsystem_view.Redraw((dc) => {
                    dc.DrawGeometry(null, pen, geometry);
                });
            };

            MouseDown += (s, e) => {
                angleGrowth += 0.001;
                Console.WriteLine("angleGrowth: {0}", angleGrowth);

                var sw = Stopwatch.StartNew();

                buildGeometry();
                populateCanvas();

                sw.Stop();

                Console.WriteLine(sw.Elapsed);
            };

            buildGeometry();

            populateCanvas();
        }
    }
}
person David Jeske    schedule 08.06.2017

Вот версия DirectX с использованием SlimDX.

person dharmatech    schedule 31.05.2014