Je dois dessiner une grande quantité d’éléments 2D dans WPF, tels que des lignes et des polygones. Leur position doit également être mise à jour en permanence.
J'ai examiné de nombreuses réponses suggérant d'utiliser DrawingVisual ou de remplacer la fonction OnRender. Pour tester ces méthodes, j'ai implémenté un système de particules simple rendant 10000 ellipses et je constate que les performances de dessin sont toujours terribles avec ces deux approches. Sur mon PC, je ne peux pas obtenir beaucoup plus que 5 à 10 images par seconde. Ce qui est totalement inacceptable quand on considère que je puise facilement 1/2 million de particules en douceur en utilisant d'autres technologies.
Ma question est donc la suivante: suis-je contre une limitation technique de WPF ou manque-t-il quelque chose? Y at-il autre chose que je peux utiliser? toutes les suggestions sont les bienvenues.
Voici le code que j'ai essayé
contenu de 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>
contenu de 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;
}
}
}
Je crois que l'exemple de code fourni est à peu près aussi bon qu'il peut être et qu'il montre les limites du framework. Dans mes mesures, je profilais un coût moyen de 15-25 ms attribuable aux frais généraux de rendu. En substance, nous parlons ici de la modification de la propriété center (dependency-), qui est assez chère. Je suppose que cela coûte cher car il propage les modifications directement dans mil-core.
Une remarque importante est que les frais généraux sont proportionnels à la quantité d'objets dont la position est modifiée dans la simulation. Rendre une grande quantité d’objets sur lui-même n’est pas un problème lorsque la majorité des objets sont cohérents dans le temps, c’est-à-dire qu’ils ne changent pas de position.
La meilleure approche alternative à cette situation consiste à faire appel à D3DImage , qui est un élément permettant à Windows Presentation Foundation de présenter les informations fournies avec DirectX. En règle générale, cette approche devrait être efficace et performante.
Vous pouvez essayer un WriteableBitmap et produire l’image en utilisant un code plus rapide sur un fil d’arrière-plan. Cependant, la seule chose que vous pouvez faire avec elle est de copier des données bitmap. Vous devez donc coder vos propres routines de dessin primitives ou (ce qui pourrait même fonctionner dans votre cas) créer une image "tampon" que vous copiez partout où vos particules aller...
La méthode de dessin WPF la plus rapide que j'ai trouvée consiste à:
La chose surprenante à ce sujet pour moi, provenant de Windows.Forms .. est que je peux mettre à jour mon groupe de dessin après que je l’ai ajouté au DrawingContext lors de OnRender (). Ceci met à jour les commandes de dessin conservées existantes dans l'arbre de dessin WPF et déclenche une nouvelle mise à jour efficace.
Dans une application simple que j'ai codée à la fois sous Windows.Forms et WPF ( SoundLevelMonitor ), cette méthode a des performances qui s'apparentent empiriquement à celles du dessin immédiat OnPaint () GDI.
Je pense que WPF a fait une panne de service en appelant la méthode OnRender (), on pourrait plutôt l'appeler AccumulateDrawingObjects()
Cela ressemble fondamentalement à:
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();
}
J'ai également essayé d'utiliser RenderTargetBitmap et WriteableBitmap, tous deux sur un Image.Source, et écrits directement sur un DrawingContext. La méthode ci-dessus est plus rapide.
Dans les formes de fenêtres, ce genre de choses m'a fait tomber;
Pas sûr que WPF le supporte.