在最后几节中,我们能够检测到一个平面并显示一个焦点方块,以帮助我们为模型指定一个位置。我们也熟悉了热门测试和世界变换。现在,我们拥有显示虚拟对象所需的所有工具。在本教程中,我们将学习如何检索模型并使用按钮的触发器将其呈现在场景中。一旦显示,我们将隐藏焦点方块。
要学习本教程,您需要Xcode 9或更高版本,以及Focus Square的最终Xcode项目。您可以下载本节的最终Xcode项目,以帮助您与自己的进度进行比较。
在Main.Storyboard中,我们已经提到ARSCNView默认放在视图控制器的顶部。但是,如果没有UIView作为基础,则仅限于您可以在用户界面上执行的操作。为了能够正确添加我们的按钮,我们必须删除当前的 ARSCNView并首先从对象库添加UIView作为底层。接下来,选择相同的ARKit SceneKit View并将其放回UIView之上。调整大小以填充整个视图控制器。
然后,单击Storyboard编辑器左下角的第四个图标,将新约束添加到场景视图中。定义约束以确保您的用户界面适应不同的屏幕尺寸或设备方向。设置为0的顶部,左,右和底部。确保它们都被约束到视图而不是安全区域,然后单击Add Constraints。安全区域是凹口下方和主页指示器上方的边距,通常是屏幕的可见部分。此外,请确保未选中“ 限制到边距”。
如果被限制在安全区域而不是超级视图,这就是看起来的样子,显然,这看起来并不好看。
横屏约束安全区
请记住,一个IBOutlet将sceneView链接到ARSCNView?因为我们删除了旧的ARSCNView,所以它打破了这个Outlet。我们需要重新考虑新的。为此,请打开“ 助理”编辑器,该图标看起来像两个交织在一起的圆圈。现在,我们并排放置两个分屏,非常适合连接。在右侧,我们有ViewController.swift,在那里我们可以找到该出口的声明。单击并拖动左侧的圆圈,它应该是第15行,然后释放到ARSCNView上。现在,关闭助理编辑。
我们想在视图中添加一个按钮,用作在场景中添加模型的触发器。从对象库中,将UIButton拖动到场景视图的顶部。在“ 属性”检查器中,删除“ 按钮”标题并将图像设置为“ 按钮/添加”。
约束到底部20但这次是在安全区域,并取消选中Constrain到边距。然后,将鼠标悬停在左侧的“ 对齐”图标上,并在“容器”中选中“水平”以在屏幕中水平居中。
我们刚刚在屏幕上添加了按钮,但它根本没有做任何事情。当我们触摸它时,让按钮执行某些操作。现在,打开Assistant编辑器并控制将故事板中的按钮拖到ViewController类。代码中的顺序并不重要,因为我们稍后会移动此函数。原因是我们不能在扩展类中执行此操作。将Connection更改为Action,将其命名为addObjectButtonTapped。保持原样。完成后,关闭“ 助理”编辑器。
@IBAction func addObjectButtonTapped(_ sender: Any) {
print("Add button tapped")
}
让我们运行应用程序来查看我们的新按钮。但在此之前,评论一些印刷品陈述是明智的。转到updateFocusSquare()并注释掉这些代码行。
// print("Focus square hits a plane")
// print("Focus square does not hit a plane")
让我们创建另一个swift文件,以便在场景中添加模型。右键单击视图控制器+ ARSCNViewDelegate.swift并选择新建文件...。然后,选择Swift File,单击Next。称之为ViewController + ObjectAddition,然后是Create。
与往常一样,用以下框架替换Foundation。然后,向ViewController添加扩展。
import UIKit
import SceneKit
import ARKit
extension ViewController {}
在扩展内部,创建一个新函数来检索我们选择的模型是一个很好的主动。此函数仅在此文件中使用,因此我们将采用fileprivate。将有一个String类型的参数,它将有两个名称。在函数外部使用的那个被命名,而在函数内使用的是名称。它将返回一个可选的SCNNode。
fileprivate func getModel(named name: String) -> SCNNode? {}
与飞船场景类似,我们将使用我们指定的名称调用场景。然后,检索该场景SketchUp的父节点。我们递归设置为false以返回具有该名称的直接子节点。如果为true,它将解析所有节点,直到找到它为止。我们知道SketchUp是场景中唯一的节点,所以在我们的情况下,真实的不准确。之后,我们将变量名称分配给模型的名称。最后,此函数将在调用时返回模型。
let scene = SCNScene(named: "art.scnassets/\(name)/\(name).scn")!
guard let model = scene.rootNode.childNode(withName: "SketchUp", recursively: false) else {return nil}
model.name = name
return model
如果您需要将模型的轴心点修改为所有3轴的中心,那么您可以在此处执行此操作。您可以使用以下公式。别客气。
let min = model.boundingBox.min
let max = model.boundingBox.max
model.pivot = SCNMatrix4MakeTranslation(
min.x + (max.x - min.x) / 2,
min.y + (max.y - min.y) / 2,
min.z + (max.z - min.z) / 2)
我们刚刚完成了这个功能,现在,我们准备在点击按钮时在场景中显示我们的模型。让我们转到ViewController.swift并剪切动作函数addObjectButtonTapped并将其粘贴到这里以将其全部放在一个地方。
我们首先确保焦点方块首先存在,因为它只在检测到表面时才出现在屏幕上。
guard focusSquare != nil else {return}
我们选择展示的模型是iPhoneX。因此,我们将使用getModel函数检索该模型。如果由于某种原因它失败了,我们将打印一条消息给我们。然后,让我们用一个小消息将它添加到场景中。
let modelName = "iPhoneX"
guard let model = getModel(named: modelName) else {
print("Unable to load \(modelName) from file")
return
}
sceneView.scene.rootNode.addChildNode(model)
print("\(modelName) added successfully")
运行应用程序。您将意识到该设备不仅站起来而且漂浮在空中。当然,我们已经在场景中添加了我们的模型,我们还没有把它放在表面上。所以,让我们这样做。
显然,我们将再次使用命中测试,方法与之前相同。
let hitTest = sceneView.hitTest(screenCenter, types: .existingPlaneUsingExtent)
guard let worldTransformColumn3 = hitTest.first?.worldTransform.columns.3 else {return}
model.position = SCNVector3(worldTransformColumn3.x, worldTransformColumn3.y, worldTransformColumn3.z)
要将电话平放在桌子上,请打开iPhoneX.scn。在“ 节点”检查器中,将x Euler Angle重置为0。
让我们再试一次。现在,我们的设备看起来更像是在房间里。
如果您选择了其他型号,您可能已经注意到尺寸不合适。因此,我们将扩展它们中的每一个。我们在iPhoneX的场景编辑器中完成了它。现在,我们在这里撤消它并代之以编码。让我们为所有边界将比例放回到1。
回到ViewController + ObjectAddition并在getModel函数中,我们首先为比例声明一个变量,然后根据模型设置不同的值。在我们的情况下,使用[switch]控制流来匹配我们设置的许多条件是完美的。switch语句必须是详尽的,这就是为什么有一个默认情况来涵盖所有其他方案。
var scale: CGFloat
switch name {
case "iPhoneX": scale = 0.025
case "iPhone6s": scale = 0.025
case "iPhone7": scale = 0.0001
case "iPhone8": scale = 0.000008
case "iPhone8Plus": scale = 0.000008
case "iPad4": scale = 0.0006
case "MacBookPro13": scale = 0.0029
case "iMacPro": scale = 0.0245
case "AppleWatch": scale = 0.0000038
default: scale = 1
}
在返回之前将模型缩放到我们之前分配的值。
model.scale = SCNVector3(scale, scale, scale)
知道我们在场景中有多少模型会很高兴。在ViewController.swift中,将一个新的类变量声明为一个节点数组,我们将其初始化为空。
var modelsInTheScene: Array<SCNNode> = []
返回ViewController + ObjectAddition.swift,并在addObjectButtonTapped操作方法的末尾,将您添加的每个模型追加到数组modelsInTheScene中。然后,打印该数组的计数。
modelsInTheScene.append(model)
print("Currently have \(modelsInTheScene.count) model(s) in the scene")
我们如何运行应用程序并坚果?
当我们在屏幕上显示模型时,我们仍然看到焦点方块干扰了我们漂亮的模型。如果我们在安置后隐藏它,你怎么说?
在FocusSquare类中,让我们创建一个函数来为焦点方块的表示设置动画。将隐藏和显示两种情况,因此隐藏值是布尔值。然后我们声明一个SCNAction用于淡入淡出,淡出用于隐藏和淡入显示。这些行动将运行根据是否隐藏是真还是假,一前一后。为此目的使用序列。
func setHidden(to hidden: Bool) {
var fadeTo: SCNAction
if hidden {
fadeTo = .fadeOut(duration: 0.5)
} else {
fadeTo = .fadeIn(duration: 0.5)
}
let actions = [fadeTo, .run({ (focusSquare: SCNNode) in
focusSquare.isHidden = hidden
})]
runAction(.sequence(actions))
}
下一步将有点棘手。如果我们看到模型,我们希望隐藏焦点方块,对吧?但是,如果我们在屏幕上看不到任何内容呢?我们再次需要它来选择下一个位置。我们在屏幕上看到的是不断变化的,所以我们需要在updateFocusSquare()中实现它。在那里,让我们将pointOfView设置为场景视图的视角。
guard let pointOfView = sceneView.pointOfView else {return}
然后,让我们将firstVisibleModel的定义作为场景中的第一个模型。我们正在使用第一个返回满足条件的第一个元素的方法。如果节点从视角可见,它将返回true或false 。
let firstVisibleModel = modelsInTheScene.first { (node) -> Bool in
return sceneView.isNode(node, insideFrustumOf: pointOfView)
}
现在,如果第一个模型是可见的而不是零,则模型将在视图中可见。请记住,如果显示模型,我们将隐藏焦点方块,反之亦然。如果这两个因子的值不相等,我们将改变焦点平方的isHidden值。
let modelsAreVisible = firstVisibleModel != nil
if modelsAreVisible != focusSquareLocal.isHidden {
focusSquareLocal.setHidden(to: modelsAreVisible)
}
实际上,这一切都令人困惑。我们实际上没有选择,因为节点具有isHidden的属性,并且不显示一个for。好吧,不是我所知道的。
那么,让我们来看看这两个场景。如果modelsAreVisible为true且focusSquareLocal.isHidden为false,则表示两者都可见,然后使setHidden为true(与modelsAreVisible值相同)以隐藏焦点方块。另一方面,如果modelsAreVisible为false且focusSquareLocal.isHidden为true,则两者都无处可见,然后setHidden为false以显示焦点方块。听起来很合乎逻辑。有了它,让我们最后一次运行应用程序。
经过漫长的旅程,我们终于将我们的模型添加到我们的环境中,好像它们属于它。我们在本节中也学到了其他有用的概念。我们在故事板中定制了我们的视图,并在代码中播放动画。在下一课中,我们将使用虚拟对象本身。敬请关注。