/// <summary> /// Insert a new node between every node segment of the area /// </summary> public static void Subdivide(VegetationMaskArea mask) { List <Node> originalNodes = new List <Node>(); originalNodes.AddRange(mask.Nodes); for (var i = 0; i < originalNodes.Count; i++) { Node curr = originalNodes[i]; Node next = mask.GetNextNode(curr); Vector3[] segment = new Vector3[] { curr.Position, next.Position }; Vector3 meanVector = PolygonUtils.GetMeanVector(segment); int index = mask.GetNodeIndex(curr); Node newNode = new Node() { Position = meanVector }; mask.Nodes.Insert(index + 1, newNode); } UpdateMask(mask); }
/// <summary> /// Get a position and angle along the biome mask's edge. /// </summary> /// <param name="position"></param> /// <param name="angle"></param> private void GetRandomBiomeEdgePosition(out Vector3 position, out float angle) { BiomeMaskArea mask = editor.extension.lineSettings.biomeMaskArea; // parameter consistency check if (mask == null) { Debug.LogError("No mask defined"); position = Vector3.zero; angle = 0; return; } List <Vector3> positions = BiomeMaskUtils.GetPositions(mask); // sort clockwise, so that the pick algorithm works // if this were counterclockwise, then the angle would make the lines face inwards PolygonUtils.SortClockWise(positions); // get from node index int nodeIndexFrom = Random.Range(0, positions.Count); // note: int is exclusive last // get to node index, consider overlap int nodeIndexTo = nodeIndexFrom + 1; if (nodeIndexTo >= mask.Nodes.Count) { nodeIndexTo = 0; } // get nodes Vector3 positionFrom = mask.transform.position + positions[nodeIndexFrom]; Vector3 positionTo = mask.transform.position + positions[nodeIndexTo]; // having the lines flip inwards into the biome is just a matter of changing the access order of the nodes // leaving this here, maybe we find a use case later bool flipAngle = editor.extension.lineSettings.attachedAngleFlip; if (flipAngle) { Vector3 tmp = positionFrom; positionFrom = positionTo; positionTo = tmp; } float distance = (positionTo - positionFrom).magnitude; Vector3 direction = (positionTo - positionFrom).normalized; // the position along the edge. 0=from, 0.5=center, 1=to float relativePosition = Random.Range(0f, 1f); // calculate the position position = positionFrom + direction * distance * relativePosition; // calculate the angle 90 degrees to the from-to points and convert to degrees angle = Mathf.Atan2(positionTo.z - positionFrom.z, positionTo.x - positionFrom.x) * Mathf.Rad2Deg; }
private List <Vector3> CreateRoad(Bounds bounds, int pointCount, Vector2[] clipPolygon) { // get offset, eg when biome mask is used for clipping float xmin = bounds.center.x - bounds.extents.x; float zmin = bounds.center.z - bounds.extents.z; // get 0/0-based bounds for graph processing Bounds graphBounds = new Bounds(bounds.size / 2, bounds.size); DelaunayVoronoiGraph graph = new DelaunayVoronoiGraph(); // algorithm is 0-based graph.GeneratePoints(0, bounds.size.x, bounds.size.z); // initialize with the dimensions which are used in the algorithm // add random points for (int i = 0; i < pointCount; i++) { // get random point starting at [0/0] Vector2 vector = PolygonUtils.GetRandomPointXZ(graphBounds); graph.AddPoint(vector.x, vector.y); } // create the graph using the points graph.CreateGraph(); int cellIndex = Random.Range(0, pointCount - 1); // normalize clip polygon, shift to [0/0] Vector2[] offsetClipPolygon = clipPolygon.Select(item => new Vector2(item.x - xmin, item.y - zmin)).ToArray(); // get cell, clip it at the clip polygon Cell cell = graph.GetVoronoiCell(cellIndex, offsetClipPolygon); if (cell == null) { return(null); } // consider biome mask shift: shift points away from 0/0 if necessary Vector3 position = new Vector3(cell.Centroid.x + xmin, 0, cell.Centroid.y + zmin); // TODO: recalculate, might have changed because of clipping // consider biome mask shift: shift points away from 0/0 if necessary List <Vector3> nodes = cell.Vertices.Select(item => new Vector3(item.x + xmin, 0, item.y + zmin)).ToList(); // apply random shape if requested if (editor.extension.shapeSettings.randomShape) { nodes = ShapeCreator.CreateRandomShape(nodes, // editor.extension.shapeSettings.RandomConvexity, // editor.extension.shapeSettings.keepOriginalPoints, // editor.extension.shapeSettings.RandomPointsCount, // editor.extension.shapeSettings.randomAngle, // editor.extension.shapeSettings.douglasPeuckerReductionTolerance); } return(nodes); }
public void OnInspectorGUI() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Terrain Processing", GUIStyles.GroupTitleStyle); EditorGUILayout.PropertyField(partitionAlgorithm, new GUIContent("Algorithm", "The algorithm to use for terrain partitioning.")); EditorGUILayout.PropertyField(terrainProcessing, new GUIContent("Bounds", "Process all terrains as a single combined terrain or all terrains individually.")); BoundsProcessing selectedTerrainProcessing = (BoundsProcessing)System.Enum.GetValues(typeof(BoundsProcessing)).GetValue(terrainProcessing.enumValueIndex); if (selectedTerrainProcessing == BoundsProcessing.Biome) { EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(boundsBiomeMaskArea, new GUIContent("Biome Mask", "The Biome used for clipping.")); // check if the changed biome mask is convex if (EditorGUI.EndChangeCheck() || editor.performInitialConsistencyCheck) { if (boundsBiomeMaskArea.objectReferenceValue != null) { boundsBiomeMaskAreaValid.boolValue = false; BiomeMaskArea biomeMaskArea = (BiomeMaskArea)boundsBiomeMaskArea.objectReferenceValue; Vector2[] clipPolygon = editor.GetBiomeClipPolygon(biomeMaskArea); if (clipPolygon != null) { // consistency check: clip polygon must be convex for sutherland hodgman bool isConvex = PolygonUtils.PolygonIsConvex(clipPolygon); if (isConvex) { boundsBiomeMaskAreaValid.boolValue = true; } else { Debug.LogError("Invalid clipping mask: " + biomeMaskArea.name + " (" + biomeMaskArea.MaskName + ")"); } } } } // show error in case the mask doesn't exist if (boundsBiomeMaskArea.objectReferenceValue == null) { EditorGUILayout.HelpBox("The Biome Mask must be defined!", MessageType.Error); } // show error in case the mask isn't convex else if (!boundsBiomeMaskAreaValid.boolValue) { EditorGUILayout.HelpBox("The Biome Mask must be convex!", MessageType.Error); } } }
private void CreateBiomeMaskArea(string gameObjectName, string maskName, Bounds bounds) { // modify bounds, reduce its rectangular distribution float minFactor = editor.extension.rectangularPartitionSettings.boundsShiftFactorMin; float maxFactor = editor.extension.rectangularPartitionSettings.boundsShiftFactorMax; bounds = PolygonUtils.ShiftResizeBounds(bounds, minFactor, maxFactor); List <Vector3> nodes; if (editor.extension.shapeSettings.randomShape) { // old mechanism: convex hull // nodes = ShapeCreator.CreateRandomShapeUsingConvexHull(bounds, randomPointCount); // new mechanism: random shape with parameters like convexity nodes = ShapeCreator.CreateRandomShape( ShapeCreator.CreateRectangularBoundsShape(bounds), editor.extension.shapeSettings.RandomConvexity, // editor.extension.shapeSettings.keepOriginalPoints, // editor.extension.shapeSettings.RandomPointsCount, // editor.extension.shapeSettings.randomAngle, // editor.extension.shapeSettings.douglasPeuckerReductionTolerance); } // exact bounds else { nodes = ShapeCreator.CreateRectangularBoundsShape(bounds); } // bounds for clipping Vector2[] clipPolygon = editor.GetBiomeClipPolygon(bounds); // clip, convert to vector2 Vector2[] polygonXY = nodes.Select(item => new Vector2(item.x, item.z)).ToArray(); Vector2[] clippedPoints = SutherlandHodgman.GetIntersectedPolygon(polygonXY, clipPolygon); if (clippedPoints == null || clippedPoints.Length < 3) { return; } // convert back to vector3 nodes = clippedPoints.Select(item => new Vector3(item.x, 0, item.y)).ToList(); // create the biome mask using the nodes CreateBiomeMaskArea(gameObjectName, maskName, bounds, nodes); }
/// <summary> /// Create a biome mask. /// The blend distance is simply calculated using the mean vector. /// </summary> /// <param name="gameObjectName"></param> /// <param name="maskName"></param> /// <param name="position"></param> /// <param name="nodes"></param> private void CreateBiomeMaskArea(string gameObjectName, string maskName, Vector3 position, List <Vector3> nodes) { float blendDistanceMin = editor.extension.biomeSettings.biomeBlendDistanceMin; float blendDistanceMax = editor.extension.biomeSettings.biomeBlendDistanceMax; float biomeBlendDistance = UnityEngine.Random.Range(blendDistanceMin, blendDistanceMax); // mean vector is just a simple measure. please adapt if you need more accuracy Vector3 meanVector = PolygonUtils.GetMeanVector(nodes); float blendDistance = meanVector.magnitude / 2f * biomeBlendDistance; // create the mask using the provided parameters editor.CreateBiomeMaskArea(gameObjectName, maskName, position, nodes, blendDistance); }
/// <summary> /// Distributes random points within bounds and creates a convex hull around it. /// </summary> /// <param name="mask"></param> /// <param name="bounds"></param> /// <param name="count"></param> public static List <Vector3> CreateRandomShapeUsingConvexHull(Bounds bounds, int count) { List <Vector2> points = new List <Vector2>(); for (int i = 0; i < count; i++) { points.Add(PolygonUtils.GetRandomPointXZ(bounds)); } List <Vector2> convexHull = PolygonUtility.GetConvexHull(points); List <Vector3> nodes = convexHull.ConvertAll <Vector3>(item => new Vector3(item.x, 0, item.y)); return(nodes); }
public void CreateBiomeMaskArea(string gameObjectName, string maskName, Vector3 position, List <Vector3> nodes, float blendDistance) { GameObject parentGameObject = extension.transform.gameObject; // create new gameobject GameObject biomeGameObject = new GameObject(gameObjectName); // add this component biomeGameObject.AddComponent <BiomeMaskAreaExtension>(); // ensure gameobject gets reparented if this was a context click (otherwise does nothing) GameObjectUtility.SetParentAndAlign(biomeGameObject, parentGameObject); // set position biomeGameObject.transform.position = position; // that's actually not necessary, we call CenterMainHandle after the mask nodes were created // modify created biome BiomeMaskArea mask = biomeGameObject.GetComponent <BiomeMaskArea>(); mask.BiomeType = extension.biomeSettings.biomeType; // blend distance mask.BlendDistance = blendDistance; // create nodes mask.MaskName = maskName; // grow/shrink mask nodes = PolygonUtils.Resize(nodes, extension.shapeSettings.resizeFactor); // random shape inside bounds SetMaskNodes(mask, nodes); // put move handle into the center of the polygon BiomeMaskUtils.CenterMainHandle(mask); #region Lake Polygon // create a lake and re-parent the biome to it if (extension.lakeSettings.createLake) { lakeModule.CreateLake(mask, gameObjectName, nodes); } #endregion Lake Polygon // tegister the creation in the undo system Undo.RegisterCreatedObjectUndo(biomeGameObject, "Create " + biomeGameObject.name); }
private void CreateRoads(Bounds bounds) { // not too close to the bounds; the easy roads spline might move the road outside bounds.size *= 0.9f; Vector2[] clipBounds = new Vector2[] { new Vector2(bounds.center.x - bounds.extents.x, bounds.center.z - bounds.extents.z), new Vector2(bounds.center.x + bounds.extents.x, bounds.center.z - bounds.extents.z), new Vector2(bounds.center.x + bounds.extents.x, bounds.center.z + bounds.extents.z), new Vector2(bounds.center.x - bounds.extents.x, bounds.center.z + bounds.extents.z), }; int pointCount = editor.extension.voronoiSettings.pointCount; List <Vector3> maskNodes = CreateRoad(bounds, pointCount, clipBounds); List <Vector3> nodes = ShapeCreator.CreateRandomShape(maskNodes, // editor.extension.shapeSettings.RandomConvexity, // editor.extension.shapeSettings.keepOriginalPoints, // editor.extension.shapeSettings.RandomPointsCount, // editor.extension.shapeSettings.randomAngle, // editor.extension.shapeSettings.douglasPeuckerReductionTolerance); PolygonUtils.SortClockWise(nodes); if (smoothEnabled.boolValue) { List <Vector2> positionsXY = nodes.ConvertAll(item => new Vector2(item.x, item.z)); positionsXY = getCurveSmoothingChaikin(positionsXY, 0.5f, 0); nodes = positionsXY.ConvertAll(item => new Vector3(item.x, 0, item.y)); } nodes = AlignToTerrainHeight(nodes); // remove nodes that are too close to each other RemoveNodes(nodes, minDistance.floatValue); // set ER markers erRoad.AddMarkers(nodes.ToArray()); // set closed track erRoad.ClosedTrack(closedTrack.boolValue); }
/// <summary> /// Get the biome clip polygon for the given bounds considering the biome mask settings. /// </summary> /// <param name="bounds"></param> /// <returns></returns> public Vector2[] GetBiomeClipPolygon(Bounds bounds) { Vector2[] clipPolygon = PolygonUtils.CreatePolygonXZ(bounds); // optionally use a biome as clip polygon. default is bounds if (extension.boundsSettings.boundsProcessing == ProcessingSettings.BoundsProcessing.Biome) { Vector2[] biomeClipPolygon = GetBiomeClipPolygon(); if (biomeClipPolygon != null) { clipPolygon = GetBiomeClipPolygon(); // consistency check: clip polygon must be convex for sutherland hodgman bool isConvex = PolygonUtils.PolygonIsConvex(clipPolygon); if (!isConvex) { Debug.LogError("Biome mask clip polygon isn't convex"); } } } return(clipPolygon); }
/// <summary> /// Modifies a polygon and creates a random shape. /// /// Algorithm: /// /// * create a new polygon, subdivided this way: /// + get center of polygon /// + create a list of angles for iteration /// + iterate through the angles, create a line using the angle and find the intersection with a line of the polygon /// * iterate through the subdivided polygon /// + move the vertex towards the center /// + consider previous vertex in order to not move in too large steps /// </summary> public static List <Vector3> CreateRandomShape(List <Vector3> polygon, float ellipseRelaxationFactor, int angleStepCount, bool randomAngleMovement, bool keepOriginalShape, float douglasPeuckerReductionTolerance) { Vector3 meanVector = PolygonUtils.GetMeanVector(polygon); float length = meanVector.magnitude * 2; #region create angles // get the list of angles to step through List <float> angleRadList = CreateAngleList(angleStepCount, randomAngleMovement); #endregion create angles #region create new polygon using angles List <Vector3> subdividedPolygon = new List <Vector3>(); // add existing points in order to keep the general voronoi shape if (keepOriginalShape) { subdividedPolygon.AddRange(polygon); } for (int i = 0; i < angleRadList.Count; i++) { // convert angle from deg to rad float angle = angleRadList[i]; float x = meanVector.x + length * Mathf.Cos(angle); float z = meanVector.z + length * Mathf.Sin(angle); // position on the ellipse Vector3 line1PositionA = meanVector; Vector3 line1PositionB = new Vector3(x, 0, z); for (var j = 0; j < polygon.Count; j++) { int currIndex = j; int nextIndex = j + 1; if (nextIndex == polygon.Count) { nextIndex = 0; } Vector3 line2PositionA = new Vector3(polygon[currIndex].x, 0, polygon[currIndex].z); Vector3 line2PositionB = new Vector3(polygon[nextIndex].x, 0, polygon[nextIndex].z); Vector3 intersection = Vector3.zero; // try and find an intersection. if one is found, add the point to the list if (PolygonUtils.LineSegmentsIntersection(line1PositionA, line1PositionB, line2PositionA, line2PositionB, out intersection)) { subdividedPolygon.Add(intersection); break; } } } // sort again PolygonUtils.SortClockWise(subdividedPolygon); #endregion create new polygon using angles #region create new polygon using the intersections List <Vector3> newPolygon = new List <Vector3>(); float prevDistance = 0; for (int i = 0; i < subdividedPolygon.Count; i++) { Vector3 curr = subdividedPolygon[i]; // position on the ellipse Vector3 position = curr; // from center to position float distance = (position - meanVector).magnitude; Vector3 direction = (position - meanVector).normalized; // move from center towards new position. but not too much, let it depend on the previous distance { // move initially from 0 to max distance. otherwise use the previous value float min = i == 0 ? distance * ellipseRelaxationFactor : prevDistance * ellipseRelaxationFactor; float max = distance; // the radius must be smaller during the next iteration, we are navigating around an ellipse => clamp the values if (min > max) { min = max; } float moveDistance = UnityEngine.Random.Range(min, max); distance = moveDistance; } position = meanVector + distance * direction; newPolygon.Add(position); prevDistance = distance; } #endregion create new polygon using the intersections if (douglasPeuckerReductionTolerance > 0) { // convert to vector2 List <Vector2> vector2List = newPolygon.ConvertAll <Vector2>(item => new Vector2(item.x, item.z)); // use vspro's DouglasPeuckerReduction algorithm vector2List = PolygonUtility.DouglasPeuckerReduction(vector2List, douglasPeuckerReductionTolerance); // convert back to vector3 if (vector2List.Count >= 3) { newPolygon = vector2List.ConvertAll <Vector3>(item => new Vector3(item.x, 0, item.y)); } } return(newPolygon); }
/// <summary> /// Get the center of the mask polygon /// </summary> /// <param name="mask"></param> /// <returns></returns> public static Vector3 GetMaskCenter(VegetationMaskArea mask) { List <Vector3> worldPositions = mask.GetWorldSpaceNodePositions(); return(PolygonUtils.GetMeanVector(worldPositions.ToArray())); }
/// <summary> /// Initializes a new instance of the ClockwiseComparer class and sets the origin to the mean vector, depending on the positions. /// </summary> /// <param name="origin">Origin.</param> public ClockwiseComparer(List <Vector3> positions) { m_Origin = PolygonUtils.GetMeanVector(positions); }
/// <summary> /// Create a list of bounds to process. Either all terrains combined or individually /// </summary> /// <returns></returns> public List <Bounds> GetBoundsToProcess() { List <Bounds> boundsList = new List <Bounds>(); List <VegetationSystemPro> VegetationSystemList = VegetationStudioInstance.VegetationSystemList; for (int i = 0; i <= VegetationSystemList.Count - 1; i++) { VegetationSystemPro vegetationSystemPro = VegetationSystemList[i]; switch (extension.boundsSettings.boundsProcessing) { case BoundsProcessing.CombinedTerrains: // get combined bounds Bounds bounds = vegetationSystemPro.VegetationSystemBounds; // use these bounds boundsList.Add(bounds); break; case BoundsProcessing.IndividualTerrains: for (int t = 0; t < vegetationSystemPro.VegetationStudioTerrainObjectList.Count; t++) { // get individual terrain bounds GameObject terrain = vegetationSystemPro.VegetationStudioTerrainObjectList[t]; IVegetationStudioTerrain vegetationStudioTerrain = VegetationStudioTerrain.GetIVegetationStudioTerrain(terrain); bounds = vegetationStudioTerrain.TerrainBounds; // use these bounds boundsList.Add(bounds); } break; case BoundsProcessing.Biome: Bounds biomeBounds; // get biome mask area Vector2[] biomeClipPolygonXY = GetBiomeClipPolygon(); // use mask to get bounds if (biomeClipPolygonXY != null) { Vector3[] biomeClipPolygonXZ = biomeClipPolygonXY.Select(item => new Vector3(item.x, 0, item.y)).ToArray(); biomeBounds = PolygonUtils.GetBounds(biomeClipPolygonXZ); } // fall back to the vegetation system mask else { biomeBounds = vegetationSystemPro.VegetationSystemBounds; Debug.LogError("Invalid biome clip polygon. Using vegetation system bounds"); } // use these bounds boundsList.Add(biomeBounds); break; default: throw new System.ArgumentException("Unsupported TerrainProcessing " + extension.boundsSettings.boundsProcessing); } } return(boundsList); }