简体   繁体   English

WPF中的快速2D图形

[英]Fast 2D graphics in WPF

I need to draw a large amount of 2D elements in WPF, such as lines and polygons. 我需要在WPF中绘制大量2D元素,例如线条和多边形。 Their position also needs to be updated constantly. 他们的位置也需要不断更新。

I have looked at many of the answers here which mostly suggested using DrawingVisual or overriding the OnRender function. 我在这里查看了许多答案,这些答案大多建议使用DrawingVisual或重写OnRender函数。 To test these methods I've implemented a simple particle system rendering 10000 ellipses and I find that the drawing performance is still really terrible using both of these approaches. 为了测试这些方法,我实现了一个渲染10000个椭圆的简单粒子系统,并且发现使用这两种方法的绘制效果仍然非常糟糕。 On my PC I can't get much above 5-10 frames a second. 在我的PC上,我每秒获得的帧数不能超过5-10帧。 which is totally unacceptable when you consider that I easily draw 1/2 million particles smoothly using other technologies. 当您考虑使用其他技术轻松绘制1/2百万个粒子时,这是完全不可接受的。

So my question is, am I running against a technical limitation here of WPF or am I missing something? 所以我的问题是,我在WPF的技术限制下运行还是缺少某些东西? Is there something else I can use? 还有其他我可以使用的东西吗? any suggestions welcome. 任何建议欢迎。

Here the code I tried 这是我尝试的代码

content of MainWindow.xaml: MainWindow.xaml的内容:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="500" Width="500" Loaded="Window_Loaded">
    <Grid Name="xamlGrid">

    </Grid>
</Window>

content of MainWindow.xaml.cs: MainWindow.xaml.cs的内容:

using System.Windows.Threading;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }


        EllipseBounce[]     _particles;
        DispatcherTimer     _timer = new DispatcherTimer();

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {

            //particles with Ellipse Geometry
            _particles = new EllipseBounce[10000];

            //define area particles can bounce around in
            Rect stage = new Rect(0, 0, 500, 500);

            //seed particles with random velocity and position
            Random rand = new Random();

            //populate
            for (int i = 0; i < _particles.Length; i++)
            {
               Point pos = new Point((float)(rand.NextDouble() * stage.Width + stage.X), (float)(rand.NextDouble() * stage.Height + stage.Y));
               Point vel = new Point((float)(rand.NextDouble() * 5 - 2.5), (float)(rand.NextDouble() * 5 - 2.5));
                _particles[i] = new EllipseBounce(stage, pos, vel, 2);
            }

            //add to particle system - this will draw particles via onrender method
            ParticleSystem ps = new ParticleSystem(_particles);


            //at this element to the grid (assumes we have a Grid in xaml named 'xmalGrid'
            xamlGrid.Children.Add(ps);

            //set up and update function for the particle position
            _timer.Tick += _timer_Tick;
            _timer.Interval = new TimeSpan(0, 0, 0, 0, 1000 / 60); //update at 60 fps
            _timer.Start();

        }

        void _timer_Tick(object sender, EventArgs e)
        {
            for (int i = 0; i < _particles.Length; i++)
            {
                _particles[i].Update();
            }
        }
    }

    /// <summary>
    /// Framework elements that draws particles
    /// </summary>
    public class ParticleSystem : FrameworkElement
    {
        private DrawingGroup _drawingGroup;

        public ParticleSystem(EllipseBounce[] particles)
        {
            _drawingGroup = new DrawingGroup();

            for (int i = 0; i < particles.Length; i++)
            {
                EllipseGeometry eg = particles[i].EllipseGeometry;

                Brush col = Brushes.Black;
                col.Freeze();

                GeometryDrawing gd = new GeometryDrawing(col, null, eg);

                _drawingGroup.Children.Add(gd);
            }

        }


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

            drawingContext.DrawDrawing(_drawingGroup);
        }
    }

    /// <summary>
    /// simple class that implements 2d particle movements that bounce from walls
    /// </summary>
    public class SimpleBounce2D
    {
        protected Point     _position;
        protected Point     _velocity;
        protected Rect     _stage;

        public SimpleBounce2D(Rect stage, Point pos,Point vel)
        {
            _stage = stage;

            _position = pos;
            _velocity = vel;
        }

        public double X
        {
            get
            {
                return _position.X;
            }
        }


        public double Y
        {
            get
            {
                return _position.Y;
            }
        }

        public virtual void Update()
        {
            UpdatePosition();
            BoundaryCheck();
        }

        private void UpdatePosition()
        {
            _position.X += _velocity.X;
            _position.Y += _velocity.Y;
        }

        private void BoundaryCheck()
        {
            if (_position.X > _stage.Width + _stage.X)
            {
                _velocity.X = -_velocity.X;
                _position.X = _stage.Width + _stage.X;
            }

            if (_position.X < _stage.X)
            {
                _velocity.X = -_velocity.X;
                _position.X = _stage.X;
            }

            if (_position.Y > _stage.Height + _stage.Y)
            {
                _velocity.Y = -_velocity.Y;
                _position.Y = _stage.Height + _stage.Y;
            }

            if (_position.Y < _stage.Y)
            {
                _velocity.Y = -_velocity.Y;
                _position.Y = _stage.Y;
            }
        }
    }


    /// <summary>
    /// extend simplebounce2d to add ellipse geometry and update position in the WPF construct
    /// </summary>
    public class EllipseBounce : SimpleBounce2D
    {
        protected EllipseGeometry _ellipse;

        public EllipseBounce(Rect stage,Point pos, Point vel, float radius)
            : base(stage, pos, vel)
        {
            _ellipse = new EllipseGeometry(pos, radius, radius);
        }

        public EllipseGeometry EllipseGeometry
        {
            get
            {
                return _ellipse;
            }
        }

        public override void Update()
        {
            base.Update();
            _ellipse.Center = _position;
        }
    }
}

