首先看看效果:
在學習二叉樹數據結構的時候,用GDI+結合隊列或者棧來畫出來一個二叉樹的結構,如果你不是一次繪製完成,而是從每兩個線的繪製有時間間隔的話,你就能看到樹的“生長過程”,而使用棧和隊列將得到完全不同的生長方式,也許這就是深度優先遍歷和廣度優先遍歷的區別把。那麼,我當時就在想,如果繪製一個空間中的三叉樹會是什麼樣的效果呢?有了想法就一步步的實現。之前也知道GDI+提供了一些旋轉的函數和一些投影的知識,所以應該不會太難。
如果你已經完成了二叉樹的繪製,那麼當繪製三維三叉樹的時候你也許將會遇到兩個大問題:
1,如何生成?雖然規則看起來就是1個分成3個,最後每個分支的角度就將會越來越小。你可以想象它們的位置是相對的,這樣思考起來就比較簡單了,也就是對於任何一個支,它所生成三個枝杈都滿足一個空間正四面體這樣的結構,然後採用一些空間幾何的運算法則你或許可以準確的計算出來每個枝杈的生長方向(角度)或者終點的XYZ座標,但是這裏有一種更爲簡單的方式,也就是運用一定的旋轉縮放規則,在經過一系列的變換,你就可以把一個線段從母分支變換到子分支。而在具體的實現細節中,一定心中始終要有一個全局座標系和局部座標系的概念。
2,如何顯示?在二維的情況下,每個線段只需要xy座標就可以顯示出來,並且也不會有遮擋的問題。如果你解決了第一個問題,現在假設你已經生成了一個完好的三維三叉樹,在屏幕上顯示的時候如果只輸出xyz座標中的兩個,那麼得到的結果和二叉樹應該是類似的,即使看到的不是一個二叉樹,那麼樹也是死的,看不到它的另一面或者側面。這個時候就需要一個投影法則,就是根據側投影角度和正投影角度來把(x,y,z)座標變成(x',y'),然後就可以顯示在屏幕上,改變投影角度,就可以得到上述動態圖的效果。
這是以前寫的程序,一些公式和旋轉步驟,細節等等記不太清了,大體思路如上。
下面是所有的VB.NET代碼:
用到了一些簡單控件,從代碼中可以看出來。
Imports System.Math
Imports System.Windows.Media.Media3D
Public Class Form1
Dim bmp As New Bitmap(1000, 1000)
Dim g As Graphics = Graphics.FromImage(bmp)
Private Sub _3DbinaryTree_Load(sender As Object, e As EventArgs) Handles MyBase.Load
HScrollBar1.Value = 45
VScrollBar1.Value = 45
'用來存放所有的線條
Dim line3dStack As New Queue(Of line3d)
line3dStack.Enqueue(New line3d(0, 0, 0, 0, 0, 180))
Do While line3dStack.Count <> 0
Dim wire As line3d = line3dStack.Dequeue
Dim q As New Point3D() With {.X = wire.x1, .Y = wire.y1, .Z = wire.z1}
Dim dx, dy, dz As Single
dx = wire.x2 - wire.x1
dy = wire.y2 - wire.y1
dz = wire.z2 - wire.z1
Dim rate As Single = 0.6
Dim Len As Single = Sqrt(dx * dx + dy * dy + dz * dz) * rate 'po長度
'樹的最小長度
If Len < 8 Then
Continue Do
End If
Dim p As New Point3D With {.X = wire.x2, .Y = wire.y2, .Z = wire.z2}
Dim o As New Point3D() With
{.X = p.X + dx * rate, .Y = p.Y + dy * rate, .Z = p.Z + dz * rate}
Dim n As New Vector3D(dx, dy, dz) 'po向量
Dim m As New Matrix3D() '繞po軸,o點旋轉的矩陣
m.RotateAt(New Quaternion(n, 120), o)
Dim mv As New Matrix3D()
Dim zz As Single
Dim pp As Vector3D
If dz = 0 Then
pp = New Vector3D(0, 0, 1)
Else
zz = -(dx + dy) / dz
pp = New Vector3D(1, 1, zz)
End If
mv.RotateAt(New Quaternion(New Vector3D(1, 1, zz), 90), o) '旋轉po到ABC平面
mv.ScaleAt(New Vector3D(1 / Sqrt(4), 1 / Sqrt(4), 1 / Sqrt(4)), o)
Dim b As Point3D = mv.Transform(p)
Dim a As Point3D = m.Transform(b)
Dim c As Point3D = m.Transform(a)
line3DList.Add(wire)
line3dStack.Enqueue(New line3d(p, a))
line3dStack.Enqueue(New line3d(p, b))
line3dStack.Enqueue(New line3d(p, c))
Loop
draw3d(line3DList, 45, 30) '45°,30°的視角觀察三維圖
End Sub
Dim line3DList As New List(Of line3d)
Sub draw3d(lines As List(Of line3d), a As Single, b As Single)
g.Clear(Color.Black)
a = a * PI / 180
b = b * PI / 180
For Each item As line3d In lines
Dim xoffset As Single = 500
Dim yoffset As Single = 700
Dim x1 As Single = item.x1 * Cos(a) - item.y1 * Sin(a) + xoffset
Dim y1 As Single = -item.x1 * Sin(a) * Sin(b) - item.y1 * Cos(a) * Cos(b) + item.z1 * Cos(b) + yoffset
Dim x2 As Single = item.x2 * Cos(a) - item.y2 * Sin(a) + xoffset
Dim y2 As Single = -item.x2 * Sin(a) * Sin(b) - item.y2 * Cos(a) * Cos(b) + item.z2 * Cos(b) + +yoffset
Dim rate As Single = 1
x2 *= rate
x1 *= rate
y1 *= rate
y2 *= rate
Dim startPoint As New Point(x1, y1)
Dim endPoint As New Point(x2, y2)
g.DrawLine(Pens.White, startPoint, endPoint)
Next
PictureBox1.Image = bmp
End Sub
Private Sub HScrollBar1_ValueChanged(sender As Object, e As EventArgs) Handles HScrollBar1.ValueChanged
draw3d(line3DList, HScrollBar1.Value, VScrollBar1.Value)
End Sub
Private Sub VScrollBar1_ValueChanged(sender As Object, e As EventArgs) Handles VScrollBar1.ValueChanged
draw3d(line3DList, HScrollBar1.Value, VScrollBar1.Value)
End Sub
End Class