Hello, and welcome to the fifth in a series of in-depth articles about Kiwi.js v1.1.0, now available for use at www.kiwijs.org. Today we’re talking about blend modes and how they unlock new possibilities for drawing fantastic effects. I’m Ben Richards, and today I’ll show you some new ways of handling Renderers in Kiwi.js.
This post refers only to the WebGL rendering system. Canvas rendering does not support this functionality at this time.
Who Needs to Read This Post?
Advanced users. If you want to write your own shaders, this contains important information. If you’re interested in particle effects or other special effects, this is also important.
WebGL Blending
When you’re rendering with WebGL, you usually render several things. For example, a city street, a ghost detective, and the rain. All these things overlap. But how do they overlap? How does the graphics card choose how much color to let through that ghost detective? He is a ghost, after all.
Blend Functions
The answer lies in the blendfunc or blend function. This is a mini-pipeline that is evaluated every time a pixel is drawn to the screen. (That is a simplification, but it’s essentially what’s happening.) The blendfunc compares two pieces of data:
- src or source pixel, the pixel that is trying to be drawn.
- dst or destination pixel, the pixel that was already there.
A conventional blendfunc looks like this:
1 2 3 4 |
// The first parameter is the src factor; the second is the dst factor // This is the default blendfunc used by many programs gl.blendfunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); |
This tells the video card how to blend the src onto the dst. The actual equation is a bit more complicated.
Here we must understand three things: channels, factors, and equations.
Channels
A channel is a single value that describes part of a color. The video card considers a color to be made of 4 channels: RGBA, or red green blue and alpha.
The video card thinks of colors as values between 0 and 1. This is mathematically convenient. Other programs may represent them as 0-255, or 0-100, but these are all abstractions for a pattern of bits.
The alpha channel is particularly important. It doesn’t mean anything intrinsic and it doesn’t ever reach the screen. However, you almost always use it to represent transparency.
Factors
The factors are specified in the blendfunc declaration. These are numbers that multiply the contents of a channel. Many of them are derived from the contents of src or dst channels. The permissible values are:
ZERO
ONE
SRC_COLOR
ONE_MINUS_SRC_COLOR
DST_COLOR
ONE_MINUS_DST_COLOR
SRC_ALPHA
ONE_MINUS_SRC_ALPHA
DST_ALPHA
ONE_MINUS_DST_ALPHA
SRC_ALPHA_SATURATE
CONSTANT_COLOR
ONE_MINUS_CONSTANT_COLOR
CONSTANT_ALPHA
ONE_MINUS_CONSTANT_ALPHA
Most of these take their value directly from the colors being blended. For example, ONE_MINUS_SRC_ALPHA
will return 0.7 if the src alpha channel has the value 0.3.
SRC_ALPHA_SATURATE has a special value: it is the minimum of either src alpha or (1 – dst alpha).
The CONSTANT types use special additional parameters. You set these using gl.blendColor(1.0, 1.0, 1.0, 1.0)
or similar values. These just define constant channels that are drawn from neither src nor dst.
Equations
The equation takes the channels and the factors and blends them together. There are three possible equations, depending on the mode set by blendEquation
:
FUNC_ADD
FUNC_SUBTRACT
FUNC_REVERSE_AND_SUBTRACT
These resolve as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
FUNC_ADD: resultChannel = (srcChannel * srcFactor) + (dstChannel * dstFactor) // FUNC_ADD is the default equation. FUNC_SUBTRACT: resultChannel = (srcChannel * srcFactor) - (dstChannel * dstFactor) FUNC_REVERSE_AND_SUBTRACT: resultChannel = (dstChannel * dstFactor) - (srcChannel * srcFactor) // There is no REVERSE_AND_ADD because addition works the same both ways, // while subtraction does not. // Other versions of GL have additional equations, but WebGL does not. |
So let’s take the above example of gl.blendfunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
, and see how this affects a color. Take the src RGBA value (1.0, 0.5, 0.0, 0.5), a kind of transparent orange, and the dst RGBA value (0.5, 0.5, 0.5, 0.2), a kind of faint gray. How do these numbers blend using FUNC_ADD
?
Each channel goes into the blend equation separately. Very simple operations are performed and combined together. In this case, SRC_ALPHA is 0.5, and ONE_MINUS_SRC_ALPHA is therefore also 0.5.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Red channel result = (1.0 * 0.5) + (0.5 * 0.5) = 0.75 // Green channel result = (0.5 * 0.5) + (0.5 * 0.5) = 0.5 // Blue channel result = (0.0 * 0.5) + (0.5 * 0.5) = 0.25 // Alpha channel result = (0.5 * 0.5) + (0.2 * 0.5) = 0.35 // Final color 0.75, 0.5, 0.25, 0.35 |
Problems with Blend Functions
You may be able to see an issue with the numbers above. Look at just the alpha channel, used to represent transparency. We took a background of 0.2, applied a foreground of 0.5, and the result was… 0.35. How can it be more transparent than the foreground we added?
The answer is simple: we’ve performed the wrong operation on the alpha channel.
But the alpha channel doesn’t render to screen, right? We only see RGB data, and the A is meaningless. Well, no. The alpha channel is still used to composite the data onto the screen. In the case of Kiwi.js, this actually caused a fairly severe bug at one point, where transparent objects would render the web page behind the game. This tended to look like unnatural whiteness, the color of a default page. A bad calculation on the alpha channel can cause major issues with the final output.
Separate Blend Functions
Fortunately, the OpenGL standard provides a solution: blendFuncSeparate
and blendEquationSeparate
. These allow you to specify different factors and equations for the alpha channel.
Using these, we can set the graphics card to blend as follows:
1 2 3 4 |
// Our default blend mode gl.blendEquationSeparate( gl.FUNC_ADD, gl.FUNC_ADD ); gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE ); |
The second half of each parameter list specifies the alpha behaviour.
If we run this blendfunc over the blend operation above, the RGB channels will resolve as normal (0.75, 0.5, 0.25). The alpha channel will resolve as follows:
1 2 3 4 |
result = (srcAlpha * ONE) + (dstAlpha * ONE) = (0.5 * 1) + (0.2 * 1) = 0.7 |
Now, this is much more logical. The opacity of the two colors is adding together. The separate blendfunc allows us to deliver blends that make perfect sense.
Blendfuncs in Kiwi.js
All blendfunc behaviour is managed behind the scenes in Kiwi.js. You can request special blendfuncs when necessary, and the rest of the time it behaves smooth and fast.
To understand blendfunc use, you must first understand the concept of Kiwi.js renderers.
Renderers
A renderer is not the same thing as a rendering engine. This can sometimes be confusing. The engine part is the whole pipeline, and we don’t care about that right now. The renderer itself is a vital part of that pipeline.
A renderer is a chunk of code that handles rendering a certain type of object. It manages shaders and blend modes. Renderers are tracked by the GLRenderManager
, the workhorse that takes care of the rendering pipeline.
Renderers are also tracked by game objects. In WebGL rendering mode, each game object has a glRenderer
property which points to a renderer.
Instanced Renderers
You can request an instance of a renderer from the manager:
1 2 |
game.renderer.requestRendererInstance( rendererID, params ) |
Where rendererID
is the name of a renderer in the Kiwi.Renderers
namespace.
An instanced renderer is a new copy of that renderer, unique to an object. We don’t actually use instances very often, because there’s a more efficient way to do more tasks.
Instanced renderers are useful for objects which require per-object shader information. For example, the Particle Effect Plugin for Kiwi.js encodes a lot of particles into the shaders on a single object. It is important to have its own renderer instance.
Shared Renderers
Most renderers are shared. This means they don’t need to set per-object information, and can be attached to multiple game objects. This is very useful for accelerating batch rendering. The default renderer is Kiwi.Renderers.TextureAtlasRenderer
, which you will probably use for 90% of game objects. When you create a normal game object, the engine requests a shared renderer. In the process it either creates the requested renderer, or passes a reference to it if it already exists. In this way, many objects may point at the same renderer.
You can get a shared renderer (agnostic of whether it must be created or simply referenced) as follows:
1 2 |
game.renderer.requestSharedRenderer( rendererID, params ) |
For example, if you wanted to access the default renderer, you would call requestSharedRenderer( "TextureAtlasRenderer" )
.
BlendMode Objects
Starting in Kiwi.js v1.1.0, the GLRenderManager
also tracks blend modes. It does so using a new GLBlendMode
object which is attached to every renderer as glRenderer.blendMode
.
The render manager uses this object to efficiently manage blend mode switching. We like to minimise direct and costly calls to the video card, so the manager only switches when the mode has actually changed between render batches.
The GLBlendMode
object tracks six key pieces of data, with default values denoted:
- srcRGB (SRC_ALPHA)
- dstRGB (ONE_MINUS_SRC_ALPHA)
- srcAlpha (ONE)
- dstAlpha (ONE)
- modeRGB (FUNC_ADD)
- modeAlpha (FUNC_ADD)
As you can see, this simply tracks the parameters necessary for setting separate blend equations and functions.
Configuring GLBlendMode
You can set up a GLBlendMode object either on creation, or after creation. Both methods use a config object. You can also call one of several preset configurations.
1 2 3 4 5 6 7 8 9 |
// Create a GLBlendMode object blendMode = new Kiwi.Renderers.GLBlendMode( glContext, params ); // Change a GLBlendMode object blendMode.readConfig( params ); // Set a predefined configuration blendMode.setMode( mode ); |
Config Objects
A config object is a simple Javascript object, defining one or more parameters.
If you specify a predefined mode, Kiwi.js will skip any other parameters and set that mode:
1 2 |
{ mode: "NORMAL" } |
If you specify the six core pieces of data, Kiwi.js will set them. You can use either a string to describe the title, or access the property directly from the GL drawing context. You can even mix and match. While we do not recommend it, the parser can even correct uncapitalised strings and accidental use of international spelling for “colour” (but not on the names of constant properties):
1 2 3 4 |
{ srcRGB: "SRC_ALPHA", dstRGB: "ONE", srcAlpha: gl.ONE, dstAlpha: gl.ONE, modeRGB: "FUNC_reverse_subtract", modeAlpha: gl.FUNC_ADD } |
If you specify an incomplete set of data, Kiwi.js will change only those parameters that you specify. It will leave the others as they are.
For a complete list of possible values, see above.
Preset Modes
We have included some useful modes that you can access quickly. To access these, use either the config object { mode: "MODE_NAME" }
, or the method blendMode.setMode( "MODE_NAME" )
.
Unfortunately, common blend modes as seen in programs like Photoshop are not supported by blend modes on the video card. These serve a different function, and are designed to work with shaders, which can extend their performance significantly.
Viable modes are:
- “NORMAL”
- “ADDITIVE” or “ADD”
- “SUBTRACTIVE” or “SUBTRACT”
- “ERASER” or “ERASE”
- “BLACKEN” or “BLACK”
“NORMAL” mode draws things as you’d expect.
“ADDITIVE” mode adds colors together. This is very useful for fiery effects, glows, and other objects that seem to emit light and eventually blow out to pure white. We strongly advise that you consider the exact pipeline of pixels when using this mode. For example, if you input pure red (RGB 1,0,0), it will never turn white, because it has no G or B values. This is what some effects technicians call “programmer colors”, because they’re simple to define but have bad behaviours on an artistic level. We recommend that you use colors with some value in all RGB channels, or in other terms, a saturation below 100%. This may result in slightly muddier colors, but they will blend in much nicer fashion.
The Particle Effect Plugin has an option to use “ADDITIVE” blending. Just one look at the example screenshot, and you can see how great it looks in action.
“SUBTRACTIVE” mode subtracts the src from the dst. This creates an eerie photo-negative effect. A similar warning to that of additive blending must be given: fully-saturated primary colors will not blow out into total darkness.
“ERASER” mode draws a hole in the game. Instead of seeing the object you are drawing, you will see the web page behind the game, as though you have clipped out part of the game canvas itself. This is a useful effect if you are integrating your game into a web layout, and want to create irregular edges. You can also use a transparent stage background, but “ERASER” mode gives you more control over layering. For example, you can eliminate sprites that move outside a certain shape on the screen.
“BLACKEN” mode draws textures as though they were black. Pretty simple. This could be useful for a highly-stylised game, or for creating shadows from color sprites.
There are 15 blend factor options for each of src and dst, and 3 possible blend equations. This makes 675 possible combinations. These five are just those that we found to be useful without further support from shaders. There are numerous other valid combinations.
For example, we found that { srcRGB: “DST_COLOR”, dstRGB: “ONE_MINUS_SRC_ALPHA”, srcAlpha: “ONE”, dstAlpha: “ONE”, modeRGB: “FUNC_ADD”, modeAlpha: “FUNC_ADD” } could create a Multiply effect, darkening those objects beneath it as in the Photoshop blend mode – but only with perfectly opaque or transparent objects. Partially translucent pixels did not blend as expected. This is the only reason there is no Multiply mode in Kiwi.js.
When designing your own shaders, you should always consider the use of blend modes. They may make your life considerably easier.
Contagion and Solution
Note that most sprites share the default renderer. If you try experimenting with these blend modes, odds are good that everything will vanish. This is because you can’t alter one thing without altering everything else.
In Kiwi.js v1.1.0, we’ve provided a new tool to make it easier to play with renderers. It is now possible to clone a shared renderer, creating a new copy which you may then request as a standard shared renderer:
1 2 3 4 5 6 |
GLRenderManager.addSharedRendererClone( rendererID, cloneID ) // For example, to create a clone of the default renderer: game.renderer.addSharedRendererClone( "TextureAtlasRenderer", "ClonedRenderer" ); state.object.glRenderer = game.renderer.requestSharedRenderer( "ClonedRenderer" ); |
Assign this clone to your experimental objects, and then tweak the new blend mode on the clone.
In Review
Blend modes are a useful tool for creating common visual effects. They are also useful as part of a more complex shader system. Today we’ve discussed the blend mode tools available in Kiwi.js, how they apply to Renderer objects, and how they create the colors you see on your screen. These can be tremendously powerful tools in your arsenal.
Benjamin D. Richards
Recent Comments