在 AR 开发中,咱们常常须要经过手机屏幕与 AR 物体进行交互,好比旋转、缩放和移动。node
通常来讲,缩放最简单,根据捏合手势对物体进行放大缩小就好了; 旋转通常是固定一个轴,常见是y轴,就是竖直方向不动,根据手势在屏幕上的移动进行水平旋转; 可是移动就稍微麻烦一点,在二维的手机屏幕上,操做三维物体的位置并不容易。常见的作法是点击屏幕时,将物体固定在手机前,跟随手机移动,松开后放下物体。git
彻底跟随手机其实很简单,就是直接将物体放在相机结点下(pointOfView) github
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
复制代码