梁宏舍(starofrainnight)

萬載星夜奮筆書,百世冷月淡然看
   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

16位色AlphaBlend深度探索

Posted on 2007-03-19 22:04 starofrainnight 阅读(26) 评论(0)  编辑 收藏 引用 所属分类: 遊戲開發

 16位色AlphaBlend深度探索

梁宏舍

   在网上看了很多关于16位色的AlphaBlend计算方法,在GameRes上看到很多高手写的文章,深有感触,这些文章要不就是单说原理却没有程序,要不就有程序却没有很好的原理解释,要不两样都有,但就程序跟原理就像硬拼在一起一样,看了原理看程序却不知道程序在干什么,要知道很多16位色处理的AlphaBlend程序是用汇编写的,这更头疼了,看见一堆没有解释的汇编代码,头都大了……也许是我太笨了……

  因此萌生了写一篇对16位色AlphaBlend的原理及程序的详解文章,希望我这篇文章能对一些初涉图像编程的后来者有所帮助:)

  为什么单单研究16位色的Alpha混合?而不研究24位,32位的?

  第一、节省空间,它只有两个字节,比起24位(三字节),32位(四节节)色的存储容量会省很多,虽然对于目前的大内存系统,不算什么,但放在显存中时,就很重要了,虽然目前显卡的缓存容量也在向内存靠拢,但省点还是好的:) 毕竟你也不希望你的程序是个吃内存的怪兽吧?!

  第二、速度,有人可能会不赞同这个观点:24位、32位色的RGB分量是单独的一个字节,计算起来起码没有16位那么麻烦!方法是人想出来的,16位的处理另有方法计算,可以不用把RGB分量拆开而直接计算,稍后会讲到。而24位不是4字节对齐,在内存的计算中会比较吃亏,相对16位,32位,特别是大面积复制及计算时会差些,而32位与16位呢?呃,在数字上我们可以看到一点,在处理32位数据时,我们可以处理两个16位的数据……

  第三、视觉,在人的视觉方面来说,高于16位的颜色,人眼几乎分辨不出区别来,那么要那么大的颜色干啥呢?当然,这里只针对做游戏来说的,如果你是想做图像处理的,那么24位以上的颜色是必要的,毕竟那些细节不能掉失:)

  接下来开始入正题了。

  在16位色状态下,根据不同显卡对像素的处理不同,16位色的像素格式共有两种,一种的RGB分量为(R:5, G:5, B:5),另一种的RGB分量为(R:5, G:6, B:5)。数字是代表所占的位(Bit)数。前一种只有15位是有效的。

  注意,在 16 位模式时要先取得显卡的像素模式,否则,处理时如果跟显卡的像素格式不一致会产生偏色的现像!还有一点, Windows Bitmap 16 位色格式是倒过来的,即颜色顺序是 BGR ,刚好跟显卡的颜色顺序调了个头,当然你可以设定 Bitmap 的颜色掩码来实现 Bitmap 与显卡格式一致。

  接下来,我们要介绍的是这个 AlphaBlend 算法了, AlphaBlend 算法即我们通常所说的半透明效果算法,我们可以设置 Alpha 值来把两图进行不同透明度混合。

   AlphaBlend 常规算法如下:

  提示:获取某个颜色的值可使用颜色掩码,如: 555 色显卡的颜色掩码为 R(0x7C00), G(0x03E0), B(0x001F), 设颜色值为 Value ,则 R = ( Value & 0x7C00 ) >> 10; G = ( Value & 0x03E0 ) >> 5; B = ( Value & 0x001F ) >> 5;  计算完毕后要把颜色分量结合为一个颜色值时使用逆操作即可。

  如若你的程序不需要太高效率,那么这种算法是挺适合你的,容易理解也容易实现。

  接下来我们对此算法作一下改进,把括号拆开,现在变成这样了,这几条公式跟上面的有什么区别呢?要知道,在计算机中,一般来说,乘除计算是比加减要慢得多的,现在我们把它由原来的两次乘法变成一次乘法:

  想想,还有没有得再进一步优化呢?有!我们知道Alpha值是在 0 ~ 1范围内的,是一个浮点数,计算机的浮点数计算也是一个效率杀手,我们应该怎样去掉它呢?把它变成整数!

  但怎样变呢?把它的范围扩大!对啦,正解!在16位色 555565模式,平均来说每个分量最大取5(bit),而25=30,我们可以把它扩大32倍,足够用了!即取 0 <= Alpha <= 32 内的整数值。那么只需要公式两边均乘以32即可。现在我们的Alpha值不再是原来的Alpha了,而是一个大于等于0且小于等于32Alpha值,我们把它命名为 Alpha32。于是,公式再次变成:

 

  提示:除以 32 可以使用 C 语言中的移位运算来实现。

