As promised earlier, I take some time to explain how I made the water surface effect.
All is made using libgdx and some GLSL shader.
Here are the steps.
1) just make a copy of the scene using a FBO
FrameBuffer fbo;
Texture img;
...
// initialize the FBO
fbo = new FrameBuffer(
Pixmap.Format.RGBA8888,
Gdx.graphics.getWidth(),
Gdx.graphics.getHeight(),
false
);
// load the texture
img = new Texture(Gdx.files.internal("landscape.jpg"));
in the main loop, render the texture on the fbo
fbo.begin();
{
batch.begin();
batch.draw(img, 0, 0);
batch.end();
}
fbo.end();
retrieve the water surface texture
Texture waterSurface;
...
// level is the y location of the water surface
float yOffset = (Gdx.graphics.getWidth() - level);
waterSurface = new TextureRegion(
fbo.getColorBufferTexture(),
0,
(int) yOffset, // is the y offset to cut the texture to the desired water level
fbo.getColorBufferTexture().getWidth(),
Gdx.graphics.getHeight()
);
then render the two texture on top of each other
batch.begin();
{
batch.draw(img, 0, 0);
batch.draw(waterSurface, 0, -level);
}
batch.end();
here is the first result
2) stretching the water surface
first start by creating a vertex shader that do nothing:
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
uniform mat4 u_projTrans;
varying vec4 v_color;
varying vec2 v_texCoord0;
void main()
{
v_color = a_color;
v_texCoord0 = a_texCoord0;
gl_Position = u_projTrans * a_position;
}
then the fragment shader
varying vec4 v_color;
varying vec2 v_texCoord0;
uniform vec4 u_color;
uniform sampler2D u_texture;
uniform sampler2D u_texture1;
uniform float u_time;
uniform float u_y_offset;
uniform float u_sparkle_intensity;
void main()
{
vec4 final_color = v_color;
vec2 new_coord = v_texCoord0;
// -----------------------------
// STRETCH
// -----------------------------
// calculate the stretching factor
float factor = ((0.5 - new_coord.x) * (new_coord.y - u_y_offset));
// apply the factor and stretch the image
new_coord.x += factor;
final_color = (final_color) * texture2D(u_texture, new_coord);
gl_FragColor = final_color;
}
in the render loop, bind the freshly created shader program
ShaderProgram waterShader;
...
waterShader = new ShaderProgram(
Gdx.files.internal("shader/water.vert.glsl"),
Gdx.files.internal("shader/water.frag.glsl")
);
...
batch.begin();
{
batch.draw(img, 0, 0);
batch.setShader(waterShader);
batch.draw(waterSurface, 0, -level);
batch.setShader(null);
}
batch.end();
here is the second result
3) the displacement map
What we want to do now is to apply some normal maps to create a nice distorion effect on the waterSurface texture.
we will use a normal map found on google image:
let's load it on our game!
Texture map;
...
map = new Texture(Gdx.files.internal("map.png"));
on our render loop, we need to bind the map to the correct opengl texture buffer
map.bind(1);
// be sure to rebind your landscape texture
// to the default buffer (0)
img.bind(0);
update the fragment shader
// -----------------------------
// FIRST HEIGHTMAP
// -----------------------------
// calculate heightmap scrolling
// based on the stretched coords
vec2 new_map_coord1 = new_coord;
// scroll the map
new_map_coord1.x += (u_time / 8.0);
// clamp the texture to make it repeat
// indefinitely
new_map_coord1 = fract(new_map_coord1);
// apply the heighmap displacement
// to the original texture
vec4 height_map_color1 = texture2D(u_texture1, new_map_coord1);
new_coord.xy += (height_map_color1.rg * 0.02);
// -----------------------------
// SECOND HEIGHTMAP
// -----------------------------
vec2 new_map_coord2 = new_coord;
new_map_coord2.x += (u_time / 30.0);
new_map_coord2 = fract(new_map_coord2);
vec4 height_map_color2 = texture2D(u_texture1, new_map_coord2);
new_coord.xy += (height_map_color2.rg * 0.02);
what this shader do is sliding the normal map and distorting the texture based on the red and blue chanel of the normal map.
I use the same normal map two time to make one distortion slide to the left quicker than the second one. It will be useful to calculate the sparkle later.
4) calculating the sparkling effect
update the fragment shader
if (( new_coord.y - v_texCoord0.y ) > u_sparkle_intensity) {
final_color = vec4(1.0, 1.0, 1.0, 1.0);
final_color = final_color;
} else {
final_color = (final_color) * texture2D(u_texture, new_coord);
}
what this snipet do is replacing the final color with full white when the two displacement map have a y delta > to the sparkle intensity parameter provided by the program. So if the normal maps generate big wave, it will add some white on top of it.
In the render loop provide some data to the shader program
waterShader.begin();
{
waterShader.setUniformi("u_texture1", 1); // enable texture buffer 1
waterShader.setUniformf("u_time", now); // the time to make the normal map slide
waterShader.setUniformf("u_y_offset", yOffset / Gdx.graphics.getHeight());
waterShader.setUniform4fv("u_color", waterColor, 0, 4); // water color if needed
waterShader.setUniformf("u_sparkle_intensity", sparkleIntensity); // the treshold when the wave will generate some sparkles
}
waterShader.end();
It is quite a long post, but some people asked me to explain the full process, so here it is.
I think it can easily be modified to fit with other languages/framework.