/// <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);
        }
Ejemplo n.º 2
0
        /// <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);
        }
Ejemplo n.º 6
0
        /// <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);
        }
Ejemplo n.º 7
0
        /// <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);
        }
Ejemplo n.º 11
0
        /// <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()));
        }
Ejemplo n.º 13
0
 /// <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);
        }