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

news2025/2/4 8:43:24

[ 目录 ]

  • 0. 前言
  • 1. 耳切法
    • (1)基础的概念
    • (2)耳点判断
    • (3)判断角度类型
    • (4)点是否在三角形内
    • (5)判断顺逆时针
  • 2. 耳切法小优化
  • 3. 耳切法实现
    • (1)基础定义
    • (2)实现
  • 4. 测试
  • 5. 结束咯

0. 前言

有个小需求是分割一下多边形,顺带记录一下。通常来说多边形的形状都比较复杂,不好进行操作,这个时候如果我们可以把一个多边形分隔为若干个三角形,回归到简单基础的形状就方便我们操作。三角形化在渲染显示中还是挺多用的。下文未列出,但涉及到的代码链接如下。

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

1. 耳切法

(1)基础的概念

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

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

所以我们可以发现使用耳切法就是重复找耳点删耳点这个过程,如下。
在这里插入图片描述
那我们的问题就变换成了如何去判断这个耳点

(2)耳点判断

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

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

其实看图也比较好理解,下图中0就是满足两个条件的耳点,而1,2分别是不满足这个两个条件的非耳点
在这里插入图片描述

(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;
}

这里有一个要注意的是,那一边才是多边形的里面。如果是多边形按顺时针排序点的话的话,就可以如上判断,但如果是逆时针的话其实是相反的。分别是顺、逆时针的图。后面我们再讲下如何判断多边形给的角的旋转角度。
在这里插入图片描述

(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,我们已经实现了耳切法,但是有特殊情况,比如多边形如下
在这里插入图片描述
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. 耳切法实现

(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;
                }
            }
            // 找不到ear
            if (i > maxI)
            {
                triangles.Clear();
                break;
            }

        }
        return triangles.ToArray();
    }
}

4. 测试

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

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);
    }
}

简单测试一下。
在这里插入图片描述
好耶,没什么问题,终于写完了…。

5. 结束咯

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

相关参考文献
耳切法(应该是原论文)
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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/647773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

openGauss5 企业版之常用运维命令

文章目录 日维护检查项检查openGauss状态检查锁信息统计事件数据对象检查SQL报告检查备份基本信息检查 检查操作系统参数检查办法异常处理 检查openGauss健康状态检查办法 本章节主要介绍在 openGauss数据库 在日常运维中的常用命令 日维护检查项 检查openGauss状态 通过open…

Java性能权威指南-总结13

Java性能权威指南-总结13 堆内存最佳实践减少内存使用减少对象大小延迟初始化 堆内存最佳实践 减少内存使用 减少对象大小 对象会占用一定数量的堆内存&#xff0c;所以要减少内存使用&#xff0c;最简单的方式就是让对象小一些。考虑运行程序的机器的内存限制&#xff0c;增…

Nautilus Chain测试网迎阶段性里程碑,模块化区块链拉开新序幕

Nautilus Chain 是目前行业内少有的真实实践的 Layer3 模块化链&#xff0c;该链曾在几个月前上线了测试网&#xff0c;并接受用户测试交互。该链目前正处于测试网阶段&#xff0c;并即将在不久上线主网&#xff0c;这也将是行业内首个正式上线的模块化区块链底层。 而在上个月…

Android 13(T) Media框架 -异步消息机制

网上有许多优秀的博文讲解了Android的异步消息机制&#xff08;ALooper/AHandler/AMessage那一套&#xff09;&#xff0c;希望看详细代码流程的小伙伴可以去网上搜索。这篇笔记将会记录我对android异步消息机制的理解&#xff0c;这次学完之后就可以熟练运用这套异步消息机制了…

【数据库二】数据库用户管理与授权

数据库用户管理与授权 1.MySQL数据库管理1.1 常用的数据类型1.2 char和varchar区别1.3 SQL语句分类 2.数据表高级操作2.1 克隆表2.2 清空表2.3 创建临时表 3.MySQL的六大约束4.外键约束4.1 外键概述4.2 创建主从表4.3 主从表中插入数据4.4 主从表中删除数据4.5 删除外键约束 5.…

conda环境中配置cuda+cudnn+pytorch深度学习环境

本文参考&#xff1a; 在conda虚拟环境中配置cudacudnnpytorch深度学习环境&#xff08;新手必看&#xff01;简单可行&#xff01;&#xff09;_conda安装cudnn_江江ahh的博客-CSDN博客 一、创建虚拟环境 conda create -n mytorch python3.8 二、执行sudo nvidia-smi查看CU…

物联网通信技术

通信的技术指标是什么&#xff1f;AB A. 可靠性 B. 有效性 C. 实时性D. 广覆盖 多路复用技术有哪些&#xff1f;ABCD A. FDMA B. CDMA C. SDMA D. TDMA 使用多个频率来传输信号的技术被称为扩展频谱技术&#xff0c;该技术使用的目的是什么&#xff1f; AB A. 抗干扰B. 提…

【VMware】VMware安装CentOS8-Stream虚拟机

