Monday 9 July 2012

Light Cycles, Ribbons and Windows Phone 7


I put a clip up the other day of the ribbon emitter I was playing with, so here is the code and blog post as promised.

The idea behind the ribbon emitter was to be able to lay down a length of connected particles, normally when we use a regular particle emitter if the velocity of the emitter is quite high you get gaps in the particles emitted rather than the nice particle stream you get at lower velocities. I guess you could compensate for this by lerping from the last particle laid to the emitters current position and then loading the path with particles in-between, but this would add more calculations and vertices to the render, a solution, but not the best.

With the ribbon emitter I have currently written, as the emitter moves I just bind to the end 2 vertices and move those with the emitter, if the emitter changes direction, I add another 4 vertices (a quad) to the end and bind the end two again to the emitter. Here is a clip of the ribbon in wireframe showing the first stages of the ribbon




As you can see, there are just 4 vertices used so far, when the cycles change direction, the old vertices are left behind and 4 new ones are added, and the last two are picked up again




If you run the solution in windows and hit ‘P’ to pause it, zoom up to the selected cycle you can use the keypad keys to rotate and translate the cycle, doing this will result in more of the sections being laid as the cycle does a tight turn like this:




And here is is in wire frame so you can see them better:




It’s really as simple as that, only thing I have not implemented is a life span for each of the sections (quads), but I am sure you don’t want me writing everything for you do you… So, lets look at the code.

Ribbon Class


I’ll start with the ribbon class, all it is really is a manager for the vertex array, so we have just that, in this case I decided to use the VertexPositionNormalTexture intrinsic XNA vertex element. I then created a method called AddSection, which accepts a Vector3 and a Quaternion for rotation. The new quad will start at the position of the last end vertices in the last quad and the last two of the new quad will be bound to the emitter.

public void AddSection(Vector3 position, Quaternion rotation)
{
Vector3 TopStart = position + ((Vector3.Up + Vector3.Left) * .5f);
Vector3 TopEnd = position + ((Vector3.Up + Vector3.Right) * .5f);
Vector3 BottomStart = position + ((Vector3.Down + Vector3.Left) * .5f);
Vector3 BottomEnd = position + ((Vector3.Down + Vector3.Right) * .5f);
VertexPositionNormalTexture[] vertsOld = verts;
short[] indexOld = index;
TopEnd = Vector3.Transform(TopEnd, Matrix.CreateFromQuaternion(rotation));
BottomEnd = Vector3.Transform(BottomEnd, Matrix.CreateFromQuaternion(rotation));
if (vertsOld != null)
{
verts = new VertexPositionNormalTexture[vertsOld.Length + 4];
vertsOld.CopyTo(verts, 0);
BottomStart = vertsOld[vertsOld.Length - 4].Position;
TopStart = vertsOld[vertsOld.Length - 1].Position;
// Bottom Right
verts[vertsOld.Length + 0] = new VertexPositionNormalTexture();
verts[vertsOld.Length + 0].Position = BottomEnd;
// Bottom Left
verts[vertsOld.Length + 1] = new VertexPositionNormalTexture();
verts[vertsOld.Length + 1].Position = BottomStart;
// Top Left
verts[vertsOld.Length + 2] = new VertexPositionNormalTexture();
verts[vertsOld.Length + 2].Position = TopStart;
// Top Right
verts[vertsOld.Length + 3] = new VertexPositionNormalTexture();
verts[vertsOld.Length + 3].Position = TopEnd;
index = new short[indexOld.Length + 6];
indexOld.CopyTo(index, 0);
}
else
{
verts = new VertexPositionNormalTexture[4];
// Bottom Right
verts[0] = new VertexPositionNormalTexture();
verts[0].Position = BottomEnd;
// Bottom Left
verts[1] = new VertexPositionNormalTexture();
verts[1].Position = BottomStart;
// Top Left
verts[2] = new VertexPositionNormalTexture();
verts[2].Position = TopStart;
// Top Right
verts[3] = new VertexPositionNormalTexture();
verts[3].Position = TopEnd;
index = new short[6];
}
// Normals
verts[verts.Length - 4].Normal = Vector3.Normalize(Vector3.Cross(verts[verts.Length - 4].Position - verts[verts.Length - 1].Position, verts[verts.Length - 4].Position - verts[verts.Length - 3].Position));
verts[verts.Length - 3].Normal = Vector3.Normalize(Vector3.Cross(verts[verts.Length - 3].Position - verts[verts.Length - 4].Position, verts[verts.Length - 3].Position - verts[verts.Length - 2].Position));
verts[verts.Length - 2].Normal = Vector3.Normalize(Vector3.Cross(verts[verts.Length - 2].Position - verts[verts.Length - 3].Position, verts[verts.Length - 2].Position - verts[verts.Length - 1].Position));
verts[verts.Length - 1].Normal = Vector3.Normalize(Vector3.Cross(verts[verts.Length - 1].Position - verts[verts.Length - 2].Position, verts[verts.Length - 1].Position - verts[verts.Length - 4].Position));
// Texcooreds
verts[verts.Length - 4].TextureCoordinate = new Vector2(1, 1);
verts[verts.Length - 3].TextureCoordinate = new Vector2(0, 1);
verts[verts.Length - 2].TextureCoordinate = new Vector2(0, 0);
verts[verts.Length - 1].TextureCoordinate = new Vector2(1, 0);
if (indexOld != null)
{
for (int i = 0; i < indexBlock.Length; i++)
{
index[(indexOld.Length) + i] = (short)(indexBlock[i] + (4 * (Count - 1)));
}
}
else
index = indexBlock;
vb = new DynamicVertexBuffer(Game.GraphicsDevice, typeof(VertexPositionNormalTexture), verts.Length, BufferUsage.WriteOnly);
vb.SetData(verts);
}

