Hey Guys,
First post here, and thought I'd immediately start by contributing, the following is my blogpost about how I added Laser FX using OpenGL to our game:
http://www.codepoke.net/2011/12/27/opengl-libgdx-laser-fx/
And the youtube link:
http://www.youtube.com/embed/qzmW9TuLRRUA couple of weeks ago I started thinking on how to integrate some nice lasers fx in our game, Prototype Defense.
After searching on the internet for some guidelines on the subject, I found a single post on a dev's blog; Cliffski's.
Sadly however, he merely gave an outline on the subject, without any real implementation or specifics.
After implementing his idea, I started trying to enhance and expand it; and this is how I did it.
Pro Tip #1: Whenever you are working with OpenGL, and designing effects; learn to use the Hot Plug feature of your desired IDE. E.g.; code is integrated into a running program whenever you save it; without having to restart the entire program. This makes it easy to try different blending techniques, etc..
Laser FX 101To create laser fx in your game, you essentially need 3 sprites:
- Start Cap background; The initial blast coming from the nozzle
- Middle Section: The repeatable part of your laser
- End Cap: The end part of your laser. (E.g. A fadeout)
- Interference Overlay: A repeatable overlay animation showing "interference" which will give direction and realism.
For our purpose, we will expand this by another 3 sprites, and adjust the other 4.
Essentially I divided the Start, End and Middle section into 2 sprites; the background and the overlay.
The background indicates the characteristic "glow" of the laser, while the overlay indicates the "white, bright" beam.
I did this so I can programmatically set the color on the background (glow) of the sprite enabling me to reduce the amount of textures required. (and enable me to change the color during gameplay for even more wackier effects)
So now we have 7 sprites:
- Background: The blast & glow coming from the nozzle
- Overlay: The blast and beam coming from the nozzle
- Background: The ending part glow; a fade out of the glow
- Overlay: The ending part beam; a fade out of the beam
- Background: The glow of the beam
- Overlay: The beam itself
[/list]
Middle Section(Author note: the px measurements are for an image of 64x64)
As the middle section will define the actual look'n'feel of the laser, we'll start with this. Open your favorite artwork program and create a
white line in the
middle of your image, from
top to bottom,
2px wide on a seperate layer we'll call
"mid-overlay". Now add a
outer glow effect to this layer,
white, which as a
non-linear fade-off so that it ends at around
5px. Copy
"overlay" into a new layer, and call this
"mid-background". Now adjust the outer glow effect such that its fade-off is much larger (~18px) and it has a
transparency of around
50%.
Now save the overlay and background layers into seperate sprites.
(E.g; laser-mid-o.png & laser-mid-b.png)Don't forget to save the whole image to a seperate file, as we will re-use it for the start & end section.
Pro Tip #2: You can add overlay / background layers with varying outer glow effects to create different types of laser. Just keep in mind that the background layers should represent a "glow", while the overlay layers should represent the beam itself.Start SectionCopy the contents of
"mid-overlay" and
"mid-background" into 2 new layers called
"start-overlay" and
"start-background".
On the
"mid-overlay" layer, disable the outer glow and draw a
white circle in the center of your image (or wherever you think the nozzle will be), slightly larger than the
"mid-overlay" line. Remove the part of the line that would be behind the nozzle, so you end up with something that resembles a ball and a stick portruding from it.
Then re-enable the glow for this layer.
Now repeat this process for the
"mid-background"; so the glow incorporates the circle.
Save the layers into seperate sprites.
(E.g; laser-start-o.png & laser-start-b.png)End SectionCopy the contents of
"mid-overlay" and
"mid-background" into 2 new layers called
"end-overlay" and
"end-background".
Create a temporary layer and fill it with a gradient going from white to transparent from top to bottom. Select the resulting fill, and use the selection to delete the contents in the
"end-*" layers.
Essentially you should now end up with a fade effect going from opaque to transparant, from bottom to top.
Again, save the layers into seperate sprites.
(E.g; laser-end-o.png & laser-end-b.png)Pro Tip #3: This can obviously be done in different ways, try and experiment!Overlay AnimationFor this, I simply used
random noise, blurred, of the length of my beam, and width of around half the glow. I then animated it by moving the texture upwards, ensuring that it repeated seamless at both bottom and top.
(e.g.: copy the portion that moves outside at the top to the bottom)Now onto the actual code!
RenderingEssentially we want to render as such:
- Set Blending to addition; {GL10.GL_SRC_ALPHA, GL10.GL_ONE}
- Adjust alpha of beam && glow color (decay effect)
- For {x = start, mid, end sprite}
- Set color to glow color
- Draw x-background sprite
- Set color to beam color
- Draw x-overlay sprite
- Set color to default
- Draw overlay animation from the center of the start sprite to the center of the end sprite.
Additions would be:
[ul]
- Add randomness to the alpha
- Add a fade to the laser; E.g.: The end of the midpoint mesh (and endcap mesh) could have targetAlpha.
(see code at the bottom)
[/ul]
BlendingFirst off, the essential part of the laser look'n'feel is a specific blending technique call
addition.
The openGL command for this is:
OpenGL1
| openGlContext.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE); |
LibGDX1 2 3
| spriteBatch.end(); spriteBatch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE); spriteBatch.start(); |
The difference? Well look:
VS. 
ColorNow, choose a color for your beam and your glow, and ensure that the mesh you use to draw the
background sprites has the
glow color as it's vertex color; and the
overlay sprites have the
beam color.
To get a traditional laser effect, use red for the glow, and white for the beam.
OpenGL1 2 3 4 5
| vertices[idx++] = x; vertices[idx++] = y; vertices[idx++] = color; vertices[idx++] = u; vertices[idx++] = v; |
LibGDX1
| spriteBatch.setColor(color); |
To make the laser look more realistic, we will add a simple decay effect by setting the alpha of both
glow and
beam to something like:
1
| color.alpha = 1 - (lifeTimeOfLaser / totalTimeOfLaser) ^ 2 |
By making this exponential instead of linear, we get a nice "afterglow" effect.
Texture repeatFor almost all texture we either don't need to repeat
{start, end} or just stretch them
{middle}.
The main problem however is the Overlay animation. We need to repeat this texture vertically so we do not get any stretch artifacts.
Normally this could be easily solved by adjusting V in the texture coordinates of the mesh.
1
| vertices[idx++] = v * scale; |
However, this will only work if the overlay animation is in a single texture.
If you are (and I really hope so; if not make this priority number 1) working with texture atlases, then the U,V coordinates point to a region within the texture, which we can not simply repeat.
The solution to this is to create a single mesh consisting of multiple quads which point to this region.
The method to create such a mesh:
(Author note: This method also incorporates setting the color of said mesh, and a extension in which a
alpha-gradient could be given. E.g.: the mesh interpolates from the colorT.alpha to colorT.alpha*alpha over its scale.)
[Based on LibGDX SpriteBatch, with minor adjustments]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
private static final float[] constructEndingMesh(float[] vertices, AtlasRegion region, int scale, float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation, Color colorT, float alpha) { if(scale*20 > vertices.length){ vertices = new float[20*scale]; } float color = colorT.toFloatBits(); float colorE; int idx = 0; final float worldOriginX = x + originX; final float worldOriginY = y + originY; float fx = -originX; float fy = -originY; float fx2 = width - originX; float fy2 = height - originY;
if (scaleX != 1 || scaleY != 1) { fx *= scaleX; fy *= scaleY; fx2 *= scaleX; fy2 *= scaleY; }
final float p1x = fx; final float p1y = fy; final float p2x = fx; final float p2y = fy2; final float p3x = fx2; final float p3y = fy2; final float p4x = fx2; final float p4y = fy;
float Fx1; float Fy1; float Fx2; float Fy2; float Fx3; float Fy3; float Fx4; float Fy4; if (rotation != 0) { final float cos = MathUtils.cosDeg(rotation); final float sin = MathUtils.sinDeg(rotation);
Fx1 = cos * p1x - sin * p1y; Fy1 = sin * p1x + cos * p1y;
Fx2 = cos * p2x - sin * p2y; Fy2 = sin * p2x + cos * p2y;
Fx3 = cos * p3x - sin * p3y; Fy3 = sin * p3x + cos * p3y;
Fx4 = Fx1 + (Fx3 - Fx2); Fy4 = Fy3 - (Fy2 - Fy1); } else { Fx1 = p1x; Fy1 = p1y;
Fx2 = p2x; Fy2 = p2y;
Fx3 = p3x; Fy3 = p3y;
Fx4 = p4x; Fy4 = p4y; }
float x1 = Fx1 + worldOriginX; float y1 = Fy1 + worldOriginY; float x2 = Fx2 + worldOriginX; float y2 = Fy2 + worldOriginY; float scaleX2 = ((Fx2-Fx1) / scale); float scaleY2 = ((Fy2-Fy1) / scale); float scaleX3 = ((Fx3-Fx4) / scale); float scaleY3 = ((Fy3-Fy4) / scale); float scaleAlpha = (colorT.a - (colorT.a*alpha)) / scale; float x3 = x1; float y3 = y1; float x4 = x2; float y4 = y2; final float u = region.getU(); final float v = region.getV(); final float u2 = region.getU2(); final float v2 = region.getV2(); for (int i = 1; i <= scale; i++) { x1 = Fx1 + scaleX2*(i-1) + worldOriginX; y1 = Fy1 + scaleY2*(i-1) + worldOriginY; x2 = Fx1 + scaleX2*i + worldOriginX; y2 = Fy1 + scaleY2*i + worldOriginY; x3 = Fx4 + scaleX3*i + worldOriginX; y3 = Fy4 + scaleY3*i + worldOriginY; x4 = Fx4 + scaleX3*(i-1) + worldOriginX; y4 = Fy4 + scaleY3*(i-1) + worldOriginY;
color = colorT.toFloatBits(); colorT.a-=scaleAlpha; colorE = colorT.toFloatBits(); vertices[idx++] = x1; vertices[idx++] = y1; vertices[idx++] = color; vertices[idx++] = u; vertices[idx++] = v; vertices[idx++] = x2; vertices[idx++] = y2; vertices[idx++] = colorE; vertices[idx++] = u; vertices[idx++] = v2; vertices[idx++] = x3; vertices[idx++] = y3; vertices[idx++] = colorE; vertices[idx++] = u2; vertices[idx++] = v2; vertices[idx++] = x4; vertices[idx++] = y4; vertices[idx++] = color; vertices[idx++] = u2; vertices[idx++] = v; } return vertices; } |
Then feed this mesh, along with the texture to either OpenGL, or LibGDX.
And that's about it. Have fun coding!
References: