Skip to content

Commit

Permalink
[orx-hash-grid] Add HashGrid3D
Browse files Browse the repository at this point in the history
  • Loading branch information
edwinRNDR committed May 13, 2024
1 parent 0a4e3fb commit 334a0c6
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 1 deletion.
2 changes: 2 additions & 0 deletions orx-hash-grid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ kotlin {
implementation(project(":orx-color"))
implementation(project(":orx-fx"))
implementation(project(":orx-noise"))
implementation(project(":orx-camera"))
implementation(project(":orx-mesh-generators"))
}
}

Expand Down
9 changes: 9 additions & 0 deletions orx-hash-grid/src/commonMain/kotlin/Box.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.openrndr.extra.hashgrid

import org.openrndr.math.Vector3

data class Box3D(val corner: Vector3, val width: Double, val height: Double, val depth: Double) {
companion object {
val EMPTY = Box3D(Vector3.ZERO, 0.0, 0.0, 0.0)
}
}
174 changes: 174 additions & 0 deletions orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.openrndr.extra.hashgrid
import org.openrndr.math.Vector3
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
import kotlin.random.Random

private fun Double.fastFloor(): Int {
return if (this >= 0) this.toInt() else this.toInt() - 1
}

private data class GridCoords3D(val x: Int, val y: Int, val z: Int) {
fun offset(i: Int, j: Int, k : Int): GridCoords3D = copy(x = x + i, y = y + j, z = z + k)
}

class Cell3D(val x: Int, val y: Int, val z: Int, val cellSize: Double) {
var xMin: Double = Double.POSITIVE_INFINITY
private set
var xMax: Double = Double.NEGATIVE_INFINITY
private set
var yMin: Double = Double.POSITIVE_INFINITY
private set
var yMax: Double = Double.NEGATIVE_INFINITY
private set
var zMin: Double = Double.POSITIVE_INFINITY
private set
var zMax: Double = Double.NEGATIVE_INFINITY
private set

val bounds: Box3D
get() {
return Box3D(Vector3(x * cellSize, y * cellSize, z * cellSize), cellSize, cellSize, cellSize)
}

val contentBounds: Box3D
get() {
return if (points.isEmpty()) {
Box3D.EMPTY
} else {
Box3D(Vector3(xMin, yMin, zMin), xMax - xMin, yMax - yMin, zMax - zMin)
}
}

internal val points = mutableListOf<Pair<Vector3, Any?>>()
internal fun insert(point: Vector3, owner: Any?) {
points.add(Pair(point, owner))
xMin = min(xMin, point.x)
xMax = max(xMax, point.x)
yMin = min(yMin, point.y)
yMax = max(yMax, point.y)
zMin = min(zMin, point.z)
zMax = max(zMax, point.z)
}

internal fun squaredDistanceTo(query: Vector3): Double {
val width = xMax - xMin
val height = yMax - yMin
val depth = zMax - zMin
val x = (xMin + xMax) / 2.0
val y = (yMin + yMax) / 2.0
val z = (zMin + zMax) / 2.0
val dx = max(abs(query.x - x) - width / 2, 0.0)
val dy = max(abs(query.y - y) - height / 2, 0.0)
val dz = max(abs(query.z - z) - depth / 2, 0.0)
return dx * dx + dy * dy + dz * dz
}

fun points() = sequence {
for (point in points) {
yield(point)
}
}
}

class HashGrid3D(val radius: Double) {
private val cells = mutableMapOf<GridCoords3D, Cell3D>()
fun cells() = sequence {
for (cell in cells.values) {
yield(cell)
}
}

var size: Int = 0
private set

val cellSize = radius / sqrt(3.0)
private inline fun coords(v: Vector3): GridCoords3D {
val x = (v.x / cellSize).fastFloor()
val y = (v.y / cellSize).fastFloor()
val z = (v.z / cellSize).fastFloor()
return GridCoords3D(x, y, z)
}

fun points() = sequence {
for (cell in cells.values) {
for (point in cell.points) {
yield(point)
}
}
}

fun random(random: Random = Random.Default): Vector3 {
return cells.values.random(random).points.random().first
}

fun insert(point: Vector3, owner: Any? = null) {
val gc = coords(point)
val cell = cells.getOrPut(gc) { Cell3D(gc.x, gc.y, gc.z, cellSize) }
cell.insert(point, owner)
size += 1
}

fun cell(query: Vector3): Cell3D? = cells[coords(query)]

fun isFree(query: Vector3, ignoreOwners: Set<Any> = emptySet()): Boolean {
val c = coords(query)
if (cells[c] == null) {
for (k in -2..2) {
for (j in -2..2) {
for (i in -2..2) {
if (i == 0 && j == 0 && k == 0) {
continue
}
val n = c.offset(i, j, k)
val nc = cells[n]
if (nc != null && nc.squaredDistanceTo(query) <= radius * radius) {
for (p in nc.points) {
if (p.second == null || p.second !in ignoreOwners) {
if (p.first.squaredDistanceTo(query) <= radius * radius) {
return false
}
}
}
}
}
}
}
return true
} else {
return cells[c]!!.points.all { it.second != null && it.second in ignoreOwners }
}
}
}

/**
* Construct a hash grid containing all points in the list
* @param radius radius of the hash grid
*/
fun List<Vector3>.hashGrid(radius: Double): HashGrid3D {
val grid = HashGrid3D(radius)
for (point in this) {
grid.insert(point)
}
return grid
}

/**
* Return a list that only contains points at a minimum distance.
* @param radius the minimum distance between any two points in the returned list
*/
fun List<Vector3>.filter(radius: Double): List<Vector3> {
return if (size <= 1) {
this
} else {
val grid = HashGrid3D(radius)
for (point in this) {
if (grid.isFree(point)) {
grid.insert(point)
}
}
grid.points().map { it.first }.toList()
}
}
2 changes: 1 addition & 1 deletion orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fun main() {
}
val filteredPoints = points.filter(20.0)
extend {
drawer.circles(filteredPoints, 4.0)
drawer.circles(filteredPoints, 10.0)
}
}
}
Expand Down
42 changes: 42 additions & 0 deletions orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.isolated
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.hashgrid.filter
import org.openrndr.extra.meshgenerators.sphereMesh
import org.openrndr.extra.noise.uniformRing
import org.openrndr.math.Vector3
import kotlin.random.Random

fun main() = application {
configure {
width = 720
height = 720
multisample = WindowMultisample.SampleCount(4)
}
program {
val r = Random(0)
val points = (0 until 10000).map {
Vector3.uniformRing(0.0, 10.0, r)
}
val sphere = sphereMesh(radius = 0.25)
val filteredPoints = points.filter(0.5)

extend(Orbital()) {
eye = Vector3(0.0, 0.0, 15.0)
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """x_fill.rgb *= abs(v_viewNormal.z);"""
}
for (point in filteredPoints) {
drawer.isolated {
drawer.translate(point)
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
}
}
}
}
}

0 comments on commit 334a0c6

Please sign in to comment.