Sunday, September 3, 2023

Basement Water, Improved

This is a short update to my previous post. I've found a way to add most of the features that were missing from my previous basement room water.

Walls Blocking Waves

One problem with my water implementation is that the ripples/waves pass straight through walls because the fragment shader that draws the water doesn't know where the walls are. Let's fix that. I don't want to add a strict line-of-sight test for every point in the water to the source of the splash because that's unrealistic and difficult. Waves can pass through narrow gaps and expand outward; trying to clip them to the exact walls looks incorrect. Instead, I can calculate the extents in each of the four cardinal directions (+X, -X, +Y, -Y) and pass them to the shader as planes bounding the waves for each splash. This is done by casting a number of rays (I used 90) out in each direction from the splash origin, clipping them to the building walls, and tracking the max distance a ray can travel in each of the four directions. This is similar to how room ceiling lights are bounded to optimize lighting in the fragment shader.

The effect is that any splash bounded on one side by a wall will have its waves blocked by the wall. This is particularly apparent for splashes made in enclosed rooms that shouldn't propagate outside the room. This appears to fix most of the obvious cases where waves went through walls.

Refraction

Adding refraction for underwater objects such as the floor requires modifying objects that were previously lit and drawn. Fortunately, I already have a system to read the current color buffer and pass it into the shader as a texture. If I then make the water fully opaque rather than transparent, I can manually blend the refraction into the background using the computed alpha (transparency) value. Now that I have the underwater part of the scene as an image, I can use the same texture coordinate domain warping approach I used with reflections to add refractions. This produces a nice wave distortion of the floor and other underwater objects under the ripples.

Caustics

Finally, it's time to add fake caustic lighting effects to the underwater objects. This is another color transform the fragment shader applies to add the effect to previously drawn geometry. Caustic lighting appears as bright and dark spots in areas where the rippled (non-planar) water surface acts as a lens to focus light onto certain areas below the water.

Most of the reference images you can find for water are taken in outdoor areas with the sun and a water body such as an ocean, lake, river, or swimming pool. My basement water is different for two reasons. First, the light source is a array of overhead rectangular area lights rather than a single high intensity light such as the sun. It's not clear exactly how to compute caustics efficiently in this case. Second, indoor water isn't subject to forces such as the wind and tide that create waves or smaller surface irregularities. Undisturbed indoor water is almost completely flat. When objects are dropped into the water, this creates ripples. The ripples from multiple nearby splashes merge and create interference patterns. Adding many splashes gives the water a very chaotic wave pattern.

I'm not entirely sure what the caustic lighting patterns on the floor should look like in this situation. So I'm going to do something simple and calculate the lighting from only the water surface normals that are used for reflections and refractions. This is simple but not physically correct. The only problem is that I can't simply adjust the normals because lighting has already been applied to the underwater objects. The best I can do is to adjust the color of the refracted pixels based on the signed wave height to increase or decrease their brightness. I had to turn the effect down so that it's not as obvious that this is a hack. I think it's still noticeable, and is the only observable effect of the ripples directly below the player.

Anyway, here is an updated video of basement water. I think the water effect looks much more realistic now.


 And here are some screenshots after I walked around in the water.

Basement water, now with refractions, caustics, and wall blockers.

The scene from above, but after moving backwards and waiting a few seconds.

In case you're curious what this looks like with the lights off, it's dark as expected:

Water shown in a dark basement with the lights off, and the only light coming through the stairs opening from the floor above.

Update

I just realized that I haven't enabled reflections in these screenshots. The zombie/people reflection code was also broken, and I had to fix it. Here are the new video and three updated screenshots with both reflections and refractions.






Saturday, September 2, 2023

Basement Water

I've added a layer of water to the lowest levels of some of the office building backrooms basements. It's about a foot deep and mostly clear. The water is drawn using a physically accurate method that includes computing a Fresnel reflection term and calculating wavelength dependent absorption and scattering. Scattered light uses the color derived from nearby room ceiling lights and includes a shadow term. The top surface appears reflective at shallow view angles. The reflection code was reused from what I originally implemented for bathroom and dresser mirrors.

I've also added a system for tracking the 32 most recent splash effects and adding them as ripples that distort the water's surface. Each splash source generates sine waves that propagate out and are computed per-pixel in the fragment shader. This changes the normal used for lighting and also the texture coordinates used for reflection lookup. In addition, splashes add a local dirt/foam effect that temporarily makes the water a bit cloudy.

Everything that touches the water produces a splash with magnitude that depends on the force of the movement. This includes the player, people, and zombies walking through the water. It includes collisions of balls with the water when kicked or thrown. Pushing and dropping objects in the water generates splashes, as does opening and closing doors submerged in water.

This video shows my progress so far. I initially tried to get a good splash by dropping the fire extinguisher in the water, but it was difficult to see from that steep view angle. Ripples are easier to see when viewed at an angle from further away when the reflection term is stronger. The slight lag that occurs while the zombie is chasing me is due to the loading of a different 3D model for a zombie on the floor above me that never quite comes into view.


Here's an image of the same scene. The default is clear, clean water. The water absorbs the red component of the color more than the other wavelengths, and scatters the blue color the most. This results in the water appearing to have a blue tint that varies from light blue/white nearby to a deep blue in the distance where the camera's path through the water is longer due to the view ray slope. The absorption wavelengths can be adjusted in the shader to give the appearance of other fluids such as dirty/muddy water and even blood.

A basement full of clean, clear water that takes on a blue tint.

The same scene, but with dirty, muddy water. However, it has the same reflectivity, only the absorption has changed.

The same scene, now shown with bloody water. (Now that I took the screenshot I realize the foam should be dark red rather than white.)

The main() function of the fragment shader code can be found in my GitHub repo here.

I haven't yet figured out how to add refractions for the floors and walls under the water because these have already been drawn by the time the water is drawn over them. It's not particularly easy to customize the lighting and shading of these objects based on the water above them like I do with terrain. There are no caustic lighting patterns on the underwater objects either, for similar reasons. I have a caustic texture that I use when drawing the terrain under ocean water, but that same approach doesn't work well here. I'll look into a screen space approach that reads the current buffer contents before the water is drawn and includes that in the water shading, which may allow me to add these features.

Oh, and the water is currently only a flat plane with no height displacement due to waves. Also, the simulation is mostly run on the GPU, which doesn't have access to the wall locations. This means that waves propagate along the surface and through walls rather than reflecting off of them. I may be able to partially work around this by computing the bounds of each ripple by ray casting it against the walls on the CPU, then sending it to the GPU as a uniform buffer. This is how I handled room lights.

Finally, it would be nice to have an animated splash effect when objects enter the water. This would add some 3D volume to the water surface. I may be able to reuse the technique I used for raindrop splats in previous work.

I'll continue to think about these limitations to see if I can resolve them in the future.