[游戏开发]Unity多边形分割为三角形_耳切法

这篇具有很好参考价值的文章主要介绍了[游戏开发]Unity多边形分割为三角形_耳切法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

0. 前言

有个小需求是分割一下多边形,顺带记录一下。通常来说多边形的形状都比较复杂,不好进行操作,这个时候如果我们可以把一个多边形分隔为若干个三角形,回归到简单基础的形状就方便我们操作。三角形化在渲染显示中还是挺多用的。下文未列出,但涉及到的代码链接如下。
// 2023.0615 更新:添加“3.耳切法小优化2” ;调整”4.耳切法实现”;更新代码链接;

链接:https://pan.baidu.com/s/12owWseztK3Re36ariG2EFg?pwd=wsad 
提取码:wsad

1. 耳切法

(1)基础的概念

先了解一下耳切法的基础概念。

  • 耳切法: 耳点简单多边形中是一个凸顶点,将该点移除之后,多边形边数减少1,重复改过程,最终完成三角化。
  • 耳点: 多边形顶点相邻两个点连成一条线段,这条线段完全落在这个多边形的内部
  • 简单多边形: 几何学中将互不相交的线段成对连接形成的闭合路径的平面图形。

所以我们可以发现使用耳切法就是重复找耳点删耳点这个过程,如下。
[游戏开发]Unity多边形分割为三角形_耳切法
那我们的问题就变换成了如何去判断这个耳点

(2)耳点判断

那耳点怎么找呢,这个可以分解为两个条件。

  • 突出的点,和两边的点的夹角需要小于180度
  • 和两边的点组成的三角形内不包含多边形内的其他点

其实看图也比较好理解,下图中0就是满足两个条件的耳点,而1,2分别是不满足这个两个条件的非耳点
[游戏开发]Unity多边形分割为三角形_耳切法

(3)判断角度类型

判断耳点条件的话,可以通过两个向量的叉乘外积的方式来判断

//OA和OB的角在180度一下
private bool IsAngleLessThan180(Vector3 o,Vector3 a,Vector3 b)
{
    return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) > 0;
}

这里有一个要注意的是,那一边才是多边形的里面。如果是多边形按顺时针排序点的话的话,就可以如上判断,但如果是逆时针的话其实是相反的。分别是顺、逆时针的图。后面我们再讲下如何判断多边形给的角的旋转角度。
[游戏开发]Unity多边形分割为三角形_耳切法

(4)点是否在三角形内

这里通过向量叉乘来判断点是否在线的左边或者右边,然后3条边对于这个点p都应该在同一边的话,说明点在三角形内,如果不是则说明在三角形内。

private bool IsContain(Vector3 a, Vector3 b, Vector3 c, Vector3 p)
{
    var c1 = (b.x - a.x) * (p.y - b.y) - (b.y - a.y) * (p.x - b.x);
    var c2 = (c.x - b.x) * (p.y - c.y) - (c.y - b.y) * (p.x - c.x);
    var c3 = (a.x - c.x) * (p.y - a.y) - (a.y - c.y) * (p.x - a.x);

    return c1 > 0f && c2 > 0f && c3 > 0f || c1 < 0f && c2 < 0f && c3 < 0f;
}

(5)判断顺逆时针

去判断顺逆时针的话也是计算叉乘的方式,至于为什么这样就能判断顺逆时针,有点类似于(3)。

private bool IsClockWise(Vector2[] points)
{
    // 通过计算叉乘来确定方向
    float sum = 0f;
    double count = points.Length;
    Vector3 va, vb;
    for (int i = 0; i < points.Length; i++)
    {
        va = points[i];
        vb = (i == count - 1) ? points[0] : points[i + 1];
        sum += va.x * vb.y - va.y * vb.x;
    }
    return sum < 0;
}

好咯,所有的条件都已经去判断了,耳切法就可以去实现了。

2. 耳切法小优化

