octree implementation
Some checks failed
studiorailgun/Renderer/pipeline/head There was a failure building this commit

This commit is contained in:
austin 2024-08-22 18:03:48 -04:00
parent a125043400
commit ef0c2ee7ca
4 changed files with 907 additions and 2 deletions

View File

@ -1,3 +1,3 @@
#maven.buildNumber.plugin properties file
#Tue Aug 20 15:21:51 EDT 2024
buildNumber=286
#Thu Aug 22 17:01:13 EDT 2024
buildNumber=287

View File

@ -0,0 +1,19 @@
package electrosphere.util;
/**
* Utilities for making devving faster
*/
public class CodeUtils {
/**
* Used as placeholder when haven't implemented error handling yet
* @param e The exception
* @param explanation Any kind of explanation string. Ideally explains what the case is
*/
public static void todo(Exception e, String explanation){
System.out.println("TODO: handle exception");
System.out.println(explanation);
e.printStackTrace();
}
}

View File

@ -0,0 +1,594 @@
package electrosphere.util.ds;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.joml.Vector3d;
/**
* An octree implementation
*/
public class Octree<T> {
/**
* The number of children in a full, non-leaf node
*/
private static final int FULL_NODE_SIZE = 8;
//The root node
private OctreeNode<T> root = null;
/**
* Table used for looking up the presence of nodes
* Maps a position key to a corresponding Octree node
*/
Map<String,OctreeNode<T>> lookupTable = new HashMap<String,OctreeNode<T>>();
/**
* Creates an octree
* @param center The center of the root node of the octree
*/
public Octree(Vector3d boundLower, Vector3d boundUpper){
root = new OctreeNode<T>(boundLower, boundUpper);
}
/**
* Adds a leaf to the octree
* @param location The location of the leaf
* @param data The data associated with the leaf
* @throws IllegalArgumentException Thrown if a location is provided that already has an exact match
*/
public void addLeaf(Vector3d location, T data) throws IllegalArgumentException {
//check if the location is already occupied
if(this.containsLeaf(location)){
throw new IllegalArgumentException("Tried adding leaf that is already occupied!");
}
OctreeNode<T> node = new OctreeNode<T>(data, location);
OctreeNode<T> current = this.root;
while(current.children.size() == FULL_NODE_SIZE){
if(current.hasChildInQuadrant(location)){
OctreeNode<T> child = current.getChildInQuadrant(location);
if(child.isLeaf()){
//get parent of the new fork
OctreeNode<T> parent = child.parent;
parent.removeChild(child);
//while the newly forked child isn't between the two children, keep forking
OctreeNode<T> nonLeaf = parent.forkQuadrant(child.getLocation());
while(nonLeaf.getQuadrant(child.getLocation()) == nonLeaf.getQuadrant(node.getLocation())){
nonLeaf = nonLeaf.forkQuadrant(child.getLocation());
}
//add the child and the new node to the non-leaf node
nonLeaf.addChild(child);
nonLeaf.addChild(node);
this.lookupTable.put(this.getLocationKey(location),node);
break;
} else {
current = child;
}
} else {
current.addChild(node);
this.lookupTable.put(this.getLocationKey(location),node);
break;
}
}
}
/**
* Checks if the octree contains a leaf at an exact location
* @param location The location
* @return true if a leaf exists for that exact location, false otherwise
*/
public boolean containsLeaf(Vector3d location){
return lookupTable.containsKey(this.getLocationKey(location));
}
/**
* Gets the leaf at an exact location
* @param location The location
* @return The leaf
* @throws ArrayIndexOutOfBoundsException Thrown if a location is queries that does not have an associated leaf in the octree
*/
public OctreeNode<T> getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException {
if(!this.containsLeaf(location)){
throw new ArrayIndexOutOfBoundsException("Tried to get leaf at position that does not contain leaf!");
}
return lookupTable.get(this.getLocationKey(location));
}
/**
* Removes a node from the octree
* @param node The node
*/
public void removeNode(OctreeNode<T> node){
OctreeNode<T> parent = node.parent;
parent.removeChild(node);
if(node.isLeaf()){
this.lookupTable.remove(this.getLocationKey(node.getLocation()));
}
}
/**
* Gets the root node
* @return The root node
*/
public OctreeNode<T> getRoot(){
return this.root;
}
/**
* Gets the number of leaves under this tree
* @return The number of leaves
*/
public int getNumLeaves(){
return this.root.getNumLeaves();
}
/**
* Gets the key from the location
* @param location The location
* @return The key
*/
private String getLocationKey(Vector3d location){
return location.x + "_" + location.y + "_" + location.z;
}
/**
* A single node in the octree
*/
public static class OctreeNode<T> {
/*
6 +----------+ 7
/| / |
/ | / |
/ | / |
2 +---------+ 3 + 5
| /4 | /
| / | /
| / | /
+---------+
0 1
Y Z
^ /
| /
|/
+---> X
*/
//the location of the node
private Vector3d midpoint;
//the bounds of the node
private Vector3d lowerBound;
private Vector3d upperBound;
//True if this is a leaf node, false otherwise
private boolean isLeaf;
//the parent node
private OctreeNode<T> parent;
//the children of this node
private List<OctreeNode<T>> children = new LinkedList<OctreeNode<T>>();
//The data at the node
private T data;
/**
* Constructor
* @param data The data at this octree node's position
* @param location The location of the node
*/
private OctreeNode(T data, Vector3d location){
this.data = data;
this.midpoint = location;
this.isLeaf = true;
}
/**
* Creates a non-leaf node
* @param lowerBound The lower bound of the node
* @param upperBound The upper bound of the node
*/
private OctreeNode(Vector3d lowerBound, Vector3d upperBound){
this.data = null;
this.midpoint = new Vector3d(
((upperBound.x - lowerBound.x) / 2.0) + lowerBound.x,
((upperBound.y - lowerBound.y) / 2.0) + lowerBound.y,
((upperBound.z - lowerBound.z) / 2.0) + lowerBound.z
);
this.lowerBound = lowerBound;
this.upperBound = upperBound;
for(int i = 0; i < FULL_NODE_SIZE; i++){
this.children.add(null);
}
this.isLeaf = false;
}
/**
* Creates a non-leaf node
* @param midpoint The midpoint of the node
*/
private OctreeNode(Vector3d midpoint){
this.data = null;
this.midpoint = midpoint;
for(int i = 0; i < FULL_NODE_SIZE; i++){
this.children.add(null);
}
this.isLeaf = false;
}
/**
* Gets the data in the node
* @return The data
*/
public T getData(){
return data;
}
/**
* Gets the location of the node
* @return The location
*/
public Vector3d getLocation(){
return midpoint;
}
/**
* Gets the parent node of this node
* @return The parent node
*/
public OctreeNode<T> getParent(){
return parent;
}
/**
* Gets the children of this node
* @return The children
*/
public List<OctreeNode<T>> getChildren(){
return this.children.stream().filter((node)->{return node != null;}).collect(Collectors.toList());
}
/**
* Checks if this node has a child in a given quadrant
* @param positionToCheck The position to check
* @return true if there is a child in that quadrant, false otherwise
*/
public boolean hasChildInQuadrant(Vector3d positionToCheck){
int positionQuadrant = this.getQuadrant(positionToCheck);
OctreeNode<T> child = this.children.get(positionQuadrant);
return child != null;
}
/**
* Gets the child in a given quadrant
* @param positionToQuery The position of the quadrant
* @return The child if it exists, null otherwise
*/
public OctreeNode<T> getChildInQuadrant(Vector3d positionToQuery){
int positionQuadrant = this.getQuadrant(positionToQuery);
OctreeNode<T> child = this.children.get(positionQuadrant);
return child;
}
/**
* Checks if this is a leaf node or not
* @return true if it is a leaf, false otherwise
*/
public boolean isLeaf(){
return this.isLeaf;
}
/**
* Gets the number of child nodes
* @return The number of child nodes
*/
public int getNumChildren(){
int acc = 0;
for(OctreeNode<T> child : children){
if(child != null){
acc++;
}
}
return acc;
}
/**
* Gets the number of leaves beneath this node
* @return The number of leaves
*/
public int getNumLeaves(){
int acc = 0;
if(this.isLeaf()){
return 1;
} else {
for(OctreeNode<T> child : this.children){
if(child != null){
if(child.isLeaf()){
acc++;
} else {
acc = acc + child.getNumLeaves();
}
}
}
}
return acc;
}
/**
* Adds a child to this node
* @param child The child
*/
private void addChild(OctreeNode<T> child){
if(hasChildInQuadrant(child.midpoint)){
throw new IllegalArgumentException("Trying to add child in occupied quadrant!");
}
int quadrant = this.getQuadrant(child.getLocation());
this.children.set(quadrant,child);
child.parent = this;
this.setChildBounds(child);
}
/**
* Removes a child from this node
* @param child The child
*/
private void removeChild(OctreeNode<T> child){
if(child == null){
throw new IllegalArgumentException("Child cannot be null!");
}
int quadrant = this.getQuadrant(child.getLocation());
this.children.set(quadrant,null);
child.parent = null;
}
/**
* Gets the quadrant of a position relative to this node
* @param positionToCheck The position to check
* @return The quadrant
*/
private int getQuadrant(Vector3d positionToCheck){
if(positionToCheck.z < this.midpoint.z){
if(positionToCheck.y < this.midpoint.y){
if(positionToCheck.x < this.midpoint.x){
return 0;
} else {
return 1;
}
} else {
if(positionToCheck.x < this.midpoint.x){
return 2;
} else {
return 3;
}
}
} else {
if(positionToCheck.y < this.midpoint.y){
if(positionToCheck.x < this.midpoint.x){
return 4;
} else {
return 5;
}
} else {
if(positionToCheck.x < this.midpoint.x){
return 6;
} else {
return 7;
}
}
}
}
/**
* Sets the bounds of the child based on the quadrant it falls under
* @param child The child
*/
private void setChildBounds(OctreeNode<T> child){
if(child.midpoint.z < this.midpoint.z){
if(child.midpoint.y < this.midpoint.y){
if(child.midpoint.x < this.midpoint.x){
child.lowerBound = new Vector3d(this.lowerBound);
child.upperBound = new Vector3d(this.midpoint);
} else {
child.lowerBound = new Vector3d(
this.midpoint.x,
this.lowerBound.y,
this.lowerBound.z
);
child.upperBound = new Vector3d(
this.upperBound.x,
this.midpoint.y,
this.midpoint.z
);
}
} else {
if(child.midpoint.x < this.midpoint.x){
child.lowerBound = new Vector3d(
this.lowerBound.x,
this.midpoint.y,
this.lowerBound.z
);
child.upperBound = new Vector3d(
this.midpoint.x,
this.upperBound.y,
this.midpoint.z
);
} else {
child.lowerBound = new Vector3d(
this.midpoint.x,
this.midpoint.y,
this.lowerBound.z
);
child.upperBound = new Vector3d(
this.upperBound.x,
this.upperBound.y,
this.midpoint.z
);
}
}
} else {
if(child.midpoint.y < this.midpoint.y){
if(child.midpoint.x < this.midpoint.x){
child.lowerBound = new Vector3d(
this.lowerBound.x,
this.lowerBound.y,
this.midpoint.z
);
child.upperBound = new Vector3d(
this.midpoint.x,
this.midpoint.y,
this.upperBound.z
);
} else {
child.lowerBound = new Vector3d(
this.midpoint.x,
this.lowerBound.y,
this.midpoint.z
);
child.upperBound = new Vector3d(
this.upperBound.x,
this.midpoint.y,
this.upperBound.z
);
}
} else {
if(child.midpoint.x < this.midpoint.x){
child.lowerBound = new Vector3d(
this.lowerBound.x,
this.midpoint.y,
this.midpoint.z
);
child.upperBound = new Vector3d(
this.midpoint.x,
this.upperBound.y,
this.upperBound.z
);
} else {
child.lowerBound = new Vector3d(this.midpoint);
child.upperBound = new Vector3d(this.upperBound);
}
}
}
}
/**
* Gets the midpoint of a given quadrant
* @param quadrant The quadrant
* @return The midpoint
*/
private Vector3d getQuadrantMidpoint(int quadrant){
Vector3d lowerBound = null;
Vector3d upperBound = null;
if(quadrant == 0){
lowerBound = new Vector3d(this.lowerBound);
upperBound = new Vector3d(this.midpoint);
} else if(quadrant == 1) {
lowerBound = new Vector3d(
this.midpoint.x,
this.lowerBound.y,
this.lowerBound.z
);
upperBound = new Vector3d(
this.upperBound.x,
this.midpoint.y,
this.midpoint.z
);
} else if(quadrant == 2){
lowerBound = new Vector3d(
this.lowerBound.x,
this.midpoint.y,
this.lowerBound.z
);
upperBound = new Vector3d(
this.midpoint.x,
this.upperBound.y,
this.midpoint.z
);
} else if(quadrant == 3) {
lowerBound = new Vector3d(
this.midpoint.x,
this.midpoint.y,
this.lowerBound.z
);
upperBound = new Vector3d(
this.upperBound.x,
this.upperBound.y,
this.midpoint.z
);
} else if(quadrant == 4){
lowerBound = new Vector3d(
this.lowerBound.x,
this.lowerBound.y,
this.midpoint.z
);
upperBound = new Vector3d(
this.midpoint.x,
this.midpoint.y,
this.upperBound.z
);
} else if(quadrant == 5) {
lowerBound = new Vector3d(
this.midpoint.x,
this.lowerBound.y,
this.midpoint.z
);
upperBound = new Vector3d(
this.upperBound.x,
this.midpoint.y,
this.upperBound.z
);
} else if(quadrant == 6){
lowerBound = new Vector3d(
this.lowerBound.x,
this.midpoint.y,
this.midpoint.z
);
upperBound = new Vector3d(
this.midpoint.x,
this.upperBound.y,
this.upperBound.z
);
} else if(quadrant == 7) {
lowerBound = new Vector3d(this.midpoint);
upperBound = new Vector3d(this.upperBound);
} else {
throw new IllegalArgumentException("Trying to get midpoint of invalid quadrant!" + quadrant);
}
Vector3d midpoint = new Vector3d(
((upperBound.x - lowerBound.x) / 2.0) + lowerBound.x,
((upperBound.y - lowerBound.y) / 2.0) + lowerBound.y,
((upperBound.z - lowerBound.z) / 2.0) + lowerBound.z
);
return midpoint;
}
/**
* Forks a quadrant
* @param location The location within the quadrant
* @return The midpoint of the new node
*/
private OctreeNode<T> forkQuadrant(Vector3d location){
int quadrant = this.getQuadrant(location);
Vector3d midpoint = this.getQuadrantMidpoint(quadrant);
//create and add the non-leaf node
OctreeNode<T> nonLeaf = new OctreeNode<T>(midpoint);
this.addChild(nonLeaf);
return nonLeaf;
}
}
}

