< Back

Rain Simulation in WebGL

Rain Simulation in WebGL

Video Overview

Abstract


For our project, we implemented a simulation of raindrops sliding on a glass window using WebGL for rendering graphics. The main components of this project were architecting the raindrops themselves, the motion of the raindrops, and the fragment shaders used to simulate the reflection and refraction of raindrops. We also used a physically-inspired hack on the background image, blurring it to give it the appearance of glass for the raindrops to "slide" on. Our simulation responds to user controls and there are two menus to change the background and shaders and a GUI available to manipulate different raindrop architecture and motion parameters.

Our project simulation is available here.

Technical Approach


💭
Disclaimer: we're all pretty new to web development, so to minimize struggles with the infrastructure surrounding this project, we decided to clone an existing project with a similar stucture (RainEffect) and use its WebGL infrastructure as a foundation. We rewrote the motion simulation of each raindrop, and the shaders, from scratch, while using the article as a guide on specific physics/graphics areas to research and understand. We also added several novel components, including a handful of different shaders and a GUI to modify hyperparameters.

Raindrop Motion Architecture

When raindrops fall onto a window, they exhibit a few key behaviors. Below, we walk through the seven attributes of a raindrop that we define to characterize the behavior of a drop. Then, we discuss how we simulate the motion of each drop. This approach doesn't explicitly model the viscocity of the water/gravitational forces/etc. - but it does look realistic, and the approach is inspired by these physical properties.


Creating the Drop

  1. Each drop has some initial size.
    • This size has three dimensions, and we can model it with a simplified three-dimensional representation (x,y,d)(x, y, d).
    • Since all drops in this simulation have the same z-position – since they're all on the same window plane – the dd parameter corresponds to the "depth" of the drop.
    • After visual inspection, we set the lower and upper bounds of the size of a drop to (10,40) pixels(10, 40) \text{ pixels}
  1. Each drop has some initial position.
    • The xx coordinate is chosen by rand(this.width)
    • The yy coordinate is chosen by rand(this.height) * 0.95, where 0.95 is a factor that forces each raindrop to be slightly above the bottom of the view
    • Over time, the position varies due to the attributes defined below.
  1. Each drop has some initial radius. This radius is chosen by rand(min_radius, max_radius)
    • Over time, drops shrink as they dry up – and larger drops collide and merge with smaller drops. Both of these physical features affect the radius of each drop.
  1. Each drop has some initial shrink.
    • This aims to model the physical property of "drying up" – smaller drops eventually dry up on a surface.
  1. Each drop has some initial momentum.
    • Intuitively, we'd like larger drops to have larger momentums, so this momentum should be some random component scaled by the drop's radius.
    • Through trial and error, we define the initial momentum to be:

      0.5 * [(d.r - min_radius) / (max_radius - min_radius)] * rand(2)

    • Over time, the momentum should vary based on randomness (how likely a drop is to "slip" down) as well as due to collisions with other drops.
    • The default momentum variable, momentum, refers to the momentum along the main axis (y-axis). We also define a momentumX variable that models the change in x position due to collisions and other randomness (intuitively, drops rarely fall in a perfectly straight line).
  1. Each drop has some initial spread.
    • The spread of a drop is a physically-inspired property that characterizes how much the drop spreads out on the surface when it comes into contact with it.
    • We model this as two-dimensional spread (different spread for xx and yy coordinates). We assign an initial spread value of 1.0 to each axis.
    • Over time, the spread should decrease (it originates from the initial impact, so this makes sense intuitively).
  1. When a large raindrop slides down a plane, it may spawn other child raindrops as it breaks up due to surface friction.
    • This doesn't happen at every time step, so we need some variable to track when to spawn new raindrops.

Simulating the Motion of Each Drop

Our application maintains a drops array, which has a list of all current Drop objects on the screen at any particular point in time. The animation loop calls the updateDrops method.

In updateDrops, we first add a raindrop if the number of raindrops on screen hasn't exceeded the maximum capacity.

Then, we apply the following logic to each drop.

  1. Simulate the independent movement of each raindrop by modifying the raindrop's momentum attribute.
    • Let norm_drop be the normalized drop size between (0, 1).
    • Each raindrop has some probability, pslidep_{\text{slide}}, of sliding down screen at any particular time step. Intuitively, we want this probability to correspond to the drop's size, so we define it as follows: chance(0, 0.5 * norm_drop)
    • If the drop should fall, then we update the momentum based on another random variable that's also dependent on the size of the drop:

      drop.momentum += random(norm_drop)

  1. Simulate the shrinking of drops due to them "drying up."
    • Intuitively, not all drops will shrink at the same rate, so we apply some randomness here.
    • We increase the drop.shrink attribute by 0.01 with a probability of 0.05.
    • Then, we reduce the radius by the current drop.shrink value.
    • As such, the drop shrinks faster over time, since the drop.shrink value increases up over time.
  1. Simulate the spread of each drop.
    • Over time, the spread decreases, so we reduce drop.spreadX and drop.spreadY by a constant value (0.9 and 0.4, respectively).
      💭
      This is inspired by this Wikipedia article describing drop impact - it identifies two possible behaviors (spreading and splashing) - for simplicity, we only implement spreading. This effectively assumes that a drop hits the glass at a velocity below the critical threshold for splashing/scattering.
  1. Add new child drops in the path of this drop if necessary.
    • Drops are spawned at a rate dependent on the size of the drop and the current momentum.
    • When a child drop is created, its size is smaller than the parent drop and its position is slightly above the parent drop
  1. If the drop moved, then update the drop's position.
    • If the drop went off screen, then remove it from the array.
    • Check for a collision with another drop:
      • Apply the distance formula to calculate the relative distance between the two drops.
      • If a collision occurred, calculate a new radius based on the combined area of both drops.
      • Update the original drop's radius, momentum, and spread. Also, update the momentum in the x-axis here (realistically, when a drop collides, it may not fall in a straight line.
  1. Simulate friction by applying an upwards force to each drop.
    • Intuitively, drops won't slide smoothly down screen.
    • Effectively, we slow down each drop's momentum by a factor that's dependent on the current momentum and drop radius.
  1. Redraw each drop to the screen.

