Floating object on ProjectedGrid water

Hi guys!



I want to let an object float on my projected grid. I tried to get the current height from a WaterHeightGenerator, but that doesn't seem to work. I can't get the actual height from the ProjectedGrid neither. Can someone point me to a working example?

Ok, I did it. Sometimes just some of you own thoughts bring you a step further.



This is a fully working example of a floating object that rotateUpTo the normal of the water surface. If the object is small, it looks good. But if you have a larger object, you should interpolate the normal for the overall length/width of the object. a simple average will do, I guess.



import com.jme.app.SimplePassGame;
import com.jme.image.Texture;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.math.Plane;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.renderer.pass.RenderPass;
import com.jme.scene.Node;
import com.jme.scene.SceneElement;
import com.jme.scene.Skybox;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.shape.Box;
import com.jme.scene.state.CullState;
import com.jme.scene.state.FogState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.util.TextureManager;
import com.jmex.effects.water.ProjectedGrid;
import com.jmex.effects.water.WaterHeightGenerator;
import com.jmex.effects.water.WaterRenderPass;

public class Shipper extends SimplePassGame {
    private WaterRenderPass waterEffectRenderPass;
    private Skybox skybox;
    private ProjectedGrid projectedGrid;
    private float farPlane = 1000.0f;
   
    //debug stuff
    private Node debugQuadsNode;
    private Box player;
    private WaterHeightGenerator whg;
    private float h;
    private Vector3f norm = new Vector3f();
   
    public static void main( String[] args ) {
        Shipper app = new Shipper();
        app.setDialogBehaviour( ALWAYS_SHOW_PROPS_DIALOG );
        app.start();
    }
   
    protected void cleanup() {
        super.cleanup();
        waterEffectRenderPass.cleanup();
    }
   
    protected void simpleUpdate() {
        super.simpleUpdate();
       
        if( KeyBindingManager.getKeyBindingManager().isValidCommand( "f", false ) ) {
            projectedGrid.switchFreeze();
        }
        if( KeyBindingManager.getKeyBindingManager().isValidCommand( "e", false ) ) {
            switchShowDebug();
        }
        if( KeyBindingManager.getKeyBindingManager().isValidCommand( "g", false ) ) {
            waterEffectRenderPass.reloadShader();
        }
       
        float time = timer.getTimeInSeconds();
        // get height from water and float player on
        h = whg.getHeight(player.getLocalTranslation().x, player.getLocalTranslation().z, time);
        player.getLocalTranslation().y = h;
        // rotate Up to match water normal
        projectedGrid.getSurfaceNormal(
                player.getLocalTranslation().x,
                player.getLocalTranslation().z,
                norm);
        player.rotateUpTo(norm);
       
        skybox.getLocalTranslation().set( cam.getLocation() );
        cam.update();
    }
   
    protected void simpleInitGame() {
        display.setTitle( "Water Test" );
        cam.setFrustumPerspective( 45.0f, (float) display.getWidth() / (float) display.getHeight(), 1f, farPlane );
        cam.setLocation( new Vector3f( 50, 20, 50 ) );
        cam.lookAt( new Vector3f( 10, 10, 10 ), Vector3f.UNIT_Y );
        cam.update();
       
        setupKeyBindings();
       
        FogState fogState = display.getRenderer().createFogState();
        fogState.setDensity( 1.0f );
        fogState.setEnabled( true );
        fogState.setColor( new ColorRGBA( 1.0f, 1.0f, 1.0f, 1.0f ) );
        fogState.setEnd( farPlane );
        fogState.setStart( farPlane / 10.0f );
        fogState.setDensityFunction( FogState.DF_LINEAR );
        fogState.setApplyFunction( FogState.AF_PER_VERTEX );
        rootNode.setRenderState( fogState );
       
        Node reflectedNode = new Node( "reflectNode" );
       
        player = new Box("player", new Vector3f(0, 0, 0), 1, 3, 10);
        player.updateModelBound();
        player.updateWorldBound();
        reflectedNode.attachChild(player);
        player.setLocalTranslation(new Vector3f(10, 10, 10));
       
        buildSkyBox();
        reflectedNode.attachChild( skybox );
       
        rootNode.attachChild( reflectedNode );
       
        waterEffectRenderPass = new WaterRenderPass( cam, 4, true, true );
        waterEffectRenderPass.setClipBias( 0.5f );
        waterEffectRenderPass.setWaterMaxAmplitude( 5.0f );
        //setting to default value just to show
        waterEffectRenderPass.setWaterPlane( new Plane( new Vector3f( 0.0f, 1.0f, 0.0f ), 0.0f ) );
       
        whg = new WaterHeightGenerator();
        projectedGrid = new ProjectedGrid( "ProjectedGrid", cam, 100, 70, 0.01f, whg );
        //or implement your own waves like this(or in a separate class)...
//        projectedGrid = new ProjectedGrid( "ProjectedGrid", cam, 50, 50, 0.01f, new HeightGenerator() {
//            public float getHeight( float x, float z, float time ) {
//                return FastMath.sin(x*0.05f+time*2.0f)+FastMath.cos(z*0.1f+time*4.0f)*2;
//            }
//        } );
       
       
        waterEffectRenderPass.setWaterEffectOnSpatial( projectedGrid );
        rootNode.attachChild( projectedGrid );
       
        waterEffectRenderPass.setReflectedScene( reflectedNode );
        waterEffectRenderPass.setSkybox( skybox );
        pManager.add( waterEffectRenderPass );
       
        RenderPass rootPass = new RenderPass();
        rootPass.add( rootNode );
        pManager.add( rootPass );
       
        RenderPass fpsPass = new RenderPass();
        fpsPass.add( fpsNode );
        pManager.add( fpsPass );
       
        rootNode.setCullMode( Spatial.CULL_NEVER );
        rootNode.setRenderQueueMode( Renderer.QUEUE_OPAQUE );
        fpsNode.setRenderQueueMode( Renderer.QUEUE_OPAQUE );
    }
   
    private void buildSkyBox() {
        skybox = new Skybox( "skybox", 10, 10, 10 );
       
        String dir = "data/";
        Texture north = TextureManager.loadTexture(
                Shipper.class.getClassLoader().getResource(
                        dir + "1.jpg" ),
                        Texture.MM_LINEAR,
                        Texture.FM_LINEAR );
        Texture south = TextureManager.loadTexture(
                Shipper.class.getClassLoader().getResource(
                        dir + "3.jpg" ),
                        Texture.MM_LINEAR,
                        Texture.FM_LINEAR );
        Texture east = TextureManager.loadTexture(
                Shipper.class.getClassLoader().getResource(
                        dir + "2.jpg" ),
                        Texture.MM_LINEAR,
                        Texture.FM_LINEAR );
        Texture west = TextureManager.loadTexture(
                Shipper.class.getClassLoader().getResource(
                        dir + "4.jpg" ),
                        Texture.MM_LINEAR,
                        Texture.FM_LINEAR );
        Texture up = TextureManager.loadTexture(
                Shipper.class.getClassLoader().getResource(
                        dir + "6.jpg" ),
                        Texture.MM_LINEAR,
                        Texture.FM_LINEAR );
        Texture down = TextureManager.loadTexture(
                Shipper.class.getClassLoader().getResource(
                        dir + "5.jpg" ),
                        Texture.MM_LINEAR,
                        Texture.FM_LINEAR );
       
        skybox.setTexture( Skybox.NORTH, north );
        skybox.setTexture( Skybox.WEST, west );
        skybox.setTexture( Skybox.SOUTH, south );
        skybox.setTexture( Skybox.EAST, east );
        skybox.setTexture( Skybox.UP, up );
        skybox.setTexture( Skybox.DOWN, down );
        skybox.preloadTextures();
       
        CullState cullState = display.getRenderer().createCullState();
        cullState.setCullMode( CullState.CS_NONE );
        cullState.setEnabled( true );
        skybox.setRenderState( cullState );
       
        ZBufferState zState = display.getRenderer().createZBufferState();
        zState.setEnabled( false );
        skybox.setRenderState( zState );
       
        FogState fs = display.getRenderer().createFogState();
        fs.setEnabled( false );
        skybox.setRenderState( fs );
       
        skybox.setLightCombineMode( LightState.OFF );
        skybox.setCullMode( Spatial.CULL_NEVER );
        skybox.setTextureCombineMode( TextureState.REPLACE );
        skybox.updateRenderState();
       
        skybox.lockBounds();
        skybox.lockMeshes();
    }
   
