首页 > 代码库 > 桌面透明窗口程序渲染

桌面透明窗口程序渲染

        市面上基本所有的3D游戏都依赖一个普通的windows窗口,包含标题栏、边框、最小化、最大化、关闭按钮。窗口的大小决定了玩家可视的游戏空间,整个窗口的像素都被游戏内容填充满,窗口背景不是透明的。渲染时,只要创建一个主渲染缓冲区,将各元素渲染在上面,再显示就可以了。

        本文介绍一种方法,窗口的背景是透明的,窗口中只渲染主要的游戏元素,比如主角,而windows桌面就是舞台,你可以看到你的角色在桌面上奔跑,还可以用鼠标与它交互,如图:



采用类似技术的游戏有“哈姆宝宝”、“宠物王国”等。该技术比较适合宠物养成类游戏,比传统2D宠物的渲染方式要复杂的多。


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////华丽的分隔线

        先介绍下渲染的基本步骤:
        1. 将角色渲染到贴图(RenderToTexture),此时贴图在显存中
        2. 贴图内容拷贝到System Memory Texture,此时贴图在内存中。(对显卡带宽要求较高,在老旧的显卡上,大部分的性能都消耗在这个步骤,CPU占用率90%以上)
        3. 从System Memory Texture“拷贝”到windows窗口的GDI位图上。这里不是简单的拷贝,需要对贴图中每个像素进行特殊处理。

        4. 更新窗口内容,就完成显示了

        需要注意的是,步骤1和2中的贴图,大小、格式一致,并且与所在窗口的客户区大小一致(客户区大小不一定等同于窗口大小)。所以,这里都是逐像素拷贝,不存在任何拉伸问题。


        按照以上渲染步骤,因此,渲染前要做的准备工作如下:

        1. 创建窗口

        2. 创建与窗口关联的GDI位图

        3. 创建System Memory Texture

        4. 创建渲染用的Texture


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////华丽的分隔线


【准备步骤1】

拿上图中的例子来说,渲染该宠物的贴图大小为256x256,那么首先要创建一个同等大小的Windows窗口,不带边框:

        m_hWnd = ::CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_LAYERED, ClassName, Title, WS_POPUP,0,0,256,256,0,NULL,hInst, NULL);

注意到style参数为WS_POPUP,就是创建不带边框、标题栏等内容的窗口,这时,客户区大小等同于窗口大小。

WS_EX_LAYERED也是至关重要的一个属性,用于实现窗口透明化功能,后面会提到。

其它参数请查阅MSDN。


【准备步骤2】

然后是创建GDI位图,代码如下:

BITMAPV4HEADERbm4;
bm4.bV4Width = 256;         //窗口宽度
bm4.bV4Height = -256;      //窗口高度。为什么是负数,请查阅BITMAP相关技术文档
bm4.bV4BitCount = 32;     //像素比特数,这里必须为32,像素格式A8R8G8B8
bm4.bV4Size = sizeof(BITMAPV4HEADER);
bm4.bV4Planes = 1;
bm4.bV4V4Compression = BI_RGB;

m_hDC = ::CreateCompatibleDC( 0 );

m_hBp = ::CreateDIBSection(m_hDC, (BITMAPINFO *)&bm4, DIB_RGB_COLORS, &m_pBitmapBits, 0, 0 );

m_hOldObj = ::SelectObject(m_hDC, m_hBp);

退出时记得销毁:

if ( m_hDC )
{
::SelectObject( m_hDC, m_hOldObj );
::DeleteDC(m_hDC);
m_hDC = NULL;
}
if ( m_hBp )
{
::DeleteObject( m_hBp );
m_hBp = NULL;
}

变量声明如下:

HDC m_hDC;
HBITMAP m_hBp;
HGDIOBJ m_hOldObj;
void *            m_pBitmapBits;


【准备步骤3】

创建System Memory Texture:
hr = dev9->CreateOffscreenPlainSurface( 256, 256, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &m_pSysMemSurface, NULL );  //D3DFMT_X8R8G8B8也可以

销毁代码:

