Skip to content

The Good, The Bad, and the Shadertoy

raeleus edited this page Feb 6, 2021 · 18 revisions

shadertoy-title

Shadertoy is host to a plethora of awesome shader effects that you can apply to your game. It's not as simple as copy and paste, however. The goal of this tutorial is not to show you the finer details of shaders. No. There are tons of books on this subject that you can read, and you frankly won't be much closer to actually implementing something useful in your game. The aim of this tutorial is to show you how to convert a shader from Shadertoy to one that is compatible with libGDX. Then, you can implement GDX-VFX to streamline and combine your effects.

Sample Project Setup

We're going to make a small LWJGL3 project to demonstrate the power of shaders. Or more specifically, how to borrow a shader and profit.

Desktop Project

Make sure your desktop project looks something like this.

public class DesktopLauncher {
    public static void main(String[] arg) {
        Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
        config.setWindowedMode(480, 480);
        new Lwjgl3Application(new Core(), config);
    }
}

Core Project

Make a basic game as seen below. Let's ignore Screen, Viewport, Camera, Stage, and all those regular niceties. We're doing game design on the cheap today.

package com.ray3k.demonstration;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;

public class Core extends ApplicationAdapter {
    @Override
    public void create () {
        
    }
    
    @Override
    public void render () {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    }
    
    @Override
    public void dispose () {
        
    }
}

image

Add the following jungle image to your project's assets. This will serve as our epic background.
jungle

It's pretty simple to draw the jungle background with SpriteBatch if you ignore all the best practices of libGDX.

...
Texture jungleTexture;
SpriteBatch batch;

@Override
public void create () {
    jungleTexture = new Texture("jungle.png");
    batch = new SpriteBatch();
}

@Override
public void render () {
    Gdx.gl.glClearColor(0, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    
    batch.begin();
    batch.draw(jungleTexture, 0, 0);
    batch.end();
}

@Override
public void dispose () {
    jungleTexture.dispose();
    batch.dispose();
}
...

image

And now it is time for our brave protagonist. She'll follow the mouse for some smooth, fruit action. She's so nice, she's a peach! Add the following image to your assets and load the texture as before. Render it at the following calculated coordinates.
peach

batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);

image

Playing with Shaders

So now we're going to get into shaders. Shaders are programs that basically render everything you see and allow you to add some wicked visual effects. We'll take inspiration from shadertoy.com. Shadertoy has quite the selection of freely available shaders, but a lot of them are psychedellic color displays not useful to us game designers. We'll focus on this one that adds an underwater or drunk effect. Stop tipping that bottle!

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    
    uv.y += (cos((uv.y + (iTime * 0.04)) * 45.0) * 0.0019) +
        (cos((uv.y + (iTime * 0.1)) * 10.0) * 0.002);

    uv.x += (sin((uv.y + (iTime * 0.07)) * 15.0) * 0.0029) +
        (sin((uv.y + (iTime * 0.1)) * 15.0) * 0.002);
    
    vec4 tex_color = texture(iChannel0, uv);
    
    for(int k = 0 ; k < 9 ; ++k){
        
    }
    
    tex_color+= vec4(0,26./256.,51./256.,1.);
    
    fragColor = tex_color;
}

Save the above as a text file called "underwater.frag" in your assets folder. This is what we call a fragment shader, which operates on each pixel for the purposes of our example. I could go into the finer details of vertex shaders and other technical crap, but I don't care. Just read about it here, here, and here. Anyway, this is how we load that shader in GDX.

...

ShaderProgram shader;

@Override
public void create() {
    ...
    batch = new SpriteBatch();
    shader = new ShaderProgram(batch.getShader().getVertexShaderSource(), Gdx.files.internal("underwater.frag").readString());
    if (!shader.isCompiled()){
        System.out.println(shader.getLog());
    }
}

@Override
public void render() {
    ...
    batch.setShader(shader);
    batch.begin();
    ...
}

@Override
public void dispose() {
    ...
    shader.dispose();
}
...

