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

一步步构建自己的 3D 图形引擎

starIconstarIconstarIconstarIconstarIcon

5.00/5 (23投票s)

2017年2月1日

CPOL

6分钟阅读

viewsIcon

48120

downloadIcon

1908

如何从零开始一步步构建自己的 3D 图形引擎

或者通过 nuget 包安装此 sciBASIC# 框架

# https://nuget.net.cn/packages/sciBASIC#
# For .NET Framework 4.6:
PM> Install-Package sciBASIC -Pre

然后引用这些 DLL 文件

  • Microsoft.VisualBasic.Architecture.Framework_v3.0_22.0.76.201__8da45dcd8060cc9a.dll
  • Microsoft.VisualBasic.Imaging.dll
  • Microsoft.VisualBasic.Imaging.Drawing3D.Landscape.dll
  • Microsoft.VisualBasic.Mathematical.dll
  • Microsoft.VisualBasic.MIME.Markup.dll

背景 & 简介

所有数学科学库/环境,如 GNU plot、matlab、R、scipy,都有自己的 3D 引擎用于科学数据 3D 图。我最近的工作需要 3D 散点图来可视化我的实验数据,而我的 sciBASIC# 库还没有自己的 3D 图形引擎,所以我决定创建我自己的 3D 图形引擎来绘制我的实验数据。通过 Google 和 Wiki 搜索,我找到了所有必需的 3D 图形算法,并成功地基于 GDI+ 图形技术实现了这个 3D 引擎。

尽管这个基于 GDI+ 的 3D 图形引擎在模型过于复杂且有很多 3D 曲面要绘制时存在性能问题,但对于科学计算的 3D 图来说已经足够好了。例如,使用这个 3D 图形引擎生成这个 3D 函数图效果非常好。

在本文中,我想从零开始一步步介绍如何构建我自己的 3D 图形引擎,并且我将主要介绍 3D 图形算法和 3mf 3D 模型格式,这些都可以帮助您在未来实现自己的 3D 图形引擎。

3D 图形算法

3D 图形对象模型

首先,我想介绍我在自己的 3D 图形引擎中使用的两个重要对象。

Point3D

<XmlType("vertex")> Public Structure Point3D
    Implements PointF3D

    Public Sub New(x As Single, y As Single, Optional z As Single = 0)
        Me.X = x
        Me.Y = y
        Me.Z = z
    End Sub

    Public Sub New(Position As Point)
        Call Me.New(Position.X, Position.Y)
    End Sub

    <XmlAttribute("x")> Public Property X As Single Implements PointF3D.X
    <XmlAttribute("y")> Public Property Y As Single Implements PointF3D.Y
    <XmlAttribute("z")> Public Property Z As Single Implements PointF3D.Z

    Public Overrides Function ToString() As String
        Return Me.GetJson
    End Function
End Structure

表面

''' <summary>
''' Object model that using for the 3D graphics.
''' (进行实际3D绘图操作的对象模型)
''' </summary>
Public Structure Surface
    Implements IEnumerable(Of Point3D)
    Implements I3DModel

    ''' <summary>
    ''' Vertix in this list have the necessary element orders
    ''' for construct a correct closed figure.
    ''' (请注意,在这里面的点都是有先后顺序分别的)
    ''' </summary>
    Public vertices() As Point3D
    ''' <summary>
    ''' Drawing texture material of this surface.
    ''' </summary>
    Public brush As Brush

    Sub New(v As Point3D(), b As Brush)
        brush = b
        vertices = v
    End Sub
End Structure

基于这两个重要的数据结构,我们就可以在这个 3D 图形引擎中应用 3D 算法。以下是本文实现的 3D 图形算法:

  • 3D 旋转
  • 3D 到 2D 投影
  • 画家算法
  • 光源算法

3D 旋转

3D 旋转比 2D 旋转更复杂,因为我们必须指定一个旋转轴。在 2D 中,旋转轴始终垂直于 **x,y** 平面,即 Z 轴;但在 3D 中,旋转轴可以具有任意空间方向。我们将首先介绍绕三个主轴 **(X, Y, Z)** 的旋转,然后介绍绕任意轴的旋转。请注意,对于逆旋转:**将 q 替换为 -q,然后 R(R-1) = 1**。

Z 轴旋转

Z 轴旋转与 2D 情况相同。

' x' = x*cos q - y*sin q
' y' = x*sin q + y*cos q
' z' = z
'
'          | cos q  sin q  0  0|
' Rz (q) = |-sin q  cos q  0  0|
'          |     0      0  1  0|
'          |     0      0  0  1|

