本文與代碼地址中的README.md文件搭配閱讀,效果更佳.node
ARKit系列文章目錄git
2017年的WWDC,蘋果演示過ARKit的一個Demo,名爲AR Interaction
,不只演示了ARKit的效果,還演示了AR應用的設計原則,交互邏輯.所以蘋果叫Handling 3D Interaction and UI Controls in Augmented Realitygithub
即:處理加強現實中的3D交互和UI控制.swift
下面咱們就來分三步,研究學習一下這個項目:app
以下圖,總共分爲如下幾個部分:控制器,控制器的分類,處理虛擬物體交互類,自定義手勢,自定義ARView,虛擬物體及其加載器,聚焦框,頂部狀態子控制器,底部列表子控制器. async
ViewController:UI設置,代理設置,AR屬性配置,生命週期 ViewController+ARSCNViewDelegate:AR場景更新,節點添加,錯誤信息 ViewController+Actions:界面UI操做,按鈕點擊,觸摸等 ViewController+ObjectSelection:虛擬物體的加載,移動ide
這個類的HitTestRay
結構體中,intersectionWithHorizontalPlane(atY planeY: Float)
方法,須要求出射線原點到(與平面)交點的距離,這裏用到了線性代數中點乘的概念:射線原點到交點的向量
與歸一化後方向向量
的倍數,其實就是距離.可是由於交點座標尚不肯定(只有y值肯定,必定在平面上),因此二者都點乘上平面法線向量,巧妙地消去了x和z的值,獲得了距離.函數
其實也能夠用初中知識,類似三角形來理解,紅色爲歸一化後的方向向量: post
下面講得這個方法已經變動了,由於ARKit後來推出了識別豎直平面的功能,官方demo中相應邏輯也作了變動,具體請看更新後的註釋版代碼.學習
另外還有worldPosition(fromScreenPosition position: CGPoint, objectPosition: float3?, infinitePlane: Bool = false)
方法,求點擊屏幕後,從屏幕中心發出的射線,命中的錨點或特徵點雲的位置.共分了5步:
hitTest(position, types: .existingPlaneUsingExtent)
獲取命中的平面,有的話直接返回;hitTestWithFeatures(position, coneOpeningAngleInDegrees: 18, minDistance: 0.2, maxDistance: 2.0).first
獲取射線錐體範圍內找到的特徵點雲,暫不返回;/** Hit tests from the provided screen position to return the most accuarte result possible. Returns the new world position, an anchor if one was hit, and if the hit test is considered to be on a plane. 從指定的屏幕位置發起命中測試,返回最精確的結果. 返回新世界座標位置,命中平面的錨點. */
func worldPosition(fromScreenPosition position: CGPoint, objectPosition: float3?, infinitePlane: Bool = false) -> (position: float3, planeAnchor: ARPlaneAnchor?, isOnPlane: Bool)? {
/* 1. Always do a hit test against exisiting plane anchors first. (If any such anchors exist & only within their extents.) 1. 優先對已存在的平面錨點進行命中測試.(若是有錨點存在&在他們的範圍內) */
let planeHitTestResults = hitTest(position, types: .existingPlaneUsingExtent)
if let result = planeHitTestResults.first {
let planeHitTestPosition = result.worldTransform.translation
let planeAnchor = result.anchor
// Return immediately - this is the best possible outcome.
// 直接返回 - 這是最佳的輸出.
return (planeHitTestPosition, planeAnchor as? ARPlaneAnchor, true)
}
/* 2. Collect more information about the environment by hit testing against the feature point cloud, but do not return the result yet. 2. 根據命中測試遇到的特徵點雲,收集更多環境信息,可是暫不返回結果. */
let featureHitTestResult = hitTestWithFeatures(position, coneOpeningAngleInDegrees: 18, minDistance: 0.2, maxDistance: 2.0).first
let featurePosition = featureHitTestResult?.position
/* 3. If desired or necessary (no good feature hit test result): Hit test against an infinite, horizontal plane (ignoring the real world). 3. 若是須要的話(沒有發現足夠好的特徵命中測試結果):命中測試遇到一個無限大的水平面(忽略真實世界). */
if infinitePlane || featurePosition == nil {
if let objectPosition = objectPosition,
let pointOnInfinitePlane = hitTestWithInfiniteHorizontalPlane(position, objectPosition) {
return (pointOnInfinitePlane, nil, true)
}
}
/* 4. If available, return the result of the hit test against high quality features if the hit tests against infinite planes were skipped or no infinite plane was hit. 4. 若是可用的話,當命中測試遇到無限平面被忽略或者沒有遇到無限平面,則返回命中測試遇到的高質量特徵點. */
if let featurePosition = featurePosition {
return (featurePosition, nil, false)
}
/* 5. As a last resort, perform a second, unfiltered hit test against features. If there are no features in the scene, the result returned here will be nil. 5. 最後萬不得已時,執行備份方案,返回未過濾的命中測試遇到的特徵點. 若是場景中沒有特徵點,返回結果將是nil. */
let unfilteredFeatureHitTestResults = hitTestWithFeatures(position)
if let result = unfilteredFeatureHitTestResults.first {
return (result.position, nil, false)
}
return nil
}
複製代碼
蘋果在這裏給出了一個幾乎完美的方案<ARKit1.5後邏輯已變動,請看更新後的代碼>:
這樣充分利用了特徵點雲數據,即便AR識別不穩定,暫未識別出平面,也能用特徵點繼續玩AR,固然了,犧牲一些精度再所不免.
另外當識別到平面後,還會把附近的特徵點上的物體,慢慢移動到新發現的平面上.這樣體驗更加完善,不會讓暫時性的精度問題一直影響AR體驗.
移動是在ViewController+ARSCNViewDelegate
中調用下面方法來實現這個移動:
updateQueue.async {
for object in self.virtualObjectLoader.loadedObjects {
object.adjustOntoPlaneAnchor(planeAnchor, using: node)
}
}
複製代碼
這個類中,須要將聚焦框老是以特定角度對準攝像機.updateTransform(for position: float3, camera: ARCamera?)
這個方法專門來處理這個問題:
其中校訂Y軸實際上是爲了當人拿着手機,左右轉身時,聚焦框不只保持在手機屏幕中間,還能夠同步旋轉以始終保持與手機屏幕底邊平行.
當人拿着手機左右轉身時,手機實際上是在豎直和水平狀態之間變化的,請看個人靈魂繪畫:首先經過let tilt = abs(camera.eulerAngles.x)
獲得手機的俯仰狀態(水平仍是豎直),而後分三種狀況:
relativeInRange
,而後用normalize()
計算最短旋轉角度(畢竟向右轉270度和向左轉90度效果是同樣的),最後用線性插值獲得混合後的旋轉角;yaw偏航
值(左右轉的角度),即方位角;// Correct y rotation of camera square.
// 校訂攝像機的y軸旋轉
guard let camera = camera else { return }
let tilt = abs(camera.eulerAngles.x)
let threshold1: Float = .pi / 2 * 0.65
let threshold2: Float = .pi / 2 * 0.75
let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x)
var angle: Float = 0
switch tilt {
case 0..<threshold1:
angle = camera.eulerAngles.y
case threshold1..<threshold2:
let relativeInRange = abs((tilt - threshold1) / (threshold2 - threshold1))
let normalizedY = normalize(camera.eulerAngles.y, forMinimalRotationTo: yaw)
angle = normalizedY * (1 - relativeInRange) + yaw * relativeInRange
default:
angle = yaw
}
eulerAngles.y = angle
複製代碼
其中求偏航值用到了atan2f(y,x)
這個求方位角的函數,只要傳入對應的y值,x值,就能夠獲得(x,y)點相對座標原點的夾角
let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x)
複製代碼
這其中用到了矩陣的相關知識:數學課本與微軟D3D用的是左手準則(行主序),而OpenGL與蘋果SceneKit用的是右手準則(列主序).排列以下:
其實每一個矩陣就至關於一個小的局部座標系,其中(Tx,Ty,Tz)
至關於局部座標系相對於世界座標系的偏移量.(Xx,Xy,Xz)
是局部座標系的X軸位置,(Yx,Yy,Yz)
是局部座標系的Y軸位置,(Zx,Zy,Zz)
是局部座標系的Z軸位置.最後右下角的1至關於全局縮放比例,通常不調整.
因此在計算atan2f()時,實際上是用到了(Yx,Xx)來計算方位角:
蘋果的這個Demo算是給出了AR應用開發的最佳實踐,不只從技術層面充分發揮出ARKit的所有潛力(截止2018年初),並且保證了良好的用戶體驗和交互邏輯.
若是須要開發本身的AR應用,建議模仿這個Demo的交互邏輯,加強本身app的用戶體驗.