Generic Mesh

From XNAWiki
Jump to: navigation, search

Much of this is designed for XNA 3.1, but I don't see why it couldn't be ported to XNA 4.0 with minimum modification.

Required supporting code.

VertexElementAttribute - used to create vertex declarations by marking up a vertex structure with VertexElement attributes.

/// <summary>
/// An attribute to use inside of vertex structures to mark the data that will be streamed to
/// the video card at draw time. The static methods provide a means of finding or building the
/// VertexElements array and VertexDeclaration depending on the vertex structure's contents.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class VertexElementAttribute : Attribute
{
    static Dictionary<Type, VertexElement[]> ms_cachedElements = new Dictionary<Type, VertexElement[]>();
    static Dictionary<Type, VertexElementFormat> ms_format = new Dictionary<Type, VertexElementFormat>();
 
    static VertexElementAttribute()
    {
        // 32 bit floating point types
        ms_format[typeof(float)] = VertexElementFormat.Single;
        ms_format[typeof(Vector2)] = VertexElementFormat.Vector2;
        ms_format[typeof(Vector3)] = VertexElementFormat.Vector3;
        ms_format[typeof(Vector4)] = VertexElementFormat.Vector4;
 
        // 16 bit floating point types
        ms_format[typeof(HalfVector2)] = VertexElementFormat.HalfVector2;
        ms_format[typeof(HalfVector4)] = VertexElementFormat.HalfVector4;
 
        // 64 bpp unsigned normalized
        ms_format[typeof(Rgba64)] = VertexElementFormat.Rgba64;
 
        // 32 bpp unsigned normalized
        ms_format[typeof(Color)] = VertexElementFormat.Color;
        ms_format[typeof(Rgba32)] = VertexElementFormat.Rgba32;
        ms_format[typeof(Rg32)] = VertexElementFormat.Rg32;
 
        // signed normalized
        ms_format[typeof(NormalizedShort2)] = VertexElementFormat.NormalizedShort2;
        ms_format[typeof(NormalizedShort4)] = VertexElementFormat.NormalizedShort4;
        ms_format[typeof(Normalized101010)] = VertexElementFormat.Normalized101010;
 
        // signed integer
        ms_format[typeof(Short2)] = VertexElementFormat.Short2;
        ms_format[typeof(Short4)] = VertexElementFormat.Short4;
 
        // unsigned integer
        ms_format[typeof(Byte4)] = VertexElementFormat.Byte4;
        ms_format[typeof(UInt101010)] = VertexElementFormat.UInt101010;
    }
 
    /// <summary>
    /// Creates the vertex elements for a given vertex type which either impliments 
    /// VertexElementAttributes or contains a VertexElements field/property.
    /// </summary>
    public static VertexElement[] GetVertexElements(Type vertexType)
    {
        // the type must be a value type for it to be accepted by a vertex buffer
        if (!vertexType.IsValueType)
            throw new ArgumentException("Only value types (structs) are valid to be " +
                                        "placed within a vertex buffer.",
                                        "vertexType");
 
        // if the type is already cached, pull it from the cache
        if (ms_cachedElements.ContainsKey(vertexType))
            return ms_cachedElements[vertexType];
        else
        {
            // check if the type has a VertexElements array defined, if so cache it so that
            //  it will not need to be reflected next time
            FieldInfo fieldVertexElements = vertexType.GetField("VertexElements");
            if (fieldVertexElements != null)
            {
                ms_cachedElements[vertexType] = (VertexElement[])fieldVertexElements.GetValue(null);
                return ms_cachedElements[vertexType];
            }
 
            PropertyInfo propVertexElements = vertexType.GetProperty("VertexElements");
            if (propVertexElements != null)
            {
                ms_cachedElements[vertexType] = (VertexElement[])propVertexElements.GetValue(null, null);
                return ms_cachedElements[vertexType];
            }
        }
 
        // if it gets this far build the vertex elements for the type based on attributes
        List<VertexElementAttribute> objAttributes = new List<VertexElementAttribute>();
        GetVertexAttributes(vertexType, objAttributes);
 
        // need the attribute mark-ups to do anything with the data at this point
        if (objAttributes.Count < 1)
            throw new ArgumentException("The type provided does not expose a VertexElements " +
                                        "static field/property, nor does it have any fields " +
                                        "marked with the VertexElementAttribute."
                                        , "vertexType");
 
        // the following is to calculate the usage index, example POSITION0, POSITION1, etc...
        List<VertexElement> elements = new List<VertexElement>();
        Dictionary<VertexElementUsage, int> usages = new Dictionary<VertexElementUsage, int>();
        for (int a = 0; a < objAttributes.Count; a++)
        {
            // if the element usage counter doen't appear then add it as 0 index
            if (!usages.ContainsKey(objAttributes[a].Usage))
                usages.Add(objAttributes[a].Usage, 0);
 
            // create the vertex element and increment the usage counter for it
            elements.Add(new VertexElement((short)objAttributes[a].Stream, (short)objAttributes[a].Offset,
                                           objAttributes[a].Format, objAttributes[a].Method,
                                           objAttributes[a].Usage, (byte)usages[objAttributes[a].Usage]++));
        }
 
        // cache the data gathered here
        VertexElement[] elementArray = elements.ToArray();
        ms_cachedElements.Add(vertexType, elementArray);
 
        return elementArray;
    }
    /// <summary>
    /// Creates the vertex declaration for a given vertex type which either impliments 
    /// VertexElementAttributes or contains a VertexElements field/property.
    /// </summary>
    public static VertexDeclaration CreateVertexDeclaration(GraphicsDevice device, Type vertexType)
    {
        return new VertexDeclaration(device, GetVertexElements(vertexType));
    }
 
    static VertexElementAttribute[] GetVertexAttributes(VertexElement[] elements)
    {
        VertexElementAttribute[] attributes = new VertexElementAttribute[elements.Length];
        for (int i = 0; i < elements.Length; i++)
        {
            attributes[i] = new VertexElementAttribute(elements[i].VertexElementUsage)
            {
                Format = elements[i].VertexElementFormat,
                Method = elements[i].VertexElementMethod,
                Offset = elements[i].Offset,
                Stream = elements[i].Stream,
                Usage = elements[i].VertexElementUsage
            };
        }
        return attributes;
    }
    static void GetVertexAttributes(Type vertexType, List<VertexElementAttribute> objAttributes)
    {
        FieldInfo[] fields = vertexType.GetFields(BindingFlags.NonPublic |
                                                  BindingFlags.Public |
                                                  BindingFlags.Instance);
        for (int f = 0; f < fields.Length; f++)
        {
            // attempt to retrieve the vertex element attribute from the current field
            object[] attributes = fields[f].GetCustomAttributes(typeof(VertexElementAttribute), false);
 
            // if the open and close markers aren't -1 then it's a property link, 
            //  unroll the property to find the element attribute data
            int openIndex = fields[f].Name.IndexOf('<'),
                closeIndex = fields[f].Name.IndexOf('>');
            if (openIndex != -1 && closeIndex != -1)
            {
                string propertyName = fields[f].Name.Substring(openIndex + 1, closeIndex - openIndex - 1);
                PropertyInfo property = vertexType.GetProperty(propertyName, fields[f].FieldType);
                if (property != null)
                    attributes = property.GetCustomAttributes(typeof(VertexElementAttribute), false);
            }
 
            // if the length of the attributes is less than 1, then this field doesn't have the VertexElementAttribute
            if (attributes.Length > 0)
            {
                // the following is to map Unused formats into their respective proper formats, 
                //  otherwise the byte offsets wont match up and wreck havok on the gpu streamers
                VertexElementAttribute vertexElementAttribute = (VertexElementAttribute)attributes[0];
                if (vertexElementAttribute.Format == VertexElementFormat.Unused &&
                    ms_format.ContainsKey(fields[f].FieldType))
                    vertexElementAttribute.Format = ms_format[fields[f].FieldType];
 
                vertexElementAttribute.Offset = Marshal.OffsetOf(vertexType, fields[f].Name).ToInt32();
                objAttributes.Add(vertexElementAttribute);
            }
            else 
            {
                // recurse into sub-types
                Type fieldType = fields[f].FieldType;
                if (ms_cachedElements.ContainsKey(fieldType))
                    objAttributes.AddRange(GetVertexAttributes(ms_cachedElements[fieldType]));
                else if (fieldType.IsValueType && !fieldType.IsPrimitive &&
                         fieldType.Namespace != null &&
                         !fieldType.Namespace.StartsWith("System.") &&
                         !fieldType.Namespace.StartsWith("Microsoft."))
                    GetVertexAttributes(fieldType, objAttributes);
            }
        }
    }
 
    /// <summary>
    /// Stream number or index of use.
    /// </summary>
    public int Stream { get; set; }
    /// <summary>
    /// Offset in bytes from the start of the stream to this element's position.
    /// </summary>
    public int Offset { get; set; }
    /// <summary>
    /// The method that the video card will use to interpolate over the vertices.
    /// </summary>
    public VertexElementMethod Method { get; set; }
    /// <summary>
    /// The format of data that will be read by the attribute
    /// </summary>
    public VertexElementFormat Format { get; set; }
    /// <summary>
    /// The format of data that will be output by the attribute
    /// </summary>
    public VertexElementUsage Usage { get; set; }
 
    /// <summary>
    /// Create a new VertexElementAttribute.
    /// </summary>
    /// <param name="usage">The format of data that will be read by the attribute.</param>
    public VertexElementAttribute(VertexElementUsage usage)
    {
        Usage = usage;
        Format = VertexElementFormat.Unused;
    }
    /// <summary>
    /// Create a new VertexElementAttribute.
    /// </summary>
    /// <param name="usage">The format of data that will be read by the attribute.</param>
    /// <param name="format">The format of data that will be output by the attribute.</param>
    public VertexElementAttribute(VertexElementUsage usage, VertexElementFormat format)
    {
        Usage = usage;
        Format = format;
    }
}

