I need to draw a large amount of 2D elements in WPF, such as lines and polygons. 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. 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. On my PC I can't get much above 5-10 frames a second. which is totally unacceptable when you consider that I easily draw 1/2 million particles smoothly using other technologies.
So my question is, am I running against a technical limitation here of WPF or am I missing something? Is there something else I can use? any suggestions welcome.
Here the code I tried
content of 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:
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. In essence we speak here about just the modification of the centre (dependency-) property, which is quite expensive. I presume it is expensive because it propagates the changes to mil-core directly.
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 i.e. 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. 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. 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...
In windows forms these kind of things made me fall back to;
- Set Visible=False for the highest level container (e.g. canvas of the form itself)
- Draw a lot
- Set Visible=True
Not sure if WPF supports this.
The fastest WPF drawing method I have found is to:
- create a DrawingGroup "backingStore".
- during OnRender(), draw my drawing group to the drawing context
- anytime I want, backingStore.Open() and draw new graphics objects into it
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(). This is updating the existing retained drawing commands in the WPF drawing tree and triggering an efficient repaint.
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.
I think WPF did a dis-service by calling the method OnRender(), it might be better termed 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. The above method is faster.
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:
<Canvas Name="xamlGrid" RenderOptions.BitmapScalingMode="LowQuality" RenderOptions.CachingHint="Cache" IsHitTestVisible = "False"> </Canvas>
Add a StaticResource for Brush used in GeometryDrawing:
<SolidColorBrush x:Key="MyBrush" Color="DarkBlue"/>
in code use as:
GeometryDrawing gd = new GeometryDrawing((SolidColorBrush)this.FindResource("MyBrush"), null, eg);
I hope this helps.
来源:https://stackoverflow.com/questions/16107877/fast-2d-graphics-in-wpf