I am building a WPF application and I want its background to be filled with particles with random:
I've found a really good example of what I'd like it to be, but unfortunately it's in Flash and it's not free...
I've tried to implement it but I can't manage to get it smooth...
So I was wondering if any of you could help me improve it in order to get it to use less CPU and more GPU so it is smoother, even with more particles and in full screen mode.
Code "Particle.cs": the class that defines a Particle with all its properties
public class Particle
{
public Point3D Position { get; set; }
public Point3D Velocity { get; set; }
public double Size { get; set; }
public Ellipse Ellipse { get; set; }
public BlurEffect Blur { get; set; }
public Brush Brush { get; set; }
}
XAML "Window1.xaml": the window's xaml code composed of a radial background and a canvas to host particles
<Window x:Class="Particles.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="600" Width="800" Loaded="Window_Loaded">
<Grid>
<Grid.Background>
<RadialGradientBrush Center="0.54326,0.45465" RadiusX="0.602049" RadiusY="1.02049" GradientOrigin="0.4326,0.45465">
<GradientStop Color="#57ffe6" Offset="0"/>
<GradientStop Color="#008ee7" Offset="0.718518495559692"/>
<GradientStop Color="#2c0072" Offset="1"/>
</RadialGradientBrush>
</Grid.Background>
<Canvas x:Name="ParticleHost" />
</Grid>
</Window>
Code "Window1.xaml.cs": where everything happens
public partial class Window1 : Window
{
// ... some var/init code...
private void Window_Loaded(object sender, RoutedEventArgs e)
{
timer.Interval = TimeSpan.FromMilliseconds(10);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
void timer_Tick(object sender, EventArgs e)
{
UpdateParticules();
}
double elapsed = 0.1;
private void UpdateParticules()
{
// clear dead particles list
deadList.Clear();
// update existing particles
foreach (Particle p in this.particles)
{
// kill a particle when its too high or on the sides
if (p.Position.Y < -p.Size || p.Position.X < -p.Size || p.Position.X > Width + p.Size)
{
deadList.Add(p);
}
else
{
// update position
p.Position.X += p.Velocity.X * elapsed;
p.Position.Y += p.Velocity.Y * elapsed;
p.Position.Z += p.Velocity.Z * elapsed;
TranslateTransform t = (p.Ellipse.RenderTransform as TranslateTransform);
t.X = p.Position.X;
t.Y = p.Position.Y;
// update brush/blur
p.Ellipse.Fill = p.Brush;
p.Ellipse.Effect = p.Blur;
}
}
// create new particles (up to 10 or 25)
for (int i = 0; i < 10 && this.particles.Count < 25; i++)
{
// attempt to recycle ellipses if they are in the deadlist
if (deadList.Count - 1 >= i)
{
SpawnParticle(deadList[i].Ellipse);
deadList[i].Ellipse = null;
}
else
{
SpawnParticle(null);
}
}
// Remove dead particles
foreach (Particle p in deadList)
{
if (p.Ellipse != null) ParticleHost.Children.Remove(p.Ellipse);
this.particles.Remove(p);
}
}
private void SpawnParticle(Ellipse e)
{
// Randomization
double x = RandomWithVariance(Width / 2, Width / 2);
double y = Height;
double z = 10 * (random.NextDouble() * 100);
double speed = RandomWithVariance(20, 15);
double size = RandomWithVariance(75, 50);
Particle p = new Particle();
p.Position = new Point3D(x, y, z);
p.Size = size;
// Blur
var blur = new BlurEffect();
blur.RenderingBias = RenderingBias.Performance;
blur.Radius = RandomWithVariance(10, 15);
p.Blur = blur;
// Brush (for opacity)
var brush = (Brush)Brushes.White.Clone();
brush.Opacity = RandomWithVariance(0.5, 0.5);
p.Brush = brush;
TranslateTransform t;
if (e != null) // re-use
{
e.Fill = null;
e.Width = e.Height = size;
p.Ellipse = e;
t = e.RenderTransform as TranslateTransform;
}
else
{
p.Ellipse = new Ellipse();
p.Ellipse.Width = p.Ellipse.Height = size;
this.ParticleHost.Children.Add(p.Ellipse);
t = new TranslateTransform();
p.Ellipse.RenderTransform = t;
p.Ellipse.RenderTransformOrigin = new Point(0.5, 0.5);
}
t.X = p.Position.X;
t.Y = p.Position.Y;
// Speed
double velocityMultiplier = (random.NextDouble() + 0.25) * speed;
double vX = (1.0 - (random.NextDouble() * 2.0)) * velocityMultiplier;
// Only going from the bottom of the screen to the top (for now)
double vY = -Math.Abs((1.0 - (random.NextDouble() * 2.0)) * velocityMultiplier);
p.Velocity = new Point3D(vX, vY, 0);
this.particles.Add(p);
}
private double RandomWithVariance(double midvalue, double variance)
{
double min = Math.Max(midvalue - (variance / 2), 0);
double max = midvalue + (variance / 2);
double value = min + ((max - min) * random.NextDouble());
return value;
}
}
Thanks a lot!
I don't think the problem is performance. The app doesn't get anywhere near pegging my CPU, but the frame rate still doesn't appear smooth.
I would look at two things. How you're calculating your position update, and how often you're firing the event to do so.
timer.Interval = TimeSpan.FromMilliseconds(10);
That's 100 frames per second. Choose 30fps instead (every other refresh on your monitor), or 60, etc. You should attempt to do your updates in sync with your monitor, like a video game would.
timer.Interval = TimeSpan.FromMilliseconds(33.33); // 30 fps
That alone probably won't solve the smoothness. You also shouldn't assume that the time between events is fixed:
double elapsed = 0.1;
While you are firing a timer to do that update every .01 seconds, that doesn't mean it's actually getting done in a consistent amount of time. Garbage Collection, OS Scheduling, whatever can affect the amount of time it actually takes. Measure the elapsed time since the last update was actually done, and do your calculation based on that number.
Good luck!
Thanks everyone for answering me.
I've taken into account each of your answers:
I've updated my source code :
The Particle class now only have the following properties:
public class Particle
{
public Point Velocity { get; set; } // Speed of move
public BlurEffect Blur { get; set; } // Blur effect
public Brush Brush { get; set; } // Brush (opacity)
}
The Window1.xaml did not change, but I changed his behind code:
public partial class Window1 : Window
{
DispatcherTimer timer = new DispatcherTimer();
Random random = new Random(DateTime.Now.Millisecond);
// Some general values
double MaxSize = 150;
double NumberOfParticles = 25;
double VerticalVelocity = 0.4;
double HorizontalVelocity = -2.2;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
for (int i = 0; i < NumberOfParticles; i++)
{
CreateParticle();
}
timer.Interval = TimeSpan.FromMilliseconds(33.33);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
void timer_Tick(object sender, EventArgs e)
{
// I control "particle" from their ellipse representation
foreach (Ellipse ellipse in ParticleHost.Children)
{
var p = ellipse.Tag as Particle;
var t = ellipse.RenderTransform as TranslateTransform;
// Update location
t.X += p.Velocity.X;
t.Y -= p.Velocity.Y;
// Check if the particle is too high
if (t.Y < -MaxSize)
{
t.Y = Height + MaxSize;
}
// Check if the particle has gone outside
if (t.X < -MaxSize || t.X > Width + MaxSize)
{
t.X = random.NextDouble() * Width;
t.Y = Height + MaxSize;
}
// Brush & Effect
ellipse.Fill = p.Brush;
// Comment this line to deactivate the Blur Effect
ellipse.Effect = p.Blur;
}
}
private void CreateParticle()
{
// Brush (White)
var brush = Brushes.White.Clone();
// Opacity (0.2 <= 1)
brush.Opacity = 0.2 + random.NextDouble() * 0.8;
// Blur effect
var blur = new BlurEffect();
blur.RenderingBias = RenderingBias.Performance;
// Radius (1 <= 40)
blur.Radius = 1 + random.NextDouble() * 39;
// Ellipse
var ellipse = new Ellipse();
// Size (from 15% to 95% of MaxSize)
ellipse.Width = ellipse.Height = MaxSize * 0.15 + random.NextDouble() * MaxSize * 0.8;
// Starting location of the ellipse (anywhere in the screen)
ellipse.RenderTransform = new TranslateTransform(random.NextDouble() * Width, random.NextDouble() * Height);
ellipse.Tag = new Particle
{
Blur = blur,
Brush = brush,
Velocity = new Point
{
X = HorizontalVelocity + random.NextDouble() * 4,
Y = VerticalVelocity + random.NextDouble() * 2
}
};
// Add the ellipse to the Canvas
ParticleHost.Children.Add(ellipse);
}
}
If you try this new version, you'll see that it's still not smooth.
But if you comment the line that affect the Blur effect, you'll see it going very smooth!
Any thoughts?
If I were you, I'd look into using WPF's built in animation system rather than updating positions manually using a callback as you're doing. For instance, it may be worth looking into the Point3DAnimation
class in the System.Windows.Media.Animation
namespace, among others. On a separate note, it doesn't look like using 3D points is actually buying you anything (as far as I can tell, you're ignoring the Z values when actually rendering the ellipses), so you might want to change to simply using Point
s
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With