[orx-math] Add RBF interpolation utilities with 2D interpolator, demos, and shader phrases
This commit is contained in:
@@ -40,6 +40,7 @@ kotlin {
|
|||||||
implementation(project(":orx-color"))
|
implementation(project(":orx-color"))
|
||||||
implementation(project(":orx-jvm:orx-gui"))
|
implementation(project(":orx-jvm:orx-gui"))
|
||||||
implementation(project(":orx-shade-styles"))
|
implementation(project(":orx-shade-styles"))
|
||||||
|
implementation(project(":orx-shader-phrases"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
orx-math/src/commonMain/kotlin/rbf/RbfInterpolator.kt
Normal file
138
orx-math/src/commonMain/kotlin/rbf/RbfInterpolator.kt
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package org.openrndr.extra.math.rbf
|
||||||
|
|
||||||
|
import org.openrndr.extra.math.matrix.Matrix
|
||||||
|
import org.openrndr.extra.math.matrix.columnMean
|
||||||
|
import org.openrndr.extra.math.matrix.invertMatrixCholesky
|
||||||
|
import org.openrndr.extra.math.matrix.minus
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
typealias Rbf = (Double) -> Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Gaussian radial basis function (RBF) with the given scale parameter.
|
||||||
|
* The resulting RBF computes the exponential decay based on the squared distance scaled by the parameter.
|
||||||
|
*
|
||||||
|
* @param scale The scale parameter influencing the width of the Gaussian RBF. Smaller values result in a steeper decay.
|
||||||
|
* @return A function representing the Gaussian RBF, which takes a square of the distance as input and returns the RBF value.
|
||||||
|
*/
|
||||||
|
fun rbfGaussian(scale: Double): Rbf {
|
||||||
|
val scale2 = scale * scale
|
||||||
|
return { d ->
|
||||||
|
exp(-d * scale2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radial basis function (RBF) using the inverse quadratic formula.
|
||||||
|
*
|
||||||
|
* Creates an RBF that calculates the inverse quadratic function based on the given scale.
|
||||||
|
*
|
||||||
|
* @param scale A scaling factor that determines the influence range of the RBF.
|
||||||
|
* @return A lambda function representing the inverse quadratic RBF.
|
||||||
|
*/
|
||||||
|
fun rbfInverseQuadratic(scale: Double): Rbf {
|
||||||
|
val scale2 = scale * scale
|
||||||
|
return { d ->
|
||||||
|
1.0 / (1.0 + d * scale2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a radial basis function (RBF) using the inverse multiquadratic kernel.
|
||||||
|
*
|
||||||
|
* @param scale The scaling factor that influences the spread and shape of the RBF.
|
||||||
|
* @return A function representing the inverse multiquadratic RBF, which computes the value
|
||||||
|
* based on the given squared distance.
|
||||||
|
*/
|
||||||
|
fun rbfInverseMultiQuadratic(scale: Double): Rbf {
|
||||||
|
val scale2 = scale * scale
|
||||||
|
return { d ->
|
||||||
|
1.0 / sqrt(1.0 + d * scale2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A two-dimensional Radial Basis Function (RBF) interpolator.
|
||||||
|
*
|
||||||
|
* This class provides functionality to interpolate values in a 2D space
|
||||||
|
* using Radial Basis Functions (RBFs). It computes interpolated values for
|
||||||
|
* input points based on given data points, their corresponding values, and
|
||||||
|
* an RBF kernel that defines the basis function.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param points A list of 2D points representing the locations of the input data.
|
||||||
|
* @param weights A 2D array of weights corresponding to each point for each output dimension.
|
||||||
|
* @param values A 2D array of known function values at the given points.
|
||||||
|
* @param rbf The radial basis function that defines how the influence of each point decreases with distance.
|
||||||
|
* It takes a squared distance as input and returns a scalar value.
|
||||||
|
* @param mean The mean values for each output dimension, used to offset the interpolated results.
|
||||||
|
*/
|
||||||
|
class Rbf2DInterpolator(
|
||||||
|
val points: List<Vector2>,
|
||||||
|
val weights: Array<DoubleArray>,
|
||||||
|
val values: Array<DoubleArray>,
|
||||||
|
val rbf: (Double) -> Double,
|
||||||
|
val mean: DoubleArray
|
||||||
|
) {
|
||||||
|
fun interpolate(x: Vector2): DoubleArray {
|
||||||
|
val c = DoubleArray(values[0].size)
|
||||||
|
for (j in points.indices) {
|
||||||
|
val r = rbf(points[j].squaredDistanceTo(x))
|
||||||
|
for (i in 0 until c.size) {
|
||||||
|
c[i] += weights[j][i] * r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i in 0 until c.size) {
|
||||||
|
c[i] += mean[i]
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a two-dimensional Radial Basis Function (RBF) interpolator using provided input points,
|
||||||
|
* their corresponding values, a smoothing factor, and a radial basis function (RBF) kernel.
|
||||||
|
*
|
||||||
|
* The interpolator computes a weight matrix derived from the RBF kernel and the supplied data.
|
||||||
|
* The resulting interpolator can be used to estimate the values at new locations in a 2D space.
|
||||||
|
*
|
||||||
|
* @param points A list of 2D points representing the input data locations.
|
||||||
|
* @param values A 2D array of known function values corresponding to the input points.
|
||||||
|
* Each row corresponds to a point, and each column corresponds to a value in a specific dimension.
|
||||||
|
* @param smoothing A non-negative smoothing factor to reduce interpolation sensitivity. Default is 0.0.
|
||||||
|
* Larger values result in smoother interpolations.
|
||||||
|
* @param rbf The radial basis function used for interpolation. This function takes a squared distance as input
|
||||||
|
* and returns a scalar value representing the influence of points at that distance.
|
||||||
|
* @return An instance of `Rbf2DInterpolator` configured with the computed weight matrix and input data.
|
||||||
|
*/
|
||||||
|
fun Rbf2DInterpolator(
|
||||||
|
points: List<Vector2>,
|
||||||
|
values: Array<DoubleArray>,
|
||||||
|
smoothing: Double = 0.0,
|
||||||
|
rbf: Rbf
|
||||||
|
): Rbf2DInterpolator {
|
||||||
|
|
||||||
|
val rmat = Matrix(points.size, points.size)
|
||||||
|
for (j in points.indices) {
|
||||||
|
for (i in points.indices) {
|
||||||
|
rmat[i, j] = rbf(points[i].squaredDistanceTo(points[j])) + if (j == i) smoothing else 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imat = invertMatrixCholesky(rmat)
|
||||||
|
|
||||||
|
val vmat = Matrix(points.size, values[0].size)
|
||||||
|
for (j in points.indices) {
|
||||||
|
for (i in values[0].indices) {
|
||||||
|
vmat[j, i] = values[j][i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mean = vmat.columnMean()
|
||||||
|
val vwmat = vmat - mean
|
||||||
|
|
||||||
|
val wmat = imat * vwmat
|
||||||
|
return Rbf2DInterpolator(points, wmat.data, values, rbf, mean.data[0])
|
||||||
|
}
|
||||||
109
orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt
Normal file
109
orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package rbf
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.isolated
|
||||||
|
import org.openrndr.draw.shadeStyle
|
||||||
|
import org.openrndr.extra.color.spaces.OKHSV
|
||||||
|
import org.openrndr.extra.color.spaces.OKLab
|
||||||
|
import org.openrndr.extra.color.tools.shadeLuminosity
|
||||||
|
import org.openrndr.extra.color.tools.shiftHue
|
||||||
|
import org.openrndr.extra.math.rbf.Rbf2DInterpolator
|
||||||
|
import org.openrndr.extra.math.rbf.rbfGaussian
|
||||||
|
import org.openrndr.extra.noise.uniform
|
||||||
|
import org.openrndr.extra.shaderphrases.noise.fhash12Phrase
|
||||||
|
import org.openrndr.extra.shaderphrases.rbf.rbfGaussianPhrase
|
||||||
|
import org.openrndr.math.Vector3
|
||||||
|
import kotlin.collections.indices
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.collections.toTypedArray
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.ranges.until
|
||||||
|
import kotlin.text.trimIndent
|
||||||
|
import kotlin.text.trimMargin
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
val r = Random(0)
|
||||||
|
val points = drawer.bounds.offsetEdges(-100.0).uniform(14, r)
|
||||||
|
|
||||||
|
val colors = (0 until points.size).map {
|
||||||
|
ColorRGBa.PINK
|
||||||
|
.shiftHue<OKHSV>(Double.uniform(-180.0, 180.0, r))
|
||||||
|
.shadeLuminosity<OKLab>(Double.uniform(0.4, 1.0, r))
|
||||||
|
.toLinear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here the `scale` and `smoothing` values are hand-tuned
|
||||||
|
val scale = 0.04 / 5.0
|
||||||
|
val interpolator = Rbf2DInterpolator(
|
||||||
|
points,
|
||||||
|
colors.map { doubleArrayOf(it.r, it.g, it.b) }.toTypedArray<DoubleArray>(),
|
||||||
|
smoothing = 0.09,
|
||||||
|
rbf = rbfGaussian(scale)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shader style that implements RBF interpolation in the fragment shader.
|
||||||
|
* Uses Gaussian RBF function to interpolate colors between given points.
|
||||||
|
* Includes custom distance calculation and color interpolation functions.
|
||||||
|
*/
|
||||||
|
val ss = shadeStyle {
|
||||||
|
fragmentPreamble = """${fhash12Phrase}
|
||||||
|
|${rbfGaussianPhrase}
|
||||||
|
|float squaredDistance(vec2 p, vec2 q) {
|
||||||
|
| vec2 d = p - q;
|
||||||
|
| return dot(d, d);
|
||||||
|
|}
|
||||||
|
|vec3 rbfInterpolate(vec2 p) {
|
||||||
|
| vec3 c = p_mean;
|
||||||
|
| for (int i = 0; i < p_weights_SIZE; ++i) {
|
||||||
|
| float r = rbfGaussian(squaredDistance(p_points[i], p), $scale);
|
||||||
|
| c.r += p_weights[i].r * r;
|
||||||
|
| c.g += p_weights[i].g * r;
|
||||||
|
| c.b += p_weights[i].b * r;
|
||||||
|
| }
|
||||||
|
| return c;
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
fragmentTransform = """
|
||||||
|
x_fill.rgb = rbfInterpolate(c_boundsPosition.xy * vec2(720.0, 720.0));
|
||||||
|
|
||||||
|
""".trimIndent()
|
||||||
|
val weights = (0 until points.size).map {
|
||||||
|
Vector3(interpolator.weights[it][0], interpolator.weights[it][1], interpolator.weights[it][2])
|
||||||
|
}.toTypedArray()
|
||||||
|
parameter("weights", weights)
|
||||||
|
parameter("points", points.toTypedArray())
|
||||||
|
parameter("mean", Vector3(interpolator.mean[0], interpolator.mean[1], interpolator.mean[2]))
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
// draw the interpolated colors
|
||||||
|
drawer.isolated {
|
||||||
|
drawer.shadeStyle = ss
|
||||||
|
drawer.rectangle(drawer.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the original points and colors for reference
|
||||||
|
drawer.circles {
|
||||||
|
for (i in points.indices) {
|
||||||
|
fill = colors[i]
|
||||||
|
circle(points[i], 10.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute color on CPU for comparison
|
||||||
|
drawer.fill = interpolator.interpolate(mouse.position).let {
|
||||||
|
ColorRGBa(it[0], it[1], it[2], 1.0)
|
||||||
|
}
|
||||||
|
drawer.circle(mouse.position, 30.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt
Normal file
113
orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package rbf
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.isolated
|
||||||
|
import org.openrndr.draw.shadeStyle
|
||||||
|
import org.openrndr.extra.color.spaces.OKHSV
|
||||||
|
import org.openrndr.extra.color.spaces.OKLab
|
||||||
|
import org.openrndr.extra.color.tools.shadeLuminosity
|
||||||
|
import org.openrndr.extra.color.tools.shiftHue
|
||||||
|
import org.openrndr.extra.math.rbf.Rbf2DInterpolator
|
||||||
|
import org.openrndr.extra.math.rbf.rbfGaussian
|
||||||
|
import org.openrndr.extra.math.rbf.rbfInverseMultiQuadratic
|
||||||
|
import org.openrndr.extra.math.rbf.rbfInverseQuadratic
|
||||||
|
import org.openrndr.extra.noise.uniform
|
||||||
|
import org.openrndr.extra.shaderphrases.noise.fhash12Phrase
|
||||||
|
import org.openrndr.extra.shaderphrases.rbf.rbfGaussianPhrase
|
||||||
|
import org.openrndr.extra.shaderphrases.rbf.rbfInverseMultiQuadraticPhrase
|
||||||
|
import org.openrndr.extra.shaderphrases.rbf.rbfInverseQuadraticPhrase
|
||||||
|
import org.openrndr.math.Vector3
|
||||||
|
import kotlin.collections.indices
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.collections.toTypedArray
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.ranges.until
|
||||||
|
import kotlin.text.trimIndent
|
||||||
|
import kotlin.text.trimMargin
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
val r = Random(0)
|
||||||
|
val points = drawer.bounds.offsetEdges(-100.0).uniform(20, r)
|
||||||
|
|
||||||
|
val colors = (0 until points.size).map {
|
||||||
|
ColorRGBa.PINK
|
||||||
|
.shiftHue<OKHSV>(Double.uniform(-180.0, 180.0, r))
|
||||||
|
.shadeLuminosity<OKLab>(Double.uniform(0.4, 1.0, r))
|
||||||
|
.toLinear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here the `scale` and `smoothing` values are hand-tuned
|
||||||
|
val scale = 0.04 / 5.0
|
||||||
|
val interpolator = Rbf2DInterpolator(
|
||||||
|
points,
|
||||||
|
colors.map { doubleArrayOf(it.r, it.g, it.b) }.toTypedArray<DoubleArray>(),
|
||||||
|
smoothing = 0.09,
|
||||||
|
rbf = rbfInverseMultiQuadratic(scale)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shader style that implements RBF interpolation in the fragment shader.
|
||||||
|
* Uses Gaussian RBF function to interpolate colors between given points.
|
||||||
|
* Includes custom distance calculation and color interpolation functions.
|
||||||
|
*/
|
||||||
|
val ss = shadeStyle {
|
||||||
|
fragmentPreamble = """${fhash12Phrase}
|
||||||
|
|${rbfInverseMultiQuadraticPhrase}
|
||||||
|
|float squaredDistance(vec2 p, vec2 q) {
|
||||||
|
| vec2 d = p - q;
|
||||||
|
| return dot(d, d);
|
||||||
|
|}
|
||||||
|
|vec3 rbfInterpolate(vec2 p) {
|
||||||
|
| vec3 c = p_mean;
|
||||||
|
| for (int i = 0; i < p_weights_SIZE; ++i) {
|
||||||
|
| float r = rbfInverseMultiQuadratic(squaredDistance(p_points[i], p), $scale);
|
||||||
|
| c.r += p_weights[i].r * r;
|
||||||
|
| c.g += p_weights[i].g * r;
|
||||||
|
| c.b += p_weights[i].b * r;
|
||||||
|
| }
|
||||||
|
| return c;
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
fragmentTransform = """
|
||||||
|
x_fill.rgb = rbfInterpolate(c_boundsPosition.xy * vec2(720.0, 720.0));
|
||||||
|
|
||||||
|
""".trimIndent()
|
||||||
|
val weights = (0 until points.size).map {
|
||||||
|
Vector3(interpolator.weights[it][0], interpolator.weights[it][1], interpolator.weights[it][2])
|
||||||
|
}.toTypedArray()
|
||||||
|
parameter("weights", weights)
|
||||||
|
parameter("points", points.toTypedArray())
|
||||||
|
parameter("mean", Vector3(interpolator.mean[0], interpolator.mean[1], interpolator.mean[2]))
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
// draw the interpolated colors
|
||||||
|
drawer.isolated {
|
||||||
|
drawer.shadeStyle = ss
|
||||||
|
drawer.rectangle(drawer.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the original points and colors for reference
|
||||||
|
drawer.circles {
|
||||||
|
for (i in points.indices) {
|
||||||
|
fill = colors[i]
|
||||||
|
circle(points[i], 10.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute color on CPU for comparison
|
||||||
|
drawer.fill = interpolator.interpolate(mouse.position).let {
|
||||||
|
ColorRGBa(it[0], it[1], it[2], 1.0)
|
||||||
|
}
|
||||||
|
drawer.circle(mouse.position, 30.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
orx-shader-phrases/src/commonMain/kotlin/rbf/RbfPhrases.kt
Normal file
51
orx-shader-phrases/src/commonMain/kotlin/rbf/RbfPhrases.kt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package org.openrndr.extra.shaderphrases.rbf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A constant string defining a C-style preprocessor directive and implementation for the
|
||||||
|
* Radial Basis Function (RBF) Gaussian formula in a shader or computational context.
|
||||||
|
*
|
||||||
|
* The definition includes a function `rbfGaussian` that computes the Gaussian value
|
||||||
|
* based on the squared distance and scale factor. It utilizes the exponential function
|
||||||
|
* for the calculation.
|
||||||
|
*/
|
||||||
|
const val rbfGaussianPhrase = """#ifndef SP_RBF_GAUSSIAN
|
||||||
|
#define SP_RBF_GAUSSIAN
|
||||||
|
float rbfGaussian(float sqrDistance, float scale) {
|
||||||
|
return exp(-sqrDistance * scale * scale);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A constant string representing a shader function definition for the
|
||||||
|
* Radial Basis Function (RBF) using the inverse quadratic formula.
|
||||||
|
*
|
||||||
|
* The function `rbfInverseQuadratic` calculates the RBF value based on
|
||||||
|
* squared distance and a scale factor.
|
||||||
|
*
|
||||||
|
* The formula for the RBF is:
|
||||||
|
* 1.0 / (1.0 + sqrDistance * scale^2)
|
||||||
|
*/
|
||||||
|
const val rbfInverseQuadraticPhrase = """#ifndef SP_RBF_INVERSE_QUADRATIC
|
||||||
|
#define SP_RBF_INVERSE_QUADRATIC
|
||||||
|
float rbfInverseQuadratic(float sqrDistance, float scale) {
|
||||||
|
return 1.0 / (1.0 + sqrDistance * scale * scale);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the implementation of the inverse multiquadratic radial basis function (RBF)
|
||||||
|
* in shader language. This constant holds the shader source code for calculating
|
||||||
|
* the inverse multiquadratic RBF given a squared distance and a scale factor.
|
||||||
|
*
|
||||||
|
* The function defined within this shader code computes the RBF as:
|
||||||
|
* 1.0 / sqrt(1.0 + sqrDistance * scale * scale)
|
||||||
|
*/
|
||||||
|
const val rbfInverseMultiQuadraticPhrase = """#ifndef SP_RBF_INVERSE_MULTIQUADRATIC
|
||||||
|
#define SP_RBF_INVERSE_MULTIQUADRATIC
|
||||||
|
float rbfInverseMultiQuadratic(float sqrDistance, float scale) {
|
||||||
|
return 1.0 / sqrt(1.0 + sqrDistance * scale * scale);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
"""
|
||||||
Reference in New Issue
Block a user