With my VB.Net Windows Forms application, I would like to rotate and project a 3D object. The program works so far, which is why I'm reporting here and not Stack Overflow, but it's slow. I've already made two optimizations:
1.) All points that are on the back of the object, i.e. that cannot be seen, are not drawn. Understandably, this effect does not occur when I look at the hemisphere from above, and with a 90° rotation around the x-axis (side view) 50% of the points are not drawn.
2.) The calculation runs asynchronously.
2.1) Since you cannot draw in the PictureBox from another thread, I did some research. This has shown that you draw in a bitmap and assign it to the PictureBox.
3.) I have already removed the procedure that computes the Cartesian x, y and z and displays it to the user.
Overall, the program runs a little faster; but is still too slow. I would like to leave the step size when calculating the surface points for the first time at 0.5, otherwise you will see gaps. I know that results in 130,320 points on a very small area. That's why I came up with the idea not to draw the back.
What's the point? I wanted to code the creation and projection of the object myself, without 3rd party libraries. I also want to add a grid in the future, but for that the program has to be faster first.
Maybe you can help me save a few clock cycles.
Ah, and by the way: I've found that the percentages don't make 100% sense. As you can see here, 49.6% of all points are not drawn (instead of 50%) in the side view.
This is my code of the class
#Disable Warning IDE1006 ' Benennungsstile
#Disable Warning CA1707 ' Bezeichner dürfen keine Unterstriche enthalten
#Disable Warning CA1051 ' Sichtbare Instanzfelder nicht deklarieren
#Disable Warning CA2211 ' Nicht konstante Felder dürfen nicht sichtbar sein
Imports System.Windows.Media.Media3D
Public NotInheritable Class ClassHemisphere
Private ReadOnly enGB As New System.Globalization.CultureInfo("en-GB")
Public Phi As UInt16
Public Theta As UInt16
''' <summary>
''' This list contains all vectors that point from the coordinate origin to the surface.
''' </summary>
Public ReadOnly List_with_all_the_vectors As New List(Of Vector3D)
Private ReadOnly Liste_thetas As New List(Of Double)
Public ReadOnly Liste_phis As New List(Of Double)
''' <summary>
''' The camera position on the z-axis (we look towards the +z arrow).
''' </summary>
Public Camera As Double = 1500.0
''' <summary>
''' The position of the projection window on the z-axis.
''' </summary>
Public Window As Double = 700.0
Private ReadOnly point_of_origin As New PointF(0.0F, 0.0F)
''' <summary>
''' Radius of this hemisphere
''' </summary>
Private ReadOnly Radius As Double
''' <summary>
''' It is to be drawn a 2D line to the point of the set Phi and Theta.
''' </summary>
Private projected_arrow As PointF
''' <summary>
''' in degrees
''' </summary>
Public rotation_angle_x As Double
Public Shared Buffer As System.Drawing.Bitmap
Private Camera_Vector As Vector3D
Public Sub New(ByVal radius As Double)
Me.Radius = radius
Camera_Vector = New Vector3D(0, 0, Camera)
For _phi As Double = 0.0 To 359.5 Step 0.5
For _theta As Double = 0.0 To 90.0 Step 0.5
List_with_all_the_vectors.Add(New Vector3D(
radius * Math.Cos(_phi * Math.PI / 180.0) * Math.Sin(_theta * Math.PI / 180.0),
radius * Math.Sin(_phi * Math.PI / 180.0) * Math.Sin(_theta * Math.PI / 180.0),
radius * Math.Cos(_theta * Math.PI / 180.0)))
Liste_thetas.Add(_theta)
Liste_phis.Add(_phi)
Next
Next
End Sub
Public Function getVector() As Vector3D
Dim Value As Vector3D
For i As Integer = 0 To List_with_all_the_vectors.Count - 1 Step 1
If Liste_phis(i) = CDbl(Phi) AndAlso Liste_thetas(i) = CDbl(Theta) Then
Value = List_with_all_the_vectors(i)
Return Value
End If
Next
Return Value
End Function
Private Shared Function Rotate_around_the_x_axis(ByVal vec As Vector3D, ByVal angle As Double) As Vector3D
Return New Vector3D(vec.X,
vec.Y * Math.Cos(angle * Math.PI / 180.0) - vec.Z * Math.Sin(angle * Math.PI / 180.0),
vec.Y * Math.Sin(angle * Math.PI / 180.0) + vec.Z * Math.Cos(angle * Math.PI / 180.0))
End Function
Public Sub change_Camera_height_a_little(ByVal dz As Double)
Camera += dz
Camera_Vector = New Vector3D(0, 0, Camera)
End Sub
Public Async Function calculate_Async() As Task(Of Boolean)
Return Await Task.Run(Function() Verarbeitung())
End Function
Private Function Verarbeitung() As Boolean
Buffer = Nothing
Dim Counter As Integer = 0
Using _buffer As New System.Drawing.Bitmap(FormMain.PictureBox1.Size.Width, FormMain.PictureBox1.Size.Height)
Using g As System.Drawing.Graphics = Graphics.FromImage(_buffer)
g.TranslateTransform(CSng(_buffer.Size.Width / 2.0F), CSng(_buffer.Size.Height / 2.0F))
g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
g.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
g.PixelOffsetMode = Drawing2D.PixelOffsetMode.HighQuality
g.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBilinear
For i As Integer = 0 To List_with_all_the_vectors.Count - 1 Step 1
Dim rotated_vec As Vector3D = Rotate_around_the_x_axis(List_with_all_the_vectors(i), rotation_angle_x)
'Everything that is more than 90° from the camera direction (=back side) does not need to be drawn.
Dim angle_view_to_a_Normal As Double = Vector3D.AngleBetween(Camera_Vector, rotated_vec)
If angle_view_to_a_Normal > 90.0001 Then ' 90.0001 because the precision of a ‘double’ is finite..
Counter += 1 ' for testing purposes
Continue For
End If
Dim Angle_in_degrees As Double = Vector3D.AngleBetween(rotated_vec, New Vector3D(rotated_vec.X, rotated_vec.Y, 0.0))
If Double.IsNaN(Angle_in_degrees) Then
Continue For
End If
Dim vertical_height As Double = Radius * Math.Sin(Angle_in_degrees * Math.PI / 180.0) ' Opposite cathetus
Dim projected_Point As New PointF(
CSng((Camera - Window) / (Camera - vertical_height) * rotated_vec.X),
CSng(-(Camera - Window) / (Camera - vertical_height) * rotated_vec.Y))
Using Pen_DarkBlue As New Pen(Color.FromArgb(0, 0, 122), 1.0F)
g.DrawLine(Pen_DarkBlue, point_of_origin, projected_Point)
End Using
Next
Dim Arrow As Vector3D = Rotate_around_the_x_axis(getVector(), rotation_angle_x)
Dim Angle As Double = Vector3D.AngleBetween(Arrow, New Vector3D(Arrow.X, Arrow.Y, 0.0))
Dim vertical As Double = Arrow.Length * Math.Sin(Angle * Math.PI / 180.0)
projected_arrow = New PointF(
CSng((Camera - Window) / (Camera - vertical) * Arrow.X),
CSng(-(Camera - Window) / (Camera - vertical) * Arrow.Y))
Using Pen_DarkYellow As New Pen(Color.FromArgb(122, 122, 0), 3.0F)
g.DrawLine(Pen_DarkYellow, point_of_origin, projected_arrow)
End Using
Buffer = New Bitmap(_buffer)
End Using
End Using
Debug.WriteLine(Math.Round(Counter / CDbl(List_with_all_the_vectors.Count) * 100.0, 1).ToString(enGB) & " % of all the points are not drawn.")
Return True
End Function
End Class
#Enable Warning IDE1006 ' Benennungsstile
#Enable Warning CA1707 ' Bezeichner dürfen keine Unterstriche enthalten
#Enable Warning CA1051 ' Sichtbare Instanzfelder nicht deklarieren
#Enable Warning CA2211 ' Nicht konstante Felder dürfen nicht sichtbar sein
And this is Form1.vb
Public NotInheritable Class FormMain
Private ReadOnly Deu As New System.Globalization.CultureInfo("de-DE")
Private hemisphere As ClassHemisphere
Private program_has_finished_loading As Boolean
Private ReadOnly DarkBlue As Color = Color.FromArgb(0, 0, 50)
Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.BackColor = Color.FromArgb(160, 176, 167)
TextBox_phi.Text = My.Resources.zero
TextBox_theta.Text = My.Resources.zero
End Sub
Private Async Sub FormMain_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
Await Task.Run(Sub() hemisphere = New ClassHemisphere(radius:=350.0))
program_has_finished_loading = True
TextBox_Kamera.Text = Math.Round(hemisphere.Camera, 0).ToString(Deu)
TextBox_Fenster.Text = Math.Round(hemisphere.Window, 0).ToString(Deu)
Await hemisphere.calculate_Async()
PictureBox1.Image = ClassHemisphere.Buffer
End Sub
Private Async Sub FormMain_KeyDown(sender As Object, e As KeyEventArgs) Handles MyBase.KeyDown
Select Case e.KeyCode
Case Keys.W
If hemisphere.Theta > 0US Then
hemisphere.Theta -= 1US
Await hemisphere.calculate_Async()
PictureBox1.Image = ClassHemisphere.Buffer
End If
Case Keys.S
If hemisphere.Theta < 90US Then
hemisphere.Theta += 1US
Await hemisphere.calculate_Async()
PictureBox1.Image = ClassHemisphere.Buffer
End If
Case Keys.A
If hemisphere.Phi < 360US Then
hemisphere.Phi += 1US
Await hemisphere.calculate_Async()
PictureBox1.Image = ClassHemisphere.Buffer
End If
If hemisphere.Phi = 360US Then hemisphere.Phi = 0US
Case Keys.D
If hemisphere.Phi > 0US Then
hemisphere.Phi -= 1US
Await hemisphere.calculate_Async()
PictureBox1.Image = ClassHemisphere.Buffer
End If
Case Else
Exit Select
End Select
TextBox_theta.Text = hemisphere.Theta.ToString(Deu).PadLeft(2, "0"c) & " °"
TextBox_phi.Text = hemisphere.Phi.ToString(Deu).PadLeft(2, "0"c) & " °"
GC.Collect()
End Sub
'Private Async Sub find_Vector()
' Dim found_Vector As Vector3D = Await find_Vector_Async()
' TextBox_x.Text = Math.Round(found_Vector.X, 3).ToString(Deu)
' TextBox_y.Text = Math.Round(found_Vector.Y, 3).ToString(Deu)
' TextBox_z.Text = Math.Round(found_Vector.Z, 3).ToString(Deu)
'End Sub
'Private Async Function find_Vector_Async() As Task(Of Vector3D)
' Return Await Task.Run(Function() hemisphere.getVector())
'End Function
Private Async Sub Form1_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseWheel
If e.Delta > 0 Then
hemisphere.change_Camera_height_a_little(10.0)
Else
hemisphere.change_Camera_height_a_little(-10.0)
End If
Await hemisphere.calculate_Async()
TextBox_Kamera.Text = Math.Round(hemisphere.Camera, 0).ToString(Deu)
End Sub
Private Sub TextBox_Kamera_TextChanged(sender As Object, e As EventArgs) Handles TextBox_Kamera.TextChanged
If hemisphere IsNot Nothing Then
TextBox_Kamera.ForeColor = If(Double.TryParse(TextBox_Kamera.Text, hemisphere.Camera), DarkBlue, Color.Red)
End If
End Sub
Private Sub TextBox_Fenster_TextChanged(sender As Object, e As EventArgs) Handles TextBox_Fenster.TextChanged
If hemisphere IsNot Nothing Then
Dim successful1 As Boolean = Double.TryParse(TextBox_Kamera.Text, hemisphere.Camera)
Dim window As Double
Dim successful2 As Boolean = Double.TryParse(TextBox_Fenster.Text, window)
If window >= hemisphere.Camera OrElse Not successful1 OrElse Not successful2 Then
TextBox_Fenster.ForeColor = Color.Red
Else
TextBox_Fenster.ForeColor = DarkBlue
hemisphere.Window = window
End If
End If
End Sub
Private Async Sub NumericUpDown_x_ValueChanged(sender As Object, e As EventArgs) Handles NumericUpDown_x.ValueChanged
hemisphere.rotation_angle_x = NumericUpDown_x.Value
Await hemisphere.calculate_Async()
PictureBox1.Image = ClassHemisphere.Buffer
End Sub
End Class