if ( m_pSysMemSurface )
{
m_pSysMemSurface->Release();
m_pSysMemSurface = NULL;
}

变量声明如下:

LPDIRECT3DSURFACE9 m_pSysMemSurface; 


【准备步骤4】

创建渲染贴图的方法就不讲了,各引擎不一样。


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////华丽的分隔线


准备工作完毕后,可以开始渲染。

【渲染步骤1】先将物体渲染到RenderTexture

【渲染步骤2】从RT拷贝到System Memory Texture

LPDIRECT3DSURFACE9 rt_surface= NULL;  

HRESULT hr;

hr = rt_texture->GetSurfaceLevel(0, &rt_surface);

//【渲染步骤2核心语句】

hr = dev9->GetRenderTargetData( rt_surface, m_pSysMemSurface );
rt_surface->Release();

//【渲染步骤3】从System Memory Texture到GDI位图

D3D9SurfaceBlt2DIBPerPixelAlpha( m_hWnd, m_pSysMemSurface, m_hDC, m_pBitmapBits, 0xff);


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////华丽的分隔线


D3D9SurfaceBlt2DIBPerPixelAlpha实现如下:

bool D3D9SurfaceBlt2DIBPerPixelAlpha( HWND window, LPDIRECT3DSURFACE9 pSurface, HDC srcDC, void *pSrcDCBmp, BYTE wndAlpha )

