[UE4 C + +] octree implementation and visualization

Keywords: data structure

preface

concept

definition

  • Octree (English: octree) is a tree data structure. Each internal node has exactly eight child nodes. Octree is often used to segment three-dimensional space and recursively subdivide it into eight trigrams.

application

  • Octree is the corresponding of quadtree in three-dimensional space. It has many applications in three-dimensional graphics, three-dimensional game engine and other fields, such as:
    • Accelerate view frustum culling for visibility judgment.
    • Accelerated ray casting, such as line of sight judgment or shooting judgment.
    • proximity query, such as querying enemy NPC s within a certain radius of a player character.
    • The broad phase of collision detection is to find out potential object pairs that may collide.

structure

  • Data storage methods are divided into:

    • Location point storage
    • Spatial area storage
  • General establishment process of octree: (you can also set the minimum partition size or the maximum data contained in the cube)

    ① Set the maximum recursion depth;

    ② Find out the maximum size of the scene and build the first cube with this size;

    ③ Sequentially throw the unit element into a cube that can be contained and has no child nodes;

    ④ If the maximum recursion depth is not reached, it is subdivided into eight equal parts, and then all the unit elements contained in the cube are shared to the eight sub cubes;

    ⑤ If it is found that the number of unit elements allocated to the subcube is not zero and is the same as the parent cube, the subcube will stop subdivision, because according to the space division theory, the allocation of subdivided space must be less. If the number is the same, the number of cuts will be the same, resulting in infinite cutting;

    ⑥ Repeat ③ until the maximum recursion depth is reached;

  • Data update method:

    • Violence is cleared and the octree is rebuilt
    • Check whether the new location of the object stored by the node exceeds the space range of the current node. If it exceeds, the object will be cleared from the object list of the current node and re inserted from the root node.
  • Find objects

    • Traverse the octree according to whether the spatial range intersects

UE4 implementation

  • effect

    • Divide into small pieces

    • Dynamic update

    • Dynamic search

  • See the appendix at the end for the main code

reference resources

appendix

Main code

  • OctreeNode code

    // Article address 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"
    
    // Nodes of quadtree
    class OctreeNode:public TSharedFromThis<OctreeNode> {
    
    public:
    	FVector center; // Center point
    	FVector extend; // Extended size
    	float miniSize=20;    //Minimum split size
    	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;
    	}
    
    	//Intersection of cube and ball
    	bool InterSection(FVector _OCenter, float _radian) {
    		FVector v = _OCenter - center; //Take the relative origin
    		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; //Note the relative coordinates of the center of the circle
    	}
    
    	//Is the point in this area
    	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 
    			);
    	}
    
    	
    	// Select which quadrant the location point is in
    	int SelectBestChild(FVector _point) {
    		return (_point.X <= center.X ? 0 : 1) + (_point.Y >= center.Y ? 0 : 4) + (_point.Z <= center.Z ? 0 : 2);
    	}
    
    	//Split eight child nodes
    	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));
    		
    	}
    
    	//Insert Object
    	void InsertObject(ABattery* obj) {
    		
    		if (obj == nullptr  || !InterSection(obj->GetActorLocation()))
    			return;
    
    		int32  childIndex;
    		if (children.Num()==0) { 
    			if (objs.Num() <= maxCount || extend.X <= miniSize) { //If the capacity of the current node is not full, it is added directly to the node
    				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;
    	}
    
    	// Merging can have the effect of optimization
    	void Merge() {
    		for (auto& child : children) {
    			objs.Append(child->objs);
    		}
    		children.Empty();
    	}
    
    	//Remove object
    	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;
    	}
    
    	// Draw area boundaries
    	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);
    		}
    		
    	}
    
    	// Determine whether the battery is within the range of the scanner
    	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();
    			}			
    		}
    	}
    
    	// Update status
    	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();// Recycle, merge and optimize
    			for (auto& child : children) {
    				child->UpdateState();
    			}
    		}
    		if (depth <0) {
    			bInRange = false;
    			DrawBound(1 / UKismetSystemLibrary::GetFrameCount(),1); //Draw according to the number of frames
    		}
    			
    	}
    };
    
  • AOctree header file

    // Article address 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

    // Article address 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(); //Update status
    		if (traceActor)
    		{
    			root->TraceObjectInRange(traceActor, affectRadianRange); //Determine whether it is within the range of the scanner	
    		}		
    	}
    }
    
    // Timed generation object
    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);	
    	}
    }
    
    // Give an object a speed at a fixed time
    void AOctree::ActorsAddVelocity()
    {
    	for (ABattery* actor :objs)
    	{
    		actor->GetStaticMeshComponent()->SetPhysicsLinearVelocity(UKismetMathLibrary::RandomUnitVector() * 100);
    		if (traceActor)
    		{
    			Cast<ABattery>(traceActor)->GetStaticMeshComponent()->SetPhysicsLinearVelocity(UKismetMathLibrary::RandomUnitVector() * 250);
    		}
    	}
    }
    

Posted by Chris_Evans on Wed, 10 Nov 2021 18:01:51 -0800