Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LibGdx Is there a way to apply shader to a section of the sprite batch for a water effect?

So I have a water effect applied to a rectangular image that is my water to apply a sin wave function across it. It is applied only for this TextureRegion:

Water.java

public  void updateshaders(){

    float dt = Gdx.graphics.getDeltaTime();

    if(waterShader != null){

        time += dt ;
        float angle = time * (2 * MathUtils.PI);
        if (angle > (2 * MathUtils.PI))
            angle -= (2 * MathUtils.PI);

        Gdx.gl20.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
        Gdx.gl20.glEnable(GL20.GL_BLEND);
        waterShader.setUniformMatrix("u_projTrans",  gs.cam.combined);

        waterShader.begin();
        waterShader.setUniformf("timedelta", -angle);
        waterShader.end();
    }
}
@Override
public void draw(SpriteBatch g) {
    g.end();
    updateshaders();
    g.setProjectionMatrix(gs.cam.combined);
    g.setShader(waterShader);
    g.begin();
    g.draw(gs.gsm.rm.water_top, x, y + height - gs.gsm.rm.water_top.getRegionHeight(), width, gs.gsm.rm.water_top.getRegionHeight());
    g.draw(gs.gsm.rm.water, x, y, width, height - gs.gsm.rm.water_top.getRegionHeight());
    g.end();
    g.setShader(null);
    g.begin();
}

Imgur

I want to add this effect to everything in the red rectangle. I was thinking of flushing the SpriteBatch and cutting out an image of the region, applying the distortion then redrawing it over the original, then finish the rest of my render thread.

UPDATE: So my solution worked... sorta. It is incredibly slow. It looks correct but makes the game unplayable and slow. (Might be a little hard to notice in the gif but it looks good in game.) gif

New code:

Water.java

public void draw(SpriteBatch g) {
    g.end();
    updateshaders();
    //Distortion
    coords = gs.cam.project(new Vector3(x,y,0));
    if(scr != null){scr.getTexture().dispose();}
    scr = ScreenUtils.getFrameBufferTexture(
            (int)coords.x,(int)coords.y,
            (int)(width * scaleX),(int)((height - gs.gsm.rm.water_top.getRegionHeight() / 4) * scaleY));

    if(scr != null){
        g.setShader(waterShader2);
        g.begin();
        g.draw(scr,
            x,y,
            width,height- gs.gsm.rm.water_top.getRegionHeight() / 4);
        g.end();
    }
    //SURFACE WAVES
    g.setShader(waterShader);
    g.begin();
    g.draw(gs.gsm.rm.water_top, x, y + height - gs.gsm.rm.water_top.getRegionHeight(), width, gs.gsm.rm.water_top.getRegionHeight());
    g.end();
    //BACK TO NORMAL
    g.setShader(null);
    g.begin();
    g.draw(gs.gsm.rm.water, x, y, width, height - gs.gsm.rm.water_top.getRegionHeight());
}

UPDATE: I tried Tenfour04's solution and it worked... sorta. While there is a distortion effect, and the game runs at full FPS, the distortion makes makes the background get shown. This is because the shader is being applied to the texture, not just within the bounds of the region that I grab using:

    scr = new TextureRegion(((PlayGameState) gs).frameBuffer.getColorBufferTexture(),
            (int)coords.x, (int)coords.y,
            (int)(width * scaleX),(int)((height - gs.gsm.rm.water_top.getRegionHeight() / 4) * scaleY));

werid

like image 798
Steven Landow Avatar asked Sep 27 '22 04:09

Steven Landow


1 Answers

On every frame, you're discarding and recreating a Pixmap and a Texture, and then capturing the screen to that texture. This is an extremely slow operation to be repeating over and over.

Instead, you can render your game directly to a persistent off-screen frame buffer.

private FrameBuffer frameBuffer;
private final Matrix4 idt = new Matrix4();

public void render() {
    if (frameBuffer == null){
        //Normally this would go in the resize method, but that causes issues on iOS 
        //because resize isn't always called on the GL thread in iOS. So lazy load here.
        try {
            frameBuffer = new FrameBuffer(Format.RGBA8888, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
        } catch (GdxRuntimeException e){ 
            frameBuffer = new FrameBuffer(Format.RGB565, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
            //RGBA8888 not supported on all devices. You might instead want to turn off 
            //the water effect if it's not supported, because 565 is kinda ugly.
        }
    }

    frameBuffer.begin();
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    //draw everything that is behind the water layer here
    frameBuffer.end();

    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    //Draw the frame buffer texture to the screen:
    spriteBatch.setProjectionMatrix(idt);
    spriteBatch.begin();
    spriteBatch.draw(frameBuffer.getColorBufferTexture(), -1, 1, 2, -2); //IIRC, you need to vertically flip it. If I remembered wrong, then do -1, -1, 2, 2
    spriteBatch.end();

    water.draw(spriteBatch, frameBuffer.getColorBufferTexture());

    //draw stuff that is in front of the water here
}

And then modify your water class as below. If I remember correctly, the screen texture is upside down, so you might have to flip some of your math. I didn't check your math below to know if you already account for that.

public void draw(SpriteBatch g, Texture scr) {
    updateshaders();
    //Distortion
    coords = gs.cam.project(new Vector3(x,y,0));

    if(scr != null){
        g.setShader(waterShader2);
        g.begin();
        g.draw(scr,
            x,y,
            width,height- gs.gsm.rm.water_top.getRegionHeight() / 4);
        g.end();
    }
    //SURFACE WAVES
    g.setShader(waterShader);
    g.begin();
    g.draw(gs.gsm.rm.water_top, x, y + height - gs.gsm.rm.water_top.getRegionHeight(), width, gs.gsm.rm.water_top.getRegionHeight());
    g.end();
    //BACK TO NORMAL
    g.setShader(null);
    g.begin();
    g.draw(gs.gsm.rm.water, x, y, width, height - gs.gsm.rm.water_top.getRegionHeight());
}

One thing that's unoptimized in this procedure is that you end up drawing all the pixels behind the water layer at least twice. Once to draw it on the frame buffer, and then a second time when the frame buffer is drawn to the screen. This could be optimized later (if you are fill rate bound) by drawing the behind-water stuff only in the areas of the view that are covered by water, and then replacing the step of drawing the frame buffer texture to the screen with drawing the background stuff normally. This would only look good if you successfully constructed an RGBA8888 frame buffer, which isn't supported on all Androids.

like image 173
Tenfour04 Avatar answered Nov 02 '22 08:11

Tenfour04