arkit与现实世界距离比

by Esteban Herrera

由Esteban Herrera

如何使用ARKit和Pusher构建实时增强现实测量应用程序 (How to Build a Real-Time Augmented Reality Measuring App with ARKit and Pusher)

Augmented reality (AR) is all about modifying our perception of the real world.

增强现实(AR)就是要改变我们对现实世界的看法。

Information about our environment and surrounding objects can be overlaid to enhance your current perception of reality. This information can presented to the user in real-time, like in the case of a news feed in a live event.

可以叠加有关我们的环境和周围物体的信息,以增强您当前对现实的感知。 该信息可以实时呈现给用户,就像在直播活动中的新闻源一样。

But the flow of information can also go the other way around. We can send the information resulting from the interaction with the augmented reality experience. In both cases, Pusher can help you with sending and receiving data in real-time.

但是信息流也可以相反。 我们可以发送与增强现实体验互动产生的信息。 在这两种情况下,Pusher都可以帮助您实时发送和接收数据。

In this tutorial, we’re going to build an ARKit app to make simple measurements. While measuring, the app will create a 3D box with a width equal to the measured size:

在本教程中,我们将构建一个ARKit应用来进行简单的测量。 在测量时,应用程序将创建一个3D框,其宽度等于所测量的尺寸:

It will also send the measurements in real-time to Pusher:

它还将实时将测量结果发送到Pusher:

One note of caution. The measurements are based on the plane detection’s capabilities of ARKit. They are not perfect in some situations, such as in low lighting or when a surface is not entirely flat. The results won’t be completely accurate all the time. They’re close, but they can vary.

注意事项之一。 这些测量基于ARKit的平面检测功能。 在某些情况下,例如在光线不足或表面不完全平坦的情况下,它们并不完美。 结果并非始终都是完全准确的。 它们很接近,但是可以有所不同。

For this tutorial, you’ll need:

对于本教程,您需要:

  • A device with an A9 or later processor (iPhone 6s or better, iPhone SE, any iPad Pro, or the 2017 iPad)具有A9或更高版本处理器的设备(iPhone 6s或更高版本,iPhone SE,任何iPad Pro或2017 iPad)
  • iOS 11 beta 5

    iOS 11 Beta 5

  • Xcode 9 beta 5 (or superior)

    Xcode 9 Beta 5 (或更高版本)

  • A free Pusher account

    免费的Pusher帐户

For reference, you can find the source code of this project on GitHub.

作为参考,您可以在Git Hub上找到该项目的源代码。

Let’s get started.

让我们开始吧。

设置Xcode项目 (Setting up the Xcode project)

Open Xcode and create a new Single View App:

打开Xcode并创建一个新的Single View App

We choose this option because we are going to manually set up an AR view along with other controls.

我们选择此选项是因为我们将手动设置AR视图以及其他控件。

Enter the project information, choosing Swift as the language:

输入项目信息,选择Swift作为语言:

Create the project and close it. We’re going to use CocoaPods to install the project’s dependencies. Open a terminal window and go to the root directory of your project. If you don’t have CocoaPods installed, execute:

创建项目并关闭它。 我们将使用CocoaPods安装项目的依赖项。 打开一个终端窗口,然后转到项目的根目录。 如果您没有安装CocoaPods,请执行:

sudo gem install cocoapods

Once installed, create the file Podfile with the command:

安装后,使用以下命令创建文件Podfile

pod init

Edit this file to set the platform to iOS 11 and add the project dependencies:

编辑此文件以将平台设置为iOS 11并添加项目依赖项:

# Uncomment the next line to define a global platform for your project
platform :ios, '11.0'
target 'MeasureARPusher' do
# Comment the next line if you're not using Swift # and don't want to use dynamic frameworks
use_frameworks!
# Pods for MeasureARPusher
pod 'PusherSwift', :git => 'https://github.com/pusher/pusher-websocket-swift.git', :branch => 'swift-3.2'
end

At the time of this writing, Pusher’s Swift library for Xcode 9 and Swift 3.2/4 is still in development. The current version throws some errors, so we have to pull the version for Swift 3.2 from the branch swift-3.2.

在撰写本文时, Pusher的 Xcode 9和Swift 3.2 / 4 的Swift库仍在开发中 。 当前版本引发了一些错误,因此我们必须从分支swift-3.2拉出Swift 3.2的版本。

Once you edit Podfile, execute the following command to install the dependencies:

编辑Podfile ,执行以下命令以安装依赖项:

pod install

Now open the Xcode workspace instead of the project file:

现在打开Xcode工作区而不是项目文件:

open MeasureARPusher.xcworkspace

We need to change the version of Swift from 4.0 (the default) to Swift 3.2 so Pusher can compile without errors. In the project navigator, select Pods. In the Targets section select PusherSwift. In the Build Settings tab look for the option Swift Language Version. Change it to Swift 3.2:

