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

随着其他许多已被我们的现代技术Swift取代的事物,似乎通用的卷尺可能会成为下一步。 在这个分为两部分的系列教程中,我们将学习如何在iOS设备上使用增强现实和相机来创建一个应用程序,该应用程序将报告两个点之间的距离。

处理水龙头

这是本教程最大的部分之一:处理用户何时点击其世界以使球体准确显示在其点击的位置。 稍后,我们将计算这些球体之间的距离,以最终向用户显示其距离。

点击手势识别器

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

// 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,您需要此标记。 您只需要知道#selector需要引用的方法即可用于Objective-C。 最后,method参数将使我们获得在屏幕上点击的确切位置。

位置侦测

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

首先将以下三行代码添加到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()方法中使用的参数,您可能会记得它被命名为sender ,并且类型为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 ,它实际上是一个四乘四的浮点值矩阵。 既然我们在   不过,我们需要将SceneKit转换为SceneKit可以理解的内容,在这种情况下,将其SCNMatrix4SCNMatrix4

然后,我们将使用此矩阵创建一个SCNVector3 ,顾名思义,它是具有三个分量的向量。 您可能已经猜到了,这些分量是x x yz ,以使我们在空间中处于一个位置。 transform.m41transform.m42transform.m43是三个分量向量的相关坐标值。

最后,让我们使用之前创建的newSphere()方法,以及我们从touch事件中解析的位置信息,来制作一个球体并将其分配给名为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 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)
    }
}

计算距离

现在,如果您还记得的话,我们在球体的SCNNode上调用了distance(to:)方法,并且我确定Xcode会因为使用未声明的方法而对您大喊大叫。 现在,通过创建SCNNode类的扩展来结束这SCNNode

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

extension SCNNode {
    // Your code goes here
}

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

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

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

如果您看到的话,有一个参数是另一个SCNNode ,它返回一个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位置。 稍后,我们将这些值插入距离公式以获取它们的距离。 另外,因为我希望结果以英寸为单位,所以我为米和英寸之间的转换率创建了一个常数,以便以后轻松转换。

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

最后,我们返回该值乘以英寸转换率,以获得适当的度量单位。 如果您居住在美国以外的地方,则可以以米为单位放置它,也可以根据需要将其转换为厘米。

结论

好吧,这就是包装! 这是您最终项目的外观:

如您所见,测量结果并不完美,但它认为15英寸的计算机约为14.998英寸,所以还不错!

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

翻译自: https://code.tutsplus.com/tutorials/code-a-measuring-app-with-arkit-placing-objects-in-the-scene--cms-30448

你可能感兴趣的:(使用ARKit编写测量应用程序代码:交互和测量)