{

RECT wr = {0};
D3DSURFACE_DESCd3dsurface_desc;
D3DLOCKED_RECTd3drt = {0};
if ( !::GetClientRect(window, &wr ) )
{
return false;
}


POINT dstPT = {wr.left, wr.top};
if (!::ClientToScreen(window, &dstPT))
{
return false;
}

HRESULT hr = pSurface->LockRect( &d3drt, NULL,  0);//D3DLOCK_NOSYSLOCK|D3DLOCK_NO_DIRTY_UPDATE|D3DLOCK_READONLY );
assert( SUCCEEDED(hr) && d3drt.pBits );
if ( NULL == d3drt.pBits ) return false;

pSurface->GetDesc(&d3dsurface_desc);

//【渲染步骤3核心语句】
BltSurface32ToDIB32_SelfMulAlpha(pSrcDCBmp, d3drt.pBits, d3dsurface_desc.Width, d3dsurface_desc.Height, d3drt.Pitch);

pSurface->UnlockRect();


SIZE dstSZ = {wr.right - wr.left, wr.bottom - wr.top};
BLENDFUNCTION bfc = { AC_SRC_OVER, 0, wndAlpha, AC_SRC_ALPHA };
POINT srcPT = {0, 0};

//【渲染步骤4】这句就是通知窗口更新显示内容,窗口必须拥有属性WS_EX_LAYERED
::UpdateLayeredWindow(window, 0, &dstPT, &dstSZ, srcDC, &srcPT, 0, &bfc, ULW_ALPHA );  

return true;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////华丽的分隔线


BltSurface32ToDIB32_SelfMulAlpha函数是用汇编实现的,有三个版本:MMX、SSE、SSE2,可以固定选择一个,也可以根据当前cpu支持的指令集动态选择一个。

这里就贴一个SSE版本,该函数的具体实现细节,因为年代久远,我也记不太清了,有问题欢迎咨询讨论 unknowayu@163.com。

【渲染步骤3】的关键问题是GDI位图需要“自乘alpha”,懒得码字了,请参阅MSDN,BLENDFUNCTION和UpdateLayeredWindow。

void BltSurface32ToDIB32_SelfMulAlphaSSE( void *pDst, void *pSrc, unsigned int width, unsigned int height, unsigned int src_pitch )

{
int src_pitch_sub_dst_pitch;//src pointer对齐到下一行scanline,需要跳过多少字节


__asm
{
//取参数,判断width和height是否有任一为0
mov eax, height  //eax = heigh
mov ebx, width
mul ebx//width * height
test eax, eax  //影响ZF
jz end_pixel


//常量赋值
mov esi, pSrc
mov edi, pDst


pcmpeqd mm5, mm5//mm5 = 0xffffffff_ffffffff
pxor mm7, mm7 //mm7 = 0x0
psrld mm5, 8 //mm5 = 0x00ffffff_00ffffff


//判断pitch
mov edx, src_pitch
shl ebx, 2//每个像素4个字节, dst_pitch = width * 4
sub edx, ebx//src_pitch - dst_pitch
jnz diff_pitch


//same_pitch:
mov ecx, eax
mov edx, 1//how many lines,eax和edx构成2层循环
and ecx, 1//一行上剩下多少个不成对的象素,same_pitch时就是(width*height & 1),diff_pitch时就是(width & 1)
shr eax, 1//一行上主循环多少次,same_pitch时就是(width*height >> 1)diff_pitchh时就是(width >> 1)
jmp test_pair_pixel


diff_pitch:
mov src_pitch_sub_dst_pitch, edx//src_pitch - dst_pitch
mov eax, width
mov edx, height//how many lines,eax和edx构成2层循环
mov ecx, eax
and ecx, 1//一行上剩下多少个不成对的象素,same_pitch时就是(width*height & 1),diff_pitch时就是(width & 1)
shr eax, 1//一行上主循环多少次,same_pitch时就是(width*height >> 1)diff_pitchh时就是(width >> 1)
mov ebx, eax//main loop count on every scanline
jmp test_pair_pixel


loop_line:
mov eax, ebx


loop_pair_pixel:
movq mm0, [esi]//mm0 = 0xaarrggbb_AARRGGBB
movq mm4, mm5 //mm4 = mm5 = 0x00ffffff_00ffffff
movq mm1, mm0 //mm1 = mm0 = 0xaarrggbb_AARRGGBB
pandn  mm4, mm0//保存alpha, mm4 = 0xaa000000_AA000000


punpcklbw mm0, mm7//mm0 = 0x00AA_00RR_00GG_00BB
punpckhbw mm1, mm7//mm1 = 0x00aa_00rr_00gg_00bb


pshufw mm2, mm0, 0xff//mm2 = 0x00AA_00AA_00AA_00AA
pshufw mm3, mm1, 0xff //mm3 = 0x00aa_00aa_00aa_00aa


pmullw mm0, mm2//自乘alpha,字组相乘,取低16位
pmullw mm1, mm3


psrlw mm0, 8 //除以256
psrlw mm1, 8


packuswb mm0, mm0//合并单个象素
packuswb mm1, mm1


punpckldq mm0, mm1//将2个象素合并


pand mm0, mm5 //恢复原始alpha
por mm0, mm4


//put_pixel:
MOVNTQ [edi], mm0


add esi, 8
add edi, 8


dec eax
test_pair_pixel:
jnz loop_pair_pixel


//rest_line_pixel:
jecxz next_line//scanline_rest_pixel不是0就是1


movq mm0, [esi]//mm0 = 0xaarrggbb_AARRGGBB
movq mm4, mm5 //mm4 = mm5 = 0x00ffffff_00ffffff
movq mm1, mm0 //mm1 = mm0 = 0xaarrggbb_AARRGGBB
pandn  mm4, mm0//保存alpha, mm4 = 0xaa000000_AA000000


punpcklbw mm0, mm7//mm0 = 0x00AA_00RR_00GG_00BB
punpckhbw mm1, mm7//mm1 = 0x00aa_00rr_00gg_00bb


pshufw mm2, mm0, 0xff //mm2 = 0x00AA_00AA_00AA_00AA
pshufw mm3, mm1, 0xff //mm3 = 0x00aa_00aa_00aa_00aa


pmullw mm0, mm2//自乘alpha,字组相乘,取低16位
pmullw mm1, mm3


psrlw mm0, 8 //除以256
psrlw mm1, 8


packuswb mm0, mm0//合并单个象素
packuswb mm1, mm1


punpckldq mm0, mm1//将2个象素合并


pand mm0, mm5 //恢复原始alpha
por mm0, mm4


movd [edi], mm0


add esi, 4
add edi, 4


next_line:
add esi, src_pitch_sub_dst_pitch//设置指针到下一个src行
dec edx
jnz loop_line


emms


end_pixel:
}
}