/*
* QuadRenderer.cs
* Authors: August Zinsser
*
* Copyright August Zinsser 2007
* This program is distributed under the terms of the GNU General Public License
*/
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Pina3D
{
///
/// Renders billboarded 3D quads in the same world space as ColladaModels.
/// Client classes request a quad is rendered during the next render
/// cycle with the RenderOnce(...) function.
/// This request only applies to the next frame, not all future frames. Does not use the Shader
/// class as that is geared for assisting 3D model rendering and these quads need to minimize impact on the system load.
///
public static class QuadRenderer
{
///
/// Stores positional and texture data. Does not use Materials or Shaders since the particle system that relies on this class
/// must be as lean as possible for the sake of performance.
///
private class Quad
{
public Texture2D Texture;
public Vector2 UVUpperLeft;
public Vector2 UVLowerRight;
public Vector3 Position;
public float Rotation;
public float Width;
public float Height;
public Color Color;
private static VertexPositionTexture[] mVerts; // A square
private static ushort[] mInds; // Indices to draw the square as 2 tris
///
/// Create a new billboarded sprite
///
public Quad(ref Texture2D texture, Vector2 uvUpperLeft, Vector2 uvLowerRight, Vector3 position, float width, float height, float rotation, Color color)
{
Texture = texture;
UVUpperLeft = uvUpperLeft;
UVLowerRight = uvLowerRight;
Position = position;
Rotation = rotation;
Width = width;
Height = height;
Color = color;
if (mVerts == null)
{
mVerts = new VertexPositionTexture[]
{
new VertexPositionTexture(new Vector3(-1f,-1f,0f), new Vector2(0f,1f)),
new VertexPositionTexture(new Vector3(1f,-1f,0f), new Vector2(1f,1f)),
new VertexPositionTexture(new Vector3(1f,1f,0f), new Vector2(1f,0f)),
new VertexPositionTexture(new Vector3(-1f,1f,0f), new Vector2(0f,0f))
};
mInds = new ushort[] { 2, 1, 0, 0, 3, 2 };
}
}
///
/// Define the data to send to the vertex buffer. Note that the "Up" vector for a sprite is being passed as the "Normal" element.
///
public static VertexElement[] VertexElements
{
get
{
VertexElement[] dec = new VertexElement[]
{
new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
new VertexElement(0, 12, VertexElementFormat.Single, VertexElementMethod.Default, VertexElementUsage.Normal, 0),
new VertexElement(0, 16, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.PointSize, 0),
new VertexElement(0, 24, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
new VertexElement(0, 32, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.Color, 0),
};
return dec;
}
}
///
/// Size of all 4 vertices
///
///
public static int SizeInBytesAllVerts()
{
return 4 * SizeInBytesOneVert();
}
///
/// Size of one vertex
///
///
public static int SizeInBytesOneVert()
{
// 4 bytes per float at 1 Vector4, 1 Vector3, 1 float and 2 Vector2s.
return 4 * 12;
}
///
/// Returns the raw vertex data, used to send to the vertex buffer
///
public float[] GetRawVertexArray()
{
int stride = 12; // 12 floats
float[] rawArray = new float[stride * mVerts.Length];
for (int i = 0; i < mVerts.Length; i++)
{
int j = 0;
rawArray[i * stride + j++] = Position.X;
rawArray[i * stride + j++] = Position.Y;
rawArray[i * stride + j++] = Position.Z;
rawArray[i * stride + j++] = Rotation;
rawArray[i * stride + j++] = Width * mVerts[i].Position.X;
rawArray[i * stride + j++] = Height * mVerts[i].Position.Y;
rawArray[i * stride + j++] = mVerts[i].TextureCoordinate.X == 0? UVUpperLeft.X : UVLowerRight.X;
rawArray[i * stride + j++] = mVerts[i].TextureCoordinate.Y == 0? UVUpperLeft.Y : UVLowerRight.Y;
rawArray[i * stride + j++] = (float)this.Color.R / 255f;
rawArray[i * stride + j++] = (float)this.Color.G / 255f;
rawArray[i * stride + j++] = (float)this.Color.B / 255f;
rawArray[i * stride + j++] = (float)this.Color.A / 255f;
}
return rawArray;
}
///
/// Returns the indices for the specified quad.
///
public static ushort[] GetRawIndexArray(int quadOffset)
{
int offset = quadOffset * mVerts.Length;
return new ushort[]
{
(ushort)(mInds[0] + offset),
(ushort)(mInds[1] + offset),
(ushort)(mInds[2] + offset),
(ushort)(mInds[3] + offset),
(ushort)(mInds[4] + offset),
(ushort)(mInds[5] + offset),
};
}
}
/////
///// Stores a texture, as well as if it was used recently
/////
//private struct TouchedTexture
//{
// public Texture2D Texture;
// public bool Touched;
// public TouchedTexture(bool touched, Texture2D texture)
// {
// Touched = touched;
// Texture = texture;
// }
//}
private static Effect mSpriteEffect; // Use a simple Effect instead of the Shader
private static VertexBuffer mVertexBuffer; // To send data to the graphics device
private static IndexBuffer mIndexBuffer; // ''
private static VertexDeclaration mVertDec; // Rendering setting
private static List mQuads; // All quads to draw on the next frame
//private static List mTextures; // The textures in use
public static VertexDeclaration VertexDeclaration { get { return mVertDec; } }
///
/// Constructor
///
static QuadRenderer()
{
// Bypass the Shader wrapper
mSpriteEffect = Shader.LoadEmbeddedFXFile("Sprite.fx");
// TODO: set this on init and if the particle budget changes, instead of each frame? Research VertexBuffer to find out more
//mVertexBuffer = new VertexBuffer(...
mVertDec = new VertexDeclaration(Pina.Graphics.GraphicsDevice, Quad.VertexElements);
mQuads = new List();
}
///
/// Draws this Quad on the next render cycle. Can render multiple Quads per cycle.
///
/// A reference to a texture
/// Where to render the quad (object space)
/// The width of the quad (object space)
/// The height of the quad (object space)
/// The camera-forward-aligned rotation in radians
/// /// RGBA Color
public static void RenderOnNextFrame(ref Texture2D texture, Vector3 position, float width, float height, float rotation, Color color)
{
RenderOnNextFrame(ref texture, Vector2.Zero, Vector2.One, position, width, height, rotation, color);
}
///
/// Draws this Quad on the next render cycle. Can render multiple Quads per cycle.
///
/// Upper left corner in UV coordinates to use
/// Lower right corner in UV coordinates to use
/// Where to render the quad (object space)
/// The width of the quad (object space)
/// The height of the quad (object space)
/// The camera-forward-aligned rotation in radians
/// RGBA Color
public static void RenderOnNextFrame(ref Texture2D texture, Vector2 uvUpperLeft, Vector2 uvLowerRight, Vector3 position, float width, float height, float rotation, Color color)
{
// Divide the size by 2 since the shader uses width and height as offsets from the center point
width *= .5f;
height *= .5f;
mQuads.Add(new Quad(ref texture, uvUpperLeft, uvLowerRight, position, width, height, rotation, color));
}
///
/// Renders all Quad requested by calls to RenderOnNextFrame(). All requests are then cleared, meaning objects must re-request a render
/// for the next frame if necessary.
///
internal static void RenderAllNow()
{
// There are a couple magic numbers in this function. Vertex Count = 6, Index Count = 4. As long as the square doesn't decide to change
// anytime soon, these magic numbers aren't a big deal.
if (mQuads.Count == 0)
return;
Pina.Graphics.GraphicsDevice.VertexDeclaration = mVertDec;
int numQuads = mQuads.Count;
// Gather all of the data into one big float array
float[] vertArray = new float[numQuads * Quad.SizeInBytesAllVerts() / 4];
ushort[] indArray = new ushort[numQuads * 6];
int qIndex = 0;
int vIndex = 0;
int iIndex = 0;
for (int i = 0; i < numQuads; i++)
{
float[] thisQuadVerts = mQuads[i].GetRawVertexArray();
for (int j = 0; j < thisQuadVerts.Length; j++)
{
vertArray[vIndex++] = thisQuadVerts[j];
}
ushort[] thisQuadInds = Quad.GetRawIndexArray(qIndex);
for (int j = 0; j < thisQuadInds.Length; j++)
{
indArray[iIndex++] = thisQuadInds[j];
}
qIndex++;
}
// TODO: possibly set this on init and if the particle budget changes, instead of each frame (see constructor)
mVertexBuffer = new VertexBuffer(
Pina.Graphics.GraphicsDevice,
Quad.SizeInBytesAllVerts() * numQuads,
ResourceUsage.WriteOnly,
ResourceManagementMode.Automatic);
mVertexBuffer.SetData(vertArray);
mIndexBuffer = new IndexBuffer(
Pina.Graphics.GraphicsDevice,
typeof(ushort),
indArray.Length,
ResourceUsage.WriteOnly,
ResourceManagementMode.Automatic);
mIndexBuffer.SetData(indArray);
// Set Effect params
mSpriteEffect.Parameters["xViewMat"].SetValue(Pina.ViewMatrix);
mSpriteEffect.Parameters["xProjMat"].SetValue(Pina.ProjectionMatrix);
mSpriteEffect.Parameters["xTexture"].SetValue(mQuads[0].Texture);
mSpriteEffect.CurrentTechnique = mSpriteEffect.Techniques["Billboard"];
mSpriteEffect.CommitChanges();
mSpriteEffect.Begin();
// TODO: Sort quads by texture and set the effect's texture param once per texture
// For now, just assume they all use the first texture
// Manually generate the effect passes, rather than get those from a Shader
List passes = new List();
for (int i = 0; i < mSpriteEffect.CurrentTechnique.Passes.Count; i++)
passes.Add(mSpriteEffect.CurrentTechnique.Passes[i]);
for (int i = 0; i < passes.Count; i++)
{
passes[i].Begin();
Pina.Graphics.GraphicsDevice.Vertices[0].SetSource(mVertexBuffer, 0, Quad.SizeInBytesOneVert());
Pina.Graphics.GraphicsDevice.Indices = mIndexBuffer;
Pina.Graphics.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
0,
0,
numQuads * 4,
0,
iIndex / 3);
passes[i].End();
}
mSpriteEffect.End();
mQuads.Clear(); // Possible optimization: Don't clear and re-add the quads each time. Remove on an as-needed basis? Need to research the List structure to figure it out.
}
}
}