View File

@ -0,0 +1,292 @@
package electrosphere.util.ds;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import org.joml.Vector3d;
import annotations.FastTest;
import electrosphere.util.ds.Octree.OctreeNode;
/**
* Unit testing for the octree implementation
*/
public class OctreeTests {
/**
* Creates an octree
*/
@FastTest
public void testCreateOctree(){
new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
}
/**
* Add a single leaf
*/
@FastTest
public void testAddNode(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
assertNull(current.getData());
for(OctreeNode<Integer> child : current.getChildren()){
if(child != null && child.getParent() != current){
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
}
}
}
}
}
/**
* Add two leaves in a line
*/
@FastTest
public void testAddTwo(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
octree.addLeaf(new Vector3d(2,1,1), 2);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
assertNull(current.getData());
for(OctreeNode<Integer> child : current.getChildren()){
if(child != null && child.getParent() != current){
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
}
}
}
}
//
//Specific, expected values to check. Verifies that the octree construct itself correctly
//
OctreeNode<Integer> current = octree.getRoot().getChildren().get(0);
assertEquals(8, current.getLocation().x);
assertEquals(8, current.getLocation().y);
assertEquals(8, current.getLocation().z);
assertTrue(!current.isLeaf());
assertEquals(1, current.getNumChildren());
current = current.getChildren().get(0);
assertEquals(4, current.getLocation().x);
assertEquals(4, current.getLocation().y);
assertEquals(4, current.getLocation().z);
assertTrue(!current.isLeaf());
assertEquals(1, current.getNumChildren());
current = current.getChildren().get(0);
assertEquals(2, current.getLocation().x);
assertEquals(2, current.getLocation().y);
assertEquals(2, current.getLocation().z);
assertTrue(!current.isLeaf());
assertEquals(2, current.getNumChildren());
OctreeNode<Integer> leaf1 = current.getChildren().get(0);
OctreeNode<Integer> leaf2 = current.getChildren().get(1);
assertTrue(leaf1.isLeaf());
assertTrue(leaf2.isLeaf());
}
/**
* Adds a whole bunch of nodes in a line
*/
@FastTest
public void testAddLine(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
for(int i = 0; i < 31; i++){
octree.addLeaf(new Vector3d(i,1,0), i);
}
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
assertNull(current.getData());
for(OctreeNode<Integer> child : current.getChildren()){
if(child != null && child.getParent() != current){
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
}
}
}
}
}
/**
* Get a single leaf
*/
@FastTest
public void testGetLeaf(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
assertNotNull(leaf);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
assertNull(current.getData());
for(OctreeNode<Integer> child : current.getChildren()){
if(child != null && child.getParent() != current){
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
}
}
}
}
}
/**
* Remove a single leaf
*/
@FastTest
public void testRemoveLeaf(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
assertNotNull(leaf);
assertNotNull(leaf.getParent());
octree.removeNode(leaf);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
assertNull(current.getData());
for(OctreeNode<Integer> child : current.getChildren()){
if(child != null && child.getParent() != current){
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
}
}
}
}
}
/**
* Remove a leaf from a more complex tree
*/
@FastTest
public void testRemoveLeaf2(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
octree.addLeaf(new Vector3d(2,1,1), 2);
octree.addLeaf(new Vector3d(3,1,1), 3);
OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(2,1,1));
assertNotNull(leaf);
assertNotNull(leaf.getParent());
//removes 2 & 3, but not 1
octree.removeNode(leaf.getParent());
assertEquals(1, octree.getNumLeaves());
//remove 1
leaf = octree.getLeaf(new Vector3d(1,1,1));
assertNotNull(leaf);
assertNotNull(leaf.getParent());
octree.removeNode(leaf.getParent());
assertEquals(0, octree.getNumLeaves());
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
assertNull(current.getData());
for(OctreeNode<Integer> child : current.getChildren()){
if(child != null && child.getParent() != current){
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
}
}
}
}
}
/**
* Adds lots of (random) points
*/
@FastTest
public void testAddManyPoints(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(256,256,256));
//
//add points
Random rand = new Random();
for(int i = 0; i < 1000; i++){
Vector3d loc = new Vector3d(
rand.nextInt(0, 256),
rand.nextInt(0, 256),
rand.nextInt(0, 256)
);
octree.addLeaf(loc, i);
}
//
//verify at least one point was added
assertEquals(true, octree.getNumLeaves() > 0);
}
}