tags:

views:

1529

answers:

9

Hello,

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 its smoother, even with more particles and in full screen mode.

PS: You can download the solution for better support.

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!

A: 

I was reading someone's blog who was trying to do this same thing, but I can't seem to find it (I'll keep looking for it). The way he was able to speed up his application was by reusing particles. You see every time you create a new particle your taking up memory. You can't afford this memory unless you have a crazy good system because .NET uses a lot of memory.

The solution: Reuse particles, once a particle is no longer on the screen either free up it's memory (probably not going to work because of GC) or reposition that particle at the bottom and reuse it.

Lucas McCoy
A: 

Not sure if this would work better, but someone has put together a Silverlight C64 emulator, and the technique they use is to basically display a movie with a custom source (your code) that provides the frames.

The advantage is that you get callbacks as the frames are displayed, so can adapt to the actual playback rate. I'm not sure how well this would work for higher resolutions though, the C64 example only has a low resolution screen to emulate.

Rob Walker
+1  A: 

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

kvb
+2  A: 

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!

John Noonan
A: 
ZogStriP
A: 

Have you looked at doing a ShaderEffect using HLSL to do the rendering on the GPU? You could write a PixelShader. Here is some other samples from one of the announcements and it also has some nice links. It should definitely be smooth in the rendering.

Erich Mirabal
A: 

Erich Mirabal >> I did try HLSL and it was quite fun to learn something new but as I'm a total newbie I did not manage to do a Box/Gaussian blur...

Anyway, I found a way that use no CPU at all.

Instead of moving Ellipse, I'm moving their Image.

I generate an in-memory PNG with RenderTargetBitmap and PngBitmapEncoder classes and move theses already-blurred-and-transparent Images!

Thanks a lot to everyone for answering!

ZogStriP
+1  A: 

The MSDN WPF has a nice Particle Effects demo. Also, the O'Reilly book Learning XNA goes into how to use Particle Effects using XNA.

Christopher Morley
A: 

I solved your problem by removing the ellipse.Effect line and instead added the following to Window1.xaml

  <Canvas x:Name="ParticleHost">
        <Canvas.Effect>
            <BlurEffect />
        </Canvas.Effect>
    </Canvas>

Granted it doesn't have the same look with them each having thier own blur radius.