diff --git a/orx-fx/README.md b/orx-fx/README.md index d810d149..340d8986 100644 --- a/orx-fx/README.md +++ b/orx-fx/README.md @@ -48,6 +48,8 @@ Most blur effects are opacity preserving - `BoxBlur`, a simple but fast box blur - `GaussianBlur`, a slow but precise Gaussian blur - `HashBlur`, a noisy blur effect + - `ZoomBlur`, a directional blur with a zooming effect + ### Color - `ChromaticAberration`, a chromatic aberration effect based on RGB color separation @@ -57,6 +59,7 @@ Most blur effects are opacity preserving - `LumaMap`, maps luminosity to two colors - `LumaOpacity`, maps luminosity to opacity but retains source color - `LumaThreshold`, applies a treshold on the input luminosity and maps to two colors + - `Sepia`, applies a reddish-brown monochrome tint that imitates an old photograph - `SubtractConstant`, subtract a constant color from the source color ### Distortion @@ -74,6 +77,7 @@ All distortion effects are opacity preserving ### Edges - `LumaSobel` - A Sobel-kernel based luminosity edge detector + - `EdgesWork` - An edges filter doubling as erosion ### Grain - `FilmGrain` - adds film-like grain to the source input diff --git a/orx-fx/src/main/kotlin/FilterTools.kt b/orx-fx/src/main/kotlin/FilterTools.kt index 6e33a93d..89cbb5c6 100644 --- a/orx-fx/src/main/kotlin/FilterTools.kt +++ b/orx-fx/src/main/kotlin/FilterTools.kt @@ -1,5 +1,7 @@ package org.openrndr.extra.fx +import org.openrndr.draw.ColorFormat +import org.openrndr.draw.ColorType import org.openrndr.resourceUrl import java.net.URL @@ -8,4 +10,6 @@ internal class FilterTools internal fun filterFragmentCode(resourceId: String): String { val urlString = resourceUrl("gl3/$resourceId", FilterTools::class.java) return URL(urlString).readText() -} \ No newline at end of file +} + +internal data class ColorBufferDescription(val width: Int, val height: Int, val contentScale: Double, val format: ColorFormat, val type: ColorType) diff --git a/orx-fx/src/main/kotlin/blur/ApproximateGaussianBlur.kt b/orx-fx/src/main/kotlin/blur/ApproximateGaussianBlur.kt index a52bfa77..795d6df1 100644 --- a/orx-fx/src/main/kotlin/blur/ApproximateGaussianBlur.kt +++ b/orx-fx/src/main/kotlin/blur/ApproximateGaussianBlur.kt @@ -2,6 +2,7 @@ package org.openrndr.extra.fx.blur import org.openrndr.draw.* import org.openrndr.extra.fx.filterFragmentCode +import org.openrndr.extra.fx.ColorBufferDescription import org.openrndr.extra.parameters.Description import org.openrndr.extra.parameters.DoubleParameter import org.openrndr.extra.parameters.IntParameter @@ -14,9 +15,6 @@ import org.openrndr.math.Vector2 @Description("Approximate Gaussian blur") class ApproximateGaussianBlur : Filter(Shader.createFromCode(Filter.filterVertexCode, filterFragmentCode("blur/approximate-gaussian-blur.frag"))) { - - data class ColorBufferDescription(val width: Int, val height: Int, val contentScale: Double, val format: ColorFormat, val type: ColorType) - /** * blur sample window, default value is 5 */ @@ -43,8 +41,6 @@ class ApproximateGaussianBlur : Filter(Shader.createFromCode(Filter.filterVertex private var intermediateCache = mutableMapOf() - - init { window = 5 spread = 1.0 diff --git a/orx-fx/src/main/kotlin/blur/BoxBlur.kt b/orx-fx/src/main/kotlin/blur/BoxBlur.kt index 2d37a07e..ef23dc8c 100644 --- a/orx-fx/src/main/kotlin/blur/BoxBlur.kt +++ b/orx-fx/src/main/kotlin/blur/BoxBlur.kt @@ -35,7 +35,7 @@ class BoxBlur : Filter(Shader.createFromCode(Filter.filterVertexCode, @DoubleParameter("gain", 0.0, 4.0) var gain: Double by parameters - private var intermediateCache = mutableMapOf() + private var intermediateCache = mutableMapOf() init { window = 5 @@ -44,7 +44,7 @@ class BoxBlur : Filter(Shader.createFromCode(Filter.filterVertexCode, } override fun apply(source: Array, target: Array) { - val intermediateDescription = ApproximateGaussianBlur.ColorBufferDescription(target[0].width, target[0].height, target[0].contentScale, target[0].format, target[0].type) + val intermediateDescription = ColorBufferDescription(target[0].width, target[0].height, target[0].contentScale, target[0].format, target[0].type) val intermediate = intermediateCache.getOrPut(intermediateDescription) { colorBuffer(target[0].width, target[0].height, target[0].contentScale, target[0].format, target[0].type) } diff --git a/orx-fx/src/main/kotlin/blur/ZoomBlur.kt b/orx-fx/src/main/kotlin/blur/ZoomBlur.kt new file mode 100644 index 00000000..5cbfda01 --- /dev/null +++ b/orx-fx/src/main/kotlin/blur/ZoomBlur.kt @@ -0,0 +1,45 @@ +package org.openrndr.extra.fx.blur + +import org.openrndr.draw.ColorBuffer +import org.openrndr.draw.Filter +import org.openrndr.draw.Shader +import org.openrndr.draw.colorBuffer +import org.openrndr.extra.fx.filterFragmentCode +import org.openrndr.extra.parameters.Description +import org.openrndr.extra.parameters.DoubleParameter +import org.openrndr.math.Vector2 + +@Description("Zoom Blur") +class ZoomBlur : Filter(Shader.createFromCode(Filter.filterVertexCode, filterFragmentCode("blur/zoom-blur.frag"))) { + var center: Vector2 by parameters + + @DoubleParameter("strength", 0.0, 1.0) + var strength: Double by parameters + + init { + center = Vector2.ONE / 2.0 + strength = 0.2 + } + + private var intermediate: ColorBuffer? = null + + override fun apply(source: Array, target: Array) { + intermediate?.let { + if (it.width != target[0].width || it.height != target[0].height) { + intermediate = null + } + } + + if (intermediate == null) { + intermediate = colorBuffer(target[0].width, target[0].height, target[0].contentScale, target[0].format, target[0].type) + } + + intermediate?.let { + parameters["dimensions"] = Vector2(it.effectiveWidth.toDouble(), it.effectiveHeight.toDouble()) + + super.apply(source, arrayOf(it)) + + it.copyTo(target[0]) + } + } +} \ No newline at end of file diff --git a/orx-fx/src/main/kotlin/color/Sepia.kt b/orx-fx/src/main/kotlin/color/Sepia.kt new file mode 100644 index 00000000..b3024d30 --- /dev/null +++ b/orx-fx/src/main/kotlin/color/Sepia.kt @@ -0,0 +1,17 @@ +package org.openrndr.extra.fx.color + +import org.openrndr.draw.Filter +import org.openrndr.draw.Shader +import org.openrndr.extra.fx.filterFragmentCode +import org.openrndr.extra.parameters.Description +import org.openrndr.extra.parameters.DoubleParameter + +@Description("Sepia") +class Sepia : Filter(Shader.createFromCode(Filter.filterVertexCode, filterFragmentCode("color/sepia.frag"))) { + @DoubleParameter("amount", 0.0, 1.0) + var amount: Double by parameters + + init { + amount = 0.5 + } +} \ No newline at end of file diff --git a/orx-fx/src/main/kotlin/edges/EdgesWork.kt b/orx-fx/src/main/kotlin/edges/EdgesWork.kt new file mode 100644 index 00000000..73ab6067 --- /dev/null +++ b/orx-fx/src/main/kotlin/edges/EdgesWork.kt @@ -0,0 +1,53 @@ +package org.openrndr.extra.fx.edges + +import org.openrndr.draw.* +import org.openrndr.extra.fx.filterFragmentCode +import org.openrndr.extra.fx.ColorBufferDescription +import org.openrndr.extra.parameters.Description +import org.openrndr.extra.parameters.IntParameter +import org.openrndr.math.Vector2 + + +internal class EdgesWork1 : Filter(Shader.createFromCode(filterVertexCode, filterFragmentCode("edges/edges-work-1.frag"))) { + var delta: Vector2 by parameters + + init { + delta = Vector2.ZERO + } +} + +@Description("Edges Work") +open class EdgesWork : Filter(Shader.createFromCode(filterVertexCode, filterFragmentCode("edges/edges-work-2.frag"))) { + /** + * radius, default value is 1.0 + */ + @IntParameter("radius", 1, 400) + var radius: Int by parameters + + private var delta: Vector2 by parameters + + private val work1 = EdgesWork1() + + private var intermediateCache = mutableMapOf() + + init { + radius = 1 + delta = Vector2.ZERO + } + + override fun apply(source: Array, target: Array) { + val intermediateDescription = ColorBufferDescription(target[0].width, target[0].height, target[0].contentScale, target[0].format, target[0].type) + val intermediate = intermediateCache.getOrPut(intermediateDescription) { + colorBuffer(target[0].width, target[0].height, target[0].contentScale, target[0].format, target[0].type) + } + + intermediate.let { + work1.delta = Vector2(radius / it.effectiveWidth.toDouble(), 0.0) + work1.apply(source, arrayOf(it)) + + parameters["delta"] = Vector2(0.0, radius / it.effectiveHeight.toDouble()) + super.apply(arrayOf(it), target) + } + } +} + diff --git a/orx-fx/src/main/kotlin/edges/LumaSobel.kt b/orx-fx/src/main/kotlin/edges/LumaSobel.kt index 3ca68afa..f76a5053 100644 --- a/orx-fx/src/main/kotlin/edges/LumaSobel.kt +++ b/orx-fx/src/main/kotlin/edges/LumaSobel.kt @@ -8,7 +8,7 @@ import org.openrndr.extra.parameters.ColorParameter import org.openrndr.extra.parameters.Description import org.openrndr.extra.parameters.DoubleParameter -@Description("Luma threshold ") +@Description("Luma Sobel") class LumaSobel : Filter(Shader.createFromCode(Filter.filterVertexCode, filterFragmentCode("edges/luma-sobel.frag"))) { @ColorParameter("background color") diff --git a/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/blur/zoom-blur.frag b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/blur/zoom-blur.frag new file mode 100644 index 00000000..f29f4800 --- /dev/null +++ b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/blur/zoom-blur.frag @@ -0,0 +1,41 @@ +#version 330 core + +in vec2 v_texCoord0; +uniform sampler2D tex0; // input +uniform vec2 center; +uniform float strength; +uniform vec2 dimensions; + +out vec4 o_color; + +float random(vec3 scale, float seed) { + /* use the fragment position for a different seed per-pixel */ + return fract(sin(dot(gl_FragCoord.xyz + seed, scale)) * 43758.5453 + seed); +} + +// Implementation by Evan Wallace (glfx.js) +void main() { + vec4 color = vec4(0.0); + float total = 0.0; + vec2 toCenter = center - v_texCoord0; + + /* randomize the lookup values to hide the fixed number of samples */ + float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0); + + for (float t = 0.0; t <= 40.0; t++) { + float percent = (t + offset) / 40.0; + float weight = 4.0 * (percent - percent * percent); + vec4 tex = texture(tex0, v_texCoord0 + toCenter * percent * strength); + + /* switch to pre-multiplied alpha to correctly blur transparent images */ + tex.rgb *= tex.a; + + color += tex * weight; + total += weight; + } + + o_color = color / total; + + /* switch back from pre-multiplied alpha */ + o_color.rgb /= o_color.a + 0.00001; +} \ No newline at end of file diff --git a/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/color/sepia.frag b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/color/sepia.frag new file mode 100644 index 00000000..7a954112 --- /dev/null +++ b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/color/sepia.frag @@ -0,0 +1,20 @@ +#version 330 core + +in vec2 v_texCoord0; +uniform sampler2D tex0; // input +uniform float amount; +out vec4 o_color; + +// Implementation by Evan Wallace (glfx.js) +void main() { + vec4 color = texture(tex0, v_texCoord0); + float r = color.r; + float g = color.g; + float b = color.b; + + color.r = min(1.0, (r * (1.0 - (0.607 * amount))) + (g * (0.769 * amount)) + (b * (0.189 * amount))); + color.g = min(1.0, (r * 0.349 * amount) + (g * (1.0 - (0.314 * amount))) + (b * 0.168 * amount)); + color.b = min(1.0, (r * 0.272 * amount) + (g * 0.534 * amount) + (b * (1.0 - (0.869 * amount)))); + + o_color = color; +} \ No newline at end of file diff --git a/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/edges/edges-work-1.frag b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/edges/edges-work-1.frag new file mode 100644 index 00000000..696163ad --- /dev/null +++ b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/edges/edges-work-1.frag @@ -0,0 +1,37 @@ +#version 330 core +uniform sampler2D tex0; +in vec2 v_texCoord0; +out vec4 o_color; + +uniform vec2 delta; + +float random(vec3 scale, float seed) { + /* use the fragment position for a different seed per-pixel */ + return fract(sin(dot(gl_FragCoord.xyz + seed, scale)) * 43758.5453 + seed); +} + +// Implementation by Evan Wallace (glfx.js) +void main() { + vec2 color = vec2(0.0); + vec2 total = vec2(0.0); + + /* randomize the lookup values to hide the fixed number of samples */ + float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0); + + for (float t = -30.0; t <= 30.0; t++) { + float percent = (t + offset - 0.5) / 30.0; + float weight = 1.0 - abs(percent); + vec3 tex = texture(tex0, v_texCoord0 + delta * percent).rgb; + float average = (tex.r + tex.g + tex.b) / 3.0; + color.x += average * weight; + total.x += weight; + + if (abs(t) < 15.0) { + weight = weight * 2.0 - 1.0; + color.y += average * weight; + total.y += weight; + } + } + + o_color = vec4(color / total, 0.0, 1.0); +} \ No newline at end of file diff --git a/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/edges/edges-work-2.frag b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/edges/edges-work-2.frag new file mode 100644 index 00000000..7a47d252 --- /dev/null +++ b/orx-fx/src/main/resources/org/openrndr/extra/fx/gl3/edges/edges-work-2.frag @@ -0,0 +1,38 @@ +#version 330 core +uniform sampler2D tex0; +in vec2 v_texCoord0; +out vec4 o_color; + +uniform vec2 delta; +uniform int radius; + +float random(vec3 scale, float seed) { + /* use the fragment position for a different seed per-pixel */ + return fract(sin(dot(gl_FragCoord.xyz + seed, scale)) * 43758.5453 + seed); +} + +// Implementation by Evan Wallace (glfx.js) +void main() { + vec2 color = vec2(0.0); + vec2 total = vec2(0.0); + + /* randomize the lookup values to hide the fixed number of samples */ + float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0); + + for (float t = -30.0; t <= 30.0; t++) { + float percent = (t + offset - 0.5) / 30.0; + float weight = 1.0 - abs(percent); + vec2 tex = texture(tex0, v_texCoord0 + delta * percent).xy; + color.x += tex.x * weight; + total.x += weight; + + if (abs(t) < 15.0) { + weight = weight * 2.0 - 1.0; + color.y += tex.y * weight; + total.y += weight; + } + } + float c = clamp(10000.0 * (color.y / total.y - color.x / total.x) + 0.5, 0.0, 1.0); + + o_color = vec4(c, c, c, 1.0); +} \ No newline at end of file