本文首发于 慕雪的寒舍 VMware安装CentOS8-Stream虚拟机 1.安装VMware 由于最新版的vm要钱&#xff0c;这里提供一个VMware16pro的安装包&#xff1b;我知道度盘下载速度慢&#xff0c;但确实没啥其他选择&#xff0c;见谅。 后文将用vm来简称VMware 提取嘛: gdt9 亚索包解…

解决UGUI的图集导致Shader采样时UV错误的问题

大家好&#xff0c;我是阿赵。 在我们用UGUI的时候&#xff0c;很多时候需要通过在UI上面挂材质球&#xff0c;写Shader&#xff0c;来实现一些特殊的效果。 这里句一个很简单的例子&#xff0c;只为说明问题。 一、简单例子说明 这个例子是这样的&#xff0c;我想在某个Imag…

Python模块openpyxl 操作Excel文件

简介 openpyxl是一个用于读取和编写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。openpyxl以Python语言和MIT许可证发布。 openpyxl可以处理Excel文件中的绝大多数内容&#xff0c;包括图表、图像和公式。它可以处理大量数据&#xff0c;支持Pandas和NumPy库导入和导出数据。…

chatgpt赋能python:Python本地安装库:一个简单易懂的指南

Python本地安装库&#xff1a;一个简单易懂的指南 Python是一种高级的编程语言&#xff0c;它拥有庞大的社区支持和无数的第三方库。如果你在使用Python时需要一些额外的功能&#xff0c;那么你可能需要安装一些库。本文将介绍如何在本地安装库&#xff0c;以及一些需要注意的…

chatgpt赋能python:如何更新Python库?Python更新库完全指南

如何更新Python库&#xff1f;Python更新库完全指南 Python作为一种最受欢迎的编程语言&#xff0c;其库和工具的数量是惊人的。这些库是Python生态系统的重要组成部分&#xff0c;以便帮助开发人员解决不同类型的问题。然而&#xff0c;这些库会更新&#xff0c;开发人员需要…

什么是椭圆曲线上的加法

椭圆曲线图形示例 注意&#xff0c;椭圆曲线随着你参数的不同&#xff0c;有不同的形态&#xff0c;这里仅是一种示例&#xff0c;详细的关于椭圆曲线的知识可以后附扩展知识连接 椭圆曲线上的加法 椭圆曲线上的加法不是我们通常意义上的数值加法&#xff0c;而是一种特殊的几…

干翻Mybatis源码系列之第十篇:Mybatis Plugins基本概念

给自己的每日一句 不从恶人的计谋&#xff0c;不站罪人的道路&#xff0c;不坐亵慢人的座位&#xff0c;惟喜爱耶和华的律法&#xff0c;昼夜思想&#xff0c;这人便为有福&#xff01;他要像一棵树栽在溪水旁&#xff0c;按时候结果子&#xff0c;叶子也不枯干。凡他所做的尽…

Oracle中的行列互转———pivot、unpivot函数用法

一、需求说明 项目开发过程中涉及到oracle数据库的数据操作&#xff1b;但是需要将数据进行列的互转&#xff0c;通过查阅资料可知在oracle中有三种方式可以实现行列互转&#xff1a; ①使用decode 函数&#xff1b; ②使用case when 函数&#xff1b; ③使用pivot函数&…

Linux之设置主机名

目录 Linux之设置主机名 查看主机名 语法格式 案例 修改主机名 语法格式 案例 --- 修改静态主机名为joker 配置静态解析 为Linux主机指派域名解析 Linux之设置主机名 查看主机名 语法格式 hostnamectl [status] [--static|--transient|--pretty] 解析&#xff1a; s…

极致呈现系列之:Echarts地图的浩瀚视野(一)

目录 Echarts中的地图组件地图组件初体验下载地图数据准备Echarts的基本结构导入地图数据并注册展示地图数据结合visualMap展示地图数据 Echarts中的地图组件 Echarts中的地图组件是一种用于展示地理数据的可视化组件。它可以显示全国、各省市和各城市的地图&#xff0c;并支持…

整形在内存中的存储-原码补码反码的理解与应用

目录 一、概论 1.1 C语言中基本的数据类型 1.2 类型的基本归类 二、整形在内存中的存储 2.1 原码、反码、补码 2.2 存储补码和大小端存储 三、计算各基本数据类型的范围计算原理 3.1 有符号类型的整形范围 3.2 无符号类型的整形范围 3.3 例题 一、概论 C语言提供了非常…

【Java基础学习打卡07】Java语言概述

目录 引言一、Java语言1.Java语言简介2.Java语言优势3.Java能做什么&#xff1f; 二、Java之父三、Java简史1.Java版本时间线2.Java发展重要节点 总结 引言 一、Java语言 1.Java语言简介 Java语言是一种以面向对象为基础的高级编程语言。吸收了C语言的各种优点&#xff0c;又…

【IMX6ULL驱动开发学习】06.APP与驱动程序传输数据+自动创建设备节点(hello驱动)

一、APP与驱动之间传输数据 /*驱动从APP获取数据*/ unsigned long copy_from_user(void *to, const void *from, unsigned long n)/*驱动传输数据到APP*/ unsigned long copy_to_user(void *to, const void *from, unsigned long n)二、使用copy_to_user、copy_from_user在AP…