[orx-quadtree] add quadtree module (#174)

This commit is contained in:
Ricardo Matias
2021-02-26 17:51:03 +01:00
committed by GitHub
parent 8d6b82a24e
commit 5b0a6f37e0
7 changed files with 395 additions and 1 deletions

22
orx-quadtree/README.md Normal file
View File

@@ -0,0 +1,22 @@
# orx-quadtree
An extension for creating a [Quadtree](https://en.wikipedia.org/wiki/Quadtree) for points. A quadtree is a spatial
partioning tree structure meant to provide fast spatial queries such as nearest points within a range.
## Example
```kotlin
val box = Rectangle.fromCenter(Vector2(400.0), 750.0)
val quadTree = Quadtree<Vector2>(box) { it }
for (point in points) {
quadTree.insert(point)
}
val nearestQuery = quadTree.nearest(points[4], 20.0)
```
### Author
Ricardo Matias / [@ricardomatias](https://github.com/ricardomatias)

19
orx-quadtree/build.gradle Normal file
View File

@@ -0,0 +1,19 @@
sourceSets {
demo {
java {
srcDirs = ["src/demo/kotlin"]
compileClasspath += main.getCompileClasspath()
runtimeClasspath += main.getRuntimeClasspath()
}
}
}
dependencies {
demoImplementation("org.openrndr:openrndr-core:$openrndrVersion")
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
demoImplementation(project(":orx-noise"))
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
demoImplementation(sourceSets.getByName("main").output)
}

View File

@@ -0,0 +1,55 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.rectangleBatch
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.gaussian
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import quadtree.Quadtree
fun main() {
application {
configure {
width = 800
height = 800
title = "QuadTree"
}
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val box = Rectangle.fromCenter(Vector2(400.0), 750.0)
val points = (0 until 1_000).map {
Vector2.gaussian(box.center, Vector2(95.0), Random.rnd)
}
val quadTree = Quadtree<Vector2>(box) { it }
for (point in points) {
quadTree.insert(point)
}
val batch = drawer.rectangleBatch {
this.fill = null
this.stroke = ColorRGBa.GRAY
this.strokeWeight = 0.5
quadTree.batch(this)
}
extend {
drawer.clear(ColorRGBa.BLACK)
drawer.rectangles(batch)
drawer.fill = ColorRGBa.PINK.opacify(0.7)
drawer.stroke = null
drawer.circles(points, 5.0)
}
}
}
}

View File

@@ -0,0 +1,82 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.rectangleBatch
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.gaussian
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import quadtree.Quadtree
fun main() {
application {
configure {
width = 800
height = 800
title = "QuadTree"
}
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val box = Rectangle.fromCenter(Vector2(400.0), 750.0)
val points = (0 until 100).map {
Vector2.gaussian(box.center, Vector2(95.0), Random.rnd)
}
val quadTree = Quadtree<Vector2>(box) { it }
for (point in points) {
quadTree.insert(point)
}
val selected = points[3]
val radius = 40.0
val nearestQuery = quadTree.nearest(selected, radius)
val batch = drawer.rectangleBatch {
this.fill = null
this.stroke = ColorRGBa.GRAY
this.strokeWeight = 0.5
quadTree.batch(this)
}
extend {
drawer.clear(ColorRGBa.BLACK)
drawer.rectangles(batch)
drawer.fill = ColorRGBa.PINK.opacify(0.7)
drawer.stroke = null
drawer.circles(points, 5.0)
nearestQuery?.let { (nearest, neighbours, nodes) ->
drawer.stroke = null
drawer.fill = ColorRGBa.YELLOW.opacify(0.2)
for (node in nodes) {
node.draw(drawer)
}
drawer.fill = ColorRGBa.GREEN.opacify(0.7)
drawer.circles(neighbours, 5.0)
drawer.fill = ColorRGBa.RED.opacify(0.9)
drawer.circle(nearest, 5.0)
drawer.fill = ColorRGBa.PINK
drawer.circle(selected, 5.0)
drawer.stroke = ColorRGBa.PINK
drawer.fill = null
drawer.circle(selected, radius)
}
}
}
}
}

View File

@@ -0,0 +1,215 @@
package quadtree
import org.openrndr.draw.Drawer
import org.openrndr.draw.RectangleBatchBuilder
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import org.openrndr.shape.intersects
data class QuadtreeQuery<T>(val nearest: T, val neighbours: List<T>, val quads: List<Quadtree<T>>)
/**
* Quadtree
*
* @param T
* @property bounds the tree's bounding box
* @property maxObjects maximum number of objects per node
* @property mapper
*/
class Quadtree<T>(val bounds: Rectangle, val maxObjects: Int = 10, val mapper: ((T) -> Vector2)) {
/**
* The 4 nodes of the tree
*/
val nodes = arrayOfNulls<Quadtree<T>>(4)
var depth = 0
val objects = mutableListOf<T>()
private val isLeaf: Boolean
get() = nodes[0] == null
/**
* Clears the whole tree
*/
fun clear() {
objects.clear()
for (i in nodes.indices) {
nodes[i]?.let {
it.clear()
nodes[i] = null
}
}
}
/**
* Finds the nearest and neighbouring points within a radius
*
* @param element
* @param radius
* @return
*/
fun nearest(element: T, radius: Double): QuadtreeQuery<T>? {
val point = mapper(element)
if (!bounds.contains(point)) return null
val r2 = radius * radius
val scaledBounds = Rectangle.fromCenter(point, radius * 2)
val intersected: List<Quadtree<T>> = intersect(scaledBounds) ?: return null
var minDist = Double.MAX_VALUE
val nearestObjects = mutableListOf<T>()
var nearestObject: T? = null
for (interNode in intersected) {
for (obj in interNode.objects) {
if (element == obj) continue
val p = mapper(obj)
val dist = p.squaredDistanceTo(point)
if (dist < r2) {
nearestObjects.add(obj)
if (dist < minDist) {
minDist = dist
nearestObject = obj
}
}
}
}
if (nearestObject == null) return null
return QuadtreeQuery(nearestObject, nearestObjects, intersected)
}
/**
* Inserts the element in the appropriate node
*
* @param element
* @return
*/
fun insert(element: T): Boolean {
// only* the root needs to check this
if (depth == 0) {
if (!bounds.contains(mapper(element))) return false
}
if ((objects.size < maxObjects && isLeaf)) {
objects.add(element)
return true
}
if (isLeaf) subdivide()
objects.add(element)
for (obj in objects) {
val p = mapper(obj)
val x = if (p.x > bounds.center.x) 1 else 0
val y = if (p.y > bounds.center.y) 1 else 0
val nodeIndex = x + y * 2
nodes[nodeIndex]?.insert(obj)
}
objects.clear()
return true
}
/**
* Finds which node the element is within (but not necessarily belonging to)
*
* @param element
* @return
*/
fun findNode(element: T): Quadtree<T>? {
val v = mapper(element)
if (!bounds.contains(v)) return null
if (isLeaf) return this
for (node in nodes) {
node?.findNode(element)?.let { return it }
}
return null
}
/**
* Draw the quadtree using batching
*
* @param batchBuilder
*/
fun batch(batchBuilder: RectangleBatchBuilder) {
batchBuilder.rectangle(bounds)
for (node in nodes) {
node?.batch(batchBuilder)
}
}
/**
* Draw the quadtree
*
* @param drawer
*/
fun draw(drawer: Drawer) {
drawer.rectangle(bounds)
for (node in nodes) {
node?.draw(drawer)
}
}
private fun intersect(rect: Rectangle): List<Quadtree<T>>? {
val intersects = intersects(bounds, rect)
if (!intersects) return null
if (isLeaf) return listOf(this)
val intersected = mutableListOf<Quadtree<T>>()
for (node in nodes) {
if (node != null) node.intersect(rect)?.let {
intersected.addAll(it)
}
}
return intersected
}
private fun subdivide() {
val width = bounds.center.x - bounds.corner.x
val height = bounds.center.y - bounds.corner.y
val newDepth = depth + 1
var node = Quadtree(Rectangle(bounds.corner, width, height), maxObjects, mapper)
node.depth = newDepth
nodes[0] = node
node = Quadtree(Rectangle(Vector2(bounds.center.x, bounds.corner.y), width, height), maxObjects, mapper)
node.depth = newDepth
nodes[1] = node
node = Quadtree(Rectangle(Vector2(bounds.corner.x, bounds.center.y), width, height), maxObjects, mapper)
node.depth = newDepth
nodes[2] = node
node = Quadtree(Rectangle(bounds.center, width, height), maxObjects, mapper)
node.depth = newDepth
nodes[3] = node
}
override fun toString(): String {
return "QuadTree { objects: ${objects.size}, depth: $depth, isLeaf: $isLeaf"
}
}