使用 VB.NET 实现基本的 DirectX 变换
使用 DirectX 和 VB.NET 在 2D 世界中应用简单的世界变换
引言
在本教程中,我们将尝试讨论 DirectX 中的基本变换。我们将只讨论世界变换,因为它最简单,但坦率地说,我不得不说这是一个复杂的主题,特别是如果你试图理解变换的底层概念,那么你将迷失在纯数学问题中。
因此,我不会讨论向量和矩阵的数学概念(因为我自己也不懂!),但我们将学习如何使用它们进行变换。
背景
本教程是我上一个教程《使用 Visual Basic 启动 DirectX》的延续,所以我假设你已经了解如何创建 DirectX 设备。
绘制一个正方形
“下载未完成的项目并开始操作。”
首先,我们将在变换之前绘制一个正方形。在你的类中声明这个变量
Dim buffer As VertexBuffer
在 creat_vertxbuffer Sub
中,写入
Sub creat_vertxbuffer()
buffer = New VertexBuffer(GetType(CustomVertex.PositionColored), 4, device, _
Usage.None, CustomVertex.PositionColored.Format, Pool.Managed)
Dim ver(3) As CustomVertex.PositionColored
ver(0) = New CustomVertex.PositionColored(-0.5F, -0.5F, 0, Color.Red.ToArgb)
ver(1) = New CustomVertex.PositionColored(-0.5F, 0.5F, 0, Color.Green.ToArgb)
ver(2) = New CustomVertex.PositionColored(0.5F, -0.5F, 0, Color.Blue.ToArgb)
ver(3) = New CustomVertex.PositionColored(0.5F, 0.5F, 0, Color.Yellow.ToArgb)
buffer.SetData(ver, 0, LockFlags.None)
'the next line is not important it's for performance:
buffer.Unlock()
End Sub
这里,我们在顶点中使用了 PositionColored
类型,而不是像之前的教程那样使用 TransformedColored
类型,因为我们想要对其应用变换。我们还使用了 4 个顶点来绘制一个正方形。
但问题是:我根据什么来确定顶点的位置?我想你对这些位置数据感到疑惑。这个 (0.5) 和 (-0.5) 是什么?你可能会说:在之前的教程中,事情很清楚,我们直接根据屏幕坐标(左上角的 (0,0) 点)绘制,但这正是区别所在!我们不是根据屏幕坐标绘制,而是根据视口坐标绘制。
视角
视口是 DirectX 投影(而不是绘制)场景的矩形,你可以将视口视为打开到场景中的窗口,如下所示
所以视口中的最大 X 值是 1,最小是 -1,Y 也一样,但是第三维 Z 呢?它在哪里?!它的最小和最大值是多少?
要了解 Direct3D 中 Z 轴的正方向,我们使用左手定则
如果你的左手指向 X 轴的正方向,手指指向 Y 轴的正方向,那么 Z 轴的正方向就是你的拇指方向,如下图所示
Z 的最小和最大值由你决定
Viewport.MinZ() as single
Viewport.MaxZ() as single
但默认值是 MinZ
= 0 和 MaxZ
= 1。任何超出此范围的值都将被裁剪(这是另一个主题!),但为了简化起见,我们将在本教程中完全忽略 Z 轴。
我们可以使用以下方法为设备设置视口
device.Viewport = ourViewport
此外,我们可以在场景中使用多个视口;目前我们将使用默认视口,即屏幕的大小。
另一件重要的事情是,视口维度不随屏幕维度或屏幕宽高比而改变,这意味着你的窗体是长方形还是正方形都无关紧要,并且在任何情况下,(-1,-1) 点位于视口的左下角,(1,1) 点位于视口的右上角。
主循环
我想在上面的解释之后,你已经想象出正方形的位置了,但你想看,而不是想象!好的,再写一些代码,你就能看到我们所做的一切了。与之前的教程不同,这里我们需要一个主循环,因为我们必须重复调用绘图例程,所以我们将其全部放入一个循环中,如下所示
initilizdx()
creat_vertxbuffer()
Me.Show()
Do While Me.Created
transform()
draw_me()
Application.DoEvents()
Loop
'end the program immediately to avoid any (null reference)error!
End
Application.DoEvents
在这个无限循环中是一个非常重要的过程。它让程序处理所有系统消息并返回循环,如果你不使用它,你的程序将不会响应任何消息(例如关闭)。
将上述代码块放入
事件中,并编写 LoadForm
draw_me Sub
Sub draw_me()
device.Clear(ClearFlags.Target, Color.Black, 0, 0)
device.BeginScene()
device.VertexFormat = CustomVertex.PositionColored.Format
device.SetStreamSource(0, buffer, 0)
device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2)
device.EndScene()
device.Present()
End Sub
这与之前的教程略有不同,因为我们在图元类型中使用了 TriangleStrip
,别担心我会解释,现在按 F5 看看结果。
一个漂亮的彩色正方形!
那么 TriangleStripe
是什么? TriangleStripe
方法从最后三个顶点形成一个三角形。如果你想要 2 个三角形,你应该像我们一样使用 4 个顶点,对于 3 个三角形使用 5 个顶点
三角形1(顶点1,顶点2,顶点3),三角形2(顶点2,顶点3,顶点4),三角形3 (顶点3,顶点4,顶点5)…
当 Ver = 顶点时
换句话说,三角形使用上一个三角形的最后一条边,导致任何连续的三角形都有共享边。
更好的做法是,你应该在循环中放置一个计时器,但为了简单起见,我们现在不使用计时器来编写它。
但是变换在哪里呢?我们已经从之前的教程中学到了如何绘制正方形和三角形,现在想让它移动。别担心,我们将学习如何做到这一点,但首先你应该了解变换的主要工具,矩阵。
注意:如果你对理论不感兴趣,请跳过以下部分,直接进入(实践)部分。
矩阵
这是任何图形和 3D 编程初学者的恐慌之源,而且确实很难理解。互联网上的大多数教程都没有解释矩阵的概念(特别是)矩阵乘法。它们说:这个矩阵,以及如何使用它,不要问任何问题!我也不例外!
我们知道,矩阵是按行和列排列的数字的矩形布局,m x n 矩阵是具有 m 行 n 列的矩阵,矩阵的元素由其行号和列号命名,例如 A2,3 表示 A 矩阵中第 2 行第 3 列的元素。
在上面的 A 矩阵中:A1,1 =2,A3,1 =12,A2,3 = 54。
在 3D 变换中,我们使用 4X4 矩阵来表示坐标系,我们称之为变换矩阵。如果我们要变换场景,我们应该将每个顶点乘以变换矩阵,我们也可以将我们的矩阵乘以另一个矩阵,从而产生协作效果。
换句话说,如果你将围绕 X 轴旋转的矩阵乘以围绕 Y 轴旋转的矩阵,结果是一个围绕 X 轴旋转,然后围绕 Y 轴旋转的矩阵。如果你反转操作,结果是一个围绕 Y 轴旋转,然后围绕 X 轴旋转的矩阵,因为矩阵乘法不具有交换性。
但是矩阵乘法是如何进行的呢?实际上,你不需要知道如何手动进行矩阵乘法。你可以简单地使用 (*) 运算符来完成(仅在托管 DirectX 中),或者你可以使用 Matrix. Multiply ()
函数,它是 Matrix
类中的一个共享 Sub
,但如果你决心理解如何进行矩阵乘法,你可以通过下面的简要描述在网上或内容中搜索。
如果 A、B 是矩阵,那么 A X B 只有在 A 的列数等于 B 的行数时才有效,结果矩阵的行数与 A 的行数相同,列数与 B 的列数相同。
通过将 A 中的每行乘以 B 中的每列来计算结果矩阵(更准确地说,是将矩阵 A 乘以 B 中的每个列向量),如下图所示
如图所示,将第 1 行乘以第 2 列的结果是一个数字,它存储在结果矩阵的元素 1,2 中。猜猜哪个元素保存了第 3 行 * 第 4 列的结果?
以下是我们如何将行乘以列
没明白?没问题。正如我上面所说,你不需要理解矩阵乘法,因为你可以简单地通过 * 运算符来完成!A*B = 结果。
回到我们的主题,如何在变换中使用这个矩阵,首先我们应该知道如何在矩阵中表示世界坐标。

这个用于变换的 4x4 矩阵
- X 矩形表示 X 轴的向量,通常是 (1, 0, 0)
- Y 矩形表示 Y 轴的向量,通常是 (0, 1, 0)
- Z 矩形表示 Z 轴的向量,通常是 (0, 0, 1)
- 而 G 矩形表示原点,通常是 (0, 0, 0)
- W 矩形在世界变换中没有用。
如果我们把这些值应用到一个 4x4 矩阵中

这个矩阵被称为单位矩阵,因为它与坐标系相同,这意味着任何通过这个矩阵变换的顶点或矩阵都不会改变。
你可能会想,我们怎么能把 3 个元素的向量乘以这个 4x4 矩阵呢?!如果你想手动乘,你必须使用 4 个元素的向量 (x,y,z,1),但如果你使用 Directx 变换(这是最好的),你不需要担心这个问题。
转换
最简单的变换,我们只改变原点

如你所见,要构建一个平移矩阵,你只需像这样改变原点

将 (x, y, z) 替换为你的平移偏移值。
要通过代码实现,在你的窗体类中声明 MatWorld
为 Matrix
Dim MatWorld As Matrix
在 sub
transform
中,写入以下内容
Sub transform()
' make MatWorld identity matrix:
MatWorld = Matrix.Identity
'set the number in row 4 , column 1:
MatWorld.M41 = 0.5
'set the number in row 4 , column 2:
MatWorld.M42 = 0.5
'assign MatWorld as world transformation matrix:
device.Transform.World = MatWorld
End Sub
在按下 F5 之前,你能想象出结果吗?正如你所注意到的,我们只改变了我们想要的值(M41,M42),其余的保持为单位矩阵。前面的代码构建了以下矩阵
1 | 0 | 0 | 0 |
0 | 1 | 0 | 0 |
0 | 0 | 1 | 0 |
0.5 | 0.5 | 0 | 1 |
DirectX 将通过这个世界矩阵变换每个顶点。
扩展
要构建一个缩放矩阵,你首先应该确定要缩放哪个轴,这里我们将在 y 和 x 轴上都进行缩放