A chunky function, but all it is really doing is adding 4 new vertices to the vertex array each time it’s called, first it copies any old vertices into the old array, extends the vertex array by 4 for the new quad, then sets the position of the two starting points of each quad so it is in the same place as the ends of vertices in the old list, then updates the index array accordingly. Once the new vertex positions are in the array I then calculate their normals and then set their tex coords, then reset the vertex buffer.

The class also has Top and Bottom fields to denote where the leading edge’s top and bottom vertices should be, so in the Update method I ensure that the last two vertices in the array are at the corresponding positions.

public override void Update(GameTime gameTime)
{
#if WINDOWS
effect.Parameters["world"].SetValue(Matrix.Identity);
effect.Parameters["wvp"].SetValue(Matrix.Identity * camera.View * camera.Projection);
effect.Parameters["CameraPosition"].SetValue(camera.Position);
#endif
#if WINDOWS_PHONE
effect.World = Matrix.Identity;
effect.View = camera.View;
effect.Projection = camera.Projection;
effect.AmbientLightColor = AmbientColor.ToVector3();
effect.DiffuseColor = DiffuseColor.ToVector3();
effect.DirectionalLight0.Direction = Vector3.Left;
effect.LightingEnabled = true;
effect.Projection = camera.Projection;
effect.SpecularPower = 35;
#endif
if (verts != null)
{
verts[verts.Length - 1].Position = Top;
verts[verts.Length - 4].Position = Bottom;
}
}

Then comes the Draw method which is pretty strait forward really

public override void Draw(GameTime gameTime)
{
if (vb != null)
{
Game.GraphicsDevice.RasterizerState = RasterizerState.CullNone;
Game.GraphicsDevice.BlendState = BlendState.Opaque;
Game.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
#if WINDOWS
//Game.GraphicsDevice.DepthStencilState = DepthStencilState.None;
effect.Parameters["DiffuseColor"].SetValue(DiffuseColor.ToVector4());
effect.Parameters["AmbientColor"].SetValue(AmbientColor.ToVector4());
#endif
Game.GraphicsDevice.SetVertexBuffer(vb);
effect.CurrentTechnique.Passes[0].Apply();
Game.GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, verts, 0, verts.Length, index, 0, verts.Length / 2);
}
}

