XNA UK User Group
A helping hand for bedroom coders throughout the land.
Volumetric Clouds - Source

Blogs

RandomChaos

Syndication

 

Now, I said I wanted to get the editor written up before I posted the basic system, but I have had so many requests for it I thought I may as well get the base up and I can build on it as the system evolves.

Before I get into the code, I'll explain a little of it's evolution. I first started out with a point sprite cloud system. This had a point sprite base cloud and a point sprite cloud manager, but found (on my Graphics card) that you get a kind of parting of the waves effect. I have recently seen this code ran on a much more superior laptop (Paul Foster from Microsoft) and he does not get the issue, anyway I digress. So with this issue I thought I would go with a billboard system, this then gave rise to a billboard cloud and respective manager as well as the point sprite version and there associated shaders. Now I am under the impression that a point sprite system is more cost effective than billboards, so what I have done in the final system in merge the two. The initial idea was to have the close up cloud sprites to be billboards and the ones in the distance to be point sprite . I decided to go a little further and you can have that and specify the distance where the two cross over or have just billboards or have just point sprites, or have none (switch the lot off)

Now you will notice in the early clips of the cloud system, the cloud sprites look a bit odd, now that's because in my shader I was using the sprites as colour sprites and not alpha sprites, this was pointed out to me by my good friend chr0n1x. Also you will see the cloud sprites are, well not too good. This is because I hacked the sprites out individually from the sprite sheet in Niniane Wangs white paper. I now pass the whole sheet to the shader and get it to sort out what image is required.Which reminds me, as it is part of this source zip, if you use this code for any commercial gain DO NOT USE THIS SPRITE SHEET, it is released under a similar license as the XNA assets so fine for educational use. To be specific, see this license.

Where to start, well I was going to start with the particle structures, but they are very similar to the ones I have posted before on both point sprite and billboard particle systems, so won't bother repeating myself.

Ill begin with the basic class that these volumous objects are derived from, VolumusObject

/// <summary>
/// Basic volumous object.
/// </summary>
public class VolumusObject
{
    // Boundingbox variables.
    BasicEffect shader;
    protected VertexPositionColor[] points;
    protected short[] index;
    public Color boxColor = Color.Red;

    // World pos, scale and rotation.
    public Vector3 myPosition;
    public Vector3 myScale;
    public Quaternion myRotation;

    // Volume
    public BoundingBox volume;

    // Drawable bounds (AABB)
    protected BoundingBox myDrawBounds;

    protected Game game;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="game">Calling game class</param>
    /// <param name="volume">Size of volume.</param>
    public VolumusObject(Game game,BoundingBox volume)
    {
        this.game = game;
        this.volume = volume;
    }

    /// <summary>
    /// Method to draw bounding box (volume)
    /// </summary>
    /// <param name="offestPos">Offset, normaly the center of a cloud formation or CloudManager</param>
    public void DrawBounds(Vector3 offestPos)
    {
        if (shader == null)
            shader = new BasicEffect(game.GraphicsDevice, null);

        myDrawBounds = new BoundingBox(volume.Min + offestPos, volume.Max + offestPos);
        BuildBoxCorners();
        game.GraphicsDevice.VertexDeclaration = new VertexDeclaration(game.GraphicsDevice, VertexPositionColor.VertexElements);

        shader.World = Matrix.CreateScale(myScale) * Matrix.CreateFromQuaternion(myRotation) * Matrix.CreateTranslation(myPosition+offestPos);
        shader.View = Camera.myView;
        shader.Projection = Camera.myProjection;
        shader.DiffuseColor = volume.Min;
        shader.DiffuseColor = boxColor.ToVector3();

        shader.Begin(SaveStateMode.SaveState);
        for (int pass = 0; pass < shader.CurrentTechnique.Passes.Count; pass++)
        {
            shader.CurrentTechnique.Passes[pass].Begin();
            game.GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(PrimitiveType.LineList, points, 0, 8, index, 0, 12);
            shader.CurrentTechnique.Passes[pass].End();
        }
        shader.End();
    }
    /// <summary>
    /// Method to get BB corners.
    /// </summary>
    protected void BuildBoxCorners()
    {
        points = new VertexPositionColor[8];

        Vector3[] corners = myDrawBounds.GetCorners();

        points[0] = new VertexPositionColor(corners[1], Color.Green);
        points[1] = new VertexPositionColor(corners[0], Color.Green);
        points[2] = new VertexPositionColor(corners[2], Color.Green);
        points[3] = new VertexPositionColor(corners[3], Color.Green);
        points[4] = new VertexPositionColor(corners[5], Color.Green);
        points[5] = new VertexPositionColor(corners[4], Color.Green);
        points[6] = new VertexPositionColor(corners[6], Color.Green);
        points[7] = new VertexPositionColor(corners[7], Color.Green);

        short[] inds = {
            0, 1, 0, 2, 1, 3, 2, 3,
            4, 5, 4, 6, 5, 7, 6, 7,
            0, 4, 1, 5, 2, 6, 3, 7
            };

        index = inds;
    }
}

