From c63c0c58449115796474cddc466fffdc45a9d2d9 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sat, 16 Aug 2025 21:50:26 +0200 Subject: [PATCH] [orx-math] Add RBF interpolation utilities with 2D interpolator, demos, and shader phrases --- orx-math/build.gradle.kts | 1 + .../commonMain/kotlin/rbf/RbfInterpolator.kt | 138 ++++++++++++++++++ .../jvmDemo/kotlin/rbf/RbfInterpolation01.kt | 109 ++++++++++++++ .../jvmDemo/kotlin/rbf/RbfInterpolation02.kt | 113 ++++++++++++++ .../src/commonMain/kotlin/rbf/RbfPhrases.kt | 51 +++++++ 5 files changed, 412 insertions(+) create mode 100644 orx-math/src/commonMain/kotlin/rbf/RbfInterpolator.kt create mode 100644 orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt create mode 100644 orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt create mode 100644 orx-shader-phrases/src/commonMain/kotlin/rbf/RbfPhrases.kt diff --git a/orx-math/build.gradle.kts b/orx-math/build.gradle.kts index 8477db5d..9525fc17 100644 --- a/orx-math/build.gradle.kts +++ b/orx-math/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { implementation(project(":orx-color")) implementation(project(":orx-jvm:orx-gui")) implementation(project(":orx-shade-styles")) + implementation(project(":orx-shader-phrases")) } } } diff --git a/orx-math/src/commonMain/kotlin/rbf/RbfInterpolator.kt b/orx-math/src/commonMain/kotlin/rbf/RbfInterpolator.kt new file mode 100644 index 00000000..6a11be53 --- /dev/null +++ b/orx-math/src/commonMain/kotlin/rbf/RbfInterpolator.kt @@ -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, + val weights: Array, + val values: Array, + 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, + values: Array, + 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]) +} \ No newline at end of file diff --git a/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt new file mode 100644 index 00000000..6e13d7ae --- /dev/null +++ b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt @@ -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(Double.uniform(-180.0, 180.0, r)) + .shadeLuminosity(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(), + 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) + } + } + } +} \ No newline at end of file diff --git a/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt new file mode 100644 index 00000000..7eddbd9a --- /dev/null +++ b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt @@ -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(Double.uniform(-180.0, 180.0, r)) + .shadeLuminosity(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(), + 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) + } + } + } +} \ No newline at end of file diff --git a/orx-shader-phrases/src/commonMain/kotlin/rbf/RbfPhrases.kt b/orx-shader-phrases/src/commonMain/kotlin/rbf/RbfPhrases.kt new file mode 100644 index 00000000..f977792a --- /dev/null +++ b/orx-shader-phrases/src/commonMain/kotlin/rbf/RbfPhrases.kt @@ -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 +""" \ No newline at end of file