65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 VB.NET 实现基本的 DirectX 变换

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (9投票s)

2011年3月15日

CPOL

11分钟阅读

viewsIcon

46048

downloadIcon

2443

使用 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 投影(而不是绘制)场景的矩形,你可以将视口视为打开到场景中的窗口,如下所示

viewport.png

所以视口中的最大 X 值是 1,最小是 -1,Y 也一样,但是第三维 Z 呢?它在哪里?!它的最小和最大值是多少?

要了解 Direct3D 中 Z 轴的正方向,我们使用左手定则
如果你的左手指向 X 轴的正方向,手指指向 Y 轴的正方向,那么 Z 轴的正方向就是你的拇指方向,如下图所示

systemcordinates.png

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 列的元素。

matrix.png

在上面的 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 中的每个列向量),如下图所示

matrixmultiplication.png

如图所示,将第 1 行乘以第 2 列的结果是一个数字,它存储在结果矩阵的元素 1,2 中。猜猜哪个元素保存了第 3 行 * 第 4 列的结果?
以下是我们如何将行乘以列
columnXrow.png

没明白?没问题。正如我上面所说,你不需要理解矩阵乘法,因为你可以简单地通过 * 运算符来完成!A*B = 结果。
回到我们的主题,如何在变换中使用这个矩阵,首先我们应该知道如何在矩阵中表示世界坐标。

systemcoordinateinmatrix2.png

这个用于变换的 4x4 矩阵

  • X 矩形表示 X 轴的向量,通常是 (1, 0, 0)
  • Y 矩形表示 Y 轴的向量,通常是 (0, 1, 0)
  • Z 矩形表示 Z 轴的向量,通常是 (0, 0, 1)
  • 而 G 矩形表示原点,通常是 (0, 0, 0)
  • W 矩形在世界变换中没有用。

如果我们把这些值应用到一个 4x4 矩阵中

identityMatrix.png

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

转换

最简单的变换,我们只改变原点

translation.png

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

translationMatrix.png

将 (x, y, z) 替换为你的平移偏移值。
要通过代码实现,在你的窗体类中声明 MatWorldMatrix

  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 轴上都进行缩放

scaling.gif

要通过代码实现,将 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 轴构建旋转矩阵,

rotation.png

要通过代码实现,请将 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日:首次发布
© . All rights reserved.