"Why didn't that work?" you must be thinking to yourself. Did you really think you could just copy paste that code into libGDX and expect it to work? You fool! Thankfully, you logged the errors to the console.
image

Lets break down how we will adjust the shader so it will work with the sample project. At the very top of the "underwater.frag" file, we need to add the following.

#ifdef GL_ES
    #define PRECISION mediump
    precision PRECISION float;
    precision PRECISION int;
#else
    #define PRECISION
#endif

This ensures that your shader will work on GWT as well as other backends. HTML5 requires you to define the precision of your variables.

We need to specify variables to interact with the shader. Shadertoy adds its own common variables silently in the background, much to the dismay of the lazy GDX programmer who just wants a working shader.

varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_amount;
uniform float u_speed;
uniform float u_time;

Change the mainImage method to the cleaner method header that GDX requires.

void main () {

Shadertoy has you do some math to figure out UV coordinates (the normalized coordinates on the texture). This is just directly available in GDX.

vec2 uv = v_texCoords;

In Shadertoy, you would just tweak the values directly in the code and recompile to change the appearance of the shader. We don't have that luxury in libGDX. We add variables to control the speed of the animation and the amount of the distortion. We'll pass those values during runtime. We also have to change the weirdly named variables from Shadertoy to the equivalents that we defined. iTime --> u_time for example.

uv.y += (cos((uv.y + (u_time * 0.04 * u_speed)) * 45.0) * 0.0019 * u_amount) + (cos((uv.y + (u_time * 0.1 * u_speed)) * 10.0) * 0.002 * u_amount);
uv.x += (sin((uv.y + (u_time * 0.07 * u_speed)) * 15.0) * 0.0029 * u_amount) + (sin((uv.y + (u_time * 0.1 * u_speed)) * 15.0) * 0.002 * u_amount);

This is actually a poorly written shader. What is that loop doing? Erase that. We'll also erase the line that tints the output because that can be added by another effect later. This is the complete shader now.

#ifdef GL_ES
	#define PRECISION mediump
	precision PRECISION float;
	precision PRECISION int;
#else
	#define PRECISION
#endif

varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_amount;
uniform float u_speed;
uniform float u_time;

void main () {
    vec2 uv = v_texCoords;

    uv.y += (cos((uv.y + (u_time * 0.04 * u_speed)) * 45.0) * 0.0019 * u_amount) + (cos((uv.y + (u_time * 0.1 * u_speed)) * 10.0) * 0.002 * u_amount);

    uv.x += (sin((uv.y + (u_time * 0.07 * u_speed)) * 15.0) * 0.0029 * u_amount) + (sin((uv.y + (u_time * 0.1 * u_speed)) * 15.0) * 0.002 * u_amount);

    gl_FragColor = texture2D(u_texture, uv);
}

There are some other differences that you need to be mindful of. If you ever see any values listed with a trailing decimal point, add a 0 at the end. 1.0 instead of 1. gl_FragColor instead of fragColor. texture2D(u_texture, uv) instead of texture(iChannel0, uv). There are other things to look out for like variables not allowed in for loops on webgl, but that is an exercise for the reader. In other words, I couldn't be arsed to explain it to you.

Whoa, whoa whoa. Hold your horses, bud. Those new values need to come from somewhere, right? We'll pass them from GDX to the shader as parameters. These values can be modified during runtime to make animated effects. Check out this updated code in your core project:

...
float time;
...

@Override
public void render() {
    Gdx.gl.glClearColor(0, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    
    time += Gdx.graphics.getDeltaTime();
    shader.setUniformf("u_amount", 10);
    shader.setUniformf("u_speed", .5f);
    shader.setUniformf("u_time", time);
    batch.setShader(shader);
    batch.begin();
    batch.draw(jungleTexture, 0, 0);
    batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f,
            Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
    batch.end();
}

bvpTYjkPnG
Note that when you're experimenting with variables, you may get strange errors about variables not existing even though you have clearly wrote them in the shader. That's because GDX strips variables that aren't directly used in the shader. You can avoid this headache by calling ShaderProgram.pedantic = false;

What if you want the shader to only apply to certain textures? You can switch shaders between textures, applying the effect only to the peach. null is the default shader.

...
batch.setShader(null);
batch.begin();
batch.draw(jungleTexture, 0, 0);
batch.setShader(shader);
batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f,
        Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
batch.end();
...

11HMGHpnkY

GDX-VFX

Messing with shaders directly is all well and good, but it gets pretty hairy when you try to layer several effects on top of each other. Say you wanted to apply an underwater effect, tint it green, add some bloom, and make it look like a CRT. Now think about adding/removing those effects dynamically during gameplay. That would be pretty wild to code in a single fragment shader. Enter GDX-VFX. This library provides a wealth of commonly used shaders and a mechanism to automatically stack the effects. No more grotesque shader management. Follow the linked instructions to setup the lib and then copy the following example project.

public class Core extends ApplicationAdapter {
    Texture jungleTexture;
    Texture peachTexture;
    SpriteBatch batch;
    VfxManager vfxManager;
    WaterDistortionEffect waterEffect;
    
    @Override
    public void create() {
        vfxManager = new VfxManager(Format.RGBA8888);
        waterEffect = new WaterDistortionEffect(10f, .5f);
        vfxManager.addEffect(waterEffect);
        
        jungleTexture = new Texture("jungle.png");
        peachTexture = new Texture("peach.png");
        batch = new SpriteBatch();
    }
    
    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        
        vfxManager.update(Gdx.graphics.getDeltaTime());
        vfxManager.cleanUpBuffers();
        vfxManager.beginInputCapture();
        batch.begin();
        batch.draw(jungleTexture, 0, 0);
        batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
        batch.end();
        vfxManager.endInputCapture();
        vfxManager.applyEffects();
        vfxManager.renderToScreen();
    }
    
    @Override
    public void dispose() {
        jungleTexture.dispose();
        peachTexture.dispose();
        batch.dispose();
        vfxManager.dispose();
        waterEffect.dispose();
    }
    
    @Override
    public void resize(int width, int height) {
        vfxManager.resize(width, height);
    }
}

bvpTYjkPnG

Nice of them to include a water distortion effect, ehh? There are many different ones to toy with. Check out bloom, for example

public class Core extends ApplicationAdapter {
    Texture jungleTexture;
    Texture peachTexture;
    SpriteBatch batch;
    VfxManager vfxManager;
    BloomEffect bloomEffect;
    
    @Override
    public void create() {
        vfxManager = new VfxManager(Format.RGBA8888);
        bloomEffect = new BloomEffect();
        vfxManager.addEffect(bloomEffect);
        
        jungleTexture = new Texture("jungle.png");
        peachTexture = new Texture("peach.png");
        batch = new SpriteBatch();
    }
    
    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        
        bloomEffect.setBloomIntensity((float) Gdx.input.getX() / Gdx.graphics.getWidth() * 20);
        vfxManager.update(Gdx.graphics.getDeltaTime());
        
        vfxManager.cleanUpBuffers();
        vfxManager.beginInputCapture();
        batch.begin();
        batch.setColor(Color.WHITE);
        batch.draw(jungleTexture, 0, 0);
        batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
        batch.end();
        vfxManager.endInputCapture();
        vfxManager.applyEffects();
        vfxManager.renderToScreen();
    }
    
    @Override
    public void resize(int width, int height) {
        vfxManager.resize(width, height);
    }
    
    @Override
    public void dispose() {
        jungleTexture.dispose();
        peachTexture.dispose();
        batch.dispose();
        vfxManager.dispose();
        bloomEffect.dispose();
    }
}

