ID:2209545
 
Color matrices have been with us for a while now, but it seems there are a lot of games not using them yet. Many games are still adding to icons to create colored icons, instead of using much simpler methods. This post will show how atom.color can be used to do a lot of work that once could only be done with icon math.

Of course it almost goes without saying that using atom.color is better than doing icon math if you can help it. Icon math means you're creating a new icon which ends up in a cache. It doesn't allow for easy changes or animation, and during character creation you can end up with a bunch of files in the cache you don't really want to keep. It's more work for the server and the client both, even though a lot of icon work gets shunted off onto the client these days. So let's look over the different coloring options.

In the following examples, I'm going to cover them like we're building an overlay. So I'll use a base object for that, defined like this:

obj/overlaybase
layer = FLOAT_LAYER
plane = FLOAT_PLANE

Now that we're all set with the preliminaries, let's get to it.

How color matrices work

What's in a color matrix? Well, let's take a look at how they're laid out:

list(rr,rg,rb,ra, gr,gg,gb,ga, br,bg,bb,ba, ar,ag,ab,aa, cr,cg,cb,ca)

Each of those two-letter vars goes from 0 to 1 (normally) and can be thought of like so: The first letter represents what you start with, and the second is what it becomes. So for instance the amount of red in an icon is multiplied by rg and that gets added to the new total for green. To put it another way, with a formula:

R' = R*rr + G*gr + B*br + A*ar + 255*cr
G' = R*rg + G*gg + B*bg + A*ag + 255*cg
B' = R*rb + G*gb + B*bb + A*ab + 255*cb
A' = R*ra + G*ga + B*ba + A*aa + 255*ca

In math parlance, the ' stands for "prime" and R' (R prime) means "new R". The meanings of r, g, b, and a are pretty clear; the c stands for "constant", because those values always get added to the result and don't depend on anything that was in the original icon.

An identity color matrix--one that does nothing--would look like this:

list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0)

A negative image, with all the colors inverted, would look like this:

list(-1,0,0,0, 0,-1,0,0, 0,0,-1,0, 0,0,0,1, 1,1,1,0)

By the way, if the order of these values looks familiar, that's because MapColors() has been using the exact same format for years. There's also a shorter format where you can leave out the alpha column and alpha row; all of the a* and *a values are 0, and aa is 1, which means alpha is left alone.

You can think of each row in the matrix as "what X becomes". The red row is what the red will become; the green row is what green becomes; and so on, plus the constant row.

Multiplication

Multiplicative coloring produces a very good result. When you use it, your base icon is usually lighter shades of gray or white, and it gets multiplied by your target color. Well heck, atom.color makes multiplying easy. So this is the easiest coloration method of all.

// old way
var/obj/overlaybase/O = new
O.icon = 'hair.dmi' * haircolor

// new way
var/obj/overlaybase/O = new
O.icon = 'hair.dmi'
O.color = haircolor

The downside to multiplicative color blending is that you can only darken the target color; you can't lighten it. In the past, I always used to do a multiplication and then add in a second icon that had all the highlights; but since the advent of MapColors, even that was no longer necessary. I'll get back to that later.

When atom.color is a simple color like this, it's equivalent to this kind of matrix:

list(R,0,0,0, 0,G,0,0, 0,0,B,0, 0,0,0,1, 0,0,0,0)

Adding

Color adding has been around since the early days of BYOND. When I first started using BYOND, about the only changes you could make to an icon were multiplying by a number to darken/lighten the whole thing, or adding/subtracting a color. Multiplying by a color wasn't even possible then, unless you came up with a hacky workaround (which I did). Maybe that's why so many games use additive colors for things like hair, clothes, etc.

Using additive colors means your base icon is probably black or shades of dark gray. Basically everything ends up being the target color or lighter, but never darker. The hue of your color can also get distorted if one of the RGB values is clipped at 255 after the addition. I've personally never been a fan of this method, but anyway color matrices make it available to you.

// old way
var/obj/overlaybase/O = new
O.icon = 'hair.dmi' + haircolor

// new way
var/obj/overlaybase/O = new
O.icon = 'hair.dmi'
O.color = list(null, null, null, null, haircolor)

The color matrix is being created in "short form", where each item in the list represents a row in the matrix. The top row is what red will become, and the null value means we'll leave it alone; the next row is green; the next blue; next alpha; and then the final row is a constant color that will be added to the result. The first four rows being null mean the original base color is left intact, and the last row just adds our target color.

Shadows and light

Obviously pure multiplication and pure addition produce lackluster results. What if you wanted something that could handle subtle shading as well as give a sense of shine by lightening the target color?

So let's say you have a base icon for armor. You want parts to be shiny and have a little bit of lightness, and some parts to be darkened, and maybe you want some desaturated bits where you have a muted version of your target color. Draw your base icon, and pretend the armor is red. All the colors you use should be shades of red, playing with saturation and lightness but not the hue. In RGB terms, R >= G and G = B. That lets you have red, white, black, dark red, pale red, pink, reddish gray, pure gray, etc.

Now that you have the base icon, say you want a matrix that colors it with whatever color you want. The math looks like this:

color' = color * (base.r - base.g) + white * base.g

We can build a matrix for that. I'm going to use an /image to cheat here and take advantage of some of BYOND's internal processing, but a /mutable_appearance (new in 511) is probably a better way to go. I actually use code like this in a project, except I also use a cache with it so I can quickly get a copy of a matrix that was calculated earlier.