Public Function RotateZ(angle As Single) As Point3D
    Dim rad As Single, cosa As Single, sina As Single, Xn As Single, Yn As Single

    rad = angle * Math.PI / 180
    cosa = Math.Cos(rad)
    sina = Math.Sin(rad)
    Xn = Me.X * cosa - Me.Y * sina
    Yn = Me.X * sina + Me.Y * cosa
    Return New Point3D(Xn, Yn, Me.Z)
End Function

X 轴旋转

X 轴旋转看起来像 Z 轴旋转,如果我们替换

  • X 轴为 Y 轴
  • Y 轴为 Z 轴
  • Z 轴为 X 轴

因此,我们在方程中进行相同的替换。

' y' = y*cos(q) - z*sin(q)
' z' = y*sin(q) + z*cos(q)
' x' = x
'
'         |1      0       0   0|
' Rx(q) = |0  cos(q)  sin(q)  0|
'         |0 -sin(q)  cos(q)  0|
'         |0      0       0   1|

Public Function RotateX(angle As Single) As Point3D
    Dim rad As Single, cosa As Single, sina As Single, yn As Single, zn As Single

    rad = angle * Math.PI / 180
    cosa = Math.Cos(rad)
    sina = Math.Sin(rad)
    yn = Me.Y * cosa - Me.Z * sina
    zn = Me.Y * sina + Me.Z * cosa
    Return New Point3D(Me.X, yn, zn)
End Function

Y 轴旋转

Y 轴旋转看起来像 Z 轴旋转,如果我们替换

  • X 轴为 Z 轴
  • Y 轴为 X 轴
  • Z 轴为 Y 轴

因此,我们在方程中进行相同的替换。

' z' = z*cos(q) - x*sin(q)
' x' = z*sin(q) + x*cos(q)
' y' = y
'
'         |cos(q)  0  -sin(q)  0|
' Ry(q) = |    0   1       0   0|
'         |sin(q)  0   cos(q)  0|
'         |    0   0       0   1|

Public Function RotateY(angle As Single) As Point3D
    Dim rad As Single, cosa As Single, sina As Single, Xn As Single, Zn As Single

    rad = angle * Math.PI / 180
    cosa = Math.Cos(rad)
    sina = Math.Sin(rad)
    Zn = Me.Z * cosa - Me.X * sina
    Xn = Me.Z * sina + Me.X * cosa

    Return New Point3D(Xn, Me.Y, Zn)
End Function

3D 投影

https://en.wikipedia.org/wiki/3D_projection

要将 3D 点 ax, ay, az 投影到 2D 点 bx, by,使用平行于 y 轴的正交投影(侧视图),可以使用以下方程:

  • bx = sx * ax + cx
  • by = sz * az + cz

其中向量 s 是任意比例因子,c 是任意偏移量。这些常数是可选的,可以用来正确对齐视口。

''' <summary>
''' Project the 3D point to the 2D screen.
''' </summary>
''' <param name="x!"></param>
''' <param name="y!"></param>
''' <param name="z!">Using for the painter algorithm.</param>
''' <param name="viewWidth%"></param>
''' <param name="viewHeight%"></param>
''' <param name="fov%"></param>
''' <param name="viewDistance%">
''' View distance to the model from the view window.
''' </param>
Public Sub Project(ByRef x!, ByRef y!, z!,
                   viewWidth%,
                   viewHeight%,
                   viewDistance%,
                   Optional fov% = 256)

    Dim factor! = fov / (viewDistance + z)

    ' 2D point result (x, y)
    x = x * factor + viewWidth / 2
    y = y * factor + viewHeight / 2
End Sub

画家算法

画家算法,也称为优先级填充,是 3D 计算机图形学中可见性问题最简单的解决方案之一。当将 3D 场景投影到 2D 平面时,有时需要决定哪些多边形可见,哪些被隐藏。

“画家算法”这个名字指的是许多画家在绘画场景时,先画远处的部分,再画靠近的部分,从而覆盖了远处部分的一些区域。画家算法按深度对场景中的所有多边形进行排序,然后按此顺序(从最远到最近)绘制它们。它会覆盖通常不可见的部分 — 从而解决了可见性问题 — 但代价是绘制了远处物体不可见的部分。算法使用的顺序称为“深度顺序”,不一定需要遵守场景各部分与相机的数值距离:这种顺序的本质属性是,如果一个物体遮挡了另一个物体的一部分,那么第一个物体将在被遮挡的物体之后绘制。因此,有效的顺序可以描述为表示物体之间遮挡的定向无环图的拓扑排序。

实现画家算法的一个简单方法是使用 z-order 方法。

