图例
- 划分示例图
- 查询示例图
- 解释一下,黄线是射线表面点到哪里,记为点P
- 小蓝色框表明能包含点P的最小空间节点
- 红框表明能包含点P的三角形,记为三角形T
- 大蓝色框表明能包含三角形T的最小空间节点
- 可以看到已经使用四叉树只需要遍历少量三角形就可以查询到目标三角形
应用
情景分析
- 导航三角形网格中的三角形不可能互相包含
- 从根节点开始找到包含三角形的最小的子节点,遍历这个节点下的所有物体,找到包含顶点的三角形
要解决的问题
- 肯定会存在多个很大的shape横跨多个区域,所以非叶子节点要能存物体
- 需要根据一个物体的位置和尺寸定位到节点
- 最小的尺寸怎么确定
- 如果用最大的物体的尺寸作为分割大小会导致树节点很少,性能退化成O(n)
- 如果用最小的物体的尺寸作为分割大小会导致节点特别细碎
- 根据使用时的性能,看自己的数据情况,如果小三角形比较多,最小节点设置的小一些,否则大一些
实现
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DC.Lockstep.navigation
{
public struct TriangleShape
{
public Vector2[] vertices;
public int a, b, c;
public Vector2 pA => vertices[a];
public Vector2 pB => vertices[b];
public Vector2 pC => vertices[c];
public Vector2 GetCenter()
{
return (vertices[a] + vertices[b] + vertices[c]) / 3f;
}
}
public class QuadTreeNode
{
public static float min_size = 4;
protected QuadTreeNode[] mChildren;
protected List<TriangleShape> mContent;
protected Rect mArea;
public QuadTreeNode()
{
}
public QuadTreeNode(Rect area)
{
SetArea(area);
}
public void SetArea(Rect area)
{
mArea = area;
}
public Rect GetArea()
{
return mArea;
}
public QuadTreeNode[] GetChildren()
{
return mChildren;
}
public List<TriangleShape> GetContent()
{
if (null == mContent)
{
return new List<TriangleShape>();
}
return new List<TriangleShape>(mContent);
}
public int GetShapeCnt()
{
return mContent?.Count ?? 0;
}
public bool Insert(TriangleShape shape)
{
if (!Contains(shape))
{
return false;
}
// 已经是最小分割
if (mArea.width <= min_size)
{
Append(shape);
return true;
}
var index = GetIndex(shape);
// 如果在某个子节点里面
if (index >= 0)
{
if (null == mChildren)
{
mChildren = new QuadTreeNode[4];
}
if (mChildren[index] == null)
{
mChildren[index] = CreateChild(index);
}
var suc = mChildren[index].Insert(shape);
if (suc) return true;
}
// 在当前节点
Append(shape);
return true;
}
protected void Append(TriangleShape shape)
{
if (null == mContent)
{
mContent = new List<TriangleShape>();
}
mContent.Add(shape);
}
public bool Contains(TriangleShape shape)
{
return Contains(shape.pA) && Contains(shape.pB) && Contains(shape.pC);
}
public bool Contains(Vector2 pos)
{
return mArea.Contains(pos);
}
protected QuadTreeNode CreateChild(int index)
{
if (index < 0 || 3 < index)
{
throw new ArgumentException("index must in [0,3], now is " + index);
}
//左下角0,左上角2,右上角3,右下角1
var child = new QuadTreeNode();
switch (index)
{
case 0:
child.SetArea(Rect.MinMaxRect(mArea.xMin, mArea.yMin, mArea.center.x, mArea.center.y));
break;
case 1:
child.SetArea(Rect.MinMaxRect(mArea.center.x, mArea.yMin, mArea.xMax, mArea.center.y));
break;
case 2:
child.SetArea(Rect.MinMaxRect(mArea.xMin, mArea.center.y, mArea.center.x, mArea.yMax));
break;
case 3:
child.SetArea(Rect.MinMaxRect(mArea.center.x, mArea.center.y, mArea.xMax, mArea.yMax));
break;
}
return child;
}
public void Remove(TriangleShape shape)
{
if (Contains(shape))
{
var index = GetIndex(shape);
if (index >= 0 && mChildren[index] != null)
{
mChildren[index].Remove(shape);
return;
}
mContent?.Remove(shape);
}
}
public int GetIndex(TriangleShape shape)
{
var i1 = GetIndex(shape.pA);
var i2 = GetIndex(shape.pA);
if (i1 != i2)
{
return -1;
}
var i3 = GetIndex(shape.pA);
if (i2 != i3)
{
return -1;
}
return i1;
}
public int GetIndex(Vector2 point)
{
if (!Contains(point))
{
return -1;
}
//左下角0,左上角2,右上角3,右下角1
var index = 0;
if (point.x > mArea.center.x)
{
index += 1;
}
if (point.y > mArea.center.y)
{
index += 2;
}
return index;
}
public void GetTriangleShapes(List<TriangleShape> list)
{
if (null != mChildren)
{
for (int i = 0; i < mChildren.Length; i++)
{
mChildren[i]?.GetTriangleShapes(list);
}
}
if (null != mContent)
{
list.AddRange(mContent);
}
}
public QuadTreeNode GetMinContainsNode(Vector2 pos)
{
if (!Contains(pos))
{
return null;
}
if (null != mChildren)
{
for (int i = 0; i < mChildren.Length; i++)
{
if(mChildren[i] == null) continue;
var node = mChildren[i].GetMinContainsNode(pos);
if (null != node && node.GetShapeCnt() > 0)
{
return node;
}
}
}
return this;
}
}
}
我的测试方式
- 使用Unity生成导航数据
- 合并导航网格中相同的三角形,导出成mesh模型文件
- 使用mesh模型文件中的三角形构造QuadTreeNode
- 编写QuadTreeNode的可视化脚本获得文中的示例图
- 将mesh模型放到unity场景,添加检测脚本,检测鼠标点击到模型上的位置,定位到对应的QuadTreeNode