OPENRNDR pen plotting tricks (Axidraw etc.)

11. Using Signed Distance Fields to create contours with math

The orx-marching-squares extension lets us define mathematical functions to generate contours which we could send to the pen plotter.

Take a look at the simpler examples available in the readme.

In the following example I ported a few functions by iquilez and from the hg_sdf library to create this design:

Basically, I created two sdf shapes (a circle and a rounded box), created “ripples” around them using cos, combined the ripples using fOpDifferenceRound, then used a circular mask to crop the design using fOpIntersectionRound.

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.math.*
import kotlin.math.*

fun main() = application {
    configure {
        width = 1000
        height = 1000
    }

    program {
        fun Vector2.abs() = Vector2(x.absoluteValue, y.absoluteValue)
        fun Vector2.ndot(other: Vector2) = x * other.x - y * other.y
        fun Vector2.dot2() = this.dot(this)

        // https://iquilezles.org/articles/distfunctions2d/
        fun sdCircle(p: Vector2, r: Double) = p.length - r

        fun sdRoundedBox(p: Vector2, b: Vector2, r: Vector4): Double {
            val r2 = if (p.x > 0.0) r else r.copy(r.z, r.w)
            val r3 = if (p.y > 0.0) r2 else r2.copy(r2.y)
            val q = p.abs() - b + r3.x
            return min(max(q.x, q.y), 0.0) + max(q, Vector2.ZERO).length - r3.x
        }

        // http://mercury.sexy/hg_sdf/
        fun fOpUnionRound(a: Double, b: Double, r: Double): Double {
            val u = max(Vector2(r - a, r - b), Vector2.ZERO)
            return max(r, min(a, b)) - u.length
        }

        fun fOpIntersectionRound(a: Double, b: Double, r: Double): Double {
            val u = max(Vector2(r + a, r + b), Vector2.ZERO)
            return min(-r, max(a, b)) + u.length
        }

        fun fOpDifferenceRound(a: Double, b: Double, r: Double) = fOpIntersectionRound(a, -b, r)

        fun f(v: Vector2): Double {
            val cir = cos(
                sdCircle(v - drawer.bounds.position(0.3, 0.3), 100.0) * 0.1
            ) - 0.7

            val box = cos(
                sdRoundedBox(
                    v - drawer.bounds.position(0.7, 0.7),
                    Vector2(150.0, 100.0),
                    Vector4(10.0, 30.0, 50.0, 70.0)
                ) * 0.15
            ) + 0.7

            val c = fOpDifferenceRound(cir, box, 1.0)
            val mask = sdCircle(v - drawer.bounds.center, 400.0)
            return fOpIntersectionRound(c, mask, 10.0)
        }

        val c = findContours(::f, drawer.bounds, 3.0)

        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.stroke = ColorRGBa.BLACK
            drawer.fill = null
            drawer.contours(c)
        }
    }
}

Note that there are many more SDF functions to play with available in the two links above.

Sculpting shapes via math can be somewhat unintutive, although this technique is common in shader programs running on the GPU.

1 Like