In this tutorial:
- creating and rendering water ripples
- optimized image generation
- JPEG decoding component for Silverlight
Source code for this sample
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:
Buffer2[x, y] = (Buffer1[x-1,y]
Buffer1[x,y-1]) / 2 - Buffer2[x,y]
Buffer2[x,y] = Buffer2[x,y] * damping
Swap the buffers
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
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
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)
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).
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.