So a pretty strait forward class, the only special feature really is the BoundingBox parameter, this is used to define the size of the volume.

Now into the first bit of nitty-gritty, the base cloud class that is derived from VolumetricObject, BaseCloud.

/// <summary>
/// Base cloud class, derived from VolumousObject
/// </summary>    
public class BaseCloud : VolumusObject 
{
    // static object instance, not used and I will probably remove it.
    protected static int instance = 0;
    protected int myID = 0;
    
    // list of verticies to draw, name left over from origional point sprite system
    public ParticleVertex[] m_sprites;

    // Base color of particles/sprites
    public Color particleColor;
    
    // Number of particles/sprites in this cloud volume
    public int partCount;

    // Legacy from point sprites.
    public float particleScale = 0;

    // Used for creating and disolving clouds.
    protected float disipation = 1f;

    // Used for psudo random numeber generation
    Random rnd;

    // Set to tru if you want the volume's AABB to be drawn.
    public bool DrawBoundingBox = false;

    /// <summary>
    /// ctor.
    /// </summary>
    /// <param name="game">Calling game class</param>
    /// <param name="particleCount">Number of partilces/sprites in this volume</param>
    /// <param name="volume">Volume</param>
    public BaseCloud(Game game, int particleCount,BoundingBox volume): base(game,volume)
    {
        instance++;
        myID = instance;

        myPosition = Vector3.Zero;
        myScale = Vector3.One;
        myRotation = new Quaternion(0, 0, 0, 1);

        partCount = particleCount;

        particleColor = Color.White;

    }

    public virtual void UnloadContent()
    {
        
    }

    public virtual void LoadContent()
    {       
        rnd = new Random((int)DateTime.Now.Ticks);
        Thread.Sleep(15);

        BuildCloud();
    }

    public Color CloudColor = Color.White;        

    /// <summary>
    /// Method to randomly fill the volume with random particles/sprites off the sprite sheet.
    /// </summary>
    protected virtual void BuildCloud()
    {
        m_sprites = new ParticleVertex[partCount];

        float maxY = volume.Max.Y;

        for (int i = 0; i < m_sprites.Length; i++)
        {
            float x, y, z;

            // Randomly position particle in the volume.                
            x = MathHelper.Lerp(volume.Min.X, volume.Max.X, (float)rnd.NextDouble());
            y = MathHelper.Lerp(volume.Min.Y, volume.Max.Y, (float)rnd.NextDouble());
            z = MathHelper.Lerp(volume.Min.Z, volume.Max.Z, (float)rnd.NextDouble());
                           
            m_sprites[i].Position = new Vector3(x, y, z);// * (displacement *= -1));                
            m_sprites[i].Color = CloudColor;

            // Setup particles data.                
            // x describes the sprite type 0 - .15 are valid values.
            // y describes sprite width
            // z describes the sprite height.
            // w alpha blend, so clouds can be disolved and re formed.

            if (particleScale == 0)
                particleScale = Vector3.Distance(volume.Min, volume.Max) / 4.5f;

            // Want the fuzzy stuff near the bottom of the cloud
            int img = (int)((16 / volume.Max.Y) * y);
            m_sprites[i].Data = new Vector4((((float)img) / 100), particleScale, particleScale, disipation);

            m_sprites[i].Data = new Vector4((((float)rnd.Next(0, 16)) / 100), particleScale, particleScale, disipation);
        }

    }
    public virtual void Update(GameTime gameTime)
    { }

}

 

So, we have the regular particle system type of stuff, particle colour, scale and count, but we have another, disipation (miss spelt it, how like me that is..); it simply regulates the alpha level of the cloud so giving the impression of forming and dissipating. The BuildCloud() method is where all the actions at. What this method does it place the cloud sprites randomly in the volume and scales them to take up the volume space. It then randomly sets the sprite image to be used for each cloud sprite.

We now have a basic, randomly generated cloud, and this was great in the early days, but I wanted to control what image sprites are used in the cloud, so I created the Cloud class which inherits from BaseCloud.

/// <summary>
/// This class creates a cloud volume using a single given sprite index.
/// </summary>
public class Cloud : BaseCloud 
{
    int[] spriteIndex;       

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="game">Calling game class</param>
    /// <param name="particleCount">Number of sprites in this cloud</param>
    /// <param name="volume">Volume of cloud</param>
    /// <param name="spriteIndex">Sprite index to use (0-15)</param>
    public Cloud(Game game, int particleCount, BoundingBox volume, params int[] spriteIndex) : base(game, particleCount, volume)
    {
        this.spriteIndex = spriteIndex;
    }

    protected override void BuildCloud()
    {
        base.BuildCloud();

        for (int p = 0,i=0; p < m_sprites.Length; p++,i++)
        {
            if (i >= spriteIndex.Length)
                i = 0;

            m_sprites[p].Data = new Vector4((float)(spriteIndex[i]/100f), m_sprites[p].Data.Y, m_sprites[p].Data.Z, m_sprites[p].Data.W);                
        }
    }
}

This is even simpler, all we do is add an array of integers to describe the cloud images to use off the sprite sheet.

So far, pretty simple eh? Well then lets take a look at the cloud manger and see if it gets complicated....

    /// <summary>
    /// Class used to managed clouds.
    /// </summary>
    public class CloudManager : DrawableGameComponent, IVolumetric
    {
        public enum SpriteType
        {
            BillboardOnly,
            PointSpriteOnly,
            MixedBBAndPS,
            None
        }

        private DynamicVertexBuffer vb;
        private DynamicIndexBuffer ib;  
        VertexDeclaration m_bbvDec;
        VertexDeclaration m_psvDec;

        // Last position in vertex stream
        int lastPos = 0;
        // Vertex data to be drawn
        ParticleVertex[] masterParticleList;
        // Close up billboard particles.
        ParticleVertex[] m_billboards;
        // Faraway pint sprite particles..
        VertexParticle[] m_sprites;
        Effect bbEffect;
        Effect psEffect;

        // List of clouds in the manager.
        public List<BaseCloud> CloudList = new List<BaseCloud>();

        // Last camera position.
        private Vector3 lastCamPos = Vector3.Zero;

        public Vector3 myPosition;
        public Vector3 myScale;
        public Quaternion myRotation;

        // Total number of particles 
        int particleCount = 0;

        /// <summary>
        /// Ambiant light color.
        /// </summary>
        public Color SunlightColor = Color.White;

        // Draw cloud bounding box's
        bool DrawBoundingBox = false;

        // Cloud disipation;
        float disipation = 0;

        // Speed clouds are formed.
        public float formSpeed = .00025f;
        public bool disipate = false;

        // switch to form or evaporate clouds.
        bool formOrEvap = false;
        
        // current frame
        short frame = 0;

        /// <summary>
        /// Time Of Day.
        /// </summary>
        public float tod = 0;

        /// <summary>
        /// Base Cloud color.
        /// </summary>
        public Color BaseCloudColor = Color.DarkGray;

        /// <summary>
        /// Cameras viewport object
        /// </summary>
        Viewport ViewPort;

        /// <summary>
        /// Distance required to turn a point sprite into a billboard.
        /// </summary>
        public float switchingDistance = 250;

        public SpriteType spriteType = SpriteType.MixedBBAndPS;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="game">Calling game class</param>
        public CloudManager(Game game) : base(game)
        {
            myPosition = Vector3.Zero;
            myScale = Vector3.One;
            myRotation = new Quaternion(0, 0, 0, 1);
        }

        /// <summary>
        /// Method to add a single cloud to the manager
        /// </summary>
        /// <param name="cloud">Cloud derived from BaseCloud</param>
        public void AddCloud(BaseCloud cloud)
        {
            CloudList.Add(cloud);
            particleCount += cloud.partCount;
        }

        /// <summary>
        /// Method to add clouds to the manager.
        /// </summary>
        /// <param name="cloud">Cloud derived from BaseCloud</param>
        public void AddClouds(params BaseCloud[] clouds)
        {
            for (int c = 0; c < clouds.Length; c++)
                AddCloud(clouds[c]);
        }
        /// <summary>
        /// Method to build cloud vertex data and add to main stream.
        /// </summary>
        /// <param name="cloud">Cloud derived from BaseCloud</param>
        protected void AddToCloudSprites(BaseCloud cloud)
        {
            for (int e = 0; e < cloud.m_sprites.Length; e++, lastPos += 4)
            {
                for (int thisP = 0; thisP < 4; thisP++)
                {
                    masterParticleList[lastPos + thisP] = new ParticleVertex();
                    masterParticleList[lastPos + thisP].Position = cloud.m_sprites[e].Position + cloud.myPosition + myPosition;
                    masterParticleList[lastPos + thisP].Color = cloud.m_sprites[e].Color;
                    masterParticleList[lastPos + thisP].Data = cloud.m_sprites[e].Data;
                    masterParticleList[lastPos + thisP].Data2 = cloud.m_sprites[e].Data2;
                    switch (thisP)
                    {
                        case 0:
                            masterParticleList[lastPos + thisP].TextureCoordinate = Vector2.Zero;
                            break;
                        case 1:
                            masterParticleList[lastPos + thisP].TextureCoordinate = new Vector2(1, 0);
                            break;
                        case 2:
                            masterParticleList[lastPos + thisP].TextureCoordinate = new Vector2(0, 1);
                            break;
                        case 3:
                            masterParticleList[lastPos + thisP].TextureCoordinate = Vector2.One;
                            break;
                    }
                }
            }
        }
        protected override void LoadContent()
        {
            m_bbvDec = new VertexDeclaration(Game.GraphicsDevice, ParticleVertex.VertexElements);
            m_psvDec = new VertexDeclaration(Game.GraphicsDevice, VertexParticle.VertexElements);

            Texture2D cloudSheet = Game.Content.Load<Texture2D>("Textures/Clouds/CloudSpriteSheetOrg");
            bbEffect = Game.Content.Load<Effect>("Shaders/Clouds/BillboardCloudShader");
            bbEffect.Parameters["partTexture"].SetValue(cloudSheet);

            psEffect = Game.Content.Load<Effect>("Shaders/Clouds/PointSpriteCloudShader");
            psEffect.Parameters["particleTexture"].SetValue(cloudSheet);

            masterParticleList = new ParticleVertex[particleCount * 4];
            lastPos = 0;
            for (int c = 0; c < CloudList.Count; c++)
            {
                CloudList[c].LoadContent();
                // Add to master list.
                AddToCloudSprites(CloudList[c]);
            }

            short[] indices = new short[6 * particleCount];

            for (int part = 0; part < particleCount; part++)
            {
                int off = part * 6;
                int offVal = part * 4;

                indices[off + 0] = (short)(0 + offVal);
                indices[off + 1] = (short)(1 + offVal);
                indices[off + 2] = (short)(2 + offVal);

                indices[off + 3] = (short)(1 + offVal);
                indices[off + 4] = (short)(3 + offVal);
                indices[off + 5] = (short)(2 + offVal);
            }

            if (particleCount > 0)
            {
                ib = new DynamicIndexBuffer(Game.GraphicsDevice, typeof(short), 6 * particleCount, BufferUsage.WriteOnly);
                ib.SetData(indices);
            }
            SortClouds();
            
            base.LoadContent();
        }

        protected override void UnloadContent()
        {
            for (int c = 0; c < CloudList.Count; c++)
                CloudList[c].UnloadContent();

            base.UnloadContent();
        }                
        public override void Update(GameTime gameTime)
        {            

            //if (lastCamPos != Camera.myPosition)
            if (Visible)
            {
#if !XBOX
                // Dont sort EVERY time the camera moves position.
                if (frame > 20)
                {
                    frame = 0;
                    lastCamPos = Camera.myPosition;
                    SortClouds();
                }
                frame++;

#else
                SortClouds();            
#endif
            }
                base.Update(gameTime);
        }
        
        /// <summary>
        /// Method to sort draw order of cloud vertices.
        /// </summary>
        private void SortClouds()
        {
            List<distData> bbDists = new List<distData>();
            List<distData> psDists = new List<distData>();

            for (int p = 0; p < masterParticleList.Length; p += 4)
            {
                float dist = (new distData()).Distance(masterParticleList[p].Position, Camera.myPosition);
                
                switch (spriteType)
                {
                    case SpriteType.MixedBBAndPS:
                        if (Math.Sqrt(dist) <= switchingDistance)
                            bbDists.Add(new distData(p, dist));
                        else
                            psDists.Add(new distData(p, dist));
                        break;
                    case SpriteType.BillboardOnly:
                        bbDists.Add(new distData(p, dist));
                        break;
                    case SpriteType.PointSpriteOnly:
                        psDists.Add(new distData(p, dist));
                        break;
                }
            }

            bbDists.Sort(new distData());
            psDists.Sort(new distData());

            ParticleVertex[] newBBSet = new ParticleVertex[bbDists.Count * 4];
            VertexParticle[] newPSSet = new VertexParticle[psDists.Count];
            
            for (int p = 0; p < bbDists.Count * 4; p += 4)
            {
                for (int ip = 0; ip < 4; ip++)
                {
                    newBBSet[p + ip] = masterParticleList[bbDists[p / 4].idx + ip];
                    if(disipate)
                        newBBSet[p + ip].AlphaValue = disipation;
                }
            }
            m_billboards = newBBSet;

            for (int p = 0; p < psDists.Count; p++)
            {
                for (int ip = 0; ip < 4; ip++)
                {
                    VertexParticle vp = new VertexParticle();
                    vp.Data = masterParticleList[psDists[p].idx + ip].Data;
                    vp.Position = masterParticleList[psDists[p].idx + ip].Position;
                    vp.Color = masterParticleList[psDists[p].idx + ip].Color;
                    if (disipate)
                        vp.AlphaValue = disipation;
                    newPSSet[p] = vp;
                }
            }
            m_sprites = newPSSet;

            // As the array size changes then this needs to be re created, will alter this so
            // it knows the last size and only recreates if different.
            vb = new DynamicVertexBuffer(Game.GraphicsDevice, typeof(ParticleVertex), m_billboards.Length, BufferUsage.WriteOnly);
            vb.ContentLost += new EventHandler(vbLost);
            vb.SetData(m_billboards);                        
        }

        protected void vbLost(object sender,EventArgs args)
        {
            try
            {
                if(!vb.IsDisposed)
                    vb.SetData(m_billboards);
            }
            catch { }
        }

        // Simple evaperation, will alter this to evaperate from the outside in.
        private void EvaporateCloud(int cloudIDX)
        {
            if (disipation > 0)
                disipation -= formSpeed;
            else
            {
                disipation = 0;
                formOrEvap = false;
            }
        }
        // opposite of the evaperate method.
        private void FormCloud(int cloudIDX)
        {
            if (disipation <= 1)
                disipation += formSpeed;
            else
            {
                disipation = 1;
                formOrEvap = true;
            }
        }

        public override void Draw(GameTime gameTime)
        {
            if (Visible)
            {
                if (DrawBoundingBox)
                    for (int c = 0; c < CloudList.Count; c++)
                        CloudList[c].DrawBounds(myPosition);

                // Set blend mode
                bool AlphaBlendEnable = Game.GraphicsDevice.RenderState.AlphaBlendEnable;
                Blend DestinationBlend = Game.GraphicsDevice.RenderState.DestinationBlend;
                Blend SourceBlend = Game.GraphicsDevice.RenderState.SourceBlend;

                Game.GraphicsDevice.RenderState.AlphaBlendEnable = true;

                Game.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
                Game.GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
                
                Game.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

                // Draw Point Sprite Clouds
                if (m_sprites != null && m_sprites.Length > 0)
                    DrawPSClouds(gameTime);

                // Draw Billboard Clouds
                if (m_billboards != null && m_billboards.Length > 0)
                    DrawBBClouds(gameTime);

                // Set the states back.
                Game.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;

                Game.GraphicsDevice.RenderState.AlphaBlendEnable = AlphaBlendEnable;

                Game.GraphicsDevice.RenderState.DestinationBlend = DestinationBlend;
                Game.GraphicsDevice.RenderState.SourceBlend = SourceBlend;
            }
            base.Draw(gameTime);
        }
        /// <summary>
        /// World View Projection of manager, for external processes (i.e DoF)
        /// </summary>
        public Matrix WVP;
        private void DrawPSClouds(GameTime gameTime)
        {
            Game.GraphicsDevice.VertexDeclaration = m_psvDec;

            bool PointSpriteEnable = Game.GraphicsDevice.RenderState.PointSpriteEnable;

            Game.GraphicsDevice.RenderState.PointSpriteEnable = true;
            Game.GraphicsDevice.RenderState.PointSizeMax = float.MaxValue;
            Game.GraphicsDevice.RenderState.PointSizeMin = float.MinValue;

            WVP = (Matrix.CreateScale(myScale) * Matrix.CreateFromQuaternion(myRotation) * Matrix.CreateTranslation(myPosition)) * Camera.myView * Camera.myProjection;

            psEffect.Parameters["Projection"].SetValue(Camera.myProjection);
            psEffect.Parameters["ViewportHeight"].SetValue(Camera.myViewport.Height);
            psEffect.Parameters["WorldViewProj"].SetValue(WVP);

            psEffect.Parameters["lightColor"].SetValue(SunlightColor.ToVector4());
            psEffect.Parameters["lightDir"].SetValue(new Vector3(0, 0, -10));

            psEffect.Parameters["EyePosition"].SetValue(Camera.myPosition);
            psEffect.Parameters["timeOfDay"].SetValue(tod);

            psEffect.Begin();
            for (int ps = 0; ps < psEffect.CurrentTechnique.Passes.Count; ps++)
            {
                psEffect.CurrentTechnique.Passes[ps].Begin();
                Game.GraphicsDevice.DrawUserPrimitives<VertexParticle>(PrimitiveType.PointList, m_sprites, 0, m_sprites.Length);
                psEffect.CurrentTechnique.Passes[ps].End();
            }
            psEffect.End();

            Game.GraphicsDevice.RenderState.PointSpriteEnable = PointSpriteEnable;
        }
        private void DrawBBClouds(GameTime gameTime)
        {
            Game.GraphicsDevice.RenderState.CullMode = CullMode.None;

            Game.GraphicsDevice.VertexDeclaration = m_bbvDec;
            Game.GraphicsDevice.Vertices[0].SetSource(vb, 0, ParticleVertex.SizeInBytes);
            Game.GraphicsDevice.Indices = ib;

            Matrix World = Matrix.CreateScale(myScale) * Matrix.CreateFromQuaternion(myRotation) * Matrix.CreateTranslation(myPosition);
            bbEffect.Parameters["world"].SetValue(World);
            Matrix vp = Camera.myView * Camera.myProjection;
            bbEffect.Parameters["vp"].SetValue(vp);

            bbEffect.Parameters["EyePosition"].SetValue(Camera.myPosition);

            bbEffect.Parameters["lightColor"].SetValue(SunlightColor.ToVector4());
            bbEffect.Parameters["lightDir"].SetValue(new Vector3(-1, 0, -100));

            bbEffect.Parameters["floor"].SetValue(BaseCloudColor.ToVector4());
            bbEffect.Parameters["basePos"].SetValue(myPosition);

            bbEffect.Parameters["timeOfDay"].SetValue(tod);

            bbEffect.Begin(SaveStateMode.SaveState);
            for (int ps = 0; ps < bbEffect.CurrentTechnique.Passes.Count; ps++)
            {
                bbEffect.CurrentTechnique.Passes[ps].Begin();
                Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, m_billboards.Length, 0, m_billboards.Length / 2);
                bbEffect.CurrentTechnique.Passes[ps].End();
            }
            bbEffect.End();

            Game.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
        }
        /// <summary>
        /// Method to switch the drawing of bounding box's (volumes) on/off
        /// </summary>
        /// <param name="onOff">true is on, false is off</param>
        public void SetDrawBounds(bool onOff)
        {
            DrawBoundingBox = onOff;
        }
        public void Rotate(Vector3 axis, float angle)
        {
            axis = Vector3.Transform(axis, Matrix.CreateFromQuaternion(myRotation));
            myRotation = Quaternion.Normalize(Quaternion.CreateFromAxisAngle(axis, angle) * myRotation);
        }
        public void Translate(Vector3 distance)
        {
            myPosition += Vector3.Transform(distance, Matrix.CreateFromQuaternion(myRotation));
        }
        public void Revolve(Vector3 target, Vector3 axis, float angle)
        {
            Rotate(axis, angle);
            Vector3 revolveAxis = Vector3.Transform(axis, Matrix.CreateFromQuaternion(myRotation));
            Quaternion rotate = Quaternion.CreateFromAxisAngle(revolveAxis, angle);
            myPosition = Vector3.Transform(target - myPosition, Matrix.CreateFromQuaternion(rotate));
        }
    }

OK, a fair bit in there, but it looks complex because of the splitting of billboard and pint sprite particles, other than that is it a pretty strait forward particle emitter. There are some areas of note here though...

AddCloud() & AddClouds()

This adds a cloud to the manager. All it does is increment the overall particle count and adds the cloud to the cloud list.

AddToCloudSprites()

This method populates the master particle list with the clouds that have been added.

SortClouds()

Now, this bit might be interesting, this is where the draw order is sorted. I started off with a bubble sort to do this, but guess what? It was very, very slow. Now I lack the education to implement great sorting algorithms (some suggested by Leaf) so I used my ingenuity and decided to use the sort method of the List object to do my dirty work. You will see there is a reference to a class called distData, this is a class that I derived from IComparer and holds my sorting logic. You will find it in the Utility.cs source file. So using that I just get the List.Sort method to do all the work for me, great!

And that is about it, pretty simple stuff I know, looks good though don't it. Wait....I am missing the vital bit that brings all this stuff together, the shader.

I am just going to put up the billboard shader as it and the point sprite shader are very similar.

half4x4 world : World;
half4x4 vp : ViewProjection;
half3 EyePosition : CAMERAPOSITION;

half3 worldUp = half3(0,1,0);
half4 floor = float4(0,0,0,1);

half4 lightColor = half4(1,1,1,1);
half3 lightDir;
half3 basePos;

