6-2c 地球繞太陽公轉 我們已經做出與黃道面垂線傾斜23.5°的地球自轉,若能繼續加入太陽,並讓地球公轉,相信接下來做整個太陽系也就水到渠成了。 太陽的3D模型做法跟地球類似,搜尋 “Full sun map”,或在 NASA “Scientific Visualization Studio” 網站 有太陽及各大行星的全景圖,這次我們只需紀錄網址,透過程式自動抓圖,省掉手動匯入Swift Playgrounds的麻煩。 接下來的重頭戲是如何讓地球繞著太陽公轉。 前一節所用的旋轉動畫,從節點.rotate(x, y, z, w) 參數可以看出,轉軸似乎僅用一個點座標 (x, y, z)來指定,其實這是從原點出發的向量值,表示轉軸方向。這裡的原點是節點的區域座標原點,通常是3D物件的中心點。 也就是說,旋轉動畫的轉軸,應該會經過3D物件的中心點;而地球繞太陽公轉時,我們需要的是以太陽南北極為軸的旋轉動畫,旋轉軸並不經過地球核心,怎麼辦呢? 雖然有難度,但至少有三種解決辦法:
仿照前一節做法,先做一個旋轉的太陽核心(藏在太陽中心點),作為地球(及自轉軸)的父節點,這樣即使地球距離很遠,仍然會與太陽核心同步轉動。
在第4單元第6課4-6a 曾經做過圓周運動,同樣可以用TimelineView來做地球公轉。
SceneKit 每個節點都有個 pivot 屬性,可用來變更旋轉軸心到任何位置。
第1種做法相對比較單純,就當作業,請自行練習;本節示範用TimelineView做法,進一步結合SceneKit與SwiftUI;至於第3種做法,語法最簡單,但背後牽涉到變換矩陣的數學原理,反而最不容易懂,下一課再說明。 在實際做公轉動畫之前,我們先來做一個空間座標系,畫出場景中的 X, Y, Z 軸,並加上公轉軌道,以便觀察地球與太陽的相對運動。 呈現的效果如下圖: 空間座標系的程式定義為函式,先用 SCNTube 做出3個長軸,預設是沿著Y軸,所以X軸、Z軸要分別旋轉90度;接著再用SCNTube做一個大圓,當作地球公轉的軌道;然後將X/Y/Z軸及公轉軌道都透過 addChildNode 綁在座標原點,最後以座標原點的節點當作函式回傳值。func 空間座標系(尺寸半徑: CGFloat) -> SCNNode { let x軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0) let x軸節點 = SCNNode(geometry: x軸) x軸節點.rotation = SCNVector4(x: 0, y: 0, z: 1, w: .pi / -2.0) let y軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0) let y軸節點 = SCNNode(geometry: y軸) let z軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0) let z軸節點 = SCNNode(geometry: z軸) z軸節點.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi / 2.0) let 公轉軌道 = SCNTube(innerRadius: 尺寸半徑, outerRadius: 尺寸半徑 + 0.01, height: 0.01) let 軌道節點 = SCNNode(geometry: 公轉軌道) let 座標原點 = SCNNode(geometry: SCNSphere(radius: 0.05)) 座標原點.addChildNode(x軸節點) 座標原點.addChildNode(y軸節點) 座標原點.addChildNode(z軸節點) 座標原點.addChildNode(軌道節點) return 座標原點 }
至於實際的公轉動畫,還記得第4單元第6課 的圓周運動嗎?其中的流程很簡單,用TimelineView送一個時間參數給另一個視圖(如旋轉地球),而視圖(旋轉地球)在 .onChange { } 裡面每次增加一點圓心角,然後計算圓周(即地球在公轉軌道的)位置即可。 具體寫法如下:.onChange(of: 時間) { _ in 圓心角 += 0.2 if 圓心角 > 360.0 { 圓心角 = 0.0 } let 地球節點 = 外太空.rootNode.childNode(withName: "地球軸心", recursively: true) 地球節點?.position.x = Float(公轉半徑) * sin(圓心角 / 180.0 * .pi) 地球節點?.position.z = Float(公轉半徑) * cos(圓心角 / 180.0 * .pi) }
圓心角每次增加0.2度,然後找出地球軸心節點,重新計算在軌道上的位置。這裡用到子節點的搜尋,記得對軸心節點加上名稱(.name)屬性。 另外還有一行關鍵的程式碼要改,原來設定外太空場景 “let 外太空 = SCNScene()”,必須改為 “@State var 外太空 = SCNScene()”,否則 TimelineView 每次更新時間送入「地球公轉()」時,「外太空」場景會被重新初始化,導致畫面會一片空白。 完整的程式碼如下,程式稍長,但大部分是已講解過的語法:// 6-2c 地球公轉 // Created by Heman, 2024/03/18 import SceneKit import SwiftUI struct 地球公轉: View { @State var 外太空 = SCNScene() @State var 圓心角: Float = 0.0 let 時間: Date let 地球半徑: CGFloat = 1.0 // 6371000m (6,371Km) let 太陽半徑: CGFloat = 3.0 // 696340000m (696,340Km) let 公轉半徑: CGFloat = 10.0 // 149590000000m (橢圓平均半徑 149,590,000km) let 自轉時間 = 2.0 // 86400s (1天) let 公轉時間 = 30.0 // 31556926s (365天) let 太陽全景圖網址 = "https://svs.gsfc.nasa.gov/vis/a030000/a030300/a030362/euvi_aia304_2012_carrington_print.jpg" func 空間座標系(尺寸半徑: CGFloat) -> SCNNode { let x軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0) let x軸節點 = SCNNode(geometry: x軸) x軸節點.rotation = SCNVector4(x: 0, y: 0, z: 1, w: .pi / -2.0) let y軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0) let y軸節點 = SCNNode(geometry: y軸) let z軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0) let z軸節點 = SCNNode(geometry: z軸) z軸節點.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi / 2.0) let 公轉軌道 = SCNTube(innerRadius: 尺寸半徑, outerRadius: 尺寸半徑 + 0.01, height: 0.01) let 軌道節點 = SCNNode(geometry: 公轉軌道) let 座標原點 = SCNNode(geometry: SCNSphere(radius: 0.05)) 座標原點.addChildNode(x軸節點) 座標原點.addChildNode(y軸節點) 座標原點.addChildNode(z軸節點) 座標原點.addChildNode(軌道節點) return 座標原點 } var body: some View { SceneView(scene: 外太空, options: [.allowsCameraControl]) .onChange(of: 時間) { _ in 圓心角 += 0.2 if 圓心角 > 360.0 { 圓心角 = 0.0 } let 地球節點 = 外太空.rootNode.childNode(withName: "地球軸心", recursively: true) 地球節點?.position.x = Float(公轉半徑) * sin(圓心角 / 180.0 * .pi) 地球節點?.position.z = Float(公轉半徑) * cos(圓心角 / 180.0 * .pi) } .task { if let myURL = URL(string: 太陽全景圖網址) { do { let (內容, 回應碼) = try await URLSession.shared.data(from: myURL) if let 太陽全景圖 = UIImage(data: 內容) { let 太陽材質 = SCNMaterial() 太陽材質.diffuse.contents = 太陽全景圖 let 太陽節點 = 外太空.rootNode.childNode(withName: "太陽", recursively: true) 太陽節點?.geometry?.materials = [太陽材質] } } catch { print("無法下載圖片") } } } .onAppear { let 太陽 = SCNSphere(radius: 太陽半徑) let 太陽節點 = SCNNode(geometry: 太陽) 太陽節點.name = "太陽" // 動態更新貼圖材質 let 地球材質 = SCNMaterial() 地球材質.diffuse.contents = UIImage(named: "earth.jpg") let 地球 = SCNSphere(radius: 地球半徑) 地球.materials = [地球材質] let 地球節點 = SCNNode(geometry: 地球) 地球節點.rotation = SCNVector4(0, 1, 0, 0) let 自轉動畫 = CABasicAnimation(keyPath: "rotation.w") 自轉動畫.toValue = 2.0 * .pi 自轉動畫.duration = 自轉時間 自轉動畫.repeatCount = .infinity 地球節點.addAnimation(自轉動畫, forKey: nil) let 傾斜度: Float = -23.5 / 180.0 * .pi let 地球軸心 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 3) let 地軸節點 = SCNNode(geometry: 地球軸心) 地軸節點.name = "地球軸心" // 動態計算座標位置 地軸節點.position = SCNVector3(0, 0, 公轉半徑) 地軸節點.rotation = SCNVector4(x: 0, y: 0, z: 1, w: 傾斜度) 地軸節點.addChildNode(地球節點) 外太空.rootNode.addChildNode(空間座標系(尺寸半徑: 公轉半徑)) 外太空.rootNode.addChildNode(地軸節點) 外太空.rootNode.addChildNode(太陽節點) 外太空.background.contents = UIColor.darkGray } } } struct 時間軸: View { var body: some View { TimelineView(.periodic(from: Date(), by: 0.03)) { 時間參數 in 地球公轉(時間: 時間參數.date) } } } import PlaygroundSupport PlaygroundPage.current.setLiveView(時間軸())
執行結果如下:VIDEO 🖖 作業
用內文提到的方法1(加個太陽核心)做出地球公轉動畫。
從其他網站找太陽全景圖的替換網址,如Solar System Scope
將空間座標系的X/Y/Z軸加上箭頭。
試著將“@State var 外太空 = SCNScene()”改回“let 外太空 = SCNScene()”,看看有何變化,想一想為什麼會這樣?