Monday, June 1, 2009

Screen effects

While animating a sprite is surely the best way to get a visual effect, there are other methods to change the look of your world which don't require nearly as much artistic skill.

Metaplace supplies a growing number of these: tinting has been available for over a year, allowing each sprite to change its colour to great effect; decals provide a way to have a ground-based effect from a sprite, such as a shadow; the general UI system allows a mask overtop of any region of the screen, which with creative use of alpha, can generate some interesting effects; the sprite light system provides a similar effect as the UI system, but provides a simpler method of attaching the effect to an object, and also supports rotating along with the object; and the effects system provides a handful of other interesting changes. And last week, the screen effects functions were added.


This new set of visual effects has three functions: UiScreenEffectColor(), UiScreenEffectMatrix() and UiScreenEffectClear(). I'll abbreviate these as USEColor/USEMatrix/USEClear to save wear and tear on my keyboard.

These effects, as you can guess, cover the whole screen, regardless of size, unlike any of the other effects mentioned above, which were specific to a sprite or a region of the screen. The USEClear function, obviously, removes the effect of an existing screen effect; it should be noted that, because it does not take any sort of "handle" or "reference", that this means you can only have one such effect in place at any one time. This is different than most of the other effect methods mentioned above, and will be discussed later.

I should mention that, until the introduction of these various effects systems, I knew NOTHING about colour theory. Playing with these various systems has made me learn a little bit, so I should be able to convey some ideas here, but for all of you that know more than I do, be sure to chime in!

Of the other two functions, the USEColor function is the simpler of the two. The definition of the function is

UiScreenEffectColor(user, time, r, g, b, method)

The "r", "g" and "b" values are, essentially, scales that are going to be applied to the red, green and blue portions of every pixel on the user's screen. The scales, perhaps a bit confusingly, are in the range of 0 to 255; while these values make sense if we're setting absolute colours, they don't make as much sense as scaling values. However, as long as we think of zero as "none" and 255 as "full", we can think of the values as everything in between, such as 127 as "half". In fact, you can just divide these values by 255 to get a percentage, if that helps (and in fact this is exactly how it's explained in the wiki, and how it's done in the USEMatrix function). The method parameter currently only accepts "multiply". You can get an "additive" effect with some of the other effect systems, but not as a full-screen effect with USEColor.

So what do these scales allow us to do? Well, if we set them all to 0, we're basically saying "set red to 0% of its original value, green to 0% of its original value, and blue to 0% of its original value" which is going to turn our RGB to 0, and give us a completely black screen:

UiScreenEffectColor(self,1000,0,0,0,"multiply")

