From 8b6c1550c289a835c5e35b568be4f3bead2790b9 Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Mon, 25 Mar 2024 14:30:13 -0400 Subject: [PATCH] Add PageCurlMixer. Cleanup docs. --- src/MediaFX/MediaSequence.qml | 2 +- src/MediaFX/Mixers/CMakeLists.txt | 2 + src/MediaFX/Mixers/LumaGradientMixer.qml | 1 + src/MediaFX/Mixers/LumaMixer.qml | 1 + src/MediaFX/Mixers/MediaMixer.qml | 5 +- src/MediaFX/Mixers/PageCurlMixer.qml | 17 +++ src/MediaFX/Mixers/WipeMixer.qml | 1 + src/MediaFX/Mixers/pageCurl.frag | 183 +++++++++++++++++++++++ src/MediaFX/media_manager.cpp | 4 +- tests/fixtures | 2 +- tests/qml/sequence.qml | 2 + 11 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/MediaFX/Mixers/PageCurlMixer.qml create mode 100644 src/MediaFX/Mixers/pageCurl.frag diff --git a/src/MediaFX/MediaSequence.qml b/src/MediaFX/MediaSequence.qml index 923bd45..8805b90 100644 --- a/src/MediaFX/MediaSequence.qml +++ b/src/MediaFX/MediaSequence.qml @@ -8,7 +8,7 @@ import "sequence.js" as Sequence /*! \qmltype MediaSequence \inqmlmodule MediaFX - \brief Plays a sequence of MediaClips in order, with MediaMixer transitions between them. + \brief Plays a sequence of \l {MediaClip}s in order, with \l MediaMixer transitions between them. \quotefile sequence.qml diff --git a/src/MediaFX/Mixers/CMakeLists.txt b/src/MediaFX/Mixers/CMakeLists.txt index cae8922..e7f352d 100644 --- a/src/MediaFX/Mixers/CMakeLists.txt +++ b/src/MediaFX/Mixers/CMakeLists.txt @@ -10,6 +10,7 @@ qt_add_shaders(mixers "shaders" FILES crossfade.frag luma.frag + pageCurl.frag OUTPUT_TARGETS shader_output_targets ) @@ -24,5 +25,6 @@ qt_add_qml_module(mixers CrossFadeMixer.qml LumaMixer.qml LumaGradientMixer.qml + PageCurlMixer.qml WipeMixer.qml ) \ No newline at end of file diff --git a/src/MediaFX/Mixers/LumaGradientMixer.qml b/src/MediaFX/Mixers/LumaGradientMixer.qml index a44057c..3610869 100644 --- a/src/MediaFX/Mixers/LumaGradientMixer.qml +++ b/src/MediaFX/Mixers/LumaGradientMixer.qml @@ -7,6 +7,7 @@ import MediaFX /*! \qmltype LumaGradientMixer + \inherits LumaMixer \inqmlmodule MediaFX.Mixers \brief LumaMixer that uses a ShapeGradient as the luma map. */ diff --git a/src/MediaFX/Mixers/LumaMixer.qml b/src/MediaFX/Mixers/LumaMixer.qml index e21b175..8fdbbc2 100644 --- a/src/MediaFX/Mixers/LumaMixer.qml +++ b/src/MediaFX/Mixers/LumaMixer.qml @@ -6,6 +6,7 @@ import MediaFX /*! \qmltype LumaMixer + \inherits MediaMixer \inqmlmodule MediaFX.Mixers \brief Crossfade/wipe source to dest using a greyscale luma "map" image. */ diff --git a/src/MediaFX/Mixers/MediaMixer.qml b/src/MediaFX/Mixers/MediaMixer.qml index a5f0146..ddf3800 100644 --- a/src/MediaFX/Mixers/MediaMixer.qml +++ b/src/MediaFX/Mixers/MediaMixer.qml @@ -5,12 +5,15 @@ import QtQuick /*! \qmltype MediaMixer + \inherits ShaderEffect \inqmlmodule MediaFX.Mixers - \brief Renders a video transition between two MediaClips in a MediaSequence. + \brief Renders a video transition between two \l {MediaClip}s in a \l MediaSequence. \sa MediaSequence */ ShaderEffect { + /*! The source item, mixer transitions from \c source to \c dest */ property Item source + /*! The destination item, mixer transitions from \c source to \c dest */ property Item dest /*! The duration of the transition in milliseconds */ property int duration: 1000 diff --git a/src/MediaFX/Mixers/PageCurlMixer.qml b/src/MediaFX/Mixers/PageCurlMixer.qml new file mode 100644 index 0000000..58d2197 --- /dev/null +++ b/src/MediaFX/Mixers/PageCurlMixer.qml @@ -0,0 +1,17 @@ +// Copyright (C) 2024 Andrew Wason +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import MediaFX + +/*! + \qmltype PageCurlMixer + \inherits MediaMixer + \inqmlmodule MediaFX.Mixers + \brief Transition from \l {MediaMixer::} {source} to \l {MediaMixer::} {dest} using a page curl effect. +*/ +MediaMixer { + id: root + + fragmentShader: "qrc:/shaders/pageCurl.frag.qsb" +} diff --git a/src/MediaFX/Mixers/WipeMixer.qml b/src/MediaFX/Mixers/WipeMixer.qml index 57e95b1..88e6e0e 100644 --- a/src/MediaFX/Mixers/WipeMixer.qml +++ b/src/MediaFX/Mixers/WipeMixer.qml @@ -7,6 +7,7 @@ import MediaFX /*! \qmltype WipeMixer + \inherits LumaGradientMixer \inqmlmodule MediaFX.Mixers \brief Wipe source to dest using a generated gradient luma map. */ diff --git a/src/MediaFX/Mixers/pageCurl.frag b/src/MediaFX/Mixers/pageCurl.frag new file mode 100644 index 0000000..4ee6ab5 --- /dev/null +++ b/src/MediaFX/Mixers/pageCurl.frag @@ -0,0 +1,183 @@ +// PageCurl fragment shader is based on code from http://labs.calyptus.eu/pagecurl/ +// Copyright (c) 2010 Calyptus Life AB +// Licensed under The MIT License: +// http://www.opensource.org/licenses/mit-license.php + +#version 440 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; // Ranges from 0.0 to 1.0 +}; +layout(binding = 1) uniform sampler2D source; +layout(binding = 2) uniform sampler2D dest; + +const float MIN_AMOUNT = -0.16; +const float MAX_AMOUNT = 1.3; +float amount = time * (MAX_AMOUNT - MIN_AMOUNT) + MIN_AMOUNT; + +const float PI = 3.141592653589793; + +const float scale = 512.0; +const float sharpness = 3.0; + +float cylinderCenter = amount; +// 360 degrees * amount +float cylinderAngle = 2.0 * PI * amount; + +const float cylinderRadius = 1.0 / PI / 2.0; + +vec3 hitPoint(float hitAngle, float yc, vec3 point, mat3 rrotation) { + float hitPoint = hitAngle / (2.0 * PI); + point.y = hitPoint; + return rrotation * point; +} + + +vec4 antiAlias(vec4 color1, vec4 color2, float distance) { + distance *= scale; + if (distance < 0.0) return color2; + if (distance > 2.0) return color1; + float dd = pow(1.0 - distance / 2.0, sharpness); + return ((color2 - color1) * dd) + color1; +} + +float distanceToEdge(vec3 point) { + float dx = abs(point.x > 0.5 ? 1.0 - point.x : point.x); + float dy = abs(point.y > 0.5 ? 1.0 - point.y : point.y); + if (point.x < 0.0) dx = -point.x; + if (point.x > 1.0) dx = point.x - 1.0; + if (point.y < 0.0) dy = -point.y; + if (point.y > 1.0) dy = point.y - 1.0; + if ((point.x < 0.0 || point.x > 1.0) && (point.y < 0.0 || point.y > 1.0)) return sqrt(dx * dx + dy * dy); + return min(dx, dy); +} + +vec4 seeThrough(float yc, vec2 p, mat3 rotation, mat3 rrotation) { + float hitAngle = PI - (acos(yc / cylinderRadius) - cylinderAngle); + vec3 point = hitPoint(hitAngle, yc, rotation * vec3(p, 1.0), rrotation); + + if (yc <= 0.0 && (point.x < 0.0 || point.y < 0.0 || point.x > 1.0 || point.y > 1.0)) { + return texture(dest, qt_TexCoord0); + } + + if (yc > 0.0) return texture(source, p); + + vec4 color = texture(source, point.xy); + vec4 tcolor = vec4(0.0); + + return antiAlias(color, tcolor, distanceToEdge(point)); +} + +vec4 seeThroughWithShadow(float yc, vec2 p, vec3 point, mat3 rotation, mat3 rrotation) { + float shadow = distanceToEdge(point) * 30.0; + shadow = (1.0 - shadow) / 3.0; + if (shadow < 0.0) shadow = 0.0; + else shadow *= amount; + + vec4 shadowColor = seeThrough(yc, p, rotation, rrotation); + shadowColor.r -= shadow; + shadowColor.g -= shadow; + shadowColor.b -= shadow; + return shadowColor; +} + +vec4 backside(float yc, vec3 point) { + vec4 color = texture(source, point.xy); + float gray = (color.r + color.b + color.g) / 15.0; + gray += (8.0 / 10.0) * (pow(1.0 - abs(yc / cylinderRadius), 2.0 / 10.0) / 2.0 + (5.0 / 10.0)); + color.rgb = vec3(gray); + return color; +} + +vec4 behindSurface(float yc, vec3 point, mat3 rrotation) { + float shado = (1.0 - ((-cylinderRadius - yc) / amount * 7.0)) / 6.0; + shado *= 1.0 - abs(point.x - 0.5); + + yc = (-cylinderRadius - cylinderRadius - yc); + + float hitAngle = (acos(yc / cylinderRadius) + cylinderAngle) - PI; + point = hitPoint(hitAngle, yc, point, rrotation); + + if (yc < 0.0 && point.x >= 0.0 && point.y >= 0.0 && point.x <= 1.0 && point.y <= 1.0 && (hitAngle < PI || amount > 0.5)){ + shado = 1.0 - (sqrt(pow(point.x - 0.5, 2.0) + pow(point.y - 0.5, 2.0)) / (71.0 / 100.0)); + shado *= pow(-yc / cylinderRadius, 3.0); + shado *= 0.5; + } else + shado = 0.0; + + return vec4(texture(dest, qt_TexCoord0).rgb - shado, 1.0); +} + +void main(void) { + const float angle = 30.0 * PI / 180.0; + float c = cos(-angle); + float s = sin(-angle); + + mat3 rotation = mat3( + c, s, 0, + -s, c, 0, + 0.12, 0.258, 1 + ); + + c = cos(angle); + s = sin(angle); + + mat3 rrotation = mat3( + c, s, 0, + -s, c, 0, + 0.15, -0.5, 1 + ); + + vec3 point = rotation * vec3(qt_TexCoord0, 1.0); + + float yc = point.y - cylinderCenter; + + if (yc < -cylinderRadius) { + // Behind surface + fragColor = behindSurface(yc, point, rrotation); + return; + } + + if (yc > cylinderRadius) { + // Flat surface + fragColor = texture(source, qt_TexCoord0); + return; + } + + float hitAngle = (acos(yc / cylinderRadius) + cylinderAngle) - PI; + + float hitAngleMod = mod(hitAngle, 2.0 * PI); + if ((hitAngleMod > PI && amount < 0.5) || (hitAngleMod > PI/2.0 && amount < 0.0)) { + fragColor = seeThrough(yc, qt_TexCoord0, rotation, rrotation); + return; + } + + point = hitPoint(hitAngle, yc, point, rrotation); + + if (point.x < 0.0 || point.y < 0.0 || point.x > 1.0 || point.y > 1.0) { + fragColor = seeThroughWithShadow(yc, qt_TexCoord0, point, rotation, rrotation); + return; + } + + vec4 color = backside(yc, point); + + vec4 otherColor; + if (yc < 0.0) { + float shado = 1.0 - (sqrt(pow(point.x - 0.5, 2.0) + pow(point.y - 0.5, 2.0)) / 0.71); + shado *= pow(-yc / cylinderRadius, 3.0); + shado *= 0.5; + otherColor = vec4(0.0, 0.0, 0.0, shado); + } else { + otherColor = texture(source, qt_TexCoord0); + } + + color = antiAlias(color, otherColor, cylinderRadius - abs(yc)); + + vec4 cl = seeThroughWithShadow(yc, qt_TexCoord0, point, rotation, rrotation); + float dist = distanceToEdge(point); + + fragColor = antiAlias(color, cl, dist) * qt_Opacity; +} \ No newline at end of file diff --git a/src/MediaFX/media_manager.cpp b/src/MediaFX/media_manager.cpp index 0c1a489..5a4edf6 100644 --- a/src/MediaFX/media_manager.cpp +++ b/src/MediaFX/media_manager.cpp @@ -114,7 +114,7 @@ void MediaManager::nextRenderTime() \qmlmethod void MediaManager::pauseRendering Pause rendering to allow for asynchronous processing. - \l resumeRendering must be called as many times as \a pauseRendering. + \l resumeRendering must be called as many times as \c pauseRendering. */ void MediaManager::pauseRendering() { @@ -127,7 +127,7 @@ void MediaManager::pauseRendering() \qmlmethod void MediaManager::resumeRendering Resume rendering after asynchronous processing. - \a resumeRendering must be called as many times as \l pauseRendering. + \c resumeRendering must be called as many times as \l pauseRendering. */ void MediaManager::resumeRendering() { diff --git a/tests/fixtures b/tests/fixtures index a42f76b..d648c7f 160000 --- a/tests/fixtures +++ b/tests/fixtures @@ -1 +1 @@ -Subproject commit a42f76bc0a159449ba5e08f64ceeff1a3311b572 +Subproject commit d648c7f382ba3bf553952696f40ad8aa4223782f diff --git a/tests/qml/sequence.qml b/tests/qml/sequence.qml index 8515cf8..b3290a3 100644 --- a/tests/qml/sequence.qml +++ b/tests/qml/sequence.qml @@ -24,6 +24,8 @@ MediaSequence { WipeMixer { direction: WipeMixer.Direction.Left blindsEffect: 0.05 + }, + PageCurlMixer { } ]