Add Poisson Disk Sampling to orx-noise

This commit is contained in:
Ricardo Matias
2020-04-25 11:27:45 +02:00
committed by Edwin Jakobs
parent e29c670cf3
commit 45c9ca11af
2 changed files with 158 additions and 0 deletions

View 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)
}
}
}
}

View 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
}