[orx-noise] Switch poissonDiskSampling to use HashGrid, add multiScatter
This commit is contained in:
@@ -23,6 +23,7 @@ kotlin {
|
|||||||
kotlin.srcDir("src/demo")
|
kotlin.srcDir("src/demo")
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":orx-camera"))
|
implementation(project(":orx-camera"))
|
||||||
|
implementation(project(":orx-hash-grid"))
|
||||||
implementation("org.openrndr:openrndr-application:$openrndrVersion")
|
implementation("org.openrndr:openrndr-application:$openrndrVersion")
|
||||||
implementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
implementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
||||||
runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||||
@@ -56,6 +57,8 @@ kotlin {
|
|||||||
implementation("org.openrndr:openrndr-math:$openrndrVersion")
|
implementation("org.openrndr:openrndr-math:$openrndrVersion")
|
||||||
implementation("org.openrndr:openrndr-shape:$openrndrVersion")
|
implementation("org.openrndr:openrndr-shape:$openrndrVersion")
|
||||||
implementation("org.openrndr:openrndr-draw:$openrndrVersion")
|
implementation("org.openrndr:openrndr-draw:$openrndrVersion")
|
||||||
|
implementation(project(":orx-hash-grid"))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package org.openrndr.extra.noise
|
package org.openrndr.extra.noise
|
||||||
|
|
||||||
|
import org.openrndr.extra.hashgrid.HashGrid
|
||||||
import org.openrndr.math.Polar
|
import org.openrndr.math.Polar
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.math.clamp
|
|
||||||
import org.openrndr.shape.Rectangle
|
import org.openrndr.shape.Rectangle
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -21,8 +19,7 @@ internal const val epsilon = 0.0000001
|
|||||||
* By default the points are generated along the circumference of r + epsilon to the 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
|
* They can also be generated on a ring like in the original algorithm from Robert Bridson
|
||||||
*
|
*
|
||||||
* @param width the width of the area
|
* @param bounds the rectangular bounds of the area to generate points in
|
||||||
* @param height the height of the area
|
|
||||||
* @param r the minimum distance between each point
|
* @param r the minimum distance between each point
|
||||||
* @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
|
||||||
@@ -33,101 +30,65 @@ internal const val epsilon = 0.0000001
|
|||||||
* @return a list of points
|
* @return a list of points
|
||||||
*/
|
*/
|
||||||
fun poissonDiskSampling(
|
fun poissonDiskSampling(
|
||||||
width: Double,
|
bounds: Rectangle,
|
||||||
height: Double,
|
radius: Double,
|
||||||
r: Double,
|
tries: Int = 30,
|
||||||
tries: Int = 30,
|
randomOnRing: Boolean = true,
|
||||||
randomOnRing: Boolean = false,
|
random: Random = Random.Default,
|
||||||
random: Random = Random.Default,
|
initialPoints: List<Vector2> = listOf(bounds.center),
|
||||||
initialPoints: List<Vector2> = listOf(Vector2(width/2.0, height/2.0)),
|
obstacleHashGrids: List<HashGrid> = emptyList(),
|
||||||
boundsMapper: ((w: Double, h: Double, v: Vector2) -> Boolean)? = null,
|
boundsMapper: ((v: Vector2) -> Boolean)? = null,
|
||||||
): List<Vector2> {
|
): List<Vector2> {
|
||||||
val disk = mutableListOf<Vector2>()
|
val disk = mutableListOf<Vector2>()
|
||||||
val queue = mutableListOf<Int>()
|
val queue = mutableSetOf<Pair<Vector2, Double>>()
|
||||||
|
val hashGrid = HashGrid(radius)
|
||||||
|
|
||||||
val r2 = r * r
|
fun addPoint(v: Vector2, radius: Double) {
|
||||||
val radius = r + epsilon
|
hashGrid.insert(v)
|
||||||
|
disk.add(v)
|
||||||
val cellSize = r / sqrt(2.0)
|
queue.add(Pair(v, radius))
|
||||||
val rows = ceil(height / cellSize).toInt()
|
|
||||||
val cols = ceil(width / cellSize).toInt()
|
|
||||||
|
|
||||||
val grid = Array(rows * cols) { -1 }
|
|
||||||
|
|
||||||
fun addPoint(v: Vector2) {
|
|
||||||
val x = (v.x / cellSize).fastFloor()
|
|
||||||
val y = (v.y / cellSize).fastFloor()
|
|
||||||
val index = x + y * cols
|
|
||||||
|
|
||||||
if (x >= 0 && y >= 0 && x < cols && y < rows) {
|
|
||||||
disk.add(v)
|
|
||||||
grid[index] = disk.lastIndex
|
|
||||||
queue.add(disk.lastIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (initialPoint in initialPoints) {
|
for (initialPoint in initialPoints) {
|
||||||
addPoint(initialPoint)
|
addPoint(initialPoint, radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
val boundsRect = Rectangle(0.0, 0.0, width, height)
|
for (ohg in obstacleHashGrids) {
|
||||||
|
for (point in ohg.points()) {
|
||||||
|
queue.add(Pair(point, ohg.radius))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (queue.isNotEmpty()) {
|
while (queue.isNotEmpty()) {
|
||||||
val activeIndex = queue.random(random)
|
val queueItem = queue.random(random)
|
||||||
val active = disk[activeIndex]
|
val (active, activeRadius) = queueItem
|
||||||
|
|
||||||
var candidateAccepted = false
|
var candidateAccepted = false
|
||||||
|
|
||||||
candidateSearch@ for (l in 0 until tries) {
|
candidateSearch@ for (l in 0 until tries) {
|
||||||
val c = if (randomOnRing) {
|
val c = if (randomOnRing) {
|
||||||
active + Vector2.uniformRing(r, 2 * r, random)
|
active + Vector2.uniformRing(activeRadius, 2 * activeRadius- epsilon, random)
|
||||||
} else {
|
} else {
|
||||||
active + Polar(random.nextDouble(0.0, 360.0), radius).cartesian
|
active + Polar(random.nextDouble(0.0, 360.0), activeRadius).cartesian
|
||||||
}
|
}
|
||||||
if (!boundsRect.contains(c)) continue@candidateSearch
|
if (!bounds.contains(c)) continue@candidateSearch
|
||||||
|
|
||||||
val x = (c.x / cellSize).fastFloor()
|
if (!hashGrid.isFree(c) || obstacleHashGrids.any { !it.isFree(c) })
|
||||||
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
|
continue@candidateSearch
|
||||||
}
|
|
||||||
|
|
||||||
// Check closest neighbours in a 5x5 grid
|
|
||||||
for (iy in (-2..2)) {
|
|
||||||
for (ix 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the candidate point is within bounds
|
// check if the candidate point is within bounds
|
||||||
// EJ: This is somewhat counter-intuitively moved to the last stage in the process;
|
// 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
|
// 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
|
// case of complex bounds (such as described by Shapes or ShapeContours). A simple benchmark shows a
|
||||||
// speed-up of roughly 300%
|
// speed-up of roughly 300%
|
||||||
if (boundsMapper != null && !boundsMapper(width, height, c)) continue@candidateSearch
|
if (boundsMapper != null && !boundsMapper(c)) continue@candidateSearch
|
||||||
|
|
||||||
addPoint(c)
|
addPoint(c, radius)
|
||||||
candidateAccepted = true
|
candidateAccepted = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no candidate was accepted, remove the sample from the active list
|
// If no candidate was accepted, remove the sample from the active list
|
||||||
if (!candidateAccepted) {
|
if (!candidateAccepted) {
|
||||||
queue.remove(activeIndex)
|
queue.remove(queueItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return disk
|
return disk
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.openrndr.extra.noise
|
package org.openrndr.extra.noise
|
||||||
|
|
||||||
|
import org.openrndr.extra.hashgrid.HashGrid
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.shape.*
|
import org.openrndr.shape.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -19,58 +20,98 @@ fun ShapeProvider.uniform(distanceToEdge: Double = 0.0, random: Random = Random.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ShapeProvider.scatter(
|
fun ShapeProvider.multiScatter(
|
||||||
pointDistance: Double,
|
radii: List<Pair<Double, Double>>,
|
||||||
distanceToEdge: Double = 0.0,
|
distanceToEdge: Double = 0.0,
|
||||||
tries: Int = 30,
|
tries: Int = 30,
|
||||||
random: Random = Random.Default
|
random: Random = Random.Default
|
||||||
|
) : List<Pair<Double, List<Vector2>>> {
|
||||||
|
|
||||||
|
val obstacles = mutableListOf<Pair<Double, List<Vector2>>>()
|
||||||
|
val result = mutableListOf<Pair<Double, List<Vector2>>>()
|
||||||
|
for ((placementRadius, objectRadius) in radii) {
|
||||||
|
val points = scatter(placementRadius, objectRadius, distanceToEdge, tries, obstacles, random)
|
||||||
|
obstacles.add(Pair(objectRadius, points))
|
||||||
|
result.add(Pair(objectRadius, points))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun ShapeProvider.scatter(
|
||||||
|
placementRadius: Double,
|
||||||
|
objectRadius: Double = placementRadius,
|
||||||
|
distanceToEdge: Double = 0.0,
|
||||||
|
tries: Int = 30,
|
||||||
|
obstacles: List<Pair<Double, List<Vector2>>> = emptyList(),
|
||||||
|
random: Random = Random.Default
|
||||||
): List<Vector2> {
|
): List<Vector2> {
|
||||||
val shape = shape
|
val shape = shape
|
||||||
if (shape.empty) {
|
if (shape.empty) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
val bounds = shape.bounds
|
val bounds = shape.bounds
|
||||||
val poissonBounds = Rectangle(0.0, 0.0, bounds.width, bounds.height)
|
|
||||||
|
|
||||||
val initialPoints = shape.splitCompounds().flatMap { compound ->
|
val obstacleHashGrids = obstacles.map { (obstacleRadius, points) ->
|
||||||
compound.outline.segments.map {
|
val hg = HashGrid(obstacleRadius + objectRadius)
|
||||||
|
for (point in points) {
|
||||||
|
hg.insert(point)
|
||||||
|
}
|
||||||
|
hg
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Segment.randomPoints(count: Int) = sequence {
|
||||||
|
for (i in 0 until count) {
|
||||||
val t = random.nextDouble()
|
val t = random.nextDouble()
|
||||||
(it.position(t) - it.normal(t).normalized * distanceToEdge)
|
yield(position(t) - normal(t).normalized * distanceToEdge)
|
||||||
}.filter { compound.contains(it) && compound.outline.nearest(it).position.distanceTo(it) >= distanceToEdge-1E-1 }.map {
|
|
||||||
it.map(bounds, poissonBounds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val initialPointHashGrid = HashGrid(placementRadius)
|
||||||
|
val initialPoints = shape.splitCompounds().flatMap { compound ->
|
||||||
|
compound.outline.segments.mapNotNull {
|
||||||
|
val point = it.randomPoints(20).firstOrNull { v ->
|
||||||
|
obstacleHashGrids.all { ohg -> ohg.isFree(v) } &&
|
||||||
|
initialPointHashGrid.isFree(v) &&
|
||||||
|
compound.contains(v) &&
|
||||||
|
compound.outline.nearest(v).position.distanceTo(v) >= distanceToEdge - 1E-1
|
||||||
|
}
|
||||||
|
if (point != null) {
|
||||||
|
initialPointHashGrid.insert(point)
|
||||||
|
}
|
||||||
|
point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require(initialPoints.isNotEmpty() || obstacles.isNotEmpty())
|
||||||
|
|
||||||
val candidatePoints = mutableListOf<Vector2>()
|
val candidatePoints = mutableListOf<Vector2>()
|
||||||
for (point in initialPoints) {
|
for (point in initialPoints) {
|
||||||
if ((candidatePoints.map { it.distanceTo(point) }.minOrNull() ?: Double.POSITIVE_INFINITY) >= pointDistance) {
|
if ((candidatePoints.map { it.distanceTo(point) }.minOrNull() ?: Double.POSITIVE_INFINITY) >= placementRadius * 2.0) {
|
||||||
candidatePoints.add(point)
|
candidatePoints.add(point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (candidatePoints.isEmpty()) {
|
if (candidatePoints.isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
return poissonDiskSampling(
|
return poissonDiskSampling(
|
||||||
bounds.width,
|
bounds,
|
||||||
bounds.height,
|
placementRadius * 2.0,
|
||||||
pointDistance,
|
|
||||||
tries,
|
tries,
|
||||||
false,
|
true,
|
||||||
random,
|
random,
|
||||||
candidatePoints,
|
candidatePoints,
|
||||||
) { _, _, point ->
|
obstacleHashGrids = obstacleHashGrids,
|
||||||
val contourPoint = point.map(poissonBounds, bounds)
|
) { point ->
|
||||||
if (distanceToEdge == 0.0) {
|
if (distanceToEdge == 0.0) {
|
||||||
shape.contains(contourPoint)
|
shape.contains(point)
|
||||||
} else {
|
} else {
|
||||||
shape.contains(contourPoint) && shape.contours.minOf { c ->
|
shape.contains(point) && shape.contours.minOf { c ->
|
||||||
c.nearest(contourPoint).position.distanceTo(contourPoint)
|
c.nearest(point).position.distanceTo(point)
|
||||||
} > distanceToEdge
|
} > distanceToEdge
|
||||||
}
|
}
|
||||||
}.map {
|
|
||||||
it.map(poissonBounds, bounds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import org.openrndr.application
|
|
||||||
import org.openrndr.color.ColorRGBa
|
|
||||||
import org.openrndr.extra.noise.poissonDiskSampling
|
|
||||||
import org.openrndr.math.Vector2
|
|
||||||
import org.openrndr.shape.Circle
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
application {
|
|
||||||
program {
|
|
||||||
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.clear(ColorRGBa.BLACK)
|
|
||||||
drawer.stroke = null
|
|
||||||
drawer.fill = ColorRGBa.PINK
|
|
||||||
drawer.circles(rectPoints)
|
|
||||||
drawer.circles(circlePoints)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user