From 01cb305e945f7cff286aaf2a516f78ddeff5d272 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Thu, 14 Dec 2023 16:11:59 +0100 Subject: [PATCH] [orx-fx] Add BlendSpectral --- .../commonMain/kotlin/blend/BlendSpectral.kt | 104 +++++++++ .../src/jvmDemo/kotlin/DemoSpectralBlend01.kt | 43 ++++ orx-fx/src/shaders/glsl/blend/spectral.frag | 209 ++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 orx-fx/src/commonMain/kotlin/blend/BlendSpectral.kt create mode 100644 orx-fx/src/jvmDemo/kotlin/DemoSpectralBlend01.kt create mode 100644 orx-fx/src/shaders/glsl/blend/spectral.frag diff --git a/orx-fx/src/commonMain/kotlin/blend/BlendSpectral.kt b/orx-fx/src/commonMain/kotlin/blend/BlendSpectral.kt new file mode 100644 index 00000000..4089d8a7 --- /dev/null +++ b/orx-fx/src/commonMain/kotlin/blend/BlendSpectral.kt @@ -0,0 +1,104 @@ +package org.openrndr.extra.fx.blend + +import org.openrndr.draw.Filter2to1 +import org.openrndr.draw.filterShaderFromCode +import org.openrndr.extra.fx.fx_spectral +import org.openrndr.extra.parameters.BooleanParameter +import org.openrndr.extra.parameters.Description +import org.openrndr.extra.parameters.DoubleParameter + +private val spectralBlendShader = """// includes +${fx_spectral} +// filter code +uniform sampler2D tex0; +uniform sampler2D tex1; +in vec2 v_texCoord0; + +out vec4 o_color; + +uniform bool linearizeInputA; +uniform bool linearizeInputB; +uniform bool delinearizeOutput; +uniform float fill; +uniform bool clip; + +vec3 srgb_to_linear(vec3 c) { + const float t = 0.00313066844250063; + return vec3( + c.r <= t ? c.r / 12.92 : pow((c.r + 0.055) / 1.055, 2.4), + c.g <= t ? c.g / 12.92 : pow((c.g + 0.055) / 1.055, 2.4), + c.b <= t ? c.b / 12.92 : pow((c.b + 0.055) / 1.055, 2.4)); +} + +vec3 linear_to_srgb(vec3 c) { + const float t = 0.00313066844250063; + return vec3( + c.r <= t ? c.r * 12.92 : 1.055 * pow(c.r, 1 / 2.4) - 0.055, + c.g <= t ? c.g * 12.92 : 1.055 * pow(c.g, 1 / 2.4) - 0.055, + c.b <= t ? c.b * 12.92 : 1.055 * pow(c.b, 1 / 2.4) - 0.055 + ); +} + +void main() { + vec4 a = texture(tex0, v_texCoord0); + vec4 b = texture(tex1, v_texCoord0); + if (linearizeInputA) { + a.rgb = srgb_to_linear(a.rgb); + } + + if (linearizeInputB) { + b.rgb = srgb_to_linear(b.rgb); + } + + // depremultiply alpha + vec3 na = a.a == 0.0 ? vec3(0.0): a.rgb / a.a; + vec3 nb = b.a == 0.0 ? vec3(0.0): b.rgb / b.a; + + vec4 mixed = vec4(spectral_mix(na, nb, min(1.0, b.a * fill) ), min(a.a, b.a)); + + // premultiply alpha + mixed.rgb *= mixed.a; + + if (!clip) { + vec4 b_over_a = a * (1.0 - b.a) + b; + mixed = b_over_a * (1.0-mixed.a) + mixed; + } + + if (delinearizeOutput) { + mixed.rgb = linear_to_srgb(mixed.rgb); + } + + o_color = mixed; +} + +""" + +/** + * Blend based on pigment simulation + */ +@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED") +@Description("Blend spectral") +class BlendSpectral : Filter2to1(filterShaderFromCode(spectralBlendShader, "color-burn")) { + @BooleanParameter("source clip") + var clip: Boolean by parameters + + @BooleanParameter("linearize input A") + var linearizeInputA: Boolean by parameters + + @BooleanParameter("linearize input B") + var linearizeInputB: Boolean by parameters + + @BooleanParameter("delinearize output") + var delinearizeOutput: Boolean by parameters + + @DoubleParameter("fill", 0.0, 1.0) + var fill: Double by parameters + + init { + clip = false + linearizeInputA = true + linearizeInputB = true + delinearizeOutput = true + fill = 1.0 + } +} \ No newline at end of file diff --git a/orx-fx/src/jvmDemo/kotlin/DemoSpectralBlend01.kt b/orx-fx/src/jvmDemo/kotlin/DemoSpectralBlend01.kt new file mode 100644 index 00000000..c92b8dde --- /dev/null +++ b/orx-fx/src/jvmDemo/kotlin/DemoSpectralBlend01.kt @@ -0,0 +1,43 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.createEquivalent +import org.openrndr.drawImage +import org.openrndr.extra.fx.blend.BlendSpectral +import org.openrndr.extra.fx.patterns.Checkers +import org.openrndr.math.Vector2 +import kotlin.math.sin + +fun main() { + application { + configure { + width = 800 + height = 800 + } + program { + val a = drawImage(width, height) { + drawer.stroke = null + drawer.fill = ColorRGBa.BLUE + drawer.circle(drawer.bounds.center - Vector2(100.0, 0.0) , drawer.width * 0.25) + } + val b = drawImage(width, height) { + drawer.clear(ColorRGBa.TRANSPARENT) + drawer.stroke = ColorRGBa.RED + drawer.strokeWeight = 10.0 + drawer.fill = ColorRGBa.YELLOW.opacify(1.0) + drawer.circle(drawer.bounds.center + Vector2(100.0, 0.0), drawer.width * 0.25) + } + + val checked = a.createEquivalent() + Checkers().apply(emptyArray(), checked) + + val mixed = a.createEquivalent() + val blendSpectral = BlendSpectral() + extend { + drawer.image(checked) + blendSpectral.fill = sin(seconds) * 0.5 + 0.5 + blendSpectral.apply(a, b, mixed) + drawer.image(mixed) + } + } + } +} \ No newline at end of file diff --git a/orx-fx/src/shaders/glsl/blend/spectral.frag b/orx-fx/src/shaders/glsl/blend/spectral.frag new file mode 100644 index 00000000..eb63d200 --- /dev/null +++ b/orx-fx/src/shaders/glsl/blend/spectral.frag @@ -0,0 +1,209 @@ +// Based on +// https://github.com/rvanwijnen/spectral.js/blob/5fb74962ea348f7385edb32bfd0fd55d8585451c/shaders/spectral.glsl + +// MIT License +// +// Copyright (c) 2023 Ronald van Wijnen +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#ifndef SPECTRAL +#define SPECTRAL + +const int SPECTRAL_SIZE = 38; +const float SPECTRAL_GAMMA = 2.4; +const float SPECTRAL_EPSILON = 0.0001; + +float spectral_uncompand(float x) { + return (x < 0.04045) ? x / 12.92 : pow((x + 0.055) / 1.055, SPECTRAL_GAMMA); +} + +float spectral_compand(float x) { + return (x < 0.0031308) ? x * 12.92 : 1.055 * pow(x, 1.0 / SPECTRAL_GAMMA) - 0.055; +} + +vec3 spectral_srgb_to_linear(vec3 srgb) { + return vec3(spectral_uncompand(srgb[0]), spectral_uncompand(srgb[1]), spectral_uncompand(srgb[2])); +} + +vec3 spectral_linear_to_srgb(vec3 lrgb) { + return clamp(vec3(spectral_compand(lrgb[0]), spectral_compand(lrgb[1]), spectral_compand(lrgb[2])), 0.0, 1.0); +} + +void spectral_upsampling(vec3 lrgb, out float w, out float c, out float m, out float y, out float r, out float g, out float b) { + // EJ: I patched this up to handle out of gamut RGB better. + w = min(lrgb.r, min(lrgb.g, lrgb.b)); + + lrgb -= w; + w = max(0.0, w); + c = max(0.0, min(lrgb.g, lrgb.b)); + m = max(0.0, min(lrgb.r, lrgb.b)); + y = max(0.0, min(lrgb.r, lrgb.g)); + r = min(max(0., lrgb.r - lrgb.b), max(0., lrgb.r - lrgb.g)); + g = min(max(0., lrgb.g - lrgb.b), max(0., lrgb.g - lrgb.r)); + b = min(max(0., lrgb.b - lrgb.g), max(0., lrgb.b - lrgb.r)); +} + +void spectral_linear_to_reflectance(vec3 lrgb, inout float R[SPECTRAL_SIZE]) { + float w, c, m, y, r, g, b; + + spectral_upsampling(lrgb, w, c, m, y, r, g, b); + + R[0] = max(SPECTRAL_EPSILON, w + c * 0.96853629 + m * 0.51567122 + y * 0.02055257 + r * 0.03147571 + g * 0.49108579 + b * 0.97901834); + R[1] = max(SPECTRAL_EPSILON, w + c * 0.96855103 + m * 0.54015520 + y * 0.02059936 + r * 0.03146636 + g * 0.46944057 + b * 0.97901649); + R[2] = max(SPECTRAL_EPSILON, w + c * 0.96859338 + m * 0.62645502 + y * 0.02062723 + r * 0.03140624 + g * 0.40165780 + b * 0.97901118); + R[3] = max(SPECTRAL_EPSILON, w + c * 0.96877345 + m * 0.75595012 + y * 0.02073387 + r * 0.03119611 + g * 0.24490420 + b * 0.97892146); + R[4] = max(SPECTRAL_EPSILON, w + c * 0.96942204 + m * 0.92826996 + y * 0.02114202 + r * 0.03053888 + g * 0.06826880 + b * 0.97858555); + R[5] = max(SPECTRAL_EPSILON, w + c * 0.97143709 + m * 0.97223624 + y * 0.02233154 + r * 0.02856855 + g * 0.02732883 + b * 0.97743705); + R[6] = max(SPECTRAL_EPSILON, w + c * 0.97541862 + m * 0.98616174 + y * 0.02556857 + r * 0.02459485 + g * 0.01360600 + b * 0.97428075); + R[7] = max(SPECTRAL_EPSILON, w + c * 0.98074186 + m * 0.98955255 + y * 0.03330189 + r * 0.01929520 + g * 0.01000187 + b * 0.96663223); + R[8] = max(SPECTRAL_EPSILON, w + c * 0.98580992 + m * 0.98676237 + y * 0.05185294 + r * 0.01423112 + g * 0.01284127 + b * 0.94822893); + R[9] = max(SPECTRAL_EPSILON, w + c * 0.98971194 + m * 0.97312575 + y * 0.10087639 + r * 0.01033111 + g * 0.02636635 + b * 0.89937713); + R[10] = max(SPECTRAL_EPSILON, w + c * 0.99238027 + m * 0.91944277 + y * 0.24000413 + r * 0.00765876 + g * 0.07058713 + b * 0.76070164); + R[11] = max(SPECTRAL_EPSILON, w + c * 0.99409844 + m * 0.32564851 + y * 0.53589066 + r * 0.00593693 + g * 0.70421692 + b * 0.46420440); + R[12] = max(SPECTRAL_EPSILON, w + c * 0.99517200 + m * 0.13820628 + y * 0.79874659 + r * 0.00485616 + g * 0.85473994 + b * 0.20123039); + R[13] = max(SPECTRAL_EPSILON, w + c * 0.99576545 + m * 0.05015143 + y * 0.91186529 + r * 0.00426186 + g * 0.95081565 + b * 0.08808402); + R[14] = max(SPECTRAL_EPSILON, w + c * 0.99593552 + m * 0.02912336 + y * 0.95399623 + r * 0.00409039 + g * 0.97170370 + b * 0.04592894); + R[15] = max(SPECTRAL_EPSILON, w + c * 0.99564041 + m * 0.02421691 + y * 0.97137099 + r * 0.00438375 + g * 0.97651888 + b * 0.02860373); + R[16] = max(SPECTRAL_EPSILON, w + c * 0.99464769 + m * 0.02660696 + y * 0.97939505 + r * 0.00537525 + g * 0.97429245 + b * 0.02060067); + R[17] = max(SPECTRAL_EPSILON, w + c * 0.99229579 + m * 0.03407586 + y * 0.98345207 + r * 0.00772962 + g * 0.97012917 + b * 0.01656701); + R[18] = max(SPECTRAL_EPSILON, w + c * 0.98638762 + m * 0.04835936 + y * 0.98553736 + r * 0.01366120 + g * 0.94258630 + b * 0.01451549); + R[19] = max(SPECTRAL_EPSILON, w + c * 0.96829712 + m * 0.00011720 + y * 0.98648905 + r * 0.03181352 + g * 0.99989207 + b * 0.01357964); + R[20] = max(SPECTRAL_EPSILON, w + c * 0.89228016 + m * 0.00008554 + y * 0.98674535 + r * 0.10791525 + g * 0.99989891 + b * 0.01331243); + R[21] = max(SPECTRAL_EPSILON, w + c * 0.53740239 + m * 0.85267882 + y * 0.98657555 + r * 0.46249516 + g * 0.13823139 + b * 0.01347661); + R[22] = max(SPECTRAL_EPSILON, w + c * 0.15360445 + m * 0.93188793 + y * 0.98611877 + r * 0.84604333 + g * 0.06968113 + b * 0.01387181); + R[23] = max(SPECTRAL_EPSILON, w + c * 0.05705719 + m * 0.94810268 + y * 0.98559942 + r * 0.94275572 + g * 0.05628787 + b * 0.01435472); + R[24] = max(SPECTRAL_EPSILON, w + c * 0.03126539 + m * 0.94200977 + y * 0.98507063 + r * 0.96860996 + g * 0.06111561 + b * 0.01479836); + R[25] = max(SPECTRAL_EPSILON, w + c * 0.02205445 + m * 0.91478045 + y * 0.98460039 + r * 0.97783966 + g * 0.08987709 + b * 0.01515250); + R[26] = max(SPECTRAL_EPSILON, w + c * 0.01802271 + m * 0.87065445 + y * 0.98425301 + r * 0.98187757 + g * 0.13656016 + b * 0.01540513); + R[27] = max(SPECTRAL_EPSILON, w + c * 0.01613460 + m * 0.78827548 + y * 0.98403909 + r * 0.98377315 + g * 0.22169624 + b * 0.01557233); + R[28] = max(SPECTRAL_EPSILON, w + c * 0.01520947 + m * 0.65738359 + y * 0.98388535 + r * 0.98470202 + g * 0.32176956 + b * 0.01565710); + R[29] = max(SPECTRAL_EPSILON, w + c * 0.01475977 + m * 0.59909403 + y * 0.98376116 + r * 0.98515481 + g * 0.36157329 + b * 0.01571025); + R[30] = max(SPECTRAL_EPSILON, w + c * 0.01454263 + m * 0.56817268 + y * 0.98368246 + r * 0.98537114 + g * 0.48361920 + b * 0.01571916); + R[31] = max(SPECTRAL_EPSILON, w + c * 0.01444459 + m * 0.54031997 + y * 0.98365023 + r * 0.98546685 + g * 0.46488579 + b * 0.01572133); + R[32] = max(SPECTRAL_EPSILON, w + c * 0.01439897 + m * 0.52110241 + y * 0.98361309 + r * 0.98550011 + g * 0.47440306 + b * 0.01572502); + R[33] = max(SPECTRAL_EPSILON, w + c * 0.01437620 + m * 0.51041094 + y * 0.98357259 + r * 0.98551031 + g * 0.48576990 + b * 0.01571717); + R[34] = max(SPECTRAL_EPSILON, w + c * 0.01436343 + m * 0.50526577 + y * 0.98353856 + r * 0.98550741 + g * 0.49267971 + b * 0.01571905); + R[35] = max(SPECTRAL_EPSILON, w + c * 0.01435687 + m * 0.50255080 + y * 0.98351247 + r * 0.98551323 + g * 0.49625685 + b * 0.01571059); + R[36] = max(SPECTRAL_EPSILON, w + c * 0.01435370 + m * 0.50126452 + y * 0.98350101 + r * 0.98551563 + g * 0.49807754 + b * 0.01569728); + R[37] = max(SPECTRAL_EPSILON, w + c * 0.01435408 + m * 0.50083021 + y * 0.98350852 + r * 0.98551547 + g * 0.49889859 + b * 0.01570020); +} + +vec3 spectral_xyz_to_srgb(vec3 xyz) { + mat3 XYZ_RGB; + + XYZ_RGB[0] = vec3( 3.24306333, -1.53837619, -0.49893282); + XYZ_RGB[1] = vec3(-0.96896309, 1.87542451, 0.04154303); + XYZ_RGB[2] = vec3( 0.05568392, -0.20417438, 1.05799454); + + float r = dot(XYZ_RGB[0], xyz); + float g = dot(XYZ_RGB[1], xyz); + float b = dot(XYZ_RGB[2], xyz); + + return spectral_linear_to_srgb(vec3(r, g, b)); +} + +vec3 spectral_reflectance_to_xyz(float R[SPECTRAL_SIZE]) { + vec3 xyz = vec3(0.0); + + xyz += R[0] * vec3(0.00006469, 0.00000184, 0.00030502); + xyz += R[1] * vec3(0.00021941, 0.00000621, 0.00103681); + xyz += R[2] * vec3(0.00112057, 0.00003101, 0.00531314); + xyz += R[3] * vec3(0.00376661, 0.00010475, 0.01795439); + xyz += R[4] * vec3(0.01188055, 0.00035364, 0.05707758); + xyz += R[5] * vec3(0.02328644, 0.00095147, 0.11365162); + xyz += R[6] * vec3(0.03455942, 0.00228226, 0.17335873); + xyz += R[7] * vec3(0.03722379, 0.00420733, 0.19620658); + xyz += R[8] * vec3(0.03241838, 0.00668880, 0.18608237); + xyz += R[9] * vec3(0.02123321, 0.00988840, 0.13995048); + xyz += R[10] * vec3(0.01049099, 0.01524945, 0.08917453); + xyz += R[11] * vec3(0.00329584, 0.02141831, 0.04789621); + xyz += R[12] * vec3(0.00050704, 0.03342293, 0.02814563); + xyz += R[13] * vec3(0.00094867, 0.05131001, 0.01613766); + xyz += R[14] * vec3(0.00627372, 0.07040208, 0.00775910); + xyz += R[15] * vec3(0.01686462, 0.08783871, 0.00429615); + xyz += R[16] * vec3(0.02868965, 0.09424905, 0.00200551); + xyz += R[17] * vec3(0.04267481, 0.09795667, 0.00086147); + xyz += R[18] * vec3(0.05625475, 0.09415219, 0.00036904); + xyz += R[19] * vec3(0.06947040, 0.08678102, 0.00019143); + xyz += R[20] * vec3(0.08305315, 0.07885653, 0.00014956); + xyz += R[21] * vec3(0.08612610, 0.06352670, 0.00009231); + xyz += R[22] * vec3(0.09046614, 0.05374142, 0.00006813); + xyz += R[23] * vec3(0.08500387, 0.04264606, 0.00002883); + xyz += R[24] * vec3(0.07090667, 0.03161735, 0.00001577); + xyz += R[25] * vec3(0.05062889, 0.02088521, 0.00000394); + xyz += R[26] * vec3(0.03547396, 0.01386011, 0.00000158); + xyz += R[27] * vec3(0.02146821, 0.00810264, 0.00000000); + xyz += R[28] * vec3(0.01251646, 0.00463010, 0.00000000); + xyz += R[29] * vec3(0.00680458, 0.00249138, 0.00000000); + xyz += R[30] * vec3(0.00346457, 0.00125930, 0.00000000); + xyz += R[31] * vec3(0.00149761, 0.00054165, 0.00000000); + xyz += R[32] * vec3(0.00076970, 0.00027795, 0.00000000); + xyz += R[33] * vec3(0.00040737, 0.00014711, 0.00000000); + xyz += R[34] * vec3(0.00016901, 0.00006103, 0.00000000); + xyz += R[35] * vec3(0.00009522, 0.00003439, 0.00000000); + xyz += R[36] * vec3(0.00004903, 0.00001771, 0.00000000); + xyz += R[37] * vec3(0.00002000, 0.00000722, 0.00000000); + + return xyz; +} + +float spectral_linear_to_concentration(float l1, float l2, float t) { + float t1 = l1 * pow(1.0 - t, 2.0); + float t2 = l2 * pow(t, 2.0); + + return t2 / (t1 + t2); +} + +vec3 spectral_mix(vec3 color1, vec3 color2, float t) { + // EJ: Assume provided arguments are linear rgb + vec3 lrgb1 = color1; + vec3 lrgb2 = color2; + + float R1[SPECTRAL_SIZE]; + float R2[SPECTRAL_SIZE]; + + spectral_linear_to_reflectance(lrgb1, R1); + spectral_linear_to_reflectance(lrgb2, R2); + + float l1 = spectral_reflectance_to_xyz(R1)[1]; + float l2 = spectral_reflectance_to_xyz(R2)[1]; + + t = spectral_linear_to_concentration(l1, l2, t); + + float R[SPECTRAL_SIZE]; + + for (int i = 0; i < SPECTRAL_SIZE; i++) { + float KS = (1.0 - t) * (pow(1.0 - R1[i], 2.0) / (2.0 * R1[i])) + t * (pow(1.0 - R2[i], 2.0) / (2.0 * R2[i])); + float KM = 1.0 + KS - sqrt(pow(KS, 2.0) + 2.0 * KS); + + //Saunderson correction + // let S = ((1.0 - K1) * (1.0 - K2) * KM) / (1.0 - K2 * KM); + + R[i] = KM; + } + + return spectral_xyz_to_srgb(spectral_reflectance_to_xyz(R)); +} + +vec4 spectral_mix(vec4 color1, vec4 color2, float t) { + return vec4(spectral_mix(color1.rgb, color2.rgb, t), mix(color1.a, color2.a, t)); +} + +#endif \ No newline at end of file