上一次我们在Managed DirectX世界里接触到了Direct3D设备的概念,它是Direct3D中几乎所有功能的开始。从这次我要开始在DirectX中绘出3D图形了。3D图形常常用一组包围形状表面的多边形来表示,无论形状多么复杂,我们都可以用一定数目的多边形来逼近。目前用于游戏的显卡多使用三角形网格系统,即用一系列三角形来表示物体的表面。在电影级渲染中,还常常用到四边形网格。关于网格(mesh)这个重要的概念以后还会遇到,这次我将学习如何绘制三角形。
Direct3D支持的图元(Primitive)类型有点、线段、线条、三角、成片三角和三角扇形等。要绘制图元,必须将点集载入到数据流中。顶点缓冲区(Vertex Buffer)即是DirectX保存顶点数据的常用方式。在顶点缓冲区内我们可以对顶点进行各种操作,比如坐标变换、光照等。Direct3D支持灵活顶点格式(FVF),用户可以自行决定在一个顶点数据中包含那些数据,比如顶点的坐标、颜色、法向量等。可以自由搭配各种属性,以至于我们可用自定义的结构体来保存顶点数据。这次我选择的是一种已经预定义在DirectX类库中的类型:CustomVertex::TransformedColored。它包含顶点的坐标、经变换后的坐标以及顶点的漫反射颜色。
创建顶点缓冲区需要使用VertexBuffer类。要定义一个顶点缓冲区,需要下列参数:顶点的结构体类型、个数、设备、顶点缓冲区属性、顶点格式和顶点缓冲区的资源位置。顶点的结构体类型是一个System::Type类型参数(主要是获取顶点结构体的内存大小)。顶点缓冲区属性可以是Usage枚举中的任意值的组合,我这次就用None。顶点格式是对于灵活顶点格式的描述,必须准确指出顶点结构体中包含哪些数据。最后一个顶点缓冲区的资源位置表示将顶点缓冲区置于内存还是显存中。基于这些设置,就可以写出一个初始化顶点缓冲区的函数:
//顶点缓冲区全局对象
static VertexBuffer^ vb;
static Boolean InitializeVertexBuffer()
{
//初始化一个三角形的顶点数组
array<CustomVertex::TransformedColored>^ vertices =
gcnew array<CustomVertex::TransformedColored>(3);
vertices[0] = CustomVertex::TransformedColored(150.0, 50.0, 0.5, 1.0, 0xffffff00);
vertices[1] = CustomVertex::TransformedColored(250.0, 250.0, 0.5, 1.0, 0xff00ff00);
vertices[2] = CustomVertex::TransformedColored(50.0, 250.0, 0.5, 1.0, 0xff0000ff);
try
{
//创建顶点缓冲区
vb = gcnew VertexBuffer(
typeid<CustomVertex::TransformedColored>,
3, /* 顶点个数 */
d3dDevice, /* 要使用的Direct3D设备 */
Usage::None, /* 顶点缓冲区属性 */
CustomVertex::TransformedColored::Format, /* 灵活顶点格式 */
Pool::Default /* 顶点缓冲区资源位置,这里选择显存 */
);
//填充顶点缓冲区
GraphicsStream^ gs = vb->Lock(0, 0, LockFlags::None);
gs->Write(vertices);
vb->Unlock();
return true;
}
catch(DirectXException^)
{
return false;
}
}
别忘了在CleanUp方法中清理使用过的VertexBuffer对象。接下来,我们需要在渲染函数中增加绘制图元的代码。注意:这些代码一定要放在对设备BeginScene函数和EndScene函数的调用之间,否则就会出错。
d3dDevice->SetStreamSource(0, vb, 0);
d3dDevice->VertexFormat = CustomVertex::TransformedColored::Format;
d3dDevice->DrawPrimitives(PrimitiveType::TriangleList, 0, 1);
最后,我们只要在main函数中添加调用创建顶点缓冲区函数的代码即可:
int Main()
{
TestMDXForm^ f = gcnew TestMDXForm();
if (DirectXProgram::InitializeDirect3D(f) &&
DirectXProgram::InitializeVertexBuffer())
{
//显示窗口
f->Show();
Application::DoEvents();
Application::Run();
}
return 0;
}
其他的代码与上次的项目完全相同。运行看看绘制出来的效果吧:

从本篇起,我将进入DirectX图形和游戏的世界。更多Managed DirectX的信息,可以参考IceShark的Blog文章,我这里简单提一下我的感受。Managed DirectX是对DirectX大部分功能的托管封装,可以用任何支持.NET的语言开发。MDX只对DirectX做了非常低层次的封装,因此保持了用COM接口DirectX开发时的大部分原貌。MDX的性能是不用担心的,因为它还是象COM DirectX一样提供对硬件层次的访问,大部分功能都是在你的显卡/网卡/声卡上起作用的,托管部分只是它的接口。事实证明MDX在DirectX Graphics中的性能与COM接口的DirectX不相上下。既然MDX的开发方式、API和性能都与COM接口的DirectX差不多,那为什么要用MDX呢?我自己对这个问题的回答是:
1、托管代码的对象模型更好。MDX基于类库的组织结构,比用COM接口的处理方式更方便。
2、用MDX,一般不用操心资源释放的问题。很大一部分资源释放的操作,都被封装好了。
3、与更多现代技术结合得更好。我们可以让DirectX程序使用XML、WebService和智能客户端等技术。
Direct3D程序最基本的流程是:创建Windows窗口、创建设备、处理消息循环、物体图形显示、退出和清理。在MDX的世界里,窗口创建和处理消息循环我们交给Windows Forms模型来做,剩下最主要的任务就是创建设备和物体图形显示。Direct3D设备是Direct3D开发最基本的入口,它定义了Direct3D所有的绘图组件,大部分的操作都需要从Direct3D设备开始。创建设备需要设定几个参数,包括显卡序号、设备类型、所属窗口、3D运算方式等。除了这些信息外,还需要一个PresentParameters类型的参数,其中定义了Direct3D设备所需的相关信息。下面代码中的InitializeDirect3D函数完整的演示了创建设备的步骤
设备类型:Direct3D支持3种设备,其中HAL和REF最为重要。HAL通过硬件进行光栅化、坐标变换和光照处理等,速度最快。REF则是用软件实现相关的操作,仅用于硬件不支持某种操作的情况。DeviceType枚举定义了设备类型可能的选项。
3D运算方式包括一些选项,如HardwareVertexProcessing,PureHardwareVertexProcessing等,用于指定顶点运算由硬件执行还是软件执行等。
PresentParameters还包括一些设置,如后台缓冲区的高度、宽度和像素格式,以及从后台缓冲区复制到前台缓存屏幕显示的方式等等。如果Direct3D采用窗口方式运行,像素格式必须查询当前的显示模式获得。
Direct3D设备创建成功以后,就可以进入图形显示阶段。在下面的代码中以Render()函数的形式出现。在绘制图形前,需要调用Device::Clear()函数重制ViewPort的颜色缓冲区。ViewPort就是3D形状投射到平面显示器上供我们看到的那个区域。Clear函数的Flag参数指定了对颜色缓冲区、深度缓冲区还是模板缓冲区进行初始化。因为我用ATI显卡,所以我选择了将颜色缓冲区初始化为红色:)。接下来是调用BeginScene()函数和EndScene()函数。实际的绘图中,所有渲染的代码都必须放在BeginScene函数和EndScene函数之间,否则就会出错。最后调用Present()函数,将后台缓冲区中的数据复制到前台缓冲区,我们就能看见图形了。
下面就是完成这个步骤所需的类。我将其定义为sealed,并将构造函数私有。这样做是为了让这个类仅仅成为我所用静态函数的容器。(在C#中,可以用静态类;在VB中,可以用模块)。
public ref class DirectXProgram sealed
{
private:
DirectXProgram(void)
{
}
public:
//Direct3D 设备全局对象
static Device^ d3dDevice;
static Boolean InitializeDirect3D(Form^ window)
{
//获取显示适配器信息,以便查询显示模式信息
AdapterListCollection^ adapters = Manager::Adapters;
DisplayMode d3ddm = adapters->Default->CurrentDisplayMode;
//创建设备所需的参数
PresentParameters^ params = gcnew PresentParameters();
params->Windowed = true;
params->SwapEffect = SwapEffect::Discard;
params->BackBufferFormat = d3ddm.Format;
try
{
d3dDevice = gcnew Device(0,
DeviceType::Hardware,
window,
CreateFlags::HardwareVertexProcessing,
params);
return true;
}
catch(DirectXException^)
{
return false;
}
}
static void Render()
{
if(!d3dDevice)
{
return;
}
//后台缓冲区设置为红色
d3dDevice->Clear(ClearFlags::Target, Color::Red, 1.0f, 0);
d3dDevice->BeginScene();
//在这里加入图形绘制程序
d3dDevice->EndScene();
d3dDevice->Present();
}
static void CleanUp()
{
if(d3dDevice)
{
d3dDevice->Dispose();
}
}
};
这是DirectXProgram.h的完整代码
我们还需要一个窗体类。添加一个空的Windows Form,除了大小之外无需其他的设置。我们所要做的就是重写它的OnPaint方法,在窗体重绘时调用Render方法绘制3D图形:
void OnPaint(PaintEventArgs^ e)override
{
DirectXProgram::Render();
}
此外我们还应该在它的Dispose方法中增加对Direct3D设备资源释放的代码:
void Dispose(Boolean disposing)
{
if (disposing && components)
{
components->Dispose();
DirectXProgram::CleanUp();
}
__super::Dispose(disposing);
}
这是Form1.h的完整代码
最后,我们在main函数中完成整个流程的控制:
[STAThreadAttribute]
int Main()
{
TestMDXForm^ f = gcnew TestMDXForm();
if (DirectXProgram::InitializeDirect3D(f))
{
//显示窗口
f->Show();
Application::DoEvents();
Application::Run(f);
}
return 0;
}
其中程序,我们可以看到我们设置的颜色缓冲区颜色。

这个就是Direct3D程序最基本的结构。与普通的Windows绘图程序相比,程序还是有点特别的,呼呼。
前一阶段微软推出了SQL Server 2005 Express版的10月CTP版,它要求新的Framework与之配合,但安装了新版的Framework 2.0以后,原来BETA1的Visual Studio Express版无法使用了。现在微软推出了与新版Framework相匹配的Visual Studio Express 10月CTP系列,包括:
Visual Basic 2005 Express
Visual C# 2005 Express
Visual C++ 2005 Express
Visual J# 2005 Express
Visual Web Developer 2005 Express
下载地址: http://lab.msdn.microsoft.com/vs2005/downloads/default.aspx
本来计划只在Visual Basic 2005中提供的功能——
Edit and Continue,现在正式宣布在Visual C# 2005中提供。C#的用户将不必等到下一个版本的Visual C#。
Somasegar在他的Blog中
宣布了这一消息。Edit and Continue是在Visual Studio 2005 BETA反馈中心中最热门的要求,开发者强烈要求C#提前获取这一特性。排在其次的两个要求是从VB2005中去除My.Forms提供的默认实例,和在VS2005中提供与Windows XP风格相符的新图标。
最近拖欠着一系列的文章实在太久了,其实我一直惦记着这件事,可是如下两个原因让我不能如期完成:
1、开始进入我不熟悉的领域,比如Assembly的新功能,泛型和其他我没有接触过的技术。实在不敢把我这点粗略的学习笔记公开出来了。
2、Visual Basic面临在BETA2时的多种变更。许多My的行为和泛型、重构、智能感知等功能都可能会改变。
所以在BETA2之前,我不再写这一系列的文章,当然我对Visual Basic/Visual Studio 2005的研究不会就此中止。最近写点什么呢?DirectX是一个不错的选择……
在学习多线程开发的时候,我们总是要处理访问同步的问题。SyncLock提供了一种给对象加锁同步的方法,可以确保同一个代码段不被多个线程同时执行。而在多线程访问中访问类的字段,仍然会出现读写不同步的问题。在需要让多线程,操作系统或硬件来访问某字段时,我们希望这些访问能够读取到字段最新的值,同时写到变量的操作能够立即实现。有人说,默认难道不是这样吗?CLR有时会对字段访问进行优化,比如他察觉到你的代码没有修改字段的值,就有可能在你访问字段时提供上次访问的缓存值。这能够提高程序的效率,但CLR对多线程环境的判断就没有那么准确了,也许这个线程没有改变字段的值,但另一个线程抢占了CPU并修改了它呢。C#对此情况提供了volatile关键字,声明字段的时候如果加上了volatile,那么对该字段的任何请求,包括读和写操作,都会立刻得到执行。将多个线程要访问的字段声明成volatile是非常有效的手段,但Visual Basic却没有提供这一机制。所以我们只能从读写上做文章:
方法一:VolatileRead和VolatileWrite
System.Threading.Thread提供了两个方法——VolatileRead和VolatileWrite。这两个方法就能确保对变量/字段的访问立即执行。他们对很多类型进行了重载,包括许多基本值类型,指针和Object类型等。我们挑其中一个看看:
Public Shared Function VolatileRead(ByRef address As Integer) As Integer
Public Shared Sub VolatileWrite(ByRef address As Integer, ByVal value As Integer)
我们看到名为address的变量都是ByRef传递的,因为ByVal在此没有意义(它本身就会对变量执行一次读取)。我们只需将这两个函数用在对变量读写的地方即可。比如:
a = Thread.VolatileRead(Me.MyField) '取代a = Me.MyField的写法
Thread.VolatileWrite(Me.MyField, b) '取代Me.MyField = b的写法
在需要将字段值传递给Windows或硬件进行读写时,最好确保每次访问都用VolatileRead或VolatileWrite方法实现。但有时候读写操作是发生在外部的,比如用ByRef传递了字段给某Windows组件等等,我们很难控制外部组件的行为,于是就只能用下面的方法。
方法二:MemoryBarrier
System.Threading.Thread.MemoryBarrier可以让内存立刻同步,即让所有缓存内容都写到主内存中。在执行此方法后,读取变量便可读取到变量内储存的真实值;而在写操作后执行此方法,就可以确保内容正确写入变量中。所以我们要做的就是:在读取前和写入后执行Thread.MemoryBarrier方法。
Thread.MemoryBarrier()
a = Me.MyField
Me.MyField = b
Thread.MemoryBarrier()
有了Thread的这两个方法,我们在多线程开发中就又多了一样利器,更好地解决了对字段访问的同步问题。
泛型类的每个构造类共享同一套静态成员还是每个构造类独有一套?这个问题可以用下面的代码来说明:
Public Class C(Of T)
Public Shared i As Integer = 0
Public Sub New()
i += 1
End Sub
End Class
'测试代码
Dim o1 As New C(Of Integer)
Debug.Print(C(Of Integer).i)
Debug.Print(C(Of Double).i)
Dim o2 As New C(Of Double)
Debug.Print(C(Of Integer).i)
Debug.Print(C(Of Double).i)
所得的结果是1,0,1,1。可见泛型类的各个构造类的静态成员是独立的,各个构造类型都像是各自定义的类型一样。那么构造类的静态构造函数是什么时候发生的呢?为了研究这个问题,我编写了如下类:
Public Class Class1(Of T)
Shared Sub New()
Debug.Print("Hi, I'm the constructed type of " & GetType(T).Name)
End Sub
End Class
这样我可以在输出窗口察觉静态构造函数执行的实际。我首先试验了下面的代码:
Dim o1 As New Class1(Of Integer)
结果按钮按下的一瞬间,就看到静态构造函数执行了。说明静态构造函数可能是在第一次正式使用构造类型的时候发生的。为了验证这个观点,我还做了这个试验:
Dim o1 As Class1(Of Integer)
'浪费时间的代码,略……
o1 = New Class1(Of Integer)
MsgBox(Class1(Of Integer).w.ElapsedMilliseconds)
结果静态构造函数的确是延后执行的。这说明声明一个构造类型的引用变量并不能导致该构造类型的静态构造函数运行,只有实际使用这个类型才会导致静态构造函数运行。当然,使用构造类型的任何静态成员也会导致他的静态构造函数立即执行,这一点也在我的后续试验中证明了。
那么若是值类型会怎样呢?我将Class简单地改为Structure,并进行了完全同样的试验。结果静态构造函数竟然不能按时发生。泛型值类型的静态构造函数一定要等到实际使用其构造类型静态成员的时候才会发生,这是泛型Structure与Class的一大不同点。