half timeOfDay;

half MistingDistance = 25;

const half2 imgageUV[16] = {
        half2(0,0),half2(.25,0),half2(.50,0),half2(.75,0),
        half2(0,.25),half2(.25,.25),half2(.50,.25),half2(.75,.25),
        half2(0,.50),half2(.25,.50),half2(.50,.50),half2(.75,.50),
        half2(0,.75),half2(.25,.75),half2(.50,.75),half2(.75,.75)};

texture partTexture;
sampler partTextureSampler = sampler_state 
{ 
    Texture = <partTexture>; 
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;
};

struct VertexIn
{
    half4 Position       : POSITION0;             
    half2 TextureCoords: TEXCOORD0;    
    half4 Color        : COLOR0;
    half4 Extras : POSITION1;
    half4 Extras2 : POSITION2;
};
struct VertexOut
{
    half4 Position       : POSITION0;      
    half2 TextureCoords: TEXCOORD0;
    half4  Color        : COLOR0;
    half image : COLOR1;
    half lightLerp : TEXCOORD2;
};

struct PixelToFrame
{
    half4 Color : COLOR0;
};

half4 GetTexture(half2 texCoord,half img)
{
    texCoord = (texCoord * .25) +  imgageUV[round(img * 100.0f)];
    return tex2D(partTextureSampler, texCoord);
}
VertexOut VS(VertexIn input)
{
    VertexOut Out = (VertexOut)0;
    
    half3 center = mul(input.Position,world);    
    half3 eyeVector = center - EyePosition;
    
    half3 finalPos = center;
    half3 sideVector;
    half3 upVector;    
    
    sideVector = normalize(cross(eyeVector,worldUp));            
    upVector = normalize(cross(sideVector,eyeVector));    
    
    finalPos += (input.TextureCoords.x - 0.5) * sideVector * input.Extras.y;
    finalPos += (0.5 - input.TextureCoords.y) * upVector * (input.Extras.z);    
    
    half4 finalPos4 = half4(finalPos,1);    
    
    Out.Position = mul(finalPos4,vp);
    Out.TextureCoords = input.TextureCoords;
    
    Out.Color = input.Color;    
    
    // Which sprite to draw...
    Out.image = input.Extras.x;
    
    // Alpha
    Out.Color.a = input.Extras.w;
    
    // Misting    
    half3 dist = abs(finalPos4 - EyePosition);
    half3 dist2 = abs(center - EyePosition);
    half distVal = dist.x + dist.y + dist.z;    
    half distVal2 = dist2.x + dist2.y + dist2.z;    
    
    if(distVal <= MistingDistance)
        Out.Color.a *= distVal / MistingDistance;        
        
    if(distVal2 <= MistingDistance)
        Out.Color.a *= distVal2 / MistingDistance;    
        
    return Out;
}