161b9vDwMo

Now combine them.

public class Core extends ApplicationAdapter {
    Texture jungleTexture;
    Texture peachTexture;
    SpriteBatch batch;
    VfxManager vfxManager;
    WaterDistortionEffect waterEffect;
    BloomEffect bloomEffect;
    
    @Override
    public void create() {
        vfxManager = new VfxManager(Format.RGBA8888);
        waterEffect = new WaterDistortionEffect(10f, .5f);
        vfxManager.addEffect(waterEffect);
        
        bloomEffect = new BloomEffect();
        vfxManager.addEffect(bloomEffect);
        
        jungleTexture = new Texture("jungle.png");
        peachTexture = new Texture("peach.png");
        batch = new SpriteBatch();
    }
    
    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    
        bloomEffect.setBloomIntensity((float) Gdx.input.getX() / Gdx.graphics.getWidth() * 20);
        vfxManager.update(Gdx.graphics.getDeltaTime());
        
        vfxManager.cleanUpBuffers();
        vfxManager.beginInputCapture();
        batch.begin();
        batch.draw(jungleTexture, 0, 0);
        batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
        batch.end();
        vfxManager.endInputCapture();
        vfxManager.applyEffects();
        vfxManager.renderToScreen();
    }
    
    @Override
    public void dispose() {
        jungleTexture.dispose();
        peachTexture.dispose();
        batch.dispose();
        vfxManager.dispose();
        waterEffect.dispose();
        bloomEffect.dispose();
    }
    
    @Override
    public void resize(int width, int height) {
        vfxManager.resize(width, height);
    }
}

ahITZrmPRq

How about bloom in the background and tripping balls foreground? The secret is that you have to make sure to enable blending: vfxManager.setBlendingEnabled(true);

public class Core extends ApplicationAdapter {
    Texture jungleTexture;
    Texture peachTexture;
    SpriteBatch batch;
    VfxManager vfxManager;
    WaterDistortionEffect waterEffect;
    BloomEffect bloomEffect;

    @Override
    public void create() {
        vfxManager = new VfxManager(Format.RGBA8888);
        waterEffect = new WaterDistortionEffect(10f, .5f);
        bloomEffect = new BloomEffect();

        jungleTexture = new Texture("jungle.png");
        peachTexture = new Texture("peach.png");
        batch = new SpriteBatch();
        vfxManager.setBlendingEnabled(true);
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        bloomEffect.setBloomIntensity((float) Gdx.input.getX() / Gdx.graphics.getWidth() * 20);
        
        vfxManager.cleanUpBuffers();
        vfxManager.beginInputCapture();
        vfxManager.addEffect(bloomEffect);
        batch.begin();
        batch.draw(jungleTexture, 0, 0);
        batch.end();
        vfxManager.endInputCapture();
        vfxManager.update(Gdx.graphics.getDeltaTime());
        vfxManager.applyEffects();
        vfxManager.renderToScreen();
        vfxManager.removeEffect(bloomEffect);
    
        vfxManager.cleanUpBuffers();
        vfxManager.beginInputCapture();
        vfxManager.addEffect(waterEffect);
        batch.begin();
        batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
        batch.end();
        vfxManager.endInputCapture();
        vfxManager.update(Gdx.graphics.getDeltaTime());
        vfxManager.applyEffects();
        vfxManager.renderToScreen();
        vfxManager.removeEffect(waterEffect);
    }

    @Override
    public void dispose() {
        jungleTexture.dispose();
        peachTexture.dispose();
        batch.dispose();
        vfxManager.dispose();
        waterEffect.dispose();
        bloomEffect.dispose();
    }

    @Override
    public void resize(int width, int height) {
        vfxManager.resize(width, height);
    }
}

4u3TaDnPXo

These default effects are nice and cozy, but they're meant for general purpose use. Sometimes you'll want something more complicated. Something more specic to your game. This is how you make your own VFX compatible shader. Take this glitch shader, for example.

//2D (returns 0 - 1)
float random2d(vec2 n) { 
    return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}

float randomRange (in vec2 seed, in float min, in float max) {
		return min + random2d(seed) * (max - min);
}

// return 1 if v inside 1d range
float insideRange(float v, float bottom, float top) {
   return step(bottom, v) - step(top, v);
}

