diff --git a/orx-hash-grid/build.gradle.kts b/orx-hash-grid/build.gradle.kts index bae8b4a1..68661c87 100644 --- a/orx-hash-grid/build.gradle.kts +++ b/orx-hash-grid/build.gradle.kts @@ -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")) } } diff --git a/orx-hash-grid/src/commonMain/kotlin/Box.kt b/orx-hash-grid/src/commonMain/kotlin/Box.kt new file mode 100644 index 00000000..ffa75b0f --- /dev/null +++ b/orx-hash-grid/src/commonMain/kotlin/Box.kt @@ -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) + } +} \ No newline at end of file diff --git a/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt b/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt new file mode 100644 index 00000000..664af03b --- /dev/null +++ b/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt @@ -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>() + 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() + 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 = 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.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.filter(radius: Double): List { + 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() + } +} \ No newline at end of file diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt index 880b2509..59acc24d 100644 --- a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt @@ -16,7 +16,7 @@ fun main() { } val filteredPoints = points.filter(20.0) extend { - drawer.circles(filteredPoints, 4.0) + drawer.circles(filteredPoints, 10.0) } } } diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt new file mode 100644 index 00000000..e2e21625 --- /dev/null +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt @@ -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) + } + } + } + } +} \ No newline at end of file