Tuesday 3 July 2012

3D Billboard Particles Tutorial VI



So, we have seen PointSprite particles in 3D, but what about billboarding??

You may be asking at this point, what is billboarding and why would I want to use it when I already have a nice PoitSprite system that does the job I want...?

A billboard object is basically a textured quad, again the question "What is a textured quad?" Well this is two triangle primitives used to draw a square and has a texture applied to it. It gets the name quad as it is four points in space rather than three.

As to why use them, well I can only tell you my experiences of the two, first off I go for a PointSprite system pretty much every time when it comes to effects as it tends to be faster and 9 times out of 10 will do all that you need to, but in the case of the cloud system I am doing my best to write, Leaf pointed out that what is described as the "Parting of the Red Sea" effect by Ninian in here paper can be avoided by using billboarding. I assume because the position of a PointSprite is indeed that a single point in space, where as a billboard is a textured quad so has five points, I say five as you have each corner plus the centre and so when passing through the camera does not give that odd parting effect, also I think you are limited to a maximum PointSprite size, this can be altered in the RenderState using PointSizeMax but this too has an upper limit, a billboard does not. So like in the CC sample for billboarding, they can be used for grass and trees or like my cloud system.....clouds. Also, if a point sprite system does all that you need, then don't worry about billboarding.

The main issues you have with creating billboards is keeping them facing the camera in the way you want them to, now you could do this how I used to in my old RandomChaos Engine and orientate each billboard on the CPU, this, as you can imagine is not a very good way of doing things as it just kills your FPS once you have a decent number of particles. The best way, just like in the CC billboard sample is to do it in the shader.

Onto some code, and I will start where I did with the other tutorials with the particle definition, this is pretty much identical to the PointSprite definition, but we do need to add a TEXCOORD field to it, if we don't then we will not be able to apply textures to the billboard correctly. I guess if you really needed it you could also add Tangent data here if you wanted to bumpmap your particles, I have done this in the past, but it looked a little odd, may well have been my lighting algorithms though. Anyway, here is the data structure we are going to use for our billboard particle system

public struct BillboardParticleElement
{
Vector3 position;
Vector2 textureCoordinate;
Color color;
Vector4 data;

public Vector3 Position
{
get { return position; }
set { position = value; }
}
public Vector2 TextureCoordinate
{
get { return textureCoordinate; }
set { textureCoordinate = value; }
}
public Vector4 Data
{
get { return extras; }
set { data = value; }
}
public Color Color
{
get { return color; }
set { color = value; }
}

public float SpriteSize
{
get { return data.X; }
set { data.X = value; }
}
public float AlphaValue
{
get { return data.Y; }
set { data.Y = value; }
}

public BillboardParticleElement(Vector3 Position, Vector2 TextureCoordinate, Vector2 XYScale, Color Color)
{
position = Position;
textureCoordinate = TextureCoordinate;
data = Vector4.One;
color = Color;
}
static public VertexElement[] VertexElements = new VertexElement[]
{
new VertexElement(0,0,VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Position,0),
new VertexElement(0,4*3,VertexElementFormat.Vector2,VertexElementMethod.Default,VertexElementUsage.TextureCoordinate,0),
new VertexElement(0,4*5,VertexElementFormat.Color ,VertexElementMethod.Default,VertexElementUsage.Color,0),
new VertexElement(0,4*6,VertexElementFormat.Vector4,VertexElementMethod.Default,VertexElementUsage.Position,1),
};

public static int SizeInBytes = (3 + 2 + 1 + 4) * 4;
}

As you can see there is little difference in this structure and the one used for the PointSprite system. I have added a Vector2 for the TEXCOORD field and so in the Element array I have added an extra element so it can be passed to the Vertex Shader.

Now onto the emitter, this again is almost identical to the one in tutorial I, only this time I am going to set up up ready to be bolted into our game Agent. There area couple of new elements to go into this emitter, we need a DynamicVertexBuffer to store our particle array in, and a DynamicIndexBuffer to store the draw order of the primitives, and a VertexDeclaration, this gives the Graphics Device a definition of our particle structure. All the other fields are pretty much the same as what we have in the previous tutorials, the real difference comes when we are loading, updating and drawing the particles. The emitter class looks like this

