首页 > 解决方案 > 如何比较两条线?

问题描述

我有一个代码可以让我画线并限制可以画的线数。

我的问题是我想创建一条线(例如线渲染器),然后允许用户尝试绘制一条相似(不一定完全相同)的线,如果线相似,代码需要根据设置知道够不够,但我想不通。

我会很感激任何提示。

public class DrawLine : MonoBehaviour
{
    public GameObject linePrefab;
    public GameObject currentLine;

    public LineRenderer lineRenderer;
    public EdgeCollider2D edgeCollider;
    public List<Vector2> fingerPositions;

    public Button[] answers;
    public bool isCurrButtonActive;

    int mouseButtonState = 0;

    void Update()
    {
        Debug.Log(rfgrhe);
        if (isCurrButtonActive)
        {
            if (Input.GetMouseButtonDown(0))
            {
                if (mouseButtonState == 0)
                {
                    CreateLine();
                }
            }
            if (Input.GetMouseButtonUp(0))
            {
                mouseButtonState++;
            }
            if (Input.GetMouseButton(0))
            {
                if (mouseButtonState == 1)
                {
                    Debug.Log(Input.mousePosition.ToString());
                    if (Input.mousePosition.x < 100 || Input.mousePosition.y > 420 || Input.mousePosition.x > 660 || Input.mousePosition.y < 7)
                    {
                        return;
                    }
                    Vector2 tempFingerPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                    if (Vector2.Distance(tempFingerPos, fingerPositions[fingerPositions.Count - 1]) > .1f)
                    {
                        UpdateLine(tempFingerPos);
                    }
                }
            }
        }
    }

    void CreateLine()
    {
        mouseButtonState++;
        currentLine = Instantiate(linePrefab, Vector3.zero, Quaternion.identity);
        lineRenderer = currentLine.GetComponent<LineRenderer>();
        edgeCollider = currentLine.GetComponent<EdgeCollider2D>();
        fingerPositions.Clear();
        fingerPositions.Add(Camera.main.ScreenToWorldPoint(Input.mousePosition));
        fingerPositions.Add(Camera.main.ScreenToWorldPoint(Input.mousePosition));
        lineRenderer.SetPosition(0, fingerPositions[0]);
        lineRenderer.SetPosition(1, fingerPositions[1]);
        edgeCollider.points = fingerPositions.ToArray();
    }

    void UpdateLine(Vector2 newFingerPos)
    {
        fingerPositions.Add(newFingerPos);
        lineRenderer.positionCount++;
        lineRenderer.SetPosition(lineRenderer.positionCount - 1, newFingerPos);
        edgeCollider.points = fingerPositions.ToArray();
    }

    public void ActivateCurrentButton()
    {
        // Debug.Log(isCurrButtonActive);
        isCurrButtonActive = true;
        for (int i = 0; i < answers.Length; i++)
        {
            if (answers[i].CompareTag("onePoint"))
            {
                answers[i].GetComponent<MapLvl>().isCurrButtonActive = false;
            }
            else if (answers[i].CompareTag("TwoPoints"))
            {
                answers[i].GetComponent<DrawLine>().isCurrButtonActive = false;
            }
        }
    }
}

在此处输入图像描述

例如在这种情况下,蓝线是正确的,绿线和红线是用户回答的两个选项。我想要的是程序只会将绿线确认为正确答案。

标签: c#unity3d

解决方案


编辑:因为现在我们想要什么更清楚了,所以这是实现它的一种方法:

下面的函数float DifferenceBetweenLines(Vector3[], Vector3[])可以测量“两条线之间的距离”。

它沿着直线行走以匹配最大步长,并为每个点计算与绘图线上最近点的距离。它将这些距离的平方相加,然后将它们除以要匹配的线的长度(不要让我用数学严谨性来解释这一点)。

返回值越小,第一行与第二行越接近——阈值由您决定。

