ABCDE
DEABC
BCDEA
EABCD
CDEAB
If you take a look at the pattern, you'll see that while it looks like it repeats every 5x5 tiles, it's actually made up of smaller tiles tilted at an angle. (Just take any four of the A's to see what I mean. They line up as a square, but tilted. Each side of the square is sqrt(5) tiles in length, and the area is 5 square tiles.) Each of the tiles fits the edges of another tile in a unique way:
C D E A B
EAB ABC BCD CDE DEA
D E A B C
Now, I wanted something that looked like water waves bobbing up and down, maybe moving around a bit. But of course, I couldn't model simple ripples because ripples don't repeat in a square pattern; they're circular. It was just last week, however, that an idea struck me: straight waves (like corrugated ridges) might form a pleasing pattern when overlapping in different directions. A simple sine or cosine calculation would suffice for each wave.
A few thoughts occurred to me then. The first was that I couldn't just use any waves, in any direction or frequency. These would have to line up precisely with the tiles so they would fit. Which tiles? Well not the 5 icons I'd be making, and not the big 5x5 grid; they'd have to line up with the slanted squares.
And then I thought: What do I mean by "line up"? Well, in each tile, a wave would have to have the same exact height on one edge as on the opposite edge; by extension, all corners must share the same height. In the horizontal or vertical directions (in tile coordinates), the wave can repeat any number of times, as long as that number is an integer. When I thought about this some more, I realized this meant I could use waves whose crest lines slanted at unusual angles.
So now I was getting somewhere. A wave would have so many periods on the x'-axis (x',y' being the tile coordinates. for convenience written as (0,0) to (1,1)), and so many on the y'-axis. This I'd call RX and RY. Starting at one corner, you'd traverse RX periods along one edge and RY along another, making RX+RY periods from one corner to another. Either (but not both) of these numbers could be zero, for a horizontal or vertical wave. Negative numbers would work too; the wave would just go in the other direction.
So what is that direction, exactly? I needed to know the exact vector from one period to the next. Well, consider the case where RX=RY=1. Draw a line from (1,0) to (0,1); that's the first period out from (0,0). (1,1) is the second period (RX+RY=2). From the starting point (0,0), draw a line perpendicular to the first period. The length of that line is the length of a period. With a little calculation, the vector for this line turns out to be <RX,RY>/(RX2+RY2). Its total length is 1/sqrt(RX2+RY2).
The distance, in periods of the wave, from (x',y') to (0,0) is simply RX*x'+RY*y', which is convenient. So the wave can be expressed like this:
t = RX*x'+RY*y' + phi + clock*speed
z = A * cos( 2*pi*t )
A is the amplitude of the wave. The clock is a value that goes from 0 to 1; speed is an integer, usually 1 but sometimes 2 or 3 (for faster waves). The phi value is a random value from 0 to 1 that offsets the wave a little bit. And 2*pi is of course 360 degrees (in radians), which gives us a full period.
For amplitude, I wanted bigger waves (smaller RX,RY values) to be bigger in size. The direction vector for the wave is directly proportional to the wave's period. Thus, bigger waves, bigger direction, bigger value of 1/sqrt(RX2+RY2). So my wave amplitude is:
A = h * (1/sqrt(RX<SUP>2</SUP>+RY<SUP>2</SUP>)) / sum(1/sqrt(RX<SUP>2</SUP>+RY<SUP>2</SUP>))
What's h? That's the maximum or minimum height.
Now, this will be repeated for lots of waves. At each (x',y') point, the height of a wave is added to the total. As the clock goes from 0 to 1, the waves will move in an interference pattern; it looks more and more like natural water as the number of waves is increased. Once the clock reaches 1, or any integer value, the waves have each moved an integral number of periods; which is to say, they all appear to be in their original position again, which is how the animation can loop smoothly.
With me so far? Good, because now it gets complicated.
To draw semi-realistic looking water, I needed to simulate a ray tracer. That is, I needed a light source and some equations to calculate the color based on which direction the water surface is facing. For that, I'd need the normal vector for my water function. Fortunately, like with height, the normals can be added together for each wav (as long as you don't scale them until after you're done). To calculate a normal vector, I dipped into calculus and got a partial derivative of z:
t = RX*x'+RY*y' + phi + clock*speed
z = A * cos( 2*pi*t )
d[t] = RX*d[x'] + RY*d[y']
d[z] = -2*pi*A * sin( 2*pi*t) * d[t]
2*pi*A * sin( 2*pi*t) * (RX*d[x'] + RY*d[y']) + d[z] = 0
A 3-dimensional normal vector would be defined as the coefficients of d[x], d[y], and d[z] when they're on the same side of the equation. d[z]'s easy; its coefficient is 1. However, we don't have d[x] and d[y]; we have d[x'] and d[y']. You see, x' and y' are calculate from pixel locations.
Back to the slanted tiles a minute: Their horizontal edge go 2 tiles to the right, 1 down. I called these variables tiltx and tilty, with tiltsq=(tiltx2+tilty2). In this case, tiltx=2, tilty=1 (using standard pixel coordinates), so tiltsq=5. I also needed a tile size for the icon tiles, so tilesize=32 since BYOND uses 32x32 pixel tiles.
x' = (tiltx*x+tilty*y)/(tilesize*tiltsq)
y' = (tilty*x-tiltx*y)/(tilesize*tiltsq)
d[x'] = (tiltx*d[x]+tilty*d[y])/(tilesize*tiltsq)
d[y'] = (tilty*d[x]-tiltx*d[y])/(tilesize*tiltsq)
Now, substituting into the original tile-coordinate normal:
2*pi*A * sin( 2*pi*t) * (RX*d[x'] + RY*d[y']) + d[z] = 0
Coefficients:
d[x]: (tiltx*RX+tilty*RY)/(tilesize*tiltsq) * 2*pi*A * sin(2*pi*t)
d[y]: (tilty*RX-tiltx*RX)/(tilesize*tiltsq) * 2*pi*A * sin(2*pi*t)
d[z]: 1
Now, once all those normals are added up, you can scale the normal down to a length of 1. Now you need a light source (direction and color), so the direction vector to that needs to be scaled to a length of 1 also. Do a dot product on the light vector and the normal vector, and you have d. d is the cosine of the angle between the light source and the normal; if it's 1, the water surface is directly facing the light; -1, it's facing directly away; 0, the light just skims the surface.
I didn't go into details of refraction and shadows, which would have been way too complicated. Instead I just focused on diffuse reflection and specular highlights. For diffuse reflection I assumed a "mid-point" water color for light just glancing off:
water color = mid color + diffuse color * d
This is a little unrealistic, in that water surfaces facing away from the light are still not totally dark, though they'll still be darker than if they were facing the light; I rationalize it as a cheap substitute for light shining through a wave. For specular highlighting, I needed to raise the value of d (if greater than 0) to a power:
spec = d ** exponent, d>0
The exponent was high, but not too high. I think I used about 40 in the end, though these values all needed tweaking a lot. Finally, I multiplied spec (which was either 0-1 or just 0) by a specular highlight color--in the end I made it gray, not white--and added this to the water color, then cut off the final color at regular minimum and maximum boundaries.
In the end, I got an animated image of water as the clock value moved from 0 to 1 and then reset. I later rendered this as a static image, in 5x1-tile strips, so I could simply copy and paste icon-sized segments into DM.
And that's how you make water.
Lummox JR