我们需要将Swift的版本从4.0(默认)更改为Swift 3.2,以便Pusher可以正确编译。 在项目导航器中,选择Pods 。 在“ 目标”部分中,选择“ PusherSwift” 。 在“ 构建设置”选项卡中,找到“ Swift语言版本 ”选项。 将其更改为Swift 3.2:

If you build your project at this point, the operation should be successful.

如果此时构建项目,则操作应该成功。

Select the Information Property List. add a row of type Privacy — Camera Usage Description (NSCameraUsageDescription) and give it a description. This is so ARKit can access the camera:

选择信息属性列表 。 添加一个类型为“ 隐私—相机使用说明 ( NSCameraUsageDescription )”的行,并为其提供描述。 这样ARKit可以访问摄像机:

Finally, configure a team so you can run the app on your device:

最后,配置一个小组,以便您可以在设备上运行该应用程序:

Now let’s start by building the user interface.

现在让我们开始构建用户界面。

建立使用者介面 (Building the user interface)

Go to Main.storyboard and drag an ARKit SceneKit View to the view:

转到Main.storyboard并将ARKit SceneKit视图拖到视图中:

Next, add constraints to all sides of this view so that it fills the entire screen. You do this by pressing the ctrl key while dragging a line to each side and choosing leading, top, trailing, and bottom to the superview, with a value of 0:

接下来,向该视图的所有侧面添加约束,以使其覆盖整个屏幕。 为此,您可以通过按ctrl键,同时将一条线拖动到每一边,然后选择到导览图的前导,顶部,尾部和底部,值为0

Add a button. Change its type to Add Contact in the Attributes inspector. Give it a white color Tint, and add a vertical and a horizontal constraint to center it in the middle of the screen:

添加一个按钮。 在“ 属性”检查器中将其类型更改为“ 添加联系人 ”。 给它一个白色的Tint ,并添加一个垂直和水平约束以使其在屏幕中间居中:

Let’s add a switch to control when the app is in measure mode. Set its initial state to Off in the Attributes inspector. Add a bottom and trailing constraint with a value of -20 to place the control in the bottom-right part of the screen. You may want to place it in the bottom-center or another part of the screen if you prefer:

让我们添加一个开关来控制应用程序何时处于测量模式。 在“属性”检查器中将其初始状态设置为“关闭”。 添加一个值为-20的底部和尾部约束,以将控件放置在屏幕的右下角。 如果您愿意,可以将其放置在屏幕的底部中心或另一部分:

Now add a text view. Disable its Editable and Selectable behaviors in the Attributes inspector. Change its background color. I chose a white color with 50% opacity.

现在添加一个文本视图。 在“属性”检查器中禁用其可编辑可选行为 更改其背景颜色。 我选择了不透明度为50%的白色。

Add a height constraint with a value of 90. Add leading, top, and trailing constraints with the value 0 so it remains fixed to the top of the screen:

添加一个值为90的高度约束。添加值为0的前,顶和尾约束,因此它固定在屏幕顶部:

In ViewController.swift, import the SceneKit and ARKit libraries:

ViewController.swift ,导入SceneKit和ARKit库:

import SceneKit
import ARKit

Then, create two IBOutlets, one to the scene view and another one to the text view:

然后,创建两个IBOutlets ,一个创建到场景视图,另一个创建到文本视图:

Finally, an action on the switch control for the changeValue event:

最后,对changeValue事件在开关控件上执行以下changeValue

And now we’re ready to start coding the app.

现在我们准备开始对该应用程序进行编码。

构建一个测量应用 (Building a measurement app)

Let’s start by making ViewController.swift implement ARSCNViewDelegate and defining the variables we’re going to need:

首先,让ViewController.swift实现ARSCNViewDelegate并定义我们需要的变量:

class ViewController: UIViewController, ARSCNViewDelegate {
...
var box: Box!
var status: String!
var startPosition: SCNVector3!
var distance: Float!
var trackingState: ARCamera.TrackingState!
enum Mode {
case waitingForMeasuring
case measuring
}
...
}

Where:

哪里:

  • box represents the 3D box that is going to get drawn when measuring. We’ll review this class later on. For now, create it to eliminate the error.

    box表示测量时将要绘制的3D框。 我们稍后将复习该课程。 现在,创建它以消除错误。

  • status is a text that tells us if the app is ready or not to take measurements (whether the app has detected planes or not).

    status是一则文字,告诉我们应用程序是否准备就绪或是否可以进行测量(应用程序是否已检测到平面)。

  • startPosition represents the measurement’s start position.

    startPosition代表测量的开始位置。

  • distance is the calculated distance from the start to the current position (the measurement itself).

    distance是从起点到当前位置(测量本身)的计算距离。

  • trackingState holds the current tracking state of the camera.

    trackingState保持摄像机的当前跟踪状态。

  • Mode is an enumeration to indicate the possible states of the app.

    Mode是一种枚举,用于指示应用程序的可能状态。

