市面上基本所有的3D游戏都依赖一个普通的windows窗口,包含标题栏、边框、最小化、最大化、关闭按钮。窗口的大小决定了玩家可视的游戏空间,整个窗口的像素都被游戏内容填充满,窗口背景不是透明的。渲染时,只要创建一个主渲染缓冲区,将各元素渲染在上面,再显示就可以了。
本文介绍一种方法,窗口的背景是透明的,窗口中只渲染主要的游戏元素,比如主角,而windows桌面就是舞台,你可以看到你的角色在桌面上奔跑,还可以用鼠标与它交互,如图:
采用类似技术的游戏有“哈姆宝宝”、“宠物王国”等。该技术比较适合宠物养成类游戏,比传统2D宠物的渲染方式要复杂的多。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////华丽的分隔线
先介绍下渲染的基本步骤: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_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:
}
}