JME3 Water Effect on arbitrary shapes (polygons) / DIY version :D

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 &amp;&amp; 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) &amp;&amp; (position.z < arr_vertices[j].y)) || ((arr_vertices[j].y <= position.z) &amp;&amp; (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! :smiley:

7 Likes

mhhh…
Couldn’t you get a similar result using the built in way of limiting the area the water covers? you can have a circular area and a square area. See TestMultiWaterFilter or something like this…

Hi,

Your idea and implementation is good :slight_smile: I hacked the “water filter” in another direction to create my clone of Riptide GP:

I cant give you the code (no screenshot also because the game is not ready for release yet) but may be some hints and suggestion for optimizing the Water surface and its reflection rendering I did for my implementation:

1- My ski slide on the water surface which made by curve just like a racing road. The surface it also move up and down by sin wave (just like one in JME). End of it are water buoyancy affect back to the ski… So you can see it’s very arbirary and dynamic.

2- The reflection “should” contain few things… I said “should” be cause the things are choosed to be appear in the reflection. Remember that another phase of rendering are trigger on the water surface. So I just have a filter over the geometry list, which geometry is important for player brain, which geometry are in a reflected frustum with limited view range… In my implemetation, the ski and the player ride it should be contained in the reflection, some very close obstable and building are also included. Everything else every except the skybox are excluded.

3- After consider 2 things above. The final things is to render the surface out. The area of water surface which near the player it render it higher resolution. The area far from view render with lower resoltion, and the over 40 meter area has no reflection at all.

4- The soap border effect, at the area near border of water surface are also implemented. But because I don’t have appropriate Z-Buffer as the normal effect (my pool have similar deep every where). The depth value is calculate easily by the distance for the center of the path to that border.

5- First i experiment with the Defered rendering method and I’m currently ported the game to DR. As you can imagine, the problem is quite easier in DR… The concept are the same but quite computation saved because it just render what should be rendered! Then I put lightprobe and refectionprobe along my racing path.

You polygon approach is also quite interesting indeed. It may also help me to solve my soap border problem but not much in optimize the rendered surface… Hope this also help you back.

Hi Rémy! Well, the 3 shapes the original filter was offering are: 1) Infinite XZ plane, 2) square, 3) circle. It does not account for more complex shapes. Why is the polygon shape useful? Because if you have a big river shaped like a “S” and you have lower parts of your terrain on the sides of the “S” or near the curves, you only want the water to appear in the “S” shape river, not on the side of the terrain. There may be tons of applications where this could be useful and where circle wouldn’t cut it. I really think combining multiple polygon shapes in the same shader instance would be a nice add-on, I just don’t have time right now to do that, but at some point I will have to do this myself if nobody does it.

Hi @atomix, polygon shaped water filter does NOT save fps over other water shapes and as a matter of fact, it is more GPU consuming (by a very low factor tough) because it has to do bounds checking on the polygon’s vertices every frame, so depending on how many bounds you have (6?..100?..1000?..) it’s prone to be slower than circle or infinite plane shapes. I like your point #2 it makes sense for fps optimization, good idea.

1 Like

Ok great, just wanted to be sure you were aware of the other way to have smaller bounds for the water.

It’s easy to get carried away and try to implement stuff like this and end up realizing it’s not optimal and draws too many fps on older hardware. Like I’ve read here somewhere last month, game programming is most of the time trickery. It’s not about if the geometry is logically correct. It’s more about if the user thinks it is. It’s all about faking stuff. That’s why I understand perfectly why you would want people to stick with the simpler circle or square shapes, but it’s just not always possible. I mean, in some situations, I could cover this by making sure the land is always above the water level but not ALL the time.