RenderOperation - a handy base class for establishing common render functionality.

/// <summary>
/// A delegate used for per-pass callbacks from a render operation.
/// </summary>
/// <param name="pass">The effect pass that will be performed after the callback.</param>
/// <param name="renderOp">The render operation that is being drawn.</param>
public delegate void EffectPassCallback(EffectPass pass, RenderOperation renderOp);
 
/// <summary>
/// An abstract class which handles the basic function structure for an object which can be rendered.
/// </summary>
abstract public class RenderOperation : IDisposable
{
    /// <summary>
    /// Load graphical content this render operation requires.
    /// </summary>
    /// <param name="device"></param>
    abstract public void LoadContent(GraphicsDevice device);
    /// <summary>
    /// Unload any graphical content this render operation has loaded.
    /// </summary>
    abstract public void UnloadContent();
 
    /// <summary>
    /// Called before this render operation draws into the buffers to ensure all preparations
    /// for rendering are completed successfully.
    /// </summary>
    /// <param name="device"></param>
    /// <returns>True if it's ok to perform the render, false if any problems were encountered
    /// that will prevent this object from being rendered properly.</returns>
    abstract protected bool PreRender(GraphicsDevice device);
    /// <summary>
    /// Called when this render operation should draw it's contents.
    /// </summary>
    /// <param name="device"></param>
    abstract protected void Render(GraphicsDevice device);
    /// <summary>
    /// Called after rendering has completed.
    /// </summary>
    /// <param name="device"></param>
    abstract protected void PostRender(GraphicsDevice device);
 
    /// <summary>
    /// Draw the contents of this render operation onto the current render target.
    /// </summary>
    /// <param name="device"></param>
    public void Draw(GraphicsDevice device)
    {
        try
        {
            if (PreRender(device))
            {
                Render(device);
                PostRender(device);
            }
        }
        catch (Exception e) { throw e; }
    }
    /// <summary>
    /// Draw the contents of this render operation onto the current render target.
    /// </summary>
    /// <param name="effect"></param>
    public void Draw(Effect effect) 
    {
        Draw(effect, SaveStateMode.None);
    }
    /// <summary>
    /// Draw the contents of this render operation onto the current render target.
    /// </summary>
    /// <param name="effect"></param>
    /// <param name="saveStateMode"></param>
    public void Draw(Effect effect, SaveStateMode saveStateMode)
    {
        try
        {
            if (PreRender(effect.GraphicsDevice))
            {
                effect.Begin(saveStateMode);
                for (int i = 0; i < effect.CurrentTechnique.Passes.Count; i++)
                {
                    effect.CurrentTechnique.Passes[i].Begin();
                    Render(effect.GraphicsDevice);
                    effect.CurrentTechnique.Passes[i].End();
                }
                effect.End();
                PostRender(effect.GraphicsDevice);
            }
        }
        catch (Exception e) { throw e; }
    }
    /// <summary>
    /// Draw the contents of this render operation onto the current render target.
    /// </summary>
    /// <param name="effect"></param>
    /// <param name="callback"></param>
    public void Draw(Effect effect, EffectPassCallback callback)
    {
        Draw(effect, SaveStateMode.None, callback);
    }
    /// <summary>
    /// Draw the contents of this render operation onto the current render target.
    /// </summary>
    /// <param name="effect"></param>
    /// <param name="saveStateMode"></param>
    /// <param name="callback"></param>
    public void Draw(Effect effect, SaveStateMode saveStateMode, EffectPassCallback callback)
    {
        try
        {
            if (PreRender(effect.GraphicsDevice))
            {
                effect.Begin(saveStateMode);
                for (int i = 0; i < effect.CurrentTechnique.Passes.Count; i++)
                {
                    callback(effect.CurrentTechnique.Passes[i], this);
 
                    effect.CurrentTechnique.Passes[i].Begin();
                    Render(effect.GraphicsDevice);
                    effect.CurrentTechnique.Passes[i].End();
                }
                effect.End();
                PostRender(effect.GraphicsDevice);
            }
        }
        catch (Exception e) { throw e; }
    }
 
