Tuesday, August 6, 2019

Lightning

It's time to take another break from my procedural city project and work on something different until I figure out what the next steps are. Last week someone posted a video of real lightning in slow motion on Reddit, which reminded me of the lightning effect I added to 3DWorld way back around 2008. I went back and looked at the lightning in 3DWorld, created a video, and posted it on YouTube. Here it is, but I'll warn you that the lighting doesn't look very good.




Here's an image of one lightning strike and some fires that have been started in the trees and grass. Keep in mind that I haven't worked on lightning in many years.

Original lightning paths were mostly vertical, heavily overlapping, snapped to a grid, and not very realistic.

There are several problems here. First, the lighting is too vertical. Some of the lines are entirely vertical and others are mostly vertical. Second, the individual paths are too close together. They should diverge rather than converge and intersect each other. Third, you can somewhat see the regular grid structure in the individual path vertices, which are spaced too regularly from each other. So of course I went back to try and improve the lightning path generation algorithm.

I won't go over the details of how lighting is formed and the physics of a lightning path. If you're interested, you can read about it here. I'm only trying to model cloud-to-ground lightning. In 3DWorld, I start with a uniform 2D grid of random charge distribution values in a plane at the altitude of the clouds. These represent charged ice particles during a thunder storm. Each lightning strike originates from the grid point containing the largest charge, and depletes the charge in a circular area around that point. This ensures that lightning doesn't start from the same location each time, and models real lightning charge formation to first order. Lightning rarely strikes the same place twice! This part of the system works fine and doesn't need to be changed.

A lightning strike is composed of a number of different paths that recursively fork from the main path and flow from the clouds to the ground in a random path. Any path that reaches the ground without going outside the scene bounds creates a fire and some smoke, unless it hits a water surface. (Yes, even when it's raining.) This sets the trees and grass on fire and will eventually burn the entire scene to black. Lightning hits also damage the player in gameplay mode, though I can't remember ever being hit by lightning while playing. This is probably because players are down below the level of the trees.

Each path is represented by a series of line segments, which are drawn as long, thin, camera-oriented quads. The vertices of adjacent quads in the path are connected together to from a sort of ribbon. These quads are textured with a Gaussian falloff alpha (transparency) value with mipmaps to make them decrease in brightness with distance and toward the edges of the path. In addition, the primary path's end point generates a high intensity point light source. Lightning strikes only last a few hundred milliseconds, but I'm able to freeze-frame them in place with a keyboard key so that I can get good screenshots.

The original algorithm generated paths by building a 3D voxel grid where each grid cell stored the direction of the nearest grounded object (tree, terrain, water, building, etc.) The grid was generated on the first lightning strike and cached for later use. Lighting would start at the point of highest charge and follow the shortest path to ground, with a bit of random variation mixed it. It stored a set of cells that had been previously visited so that each fork would follow a different path. Unfortunately, this often resulted in many of the forks following similar paths straight down to different parts of the same tree. It also had a noticeable grid pattern because each line segment was about the same length.

My new idea was to discard the 3D voxel grid and replace it with a downward biased random walk. A current direction is maintained at each step, initialized to pointing down in -z. Each iteration adds a random spherically distributed vector to the direction, re-normalizes it, and moves the path a random distance in that direction. The z component of the direction is negated if it ever becomes positive to keep the lighting pointed downward. This guarantees it will eventually reach the ground. If a direction is chosen that moves the path outside the scene bounds, a new direction is generated. Each iteration has a random chance of forking the path. The first path created is the primary path, which must continue until it hits a grounded object. This way the algorithm ensures there's at least one valid full path and one hit point. All other paths have a random chance of ending early, resulting in shorter segments.

Here is a lightning bolt generated using this algorithm. As you can see, the individual forks are well separated and hit at very different locations. They're no longer vertical and have more random segment length variation.

Improved lightning path using random walk algorithm and modified splitting constants.

However, this still isn't quite realistic. Lightning doesn't normally hit many places at the same time. There's usually only one dominant path the ground and lots of dead-end "feeler" paths that reached out but went in the wrong direction. These paths ionize the damp air, decreasing its resistance so that more current can flow. The air resistance is still high though. Once the path to ground is found, this much lower resistance channel allows a huge amount of current to flow, generating most of the light and energy of a lightning strike as the charge is sent into the ground.

I decided to try to model this effect. A final postprocessing step is run when all paths have either ended or reached the ground. The algorithm calculates the shortest completed path to ground and makes that the primary path, increasing the brightness by 2x. This is the path of least resistance where the majority of the charge will flow to ground as electric current. The other feeler paths are shortened to the length of the shortest path if there's enough distance from their last fork position. Any paths that reach the ground after this step are considered hits and generate fire and smoke.

Here are daytime and night time images created using the modified algorithm.

Lightning path with random walk and shorter segments that don't reach the ground. The primary path to the ground is brighter and spawns fire.
The same lightning strike as above, but at night, with fog and clouds.

I decided that I didn't like some of the sharp bends in the paths. As a finishing touch, I decreased the amount of random direction change added to each segment. Here are some night time images of the final lightning path code + constants. I haven't really decided if this is an improvement or not.

Lightning path with many branches shown at night, with area lighting effect.

Another nighttime lighting strike. Most of the scene lighting comes from the lightning.

Here's a newer video I recorded and posted on YouTube of lightning strikes at night. The rain looks much better uncompressed at native 1080p resolution. I don't know why it looks so bad with YouTube compression. I can freeze the physics simulation and catch lightning in mid-strike, though I don't get the point lighting effect on the terrain when it's paused. Each strike creates one or more fires in the trees and grass that will eventually spread and burn everything down.




3DWorld's lightning definitely has improved, but it's still not perfect. It's difficult to get a good trade-off between branching forks and jagged edges. If I set the random walk value too strong it tends to produce sharp turns, spirals, and self-intersecting paths rather than a regular forked tree like I see in many photos of lightning. It's difficult to find resources online for generating and drawing lighting. Maybe I'll get back to it later.

I might need to work a bit more on drawing the paths. Blending doesn't always work against transparent objects, it and would probably look better if the paths ended in something other than a sharp edge.

Fortunately, neither generating nor drawing the lightning takes any measurable amount of time, so there's no need to add complexity to optimize this system. That seems to be a pretty rare occurrence in 3DWorld.

As usual, the code is all on 3DWorld's GitHub project. The lightning source code is fairly simple and self-explanatory, and can be found here.

3 comments:

  1. Two words: Intensity. Bloom.
    Also, wondering if lightning uses the "line" light type? Is there a way to add shadows to it? The stark shadows from lightning are one of the more interesting things about them. Because they are so short, you might be able to spread the shadow calculations out over multiple frames?

    ReplyDelete
    Replies
    1. The initial strike has a thunder sound and a bright flash, but the flash is difficult to capture in a screenshot. I'm not using line lights for lightning. Maybe I could, but there are a lot of lines. Instead it has several point lights at points where the strike hits objects. I have two lighting modes for lightning, one using a single global light source and the other using dynamic point lights. The global light source has shadows, but only for the primary hit point. I could probably enable shadows for the local point light method, but I don't believe I have it enabled for this scene. I don't remember what mode I'm using here. I was concentrating on the shape of the lightning in this post, not on the lighting and shadow effects.

      Delete
    2. I added shadows to the primary lighting hit point light source. It took a while because I had to debug some things. I spent a long time debugging an assert in the cleanup of lighting data when the lightning was turned off. Then it took me awhile to figure out it wasn't working because I copied the code from an indoor light and forgot I had to set it to an outdoor light to enable shadows for trees.

      Delete