要通过代码实现,将 transform sub
重写为如下所示
Sub transform()
' make MatWorld identity matrix:
MatWorld = Matrix.Identity
'set the number in row 1 , column 1:
MatWorld.M11 = 0.5
'set the number in row 2 , column 2:
MatWorld.M22 = 0.5
'assign MatWorld as world transformation matrix:
device.Transform.World = MatWorld
End Sub
因为我们的正方形很大,所以我们将其缩小了一半。按 F5。
旋转
旋转比之前的变换更复杂(尤其是在 3D 中)。正如我之前提到的,我们将忽略 Z 轴,因此我们的旋转应该围绕 Z 轴,以避免 Z 值发生任何变化。要通过角度 ? 围绕 Z 轴构建旋转矩阵,

要通过代码实现,请将 transform sub
重写如下
Sub transform()
' make MatWorld identity matrix:
MatWorld = Matrix.Identity
'set the number in row 1 , column 1 and column 2:
MatWorld.M11 = Math.Cos(Math.PI / 4) : MatWorld.M12 = Math.Sin(Math.PI / 4)
'set the number in row 2 , column 1 and column 2:
MatWorld.M21 = -Math.Sin(Math.PI / 4) : MatWorld.M22 = Math.Cos(Math.PI / 4)
'assign MatWorld as world transformation matrix:
device.Transform.World = MatWorld
End Sub
这段代码构建了一个围绕 Z 轴旋转 45 度的矩阵。这是我们构建的矩阵
Cos(p/4) | sin(p /4) | 0 | 0 |
-sin(p/4) | cos(p/4) | 0 | 0 |
0 | 0 | 1 | 0 |
0 | 0 | 0 | 1 |
实践
如果你不理解前面的部分(或者根本没读),没问题,因为我们通常使用 Direct3D 提供的矩阵函数来构建变换矩阵。下面是构建旋转矩阵的方法
Sub transform()
' make MatWorld identity matrix:
MatWorld = Matrix.Identity
'make MatWorld as rotation matrix:
MatWorld.RotateZ(Math.PI / 4)
'or you can use the alternative :
' MatWorld = Matrix.RotationZ(Math.PI/4)
'assign MatWorld as world transformation matrix:
device.Transform.World = MatWorld
End Sub
很简单!
您可以使用其他矩阵函数
'for translation :
MatWorld.Translate()
'for scaling :
MatWorld.Scale()
'for rotation around other axes:
MatWorld.RotateX()
MatWorld.RotateY()
问答
问:我如何才能使物体围绕自身旋转,而不是围绕原点旋转?
答:最佳实践是:在原点绘制你的物体,旋转它们,然后将它们平移到它们的位置,就像这个例子一样
'declare this in your form class:
Dim matrix2 As Matrix
Sub transform()
'make MatWorld as rotation matrix:
MatWorld.RotateZ(Math.PI / 4)
'make matrix2 as translation matrix:
matrix2.Translate(0.5, 0.5, 0)
'multiply MatWorld by matrix2:
MatWorld = MatWorld * matrix2
'assign MatWorld as world transformation matrix:
device.Transform.World = MatWorld
End Sub
问:如果我写成 matrix2 * MatWorld
会发生什么?
答:Matworld * matrix2
意味着先旋转,然后平移,而 matrix2 * MatWorld
意味着先平移,然后旋转。
问:我想让我的正方形每帧都保持旋转,怎么做?
答:你必须在每一帧中保持旋转你的矩阵,像这样
'first make MatWorld identity matrix in the declaration:
Dim MatWorld As Matrix = Matrix.Identity
Sub transform()
'rotate MatWorld by a rotation matrix:
MatWorld = MatWorld * Matrix.RotationZ(0.01)
'make matrix2 as translation matrix:
matrix2.Translate(0.5, 0.5, 0)
device.Transform.World = MatWorld * matrix2
End Sub
或者您可以每次都更改角度。
问:在进行任何操作之前,将矩阵设置为单位矩阵是否重要?
答:不,但是当你声明一个新矩阵时,它的所有元素都为零,如果你将新矩阵乘以任何其他矩阵,结果也是零矩阵,所以我们在任何操作之前将其设置为单位矩阵,但是如果你使用 Matrix.rotate
(例如),这个函数是构建一个旋转矩阵,而不是旋转矩阵,所以在使用这个函数之前你不需要将其设置为单位矩阵。
问:我想绘制多个对象并对每个对象应用不同的变换,该怎么做?
答:请看这个例子
首先删除 transform sub
,并在 draw_me
中应用变换
'declare these in form class
Dim matrix1 As Matrix = Matrix.Identity
Dim matrix2 As Matrix = Matrix.Identity
Dim matrix3 As Matrix = Matrix.Identity
Sub draw_me()
device.Clear(ClearFlags.Target, Color.Black, 0, 0)
device.BeginScene()
device.VertexFormat = CustomVertex.PositionColored.Format
device.SetStreamSource(0, buffer, 0)
'rotate matrix2 and matrix3 by different rotation matrices:
matrix2 = matrix2 * Matrix.RotationZ(0.01)
matrix3 = matrix3 * Matrix.RotationZ(-0.01)
'make matrix1 a translation matrix:
matrix1 = Matrix.Translation(0.3, 0.3, 0)
'rotate then translate:
device.Transform.World = matrix2 * matrix1
'draw the first square:
device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2)
'make matrix1 as translation matrix but in different direction this time:
matrix1.Translate(-0.3, -0.3, 0)
'rotate then translate:
device.Transform.World = matrix3 * matrix1
'draw the second square:
device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2)
device.EndScene()
device.Present()
End Sub
问:你为什么忽略第三维度?我可以用 GDI 完成所有这些工作!
答:好的,我说过我们为了简化目的而忽略了第三维度,但还有另一个原因。正如你所知,DirectX 中没有真正的 3D,因为屏幕上渲染的最终图像是 2D 的,为了模拟这种 3D 效果,我们调整了投影矩阵。
device.Transform.Projection
你应该调整这个投影矩阵,使远处的物体变小,近处的物体变大,以模拟真实的眼睛观察真实世界。但我认为你了解变换的基础知识就足够了,也许将来我能向你解释其他类型的变换。
结论
我制作了一个完整的项目,作为我们学过的所有类型变换的示例。
希望我写了一些有用的东西。抱歉我的英语不好!我通常用谷歌翻译来写作,但我好几天没有网络连接,所以用了一个糟糕的手机词典!
世界变换是最简单的变换,还有视图变换和投影变换,它们更复杂,初学者通常不会关心去理解它们。如果有人感兴趣,我可以再写一个教程来解释如何使用这些变换。
历史
- 2011年3月15日:首次发布