旋转的平面——以 2D 方式投影和绘制——在某些边缘看起来是弯曲的而不是直的

Rotated plane—projected and drawn in 2D—looks crooked instead of straight at some edges

引言

该程序的目的是围绕 x 轴和 z 轴旋转 mathematical plane 并将其投影到 2D GUI 上。 首先关于符号约定: 该平面位于 xz 平面(因此在原点),其中 +x 向右,+y 向图片的深度,+z 向顶部(比较:我将另一个人看成一个人)。

许多矢量用于填充表面。根据飞机的大小,通常有超过 40,000 个矢量。所以我决定 运行 异步计算函数。由于现在在 PictureBox 中绘制很麻烦/没有意义,所以我写了一个位图,然后在每次计算过程后将其分配给 PictureBox。由于 Graphics.DrawLine 函数在性能方面达到了极限,我从 Visual Studio 自己的 Nuget 包管理器中集成了 SkiaSharp。现在,我写一个SKBtmap,可以用WASD键移动平面,并在几百毫秒内得到一个新图像。

对于投影,我使用了几个月前在 Whosebug 上找到的公式。我现在已经清楚清楚地展示了它。由于转弯时飞机有一部分朝我过来,所以与我的距离发生了变化。所以,我减去 distance_to_plane 以获得实际距离。

Dim projected As New PointF(
                            CSng((Camera - Window_distance) / (Camera - distance_to_plane) * rotatedVec.X),
                            CSng(-(Camera - Window_distance) / (Camera - distance_to_plane) * rotatedVec.Z))

我关心你什么:

正如您从图片中看到的那样,平面在应该是直线的边缘处有一些曲线(异常值)。我无法解释效果。 我怀疑上面的公式。为了解决这个问题,我将相机设置得离平面很远,并且投影 window 离相机和平面都“足够”远。 我把完整的源码放到网上,也许是别的东西(典型效果?)。

形成'Create'以实例化一个新平面。

平面围绕 z 轴强烈旋转,中间有奇数边。从这些值可以看出,相机当前距离平面 2660 个单位长度,而投影 window 是 1000 个长度单位。 (相机 – window 1660)

Form1.vb