通过1,我们已经实现了耳切法,但是有特殊情况,比如多边形如下
[游戏开发]Unity多边形分割为三角形_耳切法
1234点在同一个直线,那么当0被判断为耳点并移除之后,其他的点在同一条直线上无法组成三角形的。虽然也没有关系,因为三角形也分隔完了,但这样就会有多余的点留,剩下的点也无法判断为是耳点。我们尽量让所有点都可以被界定,除非是非简单多边形。

所以我们先做一个小调整,就判断内角的时候先判断是不是平角,如果是平角直接移除中间的点。这里我们先定义一下角度

enum AngleType
{
    /// <summary>
    /// 平角 = 180
    /// </summary>
    StraightAngle = 0,

    /// <summary>
    /// 优角 >180
    /// </summary>
    ReflexAngle = 1,

    /// <summary>
    /// 劣角 <180
    /// </summary>
    InferiorAngle = 2,
}

判断角度的函数 IsAngleLessThan18,重新调整一下, 另外加入我们之前考虑过的顺逆时针的条件,那么可以调整为

/// <summary>
/// 判断角的类型,oa & ob 之间的夹角,(右手法则)
/// </summary>
private AngleType GetAngleType(Vector2 o, Vector2 a, Vector2 b, bool isClockWise)
{
    float f = (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
    bool flag = isClockWise ? f > 0 : f < 0;
    if (f == 0)
    {
        return AngleType.StraightAngle;
    }
    else if (flag)
    {
        return AngleType.InferiorAngle;
    }
    else
    {
        return AngleType.ReflexAngle;
    }
}

3. 耳切法小优化2

在实际使用的时候发现一个问题,假设有多边形如下
[游戏开发]Unity多边形分割为三角形_耳切法

这个时候,执行耳切割算法,依次切割的结果为,(蓝色为切割处的三角形)
[游戏开发]Unity多边形分割为三角形_耳切法

会发现在第三次切割的时候,出现了误判,切割出了多余的三角形。这里主要是因为,经过两步切割后,我们得到的其实已经不是简单多边形了,变成一个小旗帜的一个形状,切割错误也难免。这个小旗帜在最顶点执行判断时,条件1角度小于180,通过,条件2其他点不在三角形内也通过,所以被判断成是耳点了,就被切出三角形了,但明显不符合我们的要求。

这里所以我们做一个小调整,在条件2改为,“其他点,不在三角形内以及不在判断点所组成的两边内”。这样就可以避免上述的情况了。

对应的,我们对判断条件的[ 2(4)]中的函数做一下调整如下,(注意c是正在判断耳点的点,ab为其相邻点)

// / <summary>
// / p点是否在点和其左右两个点组成的三角形内,或ca,cb边上
// / </summary>
private bool IsInside(Vector2 c, Vector2 a, Vector2 b, Vector2 p)
{
    // p点是否在abc三角形内
    var c1 = (b.x - a.x) * (p.y - b.y) - (b.y - a.y) * (p.x - b.x);
    var c2 = (c.x - b.x) * (p.y - c.y) - (c.y - b.y) * (p.x - c.x);
    var c3 = (a.x - c.x) * (p.y - a.y) - (a.y - c.y) * (p.x - a.x);
    return
        // (c1 > 0f && c2 > 0f && c3 > 0f) ||
        // (c1 < 0f && c2 < 0f && c3 < 0f);
        (c1 > 0f && c2 >= 0f && c3 >= 0f) ||
        (c1 < 0f && c2 <= 0f && c3 <= 0f);
}

做完调整后,重新执行一下就正常了,逐步结果如下
[游戏开发]Unity多边形分割为三角形_耳切法

4. 耳切法实现

(1)基础定义

先定义三角形Triangle,多边形Polygon两个结构体。

public struct Triangle
{
    public Vector2 a;
    public Vector2 b;
    public Vector2 c;
}

public struct Polygon
{
    public Vector2[] points;
}

此外,考虑到多边形是一个环形,而且我们要频繁的去移除这些点,所以用一个双向链表的结构来处理会更好。C#也有提供了,但也不是很方便,因为多边形还要首尾相连会更合适。我们自己先定义一下这个节点

public class PointNode
{
    public Vector2 Position;
    public PointNode PreviousNode;
    public PointNode NextNode;

    public PointNode(Vector2 Position)
    {
        this.Position = Position;
    }
}

(2)实现

前面基本把流程都说完好了,这里就单纯发一下转化函数把。

/// <summary>
/// 三角形化
/// </summary>
/// <returns></returns>
public Triangle[] Triangulate()
{
    if (points.Length < 3)
    {
        return new Triangle[0];
    }
    else
    {
        // 节点数量
        int count = points.Length;
        // 确定方向
        bool isClockWise = IsClockWise();
        // 初始化节点
        PointNode curNode = GenPointNote();
        // 三角形数量
        int triangleCount = count - 2;
        // 获取三角形
        List<Triangle> triangles = new List<Triangle>();
        AngleType angleType;
        while (triangles.Count < triangleCount)
        {
            // 获取耳点
            int i = 0, maxI = count - 1;
            for (; i <= maxI; i++)
            {
                angleType = GetAngleType(curNode, isClockWise);
                if (angleType == AngleType.StraightAngle)
                {
                    // 等于180,不可能为耳点
                    // 移除当前点,三角形数量少一个
                    curNode = RemovePoint(curNode);
                    count--;
                    triangleCount--;
                }
                else if (angleType == AngleType.ReflexAngle)
                {
                    // 大于180,不可能为耳点
                    curNode = curNode.NextNode;
                }
                else if (IsInsideOtherPoint(curNode, count))
                {
                    //包含其他点,不可能为耳点
                    curNode = curNode.NextNode;
                }
                else
                {
                    // 当前点就是ear,添加三角形,移除当前节点
                    triangles.Add(GenTriangle(curNode));
                    curNode = RemovePoint(curNode);
                    count--;
                    break;
                }
            }
            // DebugDraw(curNode, count, triangles);
            // 还需要分割耳点,但找不到ear
            if (triangles.Count < triangleCount && i > maxI)
            {
                Debug.Log("找不到ear");
                triangles.Clear();
                break;
            }
        }
        return triangles.ToArray();
    }
}

上面代码中有两个之前没有讲过,不过都比较简单,看代码估计比我叨叨要清晰得多。
一个是GenPointNote,是通过多边形点去生成完整节点PointNode,另一个是IsInsideOtherPoint是为了判断条件2,如下。

/// <summary>
/// 生成点节点
/// </summary>
private PointNode GenPointNote()
{
    // 创建第一个节点
    PointNode firstNode = new PointNode(points[0]);
    // 创建后续节点
    PointNode now = firstNode, previous;
    // Vector2[] points
    for (int i = 1; i < points.Length; i++)
    {
        previous = now;
        now = new PointNode(points[i]);
        // 关联
        now.PreviousNode = previous;
        previous.NextNode = now;
    }
    // 关联头尾
    firstNode.PreviousNode = now;
    now.NextNode = firstNode;
    return firstNode;
}

/// <summary>
/// 当前点组成的三角形,是否包含其他点
/// </summary>
private bool IsInsideOtherPoint(PointNode node, int count)
{
    bool flag = false;
    int checkCount = count - 3;
    //now 第一个开始校验其实是node.NextNode.NextNode
    PointNode now = node.NextNode;
    for (int i = 0; i < checkCount; i++)
    {
        now = now.NextNode;
        if (IsInside(node, now.Position))
        {
            flag = true;
            break;
        }
    }
    return flag;
}

5. 测试

写一个简单的脚本来测试一下效果,下面脚本的作用是鼠标点击然后绘点,再用耳切法分隔,并画出图形

using System.Collections;
using System.Collections.Generic;
using GDT;
using GenericShape;
using UnityEngine;

public class TestPMono : MonoBehaviour
{
    public List<Vector2> points;
    public Triangle[] triangles;



    // Start is called before the first frame update
    void Start()
    {
        points = new List<Vector2>();
        // points.Add(new Vector2(0, 100));
        // points.Add(new Vector2(0, 200));
        // points.Add(new Vector2(0, 300));
        // points.Add(new Vector2(200, 200));
        // points.Add(new Vector2(0, 0));
        PolygonNode node = new PolygonNode(points.ToArray());
        triangles = node.Triangulate();
        Debug.Log("triangles:" + triangles.Length);
    }

    void OnMouseDown()
    {
        points.Add(Input.mousePosition);
        if (points.Count > 3)
        {
            PolygonNode node = new PolygonNode(points.ToArray());
            triangles = node.Triangulate();
        }
    }

    // Update is called once per frame
    void Update()
    {
        // 画多边形
        DebugUtil.DrawPolygon(points, Color.red);
        for (int i = 0; i < points.Count; i++)
        {
            DebugUtil.DrawCricle(points[i], 8, 0.1f, Color.red);
        }
        // 画三角形
        if (triangles != null)
        {
            for (int i = 0; i < triangles.Length; i++)
            {
                DebugUtil.DrawTriangle(
                    triangles[i].a, triangles[i].b, triangles[i].c, Color.red);
            }
        }
    }
}

画图形的 DebugUtil.DrawTriangle等如下,后面如果有机会,再整理一下这种调试用的Draw吧

public static void DrawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color, float duration = 0)
{
    Debug.DrawLine(a, b, color, duration);
    Debug.DrawLine(b, c, color, duration);
    Debug.DrawLine(c, a, color, duration);
}

