Java Water Simulation

Description

Two height maps are used to store the current and previous states of the water. You could use int or short data types for these. Each map should be the same size as the image or screen area that you are drawing. In my implementation I've used a single array of shorts that is large enough to cater for both states, plus, I've added two rows per state. Why? That will become clear later on.

Each frame you will toggle between state maps. I'm using a simple offset to swap between starting locations within the array.

For each array element in the current state array:- Look at the neighbouring pixels from the previous state, i.e. above, below, left and right. Take the sum and divide by 2. Because we are dividing by 2 a right-shift will work beautifully. Now subtract the value in the current state map. If we left it like that the ripples would never subside so we need to diminish the strength of the ripple every pass. The most realistic way of doing this is to reduce the resulting height by a fraction of itself. Once again we can use right-shift to optimise this. In my example below I've used reduced the strength of the ripple by 1/32nd of itself each time with a right-shift 5. We now need to distort a background image based on the height of the water ripple in the current location. We do this by calculating an offset. Just as with a real pool of water, light rays penetrating the water will be refracted. We calculate an X/Y offset based on the current distance from the centre of the ripplemap and the magnitude of the ripple at this point. Perform a bounds check on the offset, i.e. check that the offset coordinates are not negative or larger than the size of the texture image. Plot the pixel at current ripplemap x/y location using the texel at the calculated offset.



Demonstration

alt="Your browser can handle applet's, but isn't for some reason!" Your browser cannot handle the applet tag!

Move your mouse over the pool balls

Main code

public void newframe() { //Toggle maps each frame i=oldind; oldind=newind; newind=i; i=0; mapind=oldind; for (int y=0;y<height;y++) { for (int x=0;x<width;x++) { short data = (short)((ripplemap[mapind-width]+ripplemap[mapind+width]+ ripplemap[mapind-1]+ripplemap[mapind+1])>>1); data -= ripplemap[newind+i]; data -= data >> 5; ripplemap[newind+i]=data; //where data=0 then still, where data>0 then wave data = (short)(1024-data); //offsets a=((x-hwidth)*data/1024)+hwidth; b=((y-hheight)*data/1024)+hheight; //bounds check if (a>=width) a=width-1; if (a<0) a=0; if (b>=height) b=height-1; if (b<0) b=0; ripple[i]=texture[a+(b*width)]; mapind++; i++; } } }

Future Improvements

Download

You can create a fairly realistic water simulation in only a few lines of code. I've seen the algorithm implemented here discussed on many sites. A good explanation of why this works can be found here . The basic approach is:-The code for the main loop is as follows:-You may have noticed that when you create a ripple at the left or right edge of the applet the water is also disturbed at the opposite edge. Not very realistic huh? This is simply because the ripplemap element corresponding to the right-hand edge pixel on one row is adjacent to the element of the left-hand pixel in the next row. This create a wraparound effect. You could get around this by ensuring that the extreme right/left elements are always zero. I've done something similar to separate the two state maps. Remember earlier when I described the size of the ripple map as being two rows larger than necessary in both states. This was so that the ripples could reach the top and bottom without crossing over into the opposite state map.You'll notice that many of the values that would alter the behaviour of the water are hard-coded, e.g. magnitude of the ripple, radius of ripple, rate of decay, etc. A wider range of effects could be acheived if these were runtime parameters.