Let’s add another property to keep track of the state of the app, and do a few things depending on the set value:

让我们添加另一个属性来跟踪应用程序的状态,并根据设置值做一些事情:

var mode: Mode = .waitingForMeasuring {
didSet {
switch mode {
case .waitingForMeasuring:
status = "NOT READY"
case .measuring:
box.update(          minExtents: SCNVector3Zero, maxExtents: SCNVector3Zero)
box.isHidden = false
startPosition = nil
distance = 0.0
setStatusText()
}
}
}

If waitingForMeasuring is set, we’ll assume that the app is not ready. Don’t worry, if it is, the status will change immediately. If the mode is set to measuring, we’ll reset the size of the box. We’ll review the method update later. We’ll show if the box is hidden, reset the startPosition and distancevariables, and call the method that displays the value of those variables.

如果设置了waitingForMeasuring ,我们将假定该应用尚未准备就绪。 不用担心,如果是这样,状态将立即更改。 如果将模式设置为测量,我们将重置盒子的大小。 稍后我们将审查方法更新。 我们将显示该框是否被隐藏,重置startPositiondistancevariables ,并调用显示这些变量值的方法。

This is the definition of the method setStatusText():

这是setStatusText()方法的定义:

func setStatusText() {
var text = "Status: \(status!)\n"
text += "Tracking: \(getTrackigDescription())\n"
text += "Distance: \(String(format:"%.2f cm", distance! * 100.0))"
statusTextView.text = text
}
func getTrackigDescription() -> String {
var description = ""
if let t = trackingState {
switch(t) {
case .notAvailable:
description = "TRACKING UNAVAILABLE"
case .normal:
description = "TRACKING NORMAL"
case .limited(let reason):
switch reason {
case .excessiveMotion:
description =               "TRACKING LIMITED - Too much camera movement"
case .insufficientFeatures:
description =               "TRACKING LIMITED - Not enough surface detail"
case .initializing:
description = "INITIALIZING"
}
}
}
return description
}

This method sets the text of the text view to the measure status of READY or NOT READY. It shows a description of the tracking status of the AR camera with the help of the method getTrackingDescription(). And the (last) calculated distance in centimeters. Notice we have to multiply the value by 100.0.

此方法将文本视图的文本设置为测量状态READY或NOT READY 表明 借助方法getTrackingDescription()描述AR摄像机的跟踪状态。 (最后)计算出的距离(以厘米为单位)。 注意,我们必须将该值乘以100.0

Next, we have the method viewDidLoad(), where we set initial values, among other things:

接下来,我们使用方法viewDidLoad() ,在其中设置初始值,其中包括:

override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Set a padding in the text view
statusTextView.textContainerInset =       UIEdgeInsetsMake(20.0, 10.0, 10.0, 0.0)
// Instantiate the box and add it to the scene
box = Box()
box.isHidden = true;
sceneView.scene.rootNode.addChildNode(box)
// Set the initial mode
mode = .waitingForMeasuring
// Set the initial distance
distance = 0.0
// Display the initial status
setStatusText()
}

In the method viewWillAppear, create and run a session with plane detection. This is important because the points of the plane will be the elements we’ll use to measure things:

viewWillAppear方法中,创建并运行带有平面检测的会话 。 这很重要,因为平面的点将成为我们用来测量事物的元素:

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration with plane detection
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
// Run the view's session
sceneView.session.run(configuration)
}

Also, override the method viewWillDisappear to pause the session when necessary:

另外,重写方法viewWillDisappear可以在必要时暂停会话:

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}

Use the following method to know when the camera’s tracking state has changed. Save a reference to that state:

使用以下方法知道相机的跟踪状态何时更改。 保存对该状态的引用:

func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
trackingState = camera.trackingState
}

These methods are part of the callbacks that come with the protocol ARSCNViewDelegate.

这些方法是协议ARSCNViewDelegate附带的回调的一部分。

Now, here comes the interesting part.

现在,这是有趣的部分。

The method:

方法:

(void)renderer:(id <SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time

It’s called exactly once per frame, like 60 times per second. So we are going to call another method to use hit-testing to detect a plane we can interact with. When that happens, we’ll change the state from NOT READY to READY.

每帧精确地称为一次,例如每秒60次。 因此,我们将调用另一种方法来使用命中测试来检测可以与之交互的平面。 发生这种情况时,我们会将状态从“未就绪”更改为“就绪”。

The implementation looks like this:

实现看起来像这样:

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// Call the method asynchronously to perform
//  this heavy task without slowing down the UI
DispatchQueue.main.async {
self.measure()
}
}
func measure() {
let screenCenter : CGPoint = CGPoint(      x: self.sceneView.bounds.midX, y: self.sceneView.bounds.midY)
let planeTestResults = sceneView.hitTest(      screenCenter, types: [.existingPlaneUsingExtent])
if let result = planeTestResults.first {
status = "READY"
} else {
status = "NOT READY"
}
...
}

