diff --git a/orx-fx/build.gradle.kts b/orx-fx/build.gradle.kts index 5ceb03a4..025a9e27 100644 --- a/orx-fx/build.gradle.kts +++ b/orx-fx/build.gradle.kts @@ -59,6 +59,7 @@ kotlin { dependencies { implementation(project(":orx-color")) implementation(project(":orx-fx")) + implementation(project(":orx-noise")) } } } diff --git a/orx-fx/src/commonMain/kotlin/composite/CompositeFilter.kt b/orx-fx/src/commonMain/kotlin/composite/CompositeFilter.kt new file mode 100644 index 00000000..e3f7503a --- /dev/null +++ b/orx-fx/src/commonMain/kotlin/composite/CompositeFilter.kt @@ -0,0 +1,122 @@ +package org.openrndr.extra.fx.composite + +import org.openrndr.draw.ColorBuffer +import org.openrndr.draw.Filter +import org.openrndr.draw.createEquivalent +import org.openrndr.draw.isEquivalentTo + +/** + * @param first the filter that is applied first + * @param second the filter that is applied second + * @param firstSource a function that maps source color buffers for the [first] filter + * @param secondSource a function that maps source color buffers for the [second] filter + * @param firstParameters a function that sets parameters for the [first] filter + * @param secondParameters a function that sets parameters for the [second] fillter + * @param useIntermediateBuffer should an intermediate buffer be maintained? when set to false the [first] filter will + * write to the target color buffer + */ +class CompositeFilter( + val first: F0, + val second: F1, + private val firstSource: (List) -> List, + private val secondSource: (List, ColorBuffer) -> List, + private val firstParameters: F0.() -> Unit, + private val secondParameters: F1.() -> Unit, + private val useIntermediateBuffer: Boolean = false +) : Filter() { + private var intermediate: ColorBuffer? = null + + override fun apply(source: Array, target: Array) { + val firstSource = firstSource(source.toList()).toTypedArray() + if (!useIntermediateBuffer) { + first.firstParameters() + first.apply(firstSource, target) + + second.secondParameters() + val secondSource = secondSource(source.toList(), target.first()).toTypedArray() + second.apply(secondSource, target) + } else { + val li = intermediate + if (li != null && !li.isEquivalentTo(target.first())) { + li.destroy() + intermediate = null + } + if (intermediate == null) { + intermediate = target.first().createEquivalent() + } + first.firstParameters() + first.apply(firstSource, arrayOf(intermediate!!)) + val secondSource = secondSource(source.toList(), intermediate!!).toTypedArray() + second.secondParameters() + second.apply(secondSource, target) + } + } + + override fun destroy() { + intermediate?.destroy() + super.destroy() + } +} + +class CompositeFilterBuilder(val first: F0, val second: F1) { + private var firstSourceFunction: (inputs: List) -> List = { inputs -> inputs } + private var secondSourceFunction: (inputs: List, intermediate: ColorBuffer) -> List = + { inputs, intermediate -> listOf(intermediate) + inputs.drop(1) } + + private var firstParametersFunction: (F0.() -> Unit) = {} + private var secondParametersFunction: (F1.() -> Unit) = {} + + + /** Supply the function that sets the source color buffers for the [first] filter */ + fun firstSource(function: (source: List) -> List) { + firstSourceFunction = function + } + + /** Supply the function that sets the source color buffers for the [second] filter */ + fun secondSource(function: (source: List, intermediate: ColorBuffer) -> List) { + secondSourceFunction = function + } + + /** + * Supply the function that sets the filter parameters for the [first] filter + */ + fun firstParameters(function: (F0.() -> Unit)) { + firstParametersFunction = function + } + + /** + * Supply the function that sets the filter parameter the [second] filter + */ + fun secondParameters(function: (F1.() -> Unit)) { + secondParametersFunction = function + } + + /** + * Should an intermediate color buffer be used? + */ + var useIntermediateBuffer = true + + fun build(): CompositeFilter { + return CompositeFilter( + first, + second, + firstSourceFunction, + secondSourceFunction, + firstParametersFunction, + secondParametersFunction, + useIntermediateBuffer + ) + } +} + +/** + * Create a composite filter that first applies [this] filter and then the [next] filter. + */ +fun F0.then( + next: F1, + builder: CompositeFilterBuilder.() -> Unit = {} +): CompositeFilter { + val compositeFilterBuilder = CompositeFilterBuilder(this, next) + compositeFilterBuilder.builder() + return compositeFilterBuilder.build() +} \ No newline at end of file diff --git a/orx-fx/src/demo/kotlin/DemoCompositeFilter01.kt b/orx-fx/src/demo/kotlin/DemoCompositeFilter01.kt new file mode 100644 index 00000000..e8381f12 --- /dev/null +++ b/orx-fx/src/demo/kotlin/DemoCompositeFilter01.kt @@ -0,0 +1,63 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.ColorType +import org.openrndr.draw.colorBuffer +import org.openrndr.draw.loadImage +import org.openrndr.extra.fx.Post +import org.openrndr.extra.fx.blur.DirectionalBlur +import org.openrndr.extra.fx.composite.then +import org.openrndr.extra.fx.grain.FilmGrain +import org.openrndr.extra.noise.* +import org.openrndr.math.smoothstep +import kotlin.math.cos + +fun main() { + application { + program { + extend(Post()) { + // -- create a color buffer and fill it with random direction vectors + val direction = colorBuffer(width, height, type = ColorType.FLOAT32) + val s = direction.shadow + val n = simplex2D.bipolar().fbm().scaleShiftInput(0.01, 0.0, 0.01, 0.0).withVector2Output() + val ng = simplex2D.unipolar().scaleShiftInput(0.005, 0.0, 0.005, 0.0) + for (y in 0 until height) { + for (x in 0 until width) { + val a = smoothstep(0.4, 0.6, cos((x + y) * 0.01) * 0.5 + 0.5) + val nv = n(2320, x.toDouble(), y.toDouble()) * smoothstep(0.45, 0.55, ng(1032, x.toDouble(), y.toDouble())) + s[x, y] = ColorRGBa(nv.x, nv.y, 0.0, 1.0) + } + } + s.upload() + + val directional = DirectionalBlur() + + // -- create a bidirectional composite filter by using a directional filter twice + val bidirectional = directional.then(directional) { + firstParameters { + window = 50 + perpendicular = false + } + secondParameters { + window = 3 + perpendicular = true + } + } + + val grain = FilmGrain() + grain.grainStrength = 1.0 + + // -- create a grain-blur composite filter + val grainBlur = grain.then(bidirectional) + + post { input, output -> + grainBlur.apply(arrayOf(input, direction), output) + } + } + + val image = loadImage("demo-data/images/image-001.png") + extend { + drawer.image(image) + } + } + } +} \ No newline at end of file