public class ParticleEmitter
{
private DynamicVertexBuffer vb;
private DynamicIndexBuffer ib;
VertexDeclaration m_vDec;

public BillboardParticleElement[] particleArray;

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

public Color particleColor;

public int partCount;

Effect shader;

Game game;

int nextParticle = 0;
BasicModel targetPos;
Vector3 myLastpos;

public ParticleEmitter(Game game, int particleCount, BasicModel model)
{
this.game = game;
myPosition = Vector3.Zero;
myScale = Vector3.One;
myRotation = new Quaternion(0, 0, 0, 1);

partCount = particleCount;
targetPos = model;

particleColor = Color.White;
}

public void LoadContent()
{
m_vDec = new VertexDeclaration(game.GraphicsDevice, BillboardParticleElement.VertexElements);

shader = game.Content.Load<Effect>("Shaders/BillboardShader");
shader.Parameters["particleTexture"].SetValue(game.Content.Load<Texture2D>("Textures/smoke"));


myLastpos = targetPos.myPosition;

LoadParticles();
}

private void LoadParticles()
{
particleArray = new BillboardParticleElement[partCount * 4];

for (int p = 0; p < partCount; p += 4)
{
for (int thisP = 0; thisP < 4; thisP++)
{
int currentParticle = p + thisP;

particleArray[currentParticle] = new BillboardParticleElement();
particleArray[currentParticle].Position = myPosition;
particleArray[currentParticle].Color = particleColor;
particleArray[currentParticle].Data = new Vector4(1f, 1f, 0, 0);
switch (thisP)
{
case 0:
particleArray[currentParticle].TextureCoordinate = Vector2.Zero;
break;
case 1:
particleArray[currentParticle].TextureCoordinate = new Vector2(1, 0);
break;
case 2:
particleArray[currentParticle].TextureCoordinate = new Vector2(0, 1);
break;
case 3:
particleArray[currentParticle].TextureCoordinate = Vector2.One;
break;
}
}
}

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

for (int part = 0; part < partCount; 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);
}

ib = new DynamicIndexBuffer(game.GraphicsDevice, typeof(short), 6 * partCount, BufferUsage.WriteOnly);
ib.SetData(indices);
}

public void Update(GameTime gameTime)
{
for (int p = 0; p < particleArray.Length; p += 4)
{
for (int thisP = 0; thisP < 4; thisP++)
{
if (p == nextParticle && myLastpos != myPosition)
{
particleArray[p + thisP].Position = myLastpos;
particleArray[p + thisP].Color = particleColor;
}

particleArray[p + thisP].SpriteSize = (Vector3.Distance(particleArray[p + thisP].Position, targetPos.myPosition) / 5);
}
}
nextParticle++;

if (nextParticle >= particleArray.Length / 4)
nextParticle = 0;

vb = new DynamicVertexBuffer(game.GraphicsDevice, typeof(BillboardParticleElement), 4 * partCount, BufferUsage.WriteOnly);
vb.SetData(particleArray);

myLastpos = targetPos.myPosition;
}
public void Draw(GameTime gameTime)
{
game.GraphicsDevice.VertexDeclaration = m_vDec;
game.GraphicsDevice.Vertices[0].SetSource(vb, 0, BillboardParticleElement.SizeInBytes);
game.GraphicsDevice.Indices = ib;

bool AlphaBlendEnable = game.GraphicsDevice.RenderState.AlphaBlendEnable;
Blend DestinationBlend = game.GraphicsDevice.RenderState.DestinationBlend;
Blend SourceBlend = game.GraphicsDevice.RenderState.SourceBlend;
bool DepthBufferWriteEnable = game.GraphicsDevice.RenderState.DepthBufferWriteEnable;
BlendFunction BlendFunc = game.GraphicsDevice.RenderState.BlendFunction;

game.GraphicsDevice.RenderState.AlphaBlendEnable = true;
game.GraphicsDevice.RenderState.SourceBlend = Blend.One;
game.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

game.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

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

shader.Begin();
for (int ps = 0; ps < shader.CurrentTechnique.Passes.Count; ps++)
{
shader.CurrentTechnique.Passes[ps].Begin();
game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 4 * partCount, 0, partCount * 2);
shader.CurrentTechnique.Passes[ps].End();
}
shader.End();

game.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;

game.GraphicsDevice.RenderState.AlphaBlendEnable = AlphaBlendEnable;

game.GraphicsDevice.RenderState.DestinationBlend = DestinationBlend;
game.GraphicsDevice.RenderState.SourceBlend = SourceBlend;

}
}

The first obvious change is in the LoadContent method, other than the assets beign from different files at the top we set up the VertexDeclarartion. LoadParticles is TOTALY different. This is because we have to set up a billboard per particle, so at the top we have to set our array to the number of particles we want * 4 as each bill board has 4 corners, then we have the main loop that go through each particle and in that a second loop that moves us through each point of that particle.

For the given array element we create a new particle instance, the position is set to that of the emitter for each corner, don't worry about this as the billboards size will be set in the shader. We set the color and the data of the particle setting the scale and alpha (unsing additive blending in this example so alpha wont be used).

