Light and Color

hue variationsA while back, in the editor basics video, Juha talked at some length about how we build levels by combining masks, patterns and color. We refer to these objects as HSVE objects after the color space, and today we are going to go over some of the reasoning and technology behind them.

Like the majority of games, we store our art assets as RGB and RGBA raster images. Every HSVE object has two textures – one pattern texture that is usually tiling, colored image with some sort of watercolor pattern and a mask texture that defines the shape of the object. For the mask texture, the alpha channel defines the opacity of the pattern texture and the red channel defines the opacity of the line – in actual fact, we remove the unused G and B channels from the final images but while working with levels we keep the masks as normal PNG files.

Texture combineThe mask is positioned in object space like any other sprite, but the pattern is in world space by default. What this means is that when we move the HSVE object around, the pattern remains in place – the big benefit of this is in how we can piece together several objects without having to worry about how the pattern will match up.

worldspace coordinatesWhile the RGB colorspace is easy to grasp and very close to how monitors actually display the art, it can be tricky to manipulate. Making small changes is usually easy but a good editor needs to encourage crazy experimentation and big, sweeping changes. The HSV colorspace corresponds more closely to how we think about color (what color is it? how strong is the color? how bright is it?) so the editor supports manipulating the pattern and line colors individually in HSV space.

tweaking parameters… The final parameter, confusingly named Equalization, is a value we can manipulate to increase or decrease the contrast between the pattern and the line.

We do the HSVE conversion in a GL_ARB_fragment_program, provided below. It is a lot more complex than most of our other programs and not really suited for low-level shading languages. It is also not very optimized. Since the HSVE objects are mostly static we can preprocess our levels and they are rarely rendered in this manner when we are not building levels, so performance is not a big issue.


# FP using the mask texture as alpha and a HSVE-modified pattern texture
OUTPUT ocol = result.color;

ATTRIB atex = fragment.texcoord[0];
ATTRIB amask = fragment.texcoord[1];

PARAM pparam = program.env[0];
PARAM pattern_offsscale = program.env[1];
PARAM pattern_ori = program.env[2];
PARAM pattern_hsve = program.env[3];
PARAM pattern_linecolor = program.env[4];

PARAM pone = {1.0, 1.0, 1.0, 1.0};
PARAM pconst = {-1.0, 0.0, 3.0, 5.0};
PARAM pconst2 = {2.0, 4.0, 6.0, 0.16666666666667};
PARAM pconst3 = {0.5, 0.25, 0.0, 0.000001};

TEMP clr;
TEMP mask,invmask,pattern;
TEMP r0,r1;
TEMP rIsMin,gIsMin,bIsMin;

# Initialization
TEX clr, atex, texture[0], 2D;

# Sample pattern texture
MUL r0, amask, pattern_offsscale.zwzw;
MUL r1, r0, pattern_ori.xxxx;
MAD r1, r0.yxyx, pattern_ori.wywy, r1;
ADD r1, r1, pattern_offsscale.xyxy;
TEX pattern, r1, texture[2], 2D;

# Apply E value
SUB r0, pattern, pattern_linecolor;
MAD_SAT pattern, r0, pattern_hsve.wwww, pattern_linecolor;

# convert to HSV
MIN RGBMinMax.x, pattern.rrrr, pattern.gggg;
MIN RGBMinMax.x, pattern.bbbb, RGBMinMax.xxxx;
MAX RGBMinMax.y, pattern.rrrr, pattern.gggg;
MAX RGBMinMax.y, pattern.bbbb, RGBMinMax.yyyy;

# mask is set if min == max
SGE mask, RGBMinMax.xxxx, RGBMinMax.yyyy;
SGE rIsMin, RGBMinMax.xxxx, pattern.rrrr;
SGE gIsMin, RGBMinMax.xxxx, pattern.gggg;
SGE bIsMin, RGBMinMax.xxxx, pattern.bbbb;

# make sure we only have one least
SUB r1, pone, rIsMin;
MIN gIsMin, r1, gIsMin;
SUB r0, pone, gIsMin;
MIN r1, r1, r0;
MIN bIsMin, r1, bIsMin;

# create all possible values
SUB r0.x, pattern.gggg, pattern.bbbb;
SUB r0.y, pattern.bbbb, pattern.rrrr;
SUB r0.z, pattern.rrrr, pattern.gggg;

# create final values
MUL r1.x, r0.xxxx, rIsMin;
MAD r1.x, r0.yyyy, gIsMin, r1;
MAD r1.x, r0.zzzz, bIsMin, r1;
MUL r1.y, pconst.zzzz, rIsMin;
MAD r1.y, pconst.wwww, gIsMin, r1;
MAD r1.y, pone, bIsMin, r1;