proc/RedToColorMatrix(c)
var/image/I = new
var/list/M
// create the matrix via short form
I.color = list(color, "#fff0", "#0000", null, null)
// get the long-form copy
M = I.color
// adjust the green row
M[5] -= M[1]
M[6] -= M[2]
M[7] -= M[3]
return M

...
var/obj/overlaybase/O = new
O.icon = 'armor.dmi'
O.color = RedToColorMatrix(armorcolor)

Now for an explanation as to how that works. Our target color matrix should look like this:

list(R,G,B,0, 1-R,1-G,1-B,0, 0,0,0,0, 0,0,0,1, 0,0,0,0)

Without parsing the color, you don't really have an easy way to grab the R,G,B values (which in this case, range from 0 to 1 rather than 0 to 255). That's why I created the matrix in two steps. The first step creates a matrix that looks like this in long form:

list(R,G,B,0, 1,1,1,0, 0,0,0,0, 0,0,0,1, 0,0,0,0)

Internally, when you use the short form to create a color matrix, BYOND will parse any colors for you so that it can create the long-form version it actually uses. And like I mentioned above, the null entries parse as "use the default", so that's why the alpha row ends up with 0,0,0,1 and the constant row is 0,0,0,0. Once I set I.color, the matrix is created, and I can read it back out into the M var.

At that point, the three subtraction lines make perfect sense. The subtraction gives us our target matrix.

Inverse multiplication

In Scream of the Stickster Volume II, I used to do something very different with icon math: I took a base wall icon that was black and white, and blended it so that the darker parts of the base icon were my target color, and the lighter parts were white. To avoid distorting the hue, it looked like this:

var/icon/I = new('wall.dmi')
var/list/RBG = ParseRBG(wallcolor) // not a real routine, just an example
I.Blend(rgb(255-RGB[1],255-RGB[2],255-RGB[3]), ICON_MULTIPLY)
I.Add(wallcolor, ICON_ADD)

Obviously matrices are a much better fit for that. The target matrix would look like this:

list(1-R,0,0,0, 0,1-G,0,0, 0,0,1-B,0, 0,0,0,1, R,G,B,0)

To create a matrix like that, let's use the two-step method again:

var/list/_whitematrix = new   // this is a cache
// black becomes color, white stays white
proc/WhiteMatrix(c)
. = _whitematrix[c]
if(!. && c)
var/image/I = new
I.color = list("#ff0", null, null, null, c) // use yellow to force a matrix
var/list/L = I.color
L[2] = 0
// rgb * (1-color) + color
L[ 1] = 1-L[17]
L[ 6] = 1-L[18]
L[11] = 1-L[19]
. = L
_whitematrix[c] = .

A little bit of explanation is in order here. In the first step, I put yellow into the red row instead of leaving it alone. There's a reason for that. If the color c were black, then I.color would end up being set to list(null,null,null,null,"#000") which ends up being the exact same thing as setting I.color to the default. The default value is not a matrix but a regular color (solid white), and the rest of the code needs a matrix to work with. So putting yellow in the first row forces I.color to be a matrix no matter what c is. This is why L[2] is set to 0 below; because it wasn't really needed.

This is just one way to do a color-to-white fade. It's basically identical in result to the old multiplication I used to do. However, I found as I went along that I wanted to also be able to show grays and blacks in the icon, and a simple color-to-white fade wasn't good enough for that. So I decided to retool my concept to this:

list(1,1,1,0, 0,0,0,0, -R,-G,-B,0, 0,0,0,1, R,G,B,0)

With that matrix, white stays white; black becomes the target color; blue becomes black. Just like the armor example above with light and shadow, this gave me the ability to use color ranges that included lighter versions of the color as well as darker versions. The math is a little bit different and there's actually a little more freedom here to play with hue variations, but basically the end result is that if I wanted to show a crack in a wall, I'd just draw a little blue onto the icon and the blue would come out as black.

Here's how that matrix is created.

var/list/_whitematrix2 = new   // this is a cache
// black becomes color, white stays white, blue becomes black
proc/WhiteMatrix2(c)
. = _whitematrix2[c]
if(!.)
var/image/I = new
I.color = list("#fff","#000",null,null,c)
var/list/L = I.color
L[ 9] = -L[17]
L[10] = -L[18]
L[11] = -L[19]
. = L
_whitematrix2[c] = .

Results

When I stopped using icon math in Scream of the Stickster Volume II, in favor of using color matrices instead, the performance gains were noticeable. What's more, that new matrix I came up with improved my ability to create special effects, like putting cracks in damaged walls and showing walls exploding with dark lines around the pieces. Another huge benefit of the switch to matrices was that I could change wall colors gradually via animate().

These are just a few samples of what color matrices can do to help you replace icon math. With these techniques, your players can have customization without increasing the size of your dynamic .rsc file, keeping your icons simple and making your character design more responsive.
A lot of people are visual beings, if you showed before and afters in pictures embedded in the post, you would increase understanding massively.
Ah, color matrix. My favorite use is in Goonlights
Message received, Lummy. Will apply this in the near future. o/
In response to MrStonedOne
MrStonedOne wrote:
A lot of people are visual beings, if you showed before and afters in pictures embedded in the post, you would increase understanding massively.

Agreed 100%