Example #1
0
        public List <ObjectPlacementData> Calculate(Quaternion rotationToApplyForStrokeAlignment)
        {
            if (!ValidateCalculationRequirements())
            {
                return(new List <ObjectPlacementData>());
            }

            _objectNodeNetwork.Clear();
            _rotationToApplyForStrokeAlignment = rotationToApplyForStrokeAlignment;
            CreateSurfaceColliderProjector();
            _elementToNewPrefabRotation.Clear();
            _allowObjectIntersection = ObjectPlacementSettings.Get().ObjectIntersectionSettings.AllowIntersectionForDecorPaintBrushModeDrag;

            int currentObjectIndex           = 0;
            var objectPlacementDataInstances = new List <ObjectPlacementData>(_workingBrush.MaxNumberOfObjects);

            while (currentObjectIndex < _workingBrush.MaxNumberOfObjects)
            {
                DecorPaintObjectPlacementBrushElement brushElement = _brushElementSpawnChanceTable.PickEntity(UnityEngine.Random.Range(0.0f, 1.0f));
                int brushElementIndex = _allValidBrushElements.FindIndex(item => item == brushElement);
                ++currentObjectIndex;

                // No object nodes were created yet?
                if (_objectNodeNetwork.NumberOfNodes == 0)
                {
                    // Create the first node at a random position inside the brush circle
                    Vector3             randomPositionInsideCircle = _workingBrushCircle.GetRandomPointInside();
                    ObjectSurfaceData   objectSurfaceData          = CalculateObjectSurfaceData(randomPositionInsideCircle);
                    MatrixObjectBoxPair matrixObjectBoxPair        = CalculateMatrixObjectBoxPair(brushElementIndex, objectSurfaceData);

                    // We need to know if the normal of the surface on which the object resides lies within the desired slope range
                    bool passesSlopeTest = DoesObjectSurfacePassSlopeTest(objectSurfaceData, brushElement);

                    // Note: Even if the slope test is not passed, we will still create an object node. The reason for this is that
                    //       we want to have some kind of continuity in the algorithm. Imagine that the brush circle is large and is
                    //       divided by a large terrain mountain which sits in the middle. If the object generation starts on one side
                    //       of the mountain, the algorithm might never get a chance to go over the other side if the slope condition
                    //       is not satisifed. We want to spread objects as much as possible so even though this object will not be
                    //       placed in the scene, we will still add it to the node network.
                    _objectNodeNetwork.AddNodeToEnd(matrixObjectBoxPair.ObjectBox, objectSurfaceData);
                    if (passesSlopeTest && DoesBoxPassObjectIntersectionTest(matrixObjectBoxPair.ObjectBox, brushElement.Prefab.UnityPrefab, matrixObjectBoxPair.ObjectMatrix))
                    {
                        objectPlacementDataInstances.Add(new ObjectPlacementData(matrixObjectBoxPair.ObjectMatrix, brushElement.Prefab, brushElement.MustEmbedInSurface));
                    }
                }
                else
                {
                    // Are there any node segments available?
                    if (_objectNodeNetwork.NumberOfSegments != 0)
                    {
                        // The first step is to generate a random node index and store references to that node and its immediate neighbour
                        _objectNodeNetwork.RemoveAllNodesWhichGenerateConcavities(_workingBrushCircle.Plane);
                        int        randomNodeIndex = _objectNodeNetwork.GetRandomNodeIndex();
                        ObjectNode firstNode       = _objectNodeNetwork.GetNodeByIndex(randomNodeIndex);
                        ObjectNode secondNode      = _objectNodeNetwork.GetNodeByIndex(randomNodeIndex + 1);

                        // Calculate the plane of the segment which unites the 2 nodes. We will also store the
                        // actual segment and the middle point on the segment. We will use this middle point to
                        // generate the initial object position.
                        Segment3D   nodeSegment     = ObjectNodeNetwork.CalculateSegmentBetweenNodes(firstNode, secondNode);
                        Vector3     segmentMidPoint = nodeSegment.GetPoint(0.5f);
                        Plane       segmentPlane    = ObjectNodeNetwork.CalculateSegmentPlaneNormal(firstNode, secondNode);
                        OrientedBox firstNodeBox    = firstNode.ObjectBox;
                        OrientedBox secondNodeBox   = secondNode.ObjectBox;

                        // Calculate the furthest point in front of the plane using the corner points of the
                        // 2 nodes. The idea is to move the new object as much as possible from the bulk of
                        // objects that have already been generated.
                        Vector3        furthestPointFromPlane;
                        List <Vector3> nodeCornerPoints = firstNodeBox.GetCornerPoints();
                        nodeCornerPoints.AddRange(secondNodeBox.GetCornerPoints());
                        if (!segmentPlane.GetFurthestPointInFront(nodeCornerPoints, out furthestPointFromPlane))
                        {
                            continue;
                        }

                        // Use the calculated furthest point from plane and the the existing plane normal to calculate the
                        // pivot plane. The new object will reside at some distance away from this plane.
                        Plane pivotPlane = new Plane(segmentPlane.normal, furthestPointFromPlane);

                        // Calculate the new object transform data. We will use the segment's mid point to generate the
                        // initial object position.
                        ObjectSurfaceData   objectSurfaceData   = CalculateObjectSurfaceData(segmentMidPoint);
                        MatrixObjectBoxPair matrixObjectBoxPair = CalculateMatrixObjectBoxPair(brushElementIndex, objectSurfaceData);
                        OrientedBox         objectBox           = matrixObjectBoxPair.ObjectBox;

                        // Identify the objects's furthest point behind the plane
                        Vector3        objectBoxPivotPoint;
                        List <Vector3> objectBoxCornerPoints = objectBox.GetCornerPoints();
                        if (!pivotPlane.GetFurthestPointBehind(objectBoxCornerPoints, out objectBoxPivotPoint))
                        {
                            continue;
                        }

                        // Use the furthest point to move the object in front of the plane and take the distance between objects into account
                        Vector3 fromPivotPointToCenter = objectBox.Center - objectBoxPivotPoint;
                        Vector3 projectedPivotPoint    = pivotPlane.ProjectPoint(objectBoxPivotPoint);
                        objectBox.Center = projectedPivotPoint + fromPivotPointToCenter + pivotPlane.normal * _workingBrush.DistanceBetweenObjects;

                        // Generate the object surface data
                        objectSurfaceData = CalculateObjectSurfaceData(objectBox.Center);
                        bool passesSlopeTest = DoesObjectSurfacePassSlopeTest(objectSurfaceData, brushElement);

                        // Now we need to adjust the orientation and center of the box. If the calculated center
                        // lies outside the brush circle, we will ignore this node.
                        AdjustObjectBoxRotationOnSurface(objectBox, objectSurfaceData, brushElement);
                        AdjustObjectBoxCenterToSitOnSurface(objectBox, objectSurfaceData, brushElement);
                        if (!_workingBrushCircle.ContainsPoint(_workingBrushCircle.Plane.ProjectPoint(objectBox.Center)))
                        {
                            continue;
                        }

                        // Recalculate the object matrix using the new box data
                        TransformMatrix objectMatrix = matrixObjectBoxPair.ObjectMatrix;
                        objectMatrix.Rotation    = objectBox.Rotation;
                        objectMatrix.Translation = ObjectPositionCalculator.CalculateObjectHierarchyPosition(brushElement.Prefab, objectBox.Center, objectMatrix.Scale, objectBox.Rotation);

                        // We have been modifying the matrix and box data independently so we will ensure that the box uses the latest data
                        OrientedBox finalBox = new OrientedBox(objectBox);
                        finalBox.SetTransformMatrix(objectMatrix);

                        // If the slope test passed, we will calculate an object placement data instance. Otherwise, we will just insert a new node.
                        if (passesSlopeTest && DoesBoxPassObjectIntersectionTest(finalBox, brushElement.Prefab.UnityPrefab, objectMatrix))
                        {
                            objectPlacementDataInstances.Add(new ObjectPlacementData(objectMatrix, brushElement.Prefab, brushElement.MustEmbedInSurface));
                        }
                        _objectNodeNetwork.InsertAfterNode(objectBox, objectSurfaceData, randomNodeIndex);
                    }
                    else
                    {
                        // When there are no segments available it means we have only one node. We will use this node to generate
                        // a new one at some distance away from it. First we will store some data that we will need during the entire
                        // procedure.
                        ObjectNode  pivotNode = _objectNodeNetwork.GetFirstNode();
                        Vector3     pivotNodeSurfaceTangent = pivotNode.ObjectSurfaceData.GetSurfaceTangentVector();
                        OrientedBox pivotNodeObjectBox      = pivotNode.ObjectBox;

                        // We will place the new node at some distance away from the first node's face which points
                        // along the calculated tangent vector. We will call this the pivot face.
                        BoxFace pivotBoxFace   = pivotNodeObjectBox.GetBoxFaceMostAlignedWithNormal(pivotNodeSurfaceTangent);
                        Plane   pivotFacePlane = pivotNodeObjectBox.GetBoxFacePlane(pivotBoxFace);

                        // Generate the data for the new node in the same position as the first node.
                        // Note: Although the same position is used, the rotation and scale will differ and they will
                        //       be established by 'CalculateMatrixObjectBoxPair'.
                        MatrixObjectBoxPair matrixObjectBoxPair = CalculateMatrixObjectBoxPair(brushElementIndex, pivotNode.ObjectSurfaceData);
                        OrientedBox         objectBox           = matrixObjectBoxPair.ObjectBox;

                        // At this point we have to start moving the generated object box to its new positino along the
                        // tangent vector. We will do this by calculating the furthest box point which lies behind the
                        // pivot plane and then move the box so that this point resides on that plane. We will call this
                        // the pivot point.
                        // Note: We will perform a safety check to see if this point could not be calculated and use the
                        //       closest point in front if necessary. However, this check should not be necessary. Because
                        //       we are placing te object box in the center of the previous box, we can be usre that there
                        //       will always be a point which lies behind the pivot plane.
                        Vector3        objectBoxPivotPoint;
                        List <Vector3> objectBoxCornerPoints = objectBox.GetCornerPoints();
                        if (!pivotFacePlane.GetFurthestPointBehind(objectBoxCornerPoints, out objectBoxPivotPoint) &&
                            !pivotFacePlane.GetClosestPointInFront(objectBoxCornerPoints, out objectBoxPivotPoint))
                        {
                            continue;
                        }

                        // Project the pivot point onto the pivot plane. We will also store a vector which goes from the
                        // original pivot point to the box center. This will allow us to maintain the relationship between
                        // the projected pivot point and the box center so that the center can be adjusted accordingly.
                        Vector3 fromPivotPointToCenter = objectBox.Center - objectBoxPivotPoint;
                        Vector3 projectedPivotPoint    = pivotFacePlane.ProjectPoint(objectBoxPivotPoint);

                        // Adjust the center using the projected pivot point and also take the distance between objects into account
                        objectBox.Center = projectedPivotPoint + fromPivotPointToCenter + pivotNodeSurfaceTangent * _workingBrush.DistanceBetweenObjects;

                        // Generate the object surface data at the current box position.
                        // Note: This is the step which can actually cause objects to intersect a little bit. The surface data is
                        //       calculated by projecting along the brush circle plane normal. If we are placing objects on a terrain
                        //       and the center of the circle lies somewhere at the base of the terrain where the normal points straight
                        //       up, but the center of the box resides somewhere on a clif, the new center might move the box closer
                        //       or even further away from the pivot node. This however, should not be a problem especially if the distance
                        //       between objects is not 0.
                        ObjectSurfaceData objectSurfaceData = CalculateObjectSurfaceData(objectBox.Center);
                        bool passesSlopeTest = DoesObjectSurfacePassSlopeTest(objectSurfaceData, brushElement);

                        // Now we need to adjust the orientation and center of the box. If the calculated center
                        // lies outside the brush circle, we will ignore this node.
                        AdjustObjectBoxRotationOnSurface(objectBox, objectSurfaceData, brushElement);
                        AdjustObjectBoxCenterToSitOnSurface(objectBox, objectSurfaceData, brushElement);
                        if (!_workingBrushCircle.ContainsPoint(_workingBrushCircle.Plane.ProjectPoint(objectBox.Center)))
                        {
                            continue;
                        }

                        // Recalculate the object matrix using the new box data
                        TransformMatrix objectMatrix = matrixObjectBoxPair.ObjectMatrix;
                        objectMatrix.Rotation    = objectBox.Rotation;
                        objectMatrix.Translation = ObjectPositionCalculator.CalculateObjectHierarchyPosition(brushElement.Prefab, objectBox.Center, objectMatrix.Scale, objectBox.Rotation);

                        // We have been modifying the matrix and box data independently so we will ensure that the box uses the latest data
                        OrientedBox finalBox = new OrientedBox(objectBox);
                        finalBox.SetTransformMatrix(objectMatrix);

                        // If the slope test passed, we will calculate an object placement data instance. Otherwise, we will just insert a new node.
                        if (passesSlopeTest && DoesBoxPassObjectIntersectionTest(finalBox, brushElement.Prefab.UnityPrefab, objectMatrix))
                        {
                            objectPlacementDataInstances.Add(new ObjectPlacementData(objectMatrix, brushElement.Prefab, brushElement.MustEmbedInSurface));
                        }
                        _objectNodeNetwork.InsertAfterNode(objectBox, objectSurfaceData, 0);
                    }
                }
            }

            // Adjust the prefab rotations for the next time the function is called
            if (_elementToNewPrefabRotation.Count != 0)
            {
                foreach (var prefabRotationPair in _elementToNewPrefabRotation)
                {
                    DecorPaintObjectPlacementBrushElement brushElement = prefabRotationPair.Key;
                    if (_elementToCurrentPrefabRotation.ContainsKey(brushElement))
                    {
                        _elementToCurrentPrefabRotation[brushElement] = prefabRotationPair.Value;
                    }
                }
            }

            return(objectPlacementDataInstances);
        }