Public NotInheritable Class FormMain
    Private Plane1 As PlaneInTermsOfGeometry = Nothing
    Public ReadOnly Deu As New System.Globalization.CultureInfo("de-DE")

    Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.BackColor = Color.FromArgb(184, 176, 143)
        For Each but As Button In Me.Controls.OfType(Of Button)
            but.BackColor = Color.FromArgb(201, 200, 193)
        Next
        TextBox_Window.Text = "-1000"
        Label5.Text = ""
        Label6.Text = ""
    End Sub

    Private Sub FormMain_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
        New_Plane()
    End Sub

    Private Sub Button_new_plane_Click(sender As Object, e As EventArgs) Handles Button_new_plane.Click
        New_Plane()
    End Sub

    Private Async Sub New_Plane()
        Using FNP As New FormCreateNewPlane
            If FNP.ShowDialog(Me) <> DialogResult.OK Then
                Return
            End If
            Plane1 = New PlaneInTermsOfGeometry(
                FNP.A0x,
                FNP.A0y,
                FNP.A0z,
                FNP.ABx,
                FNP.ABy,
                FNP.ABz,
                FNP.ACx,
                FNP.ACy,
                FNP.ACz,
                FNP.Enlargement)
            Await Plane1.process_async()
            PictureBox1.Image = Nothing
            PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
            Label5.Text = Math.Round(Plane1.current_x_angle, 0).ToString(Deu)
            Label6.Text = Math.Round(Plane1.current_z_angle, 0).ToString(Deu)
            TextBox_Kamera.Text = Math.Round(Plane1.Camera, 0).ToString(Deu)
        End Using
    End Sub

    Private Sub TextBox_Kamera_TextChanged(sender As Object, e As EventArgs) Handles TextBox_Kamera.TextChanged
        If Plane1 Is Nothing Then Return

        Dim Kamera As Double
        If Double.TryParse(TextBox_Kamera.Text, Kamera) Then
            TextBox_Kamera.ForeColor = Color.FromArgb(0, 125, 0)
            Plane1.Camera = Kamera
        Else
            TextBox_Kamera.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBox_Fenster_TextChanged(sender As Object, e As EventArgs) Handles TextBox_Window.TextChanged
        If Plane1 Is Nothing Then Return

        Dim Fenster As Double
        If Double.TryParse(TextBox_Window.Text, Fenster) Then
            TextBox_Window.ForeColor = Color.FromArgb(0, 125, 0)
            Plane1.Window_distance = Fenster
        Else
            TextBox_Window.ForeColor = Color.Red
        End If
    End Sub

    Private Async Sub FormMain_KeyDown(sender As Object, e As KeyEventArgs) Handles MyBase.KeyDown
        If Plane1 Is Nothing Then Return

        Select Case e.KeyCode
            Case Keys.W
                If Plane1.current_x_angle > -90.0 Then
                    Plane1.change_x_angle(-1.0)
                    Await Plane1.process_async()
                    PictureBox1.Image = Nothing
                    GC.Collect()
                    PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
                    Label5.Text = Math.Round(Plane1.current_x_angle, 0).ToString(Deu)
                    Label6.Text = Math.Round(Plane1.current_z_angle, 0).ToString(Deu)
                    TextBox_KOForm.Text = Plane1.Cartesian_Equation()
                End If
            Case Keys.S
                If Plane1.current_x_angle < 90.0 Then
                    Plane1.change_x_angle(1.0)
                    Await Plane1.process_async()
                    PictureBox1.Image = Nothing
                    GC.Collect()
                    PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
                    Label5.Text = Math.Round(Plane1.current_x_angle, 0).ToString(Deu)
                    Label6.Text = Math.Round(Plane1.current_z_angle, 0).ToString(Deu)
                    TextBox_KOForm.Text = Plane1.Cartesian_Equation()
                End If
            Case Keys.A
                If Plane1.current_z_angle > -90.0 Then
                    Plane1.change_z_angle(-1.0)
                    Await Plane1.process_async()
                    PictureBox1.Image = Nothing
                    GC.Collect()
                    PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
                    Label5.Text = Math.Round(Plane1.current_x_angle, 0).ToString(Deu)
                    Label6.Text = Math.Round(Plane1.current_z_angle, 0).ToString(Deu)
                    TextBox_KOForm.Text = Plane1.Cartesian_Equation()
                End If
            Case Keys.D
                If Plane1.current_z_angle < 90.0 Then
                    Plane1.change_z_angle(1.0)
                    Await Plane1.process_async()
                    PictureBox1.Image = Nothing
                    GC.Collect()
                    PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
                    Label5.Text = Math.Round(Plane1.current_x_angle, 0).ToString(Deu)
                    Label6.Text = Math.Round(Plane1.current_z_angle, 0).ToString(Deu)
                    TextBox_KOForm.Text = Plane1.Cartesian_Equation()
                End If
            Case Else
                Exit Select
        End Select
    End Sub

    Private Async Sub FormMain_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseWheel
        If Plane1 Is Nothing Then Return

        If e.Delta > 0 Then
            ' The Camera must be in front of the window.
            If (Plane1.Camera - Plane1.Window_distance) < 0.0 Then
                Plane1.change_Camera_distance(20.0)
                Await Plane1.process_async()
                PictureBox1.Image = Nothing
                GC.Collect()
                PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
            End If
        Else
            Plane1.change_Camera_distance(-20.0)
            Await Plane1.process_async()
            PictureBox1.Image = Nothing
            GC.Collect()
            PictureBox1.Image = PlaneInTermsOfGeometry.displayedBitmap
        End If

        TextBox_Kamera.Text = Math.Round(Plane1.Camera, 0).ToString(Deu)
    End Sub
End Class

class PlaneInTermsOfGeometry(顺便说一下:礼貌地指示我为这个 class 选择一个合理的名称,而不仅仅是“平面”。。 .)

