Water
Let's explore one last application of noise: water. If we were making a high-definition movie, we'd probably model the water as tiny particles acted on by forces. We don't have time to accurately simulate physics in a real-time renderer. Instead we'll render the water's surface as a simple quadrilateral tinted blue and reflecting a skybox. That looks like this:
The water is just a quadrilateral. Feel free to walk under it.
To add waves, we apply noise to the quadrilateral and derive a normal from the surface. Even though the geometric surface is flat, the derived normals will undulate across the virtual terrain of the noise. When we reflect the eye vector about these normals, they'll ripple across the environment map. To make the waves move, we make the noise a 3D volume that we raise and lower through the quadrilateral over time. The quadrilateral has 2D texture coordinates that we interpret as x- and z-coordinates. The time value gives us the y-coordinate.
How do we derive normals from the noise? Earlier we calculated a face's normal by identifying two vectors tangent to the face and then taking their cross product. We do something similar with noise using an algorithm called forward differencing. First we need three locations within the texture. One is for the fragment itself and the other two are in neighboring positions offset along the x- and z-axes. This code computes these positions with an offset of 0.01:
float offset = 0.01;
vec3 centerTexPosition = vec3(mixTexPosition.x, time, mixTexPosition.y);
vec3 rightTexPosition = vec3(mixTexPosition.x + offset, time, mixTexPosition.y);
vec3 forwardTexPosition = vec3(mixTexPosition.x, time, mixTexPosition.y + offset);
float offset = 0.01; vec3 centerTexPosition = vec3(mixTexPosition.x, time, mixTexPosition.y); vec3 rightTexPosition = vec3(mixTexPosition.x + offset, time, mixTexPosition.y); vec3 forwardTexPosition = vec3(mixTexPosition.x, time, mixTexPosition.y + offset);
Next we look up the noise “heights” at these three positions:
float centerHeight = texture(noiseTexture, centerTexPosition).r;
float rightHeight = texture(noiseTexture, rightTexPosition).r;
float forwardHeight = texture(noiseTexture, forwardTexPosition).r;
float centerHeight = texture(noiseTexture, centerTexPosition).r; float rightHeight = texture(noiseTexture, rightTexPosition).r; float forwardHeight = texture(noiseTexture, forwardTexPosition).r;
By subtracting away the center from the neighbors, we form two tangent vectors that ride along the noise surface. Then we cross them to get the normal:
vec3 rightTangent = vec3(1.0, rightHeight - centerHeight, 0.0);
vec3 forwardTangent = vec3(0.0, forwardHeight - centerHeight, 1.0);
vec3 normal = normalize(cross(rightTangent, forwardTangent));
vec3 rightTangent = vec3(1.0, rightHeight - centerHeight, 0.0); vec3 forwardTangent = vec3(0.0, forwardHeight - centerHeight, 1.0); vec3 normal = normalize(cross(rightTangent, forwardTangent));
From there, the fragment shader applies environment mapping using the regular algorithm. The end result looks like this:
This is just one strategy for rendering water, and it's not particularly memory efficient. 3D textures consume a lot of VRAM. Other strategies use simpler 2D textures, possibly layering them to effect feelings of depth and perturbing the texture coordinates to produce motion. Additionally, noise algorithms are sometimes implemented directly in the shader.
Summary
Modeling nature is tough. Algorithms produce models that look sterile and manufactured, but we can disrupt their perfection with randomness. Stock random number generators produce uniformly random values, which results in white noise. For natural phenomena, we want coherent randomness. One way to produce coherent randomness is to generate multiple levels of white noise, resample each level to a common resolution, and blend the levels together. The resulting value noise has axis-aligned artifacts. Perlin offered gradient noise as an alternative. Instead of generating random values across a grid, we generate random gradients. A noise value is produced at an arbitrary position within the grid by dotting the position's difference vectors with the four surrounding gradients. The resulting noise may be used to produce clouds, rock, wood, water, and other natural phenomena.