ARKit

ARKit 教學:實作火箭飛船發射,學習 SceneKit 和 Physics

ARKit 教學:實作火箭飛船發射,學習 SceneKit 和 Physics
ARKit 教學:實作火箭飛船發射,學習 SceneKit 和 Physics
In: ARKit, Swift 程式語言

它剛才移動了嗎?這是真的嗎?這其實是擴增實境 (Augmented Reality,簡稱 AR)。歡迎回到 ARKit 教程系列的第四部分。在本教程中,我們將在 ARKit 中理解 Physics 基礎知識,並且將在本教程結束之前製作一台飛船,開始吧!

首先,我們從下載初始項目開始。建立並運行它,應該會出現警示窗,要你允許在應用程序中使用相機。

physics-camera-access

點擊 OK,如果一切順利的話,應該可以看到相機視圖。

另外,本教程建立在先前教程的知識之上,如果在本文中遇到什麼難題,請隨時查閱 ARKit教程系列

理解 Physics Body

首先來介紹 physics body,這是建立相關應用的基本元件之一,為了讓 SceneKit 知道如何在應用程式中模擬一個 SceneKit 節點 (node),我們需要附加一個SCNPhysicsBodySCNPhysicsBody是一個將 physics simulation 添加到 node 的物件。

在渲染每一幀 (rendering a frame) 之前,SceneKit 執行 physics calculations 到具有附加 physics bodies 的節點。這些計算包括重力,摩擦以及與其他物體的碰撞,你也可以對 body 施加壓力和衝力,在這些計算之後,它更新節點的位置和方向,然後才渲染幀。

基本上,在每次渲染幀之前,都會進行物理計算。

Physics Body 類型

要構建 physics body,首先需要指定 physics body type。一個 physics body type 決定一個 physics body 如何與外在壓力和其他物體相互作用。三種 physics body types 分別是靜態的 (static),動態的 (dynamic) 和運動的 (kinematic)。

Static (靜態)

地板、牆壁和地形等類型的 SceneKit 物件適合使用靜態 physics body type。靜態類型不受力或碰撞影響,不能移動。

Dynamic (動態)

如果你想製作飛翔的噴火龍、Steph Curry 射籃、或火箭炮的爆破等效果,應該想要使用一個動態 physics body type 的 SceneKit 物件。動態類型是指可以受到壓力和碰撞影響的 physics body。

Kinematic (運動)

何時會想要使用 kinematic physics body 呢?例如當你想創建一個遊戲,需要用手指推動區塊,你就會創建一個由手指移動觸發的”隱形”推塊。這個“隱形”的推塊不受其他區塊影響,但是它能在接觸時推動其他區塊。運動類型是一個不受衝力或碰撞影響的 physics body,但在移動時可碰撞影響其他物體,換言之,它可以主動影響別人,但不能被影響。

創建一個 Physics Body

讓我們開始吧,先給 detected horizontal plane (水平檢測平台) 一個靜態的 physics body,這樣就有了穩固的基礎,讓我們的飛船能夠站穩腳跟。

ViewController.swiftrenderer(_:didUpdate:for:)下面添加以下方法:

func update(_ node: inout SCNNode, withGeometry geometry: SCNGeometry, type: SCNPhysicsBodyType) {
    let shape = SCNPhysicsShape(geometry: geometry, options: nil)
    let physicsBody = SCNPhysicsBody(type: type, shape: shape)
    node.physicsBody = physicsBody
}

在這個方法中,我們創建了一個SCNPhysicsShape物件。一個SCNPhysicsShape物件代表 physics body 的形狀,當 SceneKit 檢測到你場景的SCNPhysicsBody物件連結時,它會使用你定義的 physics shapes,而不是 rendered geometry 的可見 (visible) 物件。

接下來,我們將.static傳入到類型參數和SCNPhysicsShape物件中,藉此來創建一個SCNPhysicsBody物件。

然後,我們將節點的 physics body 設為剛才創建的 physics body。

附加一個靜態 Physics Body

現在,我們將在renderer(_:didAdd:for:)方法內部檢測到的平面附加一個 static physics body。在將planeNode添加為子節點之前,請調用以下方法:

update(&planeNode, withGeometry: plane, type: .static)

更改後,你的renderer(_:didAdd:for:)方法現在應該如下所示:

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
        
        let width = CGFloat(planeAnchor.extent.x)
        let height = CGFloat(planeAnchor.extent.z)
        let plane = SCNPlane(width: width, height: height)
        
        plane.materials.first?.diffuse.contents = UIColor.transparentWhite
        
        var planeNode = SCNNode(geometry: plane)
        
        let x = CGFloat(planeAnchor.center.x)
        let y = CGFloat(planeAnchor.center.y)
        let z = CGFloat(planeAnchor.center.z)
        planeNode.position = SCNVector3(x,y,z)
        planeNode.eulerAngles.x = -.pi / 2
        
        update(&planeNode, withGeometry: plane, type: .static)
        
        node.addChildNode(planeNode)
    }

當我們的檢測平台接收到新訊息進行更新之後,它可能會改變 geometry。因此,我們需要在render(_:didUpdate:for:)中調用相同的方法:

update(&planeNode, withGeometry: plane, type: .static)

現在,修改後的render(_:didUpdate:for:)方法應該看起來像這樣:

    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as?  ARPlaneAnchor,
            var planeNode = node.childNodes.first,
            let plane = planeNode.geometry as? SCNPlane
            else { return }
        
        let width = CGFloat(planeAnchor.extent.x)
        let height = CGFloat(planeAnchor.extent.z)
        plane.width = width
        plane.height = height
        
        let x = CGFloat(planeAnchor.center.x)
        let y = CGFloat(planeAnchor.center.y)
        let z = CGFloat(planeAnchor.center.z)
        
        planeNode.position = SCNVector3(x, y, z)
        
        update(&planeNode, withGeometry: plane, type: .static)
    }

做得不錯!

附加 Dynamic Physics Body

要讓火箭節點能受到壓力和碰撞的影響,現在我們要給這個節點一個 dynamic physics body。在ViewController中宣告一個 rocketship 節點名稱的常數:

let rocketshipNodeName = "rocketship"

然後在addRocketshipToSceneView(withGestureRecognizer:)方法中,在調整 rocketship node position 的程式碼後面添加下面的 code:

let physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
rocketshipNode.physicsBody = physicsBody
rocketshipNode.name = rocketshipNodeName

我們給了rocketshipNode一個名字和 dynamic physics body,我們待會會使用名字來識別rocketshipNode。建立並運行它,應該能夠看到如下圖的運行結果:

rocket-landing

施加力量

我們現在要對飛船施加力量。

在這之前,我們需要一個觸發動作的方法,UISwipeGestureRecognizer可以幫助我們做到這一點。首先,在addRocketshipToSceneView(withGestureRecognizer:)方法的下面添加以下程式碼:

func getRocketshipNode(from swipeLocation: CGPoint) -> SCNNode? {
    let hitTestResults = sceneView.hitTest(swipeLocation)
        
    guard let parentNode = hitTestResults.first?.node.parent,
        parentNode.name == rocketshipNodeName
        else { return nil }
                
    return parentNode
}

這個方法可以幫助我們從滑動手勢的位置獲得火箭飛船節點。你可能想知道為什麼我們需要設下多個條件來解包 parentNode,是因為來自 hitTestResults 的返回節點可能是組成火箭的五個節點中的任何一個。

rocket-ship-3dmodel

在前一個方法的正下方,添加以下函式:

@objc func applyForceToRocketship(withGestureRecognizer recognizer: UIGestureRecognizer) {
    // 1
    guard recognizer.state == .ended else { return }
    // 2
    let swipeLocation = recognizer.location(in: sceneView)
    // 3
    guard let rocketshipNode = getRocketshipNode(from: swipeLocation),
        let physicsBody = rocketshipNode.physicsBody
        else { return }
    // 4
    let direction = SCNVector3(0, 3, 0)
    physicsBody.applyForce(direction, asImpulse: true)
}

從上面的程式碼中,我們做了以下事情:

  1. 確認滑動手勢狀態為已結束。
  2. 從滑動位置獲取 hit test results。
  3. 查看滑動手勢是否在火箭飛船上執行過。
  4. 我們將 y 方向的力施加到父節點 (parent node) 的 physics body。如果你有注意到,我們也把衝力的參數設置為 true,這施用於衝力的瞬時變化,來立即加速 physics body。基本上,這個選項可以在設置為 true 時,模擬物體發射時的瞬間效果。

太好了,建立並運行它吧。請向上滑動飛船,你應該能夠在你的飛船上施加一個力量!

rocket-jump

添加 SceneKit Particle System 並更改物理屬性

起始專案在 “Particles” 文件夾內一個 reactor SceneKit 的粒子系統 (particle system)。

explode

在本教程中,我們不會介紹如何創建一個 SceneKit 粒子系統。讓我們來繼續了解如何將 SceneKit 粒子系統,添加到節點及其 physics 的屬性。

打開ViewController.swift,在ViewController中宣告以下變數:

var planeNodes = [SCNNode]()

renderer(_:didAdd:for:)函式裡面,添加以下這行 code 做為函式內的最後一行程式碼:

planeNodes.append(planeNode)

簡單地說,當檢測到一個新的平面時,將其附加到 planeNodes 陣列上。我們稍後會將 planeNodes 賦給 reactorParticleSystem 的 colliderNodes 屬性。

renderer(_:didAdd:for:)函式的下方,實作下面的委託方法 (delegate method):

func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
    guard anchor is ARPlaneAnchor,
        let planeNode = node.childNodes.first
        else { return }
    planeNodes = planeNodes.filter { $0 != planeNode }
}

當對應於一個已被移除 ARAnchor 的 SceneKit node 從場景中被刪除時,就調用這個委託方法。這此同時,我們會在 planeNodes 陣列中過濾已被移除的 plane node。

接下來,在applyForceToRocketship(withGestureRecognizer:)函式的正下方添加以下方法:

@objc func launchRocketship(withGestureRecognizer recognizer: UIGestureRecognizer) {
    // 1
    guard recognizer.state == .ended else { return }
    // 2
    let swipeLocation = recognizer.location(in: sceneView)
    guard let rocketshipNode = getRocketshipNode(from: swipeLocation),
        let physicsBody = rocketshipNode.physicsBody,
        let reactorParticleSystem = SCNParticleSystem(named: "reactor", inDirectory: nil),
        let engineNode = rocketshipNode.childNode(withName: "node2", recursively: false)
        else { return }
    // 3
    physicsBody.isAffectedByGravity = false
    physicsBody.damping = 0
    // 4
    reactorParticleSystem.colliderNodes = planeNodes
    // 5
    engineNode.addParticleSystem(reactorParticleSystem)
    // 6
    let action = SCNAction.moveBy(x: 0, y: 0.3, z: 0, duration: 3)
    action.timingMode = .easeInEaseOut
    rocketshipNode.runAction(action)
}

在以上的程式碼中,我們做了以下動作:

  1. 確保滑動手勢狀態已結束。
  2. 像剛才一樣安全地解包 (unwrapped) rocketshipNode 及 physicsBody。此外,也安全解包 reactorParticleSystem 和 engineNode。希望將 reactorParticleSystem 添加到飛船的引擎上
  3. 將 physics body *受重力影響*的屬性設置為 false。重力不會再影響飛船節點,我們還將阻尼 (damping) 屬性設置為零,阻尼屬性模擬流體摩擦或空氣阻力對 body 的影響,將其設置為零,就可以將導致火箭節點的 physics body 上流體摩擦或空氣阻力的影響零。
  4. 將 planeNodes 設置為 reactorParticleSystem 的 colliderNodes。這將使粒子系統中的粒子在接觸時從檢測平面反彈,而不是直接飛行穿過它們。
  5. 將 reactorParticleSystem 添加到 engineNode 上。
  6. 我們將火箭節點向上移動了 0.3 米,並且選擇 easeInEaseOut 動畫效果。

添加滑動手勢

在施加壓力發射火箭之前,需要在我們的場景視圖上添加 swipe gesture recognizers。請在addTapGestureToSceneView()下方添加這段程式碼:

func addSwipeGesturesToSceneView() {
    let swipeUpGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.applyForceToRocketship(withGestureRecognizer:)))
    swipeUpGestureRecognizer.direction = .up
    sceneView.addGestureRecognizer(swipeUpGestureRecognizer)
    
    let swipeDownGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.launchRocketship(withGestureRecognizer:)))
    swipeDownGestureRecognizer.direction = .down
    sceneView.addGestureRecognizer(swipeDownGestureRecognizer)
}

手勢向上滑動將對飛船節點施加力量,向下滑動則將啟動火箭,Nice!

最後,請在viewDidLoad()調用它:

addSwipeGesturesToSceneView()

以上就是本文的教學內容!

Showtime!

恭喜你,現在到了展示成果的時刻,嘗試向下滑動,看看得到什麼結果吧!

rocket-init

向下滑動,火箭起飛了!

rocket-launch

結語

希望你喜歡這篇教程,並能從中學到一些寶貴的東西。我們也歡迎讀者在社交網絡上分享這篇教程,讓你的朋友也可以獲得這些知識!

以供參考,你可以在GitHub下載專案範例

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校ALPHA Camp畢業後,積極投入iOS程式開發,目前任職於國內電商公司。聯絡方式:電郵[email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文ARKit Tutorial: Understanding Physics by Launching a Rocketship

作者
Jayven N
年輕、富創意的Jayven熱愛手機程式設計,善於發掘和凸顯不平凡處,透過文章抒發其獨特性。閒時喜歡做健身、看UFC。希望了解Jayven更多,可以到訪他的Medium平台或在LinkedIn跟他聯繫。
評論
更多來自 AppCoda 中文版
透過 Reality Composer 和 RealityKit 輕鬆地創建 3D AR Apps
ARKit

透過 Reality Composer 和 RealityKit 輕鬆地創建 3D AR Apps

RealityKit 是 2019 年推出的新框架,用於實作高性能 3D 模擬和渲染功能,而 Reality Composer 就讓初學者無需編寫任何程式碼,都可以輕鬆地創建互動的 AR 體驗。在這篇文章中,你將學會使用這兩個框架,構建互動的 3D AR App。
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。