float DifferenceBetweenLines(Vector3[] drawn, Vector3[] toMatch) {
    float sqrDistAcc = 0f;
    float length = 0f;

    Vector3 prevPoint = toMatch[0];

    foreach (var toMatchPoint in WalkAlongLine(toMatch)) {
        sqrDistAcc += SqrDistanceToLine(drawn, toMatchPoint);
        length += Vector3.Distance(toMatchPoint, prevPoint);

        prevPoint = toMatchPoint;
    }

    return sqrDistAcc / length;
}

/// <summary>
/// Move a point from the beginning of the line to its end using a maximum step, yielding the point at each step.
/// </summary>
IEnumerable<Vector3> WalkAlongLine(IEnumerable<Vector3> line, float maxStep = .1f) {
    using (var lineEnum = line.GetEnumerator()) {
        if (!lineEnum.MoveNext())
            yield break;

        var pos = lineEnum.Current;

        while (lineEnum.MoveNext()) {
            Debug.Log(lineEnum.Current);
            var target = lineEnum.Current;
            while (pos != target) {
                yield return pos = Vector3.MoveTowards(pos, target, maxStep);
            }
        }
    }
}

static float SqrDistanceToLine(Vector3[] line, Vector3 point) {
    return ListSegments(line)
        .Select(seg => SqrDistanceToSegment(seg.a, seg.b, point))
        .Min();
}

static float SqrDistanceToSegment(Vector3 linePoint1, Vector3 linePoint2, Vector3 point) {
    var projected = ProjectPointOnLineSegment(linePoint1, linePoint1, point);
    return (projected - point).sqrMagnitude;
}

/// <summary>
/// Outputs each position of the line (but the last) and the consecutive one wrapped in a Segment.
/// Example: a, b, c, d --> (a, b), (b, c), (c, d)
/// </summary>
static IEnumerable<Segment> ListSegments(IEnumerable<Vector3> line) {
    using (var pt1 = line.GetEnumerator())
    using (var pt2 = line.GetEnumerator()) {
        pt2.MoveNext();

        while (pt2.MoveNext()) {
            pt1.MoveNext();

            yield return new Segment { a = pt1.Current, b = pt2.Current };
        }
    }
}
struct Segment {
    public Vector3 a;
    public Vector3 b;
}

//This function finds out on which side of a line segment the point is located.
//The point is assumed to be on a line created by linePoint1 and linePoint2. If the point is not on
//the line segment, project it on the line using ProjectPointOnLine() first.
//Returns 0 if point is on the line segment.
//Returns 1 if point is outside of the line segment and located on the side of linePoint1.
//Returns 2 if point is outside of the line segment and located on the side of linePoint2.
static int PointOnWhichSideOfLineSegment(Vector3 linePoint1, Vector3 linePoint2, Vector3 point){
    Vector3 lineVec = linePoint2 - linePoint1;
    Vector3 pointVec = point - linePoint1;

    if (Vector3.Dot(pointVec, lineVec) > 0) {
        return pointVec.magnitude <= lineVec.magnitude ? 0 : 2;
    } else {
        return 1;
    }
}

//This function returns a point which is a projection from a point to a line.
//The line is regarded infinite. If the line is finite, use ProjectPointOnLineSegment() instead.
static Vector3 ProjectPointOnLine(Vector3 linePoint, Vector3 lineVec, Vector3 point){
    //get vector from point on line to point in space
    Vector3 linePointToPoint = point - linePoint;
    float t = Vector3.Dot(linePointToPoint, lineVec);
    return linePoint + lineVec * t;
}