# create the actual output, use some threshold values to avoid NaN poisoning
SUB r0.x, RGBMinMax.y, RGBMinMax.x;
MAX r0.x, r0.xxxx, pconst3.wwww;
RCP r0.y, r0.x;
MAX r0.z, RGBMinMax.yyyy,pconst3.wwww;
RCP r0.z, r0.z;
MUL r0.w, r1.xxxx, r0.yyyy;

SUB pattern.r, r1.yyyy, r0.wwww;
MUL pattern.g, r0.xxxx, r0.zzzz;
MOV pattern.b, RGBMinMax.yyyy;

# apply transformation

ADD_SAT pattern.g, pattern.gggg, pattern_hsve.gggg;
ADD_SAT pattern.b, pattern.bbbb, pattern_hsve.bbbb;

# Hue is special since it is in the interval 0..6 ... and since it loops
MUL r0.x, pattern.rrrr, pconst2.wwww;
ADD r0.x, r0.xxxx, pattern_hsve.rrrr;
FRC r0.x, r0.xxxx;
MUL pattern.r, r0.xxxx, pconst2.zzzz;

# convert to RGB

# set mask if undefined
FLR r0.x, pattern.rrrr;
FRC r0.y, pattern.rrrr;

# if hue was even, invert fraction
MUL r1, r0.xxxx, pconst3.xxxx;
SUB r1, r1, pconst3.yyyy;
FRC r1, r1;
SUB r1, r1, pconst3.xxxx;
SUB r0.z, pone, r0.yyyy;
CMP r0.y, r1, r0.yyyy, r0.zzzz;

SUB r0.z, pone, pattern.gggg;
MUL r0.z, r0.z, pattern.bbbb;
MUL r0.w, pattern.gggg, r0.yyyy;
SUB r0.w, pone, r0.wwww;
MUL r0.w, pattern.bbbb,r0.wwww;

# determine setup (6..0)
SGE rIsMin.r, r0.xxxx, pconst2.zzzz;
SGE rIsMin.g, r0.xxxx, pconst.wwww;
SGE rIsMin.b, r0.xxxx, pconst2.yyyy;
SGE gIsMin.r, r0.xxxx, pconst.zzzz;
SGE gIsMin.g, r0.xxxx, pconst2.xxxx;
SGE gIsMin.b, r0.xxxx, pone;

# make sure only one is set
SUB r1, pone, rIsMin.rrrr;
MIN rIsMin.g, r1, rIsMin.gggg;
SUB r1, r1, rIsMin.gggg;
MIN rIsMin.b, r1, rIsMin.bbbb;
SUB r1, r1, rIsMin.bbbb;
MIN gIsMin.r, r1, gIsMin.rrrr;
SUB r1, r1, gIsMin.rrrr;
MIN gIsMin.g, r1, gIsMin.gggg;
SUB r1, r1, gIsMin.gggg;
MIN gIsMin.b, r1, gIsMin.bbbb;
SUB r1, r1, gIsMin.bbbb;
MAX rIsMin.r, rIsMin.rrrr, r1;

# Set r0 for quick swizzling and mul in results
MOV r0.y, pattern.zzzz;
MUL pattern, rIsMin.rrrr, r0.ywzx;
MAD pattern, rIsMin.gggg, r0.yzwx, pattern;
MAD pattern, rIsMin.bbbb, r0.wzyx, pattern;
MAD pattern, gIsMin.rrrr, r0.zwyx, pattern;
MAD pattern, gIsMin.gggg, r0.zywx, pattern;
MAD pattern, gIsMin.bbbb, r0.wyzx, pattern;

# Don't forget the UNDEFINED mask! (r0.y already holds v)
LRP pattern, mask, r0.yyyy, pattern;

# Finalize
# At this point, the HSVE-manipulated color is in pattern
# The rest of the code considers backworld/frontworld mask
# and the red/alpha HSVE mask sample

# sort out mask used (if any)
TEX mask, amask, texture[1], 2D;
SUB invmask, pone, mask;
LRP mask.g, pparam.gggg, mask, invmask;
LRP mask.g, pparam.aaaa, pone, mask.gggg;
MUL clr.a, clr, mask.gggg;

# evaluate lines, finalize
LRP clr.rgb, clr.rrrr, pattern_linecolor, pattern;

MUL ocol, clr, fragment.color;


3 Responses

  1. Cool says:


    I wanted to say you should try to get more money so you can make your games.

    • Anders says:

      While we did do something similar a while back with Indiegogo, Kickstarter is only available to permanent US residents which includes neither Juha or myself.

      Thanks for the concern, though. And don’t worry, we have the means to finish this on our own – it is just going to take a little longer =)

  2. […] basically fine-tune the color of the avatar. Since we already had hue manipulation as part of our HSVE graphics objects, this did not require a lot of new […]

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.