Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fast WPF Particle Background

Tags:

c#

algorithm

wpf

I am building a WPF application and I want its background to be filled with particles with random:

  • Opacity/z-order
  • Size
  • Velocity
  • "Fuzziness" (blur effect)
  • Directions (or path)

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!

like image 851
ZogStriP Avatar asked Apr 13 '09 02:04

ZogStriP


3 Answers

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!

like image 197
John Noonan Avatar answered Nov 15 '22 19:11

John Noonan


Thanks everyone for answering me.

I've taken into account each of your answers:

  • Lucas Aardvark >> I've done that and it increased a little bit the speed of the application and used less CPU/Memory.
  • Rob Walker >> I followed the link, but I stopped when I saw : "The emulator is pretty CPU intensive due to zero optimization and lots of array copies".
  • kvb >> I've tried to use Animations but it was way more complex and did not improve the smoothness of the application... Maybe I did it wrong! I've also removed the use of Point3D as there was effectively no need to use them
  • Jogn Noonan >> Really instructing answer, but I'm not sure that will help. If I measure the time between 2 updates, then the longer it is the bigger the ratio will be. So particles will be like "teleported", right?

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?

like image 20
ZogStriP Avatar answered Nov 15 '22 17:11

ZogStriP


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 Points

like image 37
kvb Avatar answered Nov 15 '22 19:11

kvb