using System; using System.Collections.Generic; using Google.Maps.Feature; using Google.Maps.Feature.Shape; using UnityEngine; using Random = UnityEngine.Random; namespace Google.Maps.Examples.Shared { /// /// Static class containing logic for creating extrusions and lofts from given geometry built by /// the Maps SDK for Unity. /// public static class Extruder { /// Default thickness of created extrusions. /// This value will be used for all extrusions unless a custom value is /// given. private const float DefaultWidth = 1.0f; /// Name given to s created as parapets. private const string ParapetName = "Parapet"; /// /// Name given to s created as building base decorations. /// private const string BorderName = "Border"; /// Name given to s created as area outlines. private const string OutlineName = "Outline"; /// /// Physical scale (in meters) of the applied to the extrusions. /// /// /// The extrusion generation code assigns UVs based on real world scale in meters, meaning that /// two vertices that are a meter apart will have a UV coordinate that differs by 1. /// /// For example, if you have a texture that corresponds to a 5x5 meter square, this value /// should be set to 5.0f. This is an alternative to using a texture tiling of 0.2f on (i.e. /// tile 5 times per 1 uv value) on a Unity Standard Material. /// private const float UvScale = 1.0f; /// /// Two dimensional cross-sections we will use to form flat extrusions around a given shape. /// /// /// See for an explanation of the coordinate system. /// private static readonly Vector2[] BorderShape = { new Vector2(1f, 0f), new Vector2(0f, 0f) }; /// /// Default height applied to Maps SDK for Unity generated buildings that do not have stored /// height information. The chosen value of 10f matches the default value used inside the Maps /// SDK for buildings without stored heights. The Maps SDK for Unity default height can /// be overriden with styling options, specifically 's ExtrudedBuildingFootprintHeight. If this /// default height is overriden when calling , then this new /// default height value should also be given when calling /// to make sure that building parapets appear at the /// roof-level of all buildings, even if these buildings don't have a stored height. /// private const float ExtrudedBuildingFootprintHeight = 10.0f; /// /// Two dimensional cross-sections that will be used to form the parapets. /// /// /// Each entry defines the cross section of a parapet in a coordinate space relative to the /// outer roof-edge of the building, where the positive x-axis points out away from the /// building, and the positive y-axis points towards the sky. So, for example: (1, 0) is 1 /// meter out of the building. (1, -1) is 1 meter out, 1 meter down. /// Outlines should be specified with an counterclockwise winding order (assuming +x /// right, +y up) to ensure the normals of the generated geometry face in the correct /// direction. /// private static readonly Vector2[][] ParapetShapes = { // A square parapet running along the outer edge of a roof, not overhanging exterior walls. MakeVector2Array(0f, 0f, 0f, 1f, -1f, 1f, -1f, 0f), // A square parapet running along the outer edge of a roof, slightly overlapping the roof, and // overhanging exterior walls. MakeVector2Array(-0.5f, 0f, 1f, 0f, 1f, 1f, -0.5f, 1f, -0.5f, 0f), // A stepped parapet that overhangs exterior walls, with the steps facing down towards the // ground. MakeVector2Array(-1f, 0f, 0.5f, 0f, 0.5f, 0.5f, 1f, 0.5f, 1f, 1.0f, -1f, 1f, -1f, 0f), // A stepped parapet that does not overhang exterior walls, with the steps facing upwards // towards the sky. MakeVector2Array(0f, 0f, 0f, 1f, -0.5f, 1f, -0.5f, 0.5f, -1f, 0.5f, -1f, 0f), // A bevelled parapet that overhangs exterior walls, similar to the steps facing upwards but // with a slope in place of the middle step. MakeVector2Array(0f, -0.5f, 1f, -0.5f, 1f, 0.5f, 0.5f, 1f, 0f, 1f, 0f, -0.5f) }; /// Returns a cross-section to be used for an outline with a given width. private static Vector2[] OutlineCrossSectionWithWidth(float width) { return new Vector2[] { new Vector2(width / 2, 0f), new Vector2(-width / 2, 0f) }; } /// Adds a extruded border around the base of a given building. /// /// The Maps SDK for Unity created containing this building. /// /// /// The Maps SDK for Unity data defining this building's shape and /// height. /// /// /// The to apply to the extrusion once it is created. /// /// /// Newly created s containing created extrusion geometry. /// /// One will be returned for each part of the given building. /// public static GameObject[] AddBuildingBorder( GameObject buildingGameObject, ExtrudedArea buildingShape, Material borderMaterial, float thickness = DefaultWidth) { // Create list to store all created borders. List extrudedBorders = new List(); for (int i = 0; i < buildingShape.Extrusions.Length; i++) { // Use general-purpose building-extrusion function to add border around building. AddBuildingExtrusion( buildingGameObject, borderMaterial, buildingShape.Extrusions[i], BorderShape, 0f, ref extrudedBorders, false, i, buildingShape.Extrusions.Length, thickness); } // Return all created extrusions. return extrudedBorders.ToArray(); } /// /// Adds a parapet of a randomly chosen cross-section to the given building. /// /// /// The Maps SDK for Unity created containing this building. /// /// /// The Maps SDK for Unity data defining this building's shape and /// height. /// /// /// The to apply to the parapet once it is created. /// /// /// Default height applied to Maps SDK for Unity generated buildings that do not have stored /// height information. If left blank, a value of 10f matches the default value used inside the /// Maps SDK for buildings without stored heights. /// /// The Maps SDK for Unity default height can be overriden with styling options, specifically /// 's ExtrudedBuildingFootprintHeight. If /// this default height is overriden when calling , then this /// new default height value should also be used here to make sure that building parapets appear /// at the roof-level of all buildings, even if these buildings don't have a stored height. /// /// /// Newly created s containing created parapet geometry. /// /// One will be returned for each part of the given building. /// public static GameObject[] AddRandomBuildingParapet( GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial, float defaultBuildingHeight = ExtrudedBuildingFootprintHeight) { return AddBuildingParapet( buildingGameObject, buildingShape, parapetMaterial, defaultBuildingHeight, null); } /// Updates the parapet decoration on a building. /// /// This method removes any existing parapet decoration and builds a new parapet child object in /// the same manner as the method. No attempt is made to /// retain the same parapet type. In a more sophisticated implementation, the assigned parapet /// type would be stored on the building GameObject to be retrieved here. /// /// /// The Maps SDK for Unity created containing this building. /// /// /// The Maps SDK for Unity data defining this building's shape and /// height. /// /// /// The to apply to the parapet once it is created. /// /// /// Default height applied to Maps SDK for Unity generated buildings that do not have stored /// height information. If left blank, a value of 10f matches the default value used inside the /// SDK for buildings without stored heights. /// /// /// Newly created s containing created parapet geometry. /// /// One will be returned for each part of the given building. /// public static GameObject[] UpdateBuildingParapet( GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial, float defaultBuildingHeight = ExtrudedBuildingFootprintHeight) { RemoveCurrentParapet(buildingGameObject); return AddRandomBuildingParapet( buildingGameObject, buildingShape, parapetMaterial, defaultBuildingHeight); } /// /// Removes any child object from the supplied GameObject where the child's name indicates it is /// a parapet decoration. /// /// The object from which to remove existing parapet(s) private static void RemoveCurrentParapet(GameObject buildingGameObject) { for (int i = 0; i < buildingGameObject.transform.childCount; i++) { GameObject child = buildingGameObject.transform.GetChild(i).gameObject; if (child.name == ParapetName) { GameObject.Destroy(child); } } } /// /// Adds a parapet of a specifically chosen cross-section to the given building. /// /// /// The Maps SDK for Unity created containing this building. /// /// /// The Maps SDK for Unity data defining this building's shape and /// height. /// /// /// The to apply to the parapet once it is created. /// /// /// Optional index of parapet to cross-section to use. Will use a randomly chosen cross-section /// if no index given, or if given index is invalid (in which case an error will also be /// printed). /// /// /// Default height applied to Maps SDK for Unity generated buildings that do not have stored /// height information. If left blank, a value of 10f matches the default value used inside the /// Maps SDK for buildings without stored heights. /// /// The Maps SDK for Unity default height can be overriden with styling options, specifically /// 's ExtrudedBuildingFootprintHeight. If /// this default height is overriden when calling , then this /// new default height value should also be used here to make sure that building parapets appear /// at the roof-level of all buildings, even if these buildings don't have a stored height. /// /// /// Newly created s containing created parapet geometry. /// /// One will be returned for each part of the given building. /// public static GameObject[] AddBuildingParapet( GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial, int parapetType, float defaultBuildingHeight = ExtrudedBuildingFootprintHeight) { return AddBuildingParapet( buildingGameObject, buildingShape, parapetMaterial, defaultBuildingHeight, parapetType); } /// /// Adds a parapet of a randomly chosen cross-section to the given building. /// /// /// The Maps SDK for Unity created containing this building. /// /// /// The Maps SDK for Unity data defining this building's shape and /// height. /// /// /// The to apply to the parapet once it is created. /// /// /// Default height applied to Maps SDK for Unity generated buildings that do not have stored /// height information. If left blank, a value of 10f matches the default value used inside the /// Maps SDK for buildings without stored heights. /// /// The Maps SDK for Unity default height can be overriden with styling options, specifically /// 's ExtrudedBuildingFootprintHeight. If /// this default height is overriden when calling , then this /// new default height value should also be used here to make sure that building parapets appear /// at the roof-level of all buildings, even if these buildings don't have a stored height. /// /// /// Optional index of parapet to cross-section to use. Will use a randomly chosen cross-section /// if no index given, or if given index is invalid (in which case an error will also be /// printed). /// /// /// Newly created s containing created parapet geometry. /// /// One will be returned for each part of the given building. /// private static GameObject[] AddBuildingParapet( GameObject buildingGameObject, ExtrudedArea buildingShape, Material parapetMaterial, float defaultBuildingHeight, int? parapetType) { // Create list to store all created parapets. List extrudedParapets = new List(); for (int i = 0; i < buildingShape.Extrusions.Length; i++) { // Use ExtrudedBuildingFootPrintHeight constant for buildings that don't have any specified // height. The Maps SDK for Unity currently generates geometry using the default height, but // does not modify the actual MapFeature data passed to the callback. This may be addressed // in future Maps SDK for Unity releases. float height = buildingShape.Extrusions[i].MaxZ > 0.1f ? buildingShape.Extrusions[i].MaxZ : defaultBuildingHeight; // If a specific parapet type was given, verify it is valid. if (parapetType.HasValue) { if (parapetType.Value < 0 || parapetType.Value >= ParapetShapes.Length) { int invalidParapetType = parapetType.Value; parapetType = Random.Range(0, ParapetShapes.Length); Debug.LogErrorFormat( "{0} parapetType index given to {1}.AddBuildingParapet.\nValid " + "indices are in the range of 0 to {2} based on {3} cross-sections defined in " + "{1} class.\nDefaulting to randomly chosen parapetType index of {4}.", invalidParapetType < 0 ? "Negative" : "Invalid", typeof(Extruder), ParapetShapes.Length - 1, ParapetShapes.Length, parapetType.Value); } } else { // If no parapet type given, choose one at random. parapetType = Random.Range(0, ParapetShapes.Length); } // Use general-purpose building-extrusion function to add parapet around building. Do this // with a randomly chosen parapet shape for more variation throughout all created building // parapets. AddBuildingExtrusion( buildingGameObject, parapetMaterial, buildingShape.Extrusions[i], ParapetShapes[parapetType.Value], height, ref extrudedParapets, true, i, buildingShape.Extrusions.Length, DefaultWidth); } // Return all created parapets. return extrudedParapets.ToArray(); } /// /// Adds a extruded shape for a given of a given building. /// /// /// The Maps SDK for Unity created containing this building. /// /// /// The to apply to the extrusion once it is created. /// /// Current to extrude in given building. /// /// Newly created s containing created extrusion geometry. /// /// One will be returned for each part of the given building. /// /// The 2D crossSection of the shape to loft along the path. /// /// Amount to raise created shape vertically (e.g. amount to move parapet upwards so it sits at /// the top of a building). /// /// /// Reference to list used to store all extruded geometry created by this function. /// /// /// Whether or not desired extrusion is a parapet (used in error message if a problem occurs). /// /// /// Index of current extrusion (used in error message if a problem occurs). /// /// /// Total extrusions for this building (used in error message if a problem occurs). /// /// Thickness of extrusion. private static void AddBuildingExtrusion( GameObject buildingGameObject, Material extrusionMaterial, ExtrudedArea.Extrusion extrusion, Vector2[] crossSection, float yOffset, ref List extrusions, bool isParapet, int extrusionIndex, int totalExtrusions, float thickness) { // Build an extrusion in local space (at origin). Note that GenerateBoundaryEdges currently // incorrectly handles some pathological cases, for example, building chunks with a single // edge starting and ending outside the enclosing tile, so some very occasional misaligned // extrusions are to be expected. List loops = PadEdgeSequences(extrusion.FootPrint.GenerateBoundaryEdges()); for (int i = 0; i < loops.Count; i++) { // Try to make extrusion. GameObject extrusionGameObject; String objectName = isParapet ? ParapetName : BorderName; if (CanMakeLoft( loops[i].Vertices.ToArray(), extrusionMaterial, crossSection, thickness, objectName, out extrusionGameObject)) { // Parent the extrusion to the building object. extrusionGameObject.transform.parent = buildingGameObject.transform; // Move created extrusion to align with the building in world space (offset vertically if // required). extrusionGameObject.transform.localPosition = Vector3.up * yOffset; // Add to list of extrusions that will be returned for this building. extrusions.Add(extrusionGameObject); } else { // If extrusion failed for any reason, print an error to this effect. Debug.LogErrorFormat( "{0} class was not able to create a {1} for building \"{2}\", " + "parent \"{3}\", extrusion {4} of {5}, loop {6} of {7}.\nFailure was caused by " + "there being not enough vertices to make a {0}.", typeof(Extruder), isParapet ? ParapetName : BorderName, buildingGameObject.name, buildingGameObject.transform.parent.name, extrusionIndex + 1, totalExtrusions, i + 1, loops.Count); } } } /// /// Adds an extruded outline around the edge of an area. /// /// /// This method is included for backwards compatibility. It will possibly outline internal edges /// in the area. For displaying a stroked outline, it's likely better to use the /// method. /// /// /// The Maps SDK for Unity created containing this area. /// /// /// The to apply to the outline once it has been created. /// /// /// The area to be outlined. /// /// /// The width (in Unity world coordinates) of the extruded outline.. /// /// /// Newly created s containing created outline geometry. /// /// One will be returned for each part of the given area. /// public static GameObject[] AddAreaOutline( GameObject areaObject, Material extrusionMaterial, Area area, float outlineWidth) { return ExtrudeEdgeSequences( areaObject, extrusionMaterial, area.GenerateBoundaryEdges(), outlineWidth); } /// /// Adds an extruded outline around the edge of an area. This is useful for displaying a stroke /// on an area. /// /// /// The Maps SDK for Unity created containing this area. /// /// /// The to apply to the outline once it has been created. /// /// /// The area to be extruded. /// /// /// The width (in Unity world coordinates) of the extruded outline.. /// /// /// Newly created s containing created outline geometry. /// /// One will be returned for each part of the given area. /// public static GameObject[] AddAreaExternalOutline( GameObject areaObject, Material extrusionMaterial, Area area, float outlineWidth) { return ExtrudeEdgeSequences( areaObject, extrusionMaterial, area.GenerateExternalBoundaryEdges(), outlineWidth); } /// /// Extrudes a list of edge sequences outwards. /// /// /// The Maps SDK for Unity created to which the extruded outline should /// be added. /// /// /// The to apply to the outline once it has been created. /// /// /// The edge sequences to be extruded. These should come from /// or /// /// /// The width (in Unity world coordinates) of the extruded outline.. /// /// /// Newly created s containing created outline geometry. /// /// One will be returned for each edge sequence. /// private static GameObject[] ExtrudeEdgeSequences( GameObject areaObject, Material extrusionMaterial, List edgeSequences, float outlineWidth) { List loops = PadEdgeSequences(edgeSequences); List result = new List(); Vector2[] crossSection = OutlineCrossSectionWithWidth(outlineWidth); for (int i = 0; i < loops.Count; i++) { // Try to make extrusion. GameObject extrusionGameObject; if (CanMakeLoft( loops[i].Vertices.ToArray(), extrusionMaterial, crossSection, DefaultWidth, OutlineName, out extrusionGameObject)) { // Parent the extrusion to the area object. extrusionGameObject.transform.parent = areaObject.transform; // Add to list of extrusions that will be returned for this area. result.Add(extrusionGameObject); } else { // If extrusion failed for any reason, print an error to this effect. Debug.LogErrorFormat( "{0} class was not able to create an outline for area \"{1}\", " + "loop {2} of {3}.\nFailure was caused by there being not enough vertices to " + "make an outline.", typeof(Extruder), areaObject.name, i + 1, loops.Count); } } return result.ToArray(); } /// /// Returns a canonical representation of the supplied s to /// facilitate easy creation of, e.g., parapets for both open and closed edge sequences. /// /// /// /// The padded edge sequences returned by this method are designed so that a continuous sequence /// exists from Vertices[1], to Vertices[Vertices.Count - 2] (of the returned EdgeSequence) with /// adjacent vertices providing proper edge tangent directions. /// /// For closed sequences, this simply duplicates the vertices either side of the starting/ending /// vertex. For open sequences, new vertices are added by parallel extension of the first and /// last edge. This means that open sequences will have flat ends, while closed sequences can /// generate geometry with properly mitred first and last edge vertices. /// /// /// The edge sequences to canonicalize. /// Padded copies of supplied edge sequences. private static List PadEdgeSequences(List edgeSequences) { List result = new List(edgeSequences.Count); foreach (Area.EdgeSequence sequence in edgeSequences) { // Filter out any pathological sequences. if (sequence.Vertices.Count < 2) { continue; } List vertices = new List(); vertices.AddRange(sequence.Vertices); int vertexCount = vertices.Count; Vector2 start; Vector2 end; if (vertexCount > 2 && vertices[0] == vertices[vertexCount - 1]) { start = vertices[vertexCount - 2]; end = vertices[1]; } else { start = vertices[0] - (vertices[1] - vertices[0]).normalized; end = vertices[vertexCount - 1] + (vertices[vertexCount - 1] - vertices[vertexCount - 2]).normalized; } vertices.Insert(0, start); vertices.Add(end); result.Add(new Area.EdgeSequence(vertices)); } return result; } /// /// Create extrusion geometry using the supplied footprintVertices. /// /// The 2D corners of the building footprint. /// to apply to created /// extrusion. The 2D crossSection of the shape to loft along /// the path. Thickness of loft. The name that should be given to the created game object. Created extrusion , at the origin with no /// parent, or null if lofting failed for any reason. /// /// Whether or not loft could be created. private static bool CanMakeLoft( Vector2[] footprintVertices, Material extrusionMaterial, Vector2[] crossSection, float thickness, string loftName, out GameObject createdLoft) { Vector3[] vertices; int[] indices; Vector2[] uvs; // Attempt to make loft from given data. if (CanLoft(footprintVertices, crossSection, thickness, out vertices, out indices, out uvs)) { GameObject extrusion = new GameObject(loftName); MeshFilter meshFilter = extrusion.AddComponent(); MeshRenderer meshRenderer = extrusion.AddComponent(); // Add a mesh cleaner to force GC on the instantiated mesh when the GameObject is destroyed. // Note that a bulk deletion of meshes results in a slight stuttering of the dynamic // loading/unloading of new map regions. // A proper solution would be to use an object pool and recycle mesh objects as needed. extrusion.AddComponent(); meshRenderer.material = extrusionMaterial; Mesh mesh = new Mesh { vertices = vertices, triangles = indices, uv = uvs }; mesh.RecalculateNormals(); meshFilter.mesh = mesh; createdLoft = extrusion; return true; } // If have reached this point then lofting failed. createdLoft = null; return false; } /// /// Creates a 3d "loft" of a shape along a path by running a given crossSection along the path. /// /// /// Lofting refers to running a shape along a path, creating a 3d volume based on where the /// shape has travelled. For example, lofting a circle straight upwards would give a cylinder, /// while lofting a circle around another, larger circle would give a donut. In both cases the /// volume is formed by the journey of the lofted-circle along its given path. Returns 3D /// mesh data (vertices, triangles, indices) returned in the supplied output parameter arrays. /// /// /// A padded version of the path along which to loft the given cross-section. This is the /// path along which to extrude the supplied cross-section with ghost vertices added at the /// beginning and end. These ghost vertices are only used to determine the direction of the path /// at the start and end vertices. For a closed path, these should be copies of the second to /// last and second vertices. For an open path the prepended ghost vertex should be the /// reflection of the second vertex through the first path vertex (paddedPath[0] = 2 * path[0] - /// path[1]), and the appended ghost vertex should be the reflection of the second to last path /// vertex around the last path vertex (paddedPath[last + 2] = 2 * path[last] - path[last - 1]). /// See for how this is done. This pre-padding model provides /// simple, unified handling of both open and closed paths, allowing lofting to work on /// incomplete building chunks at the edge of the loaded map region. /// This path takes the form of an array of vertices, for example, defining the /// top-down shape of a building, such that traversing these vertices allows traversing /// counter-clockwise around the base of the building. These vertices are in Vector2 format, /// where x and y represent x and z coordinates (i.e. 2d coordinates in a ground-plane at y = /// 0). /// /// /// /// The 2D cross-section of the shape to loft along the given path. Much like the path, this /// cross-section takes the form of an array of vertices, such that traversing these vertices /// allows traversing counter-clockwise around the flat shape to loft. These vertices are in /// Vector2 format, where x and y represent coordinates in a flat plane comprising just the /// shape to loft. /// /// Thickness of loft. /// Outputted mesh vertices. /// Outputted mesh triangle indices. /// Outputted mesh UVs. /// Whether or not lofting succeeded.. private static bool CanLoft( Vector2[] paddedPath, Vector2[] crossSection, float thickness, out Vector3[] vertices, out int[] triangleIndices, out Vector2[] uvs) { // Make sure there are enough vertices to create a loft. if (paddedPath.Length < 2 || crossSection.Length < 2) { vertices = null; triangleIndices = null; uvs = null; return false; } // Determine the total number of vertices and triangles needed to create the lofted volume. // Note that the total number of triangle indices is based on the fact that each combination // of path-segment and cross-section segment generates a quad of two triangles, requiring 6 // triangle indices each to represent. int segments = paddedPath.Length - 3; int totalTriangleIndices = (crossSection.Length - 1) * segments * 6; int trianglesPerSegment = crossSection.Length * 2 - 2; int verticesPerJunction = crossSection.Length * 2 - 2; int totalVertices = verticesPerJunction * (segments + 1); // Create arrays to hold vertices, uvs and triangle-indices that will be used to create the // lofted volume. vertices = new Vector3[totalVertices]; uvs = new Vector2[totalVertices]; triangleIndices = new int[totalTriangleIndices]; // Perform actual lofting. int vertexIndex = 0; int triIndex = 0; int startCorner = 1; // The path has been padded by this point with ghost vertices at the beginning and end to // simplify the calculation of previous and next directions (see PadEdgeSequences), so we // ignore the first and last vertices in the following loop, only considering vertices from // the original path. for (int cornerInPath = startCorner; cornerInPath < paddedPath.Length - 1; cornerInPath++) { // Note that we refer to these vertices as corners, because even though they are three // vertices in the given path, it's better to think of what they actually represent: three // adjacent corners of a building's base. Vector3 currentCorner = new Vector3(paddedPath[cornerInPath].x, 0f, paddedPath[cornerInPath].y); Vector3 nextCorner = new Vector3(paddedPath[cornerInPath + 1].x, 0f, paddedPath[cornerInPath + 1].y); Vector3 previousCorner = new Vector3(paddedPath[cornerInPath - 1].x, 0f, paddedPath[cornerInPath - 1].y); // Get the directions from the current corner to the next and previous corners. Vector3 directionToPrevious = (previousCorner - currentCorner).normalized; Vector3 directionToNext = (nextCorner - currentCorner).normalized; // The path we are lofting is assumed to be in counterclockwise winding order (assuming +x // right, +y up). We can calculate whether the path turns left or right placing the previous // and next direction vectors on the ground plane and calculating the cross product of next // direction with previous direction. Unity uses a left handed coordinate system, so we know // that if this vector points down (y < 0), the vectors represent a left turn in the path. Vector3 turnCross = Vector3.Cross(directionToNext, directionToPrevious); bool isLeftTurn = turnCross.y < 0; // Add the previous and next edges to get their bisector -- a line that cuts this corner in // half. For very nearly parallel edges, the next and previous directions will face in // almost exactly opposite directions so the sum will be degenerately small. To avoid // numerical issues, we simply take the right hand perpendicular of the nextEdge. Vector3 bisector = directionToNext + directionToPrevious; // Consider lines to be colinear if the angle is less than approximately 1 degree. if (bisector.magnitude > 0.015f) { bisector.Normalize(); } else { // Cross product with down gives right perpendicular in Unity's left handed coordinates. bisector = Vector3.Cross(directionToNext, Vector3.down); isLeftTurn = false; } // We wish to use the right bisector as the x-axis for the coordinates in the cross section // of the loft. If the path turns left at the current point, the bisector of the inner angle // will also point left, so we need to reverse it. // Note that for lofting around buildings, the path of the walls is counterclockwise, when // viewed from above, so the rightBisector will point away from the building. Vector3 rightBisector; if (isLeftTurn) { rightBisector = -bisector; } else { rightBisector = bisector; } // Given the previous edge, imagine a new, extruded edge, parallel to the previous edge but // extruded outwards by the desired extrusion width. If we make a similar extruded edge // pushed out from the next edge, and find where these two edges intersect, we get a new, // extruded corner - the corner of an extrusion that is of uniform length all the way // around the shape. // // Previous Edge, Extruded // Extruded Corner ●──────────○────────────────────────── // ╱ ╎_| // ╱ ╎ // ╱ ╎ // Next Edge, ╱ ╎ Extrusion Width // Extruded ╱ ╎ // ╱ ╎ // ╱ ╎ // ╱ | Previous Edge // ╱ Current Corner ●─────────────────────── // ╱ ╱ // ╱ ╱ // ╱ ╱ Next Edge // ╱ ╱ // // We could calculate these lines and find their intersection. But vectors give us a // shortcut. // // To do this, we get the right hand bisector of this corner (an outward pointing line that // cuts the corner in half, such that the angle between this bisector and the previous edge // is the same as the angle between this bisector and the next edge). If we can determine // the length of this bisector, we will know exactly how far to travel along it from the // current corner to the new, extruded corner. // // Previous Edge, Extruded // Extruded Corner ●────────○────────────────────────── // ╱ ╲ | // ╱ ╲ │ // ╱ ╲ │ // ╱ ╲ │ E // ╱ B ╲ │ // ╱ ╲ │ // ╱ ╲ │_ // ╱ ╲│ │ // ╱ Current Corner ●───────────────────────> // ╱ ╱ Previous direction vector // ╱ ╱ // ╱ ╱ // ╱ ╱ // // In this diagram 'E' is the 'extrusion vector', the vector between the current corner and // the previous, extruded edge (whose length |E| is the desired extrusion width). We can get // this extrusion vector using the cross product of the previous direction vector (pointing // back along the previous edge) with the up vector to find the left hand perpendicular // vector of the previous direction vector as shown above. (Note that Unity uses a left // handed coordinate system, giving us the left hand perpendicular from the cross product) Vector3 normalizedExtrusionVector = Vector3.Cross(directionToPrevious, Vector3.up).normalized; // In the diagram above, if we project the vector B onto a unit vector in the direction of // E (unitE) we get the vector E. This implies that the length of this projected vector is // the same as the length of E, which is just the extrusion distance. This gives us: // B . unitE = |E| // which, given that B is just a unit vector in the direction of B times |B|, gives: // (|B| unitB) . unitE = |E| // giving: // |B| (unitB . unitE) = |E| // leading to: // |B| = |E| / (unitB . unitE) // // We know |E| is just the given thickness, and have calculated unitE as // normalizeExtrusionVector and unitB as rightBisector, which allows us to calculate the // length of B as distanceToExtrude as follows. float distanceToExtrude = thickness / Vector3.Dot(normalizedExtrusionVector, rightBisector); // Make sure this extrusion distance is not unrealistically long (can occur for extremely // sharp angles) by limiting it to twice the desired Extrusion Width. if (distanceToExtrude > 2f * thickness) { distanceToExtrude = 2f * thickness; } // Create a copy of the cross-section shape using a coordinate system where the x-axis is // the rightBisector and the y-axis is the up vector. This aligns the shape to the plane // bisecting the joint angle in the path. When all these cross-sections are joined, they // will form a loft of the given cross-section around the given shape. for (int pointInCrossSection = 0; pointInCrossSection < crossSection.Length; pointInCrossSection++) { // Align the values of this cross-section point to this corner - i.e. convert from a 2D, // x, y shape into a 3D x, y, z shape aligned to the desired extrusion direction. Also // multiply the x values by the extrusion distance, to make sure that the loft is // stretched to the desired extrusion distance away from all sides of the shape. Vector3 pv = crossSection[pointInCrossSection].x * rightBisector * distanceToExtrude; Vector3 uv = crossSection[pointInCrossSection].y * Vector3.up; Vector3 vertex = currentCorner + pv + uv; vertices[vertexIndex] = vertex; uvs[vertexIndex] = new Vector3(vertex.x + vertex.y, vertex.z + vertex.y) / UvScale; // Generate triangle-indices needed to connect this edge of this cross-section to the // same edge of the next cross-section. if (pointInCrossSection > 0 && cornerInPath > startCorner) { triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex - 1); triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex - trianglesPerSegment - 1); triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex); triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex); triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex - trianglesPerSegment - 1); triangleIndices[triIndex++] = SafeMod(totalVertices, vertexIndex - trianglesPerSegment); } // Copy vertices so can have un-smoothed normals. In order to have normals be unique to // each face, each corner must have multiple vertices, one for each face. if (pointInCrossSection > 0 && pointInCrossSection < crossSection.Length - 1) { vertices[vertexIndex + 1] = vertices[vertexIndex]; uvs[vertexIndex + 1] = uvs[vertexIndex]; vertexIndex++; } // Move to the next vertex to store in the generated loft. vertexIndex++; } } // If have reached this point then have successfully created loft. return true; } /// A version of mod that works for negative values. /// /// This function ensures that returned modulated value will always be positive for values /// greater than the negative of the modulus argument. /// /// The modulus argument. /// The value to modulate. /// A range safe version of value % mod. private static int SafeMod(int mod, int val) { return (val + mod) % mod; } /// /// Convert a given array of floats into an array of s. /// /// /// Each pair of arguments becomes one Vector2, with an exception triggered if the number of /// floats given is not even. /// private static Vector2[] MakeVector2Array(params float[] floats) { // Confirm an even number of floats have been given. if (floats.Length % 2 != 0) { throw new ArgumentException("Arguments must be provided in pairs"); } // Return each pair of floats as one element of an array of Vector2's. Vector2[] vectors = new Vector2[floats.Length / 2]; for (int i = 0; i < floats.Length; i += 2) { vectors[i / 2] = new Vector2(floats[i], floats[i + 1]); } return vectors; } } }