Edited to bring the projection matrix and user resizing in line with the way spritebatch looks as well as add a couple things and notes.
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Content;
// you will probably need to change the namespace in your own test run.
namespace CpuGpuParticles
{
// structs
//_________________________________________________________________________
// this was the data struct i changed the name less typing.
public static class Gv
{
public static Game1 game;
public static ContentManager content;
public static SpriteBatch spriteBatch;
public static SpriteFont currentFont;
public static GraphicsDeviceManager gdm;
public static GraphicsDevice device { get { return gdm.GraphicsDevice; } }
public static int ViewportWidth { get { return gdm.GraphicsDevice.Viewport.Width; } }
public static int ViewportHeight { get { return gdm.GraphicsDevice.Viewport.Height; } }
public static int BackBufferWidth { get { return gdm.GraphicsDevice.PresentationParameters.BackBufferWidth; } }
public static int BackBufferHeight { get { return gdm.GraphicsDevice.PresentationParameters.BackBufferHeight; } }
public static Action RegisterOnResizeCallback;
public static void TriggerOnScreenResize(Object sender, EventArgs e){ Gv.RegisterOnResizeCallback(); }
}
public struct Physics
{
public float X, Y; //current position
public float accX, accY; //accelerations
public float preX, preY; //last position
}
public struct ParticleID
{ // don't like enums even if it means a extra dot accessor ill take it.
public byte value;
public static ParticleID None { get { var t = new ParticleID(); t.value = 0; return t; } }
public static ParticleID Fire { get { var t = new ParticleID(); t.value = 1; return t; } }
public static ParticleID Rain { get { var t = new ParticleID(); t.value = 2; return t; } }
}
public struct Particle
{ //uses physics struct as component
public Physics physics;
public ParticleID id;
public short age;
}
// the instanceing and vertex structures.
public struct InstanceData : IVertexType
{
public Vector3 InstancePosition;
public Color InstanceColor;
public static readonly VertexDeclaration VertexDeclaration;
VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } }
static InstanceData()
{
var elements = new VertexElement[]
{
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 1), // Note the 1 not a 0 used by the VertexPositionTexture UsageIndex.
new VertexElement(12, VertexElementFormat.Color, VertexElementUsage.Color, 1)
};
VertexDeclaration = new VertexDeclaration(elements);
}
}
public struct VertexPositionTexture : IVertexType
{
public Vector3 Position;
public Vector2 TextureCoordinate;
public static readonly VertexDeclaration VertexDeclaration;
VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } }
static VertexPositionTexture()
{
var elements = new VertexElement[]
{
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
new VertexElement(12, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
};
VertexDeclaration = new VertexDeclaration(elements);
}
}
// ParticleSystem
//_________________________________________________________________________
public static class ParticleSystem
{
public static Texture2D Texture { get; set; }
static Random Rand = new Random();
public static Rectangle drawRec = new Rectangle(0, 0, 3, 3);
public static float gravity = 0.1f;
public static float windCounter = 0;
public static int inactiveIndexMarker = 0; // this is the dead index.
// I renamed size to this for sanity size is to vauge to remember. max size depends on the platform, 30k is baseline/lowend
public static int MaxVisibleParticles { get; private set; }
// initial size determined by maxvisibleparticles.
public static Particle[] data;
// well be adding some things for instancing.
// Vertex data
static VertexBuffer vertexBuffer;
static IndexBuffer indexBuffer;
static VertexBufferBinding vertexBufferBinding;
// Instance data
static InstanceData[] instanceData;
static VertexBuffer instanceBuffer;
static VertexBufferBinding instanceBufferBinding;
// we need a effect and some matrices.
public static Effect effect;
static Matrix world, view, projection;
static Vector2 pixelCoeff;
public static Vector2 ParticleSize { get; set; }
static double elapsedUpdateTime = 0;
/// <summary>
/// Acts as a initializer or lazy constructor.
/// Prime the particle system this is typically called in the game1 load content method.
/// We can pass a texture to be used for the particleTexture, for now though if we just pass null to the method it will create a dot texture and use that.
/// </summary>
public static void LoadUpTheParticleSystem(GraphicsDeviceManager graphics, Microsoft.Xna.Framework.Content.ContentManager Content, Texture2D particleTexture, int numberOfParticles, Vector2 defaultParticleSize)
{
var graphicsDevice = graphics.GraphicsDevice;
var viewport = graphicsDevice.Viewport;
MaxVisibleParticles = numberOfParticles;
data = new Particle[MaxVisibleParticles];
// one half a pixel on the shader in relation to -1 +1 gpu clip space,
// handy for later when you want a function to define a particles size in screen pixels.
pixelCoeff = Vector2.One / viewport.Bounds.Size.ToVector2();
// the particle size determines the size of the texture drawn.
ParticleSize = defaultParticleSize;
// we set up our camera the world view projection matrices here.
// i need to line this up better with spritebatch in total to be honest so the back buffer changes properly but eh.
SetUpWorldViewProjection();
// we get a dot texture when particle texture is null.
Texture = EnsureTextureValidOrMakeDefault(graphicsDevice, particleTexture);
// load the effect shader.
effect = Content.Load<Effect>("InstancingShader");
effect.CurrentTechnique = effect.Techniques["ParticleInstancing"];
// initialize the vertex and index buffers and initalize the default instances.
IntializeParticleSystemBuffers(graphicsDevice);
// ensure the marker is zero.
inactiveIndexMarker = 0;
// this is a callback on a user resize
Gv.RegisterOnResizeCallback += OnResize;
}
public static void Unload()
{
Texture.Dispose();
}
public static void OnResize()
{
SetUpWorldViewProjection();
}
public static void Draw(GameTime gameTime)
{
// these really only need to be called when something changes, the view changes alot typically in 3d every frame.
effect.CurrentTechnique = effect.Techniques["ParticleInstancing"];
effect.Parameters["World"].SetValue(world);
effect.Parameters["View"].SetValue(view);
effect.Parameters["Projection"].SetValue(projection);
effect.Parameters["ParticleTexture"].SetValue(Texture);
// set the altered instance buffer data to the device.
instanceBuffer.SetData(instanceData);
// Set the shader technique pass and then Draw
effect.CurrentTechnique.Passes[0].Apply();
Gv.device.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 2, inactiveIndexMarker);
}
public static void Update(GameTime gameTime)
{
elapsedUpdateTime = gameTime.ElapsedGameTime.TotalSeconds;
int width = Gv.ViewportWidth;
int height = Gv.ViewportHeight;
windCounter += 0.015f;
float wind = (float)Math.Sin(windCounter) * 0.03f;
// create particles and disallow creation from exceeding buffer limit.
int numberOfParticlesToSpawn = 100;
if (inactiveIndexMarker + (numberOfParticlesToSpawn * 2) < MaxVisibleParticles)
{
for (int i = 0; i < numberOfParticlesToSpawn; i++)
{
Spawn(ParticleID.Rain, Rand.Next(0, width), 0, 0, 0);
Spawn(ParticleID.Fire, Rand.Next(0, width), height, 0, 0);
}
}
for (int currentIndex = 0; currentIndex < inactiveIndexMarker; currentIndex++)
{
Particle P = data[currentIndex];
P.age--;
if (P.id.value == ParticleID.None.value)
{
throw new IndexOutOfRangeException("particle ["+currentIndex+"] id is none and less then the inactiveIndexMarker ("+ inactiveIndexMarker + ").");
}
// check for all the conditions that allow us to cull calculations in one place this is a late check but if its offscreen 1 frame who cares.
if (P.age < 0 || P.physics.X < 0 || P.physics.X > width || P.physics.Y < 0 || P.physics.Y > height)
{
DeActivateParticle(P.id, currentIndex);
currentIndex--;
}
else
{
if (P.id.value == ParticleID.Fire.value)
{ // fire rises, gravity does not affect it
P.physics.accY -= gravity;
P.physics.accY -= 0.05f;
}
if (P.id.value == ParticleID.Rain.value)
{ // rain falls (at different speeds)
P.physics.accY = Rand.Next(0, 100) * 0.001f;
}
P.physics.accY += gravity;
P.physics.accX += wind;
// calculate velocity using current and previous pos
float velocityX = P.physics.X - P.physics.preX;
float velocityY = P.physics.Y - P.physics.preY;
// store previous positions (the current positions)
P.physics.preX = P.physics.X;
P.physics.preY = P.physics.Y;
// set next positions using current + velocity + acceleration
P.physics.X = P.physics.X + velocityX + P.physics.accX;
P.physics.Y = P.physics.Y + velocityY + P.physics.accY;
// lear accelerations
P.physics.accX = 0; P.physics.accY = 0;
// write local to heap
data[currentIndex] = P;
// copy to instance buffer.
instanceData[currentIndex].InstancePosition = new Vector3(P.physics.X, P.physics.Y, currentIndex * 0.00001f + 1.0f);
}
}
}
public static void Spawn( ParticleID ID, float X, float Y, float accX, float accY )
{
Particle P = new Particle();
P.physics.X = X;
P.physics.Y = Y;
P.physics.preX = X;
P.physics.preY = Y;
P.physics.accX = accX;
P.physics.accY = accY;
P.id = ID;
//setup particle based on ID
if (P.id.value == ParticleID.Fire.value)
{
//fire is red
instanceData[inactiveIndexMarker].InstanceColor = Color.Red;
P.age = 300;
}
if (P.id.value == ParticleID.Rain.value)
{
//rain is blue
instanceData[inactiveIndexMarker].InstanceColor = Color.Blue;
P.age = 200;
}
data[inactiveIndexMarker] = P;
inactiveIndexMarker++;
}
public static void DeActivateParticle(ParticleID ID, int index)
{
data[index].id.value = ParticleID.None.value;
if (index < inactiveIndexMarker - 1)
data[index] = data[inactiveIndexMarker - 1];
inactiveIndexMarker--;
}
private static void IntializeParticleSystemBuffers(GraphicsDevice graphicsDevice)
{
// set up the vertex stuff.
// Create a single quad origin is dead center of the quad it could be top left instead.
float halfWidth = ParticleSize.X /2 ;
float halfHeight = ParticleSize.Y /2;
float Left = -halfWidth; float Right = halfWidth;
float Top = -halfHeight; float Bottom = halfHeight;
VertexPositionTexture[] vertices = new VertexPositionTexture[4];
vertices[0] = new VertexPositionTexture() { Position = new Vector3(Left, Top, 0f), TextureCoordinate = new Vector2(0f, 0f) };
vertices[1] = new VertexPositionTexture() { Position = new Vector3(Left, Bottom, 0f), TextureCoordinate = new Vector2(0f, 1f) };
vertices[2] = new VertexPositionTexture() { Position = new Vector3(Right, Bottom, 0f), TextureCoordinate = new Vector2(1f, 1f) };
vertices[3] = new VertexPositionTexture() { Position = new Vector3(Right, Top, 0f), TextureCoordinate = new Vector2(1f, 0f) };
// set up the indice stuff.
int[] indices = new int[6];
if (graphicsDevice.RasterizerState == RasterizerState.CullClockwise)
{
indices[0] = 0; indices[1] = 1; indices[2] = 2;
indices[3] = 2; indices[4] = 3; indices[5] = 0;
}
else
{
indices[0] = 0; indices[1] = 2; indices[2] = 1;
indices[3] = 2; indices[4] = 0; indices[5] = 3;
}
// set up the instance stuff
instanceData = new InstanceData[MaxVisibleParticles];
// set particles randomly
Random rnd = new Random();
for (int i = 0; i < MaxVisibleParticles; ++i)
{
instanceData[i].InstanceColor = Color.White;
instanceData[i].InstancePosition = new Vector3
(
(rnd.Next(0, Gv.ViewportWidth) - Gv.ViewportWidth /2),
(rnd.Next(0, Gv.ViewportHeight) - Gv.ViewportHeight /2),
rnd.Next(1, MaxVisibleParticles + 1) / (float)(MaxVisibleParticles + 1)
);
}
// create buffers and set the data to them.
indexBuffer = new IndexBuffer(graphicsDevice, typeof(int), 6, BufferUsage.WriteOnly);
indexBuffer.SetData(indices);
vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, 4, BufferUsage.WriteOnly);
vertexBuffer.SetData(vertices);
instanceBuffer = new VertexBuffer(graphicsDevice, InstanceData.VertexDeclaration, MaxVisibleParticles, BufferUsage.WriteOnly);
instanceBuffer.SetData(instanceData);
// create the bindings.
vertexBufferBinding = new VertexBufferBinding(vertexBuffer);
instanceBufferBinding = new VertexBufferBinding(instanceBuffer, 0, 1);
// set buffer bindings to the device
graphicsDevice.SetVertexBuffers(vertexBufferBinding, instanceBufferBinding);
graphicsDevice.Indices = indexBuffer;
}
public static Texture2D EnsureTextureValidOrMakeDefault(GraphicsDevice graphicsDevice, Texture2D texture)
{
Texture2D t;
if (texture == null)
{
t = new Texture2D(graphicsDevice, 1, 1);
t.SetData<Color>(new Color[] { Color.White });
}
else
t = texture;
return t;
}
public static void SetUpWorldViewProjection()
{
// when this is true the layer depth or z vertice coordinates are positive for forward depth or world position, its opposite when false.
bool useSpriteBatchProjection = true;
// Setup the worldViewProj matrix
float aspect = Gv.ViewportWidth / Gv.ViewportHeight;
var viewport = Gv.device.Viewport;
world = Matrix.Identity;
if (useSpriteBatchProjection)
{
// the below is equivillent to view = Matrix.Identity;
view = Matrix.CreateLookAt(new Vector3(0, 0, 0), Vector3.Forward, Vector3.Up);
projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, -10);
}
else
{
// the below is equivalent to view = Matrix.Identity;
view = Matrix.CreateLookAt(new Vector3(0, 0, 0), Vector3.Forward, Vector3.Up);
projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 10);
}
}
}
//_________________________________________________________________________
// Game
//_________________________________________________________________________
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
double elapsedUpdateTime = 0;
Point minimizedWindowSize = new Point(1024, 768);
public static float framesIterated = 0;
public static float timeAccumulated = 0;
public static float fps = 0;
string msg =
"AoS Particle System Example by @_mrgrak" +
"- fps: Please wait a moment " +
" - total particles: " + ParticleSystem.inactiveIndexMarker +
" / " + ParticleSystem.MaxVisibleParticles +
" inactive index marker: "
;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.GraphicsProfile = GraphicsProfile.HiDef;
graphics.PreferMultiSampling = false;
graphics.HardwareModeSwitch = true;
Window.AllowUserResizing = true;
this.IsMouseVisible = true;
this.IsFixedTimeStep = false;
graphics.SynchronizeWithVerticalRetrace = false;
//graphics.PreferredBackBufferWidth = minimizedWindowSize.X;
//graphics.PreferredBackBufferHeight = minimizedWindowSize.Y;
//graphics.ApplyChanges();
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
minimizedWindowSize = new Point(graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
base.Initialize();
}
protected override void LoadContent()
{
Gv.spriteBatch = spriteBatch = new SpriteBatch(GraphicsDevice);
Gv.gdm = graphics;
Gv.content = Content;
Gv.game = this;
ParticleSystem.LoadUpTheParticleSystem(Gv.gdm, Content, null, 30000, new Vector2(3.0f, 3.0f));
Window.ClientSizeChanged += Gv.TriggerOnScreenResize;
// this is just so classes from anywere can register or self register to the window resize event.
Gv.RegisterOnResizeCallback += OnResize;
}
protected override void UnloadContent()
{
ParticleSystem.Unload();
Content.Unload();
}
public static void OnResize()
{
}
public void UpdateUserFullScreenCheck()
{
if (Keyboard.GetState().IsKeyDown(Keys.F11))
{
Gv.gdm.IsFullScreen = true;
Gv.gdm.ApplyChanges();
}
if (Keyboard.GetState().IsKeyDown(Keys.F12))
{
graphics.IsFullScreen = false;
Gv.gdm.PreferredBackBufferWidth = minimizedWindowSize.X;
Gv.gdm.PreferredBackBufferHeight = minimizedWindowSize.Y;
Gv.gdm.ApplyChanges();
}
}
protected override void Update(GameTime gameTime)
{
elapsedUpdateTime = gameTime.ElapsedGameTime.TotalSeconds;
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
UpdateUserFullScreenCheck();
ParticleSystem.Update(gameTime);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
Gv.device.Clear(Color.CornflowerBlue);
ParticleSystem.Draw(gameTime);
framesIterated++;
timeAccumulated += (float)gameTime.ElapsedGameTime.TotalSeconds;
if (timeAccumulated > 1.0f)
{
timeAccumulated = 0f;
fps = framesIterated;
framesIterated = 0;
msg =
"AoS Particle System by @_mrgrak modified by will. " +
" fps: " + fps +
" total particles: " + ParticleSystem.inactiveIndexMarker +
" / " + ParticleSystem.MaxVisibleParticles +
" inactive index marker: "
;
}
Gv.game.Window.Title = msg + ParticleSystem.inactiveIndexMarker;
base.Draw(gameTime);
}
}
}