//inputs
float AMT = 0.2; //0 - 1 glitch amount
float SPEED = 0.6; //0 - 1 speed
   
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    
    float time = floor(iTime * SPEED * 60.0);    
	vec2 uv = fragCoord.xy / iResolution.xy;
    
    //copy orig
    vec3 outCol = texture(iChannel0, uv).rgb;
    
    //randomly offset slices horizontally
    float maxOffset = AMT/2.0;
    for (float i = 0.0; i < 10.0 * AMT; i += 1.0) {
        float sliceY = random2d(vec2(time , 2345.0 + float(i)));
        float sliceH = random2d(vec2(time , 9035.0 + float(i))) * 0.25;
        float hOffset = randomRange(vec2(time , 9625.0 + float(i)), -maxOffset, maxOffset);
        vec2 uvOff = uv;
        uvOff.x += hOffset;
        if (insideRange(uv.y, sliceY, fract(sliceY+sliceH)) == 1.0 ){
        	outCol = texture(iChannel0, uvOff).rgb;
        }
    }
    
    //do slight offset on one entire channel
    float maxColOffset = AMT/6.0;
    float rnd = random2d(vec2(time , 9545.0));
    vec2 colOffset = vec2(randomRange(vec2(time , 9545.0),-maxColOffset,maxColOffset), 
                       randomRange(vec2(time , 7205.0),-maxColOffset,maxColOffset));
    if (rnd < 0.33){
        outCol.r = texture(iChannel0, uv + colOffset).r;
        
    }else if (rnd < 0.66){
        outCol.g = texture(iChannel0, uv + colOffset).g;
        
    } else{
        outCol.b = texture(iChannel0, uv + colOffset).b;  
    }
       
	fragColor = vec4(outCol,1.0);
}

We'll take the techniques mentioned previously to convert the shader to a GDX compatible version. This time, we'll get texture input from u_texture0 instead of u_texture because GDX-VFX allows input from multiple textures. Save this shader file in your assets folder as "glitch.frag".

// Originally based on
// https://www.shadertoy.com/view/MtXBDs

#ifdef GL_ES
precision mediump float;
#endif

varying vec2 v_texCoords;
uniform sampler2D u_texture0;

uniform float u_time;
uniform float u_speed;
uniform float u_amount;
uniform vec2 u_viewport;
uniform vec2 u_position;

//2D (returns 0 - 1)
float random2d(vec2 n) {
    return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}

float randomRange (in vec2 seed, in float min, in float max) {
    return min + random2d(seed) * (max - min);
}

// return 1 if v inside 1d range
float insideRange(float v, float bottom, float top) {
   return step(bottom, v) - step(top, v);
}

void main()
{
    float time = floor(u_time * u_speed * 60.0);

    //copy orig
    vec3 outCol = texture2D(u_texture0, v_texCoords).rgb;

    //randomly offset slices horizontally
    float maxOffset = u_amount/2.0;
    for (float i = 0.0; i < 2.0; i += 1.0) {
        float sliceY = random2d(vec2(time, 2345.0 + float(i)));
        float sliceH = random2d(vec2(time, 9035.0 + float(i))) * 0.25;
        float hOffset = randomRange(vec2(time, 9625.0 + float(i)), -maxOffset, maxOffset);
        vec2 uvOff = v_texCoords;
        uvOff.x += hOffset;
        if (insideRange(v_texCoords.y, sliceY, fract(sliceY+sliceH)) == 1.0){
        	outCol = texture2D(u_texture0, uvOff).rgb;
        }
    }

    //do slight offset on one entire channel
    float maxColOffset = u_amount / 6.0;
    float rnd = random2d(vec2(time , 9545.0));
    vec2 colOffset = vec2(randomRange(vec2(time , 9545.0), -maxColOffset, maxColOffset),
                       randomRange(vec2(time , 7205.0), -maxColOffset, maxColOffset));
    if (rnd < 0.33) {
        outCol.r = texture2D(u_texture0, v_texCoords + colOffset).r;
    } else if (rnd < 0.66) {
        outCol.g = texture2D(u_texture0, v_texCoords + colOffset).g;
    } else {
        outCol.b = texture2D(u_texture0, v_texCoords + colOffset).b;
    }

	gl_FragColor = vec4(outCol, 1.0);
}

