-
Notifications
You must be signed in to change notification settings - Fork 35
The Good, The Bad, and the Shadertoy
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.
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.
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);
}
}
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 () {
}
}
Add the following jungle image to your project's assets. This will serve as our epic background.
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();
}
...
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.
batch.draw(peachTexture, Gdx.input.getX() - peachTexture.getWidth() / 2f, Gdx.graphics.getHeight() - Gdx.input.getY() - peachTexture.getHeight() / 2f);
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.
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();
}
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();
...
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);
}
}
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();
}
}
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);
}
}
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);
}
}
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);
}
}
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!
Getting Started
Windows
Linux
Mac
Features Manual
Colors
Fonts
Creating Bitmap Fonts
Creating FreeType Fonts
Creating Image Fonts
Drawables
Nine Patches
Ten Patches
Classes, Styles, and the Preview Panel
Custom Classes
Exporting
Importing
Saving / Loading
VisUI Skins
Tips and Tricks
TextraTypist Playground
Scene Composer
Scene Composer
Exporting a Scene
Modifying a Scene
Tutorials
Skin Composer Videos
From The Ground Up Series
Examples
Ray3K Skins