PixelToFrame PS(VertexOut input)
{
    PixelToFrame Out = (PixelToFrame)0;
    
    half color = GetTexture(input.TextureCoords,input.image).rgb;    
    
    if(timeOfDay <= 12)
        lightColor *= timeOfDay / 12;            
    else
        lightColor *= (timeOfDay - 24) / -12;                        
    
    lightColor += .5;
    
    Out.Color = input.Color * lightColor;    
    
    //Out.Color = input.Color;
    
    Out.Color.a *= color;
    
    // Draw lighter as we go down the texture.
    Out.Color.a *= 1-input.TextureCoords.y;

    return Out;
}

technique Go
{
    pass P0 
    {
        VertexShader = compile vs_2_0 VS();
        PixelShader  = compile ps_2_0 PS();
    }
}

 

I am quite chuffed with this shader as it is all my own work, I must be learning something :)

What is going on here? Well, in the Vertex shader, as before we are orientating the board to face the camera and scaling it, then we ready the tex coords and the color, then set the image index, this is the sprite on the sprite sheet to be used, when you look at the sprite sheet the index works like this:

0   1   2   3 

4   5   6   7 

8   9   10 11

12 13 14 15

How am I getting a single sprite off the one sheet of 16 sprites? This is done in the GetTexture function, what I am doing here is taking the texcoord and manipulating it to give me the required uv value on the sprite sheet for the given index. This manipulation is done by quartering the texcoord value (the sprite sheet is 4x4 sprites) then adding the uv value I have set up in my imageUV array for the given image index. You can see this is getting multiplied by 100, and in the code the value gets divided by 100, I had tried to pass this as an int but it never worked out so, I force it to be a float/half to 2 decimal places. Once I have this modified texccord I can get the image from the sprite sheet.