And that’s the ribbon class.

HUD Controls


I created the HUD controls so that on the phone you can control the camera, what cycle you are looking at and so on, this was pretty strait forward to do, first of all creating a set of fields so I can render the sprites to the screen

public int Top;
public int Left;
public int Width;
public int Height;
public Color Tint;
public string Sprite;
Texture2D texture;
Vector2 Origin;
Rectangle target;
Rectangle source;
Rectangle bounds;
public float Rotation;

Then adding in some fields to manage touches and mouse clicks

public string Name;
public ClickEvent OnClick;
Rectangle HIRec;
MouseState lastState;
public bool FullClick = true;
#if WINDOWS_PHONE
TouchCollection touches;
int clickID = 0;
#endif

I also made the spriteBatch in the Game1.cs a public field so I could access it in here too, as well as set up the constructor so that elements got set and ensured the sprite is always drawn last, or has a very high draw order at the very least

private SpriteBatch spriteBatch
{
get { return ((Game1)Game).spriteBatch; }
}
public HUDButton(Game game,string sprite,string name, int left, int top, int width, int height, Color tint,float rotation) : base(game)
{
Top = top;
Left = left;
Width = width;
Height = height;
Tint = tint;
Sprite = sprite;
Rotation = rotation;
Name = name;
// Always want to draw the hud last.
DrawOrder = 9999;
}

When loading the content I calculate the various elements needed for the sprite, click bounds, it’s origin etc.

protected override void LoadContent()
{
base.LoadContent();
texture = Game.Content.Load<Texture2D>(Sprite);
Origin = new Vector2(texture.Width, texture.Height) * .5f;
target = new Rectangle(Left + Width / 2, Top + Height / 2, Width, Height);
bounds = new Rectangle(Left, Top, Width, Height);
source = new Rectangle(0, 0, texture.Width, texture.Height);
}

The update method simply manages the click and touch events

public override void Update(GameTime gameTime)
{
base.Update(gameTime);
#if WINDOWS_PHONE
touches = TouchPanel.GetState();
if (touches.Count == 0)
clickID = -1;
foreach (TouchLocation tl in touches)
{
HIRec = new Rectangle((int)tl.Position.X, (int)tl.Position.Y, 16, 16);
if (tl.State != TouchLocationState.Released && tl.State != TouchLocationState.Invalid && clickID != tl.Id)
{
if(FullClick)
clickID = tl.Id;
#endif
#if WINDOWS
MouseState ms = Mouse.GetState();
bool clicked = false;
if (!FullClick)
{
if (ms.LeftButton == ButtonState.Pressed)
clicked = true;
}
else
{
if (ms.LeftButton == ButtonState.Pressed && lastState.LeftButton == ButtonState.Released)
clicked = true;
}
if (clicked)
{
HIRec = new Rectangle(ms.X, ms.Y, 1, 1);
#endif
if (bounds.Intersects(HIRec))
{
if (OnClick != null)
{
OnClick(this);
}
}
#if WINDOWS_PHONE
}
#endif
}
#if WINDOWS
lastState = ms;
#endif
}

And again a very simple draw call

public override void Draw(GameTime gameTime)
{
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
spriteBatch.Draw(texture, target, source, Tint, Rotation, Origin, SpriteEffects.None, 1);
spriteBatch.End();
}


Other Objects


As you will see I have my regular objects in this solution for the camera and to render a 3D object, so I wont bother going into those :D

In “Game”


In the constructor I just create 4 instances of the ribbon class along with 4 cycles to be used as the emitters, as well as create 4 sets of paths for the cycles so they can go from place to place, then set up the HUD and wire up the events for the buttons and that’s about it…

I feel a TRON game is a MUST do now, just need to find the time to do it :P

NOTE:
I have not applied a texture to the ribbon, but I am sure it’s pretty obvious that it will display an image per quad, and also stretch it as the quad elongates, this too can be compensated for, but this implementation doesn’t.


Down load the source project here

No comments:

Post a Comment