[scripting] Support shaders in scripted effects
The scripting API is extended to support custom fragment shaders. To support this a new method addFragmentShader is added taking ShaderTraits and fragment shader. The GLShader is not exposed, instead a uint id is provided which maps to the GLShader. This shader id can be used in the animate and set calls to specify the shader. The animation object is extended by the "fragmentShader" property. The shader sources are located in the "shaders" directory of the package contents. E.g. the scale effect extended by the shader of the invert effect will have the following layout: package/contents/ -> code/ -> main.js -> shaders/ -> invert_core.frag -> invert.frag The adjustment in code are: * in constructor to load the shader this.shader = effect.addFragmentShader(Effect.MapTexture, "invert.frag"); * in animations objects of the slots the additions fragmentShader: this.shader * using the type Effect.Shader or in full: window.scaleInAnimation = animate({ window: window, curve: QEasingCurve.OutCubic, duration: this.duration, animations: [ { type: Effect.Scale, from: this.inScale }, { type: Effect.Opacity, from: 0 }, { type: Effect.Shader, fragmentShader: this.shader } ] }); The animation settings object supports a "uniform" value which takes the string name of the uniform. For this uniform the location is resolved and stored in the meta data of the AnimationEffect. This requires the type Effect.ShaderUniform. An example animation: window.scaleInAnimation = animate({ window: window, curve: QEasingCurve.Linear, duration: this.duration, animations: [ { type: Effect.ShaderUniform, fragmentShader: this.shader, uniform: "uForOpening", from: 1.0, to: 1.0 } ] }); Furthermore a new setUniform scriptable method is added to the ScriptedEffect. This allows to update uniforms when the configuration changes. The call takes a generic QJSValue which supports: * float * array of 2, 3 or 4 components * string as color * variant as color An example usage to read a color from the configuration and set it as a uniform: effect.setUniform(this.shaderId, "uEffectColor", effect.readConfig("Color", "white"))
This commit is contained in:
parent
47b330ea23
commit
7bececfa2f
3 changed files with 157 additions and 9 deletions
|
@ -68,13 +68,15 @@ supports the following attributes:
|
|||
from: FPx2, /* for first animation, optional */
|
||||
to: FPx2, /* for first animation, optional */
|
||||
delay: int, /* for first animation, optional */
|
||||
shader: int, /* for first animation, optional */
|
||||
animations: [ /* additional animations, optional */
|
||||
{
|
||||
curve: QEasingCurve.Type, /* overrides global */
|
||||
type: Effect.Attribute,
|
||||
from: FPx2,
|
||||
to: FPx2,
|
||||
delay: int
|
||||
delay: int,
|
||||
shader: int
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -152,6 +154,22 @@ For convenience you can pass a single quint64 as well.
|
|||
<read></read>
|
||||
<detaileddescription>Registers keySequence as a global shortcut. When the shortcut is invoked the callback will be called. Title and text are used to name the shortcut and make it available to the global shortcut configuration module.</detaileddescription>
|
||||
</memberdef>
|
||||
<memberdef kind="function">
|
||||
<type>Q_SCRIPTABLE uint</type>
|
||||
<definition>uint KWin::ScriptedEffect::addFragmentShader</definition>
|
||||
<argsstring>(ShaderTrait traits, QString fragmentShaderFile)</argsstring>
|
||||
<name>addFragmentShader</name>
|
||||
<read></read>
|
||||
<detaileddescription>Creates a shader and returns an identifier which can be used in animate or set. The shader sources must be provided in the shaders sub-directory of the contents package directory. The fragment shader needs to have the file extension frag. Each shader should be provided in a GLSL 1.10 and GLSL 1.40 variant. The 1.40 variant needs to have a suffix _core. E.g. there should be a shader myCustomShader.frag and myCustomShader_core.frag. The vertex shader is generated from the ShaderTrait. The ShaderTrait enum can be used as flags in this method.</detaileddescription>
|
||||
</memberdef>
|
||||
<memberdef kind="function">
|
||||
<type>Q_SCRIPTABLE uint</type>
|
||||
<definition>void KWin::ScriptedEffect::setUniform</definition>
|
||||
<argsstring>(uint shaderId, QString name, QJSValue value)</argsstring>
|
||||
<name>setUniform</name>
|
||||
<read></read>
|
||||
<detaileddescription>Updates the uniform value of the uniform identified by @p name for the shader identified by @p shaderId. The @p value can be a floating point numeric value (integer uniform values are not supported), an array with either 2, 3 or 4 numeric values, a string to identify a color or a variant value to identify a color as returned by readConfig. This method can be used to update the state of the shader when the configuration of the effect changed.</detaileddescription>
|
||||
</memberdef>
|
||||
</sectiondef>
|
||||
</compounddef>
|
||||
<compounddef>
|
||||
|
|
|
@ -21,12 +21,15 @@
|
|||
#include <KGlobalAccel>
|
||||
#include <KPluginMetaData>
|
||||
#include <kconfigloader.h>
|
||||
#include <kwinglutils.h>
|
||||
// Qt
|
||||
#include <QAction>
|
||||
#include <QFile>
|
||||
#include <QQmlEngine>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <optional>
|
||||
|
||||
Q_DECLARE_METATYPE(KSharedConfigPtr)
|
||||
|
||||
namespace KWin
|
||||
|
@ -54,6 +57,7 @@ struct AnimationSettings
|
|||
uint metaData;
|
||||
bool fullScreenEffect;
|
||||
bool keepAlive;
|
||||
std::optional<uint> shader;
|
||||
};
|
||||
|
||||
AnimationSettings animationSettingsFromObject(const QJSValue &object)
|
||||
|
@ -121,6 +125,10 @@ AnimationSettings animationSettingsFromObject(const QJSValue &object)
|
|||
settings.frozenTime = -1;
|
||||
}
|
||||
|
||||
if (const auto shader = object.property(QStringLiteral("fragmentShader")); shader.isNumber()) {
|
||||
settings.shader = shader.toUInt();
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
@ -202,6 +210,7 @@ ScriptedEffect::ScriptedEffect()
|
|||
|
||||
ScriptedEffect::~ScriptedEffect()
|
||||
{
|
||||
qDeleteAll(m_shaders);
|
||||
}
|
||||
|
||||
bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript)
|
||||
|
@ -273,6 +282,8 @@ bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript
|
|||
QStringLiteral("redirect"),
|
||||
QStringLiteral("complete"),
|
||||
QStringLiteral("cancel"),
|
||||
QStringLiteral("addShader"),
|
||||
QStringLiteral("setUniform"),
|
||||
};
|
||||
|
||||
for (const QString &propertyName : globalProperties) {
|
||||
|
@ -381,6 +392,9 @@ QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType an
|
|||
if (!(s.set & AnimationSettings::KeepAlive)) {
|
||||
s.keepAlive = settings.at(0).keepAlive;
|
||||
}
|
||||
if (!s.shader.has_value()) {
|
||||
s.shader = settings.at(0).shader;
|
||||
}
|
||||
|
||||
s.metaData = 0;
|
||||
typedef QMap<AnimationEffect::MetaType, QString> MetaTypeMap;
|
||||
|
@ -400,6 +414,20 @@ QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType an
|
|||
AnimationEffect::setMetaData(it.key(), metaVal.toInt(), s.metaData);
|
||||
}
|
||||
}
|
||||
if (s.type == ShaderUniform && s.shader) {
|
||||
auto uniformProperty = value.property(QStringLiteral("uniform")).toString();
|
||||
auto shader = findShader(s.shader.value());
|
||||
if (!shader) {
|
||||
m_engine->throwError(QStringLiteral("Shader for given shaderId not found"));
|
||||
return {};
|
||||
}
|
||||
if (!effects->makeOpenGLContextCurrent()) {
|
||||
m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
|
||||
return {};
|
||||
}
|
||||
ShaderBinder binder{shader};
|
||||
s.metaData = shader->uniformLocation(uniformProperty.toUtf8().constData());
|
||||
}
|
||||
|
||||
settings << s;
|
||||
}
|
||||
|
@ -439,7 +467,8 @@ QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType an
|
|||
setting.curve,
|
||||
setting.delay,
|
||||
setting.fullScreenEffect,
|
||||
setting.keepAlive);
|
||||
setting.keepAlive,
|
||||
setting.shader ? setting.shader.value() : 0u);
|
||||
if (setting.frozenTime >= 0) {
|
||||
freezeInTime(animationId, setting.frozenTime);
|
||||
}
|
||||
|
@ -453,7 +482,8 @@ QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType an
|
|||
setting.curve,
|
||||
setting.delay,
|
||||
setting.fullScreenEffect,
|
||||
setting.keepAlive);
|
||||
setting.keepAlive,
|
||||
setting.shader ? setting.shader.value() : 0u);
|
||||
if (setting.frozenTime >= 0) {
|
||||
freezeInTime(animationId, setting.frozenTime);
|
||||
}
|
||||
|
@ -466,7 +496,7 @@ QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType an
|
|||
|
||||
quint64 ScriptedEffect::animate(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute,
|
||||
int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve,
|
||||
int delay, bool fullScreen, bool keepAlive)
|
||||
int delay, bool fullScreen, bool keepAlive, uint shaderId)
|
||||
{
|
||||
QEasingCurve qec;
|
||||
if (curve < QEasingCurve::Custom) {
|
||||
|
@ -475,7 +505,7 @@ quint64 ScriptedEffect::animate(KWin::EffectWindow *window, KWin::AnimationEffec
|
|||
qec.setCustomType(qecGaussian);
|
||||
}
|
||||
return AnimationEffect::animate(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec,
|
||||
delay, fpx2FromScriptValue(from), fullScreen, keepAlive);
|
||||
delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId));
|
||||
}
|
||||
|
||||
QJSValue ScriptedEffect::animate(const QJSValue &object)
|
||||
|
@ -485,7 +515,7 @@ QJSValue ScriptedEffect::animate(const QJSValue &object)
|
|||
|
||||
quint64 ScriptedEffect::set(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute,
|
||||
int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve,
|
||||
int delay, bool fullScreen, bool keepAlive)
|
||||
int delay, bool fullScreen, bool keepAlive, uint shaderId)
|
||||
{
|
||||
QEasingCurve qec;
|
||||
if (curve < QEasingCurve::Custom) {
|
||||
|
@ -494,7 +524,7 @@ quint64 ScriptedEffect::set(KWin::EffectWindow *window, KWin::AnimationEffect::A
|
|||
qec.setCustomType(qecGaussian);
|
||||
}
|
||||
return AnimationEffect::set(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec,
|
||||
delay, fpx2FromScriptValue(from), fullScreen, keepAlive);
|
||||
delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId));
|
||||
}
|
||||
|
||||
QJSValue ScriptedEffect::set(const QJSValue &object)
|
||||
|
@ -771,4 +801,87 @@ QJSEngine *ScriptedEffect::engine() const
|
|||
return m_engine;
|
||||
}
|
||||
|
||||
uint ScriptedEffect::addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile)
|
||||
{
|
||||
if (!effects->makeOpenGLContextCurrent()) {
|
||||
m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
|
||||
return 0;
|
||||
}
|
||||
const QString shaderDir{QLatin1String(KWIN_NAME "/effects/") + m_effectName + QLatin1String("/contents/shaders/")};
|
||||
const QString fragment = fragmentShaderFile.isEmpty() ? QString{} : QStandardPaths::locate(QStandardPaths::GenericDataLocation, shaderDir + fragmentShaderFile);
|
||||
|
||||
auto shader = ShaderManager::instance()->generateShaderFromFile(static_cast<KWin::ShaderTraits>(int(traits)), {}, fragment);
|
||||
if (!shader->isValid()) {
|
||||
m_engine->throwError(QStringLiteral("Shader failed to load"));
|
||||
delete shader;
|
||||
// 0 is never a valid shader identifier, it's ensured the first shader gets id 1
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint shaderId{m_nextShaderId};
|
||||
m_nextShaderId++;
|
||||
m_shaders.insert(shaderId, shader);
|
||||
return shaderId;
|
||||
}
|
||||
|
||||
GLShader *ScriptedEffect::findShader(uint shaderId) const
|
||||
{
|
||||
if (auto it = m_shaders.find(shaderId); it != m_shaders.end()) {
|
||||
return it.value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ScriptedEffect::setUniform(uint shaderId, const QString &name, const QJSValue &value)
|
||||
{
|
||||
auto shader = findShader(shaderId);
|
||||
if (!shader) {
|
||||
m_engine->throwError(QStringLiteral("Shader for given shaderId not found"));
|
||||
return;
|
||||
}
|
||||
if (!effects->makeOpenGLContextCurrent()) {
|
||||
m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
|
||||
return;
|
||||
}
|
||||
auto setColorUniform = [this, shader, name] (const QColor &color)
|
||||
{
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
if (!shader->setUniform(name.toUtf8().constData(), color)) {
|
||||
m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
|
||||
}
|
||||
};
|
||||
ShaderBinder binder{shader};
|
||||
if (value.isString()) {
|
||||
setColorUniform(value.toString());
|
||||
} else if (value.isNumber()) {
|
||||
if (!shader->setUniform(name.toUtf8().constData(), float(value.toNumber()))) {
|
||||
m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
|
||||
}
|
||||
} else if (value.isArray()) {
|
||||
const auto length = value.property(QStringLiteral("length")).toInt();
|
||||
if (length == 2) {
|
||||
if (!shader->setUniform(name.toUtf8().constData(), QVector2D{float(value.property(0).toNumber()), float(value.property(1).toNumber())})) {
|
||||
m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
|
||||
}
|
||||
} else if (length == 3) {
|
||||
if (!shader->setUniform(name.toUtf8().constData(), QVector3D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber())})) {
|
||||
m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
|
||||
}
|
||||
} else if (length == 4) {
|
||||
if (!shader->setUniform(name.toUtf8().constData(), QVector4D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber()), float(value.property(3).toNumber())})) {
|
||||
m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
|
||||
}
|
||||
} else {
|
||||
m_engine->throwError(QStringLiteral("Invalid number of elements in array"));
|
||||
}
|
||||
} else if (value.isVariant()) {
|
||||
const auto variant = value.toVariant();
|
||||
setColorUniform(variant.value<QColor>());
|
||||
} else {
|
||||
m_engine->throwError(QStringLiteral("Invalid value provided for uniform"));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
@ -32,6 +32,7 @@ class KWIN_EXPORT ScriptedEffect : public KWin::AnimationEffect
|
|||
Q_ENUMS(EasingCurve)
|
||||
Q_ENUMS(SessionState)
|
||||
Q_ENUMS(ElectricBorder)
|
||||
Q_ENUMS(ShaderTrait)
|
||||
/**
|
||||
* The plugin ID of the effect
|
||||
*/
|
||||
|
@ -59,6 +60,14 @@ public:
|
|||
enum EasingCurve {
|
||||
GaussianCurve = 128
|
||||
};
|
||||
// copied from kwinglutils.h
|
||||
enum class ShaderTrait {
|
||||
MapTexture = (1 << 0),
|
||||
UniformColor = (1 << 1),
|
||||
Modulate = (1 << 2),
|
||||
AdjustSaturation = (1 << 3),
|
||||
};
|
||||
|
||||
const QString &scriptFile() const
|
||||
{
|
||||
return m_scriptFile;
|
||||
|
@ -127,13 +136,13 @@ public:
|
|||
Q_SCRIPTABLE quint64 animate(KWin::EffectWindow *window, Attribute attribute, int ms,
|
||||
const QJSValue &to, const QJSValue &from = QJSValue(),
|
||||
uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0,
|
||||
bool fullScreen = false, bool keepAlive = true);
|
||||
bool fullScreen = false, bool keepAlive = true, uint shaderId = 0);
|
||||
Q_SCRIPTABLE QJSValue animate(const QJSValue &object);
|
||||
|
||||
Q_SCRIPTABLE quint64 set(KWin::EffectWindow *window, Attribute attribute, int ms,
|
||||
const QJSValue &to, const QJSValue &from = QJSValue(),
|
||||
uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0,
|
||||
bool fullScreen = false, bool keepAlive = true);
|
||||
bool fullScreen = false, bool keepAlive = true, uint shaderId = 0);
|
||||
Q_SCRIPTABLE QJSValue set(const QJSValue &object);
|
||||
|
||||
Q_SCRIPTABLE bool retarget(quint64 animationId, const QJSValue &newTarget,
|
||||
|
@ -156,6 +165,10 @@ public:
|
|||
|
||||
Q_SCRIPTABLE QList<int> touchEdgesForAction(const QString &action) const;
|
||||
|
||||
Q_SCRIPTABLE uint addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile = {});
|
||||
|
||||
Q_SCRIPTABLE void setUniform(uint shaderId, const QString &name, const QJSValue &value);
|
||||
|
||||
QHash<int, QJSValueList> &screenEdgeCallbacks()
|
||||
{
|
||||
return m_screenEdgeCallbacks;
|
||||
|
@ -194,6 +207,8 @@ private:
|
|||
|
||||
QJSValue animate_helper(const QJSValue &object, AnimationType animationType);
|
||||
|
||||
GLShader *findShader(uint shaderId) const;
|
||||
|
||||
QJSEngine *m_engine;
|
||||
QString m_effectName;
|
||||
QString m_scriptFile;
|
||||
|
@ -204,6 +219,8 @@ private:
|
|||
int m_chainPosition;
|
||||
QHash<int, QAction *> m_touchScreenEdgeCallbacks;
|
||||
Effect *m_activeFullScreenEffect = nullptr;
|
||||
QHash<uint, GLShader*> m_shaders;
|
||||
uint m_nextShaderId{1u};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue