Or install this sciBASIC#
framework via nuget package:
PM> Install-Package sciBASIC -Pre
And then reference to these DLL files:
- 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
Background & Introduction
All of the Mathematical Sciences library/environment, like the GNU plot, matlab, R, scipy, have their own 3D engine for the scientific data 3D plots. My recent work required of the 3D scatter plot for visualizing my experiment data, and my sciBASIC#
library didn't have its own 3D graphic engine yet so I decided to create my own 3D graphic engine for my experiment data plots. For search on the Google and wiki, I found all the necessary algorithms for the 3D graphic and implemented this 3D engine based on the GDI+ graphic techniques successfully.
Although this GDI+ based 3D graphic engine has the performance problem when the model is too complicated and has a lot of 3D surface to draw, but it is good enough for the 3D plots for the scientific computing. For example, using this 3D graphic engine works perfectly for generating this 3D function plot:
In this article, I want to introduce how to build my own 3D graphic engine from ZERO step by step, and I want to mainly introduce the 3D graphic algorithm and the 3mf 3D model format that can help you to implement your own 3D graphic engine in the future.
The 3D Graphics Algorithm
The 3D Graphic Object Models
First of all, I want to introduce the 2 important objects that I use in my own 3D graphic engine.
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
Surface
Public Structure Surface
Implements IEnumerable(Of Point3D)
Implements I3DModel
Public vertices() As Point3D
Public brush As Brush
Sub New(v As Point3D(), b As Brush)
brush = b
vertices = v
End Sub
End Structure
And based on these two important data structures, we are able to apply the 3D algorithm in this 3D graphic engine. And here is the 3D graphic algorithm that I implemented in this article:
- 3D rotation
- 3D to 2D projection
- The painter algorithm
- The light source algorithm
The 3D Rotation
3D Rotation is more complicated than 2D rotation since we must specify an axis of rotation. In 2D, the axis of rotation is always perpendicular to the x,y plane, i.e., the Z axis, but in 3D, the axis of rotation can have any spatial orientation. We will first look at rotation around the three principle axes (X, Y, Z) and then about an arbitrary axis. Note that for Inverse Rotation: replace q with -q and then R(R-1) = 1.
Z-Axis Rotation
Z-axis rotation is identical to the 2D case:
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-Axis Rotation
X-axis rotation looks like Z-axis rotation if we replace:
- X axis with Y axis
- Y axis with Z axis
- Z axis with X axis
So we do the same replacement in the equations:
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-Axis Rotation
Y-axis rotation looks like Z-axis rotation if we replace:
- X axis with Z axis
- Y axis with X axis
- Z axis with Y axis
So we do the same replacement in equations:
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
The 3D Projection
https://en.wikipedia.org/wiki/3D_projection
To project the 3D point, ax, ay, az onto the 2D point bx, by using an orthographic projection parallel to the y axis (profile view), the following equations can be used:
- bx = sx * ax + cx
- by = sz * az + cz
where the vector s
is an arbitrary scale factor, and c
is an arbitrary offset. These constants are optional, and can be used to properly align the viewport.
Public Sub Project(ByRef x!, ByRef y!, z!,
viewWidth%,
viewHeight%,
viewDistance%,
Optional fov% = 256)
Dim factor! = fov / (viewDistance + z)
x = x * factor + viewWidth / 2
y = y * factor + viewHeight / 2
End Sub
The Painter Algorithm
The painter's algorithm, also known as a priority fill, is one of the simplest solutions to the visibility problem in 3D computer graphics. When projecting a 3D scene onto a 2D plane, it is necessary at some point to decide which polygons are visible, and which are hidden.
The name "painter's algorithm" refers to the technique employed by many painters of painting distant parts of a scene before parts which are nearer thereby covering some areas of distant parts. The painter's algorithm sorts all the polygons in a scene by their depth and then paints them in this order, farthest to closest. It will paint over the parts that are normally not visible — thus solving the visibility problem — at the cost of having painted invisible areas of distant objects. The ordering used by the algorithm is called a 'depth order', and does not have to respect the numerical distances to the parts of the scene: the essential property of this ordering is, rather, that if one object obscures part of another, then the first object is painted after the object that it obscures. Thus, a valid ordering can be described as a topological ordering of a directed acyclic graph representing occlusions between objects.
One simple method to implement this painter algorithm is using the z-order
method:
https://en.wikipedia.org/wiki/Z-order
<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)
For Each i As SeqValue(Of T) In source.SeqIterator
Call avgZ.Add(z(+i))
Call order.Add(i)
Next
Dim iMax%, tmp#
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
The Display Device
I have created a Winform control for displaying the 3D model which is avaliable in namespace Microsoft.VisualBasic.Imaging.Drawing3D.Device.GDIDevice
. Here is a simple code example of using this 3D model display control in winform:
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()
This 3D model display control is based on the GDI+ graphic engine, so that if the model is too complicated and has a lot of surface to draw, then this control rendering will be very slow. For improvements on the graphics rendering performance, I using 2 worker threads for the 3D graphic display:
- Model buffer thread
- Graphic rendering thread
Buffer Thread
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
This model buffer worker thread applies the 3D projection for all surfaces in the model and runs the Z-order based painters' algorithm, and then creates the 2D polygon buffer for the rendering thread. Here is the definition of this 2D polygon buffer unit:
Public Structure Polygon
Dim points As Point()
Dim brush As Brush
End Structure
Graphics Rendering
The Light Source
Applying the light source on the 3D model can makes our graphic more natural, here is an example of applying the light effects:
One of the simple algorithms for applying the light source is darken the surface's color base on its Z-order depth, this z-order depth can be obtained from the painter's algorithm:
<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!
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
The Control Rendering
And here is the rendering thread, which is triggered by a refresh thread:
Private Sub _animationLoop_Tick(sender As Object, e As EventArgs) Handles _animationLoop.Tick
Call Me.Invalidate()
Call Me.___animationLoop()
End Sub
When the method of the winform control Me.Invalidate()
has been invoked, then this method call will force the control to refresh itself and raise the Control.Paint
event, then we are able to do the rendering job:
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 Format
Finally, we have all of the elements that can be used for displaying the 3D graphic. For displaying a 3D graphic, we must put the model data into the display device control. And I use the 3mf model file as my 3D engine input model data. Here is how I do it for loading the 3mf model data:
Open the 3mf model file and load the 3D model into the canvas control using just two simple functions:
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 is a new 3D printing format that will allow design applications to send full-fidelity 3D models to a mix of other applications, platforms, services and printers. The 3MF specification allows companies to focus on innovation, rather than on basic interoperability issues, and it is engineered to avoid the problems associated with other 3D file formats.
http://www.3mf.io/what-is-3mf/
Since 3MF is an XML-based data format designed for using additive manufacturing, we can easily load the model in the *.3mf
file using XML de-serialization in Visual Basic, and the XML model for this serialization operation is avaliable in namespace: Microsoft.VisualBasic.Imaging.Drawing3D.Landscape.Vendor_3mf.XML
Public Class Project
Public Property Thumbnail As Image
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
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