Then depending on the distance of the camera from the sprite we fade it, this is done so as you approach a mass of cloud it thins out giving a misting effect.

Then in the Pixel shader I get the rgb values from the sprite sheet for the given sprite index (remember they are alpha sprites), then, as I had integrated it with my SkySphere it calcs the shade of the cloud based on time of day and sets the color of the cloud based on base cloud color and the ambient light color then the alpha is set using the rgb from the sprite sheet. I then thin the cloud out from top to bottom giving the sprite a wispy effect. Also, you will notice in the sprite sheet assets pipeline properties I have set it so it is resized to the power of two, this speeds it up a little. You may notice on your PC or laptop that as you get close to a cloud or view a lot of clouds your FPS will drop (if you have a regular card like me), this is due to your card reaching it's fill rate, on the Xxbox 360 you don't seem to get this issue.

In the Game class you will see I have set up 3 skyTypes, this is just to show how you can have varying sky's with the system, you can do SO much more than I have in this sample, but I guess I will show you that in later posts.

And so that is about it....that is my basic volumetric cloud system. I hope you have as much fun using it as I have had creating and posting about it. Hope it lives up to your expectations too. This is not the end of it though, I will keep posting on it's evolution and giving code updates. There are a few of us working on this system now and I have set up an SVN so we can improve it, Kyle Hayward aka GraphicsRunner is helping out with the lighting and other design elements as is Michael Hansen over at EvoFX Studio we are hoping to get it integrated into his XNA games engine as well as Sora.

As ever, don't see this as a finished entity, it is a WIP, also if you do use it in your game then please give me a shout, would love to see it used. And do remember if you use this for a commercial venture DO NOT USE THE SPRITE SHEET, CREATE YOUR OWN! Again, read the license for this asset here.

Download the cloud solution here.


Posted Thu, Oct 2 2008 11:58 AM by Nemo Krad

Comments

Adam wrote re: Volumetric Clouds - Source
on Thu, Oct 2 2008 2:19 PM

Thanks a lot for posting this. The eye candy is quite nice! I know what I am doing tomorrow..... picking through your code 8)

Sharky wrote re: Volumetric Clouds - Source
on Thu, Oct 2 2008 8:45 PM

WOW!

I can't wait to try this out.  Thanks for sharing the source.  Would be an excellent addition to my game.

:P

Nemo Krad wrote re: Volumetric Clouds - Source
on Thu, Oct 2 2008 8:57 PM

Hope so,  I love air legends too!!

If you need any help, give me a shout, sure you will be fine though :D

Peter wrote re: Volumetric Clouds - Source
on Thu, Oct 2 2008 9:21 PM

