Friday 6 July 2012

Volumetric Clouds - Source



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 friendchr0n1x. 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.

2 comments:

  1. I would really like to download the "Volumetric Clouds - Source" but the link is wrong, I think?

    http://xna-uk.net/files/folders/randomchaos/entry2221.aspx

    ReplyDelete
    Replies
    1. This is an archive site, so most of the links to source are not right, ill get this one fixed in the next day or so.

      You can check out my latest version of the cloud code here
      http://randomchaosxnaadventures.blogspot.co.uk/2012/07/volumetric-clouds-in-xna-40_11.html

      Hope that helps in the mean time.

      Delete