我看到了这段Java代码,它非常有效地将两种RGB888颜色完美地混合了50%:
public static int blendRGB(int a, int b) {
return (a + b - ((a ^ b) & 0x00010101)) >> 1;
}
这显然相当于单独提取通道并取平均值。类似这样的东西:
public static int blendRGB_(int a, int b) {
int aR = a >> 16;
int bR = b >> 16;
int aG = (a >> 8) & 0xFF;
int bG = (b >> 8) & 0xFF;
int aB = a & 0xFF;
int bB = b & 0xFF;
int cR = (aR + bR) >> 1;
int cG = (aG + bG) >> 1;
int cB = (aB + bB) >> 1;
return (cR << 16) | (cG << 8) | cB;
}
但第一种方法要有效得多。我的问题是:这种魔法是如何运作的?我还能用它做什么?还有更多类似的技巧吗?
(a ^ b) & 0x00010101
是如果没有来自右边的进位,通道的最低有效位在a + b
中的值。
从和中减去它可以保证被转移到下一个通道的最高有效位的位正是该通道的进位,没有被该通道污染。当然,这也意味着该信道不再受到来自下一信道的进位的影响。
另一种看待这一点的方法,不是它的方式,而是一种可以帮助你理解它的方法,是有效地改变输入,使其总和对所有通道都是偶数。进位然后很好地进入最低有效位(因为偶数,所以为零),而不会干扰任何东西。当然,它实际上做的是另一种方式,首先它只是对它们求和,然后才确保所有通道的和都是偶数。但顺序并不重要。
更具体地说,有4种情况(在应用来自下一个信道的进位之前):
- 通道的lsb为0,并且没有来自下一个通道的进位
- 一个通道的lsb是0,并且是下一个通道的进位
- 一个信道的lsb是1,并且没有来自下一个信道中的进位
- 一个通道的lsb是1,并且是从下一个通道开始的进位
前两种情况微不足道。移位将进位位放回它所属的通道中,它是0还是1都无关紧要。
案例3更有趣。如果lsb是1,这意味着移位将把该位移位到下一个通道的最高有效位。太糟糕了。这一点必须以某种方式取消设置——但你不能只是掩盖它,因为也许你是第四种情况。
案例4是最有趣的。如果lsb是1,并且有一个进位进入该位,则它将滚动到0,并且进位被传播。这不能通过掩蔽来消除,但可以通过颠倒过程来实现,即从lsb中减去1(这将使其回到1,并消除传播进位造成的任何损坏)。
正如你所看到的,在情况3和情况4中,治愈方法都是从lsb中减去1,在这些情况下,lsb真的希望是1(尽管由于下一个通道的进位,可能不再是1),在情况1和情况2中,你不必做任何事情(换言之,减去0)。这恰好对应于减去"如果没有来自右边的进位,则lsb在a + b
中会是什么"。
此外,蓝色通道只能属于情况1或3(没有下一个可以携带的通道),并且移位将丢弃该比特,而不是将其放入下一个通道(因为没有)。因此,或者,你可以写(注意,掩码丢失了最低有效的1)
public static int blendRGB(int a, int b) {
return (a + b - ((a ^ b) & 0x00010100)) >> 1;
}
不过并没有什么区别。
为了使其适用于ARGB888,您可以切换到旧的"SWAR平均值":
// channel-by-channel average, no alpha blending
public static int blendARGB(int a, int b) {
return (a & b) + (((a ^ b) & 0xFEFEFEFE) >>> 1);
}
这是定义加法的递归方法的变体:x + y = (x ^ y) + ((x & y) << 1)
计算不带进位的和,然后分别将进位相加。基本情况是其中一个操作数为零时。
两个半部分都有效地右移1,这样最高有效位的进位就不会丢失。掩码确保比特不会向右移动到通道,同时确保进位不会传播到其通道之外。