Imports System.Windows.Media.Media3D
Imports SkiaSharp
Public NotInheritable Class PlaneInTermsOfGeometry
    Private Structure VA0
        Public x As Double
        Public y As Double
        Public z As Double
    End Structure
    Private A0 As VA0

    Private Structure VAB
        Public x As Double
        Public y As Double
        Public z As Double
    End Structure
    '       →
    Private AB As VAB

    Private Structure VAC
        Public x As Double
        Public y As Double
        Public z As Double
    End Structure
    '       →
    Private AC As VAC

    Private ReadOnly allVectors As New List(Of Vector3D)
    ''' <summary>
    ''' in degrees
    ''' </summary>
    Public current_x_angle As Double = 0.0
    ''' <summary>
    ''' in degrees
    ''' </summary>
    Public current_z_angle As Double = 0.0
    ''' <summary>
    ''' The picture in which is written and which is shown by the PictureBox. 
    ''' </summary>
    Public Shared displayedBitmap As System.Drawing.Bitmap

    ''' <summary>
    ''' The camera position on the y-axis (we look along the +y arrow). 
    ''' </summary>
    Public Camera As Double = -2660.0
    ''' <summary>
    ''' The projection window position on the y-axis. Absolute value!
    ''' </summary>
    Public Window_distance As Double = -1000.0
    ''' <summary>
    ''' The distance from the origin of coordinates to the x-length 
    ''' </summary>
    Private ReadOnly oneSide As Double

    Private ReadOnly Grid As New List(Of Vector3D)

    Public Sub New(ByVal A0x As Double,
                   ByVal A0y As Double,
                   ByVal A0z As Double,
                   ByVal ABx As Double,
                   ByVal ABy As Double,
                   ByVal ABz As Double,
                   ByVal ACx As Double,
                   ByVal ACy As Double,
                   ByVal ACz As Double,
                   ByVal enlarg As Double)
        Me.A0.x = A0x
        Me.A0.y = A0y
        Me.A0.z = A0z
        Me.AB.x = ABx * enlarg
        Me.AB.y = ABy
        Me.AB.z = ABz
        Me.AC.x = ACx
        Me.AC.y = ACy
        Me.AC.z = ACz * enlarg
        Me.oneSide = ABx * enlarg

        For x As Double = -AB.x To AB.x Step 1.0
            For z As Double = -AC.z To AC.z Step 2.0
                allVectors.Add(New Vector3D(x, 0.0, z))
                ' For the grid
                If CSng(x) Mod 15.0F = 0.0F Then
                    Grid.Add(New Vector3D(x, 0.0, z))
                Else
                    Grid.Add(New Vector3D(0.0, 0.0, 0.0))
                End If
            Next
        Next
    End Sub

    Public Sub change_Camera_distance(ByVal dy As Double)
        Camera += dy
    End Sub

    Public Sub change_x_angle(ByVal value As Double)
        current_x_angle += value
    End Sub

    Public Sub change_z_angle(ByVal value As Double)
        current_z_angle += value
    End Sub

    Private Function rotate_around_x_axis(ByVal vec1 As Vector3D) As Vector3D
        Return New Vector3D(
            vec1.X,
            vec1.Y * Math.Cos(current_x_angle * Math.PI / 180.0) - vec1.Z * Math.Sin(current_x_angle * Math.PI / 180.0),
            vec1.Y * Math.Sin(current_x_angle * Math.PI / 180.0) + Math.Cos(current_x_angle * Math.PI / 180.0) * vec1.Z)
    End Function

    Private Function rotate_around_z_axis(ByVal vec2 As Vector3D) As Vector3D
        Return New Vector3D(
            Math.Cos(current_z_angle * Math.PI / 180.0) * vec2.X - vec2.Y * Math.Sin(current_z_angle * Math.PI / 180.0),
            Math.Sin(current_z_angle * Math.PI / 180.0) * vec2.X + vec2.Y * Math.Cos(current_z_angle * Math.PI / 180.0),
            vec2.Z)
    End Function

    Public Async Function process_async() As Task(Of Boolean)
        Return Await Task.Run(Function() processing())
    End Function

    Private Function processing() As Boolean
        displayedBitmap = Nothing

        Dim i As Integer = 0
        Dim imageInfo As New SKImageInfo(FormMain.PictureBox1.Size.Width, FormMain.PictureBox1.Size.Height)
        Using surface As SKSurface = SKSurface.Create(imageInfo)
            Using canvas As SKCanvas = surface.Canvas
                canvas.Translate(FormMain.PictureBox1.Size.Width \ 2, FormMain.PictureBox1.Size.Height \ 2)

                Using DarkBlue As New SKPaint With {
                                .TextSize = 64.0F,
                                .IsAntialias = True,
                                .Color = New SKColor(0, 64, 255),
                                 .Style = SKPaintStyle.Fill
                                 }
                    Using BrightYellow As New SKPaint With {
                        .TextSize = 64.0F,
                        .IsAntialias = True,
                        .Color = New SKColor(255, 255, 64),
                        .Style = SKPaintStyle.Fill
                        }

                        For Each vec As Vector3D In allVectors
                            Dim rotatedVec As Vector3D = rotate_around_z_axis(rotate_around_x_axis(vec))

                            If rotatedVec.Y > Window_distance Then ' The object is not further back than the window (the window is not in the object). When false, don't draw!
                                Dim Angle_in_degrees As Double = Vector3D.AngleBetween(
                                    rotatedVec,
                                    New Vector3D(rotatedVec.X, 0.0, rotatedVec.Z))

                                If Double.IsNaN(Angle_in_degrees) Then
                                    i += 1
                                    Continue For
                                End If

                                ' Opposite cathetus
                                Dim distance_to_plane As Double = oneSide * Math.Sin(Angle_in_degrees * Math.PI / 180.0)
                                Dim projected As New PointF(
                                        CSng((Camera - Window_distance) / (Camera - distance_to_plane) * rotatedVec.X),
                                        CSng(-(Camera - Window_distance) / (Camera - distance_to_plane) * rotatedVec.Z))

                                If Grid(i).X = 0.0 AndAlso Grid(i).Y = 0.0 AndAlso Grid(i).Z = 0.0 Then
                                    ' draw the mathematical plane
                                    canvas.DrawPoint(projected.X, projected.Y, DarkBlue)
                                Else
                                    ' draw the grid (Gitternetz)
                                    canvas.DrawPoint(projected.X, projected.Y, BrightYellow)
                                End If

                                i += 1
                            End If
                        Next
                    End Using
                End Using
            End Using

            '–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
            ' get the data into ‘displayedBitmap’ because the PictureBox is only accepting an usual System.Drawing.Bitmap.
            '–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

            Using image As SKImage = surface.Snapshot()
                Using data As SKData = image.Encode(SKEncodedImageFormat.Png, 100)
                    Using mStream As New IO.MemoryStream(data.ToArray())
                        displayedBitmap = New Bitmap(mStream, False)
                    End Using
                End Using
            End Using

        End Using
        Return True
    End Function

    'Koordinatenform
    Public Function Cartesian_Equation() As String
        Dim _N As Vector3D = Vector3D.CrossProduct(rotate_around_z_axis(New Vector3D(AB.x, AB.y, AB.z)), rotate_around_x_axis(New Vector3D(AC.x, AC.y, AC.z)))

        Dim _xMinusA0 As String
        Dim _yMinusA0 As String
        Dim _zMinusA0 As String

        If A0.x = 0.0 Then
            _xMinusA0 = "x"
        Else
            _xMinusA0 = $"(x - {A0.x.ToString(FormMain.Deu)})"
        End If
        If A0.y = 0.0 Then
            _yMinusA0 = "y"
        Else
            _yMinusA0 = $"(y - {A0.y.ToString(FormMain.Deu)})"
        End If
        If A0.z = 0.0 Then
            _zMinusA0 = "z"
        Else
            _zMinusA0 = $"(z - {A0.z.ToString(FormMain.Deu)})"
        End If

        Return ($"{Math.Round(_N.X, 3).ToString(FormMain.Deu)} * {_xMinusA0} + {Math.Round(_N.Y, 3).ToString(FormMain.Deu)} * {_yMinusA0} + {Math.Round(_N.Z, 3).ToString(FormMain.Deu)} * {_zMinusA0}").ToString(FormMain.Deu)
    End Function