public static void DrawCricle(Vector2 center, float radius, float thetaDelta, Color color, float duration = 0)
{
    float thetaMax = Mathf.PI * 2;
    Vector2 first = new Vector2(radius, 0) + center;
    Vector2 a = first, b;
    for (float theta = 0; theta < thetaMax; theta += thetaDelta)
    {
        b = a;
        a.y = radius * Mathf.Sin(theta);
        a.x = radius * Mathf.Cos(theta);
        a += center;
        Debug.DrawLine(a, b, color, duration);
    }
    Debug.DrawLine(first, a, color, duration);
}

public static void DrawPolygon(Vector2[] points, Color color, float duration = 0)
{
    if (points.Length > 0)
    {
        for (int i = 1; i < points.Length; i++)
        {
            Debug.DrawLine(points[i - 1], points[i], color, duration);
        }
        Debug.DrawLine(points[0], points[points.Length - 1], color, duration);
    }
}

public static void DrawPolygon(List<Vector2> points, Color color, float duration = 0)
{
    if (points.Count > 0)
    {
        for (int i = 1; i < points.Count; i++)
        {
            Debug.DrawLine(points[i - 1], points[i], color, duration);
        }
        Debug.DrawLine(points[0], points[points.Count - 1], color, duration);
    }
}

简单测试一下。
[游戏开发]Unity多边形分割为三角形_耳切法
好耶,没什么问题,终于写完了…。

