前言
对ARKit感兴趣的同学,可以订阅ARKit教程专题
源代码地址在这里
正文
物理学在使事物表现得像真实一样起着至关重要的作用。 SceneKit有一个内置的物理引擎,我们可以利用它来使我们的虚拟内容更加逼真。
物理体
要使对象参与SceneKit的内置物理模拟,我们必须将SCNPhysicsBody附加到对象的SCNode。
有三种基本类型的物理体:
- A static body: 这种类型的物理体是静态的,例如墙。属于同一物理模拟的其他物理实体将与静态物体相互作用但不会影响它,并且它将始终保持静态位置。
- A dynamic body: 物理引擎完全控制这种物理体。它将动态移动并与物理模拟中的其他物理主体进行交互。这可能是某些骰子或球。
- A kinematic body: 物理模拟不控制这种物理体;相反,它是以编程方式控制的。这允许我们创建将参与物理模拟的动态可移动对象。一个很好的例子就是“突破”式游戏中的划桨。
物理形状
物理形状定义了虚拟对象的几何形状。这可能是实际对象的极低多边形版本。它也可以是一个与我们的物体形状非常相似的原始形状。
SceneKit带有一些开箱即用的原始形状。这包括诸如金字塔,盒子,球体,圆柱体,锥体,甜甜圈,胶囊和管的形状:
其他的物理属性
物理体还定义了控制虚拟对象物理行为的其他物理相关属性。
这包括诸如质量,摩擦力,恢复原状(物体的弹性),阻尼,电荷以及物理体是否受重力影响等属性。
添加physics
我们首先需要做的就是把DiceScene.scn里面的所有的骰子的中例模式改为Dynamic:
有没有看到Settings下面有很多选项?他们的具体作用如下:
- Mass: 物体的质量会影响它的动量,也会影响它对力的反应。
- Friction: 摩擦系数。
- Restitution: 以碰撞过程中损失或获得的动能量的形式确定身体的弹性。
- Rolling Friction: 指定物体对滚动的抵抗力。
- Damping: 降低物体线速度的因素。
- Angular Damping: 降低物体角速度的因素。
- Charge: 指定物体的电荷。
- Gravity:用于指示重力是否对物理机构产生影响。
- Center Of Mass: 物体的中心位置。
- Resting: 用于表明物理学是否可以休息的标志。
最后,在Physics Shape这一组,把类型改成:Bounding Box(这个选项在下面,你需要往下翻):
现在所有骰子都配置了一个动态物理体,并将一个基本边界框设置为每个骰子的碰撞边界。
控制物理世界的速度
执行快速构建并运行以进行测试。注意一下你会注意到当你向上滑动时骰子有的骰子会掉下去。这是因为他们有物理机构分配给他们,他们都受到重力的影响。如果适当加你少一些重力系数,他们掉落的时候会掉落的慢一些:
在initScene():函数中添加如下的代码:
scene.physicsWorld.speed = 0.05
注意:我们可以实时调整世界速度以创建有趣的慢动作效果。
控制物理模拟速度
默认情况下,SceneKit的物理模拟每秒更新60次。这意味着模拟中的每个物理主体都以1/60秒的时间间隔进行更新。
增加此物理速度间隔将导致更精确的物理模拟,尤其是当我们处理快速移动的对象时。这样随之而来的是CPU功耗的提升。
我们可以在initScene()方法中添加如下的方法:
scene.physicsWorld.timeStep = 1.0 / 60.0
恢复掉落的骰子
为了防止骰子陷入无尽的深渊,你需要随着时间的推移监视每个骰子的位置。当它的位置低于某个点时,你可以将它返回到用户的手中。
我们可以子在updateDiceNodes()方法中作如下处理:
func updateDiceNodes() {
// 1
for node in sceneView.scene.rootNode.childNodes {
// 2
if node.name == "dice" {
if node.presentation.position.y < -2 {
// 3
node.removeFromParentNode()
diceCount += 1
}
}
}
}
上面代码作用如下:
- 1:首先,遍历活动场景中的所有可用节点。
- 2: 你想要那些名为dice的物体在地面以下2米的位置。
- 3:一旦你找到一个合格的骰子节点,你将它从场景中移除,然后将diceCount增加1,骰子将放回到用户的手中。
接下来,我们必须确保在渲染循环中调用新函数。将以下内容添加到renderer(_:updateAtTime)函数中:
self.updateDiceNodes()
建立并运行,骰子仍然会下降。一旦他们到达低于-2米的位置,他们将返回到用户的手中。这些筛子可以继续被投掷出去!
物理平面
现在的情况是骰子扔出去就会一直往下掉落,其实咱们可以添加一个平面,这样只有在平面外面的骰子才会掉落。我们可以创建一个物理平面:
func createARPlanePhysics(geometry: SCNGeometry) -> SCNPhysicsBody {
// 1
let physicsBody = SCNPhysicsBody(
type: .kinematic,
// 2
shape: SCNPhysicsShape(geometry: geometry, options: nil))
// 3
physicsBody.restitution = 0.5
physicsBody.friction = 0.5
// 4
return physicsBody
}
这定义了一个函数,将使用它来为检测到的平面创建物理体。该函数具体作用如下:
- 1: 这会产生一个与静态物体非常相似的运动物理体。
- 2: 第二个参数是物理体形状。 SCNPhysicsShape()构造函数非常智能:如果我们为3D对象提供对象几何体,它将自动为3D对象创建适当的形状,并为其他选项提供nil。
- 3: 我们已经创建了一个物理体,现在可以调整它的一些物理属性,比如它的弹性或表面的颗粒度。
创建AR平面节点时,还需要为平面节点创建物理主体。在返回planeNode语句之前,将以下内容添加到createARPlaneNode(planeAnchor:color :)函数中:
planeNode.physicsBody = createARPlanePhysics( geometry: planeGeometry)
现在,当创建AR平面节点时,它是使用附加的物理主体创建的。
AR平面节点更新的时候,我们需要删除掉旧的物理体,之后再添加新的物理体。
在updateARPlaneNode(planeNode:planeAchor :)函数中作如下操作:
planeNode.physicsBody = nil
planeNode.physicsBody = createARPlanePhysics( geometry: planeGeometry)
上面的两行代码作用就是先把之前的物理体设置为空,之后再赋值。编译运行,效果如下:
当设备位于检测到的表面之一上方时向上滑动,我们应该注意到骰子不再只是从表面掉落。他们现在应该弹跳和滑动,就像真实的东西一样。
随机旋转
真实的环境就是筛子会旋转着或者滚动着掉下来,其实这个效果也可以实现,在throwDiceNode(transform:offset:)函数中添加如下代码:
let rotation = SCNVector3(Double.random(
min: 0,
max: Double.pi),
Double.random(min: 0, max: Double.pi),
Double.random(min: 0, max: Double.pi))
这定义了一个包含每个轴随机旋转的向量。
现在旋转骰子,在之后添加以下内容到
在throwDiceNode(transform:offset :)函数中,在diceNode.position = position后天,添加如下代码:
diceNode.eulerAngles = rotation
这设定了骰子的eulerAngles,这实际上是骰子的旋转角度。
重力
添加上重力效果的话,我们创建的场景会变得更加逼真。
添加重力
目前,throwDiceNode(transform:offset :)仅在产生骰子时设置骰子的位置。这一块的逻辑我们现在需要更改一下:
// 1
let distance = simd_distance(focusNode.simdPosition,
simd_make_float3(transform.m41,
transform.m42,
transform.m43))
// 2
let direction = SCNVector3(-(distance * 2.5) * transform.m31,
-(distance * 2.5) * (transform.m32 - Float.pi / 4),
-(distance * 2.5) * transform.m33)
上述代码作用如下:
- 1: 你需要对骰子施加可变力。该力需要结合骰子和焦点节点之间的距离。该线计算骰子和焦点节点之间的精确距离。
- 2: 向前掷骰子。这会创建一个前向矢量,包含刚刚计算的距离,包括骰子旋转。
注意:Transform是一个4x4数学矩阵的SCNMatrix4。当看到引用时,例如m41,它指的是矩阵的第4行第1列。矩阵中的每一行都与特定的变换值相关,如其平移,旋转和缩放。
之后将以下内容添加到throwDiceNode(transform:offset:)函数中diceNode.eulerAngles = rotation的后面:
// 1
diceNode.physicsBody?.resetTransform()
// 2
diceNode.physicsBody?.applyForce(direction, asImpulse: true)
上面的代码作用如下:
- 1: 这会更新骰子物理主体位置以匹配其实际位置。
- 2: 最后,这会对骰子施加一个力,将其向特定方向投掷,作为短暂的冲力。
编译运行一下,你会发现,这个时候筛子的掉落更像是我们现实场景中的掷骰子的动作了。
光照和阴影
添加阴影投射灯
你将要在现有的骰子场景中添加一个新的光源,你将会使它投射出漂亮的柔和阴影。选择PokerDice.scnassets / Models / DiceScene.scn场景。为了使事情变得更容易,你需要在模具下面的东西才能看到它们的阴影。
在Node Inspector中,将新添加的floor的位置设置为(x:0,y:-0.05,z:0),使其刚好位于骰子下方。然后,在Attributes Inspector中,通过将Reflectivity设置为0来关闭反射。
现在添加一个方向灯,方法是从对象库中拖出一个。在Node Inspector下,将其位置设置为(x:0,y:0.1,z:0),并将其Euler角度设置为(x:-90,y:0,z:-25)。
你会注意到还没有阴影。在仍然选择了方向灯的情况下,转到Attributes Inspector,然后在Shadow部分中,选中Casts shadows复选框并将Mode更改为Deferred。
注意:请务必注意,必须将Shadow Mode设置为Deferred。这样,即使平面本身被隐藏,AR平面也会拾取阴影。
将color更改为75%不透明度,使阴影不会变得如此强烈。最后将Sample Radius设置为10,使阴影变得柔和。
不出意外的话,你可以看到阴影了:
接下来,添加光照。回到ViewController.swift,添加一个光照节点成员变量:
var lightNode: SCNNode!
在loadModels()函数的下面添加如下代码:
lightNode = diceScene.rootNode.childNode( withName: "directional", recursively: false)!
sceneView.scene.rootNode.addChildNode(lightNode)
这将加载来自骰子场景的定向光,然后将其添加到最终AR场景中。
光照估计
在再次测试之前,需要添加一个设置,以便ARKit根据检测到的环境光强度管理光照强度。在initARSession()函数中,在sceneView.session.run(config):之前添加如下代码:
config.isLightEstimationEnabled = true
当用户在光线环境下使用时,光线强度会很高。当用户在黑暗环境中玩耍时,光线强度会很低。美好而浪漫。。。。。。咳咳。。。。。。
完善阶段
你的游戏大部分都已完成,但是一些松散的连接需要注意。当用户启动并重置游戏时,需要做一些小事。在接下来的步骤中,我们将实现一些可以解决这些问题的附加功能。
最后,我们将连接所有UI按钮并添加一些基本的游戏状态逻辑。
暂停AR平面检测
绝对没有理由让ARKit在用户开始游戏后试图找到更多的平面来继续处理数据。因此暂停AR平面检测是一种好习惯。
在suspendARPlaneDetection()函数中添加如下代码:
func suspendARPlaneDetection() {
// 1
let config = sceneView.session.configuration as! ARWorldTrackingConfiguration
// 2
config.planeDetection = []
// 3
sceneView.session.run(config)
}
上面的代码作用如下:
- 1: 这从场景视图会话中获取当前的AR跟踪配置。
- 2: 然后它通过将planeDetection属性设置为空数组来清除它。
- 3: 最后,它重新运行会话。
隐藏可见的平面
平面是我们检测到可以用来放置骰子的工具,其实,可以把它隐藏掉。
func hideARPlaneNodes() {
for anchor in (self.sceneView.session.currentFrame?.anchors)! {
if let node = self.sceneView.node(for: anchor) {
for child in node.childNodes {
let material = child.geometry?.materials.first!
material?.colorBufferWriteMask = []
}
}
}
}
上面的代码作用如下:
- 1:首先逐步浏览场景视图中的所有可用锚点。这是通过当前帧访问它们来实现的。
- 2: 获取当前锚点的相关节点。
- 3: 获得相关节点后,将其与所有子节点一起隐藏。
禁止滑动手势
在平面确定之前,最好不要用户把筛子投掷出来。我们可以在swipeUpGestureHandler(_:)函数中作如下操作:
guard gameState == .swipeToPlay else { return }
这样只有当进入.swipeToPlay状态,然后才允许滑动手势。
START事件
func startGame() {
DispatchQueue.main.async {
self.startButton.isHidden = true
self.suspendARPlaneDetection()
self.hideARPlaneNodes()
self.gameState = .pointToSurface
}
}
这个功能最终启动游戏。首先,它隐藏了下面的开始按钮,因为用户没有理由在按下后继续看到它。接下来,它暂停AR平面检测,因为不再需要它。然后,它隐藏检测到的平面。最后,它将游戏状态切换到.pointToSurface,要求用户指向一个检测到的表面以掷骰子。
@IBAction func startButtonPressed(_ sender: Any) {
startGame()
}
RESET事件
好了,开始游戏已经整理好了。但是,如果用户决定转移一下场地继续使用,ARKit 必须继续检测新表面。为此,我们需要重新启用表面检测,以便 ARKit 能够执行它。
func resetARSession() {
let config = sceneView.session.configuration as!
ARWorldTrackingConfiguration
config.planeDetection = .horizontal
sceneView.session.run(config,
options: [.resetTracking, .removeExistingAnchors])
}
上面的代码作用如下:
- 1: 该功能通过调整现有 AR 配置来重新启用 .水平平面检测。
- 2: 这一次,在重新运行配置时,我们需要传递一些其他选项:
.resetTracking会重新启动ARKit。
.removeExistingAnchors将销毁以前检测到的所有锚点,这也将破坏以前检测到的所有表面节点。
func resetGame() {
DispatchQueue.main.async {
self.startButton.isHidden = false
self.resetARSession()
self.gameState = .detectSurface
}
}
@IBAction func resetButtonPressed(_ sender: Any) {
self.resetGame()
}
resetGame()中将会执行resetARSession操作。
命中测试
不能让用户无限扔骰子,最好添加一个功能,点击已经投掷出去的骰子让筛子回收回来。具体做法如下:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
DispatchQueue.main.async {
if let touchLocation = touches.first?.location(in: self.sceneView){
if let hit = self.sceneView.hitTest(touchLocation, options: nil).first{
if hit.node.name == "dice"{
hit.node.removeFromParentNode()
self.diceCount += 1
}
}
}
}
}
上面的代码作用如下:
- 1:首先获取用户触摸屏幕的位置。
- 2: 使用触摸位置作为命中测试的起点。
- 3: 如果用户确实触摸过骰子,请将其从场景中移除,并通过将骰子计数增加一个将其放回用户手中。
信息量有点大,具体的看原代码吧。
上一章 | 目录 | 下一章 |
---|