1. 程式人生 > 其它 >【UE4 C++】八叉樹實現及視覺化

【UE4 C++】八叉樹實現及視覺化

前言


概念

定義

  • 八叉樹(英語:octree)是一種樹形資料結構,每個內部節點都正好有八個子節點。八叉樹常用於分割三維空間,將其遞迴細分為八個卦限。

應用

  • 八叉樹是四叉樹在三維空間中的對應,在三維圖形、三維遊戲引擎等領域有很多應用,如:
    • 加速用於可見性判斷的視錐裁剪(view frustum culling)。
    • 加速射線投射(ray casting) ,如用作視線判斷或槍擊判定。
    • 鄰近查詢(proximity query),如查詢玩家角色某半徑範圍內的敵方NPC。
    • 碰撞檢測的粗略階段(broad phase),找出潛在可能碰撞的物體對。

構建

  • 資料儲存方式分為

    • 位置點儲存
    • 空間區域儲存
  • 八叉樹一般建立過程:(也可以設定最小分割尺寸、或立方體最大容納資料)

    ① 設定最大遞迴深度;

    ② 找出場景的最大尺寸,並以此尺寸建立第一個立方體;

    ③ 依序將單位元元素丟入能被包含且沒有子節點的立方體;

    ④ 若未達到最大遞迴深度,就進行細分八等份,再將該立方體所裝的單位元元素全部分擔給八個子立方體;

    ⑤ 若發現子立方體所分配到的單位元元素數量不為零且跟父立方體是一樣的,則該子立方體停止細分,因為跟據空間分割理論,細分的空間所得到的分配必定較少,若是一樣數目,則再怎麼切數目還是一樣,會造成無窮切割的情形;

    ⑥ 重複③,直到達到最大遞迴深度;

  • 資料更新方式

    • 暴力清除,重新構建八叉樹
    • 檢查節點儲存的物件的新位置是否超出當前節點的空間範圍。如果超出,則將該物件從當前節點的物件列表中清出,重新從根節點插入。
  • 查詢物件

    • 根據空間範圍是否相交,遍歷八叉樹

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 本系列文章為筆者整理原創,只發表在部落格園上,歡迎分享本文連結,如需轉載,請註明出處!