//This function returns a point which is a projection from a point to a line segment.
//If the projected point lies outside of the line segment, the projected point will
//be clamped to the appropriate line edge.
//If the line is infinite instead of a segment, use ProjectPointOnLine() instead.
static Vector3 ProjectPointOnLineSegment(Vector3 linePoint1, Vector3 linePoint2, Vector3 point){
    Vector3 vector = linePoint2 - linePoint1;
    Vector3 projectedPoint = ProjectPointOnLine(linePoint1, vector.normalized, point);

    switch (PointOnWhichSideOfLineSegment(linePoint1, linePoint2, projectedPoint)) {
        case 0:
            return projectedPoint;
        case 1:
            return linePoint1;
        case 2:
            return linePoint2;
        default:
            //output is invalid
            return Vector3.zero;
    }
}

最后的数学函数来自3d Math Functions - Unify Community Wiki

下面是如何使用它来比较 LineRenderer 和另一个:

Array.Resize(ref lineBuffer1, lineRenderer1.positionCount);
Array.Resize(ref lineBuffer2, lineRenderer2.positionCount);

lineRenderer1.GetPositions(lineBuffer1);
lineRenderer2.GetPositions(lineBuffer2);

float diff = DifferenceBetweenLines(lineBuffer1, lineBuffer2);
const float threshold = 5f;

Debug.Log(diff < threshold ? "Pretty close!" : "Not that close...");

有几点需要考虑:

  • 的性能SqrDistanceToLine肯定可以提高
  • 您可以衡量第一条线与第二条线的接近程度,而不是相反- 也就是说,第一条线可以更长或中途散步,只要它回到正轨并“覆盖”另一条线足够接近。您可以通过DifferenceBetweenLines第二次调用、交换参数并取两者中最大的结果来解决此问题。
  • 我们可以使用 Vector2 而不是 Vector3


原答案:

相似的?

正如@Jonathan 指出的那样,您需要更准确地了解“足够相似”

  • 大小相似重要吗?
  • 方向重要吗?
  • 比例上的相似性是否重要(或者只是线条的“方向变化”)?
  • ...

正如您可能猜到的那样,这些标准越少,就越难;因为你的相似性概念将从你一开始得到的原始位置变得越来越抽象。

  • 例如,如果用户需要画一个十字,正好有两个笔划,它或多或少地覆盖了一个定义的区域,那么任务就很简单了:

    您可以测量该区域的角与每个笔划的第一个和最后一个点之间的距离,并检查线条是否笔直。

  • 如果你想检查用户是否画了一个完美的心形,在任何方向上,它明显更棘手......

    为此,您可能不得不求助于专门的图书馆。

要考虑的另一件事是,用户是否真的需要制作一条与另一条相似的线,或者它是否应该足够接近才能与其他可能的线区分开来?考虑这个例子:

用户需要画一个十字 (X) 或一个圆 (O):

  • 如果只有一个笔划回到起点附近,假设一个圆圈。
  • 如果存在大体方向正交的笔画,则假设为十字形。

在这种情况下,一个更复杂的系统可能会是矫枉过正。

一些“原始指针”

假设简单的要求(因为假设相反,我无法提供太多帮助),这里有一些元素:

完全符合

用户必须在可见线的顶部绘制:这是最简单的场景。

对于他的线的每个点,找出与参考线上最近点的距离。对这些距离的平方求和——由于某种原因,它比对距离本身求和更好,而且直接计算平方距离也更便宜。

LineRenderer.Simplify

非常具体到您的用例,因为您使用的是 Unity 的 LineRenderer,所以值得知道它包含一个Simplify(float)方法,该方法会降低曲线的分辨率,使其更易于处理,并且如果要匹配的线由有点直的段(不是复杂的曲线)。

使用角度

有时您会想要检查线的不同子段之间的角度,而不是它们的(相对)长度。无论比例如何,它都会测量方向的变化,这可能更直观。

例子

一个检测等边三角形的简单示例:

  • LineRenderer.Simplify
  • 如果两端足够接近,则关闭形状
  • 检查每个角度约为 60 度:

测试三角

对于任意线,您可以运行该线以通过与用户绘制的线相同的“过滤器”进行匹配,并比较这些值。由您决定哪些属性最重要(角度/距离/比例...),以及阈值是多少。


推荐阅读