Tech Report: A Technique for Rendering Cheap Laser Bolts That Look Good From All Angles

[Note: An explanatory video of the 3D laser bolt effect is now available on YouTube.]

In this post, I will present a technique for rendering laser bolts that look good from any viewing angle (including head-on), without the use of expensive computations such as blurring. To understand this technique, we will assume the effect has already been achieved, and work backwards to discover the correct approach.

Our completed effect is depicted below:

laser-single-no-markup

Advanced rendering techniques might accomplish this effect with post-processing: the laser would be rendered as solid geometry, and blur passes would generate the glow. Let’s assume that the costly blur operation has already been accomplished for us, i.e., that we’ve gotten it for free.

To do this, we’ll pretend that the blur itself is a simple texture (in the shape of a laser bolt) that has been overlayed on the screen such that it covers both end points of the laser:

laser-single-by-texture

No single texture will do the job, however, because the texture would have to be different for differently shaped lasers, and its appearance would depend greatly on the user’s viewing angle.

Consider a single texture like the one below:

laser-front

Interestingly, this is exactly what we would expect a laser bolt to look like if we viewed it dead-on. If we viewed the laser bolt from its side, we would expect to see something like the following:

laser-side-no-markup

We can easily see that the second texture can be generated from the first texture. We simply divide the first texture into three sections: left, middle, and right segments. The right and left segments always remain unchanged, but we can stretch the middle one to generate the second texture:

laser-front-with-markup.png          laser-side-with-markup.png

Since we can stretch the middle portion to any length, and scale or rotate the resulting image arbitrarily, we can easily see that it is possible to essentially “pre-compute” the blur effect that would be necessary for a convincing laser bolt effect. All that is necessary is to overlay the pre-computed blur onto the screen coordinates of the laser bolt itself, using an orthographic projection.

The screen coordinates of the laser bolt are easily calculated by projecting the 3D end-points of the laser into screen space. Once we have those two points, we easily compute the 8 vertex positions (necessary for the left, middle, and right segments of our blur texture) within orthographic space. We can also scale these vertex positions based on their distance from the camera to generate the illusion of a perspective view even though we’re using an orthographic projection.

laser-single-with-markup

Note that the laser bolt also looks exactly like we would expect, when viewed directly from the front:

laser-demo-screenshot-front

There is one more hurdle to overcome. Because we have rendered the laser bolt using an orthographic projection, our fragment depth values do not exist in the same coordinate system as the rest of our (3D) scene. In other words, our lasers will not occlude (or be occluded by) other geometry. To correct this, we will use the z-coordinates that were computed during our projection step above to obtain depth values for the laser end points. In our vertex shader, we can then assign appropriate depth values to each of the 8 vertices used to render our laser. This will allow us to depth-test the lasers against the rest of the scene.

By using batch rendering techniques, we can render a large number of laser bolts efficiently:

laser-demo-screenshot

An example implementation and complete source code for the above screenshot is available here.

GN

Advertisements

Tech Report — Efficient Laser Bolt Collision Checks

I’m going to depart from my usual style of blog post and exercise my right to describe some technical stuff. This week’s topic is going to be about collision detection. It’s a massive topic, but for the purposes of today’s post, I’m going to focus on a very specific portion of Gateway’s collision detection system: how it handles laser bolts colliding with other objects.

Collision detection is potentially very costly, and the amount of collision checks between objects doesn’t scale linearly. The real trick is to discard potential collisions with as many objects as quickly as possible. In my attempts to reduce how often these checks had to take place in Gateway, I focused my efforts upon the most prevalent collision-enabled objects in the game world: the laser bolts.

In my game, lasers move very fast. We can’t rely on simple distance checks to determine if a bolt hits a fighter; the ordnance may very well simply ‘jump’ over the fighter and miss it entirely.

Simple distance checks will not suffice because the bolt moves so fast that it may skip over a potential victim

Simple distance checks will not suffice because the bolt moves so fast that it may skip over a potential victim, missing it

So, instead, we cast a ray between the old and new positions of the laser bolt.

As the laser bolt moves forward, our collision system performs a ray cast between its old position and its new one to see if it hit anything in between

As the laser bolt moves forward, our collision system performs a ray cast between its old position and its new one to see if it hit anything in between

This works fine, but we’re unnecessarily testing the ray cast against objects that fall behind the bolt. So, we discard any objects from the ray cast test that aren’t in its path. This can be done with a fast vector dot-product calculation and discards half of the objects we have to test against, on average.

Ray casting is expensive, so we should only test the objects that fall in front of the laser bolt's path

Ray casting is expensive, so we should only test the objects that fall in front of the laser bolt’s path

But, we can still do better than this.

Now, we’re performing these collision checks every frame. Even though we’re discarding a good chunk of the objects in our scene fairly quickly, that can still be costly in terms of pure iteration. These checks for discarding objects also consume resources.

Let’s think about this: laser bolts move really fast. Over the life of a single bolt (about one second or two), no ship is likely to change it’s position much. In other words, the set of potential colliders for any single laser bolt is fairly static throughout its entire life. So, why not just pre-compute a list of potential colliders at the beginning of the bolt’s life? This is likely to be a very short (or empty) list, and we can re-use it for every frame of the bolt’s life until it dies. In other words, we’re only computing the list of possible collision candidates once.

This page has some great code for a fast check to see if a point lies within a cylinder in 3D space. By specifying a radius wide enough to account for the fact that certain ships may cross the path of the laser bolt, we can use this to build a list of potential colliders that will remain valid until the laser dies. This list might only ever contain just a handful of ships out of hundreds, and will save our collision engine from tons of unnecessary iteration.

When a laser bolt is first spawned, compute a small list of candidates which we may collide against, and re-use it continually throughout the laser's life

When a laser bolt is first spawned, compute a small list of candidates which we may collide against, and re-use it continually throughout the laser’s life

In the image above, we can see that only two ships are within this cylinder. For that particular laser bolt, those are the only objects we have to check for collisions against. Pretty neat.

Of course, we’ve made a number of assumptions. We assume that the laser bolt is fairly short-lived and travels very fast. Slower or long-lived ordnance would necessitate the use of a wider cylinder, if we could still use this technique at all. Additionally, weapons that don’t move in a straight line (such as heat-seeking missiles) would present a problem. But for standard laser bolts, this works just fine.

GN