Java Water Simulation
Description
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:-
- 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
Move your mouse over the pool balls
Main code
The code for the main loop is as follows:-
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
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.
Download
water.javawater.class