Engine question: Transparency % to 8-bit alpha

Started by Snarky, Fri 17/11/2017 23:05:21

Previous topic - Next topic

Snarky

I have a question for the engine experts. In 32-bit, AGS allows you to set the transparency of various things and drawing operations as a percentage (0-100). However, 32-bit color actually uses an 8-bit alpha channel internally (0-255). Do you know how this is mapped?

For example, if I set the transparency of a GUI to 3%, am I really setting it to the closest alpha value, 8? (8/255 = 0.03137..., or about 3.14%). Is this calculated as (in AGS syntax) int alpha = FloatToInt( IntToFloat(percentage)*255.0/100.0, eRoundNearest), or some other way?

Crimson Wizard

#1
Transparency works bit differently in different parts of AGS, probably because its concept changed over time.

GUI, in particular, use following algorithm (refactored into separate utility function):
EDIT: posted wrong function first.
Code: cpp

    // Special formulae to reduce precision loss and support flawless forth &
    // reverse conversion for multiplies of 10%
    inline int Trans100ToAlpha250(int transparency)
    {
        return ((100 - transparency) * 25) / 10;
    }

    inline int Alpha250ToTrans100(int alpha)
    {
        return 100 - ((alpha * 10) / 25);
    }

Snarky

#2
Umm, wow! Thanks â€" I guess my next question is how Trans100ToAlpha250() (250, WTF?) is implemented, but I'll see if I can find it in code myself. (Edit: Oh, you changed it. Thanks again!)

I should also have been more precise: I am particularly interested in how it works in DrawingSurface.DrawImage().

Crimson Wizard

Fixed first answer, the previously posted function was not really interesting so I just posted important bits

As for DrawingSurface.DrawImage(), that uses slightly different ones:

Code: cpp

    inline int Trans100ToAlpha255(int transparency)
    {
        return ((100 - transparency) * 255) / 100;
    }

    inline int Alpha255ToTrans100(int alpha)
    {
        return 100 - ((alpha * 100) / 255);
    }

Snarky

Perfect, just what I was looking for. Thanks!

Snarky

#5
Hmmm... I might need a bit more help.

My aim is to be able to create the full range of 32-bit colors in AGS Script. I had the idea that you could do this by combining two 16-bit colors with alpha-transparency, using DrawImage to overlay one on another. (Thereby creating a sprite of the desired color, which you could then use to draw with.) My initial calculations seemed to prove that you could create any 32-bit color in this way, and that the 12% transparency setting (corresponding to a 255-alpha of 224) would be a good basis for mixing, with less than a .2 error in any of the RGB values produced (which I assumed would be rounded to the closest). By precalculating a lookup-table for every 0-255 value (actually two tables, one for the 5-bit channels and one for the 6-bit channel), I can figure out which two colors to mix, calculating each channel separately.

However, I'm finding that my calculations don't produce quite the correct colors. Presumably AGS (which is to say, Allegro) doesn't use the same calculation I am using to figure out the combined color when using DrawImage().

My initial attempt was this:

Code: ags
#define COLOR_MIX_PERCENT 12
{
  // ...
  int c1, c2; // These are the 0-255 values of one of the RGB channels that we're mixing. 
  int alpha = ((100-COLOR_MIX_PERCENT)*255)/100;
  float alphaFloat = IntToFloat(alpha) / 255.0;    // Gives alpha as a float in [0.0, 1.0]
  float colMixFloat = IntToFloat(c1)*alphaFloat + IntToFloat(c2)*(1.0-alphaFloat);
  int c = FloatToInt(cm, eRoundNearest);
}


This calculates the exact mix of the two colors using the 255-alpha proportion, then rounds to the nearest integer value. However, I find that the actual result of doing this mix is, for almost all values, darker than my predicted/desired result.

OK, so probably the graphics routine takes a few shortcuts, and calculates the mix with int operations (rounding down)?

Code: ags
#define COLOR_MIX_PERCENT 12
{
  // ...
  int c1, c2; // These are the 0-255 values of one of the RGB channels that we're mixing. 
  int alpha = ((100-COLOR_MIX_PERCENT)*255)/100;
  int c = (c1*alpha + c2*(255-alpha)) / 255;
}


