You fly your cave in your ship, dodging enemy fire. However, pretty soon you realize that there are too many enemies and it seems that this is the end. In a desperate attempt to survive, you press the Button. Yes, to that button. On the one that you have prepared for a special occasion. Your ship is charging and emitting deadly lightning at enemies, one after another, destroying the entire fleet of the enemy.
At least that is the plan.
But how exactly do you, as a game developer, render such an effect?
As it turned out, generating lightning between two points can be a surprisingly simple task. It can be generated as an L-System (with a little randomness during generation). Below is an example of a simple pseudo-code (this code, like everything else in this article, refers to 2d lightning. Usually this is all you need. In 3d, just generate a lightning so that its displacements are relative to the camera plane. Or you can generate a full lightning in all three dimensions - the choice is yours)
segmentList.Add(new Segment(startPoint, endPoint)); offsetAmount = maximumOffset; // максимальное смещение вершины молнии for each iteration // (некоторое число итераций) for each segment in segmentList // Проходим по списку сегментов, которые были в начале текущей итерации segmentList.Remove(segment); // Этот сегмент уже не обязателен midPoint = Average(startpoint, endPoint); // Сдвигаем midPoint на случайную величину в направлении перепендикуляра midPoint += Perpendicular(Normalize(endPoint-startPoint))*RandomFloat(-offsetAmount,offsetAmount); // Делаем два новых сегмента, из начальной точки к конечной // и через новую (случайную) центральную segmentList.Add(new Segment(startPoint, midPoint)); segmentList.Add(new Segment(midPoint, endPoint)); end for offsetAmount /= 2; // Каждый раз уменьшаем в два раза смещение центральной точки по сравнению с предыдущей итерацией end for
In fact, each iteration, each segment is divided in half, with a slight shift of the central point. Each iteration, this shift is halved. So, for five iterations, the following will turn out:
Not bad. Already looks at least like lightning. However, lightnings often have branches running in different directions.
To create them, sometimes when you separate a zipper segment, instead of adding two segments, you need to add three. The third segment is simply a continuation of the lightning in the direction of the first (with a slight random deviation).
direction = midPoint - startPoint; splitEnd = Rotate(direction, randomSmallAngle)*lengthScale + midPoint; // lengthScale лучше взять < 1. С 0.7 выглядит неплохо. segmentList.Add(new Segment(midPoint, splitEnd));
Then, at the next iterations, these segments are also divided. It will also be nice to reduce the brightness of the branch. Only the main lightning should have full brightness, since only it is connected to the target.
Now it looks like this:
Now it looks more like lightning! Well ... at least the form. But what about the rest?
The original system developed for the game used rounded rays. Each lightning segment was rendered using three quadrilaterals, each of which had a texture with light applied (to make it look like a rounded line). Rounded edges intersect to form joints. It looked pretty good:
... but, as you can see, it turned out pretty bright. And, as lightning decreased, the brightness only increased (as the intersections became closer). When trying to reduce the brightness, another problem arose - the transitions became very noticeable, like small dots throughout the entire lightning.
If you have the ability to render lightning on an off-screen buffer, you can render it by applying maximum blending (D3DBLENDOP_MAX) to the off-screen buffer, and then simply add the result to the main screen. This will avoid the problem described above. If you don’t have such an opportunity, you can create a vertex cut from a zipper by creating two vertices for each zipper point and moving each of them in the direction of the 2D normal (normal is perpendicular to the middle direction between two segments going to this vertex).
You should get something like the following:
And this is the most interesting. How do we animate this thing?
Having experimented a bit, I found the following useful:
Each lightning is actually two lightning at a time. In this case, every 1/3 of a second, one of the lightning ends, and the cycle of each lightning is 1/6 of a second. With 60 FPS, you get this:
- Frame 0: Lightning1 is generated with full brightness
- Frame 10: Lightning1 is generated with partial brightness, Lightning2 is generated with full brightness
- Frame 20: New lightning1 is generated with full brightness, lightning2 is generated with partial brightness
- Frame 30: New lightning2 is generated with full brightness, lightning1 is generated with partial brightness
- Frame 40: New lightning1 is generated with full brightness, lightning2 is generated with partial brightness
That is, they alternate. Of course, a simple static attenuation doesn’t look very good, so each frame makes sense to shift every point a little (it looks especially cool to shift end points more strongly - this makes everything more dynamic). As a result, we get:
And, of course, you can move the end points ... say, if you aim at moving targets:
And it's all! As you can see, making a cool looking zipper is not so difficult.