We then need to set the TEXCOORD of this corner, as I am sure you know TEXTCOORDS range from 0,0 which is the top left corner to 1,1 which is the bottom right corner. So if I had a texture that was 100 x 100 pixels and I wanted to get the pixel in the very centre (well just off centre) it would be located at .5,.5 this would get me pixel 50,50 off the image.

We then set up the draw order of our billboard corners in the DynamicIndexBuffer, you will see I am using a short[] array to store this data, this is because I have a very poor Graphics Card that can't handle 32 bit index buffers, if you have a good card, swap these types for an int. You will also notice that we have 6 indices per particle, this is because the bill board is made up from 2 triangles.

The Update method is really the same as before only we have to jump 4 elements per particle as we now have 4 elements per particle. The major change here is the addition of setting the DynamicVertexBuffer
The Draw method again has few changes, at the top we set the VertexDeclaration, bind the vertex buffer and index buffer to the device, setup our blending method and shader, send the particles to the device and reset our RenderStates.

So as you can see the major difference is setting up the Graphics device to use our particle array and the fact that our particles are now made from 4 corners.

In this tutorial I have gone strait to binding the emitter to a game agent, for simplicity I have taken the BasicModel class I did for my A.I. Finite State Machine sample and removed the A.I. from it, added an instance of our emitter and that was pretty much it. The BasicModel class now looks like this

