四叉树是一种空间划分树,每个节点最多有四个子树的数据结构。
主要应用于:
-
场景管理:
特别适合大规模的广阔室外场景管理。
一般来说如果游戏场景是基于地形的(甚至没有高度)(如城市、平原、2D场景),那么适合用四叉树来管理。
而如果游戏场景在高度轴上也有大量物体需要管理(如太空、高山),那么适合用八叉树来管理。 -
感知检测
假如保证一个智能体最远不会感知到所在区域外的地方。
那么通过四叉树,可以快速过滤掉K区域外的目标,只需考虑K区域内的目标。 -
碰撞检测
类似上面感知检测。不同划分区域保证不会碰撞的情况下,就能快速过滤与本物体不同区域的其他潜在物体碰撞。 -
光线追踪(Ray Tracing)过滤
光线追踪渲染,可使用八叉树来划分3D空间区域,从而过滤掉大量不必要的区域。
四叉树搜索和插入的效率为O(nlogn)
// 四叉树根节点,用于管理四叉树
public class Tree {
public INode root;
public List<INode> nodes;
public int maxDepth;
public int scale; // 树的规模
/// <summary>
/// 默认为四叉树构造方式, 可以修改Constructor方法实现不同的Tree类型
/// </summary>
public virtual void Constructor(Bounds boundary, int maxDepth=5, int scale=4) {
this.maxDepth = maxDepth;
int nodesCount = 1;
for (int i = 1; i < maxDepth; ++i)
nodesCount |= 1 << (i<<1);
// 如果需要扩展Tree的功能, 可以修改这两个地方Node的实例化调用
nodes = new List<INode>(new INode[nodesCount]);
root = new Node(0, 1, boundary.center, boundary.size, scale);
AppendNode(0, root);
}
public void Insert(NodeInfo nodeInfo) {
root.Insert(this, nodeInfo, maxDepth);
}
public virtual void AppendNode(int nodeIndex, INode nodeInfo) {
nodes[nodeIndex] = nodeInfo;
}
public void MoveNext(Camera camera) {
root.MoveNext(camera);
}
public void DrawGizmos() {
root.DrawGizmos();
}
}
// 实现Node算法接口
public class Node : INode {
public int id;
public int index; // 节点的索引
public int depth; // 节点的深度
public int scale; // 节点规模
public Bounds boundary;
public List<NodeInfo> gameObjects;
public int isRendering = 2;
public bool divided; // 节点是否已经分裂
public Node[] children; // 四叉树节点的子节点
public Vector4[] corners; // 四叉树节点的边界点
public Vector3 FLB { get => corners[0]; } // 前左下
public Vector3 FLT { get => corners[1]; } // 前左上
public Vector3 FRT { get => corners[2]; } // 前右上
public Vector3 FRB { get => corners[3]; } // 前右下
public Vector3 BRT { get => corners[4]; } // 后右上
public Vector3 BRB { get => corners[5]; } // 后右下
public Vector3 BLB { get => corners[6]; } // 后左下
public Vector3 BLT { get => corners[7]; } // 后左上
public Node(int index, int depth, Vector3 center, Vector3 size, int scale=4) {
Constructor(index, depth, center, size, scale);
}
public void CalculateCorners() {
corners[0].w = 1;
corners[1].w = 1;
corners[2].w = 1;
corners[3].w = 1;
corners[4].w = 1;
corners[5].w = 1;
corners[6].w = 1;
corners[7].w = 1;
corners[0].x = boundary.center.x - boundary.extents.x; // 前左下
corners[0].y = boundary.center.y - boundary.extents.y; // 前左下
corners[0].z = boundary.center.z - boundary.extents.z; // 前左下
corners[1].x = boundary.center.x - boundary.extents.x; // 前左上
corners[1].y = boundary.center.y + boundary.extents.y; // 前左上
corners[1].z = boundary.center.z - boundary.extents.z; // 前左上
corners[2].x = boundary.center.x - boundary.extents.x; // 前右上
corners[2].y = boundary.center.y + boundary.extents.y; // 前右上
corners[2].z = boundary.center.z + boundary.extents.z; // 前右上
corners[3].x = boundary.center.x - boundary.extents.x; // 前右下
corners[3].y = boundary.center.y - boundary.extents.y; // 前右下
corners[3].z = boundary.center.z + boundary.extents.z; // 前右下
corners[4].x = boundary.center.x + boundary.extents.x; // 后右上
corners[4].y = boundary.center.y + boundary.extents.y; // 后右上
corners[4].z = boundary.center.z + boundary.extents.z; // 后右上
corners[5].x = boundary.center.x + boundary.extents.x; // 后右下
corners[5].y = boundary.center.y - boundary.extents.y; // 后右下
corners[5].z = boundary.center.z + boundary.extents.z; // 后右下
corners[6].x = boundary.center.x + boundary.extents.x; // 后左下
corners[6].y = boundary.center.y - boundary.extents.y; // 后左下
corners[6].z = boundary.center.z - boundary.extents.z; // 后左下
corners[7].x = boundary.center.x + boundary.extents.x; // 后左上
corners[7].y = boundary.center.y + boundary.extents.y; // 后左上
corners[7].z = boundary.center.z - boundary.extents.z; // 后左上
}
public void Constructor(int index, int depth, Vector3 center, Vector3 size, int scale=4) {
this.index = index;
this.depth = depth;
this.scale = scale;
boundary.center = center;
boundary.size = size;
gameObjects = new List<NodeInfo>(128);
isRendering = 2;
divided = false;
corners = new Vector4[8];
CalculateCorners();
}
public void Divide(Tree tree) {
this.divided = true;
children = new Node[scale];
Vector3 center, size = boundary.size*0.5f;
size.y = boundary.size.y;
center.y = 0;
center.x = boundary.center.x - boundary.extents.x*0.5f;
center.z = boundary.center.z - boundary.extents.z*0.5f;
children[0] = new Node((index<<2) + 1, depth+1, center, size);
center.x = boundary.center.x + boundary.extents.x*0.5f;
center.z = boundary.center.z - boundary.extents.z*0.5f;
children[1] = new Node((index<<2) + 2, depth+1, center, size);
center.x = boundary.center.x + boundary.extents.x*0.5f;
center.z = boundary.center.z + boundary.extents.z*0.5f;
children[2] = new Node((index<<2) + 3, depth+1, center, size);
center.x = boundary.center.x - boundary.extents.x*0.5f;
center.z = boundary.center.z + boundary.extents.z*0.5f;
children[3] = new Node((index<<2) + 4, depth+1, center, size);
tree.AppendNode(children[0].index, children[0]);
tree.AppendNode(children[1].index, children[1]);
tree.AppendNode(children[2].index, children[2]);
tree.AppendNode(children[3].index, children[3]);
}
public void Insert(Tree tree, NodeInfo nodeInfo, int maxDepth) {
if (!divided && depth < maxDepth)
Divide(tree);
int insertIdx = -1;
if (divided) {
for (int i = 0; i < scale; ++i) {
if (children[i].boundary.Intersects(nodeInfo.boundary)) {
if (insertIdx != -1) {
insertIdx = -1;
break;
}
insertIdx = i;
}
}
}
if (insertIdx == -1) {
gameObjects.Add(nodeInfo);
} else {
children[insertIdx].Insert(tree, nodeInfo, maxDepth);
}
}
public void Destroy() {
foreach (NodeInfo nodeInfo in gameObjects) {
nodeInfo.gameObject.SetActive(true);
}
gameObjects.Clear();
foreach (var child in children) {
child.Destroy();
}
divided = false;
}
public void MoveNext(Camera camera) {
if (CameraSight.Visible(corners, camera))
Render(camera);
else
Culling(camera);
}
public void Render(Camera camera) {
if (divided) {
foreach (var child in children) {
if (CameraSight.Visible(child.corners, camera)) {
child.Render(camera);
} else {
child.Culling(camera);
}
}
}
if (isRendering == 0)
return;
isRendering = 0;
for (int i = 0; i < gameObjects.Count; ++i) {
if (gameObjects[i] != null && gameObjects[i].gameObject)
gameObjects[i].gameObject.SetActive(true);
}
}
public void Culling(Camera camera) {
if (divided) {
foreach (var child in children) {
child.Culling(camera);
}
}
if (isRendering == 1)
return;
isRendering = 1;
for (int i = 0; i < gameObjects.Count; ++i) {
if (gameObjects[i] != null && gameObjects[i].gameObject)
gameObjects[i].gameObject.SetActive(false);
}
}
public void DrawGizmos() {
if (isRendering == 0) {
Gizmos.color = Color.yellow;
} else if (gameObjects.Count > 0) {
Gizmos.color = Color.blue;
} else {
Gizmos.color = Color.green;
}
Gizmos.DrawWireCube(boundary.center, boundary.size);
if (!divided || (isRendering & -isRendering) != 0)
return;
var to = boundary.center + Vector3.up*3;
Gizmos.color = Color.red;
foreach (var child in gameObjects) {
if (child != null && child.gameObject)
Gizmos.DrawLine(child.gameObject.transform.position, to);
}
foreach (var child in children) {
child.DrawGizmos();
}
}
}
// 实现计算点在相机范围内的计算
public static class CameraSight {
public static void IsInView(Vector4 position, ref int inViewport) {
int visible = 0;
if (position.x < -position.w) visible |= 1;
if (position.x > position.w) visible |= 2;
if (position.y < -position.w) visible |= 4;
if (position.y > position.w) visible |= 8;
if (position.z < -position.w) visible |= 16;
if (position.z > position.w) visible |= 32;
inViewport &= visible;
}
public static bool Visible(Vector4[] corners, Camera camera, float fov=1.0f) {
int visible = 63;
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[0] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[1] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[2] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[3] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[4] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[5] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[6] * fov, ref visible);
IsInView(camera.projectionMatrix * camera.worldToCameraMatrix * corners[7] * fov, ref visible);
return visible == 0;
}
}
// 生成测试Unity代码
public class Test : MonoBehaviour {
public Bounds bounds;
public int maxDepth;
public int generateCount;
public GameObject prefab;
public QuadTree tree;
// Start is called before the first frame update
void Start() {
tree = new QuadTree();
tree.Constructor(bounds, maxDepth);
RandomGenerator();
}
void RandomGenerator() {
for (int i = 0; i < generateCount && Application.isPlaying; ++i) {
GameObject go = Instantiate(prefab);
go.name = $"Point {i}";
go.transform.localScale = new Vector3(Random.Range(0.3f, 3), Random.Range(0.3f, 3), Random.Range(0.3f, 3));
go.transform.position = new Vector3(UnityEngine.Random.Range(-bounds.size.x * 0.5f, bounds.size.x * 0.5f), go.transform.localScale.y*0.5f, UnityEngine.Random.Range(-bounds.size.z * 0.5f, bounds.size.z * 0.5f));
tree.Insert(new NodeInfo(go));
}
}
// Update is called once per frame
void Update() {
if (tree != null) {
tree.MoveNext(Camera.main);
}
}
void OnDrawGizmos() {
if (tree != null) {
tree.DrawGizmos();
}
}
}
以上,只实现了四叉树的插入及区域控制代码,黄色区域为相机显示区域,蓝色区域为存在物体未显示区域,绿色为未显示区域。