到目前为止,好像能优化的地方已经很难找了,还有其它方法能更快地实现运算吗?当然是有的,否则我还用写吗?!那就是不要把各个分量拆开再运算,而把整个颜色值进行运算,这样就省去了拆分的步骤,也不用进行三次相同的操作了!但不是直接把颜色值拿来进行运算,需要对颜色值进行一些前期处理再进行运算。事实上,我们接下来的这个方法还是针对每个分量进行处理的,只不过,我们把它这个处理变成一次处理三个分量。这样运算一次即相当于三次的运算。

  以下的所有的文字如无特别说明则均针对 16 位色 555 模式 RGB 顺序进行处理。

  首先,我们分析一下像素的格式,其格式为 ( 二进制 ) : 0RRR RRGG GGGB BBBB

  如果我们直接对像素进行运算,那么其运算过程如下:

  但大家需要留意的是,红绿蓝分量乘后,如果有进位,向哪里进位?必然会溢出到相邻的分量上,这样计算结果就会出现错误了!那么需要有多大空间才不会相互影响呢?我们知道25=32,而我们的Alpha位数为55位的分量乘以最大级数32也只是向左移5位,所以我们只需要5位就足够了,这里只是作下解释,下面这个方法不需要你亲自去移位D:)

  因此,我们需要想一个办法来让它有足够多的位置来进位且不用过多的复杂计算。我们可以把这个像素复制一份,这样,像素就变成了32位了(Value32): 0RRR RRGG GGGB BBBB 0RRR RRGG GGGB BBBB。然后怎样做呢?我们使用一个掩码(Mask32): 0000 0011 1110 0000 0111 1100 0001 1111(0x3E07C1F)。我们把 Value32 & Mask32 即可得  0000 00GG GGG0 0000 0RRR RR00 000B BBBB。大家可以留意到,刚好它们之间就间隔了5位,如果两个这样的32位值相乘,它们就有足够的进位空间了。如下:

  但是,我们不能给你 5 位,你给我生成 10 位数啊,那样我们还怎样运算?好的,现在把它变回 5 位,不知道你留意到没有,在上面我们讲述公式 3 时, Alpha32 后来还需要除以 32 的,除以 32 是什么意思?刚好是右移 5 位!这样 10 – 5 = 5 刚刚好!现在还有个问题,有人会说进行乘或加还好办,有足够位可以进位,那么当Color1-Color2这样怎么办呢?它会向前借位的啊!呃……这个是一个问题,但似乎这个问题并不影响大局,只要你眼力不是特别好的话,在效果上看不出有什么太大区别的,我们是求速度嘛,别计较那么多!

  好,假设我们通过运算得到了一个源点及目标点的计算结果Color3GGGG GGGG GGG0 RRRR RRRR RRBB BBBBBBBB,我们怎样把它还原到我们的16位的颜色值呢?OK,执行一下我们怎样把它变成这个样子的逆运算就行了!如下:

  哈哈,还是用 C 语言描述舒服啊,不用写那么多公式!

  这样我们就得到了一个经过 AlphaBlend 的结果值

下面就是我们的重头戏了,代码实现!可能会有人奇怪,为什么要写三份实现代码呢?那是因为,使用纯C语言实现的函数虽然可移植性佳,但同时,它也失去了使用机器更强大功能的机会。使用普通汇编代码也是同类问题,就是在没有MMX的机器上,普通汇编代码能实现比纯C更高的效率,当然,如果编译器的能力足够强大,纯C程序与汇编的速度可以几乎相等。还有个版本就是MMX版本,此版本利用了MMX加速功能,速度比普通的汇编代码提高一倍不止!要注意的是, MMX版本我们采用的仍然是颜色量计算的,原因在后面在该算法前再详述。  

  好了,废话少说:

//首先是登场的是使用C++的实现
void DrawAlphaBlend_cpp(
  
WORD * dstAddr, // 目标图的起始地址
  
WORD * srcAddr, // 源图的起始地址
  
DWORD cntX, // 横轴要处理的宽度
  
DWORD cntY, // 纵轴要处理的高度
  
DWORD dstSkipBytes, // 目标地址在处理完横向宽度最后一个点后要跳过多少个字节
  
DWORD srcSkipBytes, // 源地址在处理完横向宽度最后一个点后要跳过多少个字节
  
DWORD srcAlphaLevel )// Alpha 级数 0 ~ 32
{
  
DWORD srcColor; // 源点颜色
  
DWORD dstColor; // 目标点颜色
  //
此值要根据你所需要处理的像素格式而定 , 这里仅指 16 位色 555 模式 RGB 顺序
  
DWORD Mask32 = 0x3E07C1F;
  
WORD xCnt = cntX; // 暂存宽度值,以便循环运算

   for( ;cntY>0; --cntY, cntX = xCnt )
  
{
    
for( ; cntX>0; --cntX )
    {
      
srcColor = *srcAddr | (*srcAddr<<16); // 扩宽到 32
      
srcColor &= Mask32; // 通过掩码得到正确的扩宽颜色值
      
dstColor = *dstAddr | (*dstAddr<<16);
      
dstColor &= Mask32;
      
srcColor = (srcColor - dstColor) * srcAlphaLevel / 32 + dstColor;
      
srcColor &= Mask32; // 通过掩码把结果的其它无用数据去掉
      
// 还原到正确的 16 位颜色值
      
srcColor = (srcColor & 0xffff0000)>>16 | (srcColor & 0x0000ffff );
      
      
// 改变目标图的点为结果点,这样直接显示目标图即可知道整个结果
      
*dstAddr = srcColor;
      
++dstAddr;
      
++srcAddr;
    }
    dstAddr = (WORD*)((BYTE*)dstAddr + dstSkipBytes);
    srcAddr = (WORD*)((BYTE*)srcAddr + srcSkipBytes);
  
}
}

// 接下来是普通汇编的实现
void DrawAlphaBlend_asm(
  
WORD * dstAddr, // 目标图的起始地址
  
WORD * srcAddr, // 源图的起始地址
  
WORD cntX, // 横轴要处理的宽度
  
WORD cntY, // 纵轴要处理的高度
  
DWORD dstSkipBytes, // 目标地址在处理完横向宽度最后一个点后要跳过多少个字节到下一行
  
DWORD srcSkipBytes, // 源地址在处理完横向宽度最后一个点后要跳过多少个字节到下一行
  
WORD
srcAlphaLevel )// Alpha 级数 0 ~ 32
{
  
DWORD Mask32 = 0x3E07C1F;

   // 简单 alpha 运算  
  
__asm
  
{
    
mov edi, dword ptr dstAddr; // 源地址
    
mov esi, dword ptr srcAddr; // 目标地址
    
mov cx, cntY; // cx 为行数

Next_Row:
    
cmp cx, 0; // 如果 高度 ( 即要处理的行数 ) 为零则结束运算
    
je All_End;

     mov dx, cntX;
    
mov word ptr PointCnt, dx; // PointCnt 为一行的点数
Next_Point:
    
cmp PointCnt, 0; // 点数为零则开始下一行的计算
    
je Pre_Next_Row;

     mov ax, [esi]; // 装载 源点颜色值
    
mov bx, [edi]; // 装载 目标点颜色值

     shl eax, 16; // eax, 左移 16 ,
    
mov ax, [esi]; // eax = 0RRR RRGG GGGB BBBB 0RRR RRGG GGGB BBBB

     shl ebx, 16; // ebx 左移 16
    
mov bx, [edi]; // ebx = 0RRR RRGG GGGB BBBB 0RRR RRGG GGGB BBBB

     // ... 源点及目标点颜色拆包 , 不懂什么叫拆包?算了这个称呼是我随便安上去的……
    
and eax, Mask32; // eax = 0000 00GG GGG0 0000 0RRR RR00 000B BBBB
    
and ebx, Mask32; // ebx = 0000 00GG GGG0 0000 0RRR RR00 000B BBBB
    
// ... 源点及目标点颜色拆包 完毕 , 下面进行计算
    
sub eax, ebx; // 源颜色减去目标颜色
    
xor edx, edx; // 清空 edx
    
mov dx, word ptr srcAlphaLevel ; // dx = srcAlphaLevel
     mul edx; // Alpha , edx 被冲掉数据
    
shr eax, 5; // 除以 32
    
add eax, ebx; // 加上 ebx 目标颜色
    
and eax, Mask32; // 下面进行打包
    
mov bx, ax; // bx = Low_Word( eax );
    
shr eax, 16; // 把 结果颜色右移 16 位,现在 ax = High_Word( eax );
    
or ax, bx; // ax bx 作一下或,即成为 16 位的结果颜色值 , 保存在 ax

     mov word ptr [edi], ax; // 得到结果

     add esi, 2;
    
add edi, 2;
     dec PointCnt;
     jmp Next_Point; // 下一点处理

Pre_Next_Row: // 在开始下一行时的准备工作
    
add esi, srcSkipBytes;
    
add edi, dstSkipBytes;     
    
dec cx;
    
jmp Next_Row;
All_End:
  
}
}


接下来,是我们的MMX算法了,但我们注意到MMX没有针对于DWORD及QWORD的乘法,每次我们对像素进行处理时,我们必须进行两次乘法,实际来说,采用以上的算法在我的赛扬M处理器1.6G,512DDR内存,40GSTAT硬盘上,用MMX算法会比使用常规分色量MMX算法要慢,两图互换的速度是170fps 左右而常规分分色量MMX能达到270fps,在云风前辈的文章(参考资料13)说能带来10%的效率提升,但我做不到……因此这里提供是常规的分色量 MMX算法。

void DrawAlphaBlend_MMX(
  WORD * dstAddr,
  WORD * srcAddr,
  DWORD cntX,
  DWORD cntY,
  DWORD dstSkipBytes,
  DWORD srcSkipBytes,
  DWORD srcAlphaLevel )
{
  DWORD LeavePoint = cntX & 0x03; // 每次处理四个点,有没剩余点?
  DWORD RMask = 0x1F<<10;
  DWORD GMask = 0x1F<<5;
  DWORD BMask = 0x1F;

  cntX >>= 2;// 每行取4的倍数个点
  __asm
  {
    mov esi, dword ptr srcAddr; // ebx 为源地址
    mov edi, dword ptr dstAddr; // eax 为目标地址  

    // red mask
    // mm4 = (2进制) 0111 1100 0000 0000 | 0111 1100 0000 0000 | 0111 1100 0000 0000 | 0111 1100 0000 0000
    mov eax, RMask;
    mov ebx, eax;
    shl eax, 16;
    mov ax, bx;
    movd mm4, eax;
    movq mm1, mm4;
    psllq mm4, 32;
    por mm4, mm1;

    // green mask
    // mm5 = (2进制) 0000 0011 1110 0000 | 0000 0011 1110 0000 | 0000 0011 1110 0000 | 0000 0011 1110 0000
    mov eax, GMask;
    mov ebx, eax;
    shl eax, 16;
    mov ax, bx;
    movd mm5, eax;
    movq mm1, mm5;
    psllq mm5, 32;
    por mm5, mm1;

    // blue mask
    // mm6 = (2进制) 0000 0000 0001 1111 | 0000 0000 0001 1111 | 0000 0000 0001 1111 |  0000 0000 0001 1111
    mov eax, BMask;
    mov ebx, eax;
    shl eax, 16;
    mov ax, bx;
    movd mm6, eax;
    movq mm1, mm6;
    psllq mm6, 32;
    por mm6, mm1;

    // alpha group
    // mm7 = (16进制) 00AA 00AA 00AA 00AA
    mov eax, srcAlphaLevel;
    mov bx, ax;
    shl eax, 16;
    mov ax, bx;
    movd mm7, eax;
    movq mm1, mm7;
    psllq mm7, 32;
    por mm7, mm1;

    mov ecx, cntY; // ecx 为要复制的行数
Next_Row: // 下一行处理
    cmp ecx, 0; // 如果 ecx 计数为零,则退出处理
    je All_End;

    mov ebx, cntX; // edx 存储的是要处理的次数 (每两个点为一次,两个点为同时处理的)
Next_Point: // 下两个点处理
    cmp ebx, 0; // 如果 edx 为零, 则跳到下一行
    je Prepare_Next_Row;

Calculate_Points:
    movq mm0, [esi]; // 取源四点像素
    movq mm1, [edi]; // 取目标四点像素

    movq mm2, mm0;
    movq mm3, mm1;

    // Red
    pand mm2, mm4;
    pand mm3, mm4;
    psrlw mm2, 5; // 右移5位,这个没什么关系,只要有足够的进位位置就行了
    psrlw mm3, 5;
    psubsw mm2, mm3; // 有符号减    
    pmullw mm2, mm7; // 有符号乘取低位
    psraw mm2, 5; // mm2 /= 32;
    paddsw mm2, mm3;
    psllw mm2, 5;
    pand mm2, mm4; // mm2 存储的是红分量的结果

    // Green
    movq mm3, mm0; // 备份mm0 ---> mm3
    pand mm0, mm5;
    pand mm1, mm5;
    psubsw mm0, mm1; // 有符号减    
    pmullw mm0, mm7; // 有符号乘取低位
    psraw mm0, 5; // mm0 /= 32
    paddsw mm0, mm1;
    pand mm0, mm5;// mm0 存储的是红分量的结果

    // Blue
    movq mm1, [edi]; // mm1 = dstColor ( B )
    pand mm3, mm6;
    pand mm1, mm6;
    psubsw mm3, mm1; // 有符号减
    pmullw mm3, mm7; // 有符号乘取低位
    psraw mm3, 5; // mm3 /= 32
    paddsw mm3, mm1;    
    pand mm3, mm6;// mm3 存储的是红分量的结果

    por mm0, mm3;
    por mm0, mm2;

    cmp ebx, 0; // 为了复用以上的计算代码,因此加多一步检测操作
    je Handle_Leave_Point;

    movq qword ptr [edi], mm0; // 存数据

    // 地址增加
    add esi, 8;
    add edi, 8;
    dec ebx;
    jmp Next_Point; // 跳到下两个点

Prepare_Next_Row:
    cmp LeavePoint, 0;
    ja Calculate_Points;

Prepare_Next_Row_2:
    add esi, srcSkipBytes; // 源地址跳过不处理字节
    add edi, dstSkipBytes; // 目标地址跳过不处理字节    
    dec ecx;
    jmp Next_Row; // 跳去处理下一行

Handle_Leave_Point: // 处理剩下的点
    // 来到这里肯定是有 1 ~ 3个点
    movd eax, mm0;
    mov word ptr [edi], ax;
    inc edi;
    inc edi;
    inc esi;
    inc esi;

    // 判断是否大于1个点
    cmp LeavePoint, 1;
    jna Prepare_Next_Row_2;

    shr eax, 16;
    mov word ptr [edi], ax;
    inc edi;
    inc edi;
    inc esi;
    inc esi;

    // 判断是否大于2个点
    cmp LeavePoint, 2;
    jna Prepare_Next_Row_2;

    psrlq mm0, 32;
    movd eax, mm0;
    mov word ptr [edi], ax;
    inc edi;
    inc edi;
    inc esi;
    inc esi;

    jmp Prepare_Next_Row_2;

All_End: // 操作结束
    emms;
  };
}



