diff --git a/orx-compositor/build.gradle.kts b/orx-compositor/build.gradle.kts index 78fb5ab2..73c3adc9 100644 --- a/orx-compositor/build.gradle.kts +++ b/orx-compositor/build.gradle.kts @@ -43,6 +43,7 @@ kotlin { val jvmDemo by getting { dependencies { implementation(project(":orx-fx")) + implementation(project(":orx-compositor")) } } } diff --git a/orx-compositor/src/commonMain/kotlin/Compositor.kt b/orx-compositor/src/commonMain/kotlin/Compositor.kt index 90d9fbfc..e50ea10a 100644 --- a/orx-compositor/src/commonMain/kotlin/Compositor.kt +++ b/orx-compositor/src/commonMain/kotlin/Compositor.kt @@ -10,7 +10,6 @@ import org.openrndr.extra.parameters.BooleanParameter import org.openrndr.extra.parameters.Description import org.openrndr.math.Matrix44 -private val postBufferCache = mutableListOf() fun RenderTarget.deepDestroy() { val cbcopy = colorAttachments.map { it } @@ -30,19 +29,26 @@ fun RenderTarget.deepDestroy() { destroy() } +enum class LayerType { + LAYER, + ASIDE +} + /** * A single layer representation */ @Description("Layer") -open class Layer internal constructor(val bufferMultisample: BufferMultisample = BufferMultisample.Disabled) { - var copyLayers: List = listOf() +open class Layer internal constructor( + val type: LayerType, + val bufferMultisample: BufferMultisample = BufferMultisample.Disabled +) { var sourceOut = SourceOut() var sourceIn = SourceIn() var maskLayer: Layer? = null var drawFunc: () -> Unit = {} val children: MutableList = mutableListOf() var blendFilter: Pair Unit>? = null - val postFilters: MutableList Unit>> = mutableListOf() + val postFilters: MutableList, Filter.() -> Unit>> = mutableListOf() var colorType = ColorType.UINT8 private var unresolvedAccumulation: ColorBuffer? = null var accumulation: ColorBuffer? = null @@ -63,7 +69,7 @@ open class Layer internal constructor(val bufferMultisample: BufferMultisample = /** * draw the layer */ - fun drawLayer(drawer: Drawer) { + protected fun drawLayer(drawer: Drawer, cache: ColorBufferCache) { if (!enabled) { return } @@ -75,25 +81,6 @@ open class Layer internal constructor(val bufferMultisample: BufferMultisample = } layerTarget?.let { target -> - if (copyLayers.isNotEmpty()) { - copyLayers.forEach { - drawer.isolatedWithTarget(target) { - clearColor?.let { - drawer.clear(it) - } - - it.layerTarget?.let { copyTarget -> - if (it.bufferMultisample == BufferMultisample.Disabled) { - drawer.image(copyTarget.colorBuffer(0)) - } else { - copyTarget.colorBuffer(0).copyTo(it.accumulation!!) - drawer.image(it.accumulation!!) - } - } - } - } - } - maskLayer?.let { if (it.shouldCreateLayerTarget(activeRenderTarget)) { it.createLayerTarget(activeRenderTarget, drawer, it.bufferMultisample) @@ -101,11 +88,6 @@ open class Layer internal constructor(val bufferMultisample: BufferMultisample = it.layerTarget?.let { maskRt -> drawer.isolatedWithTarget(maskRt) { - if (copyLayers.isEmpty()) { - clearColor?.let { color -> - drawer.clear(color) - } - } drawer.fill = ColorRGBa.WHITE drawer.stroke = ColorRGBa.WHITE it.drawFunc() @@ -114,48 +96,34 @@ open class Layer internal constructor(val bufferMultisample: BufferMultisample = } drawer.isolatedWithTarget(target) { - if (copyLayers.isEmpty()) { - clearColor?.let { - drawer.clear(it) - } + children.filter { it.type == LayerType.ASIDE }.forEach { + it.drawLayer(drawer, cache) + } + + clearColor?.let { + drawer.clear(it) } drawFunc() - children.forEach { - it.drawLayer(drawer) + children.filter { it.type == LayerType.LAYER }.forEach { + it.drawLayer(drawer, cache) } } - if (postFilters.size > 0) { - val sizeMismatch = postBufferCache.isNotEmpty() - && (postBufferCache[0].width != activeRenderTarget.width - || postBufferCache[0].height != activeRenderTarget.height) - - if (sizeMismatch) { - postBufferCache.forEach { it.destroy() } - postBufferCache.clear() + val layerPost = if (postFilters.isEmpty()) target.colorBuffer(0) else postFilters.let { filters -> + val targets = cache[ColorBufferCacheKey(colorType, target.contentScale)] + targets.forEach { + it.fill(ColorRGBa.TRANSPARENT) } - - if (postBufferCache.isEmpty()) { - postBufferCache += persistent { - colorBuffer(activeRenderTarget.width, activeRenderTarget.height, - activeRenderTarget.contentScale, type = colorType) - } - postBufferCache += persistent { - colorBuffer(activeRenderTarget.width, activeRenderTarget.height, - activeRenderTarget.contentScale, type = colorType) - } + var localSource = target.colorBuffer(0) + for ((i, filter) in filters.withIndex()) { + filter.first.apply(filter.third) + val sources = + arrayOf(localSource) + filter.second.map { it.result ?: error("no result for layer $it") } + .toTypedArray() + filter.first.apply(sources, arrayOf(targets[i % targets.size])) + localSource = targets[i % targets.size] } - } - - val layerPost = postFilters.let { filters -> - val targets = postBufferCache - val result = filters.foldIndexed(target.colorBuffer(0)) { i, source, filter -> - val localTarget = targets[i % targets.size] - filter.first.apply(filter.second) - filter.first.apply(source, localTarget) - localTarget - } - result + targets[postFilters.lastIndex % targets.size] } maskLayer?.let { @@ -163,41 +131,52 @@ open class Layer internal constructor(val bufferMultisample: BufferMultisample = maskFilter.apply(arrayOf(layerPost, it.layerTarget!!.colorBuffer(0)), layerPost) } - val localBlendFilter = blendFilter - if (localBlendFilter == null) { - drawer.isolatedWithTarget(activeRenderTarget) { - drawer.ortho() - drawer.view = Matrix44.IDENTITY - drawer.model = Matrix44.IDENTITY + if (type == LayerType.ASIDE) { + if (postFilters.isNotEmpty()) { + require(layerPost != result) + layerPost.copyTo(result ?: error("no result")) + } + } else if (type == LayerType.LAYER) { + val localBlendFilter = blendFilter + if (localBlendFilter == null) { + drawer.isolated { + drawer.defaults() + if (bufferMultisample == BufferMultisample.Disabled) { + drawer.image(layerPost, layerPost.bounds, drawer.bounds) + } else { + layerPost.copyTo(accumulation!!) + drawer.image(accumulation!!, layerPost.bounds, drawer.bounds) + } + } + } else { + localBlendFilter.first.apply(localBlendFilter.second) + activeRenderTarget.colorBuffer(0).copyTo(unresolvedAccumulation!!) if (bufferMultisample == BufferMultisample.Disabled) { - drawer.image(layerPost, layerPost.bounds, drawer.bounds) + localBlendFilter.first.apply( + arrayOf(unresolvedAccumulation!!, layerPost), + unresolvedAccumulation!! + ) } else { layerPost.copyTo(accumulation!!) - drawer.image(accumulation!!, layerPost.bounds, drawer.bounds) + localBlendFilter.first.apply( + arrayOf(unresolvedAccumulation!!, accumulation!!), + unresolvedAccumulation!! + ) } - } - } else { - localBlendFilter.first.apply(localBlendFilter.second) - activeRenderTarget.colorBuffer(0).copyTo(unresolvedAccumulation!!) - if (bufferMultisample == BufferMultisample.Disabled) { - localBlendFilter.first.apply(arrayOf(unresolvedAccumulation!!, layerPost), unresolvedAccumulation!!) - } else { - layerPost.copyTo(accumulation!!) - localBlendFilter.first.apply(arrayOf(unresolvedAccumulation!!, accumulation!!), unresolvedAccumulation!!) - } - if (activeRenderTarget !is ProgramRenderTarget) { - unresolvedAccumulation!!.copyTo(target.colorBuffer(0)) + if (activeRenderTarget !is ProgramRenderTarget) { + unresolvedAccumulation!!.copyTo(target.colorBuffer(0)) + } + unresolvedAccumulation!!.copyTo(activeRenderTarget.colorBuffer(0)) } - unresolvedAccumulation!!.copyTo(activeRenderTarget.colorBuffer(0)) } } } private fun shouldCreateLayerTarget(activeRenderTarget: RenderTarget): Boolean { return layerTarget == null - || ((layerTarget?.width != activeRenderTarget.width || layerTarget?.height != activeRenderTarget.height) - && activeRenderTarget.width > 0 && activeRenderTarget.height > 0) + || ((layerTarget?.width != activeRenderTarget.width || layerTarget?.height != activeRenderTarget.height) + && activeRenderTarget.width > 0 && activeRenderTarget.height > 0) } private fun createLayerTarget( @@ -229,26 +208,72 @@ open class Layer internal constructor(val bufferMultisample: BufferMultisample = } } } + + fun Drawer.image(layer: Layer) { + val cb = layer.result + if (cb != null) { + image(cb) + } + } } /** * create a layer within the composition */ -fun Layer.layer(function: Layer.() -> Unit): Layer { - val layer = Layer().apply { function() } +fun Layer.layer( + colorType: ColorType = this.colorType, + multisample: BufferMultisample = BufferMultisample.Disabled, + function: Layer.() -> Unit +): Layer { + val layer = Layer(LayerType.LAYER, multisample).apply { function() } + layer.colorType = colorType children.add(layer) return layer } -/** - * create a layer within the composition with a custom [BufferMultisample] - */ -fun Layer.layer(bufferMultisample: BufferMultisample, function: Layer.() -> Unit): Layer { - val layer = Layer(bufferMultisample).apply { function() } +fun Layer.aside( + colorType: ColorType = this.colorType, + multisample: BufferMultisample = BufferMultisample.Disabled, + function: Layer.() -> Unit +): Layer { + val layer = Layer(LayerType.ASIDE, multisample).apply { function() } + layer.colorType = colorType children.add(layer) return layer } +fun Layer.apply(drawer: Drawer, + filter: T, source: Layer, colorType: ColorType = this.colorType, + function: T.() -> Unit +): Layer { + val layer = Layer(LayerType.ASIDE) + layer.colorType = colorType + layer.draw { + drawer.image(source.result!!) + //source.result!!.copyTo(result!!) + } + layer.post(filter, function) + children.add(layer) + return layer +} + +fun Layer.apply(drawer: Drawer, + filter: T, source0: Layer, source1:Layer, colorType: ColorType = this.colorType, + function: T.() -> Unit +): Layer { + val layer = Layer(LayerType.ASIDE) + layer.colorType = colorType + layer.draw { + //source0.result!!.copyTo(result!!) + drawer.image(source0.result!!) + } + layer.post(filter, source1, function) + children.add(layer) + return layer +} + + + /** * set the draw contents of the layer */ @@ -256,18 +281,12 @@ fun Layer.draw(function: () -> Unit) { drawFunc = function } -/** - * use the layer as a base - */ -fun Layer.use(vararg layer: Layer) { - copyLayers = layer.toList() -} /** * the drawing acts as a mask on the layer */ fun Layer.mask(function: () -> Unit) { - maskLayer = Layer().apply { + maskLayer = Layer(LayerType.LAYER).apply { this.drawFunc = function } } @@ -275,24 +294,73 @@ fun Layer.mask(function: () -> Unit) { /** * add a post-processing filter to the layer */ -fun Layer.post(filter: F, configure: F.() -> Unit = {}): F { +fun Layer.post(filter: F, configure: F.() -> Unit = {}): F { @Suppress("UNCHECKED_CAST") - postFilters.add(Pair(filter as Filter, configure as Filter.() -> Unit)) + postFilters.add(Triple(filter as Filter, emptyArray(), configure as Filter.() -> Unit)) return filter } +fun Layer.post(filter: F, input1: Layer, configure: F.() -> Unit = {}): F { + require(input1.type == LayerType.ASIDE) + @Suppress("UNCHECKED_CAST") + postFilters.add(Triple(filter as Filter, arrayOf(input1), configure as Filter.() -> Unit)) + return filter +} + +fun Layer.post(filter: F, input1: Layer, input2: Layer, configure: F.() -> Unit = {}): F { + require(input1.type == LayerType.ASIDE) + require(input2.type == LayerType.ASIDE) + @Suppress("UNCHECKED_CAST") + postFilters.add(Triple(filter as Filter, arrayOf(input1, input2), configure as Filter.() -> Unit)) + return filter +} + + /** * add a blend filter to the layer */ -fun Layer.blend(filter: F, configure: F.() -> Unit = {}): F { +fun Layer.blend(filter: F, configure: F.() -> Unit = {}): F { @Suppress("UNCHECKED_CAST") blendFilter = Pair(filter as Filter, configure as Filter.() -> Unit) return filter } -class Composite : Layer() { +data class ColorBufferCacheKey( + val colorType: ColorType, + val contentScale: Double +) + +class ColorBufferCache(val width: Int, val height: Int) { + val cache = mutableMapOf>() + + operator fun get(key: ColorBufferCacheKey): List { + return cache.getOrPut(key) { + listOf( + colorBuffer(width, height, type = key.colorType, contentScale = key.contentScale), + colorBuffer(width, height, type = key.colorType, contentScale = key.contentScale), + ) + } + } + + fun destroy() { + cache.forEach { + it.value.forEach { cb -> cb.destroy() } + } + } + +} + +class Composite : Layer(LayerType.LAYER) { + + private var cache = ColorBufferCache(RenderTarget.active.width, RenderTarget.active.height) fun draw(drawer: Drawer) { - drawLayer(drawer) + + if (cache.width != RenderTarget.active.width || cache.height != RenderTarget.active.height) { + cache.destroy() + cache = ColorBufferCache(RenderTarget.active.width, RenderTarget.active.height) + } + + drawLayer(drawer, cache) } } @@ -305,6 +373,9 @@ fun compose(function: Layer.() -> Unit): Composite { return root } + + + class Compositor : Extension { override var enabled: Boolean = true var composite = Composite() @@ -312,7 +383,7 @@ class Compositor : Extension { override fun afterDraw(drawer: Drawer, program: Program) { drawer.isolated { drawer.defaults() - composite.drawLayer(drawer) + composite.draw(drawer) } } } diff --git a/orx-compositor/src/demo/kotlin/DemoAside01.kt b/orx-compositor/src/demo/kotlin/DemoAside01.kt new file mode 100644 index 00000000..426b92f6 --- /dev/null +++ b/orx-compositor/src/demo/kotlin/DemoAside01.kt @@ -0,0 +1,37 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.ColorType +import org.openrndr.extra.compositor.* +import org.openrndr.extra.fx.blur.DirectionalBlur +import org.openrndr.extra.fx.blur.HashBlurDynamic +import org.openrndr.extra.fx.patterns.Checkers +import kotlin.math.cos + +fun main() { + application { + program { + val c = compose { + layer { + val a = aside(colorType = ColorType.FLOAT32) { + post(Checkers()) { + this.size = cos(seconds)*0.5 + 0.5 + } + } + draw { + drawer.clear(ColorRGBa.GRAY.shade(0.5)) + drawer.circle(width/2.0, height/2.0, 100.0) + } + post(HashBlurDynamic(), a) { + time = seconds + radius = 25.0 + } + } + } + extend { + c.draw(drawer) + } + } + } + + +} \ No newline at end of file diff --git a/orx-compositor/src/demo/kotlin/DemoCompositor01.kt b/orx-compositor/src/demo/kotlin/DemoCompositor01.kt index 43c924f4..3cf4b2d8 100644 --- a/orx-compositor/src/demo/kotlin/DemoCompositor01.kt +++ b/orx-compositor/src/demo/kotlin/DemoCompositor01.kt @@ -23,13 +23,6 @@ fun main() = application { } program { - // -- this block is for automation purposes only - if (System.getProperty("takeScreenshot") == "true") { - extend(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } - } - data class Item(var pos: Vector3, val color: ColorRGBa) { fun draw(drawer: Drawer) { pos -= Vector3(pos.z * 3.0, 0.0, 0.0) diff --git a/orx-compositor/src/demo/kotlin/DemoCompositor02.kt b/orx-compositor/src/demo/kotlin/DemoCompositor02.kt index c1e6dd32..ec152ceb 100644 --- a/orx-compositor/src/demo/kotlin/DemoCompositor02.kt +++ b/orx-compositor/src/demo/kotlin/DemoCompositor02.kt @@ -21,7 +21,7 @@ fun main() = application { program { val layers = compose { - layer(BufferMultisample.SampleCount(16)) { + layer(multisample = BufferMultisample.SampleCount(16)) { draw { drawer.translate(drawer.bounds.center) drawer.rotate(seconds) @@ -29,7 +29,7 @@ fun main() = application { drawer.rectangle(Rectangle.fromCenter(Vector2.ZERO, 200.0)) } - layer() { + layer { blend(Normal()) { clip = true } diff --git a/orx-compositor/src/demo/kotlin/DemoUse01.kt b/orx-compositor/src/demo/kotlin/DemoUse01.kt deleted file mode 100644 index f275b3f0..00000000 --- a/orx-compositor/src/demo/kotlin/DemoUse01.kt +++ /dev/null @@ -1,90 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.color.rgb -import org.openrndr.extensions.SingleScreenshot -import org.openrndr.extra.compositor.* -import org.openrndr.extra.fx.blend.Add -import org.openrndr.extra.fx.blur.ApproximateGaussianBlur -import org.openrndr.extra.fx.color.ColorCorrection -import kotlin.random.Random - -/** - * Compositor demo of `use`, which makes it possible to - * use the color buffer of a previous layer. - * - * This program draws a series of concentric rings, most of them gray, - * 10% are pink. - * - * In a second layer we reuse that image with rings, applying an extreme - * color correction to make everything black except the pink rings, - * then apply a strong blur and finally compose it over the original - * image using blend mode Add. - * - * The result is an sharp image of gray rings with glowing pink rings. - * - * Note: see also orx-fx Bloom() - * - */ - -// Toggle to see the difference between a simple blur and multilayer bloom -const val effectEnabled = true - -fun main() = application { - configure { - width = 900 - height = 900 - } - - program { - // -- this block is for automation purposes only - if (System.getProperty("takeScreenshot") == "true") { - extend(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } - } - - val composite = compose { - val circles = layer { - draw { - drawer.stroke = null - val rnd = Random(frameCount / 100 + 1) - for (i in 18 downTo 0) { - drawer.fill = if (rnd.nextDouble() < 0.1) - ColorRGBa.PINK.shade(Random.nextDouble(0.88, 1.0)) - else - rgb(rnd.nextInt(6) / 15.0) - - drawer.circle(drawer.bounds.center, 50.0 + i * 20) - } - } - // A. To see how the plain blur looks like - if (!effectEnabled) { - post(ApproximateGaussianBlur()) { - sigma = 25.0 - window = 25 - } - } - } - // B. This is the bloom effect - if (effectEnabled) { - layer { - use(circles) // <-- use the previous layer as starting point - post(ColorCorrection()) { - brightness = -0.3 - contrast = 0.8 - } - post(ApproximateGaussianBlur()) { - sigma = 25.0 - window = 25 - } - blend(Add()) - } - } - } - - extend { - drawer.clear(rgb(0.2)) - composite.draw(drawer) - } - } -}