PrimitiveDrawing.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. // Copyright (c) Craftwork Games. All rights reserved.
  2. // Licensed under the MIT license.
  3. // See LICENSE file in the project root for full license information.
  4. // Adapted from Velcro Physics (formerly known as Farseer Physics).
  5. // Used with permission: https://github.com/craftworkgames/MonoGame.Extended/issues/574
  6. using System;
  7. using Microsoft.Xna.Framework;
  8. using Microsoft.Xna.Framework.Graphics;
  9. using MonoGame.Extended.Triangulation;
  10. namespace MonoGame.Extended.VectorDraw
  11. {
  12. /// <summary>
  13. /// Provides methods for drawing primitive shapes using a <see cref="PrimitiveBatch"/>.
  14. /// </summary>
  15. /// <remarks>
  16. /// <para>
  17. /// This class renders filled shapes and outlines directly via the GPU, producing correct alpha-blended
  18. /// results even with semi-transparent colors. For quick outline-only drawing via <see cref="SpriteBatch"/>
  19. /// during prototyping or debugging, see <c>ShapeExtensions</c>.
  20. /// </para>
  21. /// <para>
  22. /// Call <see cref="PrimitiveBatch.Begin"/> on the associated <see cref="PrimitiveBatch"/> before issuing any
  23. /// draw calls, and <see cref="PrimitiveBatch.End"/> when finished to flush geometry to the GPU.
  24. /// </para>
  25. /// </remarks>
  26. public class PrimitiveDrawing
  27. {
  28. /// <summary>
  29. /// The default number of segments used when approximating circles and ellipses.
  30. /// </summary>
  31. #if XBOX || WINDOWS_PHONE
  32. public const int CircleSegments = 16;
  33. #else
  34. public const int CircleSegments = 32;
  35. #endif
  36. private readonly PrimitiveBatch _primitiveBatch;
  37. /// <summary>
  38. /// Initializes a new instance of <see cref="PrimitiveDrawing"/>.
  39. /// </summary>
  40. /// <param name="primitiveBatch">The <see cref="PrimitiveBatch"/> used to issue draw calls.</param>
  41. /// <exception cref="ArgumentNullException"><paramref name="primitiveBatch"/> is <see langword="null"/>.</exception>
  42. public PrimitiveDrawing(PrimitiveBatch primitiveBatch)
  43. {
  44. ArgumentNullException.ThrowIfNull(primitiveBatch);
  45. _primitiveBatch = primitiveBatch;
  46. }
  47. /// <summary>
  48. /// Draws a single point at the specified position.
  49. /// </summary>
  50. /// <param name="center">The position of the point.</param>
  51. /// <param name="color">The color of the point.</param>
  52. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  53. public void DrawPoint(Vector2 center, Color color)
  54. {
  55. if (!_primitiveBatch.IsReady())
  56. {
  57. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  58. }
  59. _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
  60. _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
  61. }
  62. /// <summary>
  63. /// Draws a rectangle outline.
  64. /// </summary>
  65. /// <param name="location">The top-left position of the rectangle in world space.</param>
  66. /// <param name="width">The width of the rectangle.</param>
  67. /// <param name="height">The height of the rectangle.</param>
  68. /// <param name="color">The color of the outline.</param>
  69. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  70. public void DrawRectangle(Vector2 location, float width, float height, Color color)
  71. {
  72. if (!_primitiveBatch.IsReady())
  73. {
  74. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  75. }
  76. Vector2[] rectVerts = new Vector2[4]
  77. {
  78. new Vector2(0, 0),
  79. new Vector2(width, 0),
  80. new Vector2(width, height),
  81. new Vector2(0, height)
  82. };
  83. DrawPolygon(location, rectVerts, color);
  84. }
  85. /// <summary>
  86. /// Draws a solid (filled) rectangle with an optional outline.
  87. /// </summary>
  88. /// <param name="location">The top-left position of the rectangle in world space.</param>
  89. /// <param name="width">The width of the rectangle.</param>
  90. /// <param name="height">The height of the rectangle.</param>
  91. /// <param name="color">
  92. /// The fill color. When <paramref name="outline"/> is <see langword="true"/>, also used for the outline.
  93. /// </param>
  94. /// <param name="outline">
  95. /// When <see langword="true"/>, an outline is drawn over the filled rectangle. Defaults to <see langword="true"/>.
  96. /// </param>
  97. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  98. public void DrawSolidRectangle(Vector2 location, float width, float height, Color color, bool outline = true)
  99. {
  100. if (!_primitiveBatch.IsReady())
  101. {
  102. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  103. }
  104. Vector2[] rectVerts = new Vector2[4]
  105. {
  106. new Vector2(0, 0),
  107. new Vector2(width, 0),
  108. new Vector2(width, height),
  109. new Vector2(0, height)
  110. };
  111. DrawSolidPolygon(location, rectVerts, color, outline);
  112. }
  113. /// <summary>
  114. /// Draws a circle outline using <see cref="CircleSegments"/> segments.
  115. /// </summary>
  116. /// <param name="center">The center of the circle.</param>
  117. /// <param name="radius">The radius of the circle.</param>
  118. /// <param name="color">The color of the outline.</param>
  119. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  120. public void DrawCircle(Vector2 center, float radius, Color color)
  121. {
  122. if (!_primitiveBatch.IsReady())
  123. {
  124. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  125. }
  126. double increment = Math.PI * 2.0 / CircleSegments;
  127. double theta = 0.0;
  128. for (int i = 0; i < CircleSegments; i++)
  129. {
  130. Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
  131. Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
  132. _primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList);
  133. _primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList);
  134. theta += increment;
  135. }
  136. }
  137. /// <summary>
  138. /// Draws a solid (filled) circle with an optional outline using <see cref="CircleSegments"/> segments.
  139. /// The fill and outline use the same color.
  140. /// </summary>
  141. /// <param name="center">The center of the circle.</param>
  142. /// <param name="radius">The radius of the circle.</param>
  143. /// <param name="color">The color used for both the fill and the outline.</param>
  144. /// <param name="outline">
  145. /// When <see langword="true"/>, an outline is drawn over the filled circle. Defaults to <see langword="true"/>.
  146. /// </param>
  147. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  148. public void DrawSolidCircle(Vector2 center, float radius, Color color, bool outline = true)
  149. {
  150. DrawSolidCircle(center, radius, color, color, outline);
  151. }
  152. /// <summary>
  153. /// Draws a solid (filled) circle with an optional outline using <see cref="CircleSegments"/> segments.
  154. /// </summary>
  155. /// <param name="center">The center of the circle.</param>
  156. /// <param name="radius">The radius of the circle.</param>
  157. /// <param name="color">The color of the outline.</param>
  158. /// <param name="fillColor">The color of the fill.</param>
  159. /// <param name="outline">
  160. /// When <see langword="true"/>, an outline is drawn over the filled circle. Defaults to <see langword="true"/>.
  161. /// </param>
  162. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  163. public void DrawSolidCircle(Vector2 center, float radius, Color color, Color fillColor, bool outline = true)
  164. {
  165. if (!_primitiveBatch.IsReady())
  166. {
  167. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  168. }
  169. double increment = Math.PI * 2.0 / CircleSegments;
  170. double theta = 0.0;
  171. Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
  172. theta += increment;
  173. for (int i = 1; i < CircleSegments - 1; i++)
  174. {
  175. Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
  176. Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
  177. _primitiveBatch.AddVertex(v0, fillColor, PrimitiveType.TriangleList);
  178. _primitiveBatch.AddVertex(v1, fillColor, PrimitiveType.TriangleList);
  179. _primitiveBatch.AddVertex(v2, fillColor, PrimitiveType.TriangleList);
  180. theta += increment;
  181. }
  182. if (outline)
  183. {
  184. DrawCircle(center, radius, color);
  185. }
  186. }
  187. /// <summary>
  188. /// Draws an arc outline.
  189. /// </summary>
  190. /// <param name="center">The center point of the arc.</param>
  191. /// <param name="radius">The radius of the arc.</param>
  192. /// <param name="startAngle">The starting angle in radians.</param>
  193. /// <param name="sweepAngle">
  194. /// The sweep angle in radians. Positive values sweep counter-clockwise.
  195. /// Use <c>MathHelper.TwoPi</c> to draw a full circle.
  196. /// </param>
  197. /// <param name="sides">The number of line segments used to approximate the arc.</param>
  198. /// <param name="color">The color of the arc.</param>
  199. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  200. public void DrawArc(Vector2 center, float radius, float startAngle, float sweepAngle, int sides, Color color)
  201. {
  202. if (!_primitiveBatch.IsReady())
  203. {
  204. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  205. }
  206. float step = sweepAngle / sides;
  207. float theta = startAngle;
  208. for (int i = 0; i < sides; i++)
  209. {
  210. Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
  211. Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + step), (float)Math.Sin(theta + step));
  212. _primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList);
  213. _primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList);
  214. theta += step;
  215. }
  216. }
  217. /// <summary>
  218. /// Draws a solid (filled) arc (pie slice) with an outline. The fill and outline use the same color.
  219. /// </summary>
  220. /// <param name="center">The center point of the arc.</param>
  221. /// <param name="radius">The radius of the arc.</param>
  222. /// <param name="startAngle">The starting angle in radians.</param>
  223. /// <param name="sweepAngle">
  224. /// The sweep angle in radians. Positive values sweep counter-clockwise.
  225. /// Use <c>MathHelper.TwoPi</c> for a full circle.
  226. /// </param>
  227. /// <param name="sides">The number of triangle segments used to fill the arc.</param>
  228. /// <param name="color">The color used for both the fill and the outline.</param>
  229. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  230. public void DrawSolidArc(Vector2 center, float radius, float startAngle, float sweepAngle, int sides, Color color)
  231. {
  232. DrawSolidArc(center, radius, startAngle, sweepAngle, sides, color, color);
  233. }
  234. /// <summary>
  235. /// Draws a solid (filled) arc (pie slice) with an outline.
  236. /// </summary>
  237. /// <param name="center">The center point of the arc.</param>
  238. /// <param name="radius">The radius of the arc.</param>
  239. /// <param name="startAngle">The starting angle in radians.</param>
  240. /// <param name="sweepAngle">
  241. /// The sweep angle in radians. Positive values sweep counter-clockwise.
  242. /// Use <c>MathHelper.TwoPi</c> for a full circle.
  243. /// </param>
  244. /// <param name="sides">The number of triangle segments used to fill the arc.</param>
  245. /// <param name="color">The color of the outline.</param>
  246. /// <param name="fillColor">The color of the fill.</param>
  247. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  248. public void DrawSolidArc(Vector2 center, float radius, float startAngle, float sweepAngle, int sides, Color color, Color fillColor)
  249. {
  250. if (!_primitiveBatch.IsReady())
  251. {
  252. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  253. }
  254. float step = sweepAngle / sides;
  255. float theta = startAngle;
  256. for (int i = 0; i < sides; i++)
  257. {
  258. Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
  259. Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + step), (float)Math.Sin(theta + step));
  260. _primitiveBatch.AddVertex(center, fillColor, PrimitiveType.TriangleList);
  261. _primitiveBatch.AddVertex(v1, fillColor, PrimitiveType.TriangleList);
  262. _primitiveBatch.AddVertex(v2, fillColor, PrimitiveType.TriangleList);
  263. theta += step;
  264. }
  265. DrawArc(center, radius, startAngle, sweepAngle, sides, color);
  266. }
  267. /// <summary>
  268. /// Draws a line segment between two points.
  269. /// </summary>
  270. /// <param name="start">The start point.</param>
  271. /// <param name="end">The end point.</param>
  272. /// <param name="color">The color of the line.</param>
  273. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  274. public void DrawSegment(Vector2 start, Vector2 end, Color color)
  275. {
  276. if (!_primitiveBatch.IsReady())
  277. {
  278. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  279. }
  280. _primitiveBatch.AddVertex(start, color, PrimitiveType.LineList);
  281. _primitiveBatch.AddVertex(end, color, PrimitiveType.LineList);
  282. }
  283. /// <summary>
  284. /// Draws a polygon outline.
  285. /// </summary>
  286. /// <param name="position">The world offset applied to all vertices.</param>
  287. /// <param name="vertices">The polygon vertices in local space.</param>
  288. /// <param name="color">The color of the outline.</param>
  289. /// <param name="closed">
  290. /// When <see langword="true"/>, an edge is drawn between the last and first vertex to close the polygon.
  291. /// Defaults to <see langword="true"/>.
  292. /// </param>
  293. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  294. public void DrawPolygon(Vector2 position, Vector2[] vertices, Color color, bool closed = true)
  295. {
  296. if (!_primitiveBatch.IsReady())
  297. {
  298. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  299. }
  300. int count = vertices.Length;
  301. for (int i = 0; i < count - 1; i++)
  302. {
  303. _primitiveBatch.AddVertex(new Vector2(vertices[i].X + position.X, vertices[i].Y + position.Y), color, PrimitiveType.LineList);
  304. _primitiveBatch.AddVertex(new Vector2(vertices[i + 1].X + position.X, vertices[i + 1].Y + position.Y), color, PrimitiveType.LineList);
  305. }
  306. if (closed)
  307. {
  308. _primitiveBatch.AddVertex(new Vector2(vertices[count - 1].X + position.X, vertices[count - 1].Y + position.Y), color, PrimitiveType.LineList);
  309. _primitiveBatch.AddVertex(new Vector2(vertices[0].X + position.X, vertices[0].Y + position.Y), color, PrimitiveType.LineList);
  310. }
  311. }
  312. /// <summary>
  313. /// Draws a solid (filled) polygon with an optional outline.
  314. /// </summary>
  315. /// <param name="position">The world offset applied to all vertices.</param>
  316. /// <param name="vertices">The polygon vertices in local space.</param>
  317. /// <param name="color">
  318. /// The fill color. When <paramref name="outline"/> is <see langword="true"/>, also used for the outline.
  319. /// </param>
  320. /// <param name="outline">
  321. /// When <see langword="true"/>, an outline is drawn over the filled polygon. Defaults to <see langword="true"/>.
  322. /// </param>
  323. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  324. public void DrawSolidPolygon(Vector2 position, Vector2[] vertices, Color color, bool outline = true)
  325. {
  326. if (!_primitiveBatch.IsReady())
  327. {
  328. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  329. }
  330. int count = vertices.Length;
  331. if (count == 2)
  332. {
  333. DrawPolygon(position, vertices, color);
  334. return;
  335. }
  336. Vector2[] outVertices;
  337. int[] outIndices;
  338. Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
  339. for (int i = 0; i < outIndices.Length - 2; i += 3)
  340. {
  341. _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + position.X, outVertices[outIndices[i]].Y + position.Y), color, PrimitiveType.TriangleList);
  342. _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + position.X, outVertices[outIndices[i + 1]].Y + position.Y), color, PrimitiveType.TriangleList);
  343. _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + position.X, outVertices[outIndices[i + 2]].Y + position.Y), color, PrimitiveType.TriangleList);
  344. }
  345. if (outline)
  346. {
  347. DrawPolygon(position, vertices, color);
  348. }
  349. }
  350. /// <summary>
  351. /// Draws an ellipse outline.
  352. /// </summary>
  353. /// <param name="center">The center of the ellipse.</param>
  354. /// <param name="radius">The horizontal (X) and vertical (Y) radii of the ellipse.</param>
  355. /// <param name="sides">The number of line segments used to approximate the ellipse.</param>
  356. /// <param name="color">The color of the outline.</param>
  357. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  358. public void DrawEllipse(Vector2 center, Vector2 radius, int sides, Color color)
  359. {
  360. if (!_primitiveBatch.IsReady())
  361. {
  362. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  363. }
  364. DrawPolygon(center, CreateEllipse(radius.X, radius.Y, sides), color);
  365. }
  366. /// <summary>
  367. /// Draws a solid (filled) ellipse with an optional outline.
  368. /// </summary>
  369. /// <param name="center">The center of the ellipse.</param>
  370. /// <param name="radius">The horizontal (X) and vertical (Y) radii of the ellipse.</param>
  371. /// <param name="sides">The number of segments used to approximate the ellipse.</param>
  372. /// <param name="color">
  373. /// The fill color. When <paramref name="outline"/> is <see langword="true"/>, also used for the outline.
  374. /// </param>
  375. /// <param name="outline">
  376. /// When <see langword="true"/>, an outline is drawn over the filled ellipse. Defaults to <see langword="true"/>.
  377. /// </param>
  378. /// <exception cref="InvalidOperationException"><see cref="PrimitiveBatch.Begin"/> must be called first.</exception>
  379. public void DrawSolidEllipse(Vector2 center, Vector2 radius, int sides, Color color, bool outline = true)
  380. {
  381. if (!_primitiveBatch.IsReady())
  382. {
  383. throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
  384. }
  385. Vector2[] vertices = CreateEllipse(radius.X, radius.Y, sides);
  386. Vector2[] outVertices;
  387. int[] outIndices;
  388. Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
  389. for (int i = 0; i < outIndices.Length - 2; i += 3)
  390. {
  391. _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + center.X, outVertices[outIndices[i]].Y + center.Y), color, PrimitiveType.TriangleList);
  392. _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + center.X, outVertices[outIndices[i + 1]].Y + center.Y), color, PrimitiveType.TriangleList);
  393. _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + center.X, outVertices[outIndices[i + 2]].Y + center.Y), color, PrimitiveType.TriangleList);
  394. }
  395. if (outline)
  396. {
  397. DrawPolygon(center, vertices, color);
  398. }
  399. }
  400. private static Vector2[] CreateEllipse(float rx, float ry, int sides)
  401. {
  402. Vector2[] vertices = new Vector2[sides];
  403. double t = 0.0;
  404. double dt = 2.0 * Math.PI / sides;
  405. for (int i = 0; i < sides; i++, t += dt)
  406. {
  407. vertices[i] = new Vector2((float)(rx * Math.Cos(t)), (float)(ry * Math.Sin(t)));
  408. }
  409. return vertices;
  410. }
  411. }
  412. }