    private void setupKeyBindings() {
        KeyBindingManager.getKeyBindingManager().set( "f", KeyInput.KEY_F );
        KeyBindingManager.getKeyBindingManager().set( "e", KeyInput.KEY_E );
        KeyBindingManager.getKeyBindingManager().set( "g", KeyInput.KEY_G );
       
        Text t = new Text( "Text", "F: switch freeze/unfreeze projected grid" );
        t.setRenderQueueMode( Renderer.QUEUE_ORTHO );
        t.setLightCombineMode( LightState.OFF );
        t.setLocalTranslation( new Vector3f( 0, 20, 1 ) );
        fpsNode.attachChild( t );
    }
   
    private void switchShowDebug() {
        if( debugQuadsNode.getCullMode() == SceneElement.CULL_NEVER ) {
            debugQuadsNode.setCullMode( SceneElement.CULL_ALWAYS );
        }
        else {
            debugQuadsNode.setCullMode( SceneElement.CULL_NEVER );
        }
    }
}

I think that's two users today who posted a question and then the answer… if only all our users were like this :smiley:

Hehe, often it is like I'm posting the question and then thinking: Stupid me, it can't be so hard and now you're blaming yourself with questioning the developers of this fabulous engine with this bloody noob crap. And then I go back to my code and try to figure it out because I don't want to stay such a looser. :wink:

Any user that will willingly and ably answer their own question is not a loser in my book.

Yesterday evening I thought about realistic movement on those waves generated by the default WaterHeightGenerator. I included a free ship model from turbosquid and added the interpolated normal calculation. It’s not looking right for large models anyway.



Anyone here thought about a realistic floating behavior yet? I think you really have to calculate the balance point and a huge amount of wave normals along your objects bounds to get it right. I’ll try to increase the normal caluclations, but I bet it gets really slow. I’ll keep posting my progress here, maybe someone can use it, too. :wink:

I think for larger models you'd need to rely more on volume displacement and average water height?

What do you mean with "volume displacement"? A real calculation of how much water is displaced by the object? I tried to find some docs about this topic, but there isn't any good available.

I mean using the physics behind bouyancy to determine how low something sits in the water and the average height of the water to determine the position of the object in the world.  Using a single point and the normal of the point would make for a very odd scene if you were showing off a large boat for example.  :slight_smile:

Are you aiming to make it look 'good enuff' or be simulation accurate?



To look 'good enuff' I would treat the peak point of the wave like the middle of a 'teeter-toter', with the boat pivoting on it.  This point would be (in the math not in the scene) then slowly decreased to zero since the wave loses its energy under the boat.  The boat only cares about the largest wave under it… this way multiple waves could hit the boat and it would rise according to the largest wave and not the order they are received in.  Since the wave loses power, new waves of equal starting power would effect the boat more than ones already passing under.



/Oh and you probably need some method to smooth out the motions so that a new pivot point doesn't cause it to flick into the new pivot position.



It really depends on what your doing, once you have sails you might want the ship to rock in the wind as well… and then you need another pivot point and then combine the pivoting together.



Thats the best I can think up.

If you used the highest wav as a teeter pivot, your ship's hull would sit on top of the water line and look pretty odd.  :slight_smile:

Well I assumed it would be translated into the water a few feet…

EmperorLiam said:
Are you aiming to make it look 'good enuff' or be simulation accurate?

To look 'good enuff' I would treat the peak point of the wave like the middle of a 'teeter-toter', with the boat pivoting on it.  This point would be (in the math not in the scene) then slowly decreased to zero since the wave loses its energy under the boat.  The boat only cares about the largest wave under it... this way multiple waves could hit the boat and it would rise according to the largest wave and not the order they are received in.  Since the wave loses power, new waves of equal starting power would effect the boat more than ones already passing under.

Thanks for your tips. I only want it to look right, not to be an exact simulation. I will try your tips as soon as I figure out how to get the highest point under my models volume... ;)

But your explanation sounds like my original problem again: When I use a single point under the middle of my model to generate the movement and apply the waters normal to the model, it will look like the model is going up and down too much on the wave. So the front will stake up in the air when the waves is getting real steep and it will rush under water when the model "hits" the valley.

Maybe I shoot a FRAPS video of this this evening. I don't know if I got the right english words to explain.  :?