桌面透明窗口程序渲染

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

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

桌面透明窗口程序渲染_第1张图片桌面透明窗口程序渲染_第2张图片桌面透明窗口程序渲染_第3张图片


采用类似技术的游戏有“哈姆宝宝”、“宠物王国”等。该技术比较适合宠物养成类游戏,比传统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 )	//只支持32bit的surface
{
	RECT	wr = {0};
	D3DSURFACE_DESC		d3dsurface_desc;
	D3DLOCKED_RECT		d3drt = {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版本,该函数的具体实现细节,因为年代久远,我也记不太清了,有问题欢迎咨询讨论 [email protected]

【渲染步骤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:
	}
}


你可能感兴趣的:(游戏开发)