/Utilities/Graphics/VertexFormat.cs
C# | 655 lines | 379 code | 41 blank | 235 comment | 32 complexity | 2e3d55d54743701f61164965b1b42f51 MD5 | raw file
Possible License(s): Apache-2.0
- using System;
- using System.IO;
- using Delta.Utilities.Helpers;
- using NUnit.Framework;
-
- namespace Delta.Utilities.Graphics
- {
- /// <summary>
- /// Vertex format for GeometryData, check out VertexFormatType for details!
- /// </summary>
- /// <remarks>
- /// Interesting papers and sites about vertex compression:
- /// http://blogs.msdn.com/b/shawnhar/archive/2010/11/19/compressed-vertex-data.aspx
- /// http://gsdwrdx.tistory.com/92
- /// http://stackoverflow.com/questions/1762866/using-gl-short-instead-of-gl-float-in-an-opengl-es-vertex-array
- /// http://wiki.gamedev.net/index.php/D3DBook:(Lighting)_Per-Pixel_Lighting
- /// http://www.cs.jhu.edu/~jab/publications/vertexcompression.pdf
- /// http://www.graphcomp.com/info/specs/ibm/cgd.html
- /// http://irrlicht.sourceforge.net/docu/_i_animated_mesh_m_d3_8h_source.html
- /// http://viola.usc.edu/Publication/PDF/selected/2000_JVCIR_Cheang.pdf
- /// http://www.cse.ohio-state.edu/~hwshen/Su01_888/deering.pdf
- /// http://www.cg.tuwien.ac.at/studentwork/VisFoSe98/lm/
- /// http://personal.redestb.es/jmovill/opengl/openglonwin-15.html
- /// https://www.opengl.org/sdk/docs/man3/xhtml/glVertexAttribPointer.xml
- /// Seems like support for 16 bit floats is sometimes there, but we cannot
- /// guarantee that it works on all platforms, so we cannot use it :(
- /// Instead we use shorts to compress data into 16 bits as well, but this
- /// has sometimes some additional overhead in the vertex shader.
- /// For example Position3DCompressed | TextureUVCompressed |
- /// NormalCompressed | TangentCompressed | SkinWeightCompressed |
- /// SkinIndexCompressed is just 6+4+6+6+2+4 = 28 bytes instead of
- /// 12+8+12+12+4+4 = 52 bytes uncompressed :)
- /// <para />
- /// Note: When we use different sizes, we must make sure that we align all
- /// our components to their native alignment, e.g. floats are on 4-byte
- /// boundaries, shorts are on 2-byte boundaries. If we don't do this it will
- /// tank our performance. It might be helpful to mentally map it by typing
- /// out your attribute ordering as a struct definition so we can sanity
- /// check our layout and alignment (this is not easy). Also:
- /// * making sure your data is stripped to share vertices
- /// * using a texture atlas to reduce texture swaps (see Image)
- /// <para />
- /// Optimizing Vertex Data
- /// Each API call results in a certain amount of overhead that is caused by
- /// translating the standard API calls into commands understood by the
- /// hardware. The amount of overhead can be reduced by minimizing the
- /// translation work required by making sure that the data submitted to the
- /// API is already in a hardware friendly format. This can be achieved by
- /// following the simple recommendation when submitting vertex data: submit
- /// strip-ordered indexed triangles with per vertex data interleaved.
- /// <para />
- /// Indexed triangles (glDrawElements) is preferred over non-indexed strips
- /// (glDrawArrays). Indexing allows for a higher amount of vertex reuse. The
- /// same vertex in strips can not be used for more than 3 triangles; indexed
- /// triangles does not have this constraint. Further, OpenGL ES requires one
- /// draw call per strip. If you want to collapse draw calls for multiple
- /// strips into one, you will have to add degenerate strips in between to
- /// introduce discontinuities. This extra work is not required using indexed
- /// triangles. With strip-ordered indexed triangles, multiple disconnected
- /// strips can be drawn in a single draw call, while avoiding the cost of
- /// inserting elements to create degenerate triangles.
- /// <para />
- /// Sorting the vertex data (either using indexed triangles or non-indexed
- /// strips) is very important. Sorting allows you to generate more triangles
- /// per vertex submitted. For peak performance, sort index triangles so that
- /// connected indexed triangles can build strips. This improves memory
- /// access patterns and the usage of the various data caches.
- /// <para />
- /// Use interleaved or tightly packed vertex arrays. OpenGL ES allows vertex
- /// data elements to be spread out over memory thus resulting in scatter
- /// reads to fetch the required data. On the other hand it is also possible
- /// to interleave the data such that it is already together in memory.
- /// Interleaving of the per vertex elements improves the memory efficiency
- /// and is more logical for the processing that needs to be done. For
- /// optimal performance, interleave the individual components in an order of
- /// Position, Normal, Color, TexCoord0, TexCoord1, PointSize, Weight,
- /// MatrixIndex. Memory bandwidth is limited, so try to use the smallest
- /// acceptable type for each component. Specify vertex colors using 4
- /// unsigned byte values. Specify texture coordinates with 2 or 4 unsigned
- /// byte or short values, instead of floating-point values, if you can.
- /// </remarks>
- public sealed class VertexFormat : ISaveLoadBinary
- {
- #region Constants
- /// <summary>
- /// Version number for this VertexFormat. If this goes above 1, we need
- /// to support loading older versions as well. Saving is always going
- /// to be the latest version (this one).
- /// </summary>
- private const int VersionNumber = 1;
-
- /// <summary>
- /// Empty vertex format, currently only used for OpenGL ES 1.1, where we
- /// need to reset the vertex format after rendering else we are in a
- /// messed up state and problems in follow up code can happen. This is
- /// not required for any other OpenGL or Graphics implementation.
- /// </summary>
- public static readonly VertexFormat None =
- new VertexFormat(new VertexElement[]
- {
- });
-
- /// <summary>
- /// Used mostly for simple 2D and UI data (basic shader, no extra features)
- /// </summary>
- public static readonly VertexFormat Position2DTextured =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position2D),
- new VertexElement(VertexElementType.TextureUV),
- });
-
- /// <summary>
- /// This vertex format is used mostly for simple 3D meshes, e.g.
- /// Mesh.CreateSphere (uncompressed 3+2 floats = 20 bytes, compressed 10)
- /// </summary>
- public static readonly VertexFormat Position3DTextured =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- new VertexElement(VertexElementType.TextureUV),
- });
-
- /// <summary>
- /// Simple format for 2D points and colored vertices, used for lines.
- /// </summary>
- public static readonly VertexFormat Position2DColor =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position2D),
- new VertexElement(VertexElementType.Color),
- });
-
- /// <summary>
- /// Simple format for textured 2D drawing with vertex colors. Mostly
- /// used for effects because each vertex can have different colors.
- /// </summary>
- public static readonly VertexFormat Position2DColorTextured =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position2D),
- new VertexElement(VertexElementType.Color),
- new VertexElement(VertexElementType.TextureUV),
- });
-
- /// <summary>
- /// Simple format for 3D points and colored vertices, used for 3D lines.
- /// </summary>
- public static readonly VertexFormat Position3DColor =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- new VertexElement(VertexElementType.Color),
- });
-
- /// <summary>
- /// This vertex format is used mostly for simple 3D meshes, e.g.
- /// Mesh.CreateSphere (uncompressed 24 bytes, compressed 16)
- /// </summary>
- public static readonly VertexFormat Position3DColorTextured =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- new VertexElement(VertexElementType.Color),
- new VertexElement(VertexElementType.TextureUV),
- });
-
- /// <summary>
- /// 3D position, uv and lightmap textured vertex format for simple 3D
- /// geometry using light maps.
- /// </summary>
- public static readonly VertexFormat Position3DTexturedLightMap =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- new VertexElement(VertexElementType.TextureUV),
- new VertexElement(VertexElementType.LightMapUV),
- });
-
- /// <summary>
- /// Position textured skinned
- /// </summary>
- public static readonly VertexFormat PositionTexturedSkinned =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- new VertexElement(VertexElementType.TextureUV),
- // Skin data are always handled as compressed
- new VertexElement(VertexElementType.SkinIndices, true),
- new VertexElement(VertexElementType.SkinWeights, true),
- });
-
- /// <summary>
- /// Position skinned
- /// </summary>
- public static readonly VertexFormat PositionSkinned =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- // Skin data are always handled as compressed
- new VertexElement(VertexElementType.SkinIndices, true),
- new VertexElement(VertexElementType.SkinWeights, true),
- });
-
- /// <summary>
- /// Position normal textured skinned
- /// </summary>
- public static readonly VertexFormat PositionNormalTexturedSkinned =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D),
- new VertexElement(VertexElementType.Normal),
- new VertexElement(VertexElementType.TextureUV),
- // Skin data are always handled as compressed
- new VertexElement(VertexElementType.SkinIndices, true),
- new VertexElement(VertexElementType.SkinWeights, true),
- });
- #endregion
-
- #region Elements (Public)
- /// <summary>
- /// Elements
- /// </summary>
- public VertexElement[] Elements
- {
- get;
- private set;
- }
- #endregion
-
- #region LengthInBytes (Public)
- /// <summary>
- /// Get length in bytes, cached in GeometryData as this is used quite often
- /// and never changes.
- /// </summary>
- public int LengthInBytes
- {
- get;
- private set;
- }
- #endregion
-
- #region IsCompressed (Public)
- /// <summary>
- /// Is data in this vertex format compressed? Only will return true if
- /// all elements are compressed that can be compressed, e.g. position, uv
- /// or normal data. Color or skinning data is always in the same format
- /// (compressed), it will not be used for this check. Calculated in the
- /// constructor.
- /// </summary>
- public bool IsCompressed
- {
- get;
- private set;
- }
- #endregion
-
- #region HasNormals (Public)
- /// <summary>
- /// Has normals? Used to check if we need to compare normals for
- /// optimizing meshes.
- /// </summary>
- public bool HasNormals
- {
- get
- {
- for (int index = 0; index < Elements.Length; index++)
- {
- if (Elements[index].Type ==
- VertexElementType.Normal)
- {
- return true;
- }
- }
- return false;
- }
- }
- #endregion
-
- #region HasTangents (Public)
- /// <summary>
- /// Has tangents? Used to check if we need to generate tangents when
- /// importing meshes.
- /// </summary>
- public bool HasTangents
- {
- get
- {
- for (int index = 0; index < Elements.Length; index++)
- {
- if (Elements[index].Type ==
- VertexElementType.Tangent)
- {
- return true;
- }
- }
- return false;
- }
- }
- #endregion
-
- #region HasLightmapUVs (Public)
- /// <summary>
- /// Has lightmap UV texture coordinates? Good check for merging meshes.
- /// </summary>
- public bool HasLightmapUVs
- {
- get
- {
- for (int index = 0; index < Elements.Length; index++)
- {
- if (Elements[index].Type ==
- VertexElementType.LightMapUV)
- {
- return true;
- }
- }
- return false;
- }
- }
- #endregion
-
- #region Constructors
- /// <summary>
- /// Create vertex format
- /// </summary>
- /// <param name="setElements">Set elements</param>
- public VertexFormat(VertexElement[] setElements)
- {
- if (setElements == null) // ||
- //now allowed for None above: setElements.Length == 0)
- {
- throw new InvalidOperationException("You have to specify vertex" +
- " elements to define a VertexFormat");
- }
-
- // Set all elements, not allowed to be modified outside this constructor
- Elements = setElements;
-
- // Get length of the format in bytes and also calculate the offsets.
- LengthInBytes = 0;
- VertexElementType lastType = (VertexElementType)MathHelper.InvalidIndex;
- IsCompressed = false;
- for (int num = 0; num < Elements.Length; num++)
- {
- Elements[num].Offset = LengthInBytes;
- LengthInBytes += Elements[num].Size;
- // Ignore color and skinning elements, they are always compressed
- VertexElementType elementType = Elements[num].Type;
- if (elementType != VertexElementType.Color &&
- elementType != VertexElementType.SkinIndices &&
- elementType != VertexElementType.SkinWeights)
- {
- IsCompressed = Elements[num].IsCompressed;
- }
-
- // Show warning if user specified a strange format, we want to have
- // vertex elements sorted this way: Position, Normal, Color, TexCoords,
- // PointSize, Weight, MatrixIndex. Since VertexElementType is sorted
- // this way we just need to check if a type was going backwards.
- if (elementType < lastType)
- {
- Log.Warning("Your vertex format seems to be not optimal, the type " +
- elementType + " should be before the last type=" + lastType +
- ". Check out the VertexFormatType and the wiki for details: " +
- this);
- }
- lastType = elementType;
- }
- }
-
- /// <summary>
- /// Create vertex format from binary stream, will load all elements.
- /// </summary>
- public VertexFormat(BinaryReader reader)
- {
- Load(reader);
- }
- #endregion
-
- #region ISaveLoadBinary Members
- /// <summary>
- /// Load VertexFormat from a binary data stream.
- /// </summary>
- public void Load(BinaryReader reader)
- {
- // We currently only support our version, if more versions are added,
- // we need to do different loading code depending on the version here.
- int version = reader.ReadInt32();
- if (version != VersionNumber)
- {
- Log.InvalidVersionWarning("VertexFormat", version, VersionNumber);
- return;
- }
- IsCompressed = false;
- // Read all data in the way it was saved above
- int numberOfElements = reader.ReadInt32();
- Elements = new VertexElement[numberOfElements];
- for (int num = 0; num < numberOfElements; num++)
- {
- Elements[num].Load(reader);
- if (Elements[num].Type != VertexElementType.Color &&
- Elements[num].Type != VertexElementType.SkinIndices &&
- Elements[num].Type != VertexElementType.SkinWeights)
- {
- IsCompressed = Elements[num].IsCompressed;
- }
- }
- LengthInBytes = reader.ReadInt32();
- }
-
- /// <summary>
- /// Save VertexFormat, will save all vertex elements in here.
- /// </summary>
- public void Save(BinaryWriter writer)
- {
- writer.Write(VersionNumber);
- writer.Write(Elements.Length);
- foreach (VertexElement element in Elements)
- {
- element.Save(writer);
- }
- writer.Write(LengthInBytes);
- }
- #endregion
-
- #region op_Equality (Operator)
- /// <summary>
- /// Op equality
- /// </summary>
- /// <param name="a">A</param>
- /// <param name="b">B</param>
- public static bool operator ==(VertexFormat a, VertexFormat b)
- {
- if (a is VertexFormat)
- {
- return a.Equals(b);
- }
-
- return false;
- }
- #endregion
-
- #region op_Inequality (Operator)
- /// <summary>
- /// Op inequality
- /// </summary>
- /// <param name="a">A</param>
- /// <param name="b">B</param>
- public static bool operator !=(VertexFormat a, VertexFormat b)
- {
- if (a is VertexFormat)
- {
- return a.Equals(b) == false;
- }
-
- return true;
- }
- #endregion
-
- #region Equals (Public)
- /// <summary>
- /// Equals, used to check if two VertexFormats are the same.
- /// </summary>
- /// <param name="obj">Object</param>
- public override bool Equals(object obj)
- {
- // In the case we check against another vertex format
- if (obj is VertexFormat)
- {
- VertexFormat otherFormat = (VertexFormat)obj;
- // Then check first if the data length matches
- if (LengthInBytes == otherFormat.LengthInBytes &&
- // And if the number of elements matches
- Elements.Length == otherFormat.Elements.Length)
- {
- // Now check if every element of the other vertex format matches
- // really with our elements (incl. the same order)
- for (int index = 0; index < otherFormat.Elements.Length; index++)
- {
- // If one element doesn't match (we only need to check type and
- // size, all other properties will always be the same anyway if
- // type and size already match).
- if (otherFormat.Elements[index].Type !=
- Elements[index].Type) // ||
- //ignore size, else compression compare does not work:
- //otherFormat.Elements[index].Size != Elements[index].Size)
- {
- // Then the other format isn't the same obviously
- return false;
- }
- }
-
- // Every element matches, we can savely say this is the same format
- return true;
- }
- }
-
- // Else it can't be equal (obj is not even VertexFormat)
- return false;
- }
- #endregion
-
- #region GetHashCode (Public)
- /// <summary>
- /// Get hash code
- /// </summary>
- public override int GetHashCode()
- {
- return base.GetHashCode();
- }
- #endregion
-
- #region GetElementIndex (Public)
- /// <summary>
- /// Does this vertex format contain a specific vertex element type?
- /// Will return the index if found, else InvalidIndex. Index can be used
- /// to learn more about this element (e.g. IsCompressed, Offset, Size).
- /// </summary>
- public int GetElementIndex(VertexElementType checkType)
- {
- int index = 0;
- foreach (VertexElement element in Elements)
- {
- if (element.Type == checkType)
- {
- return index;
- }
- index++;
- }
-
- // Not found?
- return MathHelper.InvalidIndex;
- }
- #endregion
-
- #region ToString (Public)
- /// <summary>
- /// To string
- /// </summary>
- public override string ToString()
- {
- return "VertexFormat with " + Elements.Write() +
- ", LengthInBytes=" + LengthInBytes;
- }
- #endregion
-
- /// <summary>
- /// Tests
- /// </summary>
- internal class VertexFormatTests
- {
- #region LengthInBytes
- /// <summary>
- /// Checks if LengthInBytes returns the correct size.
- /// </summary>
- [Test]
- public void LengthInBytes()
- {
- // A vertex with a 3D position and UV coordinates has 5 floats.
- Assert.Equal(Position3DTextured.LengthInBytes, 5 * 4);
- // A vertex with a 2D position and UV coordinates has 4 floats.
- Assert.Equal(Position2DTextured.LengthInBytes, 4 * 4);
- // A vertex with compressed 2D position and UV coordinates has 4 shorts
- VertexFormat compressedPos2DTextured =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position2D, true),
- new VertexElement(VertexElementType.TextureUV, true),
- });
- Assert.Equal(compressedPos2DTextured.LengthInBytes, 4 * 2);
- // 7 floats for 3D position and UV and LightMap coordinates
- Assert.Equal(Position3DTexturedLightMap.LengthInBytes,
- 7 * 4);
- // 15 floats for 3D position and UV and LightMap coordinates with
- // normal and tangent vector data and skinning too (thats 60 bytes!)
- VertexFormat Pos3DNormalTangentLightMapSkinned =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D, false),
- new VertexElement(VertexElementType.Normal, false),
- new VertexElement(VertexElementType.Tangent, false),
- new VertexElement(VertexElementType.TextureUV, false),
- new VertexElement(VertexElementType.LightMapUV, false),
- // Skin data are always handled as compressed
- new VertexElement(VertexElementType.SkinIndices, true),
- new VertexElement(VertexElementType.SkinWeights, true),
- });
- Assert.Equal(Pos3DNormalTangentLightMapSkinned.LengthInBytes, 15 * 4);
- // 32 (8+4+4+4+4+4+4) bytes for compressed 3D position and UV and
- // LightMap UV with normal and tangent vector data and skinning too.
- VertexFormat compressedPos3DNormalTangentLightMapSkinned =
- new VertexFormat(new[]
- {
- new VertexElement(VertexElementType.Position3D, true),
- new VertexElement(VertexElementType.Normal, true),
- new VertexElement(VertexElementType.Tangent, true),
- new VertexElement(VertexElementType.TextureUV, true),
- new VertexElement(VertexElementType.LightMapUV, true),
- new VertexElement(VertexElementType.SkinIndices, true),
- new VertexElement(VertexElementType.SkinWeights, true),
- });
- Assert.Equal(compressedPos3DNormalTangentLightMapSkinned.LengthInBytes,
- 32);
- }
- #endregion
-
- #region GetElementIndex
- /// <summary>
- /// Get element index
- /// </summary>
- [Test]
- public void GetElementIndex()
- {
- // Use a simple vertex format
- VertexFormat format = Position3DTextured;
-
- Assert.Equal(format.Elements[0].Offset, 0);
- Assert.Equal(format.Elements[1].Offset, 12);
-
- int pos2DIndex = format.GetElementIndex(VertexElementType.Position2D);
- int uvIndex = format.GetElementIndex(VertexElementType.TextureUV);
- Assert.Equal(pos2DIndex, -1);
- Assert.Equal(uvIndex, 1);
- Assert.Equal(format.Elements[uvIndex].Offset, 12);
- }
- #endregion
-
- #region SaveAndLoad
- /// <summary>
- /// Save and load
- /// </summary>
- [Test]
- public void SaveAndLoad()
- {
- // Write vertex format into a memory stream
- MemoryStream memStream = new MemoryStream();
- BinaryWriter writer = new BinaryWriter(memStream);
- Position2DColor.Save(writer);
- // And read it out again (into a new stream)
- memStream = new MemoryStream(memStream.GetBuffer());
- BinaryReader reader = new BinaryReader(memStream);
- VertexFormat loadedFormat = new VertexFormat(reader);
- // Check some element data
- Assert.Equal(loadedFormat.LengthInBytes,
- Position2DColor.LengthInBytes);
- Assert.Equal(loadedFormat.Elements[0].Type,
- Position2DColor.Elements[0].Type);
- Assert.Equal(loadedFormat.Elements[1].Offset,
- Position2DColor.Elements[1].Offset);
- }
- #endregion
- }
- }
- }