【UE4 C++】八叉树实现及可视化
前言
- 实现版本:4.26
- 参考见文末
- 本文地址 https://www.cnblogs.com/shiroe/p/15534304.html
概念
定义
-
八叉树(英语:octree)是一种树形数据结构,每个内部节点都正好有八个子节点。八叉树常用于分割三维空间,将其递归细分为八个卦限。
应用
- 八叉树是四叉树在三维空间中的对应,在三维图形、三维游戏引擎等领域有很多应用,如:
- 游戏
- 加速用于可见性判断的视锥裁剪(view frustum culling)。
- 加速射线投射(ray casting) ,如用作视线判断或枪击判定。
- 邻近查询(proximity query),如查询玩家角色某半径范围内的敌方NPC。
- 碰撞检测的粗略阶段(broad phase),找出潜在可能碰撞的物体对。
- 点云建图
- 颜色量化
- 游戏
构建
-
数据存储方式分为:
- 位置点存储
- 空间区域存储
-
八叉树一般建立过程:(也可以设定最小分割尺寸、或立方体最大容纳数据)
① 设定最大递归深度;
② 找出场景的最大尺寸,并以此尺寸建立第一个立方体;
③ 依序将单位元元素丢入能被包含且没有子节点的立方体;
④ 若未达到最大递归深度,就进行细分八等份,再将该立方体所装的单位元元素全部分担给八个子立方体;
⑤ 若发现子立方体所分配到的单位元元素数量不为零且跟父立方体是一样的,则该子立方体停止细分,因为跟据空间分割理论,细分的空间所得到的分配必定较少,若是一样数目,则再怎么切数目还是一样,会造成无穷切割的情形;
⑥ 重复③,直到达到最大递归深度;
-
数据更新方式:
- 暴力清除,重新构建八叉树
- 检查节点存储的对象的新位置是否超出当前节点的空间范围。如果超出,则将该对象从当前节点的对象列表中清出,重新从根节点插入。
-
查找对象
- 根据空间范围是否相交,遍历八叉树
UE4 实现
本例主要按照位置点存储方式进行实现
-
效果
-
划分到小块
-
动态更新
-
动态查找
-
-
主要代码,见尾部附录
参考
-
游戏场景管理的八叉树算法是怎样的?-知乎 (含 Milo Yip 回答)
-
UE4也有自己的八叉树
附录
主要代码
-
OctreeNode代码
// 本文地址 https://www.cnblogs.com/shiroe/p/15534304.html #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Kismet/KismetMathLibrary.h" #include "Kismet/KismetSystemLibrary.h" #include "../QuadTree/Battery.h" #include "Octree.generated.h" // 四叉树的节点 class OctreeNode:public TSharedFromThis<OctreeNode> { public: FVector center; // 中心点 FVector extend; // 扩展尺寸 float miniSize=20; //最小分割尺寸 int32 maxCount = 4; int32 depth; TArray<ABattery*>objs; static UObject* worldObject; bool bInRange; TSharedPtr<OctreeNode> root; TArray<TSharedPtr<OctreeNode>> children; public: OctreeNode(FVector _center, FVector _extend, int32 _depth, TSharedPtr<OctreeNode> _root=nullptr) : center(_center), extend(_extend), depth(_depth) { root = _root; bInRange = false; } ~OctreeNode() { root = nullptr; objs.Empty(); children.Empty(); } bool IsNotUsed() { return true; } //立方体与球 求交 bool InterSection(FVector _OCenter, float _radian) { FVector v = _OCenter - center; //取相对原点 float x = UKismetMathLibrary::Min(v.X, extend.X); x = UKismetMathLibrary::Max(x, -extend.X); float y = UKismetMathLibrary::Min(v.Y, extend.Y); y = UKismetMathLibrary::Max(y, -extend.Y); float z = UKismetMathLibrary::Min(v.Z, extend.Z); z = UKismetMathLibrary::Max(z, -extend.Z); return (x - v.X) * (x - v.X) + (y - v.Y) * (y - v.Y) + (z - v.Z) * (z - v.Z) <= _radian * _radian * _radian; //注意此时圆心的相对坐标 } //点是否在本区域内 bool InterSection(FVector _point) { return (_point.X >= center.X - extend.X && _point.X <= center.X + extend.X && _point.Y >= center.Y - extend.Y && _point.Y <= center.Y + extend.Y && _point.Z >= center.Z - extend.Z && _point.Z <= center.Z + extend.Z ); } // 选择位置点在哪个象限 int SelectBestChild(FVector _point) { return (_point.X <= center.X ? 0 : 1) + (_point.Y >= center.Y ? 0 : 4) + (_point.Z <= center.Z ? 0 : 2); } //分割出八个子节点 void split() { float quarter = extend.X / 2.0f; root = root.IsValid() ? root : this->AsShared(); children.Init(nullptr, 8); children[0]=MakeShareable(new OctreeNode(center+FVector(-quarter,quarter,-quarter), extend / 2, depth + 1, root)); children[1]=MakeShareable(new OctreeNode(center+FVector( quarter,quarter,-quarter), extend / 2, depth + 1, root)); children[2]=MakeShareable(new OctreeNode(center+FVector(-quarter,quarter, quarter), extend / 2, depth + 1, root)); children[3]=MakeShareable(new OctreeNode(center+FVector( quarter,quarter, quarter), extend / 2, depth + 1, root)); children[4]=MakeShareable(new OctreeNode(center+FVector(-quarter,-quarter,-quarter), extend / 2, depth + 1, root)); children[5]=MakeShareable(new OctreeNode(center+FVector( quarter,-quarter,-quarter), extend / 2, depth + 1, root)); children[6]=MakeShareable(new OctreeNode(center+FVector(-quarter,-quarter, quarter), extend / 2, depth + 1, root)); children[7]=MakeShareable(new OctreeNode(center+FVector( quarter,-quarter, quarter), extend / 2, depth + 1, root)); } //插入对象 void InsertObject(ABattery* obj) { if (obj == nullptr || !InterSection(obj->GetActorLocation())) return; int32 childIndex; if (children.Num()==0) { if (objs.Num() <= maxCount || extend.X <= miniSize) { //如果当前节点容纳量未满,则直接添加到该节点 objs.Add(obj); return; } split(); check(children.Num()>0); for (int32 i = objs.Num() - 1; i >= 0; i--) { //ABattery* battery = objs[i]; childIndex = SelectBestChild(objs[i]->GetActorLocation()); children[childIndex]->InsertObject(objs[i]); objs.Swap(i, objs.Num() - 1); objs.Pop(); } } childIndex = SelectBestChild(obj->GetActorLocation()); children[childIndex]->InsertObject(obj); } bool canMerge() { int TotalObjCount = objs.Num(); if (children.Num() > 0) { for (auto& child : children) { if (child->children.Num() > 0) { return false; } TotalObjCount += child->objs.Num(); } } return TotalObjCount <= maxCount; } // 合并可以起到优化的效果 void Merge() { for (auto& child : children) { objs.Append(child->objs); } children.Empty(); } //移除对象 bool RmoveObject(ABattery* obj) { bool bRemove = false; for (int32 i = 0; i < objs.Num(); i++) { if (objs[i] == obj) { objs.RemoveSwap(obj); bRemove = true; break; } } if (!bRemove && children.Num() > 0) { int32 childIndex = SelectBestChild(obj->GetActorLocation()); children[childIndex]->RmoveObject(obj); bRemove = true; } if (bRemove && children.Num() > 0 && canMerge()) { Merge(); } return bRemove; } // 绘制区域边界 void DrawBound(float time = 0.02f, float thickness = 2.0f) { if (worldObject) { TArray<FLinearColor> colors = { FLinearColor(0.5,0,0,1),FLinearColor(0.5,0,0.5,1),FLinearColor(1,0.5,0,1),FLinearColor(1,0,0,1) }; FLinearColor drawColor = bInRange ? FLinearColor::Green : colors[UKismetMathLibrary::Clamp(depth,0,3)]; FVector drawCenter = center;// +(bInRange ? FVector(0, 0, 8) : FVector(0, 0, 5)); UKismetSystemLibrary::DrawDebugBox(worldObject, drawCenter, extend, drawColor, FRotator::ZeroRotator, time, thickness+depth*0.2); } } // 判断电池是否在扫描器的范围类 void TraceObjectInRange(AActor* traceActor, float _radian) { FVector _OCenter = traceActor->GetActorLocation(); bInRange = false; if (InterSection(_OCenter, _radian)) { bInRange = true; for (int32 i = objs.Num()-1; i >=0; i--) { for (ABattery* obj : objs){ { bool bCanActive = FVector::Distance(_OCenter, obj->GetActorLocation()) <= _radian; obj->ActiveState(bCanActive, traceActor); //if (bCanActive) //DrawBound(1 / UKismetSystemLibrary::GetFrameCount(),0.5); } } } if (children.Num() > 0) { for (auto& child : children) { child->TraceObjectInRange(traceActor, _radian); } } } else { TraceObjectOutRange(); } } void TraceObjectOutRange() { bInRange = false; for (int32 i = objs.Num()-1; i >=0; i--) { objs[i]->ActiveState(false, nullptr); } for (auto& node: children) { if (node.IsValid()) { node->TraceObjectOutRange(); } } } // 更新状态 void UpdateState() { for (int32 i = objs.Num()-1; i >=0; i--) { if (!InterSection(objs[i]->GetActorLocation())) { ABattery* obj = objs[i]; RmoveObject(obj); root->InsertObject(obj); } } if (children.Num() > 0) { if (canMerge())Merge();// 回收合并,进行优化 for (auto& child : children) { child->UpdateState(); } } if (depth <0) { bInRange = false; DrawBound(1 / UKismetSystemLibrary::GetFrameCount(),1); //根据帧数绘制 } } };
-
AOctree 头文件
// 本文地址 https://www.cnblogs.com/shiroe/p/15534304.html UCLASS() class PRIME_API AOctree : public AActor { GENERATED_BODY() public: AOctree(); virtual void Tick(float DeltaTime) override; void SpawnActors(); void ActorsAddVelocity(); protected: virtual void BeginPlay() override; public: UPROPERTY(EditAnywhere) int32 cubeCount=20; int32 widthX=800; int32 widthY=800; int32 widthZ=800; UPROPERTY(EditAnywhere) float playRate=0.05; UPROPERTY(EditAnywhere) TSubclassOf<ABattery> BatteryClass; UPROPERTY(EditAnywhere) AActor* traceActor; UPROPERTY(EditAnywhere) float affectRadianRange=50; UPROPERTY() TArray<ABattery*> objs; TSharedPtr<OctreeNode> root; FTimerHandle timer; FTimerHandle timer2; };
-
AOctree cpp
// 本文地址 https://www.cnblogs.com/shiroe/p/15534304.html #include "Octree.h" AOctree::AOctree(){ PrimaryActorTick.bCanEverTick = true;} UObject* OctreeNode::worldObject=nullptr; void AOctree::BeginPlay() { Super::BeginPlay(); OctreeNode::worldObject = GetWorld(); root = MakeShareable(new OctreeNode(FVector(0,0,400), FVector(400, 400, 400),0)); GetWorld()->GetTimerManager().SetTimer(timer, this, &AOctree::SpawnActors, playRate, true); GetWorld()->GetTimerManager().SetTimer(timer2, this, &AOctree::ActorsAddVelocity, 2, true); } void AOctree::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (root.IsValid()) { root->UpdateState(); //更新状态 if (traceActor) { root->TraceObjectInRange(traceActor, affectRadianRange); //判断是否在扫描器的范围内 } } } // 定时生成物体 void AOctree::SpawnActors() { if (cubeCount < 0) { GetWorld()->GetTimerManager().ClearTimer(timer); return; } cubeCount--; FVector pos = FVector( UKismetMathLibrary::RandomIntegerInRange(-widthX/2+10, widthX/2-10), UKismetMathLibrary::RandomIntegerInRange(-widthY/2+10, widthY/2-10), UKismetMathLibrary::RandomIntegerInRange(10, widthZ-10)); FTransform trans = FTransform(FRotator(0, UKismetMathLibrary::RandomFloatInRange(0, 360), 0), pos, FVector(0.2)); ABattery* actor= GetWorld()->SpawnActor<ABattery>(BatteryClass, trans); if (IsValid(actor)) { objs.Add(actor); root->InsertObject(actor); } } // 定时给物体一个速度 void AOctree::ActorsAddVelocity() { for (ABattery* actor :objs) { actor->GetStaticMeshComponent()->SetPhysicsLinearVelocity(UKismetMathLibrary::RandomUnitVector() * 100); if (traceActor) { Cast<ABattery>(traceActor)->GetStaticMeshComponent()->SetPhysicsLinearVelocity(UKismetMathLibrary::RandomUnitVector() * 250); } } }
作者:砥才人
出处:https://www.cnblogs.com/shiroe
本系列文章为笔者整理原创,只发表在博客园上,欢迎分享本文链接,如需转载,请注明出处!