I believe the sample code provided is pretty much as good as it gets, and is showcasing the limits of the framework. 我相信所提供的示例代码非常好,并且展示了框架的局限性。 In my measurements I profiled an average cost of 15-25ms is attributed to render-overhead. 在我的测量中,我将15-25毫秒的平均成本归因于渲染开销。 In essence we speak here about just the modification of the centre (dependency-) property, which is quite expensive. 从本质上讲,我们在这里只谈论对中心(dependency)属性的修改,这很昂贵。 I presume it is expensive because it propagates the changes to mil-core directly. 我认为这很昂贵,因为它会将更改直接传播到mil-core。

One important note is that the overhead cost is proportional to the amount of objects whose position are changed in the simulation. 重要的一点是,间接费用与模拟中位置发生变化的对象数量成正比。 Rendering a large quantity of objects on itself is not an issue when a majority of objects are temporal coherent ie don't change positions. 当大多数对象在时间上是连贯的,即不改变位置时,在其自身上渲染大量对象不是问题。

The best alternative approach for this situation is to resort to D3DImage , which is an element for the Windows Presentation Foundation to present information rendered with DirectX. 针对这种情况的最佳替代方法是求助于D3DImage ,它是Windows Presentation Foundation呈现DirectX渲染的信息的元素。 Generally spoken that approach should be effective, performance wise. 一般而言,该方法应该有效,明智地执行。

You could try a WriteableBitmap, and produce the image using faster code on a background thread. 您可以尝试使用WriteableBitmap,然后在后台线程上使用更快的代码生成图像。 However, the only thing you can do with it is copy bitmap data, so you either have to code your own primitive drawing routines, or (which might even work in your case) create a "stamp" image which you copy to everywhere your particles go... 但是,您唯一可以做的就是复制位图数据,因此您要么必须编写自己的原始绘图例程,要么(甚至在您的情况下可行)创建“邮票”图像,然后将其复制到粒子的任何地方走...

The fastest WPF drawing method I have found is to: 我发现最快的WPF绘制方法是:

  1. create a DrawingGroup "backingStore". 创建一个DrawingGroup“ backingStore”。
  2. during OnRender(), draw my drawing group to the drawing context 在OnRender()期间,将我的绘图组绘制到绘图上下文中
  3. anytime I want, backingStore.Open() and draw new graphics objects into it 我随时都可以backingStore.Open()并在其中绘制新的图形对象

The surprising thing about this for me, coming from Windows.Forms.. is that I can update my DrawingGroup after I've added it to the DrawingContext during OnRender(). 对于我来说,来自Windows.Forms ..的这件事令人惊讶,是在OnRender()期间将我的DrawingGroup添加到DrawingContext 之后 ,我可以对其进行更新。 This is updating the existing retained drawing commands in the WPF drawing tree and triggering an efficient repaint. 这将更新WPF绘图树中现有的保留绘图命令,并触发有效的重绘。

In a simple app I've coded in both Windows.Forms and WPF ( SoundLevelMonitor ), this method empirically feels pretty similar in performance to immediate OnPaint() GDI drawing. 在我用Windows.Forms和WPF( SoundLevelMonitor )编写的一个简单应用程序中,根据经验,此方法的性能与即时OnPaint()GDI绘图非常相似。

I think WPF did a dis-service by calling the method OnRender(), it might be better termed AccumulateDrawingObjects() 我认为WPF通过调用方法OnRender()取消了服务,因此最好将其称为AccumulateDrawingObjects()

This basically looks like: 基本上看起来像这样:

DrawingGroup backingStore = new DrawingGroup();

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

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);
}

// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render() {            
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            
}

I've also tried using RenderTargetBitmap and WriteableBitmap, both to an Image.Source, and written directly to a DrawingContext. 我也尝试过将RenderTargetBitmap和WriteableBitmap都用于Image.Source,并直接写入DrawingContext。 The above method is faster. 上面的方法更快。

In windows forms these kind of things made me fall back to; 在Windows窗体中,这些事情使我跌倒了。

  • Set Visible=False for the highest level container (eg canvas of the form itself) 为最高级别的容器设置Visible = False(例如,表单本身的画布)
  • Draw a lot 画很多
  • Set Visible=True 设置可见=真

Not sure if WPF supports this. 不确定WPF是否支持此功能。

Here are some of the things you may try: (I tried them with your sample and it seems to look faster (at least on my system)). 以下是您可以尝试的一些方法:(我用您的示例对其进行了尝试,并且看起来更快(至少在我的系统上))。

  • Use Canvas instead of Grid (unless you have other reasons). 使用画布而不是网格(除非有其他原因)。 Play BitmapScalingMode and CachingHint: 播放BitmapScalingMode和CachingHint:

     <Canvas Name="xamlGrid" RenderOptions.BitmapScalingMode="LowQuality" RenderOptions.CachingHint="Cache" IsHitTestVisible = "False"> </Canvas> 
  • Add a StaticResource for Brush used in GeometryDrawing: 为GeometryDrawing中使用的Brush添加一个StaticResource:

     <SolidColorBrush x:Key="MyBrush" Color="DarkBlue"/> 

in code use as: 在代码中用作:

    GeometryDrawing gd = new GeometryDrawing((SolidColorBrush)this.FindResource("MyBrush"), null, eg);

I hope this helps. 我希望这有帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM