[orx-noise] Improve ShapeProvider.poissonDiskSampling by allowing multiple initialPoints. Boost poissonDiskSampling performance
This commit is contained in:
@@ -27,6 +27,7 @@ internal const val epsilon = 0.0000001
|
|||||||
* @param tries number of candidates per point
|
* @param tries number of candidates per point
|
||||||
* @param randomOnRing generate random points on a ring with an annulus from r to 2r
|
* @param randomOnRing generate random points on a ring with an annulus from r to 2r
|
||||||
* @param random a random number generator, default value is [Random.Default]
|
* @param random a random number generator, default value is [Random.Default]
|
||||||
|
* @param initialPoints a list of points in sampler space, these points will not be tested against [r]
|
||||||
* @param boundsMapper a custom function to check if a point is within bounds
|
* @param boundsMapper a custom function to check if a point is within bounds
|
||||||
|
|
||||||
* @return a list of points
|
* @return a list of points
|
||||||
@@ -38,7 +39,7 @@ fun poissonDiskSampling(
|
|||||||
tries: Int = 30,
|
tries: Int = 30,
|
||||||
randomOnRing: Boolean = false,
|
randomOnRing: Boolean = false,
|
||||||
random: Random = Random.Default,
|
random: Random = Random.Default,
|
||||||
initialPoint: Vector2 = Vector2(width/2.0, height/2.0),
|
initialPoints: List<Vector2> = listOf(Vector2(width/2.0, height/2.0)),
|
||||||
boundsMapper: ((w: Double, h: Double, v: Vector2) -> Boolean)? = null,
|
boundsMapper: ((w: Double, h: Double, v: Vector2) -> Boolean)? = null,
|
||||||
): List<Vector2> {
|
): List<Vector2> {
|
||||||
val disk = mutableListOf<Vector2>()
|
val disk = mutableListOf<Vector2>()
|
||||||
@@ -51,21 +52,23 @@ fun poissonDiskSampling(
|
|||||||
val rows = ceil(height / cellSize).toInt()
|
val rows = ceil(height / cellSize).toInt()
|
||||||
val cols = ceil(width / cellSize).toInt()
|
val cols = ceil(width / cellSize).toInt()
|
||||||
|
|
||||||
val grid = MutableList(rows * cols) { -1 }
|
val grid = Array(rows * cols) { -1 }
|
||||||
|
|
||||||
fun addPoint(v: Vector2) {
|
fun addPoint(v: Vector2) {
|
||||||
val x = (v.x / cellSize).fastFloor()
|
val x = (v.x / cellSize).fastFloor()
|
||||||
val y = (v.y / cellSize).fastFloor()
|
val y = (v.y / cellSize).fastFloor()
|
||||||
val index = x + y * cols
|
val index = x + y * cols
|
||||||
|
|
||||||
disk.add(v)
|
if (x >= 0 && y >= 0 && x < cols && y < rows) {
|
||||||
|
disk.add(v)
|
||||||
grid[index] = disk.lastIndex
|
grid[index] = disk.lastIndex
|
||||||
|
queue.add(disk.lastIndex)
|
||||||
queue.add(disk.lastIndex)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addPoint(initialPoint)
|
for (initialPoint in initialPoints) {
|
||||||
|
addPoint(initialPoint)
|
||||||
|
}
|
||||||
|
|
||||||
val boundsRect = Rectangle(0.0, 0.0, width, height)
|
val boundsRect = Rectangle(0.0, 0.0, width, height)
|
||||||
|
|
||||||
@@ -81,19 +84,20 @@ fun poissonDiskSampling(
|
|||||||
} else {
|
} else {
|
||||||
active + Polar(random.nextDouble(0.0, 360.0), radius).cartesian
|
active + Polar(random.nextDouble(0.0, 360.0), radius).cartesian
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!boundsRect.contains(c)) continue@candidateSearch
|
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 x = (c.x / cellSize).fastFloor()
|
||||||
val y = (c.y / cellSize).fastFloor()
|
val y = (c.y / cellSize).fastFloor()
|
||||||
|
|
||||||
|
// EJ: early bail-out;
|
||||||
|
// if grid[y,x] is populated we know that its inhabitant is within the minimum point distance
|
||||||
|
if (grid[x + y * cols] != -1) {
|
||||||
|
continue@candidateSearch
|
||||||
|
}
|
||||||
|
|
||||||
// Check closest neighbours in a 5x5 grid
|
// Check closest neighbours in a 5x5 grid
|
||||||
for (ix in (-2..2)) {
|
for (iy in (-2..2)) {
|
||||||
for (iy in (-2..2)) {
|
for (ix in (-2..2)) {
|
||||||
val nx = clamp(x + ix, 0, cols - 1)
|
val nx = clamp(x + ix, 0, cols - 1)
|
||||||
val ny = clamp(y + iy, 0, rows - 1)
|
val ny = clamp(y + iy, 0, rows - 1)
|
||||||
|
|
||||||
@@ -109,10 +113,15 @@ fun poissonDiskSampling(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if the candidate point is within bounds
|
||||||
|
// EJ: This is somewhat counter-intuitively moved to the last stage in the process;
|
||||||
|
// It turns out that the above neighbour search is much more affordable than the bounds check in the
|
||||||
|
// case of complex bounds (such as described by Shapes or ShapeContours). A simple benchmark shows a
|
||||||
|
// speed-up of roughly 300%
|
||||||
|
if (boundsMapper != null && !boundsMapper(width, height, c)) continue@candidateSearch
|
||||||
|
|
||||||
addPoint(c)
|
addPoint(c)
|
||||||
|
|
||||||
candidateAccepted = true
|
candidateAccepted = true
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +130,6 @@ fun poissonDiskSampling(
|
|||||||
queue.remove(activeIndex)
|
queue.remove(activeIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return disk
|
return disk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,72 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.shape.*
|
import org.openrndr.shape.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
fun ShapeProvider.uniform(random: Random = Random.Default): Vector2 {
|
fun ShapeProvider.uniform(distanceToEdge: Double = 0.0, random: Random = Random.Default): Vector2 {
|
||||||
val shape = shape
|
val shape = shape
|
||||||
|
require(!shape.empty)
|
||||||
|
var attempts = 0
|
||||||
return Vector2.uniformSequence(shape.bounds, random).first {
|
return Vector2.uniformSequence(shape.bounds, random).first {
|
||||||
shape.contains(it)
|
attempts++
|
||||||
|
require(attempts < 100)
|
||||||
|
if (distanceToEdge == 0.0) {
|
||||||
|
shape.contains(it)
|
||||||
|
} else {
|
||||||
|
shape.contains(it) && shape.contours.minOf { c -> c.nearest(it).position.distanceTo(it) } > distanceToEdge
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ShapeProvider.poissonDiskSampling(
|
fun ShapeProvider.poissonDiskSampling(
|
||||||
r: Double,
|
pointDistance: Double,
|
||||||
|
distanceToEdge: Double = 0.0,
|
||||||
tries: Int = 30,
|
tries: Int = 30,
|
||||||
random: Random = Random.Default
|
random: Random = Random.Default
|
||||||
): List<Vector2> {
|
): List<Vector2> {
|
||||||
val shape = shape
|
val shape = shape
|
||||||
|
if (shape.empty) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
val bounds = shape.bounds
|
val bounds = shape.bounds
|
||||||
val poissonBounds = Rectangle(0.0, 0.0, bounds.width, bounds.height)
|
val poissonBounds = Rectangle(0.0, 0.0, bounds.width, bounds.height)
|
||||||
|
|
||||||
val initialPoint = this.uniform(random).map(bounds, poissonBounds)
|
val initialPoints = shape.splitCompounds().flatMap { compound ->
|
||||||
|
compound.outline.segments.map {
|
||||||
|
val t = random.nextDouble()
|
||||||
|
(it.position(t) - it.normal(t).normalized * distanceToEdge)
|
||||||
|
}.filter { compound.contains(it) && compound.outline.nearest(it).position.distanceTo(it) >= distanceToEdge-1E-1 }.map {
|
||||||
|
it.map(bounds, poissonBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return poissonDiskSampling(bounds.width, bounds.height, r, tries, false, random, initialPoint) { _, _, point ->
|
val candidatePoints = mutableListOf<Vector2>()
|
||||||
|
for (point in initialPoints) {
|
||||||
|
if ((candidatePoints.map { it.distanceTo(point) }.minOrNull() ?: Double.POSITIVE_INFINITY) >= pointDistance) {
|
||||||
|
candidatePoints.add(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (candidatePoints.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return poissonDiskSampling(
|
||||||
|
bounds.width,
|
||||||
|
bounds.height,
|
||||||
|
pointDistance,
|
||||||
|
tries,
|
||||||
|
false,
|
||||||
|
random,
|
||||||
|
candidatePoints,
|
||||||
|
) { _, _, point ->
|
||||||
val contourPoint = point.map(poissonBounds, bounds)
|
val contourPoint = point.map(poissonBounds, bounds)
|
||||||
shape.contains(contourPoint)
|
if (distanceToEdge == 0.0) {
|
||||||
|
shape.contains(contourPoint)
|
||||||
|
} else {
|
||||||
|
shape.contains(contourPoint) && shape.contours.minOf { c ->
|
||||||
|
c.nearest(contourPoint).position.distanceTo(contourPoint)
|
||||||
|
} > distanceToEdge
|
||||||
|
}
|
||||||
}.map {
|
}.map {
|
||||||
it.map(poissonBounds, bounds)
|
it.map(poissonBounds, bounds)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user