    #region IDisposable Members
 
    ~RenderOperation() 
    { 
        Dispose(false);
    }
 
    /// <summary>
    /// Dispose of this render operation and all of the data it has loaded.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    /// <summary>
    /// Called when this render operation is being disposed of.
    /// </summary>
    /// <param name="disposing">Should managed resources be disposed of as well?</param>
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            UnloadContent();
        }
    }
 
    #endregion
}

Mesh<VertexType> generic class, for non-indexed meshes. Primitive count will be determined automatically based on the vertex count and primitive type.

/// <summary>
/// A basic mesh which provides a wrapper around the vertex data required for drawing a
/// 3D object.
/// </summary>
/// <typeparam name="VertexType">A structure which either exposes a VertexElements static array,
/// or has fields marked with VertexElementAttributes.</typeparam>
public class Mesh<VertexType> : RenderOperation
    where VertexType : struct
{
    protected VertexType[] m_vertexData;
 
    protected VertexDeclaration m_vertexDeclaration;
    protected VertexBuffer m_vertexBuffer;
 
    protected PrimitiveType m_primitiveType;
    protected int m_vertexStride, m_vertexCount, m_primitiveCount;
 
    /// <summary>
    /// Gets the type of primitives contained in this mesh.
    /// </summary>
    public PrimitiveType PrimitiveType { get { return m_primitiveType; } }
    /// <summary>
    /// Gets the number of vertices contained in this mesh.
    /// </summary>
    public int VertexCount { get { return m_vertexCount; } }
    /// <summary>
    /// Gets the number of primitives contained in this mesh.
    /// </summary>
    public int PrimitiveCount { get { return m_primitiveCount; } }
 
    /// <summary>
    /// Should a copy of the mesh data be stored in system memory even after the buffer(s) have
    /// been populated with that data?
    /// (default = false)
    /// </summary>
    public bool ShadowCopy;
 
    /// <summary>
    /// Create a new Mesh.
    /// </summary>
    /// <param name="primitiveType">Type of primitives stored in the mesh.</param>
    /// <param name="vertexData">Data that will be placed in the vertex buffer.</param>
    public Mesh(PrimitiveType primitiveType, VertexType[] vertexData)
    {
        m_primitiveType = primitiveType;
 
        if (vertexData == null)
            throw new ArgumentNullException("vertexData");
 
        m_vertexData = vertexData;
        m_vertexCount = m_vertexData.Length;
        m_vertexStride = VertexDeclaration.GetVertexStrideSize(VertexElementAttribute.GetVertexElements(typeof(VertexType)), 0);
 
        switch (m_primitiveType)
        {
            case PrimitiveType.TriangleList: m_primitiveCount = m_vertexCount / 3; break;
            case PrimitiveType.TriangleStrip: m_primitiveCount = m_vertexCount - 2; break;
            case PrimitiveType.TriangleFan: m_primitiveCount = (m_vertexCount - 1) / 2; break;
 
            case PrimitiveType.LineList: m_primitiveCount = m_vertexCount / 2; break;
            case PrimitiveType.LineStrip: m_primitiveCount = m_vertexCount - 1; break;
 
            case PrimitiveType.PointList: m_primitiveCount = m_vertexCount; break;
 
            default: m_primitiveCount = 0; break;
        }
    }
 
    /// <summary>
    /// Sets the vertex data into the vertex buffer if one has been created or into system
    /// memory for later loading if no vertex buffer has been created yet.
    /// </summary>
    /// <param name="vertexData">An array equivalent in size to the vertex allocation when
    /// this instance was created.</param>
    public virtual void SetVertexData(VertexType[] vertexData)
    {
        if (vertexData.Length != m_vertexCount)
            throw new ArgumentOutOfRangeException("vertexData", "The array provided is either " +
                                                  "too large or too small to fit within the " +
                                                  "vertex buffer.");
 
        if (m_vertexBuffer == null || ShadowCopy)
            m_vertexData = vertexData;
 
        if (m_vertexBuffer != null)
        {
            m_vertexBuffer.SetData<VertexType>(vertexData);
 
            if (!ShadowCopy)
                vertexData = null;
        }
    }
 
    public override void LoadContent(GraphicsDevice device)
    {
        if (m_vertexDeclaration == null || m_vertexDeclaration.IsDisposed)
        {
            m_vertexDeclaration = VertexElementAttribute.CreateVertexDeclaration(device, typeof(VertexType));
            m_vertexStride = m_vertexDeclaration.GetVertexStrideSize(0);
        }
 
        if (m_vertexBuffer == null || m_vertexBuffer.IsDisposed)
        {
            m_vertexBuffer = new VertexBuffer(device, m_vertexStride * m_vertexCount, BufferUsage.WriteOnly);
            SetVertexData(m_vertexData);
        }
    }
    public override void UnloadContent()
    {
        if (m_vertexDeclaration != null)
        {
            m_vertexDeclaration.Dispose();
            m_vertexDeclaration = null;
        }
        if (m_vertexBuffer != null)
        {
            if (!m_vertexBuffer.IsDisposed && ShadowCopy)
                m_vertexBuffer.GetData<VertexType>(m_vertexData);
 
            m_vertexBuffer.Dispose();
            m_vertexBuffer = null;
        }
    }
 
    protected override bool PreRender(GraphicsDevice device)
    {
        device.VertexDeclaration = m_vertexDeclaration;
        device.Vertices[0].SetSource(m_vertexBuffer, 0, m_vertexStride);
        return true;
    }
    protected override void Render(GraphicsDevice device)
    {
        device.DrawPrimitives(m_primitiveType, 0, m_primitiveCount);
    }
    protected override void PostRender(GraphicsDevice device)
    {
        device.VertexDeclaration = null;
        device.Vertices[0].SetSource(null, 0, 0);
    }
 
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (disposing)
        {
            m_vertexData = null;
        }
    }
}

