# 动画技术

[ 主页 ] [上一层]

所谓动画也就使一幅图像“活”起来的过程。使用动画可以清楚的表现出一个事件的过程，或是展现一个活灵活现的画面。如今电脑动画的应用十分广泛，无论是让应用程序更加生动，增添多媒体的感官效果；还应用于游戏的开发，电视动画制作，创作吸引人的广告，电影特技制作，生产过程及科研的模拟等等。
从某种极端的意义上说，产生动画的最好方法并不是编程，而是……放电影。大概你还记得童年时在课本每页右下角画上做不同连续动作的小人再快速翻阅产生的“原始动画”。我们在常识课上就知道利用人类视觉暂留的原理快速连续显示一系列静止画面可以产生动画。每秒播映24张胶片的电影和每秒显示30帧画面的电视（NTSC制）足以蒙骗我们可怜、不灵敏的眼睛。计算机动画（Computer Animation）也不例外，应用程序以一定的规律快速连续绘制并显示一系列有关联的静止图像就产生了电脑动画。
根据计算机动画绘制和显示的原理，根据动画的交互程度我们可以将计算机动画分为非交互动画和交互动画两大类。而交互动画又可进一步被区分为非即时计算交互动画和即时计算交互动画两种。
在本章里，我们将简单介绍一些动画制作的基本概念，并结合几个简单的程序来说明如何荣编程的方法来实现动画过程。
动画过程的实现有很多种方法，下面我们简单介绍其中的几种：
子图形动画
子图形（sprite）在交互式电子游戏中是用来表示各个角色以及不动的背景。当游戏者移动输入设备如游戏杆、跟踪球或者鼠标时，子图形就在背景上移动。玩电子游戏的游戏者实际上是在操作子图形。Uideo游戏机通常都带有图形硬件，内部支持子图形。而IBM兼容pc机内的显示硬设备并不支持子图形，所以必须依靠软件技术实现。
擦除和重画技术
在旧位置擦除图像并在新位置重新绘出，这显然是移动图像的一种方法，在Windows程序中，可以通过调用BitBlt函数实现这个方法，如果反复不断地擦除和重绘，图像看起来就会像是在屏幕上穿越移动。但是，这种方法的主要缺点是在擦除和重绘图像时会发生显示闪烁现象。
有一个办法可以避免擦除－重绘动画时产生的闪烁现象，那就是采用视频页切换，只要显示硬设备支持一个以上的视频页。使用多个视频页，就可以在显示活动页的同时在隐藏的视频页上绘制整个屏幕以显示更新后的图形。要继续显示动画，只需用一个循环重复这一过程。许多高级图形工作站（例如， Silicon Graphics 工作站） 支持通过页的切换来实现动画——用工作站界的行话来说是“缓冲区交换”技术。
不巧的是，绝大多数PC显示适配器在高分辨率视频方式下并不支持多视频页。更重要的是，Microsoft Windows也不支持多视频页。所以必须想其他办法在Windows中创建不闪烁的动画。
擦除和重绘动画伴随着屏幕闪烁，其原因是屏幕绘制操作都是可见的，当图像被擦除，它就从屏幕上消失了，然后这个图像又出现在一个新的位置上。如果使用两个视频页，闪烁现象将不复存在，因为屏幕的更新是在隐藏页上完成的，视频页交换的同时就显现整个已更新的屏幕。按照这个逻辑，只要图像准备好，并且更新的屏幕能迅速重绘，就可以避免闪烁。
好在Windows支持在离屏位图上绘图，这样离屏位图就可以作为准备显示屏幕的理想的“画布”，简单调用BitBlt函数，更新后的图像就会迅速地传输到显示屏幕上，当然，还得注意众多细节，以能够在离屏位图上正确地绘制图像，不过这个基本思想倒是很适用于在Windows下的图像动画。
调用SetBackground函数，其参数是一个指向新的被用作背景图像的DIB指针。这个调用的结果是文档拥有这个DIB，当文档被破坏或背景DIB被新的替代时，此DIB将被消除。显然，改变背景图像将涉及到一些重画操作，但稍后用户才能碰到这些问题，以下是SetBackground函数的程序代码：
BOOL CAnimDoc::SetBackground(CDIB* pDIB)
{kk1}
// Delete any existing background DIB and set the new one.
if (m_pBkgndDIB) delete m_pBkgndDIB;
m_pBkgndDIB = pDIB;
// Note that the document has changed.
SetModifiedFlag(TRUE);
// Tell the view that it needs to create a new buffer
// and palette.
CAnimView* pView = GetAnimView( );
ASSERT(pView);
return pView->NewBackground(m_pBkgndDIB);
}
清除现存的背景图像，并用新的背景代替。注意：传送空指针给SetBackground将导致那根本没有背景图像，然后文档也标明已发生改变。
我们再来看一下CanimView：：NEW Background函数上来，首先，添加一个装入新背景图像的菜单选项。用App Studio在File菜单中添加一个Load Background选项，然后在CanimDoc类中用ClassWizard为该命令建立一个处理函数。一下是处理程序代码：
void CAnimDoc::OnFileLoadBkgnd()
{kk1}
// Create a DIB to hold the image.
CDIB* pDIB = new CDIB;
// Show the file open dialog for a DIB.
if (!pDIB->Load()) {kk1}
delete pDIB;
return;
}
// Make sure this is an 8-bpp DIB
BITMAPINFO* pBMI = pDIB->GetBitmapInfoAddress();
ASSERT(pBMI);
if (pBMI->bmiHeader.biBitCount != 8) {kk1}
AfxMessageBox("Only 8-bpp DIBs are supported");
delete pDIB;
return;
}
// Replace any existing background DIB with the new one.
if (!SetBackground(pDIB)) {kk1}
delete pDIB;
}
}
OnFileLoadBkgnd使用CDIB类中的新Load函数显示一个允许用户选择并装入背景图像DIB的对话框。如果成功，则SetBackground函数把新装入的DIB用作当前的背景图像。对于CDIB：：Load函数，它带有一个文件名参数活不带参数，读者可以参照例程。
现在已有装入新背景图像DIB的菜单选项及将该图像附给文档的程序代码。下面我们只需作两件事：
传建一个新的屏幕缓冲区
创建一个新的待用体调色板，把创建的屏幕缓冲区画到视图窗口的设备上下文（DC）。还可以调整父画面的大小从而使视图窗口正好适合背景图像。
这里我们看一下获取视图指针后，如何调用视图的公共成员函数。以下是CAnimView：：NewBackground的功能：
BOOL CAnimView::NewBackground(CDIB* pDIB)
{kk1}
// Create a new buffer and palette.
if (!Create(pDIB)) {kk1}
return FALSE;
}
// Map the colors of the background DIB to
// the identity palette we just created for the background.
pDIB->MapColorsToPalette(GetPalette());
// Resize the main frame window to fit the background image.
GetParentFrame()->RecalcLayout();
ResizeParentToFit(FALSE);
// Try shrinking first.
ResizeParentToFit(TRUE);
// Let's be daring.
// Render the entire scene to the off-screen buffer.
Render();
// Paint the off-screen buffer to the window.
Draw();
return TRUE;
}
重画视图
将背景DIB画到缓冲区并更新屏幕到视图窗口。这时构图（Render）函数将背景DIB拷贝到屏幕缓冲区，画图（Draw）函数把屏幕缓冲区画到视图窗口。画图（Draw）实在COSBView基类中实现的，读者可参见例程。下面是构图（Render）函数：
void CAnimView::Render(CRect* pClipRect)
{kk1}
CAnimDoc* pDoc = GetDocument();
CRect rcDraw;
// Get the background DIB and render it.
CDIB* pDIB = pDoc->GetBackground();
if (pDIB) {kk1}
pDIB->GetRect(&rcDraw);
// If a clipping rectangle was supplied, use it.
if (pClipRect) {kk1}
rcDraw.IntersectRect(pClipRect, &rcDraw);
}
// Draw the image of the DIB to the off-screen buffer.
ASSERT(m_pDIB);
pDIB->CopyBits(m_pDIB,
rcDraw.left,
rcDraw.top,
rcDraw.right - rcDraw.left,
rcDraw.bottom - rcDraw.top,
rcDraw.left,
rcDraw.top);
以上就是创建背景图像的基本过程。
在Windows应用程序中，如果想使用离屏位图制作子图形动画，就需要C＋＋类来表示子图形和便于图形动起来。动画包含一个固定的背景图像，零个或多个能在背景上四处移动的子图形。下面各节定义了 Sprite 类用以构造子图形的模型，还定义了 SpriteAnimation 类来维护子图形和背景图像。
Sprite类
Sprite有两个Image对象：
·黑色背景上的Sprite图像
·白色背景上的Sprite图像的黑色轮廓像（掩膜）
animate 函数是 SpriteAnimation 类不可少的函数，由这个函数可知，要做到绘制 Sprite 轮廓时丝毫不影响其背景图像和掩膜是必不可少的。除开图像和掩膜，Sprite 类还包括x，y位置，以及其他一些记录它在背景上的运行轨迹的变量。
一个Sprite对象还有它自己的显示优先级。显示优先级用整型量表示，存储在成员变量disp_ priority中，它决定了绘制重叠子图形的次序——优先级高的子图形被绘制到优先级低的子图形的上面。
另一个成员变量，DRAWPROC类型的dproc很有意思，它由下面的typedef语句声明：
typedef void （_FAR PASCAL *DRAWPROC） （HDC hdc，short x，short y，LFVOID data）
可见，dproc 是指向函数的指针。在需要绘制 Sprite 图像的任何时候都会调用dproc指定的函数。您可以绘制对象——例如直线、矩形、椭圆或文本——这样一个子图形就可以有多个位图式图像，例如应用程序 ANIMATE 显示移动文本，就用到了 Sprite 的这个特性。
Sprite类继承了Borland类库(CLASSLIB)中的Sortable类，由于只有Sortable 对象才可以放在SortedArray（Borland CLASSLIB 的另一个类）中，继承后就可以把Sprite 对象存储在SortedArray 中。由于继承了Sortable ，Sprite 类必须定义以下的成员函数：
classType isA ( ) const ；
char_FAR * nameof( ) const ；
hashValueType hash Value( ) const ；
void printOn (ostream_FAR& os) ；
这四个函数的定义都很简单。函数的isA返回唯一的整型标识符，表示Sprite类，函数nameOf返回类名，即字符串“Sprite”。
此外，为了Sprite对象能够正确地排序，Sprite类必须包括下面的函数：
int isLessThan (const Object_FAR& ob ) const ；
int isEqual(const Object_FAR& ob ) const ；
函数isLessThan用来检验某个Sprite是否“小于”另一个。正如程序所定义的那样，isLessThan通过比较子图形的是显示优先级来决定哪个子图形“小些”。
初始化 Sprite
创建和初始化Sprite的典型方法是使用构造函数，它以图像文件名和掩膜文件名为多数。
Sprite：：sprite （HDC hdc，LPSTR imagefilename
，LPSTR maskfilename，short priority）
该构造函数调用init_image函数装入相应的图像位图和掩膜位图。构造函数还要求有设备场景的句柄。这是因为图像位图和掩膜位图要转换为设备无关格式，这一步骤需要一个DC。
函数init_image从文件中装入图像。它通过文件扩展名来判断图像类型。
该函数接受下列扩展名：
．BMP 即 Windows DIB 文件。
．PCX 即 PC PaintBrush 文件。
．TGA 即 24 位，Truevision Targa 文件。
上面介绍了如何生成背景图像，子图形的基本概念。这一节让我们看一下怎样创建一个或两个子图形，并把它们添加到场景中去。
为何需要通知视图在场景中装入了一个新的子图形有两个原因：首先，子图形只有被画出来才能成为可见的，而第二，子图形中的颜色必须被映射到由屏幕视图缓冲区使用的调色板中。
注意，即使背景和子图形使用的是同一个调色板，但由于考虑到系统颜色和调色板项的逻辑到物理映射之间的作用方式，仍然需要将子图形映射到屏幕缓冲区调色板上。屏幕缓冲区中的调色板是由背景DIB中的颜色表而创建的。当调色板成为一种等同调色板时（出于最好性能的考虑），可以对颜色进行重新排序，因此背景DIB和所有子图形中的象素必须被改变以具有在新的（等同）调色板中的正确索引。如果你没有将子图形的颜色映射到屏幕缓冲区中，那么显示出来的子图形颜色将都是错误的。
首先，让我们看一看简单的CAnimView： ：NewSprite函数：
void CAnimView::NewSprite(CSprite* pSprite)
{ kk1}
// Map the colors in the sprite DIB to the
// palette in the off-screen buffered view.
if (m_pPal) {kk1}
pSprite->MapColorsToPalette(m_pPal);
}
// Render the scene with the new sprite.
Render();
// Draw the result.
Draw();
}
通过调用MapColorsToPalette函数，DIB的颜色被映射到调色板上。调用Render函数在屏幕缓冲区构画子图形图象，然后Draw函数将缓冲区拷贝到屏幕上，使子图形在场景中可见。Map Colors To Palette函数和我们为CDIB类开发的函数几乎完全一样：
BOOL CSprite::MapColorsToPalette(CPalette* pPal)
{kk1}
BOOL bResult = CDIB::MapColorsToPalette(pPal);
// Get the transparency info again.
Initialize();
return bResult;
}
事实上，你可以看到CDIB： ：Map Colors To Palette被调用来进行实际的映射，而CSprite类所特有的唯一工作是再次获取透明信息，这是因为我们使用的是调色板索引来定义子图形图象中的哪种颜色是透明的，因而需要在调用Map Colors To Palette时被重新映射。
需要对Render函数进行一点小的改进以处理在构画背景图象之外还得构画子图形。
void CAnimView::Render(CRect* pClipRect)
{kk1}
CAnimDoc* pDoc = GetDocument();
CRect rcDraw;
// Get the background DIB and render it.
CDIB* pDIB = pDoc->GetBackground();
if (pDIB) {kk1}
pDIB->GetRect(&rcDraw);
// If a clipping rectangle was supplied, use it.
if (pClipRect) {kk1}
rcDraw.IntersectRect(pClipRect, &rcDraw);
}
// Draw the image of the DIB to the off-screen buffer.
ASSERT(m_pDIB);
pDIB->CopyBits(m_pDIB,
rcDraw.left,
rcDraw.top,
rcDraw.right - rcDraw.left,
rcDraw.bottom - rcDraw.top,
rcDraw.left,
rcDraw.top);
}
// Get the sprite and render it.
CSprite* pSprite = pDoc->GetSprite();
if (pSprite) {kk1}
pSprite->Render(m_pDIB, &rcDraw);
}
}
背景图象被首先画到缓冲区。如果存在一个子图形，那么它也被画到屏幕缓冲区，位于背景之上。这就是本书中所说的“面向对象欺骗”。我们并没有书写大量的代码来将子图形的图象画到屏幕缓冲区，而只是让子图形自己来完成这一工作。这样作意义重大，因为这种作法使子图形的特有代码集中在子图形对象中，从而使我们可以将注意力放在功能上而不必纠缠在实现细节上。
当然，还得我们来书写子图形的构图代码，因此让我们看一看下面的代码：
void CSprite::Render(CDIB* pDIB, CRect* pClipRect)
{kk1}
ASSERT(pDIB);
// Get the sprite rectangle.
CRect rcDraw;
GetRect(&rcDraw);
// If a clipping rectangle was supplied, see if the sprite
// is visible inside the rectangle.
if (pClipRect) {kk1}
if (!rcDraw.IntersectRect(pClipRect, &rcDraw)) {kk1}
return; // Not visible
}
// Copy the image of the sprite.
CopyBits(pDIB, // Dest DIB
rcDraw.left, // Dest x
rcDraw.top, // Dest y
rcDraw.right - rcDraw.left,// Width
rcDraw.bottom - rcDraw.top,// Height
rcDraw.left - m_x, // Source x
rcDraw.top - m_y, // Source y
PALETTEINDEX(m_bTransIndex));
// Transparent color index
}
当然我们需要检验子图形是否在裁剪板矩形中是可见的（如果提供了裁剪板矩形的话）。一旦计算出实际的绘图矩形后，CDIB：：CopyBits函数被用来将子图形图像的非透明部分拷贝到屏幕缓冲区中。CDIB：：CopyBits的透明特征是一种新的特性，请读者在动画设计时注意。
为了实现重画区域，需要将一个区域列表保存在某个位置，以及一种往列表中添加列表项的方法，而当要执行一次构图和画图动作时，需要一种遍历列表的方法。因为构图和画图主要涉及到COSBView类中的屏幕缓冲区，因此作者决定在COSBView中增加重画列表及其管理函数。
首先来看一看应用程序的其余部分是如何使用重画列表的，然后再进一步讨论重画列表的函数是如何实现的。先看一看下面的这个代码，此代码实现了对ANIMVIEW. CPP中一个子图形的鼠标拖动：
void CAnimView::OnMouseMove(UINT nFlags, CPoint point)
{kk1}
if (m_bMouseCaptured) {kk1}
ASSERT(m_pCapturedSprite);
CRect rcOld, rcNew;
m_pCapturedSprite->GetRect(&rcOld);
m_pCapturedSprite->SetPosition(point.x - m_ptOffset.x,
point.y - m_ptOffset.y);
m_pCapturedSprite->GetRect(&rcNew);
// Add the changed regions to the view's dirty list.
AddDirtyRegion(&rcOld);
AddDirtyRegion(&rcNew);
// Render and draw the changes.
RenderAndDrawDirtyList();
}
}
子图形的位置被保存在一个CRect对象中。移动子图形，并获得其新的位置。通过调用AddDirtyRegion，将新旧矩形添加到重画列表中。一旦这些区域被添加到列表中，将产生对RenderAndDrawDirtyList的一个调用从而画出改变后的区域。AddDirtyRegion和RenderAndDraWDirtyList都是COSBView中的新函数。
COSBView类具有一个新的对象来保存重画列表和几个新的成员函数。下面是COSBVIEW. H的新添加的部分：
class COSBView : public CScrollView
{kk1}
public:
virtual void Render(CRect* pClipRect = NULL) {kk1}return;}
void AddDirtyRegion(CRect* pRect);
void RenderAndDrawDirtyList();
private:
CObList m_DirtyList; // Dirty regions
void EmptyDirtyList();
};
#ifndef _DEBUG // Debug version in osbview.cpp
inline CDocument* COSBView::GetDocument()
{kk1} return (CDocument*) m_pDocument; }
注意重画列表是作为由Microsoft基类库（MFC）所提供的一个CObList对象实现的。CObList类保留了一个对象指针的列表，而其中每一个对象又都是从CObject类导出的。此表在这用于保存指向CRect对象（重画区域在这里的实现）的指针，而通过创建一个由CObList导出而只处理CRect对象的类，从而使实现更多了一些类安全性。采用这种方法要求进行类型强制，因此对新手来说也更多了一些危险性。现在来看一看AddDirtyRegion函数，这样可以了解列表的使用方法：
void COSBView::AddDirtyRegion(CRect* prcNew)
{kk1}
// Get the rectangle currently at the top of the list.
POSITION pos = m_DirtyList.GetHeadPosition();
if (pos) {kk1}
CRect* prcTop = CRect*)m_DirtyList.GetNext(pos);
CRect rcTest;
// If the new one intersects the top one merge them.
if (rcTest.IntersectRect(prcTop, prcNew)) {kk1}
prcTop->UnionRect(prcTop, prcNew);
return;
}
}
// List is empty, or there was no intersection.
CRect* prc = new CRect;
*prc = *prcNew; // Copy the data.
// Add a new rectangle to the list. . m_DirtyList.AddHead((CObject*)prc);
}
记住在这里使用的唯一优化是如果一个新添加的矩形和重画列表中位于其上面的那个矩形之间存在重叠，则进行合并。代码首先获得位于列表顶部的矩形——注意CRect*强制，如果列表是空的，那么新的矩形被简单地添加到列表的顶部，如果在列表的顶部
已经存在有一个矩形，则检测该矩形是否和新矩形之间有相交。如果有，则简单地进行扩大以包括新的矩形。如果没有发现有交叉，则将新矩形添加到列表的顶部。
现在，让我们再来看一看
RenderAndDrawDirtyList函数：
void COSBView::RenderAndDrawDirtyList()
{kk1}
POSITION pos = m_DirtyList.GetHeadPosition();
// Render all the dirty regions.
while (pos) {kk1}
// Get the next region.
CRect* pRect = (CRect*)m_DirtyList.GetNext(pos);
// Render it.
Render(pRect);
}
// Draw all the dirty regions to the screen.
while (!m_DirtyList.IsEmpty()) {kk1}
// Get the next region.
CRect* pRect = (CRect*)m_DirtyList.RemoveHead();
Draw(NULL, pRect);
// Done with it.
delete pRect;
}
}
对重画列表中的每一个矩形，将它作为一个剪切区域的矩形传递给Render函数，进行函数调用。然后再遍历一次重画列表，这次是通过调用一次Draw函数，将被构图的缓冲区区域拷贝到屏幕上。
为了能够将这个函数包含在COSBView类中，作者还增加了一个模拟Render函数。这是一个虚拟函数，由用户提供一种实现来覆盖它。下面是在头文件中实际定义的模拟函数：
virtual void Render(CRect* pClipRect = NULL) {kk1}return;}
现在来看一看CAnimView中真正的Render函数：
void CAnimView::Render(CRect* pClipRect)
{kk1}
CAnimDoc* pDoc = GetDocument();
CRect rcDraw;
// Get the background DIB and render it.
CDIB* pDIB = pDoc->GetBackground();
if (pDIB) {kk1}
pDIB->GetRect(&rcDraw);
// If a clipping rectangle was supplied, use it.
if (pClipRect) {kk1}
rcDraw.IntersectRect(pClipRect, &rcDraw);
}
// Draw the image of the DIB to the off-screen buffer.
ASSERT(m_pDIB);
pDIB->CopyBits(m_pDIB,
rcDraw.left,
rcDraw.top,
rcDraw.right - rcDraw.left,
rcDraw.bottom - rcDraw.top,
rcDraw.left,
rcDraw.top);
}
// Get the sprite and render it.
CSprite* pSprite = pDoc->GetSprite();
if (pSprite) {kk1}
pSprite->Render(m_pDIB, &rcDraw);
}
}
在这个函数中没什么新东西，但作者想再显示一次Render，这样用户可以看到剪切矩形是如何被用来限制缓冲区的受影响区域的。
这一章里，我们简单介绍了计算机动画制作的基础知识，并通过上述几个例程讲解了如何在Windows 下实现简单动画过程。实际上要想做出真正精美的动画，或是进行游戏创作还需要借助应用软件和图形处理工具。
象电视电影一样，有很多动画软件将动画过程分解为不同的图片，再把它们连起来播放，达到动画效果。在这里，我们介绍一下，如何方便的制作出三维动画。在三维图像处理软件3D MAX中提供了将操作过程连接城动画的功能，利用这一点，我们可以很容易的制作出逼真的三维动画。
这一节我们制作一个简单的动画，一只茶壶从飘着雪花的雪山下飞出。由于具体的制作步骤依存放于历程中，具体的制作过程也在教程中说明。这里我们只是对教程中录制动画时帧数的选取作以补充说明。由于3D MAX自动会将指定帧数到调整前帧数或是动画的开始（0帧）之间物体的移动、旋转、变形记录下来。所以一开始我们将帧数改为30，然后移动壶从山底到山顶，这一过程就表现为从0帧到30帧壶从山底飞出。而此后将帧数改为80，将壶拉近屏幕并旋转，就停止录制。我们可以看到飞出山顶的壶又飞近屏幕并转了近180度，在原地停留了一会又重新从山底飞出。
总的来说，动画制作的方法有很多种，而且功能更强的工具软件也一定会层出不穷。只要你有好的创意，就一定会制作出优秀的动画。
DirectDraw是微软新近发行的DirectX3软件开发工具箱（SDK）中的一部份。对那些不甚了解的人来说，DirectX3SDK是原来称为游戏SDK的最新版本。和在游戏SDK中一样，DirectX3SDK包含了一组动态链接库，用来图形加速，3─D图形服务，声音加速，扩展连接，游戏杆操纵以及CD─ROM自动化。
关于DirectX3SDK可以有很多的话题讨论，本篇只涉及如何用DirectDraw来编写游戏中的图形部份。尽管需要一些有关OLE和构件模型（COM）接口的基础知识，整个过程还是相当简单的。而且，所需的有关OLE和COM接口的知识也将在此讨论到。
在这里，我们只是讨论如何使用最基本的DirectDraw函数。所举的例子仅涉及到全屏和翻页，对于如何在窗口中使用DirectDraw，在3维图中使用文字，使用视频剪接或是用Direct3D生成表面不作过多介绍。
DirectX3SDK的基本环境
DirectX3SDK可以在Windows95或WindowsNT4.0中使用。基于本文的目的，假定使用的环境是IBM─PC和Windows95。另外，还需要安装DirectX3SDK，以便编译连接应用。
同时，使用的C或C++编译器应能生成32位的应用。您也可以使用其他的语言，但这里并不想涉及。当然您还应当具有Windows的编程技术。如果使用的是C编译器，则还必须包含Win32SDK。Win32SDK包含了生成可执行程序时的一些库。
DirectDraw,OLE,和COM接口
DirectDraw是围绕着OLE和COM接口来设计的。如果您不熟悉OLE编程，那将很难开始用DirectDraw编程，这是最基本的一点。尽管在很多书上解释了很多概念，介绍OLE和COM接口是如何工作的，但要用好DirectDraw其实只要了解有限的一些。
首先看看OLE和COM的定义。OLE是微软为在不同的进程和机器间共享信息和服务而引进的基于对象的技术。COM指的是构件对象模型，在OLE编程中，它是接口模型。
DirectDraw创建了函数来负责增加引用计数并返回指向接口的指针。但您仍要对没一个隐式创建的接口指针执行Release操作。如果没有维护好对象的引用计数，将很容易引起内存泄露。在后面编码的例子中，将介绍如何进行。
下面看一个使用IDirectDraw接口方法的C程序行。
ddrval=lpDD->lpVtbl->SetDisplayMode(lpDD,ScreenX,ScreenY,ScreenBpp);
程序用了SetDisplayMode方法来设置显示模式，返回值表示成功或失败。在此重要的一点是如何取得指向方法的指针，不能直接访问IDirectDraw接口方法。当一对象实例化时，它生成一个虚函数表，称为vtable，它包含了指向该对象接口方法的所有指针。在上例中，指向DirectDraw对象的指针（lpDD）指向了指向vtable的指针（lpVtbl），vtable中包含了对象所有方法的指针，具体地说，本例中指向了SetDisplayMode方法。应用和方法接口间的联系可以清楚地看成这样：
下一个例子看看如何用C++来做：
ddrval=lpDD->SetDisplayMode(ScreenX,ScreenY,ScreenBpp);
注意vtable不再显式地使用，C++自动地将对象（lpDD）作为第一个参数。不需要this指针，因为C++用指向当前对象的指针（本例中为lpDD）隐式地执行该方法。
在使用DirextDraw时，需要首先创建一个对象DirectDraw的实体，该对象实体代表了微机显示适配器。然后，使用接口所提供的方法来操作该对象实体，使之完成有关命令和任务。接着，你还需要创建一个或多个DirectDraw-surface对象的实体，以便能在图形表面(Surface)上展示你的游戏画面。
下面，在例程DDEX1中展示如何使用Directx3SDK来DirectDraw对象实体，如何创建一个带有后台缓冲区的基本表面(Surface)，以及如何弹出表面(Surface)。
注意:所有的例程都是用C++写成的，如果你的编辑器是C，你需要在文件中作出某些改动(至少，你要加入Vtable和指向各种接口方法的this指针)。
DirectDraw初始化:
DirectDraw 初始化代码写在dolnit函数中。
/**CreatethemainDirectDrawobject.*/
ddrval=DirectDrawCreate(NULL,&lpDD,NULL);
if(ddrval==DD_OK)
{kk1}
//Getexclusivemode.
ddrval=lpdd->SetCooperativeLevel(hwnd,
DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN);
if(ddrval==DD_OK)
{kk1}
//Createtheprimarysurfacewith1backbuffer.
ddsd.dwSize=sizeof(ddsd);
ddsd.dwFlags=DDSD_CAPS\DSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE|
DDSCAPS_FLIP|
DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount=1;
ddrval=lpDD->CreateSurfae(&ddsd,&lpDDSPrimary,
NULL);
if(ddrval==DD_OK)
{kk1}
//Cetapointertothebackbuffer.
ddscaps.dwCaps=DDSCAPS_BACKBUFFER;
ddrval=lpDDSPrimary->GetAttachedSurface(&ddscaps,&lpDDSBack);
if(ddrval==DD_OK)
{kk1}
//Drawsometext.
if(lpDDSPrimary->GetDC(&hdc)==DD_OK)
{kk1}
SetBkColor(hdc,RGB(0,0,255));
SetTextColor(hdc,RGB(255,255,0));
TextOut(hdc,0,0,sxFrontMsg,lstrlen(szFrontMsg));
lpDDSPrimary->ReleaseDC(hdc);
}
if(lpDDSBack->GetDC(&hdc)==DD_OK)
{kk1}
SetBkColor(hdc,RGB(0,0,255));
SetTextColor(hdc,RGB(255,255,0)):
TexOut(hdc,0,0,szBackMsg,lstrlen(szBackMsg));
lpDDSBack->ReleaseDC(hdc);
}
//Createatimertoflopthepages.
if(SetTimer(hwnd,TIMER_ID,TIMER_RATE,NULL))
{kk1}
returnTRUE;
}}}}}
wsprintf(buf,"DirectDrawInitFailed(%08lx)\n",ddrval);
以下针对初始化DirectDraw对象和准备表面(Surface)集的各个步骤分别进行讨论。
为了创建一个DirecDraw对象实体，你应该在程序中使用DirectDrawCreateAPI函数(注意:这里我所说的是应该，而不是必须)，这是因为使用OLE中的CoCreatelnstance函数也能创建一个DirectDraw对象实体，但这不在我们的讨论范围之中)。DirectDrawCreate采用全球统一的标准，它代表显示设备，这些显示设备在大多数情况下被定为NULL(即:系统使用缺省的显示设备)。当DirectDraw对象实体创建好后，就会有一个指针指向该对象实体。而且，在调色板中有三分之一的指针指向NULL(这样做的目的是为了今后的扩展)。
接下来的例子说明如何创建一个DirectDraw对象，并判别该对象是否创建成功:
ddrval=directDrawCreat(NULL，&lpDD,NULL);
if(ddrval==DD_OK)
{kk1}
//lpDDisavalidDirectDrawobject.
}
else
{kk1}
//DirectDrawobjectcouldnotbecreated.
}
使用IDirectDraw2和IDirectDrawSurface2接口
在你读本文的其他部份时，你会注意到所有的例程都使用的是IDirectDraw和IDirectDrawSurface的老版本接口。这是因为DirectX3SDK所给出的例程还没有来及使用IDirectDraw和IDirectDrawSurface更新后的接口。你可以通过调用IDirectDraw::QueryInterface方法来得到IDirectDraw2和IDirectDrawSurface2接口。
下面的代码给出如何得到IDirectDraw2接口:
//CreateanIDirectDraw2interface.
LPDIRECTDRAWlpDD;
ddrval=DirectDrawCreate(NULL,&lpDD,NULL);
if(ddrval!=DD_OK)
return;
ddrval=lpDD->SetCooperativeLevel(hwnd,DDSCL_NORMAL);
if(ddrval!=DD_OK)
return;
ddrval=lpDD->QueryInterfave(IID_IDirectDraw2,(lPVOID*)&lpDD2);
if(ddrval!=DD_OK)
return;
下面的代码给出如何得到IDirectDrawSurface2接口:
LPDIRECTDRAWSURFACElpSurf;
LPDIRECTDRAWSURFACElpSurf2;
//Createsurfaces.
memset(&ddsd,0,sizeof(ddsd));
ddsd.dwSize=sizeof(ddsd);
ddsd.dwFlags=DDSD_CAPS|DDSD_WIDTH|DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps=DDSCAPS_OFFSCREENPLAIN|
DDSCAPS_SYSTEMMEMORY;
ddsd.dwWidth=10;
ddsd.dwHeight=10;
ddrval=lpDD2->CreateSurface(&ddsd,&lpSurf,NULL);
if(ddrval!=DD_OK)
return;
ddrval=lpSurf->QueryInterface(
IID_IDirectDrawSurface2,(LPVOID*(&lpSurf2);
if(ddrval!=DD_OK)
return;
返回到上面