Latest ArticlesEngine Architecture #1
Static Shadows #1
Paint on Mesh
A Tale of the Internet
Water Rendering Aug 11, 2006
This week I'm trying something different. I'm writing about what I just started working on last night, so I don't really know much about it =) Specifically, I added a water shader with planar reflections. Water shaders are pretty much a standard feature on 3d engines nowadays. This quick first implementation is simple, has a few outstanding issues, and probably needs extra optimization, but it's important to get things into an engine so they're available to be used in levels. Then the process of tweaking/experimenting for best visuals and speed can begin. Here's a screenshot of the water from a test that I slapped together. It looks a bit cooler in motion, and hopefully we'll find good use for it in real levels.
Starting IdeasI have a pretty good idea how to do planar reflections. Once, many years ago when I was working on what I now call my old 3d game engine, I created a "fake" reflective floor effect. To make this effect I just created below the floor a vertically mirrored version of the wall geometry( which was all above the floor.) Then I turned on transparency when rendering the floor itself. I thought it looked pretty good at the time. I wish I had more screenshots from that engine.
I based the shader itself on my frame buffer distortion shader. The main difference is instead of rendering the frame buffer to a reduced size texture, and using that as input, it will have to flip the scene along the water plane and re-render it into the reduced texture. This re-rendering means it will be a speed hit, and will have to be used with care. If I didn't need to reflect local geometry, I could use a static cubemap to avoid the extra rendering.
Someday I'll have time to tweak it to look more like water. Animation support was trivial, since the heat haze distortion effect is already animated. The similarities also mean I can just put the water shader in the same place as frame buffer distortion in my rendering pipeline. (Actually it wasn't quite that easy, but it was close enough that the pipeline complications weren't as tricky as they could have been.)
Reflection ImplementationThere's something about implementing something new that always makes me feel slightly incompetent. There's always unexpected issues, especially in a full game engine where you're combining a lot of different techniques. However, it always seems to come together eventually.
The first thing I did was to flip the water around the ground plane with glScalef( 1.0f, -1.0f, 1.0f ). This turned everything inside out, because it reversed the winding order used for backface culling, but that was easily fixed with glFrontFace( GL_CW ). Then I realized I needed to flip my camera position too ( the cam/view position is used for specular lighting, and some line of sight calculations. )
After this I had a flipped view that looked like a reflection, but rendering the reflection on a water plane would for some angles look like it turned black. After staring at it for a while, I hid the ground below the water and realized this was caused by rendering flipped geometry below the surface of the water. I knew I needed a clip plane, but I didn't associate this with the problem right away.
Not being a shader expert, I'm not sure if there's a way to get glClipPlane to work with Cg shaders, so I searched the internet for another way to set a clip plane, and found this method for adjusting the near clip plane. So I wrote a similar function to fit with my code that just copied the exact algorithm ( later I'll make sure I understand it. ) I added a temporary toggle turn on flipping and the extra clip plane on the standard views so I wouldn't have to guess what was happening by looking at the reflection. I zoned out briefly trying to figure out what plane to send the new function before finding the rather simple plane calculation that's part of the full code below works:
// Flip Vertically around Y = PlaneY glFrontFace(GL_CW); glTranslatef( 0.0f, PlaneY * 2.0f, 0.0f ); glScalef( 1.0f, -1.0f, 1.0f ); // Compute Clip Plane CVec3 PlaneNormWS( 0, -1, 0 ); float PlaneDist = PlaneNormWS.Dot( CamPosWS - CVec3( 0, PlaneY, 0 ) ); CVec3 PlaneNorm = PlaneNormWS.RotByMatrix( CamRot.Invert() );
ShaderFor the actual shader itself, I copied a version of my framer buffer distortion shader, and after a few changes had a reasonable initial water shader.
The distortion is controlled by normal maps. It's animated by scrolling the texture coordinates on the normal maps. It has a uniform float input named "Seconds", which my shader system automatically fills with time in milliseconds. Settable uniform inputs include ScrollSpeed and distortScale. IN.ScreenPos is the transformed (clip space) vertex position sent from the vertex shader. I turn on alpha blending and write an alpha value so the water can be transparent. Hopefully the excessive commenting will explain the algorithm of the code below.
// convert from [-W,W] to [0,1] float2 reflectUV = (IN.ScreenPos.xy / (2*IN.ScreenPos.w) ) + .5; // animate the normal map texture UV coordinates float2 normUV = IN.TexUV + float2( ScrollSpeedX, ScrollSpeedY ) * Seconds; // get the normal from a tangent-space normal map float3 N = normalize( tex2D( normalMap, normUV ).rgb * 2.0 - 1.0 ); // offset the UV by the normal's (x,y) coordinates reflectUV += N.xy * distortScale; // read from the reflected texture half3 C = tex2D( reflectMap, reflectUV ).rgb; return half4(C, IN.alpha );Note that this doesn't actually have a diffuse texture or any water color. You can add those as you would in a normal shader, and blend between the reflection and diffuse color. You can also sample extra normals( either using different scrolling on the same normal map, or a secondary normal map ), and blend them together by adding them then normalizing.
Limitations & To-Do ListWith planar reflections, you can only reflect in a single plane. ( Here we cheated it a bit by distorting the texture coordinates, which isn't entirely accurate and only looks okay since it's all close to a single plane. ) One minor problem I figured would show up is the distortion causing bits of the background to show through where something intersects the water plane. I tried making it so the actual clip plane could be adjusted to be a bit below the water and that seemed to work, although I can't be sure yet if it will cause other problems.
My code is just for a single reflection. No house of mirrors effect allowed, but I didn't expect it to be. I even accidently created an infinite loop on my first test by just adding the flipped rendering before the water rendering, but not turning off the water itself for that stage.
The Stencil Shadows in the reflection map can get messed up for objects that cross the clip plane boundary. This one I'll definitely have to look into when I have time.
I haven't done anything yet for going underneath the water. I should test some kind of distortion on rendering objects below the water too.
Optimization is probably needed. We'll have to be careful where we use this because of all the extra rendering. I can't comment on optimization yet, I have a few ideas, but I won't know if they're good ones until I find time to try them out.