6. 结束咯

终于写完了…之后估计会考虑一下非简单多边形怎么处理,比如有两线交叉的情况。第二个情况是多边形中间有岛洞的情况,这两种情况后续有时间再考虑吧。没时间处理了。

相关参考文献
耳切法(应该是原论文)
https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
耳切实现(虽然是日文的,但有图例,写的很清楚,非常推荐)
https://qiita.com/fujii-kotaro/items/a411f2a45627ed2f156e
其他介绍
https://blog.csdn.net/u010019717/article/details/52753855/
https://blog.csdn.net/THUNDERDREAMER_OR/article/details/104184589文章来源地址https://www.toymoban.com/news/detail-497481.html

到了这里,关于[游戏开发]Unity多边形分割为三角形_耳切法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • BoxPolyp:使用超粗边界框注释的提升广义多边形分割

    在本文中,提出了一种增强的BoxPolyp模型使用精确掩码和超粗框注释。在实践中,应用框注释来缓解先前息肉分割模型的过拟合问题,该模型通过迭代增强分割模型生成细粒度的息肉区域。 首先提出了一种融合滤波器采样(FFS)模块,用于从具有较少噪声的框注释中生成逐像

    2024年02月03日
    浏览(28)
  • cv2.polylines、cv2.fillPoly 和 多边形绘制分割结果Python函数(一)

    如果只是想撸代码,直接看下一篇: https://blog.csdn.net/HaoZiHuang/article/details/127027469 先来铺垫几个用到的函数 cv2.polylines 、 cv2.fillPoly 以下内容部分摘自: http://www.juzicode.com/opencv-python-polylines-puttext 先看一下代码吧: cv2.polylines 的参数: 绘制的画板图 绘制的多边形列表 是否闭合

    2024年02月04日
    浏览(35)
  • 超详细图文教程:3DS Max 中创建低多边形游戏长剑模型

    推荐: NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 在此,由两部分组成的教程的第一部分中,我将向您展示如何: 对剑柄进行建模 剑的护手模型 剑刃建模 步骤 1 在本教程中使用 正交视图 。要更改视图,请单击视口上任意位置的 鼠标中键 或屏幕左上角的小按钮。

    2024年02月16日
    浏览(28)
  • 如何判断两个多边形是否相交?——多边形相交判定算法详解

    如何判断两个多边形是否相交?——多边形相交判定算法详解 在计算机图形学中,判断两个多边形是否相交是一项很重要的任务。这涉及到各种应用场景,如碰撞检测、模拟物理效果等。在本篇文章中,我们将会介绍多边形相交判定算法的相关知识和实现方式。 首先,我们

    2024年02月14日
    浏览(36)
  • 3DS MAX三维建模平面基础与初级多边形(可编辑多边形的讲解)

            3DS MAX三维建模平面基础与初级多边形(可编辑多边形的讲解)         欢迎大家来学习3DS MAX教程,在这里先说一下研究好3ds Max一定要一边看教程一边要自己学的操作才能更快的进步,预祝大家学习顺利。         这篇是第四篇关于3ds Max的文章了,基于上一

    2024年04月12日
    浏览(44)
  • 基于C++ 的OpenCV绘制多边形,多边形多条边用不用的颜色绘制

    使用基于C++的OpenCV库来绘制多边形,并且为多边形的不同边使用不同的颜色,可以按照以下步骤进行操作: 首先,确保你已经安装了OpenCV库并配置好了你的开发环境。 导入必要的头文件: 创建一个空白的图像,然后绘制多边形,并为每条边选择不同的颜色: 在这个示例中,

    2024年02月13日
    浏览(28)
  • 计算两个多边形的交集

    已知两个多边形Polygon1和Polygon2,分别由点集C1={P1,P2,...,Pm}和C2={Q1,Q2,...,Qn}表示,求这两个多边形的交集。 两个多边形相交后,其顶点要么是两个多边形边的交点,要么是在多边形内部的点。 计算两个多边形每条边之间的交点。 计算包含在多边形内部的点。 将交点和多边形内

    2024年02月12日
    浏览(48)
  • 多边形边的插值

    算法描述及提问: 给定一个最小长度,对多边形的每一条边不断的对半插值,使得插值后的每一条边都要不大于最小长度。 测试ChatGPT - 中文版 VSCode插件。 显然是错误的。 正确的结果: 使用ChatGPT-中文版 VSCode,基本可以写出一个简单的算法,但是正确与否还需要个人Debug及

    2024年02月12日
    浏览(45)
  • 使用OpenCV的函数polylines()绘制多条相连的线段和多边形;使用函数fillPoly()绘制带填充效果的多边形

    函数polylines()可用来根据点集绘制多条相连的线段,也可用来绘制多边形。 函数polylines()有两种原型,这里只向大家介绍比较常用的那种原型。 函数polylines()的C++原型如下: 函数polylines()的Python原型如下: 函数polylines()的参数意义如下: img—绘制的多条相连线段或多边形所在

    2024年02月04日
    浏览(41)
  • C#凹多边形求内心

    在计算凹多边形内心时,一种常见的方法是使用三角剖分和重心法。您可以按照以下步骤进行: 将凹多边形进行三角剖分,得到一系列三角形。 对每个三角形计算其重心,重心是三个顶点的平均值。 将所有三角形的重心进行平均,得到凹多边形的内心。 以下是一个简单的示

    2024年01月25日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包