Wednesday, February 15, 2012

PSP Water

Today, a post that has nothing to do with my game project :)

I've been asked several times how we do our water effect on the PSP:

As the PSP is ageing now and slowly moving to the exclusives realms of homebrew and demo makers, I thought it wouldn't hurt to explain how we do.

It's a bit tricky so I hope I can make myself clear enough :) Here it goes:

The idea is first to have an animated map representing the waves moving. To do that I used some procedural texture generation tool to create each frame of the moving waves. Here is a frame of the animation.

You have to pack all the frames of the animation into a single 512x512 texture (the largest supported by the PSP) and then later do the animation by moving the UV coordinates over each frame.

I choose 102x102 pixels for each frame so i could pack 25 frames of animation into the 512x512 texture (five rows of five frames). I know it sounds weird.

I could have used 16 128x128 frames, but that would have been a short loop @ 60 frames per second.
Or I could have used 64 64x64 frames, but those would have been pretty low def.
Hence the peculiar resolution choice.

Then I used nVidia normal map filter pluggin for photoshop in order to generate a normal texture.

Then you need to quantize your texture using a very specific 256 colors palette. To create the palette, think of it as 16 row of 16 columns. In each column you can store the X component of a normal spanning an entire hemisphere, and on each row you can store the Y component.

Basically that gives you all the possible normals over an hemisphere stored into the palette:

Now when you quantize the texture with this palette, that means each of the texels now uses one of the 256 possible normals. It's a little rough, but when it's moving you barely see the quantization. Here's one frame from the quantized texture (cropped).

To render the reflection effect,you just have for each palette entry (256 entries) to compute the reflected vector from the eye vector around the normal vector that you know is stored in that palette entry (it's just a XY gradient so you can do that procedurally without actually using the palette).

That gives you a new vector that you can use as texture coordinates to sample an hemispheric environment texture. We use a small 32x32 texture of a sky with clouds and sun. The reflection and sampling with bilinear filtering is done in software using the VFPU (implemented by my partner @ Fresh3d Yann Robert), so it's very fast (a few microseconds).

Here's the environment texture:

Last you have to replace the palette entry used by the texture with this sampled color. So basically 256 vector reflection and color sampling per frame.

For every texel using this entry (normal) in the texture the reflected color will be displayed. And voila.

I think it's a neat method because of its fixed cost and no use of multiple pass fill-rate hungry methods.

There's a drawback in that the reflection is not in perspective, it's parallel, but it's hard to say if you don't know it. Furthermore this can be hidden by some billboard fake global, low freq, specular effect on top of it (that's what we do) to modulate the hi-freq wave effect.

Told ya it would be tricky :)

There's a envMap() lua function that applies to a color lookup table (palette) to directly compute all this and do the palette update. For those of you lucky enough to be able to use the engine :) Check this on the engine documentation here:

Look for clut (color lookup table) in the effect section.