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;
}
{
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;
}
}
{
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);
}
}
{
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();
}
}
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.
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
ReplyDeleteYes, 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...
DeleteLink should be fixed now, give it a go ;)
Delete