https://en.wikipedia.org/wiki/Z-order

''' <summary>
''' ``PAINTERS ALGORITHM`` kernel
''' </summary>
''' <typeparam name="T"></typeparam>
''' <param name="source"></param>
''' <param name="z">计算出z轴的平均数据</param>
''' <returns></returns>
<Extension>
Public Function OrderProvider(Of T)(source As IEnumerable(Of T), _
       z As Func(Of T, Double)) As List(Of Integer)
    Dim order As New List(Of Integer)
    Dim avgZ As New List(Of Double)

    ' Compute the average Z value of each face.
    For Each i As SeqValue(Of T) In source.SeqIterator
        Call avgZ.Add(z(+i))
        Call order.Add(i)
    Next

    Dim iMax%, tmp#

    ' Next we sort the faces in descending order based on the Z value.
    ' The objective is to draw distant faces first. This is called
    ' the PAINTERS ALGORITHM. So, the visible faces will hide the invisible ones.
    ' The sorting algorithm used is the SELECTION SORT.
    For i% = 0 To avgZ.Count - 1
        iMax = i

        For j = i + 1 To avgZ.Count - 1
            If avgZ(j) > avgZ(iMax) Then
                iMax = j
            End If
        Next

        If iMax <> i Then
            tmp = avgZ(i)
            avgZ(i) = avgZ(iMax)
            avgZ(iMax) = tmp

            tmp = order(i)
            order(i) = order(iMax)
            order(iMax) = tmp
        End If
    Next

    Call order.Reverse()

    Return order
End Function

显示设备

我创建了一个用于显示 3D 模型的 Winform 控件,它在 Microsoft.VisualBasic.Imaging.Drawing3D.Device.GDIDevice 命名空间中可用。下面是一个在 winform 中使用此 3D 模型显示控件的简单代码示例。

Dim project As Vendor_3mf.Project = Vendor_3mf.IO.Open(file.FileName)
Dim surfaces As Surface() = project.GetSurfaces(True)
Dim canvas As New GDIDevice With {
    .bg = Color.LightBlue,
    .Model = Function() surfaces,
    .Dock = DockStyle.Fill,
    .LightIllumination = True,
    .AutoRotation = True,
    .ShowDebugger = True
}

Call Controls.Add(canvas)
Call canvas.Run()

这个 3D 模型显示控件基于 GDI+ 图形引擎,因此如果模型过于复杂且有很多曲面要绘制,那么控件的渲染速度会非常慢。为了提高图形渲染性能,我使用了 2 个工作线程来进行 3D 图形显示。

  • 模型缓冲区线程
  • 图形渲染线程

缓冲区线程

Private Sub CreateBuffer()
    Dim now& = App.NanoTime

    With device._camera
        Dim surfaces As New List(Of Surface)

        For Each s As Surface In model()()
            surfaces += New Surface(.Rotate(s.vertices).ToArray, s.brush)
        Next

        If device.ShowHorizontalPanel Then
            surfaces += New Surface(
                .Rotate(__horizontalPanel.vertices).ToArray,
                __horizontalPanel.brush)
        End If

        buffer = .PainterBuffer(surfaces)

        If .angleX > 360 Then
            .angleX = 0
        End If
        If .angleY > 360 Then
            .angleY = 0
        End If
        If .angleZ > 360 Then
            .angleZ = 0
        End If

        Call device.RotationThread.Tick()
    End With

    debugger.BufferWorker = App.NanoTime - now
End Sub

这个模型缓冲区工作线程对模型中的所有曲面应用 3D 投影,并运行基于 Z-order 的画家算法,然后为渲染线程创建 2D 多边形缓冲区。以下是这个 2D 多边形缓冲区单元的定义。

''' <summary>
''' The polygon buffer unit after the 3D to 2D projection and the z-order sorts.
''' (经过投影和排序操作之后的多边形图形缓存单元)
''' </summary>
Public Structure Polygon

    ''' <summary>
    ''' The 3D projection result buffer
    ''' </summary>
    Dim points As Point()
    ''' <summary>
    ''' Surface fill
    ''' </summary>
    Dim brush As Brush
End Structure

图形渲染

光源

将光源应用于 3D 模型可以使我们的图形更自然,下面是一个应用光照效果的例子。

应用光源的一个简单算法是根据曲面的 Z-order 深度使其颜色变暗,这个 z-order 深度可以从画家算法中获得。

''' <summary>
''' Makes the 3D graphic more natural.
''' </summary>
''' <param name="surfaces">
''' Polygon buffer.(经过投影和排序之后得到的多边形的缓存对象)
''' </param>
''' <returns></returns>
<Extension>
Public Function Illumination(surfaces As IEnumerable(Of Polygon)) As IEnumerable(Of Polygon)
    Dim array As Polygon() = surfaces.ToArray
    Dim steps! = 0.75! / array.Length
    Dim dark! = 1.0!

    ' 不能够打乱经过painter算法排序的结果,所以使用for循环
    For i As Integer = 0 To array.Length - 1
        With array(i)
            If TypeOf .brush Is SolidBrush Then
                Dim color As Color = DirectCast(.brush, SolidBrush).Color
                Dim points As Point() = .points

                color = color.Dark(dark)
                array(i) = New Polygon With {
                    .brush = New SolidBrush(color),
                    .points = points
                }
            End If
        End With

        dark -= steps
    Next

    Return array