End Class

为了完整起见,如果有人要重新创建,这里FormNewPlane.vb创建一个新的位面,如第一张图

Imports Microsoft.VisualBasic.ControlChars
Public NotInheritable Class FormCreateNewPlane
    Public A0x, A0y, A0z, ABx, ABy, ABz, ACx, ACy, ACz, Enlargement As Double

    Private Sub FormCreateNewPlane_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.BackColor = Color.FromArgb(184, 176, 143)
        For Each but As Button In Me.Controls.OfType(Of Button)
            but.BackColor = Color.FromArgb(201, 200, 193)
        Next

        If System.IO.File.Exists(Application.StartupPath & "\Preview.png") Then
            PictureBox1.Image = Image.FromFile(Application.StartupPath & "\Preview.png")
        End If

        'Since this is a plane that lies in the xz plane, only the text box contents that display a 1 should be changed.
        Label5.Text = $"Da es hier um eine Ebene geht, die{NewLine}in der xz-Ebene liegt, sollen nur die{NewLine}Textbox-Inhalte verändert werden,{NewLine}die eine 1 anzeigen."
    End Sub

    Private Sub FormCreateNewPlane_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        If PictureBox1.Image IsNot Nothing Then PictureBox1.Image.Dispose()
    End Sub

    Private Sub ButtonOK_Click(sender As Object, e As EventArgs) Handles ButtonOK.Click
        Me.DialogResult = DialogResult.OK
    End Sub

    Private Sub TextBoxA0x_TextChanged(sender As Object, e As EventArgs) Handles TextBoxA0x.TextChanged
        If Double.TryParse(TextBoxA0x.Text, A0x) Then
            TextBoxA0x.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxA0x.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxA0y_TextChanged(sender As Object, e As EventArgs) Handles TextBoxA0y.TextChanged
        If Double.TryParse(TextBoxA0y.Text, A0y) Then
            TextBoxA0y.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxA0y.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxA0z_TextChanged(sender As Object, e As EventArgs) Handles TextBoxA0z.TextChanged
        If Double.TryParse(TextBoxA0z.Text, A0z) Then
            TextBoxA0z.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxA0z.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxABx_TextChanged(sender As Object, e As EventArgs) Handles TextBoxABx.TextChanged
        If Double.TryParse(TextBoxABx.Text, ABx) Then
            TextBoxABx.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxABx.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxABy_TextChanged(sender As Object, e As EventArgs) Handles TextBoxABy.TextChanged
        If Double.TryParse(TextBoxABy.Text, ABy) Then
            TextBoxABy.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxABy.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxABz_TextChanged(sender As Object, e As EventArgs) Handles TextBoxABz.TextChanged
        If Double.TryParse(TextBoxABz.Text, ABz) Then
            TextBoxABz.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxABz.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxACx_TextChanged(sender As Object, e As EventArgs) Handles TextBoxACx.TextChanged
        If Double.TryParse(TextBoxACx.Text, ACx) Then
            TextBoxACx.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxACx.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxACy_TextChanged(sender As Object, e As EventArgs) Handles TextBoxACy.TextChanged
        If Double.TryParse(TextBoxACy.Text, ACy) Then
            TextBoxACy.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxACy.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBoxACz_TextChanged(sender As Object, e As EventArgs) Handles TextBoxACz.TextChanged
        If Double.TryParse(TextBoxACz.Text, ACz) Then
            TextBoxACz.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBoxACz.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBox_Enlarg_TextChanged(sender As Object, e As EventArgs) Handles TextBox_Enlarg.TextChanged
        If Double.TryParse(TextBox_Enlarg.Text, Enlargement) Then
            TextBox_Enlarg.ForeColor = Color.FromArgb(0, 125, 0)
        Else
            TextBox_Enlarg.ForeColor = Color.Red
        End If
    End Sub

    Private Sub TextBox_Enlarg_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBox_Enlarg.KeyDown
        If e.KeyCode = Keys.Enter Then
            If Double.TryParse(TextBox_Enlarg.Text, Enlargement) Then
                TextBox_Enlarg.ForeColor = Color.FromArgb(0, 125, 0)
            Else
                TextBox_Enlarg.ForeColor = Color.Red
            End If

            Me.DialogResult = DialogResult.OK
        End If
    End Sub

    Private Sub TextBox_Enlarg_KeyPress(sender As Object, e As KeyPressEventArgs) Handles TextBox_Enlarg.KeyPress
        If e.KeyChar = Convert.ToChar(13) Then e.Handled = True ' This suppresses the ‘ding’ sound.
    End Sub
End Class

如果您想将 perspective projection 应用于积分,projected 积分应该是这样的;

Dim projected As New PointF(
    CSng((Camera - Window_distance) / (Camera - rotatedVec.Y) * rotatedVec.X),
    CSng(-(Camera - Window_distance) / (Camera - rotatedVec.Y) * rotatedVec.Z))

' In short, distance_to_plane = rotatedVec.Y