SimpleWater with horizontal camera

I’ve found yet another odd rendering bug in my game, which (as usual) I’ve distilled down to a short test app. The test app is closely based on jme3test.water.TestSimpleWater: basically, a scene consisting of an unlit box, a skybox, and a horizontal water quad.

The camera starts out 1 unit above the water, in a horizontal orientation. I assert that the image reflected in the water is incorrect because the box is not visible in the reflected image and the reflected skyline does not match the directly visible skyline above it.

If you’re unconvinced, press the ‘N’ key to enable flyCam, then jiggle the mouse. The moment you adjust the camera’s orientation, the reflected image changes suddenly.

I’d appreciate any insight into why SimpleWater seems to fail for this case. Here’s my test app:
[java]
package test;

import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.texture.Texture;
import com.jme3.util.SkyFactory;
import com.jme3.water.SimpleWaterProcessor;

/**

  • A simple JME3 app to demonstrate simple water rendering issues.
    */
    public class TestSimple
    extends SimpleApplication
    implements ActionListener {

    public static void main(String[] args) {
    TestSimple app = new TestSimple();
    app.setShowSettings(false);
    app.start();
    }

    @Override
    public void simpleInitApp() {
    setDisplayStatView(false);

     Node sceneNode = new Node("scene node");
     rootNode.attachChild(sceneNode);
     /*
      * Add a logo box and sky cube to the scene.
      */
     Material logoMaterial = new Material(assetManager,
             "Common/MatDefs/Misc/Unshaded.j3md");
     Texture logoTexture =
             assetManager.loadTexture("Interface/Logo/Monkey.jpg");
     logoMaterial.setTexture("ColorMap", logoTexture);
     Box boxMesh = new Box(1f, 1f, 1f);
     Geometry boxGeometry = new Geometry("Box", boxMesh);
     boxGeometry.setMaterial(logoMaterial);
     sceneNode.attachChild(boxGeometry);
    
     Spatial skyCube = SkyFactory.createSky(assetManager,
             "Textures/Sky/Bright/BrightSky.dds", false);
     sceneNode.attachChild(skyCube);
     /*
      * Add a water processor.
      */
     SimpleWaterProcessor waterProcessor =
             new SimpleWaterProcessor(assetManager);
     //waterProcessor.setDebug(true);
     waterProcessor.setLightPosition(new Vector3f(33f, 12f, -29f));
     waterProcessor.setReflectionScene(sceneNode);
     viewPort.addProcessor(waterProcessor);
     /*
      * Add a square water quad to the scene.
      */
     Spatial waterQuad = assetManager.loadModel(
             "Models/WaterTest/WaterTest.mesh.xml");
     waterQuad.setLocalScale(40f);
     waterQuad.setMaterial(waterProcessor.getMaterial());
     rootNode.attachChild(waterQuad);
     /*
      *  The camera starts horizontal, one unit above the water, 
      *  looking toward the top of the cube.
      * 
      *  Press the 'N' key to enable camera movement.
      */
     flyCam.setEnabled(false);
     cam.setLocation(new Vector3f(0f, 1f, 10f));
     cam.setRotation(new Quaternion(0f, 1f, 0f, 0f));
     inputManager.addMapping("fly", new KeyTrigger(KeyInput.KEY_N));
     inputManager.addListener(this, "fly");
    

    }

    @Override
    public void onAction(String actionString, boolean ongoing, float unused) {
    if (ongoing && “fly”.equals(actionString)) {
    flyCam.setEnabled(true);
    }
    }
    }
    [/java]

1 Like

Screenshots and/or videos help :slight_smile:

1 Like

Sorry – I keep forgetting that not everyone is as interested in this stuff as I am.

Here’s before pressing ‘N’: PNG screen shot #1.

And here’s after the camera moves slightly: PNG screen shot #2.

Just a tip: use imgur.com for your images and then we don’t have to wait for dropbox (timing out for me right now). Also, you can embed the images right in the post and save us some clicks. :smiley:

1 Like

@sgold, i’ll look into it

1 Like

Thank you, @nehon.

@Pspeed: I’ve created an Imgur.com account and will use it in the future.

@sgold said: Thank you, @nehon.

@Pspeed: I’ve created an Imgur.com account and will use it in the future.

FYI: you can use imgur without an account.

I love the fact that you can take a screenshot, go to imgur and <ctrl> v paste it directly into the browser.

So nice.

@zarch said: I love the fact that you can take a screenshot, go to imgur and <ctrl> v paste it directly into the browser.

So nice.

Yeah, I like to use Hyperdesktop just because I can scribble on it first and it lets me take partial screens.

There are a bunch of different imgur apps: Apps — Imgur: The magic of the Internet

Hey, @nehon … any progress toward understanding the bug?

I didn’t look into it yet

I’ve done some investigation, and I suspect the bug is related to this test in com.jme3.water.SimpleWaterProcessor.postQueue():

