PageRenderTime 148ms CodeModel.GetById 60ms app.highlight 46ms RepoModel.GetById 35ms app.codeStats 0ms

/Utilities/Graphics/VertexFormat.cs

#
C# | 655 lines | 379 code | 41 blank | 235 comment | 32 complexity | 2e3d55d54743701f61164965b1b42f51 MD5 | raw file
  1using System;
  2using System.IO;
  3using Delta.Utilities.Helpers;
  4using NUnit.Framework;
  5
  6namespace Delta.Utilities.Graphics
  7{
  8	/// <summary>
  9	/// Vertex format for GeometryData, check out VertexFormatType for details!
 10	/// </summary>
 11	/// <remarks>
 12	/// Interesting papers and sites about vertex compression:
 13	/// http://blogs.msdn.com/b/shawnhar/archive/2010/11/19/compressed-vertex-data.aspx
 14	/// http://gsdwrdx.tistory.com/92
 15	/// http://stackoverflow.com/questions/1762866/using-gl-short-instead-of-gl-float-in-an-opengl-es-vertex-array
 16	/// http://wiki.gamedev.net/index.php/D3DBook:(Lighting)_Per-Pixel_Lighting
 17	/// http://www.cs.jhu.edu/~jab/publications/vertexcompression.pdf
 18	/// http://www.graphcomp.com/info/specs/ibm/cgd.html
 19	/// http://irrlicht.sourceforge.net/docu/_i_animated_mesh_m_d3_8h_source.html
 20	/// http://viola.usc.edu/Publication/PDF/selected/2000_JVCIR_Cheang.pdf
 21	/// http://www.cse.ohio-state.edu/~hwshen/Su01_888/deering.pdf
 22	/// http://www.cg.tuwien.ac.at/studentwork/VisFoSe98/lm/
 23	/// http://personal.redestb.es/jmovill/opengl/openglonwin-15.html
 24	/// https://www.opengl.org/sdk/docs/man3/xhtml/glVertexAttribPointer.xml
 25	/// Seems like support for 16 bit floats is sometimes there, but we cannot
 26	/// guarantee that it works on all platforms, so we cannot use it :(
 27	/// Instead we use shorts to compress data into 16 bits as well, but this
 28	/// has sometimes some additional overhead in the vertex shader.
 29	/// For example Position3DCompressed | TextureUVCompressed |
 30	/// NormalCompressed | TangentCompressed | SkinWeightCompressed |
 31	/// SkinIndexCompressed is just 6+4+6+6+2+4 = 28 bytes instead of
 32	/// 12+8+12+12+4+4 = 52 bytes uncompressed :)
 33	/// <para />
 34	/// Note: When we use different sizes, we must make sure that we align all
 35	/// our components to their native alignment, e.g. floats are on 4-byte
 36	/// boundaries, shorts are on 2-byte boundaries. If we don't do this it will
 37	/// tank our performance. It might be helpful to mentally map it by typing
 38	/// out your attribute ordering as a struct definition so we can sanity
 39	/// check our layout and alignment (this is not easy). Also:
 40	/// * making sure your data is stripped to share vertices
 41	/// * using a texture atlas to reduce texture swaps (see Image)
 42	/// <para />
 43	/// Optimizing Vertex Data
 44	/// Each API call results in a certain amount of overhead that is caused by
 45	/// translating the standard API calls into commands understood by the
 46	/// hardware. The amount of overhead can be reduced by minimizing the
 47	/// translation work required by making sure that the data submitted to the
 48	/// API is already in a hardware friendly format. This can be achieved by
 49	/// following the simple recommendation when submitting vertex data: submit
 50	/// strip-ordered indexed triangles with per vertex data interleaved.
 51	/// <para />
 52	/// Indexed triangles (glDrawElements) is preferred over non-indexed strips
 53	/// (glDrawArrays). Indexing allows for a higher amount of vertex reuse. The
 54	/// same vertex in strips can not be used for more than 3 triangles; indexed
 55	/// triangles does not have this constraint. Further, OpenGL ES requires one
 56	/// draw call per strip. If you want to collapse draw calls for multiple
 57	/// strips into one, you will have to add degenerate strips in between to
 58	/// introduce discontinuities. This extra work is not required using indexed
 59	/// triangles. With strip-ordered indexed triangles, multiple disconnected
 60	/// strips can be drawn in a single draw call, while avoiding the cost of
 61	/// inserting elements to create degenerate triangles.
 62	/// <para />
 63	/// Sorting the vertex data (either using indexed triangles or non-indexed
 64	/// strips) is very important. Sorting allows you to generate more triangles
 65	/// per vertex submitted. For peak performance, sort index triangles so that
 66	/// connected indexed triangles can build strips. This improves memory
 67	/// access patterns and the usage of the various data caches.
 68	/// <para />
 69	/// Use interleaved or tightly packed vertex arrays. OpenGL ES allows vertex
 70	/// data elements to be spread out over memory thus resulting in scatter
 71	/// reads to fetch the required data. On the other hand it is also possible
 72	/// to interleave the data such that it is already together in memory.
 73	/// Interleaving of the per vertex elements improves the memory efficiency
 74	/// and is more logical for the processing that needs to be done. For
 75	/// optimal performance, interleave the individual components in an order of
 76	/// Position, Normal, Color, TexCoord0, TexCoord1, PointSize, Weight,
 77	/// MatrixIndex. Memory bandwidth is limited, so try to use the smallest
 78	/// acceptable type for each component. Specify vertex colors using 4
 79	/// unsigned byte values. Specify texture coordinates with 2 or 4 unsigned
 80	/// byte or short values, instead of floating-point values, if you can.
 81	/// </remarks>
 82	public sealed class VertexFormat : ISaveLoadBinary
 83	{
 84		#region Constants
 85		/// <summary>
 86		/// Version number for this VertexFormat. If this goes above 1, we need
 87		/// to support loading older versions as well. Saving is always going
 88		/// to be the latest version (this one).
 89		/// </summary>
 90		private const int VersionNumber = 1;
 91
 92		/// <summary>
 93		/// Empty vertex format, currently only used for OpenGL ES 1.1, where we
 94		/// need to reset the vertex format after rendering else we are in a
 95		/// messed up state and problems in follow up code can happen. This is
 96		/// not required for any other OpenGL or Graphics implementation.
 97		/// </summary>
 98		public static readonly VertexFormat None =
 99			new VertexFormat(new VertexElement[]
100			{
101			});
102
103		/// <summary>
104		/// Used mostly for simple 2D and UI data (basic shader, no extra features)
105		/// </summary>
106		public static readonly VertexFormat Position2DTextured =
107			new VertexFormat(new[]
108			{
109				new VertexElement(VertexElementType.Position2D),
110				new VertexElement(VertexElementType.TextureUV),
111			});
112
113		/// <summary>
114		/// This vertex format is used mostly for simple 3D meshes, e.g.
115		/// Mesh.CreateSphere (uncompressed 3+2 floats = 20 bytes, compressed 10)
116		/// </summary>
117		public static readonly VertexFormat Position3DTextured =
118			new VertexFormat(new[]
119			{
120				new VertexElement(VertexElementType.Position3D),
121				new VertexElement(VertexElementType.TextureUV),
122			});
123
124		/// <summary>
125		/// Simple format for 2D points and colored vertices, used for lines.
126		/// </summary>
127		public static readonly VertexFormat Position2DColor =
128			new VertexFormat(new[]
129			{
130				new VertexElement(VertexElementType.Position2D),
131				new VertexElement(VertexElementType.Color),
132			});
133
134		/// <summary>
135		/// Simple format for textured 2D drawing with vertex colors. Mostly
136		/// used for effects because each vertex can have different colors.
137		/// </summary>
138		public static readonly VertexFormat Position2DColorTextured =
139			new VertexFormat(new[]
140			{
141				new VertexElement(VertexElementType.Position2D),
142				new VertexElement(VertexElementType.Color),
143				new VertexElement(VertexElementType.TextureUV),
144			});
145
146		/// <summary>
147		/// Simple format for 3D points and colored vertices, used for 3D lines.
148		/// </summary>
149		public static readonly VertexFormat Position3DColor =
150			new VertexFormat(new[]
151			{
152				new VertexElement(VertexElementType.Position3D),
153				new VertexElement(VertexElementType.Color),
154			});
155
156		/// <summary>
157		/// This vertex format is used mostly for simple 3D meshes, e.g.
158		/// Mesh.CreateSphere (uncompressed 24 bytes, compressed 16)
159		/// </summary>
160		public static readonly VertexFormat Position3DColorTextured =
161			new VertexFormat(new[]
162			{
163				new VertexElement(VertexElementType.Position3D),
164				new VertexElement(VertexElementType.Color),
165				new VertexElement(VertexElementType.TextureUV),
166			});
167
168		/// <summary>
169		/// 3D position, uv and lightmap textured vertex format for simple 3D
170		/// geometry using light maps.
171		/// </summary>
172		public static readonly VertexFormat Position3DTexturedLightMap =
173			new VertexFormat(new[]
174			{
175				new VertexElement(VertexElementType.Position3D),
176				new VertexElement(VertexElementType.TextureUV),
177				new VertexElement(VertexElementType.LightMapUV),
178			});
179
180		/// <summary>
181		/// Position textured skinned
182		/// </summary>
183		public static readonly VertexFormat PositionTexturedSkinned =
184			new VertexFormat(new[]
185			{
186				new VertexElement(VertexElementType.Position3D),
187				new VertexElement(VertexElementType.TextureUV),
188				// Skin data are always handled as compressed
189				new VertexElement(VertexElementType.SkinIndices, true),
190				new VertexElement(VertexElementType.SkinWeights, true),
191			});
192
193		/// <summary>
194		/// Position skinned
195		/// </summary>
196		public static readonly VertexFormat PositionSkinned =
197			new VertexFormat(new[]
198			{
199				new VertexElement(VertexElementType.Position3D),
200				// Skin data are always handled as compressed
201				new VertexElement(VertexElementType.SkinIndices, true),
202				new VertexElement(VertexElementType.SkinWeights, true),
203			});
204
205		/// <summary>
206		/// Position normal textured skinned
207		/// </summary>
208		public static readonly VertexFormat PositionNormalTexturedSkinned =
209			new VertexFormat(new[]
210			{
211				new VertexElement(VertexElementType.Position3D),
212				new VertexElement(VertexElementType.Normal),
213				new VertexElement(VertexElementType.TextureUV),
214				// Skin data are always handled as compressed
215				new VertexElement(VertexElementType.SkinIndices, true),
216				new VertexElement(VertexElementType.SkinWeights, true),
217			});
218		#endregion
219
220		#region Elements (Public)
221		/// <summary>
222		/// Elements
223		/// </summary>
224		public VertexElement[] Elements
225		{
226			get;
227			private set;
228		}
229		#endregion
230
231		#region LengthInBytes (Public)
232		/// <summary>
233		/// Get length in bytes, cached in GeometryData as this is used quite often
234		/// and never changes.
235		/// </summary>
236		public int LengthInBytes
237		{
238			get;
239			private set;
240		}
241		#endregion
242
243		#region IsCompressed (Public)
244		/// <summary>
245		/// Is data in this vertex format compressed? Only will return true if
246		/// all elements are compressed that can be compressed, e.g. position, uv
247		/// or normal data. Color or skinning data is always in the same format
248		/// (compressed), it will not be used for this check. Calculated in the
249		/// constructor.
250		/// </summary>
251		public bool IsCompressed
252		{
253			get;
254			private set;
255		}
256		#endregion
257
258		#region HasNormals (Public)
259		/// <summary>
260		/// Has normals? Used to check if we need to compare normals for
261		/// optimizing meshes.
262		/// </summary>
263		public bool HasNormals
264		{
265			get
266			{
267				for (int index = 0; index < Elements.Length; index++)
268				{
269					if (Elements[index].Type ==
270					    VertexElementType.Normal)
271					{
272						return true;
273					}
274				}
275				return false;
276			}
277		}
278		#endregion
279
280		#region HasTangents (Public)
281		/// <summary>
282		/// Has tangents? Used to check if we need to generate tangents when
283		/// importing meshes.
284		/// </summary>
285		public bool HasTangents
286		{
287			get
288			{
289				for (int index = 0; index < Elements.Length; index++)
290				{
291					if (Elements[index].Type ==
292					    VertexElementType.Tangent)
293					{
294						return true;
295					}
296				}
297				return false;
298			}
299		}
300		#endregion
301
302		#region HasLightmapUVs (Public)
303		/// <summary>
304		/// Has lightmap UV texture coordinates? Good check for merging meshes.
305		/// </summary>
306		public bool HasLightmapUVs
307		{
308			get
309			{
310				for (int index = 0; index < Elements.Length; index++)
311				{
312					if (Elements[index].Type ==
313					    VertexElementType.LightMapUV)
314					{
315						return true;
316					}
317				}
318				return false;
319			}
320		}
321		#endregion
322
323		#region Constructors
324		/// <summary>
325		/// Create vertex format
326		/// </summary>
327		/// <param name="setElements">Set elements</param>
328		public VertexFormat(VertexElement[] setElements)
329		{
330			if (setElements == null) // ||
331				//now allowed for None above: setElements.Length == 0)
332			{
333				throw new InvalidOperationException("You have to specify vertex" +
334				                                    " elements to define a VertexFormat");
335			}
336
337			// Set all elements, not allowed to be modified outside this constructor
338			Elements = setElements;
339
340			// Get length of the format in bytes and also calculate the offsets.
341			LengthInBytes = 0;
342			VertexElementType lastType = (VertexElementType)MathHelper.InvalidIndex;
343			IsCompressed = false;
344			for (int num = 0; num < Elements.Length; num++)
345			{
346				Elements[num].Offset = LengthInBytes;
347				LengthInBytes += Elements[num].Size;
348				// Ignore color and skinning elements, they are always compressed
349				VertexElementType elementType = Elements[num].Type;
350				if (elementType != VertexElementType.Color &&
351				    elementType != VertexElementType.SkinIndices &&
352				    elementType != VertexElementType.SkinWeights)
353				{
354					IsCompressed = Elements[num].IsCompressed;
355				}
356
357				// Show warning if user specified a strange format, we want to have
358				// vertex elements sorted this way: Position, Normal, Color, TexCoords,
359				// PointSize, Weight, MatrixIndex. Since VertexElementType is sorted
360				// this way we just need to check if a type was going backwards.
361				if (elementType < lastType)
362				{
363					Log.Warning("Your vertex format seems to be not optimal, the type " +
364					            elementType + " should be before the last type=" + lastType +
365					            ". Check out the VertexFormatType and the wiki for details: " +
366					            this);
367				}
368				lastType = elementType;
369			}
370		}
371
372		/// <summary>
373		/// Create vertex format from binary stream, will load all elements.
374		/// </summary>
375		public VertexFormat(BinaryReader reader)
376		{
377			Load(reader);
378		}
379		#endregion
380
381		#region ISaveLoadBinary Members
382		/// <summary>
383		/// Load VertexFormat from a binary data stream.
384		/// </summary>
385		public void Load(BinaryReader reader)
386		{
387			// We currently only support our version, if more versions are added,
388			// we need to do different loading code depending on the version here.
389			int version = reader.ReadInt32();
390			if (version != VersionNumber)
391			{
392				Log.InvalidVersionWarning("VertexFormat", version, VersionNumber);
393				return;
394			}
395			IsCompressed = false;
396			// Read all data in the way it was saved above
397			int numberOfElements = reader.ReadInt32();
398			Elements = new VertexElement[numberOfElements];
399			for (int num = 0; num < numberOfElements; num++)
400			{
401				Elements[num].Load(reader);
402				if (Elements[num].Type != VertexElementType.Color &&
403				    Elements[num].Type != VertexElementType.SkinIndices &&
404				    Elements[num].Type != VertexElementType.SkinWeights)
405				{
406					IsCompressed = Elements[num].IsCompressed;
407				}
408			}
409			LengthInBytes = reader.ReadInt32();
410		}
411
412		/// <summary>
413		/// Save VertexFormat, will save all vertex elements in here.
414		/// </summary>
415		public void Save(BinaryWriter writer)
416		{
417			writer.Write(VersionNumber);
418			writer.Write(Elements.Length);
419			foreach (VertexElement element in Elements)
420			{
421				element.Save(writer);
422			}
423			writer.Write(LengthInBytes);
424		}
425		#endregion
426
427		#region op_Equality (Operator)
428		/// <summary>
429		/// Op equality
430		/// </summary>
431		/// <param name="a">A</param>
432		/// <param name="b">B</param>
433		public static bool operator ==(VertexFormat a, VertexFormat b)
434		{
435			if (a is VertexFormat)
436			{
437				return a.Equals(b);
438			}
439
440			return false;
441		}
442		#endregion
443
444		#region op_Inequality (Operator)
445		/// <summary>
446		/// Op inequality
447		/// </summary>
448		/// <param name="a">A</param>
449		/// <param name="b">B</param>
450		public static bool operator !=(VertexFormat a, VertexFormat b)
451		{
452			if (a is VertexFormat)
453			{
454				return a.Equals(b) == false;
455			}
456
457			return true;
458		}
459		#endregion
460
461		#region Equals (Public)
462		/// <summary>
463		/// Equals, used to check if two VertexFormats are the same.
464		/// </summary>
465		/// <param name="obj">Object</param>
466		public override bool Equals(object obj)
467		{
468			// In the case we check against another vertex format
469			if (obj is VertexFormat)
470			{
471				VertexFormat otherFormat = (VertexFormat)obj;
472				// Then check first if the data length matches
473				if (LengthInBytes == otherFormat.LengthInBytes &&
474				    // And if the number of elements matches
475				    Elements.Length == otherFormat.Elements.Length)
476				{
477					// Now check if every element of the other vertex format matches
478					// really with our elements (incl. the same order)
479					for (int index = 0; index < otherFormat.Elements.Length; index++)
480					{
481						// If one element doesn't match (we only need to check type and
482						// size, all other properties will always be the same anyway if
483						// type and size already match).
484						if (otherFormat.Elements[index].Type !=
485						    Elements[index].Type) // ||
486							//ignore size, else compression compare does not work:
487							//otherFormat.Elements[index].Size != Elements[index].Size)
488						{
489							// Then the other format isn't the same obviously
490							return false;
491						}
492					}
493
494					// Every element matches, we can savely say this is the same format
495					return true;
496				}
497			}
498
499			// Else it can't be equal (obj is not even VertexFormat)
500			return false;
501		}
502		#endregion
503
504		#region GetHashCode (Public)
505		/// <summary>
506		/// Get hash code
507		/// </summary>
508		public override int GetHashCode()
509		{
510			return base.GetHashCode();
511		}
512		#endregion
513
514		#region GetElementIndex (Public)
515		/// <summary>
516		/// Does this vertex format contain a specific vertex element type?
517		/// Will return the index if found, else InvalidIndex. Index can be used
518		/// to learn more about this element (e.g. IsCompressed, Offset, Size).
519		/// </summary>
520		public int GetElementIndex(VertexElementType checkType)
521		{
522			int index = 0;
523			foreach (VertexElement element in Elements)
524			{
525				if (element.Type == checkType)
526				{
527					return index;
528				}
529				index++;
530			}
531
532			// Not found?
533			return MathHelper.InvalidIndex;
534		}
535		#endregion
536
537		#region ToString (Public)
538		/// <summary>
539		/// To string
540		/// </summary>
541		public override string ToString()
542		{
543			return "VertexFormat with " + Elements.Write() +
544			       ", LengthInBytes=" + LengthInBytes;
545		}
546		#endregion
547
548		/// <summary>
549		/// Tests
550		/// </summary>
551		internal class VertexFormatTests
552		{
553			#region LengthInBytes
554			/// <summary>
555			/// Checks if LengthInBytes returns the correct size.
556			/// </summary>
557			[Test]
558			public void LengthInBytes()
559			{
560				// A vertex with a 3D position and UV coordinates has 5 floats.
561				Assert.Equal(Position3DTextured.LengthInBytes, 5 * 4);
562				// A vertex with a 2D position and UV coordinates has 4 floats.
563				Assert.Equal(Position2DTextured.LengthInBytes, 4 * 4);
564				// A vertex with compressed 2D position and UV coordinates has 4 shorts
565				VertexFormat compressedPos2DTextured =
566					new VertexFormat(new[]
567					{
568						new VertexElement(VertexElementType.Position2D, true),
569						new VertexElement(VertexElementType.TextureUV, true),
570					});
571				Assert.Equal(compressedPos2DTextured.LengthInBytes, 4 * 2);
572				// 7 floats for 3D position and UV and LightMap coordinates
573				Assert.Equal(Position3DTexturedLightMap.LengthInBytes,
574					7 * 4);
575				// 15 floats for 3D position and UV and LightMap coordinates with
576				// normal and tangent vector data and skinning too (thats 60 bytes!)
577				VertexFormat Pos3DNormalTangentLightMapSkinned =
578					new VertexFormat(new[]
579					{
580						new VertexElement(VertexElementType.Position3D, false),
581						new VertexElement(VertexElementType.Normal, false),
582						new VertexElement(VertexElementType.Tangent, false),
583						new VertexElement(VertexElementType.TextureUV, false),
584						new VertexElement(VertexElementType.LightMapUV, false),
585						// Skin data are always handled as compressed
586						new VertexElement(VertexElementType.SkinIndices, true),
587						new VertexElement(VertexElementType.SkinWeights, true),
588					});
589				Assert.Equal(Pos3DNormalTangentLightMapSkinned.LengthInBytes, 15 * 4);
590				// 32 (8+4+4+4+4+4+4) bytes for compressed 3D position and UV and
591				// LightMap UV with normal and tangent vector data and skinning too.
592				VertexFormat compressedPos3DNormalTangentLightMapSkinned =
593					new VertexFormat(new[]
594					{
595						new VertexElement(VertexElementType.Position3D, true),
596						new VertexElement(VertexElementType.Normal, true),
597						new VertexElement(VertexElementType.Tangent, true),
598						new VertexElement(VertexElementType.TextureUV, true),
599						new VertexElement(VertexElementType.LightMapUV, true),
600						new VertexElement(VertexElementType.SkinIndices, true),
601						new VertexElement(VertexElementType.SkinWeights, true),
602					});
603				Assert.Equal(compressedPos3DNormalTangentLightMapSkinned.LengthInBytes,
604					32);
605			}
606			#endregion
607
608			#region GetElementIndex
609			/// <summary>
610			/// Get element index
611			/// </summary>
612			[Test]
613			public void GetElementIndex()
614			{
615				// Use a simple vertex format
616				VertexFormat format = Position3DTextured;
617
618				Assert.Equal(format.Elements[0].Offset, 0);
619				Assert.Equal(format.Elements[1].Offset, 12);
620
621				int pos2DIndex = format.GetElementIndex(VertexElementType.Position2D);
622				int uvIndex = format.GetElementIndex(VertexElementType.TextureUV);
623				Assert.Equal(pos2DIndex, -1);
624				Assert.Equal(uvIndex, 1);
625				Assert.Equal(format.Elements[uvIndex].Offset, 12);
626			}
627			#endregion
628
629			#region SaveAndLoad
630			/// <summary>
631			/// Save and load
632			/// </summary>
633			[Test]
634			public void SaveAndLoad()
635			{
636				// Write vertex format into a memory stream
637				MemoryStream memStream = new MemoryStream();
638				BinaryWriter writer = new BinaryWriter(memStream);
639				Position2DColor.Save(writer);
640				// And read it out again (into a new stream)
641				memStream = new MemoryStream(memStream.GetBuffer());
642				BinaryReader reader = new BinaryReader(memStream);
643				VertexFormat loadedFormat = new VertexFormat(reader);
644				// Check some element data
645				Assert.Equal(loadedFormat.LengthInBytes,
646					Position2DColor.LengthInBytes);
647				Assert.Equal(loadedFormat.Elements[0].Type,
648					Position2DColor.Elements[0].Type);
649				Assert.Equal(loadedFormat.Elements[1].Offset,
650					Position2DColor.Elements[1].Offset);
651			}
652			#endregion
653		}
654	}
655}