For me it is XNA Component of a Year ... :) ... THANK YOU!!!!

Nemo Krad wrote re: Volumetric Clouds - Source
on Thu, Oct 2 2008 9:52 PM

LOL! Thank you Peter, if only they had awards eh....lol

Lintford Pickle wrote re: Volumetric Clouds - Source
on Fri, Oct 3 2008 8:13 PM

This looks amazing, thanks for posting the source code.  I too will be looking through it tomorrow :)

Astror Enales wrote re: Volumetric Clouds - Source
on Sat, Oct 4 2008 12:05 AM

Hello Nemo

thanks for this wonderfull eye candy.

it looks really cool and i really enjoyed to fly through the clouds myself, watching no video ;)

hope anyone will reward you for this in some way

go on with your work

Regards from Germany

GameDevKicks.com wrote Volumetric Clouds - Source
on Tue, Oct 7 2008 6:52 PM

You've been kicked (a good thing) - Trackback from GameDevKicks.com

Michael Hansen wrote re: Volumetric Clouds - Source
on Wed, Oct 8 2008 5:53 PM

Nice verry nice

The source code of my engine will so be avelibe to you all

sg wrote re: Volumetric Clouds - Source
on Wed, Oct 29 2008 5:04 AM

I'm wondering why you refer to this method as "Volumetric"? While it does look nice, it is not a volumetric approach. It is only layered billboards with alpha transparency. If you place any object in the middle of the clouds, you will quickly see that it is not volumetric - the billboards will be clipped and the effect will be instantly lost.

Nemo Krad wrote re: Volumetric Clouds - Source
on Wed, Oct 29 2008 8:51 AM

I guess I am out on my semantics.

To me at the time I considered it volumetric as the billboards/pointsprites sit within a volume (bounding box).

Sorry if this has mislead you into thinking it is something else.

sg wrote re: Volumetric Clouds - Source
on Sun, Nov 2 2008 7:13 AM

:) No apologies necessary. It was great to see your method, and very helpful for learning.

I would like to point out, however, that you can GREATLY improve your efficiency by moving the GetTexture function the Vertex Shader.

With your current code, the GetTexture lookup is called from the Pixel Shader. round() is a very expensive operation, especially if you cover your entire viewport with a cloud billboard.. that is 1280 * 720 round operations! You can do the exact same calculation once per vertex of the billboard, then pass these texture coordinates directly to the pixel shader where they will be interpolated.

This also eliminates the need to pass the image # to the pixel shader.

Nemo Krad wrote re: Volumetric Clouds - Source
on Mon, Nov 3 2008 10:17 AM

sg, thanks for that :)

It was on my list of todo's (long list), it will be in the next cloud update.

Real-Time Rendering » Blog Archive » This and That wrote Real-Time Rendering &raquo; Blog Archive &raquo; This and That
on Fri, Nov 21 2008 3:54 AM

Pingback from  Real-Time Rendering  &raquo; Blog Archive   &raquo; This and That

Game Development Blog wrote Volumetric Clouds
on Thu, Apr 16 2009 8:05 AM

Volumetrische Wolken sehen fantastisch aus und verleihen einem Spiel das i-Tüpfelchen. Kommerzielle Titel wie Crysis machen von dieser Technik ebenfalls Gebrauch, um realistisch aussehende Wolken darzustellen und der Umgebungsbeleuchtung anzupassen. Nemo

omar farhanah wrote re: Volumetric Clouds - Source
on Sat, Apr 18 2009 1:23 PM

How do I create clouds with rain, with thunder and lightning Lahud and storms

In the remote island

Sandstorm wrote re: Volumetric Clouds - Source
on Sat, Apr 18 2009 1:45 PM

As I was studying your code, I found something that I would like to ask about:

in the function:

public virtual void LoadContent()

{      

       rnd = new Random((int)DateTime.Now.Ticks);

       Thread.Sleep(15);

       BuildCloud();

}

What is the Sleep(15) for?

Thanks!

Nemo Krad wrote re: Volumetric Clouds - Source
on Sat, Apr 18 2009 3:50 PM

Omar,

Lots of billboards, some post processing and some nice sound effects.

Sandstorm,

As you can see I am using the time for the random seed, this sleep ensures that the seed is different each time. I will have to look at the code again to remind me why, but that's what the sleep is for in that case.

Matt wrote re: Volumetric Clouds - Source
on Thu, Jun 3 2010 10:55 PM

Hello Nemo,

I am trying to integrate your code in a prototype that I am developing as a way to generate Space clouds.

The problem is that I have been trying to make them visible, and until i am only able to see them very small. Almost invisible when compared with planets and stars.

I have been trying to change the parameters as to render them much more larger, but until know no success.

How can I scale everything up?

Do you have an email where I can contact you?

Regards,

Matt

Nemo Krad wrote re: Volumetric Clouds - Source
on Fri, Jul 30 2010 7:25 PM

Matt,

Sorry for the late reply, PM me here and I can see what I can do to help :)