Rendering Each Raindrop (Drop Alpha)

We use the approach here in order to render each raindrop.

Every raindrop starts off as a square canvas, as seen in this image.
We use this image as an alpha layer to create the raindrop shape.

We utilize the rgb values to model the depth and refraction vectors of the droplet.

In order to shape our droplets, we utilize a css feature that allows us to "layer" the drop_alpha image over each of our raindrop canvases. In effect, this layering will only show the parts of the canvas that are encompassed in the black portion of the alpha image. Since the image is blurred around the edge, that makes for natural and realistic-looking raindrops!


Incorporating Reflection, Refraction, and Alpha Blending

REFRACTION: In order to model refraction in the droplet, we make use of the drop color image. Using the x and y coordinates, we calculate an arbitrary incoming light ray's refraction vector into the water droplet. By flipping this vector, we are able to backtrace the ray to find the background pixel that will be shown at the given pixel. Then, we use alpha blending to create realistic, semi-translucent coloring. For alpha blending, we have a couple different cases:

REFLECTION: We compute reflection values by by simply taking the background, scaling it down, flipping it across the y-axis, and applying an alpha transparency value to it. This reflection component gives it a depth effect that makes the image look much more real and 3D.

Finally, we created a variable that connects to our RaindropRenderer, which pipes in a user input from our GUI. This variable controls the mode, and as such, the shader that's applied to the image!


Shaders

We implemented two shaders that built off of our baseline, realistic shader. These shaders were based on a shadertoy project we came across in our research.

The Rainbow Shader

The color augmentation seen here is actually a result of applying a uniform color transformation to the given RGB values. We take in given color values and apply an arbitrary scaling of saturation, hue, and brightness for making finer changes. We then convert our HSV value back into the RGB color space. This is our returned value!

One interesting element to note here is our separation of the foreground and background. When processing the image, a raindrop is present when the foreground has an alpha value > 0.0, since the raindrops are only present in the this foreground canvas. As such, we were able to choose when to apply our filter based on this alpha value. For the rainbow filter, we decided to only apply this shader to our raindrops, which gives it the really interesting effect you see in our demo!

The Spooky Shader

This shader has very similar color processing to the rainbow shader. However, after computing the augmented rgb values, we applied a vectorized filter to the values to convert them into grayscale. We applied this shader to the entire backdrop (both the foreground and the background).

Shaders Menu: Select between the original, rainbow, and a black and white (haunted) shaders

Challenges

One of the challenges we encountered was designing the architecture to track the spawning of new child drops efficiently. As it turns out, we needed to set a parent attribute to the drop in order to ensure that the original drop didn't immediately collide with the newly spawned dot in the collision logic below.

Fine tuning the shrink/spread parameters was tricky - we wrote this logic, and then disabled it until we had built our GUI layer. Then, we used the sliders to determine parameters for shrink and spread that seemed realistic.

Lessons Learned

One of the things that we learned is that for a lot of things in computer graphics, it's possible to get results that are quite realistic without fully modeling the physics behind every single particle in the scene. Our approach to rendering raindrops was very much a hack (we're not independently modeling each water atom + the viscosity between them).

We also learned how to use WebGL and JavaScript for graphics implementations. This included understanding how to debug, format, and integrate new libraries within our HTML/CSS/JS workspace.

Results


We implemented a GUI to let users control the raindrop architecture, motion, scene background, and fragment shaders. A full project simulation is available here. The following animations show these results:

Original.
Extra friction. Notice how raindrops pretty much don't slide anymore - they land, and shrink until they disappear.
Without friction. Notice how the rate at which raindrops fall is constant.
Huge drops.
Without spawn. Notice how the drops don't spawn new drops behind them (once a raindrop lands on the plane, it slides down, and doesn't break up).
Without shrink. Notice how raindrops don't get any smaller.

We also implemented custom menus to change the scene background and the shaders used:

Background Menu: Select between a background of Berkeley, a city, and a mountain scene.
Shaders Menu: Select between the original, rainbow, and a black and white (haunted) shaders

Lastly, we used dat.GUI to let users alter the architecture and motion of the raindrops:

Raindrop Architecture Menu - Control fall amount, friction, and raindrop radius
Raindrop Motion Menu - Control spread, shrink, spawn, depth, and raindrop speed

References


Contributions


Shomil: Initial framework setup, raindrop architecture and mathematical motion simulation

Anjali: Wrote the fragment shaders (reflection, refraction, alpha drop + blending)

Niky: Configured background image setup, user interface menus + dat.GUI implementation