Search code examples
graphicslibgdxglslshader

GLSL shader circle is wobbling as it changes size in LibGDX


In LibGDX, I created a shader to do a transition effect where a circle forms over top of the image, where everything outside the circle is black, and everything inside the circle shows up as normal. This circle starts out big and shrinks to nothing. But I have a problem, as the circle is shrinking, it wobbles around. I also found I had this problem when using a ShadeRenderer to create a circle that also shrank over time, before using a shader. I think the problem as something to do with floating point numbers, or the way that circles are rendered. Anyway, I need to know, how do I fix it, so that I can get the circle to shrink smoothly?

Here is a gif demonstrating my problem of the wobbling circle (which I need to be smooth): my wobbling circle

Here is my Fragment Shader Code:

varying vec4 v_color;
varying vec3 v_position;
varying vec2 v_texCoord0;

uniform vec2 u_resolution;
uniform sampler2D u_sampler2D;

uniform float u_radius;
uniform int u_hasCircle;

void main() {

    vec4 color = texture2D(u_sampler2D, v_texCoord0) * v_color;

    vec2 relativePosition = gl_FragCoord.xy / u_resolution - .5;
    relativePosition.x *= u_resolution.x / u_resolution.y;
    float len = length(relativePosition);

    if (u_hasCircle == 1 && len > u_radius) {
        color = vec4(0, 0, 0, 1);
    }

    gl_FragColor = color;
}

And here is my vertex shader code that runs before that:

attribute vec4 a_color; attribute vec3 a_position; attribute vec2 a_texCoord0;

uniform mat4 u_projTrans; uniform vec3 u_distort;

varying vec4 v_color;
varying vec3 v_position;
varying vec2 v_texCoord0;

void main() {
    v_color = a_color;
    v_position = a_position;
    v_texCoord0 = a_texCoord0;
    gl_Position = u_projTrans * vec4(a_position, 1.0);
}

When I want to transition to run, u_hasCircle is passed 1, otherwise it is passed 0. When the transition is running, I start with passing u_radius 1, and then I gradually decrease the value to 0, using a FloatAction in LibGDX. I send these values to the shader once per frame.

Here is the relevant Libgdx Java code that interacts with the shader:

public class PlayWorld extends Group implements InputProcessor, Disposable
{
    //various members

    private PlayScreen screen;

    private OrthographicCamera camera;

    private FloatAction transitionToBattleAction;
    private final float TRANS_TO_BATTLE_DURATION = 10f;

    private float circleSize;
    private boolean hasCircle = false;

    public PlayWorld(PlayWorld playWorld) {
        this.playWorld = playWorld;

        camera = new OrthographicCamera();

        tiledMap = new TiledMapActor(camera);

        addActor(tiledMap);

        transitionToBattleAction = new FloatAction();
    }

    //function that triggers transition
    public void enterBattle() {
        transitionToBattleAction.reset();

        transitionToBattleAction.setStart(0);
        transitionToBattleAction.setEnd(1);
        transitionToBattleAction.setDuration(TRANS_TO_BATTLE_DURATION);

        addAction();
    }

    // this function gets called every frame
    @Override
    public void act(float delta) {
        super.act(delta);

        if (transitionToBattleAction.getValue() == 0) {

            //this function is defined in code shown below
            tiledMap.unsetCircleSize();

        } else if (transitionToBattleAction.getValue() < 1) {

            //this function is defined in code shown below
            tiledMap.setCircleSize(
                1 - transitionToBattleAction.getValue());

        } else if (transitionToBattleAction.getValue() == 1) {

            //this function is defined in code shown below
            tiledMap.setCircleSize(
                1 - transitionToBattleAction.getValue());

            transitionToBattleAction.restart();

            screen.getGame().setScreen("battle");

        } else if (transitionToBattleAction.getValue() > 1) {

            //this function is defined in code shown below
            tiledMap.unsetCircleSize();

          transitionToBattleAction.restart();
        }                
    }

    //this gets called whenever the window resizes
    public void resize(int width, int height) {
        // this function is defined in code shown below
        tiledMap.resize(width, height);
    }

    //various other methods
}

public class TiledMapActor extends Actor implements InputProcessor, Disposable
{
    //various variables

    private ShaderProgram shader;

    private OrthographicCamera camera;

    private TiledMap map;
    private OrthogonalTiledMapRenderer renderer;
    private float UNIT_SCALE = 1 / 16f;

    public TiledMapActor(OrthographicCamera camera) {
        super();

        this.camera = camera;

        map = new TmxMapLoader().load("myMap.tmx");
        renderer = new OrthogonalTiledMapRenderer(map, UNIT_SCALE);

        shader = new ShaderProgram(
            Gdx.files.internal("shaders/myshader.vsh"), 
            Gdx.files.internal("shaders/myshader.fsh");

        System.out.println(
            shader.isCompiled() ? 
                "shader compiled" : shader.getLog());

        renderer.getBatch().setShader(shader);

        shader.begin();
        shader.setUniformi("u_hasCircle", 0);
        shader.end();
    }

    // this is called every time the window changes size
    // from the PlayScreen class, see code above
    public void resize(int width, int height) {
        camera.viewportWidth = width;
        camera.viewportHeight = height;
        camera.update();

        shader.begin();
        shader.setUniformf("u_resolution", (float)width, (float)height);
        shader.end();
    }

    //this method is called from code above, seen PlayScreen class code
    //
    public void setCircleSize(float circleSize) {
        this.circleSize = circleSize;
        hasCircle = true;
        shader.begin();
        shader.setUniformf("u_radius", circleSize);
        shader.setUniformi("u_hasCircle", 1);
        shader.end();
    }

    //this method is called from code above, seen PlayScreen class code
    //
    public void unsetCircleSize() {
        hasCircle = false;
        shader.begin();
        shader.setUniformi("u_hasCircle", 0);
        shader.end();
    }

    // Various other methods
}

Solution

  • So I found the problem, and stupid me! I was re-entering the transition scene every frame, once it started. I fixed it by using a boolean flag to tell me if the transition had started, and only starting the transition if that boolean flag wasn't set yet. Then later, at a point, after the transition had completed, I set that boolean flag back to false, so the transition could occur again!