在 AR 開發中,咱們常常須要經過手機屏幕與 AR 物體進行交互,好比旋轉、縮放和移動。node
通常來講,縮放最簡單,根據捏合手勢對物體進行放大縮小就好了; 旋轉通常是固定一個軸,常見是y軸,就是豎直方向不動,根據手勢在屏幕上的移動進行水平旋轉; 可是移動就稍微麻煩一點,在二維的手機屏幕上,操做三維物體的位置並不容易。常見的作法是點擊屏幕時,將物體固定在手機前,跟隨手機移動,鬆開後放下物體。git
徹底跟隨手機其實很簡單,就是直接將物體放在相機結點下(pointOfView) github
建立 Xcode 的默認 AR 項目,只須要多保存一個var shipNode:SCNNode!
就能夠了,其他代碼不變:
@IBOutlet var sceneView: ARSCNView!
var shipNode:SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// 此處保存一下 shipNode
shipNode = scene.rootNode.childNode(withName: "ship", recursively: true)!
// Set the scene to the view
sceneView.scene = scene
}
// 不用改
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
// 不用改
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
複製代碼
而後添加 touch 相關手勢swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let firstResult = sceneView.hitTest((touch?.location(in: sceneView))!, options: nil).first
if let node = firstResult?.node {
//這裏由於 shipNode 自身沒有 geometry 因此 hitTest 找到了它的子結點:shipMesh,因此它的父結點纔是 shipNode
if node == shipNode || node.parent == shipNode {
// 將 shipNode 從sceneView.scene.rootNode座標系下,轉換到sceneView.pointOfView座標系下
let matrixInPOV = sceneView.scene.rootNode.simdConvertTransform(shipNode.simdTransform, to: sceneView.pointOfView)
// 添加到相機結點下
sceneView.pointOfView?.addChildNode(shipNode)
shipNode.simdTransform = matrixInPOV
shipNode.opacity = 0.5;//半透明
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if shipNode.opacity < 1 {//這裏偷懶,用透明度作個判斷
// 將 shipNode 從sceneView.pointOfView座標系下,轉換到sceneView.scene.rootNode座標系下,傳 nil 默認就是 scene.rootNode
let matrixInRoot = sceneView.pointOfView!.simdConvertTransform(shipNode.simdTransform, to: nil)
// 回到原來的結點下
sceneView.scene.rootNode.addChildNode(shipNode)
shipNode.simdTransform = matrixInRoot
shipNode.opacity = 1;
}
}
複製代碼
但這種方式有個缺點,就是會改變物體的旋轉。因此更多使用下面的方式session
跟隨手機移動但不旋轉也不麻煩,只是不能直接將移動放置在手機結點下(pointOfView),而是須要不斷計算手機座標系下的位置,並移動物體。 app
這裏須要保存一下飛機在相機中的位置,因此添加一個var positionInPOV:simd_float3 = simd_make_float3(0, 0, 0)
@IBOutlet var sceneView: ARSCNView!
var shipNode:SCNNode!
var positionInPOV:simd_float3 = simd_make_float3(0, 0, 0)
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
shipNode = scene.rootNode.childNode(withName: "ship", recursively: true)!
// 須要成爲 session 的代理,以便在每次刷新時更新位置
sceneView.session.delegate = self
// Set the scene to the view
sceneView.scene = scene
}
複製代碼
咱們須要在點擊時,保存這個位置,而後不斷刷新計算它在世界座標中的位置,並更改飛機結點的位置(shipNode.simdPosition):ide
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let firstResult = sceneView.hitTest((touch?.location(in: sceneView))!, options: nil).first
if let node = firstResult?.node {
//這裏由於 shipNode 自身沒有 geometry 因此 hitTest 找到了它的子結點:shipMesh
if node == shipNode || node.parent == shipNode {
// 將 shipNode 的位置從sceneView.scene.rootNode座標系下,轉換到sceneView.pointOfView座標系下,並保存
positionInPOV = sceneView.scene.rootNode.simdConvertPosition(shipNode.simdPosition, to: sceneView.pointOfView)
shipNode.opacity = 0.5;
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if shipNode.opacity < 1 {//這裏偷懶,用透明度作個判斷
shipNode.opacity = 1;
}
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
if shipNode.opacity < 1 {//這裏偷懶,用透明度作個判斷
// 計算positionInPOV在sceneView.scene.rootNode座標系下的位置,這裏 nil 就表明了 rootNode;並更新 shipNode 的位置
let positionInRoot = sceneView.pointOfView!.simdConvertPosition(positionInPOV, to: nil)
shipNode.simdPosition = positionInRoot
}
}
複製代碼
若是你是直接在 Xcode 的默認 AR 項目中更改的,可能會以爲 AR 移動的效果有問題,尤爲是在旋轉的時候,shipNode 位置明顯沒有跟隨手指的位置。 測試
這個問題產生的緣由有兩個:解決的辦法,一是在座標轉換時,以 shipMesh 的實際位置爲準;二是直接將 shipMesh 相對其父結點的 position 改成 0。這裏爲了簡單,就採用了第二種: 動畫
位置調整後,再拖動或旋轉,就正常了: ui
在平面上移動,其實就是對平面不停地進行hitTest
操做,找到屏幕中心與平面的交點,而後移動物體就好了。
2018年的 Session 605 - Inside SwiftShot: Creating an AR Game 上演示了一個叫SwiftShot的多人遊戲Demo
其中涉及到的內容很是多。
註釋版代碼
關鍵代碼以下:
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// Update game board placement in physical world
// 更新遊戲底座在物理世界的位置
if gameManager != nil {
// this is main thread calling into init code
// 主線程調用
updateGameBoard(frame: frame)
}
}
// MARK: - Board management
func updateGameBoard(frame: ARFrame) {
// Perform hit testing only when ARKit tracking is in a good state.
// 只有在ARKit追蹤狀態正常時,才執行命中測試
if case .normal = frame.camera.trackingState {
// 執行 hitTest
if let result = sceneView.hitTest(screenCenter, types: [.estimatedHorizontalPlane, .existingPlaneUsingExtent]).first {
// 當初始化放置時,忽略那些太靠近相機的點
guard result.distance > 0.5 || sessionState == .placingBoard else { return }
// 更新物體的位置
gameBoard.update(with: result, camera: frame.camera)
} else {
sessionState = .lookingForSurface
if !gameBoard.isBorderHidden {
gameBoard.hideBorder()
}
}
}
}
複製代碼
本文中使用的最核心的API,其實只有兩個,算上相關聯的也只有四個。
先看前兩個hitTest
,用來從屏幕指定位置發射射線,進行命中測試的 API:
// 這是 SceneKit 的 API,主要用來返回被射線命中的 SCNNode 等物體
func hitTest(_ point: CGPoint, options: [SCNHitTestOption : Any]? = nil) -> [SCNHitTestResult]
// 這是 ARKit 的 API,主要用來返回被射線命中的 ARAnchor 等物體
open func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]
複製代碼
後兩個則是座標轉換的 API,用來將一個矩陣,從一個座標系轉換到另外一個座標系。另外還有向量和位置的轉換方法,此處再也不列出:
// 將 self (一個SCNNode 對象)座標系下的 transform,轉換到另外一個 node 的座標系下,若是另外一個 node 爲 nil,則轉換到 rootNode 的座標系下
open func simdConvertTransform(_ transform: simd_float4x4, to node: SCNNode?) -> simd_float4x4
// 將 node 對象座標系下的 transform,轉換到 self (一個SCNNode 對象) 的座標系下,若是 node 爲 nil,則表示來自 rootNode 座標系下
open func simdConvertTransform(_ transform: simd_float4x4, from node: SCNNode?) -> simd_float4x4
複製代碼