End Function
控件渲染

下面是渲染线程,它由一个刷新线程触发。

''' <summary>
''' Forces the Paint event to be called.
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub _animationLoop_Tick(sender As Object, e As EventArgs) Handles _animationLoop.Tick
    Call Me.Invalidate()
    Call Me.___animationLoop()
End Sub

当调用 winform 控件的 Me.Invalidate() 方法时,此方法调用将强制控件刷新自身并引发 Control.Paint 事件,然后我们就可以进行渲染工作了。

Private Sub RenderingThread(sender As Object, e As PaintEventArgs) Handles device.Paint
    Dim canvas As Graphics = e.Graphics
    Dim now& = App.NanoTime

    canvas.CompositingQuality = CompositingQuality.HighQuality
    canvas.InterpolationMode = InterpolationMode.HighQualityBilinear

    With device
        If Not buffer Is Nothing Then
            Call canvas.Clear(device.bg)
            Call canvas.BufferPainting(buffer, .drawPath, .LightIllumination)
        End If
        If Not .Plot Is Nothing Then
            Call .Plot()(canvas, ._camera)
        End If
        If device.ShowDebugger Then
            Call debugger.DrawInformation(canvas)
        End If
    End With

    debugger.RenderingWorker = App.NanoTime - now
End Sub

3mf 格式

最后,我们有了可以用于显示 3D 图形的所有元素。为了显示 3D 图形,我们必须将模型数据放入显示设备控件。我使用 **3mf** 模型文件作为我的 3D 引擎输入模型数据。以下是我加载 3mf 模型数据的方法。

打开 3mf 模型文件,只需两个简单的函数即可将 3D 模型加载到画布控件中。

Imports Microsoft.VisualBasic.Imaging.Drawing3D.Landscape

Dim project As Vendor_3mf.Project = Vendor_3mf.IO.Open(file)
Dim surfaces As Surface() = project.GetSurfaces(True)

3MF 是一种新的 3D 打印格式,它允许设计应用程序将全保真 3D 模型发送到各种其他应用程序、平台、服务和打印机。3MF 规范允许公司专注于创新,而不是基本互操作性问题,并且它经过工程设计,可以避免与其他 3D 文件格式相关的问题。

http://www.3mf.io/what-is-3mf/

由于 3MF 是一种基于 XML 的数据格式,用于增材制造,因此我们可以轻松地使用 Visual Basic 中的 XML 反序列化来加载 *.3mf 文件中的模型,并且用于此序列化操作的 XML 模型在命名空间:Microsoft.VisualBasic.Imaging.Drawing3D.Landscape.Vendor_3mf.XML 中可用。

Public Class Project

    ''' <summary>
    ''' ``*.3mf/Metadata/thumbnail.png``
    ''' </summary>
    ''' <returns></returns>
    Public Property Thumbnail As Image
    ''' <summary>
    ''' ``*.3mf/3D/3dmodel.model``
    ''' </summary>
    ''' <returns></returns>
    Public Property model As XmlModel3D

    Public Shared Function FromZipDirectory(dir$) As Project
        Return New Project With {
            .Thumbnail = $"{dir}/Metadata/thumbnail.png".LoadImage,
            .model = IO.Load3DModel(dir & "/3D/3dmodel.model")
        }
    End Function

    ''' <summary>
    ''' Get all of the 3D surface model data in this 3mf project.
    ''' </summary>
    ''' <param name="centraOffset"></param>
    ''' <returns></returns>
    Public Function GetSurfaces(Optional centraOffset As Boolean = False) As Surface()
        If model Is Nothing Then
            Return {}
        Else
            Dim out As Surface() = model.GetSurfaces.ToArray

            If centraOffset Then
                With out.Centra
                    out = .Offsets(out).ToArray
                End With
            End If

            Return out
        End If
    End Function
End Class
© . All rights reserved.