Building the Water Ripples Sample in Silverlight
Source
code for this sample
View sample
Algorithm
The algorithm is based on Hugo Elias’
2D water tutorial.
On each render step we have the state of the water for the
current frame and the previous frame.
The state is stored in two 2-dimensional arrays of integers
that are as big as the image.
For each pixel position of the image we store the height of
the water (or wave) in that position. 0 means “sea level”. Larger than 0 means
that we have a raised wave, less than zero means that we have wave below “sea
level”. We need information for both raised and low waves in order to be able
to combine them.
On each render step we use data from the current frame
(Buffer2) and the previous frame (Buffer1) and write the results into
Buffer1.
damping = some non-integer between 0 and 1 (I use
0.94)
for every non-edge element:
loop
Buffer2[x, y] = (Buffer1[x-1,y]
Buffer1[x+1,y]
Buffer1[x,y+1]
Buffer1[x,y-1]) / 2 -
Buffer2[x,y]
Buffer2[x,y] = Buffer2[x,y] * damping
end loop
Swap the buffers
Display Buffer1
end loop
You can go ahead and look at Hugo’s explanation about why
does this work, or continue reading here.
Because the 2 buffers contain consecutive steps for the
water, we can get the water velocity at a given location [x, y] by subtracting:
Buffer2[x, y] – Buffer1[x, y]
Also we want the waves to spread out, so we smooth the
buffers on every frame:
Smoothed[x,y] = (Buffer1[x-1, y] +
Buffer1[x+1, y] +
Buffer1[x, y-1] +
Buffer1[x, y+1]) / 4
In the actual algorithm we multiply the smoothed value by 2
in order to decrease the effect of velocity.
And last, the waves lose energy as they travel:
Buffer2[x,y] = Buffer2[x,y] * damping
Rendering the Water
The render buffer contains heights of the water in each
pixel. We’ll render it using shading and refraction.
The shading variable below determines the direction
and intensity of the light. For example, if you set shading = xoffset, you’ll
get light straight from the left. I decided to set the light at the
bottom-right part of the screen.
For every non-edge pixel in the buffer
Xoffset = buffer[x-1, y] – buffer[x+1, y]
Yoffset = buffer[x, y-1] – buffer[x, y+1]
shading = (xoffset
- yoffset) / 2
// note: x+xoffset and
y+yoffset do not wrap around the texture
t = texture[x+Xoffset,
y+Yoffset]
resultColor = t + Shading
// make sure the color is
within limits
resultColor =
SaturateTo255Max(resultColor)
plot pixel at (x,y) with color
resultColor
End loop
Creating Drops/Splashes
I made a simple function to create a circular splash,
although you can create different splashes to simulate dropping irregular
shapes into the water or other motion effects (e.g. star, line or use the
letters of your name).
The function creates a splash given its radius at location
(cx, cy). The splash begins below water level and rises above at the end.
Splash(cx, cy, radius):
for each y from (cy -
radius) to (cy + radius)
for each x from (cx
- radius) to (cx + radius)
dist = distance from point (x,y) to (cx, cy)
if (dist <
radius) // if within splash circle
buffer1[x, y] = 255 - (512 * 1 - dist /
radius)
end if
end loop
end loop
The Silverlight Side
The rendering loop is called every 60ms and does this:
1.
Add a random splash (rain drop) on the image
2.
Calculate the next frame to render
3.
Display the next frame
There are 3 components used to render each frame:
Renderer has the “raw” buffers of integers containing
wave height for each pixel in the frame. On each frame the renderer mixes its
raw buffer with the background image (decoded using FluxJpeg.Core.Image
component) and outputs each it to a dynamic image generation surface (EditableImage).
Links
Original 2D
water algorithm (Hugo Elias)
This is the algorithm used as a base for the sample.
Dynamic
image generation (Joe Stegman)
Optimized
dynamic image generation based on Joe's (Nokola)
I’m using optimized version of Joe’s algorithm to render the
effect on screen.
Silverlight
JPEG encoder/decoder in C# - FJCore on fluxcapacity.net
The binary is used to decode the JPEG image in Silverlight.