[java]
if (!ray.intersectsWherePlane(plane, targetLocation)) {
ray.setDirection(ray.getDirection().negateLocal());
ray.intersectsWherePlane(plane, targetLocation);
inv = true;
}
[/java]

This appears to reverse the ray if it doesn’t intersect the plane of the water.
Perhaps the intent was to ensure that the ray and the plane intersect.

In my case, the plane and ray are both horizontal, so they never intersect, even
after the ray gets reversed. Could this be the cause of the issue I reported?

Yeah though I had updated this thread.
I looked into it, but couldn’t find a suitable solution.

the reverse ray is to ensure the correctness of the reflection when the camera is looking away from the water. That what’s this code does. The problem is that in the particular case where the camera direction is strictly parallel to the watr plane, there is no intersection and every thing is screwed up, because all the code assumes that there is that intersection.
Actually the whole processor rely on this assumption.
One way to alleviate it would be to “rotate” the camera direction used for the ray by an epsilon…but that felt really hackish and I couldn’t resolve to commit it.

1 Like

Thanks for your analysis, @nehon. I agree that rotating the camera by epsilon would not be a great solution.

It looks to me like targetLocation is used solely to aim reflectionCam. If reflectionCam were aimed using setRotation() instead, the code would be simpler and more robust. I’ll dig into the math and see if I can work out how to reflect a quaternion in a plane.

I didn’t figure out how to reflect a quaternion. However, I did work out a simple solution using Camera.setAxes(). Basically, you reflect each of sceneCam’s axes in the plane, negate the up axis,
and then apply the reflected axes to reflectionCam.

Here’s a patch:

Index: SimpleWaterProcessor.java --- SimpleWaterProcessor.java Base (BASE) +++ SimpleWaterProcessor.java Locally Modified (Based On LOCAL) @@ -120,9 +120,6 @@ private Plane refractionClipPlane; private float refractionClippingOffset = 0.3f; private float reflectionClippingOffset = -5f; - private Vector3f vect1 = new Vector3f(); - private Vector3f vect2 = new Vector3f(); - private Vector3f vect3 = new Vector3f(); private float distortionScale = 0.2f; private float distortionMix = 0.5f; private float texScale = 1f; @@ -221,22 +218,21 @@ sceneCam.getFrustumTop(), sceneCam.getFrustumBottom()); reflectionCam.setParallelProjection(false); - // tempVec and calcVect are just temporary vector3f objects - vect1.set(sceneCam.getLocation()).addLocal(sceneCam.getUp()); - float planeDistance = plane.pseudoDistance(vect1); - vect2.set(plane.getNormal()).multLocal(planeDistance * 2.0f); - vect3.set(vect1.subtractLocal(vect2)).subtractLocal(loc).normalizeLocal().negateLocal(); - // now set the up vector - reflectionCam.lookAt(targetLocation, vect3); - if (inv) { - reflectionCam.setAxes(reflectionCam.getLeft().negateLocal(), reflectionCam.getUp(), reflectionCam.getDirection().negateLocal()); - } + + Vector3f sceneTarget = sceneCam.getLocation().add(sceneCam.getDirection()); + Vector3f reflectDirection = plane.reflect(sceneTarget, null); + reflectDirection.subtractLocal(loc);
  •    //we are rendering a sub part of the scene so the camera planeState may never be reseted to 0.
    

-// reflectionCam.setPlaneState(0);
-// refractionCam.setPlaneState(0);

  •    sceneTarget = sceneCam.getLocation().subtract(sceneCam.getUp());
    
  •    Vector3f reflectUp = plane.reflect(sceneTarget, null);
    
  •    reflectUp.subtractLocal(loc);
    
  •    sceneTarget = sceneCam.getLocation().add(sceneCam.getLeft());
    
  •    Vector3f reflectLeft = plane.reflect(sceneTarget, null);
    
  •    reflectLeft.subtractLocal(loc);
    
  •    reflectionCam.setAxes(reflectLeft, reflectUp, reflectDirection);
    
  •    //Rendering reflection and refraction
       rm.renderViewPort(reflectionView, savedTpf);
       rm.renderViewPort(refractionView, savedTpf);
    
3 Likes

After looking at the code, I have an additional concern. It has to do with this line:

reflectionCam.setParallelProjection(false);

It seems to me that reflectionCam ought to use parallel projection if (and only if) sceneCam does. In that case, the line would look something like this:

reflectionCam.setParallelProjection(sceneCam.isParallelProjection());

1 Like

You’re right.
I guess the problem never arise because parallel projection is seldom used in a “classic” scene that involve water. But could be…
I’ll fix it

1 Like

Oh I missed your previous post, that’s pretty neat thanks.
I’ll integrate it.

1 Like

Thanks, @nehon. One point I forgot to mention: delete the whole ray intersection test, it will gum up my patch.

2 Likes

I made the change https://code.google.com/p/jmonkeyengine/source/detail?r=10899
Thank you very much it works like a charm.

3 Likes