The reference point will always be the center of the screen. That’s why we place that button with the plus sign. We’ll get the screen center coordinates to hit-test against an existing plane. If there’s a result, it means we can start measuring.

参考点将始终位于屏幕的中心。 这就是为什么我们在按钮上加上加号。 我们将获得屏幕中心坐标,以对现有平面进行命中测试。 如果有结果,那就意味着我们可以开始测量了。

There are four types of objects we can find in a search:

我们可以在搜索中找到四种类型的对象 :

  • featurePoints

    FeaturePoints

    A point automatically identified by ARKit as part of a continuous surface, but without a corresponding anchor.

    由ARKit自动识别为连续曲面的一部分的点,但没有相应的锚点。

  • estimatedHorizontalPlane

    估计水平平面

    A real-world planar surface detected by the search and without a corresponding anchor. The orientation is perpendicular to gravity.

    通过搜索检测到的真实世界平面表面,没有相应的锚点。 方向垂直于重力。

  • existingPlane

    现有飞机

    A plane anchor already in the scene, without considering the plane’s size.

    场景中已经存在一个平面锚,而不考虑平面的大小。

  • existingPlaneUsingExtent

    现有的平面使用范围

    A plane anchor already in the scene, respecting the plane’s limited size.

    考虑到飞机的有限尺寸,已经在场景中的飞机锚定了。

The easiest option would be to use feature points. They represent notable features detected in the camera image. They are detected quicker than planes and there are more of them.

最简单的选择是使用特征点。 它们代表在相机图像中检测到的显着特征。 它们比飞机检测得更快,并且有更多的飞机。

But, this will give us very unstable results. Feature points are intermediate results of the scene analysis that ARKit uses to perform world tracking. So it’s recommended to do a hit test against existing plane anchors first.

但是,这将给我们带来非常不稳定的结果。 特征点是ARKit用于执行世界跟踪的场景分析的中间结果。 因此,建议先对现有的飞机锚进行碰撞测试。

If there’s a hit and the mode is measuring, we need to change the status to MEASURING. And extract the position as a vector of three elements (x, y, z) from the transformation matrix of the result:

如果遇到问题并且模式正在测量,我们需要将状态更改为“正在测量”。 并从结果的转换矩阵中提取位置作为三个元素(x,y,z)的向量:

func measure() {
...
if let result = planeTestResults.first {
status = "READY"
if mode == .measuring {
status = "MEASURING"
let worldPosition = SCNVector3Make(        result.worldTransform.columns.3.x,              result.worldTransform.columns.3.y,        result.worldTransform.columns.3.z      )
...
}
} ...
}

If startPosition is nil (the first measure), we use worldPosition to set its value as well as the box position:

如果startPositionnil (第一个度量),则使用worldPosition设置其值以及框的位置:

func measure() {
...
if let result = planeTestResults.first {
status = "READY"
if mode == .measuring {
...
let worldPosition = SCNVector3Make(        result.worldTransform.columns.3.x,         result.worldTransform.columns.3.y,         result.worldTransform.columns.3.z      )
if startPosition == nil {
startPosition = worldPosition
box.position = worldPosition
}
...
}
} ...
}

We can calculate the distance between startPosition and worldPosition (the actual position) in a 3D space using the Pythagorean theorem and resize the box accordingly:

我们可以使用勾股定理来计算3D空间中startPositionworldPosition (实际位置)之间的距离,并worldPosition调整框的大小:

func measure() {
...
if let result = planeTestResults.first {
status = "READY"
if mode == .measuring {
...
distance = calculateDistance(          from: startPosition!, to: worldPosition      )
box.resizeTo(extent: distance)
...
}
} ...
}
func calculateDistance(from: SCNVector3, to: SCNVector3) -> Float {
let x = from.x - to.x
let y = from.y - to.y
let z = from.z - to.z
return sqrtf( (x * x) + (y * y) + (z * z))
}

But the math is not done yet. We won’t be measuring straight lines all the time. To take measures in all directions and have the 3D box to follow, we need to take into account the rotation in the Y-axis.

但是数学还没有完成。 我们不会一直测量直线。 要全方位地采取措施并遵循3D框,我们需要考虑Y轴的旋转。

We can get the angle (in radians) between two vectors using atan2 in this way:

我们可以使用atan2这样获得两个向量之间的角度(以弧度为单位):

func measure() {
...
if let result = planeTestResults.first {
status = "READY"
if mode == .measuring {
...
let angleInRadians = calculateAngleInRadians(          from: startPosition!, to: worldPosition      )
box.rotation = SCNVector4(x: 0, y: 1, z: 0,           w: -(angleInRadians + Float.pi)      )
}
} ...
}
...
func calculateAngleInRadians(from: SCNVector3, to: SCNVector3) -> Float {
let x = from.x - to.x
let z = from.z - to.z
return atan2(z, x)
}

However, the atan2 function returns an angle from 0º to +/- 180º. Since we’re dealing with radians, we have to add the value of PI for normalization (PI in radians equals to 180º).

但是, atan2函数返回的角度为0º至+/-180º。 由于我们要处理弧度,因此必须添加PI的值以进行标准化(弧度的PI等于180º)。

Also, notice that the rotation property of the 3D box takes a vector of four elements. The three first components are the axis (we need to rotate on the Y-axis). The fourth one is the rotation in radians.

另外,请注意,3D框的旋转属性采用四个元素的向量。 前三个组件是轴(我们需要在Y轴上旋转)。 第四个是弧度的旋转。

Let’s not forget the action for the switch control. It just changes the mode according to its state:

我们不要忘记开关控件的动作。 它只是根据其状态更改模式:

@IBAction func switchChanged(_ sender: UISwitch) {
if sender.isOn {
mode = .measuring
} else {
mode = .waitingForMeasuring
}
}

For the 3D box, if you haven’t already, create the class extending from SCNNode:

对于3D框,如果尚未创建,请创建从SCNNode扩展的类:

import SceneKit
class Box: SCNNode {
}

Let’s also define two static functions outside the class that will help us add and subtract two vectors:

让我们在类之外定义两个静态函数,这将帮助我们添加和减去两个向量:

class Box: SCNNode {
}
func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3Make(      left.x + right.x, left.y + right.y, left.z + right.z  )
}
func - (left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3Make(      left.x - right.x, left.y - right.y, left.z - right.z  )
}

Add the required constructors:

添加所需的构造函数:

class Box: SCNNode {
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Let’s add a lazy variable so it’s initialized until the first time it is used. It will hold a reference to the 3D box (SCNBox) inside a node of type SCNNode:

让我们添加一个惰性变量,以便对其进行初始化,直到第一次使用它为止。 它将保存对SCNNode类型的节点内的3D框( SCNBox )的引用 :

class Box: SCNNode {
lazy var box: SCNNode = makeBox()
...
func makeBox() -> SCNNode {
let box = SCNBox(        width: 0.01, height: 0.01, length: 0.01, chamferRadius: 0    )
return convertToNode(geometry: box)
}
func convertToNode(geometry: SCNGeometry) -> SCNNode {
for material in geometry.materials {
material.lightingModel = .constant
material.diffuse.contents = UIColor.white
material.isDoubleSided = false
}
let node = SCNNode(geometry: geometry)
self.addChildNode(node)
return node
}
}

In theory, we could use any primitive shape derived from SCNGeometry. But in practice, SCNBox is easier to use because we can define its X, Y, and Z-axis dimensions by setting its width, height, and length properties. In this case, we use small values so the box is shown with a good size.

从理论上讲,我们可以使用从SCNGeometry派生的任何原始形状。 但是实际上,SCNBox易于使用,因为我们可以通过设置其宽度,高度和长度属性来定义其X,Y和Z轴尺寸。 在这种情况下,我们使用较小的值,因此显示的框大小合适。

Also, notice we’re assigning a uniform white color to the box. You can use textures or configure the material in other ways to give it a more polished look.

另外,请注意,我们为盒子分配了统一的白色。 您可以使用纹理或以其他方式配置材料以使其外观更加优美。

In the resizeTo method, we get elements of the bounding box of the node and set the X-axis to the distance provided:

resizeTo方法中,我们获取节点边界框​​的元素,并将X轴设置为提供的距离:

func resizeTo(extent: Float) {
var (min, max) = boundingBox
max.x = extent
update(minExtents: min, maxExtents: max)
}

The update method takes the min and the modified max element to update the width of the box and the position of the node:

update方法采用min和经过修改的max元素来更新框的宽度和节点的位置:

func update(minExtents: SCNVector3, maxExtents: SCNVector3) {
guard let scnBox = box.geometry as? SCNBox else {
fatalError("Geometry is not SCNBox")
}
// Normalize the bounds so that min is always < max
let absMin = SCNVector3(      x: min(minExtents.x, maxExtents.x),       y: min(minExtents.y, maxExtents.y),       z: min(minExtents.z, maxExtents.z)  )
let absMax = SCNVector3(      x: max(minExtents.x, maxExtents.x),       y: max(minExtents.y, maxExtents.y),       z: max(minExtents.z, maxExtents.z)  )
// Set the new bounding box
boundingBox = (absMin, absMax)
// Calculate the size vector
let size = absMax - absMin
// Take the absolute distance
let absDistance = CGFloat(abs(size.x))
// The new width of the box is the absolute distance
scnBox.width = absDistance
// Give it a offset of half the new size so they box remains fixed
let offset = size.x * 0.5
// Create a new vector with the min position   // of the new bounding box
let vector = SCNVector3(x: absMin.x, y: absMin.y, z: absMin.z)
// And set the new position of the node with the offset
box.position = vector + SCNVector3(x: offset, y: 0, z: 0)
}

At this point, you’ll have a working AR measurement app. But let’s add the Pusher library to publish those measurements in real-time.

至此,您将拥有一个可以正常工作的AR测量应用程序。 但是,让我们添加Pusher库以实时发布这些度量。

用Pusher发送测量数据 (Sending out the measured data with Pusher)

If you haven’t already, create a free account at Pusher. Go to your Dashboard and create an app. Choosing a name, the cluster closest to you location, and iOS as your front-end technology:

如果您还没有,请在Pusher创建一个免费帐户。 转到仪表板并创建一个应用程序。 选择一个名称,最靠近您的位置的集群以及iOS作为您的前端技术:

This will give you some sample code to get started:

这将为您提供一些示例代码以开始使用:

Save your key, secret and cluster values, as we’ll need them later.

保存您的密钥,机密和集群值,稍后我们将需要它们。

Finally, go to the App Setting tab, check the option Enable client events and click on Update:

最后,转到“ 应用程序设置”标签,选中“ 启用客户端事件 ”选项,然后单击“ 更新”

What we are going to do is publish a client event to send the calculated measure in real-time.

我们要做的是发布一个客户端事件 ,以实时发送计算出的度量。

Events are the primary way of packaging messages in Pusher. Not all these events need to go to a web server for validation or persistence when using Pusher.

事件是在Pusher中打包消息的主要方式。 使用Pusher时,并非所有这些事件都需要进入Web服务器进行验证或持久性。

In some cases, like in this app, events can be sent directly from the client to Pusher, and from there, to all the other clients connected to the channel. However, there are some things we need to take into account:

在某些情况下,例如在此应用中,事件可以直接从客户端发送到Pusher,然后从那里发送到连接到该频道的所有其他客户端。 但是, 我们需要考虑一些事项 :

  • Client events must be enabled for the application (like we did).必须为应用程序启用客户端事件(就像我们一样)。
  • The user must be subscribed to the channel that the event is being triggered on.用户必须订阅触发事件的通道。
  • Client events can only be triggered on private and presence channels because they require authentication

    客户端事件只能在专用和在线渠道上触发,因为它们需要身份验证

  • Client events must be prefixed by client-.

    客户事件必须以client-为前缀。

  • Don’t publish more than 10 messages per second per client (connection). Any events triggered above this rate limit will be rejected.

    每个客户端(连接)每秒发布的消息不要超过10条 。 超过此速率限制触发的任何事件将被拒绝。

In ViewController, let’s import the Pusher library and instantiate the object. We’ll define a variable for the channel and another variable to control the rate of the triggered events:

ViewController ,让我们导入Pusher库并实例化该对象。 我们将为通道定义一个变量,并为控制触发事件的速率定义另一个变量:

...
import PusherSwift
class ViewController: UIViewController, ARSCNViewDelegate {
...
let pusher = Pusher(
key: "<YOUR_PUSHER_APP_KEY>",
options: PusherClientOptions(
authMethod: .inline(secret: "<YOUR_PUSHER_APP_SECRET&gt;"),
host: .cluster("YOUR_PUSHER_APP_CLUSTER")
)
)
var channel: PusherChannel!
var sendingTime : TimeInterval = 0
...
}

We have to use a private authenticated channel for client events. The Pusher Swift library provides the following methods of authentication with the authMethod option:

我们必须为客户事件使用经过身份验证的专用通道。 Pusher Swift库使用authMethod选项提供以下身份验证方法:

  • endpoint(authEndpoint:String)

    endpoint(authEndpoint:String)

    The client will make a POST request to the endpoint you specify.

    客户端将向您指定的端点发出POST请求。

  • authRequestBuilder(authRequestBuilder:AuthRequestBuilderProtocol)

    authRequestBuilder(authRequestBuilder:AuthRequestBuilderProtocol)

    You specify an object that conforms to the

    您指定的对象符合

    AuthRequestBuilderProtocol.

    AuthRequestBuilderProtocol 。

  • inline(secret:String)

    inline(secret:String)

    Your app’s secret so that authentication requests do not need to be made to your authentication endpoint. Instead subscriptions can be authenticated directly inside the library and used for development.

    应用程序的秘密,因此无需向身份验证端点发出身份验证请求。 相反,可以直接在库中对订阅进行身份验证并将其用于开发。

  • authorizer(authorizer:Authorizer)

    authorizer(authorizer:Authorizer)

    You specify an object that conforms to the

    您指定的对象符合

    Authorizer protocol to provide the appropriate auth information.

    授权者协议提供适当的身份验证信息。

  • noMethod

    noMethod

    If you do not need to set an authentication method, this is the default value.

    如果不需要设置身份验证方法,则这是默认值。

You can learn how to create an authentication endpoint on this page. For simplicity, we are using the inline option that doesn’t require a server for authentication.

您可以在此页面上学习如何创建身份验证终结点 。 为简单起见,我们使用不需要服务器进行身份验证的inline选项。

In the method viewDidLoad, subscribe to a private channel. Remember to use the private- prefix and connect to Pusher:

在方法viewDidLoad ,订阅专用频道。 请记住使用private-前缀并连接到Pusher:

override func viewDidLoad() {
...
// subscribe to channel and connect
channel = pusher.subscribe("private-channel")
pusher.connect()
}

Let’s also add to the class a function to send a client event. Remember to use the client- prefix:

我们还向类添加一个发送客户端事件的函数。 请记住使用client-前缀:

func sendPusherEvent() {
channel.trigger(eventName: "client-new-measurement",       data: String(format: "%.2f cm", distance * 100.0)  )
}

Remember that the method renderer and the measure function is executed once per frame. Ideally, this is 60 times per second. To limit the number of client events sent to Pusher, we are going to use the TimeInterval parameter of the renderer method. This tells us the time at which the scene is updated.

请记住,方法renderermeasure函数每帧执行一次。 理想情况下,这是每秒60次。 为了限制发送到Pusher的客户端事件的数量,我们将使用renderer方法的TimeInterval参数。 这告诉我们场景更新的时间。

Modify the method measure to pass the parameter as its argument:

修改方法measure以将参数作为参数传递:

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// Call the method asynchronously to perform
//  this heavy task without slowing down the UI
DispatchQueue.main.async {
self.measure(time: time)
}
}
func measure(time: TimeInterval) {
...
}

Now add the following ifblock after setting the box rotation:

现在,在设置框旋转之后添加以下if块:

func measure(time: TimeInterval) {
...
if let result = planeTestResults.first {
status = "READY"
if mode == .measuring {
...
box.rotation = SCNVector4(x: 0, y: 1, z: 0,           w: -(angleInRadians + Float.pi)      )
// Only send the Pusher event after the specified interval
if time > sendingTime {
sendPusherEvent();
sendingTime = time + TimeInterval(0.2)
}
}
} ...
}

At first run, time will be greater than sendingTime. But then, we add 0.2 seconds (or 200 milliseconds) to time to set the new value of sendingTime. This way, we can be sure that the Pusher event will be executed at most 5 times per second.

在第一次运行时,时间将大于sendingTime. 但是,然后,我们将时间增加0.2秒(或200毫秒)来设置sendingTime的新值。 这样,我们可以确保Pusher事件每秒最多执行5次。

However, due to this time difference, the last measure probably won’t be sent. So we can call the sendPusherEvent method when the user finishes measuring:

但是,由于这个时间差,最后的小节可能不会发送。 因此,我们可以在用户完成测量后调用sendPusherEvent方法:

@IBAction func switchChanged(_ sender: UISwitch) {
if sender.isOn {
mode = .measuring
} else {
mode = .waitingForMeasuring
sendPusherEvent()
}
}

Of course, we could just send the last measure to Pusher to avoid the issue. But what will be the fun of sending only one value?

当然,我们可以将最后一个措施发送给Pusher以避免该问题。 但是只发送一个值的乐趣是什么?

测试应用 (Testing the app)

Launch the app:

启动应用程序:

It may take several seconds to initialize and find a plane so the status can change to READY, move around your device slowly and watch for the tracking state of the camera to speed up the process a little bit.

初始化和找到飞机可能需要几秒钟的时间,因此状态可以更改为READY ,在设备上缓慢移动,并注意相机的跟踪状态以加快速度。

Once it’s READY, you will be able to take measurements and see them in real-time on your device:

就绪后 ,您就可以进行测量并在设备上实时查看它们:

And on the Debug console of your Pusher dashboard:

在Pusher 仪表板的调试控制台上:

结论 (Conclusion)

In this tutorial, you have learned how to create a basic measurement app with ARKit and how to use Pusher to send client events from an iOS app.

在本教程中,您学习了如何使用ARKit创建基本的测量应用程序以及如何使用Pusher从iOS应用程序发送客户端事件。

Tracking the features of the real world is an important part of an augmented reality experience. However, sometimes the results are not accurate because the environment can be difficult to measure. In the ARKit documentation, you can find some tips to improve the AR experience.

跟踪现实世界的特征是增强现实体验的重要组成部分。 但是,有时结果可能不准确,因为环境可能难以衡量。 在ARKit文档中 ,您可以找到一些技巧来改善AR体验。

Remember that you can find the complete project on this GitHub repository.

请记住,您可以在此GitHub存储库中找到完整的项目。

