Extruder.cs 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. using System;
  2. using System.Collections.Generic;
  3. using Google.Maps.Feature;
  4. using Google.Maps.Feature.Shape;
  5. using UnityEngine;
  6. using Random = UnityEngine.Random;
  7. namespace Google.Maps.Examples.Shared {
  8. /// <summary>
  9. /// Static class containing logic for creating extrusions and lofts from given geometry built by
  10. /// the Maps SDK for Unity.
  11. /// </summary>
  12. public static class Extruder {
  13. /// <summary>Default thickness of created extrusions.</summary>
  14. /// <remarks>This value will be used for all extrusions unless a custom value is
  15. /// given.</remarks>
  16. private const float DefaultWidth = 1.0f;
  17. /// <summary>Name given to <see cref="GameObject"/>s created as parapets.</summary>
  18. private const string ParapetName = "Parapet";
  19. /// <summary>
  20. /// Name given to <see cref="GameObject"/>s created as building base decorations.
  21. /// </summary>
  22. private const string BorderName = "Border";
  23. /// <summary>Name given to <see cref="GameObject"/>s created as area outlines.</summary>
  24. private const string OutlineName = "Outline";
  25. /// <summary>
  26. /// Physical scale (in meters) of the <see cref="Material"/> applied to the extrusions.
  27. /// </summary>
  28. /// <remarks>
  29. /// The extrusion generation code assigns UVs based on real world scale in meters, meaning that
  30. /// two vertices that are a meter apart will have a UV coordinate that differs by 1.
  31. /// <para>
  32. /// For example, if you have a texture that corresponds to a 5x5 meter square, this value
  33. /// should be set to 5.0f. This is an alternative to using a texture tiling of 0.2f on (i.e.
  34. /// tile 5 times per 1 uv value) on a Unity Standard Material.
  35. /// </para></remarks>
  36. private const float UvScale = 1.0f;
  37. /// <summary>
  38. /// Two dimensional cross-sections we will use to form flat extrusions around a given shape.
  39. /// </summary>
  40. /// <remarks>
  41. /// See <see cref="ParapetShapes"/> for an explanation of the coordinate system.
  42. /// </remarks>
  43. private static readonly Vector2[] BorderShape = { new Vector2(1f, 0f), new Vector2(0f, 0f) };
  44. /// <summary>
  45. /// Default height applied to Maps SDK for Unity generated buildings that do not have stored
  46. /// height information. The chosen value of 10f matches the default value used inside the Maps
  47. /// SDK for buildings without stored heights. <para> The Maps SDK for Unity default height can
  48. /// be overriden with styling options, specifically <see
  49. /// cref="GameObjectOptions.ExtrudedStructureStyle"/>'s ExtrudedBuildingFootprintHeight. If this
  50. /// default height is overriden when calling <see cref="MapsService.LoadMap"/>, then this new
  51. /// default height value should also be given when calling
  52. /// <see cref="Extruder.AddBuildingParapet"/> to make sure that building parapets appear at the
  53. /// roof-level of all buildings, even if these buildings don't have a stored height.
  54. /// </para></summary>
  55. private const float ExtrudedBuildingFootprintHeight = 10.0f;
  56. /// <summary>
  57. /// Two dimensional cross-sections that will be used to form the parapets.
  58. /// </summary>
  59. /// <remarks>
  60. /// Each entry defines the cross section of a parapet in a coordinate space relative to the
  61. /// outer roof-edge of the building, where the positive x-axis points out away from the
  62. /// building, and the positive y-axis points towards the sky. So, for example: <para>(1, 0) is 1
  63. /// meter out of the building.</para> <para>(1, -1) is 1 meter out, 1 meter down.</para>
  64. /// <para>Outlines should be specified with an counterclockwise winding order (assuming +x
  65. /// right, +y up) to ensure the normals of the generated geometry face in the correct
  66. /// direction.</para>
  67. /// </remarks>
  68. private static readonly Vector2[][] ParapetShapes = {
  69. // A square parapet running along the outer edge of a roof, not overhanging exterior walls.
  70. MakeVector2Array(0f, 0f, 0f, 1f, -1f, 1f, -1f, 0f),
  71. // A square parapet running along the outer edge of a roof, slightly overlapping the roof, and
  72. // overhanging exterior walls.
  73. MakeVector2Array(-0.5f, 0f, 1f, 0f, 1f, 1f, -0.5f, 1f, -0.5f, 0f),
  74. // A stepped parapet that overhangs exterior walls, with the steps facing down towards the
  75. // ground.
  76. MakeVector2Array(-1f, 0f, 0.5f, 0f, 0.5f, 0.5f, 1f, 0.5f, 1f, 1.0f, -1f, 1f, -1f, 0f),
  77. // A stepped parapet that does not overhang exterior walls, with the steps facing upwards
  78. // towards the sky.
  79. MakeVector2Array(0f, 0f, 0f, 1f, -0.5f, 1f, -0.5f, 0.5f, -1f, 0.5f, -1f, 0f),
  80. // A bevelled parapet that overhangs exterior walls, similar to the steps facing upwards but
  81. // with a slope in place of the middle step.
  82. MakeVector2Array(0f, -0.5f, 1f, -0.5f, 1f, 0.5f, 0.5f, 1f, 0f, 1f, 0f, -0.5f)
  83. };
  84. /// <summary>Returns a cross-section to be used for an outline with a given width.</summary>
  85. private static Vector2[] OutlineCrossSectionWithWidth(float width) {
  86. return new Vector2[] { new Vector2(width / 2, 0f), new Vector2(-width / 2, 0f) };
  87. }
  88. /// <summary>Adds a extruded border around the base of a given building.</summary>
  89. /// <param name="buildingGameObject">
  90. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this building.
  91. /// </param>
  92. /// <param name="buildingShape">
  93. /// The Maps SDK for Unity <see cref="MapFeature"/> data defining this building's shape and
  94. /// height.
  95. /// </param>
  96. /// <param name="borderMaterial">
  97. /// The <see cref="Material"/> to apply to the extrusion once it is created.
  98. /// </param>
  99. /// <returns>
  100. /// Newly created <see cref="GameObject"/>s containing created extrusion geometry.
  101. /// <para>
  102. /// One <see cref="GameObject"/> will be returned for each part of the given building.
  103. /// </para></returns>
  104. public static GameObject[] AddBuildingBorder(
  105. GameObject buildingGameObject, ExtrudedArea buildingShape, Material borderMaterial,
  106. float thickness = DefaultWidth) {
  107. // Create list to store all created borders.
  108. List<GameObject> extrudedBorders = new List<GameObject>();
  109. for (int i = 0; i < buildingShape.Extrusions.Length; i++) {
  110. // Use general-purpose building-extrusion function to add border around building.
  111. AddBuildingExtrusion(
  112. buildingGameObject,
  113. borderMaterial,
  114. buildingShape.Extrusions[i],
  115. BorderShape,
  116. 0f,
  117. ref extrudedBorders,
  118. false,
  119. i,
  120. buildingShape.Extrusions.Length,
  121. thickness);
  122. }
  123. // Return all created extrusions.
  124. return extrudedBorders.ToArray();
  125. }
  126. /// <summary>
  127. /// Adds a parapet of a randomly chosen cross-section to the given building.
  128. /// </summary>
  129. /// <param name="buildingGameObject">
  130. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this building.
  131. /// </param>
  132. /// <param name="buildingShape">
  133. /// The Maps SDK for Unity <see cref="MapFeature"/> data defining this building's shape and
  134. /// height.
  135. /// </param>
  136. /// <param name="parapetMaterial">
  137. /// The <see cref="Material"/> to apply to the parapet once it is created.
  138. /// </param>
  139. /// <param name="defaultBuildingHeight">
  140. /// Default height applied to Maps SDK for Unity generated buildings that do not have stored
  141. /// height information. If left blank, a value of 10f matches the default value used inside the
  142. /// Maps SDK for buildings without stored heights.
  143. /// <para>
  144. /// The Maps SDK for Unity default height can be overriden with styling options, specifically
  145. /// <see cref="GameObjectOptions.ExtrudedStructureStyle"/>'s ExtrudedBuildingFootprintHeight. If
  146. /// this default height is overriden when calling <see cref="MapsService.LoadMap"/>, then this
  147. /// new default height value should also be used here to make sure that building parapets appear
  148. /// at the roof-level of all buildings, even if these buildings don't have a stored height.
  149. /// </para></param>
  150. /// <returns>
  151. /// Newly created <see cref="GameObject"/>s containing created parapet geometry.
  152. /// <para>
  153. /// One <see cref="GameObject"/> will be returned for each part of the given building.
  154. /// </para></returns>
  155. public static GameObject[] AddRandomBuildingParapet(
  156. GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial,
  157. float defaultBuildingHeight = ExtrudedBuildingFootprintHeight) {
  158. return AddBuildingParapet(
  159. buildingGameObject, buildingShape, parapetMaterial, defaultBuildingHeight, null);
  160. }
  161. /// <summary>Updates the parapet decoration on a building.</summary>
  162. /// <remarks>
  163. /// This method removes any existing parapet decoration and builds a new parapet child object in
  164. /// the same manner as the <see cref="AddRandomBuildingParapet"/> method. No attempt is made to
  165. /// retain the same parapet type. In a more sophisticated implementation, the assigned parapet
  166. /// type would be stored on the building GameObject to be retrieved here.
  167. /// </remarks>
  168. /// <param name="buildingGameObject">
  169. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this building.
  170. /// </param>
  171. /// <param name="buildingShape">
  172. /// The Maps SDK for Unity <see cref="MapFeature"/> data defining this building's shape and
  173. /// height.
  174. /// </param>
  175. /// <param name="parapetMaterial">
  176. /// The <see cref="Material"/> to apply to the parapet once it is created.
  177. /// </param>
  178. /// <param name="defaultBuildingHeight">
  179. /// Default height applied to Maps SDK for Unity generated buildings that do not have stored
  180. /// height information. If left blank, a value of 10f matches the default value used inside the
  181. /// SDK for buildings without stored heights.
  182. /// </param>
  183. /// <returns>
  184. /// Newly created <see cref="GameObject"/>s containing created parapet geometry.
  185. /// <para>
  186. /// One <see cref="GameObject"/> will be returned for each part of the given building.
  187. /// </para></returns>
  188. public static GameObject[] UpdateBuildingParapet(
  189. GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial,
  190. float defaultBuildingHeight = ExtrudedBuildingFootprintHeight) {
  191. RemoveCurrentParapet(buildingGameObject);
  192. return AddRandomBuildingParapet(
  193. buildingGameObject, buildingShape, parapetMaterial, defaultBuildingHeight);
  194. }
  195. /// <summary>
  196. /// Removes any child object from the supplied GameObject where the child's name indicates it is
  197. /// a parapet decoration.
  198. /// </summary>
  199. /// <param name="buildingGameObject">The object from which to remove existing parapet(s)</param>
  200. private static void RemoveCurrentParapet(GameObject buildingGameObject) {
  201. for (int i = 0; i < buildingGameObject.transform.childCount; i++) {
  202. GameObject child = buildingGameObject.transform.GetChild(i).gameObject;
  203. if (child.name == ParapetName) {
  204. GameObject.Destroy(child);
  205. }
  206. }
  207. }
  208. /// <summary>
  209. /// Adds a parapet of a specifically chosen cross-section to the given building.
  210. /// </summary>
  211. /// <param name="buildingGameObject">
  212. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this building.
  213. /// </param>
  214. /// <param name="buildingShape">
  215. /// The Maps SDK for Unity <see cref="MapFeature"/> data defining this building's shape and
  216. /// height.
  217. /// </param>
  218. /// <param name="parapetMaterial">
  219. /// The <see cref="Material"/> to apply to the parapet once it is created.
  220. /// </param>
  221. /// <param name="parapetType">
  222. /// Optional index of parapet to cross-section to use. Will use a randomly chosen cross-section
  223. /// if no index given, or if given index is invalid (in which case an error will also be
  224. /// printed).
  225. /// </param>
  226. /// <param name="defaultBuildingHeight">
  227. /// Default height applied to Maps SDK for Unity generated buildings that do not have stored
  228. /// height information. If left blank, a value of 10f matches the default value used inside the
  229. /// Maps SDK for buildings without stored heights.
  230. /// <para>
  231. /// The Maps SDK for Unity default height can be overriden with styling options, specifically
  232. /// <see cref="GameObjectOptions.ExtrudedStructureStyle"/>'s ExtrudedBuildingFootprintHeight. If
  233. /// this default height is overriden when calling <see cref="MapsService.LoadMap"/>, then this
  234. /// new default height value should also be used here to make sure that building parapets appear
  235. /// at the roof-level of all buildings, even if these buildings don't have a stored height.
  236. /// </para></param>
  237. /// <returns>
  238. /// Newly created <see cref="GameObject"/>s containing created parapet geometry.
  239. /// <para>
  240. /// One <see cref="GameObject"/> will be returned for each part of the given building.
  241. /// </para></returns>
  242. public static GameObject[] AddBuildingParapet(
  243. GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial,
  244. int parapetType, float defaultBuildingHeight = ExtrudedBuildingFootprintHeight) {
  245. return AddBuildingParapet(
  246. buildingGameObject, buildingShape, parapetMaterial, defaultBuildingHeight, parapetType);
  247. }
  248. /// <summary>
  249. /// Adds a parapet of a randomly chosen cross-section to the given building.
  250. /// </summary>
  251. /// <param name="buildingGameObject">
  252. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this building.
  253. /// </param>
  254. /// <param name="buildingShape">
  255. /// The Maps SDK for Unity <see cref="MapFeature"/> data defining this building's shape and
  256. /// height.
  257. /// </param>
  258. /// <param name="parapetMaterial">
  259. /// The <see cref="Material"/> to apply to the parapet once it is created.
  260. /// </param>
  261. /// <param name="defaultBuildingHeight">
  262. /// Default height applied to Maps SDK for Unity generated buildings that do not have stored
  263. /// height information. If left blank, a value of 10f matches the default value used inside the
  264. /// Maps SDK for buildings without stored heights.
  265. /// <para>
  266. /// The Maps SDK for Unity default height can be overriden with styling options, specifically
  267. /// <see cref="GameObjectOptions.ExtrudedStructureStyle"/>'s ExtrudedBuildingFootprintHeight. If
  268. /// this default height is overriden when calling <see cref="MapsService.LoadMap"/>, then this
  269. /// new default height value should also be used here to make sure that building parapets appear
  270. /// at the roof-level of all buildings, even if these buildings don't have a stored height.
  271. /// </para></param>
  272. /// <param name="parapetType">
  273. /// Optional index of parapet to cross-section to use. Will use a randomly chosen cross-section
  274. /// if no index given, or if given index is invalid (in which case an error will also be
  275. /// printed).
  276. /// </param>
  277. /// <returns>
  278. /// Newly created <see cref="GameObject"/>s containing created parapet geometry.
  279. /// <para>
  280. /// One <see cref="GameObject"/> will be returned for each part of the given building.
  281. /// </para></returns>
  282. private static GameObject[] AddBuildingParapet(
  283. GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial,
  284. float defaultBuildingHeight, int? parapetType) {
  285. // Create list to store all created parapets.
  286. List<GameObject> extrudedParapets = new List<GameObject>();
  287. for (int i = 0; i < buildingShape.Extrusions.Length; i++) {
  288. // Use ExtrudedBuildingFootPrintHeight constant for buildings that don't have any specified
  289. // height. The Maps SDK for Unity currently generates geometry using the default height, but
  290. // does not modify the actual MapFeature data passed to the callback. This may be addressed
  291. // in future Maps SDK for Unity releases.
  292. float height = buildingShape.Extrusions[i].MaxZ > 0.1f ? buildingShape.Extrusions[i].MaxZ
  293. : defaultBuildingHeight;
  294. // If a specific parapet type was given, verify it is valid.
  295. if (parapetType.HasValue) {
  296. if (parapetType.Value < 0 || parapetType.Value >= ParapetShapes.Length) {
  297. int invalidParapetType = parapetType.Value;
  298. parapetType = Random.Range(0, ParapetShapes.Length);
  299. Debug.LogErrorFormat(
  300. "{0} parapetType index given to {1}.AddBuildingParapet.\nValid " +
  301. "indices are in the range of 0 to {2} based on {3} cross-sections defined in " +
  302. "{1} class.\nDefaulting to randomly chosen parapetType index of {4}.",
  303. invalidParapetType < 0 ? "Negative" : "Invalid",
  304. typeof(Extruder),
  305. ParapetShapes.Length - 1,
  306. ParapetShapes.Length,
  307. parapetType.Value);
  308. }
  309. } else {
  310. // If no parapet type given, choose one at random.
  311. parapetType = Random.Range(0, ParapetShapes.Length);
  312. }
  313. // Use general-purpose building-extrusion function to add parapet around building. Do this
  314. // with a randomly chosen parapet shape for more variation throughout all created building
  315. // parapets.
  316. AddBuildingExtrusion(
  317. buildingGameObject,
  318. parapetMaterial,
  319. buildingShape.Extrusions[i],
  320. ParapetShapes[parapetType.Value],
  321. height,
  322. ref extrudedParapets,
  323. true,
  324. i,
  325. buildingShape.Extrusions.Length,
  326. DefaultWidth);
  327. }
  328. // Return all created parapets.
  329. return extrudedParapets.ToArray();
  330. }
  331. /// <summary>
  332. /// Adds a extruded shape for a given <see cref="ExtrudedArea.Extrusion"/> of a given building.
  333. /// </summary>
  334. /// <param name="buildingGameObject">
  335. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this building.
  336. /// </param>
  337. /// <param name="extrusionMaterial">
  338. /// The <see cref="Material"/> to apply to the extrusion once it is created. </param>
  339. /// <param name="extrusion">
  340. /// Current <see cref="ExtrudedArea.Extrusion"/> to extrude in given building.
  341. /// </param>
  342. /// Newly created <see cref="GameObject"/>s containing created extrusion geometry.
  343. /// <para>
  344. /// One <see cref="GameObject"/> will be returned for each part of the given building.
  345. /// </para>
  346. /// <param name="crossSection">The 2D crossSection of the shape to loft along the path.</param>
  347. /// <param name="yOffset">
  348. /// Amount to raise created shape vertically (e.g. amount to move parapet upwards so it sits at
  349. /// the top of a building).
  350. /// </param>
  351. /// <param name="extrusions">
  352. /// Reference to list used to store all extruded geometry created by this function.
  353. /// </param>
  354. /// <param name="isParapet">
  355. /// Whether or not desired extrusion is a parapet (used in error message if a problem occurs).
  356. /// </param>
  357. /// <param name="extrusionIndex">
  358. /// Index of current extrusion (used in error message if a problem occurs).
  359. /// </param>
  360. /// <param name="totalExtrusions">
  361. /// Total extrusions for this building (used in error message if a problem occurs).
  362. /// </param>
  363. /// <param name="thickness">Thickness of extrusion.</param>
  364. private static void AddBuildingExtrusion(
  365. GameObject buildingGameObject, Material extrusionMaterial, ExtrudedArea.Extrusion extrusion,
  366. Vector2[] crossSection, float yOffset, ref List<GameObject> extrusions, bool isParapet,
  367. int extrusionIndex, int totalExtrusions, float thickness) {
  368. // Build an extrusion in local space (at origin). Note that GenerateBoundaryEdges currently
  369. // incorrectly handles some pathological cases, for example, building chunks with a single
  370. // edge starting and ending outside the enclosing tile, so some very occasional misaligned
  371. // extrusions are to be expected.
  372. List<Area.EdgeSequence> loops = PadEdgeSequences(extrusion.FootPrint.GenerateBoundaryEdges());
  373. for (int i = 0; i < loops.Count; i++) {
  374. // Try to make extrusion.
  375. GameObject extrusionGameObject;
  376. String objectName = isParapet ? ParapetName : BorderName;
  377. if (CanMakeLoft(
  378. loops[i].Vertices.ToArray(),
  379. extrusionMaterial,
  380. crossSection,
  381. thickness,
  382. objectName,
  383. out extrusionGameObject)) {
  384. // Parent the extrusion to the building object.
  385. extrusionGameObject.transform.parent = buildingGameObject.transform;
  386. // Move created extrusion to align with the building in world space (offset vertically if
  387. // required).
  388. extrusionGameObject.transform.localPosition = Vector3.up * yOffset;
  389. // Add to list of extrusions that will be returned for this building.
  390. extrusions.Add(extrusionGameObject);
  391. } else {
  392. // If extrusion failed for any reason, print an error to this effect.
  393. Debug.LogErrorFormat(
  394. "{0} class was not able to create a {1} for building \"{2}\", " +
  395. "parent \"{3}\", extrusion {4} of {5}, loop {6} of {7}.\nFailure was caused by " +
  396. "there being not enough vertices to make a {0}.",
  397. typeof(Extruder),
  398. isParapet ? ParapetName : BorderName,
  399. buildingGameObject.name,
  400. buildingGameObject.transform.parent.name,
  401. extrusionIndex + 1,
  402. totalExtrusions,
  403. i + 1,
  404. loops.Count);
  405. }
  406. }
  407. }
  408. /// <summary>
  409. /// Adds an extruded outline around the edge of an area.
  410. /// </summary>
  411. /// <remarks>
  412. /// This method is included for backwards compatibility. It will possibly outline internal edges
  413. /// in the area. For displaying a stroked outline, it's likely better to use the
  414. /// <see cref="AddAreaExternalOutline"/> method.
  415. /// </remarks>
  416. /// <param name="areaObject">
  417. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this area.
  418. /// </param>
  419. /// <param name="extrusionMaterial">
  420. /// The <see cref="Material"/> to apply to the outline once it has been created.
  421. /// </param>
  422. /// <param name="area">
  423. /// The area to be outlined.
  424. /// </param>
  425. /// <param name="outlineWidth">
  426. /// The width (in Unity world coordinates) of the extruded outline..
  427. /// </param>
  428. /// <returns>
  429. /// Newly created <see cref="GameObject"/>s containing created outline geometry.
  430. /// <para>
  431. /// One <see cref="GameObject"/> will be returned for each part of the given area.
  432. /// </para></returns>
  433. public static GameObject[] AddAreaOutline(
  434. GameObject areaObject, Material extrusionMaterial, Area area, float outlineWidth) {
  435. return ExtrudeEdgeSequences(
  436. areaObject, extrusionMaterial, area.GenerateBoundaryEdges(), outlineWidth);
  437. }
  438. /// <summary>
  439. /// Adds an extruded outline around the edge of an area. This is useful for displaying a stroke
  440. /// on an area.
  441. /// </summary>
  442. /// <param name="areaObject">
  443. /// The Maps SDK for Unity created <see cref="GameObject"/> containing this area.
  444. /// </param>
  445. /// <param name="extrusionMaterial">
  446. /// The <see cref="Material"/> to apply to the outline once it has been created.
  447. /// </param>
  448. /// <param name="area">
  449. /// The area to be extruded.
  450. /// </param>
  451. /// <param name="outlineWidth">
  452. /// The width (in Unity world coordinates) of the extruded outline..
  453. /// </param>
  454. /// <returns>
  455. /// Newly created <see cref="GameObject"/>s containing created outline geometry.
  456. /// <para>
  457. /// One <see cref="GameObject"/> will be returned for each part of the given area.
  458. /// </para></returns>
  459. public static GameObject[] AddAreaExternalOutline(
  460. GameObject areaObject, Material extrusionMaterial, Area area, float outlineWidth) {
  461. return ExtrudeEdgeSequences(
  462. areaObject, extrusionMaterial, area.GenerateExternalBoundaryEdges(), outlineWidth);
  463. }
  464. /// <summary>
  465. /// Extrudes a list of edge sequences outwards.
  466. /// </summary>
  467. /// <param name="areaObject">
  468. /// The Maps SDK for Unity created <see cref="GameObject"/> to which the extruded outline should
  469. /// be added.
  470. /// </param>
  471. /// <param name="extrusionMaterial">
  472. /// The <see cref="Material"/> to apply to the outline once it has been created.
  473. /// </param>
  474. /// <param name="edgeSequences">
  475. /// The edge sequences to be extruded. These should come from
  476. /// <see cref="Area.GenerateBoundaryEdges"/> or <see cref="Area.GenerateExternalBoundaryEdges"/>
  477. /// </param>
  478. /// <param name="outlineWidth">
  479. /// The width (in Unity world coordinates) of the extruded outline..
  480. /// </param>
  481. /// <returns>
  482. /// Newly created <see cref="GameObject"/>s containing created outline geometry.
  483. /// <para>
  484. /// One <see cref="GameObject"/> will be returned for each edge sequence.
  485. /// </para></returns>
  486. private static GameObject[] ExtrudeEdgeSequences(
  487. GameObject areaObject, Material extrusionMaterial, List<Area.EdgeSequence> edgeSequences,
  488. float outlineWidth) {
  489. List<Area.EdgeSequence> loops = PadEdgeSequences(edgeSequences);
  490. List<GameObject> result = new List<GameObject>();
  491. Vector2[] crossSection = OutlineCrossSectionWithWidth(outlineWidth);
  492. for (int i = 0; i < loops.Count; i++) {
  493. // Try to make extrusion.
  494. GameObject extrusionGameObject;
  495. if (CanMakeLoft(
  496. loops[i].Vertices.ToArray(),
  497. extrusionMaterial,
  498. crossSection,
  499. DefaultWidth,
  500. OutlineName,
  501. out extrusionGameObject)) {
  502. // Parent the extrusion to the area object.
  503. extrusionGameObject.transform.parent = areaObject.transform;
  504. // Add to list of extrusions that will be returned for this area.
  505. result.Add(extrusionGameObject);
  506. } else {
  507. // If extrusion failed for any reason, print an error to this effect.
  508. Debug.LogErrorFormat(
  509. "{0} class was not able to create an outline for area \"{1}\", " +
  510. "loop {2} of {3}.\nFailure was caused by there being not enough vertices to " +
  511. "make an outline.",
  512. typeof(Extruder),
  513. areaObject.name,
  514. i + 1,
  515. loops.Count);
  516. }
  517. }
  518. return result.ToArray();
  519. }
  520. /// <summary>
  521. /// Returns a canonical representation of the supplied <see cref="Area.EdgeSequence"/>s to
  522. /// facilitate easy creation of, e.g., parapets for both open and closed edge sequences.
  523. /// </summary>
  524. /// <remarks>
  525. /// <para>
  526. /// The padded edge sequences returned by this method are designed so that a continuous sequence
  527. /// exists from Vertices[1], to Vertices[Vertices.Count - 2] (of the returned EdgeSequence) with
  528. /// adjacent vertices providing proper edge tangent directions.
  529. /// </para><para>
  530. /// For closed sequences, this simply duplicates the vertices either side of the starting/ending
  531. /// vertex. For open sequences, new vertices are added by parallel extension of the first and
  532. /// last edge. This means that open sequences will have flat ends, while closed sequences can
  533. /// generate geometry with properly mitred first and last edge vertices.
  534. /// </para>
  535. /// </remarks>
  536. /// <param name="edgeSequences">The edge sequences to canonicalize.</param>
  537. /// <returns>Padded copies of supplied edge sequences.</returns>
  538. private static List<Area.EdgeSequence> PadEdgeSequences(List<Area.EdgeSequence> edgeSequences) {
  539. List<Area.EdgeSequence> result = new List<Area.EdgeSequence>(edgeSequences.Count);
  540. foreach (Area.EdgeSequence sequence in edgeSequences) {
  541. // Filter out any pathological sequences.
  542. if (sequence.Vertices.Count < 2) {
  543. continue;
  544. }
  545. List<Vector2> vertices = new List<Vector2>();
  546. vertices.AddRange(sequence.Vertices);
  547. int vertexCount = vertices.Count;
  548. Vector2 start;
  549. Vector2 end;
  550. if (vertexCount > 2 && vertices[0] == vertices[vertexCount - 1]) {
  551. start = vertices[vertexCount - 2];
  552. end = vertices[1];
  553. } else {
  554. start = vertices[0] - (vertices[1] - vertices[0]).normalized;
  555. end = vertices[vertexCount - 1] +
  556. (vertices[vertexCount - 1] - vertices[vertexCount - 2]).normalized;
  557. }
  558. vertices.Insert(0, start);
  559. vertices.Add(end);
  560. result.Add(new Area.EdgeSequence(vertices));
  561. }
  562. return result;
  563. }
  564. /// <summary>
  565. /// Create extrusion geometry using the supplied footprintVertices.
  566. /// </summary>
  567. /// <param name="footprintVertices">The 2D corners of the building footprint.</param>
  568. /// <param name="extrusionMaterial"><see cref="Material"/> to apply to created
  569. /// extrusion.</param> <param name="crossSection">The 2D crossSection of the shape to loft along
  570. /// the path.</param> <param name="thickness">Thickness of loft.</param> <param
  571. /// name="loftName">The name that should be given to the created game object.</param> <param
  572. /// name="createdLoft"> Created extrusion <see cref="GameObject"/>, at the origin with no
  573. /// parent, or null if lofting failed for any reason.
  574. /// </param>
  575. /// <returns>Whether or not loft could be created.</returns>
  576. private static bool CanMakeLoft(
  577. Vector2[] footprintVertices, Material extrusionMaterial, Vector2[] crossSection,
  578. float thickness, string loftName, out GameObject createdLoft) {
  579. Vector3[] vertices;
  580. int[] indices;
  581. Vector2[] uvs;
  582. // Attempt to make loft from given data.
  583. if (CanLoft(footprintVertices, crossSection, thickness, out vertices, out indices, out uvs)) {
  584. GameObject extrusion = new GameObject(loftName);
  585. MeshFilter meshFilter = extrusion.AddComponent<MeshFilter>();
  586. MeshRenderer meshRenderer = extrusion.AddComponent<MeshRenderer>();
  587. // Add a mesh cleaner to force GC on the instantiated mesh when the GameObject is destroyed.
  588. // Note that a bulk deletion of meshes results in a slight stuttering of the dynamic
  589. // loading/unloading of new map regions.
  590. // A proper solution would be to use an object pool and recycle mesh objects as needed.
  591. extrusion.AddComponent<MeshCleaner>();
  592. meshRenderer.material = extrusionMaterial;
  593. Mesh mesh = new Mesh { vertices = vertices, triangles = indices, uv = uvs };
  594. mesh.RecalculateNormals();
  595. meshFilter.mesh = mesh;
  596. createdLoft = extrusion;
  597. return true;
  598. }
  599. // If have reached this point then lofting failed.
  600. createdLoft = null;
  601. return false;
  602. }
  603. /// <summary>
  604. /// Creates a 3d "loft" of a shape along a path by running a given crossSection along the path.
  605. /// </summary>
  606. /// <remarks>
  607. /// Lofting refers to running a shape along a path, creating a 3d volume based on where the
  608. /// shape has travelled. For example, lofting a circle straight upwards would give a cylinder,
  609. /// while lofting a circle around another, larger circle would give a donut. In both cases the
  610. /// volume is formed by the journey of the lofted-circle along its given path. <para> Returns 3D
  611. /// mesh data (vertices, triangles, indices) returned in the supplied output parameter arrays.
  612. /// </para></remarks>
  613. /// <param name="paddedPath">
  614. /// <para>A padded version of the path along which to loft the given cross-section. This is the
  615. /// path along which to extrude the supplied cross-section with ghost vertices added at the
  616. /// beginning and end. These ghost vertices are only used to determine the direction of the path
  617. /// at the start and end vertices. For a closed path, these should be copies of the second to
  618. /// last and second vertices. For an open path the prepended ghost vertex should be the
  619. /// reflection of the second vertex through the first path vertex (paddedPath[0] = 2 * path[0] -
  620. /// path[1]), and the appended ghost vertex should be the reflection of the second to last path
  621. /// vertex around the last path vertex (paddedPath[last + 2] = 2 * path[last] - path[last - 1]).
  622. /// See <see cref="PadEdgeSequences"/> for how this is done. This pre-padding model provides
  623. /// simple, unified handling of both open and closed paths, allowing lofting to work on
  624. /// incomplete building chunks at the edge of the loaded map region.
  625. /// </para><para>This path takes the form of an array of vertices, for example, defining the
  626. /// top-down shape of a building, such that traversing these vertices allows traversing
  627. /// counter-clockwise around the base of the building. These vertices are in Vector2 format,
  628. /// where x and y represent x and z coordinates (i.e. 2d coordinates in a ground-plane at y =
  629. /// 0).
  630. /// </para>
  631. /// </param>
  632. /// <param name="crossSection">
  633. /// The 2D cross-section of the shape to loft along the given path. Much like the path, this
  634. /// cross-section takes the form of an array of vertices, such that traversing these vertices
  635. /// allows traversing counter-clockwise around the flat shape to loft. These vertices are in
  636. /// Vector2 format, where x and y represent coordinates in a flat plane comprising just the
  637. /// shape to loft.
  638. /// </param>
  639. /// <param name="thickness">Thickness of loft.</param>
  640. /// <param name="vertices">Outputted mesh vertices.</param>
  641. /// <param name="triangleIndices">Outputted mesh triangle indices.</param>
  642. /// <param name="uvs">Outputted mesh UVs.</param>
  643. /// <returns>Whether or not lofting succeeded.</returns>.
  644. private static bool CanLoft(
  645. Vector2[] paddedPath, Vector2[] crossSection, float thickness, out Vector3[] vertices,
  646. out int[] triangleIndices, out Vector2[] uvs) {
  647. // Make sure there are enough vertices to create a loft.
  648. if (paddedPath.Length < 2 || crossSection.Length < 2) {
  649. vertices = null;
  650. triangleIndices = null;
  651. uvs = null;
  652. return false;
  653. }
  654. // Determine the total number of vertices and triangles needed to create the lofted volume.
  655. // Note that the total number of triangle indices is based on the fact that each combination
  656. // of path-segment and cross-section segment generates a quad of two triangles, requiring 6
  657. // triangle indices each to represent.
  658. int segments = paddedPath.Length - 3;
  659. int totalTriangleIndices = (crossSection.Length - 1) * segments * 6;
  660. int trianglesPerSegment = crossSection.Length * 2 - 2;
  661. int verticesPerJunction = crossSection.Length * 2 - 2;
  662. int totalVertices = verticesPerJunction * (segments + 1);
  663. // Create arrays to hold vertices, uvs and triangle-indices that will be used to create the
  664. // lofted volume.
  665. vertices = new Vector3[totalVertices];
  666. uvs = new Vector2[totalVertices];
  667. triangleIndices = new int[totalTriangleIndices];
  668. // Perform actual lofting.
  669. int vertexIndex = 0;
  670. int triIndex = 0;
  671. int startCorner = 1;
  672. // The path has been padded by this point with ghost vertices at the beginning and end to
  673. // simplify the calculation of previous and next directions (see PadEdgeSequences), so we
  674. // ignore the first and last vertices in the following loop, only considering vertices from
  675. // the original path.
  676. for (int cornerInPath = startCorner; cornerInPath < paddedPath.Length - 1; cornerInPath++) {
  677. // Note that we refer to these vertices as corners, because even though they are three
  678. // vertices in the given path, it's better to think of what they actually represent: three
  679. // adjacent corners of a building's base.
  680. Vector3 currentCorner =
  681. new Vector3(paddedPath[cornerInPath].x, 0f, paddedPath[cornerInPath].y);
  682. Vector3 nextCorner =
  683. new Vector3(paddedPath[cornerInPath + 1].x, 0f, paddedPath[cornerInPath + 1].y);
  684. Vector3 previousCorner =
  685. new Vector3(paddedPath[cornerInPath - 1].x, 0f, paddedPath[cornerInPath - 1].y);
  686. // Get the directions from the current corner to the next and previous corners.
  687. Vector3 directionToPrevious = (previousCorner - currentCorner).normalized;
  688. Vector3 directionToNext = (nextCorner - currentCorner).normalized;
  689. // The path we are lofting is assumed to be in counterclockwise winding order (assuming +x
  690. // right, +y up). We can calculate whether the path turns left or right placing the previous
  691. // and next direction vectors on the ground plane and calculating the cross product of next
  692. // direction with previous direction. Unity uses a left handed coordinate system, so we know
  693. // that if this vector points down (y < 0), the vectors represent a left turn in the path.
  694. Vector3 turnCross = Vector3.Cross(directionToNext, directionToPrevious);
  695. bool isLeftTurn = turnCross.y < 0;
  696. // Add the previous and next edges to get their bisector -- a line that cuts this corner in
  697. // half. For very nearly parallel edges, the next and previous directions will face in
  698. // almost exactly opposite directions so the sum will be degenerately small. To avoid
  699. // numerical issues, we simply take the right hand perpendicular of the nextEdge.
  700. Vector3 bisector = directionToNext + directionToPrevious;
  701. // Consider lines to be colinear if the angle is less than approximately 1 degree.
  702. if (bisector.magnitude > 0.015f) {
  703. bisector.Normalize();
  704. } else {
  705. // Cross product with down gives right perpendicular in Unity's left handed coordinates.
  706. bisector = Vector3.Cross(directionToNext, Vector3.down);
  707. isLeftTurn = false;
  708. }
  709. // We wish to use the right bisector as the x-axis for the coordinates in the cross section
  710. // of the loft. If the path turns left at the current point, the bisector of the inner angle
  711. // will also point left, so we need to reverse it.
  712. // Note that for lofting around buildings, the path of the walls is counterclockwise, when
  713. // viewed from above, so the rightBisector will point away from the building.
  714. Vector3 rightBisector;
  715. if (isLeftTurn) {
  716. rightBisector = -bisector;
  717. } else {
  718. rightBisector = bisector;
  719. }
  720. // Given the previous edge, imagine a new, extruded edge, parallel to the previous edge but
  721. // extruded outwards by the desired extrusion width. If we make a similar extruded edge
  722. // pushed out from the next edge, and find where these two edges intersect, we get a new,
  723. // extruded corner - the corner of an extrusion that is of uniform length all the way
  724. // around the shape.
  725. //
  726. // Previous Edge, Extruded
  727. // Extruded Corner ●──────────○──────────────────────────
  728. // ╱ ╎_|
  729. // ╱ ╎
  730. // ╱ ╎
  731. // Next Edge, ╱ ╎ Extrusion Width
  732. // Extruded ╱ ╎
  733. // ╱ ╎
  734. // ╱ ╎
  735. // ╱ | Previous Edge
  736. // ╱ Current Corner ●───────────────────────
  737. // ╱ ╱
  738. // ╱ ╱
  739. // ╱ ╱ Next Edge
  740. // ╱ ╱
  741. //
  742. // We could calculate these lines and find their intersection. But vectors give us a
  743. // shortcut.
  744. //
  745. // To do this, we get the right hand bisector of this corner (an outward pointing line that
  746. // cuts the corner in half, such that the angle between this bisector and the previous edge
  747. // is the same as the angle between this bisector and the next edge). If we can determine
  748. // the length of this bisector, we will know exactly how far to travel along it from the
  749. // current corner to the new, extruded corner.
  750. //
  751. // Previous Edge, Extruded
  752. // Extruded Corner ●────────○──────────────────────────
  753. // ╱ ╲ |
  754. // ╱ ╲ │
  755. // ╱ ╲ │
  756. // ╱ ╲ │ E
  757. // ╱ B ╲ │
  758. // ╱ ╲ │
  759. // ╱ ╲ │_
  760. // ╱ ╲│ │
  761. // ╱ Current Corner ●───────────────────────>
  762. // ╱ ╱ Previous direction vector
  763. // ╱ ╱
  764. // ╱ ╱
  765. // ╱ ╱
  766. //
  767. // In this diagram 'E' is the 'extrusion vector', the vector between the current corner and
  768. // the previous, extruded edge (whose length |E| is the desired extrusion width). We can get
  769. // this extrusion vector using the cross product of the previous direction vector (pointing
  770. // back along the previous edge) with the up vector to find the left hand perpendicular
  771. // vector of the previous direction vector as shown above. (Note that Unity uses a left
  772. // handed coordinate system, giving us the left hand perpendicular from the cross product)
  773. Vector3 normalizedExtrusionVector =
  774. Vector3.Cross(directionToPrevious, Vector3.up).normalized;
  775. // In the diagram above, if we project the vector B onto a unit vector in the direction of
  776. // E (unitE) we get the vector E. This implies that the length of this projected vector is
  777. // the same as the length of E, which is just the extrusion distance. This gives us:
  778. // B . unitE = |E|
  779. // which, given that B is just a unit vector in the direction of B times |B|, gives:
  780. // (|B| unitB) . unitE = |E|
  781. // giving:
  782. // |B| (unitB . unitE) = |E|
  783. // leading to:
  784. // |B| = |E| / (unitB . unitE)
  785. //
  786. // We know |E| is just the given thickness, and have calculated unitE as
  787. // normalizeExtrusionVector and unitB as rightBisector, which allows us to calculate the
  788. // length of B as distanceToExtrude as follows.
  789. float distanceToExtrude = thickness / Vector3.Dot(normalizedExtrusionVector, rightBisector);
  790. // Make sure this extrusion distance is not unrealistically long (can occur for extremely
  791. // sharp angles) by limiting it to twice the desired Extrusion Width.
  792. if (distanceToExtrude > 2f * thickness) {
  793. distanceToExtrude = 2f * thickness;
  794. }
  795. // Create a copy of the cross-section shape using a coordinate system where the x-axis is
  796. // the rightBisector and the y-axis is the up vector. This aligns the shape to the plane
  797. // bisecting the joint angle in the path. When all these cross-sections are joined, they
  798. // will form a loft of the given cross-section around the given shape.
  799. for (int pointInCrossSection = 0; pointInCrossSection < crossSection.Length;
  800. pointInCrossSection++) {
  801. // Align the values of this cross-section point to this corner - i.e. convert from a 2D,
  802. // x, y shape into a 3D x, y, z shape aligned to the desired extrusion direction. Also
  803. // multiply the x values by the extrusion distance, to make sure that the loft is
  804. // stretched to the desired extrusion distance away from all sides of the shape.
  805. Vector3 pv = crossSection[pointInCrossSection].x * rightBisector * distanceToExtrude;
  806. Vector3 uv = crossSection[pointInCrossSection].y * Vector3.up;
  807. Vector3 vertex = currentCorner + pv + uv;
  808. vertices[vertexIndex] = vertex;
  809. uvs[vertexIndex] = new Vector3(vertex.x + vertex.y, vertex.z + vertex.y) / UvScale;
  810. // Generate triangle-indices needed to connect this edge of this cross-section to the
  811. // same edge of the next cross-section.
  812. if (pointInCrossSection > 0 && cornerInPath > startCorner) {
  813. triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex - 1);
  814. triangleIndices[triIndex++] =
  815. SafeMod(totalVertices, vertexIndex - trianglesPerSegment - 1);
  816. triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex);
  817. triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex);
  818. triangleIndices[triIndex++] =
  819. SafeMod(totalVertices, vertexIndex - trianglesPerSegment - 1);
  820. triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex - trianglesPerSegment);
  821. }
  822. // Copy vertices so can have un-smoothed normals. In order to have normals be unique to
  823. // each face, each corner must have multiple vertices, one for each face.
  824. if (pointInCrossSection > 0 && pointInCrossSection < crossSection.Length - 1) {
  825. vertices[vertexIndex + 1] = vertices[vertexIndex];
  826. uvs[vertexIndex + 1] = uvs[vertexIndex];
  827. vertexIndex++;
  828. }
  829. // Move to the next vertex to store in the generated loft.
  830. vertexIndex++;
  831. }
  832. }
  833. // If have reached this point then have successfully created loft.
  834. return true;
  835. }
  836. /// <summary>A version of mod that works for negative values.</summary>
  837. /// <remarks>
  838. /// This function ensures that returned modulated value will always be positive for values
  839. /// greater than the negative of the modulus argument.
  840. /// </remarks>
  841. /// <param name="mod">The modulus argument.</param>
  842. /// <param name="val">The value to modulate.</param>
  843. /// <returns>A range safe version of value % mod.</returns>
  844. private static int SafeMod(int mod, int val) {
  845. return (val + mod) % mod;
  846. }
  847. /// <summary>
  848. /// Convert a given array of floats into an array of <see cref="Vector2"/>s.
  849. /// </summary>
  850. /// <remarks>
  851. /// Each pair of arguments becomes one Vector2, with an exception triggered if the number of
  852. /// floats given is not even.
  853. /// </remarks>
  854. private static Vector2[] MakeVector2Array(params float[] floats) {
  855. // Confirm an even number of floats have been given.
  856. if (floats.Length % 2 != 0) {
  857. throw new ArgumentException("Arguments must be provided in pairs");
  858. }
  859. // Return each pair of floats as one element of an array of Vector2's.
  860. Vector2[] vectors = new Vector2[floats.Length / 2];
  861. for (int i = 0; i < floats.Length; i += 2) {
  862. vectors[i / 2] = new Vector2(floats[i], floats[i + 1]);
  863. }
  864. return vectors;
  865. }
  866. }
  867. }