@.Ben. said: It's easy to get carried away and try to implement stuff like this and end up realizing it's not optimal and draws too many fps on older hardware. Like I've read here somewhere last month, game programming is most of the time trickery. It's not about if the geometry is logically correct. It's more about if the user thinks it is. It's all about faking stuff. That's why I understand perfectly why you would want people to stick with the simpler circle or square shapes, but it's just not always possible. I mean, in some situations, I could cover this by making sure the land is always above the water level but not ALL the time.
I agree, really. You made a point with your previous post. the S shaped water and the uneven ground convinced me ;)

Afaik if I use very small lakes (from the eye perspective), one could use another approach,

Build a full cubemap (preferable at gamestartup or level editing time) from the middle of the sea, and use this for everything, until the player comes very near.

This should clearly be added to the actual jme postwater! :smiley:

Edit: I’ve tried to make it work, but there is some part of the file that seem to be different from yours… It only make a circle shape lake.
I will have to stick with the square lake for now. But thank you for the help anyway!

Hi, thx. Make sure that after the place you’re doing: WaterFilter water_filter = new WaterFilter()… you’re also configuring it like this:

ArrayList<Vector2f> list_vector2f_vertices = new ArrayList<>();
list_vector2f_vertices.add(new Vector2f(20f, 30f)); // your lake bound vertices go here...
list_vector2f_vertices.add(new Vector2f(35f, 54f)); // your lake bound vertices go here...
....... add your lake bound vertices here.....

water_filter.setShapeType(WaterFilter.AreaShape.Polygon);
water_filter.setArrPolygonShapeVertices(list_vector2f_vertices);

Can you provide a complete demo download address

ERHH… I guess I could put it together, omg this new forum messed up my post, wtf…

It’s just that we don’t support [java] tags any longer.

I get it, but I’m not going to reformat all my previous posts…
EDIT: Wait, now it displays perfectly… Erlend, did you just re-enable java tags?

No, I just edited your posts in this thread :stuck_out_tongue: You can click the red pencil icon for a nifty changelog!

1 Like

Oh! Thanks Erland :smiley:
EDIT: You only forgot about the diamond operators, they’re shown as HTML entities at the moment :stuck_out_tongue: I’ll change them right now to avoid future confusion.

java.lang.NullPointerException
    at com.jme3.util.BufferUtils.setInBuffer(BufferUtils.java:565)
    at com.jme3.shader.Uniform.setValue(Uniform.java:233)
    at com.jme3.material.Technique.updateUniformParam(Technique.java:151)
    at com.jme3.material.MatParam.apply(MatParam.java:147)
    at com.jme3.material.Material.render(Material.java:1088)
    at com.jme3.renderer.RenderManager.renderGeometry(RenderManager.java:523)
    at com.jme3.post.FilterPostProcessor.renderProcessing(FilterPostProcessor.java:202)
    at com.jme3.post.FilterPostProcessor.renderFilterChain(FilterPostProcessor.java:281)
    at com.jme3.post.FilterPostProcessor.postFrame(FilterPostProcessor.java:294)
    at com.jme3.renderer.RenderManager.renderViewPort(RenderManager.java:987)
    at com.jme3.renderer.RenderManager.render(RenderManager.java:1029)
    at com.jme3.app.SimpleApplication.update(SimpleApplication.java:252)
    at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
    at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:185)
    at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:228)
    at java.lang.Thread.run(Thread.java:744)

OK let me create a new project and do all of this. Then you can copy all the files in your own project OK? BTW, just to make sure you know, this technique will become VERY SLOW if you put more than like 4-5-6 lakes on the same scene. It’s not optimized enough for putting a lot of lakes.

OK, they updated the WaterFilter and my tweak wouldn’t work anymore, so I just fixed it so that it works again. It had to do with variables they renamed in the shader. Here it is working again, feel free to download the zipped project file so that you can exactly see a fully working example and copy from it what you want. AGAIN, this needs to be optimized, it’s very slow and probably would not be usable in a game as it sits.

DOWNLOAD: The project ZIP file

PREVIEW:

Thank you for your warm help ! In addition I have one other approach using Geometry to do, wait a moment I attached program