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