3
\$\begingroup\$

I'm working on polishing up the particle system in the project I'm working on in Monogame.

Currently particles and objects are all drawn within the same Begin/End block to allow for FrontToBack depth sorting, however my particles use the Additive BlendState, while my actual objects use AlphaBlend.

How would one go about changing the BlendState mid block while still being able to preserve depth?

This is part of a much larger system that I've been working on over the course of a few months, so everything in my library code is pretty well ingrained within everything else, so I can't really post all of it. If there are any questions on something I can answer.

Particle:

using GurmLib.Framework.Graphics;
using GurmLib.Framework.Tools.Trig;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace GurmLib.Framework.Particles
{
    public class Particle
    {
        #region Variables

        public float
            StartAge,
            EndAge;

        public Sprite SpriteIndex;

        public Vector2
            Pos,
            Vel,
            Gravity;

        public Vector2 MaxBounds;

        public Color
            Blend,
            StartBlend,
            EndBlend;

        public float
            Scale,
            ScaleVel,
            ScaleAcc,
            ScaleMax,
            Rot,
            RotVel,
            RotDamp,
            Depth,
            Friction,
            Speed;

        #endregion

        #region Properties

        public bool Dead
        {
            get
            {
                bool outside =
                    Pos.X < Width ||
                    Pos.X >= Window.Width + Width ||
                    Pos.Y < Height ||
                    Pos.Y >= Window.Height + Height;

                return (StartAge <= 0) || outside;
            }
        }

        public int Width
        {
            get { return SpriteIndex.Width; }
        }

        public int Height
        {
            get { return SpriteIndex.Height; }
        }

        #endregion

        #region Constructor

        public Particle(Sprite sprite)
        {
            StartAge = -1;

            Pos = Vector2.Zero;
            SpriteIndex = sprite;
        }

        #endregion

        #region Core

        public void Update(GameTime gameTime)
        {
            if (Dead)
                return;

            StartAge -= gameTime.ElapsedGameTime.Milliseconds;

            float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

            SpriteIndex.Update(gameTime);

            UpdatePos(deltaTime);
            UpdateRot(deltaTime);
            UpdateScale(deltaTime);
            UpdateColor(deltaTime);
        }

        public void Draw(SpriteBatch spriteBatch)
        {
            if (Dead)
                return;

            SpriteIndex.Draw(
                spriteBatch, 
                Pos, 
                Blend, 
                new Vector2(Scale),
                SpriteIndex.Origin,
                MathHelper.ToRadians(Rot), 
                Depth,
                SpriteEffects.None);
        }

        #endregion

        #region Functions

        public void Create(Vector2 startPos, float age, Vector2 dir, Vector2 gravity, float friction, float speed)
        {
            StartAge = age;

            Pos = startPos;
            Vel = dir;
            Gravity = gravity;

            Friction = friction;
            Rot = Trig.PointDirection(Pos, Pos + Vel);
            Speed = speed;
        }

        public void Create(
            float age,
            float fade,
            Vector2 startPos,
            Vector2 dir,
            Vector2 gravity,
            float friction,
            float speed,
            float rotation,
            float rotVel,
            float rotDamp,
            float scale,
            float scaleVel,
            float scaleAcc,
            float scaleMax,
            Color colorStart,
            Color colorEnd)
        {
            StartAge = age;
            EndAge = fade;

            Pos = startPos;
            Vel = dir;
            Gravity = gravity;

            Friction = friction;
            Speed = speed;

            Rot = rotation;
            RotVel = rotVel;
            RotDamp = rotDamp;

            Scale = scale;
            ScaleVel = scaleVel;
            ScaleAcc = scaleAcc;
            ScaleMax = scaleMax;

            StartBlend = colorStart;
            EndBlend = colorEnd;
            Blend = StartBlend;
        }

        public void UpdatePos(float deltaTime)
        {
            Vel *= Friction;
            Vel += (Gravity * deltaTime);
            Pos += (Vel * deltaTime * Speed);
        }

        public void UpdateRot(float deltaTime)
        {
            Rot *= RotDamp;

            RotDamp += (RotVel * deltaTime);
        }

        public void UpdateScale(float deltaTime)
        {
            ScaleVel += (ScaleAcc * deltaTime);
            ScaleAcc += (ScaleVel * deltaTime);
            ScaleVel = MathHelper.Clamp(Scale, 0.0f, ScaleMax);
        }

        public void UpdateColor(float deltaTime)
        {
            if ((StartAge > EndAge) && (EndAge != 0))
            {
                Blend = StartBlend;
            }
            else
            {
                float amtInit = StartAge / EndAge;
                float amtFinal = 1.0f - amtInit;

                Blend.R = (byte)((amtInit * StartBlend.R) + (amtFinal * EndBlend.R));
                Blend.G = (byte)((amtInit * StartBlend.G) + (amtFinal * EndBlend.G));
                Blend.B = (byte)((amtInit * StartBlend.B) + (amtFinal * EndBlend.B));
                Blend.A = (byte)((amtInit * StartBlend.A) + (amtFinal * EndBlend.A));
            }

        }

        #endregion
    }
}

One of my spawner functions:

public static Particle Fire(Vector2 startPos, float fxScale = 1.0f)
{
    float age = 500 + _rand.Next(500);
    float fade = age + _rand.Next(100);
    float speed = 1;

    Vector2 origin = startPos;

    float radius = 50 * fxScale;
    Vector2 offset = Trig.LengthDir(_rand.Next((int)-radius, (int)radius), _rand.Next(360));
    origin += offset;

    Vector2 direction = Vector2.Zero;
    //direction.X = -(offset.X);
    direction.Y = (-500f) * fxScale;

    Vector2 gravity = new Vector2(0, - _rand.Next(300));

    float friction = 0.5f;

    float rotation = 0.0f;
    float rotVel = 2.0f;
    float rotDamp = 0.99f;

    float scale = 0.5f * fxScale;
    float scaleVel = -0.1f;
    float scaleAcc = 0.0f;
    float scaleMax = 1.0f;

    Color colorStart = Color.Blue;
    Color colorEnd = Color.Orange;
    colorStart.A = 1;
    colorEnd.A = 0;

    Particle part = new Particle(_Sprites.spr_part_circle);

    part.Create(
        age, 
        fade, 
        origin, 
        direction, 
        gravity, 
        friction, 
        speed,
        rotation, 
        rotVel, 
        rotDamp,
        scale, 
        scaleVel, 
        scaleAcc, 
        scaleMax,
        colorStart, 
        colorEnd);

    return part;
}

My Particle Manager:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;

namespace GurmLib.Framework.Particles
{
    public static class FxManager
    {
        #region Variables

        private static int 
            _deltaTime = 0, 
            _timer = 0;

        private static List<Particle> _particles = new List<Particle>();

        #endregion

        #region Properties

        public static int Count
        {
            get { return _particles.Count; }
        }

        #endregion

        #region Core

        public static void Update(GameTime gameTime)
        {
            _deltaTime = gameTime.ElapsedGameTime.Milliseconds;

            for (int i = 0; i < Count; i++)
            {
                _particles[i].Update(gameTime);

                if (_particles[i].Dead)
                {
                    _particles.RemoveAt(i);
                }
            }
        }

        public static void Draw(SpriteBatch spriteBatch)
        {
            foreach (Particle p in _particles)
            {
                if (p != null)
                    p.Draw(spriteBatch);
            }
        }

        #endregion

        #region Functions

        public static void Create(
            SpriteBatch spriteBatch,
            Func<Vector2, float, Particle> srcFunc,
            Vector2 startPos,
            float fxScale = 1,
            int amount = 15,
            int frequency = 16,
            float depth = 1)
        {
            Create(srcFunc, startPos, fxScale, amount, frequency, depth);
            Draw(spriteBatch);
        }

        public static void Create(
            Func<Vector2, float, Particle> srcFunc,
            Vector2 startPos,
            float fxScale,
            int amount,
            int frequency,
            float depth)
        {
            _timer -= _deltaTime;

            if (_timer <= 0)
            {
                for (int i = 0; i < amount; i++)
                {
                    Particle part = srcFunc(startPos, fxScale);

                    part.Depth = depth;

                    Create(part);
                }

                _timer = frequency;
            }
        }

        public static void Create(Particle part)
        {
            _particles.Add(part);
        }

        public static void Clear()
        {
            _particles.Clear();
        }

        #endregion
    }
}

FxManager.Create is called like...

public void Draw(SpriteBatch spriteBatch)
{
    Vector2 pos = new Vector2(100, 100);
    float depth = /* Some function to calc depth */

    FxManager.Create(
        spriteBatch, _Particles.Fire, pos, 0.08f, 5, 16, depth);
}

GameState.Draw:

public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    Graphics2D.Clear(Color.Black);

    spriteBatch.Begin(
        SpriteSortMode.FrontToBack,
        BlendState.AlphaBlend,
        SamplerState.PointClamp,
        null, null, null,
        View.TransMatrix);
    {
        View.TileMaps[0].Draw(spriteBatch);

        DrawGameObjects(spriteBatch);
    }
    spriteBatch.End();
}

↑ And this is (basically) called by the main draw loop (there's a few more hoops to jump through to get to Game1, but I'd end up posting all my source). I only have one SpriteBatch throughout the entire program.

DrawGameObjects calls the draw event for every GameObject, passing the spriteBatch along as it goes. It's in here that I want to be able to change the BlendState while still using the same depth calculations as the GameObjects. Like if a GameObject called FxManager.Create in it's own draw event.

Minus a few asset references this is pretty much the entirety of my Particle system. Idk if it'll help formulate a solution, but it's here anyways. Everything should work as is, minus my assets, so feel free to copy pasta and use it on whatever.

\$\endgroup\$
  • \$\begingroup\$ Can you show what you've got so far? \$\endgroup\$ – Ahmet Zambak Dec 3 '15 at 22:45
  • \$\begingroup\$ I posted the majority of my code. There's a lot so I couldn't post everything. \$\endgroup\$ – Gurman8r Dec 3 '15 at 23:35
2
\$\begingroup\$

You can't. FrontToBack means that it's going to sort it and do one draw call with the current sprite batch settings.

You are most likely going to have to split it into multiple sprite batches. You'll need to begin and end a sprite batch for each unique setting combination you have.

For example if you have these 5 unsorted objects:

  1. Particle (Additive), Depth: 0.1
  2. Particle (Additive), Depth: 0.2
  3. Object (AlphaBlend), Depth: 0.15
  4. Particle (Additive), Depth: 0.23
  5. Object (AlphaBlend), Depth: 0.4

Then you'll need 4 logical sprite batch groupings:

  1. Draw #1
  2. Draw #3
  3. Draw #2, #4
  4. Draw #5
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.