Skip to content

Commit

Permalink
Add Poisson Disk Sampling to orx-noise
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardomatias authored and edwinRNDR committed Apr 25, 2020
1 parent e29c670 commit 45c9ca1
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 0 deletions.
37 changes: 37 additions & 0 deletions orx-noise/src/demo/kotlin/DemoPoissonDiskSampling.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 121 additions & 0 deletions orx-noise/src/main/kotlin/PoissonDisk.kt
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 45c9ca1

Please sign in to comment.