• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

使用ARKit编写测量应用程序:交互和测量

连同许多其他已被我们的现代技术迅速取代的东西,看起来普通卷尺可能会成为下一个选择。在这个由两部分组成的教程系列中,我们正在学习如何使用增强现实和 ios 设备上的相机来创建一个应用程序,该应用程序将报告两点之间的距离。

在第一篇文章中,我们创建了应用程序项目并编写了它的主要界面元素。在这篇文章中,我们将通过在 AR 场景中的两点之间进行测量来完成它。如果您还没有,请按照第一篇文章来设置您的 ARKit 项目。

处理水龙头

这是本教程最重要的部分之一:当用户点击他们的世界以使球体准确出现在他们点击的位置时进行处理。稍后,我们将计算这些球体之间的距离,最终向用户显示它们的距离。

点击手势识别器

检查点击的第一步是在应用启动时创建一个点击手势识别器。为此,请创建一个点击处理程序,如下所示:

// Creates a tap handler and then sets it to a constant
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))

第一行创建一个类的实例, 并在初始化时UITapGestureRecognizer()传入两个参数:目标和动作。目标是此识别器发送的通知的接收者,我们希望我们的 ViewController 类成为目标。动作只是一个方法,每次点击时都应该调用它。

要设置抽头数,请添加:

// Sets the amount of taps needed to trigger the handler
tapRecognizer.numberOfTapsrequired = 1

接下来,我们之前创建的类的实例需要知道实际需要多少次点击才能激活识别器。在我们的例子中,我们只需要点击一下,但在其他应用程序中,在某些情况下您可能需要更多(例如双击)。

像这样将处理程序添加到场景视图中:

// Adds the handler to the scene view
sceneView.addGestureRecognizer(tapRecognizer)

最后,这行代码只是将手势识别器添加到 中 sceneView,我们将在其中执行所有操作。这是相机的预览以及用户将直接点击以使球体出现在屏幕上的位置,因此将识别器添加到用户将与之交互的视图中是有意义的。

手柄敲击法

当我们创建 时 UITapGestureRecognizer(),您可能还记得我们 handleTap为动作设置了一个方法。现在,我们已准备好声明该方法。为此,只需将以下内容添加到您的应用程序中:

@objc func handleTap(sender: UITapGestureRecognizer) {
    // Your code goes here
}

尽管函数声明可能非常不言自明,但您可能想知道为什么 @objc它前面有一个标签。在当前版本的 swift 中,要向 Objective-C 公开方法,你需要这个标签。您需要知道的是,  #selectorObjective-C 需要引用的方法。最后,方法参数将让我们获得在屏幕上点击的确切位置。

位置检测

让我们的球体出现在用户点击的位置的下一步是检测他们点击的确切位置。现在,这并不像获取位置和放置球体那么简单,但我相信您很快就会掌握它。 

首先将以下三行代码添加到您的 handleTap()方法中:

// Gets the location of the tap and assigns it to a constant
let location = sender.location(in: sceneView)

// Searches for real world objects such as surfaces and filters out flat surfaces
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])

// Assigns the most accurate result to a constant if it is non-nil
guard let result = hitTest.last else { return }

如果您还记得我们在 handleTap()方法中使用的参数,您可能还记得它是 named  sender,它是 type  UITapGestureRecognizer好吧,这第一行代码只是获取屏幕上点击的位置(相对于场景视图),并将其设置为一个名为 location.

接下来,我们正在对 SceneView自身进行所谓的命中测试。简单来说,它的作用是检查场景中的真实物体,例如桌子、表面、墙壁、地板等。这使我们能够获得深度感并在两点之间获得非常准确的测量结果。此外,我们指定要检测的对象类型,如您所见,我们告诉它寻找 featurePoints,它们本质上是平坦的表面,这对于测量应用程序是有意义的。

最后,这行代码获取最准确的结果,在 的情况下 hitTest 是最后一个结果,并检查它是否不是 nil如果是,则忽略此方法中的其余行,但如果确实有结果,则将其分配给名为 的常量 result

矩阵

如果你回想你的高中代数课,你可能还记得矩阵,当时它可能不像现在那么重要。它们通常用于与计算机图形相关的任务,我们将在这个应用程序中一睹它们的风采。

将以下行添加到您的 handleTap()方法中,我们将详细介绍它们:

// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)

// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)

// Makes a new sphere with the created method
let sphere = newSphere(at: vector)

在进入第一行代码之前,重要的是要了解我们之前所做的命中测试返回一个 类型 matrix_float4x4,它本质上是一个 4×4 浮点值矩阵。但是,由于我们在 SceneKit 中,我们需要将其转换为 SceneKit 可以理解的内容——在本例中,转换为 SCNMatrix4.

然后,我们将使用这个矩阵来创建一个 SCNVector3,顾名思义,它是一个包含三个分量的向量。正如您可能已经猜到的那样,这些组件是 、 和 ,以给我们在空间中的位置。 ,  , 和 是三个分量向量的相关坐标值。xyztransform.m41transform.m42transform.m43

最后,让我们使用 newSphere()我们之前创建的方法,连同我们从触摸事件中解析的位置信息,制作一个球体并将其分配给一个名为 的常量 sphere

解决双击错误

现在,您可能已经意识到我们代码中的一个小缺陷;如果用户继续点击,则会不断创建一个新球体。我们不希望这样做,因为它很难确定需要测量哪些球体。此外,用户很难跟踪所有球体!

用数组求解

解决这个问题的第一步是在类的顶部创建一个数组。

var spheres: [SCNnode] = []

