Development Diaries, Volume 8
Posted by Alex Jordan on
Huzzah! My water is working as advertised and it's just about done:
Not too bad, considering my knowledge of refraction math is about 7 days old.
The above video showcases implementation of the following features:
- Functional multi-directional refraction (scalable in-game)
- Tweakable water scrolling, i.e. the waves move on their own
- Much improved background art for the water
- More efficient 2D textures leading back to the desired 60 frames per second framerate.
Unfortunately, the above video also omits my failure to implement the following features:
- Lighting the water from information inherited from the same light source that affects the world model
- Specularity, i.e. the water surface catching light from the light source (in this case, the sun) and causing the water to sparkle
Fortunately, those omissions haven't sent me back to the drawing board. They're just minor delays on my To Do list. The other successes are much more important, as they've (a) shored up my ability to write functional code in large batches, and (b) increased my knowledge of how the Xbox 360 works. [break]
These accomplishments also allowed me to hone my problem-solving skills. For instance, in the last Diary, all my water was refracting in the same direction, to the upper left. Why was that? To answer the question, I had to learn exactly how normal maps store data.
Click for a larger version.
For those of you playing the home game, all image formats store data in a type called "RGB", for Red-Green-Blue. Combinations of red, green, and blue produce different colors, and an RGB value is stored for each pixel in the image. The red, green, or blue value can be anywhere from 0 to 255. So, true red has an RGB value of 255, 0, 0, while true blue has an RGB value of 0, 0, 255.
It makes sense that three data points (RGB) can be interpreted as three data points for a different purpose: RGB can be interpreted for "XYZ", i.e. points in 3D space! We can derive surface normals (the direction any given surface of a 3D object faces) from an XYZ value. That's why we use normal maps.
However, vertex and pixel shaders don't use the full range of 0 to 255. They use a range of 0 to 1. Therefore, an R value of 127 would be equivalent to an R value of 0.5 in a shader. So, for the image above, I converted each color from the 0-255 range to the 0-1 range to figure out what each direction represented. (That image is just the normal map for a stubby pyramid that I made in five seconds in 3D Studio Max. It's a view of the pyramid from the top, looking down.)
The first issue I noted was that each number is positive. In programming, though, the direction Left is usually a negative number while the direction Right is a positive number. Ditto for Up and Down. So my refraction was all going off in the same direction because each number was positive. Well, some of the numbers need to be negative. But how to choose which ones?
Simple, it turns out. Compare the medium blue pyramid edge to the hot pink pyramid edge. Those faces point due Left and due Right. Left and Right are on the X axis, the first of the three numbers assigned to that color. Left is 0.15, whereas Right is 0.85. Each represents exactly 0.15 units of offset... Left is 0.15 greater than true 0, whereas Right is 0.15 units less than 1.0. So, how to make Left negative and Right positive, as well as make it work not just for Left and Right, but any conceivable direction.
The answer: subtract 0.5 from each coordinate! 0.5 is exactly halfway on the 0-1 range that RGB/XYZ uses. Thus, Left (0.15) turns into -0.35, while Right (0.85) turns into 0.35. Positive versus negative, left versus right. I just made sure to subtract 0.5 from each coordinate, and then, voila! My refraction started working.
That was one major problem solved. The other was something I can't post pictures of, as it occurred on the Xbox 360. Basically, when I got my water shader working, instead of drawing the whole water surface, the 360 drew the top half... and then drew the top half again! No bottom half. I did some research, and found out it was the result of the 360's predicated tiling.
Predicated tiling, it turns out, is when the Xbox 360 makes multiple attempts to draw to the screen when it's processing very big textures. The 360 has 10 megabytes of EDRAM, and it turns out the huge world textures I was sending to it made it run out of that RAM. It would draw half the screen, clear the RAM as if it were clearing its throat, get the rest of my texture information, and draw it again (from scratch, it turned out).
I grimaced, as this was all my fault. I was passing the Xbox 360 not one, but three huge textures that wiped out its RAM: the world map texture (with the borders and countries), the the normal map for the world (for the detailed shadowing and the twinkling nighttime lights), and the water background. The 360 was choking on these three textures. So, I kept the world map texture at full resolution but scaled down the normal map and the water background map significantly. Not much quality was lost, and the 360 started drawing my screen all in one pass. Problem solved.
And there you have it. Hopefully you now have an idea of the promises and pitfalls involved with just doing a simple geography game.
Next up, I have to light the damned water correctly, so that the oceans aren't always in daytime when the world model is in nighttime. Also, sparkling waves would be so pretty. I'll work on that too.