u_texture0 and u_time are provided by GDX-VFX by default. u_time is updated by the user when vfxManager.update(Gdx.graphics.getDeltaTime()); is called. These other values need to be provided by a ChainVfxEffect. Check out this GlitchEffect class:

public class GlitchEffect extends ShaderVfxEffect implements ChainVfxEffect {
    private static final String U_TEXTURE0 = "u_texture0";
    private static final String U_TIME = "u_time";
    private static final String U_SPEED = "u_speed";
    private static final String U_AMOUNT = "u_amount";
    
    private float speed = .6f; //0 - 1 speed
    private float amount = .2f; //0 -1 glitch amount
    private float time = 0f;

    public GlitchEffect() {
        super(VfxGLUtils.compileShader(Gdx.files.classpath("gdxvfx/shaders/screenspace.vert"), Gdx.files.internal("glitch.frag")));
        rebind();
    }
    
    @Override
    public void update(float delta) {
        super.update(delta);
        setTime(this.time + delta);
    }
    
    public float getTime() {
        return time;
    }
    
    public void setTime(float time) {
        this.time = time;
        setUniform(U_TIME, time);
    }
    
    public float getSpeed() {
        return speed;
    }
    
    public void setSpeed(float speed) {
        this.speed = speed;
        setUniform(U_SPEED, speed);
    }
    
    public float getAmount() {
        return amount;
    }
    
    public void setAmount(float amount) {
        this.amount = amount;
    }
    
    @Override
    public void rebind() {
        super.rebind();
        program.bind();
        program.setUniformi(U_TEXTURE0, TEXTURE_HANDLE0);
        program.setUniformf(U_TIME, time);
        program.setUniformf(U_SPEED, speed);
        program.setUniformf(U_AMOUNT, amount);
    }

    @Override
    public void render(VfxRenderContext context, VfxPingPongWrapper buffers) {
        render(context, buffers.getSrcBuffer(), buffers.getDstBuffer());
    }
    
    public void render(VfxRenderContext context, VfxFrameBuffer src, VfxFrameBuffer dst) {
        // Bind src buffer's texture as a primary one.
        src.getTexture().bind(TEXTURE_HANDLE0);
        // Apply shader effect and render result to dst buffer.
        renderShader(context, dst);
    }
}

gdxvfx/shaders/screenspace.vert is a vertex shader provided by GDX-VFX. Besides that, the effect is largely just getters and setters for the provided values. rebind() is called whenever those values need to be updated to the shader. render() binds the texture and applies the shader. All very basic stuff. Now you can add this effect like any other effect.

public class Core extends ApplicationAdapter {
    Texture jungleTexture;
    Texture peachTexture;
    SpriteBatch batch;
    VfxManager vfxManager;
    GlitchEffect glitchEffect;

    @Override
    public void create() {
        vfxManager = new VfxManager(Format.RGBA8888);
        glitchEffect = new GlitchEffect();

        jungleTexture = new Texture("jungle.png");
        peachTexture = new Texture("peach.png");
        batch = new SpriteBatch();
        vfxManager.setBlendingEnabled(true);
        vfxManager.addEffect(glitchEffect);
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    
        vfxManager.cleanUpBuffers();
        vfxManager.beginInputCapture();
        batch.begin();
        batch.draw(jungleTexture, 0, 0);
        batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
        batch.end();
        vfxManager.endInputCapture();
        vfxManager.update(Gdx.graphics.getDeltaTime());
        vfxManager.applyEffects();
        vfxManager.renderToScreen();
    }

    @Override
    public void dispose() {
        jungleTexture.dispose();
        peachTexture.dispose();
        batch.dispose();
        vfxManager.dispose();
        glitchEffect.dispose();
    }

    @Override
    public void resize(int width, int height) {
        vfxManager.resize(width, height);
    }
}

9HOUeZPQyT

That's my process from taking a shader from ShaderToy and applying it to GDX-VFX. I suggest testing the shader directly in very basic GDX project first before transforming it into a ChainVfxEffect. Read the errors in the log and google them damn thangs. Make sure it runs well on HTML5 if you plan to target that platform. Good luck, my FX hungry friends!

Clone this wiki locally