这是一个数组, SCNNodes因为这是我们从 本教程开始newSphere()时创建的方法返回的类型。稍后,我们会将球体放入这个数组中并检查有多少个。基于此,我们将能够通过删除和添加它们来操纵它们的数字。

可选绑定

接下来,我们将使用一系列 if-else 语句和 for 循环来确定数组中是否有任何球体。对于初学者,将以下可选绑定添加到您的应用程序:

if let first = spheres.first {
    // Your code goes here
} else {
    // Your code goes here
}

首先,我们检查数组中是否有 任何项目 spheres,如果没有,则执行 else子句中的代码。

审计领域

之后,将以下内容添加到if-else语句的第一部分(if分支) : 

// Adds a second sphere to the array
spheres.append(sphere)
print(sphere.distance(to: first))

// If more that two are present...
if spheres.count > 2 {
    
    // Iterate through spheres array
    for sphere in spheres {
        
        // Remove all spheres
        sphere.removeFromParentNode()
    }
    
    // Remove extraneous spheres
    spheres = [spheres[2]]
}

由于我们已经在点击事件中,我们知道我们正在创建另一个球体。因此,如果已经有一个球体,我们需要获取距离并将其显示给用户。您可以在球体上调用该 distance()方法,因为稍后我们将创建 SCNNode.

接下来,我们需要知道是否已经超过了两个球体的最大值。为此,我们只需使用 spheres数组的 count 属性和一条 if语句。我们遍历数组中的所有球体并将它们从场景中移除。(别担心,我们稍后会回来。)

最后,因为我们已经在 if声明告诉我们有两个以上的球体,我们可以删除数组中的第三个,这样我们就可以确保数组中始终只剩下两个。

添加球体

最后,在 else子句中,我们知道 spheres数组是空的,所以我们需要做的就是添加我们在方法调用时创建的球体。在您的 else子句中,添加以下内容:

// Add the sphere
spheres.append(sphere)

耶!我们刚刚将球体添加到spheres数组中,我们的数组已准备好进行下一次点击。我们现在已经为我们的数组准备了应该在屏幕上的球体,所以现在,让我们将它们添加到数组中。

为了迭代并添加球体,添加以下代码:

// Iterate through spheres array
for sphere in spheres {
    
    // Add all spheres in the array
    self.sceneView.scene.rootNode.addChildNode(sphere)
}

这只是一个简单的 for循环,我们将球体 ( SCNNode) 添加为场景根节点的子节点。在 SceneKit 中,这是添加内容的首选方式。

完整方法

这是最终 handleTap()方法的外观:

@objc func handleTap(sender: UITapGestureRecognizer) {
    
    let location = sender.location(in: sceneView)
    let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
    
    guard let result = hitTest.last else { return }
    
    let transform = SCNMatrix4.init(result.worldTransform)
    let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
    let sphere = newSphere(at: vector)
    
    if let first = spheres.first {
        spheres.append(sphere)
        print(sphere.distance(to: first))
        
        if spheres.count > 2 {
            for sphere in spheres {
                sphere.removeFromParentNode()
            }
            
            spheres = [spheres[2]]
        }
    
    } else {
        spheres.append(sphere)
    }
    
    for sphere in spheres {
        self.sceneView.scene.rootNode.addChildNode(sphere)
    }
}

计算距离

现在,如果您还记得的话,我们 distance(to:)在我们的 sphere 上调用了一个方法 SCNNode,我敢肯定 Xcode 会因为您使用未声明的方法而对您大喊大叫。现在让我们通过创建 SCNNode类的扩展来结束它。

要创建扩展,只需在 ViewController课堂之外执行以下操作:

extension SCNNode {
    // Your code goes here
}

这只是让您更改类(就好像您正在编辑实际类)。然后,我们将添加一个计算两个节点之间距离的方法。

这是执行此操作的函数声明:

func distance(to destination: SCNNode) -> CGFloat {
    // Your code goes here
}

如果您会看到,有一个参数是 another  SCNNode,它返回 a CGFloat作为结果。对于实际计算,将其添加到您的 distance()函数中:

let dx = destination.position.x - position.x
let dy = destination.position.y - position.y
let dz = destination.position.z - position.z

let inches: Float = 39.3701
let meters = sqrt(dx*dx + dy*dy + dz*dz)

return CGFloat(meters * inches)

前三行代码从SCNNode 作为参数传递的节点的坐标中减去当前的 x、y 和 z 位置。稍后我们会将这些值代入距离公式以获得它们的距离。另外,因为我想要以英寸为单位的结果,所以我为米和英寸之间的转换率创建了一个常数,以便以后轻松转换。 

现在,要获得两个节点之间的距离,请回想一下您的中学数学课:您可能还记得笛卡尔平面的距离公式。在这里,我们将其应用于三维空间中的点。

最后,我们返回该值乘以英寸转换比率,以获得适当的度量单位。如果您居住在美国境外,您可以将其保留为米或根据需要将其转换为厘米。

结论

嗯,这是一个包装!这是您的最终项目的外观:

使用ARKit编写测量应用程序:交互和测量  第1张如您所见,测量结果并不完美,但它认为 15 英寸计算机约为 14.998 英寸,因此还不错!

您现在知道如何使用 Apple 的新库 ARKit这个应用程序可以用于很多事情,我挑战你思考可以在现实世界中使用它的不同方式,并确保在下面的评论中留下你的想法。

文章目录
  • 处理水龙头
    • 点击手势识别器
    • 手柄敲击法
    • 位置检测
    • 矩阵
    • 解决双击错误
      • 用数组求解
      • 可选绑定
      • 审计领域
      • 添加球体
      • 完整方法
  • 计算距离
  • 结论