This is closer, but still not correct. For example, it predicts that a 12% mix of 115 + 88% of 8 should give 21:

Code: ags
  int c = (8*224 + 115*(255-224)) / 255; // division gives 21.008, rounded down to 21


However, in reality it gives 20. So maybe it's dividing by 256 instead of 255, to speed things up further? Making that substitution gives the right result for most values, but now the mix 0.12*247 + 0.88*255, which should give 253 by this formula, actually appears as 254 on screen.

So I'm a bit stumped. I need to replicate the exact calculation used by DrawImage() under the hood to do the alpha-blending (or at least a calculation that gives the same result). I've tried to dig into the engine code on github to see what's going on, and I get as far as draw_trans_sprite() in include/allegro/gfx.h

When I look at Allegro documentation, it looks like they have a bunch of different libraries/routines (Fblend, Fladimir's alpha blending routines, etc.), and I have no clue exactly which version AGS is using.

Crimson Wizard

#6
Okay, I have to admit that did not read whole post (yet), but I noticed your question about alpha blending.

We are using our own blending functions in the engine code; of course math is standart, but Allegro blending functions reset alpha in result (they are assuming an image is drawn to would-be opaque surface), so first Chris Jones, and then me had to write more specialized versions for AGS.
These functions are located in Engine/gfx/blender.cpp

For example, the main generic alpha blending routine:
Code: cpp

FORCEINLINE unsigned long argb2argb_blend_core(unsigned long src_col, unsigned long dst_col, unsigned long src_alpha)
{
    unsigned long dst_g, dst_alpha;
    src_alpha++;
    dst_alpha = geta32(dst_col);
    if (dst_alpha)
        dst_alpha++;

    // dst_g now contains the green hue from destination color
    dst_g   = (dst_col & 0x00FF00) * dst_alpha / 256;
    // dst_col now contains the red & blue hues from destination color
    dst_col = (dst_col & 0xFF00FF) * dst_alpha / 256;

    // res_g now contains the green hue of the pre-final color
    dst_g   = (((src_col & 0x00FF00) - (dst_g   & 0x00FF00)) * src_alpha / 256 + dst_g)   & 0x00FF00;
    // res_rb now contains the red & blue hues of the pre-final color
    dst_col = (((src_col & 0xFF00FF) - (dst_col & 0xFF00FF)) * src_alpha / 256 + dst_col) & 0xFF00FF;

    // dst_alpha now contains the final alpha
    // we assume that final alpha will never be zero
    dst_alpha  = 256 - (256 - src_alpha) * (256 - dst_alpha) / 256;
    // src_alpha is now the final alpha factor made for being multiplied by,
    // instead of divided by: this makes it possible to use it in faster
    // calculation below
    src_alpha  = /* 256 * 256 == */ 0x10000 / dst_alpha;

    // setting up final color hues
    dst_g   = (dst_g   * src_alpha / 256) & 0x00FF00;
    dst_col = (dst_col * src_alpha / 256) & 0xFF00FF;
    return dst_col | dst_g | (--dst_alpha << 24);
}


The code here uses few tricks to reduce the amount of operations and speed it up a little. For instance, it increases alpha values to the range of 1-256, because in real machine code working with 256 is faster than with 255 since 256 is a power of 2 (CPU has special operation for these). You won't need that for AGS script, because it does not support such thing (AFAIK).

As you may notice from the function name, it draws ARGB image to ARGB surface, keeping combined alpha.

There are other functions nearby, for drawing ARGB to RGB surface, etc.

Snarky

#7
Once again big thanks, CW! That's exactly what I was looking for. I'm not 100% sure I've wrapped my head around the ins and outs, but I suspect that the mismatch has to do with the range shift, and I'll use this as my first attempt:

Code: ags
  int c = (c1*(alpha+1) + c2*(256-alpha)) / 256;


AGS Script does have bit shifts, though I have no idea whether they're actually any faster than doing regular multiplications/divisions.

Oh, and just to be on the safe side, I hope I'm right in thinking that this is the formula for the 32-bit RGB value of an AGS color (ignoring the first palette-mapped exceptions):

Code: ags
  int rBits = (colorNum >> 11) & 31;
  int gBits = (colorNum >> 5) & 63;
  int bBits = colorNum & 31;

  int r32 = (rBits << 3) + (rBits >> 2);
  int g32 = (gBits << 2) + (gBits >> 4);
  int b32 = (bBits << 3) + (bBits >> 2);

Crimson Wizard

Quote from: Snarky on Sun 26/11/2017 14:11:03
AGS Script does have bit shifts, though I have no idea whether they're actually any faster than doing regular multiplications/divisions.

In the end AGS does corresponding operation when interpreting the script (e.g. '>>' instead of '/' ), so formally speaking they are faster, but since all this is wrapped into layer of interpreter's own operations, the actual speed benefit may be much less significant than for real compiled languages.

Snarky

Well, I've tried a few different variations, and can't seem to get it to work. :(

I'm looking at _argb2rgb_blender(), which I think is the function I actually need to emulate:

Code: cpp
unsigned long _argb2rgb_blender(unsigned long src_col, unsigned long dst_col, unsigned long src_alpha)
{
   unsigned long res, g;

   if (src_alpha > 0)
        src_alpha = geta32(src_col) * ((src_alpha & 0xFF) + 1) / 256;
    else
        src_alpha = geta32(src_col);
   if (src_alpha)
      src_alpha++;

   res = ((src_col & 0xFF00FF) - (dst_col & 0xFF00FF)) * src_alpha / 256 + dst_col;
   dst_col &= 0xFF00;
   src_col &= 0xFF00;
   g = (src_col - dst_col) * src_alpha / 256 + dst_col;

   res &= 0xFF00FF;
   g &= 0xFF00;

   return res | g;
}


Just to make sure I follow along: the res and g variables is just a way to keep the different channels contained, so that you don't overflow or underflow, spilling over into one of the other channels?

Maybe I'll put it aside for now and come back to it with fresh eyes later.

Crimson Wizard

#10
One thing, you said you want to do something by combining two 16-bit colors... Do you mean the AGS integer color which you use e.g. to set to DrawingSurface.DrawingColor or pass to PutPixel?

The thing is that these 16-bit colors are packed in certain format, hues do not have equally 8-bit.


EDIT: Oh, you actually mentioned that. Well, I will post contents of Game.GetColorFromRGB.

Code: ags

int Game_GetColorFromRGB(int red, int grn, int blu) {
    <... skipping uninteresting part ...>

    int agscolor = ((blu >> 3) & 0x1f);
    agscolor += ((grn >> 2) & 0x3f) << 5;
    agscolor += ((red >> 3) & 0x1f) << 11;
    return agscolor;
}



Quote from: Snarky on Sun 26/11/2017 14:52:10
Just to make sure I follow along: the res and g variables is just a way to keep the different channels contained, so that you don't overflow or underflow, spilling over into one of the other channels?

I do not remember tbh, this may be done purely as a speed optimization.

Monsieur OUXX

Quote from: Crimson Wizard on Sun 26/11/2017 15:10:14
Quote from: Snarky on Sun 26/11/2017 14:52:10
Just to make sure I follow along: the res and g variables is just a way to keep the different channels contained, so that you don't overflow or underflow, spilling over into one of the other channels?
I do not remember tbh, this may be done purely as a speed optimization.

I'm pretty sure Snarky guessed right. CJ does calculations that makes him unsure about the result value in case things go wrong, but the thing he's sure about is that result value cannot be that bad that it exceeds an 8-bits overflow to the left (the result can't be off more than a 256 factor). Therefore he creates some sort of 8-bits long safe zone inbetween Red and Blue (0xFF00FF). Then he calcultates Green inbetween separately. Then he applies the mask again just in case the values have overflown to the left (res &= 0xFF00FF; g &= 0xFF00;). And then he just merges everything together.
)
 

SMF spam blocked by CleanTalk