Add Poisson Disk Sampling to orx-noise
This commit is contained in:
committed by
Edwin Jakobs
parent
e29c670cf3
commit
45c9ca11af
37
orx-noise/src/demo/kotlin/DemoPoissonDiskSampling.kt
Normal file
37
orx-noise/src/demo/kotlin/DemoPoissonDiskSampling.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
import org.openrndr.extra.noise.poissonDiskSampling
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
program {
|
||||||
|
if (System.getProperty("takeScreenshot") == "true") {
|
||||||
|
extend(SingleScreenshot()) {
|
||||||
|
this.outputFile = System.getProperty("screenshotPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var points = poissonDiskSampling(200.0, 200.0, 5.0, 10)
|
||||||
|
|
||||||
|
val rectPoints = points.map { Circle(Vector2(100.0, 100.0) + it, 3.0) }
|
||||||
|
|
||||||
|
points = poissonDiskSampling(200.0, 200.0, 5.0, 10, true) { w: Double, h: Double, v: Vector2 ->
|
||||||
|
Circle(Vector2(w, h) / 2.0, 100.0).contains(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
val circlePoints = points.map { Circle(Vector2(350.0, 100.0) + it, 3.0) }
|
||||||
|
|
||||||
|
extend {
|
||||||
|
drawer.background(ColorRGBa.BLACK)
|
||||||
|
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.fill = ColorRGBa.PINK
|
||||||
|
drawer.circles(rectPoints)
|
||||||
|
drawer.circles(circlePoints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
orx-noise/src/main/kotlin/PoissonDisk.kt
Normal file
121
orx-noise/src/main/kotlin/PoissonDisk.kt
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package org.openrndr.extra.noise
|
||||||
|
|
||||||
|
import org.openrndr.math.Polar
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.math.clamp
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO v2
|
||||||
|
* * Generalize to 3 dimensions
|
||||||
|
*/
|
||||||
|
|
||||||
|
internal const val epsilon = 0.0000001
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a random point distribution on a given area
|
||||||
|
* Each point gets n [tries] at generating the next point
|
||||||
|
* By default the points are generated along the circumference of r + epsilon to the point
|
||||||
|
* They can also be generated on a ring like in the original algorithm from Robert Bridson
|
||||||
|
*
|
||||||
|
* @param width the width of the area
|
||||||
|
* @param height the height of the area
|
||||||
|
* @param r the minimum distance between each point
|
||||||
|
* @param tries number of candidates per point
|
||||||
|
* @param boundsMapper a custom function to check if a point is within bounds
|
||||||
|
* @param randomOnRing generate random points on a ring with an annulus from r to 2r
|
||||||
|
* @return a list of points
|
||||||
|
*/
|
||||||
|
fun poissonDiskSampling(
|
||||||
|
width: Double,
|
||||||
|
height: Double,
|
||||||
|
r: Double,
|
||||||
|
tries: Int = 30,
|
||||||
|
randomOnRing: Boolean = false,
|
||||||
|
boundsMapper: ((w: Double, h: Double, v: Vector2) -> Boolean)? = null
|
||||||
|
): List<Vector2> {
|
||||||
|
val disk = mutableListOf<Vector2>()
|
||||||
|
val queue = mutableListOf<Int>()
|
||||||
|
|
||||||
|
val r2 = r * r
|
||||||
|
val radius = r + epsilon
|
||||||
|
|
||||||
|
val cellSize = r / sqrt(2.0)
|
||||||
|
val rows = ceil(height / cellSize).toInt()
|
||||||
|
val cols = ceil(width / cellSize).toInt()
|
||||||
|
|
||||||
|
val grid = List(rows * cols) { -1 }.toMutableList()
|
||||||
|
|
||||||
|
fun addPoint(v: Vector2) {
|
||||||
|
val x = (v.x / cellSize).fastFloor()
|
||||||
|
val y = (v.y / cellSize).fastFloor()
|
||||||
|
val index = x + y * cols
|
||||||
|
|
||||||
|
disk.add(v)
|
||||||
|
|
||||||
|
grid[index] = disk.lastIndex
|
||||||
|
|
||||||
|
queue.add(disk.lastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
addPoint(Vector2(width / 2.0, height / 2.0))
|
||||||
|
|
||||||
|
val boundsRect = Rectangle(0.0, 0.0, width, height)
|
||||||
|
|
||||||
|
while (queue.isNotEmpty()) {
|
||||||
|
val activeIndex = Random.pick(queue)
|
||||||
|
val active = disk[activeIndex]
|
||||||
|
|
||||||
|
var candidateAccepted = false
|
||||||
|
|
||||||
|
candidateSearch@ for (l in 0 until tries) {
|
||||||
|
val c = if (randomOnRing) {
|
||||||
|
active + Random.ring2d(r, 2 * r) as Vector2
|
||||||
|
} else {
|
||||||
|
active + Polar(Random.double0(360.0), radius).cartesian
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boundsRect.contains(c)) continue@candidateSearch
|
||||||
|
|
||||||
|
// check if it's within bounds
|
||||||
|
// choose another candidate if it's not
|
||||||
|
if (boundsMapper != null && !boundsMapper(width, height, c)) continue@candidateSearch
|
||||||
|
|
||||||
|
val x = (c.x / cellSize).fastFloor()
|
||||||
|
val y = (c.y / cellSize).fastFloor()
|
||||||
|
|
||||||
|
// Check closest neighbours in a 5x5 grid
|
||||||
|
for (ix in (-2..2)) {
|
||||||
|
for (iy in (-2..2)) {
|
||||||
|
val nx = clamp(x + ix, 0, cols - 1)
|
||||||
|
val ny = clamp(y + iy, 0, rows - 1)
|
||||||
|
|
||||||
|
val neighborIdx = grid[nx + ny * cols]
|
||||||
|
|
||||||
|
// -1 means the grid has no sample at that point
|
||||||
|
if (neighborIdx == -1) continue
|
||||||
|
|
||||||
|
val neighbor = disk[neighborIdx]
|
||||||
|
|
||||||
|
// if the candidate is within one of the neighbours radius, try another candidate
|
||||||
|
if ((neighbor - c).squaredLength <= r2) continue@candidateSearch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addPoint(c)
|
||||||
|
|
||||||
|
candidateAccepted = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no candidate was accepted, remove the sample from the active list
|
||||||
|
if (!candidateAccepted) {
|
||||||
|
queue.remove(activeIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return disk
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user