我在GameRes发布SheRO后,收到了几封热心朋友的来信,都希望我坚持把SheRO做下去,还意外的发现有一个志同道合者theshine以前也做过模拟RO的客户端,不过中途太监了。。他给我提供了很多有用的资料和意见,再次表示感谢:)
不管坚持不坚持做下去,现在首要任务是开源,所以有了关于SheRO的第一篇开发资料《用D3D绘制2D图像》。因为从图书馆借的资料快到期了,加之最近改正了一个2D渲染的性能问题,所以先从这个题目讲解了。
在D3D之前,绘制2D图像都用的是DirectDraw的API,可是现在微软的D3D根本没有DirectDraw了。那么如何在D3D里绘制2D图像呢?比较普遍的方法是使用D3D提供的ID3DXSprite,但是局限性太大,其一是它是微软封装好的类,我们不知道它的底层原理,其二是,它不提供自身顶点的操作,所以无法使用顶点着色器,纹理效果等。其三是,使用自己打造的ID3DXSprite,性能会更优。还有人可能会想,用BillBoard也可以绘制2D效果,的确是这样,但是BillBoard始终是3D世界的东西,我们需要的是直接在屏幕坐标绘制2D图像。
下面开始讲解它的具体实现:
从3D到2D,只是丢掉一个"D”
的确是这样,2D空间实际上是3D空间的一个子集,从3D到2D,我们需要的就是丢掉一个D。我们知道,在3D流水线中,有一步是透视投影变换,这就是从3D到2D的过程,如下图所示:
也就是从q点到q'的过程,透视投影变换可以产生近大远小的效果,那么显然,透视投影变换并没有丢弃z坐标,因为q点的z坐标就是产生近大远小的原因,实际上透视投影变换的方程可以用相似三角形推导出来,具体的推导请查阅相关资料。
另外还有一种变换,叫做正交投影,一般在D3D里并没有使用它,但是D3D提供了创建正交投影矩阵的API,下图是透视投影与正交投影的区别:
由图中可以看出正交投影就是简单的将z坐标丢弃,也就是:
x1' = x1;
y1' = y1;
这正是我们绘制2D所需要的效果。
剩下的实现也就很简单了,我们需要的是,绘制2D图像时,不使用透视投影,而使用正交投影,还有变换到观察坐标系时,直接使用单位矩阵,因为我们的2D图像与摄像机无关,只在屏幕绘制。在SheRO里,你会发现在SpriteNode的PreRender()里有该变换的代码。
代码如下:
Mat4x4 m_PixelProjection;
Mat4x4 m_ViewMatrix = Mat4x4::g_Identity;
D3DXMatrixOrthoOffCenterLH(&m_PixelProjection, 0.0f, 800.0f, 600.0f, 0.0f,0.0f, 1.0f);
DXUTGetD3D9Device()->SetTransform(D3DTS_VIEW, &m_ViewMatrix);
DXUTGetD3D9Device()->SetTransform(D3DTS_PROJECTION, &m_PixelProjection);
注意我们创建正交矩阵使用的是D3D的API,D3DXMatrixOrthoOffCenterLH, 上述代码创建了一个,原点为左上角,长度为800,宽度为600的的正交矩阵,也就是SheRo的默认窗口。
这样做过后,顶点的x,y坐标对应屏幕的x,y坐标,z坐标用作深度缓存使用(这点很重要,对于2.5D游戏,我们的Sprite需要放在3D世界,如何控制遮挡还是需要Z-buffer。)
如何实现自己的Sprite类?
在SheRO中,所有的2D图像都使用SpriteNode绘制,下面讨论它的具体实现。
理所当然,我们只需要4个顶点就可以绘制所有2D图像了,当需要绘制一个2D图像时,只需要把这个2D图像的帧信息(包括texture,大小,位置,alpha,旋转角度,渲染模式等)加入SpriteNode的m_frame即可。然后每次SpriteNode绘制时,遍历所有2D帧,一一绘制:
for (unsigned int i = 0; i < m_frame.size(); i++)
{
RenderOneFrame(pScene,m_frame[i]);
}
关于RenderOneFrame的实现,我曾经使用的是在绘制每一个2D图像时,都锁定4个顶点缓冲区,然后改变顶点信息以达到绘制效果,但是这是灾难性的,当我企图在一个屏幕绘制20个以上的2D图像时,帧率只有30几帧。后来才发现,锁定顶点缓冲区的操作是非常费时的,当2D图像增多时,每绘制一个都要锁定一次,那肯定是不行的。所以现在的实现方案如下:
在创建SpriteNode时,把4个顶点设置为中心为原点,单位为1的图元,这样在绘制每一个2D图像时,用缩放矩阵控制大小,移动矩阵控制位置。图像坐标不变。另外,我们把Z坐标设置成1,用缩放矩阵同样可以控制深度缓存的值。
if(FAILED(DXUTGetD3D9Device()->CreateVertexBuffer(sizeof(TEXTURED_VERTEX)*4, 0, TEXTURED_VERTEX::FVF, D3DPOOL_MANAGED,
&m_pVerts, NULL)))
return FALSE;
TEXTURED_VERTEX* pVertices;
if( FAILED( m_pVerts->Lock( 0, 0, (void**)&pVertices, 0 ) ) )
return E_FAIL;
D3DXVECTOR3 normal(1.0f,1.0f,1.0f);
pVertices[0]= TEXTURED_VERTEX(D3DXVECTOR3(-0.5f,-0.5f,1.0f),normal,0.0f,0.0f);
pVertices[1]= TEXTURED_VERTEX(D3DXVECTOR3(0.5f,-0.5f,1.0f),normal,1.0f,0.0f);
pVertices[2]= TEXTURED_VERTEX(D3DXVECTOR3(-0.5f,0.5f,1.0f),normal,0.0f,1.0f);
pVertices[3]= TEXTURED_VERTEX(D3DXVECTOR3(0.5f,0.5f,1.0f),normal,1.0f,1.0f);
m_pVerts->Unlock();
在绘制每一个2D图像时,都使用矩阵变换来操作顶点:
D3DXMATRIX trans, rot, scale;
D3DXMatrixScaling(&scale,width*scaleX*scaleview,height*scaleY*scaleview,zDepth);
D3DXMatrixRotationZ(&rot, info.angle *D3DX_PI/180);
D3DXMatrixTranslation(&trans, pos.x, pos.y, pos.z );
DXUTGetD3D9Device()->SetTransform(D3DTS_WORLD,&(scale * rot * trans ));
这样就能达到满足我们要求的2D渲染了~
更详细的代码请参阅SheRO源码的SpriteNode类。