翻译自: https://www.freecodecamp.org/news/how-to-build-a-real-time-augmented-reality-measuring-app-with-arkit-and-pusher-41da426dedf9/

arkit与现实世界距离比

arkit与现实世界距离比_如何使用ARKit和Pusher构建实时增强现实测量应用程序相关推荐

  1. ios pusher使用_如何使用JavaScript和Pusher构建实时评论功能

    ios pusher使用 by Rahat Khanna 通过拉哈特·汉娜 如何使用JavaScript和Pusher构建实时评论功能 (How to build a Live Comment fea ...

  2. ios pusher使用_如何使用JavaScript和Pusher构建实时图

    ios pusher使用 by Rahat Khanna 通过拉哈特·汉娜 如何使用JavaScript和Pusher构建实时图 (How to build a Realtime Graph usin ...

  3. 对话声网CEO赵斌:API吃掉世界,在RTC+AI+5G的交点上构建实时场景

    郭一璞 发自 悠唐 量子位 报道 | 公众号 QbitAI 2014年,曾经是YY CTO的赵斌在硅谷创立了声网. 这家公司的主要业务,就是借助API的方式,让全世界的开发者都能方便的在自家应用里接入 ...

  4. ios pusher使用_使用.NET和Pusher构建实时评论功能

    ios pusher使用 by Ogundipe Samuel 由Ogundipe Samuel 使用.NET和Pusher构建实时评论功能 (Build a real-time commenting ...

  5. vue js 对象下的原型_如何使用Vue.js和Pusher创建实时原型反馈应用程序

    vue js 对象下的原型 by Neo Ighodaro 由新Ighodaro 如何使用Vue.js和Pusher创建实时原型反馈应用程序 (How to create a realtime pro ...

  6. python与word交互_厉害了word哥,交互式实时监控调整python程序执行!

    之前调试线上程序时,经常遇到的一个问题就是,线上程序出现bug,但线下调试时却无法重现,或者说想重现需要的准备条件很繁琐.所以我就想,能不能在线上程序出现问题时,直接切入程序,实时看到程序内部状况,数 ...

  7. 干货分享——数字孪生应用与现实世界的虚实交互

    数字孪生技术是实现行业数字孪生应用的基础,能够基于真实地理信息,集合物理世界 各种要素,通过计算机实时渲染生成的可交互三维仿真场景,实现物理世界对象在数据世界 中完成精准映射.真实重现. 通过数字孪生 ...

  8. ai与虚拟现实_将AI推向现实世界

    ai与虚拟现实 If you fit one of these profiles, this article is for you: 如果您适合这些配置文件之一,那么本文适合您: ● You are ...

  9. 现实世界 机器学习_公司沟通分析简介现实世界的机器学习方法

    现实世界 机器学习 In my previous posts I covered analytical subjects from a scientific point of view, rather ...

最新文章

  1. 哈希表(散列表)的介绍,代码实现
  2. CodeForces - 1407E Egor in the Republic of Dagestan(最短路+dp)
  3. js ...运算符_「 giao-js 」用js写一个js解释器
  4. 我敢说,这个版本的斗地主你肯定没玩过?
  5. Spring Boot学习总结(17)——Spring Boot常见面试题再总结
  6. c++语言函数参数传递方式,C++函数传递三种方式解读
  7. Spring MVC学习总结(21)——i18n国际化资源配置各个国家语言汇总
  8. 详解相机的sensor工作原理
  9. 74HC/LS/HCT/F系列芯片的区别
  10. Nutch爬虫爬取视频研究文档
  11. java po vo bo是什么以及_Java中VO , PO , BO, DAO ,POJO是什么意思
  12. 如何看懂财务报表|介绍
  13. Snmp拓扑结构php,基于SNMP的网络拓扑结构生成
  14. 360大战QQ,用户被“保护”还是被“偷窥”
  15. 大数据难学吗?如何学习大数据?(附学习路线)
  16. Ipad开发课程系列目录--很好的教程,推荐给大家
  17. tabar被手机虚拟键挡住_bug_ _Popwindow被魅族手机自带虚拟键挡住完美解决
  18. 网络游戏中玩家在线数据的存取
  19. Fiddler 抓包工具使用详解
  20. JTAG (一)杂谈

热门文章

  1. Ant Design Pro 跳转路由 传参数,接收参数
  2. Ant Design Pro 使用图表 charts bizcharts
  3. 腾讯广告广点通API接入文档(Android)
  4. 自动化运维之SaltStack,批量安装httpd实战
  5. Win10 下 RabbitMQ 的 安装 配置
  6. Vue.js双向绑定的实现原理
  7. 哪些物联网应用最适合采用雾计算?
  8. windows XP下Python2.7包管理工具安装-setuptool,pip、distribute、nose、virtualenv
  9. 论排列组合,持续更新
  10. 解决vim没有颜色的办法