What's a good algorithm for converting true colors to simplified colors?

The Z-machine has two ways of specifying colors: simplified colors and true colors.

Simplified colors are four-bit: one bit each for red, green, and blue (so you get black, red, green, yellow, blue, magenta, cyan, white), plus three shades of gray (light, medium, and dark). True colors are fifteen-bit, with five bits each for red, green, and blue.

On Dialog’s Z-machine backend, you can specify a color name (color: red;) for a simplified color, or a hex code (color: #FF0000;) for a true color. This mostly works—but I’ve been informed there’s a way to detect which interpreters don’t support true colors. And in that case, it would be nice to fall back to an appropriate simplified color instead of just giving up on color entirely.

Which means I need an algorithm that can be implemented in Z-machine assembly to convert a 15-bit true color into a four-bit simplified color. My first thought is:

  • Extract the three five-bit channels from the fifteen-bit number (call them R, G, and B)
  • Calculate |R-G|, |G-B|, and |B-R|. If none of them is larger than some threshold (15?):
    • Take the maximum of R, G, and B.
    • If this maximum < 4, return black.
    • If this maximum < 10, return dark gray.
    • If this maximum < 22, return medium gray.
    • If this maximum < 28, return light gray.
    • Return white.
  • Let R2 be (R > G or R > B). Let G2 be (G > B or G > R). Let B2 be (B > R or B > G).
  • Return the simplified color (R2, G2, B2).

But, this feels overcomplicated, especially to implement in Z-machine assembly. Is there a simpler or more standard way to do this kind of conversion?

2 Likes

One way that I’ve done this in the past is to sort the available colors by 3D Euclidean distance (or squared distance) from the true color you’re trying to represent, and choose the best match. Each color component becomes a dimension of the space.

As for implementing it in Z-machine assembly… it probably wouldn’t be that hard, by assembly programming standards. With three static arrays containing 5-bit equivalents of the RGB values for the 13 (?) simplified colors, calculating the squared distance for each color would just be a few subtractions, multiplications, and additions.

2 Likes

Definitely possible, but that seems like overkill in this case. In RGB space, all the non-gray simplified colors are effectively the vertices of a unit cube, right? And the gray ones are just along the solid diagonal from black to white.

I would use Euclidean distance in oklab space, but you’re right that that feels like overkill.

1 Like

I could also implement the whole algorithm in C as part of the Dialog compiler, and just embed both the true color and the appropriate simplified color in the generated Z-code. But that’s its own logistical hassle: Dialog’s Z-machine assembler doesn’t support the eight-argument forms of the routine call opcodes, so I’m limited to three arguments per routine, and adding an extra one is a pain.

(Of course, at some point I should just add that capability to the assembler, or reserve a block of RAM to copy the arguments into before the call, then copy them into locals at the start of the routine.)

There is no easier way, and you definitely need to detect greyscale as distinct from other colors. With 8-bit RGB I found 80 to be a good threshold (which would be 10-12 for five bits).

Your greyscale thresholds (4/10/22/28) look a bit low to me, I think pure linear (6/12/20/26) would work better.

1 Like

I was going to suggest this, but I couldn’t remember if there was a way to generate colours programmatically (in which case you’d need code to do it at runtime anyway). But if there isn’t, it definitely seems like the better option in the long term.

1 Like

Currently, all details of style classes (so colors, fonts, dimensions…) have to be known at compile-time, because the Z-machine backend generates specific inline code to activate and deactivate the style. I’ve made slight gestures toward changing that (allowing the height of the status bar to be changed at runtime), but for the most part all of these things are known to the compiler.

Which is helpful here! But it also makes it nearly impossible to implement something like a dark mode within the Dialog code itself; that has to be handled by the interpreter.

1 Like

Note that the greys are only valid in Z6.

I doubt there are many Zcode interpreters these days which support basic colours but not the full colours. Just looking for the version 1.1 header would be enough IMO.

1 Like

Oh, the grays are only in Z6? Well, that makes things a lot easier! Just take the highest bit of each component, and that’s the result.

And yeah, checking the Standard revision is my new plan. I just want to be able to fall back to @set_color at runtime for terps like iOS Frotz, in case it’s semantically meaningful in a particular game.

This feels like it could create more problems than it solves, if the fallback colour scheme doesn’t work well for some reason. (E.g. it doesn’t seem totally inconceivable that someone could create a scheme which looks okay in true colour but where foreground and background map to the same colours in the reduced palette. Which would make the game unplayable on pre-1.1 interpreters without the author even necessarily realising it.)

1 Like

I can suggest a good look at the dmagnetic’s sources ? its the top of colour (and graphic) handling in the 'terp, so I guess that there’s a good terminal color converter…

my 2 pence, and

Best regards from Italy,
dott. Piergiorgio.

If you have Cyan, Magenta, Yellow and Black in your disposal, you can try creating a dithering effect. This way Printers work. They have just these colors, and by putting them in the short distance in between, they create all the colors. Dithering may be simple and create attractive effect. As you also have Red, Green and Blue and three Greys shades it may be even better.

If you wish to convert just colors, I think that picking 2 or 3 most significant bit is good solution. The more bits are needed to detect shades. Convert to Red (respectively Green, Blue, Cyan, Magenta, Yellow) if it’s #400, #040, #004, #440, #404, #044 (with intermediate colors rounded to nearest). Convert to Grey shades if it’s #000, #111, #222, #333, #444.

NOTE: I didn’t notice that you need to implement algorithm in Z-machine. Of course I don’t recommend dithering in such case. It’s useful if we convert image.

I’ve been using them in version 8 and 5… I mean, they work fine, but that’s odd!

Also possible! I suppose I could start by just disabling the @set_true_color opcodes on non-1.1 interpreters. Games that convey something important with color already need to provide an alternative, or accept that they won’t work on certain platforms.

Not all interpreters will support them. Hopefully they won’t crash if they don’t..

1 Like