[orx-shapes] Add alpha shapes (#203)

This commit is contained in:
Steven van den Broek
2021-10-24 20:41:40 +02:00
committed by GitHub
parent af524b8e42
commit ee3a3603c0
7 changed files with 186 additions and 2 deletions

View File

@@ -26,6 +26,7 @@ kotlin {
dependencies { dependencies {
implementation(project(":orx-camera")) implementation(project(":orx-camera"))
implementation(project(":orx-color")) implementation(project(":orx-color"))
implementation(project(":orx-jvm:orx-triangulation"))
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")
@@ -67,6 +68,12 @@ kotlin {
} }
} }
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val jvmMain by getting {
dependencies {
implementation(project(":orx-jvm:orx-triangulation"))
}
}
@Suppress("UNUSED_VARIABLE")
val commonTest by getting { val commonTest by getting {
dependencies { dependencies {
implementation(kotlin("test-common")) implementation(kotlin("test-common"))

View File

@@ -0,0 +1,24 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.shapes.AlphaShape
import org.openrndr.math.Vector2
import kotlin.random.Random
fun main() = application {
program {
val points = List(20) {
Vector2(
Random.nextDouble(width*0.25, width*0.75),
Random.nextDouble(height*0.25, height*0.75)
)
}
val alphaShape = AlphaShape(points)
val c = alphaShape.create()
extend {
drawer.fill = ColorRGBa.PINK
drawer.contour(c)
drawer.fill = ColorRGBa.WHITE
drawer.circles(points, 4.0)
}
}
}

View File

@@ -2,9 +2,9 @@ import org.openrndr.application
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.draw.loadFont import org.openrndr.draw.loadFont
import org.openrndr.extensions.SingleScreenshot import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.color.spaces.toOKLABa
import org.openrndr.extra.shapes.bezierPatch import org.openrndr.extra.shapes.bezierPatch
import org.openrndr.extra.shapes.drawers.bezierPatch import org.openrndr.extra.shapes.drawers.bezierPatch
import org.openrndr.extras.color.spaces.toOKLABa
import org.openrndr.shape.Circle import org.openrndr.shape.Circle
fun main() { fun main() {

View File

@@ -6,7 +6,7 @@ import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.shapes.bezierPatch import org.openrndr.extra.shapes.bezierPatch
import org.openrndr.extra.shapes.drawers.bezierPatch import org.openrndr.extra.shapes.drawers.bezierPatch
import org.openrndr.extra.shapes.grid import org.openrndr.extra.shapes.grid
import org.openrndr.extras.color.spaces.toOKLABa import org.openrndr.extra.color.spaces.toOKLABa
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.math.Vector3 import org.openrndr.math.Vector3
import org.openrndr.math.min import org.openrndr.math.min

View File

@@ -10,6 +10,8 @@ fun main() = application {
drawer.stroke = ColorRGBa.BLACK drawer.stroke = ColorRGBa.BLACK
drawer.fill = ColorRGBa.PINK drawer.fill = ColorRGBa.PINK
drawer.contour(hobbyCurve(points, closed=true)) drawer.contour(hobbyCurve(points, closed=true))
drawer.fill = ColorRGBa.WHITE
drawer.circles(points, 4.0)
} }
} }
} }

View File

@@ -0,0 +1,26 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.shapes.AlphaShape
import org.openrndr.extra.shapes.hobbyCurve
import org.openrndr.math.Vector2
import kotlin.random.Random
fun main() = application {
program {
val points = List(20) {
Vector2(
Random.nextDouble(width*0.25, width*0.75),
Random.nextDouble(height*0.25, height*0.75)
)
}
val alphaShape = AlphaShape(points)
val c = alphaShape.create()
val hobby = hobbyCurve(c.segments.map { it.start }, closed=true)
extend {
drawer.fill = ColorRGBa.PINK
drawer.contour(hobby)
drawer.fill = ColorRGBa.WHITE
drawer.circles(points, 4.0)
}
}
}

View File

@@ -0,0 +1,125 @@
package org.openrndr.extra.shapes
import org.openrndr.extra.triangulation.Delaunay
import org.openrndr.math.Vector2
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contains
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
private fun circumradius(p1: Vector2, p2: Vector2, p3: Vector2): Double {
val a = (p2 - p1).length
val b = (p3 - p2).length
val c = (p1 - p3).length
return (a*b*c) / sqrt((a+b+c)*(b+c-a)*(c+a-b)*(a+b-c))
}
/**
* Class for creating alpha shapes.
* Use the [create] method to create an alpha shape.
* @param points The points for which an alpha shape is calculated.
*/
class AlphaShape(val points: List<Vector2>) {
val delaunay = Delaunay.from(points)
private fun <A, B> Pair<A, B>.flip() = Pair(second, first)
/**
* Creates an alpha shape.
* @param alpha The alpha parameter from the mathematical definition of an alpha shape.
* If alpha is 0.0 the alpha shape consists only of the set of input points, yielding [ShapeContour.EMPTY].
* As alpha goes to infinity, the alpha shape becomes equal to the convex hull of the input points.
* @return A closed [ShapeContour] representing the outer boundary of the alpha shape.
*/
fun create(alpha: Double): ShapeContour {
if (delaunay.points.size < 9) return ShapeContour.EMPTY
val triangles = delaunay.triangles
var allEdges = mutableSetOf<Pair<Int, Int>>()
var perimeterEdges = mutableSetOf<Pair<Int, Int>>()
for (i in triangles.indices step 3){
val t0 = triangles[i] * 2
val t1 = triangles[i + 1] * 2
val t2 = triangles[i + 2] * 2
val p1 = getVec(t0)
val p2 = getVec(t1)
val p3 = getVec(t2)
val r = circumradius(p1, p2, p3)
if (r < alpha){
val edges = listOf(Pair(t0, t1), Pair(t1, t2), Pair(t2, t0))
for (edge in edges){
val fEdge = edge.flip()
if (edge !in allEdges && fEdge !in allEdges){
allEdges.add(edge)
perimeterEdges.add(edge)
} else {
perimeterEdges.remove(edge)
perimeterEdges.remove(fEdge)
}
}
}
}
return edgesToShapeContour(perimeterEdges.toList())
}
/**
* Returns the alpha shape with the smallest alpha such that all input points are contained in the alpha shape.
*/
fun create(): ShapeContour = create(determineAlpha())
private fun getVec(i: Int) = Vector2(delaunay.points[i], delaunay.points[i + 1])
private fun edgesToShapeContour(edges: List<Pair<Int, Int>>): ShapeContour {
if (edges.isEmpty()) return ShapeContour.EMPTY
val mapping = edges.toMap()
val segments = mutableListOf<Segment>()
val start = edges.first().first
var current = start
repeat(edges.size) {
val next = mapping[current]!!
segments.add(Segment(getVec(current), getVec(next)))
current = next
}
return if (current == start) {
ShapeContour(segments, closed = true)
} else {
ShapeContour.EMPTY
}
}
/**
* Performs binary search to find the smallest alpha such that all points are inside the alpha shape.
*/
fun determineAlpha(): Double {
// Compute bounding box to find an upper bound for the binary search
var minX = Double.POSITIVE_INFINITY
var minY = Double.POSITIVE_INFINITY
var maxX = Double.NEGATIVE_INFINITY
var maxY = Double.NEGATIVE_INFINITY
for (i in delaunay.points.indices step 2){
val x = delaunay.points[i]
val y = delaunay.points[i+1]
minX = min(minX, x)
maxX = max(maxX, x)
minY = min(minY, y)
maxY = max(maxY, y)
}
// Perform binary search
var lower = 0.0
var upper = (maxX - minX).pow(2) + (maxY - minY).pow(2)
val precision = 0.001
while(lower < upper - precision){
val mid = (lower + upper)/2
val polygon = create(mid)
if (points.all { it in polygon }) upper = mid else lower = mid
}
return upper
}
}