WPF中快速的2D图形绘制

42

我需要在WPF中绘制大量的2D元素,例如线条和多边形,它们的位置也需要不断更新。

我已经查看了很多答案,大多建议使用DrawingVisual或覆盖OnRender函数。为了测试这些方法,我实现了一个简单的粒子系统,渲染了10000个椭圆,并发现无论是使用哪种方法,绘图性能都非常糟糕。在我的PC上,帧率最多只有5-10帧,如果考虑到我可以轻松地使用其他技术流畅地绘制50万个粒子,这是完全不可接受的。

所以我的问题是,我是否在WPF的技术限制下运行,还是我遗漏了什么?还有其他我可以使用的东西吗?欢迎任何建议。

以下是我尝试的代码

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>

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;
        }
    }
}

8
我只是在重写 OnRender() 并随机绘制 10000 条线段 drawingContext.DrawLine() 进行一些测试。我发现通过冻结可冻结对象(例如 PenBrush),性能会有很大改善。Freezing Freezables 是一个有效的方法。 - Federico Berasategui
2
不幸的是,当冻结Brush时,我无法获得明显的性能变化。我的测试粒子渲染器仍然只运行在大约5帧每秒的速度,这太慢了。以这种速度,手动在CPU上绘制粒子到位图可能会更快 - 我只是不明白WPF建立在DirectX上时为什么会这么慢。 - morishuz
发布一些示例代码...你也看过这个了吗? - Federico Berasategui
2
WPF是一种保留模式系统,大多数情况下,重写OnRender并不是最佳选择。组合您的场景并让其进行绘制。您可以通过此链接查看如何绘制百万个多边形:http://blogs.msdn.com/b/kaelr/archive/2010/08/11/zoomableapplication2-a-million-items.aspx 它使用了“VirtualCanvas”。 - Simon Mourier
@SimonMourier +1 绝对同意。我猜测通过重写OnRender,您正在移动CPU/GPU工作的平衡,并在CPU上增加了一些工作量,从而导致有更多的P/Invoke。WPF团队面临的最大障碍是P/Invoke的性能缓慢。因此,大量的代码都是用C++编写的来巩固它。 - Aron
显示剩余6条评论
5个回答

14

我认为提供的示例代码已经非常完美了,并展示了框架的局限性。在我的测量中,平均花费15-25毫秒用于渲染开销。本质上,我们在这里谈论的只是修改中心(依赖)属性,这是相当昂贵的。我猜它很昂贵是因为它将更改直接传播到mil-core。

一个重要的注意事项是,开销成本与在模拟中更改位置的对象数量成正比。当大多数对象是时间上连续的即不改变位置时,渲染大量对象本身并不是一个问题。

对于这种情况,最好的替代方法是使用D3DImage,这是用DirectX呈现信息的Windows Presentation Foundation元素。一般来说,这种方法在性能方面应该是有效的。


4

您可以尝试使用WriteableBitmap,并使用后台线程上的更快的代码生成图像。但是,您能做的唯一一件事就是复制位图数据,因此您必须编写自己的基本绘图例程,或者(在您的情况下甚至可能有效)创建一个“印章”图像,将其复制到所有粒子经过的地方…


是的,绝对没错。我敢打赌使用agg库,我可以在CPU上绘制比使用WPF库在GPU上绘制更多的粒子。然而,我需要CPU来处理其他任务,而且当我知道在GPU上可以非常快速地完成这个任务时,这种做法似乎不太合适。 - morishuz
我已经测试了不同种类的绘图功能,首先将其渲染到位图,然后仅绘制该位图(而不依赖于WPF绘图函数)比其他WPF方法更快。 - Gorkem

2
我发现最快的WPF绘图方法是:
  1. 创建一个名为“backingStore”的DrawingGroup。
  2. 在OnRender()期间,将我的绘画组绘制到绘画上下文中。
  3. 随时可以使用backingStore.Open()将新的图形对象绘制到其中。
对我来说,这个方法令人惊讶的是,来自Windows.Forms.. 我可以在将其添加到DrawingContext期间更新我的DrawingGroup 之后。这会更新WPF绘图树中现有的保留绘图命令并触发有效的重绘。
在我编写的一个简单应用程序SoundLevelMonitor中,这种方法在性能上感觉与即时OnPaint() GDI绘图相当。
我认为WPF通过称之为OnRender()的方法做了不好的服务,更好的术语可能是AccumulateDrawingObjects()
这基本上看起来像:
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();            
}

我也尝试过使用RenderTargetBitmap和WriteableBitmap,将其都应用于Image.Source,并直接写入DrawingContext中。但以上方法更快。


2
Render(drawingContext); -- 这是在调用什么?没有一个接受DrawingContext的"Render"方法。这应该是在其他东西上调用OnRender吗? - 00jt

0
在 Windows Forms 中,这些事情让我不得不退回到以下方法:
  • 对于最高级别的容器(例如窗体本身的画布),设置 Visible=False
  • 进行大量绘制
  • 设置 Visible=True
不确定 WPF 是否支持此方法。

-3

以下是您可以尝试的一些方法:(我在您的示例中尝试了它们,看起来速度更快(至少在我的系统上))。

  • 除非有其他原因,否则请使用Canvas而不是Grid。尝试使用BitmapScalingMode和CachingHint:

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

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

在代码中使用:

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

我希望这能有所帮助。


1
一个转换会如何提高性能?此外,将已冻结的自由对象 (Brushes.Black) 作为 StaticResource 使用也无助于改善。 - Federico Berasategui
@HighCore:一般情况下应避免使用强制类型转换。但在这种情况下,它要么与为每个项创建画笔一样好,要么更好。我认为您应该在评判之前进行测试!最好在样式/模板中使用StaticResource,但这需要改变他创建粒子的方式。 - FHnainia
3
抱歉,不是真的。 System.Windows.Media.Brushes.Black 是一个静态实例,因此当您引用它时,您并没有“每次创建一个新实例”,而是使用同一个实例。顺便说一下,这个实例已经被冻结了。 - Federico Berasategui

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接