Mesh<VertexType, IndexType> - same as above, but for indexed meshes instead. This time primitive count is determined based on the index count and primitive type.

/// <summary>
/// A basic mesh which provides a wrapper around the vertex and index data required for 
/// drawing a 3D object.
/// </summary>
/// <typeparam name="VertexType">A structure which either exposes a VertexElements static array,
/// or has fields marked with VertexElementAttributes.</typeparam>
/// <typeparam name="IndexType">A integral type used for integers, to save memory this should
/// be ushort if there is less than or equal to ushort.MaxValue of vertices.</typeparam>
public class Mesh<VertexType, IndexType> : Mesh<VertexType>
    where VertexType : struct
    where IndexType : struct
{
    protected IndexType[] m_indexData;
    protected IndexBuffer m_indexBuffer;
    protected int m_indexCount;
 
    /// <summary>
    /// Gets the number of indicies contained in this mesh.
    /// </summary>
    public int IndexCount { get { return m_indexCount; } }
 
    /// <summary>
    /// Create a new Mesh.
    /// </summary>
    /// <param name="primitiveType">Type of primitives stored in the mesh.</param>
    /// <param name="vertexData">Data that will be placed in the vertex buffer.</param>
    /// <param name="indexData">(Optional) Data that will be placed in the vertex buffer.</param>
    public Mesh(PrimitiveType primitiveType, VertexType[] vertexData, IndexType[] indexData)
        : base(primitiveType, vertexData)
    {
        if (indexData == null)
            throw new ArgumentNullException("indexData");
 
        Type indexType = typeof(IndexType);
        if (indexType != typeof(uint) && indexType != typeof(int) &&
            indexType != typeof(ushort) && indexType != typeof(short))
            throw new ArgumentException("Valid index types are: uint, int, ushort, and short.",
                                        "IndexType");
 
        m_indexData = indexData;
        m_indexCount = m_indexData.Length;
 
        switch (m_primitiveType)
        {
            case PrimitiveType.TriangleList: m_primitiveCount = m_indexCount / 3; break;
            case PrimitiveType.TriangleStrip: m_primitiveCount = m_indexCount - 2; break;
            case PrimitiveType.TriangleFan: m_primitiveCount = (m_indexCount - 1) / 2; break;
 
            case PrimitiveType.LineList: m_primitiveCount = m_indexCount / 2; break;
            case PrimitiveType.LineStrip: m_primitiveCount = m_indexCount - 1; break;
 
            case PrimitiveType.PointList: m_primitiveCount = m_indexCount; break;
 
            default: m_primitiveCount = 0; break;
        }
    }
 
    /// <summary>
    /// Sets the index data into the index buffer if one has been created or into system
    /// memory for later loading if no index buffer has been created yet.
    /// </summary>
    /// <param name="indexData">An array equivalent in size to the index allocation when
    /// this instance was created.</param>
    public virtual void SetIndexData(IndexType[] indexData)
    {
        if (indexData.Length != m_indexCount)
            throw new ArgumentOutOfRangeException("indexData", "The array provided is either " +
                                                  "too large or too small to fit within the " +
                                                  "index buffer.");
 
        if (m_indexBuffer == null || ShadowCopy)
            m_indexData = indexData;
 
        if (m_indexBuffer != null)
        {
            m_indexBuffer.SetData<IndexType>(indexData);
 
            if (!ShadowCopy)
                m_indexData = null;
        }
    }
 
    public override void LoadContent(GraphicsDevice device)
    {
        base.LoadContent(device);
        if ((m_indexBuffer == null || m_indexBuffer.IsDisposed) && m_indexData != null)
        {
            m_indexBuffer = new IndexBuffer(device, typeof(IndexType), m_indexCount, BufferUsage.WriteOnly);
            SetIndexData(m_indexData);
        }
    }
    public override void UnloadContent()
    {
        base.UnloadContent();
        if (m_indexBuffer != null)
        {
            if (!m_indexBuffer.IsDisposed && ShadowCopy)
                m_indexBuffer.GetData<IndexType>(m_indexData);
 
            m_indexBuffer.Dispose();
            m_indexBuffer = null;
        }
    }
 
    protected override bool PreRender(GraphicsDevice device)
    {
        bool result = base.PreRender(device);
        device.Indices = m_indexBuffer;
        return result && true;
    }
    protected override void Render(GraphicsDevice device)
    {
        device.DrawIndexedPrimitives(m_primitiveType,
                                     0, 0, m_vertexCount,
                                     0, m_primitiveCount);
    }
    protected override void PostRender(GraphicsDevice device)
    {
        base.PostRender(device);
        device.Indices = null;
    }
 
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            m_indexData = null;
        }
    }
}