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();
}
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.)
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));
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With