Hi everybody,
Today I’m going to share a somewhat small adjustment I made to the very good Water Filter shader that @nehon did few years ago.
// SMALL WARNING
I just want you to know that a single lake is processing about the same speed as the regular infinite plane (the original shader) but if you add more lakes, it will become slower and slower. This behavior is also seen with the original filter. There is not much change about this… so don’t make 10 lakes or it will be very very slow. The problem is that it recalculates the whole scene mirror texture for every lake, taking ALL the objects, lighting, shadows, etc… once for every lake on every frame, so it’s very intensive. I think if you need more than 2-3 lakes at the same time, you should consider rewriting a part of the shader to maybe pass all the lakes polygons in the same array uniform… or maybe have that array uniform which is all the coordinates and another array uniform which is all the polygon’s first vertices (the indexes) or something. Anyhow, this is a quick hack and nothing fancy here so please feel free to adjust it but please share back with us what you did. Also, just to be clear, this does not WRAP a 3D object in water, it only cuts out the XZ plane in a custom shaped polygon of your choice, that’s all it does.
// WHAT CHANGED
The biggest change I made is that the shader now takes 2 more array uniforms. One is the polygon’s vertices (Vector2f for X and Z planes respectively) and the other uniform is the number of vertices as an Integer… WHY? Because I couldn’t figure out a nicer way to implement this in GLSL 1.5. I know there are dynamic array uniforms in newest GLSL versions but I’m not developping for 2014 graphics cards, I’m doing it for older, cheaper graphics cards so that everybody can run my software. That’s why the code is ugly like this… but it runs on older hardware… that’s a tradeoff I’m ready to accept. Then, I made a new function which is an algorithm that detects if the camera position (a simple Vector2f) is inside the polygon shape (the Vector2f vertices array) and then what I did is simply copy paste the older IF branch and fed my function instead of the circle shape one. That’s it. And in the Java class code, I then fed the material with those 2 new uniforms and made get/set functions for handiness. That’s all there is to it, so here’s the new code:
// THE CODE
In the WaterFilter.java file (which you should duplicate and put in your own project folder) I declared the 2 new variables at the top and the new polygon shape:
// [...]
private Vector3f center;
private float radius;
private ArrayList<Vector2f> arrPolygonShapeVertices; // <-- ADD THIS
private WaterFilter.AreaShape shapeType = WaterFilter.AreaShape.Circular; // <-- ADD THIS
public enum AreaShape{
Circular,
Square,
Polygon // <-- ADD THIS
}
Then around line 243, we had this, so duplicate the Water.j3md file along with WaterUtil.glsllib and Water15.frag and put them in your project folder and edit this:
// [...]
//material = new Material(manager, "Common/MatDefs/Water/Water.j3md"); // <-- CHANGE THIS FOR...
material = new Material(manager, "MatDefs/Water/Water.j3md"); // ...THIS (check paths depending on where you put yours)
Then around line 277, just after all the material stuff, add the 2 new uniforms (polygon shape vertices array and the number of vertices as an Integer):
if (center != null) {
material.setVector3("Center", center);
material.setFloat("Radius", radius * radius);
material.setBoolean("SquareArea", shapeType==WaterFilter.AreaShape.Square);
}
// ------------------v ADD THIS NEW PART v---------------------
else if(arrPolygonShapeVertices != null && arrPolygonShapeVertices.size() > 2){
// Convert ListArray<Vector2f> to Vector2f[]
Vector2f[] polygonShapeVerticesArray = new Vector2f[260];
for(int i = 0; i < Math.min(arrPolygonShapeVertices.size(), 260); i++) {
polygonShapeVerticesArray[i] = arrPolygonShapeVertices.get(i);
}
for(int i = arrPolygonShapeVertices.size(); i < polygonShapeVerticesArray.length; i++) {
polygonShapeVerticesArray[i] = new Vector2f(0,0);
}
material.setParam("arrPolygonShapeVertices", VarType.Vector2Array, polygonShapeVerticesArray);
material.setInt("polygonShapeVerticesLength", arrPolygonShapeVertices.size());
}
Add these getter and setter functions to take care of inputing and outputing the polygon vertices array:
/**
* returns the polygon shape vertices array of this effect
* @return the polygon shape vertices array of this effect
*/
public List<Vector2f> getArrPolygonShapeVertices() {
return arrPolygonShapeVertices;
}
/**
* Set the polygon shape vertices array of the effect.
* By default the water will extent to the entire scene.
* By setting a polygon shape vertices array you can restrain it to a portion of the scene.
* @param arrPolygonShapeVertices the polygon shape vertices array
*/
public void setArrPolygonShapeVertices(ArrayList<Vector2f> arrPolygonShapeVertices) {
this.arrPolygonShapeVertices = arrPolygonShapeVertices;
if(material != null){
if(this.arrPolygonShapeVertices != null && this.arrPolygonShapeVertices.size() > 2){
// Convert ListArray<Vector2f> to Vector2f[]
Vector2f[] polygonShapeVerticesArray = new Vector2f[260];
for(int i = 0; i < Math.min(this.arrPolygonShapeVertices.size(), 260); i++) { // You'll notice "260", you can make it anything < 1024
polygonShapeVerticesArray[i] = this.arrPolygonShapeVertices.get(i);
}
for(int i = this.arrPolygonShapeVertices.size(); i < polygonShapeVerticesArray.length; i++) {
polygonShapeVerticesArray[i] = new Vector2f(0,0);
}
material.setParam("arrPolygonShapeVertices", VarType.Vector2Array, polygonShapeVerticesArray);
material.setInt("polygonShapeVerticesLength", this.arrPolygonShapeVertices.size());
}
}
}
OK so now let’s take a look at the material file, we only need to add the 2 uniforms declaration near the top:
Float Radius
Vector3 Center
Boolean SquareArea
Vector2Array arrPolygonShapeVertices // <--------------- THIS IS NEW
Int polygonShapeVerticesLength // <--------------- THIS IS NEW
}
Now for the fragment shader, at the top declare the 2 new uniforms:
#ifdef ENABLE_AREA
uniform vec3 m_Center;
uniform float m_Radius;
#endif
uniform vec2 m_arrPolygonShapeVertices[260]; // <------ ADD THIS // You'll notice "260", you can make it anything < 1024
uniform int m_polygonShapeVertices_length; // <------ ADD THIS
The interesting part, around line 253, add another IF branch for the polygon shape like this:
#ifdef ENABLE_AREA
vec3 dist = m_CameraPosition-m_Center;
if(isOverExtent(m_CameraPosition, m_Center, m_Radius)){
gl_FragColor = vec4(color2, 1.0);
return;
}
#endif
// ----------------ADD THIS NEW IF BRANCH: --------------------
if(m_polygonShapeVertices_length > 2){
if(isOverExtentCustomShape(m_CameraPosition, m_arrPolygonShapeVertices, m_polygonShapeVertices_length)){
gl_FragColor = vec4(color2, 1.0);
return;
}
}
…and around line 270, add this new IF branch too:
#ifdef ENABLE_AREA
if(isOverExtent(position, m_Center, m_Radius)){
gl_FragColor = vec4(color2, 1.0);
return;
}
#endif
// ----------------ADD THIS NEW IF BRANCH: --------------------
if(m_polygonShapeVertices_length > 2){
if(isOverExtentCustomShape(position, m_arrPolygonShapeVertices, m_polygonShapeVertices_length)){
gl_FragColor = vec4(color2, 1.0);
return;
}
}
Now all is left is this big maths algorithm that checks if camera is inbound of the polygon, that’s in the WaterUtil.glsllib file:
// ADD THIS FUNCTION AT THE END OF THE FILE:
bool isOverExtentCustomShape(vec3 position, vec2[260] arr_vertices, int npol){
bool c = true;
int i, j;
for (i = 0, j = npol-1; i < npol; j = i++) {
if ((((arr_vertices[i].y <= position.z) && (position.z < arr_vertices[j].y)) || ((arr_vertices[j].y <= position.z) && (position.z < arr_vertices[i].y))) &&
(position.x < (arr_vertices[j].x - arr_vertices[i].x) * (position.z - arr_vertices[i].y) / (arr_vertices[j].y - arr_vertices[i].y) + arr_vertices[i].x)){
c = !c;
}
}
return c;
}
So if you did everything correctly, you can inspire yourself from this Java snippet to add a polygon shaped lake to your scene, but of course make sure you set the water height and vertices accordingly to YOUR scene:
// Make some vertices for the lake's bounds
ArrayList<Vector2f> list_vector2f_vertices = new ArrayList<>();
list_vector2f_vertices.add(new Vector2f(20f, 30f));
list_vector2f_vertices.add(new Vector2f(35f, 54f));
list_vector2f_vertices.add(new Vector2f(9f, 84f));
list_vector2f_vertices.add(new Vector2f(12f, 15f));
list_vector2f_vertices.add(new Vector2f(18f, 22f));
// Water and under-water filter
water_filter = new WaterFilter(rootNode, directionallight_sun.getDirection()); // Put your own lighting or simply do: new Vector3f(0, -0.5f, -0.5f)
water_filter.setName("My custom shaped lake");
water_filter.setWaveScale(0.003f);
water_filter.setMaxAmplitude(2f);
water_filter.setFoamExistence(new Vector3f(1f, 4, 0.5f));
water_filter.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam2.jpg"));
water_filter.setRefractionStrength(0.2f);
water_filter.setWaterHeight(50f); // Adjust this value to something above your scene's height...
water_filter.setShapeType(WaterFilter.AreaShape.Polygon); // <-- Notice this
water_filter.setArrPolygonShapeVertices(list_vector2f_vertices); // <-- Notice this
filterPostProcessor.addFilter(water_filter);
// SCREENSHOTS
You can see in these 2 screenshots, those lakes are a result of 6 vertices each.
Same lakes but GLSL was set to output only red color, just to show the polygon shape.
I hope it’s going to be useful to others and if anybody wants to optimize it, like I said at the beginning, I think one cool thing we could do is to have the shader process multiple polygon shapes (many lakes) in the same render pass.
Thx for reading!