参考资料:
1. http://dev.gameres.com/Program/Visual/2D/WindowsAlpha.mht Windows的位图alpha混合技术
2. http://dev.gameres.com/Program/Visual/2D/256Alpha.htm 基于256色的Alpha混合方法(查表法)的实现方法
3. http://dev.gameres.com/Program/Visual/2D/Alphajd.htm 16位Alpha混合的简单算法
4. http://dev.gameres.com/Program/Visual/2D/Ddutil.htm  来自alpha混合的困惑
5. http://dev.gameres.com/Program/Visual/2D/IntelAlpha.htm 可能是最快的算法alpha blend汇编源代码,Intel官方提供
6. http://dev.gameres.com/Program/Visual/2D/qAlpha.htm 快速Alpha混合
7. http://dev.gameres.com/Program/Visual/2D/AlphaQiantan.htm Alpha混合浅谈
8. http://dev.gameres.com/Program/Visual/2D/16MMXa.htm 16位Alpha混合的MMX优化
9. http://dev.gameres.com/Program/Visual/2D/AlphaBlending.htm Alpha-Blending 技术简介
10. http://dev.gameres.com/Program/Visual/2D/mmxaddalpha.htm MMX版本的Alpha Blend算法实现
11. http://dev.gameres.com/Program/Visual/2D/16bitalpha.htm 16位BIT模式下的ALPHA运算
12. http://dev.gameres.com/Program/Visual/2D/64Kalpha.htm 64K 色模式下的快速 Alpha混合算法
13. http://dev.gameres.com/Program/Visual/2D/MMXAlpha.htm 利用MMX优化64K色Alpha混合算法
14. http://dev.gameres.com/Program/Visual/2D/JianYiAlpha.htm 简易Alpha混合算法

--------------------------------------------------------------------------------------------------------------------------------------------------
( 转载时请注明作者和出处。未经许可,请勿用于商业用途 )

标题  
姓名  
主页
验证码 *
内容(提交失败后,可以通过“恢复上次提交”恢复刚刚提交的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
[使用Ctrl+Enter键可以直接提交]
相关链接:
网站导航: