【问题标题】:Fast 2D graphics in WPFWPF 中的快速 2D 图形
【发布时间】:2013-04-13 00:56:21
【问题描述】:

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

我在这里查看了许多答案,这些答案大多建议使用 DrawingVisual 或覆盖 OnRender 函数。为了测试这些方法,我实现了一个简单的粒子系统渲染 10000 个椭圆,我发现使用这两种方法的绘图性能仍然非常糟糕。在我的 PC 上,每秒的帧数不能超过 5-10 帧。当您考虑到我使用其他技术轻松绘制 1/2 百万个粒子时,这是完全不能接受的。

所以我的问题是,我是在违反 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;
        }
    }
}

【问题讨论】:

  • 我只是在做一些测试,覆盖OnRender() 并随机输入10000 个drawingContext.DrawLine()。我发现仅Freezing Freezables(例如PenBrush)在性能上就产生了巨大差异。
  • 好的,谢谢你试试看。除了 pen(在我的实现中为 null)和 Brush 之外还有什么应该冻结的吗?
  • 不幸的是,当我冻结 Brush 时,性能没有明显变化。我的测试粒子渲染器仍然只能以每秒 5 帧左右的速度运行,这实在是太慢了。以这种速度,手动将粒子绘制到 CPU 上的位图可能会更快——我只是不明白 WPF 在 DirectX 上构建时怎么会这么慢
  • 贴一些示例代码...你也看过this
  • WPF 是一个保留模式系统,在大多数情况下,重写 OnRender 是不可行的。组合你的场景并让它绘制。您可以检查一下以绘制一百万个多边形:blogs.msdn.com/b/kaelr/archive/2010/08/11/… 它正在使用“VirtualCanvas”

标签: c# wpf performance drawing drawingvisual


【解决方案1】:

我相信提供的示例代码和它所得到的一样好,并且展示了框架的局限性。在我的测量中,我分析了 15-25 毫秒的平均成本归因于渲染开销。本质上,我们在这里只讨论中心(依赖)属性的修改,这是相当昂贵的。我认为它很昂贵,因为它将更改直接传播到 mil-core。

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

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

【讨论】:

    【解决方案2】:

    您可以尝试 WriteableBitmap,并在后台线程上使用更快的代码生成图像。然而,你唯一能做的就是复制位图数据,所以你要么必须编写自己的原始绘图例程,要么(甚至可能在你的情况下工作)创建一个“印章”图像,你可以将它复制到你的粒子的任何地方走吧……

    【讨论】:

    • 绝对是的。我敢打赌,使用 agg 我可以在 CPU 上绘制比在 GPU 上使用 WPF 更多的粒子。但是,我需要 CPU 来处理其他事情,当我知道在 GPU 上可以非常快地做到这一点时,这似乎是错误的。
    • 我测试了不同类型的绘图功能,首先渲染位图比仅绘制位图(不依赖 wpf 绘图函数)比其他 wpf 方法快得多。
    【解决方案3】:

    我发现的最快的WPF绘图方法是:

    1. 创建一个绘图组“backingStore”。
    2. 在 OnRender() 期间,将我的绘图组绘制到绘图上下文中
    3. 只要我愿意,backingStore.Open() 并在其中绘制新的图形对象

    这对我来说令人惊讶的是,来自 Windows.Forms.. 是我可以在 OnRender() 期间将它添加到 DrawingContext 之后更新我的 DrawingGroup。。这将更新 WPF 绘图树中现有的保留绘图命令并触发有效的重绘。

    在我用 Windows.Forms 和 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。上述方法更快。

    【讨论】:

    • 渲染(drawingContext); ——这是什么召唤?没有采用 DrawingContext 的方法“Render”。这应该是对其他东西的 OnRender 调用吗?
    【解决方案4】:

    在 windows 窗体中,这些东西让我回退到;

    • 为最高级别的容器(例如表单本身的画布)设置 Visible=False
    • 画了很多
    • 设置可见=真

    不确定 WPF 是否支持这一点。

    【讨论】:

      【解决方案5】:

      以下是您可以尝试的一些方法:(我用您的示例进行了尝试,它似乎看起来更快(至少在我的系统上)。

      • 使用 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 将无济于事。
      • @HighCore:一般要避免强制转换。但是,在这种情况下,它与为每个项目创建画笔一样好或更好。我认为你应该在判断之前测试它!在样式/模板中使用 StaticResource 会更好,但这会涉及更改他创建粒子的方式。
      • 对不起,不是真的。 System.Windows.Media.Brushes.Black 是一个静态实例,因此当您引用它时,您并不是“每次都创建一个新实例”,而是实际使用同一个实例。顺便说一句,它已经被冻结了。
      猜你喜欢
      • 1970-01-01
      • 2010-12-06
      • 2023-03-12
      • 2011-01-10
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多