You can try this out by going to my crwth_ui test world, pressing the backtick (`) key (or, if on a non-US keyboard, try the single-quote key (')) to get a command console, and typing

usec 1000 0 0 0

You should see the scene fade to black in one second (the 1000 value is for time-to-fade). Note the little sprite light beaming from your head -- I'll talk about that in another post. You can return it to normal by typing

useclear

You can get a nice dusk effect by using values of 127:

UiScreenEffectColor(self,1000,127,127,127,"multiply") (or "usec 1000 127 127 127")

Also, I kinda lied above, when I said that the scale range was 0-255; you can actually go higher than that, to scale the original values even higher than they were (which just makes the 0-255 range even more confusing):

UiScreenEffectColor(self,1000,500,500,500,"multiply") (or "usec 1000 500 500 500")

Other interesting effects include removing all of the red from your view:

UiScreenEffectColor(self,1000,0,255,255,"multiply") (or "usec 1000 0 255 255")

or getting the red to stand out a little more than usual:

UiScreenEffectColor(self,1000,400,200,200,"multiply") (or "usec 1000 400 200 200")

or you can just see how much blue there was in your world:

UiScreenEffectColor(self,1000,0,0,255,"multiply") (or "usec 1000 0 0 255")

Remember that between each of these, you don't need to USEClear the effect, because you can only have one going at a time. You might want to clear it if you're trying this out in crwth_ui, though, to "reset" the view to the original, to get a better impression on the change each is making.

These effects aren't too bad; they allow for simple brightening and darkening of our world, and also allow for a little bit of colour shifting. But the real power comes from the last function in the set.


USEMatrix is defined as the following:

UiScreenEffectMatrix(user, time, matrix)

where the matrix is a table of 20 values. These 20 values are used in this way:

redResult=(m[0] * srcR) + (m[1] * srcG) + (m[2] * srcB) + (m[3] * srcA) + m[4]
greenResult=(m[5] * srcR) + (m[6] * srcG) + (m[7] * srcB) + (m[8] * srcA) + m[9]
blueResult=(m[10] * srcR) + (m[11] * srcG) + (m[12] * srcB) + (m[13] * srcA) + m[14]
alphaResult=(m[15] * srcR) + (m[16] * srcG) + (m[17] * srcB) + (m[18] * srcA) + m[19]

This sure looks like a mess, with a lot of multiplication, a lot of addition, values from blue being added to the green, and alpha to the red... what's going on?

If you learned (and remember) any matrix mathematics in high school or post-secondary school, then this might look familiar. If not, don't worry about it: you don't need to understand the math behind HOW this is applied, just what each number is going to contribute. For those that do remember matrices, and are curious, it might help if I write it out this way (I have no doubt the proportional font is going to make a mess of this):


| m0 m5 m10 m15 |
| m1 m6 m11 m16 |
| r g b a 1 | * | m2 m7 m12 m17 | = | r' g' b' a' |
| m3 m8 m13 m18 |
| m4 m9 m14 m19 |


The row on the left is a 1x5 matrix of our pixel's value (the "1" at the bottom is there for a reason, I promise), and the block in the middle is our matrix of values. The final row will be the new value of our pixel.

Why is this so complicated? First of all, using a matrix means that we can allow any of our original values -- our original red, green, blue and even alpha values -- to have an effect on the result. With USEColor, all we could do was modify each of the colours only with respect to itself (and not alpha at all), which meant that if we increased our red's scale, our image was going to get brighter overall; we could try to guess how much to reduce our green and blue by to keep the total brightness the same, but we'd be guessing, because each pixel is different. Using this matrix means that, if we choose, our new red value can be affected by how much green and blue we also have in this pixel, not only the red.

The second reason to use a matrix is because it can make it easier to apply multiple effects. The screen effect system only allows one effect in place, so if we want to increase the red but decrease the green, we'd have to figure out a way to "add" these two effects together into a single call. This is easy enough using USEColor, because each of the scaling values is separate from the other. But when we extend our abilities with USEMatrix, it's not so simple. Luckily, matrix multiplication of two different effects can let us do them both at once (with a challenge or two, if you know your matrix math). Even if we could have more than one effect going at once, we'd likely want to multiply our multiple effects into a single matrix, because those formulas above, with sixteen multiplies and sixteen additions, are going to be done to every single pixel on your screen; that can approach two million pixels at full screen, on a nice monitor, and if every one of those pixels has to go through all those multiplications and additions every time the screen is redrawn (20, 30, 40 times a second?), that's a lot of processing no matter how fast your computer is, how efficient the client's renderer is, or if your video card is doing the work.

The values in the matrix are numbers more in line with the idea of scaling, compared to those used in USEColor, but because of the additive nature of the formulas above, it's best to think of them more as a "ratio" of contribution to the final value. What does this mean? Well, look above at the formulas, and find the mention of m[0]:

redResult=(m[0] * srcR) + (m[1] * srcG) + (m[2] * srcB) + (m[3] * srcA) + m[4]

This shows that the "redResult", the red portion of each pixel, is affected by m[0] through m[4], and each affects a different "source colour"; m[0] is multiplied by the pixel's red value, so m[0] can be thought of as "the amount that the original red affects the resultant red", and m[2] as "the amount that the original blue affects the resultant red."

Specifically, look at m[0], m[6] and m[12]. These three are the amount that red, green and blue affect red, green and blue in the result. Sound familiar? This are the positions in the matrix that simulate the effect of the r,g,b values in USEColor. In fact, if we scale the USEColor values down from the 0-255 range to a 0.0-1.0 range (by dividing them by 255, as mentioned before), and turn all of the other matrix values to zero (except for the alpha one, which we'll discuss shortly), we discover that we can make a USEMatrix do the same thing as a USEColor, such that

UiScreenEffectColor(user, time, r, g, b, method)

is equivalent to

UiScreenEffectMatrix(user, time, {
r/255 , 0 , 0 , 0 , 0 ,
0 , g/255 , 0 , 0 , 0 ,
0 , 0 , b/255 , 0 , 0 ,
0 , 0 , 0 , 1 , 0
}

and that

UiScreenEffectColor(self,1000,127,127,127,"multiply")

is the same as

UiScreenEffectMatrix(self,1000,{0.5,0,0,0,0,0,0.5,0,0,0,0,0,0.5,0,0,0,0,0,1,0})

or

UiScreenEffectMatrix(self,1000,{
0.5, 0, 0, 0, 0,
0, 0.5, 0, 0, 0,
0, 0, 0.5, 0, 0,
0, 0, 0, 1, 0})

for legibility. (Yes, 127/255 isn't the same as 0.5 ...)

You can try this in the crwth_ui world with the following:

usem 1000 0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0

or

usem 1000 0.5,0,0,0,0,0,0.5,0,0,0,0,0,0.5,0,0,0,0,0,1,0

or

usem 1000 {0.5,0,0,0,0,0,0.5,0,0,0,0,0,0.5,0,0,0,0,0,1,0}

Each of our USEColor examples can be turned into a USEMatrix call in the same way, by only considering m[0], m[6] and m[12], and leaving the other colours' contributions to the result as zero. This was what we noted above as a limitation of USEColor.

So why would we want the other colours to contribute? Why would I want the amount of green in my original pixel to affect how much red I have in my new one?

One reason was hinted at above, with USEColor: the fact that the total brightness of the image is going to be changed, if we just start increasing or decreasing each of the colours. However, if we can "know" the brightness of each of the pixels, somehow represented in our matrix, then we can maintain that brightness even while changing the colours! But how do we do that?

Leave it to the colour scientists to tell us. That's right, the colour scientists. They've gone and figured out the amount that each of the red, green and blue colours contribute to the overall brighness of a pixel. If it was completely even, then we could determine the brightness by just adding up the values of red, green and blue. If we had those, we could then turn every one of the pixels to a grey that matches that brightness. Because our total "brightness" can be anywhere from 0 to 765 (red/green/blue all zero, to red/green/blue all 255), we would divide the total by 3 to get the range back to our 0-255 range. We can do all of this with our matrix -- remember that it does multiplication and addition for us, so if we just do the dividing-by-three to all values first (by multiplying by 1/3, or 0.33), and then add them together, we can get a greyscale image:

UiScreenEffectMatrix(self,1000, {0.333,0.333,0.333,0,0,0.333,0.333,0.333,0,0,0.333,0.333,0.333,0,0,0,0,0,1,0} ) (or " usem 1000 {0.333,0.333,0.333,0,0,0.333,0.333,0.333,0,0,0.333,0.333,0.333,0,0,0,0,0,1,0} ")

Not bad, right? But the colour scientists claim that red, green and blue don't actually contribute evenly to the brightness; try this:

UiScreenEffectMatrix(self,1000, {0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0,0,0,1,0} ) (or " usem 1000 {0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0,0,0,1,0} ")

These values -- 0.3086, 0.6094, 0.0820 -- are apparently the ratio of brightness for each of red, green and blue; you'll notice that they add up nicely to 1.0000 , to get the total brightness. (I got these numbers here, where they mention another set of numbers used for NTSC, but that depends on the gamma of the image, which is beyond the scope of this post (and beyond my knowledge)).

Having this matrix is useful, because it allows us to get an screen that has the same brightness as before, but with no colour, leaving it open to colouring in any way we choose. Of course, to colour it afterwards isn't possible, since we can't apply a second filter, so we'd need to combine the two effects together, which would require some matrix math. I'm currently finishing up a Lua matrix math module, which I'll post and allow people to blend various screen effects together.


So what about that alpha stuff? This is the transparency of the pixels, going from zero for fully-transparent, to 255 for fully opaque. For instance, you could use

UiScreenEffectMatrix(self,1000, {1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0.5,0} ) (or " usem 1000 {1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0.5,0} ")

to turn the whole world half as transparent as it used to be; in the case of the crwth_ui world, you should now see the background "metaplace" logo through the cacti and such. The matrix we used said to leave red, green and blue alone, but to change the alpha of every pixel to half of its former value. If you combine that with the grey-scaling matrix (easy to do by-hand, without complex matrix math),

UiScreenEffectMatrix(self,1000, {0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0,0,0,0.5,0} ) (or " usem 1000 {0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0.3086,0.6094,0.0820,0,0, 0,0,0,0.5,0} ")

you get what might be a ghost state of the world, where the background coming fully through might be a useful effect for a world builder.

The original matrix formulas above had things like m[3], which set "how much the pixel's new red is affected by the original pixel's alpha". I'm really not sure if there's a good use case for this, but perhaps the more creative types can prove me wrong; the only reason that that exists is to "fill in" the matrix.

And what about the last set of values, m[4],m[9],m[14],m[19]? These don't take any of the previous values from the pixel, but just simply add in a value. Again, I can't really think of a good example for this one, but I welcome anyone who can!


I hope to add something to the crwth_effects world to demo these new features, in a nicer interface, but for now, I hope that anyone interested in it can try out the crwth_ui world and the "usec" and "usem" commands for now. As for my plans for screen effects, I want to make a module that provides sun(s) and moon(s), on user-settable revolutions; they could each cast their own colours, would vary their light based on angle, and could introduce colours (such as a red sunrise or sunset) based on angle as well. Two moons would cast more than one, and the sun (or two!) might drown out any effect the moons have. All of this will have to be compacted into a single matrix, of course, which is why I'm writing the matrix library. To see something along these lines, check out Brooke's (chooseareality) world model_testing world, where she has a simple day and night cycle with the USEColor function (and her really cool fog!)

1 comment: