diff --git a/docs/changelog.md b/docs/changelog.md index acd2552..364c815 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,8 @@ #### Bug Fixes +* **Fixed a major memory leak**. Now, shaders are not re-created anymore whenever a window is opened or closed. Instead, previously created shaders are re-used as often as possible. This requires that all settings are set as uniform values (previously they were injected into the shader source) which in turn requires that `Shell.GLSLEffect` is used instead of `Clutter.ShaderEffect`. This change also requires that all shaders now work with straight instead of premultiplied alpha. Anyways, a lot needed to be refactored for this fix and I hope that it did not bring too many visual changes. +* Fixed the window-close animation of windows which were opened before the session was started (e.g. before GNOME Shell has been restarted). * There seem to be cases were `libadwaita` is not available on GNOME 42 (e.g. Pop!_OS 22.04 beta). The preferences dialog now tries to fallback to the GTK4-only variant if `libadwaita` is not available. ## [Burn My Windows 15](https://github.com/schneegans/Burn-My-Windows/releases/tag/v15) diff --git a/docs/how-to-create-new-effects.md b/docs/how-to-create-new-effects.md index 7392f69..6957067 100644 --- a/docs/how-to-create-new-effects.md +++ b/docs/how-to-create-new-effects.md @@ -101,6 +101,8 @@ Please study this code carefully, all of it is explained with inline comments. const GObject = imports.gi.GObject; +const _ = imports.gettext.domain('burn-my-windows').gettext; + const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; @@ -110,8 +112,13 @@ const utils = Me.imports.src.utils; // <- Please add a description of your effect here -> // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -136,7 +143,7 @@ var SimpleFade = class SimpleFade { // This will be shown in the sidebar of the preferences dialog as well as in the // drop-down menus where the user can choose the effect. static getLabel() { - return 'Simple Fade Effect'; + return _('Simple Fade Effect'); } // -------------------------------------------------------------------- API for prefs.js @@ -151,9 +158,21 @@ var SimpleFade = class SimpleFade { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -184,15 +203,35 @@ var SimpleFade = class SimpleFade { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; + const Shell = imports.gi.Shell; const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { _init(settings, forOpening) { super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); - this.set_shader_source(` + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uFadeWidth = this.get_uniform_location('uFadeWidth'); + } + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uFadeWidth, 1, [settings.get_double('simple-fade-width')]); + } + + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + + const declarations = ` // The code below injects some standard uniforms which will be updated during the // animation. This includes: // uTexture: Contains the texture of the window. @@ -202,29 +241,30 @@ if (utils.isInShellProcess()) { // uSizeY: The vertical size of uTexture in pixels. ${shaderSnippets.standardUniforms()} - // The width of the fading effect is directly loaded from the settings. - const float FADE_WIDTH = ${settings.get_double('simple-fade-width')}; + // The width of the fading effect is loaded from the settings. + uniform float uFadeWidth; + `; - void main() { + const code = ` + // Get the color from the window texture. + vec4 windowColor = texture2D(uTexture, cogl_tex_coord_in[0].st); - // Get the color from the window texture. - vec4 windowColor = texture2D(uTexture, cogl_tex_coord_in[0].st); + // Radial distance from window edge to the window's center. + float dist = length(cogl_tex_coord_in[0].st - 0.5) * 2.0 / sqrt(2.0); - // Radial distance from window edge to the window's center. - float dist = length(cogl_tex_coord_in[0].st - 0.5) * 2.0 / sqrt(2.0); + // This gradually dissolves from [1..0] from the outside to the center. We + // switch the direction for opening and closing. + float progress = uForOpening ? 1.0 - uProgress : uProgress; + float mask = (1.0 - progress * (1.0 + uFadeWidth) - dist + uFadeWidth) / uFadeWidth; - // This gradually dissolves from [1..0] from the outside to the center. We - // switch the direction for opening and closing. - float progress = ${forOpening ? '1.0 - uProgress' : 'uProgress'}; - float mask = (1.0 - progress * (1.0 + FADE_WIDTH) - dist + FADE_WIDTH) / FADE_WIDTH; + // Make the mask smoother. + mask = smoothstep(0, 1, mask); - // Make the mask smoother. - mask = smoothstep(0, 1, mask); + // Set the final output color. This uses premultiplied alpha. + cogl_color_out = windowColor * mask; + `; - // Set the final output color. This uses premultiplied alpha. - cogl_color_out = windowColor * mask; - } - `); + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); }; }); } diff --git a/extension.js b/extension.js index f989c95..51ec11e 100644 --- a/extension.js +++ b/extension.js @@ -70,10 +70,6 @@ class Extension { // Store a reference to the settings object. this._settings = ExtensionUtils.getSettings(); - // This will store an item of ALL_EFFECTS array which was used the last time a window - // was opened / closed. - this._currentEffect = 0; - // We will use extensionThis to refer to the extension inside the patched methods. const extensionThis = this; @@ -83,6 +79,7 @@ class Extension { this._origDoRemoveWindow = Workspace.prototype._doRemoveWindow; this._origShouldAnimateActor = WindowManager.prototype._shouldAnimateActor; this._origWaitForOverviewToHide = WindowManager.prototype._waitForOverviewToHide; + this._origDestroyWindowDone = WindowManager.prototype._destroyWindowDone; // We will also override these animation times. this._origWindowTime = imports.ui.windowManager.DESTROY_WINDOW_ANIMATION_TIME; @@ -204,9 +201,9 @@ class Extension { // ----------------------------------------------- patching the window-close animation - // The signal handler below is all which is required outside of the overview. All - // other hacks further below are just required to defer the window-hiding in the - // overview until the effect is finished. + // The signal handler below and the following patch are all which is required outside + // of the overview. All other hacks further below are just required to defer the + // window-hiding in the overview until the effect is finished. // The close animation is set up in WindowManager's _destroyWindow: // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1541 @@ -216,6 +213,23 @@ class Extension { this._setupEffect(actor, false); }); + // Once the window-close animation is is finished, the window manager's + // _destroyWindowDone is called. We use this to free the effect so that it can be + // re-used in future. + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1541 + WindowManager.prototype._destroyWindowDone = function(shellwm, actor) { + if (this._destroying.has(actor)) { + const shader = actor.get_effect('burn-my-windows-effect'); + if (shader) { + actor.remove_effect(shader); + shader.free(); + } + } + + // Call the original method. + extensionThis._origDestroyWindowDone.apply(this, [shellwm, actor]); + }; + // These three method overrides are mega-hacky! Usually, windows are not faded when // closed from the overview (why?). With these overrides we make sure that they are // actually faded out. To do this, _windowRemoved and _doRemoveWindow now check @@ -291,6 +305,9 @@ class Extension { // Tweaks, when you log out or when the screen locks. disable() { + // Free all effect resources. + ALL_EFFECTS.forEach(Effect => Effect.cleanUp()); + // Unregister our resources. Gio.resources_unregister(this._resources); @@ -303,6 +320,7 @@ class Extension { Workspace.prototype._doRemoveWindow = this._origDoRemoveWindow; WindowManager.prototype._shouldAnimateActor = this._origShouldAnimateActor; WindowManager.prototype._waitForOverviewToHide = this._origWaitForOverviewToHide; + WindowManager.prototype._destroyWindowDone = this._origDestroyWindowDone; imports.ui.windowManager.DESTROY_WINDOW_ANIMATION_TIME = this._origWindowTime; imports.ui.windowManager.DIALOG_DESTROY_WINDOW_ANIMATION_TIME = this._origDialogTime; @@ -337,7 +355,7 @@ class Extension { // We do nothing if a dialog got closed and we should not burn them. const shouldDestroyDialogs = this._settings.get_boolean('destroy-dialogs'); - // If an effect is to be previewed however, we have to affect dialogs es well. This is + // If an effect is to be previewed, we have to affect dialogs es well. This is // because the preview window is a dialog window... const action = forOpening ? 'open' : 'close'; const previewNick = this._settings.get_string(action + '-preview-effect'); @@ -347,14 +365,23 @@ class Extension { return; } + // There is the weird case where an animation is already. This happens when a window + // is closed which has been created before the session was started (e.g. when GNOME + // Shell has been restarted in the meantime). + const oldShader = actor.get_effect('burn-my-windows-effect'); + if (oldShader) { + actor.remove_effect(oldShader); + oldShader.free(); + } + // ------------------------------------------------------------------ choose an effect // Now we chose a random effect from all enabled effects. - this._currentEffect = null; + let effect = null; // First we check if an effect is to be previewed. if (previewNick != '') { - this._currentEffect = ALL_EFFECTS.find(Effect => { + effect = ALL_EFFECTS.find(Effect => { return Effect.getNick() == previewNick; }); @@ -372,12 +399,12 @@ class Extension { // And then choose a random effect. if (enabled.length > 0) { - this._currentEffect = enabled[Math.floor(Math.random() * enabled.length)]; + effect = enabled[Math.floor(Math.random() * enabled.length)]; } } // If nothing was enabled, we have to do nothing :) - if (this._currentEffect == null) { + if (effect == null) { this._fixAnimationTimes(isDialogWindow, forOpening, null); return; } @@ -391,10 +418,9 @@ class Extension { // The following is used to tweak the ongoing transitions of a window actor. Usually // windows are faded in / out scaled up / down slightly by GNOME Shell. Here, we allow // modifications to this behavior by the effects. - const config = this._currentEffect.tweakTransition(actor, this._settings, forOpening); - const duration = testMode ? - 5000 : - this._settings.get_int(this._currentEffect.getNick() + '-animation-time'); + const config = effect.tweakTransition(actor, this._settings, forOpening); + const duration = + testMode ? 5000 : this._settings.get_int(effect.getNick() + '-animation-time'); // All animations are relative to the window's center. actor.set_pivot_point(0.5, 0.5); @@ -445,7 +471,7 @@ class Extension { // -------------------------------------------------------------------- add the shader // Now add a cool shader to our window actor! - const shader = this._currentEffect.createShader(actor, this._settings, forOpening); + const shader = effect.getShader(actor, this._settings, forOpening); if (shader) { // There should always be an opacity transition going on... @@ -457,23 +483,28 @@ class Extension { return; } - // First remove any old effect. - actor.remove_effect_by_name(`burn-my-windows-effect`); - actor.add_effect_with_name(`burn-my-windows-effect`, shader); + actor.add_effect_with_name('burn-my-windows-effect', shader); // Update uniforms at each frame. transition.connect('new-frame', (t) => { - shader.set_uniform_value('uProgress', testMode ? 0.5 : t.get_progress()); - shader.set_uniform_value('uTime', - testMode ? duration / 2 : 0.001 * t.get_elapsed_time()); - shader.set_uniform_value('uSizeX', actor.width); - shader.set_uniform_value('uSizeY', actor.height); + shader.set_uniform_float(shader.get_uniform_location('uProgress'), 1, + [testMode ? 0.5 : t.get_progress()]); + shader.set_uniform_float( + shader.get_uniform_location('uTime'), 1, + [testMode ? duration / 2 : 0.001 * t.get_elapsed_time()]); + shader.set_uniform_float(shader.get_uniform_location('uSizeX'), 1, [actor.width]); + shader.set_uniform_float(shader.get_uniform_location('uSizeY'), 1, + [actor.height]); }); // Remove the effect if the animation finished or was interrupted. if (forOpening) { transition.connect('stopped', () => { - actor.remove_effect_by_name(`burn-my-windows-effect`); + const oldShader = actor.get_effect('burn-my-windows-effect'); + if (oldShader) { + actor.remove_effect(oldShader); + oldShader.free(); + } }); } } diff --git a/src/Apparition.js b/src/Apparition.js index 981fb16..e7c31c4 100644 --- a/src/Apparition.js +++ b/src/Apparition.js @@ -27,8 +27,13 @@ const utils = Me.imports.src.utils; // the center. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -80,9 +85,21 @@ var Apparition = class Apparition { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(actor, settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -103,6 +120,12 @@ var Apparition = class Apparition { 'scale-y': {from: 2.0, to: 2.0, mode: 1} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } } @@ -114,59 +137,98 @@ var Apparition = class Apparition { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; + const Shell = imports.gi.Shell; const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(actor, settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uShake = this.get_uniform_location('uShake'); + this._uTwirl = this.get_uniform_location('uTwirl'); + this._uSuction = this.get_uniform_location('uSuction'); + this._uRandomness = this.get_uniform_location('uRandomness'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { // If we are currently performing integration test, the animation uses a fixed seed. const testMode = settings.get_boolean('test-mode'); - // The math for the whirling is inspired by this post: - // http://www.geeks3d.com/20110428/shader-library-swirl-post-processing-filter-in-glsl - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uShake, 1, [settings.get_double('apparition-shake-intensity')]); + this.set_uniform_float(this._uTwirl, 1, [settings.get_double('apparition-twirl-intensity')]); + this.set_uniform_float(this._uSuction, 1, [settings.get_double('apparition-suction-intensity')]); + this.set_uniform_float(this._uRandomness, 1, [settings.get_double('apparition-randomness')]); + // clang-format on + } + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} - - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float SHAKE = ${settings.get_double('apparition-shake-intensity')}; - const float TWIRL = ${settings.get_double('apparition-twirl-intensity')}; - const float SUCTION = ${settings.get_double('apparition-suction-intensity')}; - const float RANDOMNESS = ${settings.get_double('apparition-randomness')}; + + uniform bool uForOpening; + uniform vec2 uSeed; + uniform float uShake; + uniform float uTwirl; + uniform float uSuction; + uniform float uRandomness; + const float ACTOR_SCALE = 2.0; const float PADDING = ACTOR_SCALE / 2.0 - 0.5; + `; - void main() { + // The math for the whirling is inspired by this post: + // http://www.geeks3d.com/20110428/shader-library-swirl-post-processing-filter-in-glsl + const code = ` + // We simply inverse the progress for opening windows. + float progress = uForOpening ? 1.0-uProgress : uProgress; - // We simply inverse the progress for opening windows. - float progress = ${forOpening ? '1.0-uProgress' : 'uProgress'}; + // Choose a random suction center. + vec2 center = uSeed * uRandomness + 0.5 * (1.0 - uRandomness); + vec2 coords = cogl_tex_coord_in[0].st * ACTOR_SCALE - PADDING - center; - // Choose a random suction center. - vec2 center = SEED * RANDOMNESS + 0.5 * (1.0 - RANDOMNESS); - vec2 coords = cogl_tex_coord_in[0].st * ACTOR_SCALE - PADDING - center; + // Add some shaking. + coords.x += progress * 0.05 * uShake * sin((progress + uSeed.x) * (1.0 + uSeed.x) * uShake); + coords.y += progress * 0.05 * uShake * cos((progress + uSeed.y) * (1.0 + uSeed.y) * uShake); - // Add some shaking. - coords.x += progress * 0.05 * SHAKE * sin((progress + SEED.x) * (1.0 + SEED.x) * SHAKE); - coords.y += progress * 0.05 * SHAKE * cos((progress + SEED.y) * (1.0 + SEED.y) * SHAKE); + // "Suck" the texture into the center. + float dist = length(coords) / sqrt(2); + coords += progress * coords / dist * 0.5 * uSuction; - // "Suck" the texture into the center. - float dist = length(coords) / sqrt(2); - coords += progress * coords / dist * 0.5 * SUCTION; + // Apply some whirling. + float angle = pow(1.0 - dist, 2.0) * uTwirl * progress; + float s = sin(angle); + float c = cos(angle); + coords = vec2(dot(coords, vec2(c, -s)), dot(coords, vec2(s, c))); - // Apply some whirling. - float angle = pow(1.0 - dist, 2.0) * TWIRL * progress; - float s = sin(angle); - float c = cos(angle); - coords = vec2(dot(coords, vec2(c, -s)), dot(coords, vec2(s, c))); - - // Fade out the window texture. - cogl_color_out = texture2D(uTexture, coords + center) * (1.0 - progress); + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + cogl_color_out = texture2D(uTexture, coords + center); + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; } - `); - }; + + // Fade out the window texture. + cogl_color_out.a *= 1.0 - progress; + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } }); } \ No newline at end of file diff --git a/src/BrokenGlass.js b/src/BrokenGlass.js index d253c0f..fc22b7d 100644 --- a/src/BrokenGlass.js +++ b/src/BrokenGlass.js @@ -28,8 +28,16 @@ const utils = Me.imports.src.utils; // of vfunc_paint_target further down in this file. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; + +// This texture will be loaded when the effect is used for the first time. +let shardTexture = null; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -80,9 +88,21 @@ var BrokenGlass = class BrokenGlass { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(actor, settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -103,6 +123,13 @@ var BrokenGlass = class BrokenGlass { 'scale-y': {from: 2.0, to: 2.0, mode: 1} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + shardTexture = null; + } } @@ -114,8 +141,8 @@ var BrokenGlass = class BrokenGlass { if (utils.isInShellProcess()) { - const {Clutter, GdkPixbuf, Cogl} = imports.gi; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; // This shader creates a complex-looking effect with rather simple means. Here is how it // works: The window is drawn five times on top of each other (see the SHARD_LAYERS @@ -126,17 +153,32 @@ if (utils.isInShellProcess()) { // belongs to which layer is defined by the green channel of the texture // resources/img/shards.png. The red channel of the texture contains the distance to the // shard edges. This information is used to fade out the shards. - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(actor, settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - // Load the shards texture. As the shader is re-created for each window animation, - // this texture is also re-created each time. This could be improved in the future! - const shardData = GdkPixbuf.Pixbuf.new_from_resource('/img/shards.png'); - this._shardTexture = new Clutter.Image(); - this._shardTexture.set_data(shardData.get_pixels(), Cogl.PixelFormat.RGB_888, - shardData.width, shardData.height, shardData.rowstride); + // Load the shards texture. + if (shardTexture == null) { + const shardData = GdkPixbuf.Pixbuf.new_from_resource('/img/shards.png'); + shardTexture = new Clutter.Image(); + shardTexture.set_data(shardData.get_pixels(), Cogl.PixelFormat.RGB_888, + shardData.width, shardData.height, shardData.rowstride); + } + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uShardTexture = this.get_uniform_location('uShardTexture'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uEpicenter = this.get_uniform_location('uEpicenter'); + this._uShardScale = this.get_uniform_location('uShardScale'); + this._uBlowForce = this.get_uniform_location('uBlowForce'); + this._uGravity = this.get_uniform_location('uGravity'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { // Usually, the shards fly away from the center of the window. let epicenterX = 0.5; let epicenterY = 0.5; @@ -155,76 +197,95 @@ if (utils.isInShellProcess()) { // If we are currently performing integration test, the animation uses a fixed seed. const testMode = settings.get_boolean('test-mode'); - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uEpicenter, 2, [epicenterX, epicenterY]); + this.set_uniform_float(this._uShardScale, 1, [settings.get_double('broken-glass-scale')]); + this.set_uniform_float(this._uBlowForce, 1, [settings.get_double('broken-glass-blow-force')]); + this.set_uniform_float(this._uGravity, 1, [settings.get_double('broken-glass-gravity')]); + // clang-format on + } + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} + uniform bool uForOpening; uniform sampler2D uShardTexture; + uniform vec2 uSeed; + uniform vec2 uEpicenter; + uniform float uShardScale; + uniform float uBlowForce; + uniform float uGravity; - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float SHARD_SCALE = ${settings.get_double('broken-glass-scale')}; const float SHARD_LAYERS = 5; - const float BLOW_FORCE = ${settings.get_double('broken-glass-blow-force')}; const float ACTOR_SCALE = 2.0; const float PADDING = ACTOR_SCALE / 2.0 - 0.5; - const vec2 EPICENTER = vec2(${epicenterX}, ${epicenterY}); - const float GRAVITY = ${forOpening ? '-1.0' : '1.0'} * - ${settings.get_double('broken-glass-gravity')}; + `; - void main() { + const code = ` + cogl_color_out = vec4(0, 0, 0, 0); - cogl_color_out = vec4(0, 0, 0, 0); + float progress = uForOpening ? 1.0-uProgress : uProgress; - float progress = ${forOpening ? '1.0-uProgress' : 'uProgress'}; + // Draw the individual shard layers. + for (float i=0; i 0 ? 1: 0; - cogl_color_out = mix(cogl_color_out, windowColor, dissolve); - } + if (shardGroup == i && (shardMap.x - pow(progress+0.1, 2)) > 0) { + cogl_color_out = texture2D(uTexture, coords); } } - `); - }; + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; + } + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } // This is overridden to bind the shard texture for drawing. Sadly, this seems to be // impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called @@ -238,9 +299,9 @@ if (utils.isInShellProcess()) { Cogl.PipelineFilter.LINEAR); // Bind the shard texture. - pipeline.set_layer_texture(1, this._shardTexture.get_texture()); + pipeline.set_layer_texture(1, shardTexture.get_texture()); pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); - this.set_uniform_value('uShardTexture', 1); + pipeline.set_uniform_1i(this._uShardTexture, 1); super.vfunc_paint_target(node, paint_context); } diff --git a/src/EnergizeA.js b/src/EnergizeA.js index 56a75b1..0bd94b9 100644 --- a/src/EnergizeA.js +++ b/src/EnergizeA.js @@ -25,8 +25,13 @@ const utils = Me.imports.src.utils; // This effect looks a bit like the transporter effect from TOS. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -75,9 +80,21 @@ var EnergizeA = class EnergizeA { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -97,6 +114,12 @@ var EnergizeA = class EnergizeA { 'scale-y': {from: 1.0, to: 1.0, mode: 3} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } } @@ -108,79 +131,106 @@ var EnergizeA = class EnergizeA { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - const color = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1]; + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uColor = this.get_uniform_location('uColor'); + this._uScale = this.get_uniform_location('uScale'); + } - // If we are currently performing integration test, the animation uses a fixed seed. - const testMode = settings.get_boolean('test-mode'); + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + const c = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1]; - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-a-scale')]); + // clang-format on + } - // Inject some common shader snippets. - ${shaderSnippets.standardUniforms()} - ${shaderSnippets.noise()} - ${shaderSnippets.edgeMask()} + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float FADE_IN_TIME = 0.3; - const float FADE_OUT_TIME = 0.6; - const float HEART_FADE_TIME = 0.3; - const float EDGE_FADE_WIDTH = 50; + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` + // Inject some common shader snippets. + ${shaderSnippets.standardUniforms()} + ${shaderSnippets.noise()} + ${shaderSnippets.edgeMask()} - // This method returns two values: - // result.x: A mask for the particles. - // result.y: The opacity of the fading window. - vec2 getMasks() { - float fadeInProgress = clamp(uProgress/FADE_IN_TIME, 0, 1); - float fadeOutProgress = clamp((uProgress-FADE_IN_TIME)/FADE_OUT_TIME, 0, 1); - float heartProgress = clamp((uProgress-(1.0-HEART_FADE_TIME))/HEART_FADE_TIME, 0, 1); + uniform bool uForOpening; + uniform vec3 uColor; + uniform float uScale; - // Compute mask for the "atom" particles. - float dist = length(cogl_tex_coord_in[0].st - 0.5) * 4.0; - float atomMask = smoothstep(0.0, 1.0, (fadeInProgress * 2.0 - dist + 1.0)); - atomMask *= fadeInProgress; - atomMask *= smoothstep(1.0, 0.0, fadeOutProgress); + const float FADE_IN_TIME = 0.3; + const float FADE_OUT_TIME = 0.6; + const float HEART_FADE_TIME = 0.3; + const float EDGE_FADE_WIDTH = 50; - // Fade-out the masks at the window edges. - float edgeFade = getAbsoluteEdgeMask(EDGE_FADE_WIDTH); - atomMask *= edgeFade; + // This method returns two values: + // result.x: A mask for the particles. + // result.y: The opacity of the fading window. + vec2 getMasks() { + float fadeInProgress = clamp(uProgress/FADE_IN_TIME, 0, 1); + float fadeOutProgress = clamp((uProgress-FADE_IN_TIME)/FADE_OUT_TIME, 0, 1); + float heartProgress = clamp((uProgress-(1.0-HEART_FADE_TIME))/HEART_FADE_TIME, 0, 1); - float heartMask = getRelativeEdgeMask(0.5); - heartMask = 3.0 * pow(heartMask, 5); - heartMask *= fadeOutProgress; - heartMask *= 1.0 - heartProgress; - atomMask = clamp(heartMask+atomMask, 0, 1); + // Compute mask for the "atom" particles. + float dist = length(cogl_tex_coord_in[0].st - 0.5) * 4.0; + float atomMask = smoothstep(0.0, 1.0, (fadeInProgress * 2.0 - dist + 1.0)); + atomMask *= fadeInProgress; + atomMask *= smoothstep(1.0, 0.0, fadeOutProgress); - // Compute fading window opacity. - float windowMask = pow(1.0 - fadeOutProgress, 2.0); + // Fade-out the masks at the window edges. + float edgeFade = getAbsoluteEdgeMask(EDGE_FADE_WIDTH); + atomMask *= edgeFade; - #if ${forOpening ? '1' : '0'} - windowMask = 1.0 - windowMask; - #endif + float heartMask = getRelativeEdgeMask(0.5); + heartMask = 3.0 * pow(heartMask, 5); + heartMask *= fadeOutProgress; + heartMask *= 1.0 - heartProgress; + atomMask = clamp(heartMask+atomMask, 0, 1); - return vec2(atomMask, windowMask); - } + // Compute fading window opacity. + float windowMask = pow(1.0 - fadeOutProgress, 2.0); - void main() { - + if (uForOpening) { + windowMask = 1.0 - windowMask; + } + + return vec2(atomMask, windowMask); + } + `; + + const code = ` vec2 masks = getMasks(); vec4 windowColor = texture2D(uTexture, cogl_tex_coord_in[0].st); - vec3 effectColor = vec3(${color.red / 255}, - ${color.green / 255}, - ${color.blue / 255}); + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (windowColor.a > 0) { + windowColor.rgb /= windowColor.a; + } // Dissolve window to effect color / transparency. - cogl_color_out = mix(vec4(effectColor, 1.0) * windowColor.a, windowColor, 0.2 * masks.y + 0.8) * masks.y; + cogl_color_out.rgb = mix(uColor, windowColor.rgb, 0.2 * masks.y + 0.8); + cogl_color_out.a = windowColor.a * masks.y; vec2 scaledUV = (cogl_tex_coord_in[0].st-0.5) * (1.0 + 0.1*uProgress); - scaledUV /= ${settings.get_double('energize-a-scale')}; + scaledUV /= uScale; // Add molecule particles. vec2 uv = scaledUV + vec2(0, 0.1*uTime); @@ -194,15 +244,17 @@ if (utils.isInShellProcess()) { particles += 0.5 * pow(0.2 * (1.0 / (1.0 - atoms)-1.0), 2); } - cogl_color_out.rgb += effectColor * particles * masks.x; + cogl_color_out.rgb += uColor * particles * masks.x; + cogl_color_out.a += particles * masks.x; // These are pretty useful for understanding how this works. // cogl_color_out = vec4(masks, 0.0, 1.0); // cogl_color_out = vec4(vec3(masks.x), 1.0); // cogl_color_out = vec4(vec3(masks.y), 1.0); // cogl_color_out = vec4(vec3(particles), 1.0); - } - `); - }; + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } }); } \ No newline at end of file diff --git a/src/EnergizeB.js b/src/EnergizeB.js index 6bddbb0..331f00d 100644 --- a/src/EnergizeB.js +++ b/src/EnergizeB.js @@ -25,8 +25,13 @@ const utils = Me.imports.src.utils; // This effect looks a bit like the transporter effect from TNG. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -75,9 +80,21 @@ var EnergizeB = class EnergizeB { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -97,6 +114,12 @@ var EnergizeB = class EnergizeB { 'scale-y': {from: 1.0, to: 1.0, mode: 3} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } } @@ -108,112 +131,141 @@ var EnergizeB = class EnergizeB { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - const color = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1]; + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uColor = this.get_uniform_location('uColor'); + this._uScale = this.get_uniform_location('uScale'); + } - // If we are currently performing integration test, the animation uses a fixed seed. - const testMode = settings.get_boolean('test-mode'); + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + const c = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1]; - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-b-scale')]); + // clang-format on + } - // Inject some common shader snippets. - ${shaderSnippets.standardUniforms()} - ${shaderSnippets.noise()} - ${shaderSnippets.edgeMask()} + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float SHOWER_TIME = 0.3; - const float SHOWER_WIDTH = 0.3; - const float STREAK_TIME = 0.6; - const float EDGE_FADE = 50; - const float SCALE = ${settings.get_double('energize-b-scale')}; + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` + // Inject some common shader snippets. + ${shaderSnippets.standardUniforms()} + ${shaderSnippets.noise()} + ${shaderSnippets.edgeMask()} - // This method returns four values: - // result.x: A mask for the particles which lead the shower. - // result.y: A mask for the streaks which follow the shower particles. - // result.z: A mask for the final "atom" particles. - // result.w: The opacity of the fading window. - vec4 getMasks() { - float showerProgress = uProgress/SHOWER_TIME; - float streakProgress = clamp((uProgress-SHOWER_TIME)/STREAK_TIME, 0, 1); - float fadeProgress = clamp((uProgress-SHOWER_TIME)/(1.0 - SHOWER_TIME), 0, 1); + uniform bool uForOpening; + uniform vec3 uColor; + uniform float uScale; - // Gradient from top to bottom. - float t = cogl_tex_coord_in[0].t; + const float SHOWER_TIME = 0.3; + const float SHOWER_WIDTH = 0.3; + const float STREAK_TIME = 0.6; + const float EDGE_FADE = 50; - // A smooth gradient which moves to the bottom within the showerProgress. - float showerMask = smoothstep(1, 0, abs(showerProgress - t - SHOWER_WIDTH) / SHOWER_WIDTH); + // This method returns four values: + // result.x: A mask for the particles which lead the shower. + // result.y: A mask for the streaks which follow the shower particles. + // result.z: A mask for the final "atom" particles. + // result.w: The opacity of the fading window. + vec4 getMasks() { + float showerProgress = uProgress/SHOWER_TIME; + float streakProgress = clamp((uProgress-SHOWER_TIME)/STREAK_TIME, 0, 1); + float fadeProgress = clamp((uProgress-SHOWER_TIME)/(1.0 - SHOWER_TIME), 0, 1); - // This is 1 above the streak mask. - float streakMask = (showerProgress - t - SHOWER_WIDTH) > 0 ? 1 : 0; + // Gradient from top to bottom. + float t = cogl_tex_coord_in[0].t; - // Compute mask for the "atom" particles. - float atomMask = getRelativeEdgeMask(0.2); - atomMask = max(0, atomMask - showerMask); - atomMask *= streakMask; - atomMask *= sqrt(1-fadeProgress*fadeProgress); + // A smooth gradient which moves to the bottom within the showerProgress. + float showerMask = smoothstep(1, 0, abs(showerProgress - t - SHOWER_WIDTH) / SHOWER_WIDTH); - // Make some particles visible in the streaks. - showerMask += 0.05 * streakMask; + // This is 1 above the streak mask. + float streakMask = (showerProgress - t - SHOWER_WIDTH) > 0 ? 1 : 0; - // Add shower mask to streak mask. - streakMask = max(streakMask, showerMask); + // Compute mask for the "atom" particles. + float atomMask = getRelativeEdgeMask(0.2); + atomMask = max(0, atomMask - showerMask); + atomMask *= streakMask; + atomMask *= sqrt(1-fadeProgress*fadeProgress); - // Fade-out the masks at the window edges. - float edgeFade = getAbsoluteEdgeMask(EDGE_FADE); - streakMask *= edgeFade; - showerMask *= edgeFade; - - // Fade-out the masks from top to bottom. - float fade = smoothstep(0.0, 1.0, 1.0 + t - 2.0 * streakProgress); - streakMask *= fade; - showerMask *= fade; + // Make some particles visible in the streaks. + showerMask += 0.05 * streakMask; - // Compute fading window opacity. - float windowMask = pow(1.0 - fadeProgress, 2.0); + // Add shower mask to streak mask. + streakMask = max(streakMask, showerMask); - #if ${forOpening ? '1' : '0'} - windowMask = 1.0 - windowMask; - #endif + // Fade-out the masks at the window edges. + float edgeFade = getAbsoluteEdgeMask(EDGE_FADE); + streakMask *= edgeFade; + showerMask *= edgeFade; + + // Fade-out the masks from top to bottom. + float fade = smoothstep(0.0, 1.0, 1.0 + t - 2.0 * streakProgress); + streakMask *= fade; + showerMask *= fade; - return vec4(showerMask, streakMask, atomMask, windowMask); - } + // Compute fading window opacity. + float windowMask = pow(1.0 - fadeProgress, 2.0); - void main() { - + if (uForOpening) { + windowMask = 1.0 - windowMask; + } + + return vec4(showerMask, streakMask, atomMask, windowMask); + } + `; + + const code = ` vec4 masks = getMasks(); vec4 windowColor = texture2D(uTexture, cogl_tex_coord_in[0].st); - vec3 effectColor = vec3(${color.red / 255}, - ${color.green / 255}, - ${color.blue / 255}); + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (windowColor.a > 0) { + windowColor.rgb /= windowColor.a; + } // Dissolve window to effect color / transparency. - cogl_color_out = mix(vec4(effectColor, 1.0) * windowColor.a, windowColor, 0.5 * masks.w + 0.5) * masks.w; + cogl_color_out.rgb = mix(uColor, windowColor.rgb, 0.5 * masks.w + 0.5); + cogl_color_out.a = windowColor.a * masks.w; // Add leading shower particles. vec2 showerUV = cogl_tex_coord_in[0].st + vec2(0, -0.7*uProgress/SHOWER_TIME); - showerUV *= 0.02 * vec2(uSizeX, uSizeY) / SCALE; + showerUV *= 0.02 * vec2(uSizeX, uSizeY) / uScale; float shower = pow(simplex2D(showerUV), 10.0); - cogl_color_out.rgb += effectColor * shower * masks.x; + cogl_color_out.rgb += uColor * shower * masks.x; + cogl_color_out.a += shower * masks.x; // Add trailing streak lines. vec2 streakUV = cogl_tex_coord_in[0].st + vec2(0, -uProgress/SHOWER_TIME); - streakUV *= vec2(0.05 * uSizeX, 0.001 * uSizeY) / SCALE; + streakUV *= vec2(0.05 * uSizeX, 0.001 * uSizeY) / uScale; float streaks = simplex2DFractal(streakUV) * 0.5; - cogl_color_out.rgb += effectColor * streaks * masks.y; + cogl_color_out.rgb += uColor * streaks * masks.y; + cogl_color_out.a += streaks * masks.y; // Add glimmering atoms. vec2 atomUV = cogl_tex_coord_in[0].st + vec2(0, -0.025*uProgress/SHOWER_TIME); - atomUV *= 0.2 * vec2(uSizeX, uSizeY) / SCALE; + atomUV *= 0.2 * vec2(uSizeX, uSizeY) / uScale; float atoms = pow((simplex3D(vec3(atomUV, uTime))), 5.0); - cogl_color_out.rgb += effectColor * atoms * masks.z; + cogl_color_out.rgb += uColor * atoms * masks.z; + cogl_color_out.a += atoms * masks.z; // These are pretty useful for understanding how this works. // cogl_color_out = vec4(masks.rgb, 1.0); @@ -224,8 +276,9 @@ if (utils.isInShellProcess()) { // cogl_color_out = vec4(vec3(shower), 1.0); // cogl_color_out = vec4(vec3(streaks), 1.0); // cogl_color_out = vec4(vec3(atoms), 1.0); - } - `); - }; + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } }); } \ No newline at end of file diff --git a/src/Fire.js b/src/Fire.js index 46a2b23..8f8173f 100644 --- a/src/Fire.js +++ b/src/Fire.js @@ -24,13 +24,18 @@ const utils = Me.imports.src.utils; ////////////////////////////////////////////////////////////////////////////////////////// // This effect is a homage to the good old Compiz days. However, it is implemented // // quite differently. While Compiz used a particle system, this effect uses a noise // -// shader. The noise is moved vertically over time and mapped to a configurable color // +// shader. The noise is moved vertically over time and mapped to a configurable color // // gradient. It is faded to transparency towards the edges of the window. In addition, // // there are a couple of moving gradients which fade-in or fade-out the fire effect. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -97,9 +102,21 @@ var Fire = class Fire { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -120,6 +137,12 @@ var Fire = class Fire { }; } + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } + // ----------------------------------------------------------------------- private stuff // This populates the preset dropdown menu for the fire options. @@ -219,48 +242,89 @@ var Fire = class Fire { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - // Load the gradient values from the settings. We directly inject the values in the - // GLSL code below. The shader is compiled once for each window-closing anyways. In - // the future, we may want to prevent this frequent recompilations of shaders, - // though. - const gradient = []; + this._uGradient = [ + this.get_uniform_location('uGradient1'), + this.get_uniform_location('uGradient2'), + this.get_uniform_location('uGradient3'), + this.get_uniform_location('uGradient4'), + this.get_uniform_location('uGradient5'), + ]; + + this._uForOpening = this.get_uniform_location('uForOpening'); + this._u3DNoise = this.get_uniform_location('u3DNoise'); + this._uScale = this.get_uniform_location('uScale'); + this._uMovementSpeed = this.get_uniform_location('uMovementSpeed'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + + // Load the gradient values from the settings. for (let i = 1; i <= 5; i++) { - const color = - Clutter.Color.from_string(settings.get_string('fire-color-' + i))[1]; - gradient.push(`vec4(${color.red / 255}, ${color.green / 255}, ${ - color.blue / 255}, ${color.alpha / 255})`); + const c = Clutter.Color.from_string(settings.get_string('fire-color-' + i))[1]; + this.set_uniform_float(this._uGradient[i - 1], 4, + [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); } - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._u3DNoise, 1, [settings.get_boolean('flame-3d-noise')]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('flame-scale')]); + this.set_uniform_float(this._uMovementSpeed, 1, [settings.get_double('flame-movement-speed')]); + // clang-format on + } + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} ${shaderSnippets.noise()} ${shaderSnippets.edgeMask()} + ${shaderSnippets.compositing()} + + uniform bool uForOpening; + uniform bool u3DNoise; + uniform float uScale; + uniform float uMovementSpeed; + uniform vec4 uGradient1; + uniform vec4 uGradient2; + uniform vec4 uGradient3; + uniform vec4 uGradient4; + uniform vec4 uGradient5; // These may be configurable in the future. const float EDGE_FADE = 70; const float FADE_WIDTH = 0.1; const float HIDE_TIME = 0.4; - const vec2 FIRE_SCALE = vec2(400, 600) * ${settings.get_double('flame-scale')}; - const float FIRE_SPEED = ${settings.get_double('flame-movement-speed')}; // This maps the input value from [0..1] to a color from the gradient. vec4 getFireColor(float v) { const float steps[5] = float[](0.0, 0.2, 0.35, 0.5, 0.8); - const vec4 colors[5] = vec4[]( - ${gradient[0]}, - ${gradient[1]}, - ${gradient[2]}, - ${gradient[3]}, - ${gradient[4]} + vec4 colors[5] = vec4[]( + uGradient1, + uGradient2, + uGradient3, + uGradient4, + uGradient5 ); if (v < steps[0]) { @@ -308,45 +372,50 @@ if (utils.isInShellProcess()) { // Fade at window borders. effectMask *= getAbsoluteEdgeMask(edgeFadeWidth); - #if ${forOpening ? '1' : '0'} + if (uForOpening) { windowMask = 1.0 - windowMask; - #endif + } return vec2(windowMask, effectMask); } + `; - void main() { + const code = ` + // Get a noise value which moves vertically in time. + vec2 uv = cogl_tex_coord_in[0].st * vec2(uSizeX, uSizeY) / vec2(400, 600) / uScale; + uv.y += uTime * uMovementSpeed; - // Get a noise value which moves vertically in time. - vec2 uv = cogl_tex_coord_in[0].st * vec2(uSizeX, uSizeY) / FIRE_SCALE; - uv.y += uTime * FIRE_SPEED; + float noise = u3DNoise ? simplex3DFractal(vec3(uv*4.0, uTime*uMovementSpeed*1.5)) + : simplex2DFractal(uv * 4.0); - #if ${settings.get_boolean('flame-3d-noise') ? 1 : 0} - float noise = simplex3DFractal(vec3(uv*4.0, uTime*FIRE_SPEED*1.5)); - #else - float noise = simplex2DFractal(uv * 4.0); - #endif + // Modulate noise by effect mask. + vec2 effectMask = effectMask(HIDE_TIME, FADE_WIDTH, EDGE_FADE); + noise *= effectMask.y; - // Modulate noise by effect mask. - vec2 effectMask = effectMask(HIDE_TIME, FADE_WIDTH, EDGE_FADE); - noise *= effectMask.y; + // Map noise value to color. + vec4 fire = getFireColor(noise); - // Map noise value to color. - vec4 fire = getFireColor(noise); - fire.rgb *= fire.a; + // Get the window texture. + cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st); - // Get the window texture and fade it according to the effect mask. - cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st) * effectMask.x; - - // Add the fire to the window. - cogl_color_out = cogl_color_out * (1.0 - fire.a) + fire; - - // These are pretty useful for understanding how this works. - // cogl_color_out = vec4(vec3(noise), 1); - // cogl_color_out = vec4(vec3(effectMask.x), 1); - // cogl_color_out = vec4(vec3(effectMask.y), 1); + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; } - `); - }; + + // Fade the window according to the effect mask. + cogl_color_out.a *= effectMask.x; + + // Add the fire to the window. + cogl_color_out = alphaOver(cogl_color_out, fire); + + // These are pretty useful for understanding how this works. + // cogl_color_out = vec4(vec3(noise), 1); + // cogl_color_out = vec4(vec3(effectMask.x), 1); + // cogl_color_out = vec4(vec3(effectMask.y), 1); + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } }); } \ No newline at end of file diff --git a/src/Hexagon.js b/src/Hexagon.js index 06ebcd2..34bd360 100644 --- a/src/Hexagon.js +++ b/src/Hexagon.js @@ -26,8 +26,13 @@ const utils = Me.imports.src.utils; // gradually shrink until the window is fully dissolved. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -79,9 +84,21 @@ var Hexagon = class Hexagon { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -101,6 +118,12 @@ var Hexagon = class Hexagon { 'scale-y': {from: 1.0, to: 1.0, mode: 1} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } } @@ -112,34 +135,71 @@ var Hexagon = class Hexagon { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); + + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uAdditiveBlending = this.get_uniform_location('uAdditiveBlending'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uScale = this.get_uniform_location('uScale'); + this._uLineWidth = this.get_uniform_location('uLineWidth'); + this._uGlowColor = this.get_uniform_location('uGlowColor'); + this._uLineColor = this.get_uniform_location('uLineColor'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { // Get the two configurable colors. They are directly injected into the shader code // below. - const glowColor = - Clutter.Color.from_string(settings.get_string('hexagon-glow-color'))[1]; - const lineColor = - Clutter.Color.from_string(settings.get_string('hexagon-line-color'))[1]; + const gc = Clutter.Color.from_string(settings.get_string('hexagon-glow-color'))[1]; + const lc = Clutter.Color.from_string(settings.get_string('hexagon-line-color'))[1]; // If we are currently performing integration test, the animation uses a fixed seed. const testMode = settings.get_boolean('test-mode'); + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uAdditiveBlending, 1, [settings.get_boolean('hexagon-additive-blending')]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('hexagon-scale')]); + this.set_uniform_float(this._uLineWidth, 1, [settings.get_double('hexagon-line-width')]); + this.set_uniform_float(this._uGlowColor, 4, [gc.red / 255, gc.green / 255, gc.blue / 255, gc.alpha / 255]); + this.set_uniform_float(this._uLineColor, 4, [lc.red / 255, lc.green / 255, lc.blue / 255, lc.alpha / 255]); + // clang-format on + } + + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + // Feel free to read the inline comments below to learn how this shader works. - this.set_shader_source(` + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} ${shaderSnippets.noise()} - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float SCALE = 10.0 * ${settings.get_double('hexagon-scale')}; - const float LINE_WIDTH = 0.02 * ${settings.get_double('hexagon-line-width')}; + uniform bool uForOpening; + uniform bool uAdditiveBlending; + uniform vec2 uSeed; + uniform float uScale; + uniform float uLineWidth; + uniform vec4 uGlowColor; + uniform vec4 uLineColor; // This methods generates a procedural hexagonal pattern. It returns four values: // result.xy: This contains cell-relative coordinates for the given point. @@ -182,69 +242,73 @@ if (utils.isInShellProcess()) { return vec4(cellCoords, dist, glow); } + `; - void main() { + const code = ` + // We simply inverse the progress for opening windows. + float progress = uForOpening ? 1.0-uProgress : uProgress; - // We simply inverse the progress for opening windows. - float progress = ${forOpening ? '1.0-uProgress' : 'uProgress'}; + // Add some smooth noise to the progress so that not every tile behaves the + // same. + float noise = simplex2D(cogl_tex_coord_in[0].st + uSeed); + progress = clamp(mix(noise - 1.0, noise + 1.0, progress), 0.0, 1.0); - // Add some smooth noise to the progress so that not every tile behaves the - // same. - float noise = simplex2D(cogl_tex_coord_in[0].st + SEED); - progress = clamp(mix(noise - 1.0, noise + 1.0, progress), 0.0, 1.0); + // glowProgress fades in in the first half of the animation, tileProgress fades + // in in the second half. + float glowProgress = smoothstep(0, 1, clamp(progress / 0.5, 0, 1)); + float tileProgress = smoothstep(0, 1, clamp((progress - 0.5) / 0.5, 0, 1)); - // glowProgress fades in in the first half of the animation, tileProgress fades - // in in the second half. - float glowProgress = smoothstep(0, 1, clamp(progress / 0.5, 0, 1)); - float tileProgress = smoothstep(0, 1, clamp((progress - 0.5) / 0.5, 0, 1)); + vec2 texScale = 0.1 * vec2(uSizeX, uSizeY) / uScale; + vec4 hex = getHexagons(cogl_tex_coord_in[0].st * texScale); - vec2 texScale = vec2(uSizeX, uSizeY) / SCALE; - vec4 hex = getHexagons(cogl_tex_coord_in[0].st * texScale); + if (tileProgress > hex.z) { - if (tileProgress > hex.z) { + // Crop outer parts of the shrinking tiles. + cogl_color_out.a = 0.0; - // Crop outer parts of the shrinking tiles. - cogl_color_out *= vec4(0.0); + } else { + // Make the tiles shrink by offsetting the texture lookup towards the edge + // of the cell. + vec2 lookupOffset = tileProgress * hex.xy / texScale / (1.0 - tileProgress); + cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st + lookupOffset); + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; + } + + vec4 glow = uGlowColor; + vec4 line = uLineColor; + + // For the glow, we accumulate a few exponentially scaled versions of hex.w. + glow.a *= pow(hex.w, 20.0) * 10.0 + + pow(hex.w, 10.0) * 5.0 + + pow(hex.w, 2.0) * 0.5; + + // Using step(uLineWidth, hex.z) would be simpler, but the below creates some + // fake antialiasing. + line.a *= 1.0 - smoothstep(uLineWidth*0.02*0.5, uLineWidth*0.02, hex.z); + + // Fade in the glowing lines. + glow.a *= glowProgress; + line.a *= glowProgress; + + // Do not add the hexagon lines onto transparent parts of the window. + glow *= cogl_color_out.a; + line *= cogl_color_out.a; + + if (uAdditiveBlending) { + cogl_color_out.rgb += glow.rgb * glow.a; + cogl_color_out.rgb += line.rgb * line.a; } else { - - // Make the tiles shrink by offsetting the texture lookup towards the edge - // of the cell. - vec2 lookupOffset = tileProgress * hex.xy / texScale / (1.0 - tileProgress); - cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st + lookupOffset); - - vec4 glow = vec4(${glowColor.red / 255}, ${glowColor.green / 255}, - ${glowColor.blue / 255}, ${glowColor.alpha / 255}); - vec4 line = vec4(${lineColor.red / 255}, ${lineColor.green / 255}, - ${lineColor.blue / 255}, ${lineColor.alpha / 255}); - - // For the glow, we accumulate a few exponentially scaled versions of hex.w. - glow.a *= pow(hex.w, 20.0) * 10.0 + - pow(hex.w, 10.0) * 5.0 + - pow(hex.w, 2.0) * 0.5; - - // Using step(LINE_WIDTH, hex.z) would be simpler, but the below creates some - // fake antialiasing. - line.a *= 1.0 - smoothstep(LINE_WIDTH*0.5, LINE_WIDTH, hex.z); - - // Fade in the glowing lines. - glow.a *= glowProgress; - line.a *= glowProgress; - - // Do not add the hexagon lines onto transparent parts of the window. - glow *= cogl_color_out.a; - line *= cogl_color_out.a; - - if (${settings.get_boolean('hexagon-additive-blending')}) { - cogl_color_out.rgb += glow.rgb * glow.a; - cogl_color_out.rgb += line.rgb * line.a; - } else { - cogl_color_out.rgb = mix(cogl_color_out.rgb, glow.rgb, glow.a); - cogl_color_out.rgb = mix(cogl_color_out.rgb, line.rgb, line.a); - } + cogl_color_out.rgb = mix(cogl_color_out.rgb, glow.rgb, glow.a); + cogl_color_out.rgb = mix(cogl_color_out.rgb, line.rgb, line.a); } } - `); - }; + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } }); } \ No newline at end of file diff --git a/src/Matrix.js b/src/Matrix.js index 834d36e..5aab908 100644 --- a/src/Matrix.js +++ b/src/Matrix.js @@ -29,8 +29,16 @@ const utils = Me.imports.src.utils; // documentation of vfunc_paint_target further down in this file. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; + +// This texture will be loaded when the effect is used for the first time. +let fontTexture = null; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -82,9 +90,21 @@ var Matrix = class Matrix { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -106,6 +126,13 @@ var Matrix = class Matrix { 'scale-y': {from: yScale, to: yScale, mode: 1} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + fontTexture = null; + } } @@ -117,38 +144,75 @@ var Matrix = class Matrix { if (utils.isInShellProcess()) { - const {Clutter, GdkPixbuf, Cogl} = imports.gi; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - // Load the font texture. As the shader is re-created for each window animation, - // this texture is also re-created each time. This could be improved in the future! - const fontData = GdkPixbuf.Pixbuf.new_from_resource('/img/matrixFont.png'); - this._fontTexture = new Clutter.Image(); - this._fontTexture.set_data( - fontData.get_pixels(), - fontData.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, - fontData.width, fontData.height, fontData.rowstride); + // Load the font texture. + if (fontTexture == null) { + const fontData = GdkPixbuf.Pixbuf.new_from_resource('/img/matrixFont.png'); + fontTexture = new Clutter.Image(); + fontTexture.set_data( + fontData.get_pixels(), + fontData.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, + fontData.width, fontData.height, fontData.rowstride); + } - const trailColor = - Clutter.Color.from_string(settings.get_string('matrix-trail-color'))[1]; - const tipColor = - Clutter.Color.from_string(settings.get_string('matrix-tip-color'))[1]; + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uFontTexture = this.get_uniform_location('uFontTexture'); + this._uTrailColor = this.get_uniform_location('uTrailColor'); + this._uTipColor = this.get_uniform_location('uTipColor'); + this._uLetterSize = this.get_uniform_location('uLetterSize'); + this._uRandomness = this.get_uniform_location('uRandomness'); + this._uOverShoot = this.get_uniform_location('uOverShoot'); + } - // The technique for this effect was inspired by - // https://www.shadertoy.com/view/ldccW4, however the implementation is quite - // different as the letters drop only once and there is no need for a noise texture. - this.set_shader_source(` + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + const c1 = Clutter.Color.from_string(settings.get_string('matrix-trail-color'))[1]; + const c2 = Clutter.Color.from_string(settings.get_string('matrix-tip-color'))[1]; + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uTrailColor, 3, [c1.red / 255, c1.green / 255, c1.blue / 255]); + this.set_uniform_float(this._uTipColor, 3, [c2.red / 255, c2.green / 255, c2.blue / 255]); + this.set_uniform_float(this._uLetterSize, 1, [settings.get_int('matrix-scale')]); + this.set_uniform_float(this._uRandomness, 1, [settings.get_double('matrix-randomness')]); + this.set_uniform_float(this._uOverShoot, 1, [settings.get_double('matrix-overshoot')]); + // clang-format on + } + + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. The technique for this effect was inspired by + // https://www.shadertoy.com/view/ldccW4, however the implementation is quite + // different as the letters drop only once and there is no need for a noise texture. + vfunc_build_pipeline() { + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} ${shaderSnippets.noise()} ${shaderSnippets.edgeMask()} + ${shaderSnippets.compositing()} + uniform bool uForOpening; uniform sampler2D uFontTexture; + uniform vec3 uTrailColor; + uniform vec3 uTipColor; + uniform float uLetterSize; + uniform float uRandomness; + uniform float uOverShoot; // These may be configurable in the future. const float EDGE_FADE = 30; @@ -156,16 +220,13 @@ if (utils.isInShellProcess()) { const float TRAIL_LENGTH = 0.2; const float FINAL_FADE_START_TIME = 0.8; const float LETTER_TILES = 16.0; - const float LETTER_SIZE = ${settings.get_int('matrix-scale')}; const float LETTER_FLICKER_SPEED = 2.0; - const float RANDOMNESS = ${settings.get_double('matrix-randomness')}; - const float OVERSHOOT = ${settings.get_double('matrix-overshoot')}; // This returns a flickering grid of random letters. float getText(vec2 fragCoord) { vec2 pixelCoords = fragCoord * vec2(uSizeX, uSizeY); - vec2 uv = mod(pixelCoords.xy, LETTER_SIZE)/LETTER_SIZE; - vec2 block = pixelCoords/LETTER_SIZE - uv; + vec2 uv = mod(pixelCoords.xy, uLetterSize)/uLetterSize; + vec2 block = pixelCoords/uLetterSize - uv; // Choose random letter. uv += floor(hash22(floor(hash22(block)*vec2(12.9898,78.233) + LETTER_FLICKER_SPEED*uTime + 42.254))*LETTER_TILES); @@ -179,10 +240,10 @@ if (utils.isInShellProcess()) { // the window texture. vec2 getRain(vec2 fragCoord) { float column = cogl_tex_coord_in[0].x * uSizeX; - column -= mod(column, LETTER_SIZE); + column -= mod(column, uLetterSize); - float delay = fract(sin(column)*78.233) * mix(0.0, 1.0, RANDOMNESS); - float speed = fract(cos(column)*12.989) * mix(0.0, 0.3, RANDOMNESS) + 1.5; + float delay = fract(sin(column)*78.233) * mix(0.0, 1.0, uRandomness); + float speed = fract(cos(column)*12.989) * mix(0.0, 0.3, uRandomness) + 1.5; float distToDrop = (uProgress*2-delay)*speed - cogl_tex_coord_in[0].y; @@ -193,50 +254,54 @@ if (utils.isInShellProcess()) { rainAlpha *= getAbsoluteEdgeMask(EDGE_FADE); // Add some variation to the drop start and end position. - float shorten = fract(sin(column+42.0)*33.423) * mix(0.0, OVERSHOOT*0.25, RANDOMNESS); + float shorten = fract(sin(column+42.0)*33.423) * mix(0.0, uOverShoot*0.25, uRandomness); rainAlpha *= smoothstep(0, 1, clamp(cogl_tex_coord_in[0].y / shorten, 0, 1)); rainAlpha *= smoothstep(0, 1, clamp((1.0 - cogl_tex_coord_in[0].y) / shorten, 0, 1)); - #if ${forOpening ? '1' : '0'} + if (uForOpening) { windowAlpha = 1.0 - windowAlpha; - #endif + } return vec2(rainAlpha, windowAlpha); } + `; - void main() { + const code = ` + vec2 coords = cogl_tex_coord_in[0].st; + coords.y = coords.y * (uOverShoot + 1.0) - uOverShoot * 0.5; - vec2 coords = cogl_tex_coord_in[0].st; - coords.y = coords.y * (OVERSHOOT + 1.0) - OVERSHOOT * 0.5; + // Get a cool matrix effect. See comments for those methods above. + vec2 rainMask = getRain(coords); + float textMask = getText(coords); - // Get a cool matrix effect. See comments for those methods above. - vec2 rain = getRain(coords); - float text = getText(coords); - - // Get the window texture and fade it according to the effect mask. - cogl_color_out = texture2D(uTexture, coords) * rain.y; - - // This is used to fade out the remaining trails in the end. - float finalFade = 1-clamp((uProgress-FINAL_FADE_START_TIME)/ - (1-FINAL_FADE_START_TIME), 0, 1); - float rainAlpha = finalFade * rain.x; - - // Add the matrix effect to the window. - vec3 trailColor = vec3(${trailColor.red / 255}, - ${trailColor.green / 255}, - ${trailColor.blue / 255}); - vec3 tipColor = vec3(${tipColor.red / 255}, - ${tipColor.green / 255}, - ${tipColor.blue / 255}); - cogl_color_out.rgb += mix(trailColor, tipColor, min(1, pow(rainAlpha+0.1, 4))) * rainAlpha * text; - - // These are pretty useful for understanding how this works. - // cogl_color_out = vec4(vec3(text), 1); - // cogl_color_out = vec4(vec3(rain.x), 1); - // cogl_color_out = vec4(vec3(rain.y), 1); + // Get the window texture. + cogl_color_out = texture2D(uTexture, coords); + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; } - `); - }; + + // Fade the window according to the effect mask. + cogl_color_out.a *= rainMask.y; + + // This is used to fade out the remaining trails in the end. + float finalFade = 1-clamp((uProgress-FINAL_FADE_START_TIME)/ + (1-FINAL_FADE_START_TIME), 0, 1); + float rainAlpha = finalFade * rainMask.x; + + // Add the matrix effect to the window. + vec4 text = vec4(mix(uTrailColor, uTipColor, min(1, pow(rainAlpha+0.1, 4))), rainAlpha * textMask); + cogl_color_out = alphaOver(cogl_color_out, text); + + // These are pretty useful for understanding how this works. + // cogl_color_out = vec4(vec3(textMask), 1); + // cogl_color_out = vec4(vec3(rainMask.x), 1); + // cogl_color_out = vec4(vec3(rainMask.y), 1); + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } // This is overridden to bind the font texture for drawing. Sadly, this seems to be // impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called @@ -244,8 +309,9 @@ if (utils.isInShellProcess()) { // https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598 vfunc_paint_target(node, paint_context) { const pipeline = this.get_pipeline(); - pipeline.set_layer_texture(1, this._fontTexture.get_texture()); - this.set_uniform_value('uFontTexture', 1); + pipeline.set_layer_texture(1, fontTexture.get_texture()); + pipeline.set_uniform_1i(this._uFontTexture, 1); + super.vfunc_paint_target(node, paint_context); } }); diff --git a/src/SnapOfDisintegration.js b/src/SnapOfDisintegration.js index 7a94ed4..45435ec 100644 --- a/src/SnapOfDisintegration.js +++ b/src/SnapOfDisintegration.js @@ -31,8 +31,16 @@ const utils = Me.imports.src.utils; // documentation of vfunc_paint_target further down in this file. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; + +// This texture will be loaded when the effect is used for the first time. +let dustTexture = null; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -81,9 +89,21 @@ var SnapOfDisintegration = class SnapOfDisintegration { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(actor, settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -104,6 +124,13 @@ var SnapOfDisintegration = class SnapOfDisintegration { 'scale-y': {from: 1.2, to: 1.2, mode: 1} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + dustTexture = null; + } } @@ -115,116 +142,151 @@ var SnapOfDisintegration = class SnapOfDisintegration { if (utils.isInShellProcess()) { - const {Clutter, GdkPixbuf, Cogl} = imports.gi; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(actor, settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - // Load the dust texture. As the shader is re-created for each window animation, - // this texture is also re-created each time. This could be improved in the future! - const dustData = GdkPixbuf.Pixbuf.new_from_resource('/img/dust.png'); - this._dustTexture = new Clutter.Image(); - this._dustTexture.set_data(dustData.get_pixels(), Cogl.PixelFormat.RGB_888, - dustData.width, dustData.height, dustData.rowstride); + // Load the dust texture. + if (dustTexture == null) { + const dustData = GdkPixbuf.Pixbuf.new_from_resource('/img/dust.png'); + dustTexture = new Clutter.Image(); + dustTexture.set_data(dustData.get_pixels(), Cogl.PixelFormat.RGB_888, + dustData.width, dustData.height, dustData.rowstride); + } + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uDustTexture = this.get_uniform_location('uDustTexture'); + this._uDustColor = this.get_uniform_location('uDustColor'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uDustScale = this.get_uniform_location('uDustScale'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { // The dust particles will fade to this color over time. - const color = Clutter.Color.from_string(settings.get_string('snap-color'))[1]; + const c = Clutter.Color.from_string(settings.get_string('snap-color'))[1]; // If we are currently performing integration test, the animation uses a fixed seed. const testMode = settings.get_boolean('test-mode'); - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uDustColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uDustScale, 1, [settings.get_double('snap-scale')]); + // clang-format on + } + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} ${shaderSnippets.noise()} ${shaderSnippets.math2D()} + uniform bool uForOpening; uniform sampler2D uDustTexture; + uniform vec4 uDustColor; + uniform vec2 uSeed; + uniform float uDustScale; - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float DUST_SCALE = ${settings.get_double('snap-scale')}; const float DUST_LAYERS = 4; const float GROW_INTENSITY = 0.05; const float SHRINK_INTENSITY = 0.05; const float WIND_INTENSITY = 0.05; const float ACTOR_SCALE = 1.2; const float PADDING = ACTOR_SCALE / 2.0 - 0.5; - const vec4 DUST_COLOR = vec4(${color.red / 255}, ${color.green / 255}, - ${color.blue / 255}, ${color.alpha / 255}); - void main() { + `; - // We simply inverse the progress for opening windows. - float progress = ${forOpening ? 'uProgress' : '1.0 - uProgress'}; + const code = ` + // We simply inverse the progress for opening windows. + float progress = uForOpening ? uProgress : 1.0 - uProgress; - float gradient = cogl_tex_coord_in[0].t * ACTOR_SCALE - PADDING; - progress = 2.0 - gradient - 2.0 * progress; - progress = progress + 0.25 - 0.5 * simplex2D((cogl_tex_coord_in[0].st + SEED) * 2.0); - progress = pow(max(0, progress), 2.0); + float gradient = cogl_tex_coord_in[0].t * ACTOR_SCALE - PADDING; + progress = 2.0 - gradient - 2.0 * progress; + progress = progress + 0.25 - 0.5 * simplex2D((cogl_tex_coord_in[0].st + uSeed) * 2.0); + progress = pow(max(0, progress), 2.0); - // This may help you to understand how this effect works. - // cogl_color_out = vec4(progress, 0, 0, 0); - // return; + // This may help you to understand how this effect works. + // cogl_color_out = vec4(progress, 0, 0, 0); + // return; - cogl_color_out = vec4(0, 0, 0, 0); + cogl_color_out = vec4(0, 0, 0, 0); - for (float i=0; i 0) { + direction *= -1; + } + + // Flip direction for half the layers. + if (factor > 0.5) { + direction *= -1; + } + + // We grow the layer along the random direction, shrink it orthogonally to it + // and scale it up slightly. + float dist = distToLine(vec2(0.0), direction, coords); + vec2 grow = direction * dist * mix(0, GROW_INTENSITY, progress); + vec2 shrink = vec2(direction.y, -direction.x) * dist * mix(0, SHRINK_INTENSITY, progress); + float scale = mix(1.0, 1.05, factor * progress); + coords = (coords + grow + shrink) / scale; + + // Add some wind. + coords.x += WIND_INTENSITY * progress * (uForOpening ? 1.0 : -1.0); + + // Now check wether there is actually something in the current dust layer at + // the coords position. + vec2 dustCoords = (coords + uSeed) * vec2(uSizeX, uSizeY) / uDustScale / 100.0; + vec2 dustMap = texture2D(uDustTexture, dustCoords).rg; + float dustGroup = floor(dustMap.g * DUST_LAYERS * 0.999); + + if (dustGroup == i) { + + // Get the window color. + vec4 windowColor = texture2D(uTexture, coords + 0.5); - // Create a random direction. - float factor = DUST_LAYERS == 1 ? 0 : i/(DUST_LAYERS-1); - float angle = 123.123 * (SEED.x + factor); - vec2 direction = vec2(1.0, 0.0); - direction = rotate(direction, angle); - - // Flip direction for one side of the window. - vec2 coords = cogl_tex_coord_in[0].st * ACTOR_SCALE - PADDING - 0.5; - if (getWinding(direction, coords) > 0) { - direction *= -1; - } - - // Flip direction for half the layers. - if (factor > 0.5) { - direction *= -1; + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (windowColor.a > 0) { + windowColor.rgb /= windowColor.a; } - // We grow the layer along the random direction, shrink it orthogonally to it - // and scale it up slightly. - float dist = distToLine(vec2(0.0), direction, coords); - vec2 grow = direction * dist * mix(0, GROW_INTENSITY, progress); - vec2 shrink = vec2(direction.y, -direction.x) * dist * mix(0, SHRINK_INTENSITY, progress); - float scale = mix(1.0, 1.05, factor * progress); - coords = (coords + grow + shrink) / scale; + // Fade the window color to uDustColor. + vec3 dustColor = mix(windowColor.rgb, uDustColor.rgb, uDustColor.a); + windowColor.rgb = mix(windowColor.rgb, dustColor, progress); - // Add some wind. - coords.x = coords.x ${forOpening ? ' + ' : ' - '} progress * WIND_INTENSITY; - - // Now check wether there is actually something in the current dust layer at - // the coords position. - vec2 dustCoords = (coords + SEED) * vec2(uSizeX, uSizeY) / DUST_SCALE / 100.0; - vec2 dustMap = texture2D(uDustTexture, dustCoords).rg; - float dustGroup = floor(dustMap.g * DUST_LAYERS * 0.999); - - if (dustGroup == i) { - - // Fade the window color to DUST_COLOR. - vec4 windowColor = texture2D(uTexture, coords + 0.5); - vec3 dustColor = mix(windowColor.rgb, DUST_COLOR.rgb, DUST_COLOR.a); - windowColor.rgb = mix(windowColor.rgb, dustColor*windowColor.a, progress); - - // Dissolve the dust particles. - float dissolve = (dustMap.x - progress) > 0 ? 1 : 0; - windowColor *= dissolve; - - // Blend the layers. - cogl_color_out = mix(cogl_color_out, windowColor, windowColor.a); + // Dissolve and blend the layers. + if (dustMap.x - progress > 0) { + cogl_color_out = windowColor; } } } - `); - }; + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } // This is overridden to bind the dust texture for drawing. Sadly, this seems to be // impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called @@ -234,9 +296,9 @@ if (utils.isInShellProcess()) { const pipeline = this.get_pipeline(); pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, Cogl.PipelineFilter.LINEAR); - pipeline.set_layer_texture(1, this._dustTexture.get_texture()); + pipeline.set_layer_texture(1, dustTexture.get_texture()); pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); - this.set_uniform_value('uDustTexture', 1); + pipeline.set_uniform_1i(this._uDustTexture, 1); super.vfunc_paint_target(node, paint_context); } }); diff --git a/src/TRexAttack.js b/src/TRexAttack.js index e76f2c1..b43ddcc 100644 --- a/src/TRexAttack.js +++ b/src/TRexAttack.js @@ -27,8 +27,16 @@ const utils = Me.imports.src.utils; // documentation of vfunc_paint_target further down in this file. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; + +// This texture will be loaded when the effect is used for the first time. +let clawTexture = null; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -79,11 +87,22 @@ var TRexAttack = class TRexAttack { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); - } + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; + } // The tweakTransition() is called from extension.js to tweak a window's open / close // transitions - usually windows are faded in / out and scaled up / down by GNOME Shell. // The parameter 'forOpening' is set to true if this is called for a window-open @@ -102,6 +121,13 @@ var TRexAttack = class TRexAttack { 'scale-y': {from: forOpening ? warp : 1.0, to: forOpening ? 1.0 : warp, mode: 3} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + clawTexture = null; + } } @@ -113,41 +139,74 @@ var TRexAttack = class TRexAttack { if (utils.isInShellProcess()) { - const {Clutter, GdkPixbuf, Cogl} = imports.gi; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - // Load the claw texture. As the shader is re-created for each window animation, - // this texture is also re-created each time. This could be improved in the future! - // See assets/README.md for how this texture was created. - const clawData = GdkPixbuf.Pixbuf.new_from_resource('/img/claws.png'); - this._clawTexture = new Clutter.Image(); - this._clawTexture.set_data(clawData.get_pixels(), Cogl.PixelFormat.RGB_888, - clawData.width, clawData.height, clawData.rowstride); + // Load the claw texture. + if (clawTexture == null) { + const clawData = GdkPixbuf.Pixbuf.new_from_resource('/img/claws.png'); + clawTexture = new Clutter.Image(); + clawTexture.set_data(clawData.get_pixels(), Cogl.PixelFormat.RGB_888, + clawData.width, clawData.height, clawData.rowstride); + } - const color = - Clutter.Color.from_string(settings.get_string('claw-scratch-color'))[1]; + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uClawTexture = this.get_uniform_location('uClawTexture'); + this._uFlashColor = this.get_uniform_location('uFlashColor'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uClawSize = this.get_uniform_location('uClawSize'); + this._uNumClaws = this.get_uniform_location('uNumClaws'); + this._uWarpIntensity = this.get_uniform_location('uWarpIntensity'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + const c = Clutter.Color.from_string(settings.get_string('claw-scratch-color'))[1]; // If we are currently performing integration test, the animation uses a fixed seed. const testMode = settings.get_boolean('test-mode'); - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uFlashColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uClawSize, 1, [settings.get_double('claw-scratch-scale')]); + this.set_uniform_float(this._uNumClaws, 1, [settings.get_int('claw-scratch-count')]); + this.set_uniform_float(this._uWarpIntensity, 1, [settings.get_double('claw-scratch-warp')]); + // clang-format on + } + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } + + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` // Inject some common shader snippets. ${shaderSnippets.standardUniforms()} ${shaderSnippets.noise()} + ${shaderSnippets.compositing()} // See assets/README.md for how this texture was created. + uniform bool uForOpening; uniform sampler2D uClawTexture; + uniform vec4 uFlashColor; + uniform vec2 uSeed; + uniform float uClawSize; + uniform float uNumClaws; + uniform float uWarpIntensity; - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float CLAW_SIZE = ${settings.get_double('claw-scratch-scale')}; - const float NUM_CLAWS = ${settings.get_int('claw-scratch-count')}; - const float WARP_INTENSITY = 1.0 + ${settings.get_double('claw-scratch-warp')}; const float FLASH_INTENSITY = 0.1; const float MAX_SPAWN_TIME = 0.6; // Scratches will only start in the first half of the animation. const float FF_TIME = 0.6; // Relative time for the final fade to transparency. @@ -188,60 +247,64 @@ if (utils.isInShellProcess()) { // Clamp resulting coordinates. return clamp(cellUV, vec2(0), vec2(1)); } + `; - void main() { + const code = ` + float progress = uForOpening ? 1.0-uProgress : uProgress; - float progress = ${forOpening ? '1.0-uProgress' : 'uProgress'}; + // Warp the texture coordinates to create a blow-up effect. + vec2 coords = cogl_tex_coord_in[0].st * 2.0 - 1.0; + float dist = length(coords); + coords = (coords/dist * pow(dist, 1.0 + uWarpIntensity)) * 0.5 + 0.5; + coords = mix(cogl_tex_coord_in[0].st, coords, progress); - // Warp the texture coordinates to create a blow-up effect. - vec2 coords = cogl_tex_coord_in[0].st * 2.0 - 1.0; - float dist = length(coords); - coords = (coords/dist * pow(dist, WARP_INTENSITY)) * 0.5 + 0.5; - coords = mix(cogl_tex_coord_in[0].st, coords, progress); - - // Accumulate several random scratches. The color in the scratch map refers to the - // relative time when the respective part will become invisible. Therefore we can - // add a value to make the scratch appear later. - float scratchMap = 1.0; - for (int i=0; i= 1) { - flashIntensity = 0; - } - - // Hide flashes where there is now window. - flashIntensity *= cogl_color_out.a; - - // Hide scratched out parts. - cogl_color_out *= (scratchMap > progress ? 1 : 0); - - vec3 flashColor = vec3(${color.red / 255}, - ${color.green / 255}, - ${color.blue / 255}) * ${color.alpha / 255}; - - cogl_color_out.rgb += flashIntensity * mix(flashColor, vec3(0), progress); - - // Fade out the remaining shards. - float fadeProgress = smoothstep(0, 1, (progress - 1.0 + FF_TIME)/FF_TIME); - cogl_color_out *= sqrt(1-fadeProgress*fadeProgress); - - // These are pretty useful for understanding how this works. - // cogl_color_out = vec4(vec3(flashIntensity), 1); - // cogl_color_out = vec4(vec3(scratchMap), 1); + // Accumulate several random scratches. The color in the scratch map refers to the + // relative time when the respective part will become invisible. Therefore we can + // add a value to make the scratch appear later. + float scratchMap = 1.0; + for (int i=0; i 0) { + cogl_color_out.rgb /= cogl_color_out.a; + } + + // Add colorful flashes. + float flashIntensity = 1.0 / FLASH_INTENSITY * (scratchMap - progress) + 1; + if (flashIntensity < 0 || flashIntensity >= 1) { + flashIntensity = 0; + } + + // Hide flashes where there is now window. + vec4 flash = uFlashColor; + flash.a *= flashIntensity * cogl_color_out.a * (1.0 - progress); + + // Hide scratched out parts. + cogl_color_out.a *= (scratchMap > progress ? 1 : 0); + + // Add flash color. + cogl_color_out = alphaOver(cogl_color_out, flash); + + // Fade out the remaining shards. + float fadeProgress = smoothstep(0, 1, (progress - 1.0 + FF_TIME)/FF_TIME); + cogl_color_out.a *= sqrt(1-fadeProgress*fadeProgress); + + // These are pretty useful for understanding how this works. + // cogl_color_out = vec4(vec3(flashIntensity), 1); + // cogl_color_out = vec4(vec3(scratchMap), 1); + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } // This is overridden to bind the claw texture for drawing. Sadly, this seems to be // impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called @@ -249,8 +312,9 @@ if (utils.isInShellProcess()) { // https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598 vfunc_paint_target(node, paint_context) { const pipeline = this.get_pipeline(); - pipeline.set_layer_texture(1, this._clawTexture.get_texture()); - this.set_uniform_value('uClawTexture', 1); + pipeline.set_layer_texture(1, clawTexture.get_texture()); + pipeline.set_uniform_1i(this._uClawTexture, 1); + super.vfunc_paint_target(node, paint_context); } }); diff --git a/src/TVEffect.js b/src/TVEffect.js index 826cf57..f6463ef 100644 --- a/src/TVEffect.js +++ b/src/TVEffect.js @@ -27,8 +27,13 @@ const utils = Me.imports.src.utils; // the center. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // The effect class is completely static. It can be used to get some metadata (like the // effect's name or supported GNOME Shell versions), to initialize the respective page of @@ -76,9 +81,21 @@ var TVEffect = class TVEffect { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -98,6 +115,12 @@ var TVEffect = class TVEffect { 'scale-y': {from: forOpening ? 0.5 : 1.0, to: forOpening ? 1.0 : 0.5, mode: 3} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } } @@ -109,29 +132,55 @@ var TVEffect = class TVEffect { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - const color = Clutter.Color.from_string(settings.get_string('tv-effect-color'))[1]; + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uColor = this.get_uniform_location('uColor'); + } - this.set_shader_source(` + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + const c = Clutter.Color.from_string(settings.get_string('tv-effect-color'))[1]; - // Inject some common shader snippets. - ${shaderSnippets.standardUniforms()} + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + // clang-format on + } - const float BLUR_WIDTH = 0.01; // Width of the gradients. - const float TB_TIME = 0.7; // Relative time for the top/bottom animation. - const float LR_TIME = 0.4; // Relative time for the left/right animation. - const float LR_DELAY = 0.6; // Delay after which the left/right animation starts. - const float FF_TIME = 0.1; // Relative time for the final fade to transparency. + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } - void main() { + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` + // Inject some common shader snippets. + ${shaderSnippets.standardUniforms()} - float progress = ${forOpening ? '1.0-uProgress' : 'uProgress'}; + uniform bool uForOpening; + uniform vec3 uColor; + + const float BLUR_WIDTH = 0.01; // Width of the gradients. + const float TB_TIME = 0.7; // Relative time for the top/bottom animation. + const float LR_TIME = 0.4; // Relative time for the left/right animation. + const float LR_DELAY = 0.6; // Delay after which the left/right animation starts. + const float FF_TIME = 0.1; // Relative time for the final fade to transparency. + `; + + const code = ` + float progress = uForOpening ? 1.0-uProgress : uProgress; // All of these are in [0..1] during the different stages of the animation. // tb refers to the top-bottom animation. @@ -157,22 +206,24 @@ if (utils.isInShellProcess()) { // Assemble the final alpha value. float mask = tbMask * lrMask * ffMask; - vec4 windowColor = texture2D(uTexture, cogl_tex_coord_in[0].st); - vec4 effectColor = vec4(${color.red / 255}, - ${color.green / 255}, - ${color.blue / 255}, 1.0) * windowColor.a; + cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st); + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; + } - windowColor.rgb = mix(windowColor.rgb, effectColor.rgb, smoothstep(0, 1, progress)); - - cogl_color_out = windowColor * mask; + cogl_color_out.rgb = mix(cogl_color_out.rgb, uColor * cogl_color_out.a, smoothstep(0, 1, progress)); + cogl_color_out.a *= mask; // These are pretty useful for understanding how this works. // cogl_color_out = vec4(vec3(tbMask), 1); // cogl_color_out = vec4(vec3(lrMask), 1); // cogl_color_out = vec4(vec3(ffMask), 1); // cogl_color_out = vec4(vec3(mask), 1); - } - `); - }; + `; + + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, code, true); + } }); } \ No newline at end of file diff --git a/src/Wisps.js b/src/Wisps.js index ccbc608..42b14fa 100644 --- a/src/Wisps.js +++ b/src/Wisps.js @@ -26,8 +26,13 @@ const utils = Me.imports.src.utils; // fairies. It's implemented with several overlaid grids of randomly moving points. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. -let Shader = null; +// The shader class for this effect is registered further down in this file. When this +// effect is used for the first time, an instance of this shader class is created. Once +// the effect is finished, the shader will be stored in the freeShaders array and will +// then be reused if a new shader is requested. ShaderClass which will be used whenever +// this effect is used. +let ShaderClass = null; +let freeShaders = []; // This will be called in various places where a unique identifier for this effect is // required. It should match the prefix of the settings keys which store whether the @@ -76,9 +81,21 @@ var Wisps = class Wisps { // ---------------------------------------------------------------- API for extension.js - // This is called from extension.js whenever a window is closed with this effect. - static createShader(actor, settings, forOpening) { - return new Shader(settings, forOpening); + // This is called from extension.js whenever a window is opened or closed with this + // effect. It returns an instance of the shader class, trying to reuse previously + // created shaders. + static getShader(actor, settings, forOpening) { + let shader; + + if (freeShaders.length == 0) { + shader = new ShaderClass(); + } else { + shader = freeShaders.pop(); + } + + shader.setUniforms(actor, settings, forOpening); + + return shader; } // The tweakTransition() is called from extension.js to tweak a window's open / close @@ -98,6 +115,12 @@ var Wisps = class Wisps { 'scale-y': {from: forOpening ? 0.9 : 1.0, to: forOpening ? 1.0 : 0.9, mode: 3} }; } + + // This is called from extension.js if the extension is disabled. This should free all + // static resources. + static cleanUp() { + freeShaders = []; + } } @@ -109,88 +132,121 @@ var Wisps = class Wisps { if (utils.isInShellProcess()) { - const Clutter = imports.gi.Clutter; - const shaderSnippets = Me.imports.src.shaderSnippets; + const {Clutter, Shell} = imports.gi; + const shaderSnippets = Me.imports.src.shaderSnippets; - Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect { - _init(settings, forOpening) { - super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER}); + ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect { + // This is called when the effect is used for the first time. This can be used to + // store all required uniform locations. + _init() { + super._init(); - const color = Clutter.Color.from_string(settings.get_string('wisps-color'))[1]; + this._uForOpening = this.get_uniform_location('uForOpening'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uColor = this.get_uniform_location('uColor'); + this._uScale = this.get_uniform_location('uScale'); + } + + // This is called each time the effect is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + setUniforms(actor, settings, forOpening) { + const c = Clutter.Color.from_string(settings.get_string('wisps-color'))[1]; // If we are currently performing integration test, the animation uses a fixed seed. const testMode = settings.get_boolean('test-mode'); - this.set_shader_source(` + // clang-format off + this.set_uniform_float(this._uForOpening, 1, [forOpening]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('wisps-scale')]); + // clang-format on + } - // Inject some common shader snippets. - ${shaderSnippets.standardUniforms()} - ${shaderSnippets.noise()} - ${shaderSnippets.edgeMask()} + // This is called by extension.js when the shader is not used anymore. We will store + // this instance of the shader so that it can be re-used in th future. + free() { + freeShaders.push(this); + } - const vec2 SEED = vec2(${testMode ? 0 : Math.random()}, - ${testMode ? 0 : Math.random()}); - const float WISPS_RADIUS = 20.0; - const float WISPS_SPEED = 10.0; - const float WISPS_SPACING = 40 + WISPS_RADIUS; - const int WISPS_LAYERS = 8; - const float WISPS_IN_TIME = 0.5; - const float WINDOW_OUT_TIME = 1.0; + // This is called by the constructor. This means, it's only called when the effect + // is used for the first time. + vfunc_build_pipeline() { + const declarations = ` + // Inject some common shader snippets. + ${shaderSnippets.standardUniforms()} + ${shaderSnippets.noise()} + ${shaderSnippets.edgeMask()} + ${shaderSnippets.compositing()} - // Returns a grid of randomly moving points. Each grid cell contains one point which - // moves on an ellipse. - float getWisps(vec2 texCoords, float gridSize, vec2 seed) { + uniform bool uForOpening; + uniform vec2 uSeed; + uniform vec3 uColor; + uniform float uScale; - // Shift coordinates by a random offset and make sure the have a 1:1 aspect ratio. - vec2 coords = (texCoords + hash22(seed)) * vec2(uSizeX, uSizeY); + const float WISPS_RADIUS = 20.0; + const float WISPS_SPEED = 10.0; + const float WISPS_SPACING = 40 + WISPS_RADIUS; + const int WISPS_LAYERS = 8; + const float WISPS_IN_TIME = 0.5; + const float WINDOW_OUT_TIME = 1.0; - // Apply global scale. - coords /= gridSize; + // Returns a grid of randomly moving points. Each grid cell contains one point which + // moves on an ellipse. + float getWisps(vec2 texCoords, float gridSize, vec2 seed) { - // Get grid cell coordinates in [0..1]. - vec2 cellUV = mod(coords, vec2(1)); + // Shift coordinates by a random offset and make sure the have a 1:1 aspect ratio. + vec2 coords = (texCoords + hash22(seed)) * vec2(uSizeX, uSizeY); - // This is unique for each cell. - vec2 cellID = coords-cellUV + vec2(362.456); + // Apply global scale. + coords /= gridSize; - // Add random rotation, scale and offset to each grid cell. - float speed = mix(10.0, 15.0, hash12(cellID*seed*134.451)) / gridSize * WISPS_SPEED; - float rotation = mix( 0.0, 6.283, hash12(cellID*seed*54.4129)); - float radius = mix( 0.5, 1.0, hash12(cellID*seed*19.1249)) * WISPS_RADIUS; - float roundness = mix(-1.0, 1.0, hash12(cellID*seed*7.51949)); + // Get grid cell coordinates in [0..1]. + vec2 cellUV = mod(coords, vec2(1)); - vec2 offset = vec2(sin(speed * (uTime+1)) * roundness, cos(speed * (uTime+1))); - offset *= 0.5 - 0.5 * radius / gridSize; - offset = vec2(offset.x * cos(rotation) - offset.y * sin(rotation), - offset.x * sin(rotation) + offset.y * cos(rotation)); + // This is unique for each cell. + vec2 cellID = coords-cellUV + vec2(362.456); - cellUV += offset; + // Add random rotation, scale and offset to each grid cell. + float speed = mix(10.0, 15.0, hash12(cellID*seed*134.451)) / gridSize * WISPS_SPEED; + float rotation = mix( 0.0, 6.283, hash12(cellID*seed*54.4129)); + float radius = mix( 0.5, 1.0, hash12(cellID*seed*19.1249)) * WISPS_RADIUS; + float roundness = mix(-1.0, 1.0, hash12(cellID*seed*7.51949)); - // Use distance to center of shifted / rotated UV coordinates to draw a glaring point. - float dist = length(cellUV - 0.5) * gridSize / radius; - if (dist < 1.0) { - return min(10, 0.01 / pow(dist, 2.0)); + vec2 offset = vec2(sin(speed * (uTime+1)) * roundness, cos(speed * (uTime+1))); + offset *= 0.5 - 0.5 * radius / gridSize; + offset = vec2(offset.x * cos(rotation) - offset.y * sin(rotation), + offset.x * sin(rotation) + offset.y * cos(rotation)); + + cellUV += offset; + + // Use distance to center of shifted / rotated UV coordinates to draw a glaring point. + float dist = length(cellUV - 0.5) * gridSize / radius; + if (dist < 1.0) { + return min(5, 0.01 / pow(dist, 2.0)); + } + + return 0.0; } + `; - return 0.0; - } - - void main() { - - float progress = ${forOpening ? '1.0-uProgress' : 'uProgress'}; + const code = ` + float progress = uForOpening ? 1.0-uProgress : uProgress; // Get the color of the window. - vec4 windowColor = texture2D(uTexture, cogl_tex_coord_in[0].st); - vec4 effectColor = vec4(${color.red / 255}, - ${color.green / 255}, - ${color.blue / 255}, 1.0) * windowColor.a; + cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st); + + // Shell.GLSLEffect uses straight alpha. So we have to convert from premultiplied. + if (cogl_color_out.a > 0) { + cogl_color_out.rgb /= cogl_color_out.a; + } // Compute several layers of moving wisps. vec2 uv = (cogl_tex_coord_in[0].st-0.5) / mix(1.0, 0.5, progress) + 0.5; - uv /= ${settings.get_double('wisps-scale')}; + uv /= uScale; float wisps = 0; for (int i=0; i