简短回答:
要获得像苹果演示项目中那样流畅的拖动效果,您必须像在苹果演示项目(处理3D交互)中一样进行操作。另一方面,我同意您的观点,如果您第一次查看代码可能会感到困惑。对于放置在地板平面上的对象计算正确的移动量并不容易 - 总是从每个位置或视角进行计算。这是一个复杂的代码结构,可以实现出色的拖动效果。苹果公司做得很好,但并没有让我们轻松地实现它。
完整回答:
为了获得所需的结果,剥离AR Interaction模板会变成噩梦 - 但如果您投入足够的时间,也应该能够实现。如果您喜欢从头开始,请基本上使用常见的swift ARKit / SceneKit Xcode模板(包含太空飞船)。
您还需要从苹果获取整个AR交互模板项目(链接包含在SO问题中)。
最终,您应该能够拖动名为VirtualObject的东西,它实际上是一个特殊的SCNNode。此外,您将拥有一个漂亮的焦点方块,可以用于任何目的 - 比如最初放置对象或添加地板或墙壁。(一些拖动效果和焦点方块使用的代码有点合并或链接在一起 - 如果没有焦点方块,实际上会更加复杂)
开始吧:
将以下文件从AR交互模板复制到您的空项目中:
- Utilities.swift(通常我将此文件命名为Extensions.swift,其中包含一些基本扩展,这些扩展是必需的)
- FocusSquare.swift
- FocusSquareSegment.swift
- ThresholdPanGesture.swift
- VirtualObject.swift
- VirtualObjectLoader.swift
- VirtualObjectARView.swift
像这样将UIGestureRecognizerDelegate添加到ViewController类定义中:
class ViewController: UIViewController, ARSCNViewDelegate, UIGestureRecognizerDelegate {
将以下代码添加到您的ViewController.swift文件中定义部分的viewDidLoad之前:
var focusSquare = FocusSquare()
var screenCenter: CGPoint {
let bounds = sceneView.bounds
return CGPoint(x: bounds.midX, y: bounds.midY)
}
var isFocusSquareEnabled : Bool = true
private var currentTrackingPosition: CGPoint?
var selectedObject: VirtualObject?
private var trackedObject: VirtualObject? {
didSet {
guard trackedObject != nil else { return }
selectedObject = trackedObject
}
}
let translateAssumingInfinitePlane = true
在viewDidLoad中,在设置场景之前添加以下代码:
// *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
let panGesture = ThresholdPanGesture(target: self, action: #selector(didPan(_:)))
panGesture.delegate = self
// Add gestures to the `sceneView`.
sceneView.addGestureRecognizer(panGesture)
// *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
在你的ViewController.swift文件的最后添加如下代码:
@objc
func didPan(_ gesture: ThresholdPanGesture) {
switch gesture.state {
case .began:
if let object = objectInteracting(with: gesture, in: sceneView) {
trackedObject = object
}
case .changed where gesture.isThresholdExceeded:
guard let object = trackedObject else { return }
let translation = gesture.translation(in: sceneView)
let currentPosition = currentTrackingPosition ?? CGPoint(sceneView.projectPoint(object.position))
currentTrackingPosition = CGPoint(x: currentPosition.x + translation.x, y: currentPosition.y + translation.y)
gesture.setTranslation(.zero, in: sceneView)
case .changed:
break
case .ended:
guard let existingTrackedObject = trackedObject else { break }
addOrUpdateAnchor(for: existingTrackedObject)
fallthrough
default:
currentTrackingPosition = nil
trackedObject = nil
}
}
func addOrUpdateAnchor(for object: VirtualObject) {
if let anchor = object.anchor {
sceneView.session.remove(anchor: anchor)
}
let newAnchor = ARAnchor(transform: object.simdWorldTransform)
object.anchor = newAnchor
sceneView.session.add(anchor: newAnchor)
}
private func objectInteracting(with gesture: UIGestureRecognizer, in view: ARSCNView) -> VirtualObject? {
for index in 0..<gesture.numberOfTouches {
let touchLocation = gesture.location(ofTouch: index, in: view)
if let object = virtualObject(at: touchLocation) {
return object
}
}
return virtualObject(at: (gesture.view?.center)!)
}
func virtualObject(at point: CGPoint) -> VirtualObject? {
let hitTestResults = sceneView.hitTest(point, options: [SCNHitTestOption.categoryBitMask: 0b00000010, SCNHitTestOption.searchMode: SCNHitTestSearchMode.any.rawValue as NSNumber])
return hitTestResults.lazy.compactMap { result in
return VirtualObject.existingObjectContainingNode(result.node)
}.first
}
@objc
func updateObjectToCurrentTrackingPosition() {
guard let object = trackedObject, let position = currentTrackingPosition else { return }
translate(object, basedOn: position, infinitePlane: translateAssumingInfinitePlane, allowAnimation: true)
}
func translate(_ object: VirtualObject, basedOn screenPos: CGPoint, infinitePlane: Bool, allowAnimation: Bool) {
guard let cameraTransform = sceneView.session.currentFrame?.camera.transform,
let result = smartHitTest(screenPos,
infinitePlane: infinitePlane,
objectPosition: object.simdWorldPosition,
allowedAlignments: [ARPlaneAnchor.Alignment.horizontal]) else { return }
let planeAlignment: ARPlaneAnchor.Alignment
if let planeAnchor = result.anchor as? ARPlaneAnchor {
planeAlignment = planeAnchor.alignment
} else if result.type == .estimatedHorizontalPlane {
planeAlignment = .horizontal
} else if result.type == .estimatedVerticalPlane {
planeAlignment = .vertical
} else {
return
}
let transform = result.worldTransform
let isOnPlane = result.anchor is ARPlaneAnchor
object.setTransform(transform,
relativeTo: cameraTransform,
smoothMovement: !isOnPlane,
alignment: planeAlignment,
allowAnimation: allowAnimation)
}
添加一些焦点方形代码
func updateFocusSquare(isObjectVisible: Bool) {
if isObjectVisible {
focusSquare.hide()
} else {
focusSquare.unhide()
}
if let camera = sceneView.session.currentFrame?.camera, case .normal = camera.trackingState,
let result = smartHitTest(screenCenter) {
DispatchQueue.main.async {
self.sceneView.scene.rootNode.addChildNode(self.focusSquare)
self.focusSquare.state = .detecting(hitTestResult: result, camera: camera)
}
} else {
DispatchQueue.main.async {
self.focusSquare.state = .initializing
self.sceneView.pointOfView?.addChildNode(self.focusSquare)
}
}
}
并添加一些控制功能:
func hideFocusSquare() { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: true) } }
func showFocusSquare() { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: false) } }
从VirtualObjectARView.swift中复制整个smartHitTest函数到ViewController.swift中(这样它们就存在两次)。
func smartHitTest(_ point: CGPoint,
infinitePlane: Bool = false,
objectPosition: float3? = nil,
allowedAlignments: [ARPlaneAnchor.Alignment] = [.horizontal, .vertical]) -> ARHitTestResult? {
let results = sceneView.hitTest(point, types: [.existingPlaneUsingGeometry, .estimatedVerticalPlane, .estimatedHorizontalPlane])
if let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
let planeAnchor = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) {
return existingPlaneUsingGeometryResult
}
if infinitePlane {
let infinitePlaneResults = sceneView.hitTest(point, types: .existingPlane)
for infinitePlaneResult in infinitePlaneResults {
if let planeAnchor = infinitePlaneResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) {
if planeAnchor.alignment == .vertical {
return infinitePlaneResult
} else {
if let objectY = objectPosition?.y {
let planeY = infinitePlaneResult.worldTransform.translation.y
if objectY > planeY - 0.05 && objectY < planeY + 0.05 {
return infinitePlaneResult
}
} else {
return infinitePlaneResult
}
}
}
}
}
let vResult = results.first(where: { $0.type == .estimatedVerticalPlane })
let hResult = results.first(where: { $0.type == .estimatedHorizontalPlane })
switch (allowedAlignments.contains(.horizontal), allowedAlignments.contains(.vertical)) {
case (true, false):
return hResult
case (false, true):
return vResult ?? hResult
case (true, true):
if hResult != nil && vResult != nil {
return hResult!.distance < vResult!.distance ? hResult! : vResult!
} else {
return hResult ?? vResult
}
default:
return nil
}
}
您可能会在复制的函数中看到一些关于hitTest的错误。只需像这样进行更正:
hitTest... // which gives an Error
sceneView.hitTest... // this should correct it
实现渲染器的updateAtTime函数并添加以下代码:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
if isFocusSquareEnabled { showFocusSquare() }
self.updateObjectToCurrentTrackingPosition()
}
最后,为焦点平面添加一些辅助函数。
func hideFocusSquare() { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: true) } }
func showFocusSquare() { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: false) } }
此时,您可能仍会在导入的文件中看到大约十几个错误和警告,这可能发生在使用Swift 5并且您有一些Swift 4文件的情况下。只需让Xcode纠正错误即可。(这都是关于重命名一些代码语句,Xcode最懂)
进入VirtualObject.swift并搜索此代码块:
if smoothMovement {
let hitTestResultDistance = simd_length(positionOffsetFromCamera)
// Add the latest position and keep up to 10 recent distances to smooth with.
recentVirtualObjectDistances.append(hitTestResultDistance)
recentVirtualObjectDistances = Array(recentVirtualObjectDistances.suffix(10))
let averageDistance = recentVirtualObjectDistances.average!
let averagedDistancePosition = simd_normalize(positionOffsetFromCamera) * averageDistance
simdPosition = cameraWorldPosition + averagedDistancePosition
} else {
simdPosition = cameraWorldPosition + positionOffsetFromCamera
}
注释掉或替换掉这整个代码块,使用以下一行代码:
simdPosition = cameraWorldPosition + positionOffsetFromCamera
此时,您应该能够编译项目并在设备上运行它。您应该能够看到飞船和一个黄色的焦点方块,这些应该已经可以工作了。
要开始放置一个可拖动的对象,您需要一些函数来创建所谓的VirtualObject,就像我在开头说的那样。
使用此示例函数进行测试(将其添加到视图控制器中的某个位置):
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if focusSquare.state != .initializing {
let position = SCNVector3(focusSquare.lastPosition!)
let testObject = VirtualObject()
testObject.geometry = SCNCone(topRadius: 0.0, bottomRadius: 0.2, height: 0.5)
testObject.geometry?.firstMaterial?.diffuse.contents = UIColor.red
testObject.categoryBitMask = 0b00000010
testObject.name = "test"
testObject.castsShadow = true
testObject.position = position
sceneView.scene.rootNode.addChildNode(testObject)
}
}
注意:您想在平面上拖动的所有内容,必须使用VirtualObject()进行设置,而不是SCNNode()。关于VirtualObject的其他方面与SCNNode相同。
(您还可以添加一些常见的SCNNode扩展,例如按名称加载场景的扩展-在引用导入模型时非常有用)
玩得开心!