public class BasicModel : DrawableGameComponent
{
private Model Mesh;
public Vector3 myPosition;
public Quaternion myRotation;
public Vector3 myScale;

ParticleEmitter emitter;

Effect shader;

string modelAsset;
string shaderAsset;

Vector4 AmbientColor = Color.Brown.ToVector4();

public Vector3 velocity = Vector3.Zero;
public float speed = .01f;

public BasicModel(Game game, string modelAsset, string shaderAsset)
: base(game)
{
this.modelAsset = modelAsset;
this.shaderAsset = shaderAsset;

myPosition = new Vector3(0, 0, -1);
myRotation = new Quaternion(0, 0, 0, 1);
float scale = .5f;
myScale = new Vector3(scale, scale, scale);

emitter = new ParticleEmitter(game, 200, this);

}
public override void Update(GameTime gameTime)
{
velocity = new Vector3((float)Math.Cos(gameTime.TotalGameTime.TotalSeconds) * 20, (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 20, (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 30);
Move();
emitter.Update(gameTime);

base.Update(gameTime);
}
protected override void LoadContent()
{
emitter.LoadContent();
Mesh = Game.Content.Load<Model>(modelAsset);
shader = Game.Content.Load<Effect>(shaderAsset);

base.LoadContent();
}
public override void Draw(GameTime gameTime)
{
Matrix World = Matrix.CreateScale(myScale) *
Matrix.CreateFromQuaternion(myRotation) *
Matrix.CreateTranslation(myPosition);

shader.Parameters["World"].SetValue(World);
shader.Parameters["View"].SetValue(Camera.myView);
shader.Parameters["Projection"].SetValue(Camera.myProjection);

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

shader.Parameters["AmbientLightColor"].SetValue(AmbientColor);
shader.Parameters["DiffuseColor"].SetValue(Color.Aquamarine.ToVector4());
shader.Parameters["LightDiffuseColor"].SetValue(Color.SteelBlue.ToVector4());
shader.Parameters["SpecularPower"].SetValue(16);
shader.Parameters["LightSpecularColor"].SetValue(Color.Silver.ToVector4());

shader.Parameters["LightPosition"].SetValue(new Vector3(-10, 10, 0));

for (int pass = 0; pass < shader.CurrentTechnique.Passes.Count; pass++)
{
for (int msh = 0; msh < Mesh.Meshes.Count; msh++)
{
ModelMesh mesh = Mesh.Meshes[msh];
for (int prt = 0; prt < mesh.MeshParts.Count; prt++)
mesh.MeshParts[prt].Effect = shader;
mesh.Draw();
}
}

emitter.Draw(gameTime);
base.Draw(gameTime);
}

public virtual void Translate(Vector3 distance)
{
myPosition += Vector3.Transform(distance, Matrix.CreateFromQuaternion(new Quaternion(0, 0, 0, 1)));
}
public virtual void Rotate(Vector3 axis, float angle)
{
axis = Vector3.Transform(axis, Matrix.CreateFromQuaternion(myRotation));
myRotation = Quaternion.Normalize(Quaternion.CreateFromAxisAngle(axis, angle) * myRotation);
}
public void LookAt(Vector3 target, float speed)
{
Vector3 tminusp = target - myPosition;
Vector3 ominusp = Vector3.Backward;

tminusp.Normalize();

float theta = (float)System.Math.Acos(Vector3.Dot(tminusp, ominusp));
Vector3 cross = Vector3.Cross(ominusp, tminusp);

cross.Normalize();

Quaternion targetQ = Quaternion.CreateFromAxisAngle(cross, theta);
myRotation = Quaternion.Slerp(myRotation, targetQ, speed);
}

private void Move()
{
// Calulate the distance I need to travel.
Vector3 distance = velocity * speed;

// Find out my direction of facing as I move.
Vector3 target = Vector3.Transform(distance, Matrix.CreateFromQuaternion(new Quaternion(0, 0, 0, 1)));
target += myPosition;
LookAt(target, .1f);

// Move.
Translate(distance);

}
}

All we need to do now is add our agent to the Game.Components collection and away we go.

HLSL

OK now as you can see in all that code, not once do we try and make the billboard face the camera, this is all done in the shader. This is how I do it.

First of all I get me world UP vector, this tells me what orientation is the UP vector, I do this by getting the cross product of -1,0,0 and 0,0,-1, I then need the eye vector of the viewer and I can get this from the View elements of the ViewProjection Matrix like this vp._m02_m12_m22, I can then get the side and up vector of the billboard orientation by getting the normalized cross produce of the eyeVector and the world up vector. I can then apply this to the final position of my Vertex. This is done in two steps, the first manages the sideVector orientation, I subtract .5 from the current TEXCOORD.x multiply that by the sideVector and the scale I want the particle to be, the second step I subtract the current TEXCCORD.y from .5 and multiply that by the upVector and the scale I want the particle to be. As you can see with that methods you can scale both the X and Y dimensions of your billboard differently.

Why am I subtracting .5 from the TEXCOORD in the first stage and the TEXCOORD from .5 in the second stage. Well, as I said before .5 puts us in the centre of our billboard so for the side orientation the left most point with be at x:0 so giving us -.5 to add to our particle position, this will give us a point in space .5 to the left of the particle, the right most will be x:1 so this will give us the right most point from the centre in space. In the case of the second stage, subtracting .5 from the TEXCOORD puts us at -.5 above the centre, and from the other extream it puts us .5 bellow that centre.

The rest of the shader is pretty much as before, other than the extra handling TEXCOORDS

half4x4 world : World;
half4x4 vp : ViewProjection;

#define worldUp cross(half3(-1,0,0),half3(0,0,-1))

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

struct VertexIn
{
half4 Position : POSITION0;
half2 TextureCoords: TEXCOORD0;
half4 Color : COLOR0;
half4 Data : POSITION1;
};
struct VertexOut
{
half4 Position : POSITION0;
half2 TextureCoords: TEXCOORD0;
half4 Color : COLOR0;
};

struct PixelToFrame
{
half4 Color : COLOR0;
};
VertexOut VS(VertexIn input)
{
VertexOut Out = (VertexOut)0;

half3 center = mul(input.Position,world);
half3 eyeVector = vp._m02_m12_m22;

half3 finalPos = center;
half3 side;
half3 up;

side = normalize(cross(eyeVector,worldUp));
up = normalize(cross(side,eyeVector));

finalPos += (input.TextureCoords.x - 0.5) * side * input.Data.x;
finalPos += (0.5 - input.TextureCoords.y) * up * input.Data.x;

half4 finalPos4 = half4(finalPos,1);

Out.Position = mul(finalPos4,vp);
Out.TextureCoords = input.TextureCoords;

Out.Color = input.Color;

// Alpha
Out.Color.a = input.Data.y;

return Out;
}

PixelToFrame PS(VertexOut input)
{
PixelToFrame Out = (PixelToFrame)0;

half2 texCoord;

texCoord = input.TextureCoords.xy;

half4 color = tex2D(partTextureSampler,texCoord);

Out.Color = color * input.Color;

return Out;
}

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

Here is another example of billboards being used, the code and sahders are a little more work than shown here but it is pretty much the same technique.



In the sample source I'm using the smoke texture provided by MS, and the biplane model that comes with the DirectX SDK. You can download the solution project here.

3 comments:

  1. I noticed that the link to the finished program doesn't work anymore - are you intending to change the link in future? I would very much like to see the source code :s

    ReplyDelete
    Replies
    1. Yes, lots of links are broke on this archive, I fix them as people find them and want the code, still have it all on my server... ill repair this one and then post here to let you know it's up. It's a very old sample this...

      Delete
    2. Link should be fixed now, give it a go ;)

      Delete