如何在运行时操纵形状的地形区域 - Unity 3D

时间:2018-02-22 19:37:15

标签: unity3d runtime terrain

我的游戏有一个绘图工具 - 一个循环线渲染器,用作标记来操纵线条形状的地形区域。一旦玩家停止绘制线条,这一切都会在运行时发生。 到目前为止,我已经设法提升了与线条渲染器点的坐标相匹配的地形椎体,但是我很难提高落在标记形状内的点。这是描述我目前所拥有的图像:

我尝试使用“多边形填充算法”(http://alienryderflex.com/polygon_fill/),但是一次一行地提高地形顶点是太有用了(即使算法缩小为仅围绕标记区域的矩形) 。此外,我的标记的轮廓点之间存在间隙,这意味着我需要在引发地形的线上添加半径,但这可能会使结果变得粗糙。

也许我应该丢弃绘图机制并使用带网格对撞机的网格作为标记?

如何将地形操作为标记的确切形状,我们对任何想法都很感激。

当前代码: 我使用this script来创建线 - 第一个和最后一个线点具有相同的坐标。 当单击GUI按钮时,当前触发了用于操纵地形操作的代码:

using System;
using System.Collections;
using UnityEngine;
public class changeTerrainHeight_lineMarker : MonoBehaviour
{
    public Terrain TerrainMain;
    public LineRenderer line;

    void OnGUI()
    {
        //Get the terrain heightmap width and height.
        int xRes = TerrainMain.terrainData.heightmapWidth;
        int yRes = TerrainMain.terrainData.heightmapHeight;

        //GetHeights - gets the heightmap points of the tarrain. Store them in array
        float[,] heights = TerrainMain.terrainData.GetHeights(0, 0, xRes, yRes);

        if (GUI.Button(new Rect(30, 30, 200, 30), "Line points"))
        {
            /* Set the positions to array "positions" */
            Vector3[] positions = new Vector3[line.positionCount];
            line.GetPositions(positions);

            /* use this height to the affected terrain verteces */
            float height = 0.05f;

            for (int i = 0; i < line.positionCount; i++)
            {
                /* Assign height data */
                heights[Mathf.RoundToInt(positions[i].z), Mathf.RoundToInt(positions[i].x)] = height;
            }

            //SetHeights to change the terrain height.
            TerrainMain.terrainData.SetHeights(0, 0, heights);
        } 
    }
}

1 个答案:

答案 0 :(得分:2)

通过Siim的个人帮助获得解决方案,并感谢文章:How can I determine whether a 2D Point is within a Polygon?

最终结果在这里可视化:

首先是代码,然后是解释:

using System;
using System.Collections;
using UnityEngine;
public class changeTerrainHeight_lineMarker : MonoBehaviour
{

    public Terrain TerrainMain;
    public LineRenderer line;

    void OnGUI()
    {
        //Get the terrain heightmap width and height.
        int xRes = TerrainMain.terrainData.heightmapWidth;
        int yRes = TerrainMain.terrainData.heightmapHeight;

        //GetHeights - gets the heightmap points of the tarrain. Store them in array
        float[,] heights = TerrainMain.terrainData.GetHeights(0, 0, xRes, yRes);

        //Trigger line area raiser
        if (GUI.Button(new Rect(30, 30, 200, 30), "Line fill"))
        {
            /* Set the positions to array "positions" */
            Vector3[] positions = new Vector3[line.positionCount];
            line.GetPositions(positions);

            float height = 0.10f; // define the height of the affected verteces of the terrain

            /* Find the reactangle the shape is in! The sides of the rectangle are based on the most-top, -right, -bottom and -left vertex. */
            float ftop = float.NegativeInfinity;
            float fright = float.NegativeInfinity;
            float fbottom = Mathf.Infinity;
            float fleft = Mathf.Infinity;
            for (int i = 0; i < line.positionCount; i++)
            {
                //find the outmost points
                if (ftop < positions[i].z)
                {
                    ftop = positions[i].z;
                }
                if (fright < positions[i].x)
                {
                    fright = positions[i].x;
                }
                if (fbottom > positions[i].z)
                {
                    fbottom = positions[i].z;
                }
                if (fleft > positions[i].x)
                {
                    fleft = positions[i].x;
                }
            }
            int top = Mathf.RoundToInt(ftop);
            int right = Mathf.RoundToInt(fright);
            int bottom = Mathf.RoundToInt(fbottom);
            int left = Mathf.RoundToInt(fleft);

            int terrainXmax = right - left; // the rightmost edge of the terrain
            int terrainZmax = top - bottom; // the topmost edge of the terrain 

            float[,] shapeHeights = TerrainMain.terrainData.GetHeights(left, bottom, terrainXmax, terrainZmax);

            Vector2 point; //Create a point Vector2 point to match the shape

            /* Loop through all points in the rectangle surrounding the shape */
            for (int i = 0; i < terrainZmax; i++)
            {
                point.y = i + bottom; //Add off set to the element so it matches the position of the line
                for (int j = 0; j < terrainXmax; j++)
                {
                    point.x = j + left; //Add off set to the element so it matches the position of the line
                    if (InsidePolygon(point, bottom))
                    {
                        shapeHeights[i, j] = height; // set the height value to the terrain vertex
                    }
                }
            }

            //SetHeights to change the terrain height.
            TerrainMain.terrainData.SetHeightsDelayLOD(left, bottom, shapeHeights);
            TerrainMain.ApplyDelayedHeightmapModification();
        }
    }

    //Checks if the given vertex is inside the the shape.
    bool InsidePolygon(Vector2 p, int terrainZmax)
    {
        // Assign the points that define the outline of the shape
        Vector3[] positions = new Vector3[line.positionCount];
        line.GetPositions(positions);

        int count = 0;
        Vector2 p1, p2;
        int n = positions.Length;

        // Find the lines that define the shape
        for (int i = 0; i < n; i++)
        {
            p1.y = positions[i].z;// - p.y;
            p1.x = positions[i].x;// - p.x;
            if (i != n - 1)
            {
                p2.y = positions[(i + 1)].z;// - p.y;
                p2.x = positions[(i + 1)].x;// - p.x;
            }
            else
            {
                p2.y = positions[0].z;// - p.y;
                p2.x = positions[0].x;// - p.x;
            }

            // check if the given point p intersects with the lines that form the outline of the shape.
            if (LinesIntersect(p1, p2, p, terrainZmax))
            {
                count++;
            }
        }

        // the point is inside the shape when the number of line intersections is an odd number
        if (count % 2 == 1)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    // Function that checks if two lines intersect with each other
    bool LinesIntersect(Vector2 A, Vector2 B, Vector2 C, int terrainZmax)
    {
        Vector2 D = new Vector2(C.x, terrainZmax);
        Vector2 CmP = new Vector2(C.x - A.x, C.y - A.y);
        Vector2 r = new Vector2(B.x - A.x, B.y - A.y);
        Vector2 s = new Vector2(D.x - C.x, D.y - C.y);

        float CmPxr = CmP.x * r.y - CmP.y * r.x;
        float CmPxs = CmP.x * s.y - CmP.y * s.x;
        float rxs = r.x * s.y - r.y * s.x;

        if (CmPxr == 0f)
        {
            // Lines are collinear, and so intersect if they have any overlap

            return ((C.x - A.x < 0f) != (C.x - B.x < 0f))
                || ((C.y - A.y < 0f) != (C.y - B.y < 0f));
        }

        if (rxs == 0f)
            return false; // Lines are parallel.

        float rxsr = 1f / rxs;
        float t = CmPxs * rxsr;
        float u = CmPxr * rxsr;

        return (t >= 0f) && (t <= 1f) && (u >= 0f) && (u <= 1f);
    }

}

使用过的方法是一次填充一行形状 - &#34; Ray Casting方法&#34;。事实证明,只有当给定的形状作为很多方面时,这种方法才开始占用更多的资源。 (形状的一侧是连接形状轮廓中两个点的线。) 当我发布这个问题时,我的Line Renderer有134个点来定义该行。这也意味着形状具有需要通过光线投射检查的相同数量的边。 当我将点数缩小到42时,方法变得足够快,而且形状几乎没有丢失任何细节。 此外,我计划使用一些方法使轮廓更平滑,因此可以用更少的点来定义形状。

简而言之,您需要以下步骤才能获得结果:

  1. 创建形状轮廓;
  2. 找出标记形状周围边界框的4个点;
  3. 开始射线投射盒子;
  4. 检查光线与形状边相交的次数。具有奇数的点位于形状内:
  5. 将您的属性分配给在形状中找到的所有点。