版权声明:Davidwang原创文章,严禁用于任何商业途径,授权后方可转载。
Ray casting,直译为射线投射,通常我们根据它的作用称为射线检测。射线检测是在3D数字世界里选择某个特定物体常用的一种技术,如在3D、VR游戏中检测子弹命中敌人情况或者从地上捡起一支枪,这都要用到射线检测,射线检测是在3D数字空间中选择虚拟物体的最基本方法。
在AR中,当检测并可视化一个平面后,如果需要在平面上放置虚拟物体,这时就会碰到在平面上什么位置放置虚拟物体的问题,因为检测到的平面是三维的,而手机/平板显示屏幕却是二维的,如何在二维的平面上选择三维放置点?解决这个问题的通常做法就是作射线检测。
射线检测的基本思路是在三维空间中从一个点沿一个方向发射出一条无限长的射线,在射线的方向上,一旦与添加了碰撞器的物体发生碰撞,则产生一个碰撞检测对象。因此,可以利用射线实现子弹击中目标的检测,也可以用射线来检测发生碰撞的位置,例如,我们可以从屏幕中用户点击点位置,利用摄像机(AR中就是我们的眼睛)的位置来构建一条射线,与场景中的平面进行碰撞检测,如果发生碰撞则返回碰撞的位置,这样就可以在检测到的平面上放置虚拟物体了,射线检测原理如下图所示。
在上图中,对AR应用来说,使用者操作的是其手机设备屏幕,射线检测的具体做法是检测用户点击屏幕的操作,以点击的位置为基准,连接该位置与摄像机就可以两点构成一条直线。从摄像机位置出发,通过点击点就可以构建一条射线,利用该射线与场景中的物体进行碰撞检测,如果发生碰撞则返回碰撞对象,这就是AR中射线检测的技术原理。
ARKit在ARView、Scene、ARSession类中都提供了射线检测方法,这些方法灵活多样,不仅可以满足对场景中虚拟物体的检测,也可以实现对检测到的平面、特征点、场景几何网格的碰撞检测,极大的方便了开发者的使用。
ARView中提供的射线检测方法如下表所示。
射线检测方法 | 描述 |
---|---|
makeRaycastQuery(from: CGPoint, allowing: ARRaycastQuery.Target, alignment: ARRaycastQuery.TargetAlignment) -> ARRaycastQuery? | 这是一个射线检测辅助方法,用于构建一个ARView射线检测请求 |
raycast(from: CGPoint, allowing: ARRaycastQuery.Target, alignment: ARRaycastQuery.TargetAlignment) -> [ARRaycastResult] | 投射一条从摄像机到场景中的射线,返回射线检测结果 |
trackedRaycast(from: CGPoint, allowing: ARRaycastQuery.Target, alignment: ARRaycastQuery.TargetAlignment, updateHandler: ([ARRaycastResult]) -> Void) -> ARTrackedRaycast? | 投射一条从摄像机到场景中的射线,进行射线检测并返回结果。与raycast()方法不同的是,该方法会对投射的射线进行跟踪以便优化检测结果 |
上表中的射线方法中,from参数为屏幕点击位置;allowing参数为ARRaycastQuery.Target类型,可以为estimatedPlane(ARKit估计的平面,但此时位置与尺寸都不稳定)、existingPlaneGeometry(位置与尺寸都稳定的平面)、existingPlaneInfinite(稳定平面所在的无限平面)3个值中的一个或者几个(多个时使用或(|)连接);alignment参数为ARRaycastQuery.TargetAlignment类型,可以为horizontal(水平平面)、vertical(垂直平面)、any(水平平面、垂直平面、场景几何)。
进行射线检测后,检测结果为ARRaycastResult数组,当没有发生碰撞时,该数组为nil,如果发生碰撞,则所有发生碰撞的信息(碰撞位置、碰撞对象名、距离等)都会存储在这个数组中,大多数情况下,数组序号为0的结果为我们所需要的结果(即离使用者最近的碰撞对象)。
ARView中提供的射线检测方法以当前设备屏幕中的2D坐标为起始点向场景中发射射线进行碰撞检测,因此非常方便手势操作使用。
Scene中提供的射线检测方法如下表所示。
射线检测方法 | 描述 |
---|---|
raycast(origin: SIMD3, direction: SIMD3, length: Float, query: CollisionCastQueryType, mask: CollisionGroup, relativeTo: Entity?) -> [CollisionCastHit] | 在场景中指定起始点、方向、长度构建射线,并可指定碰撞类型等其他属性的射线检测方法 |
raycast(from: SIMD3, to: SIMD3, query: CollisionCastQueryType, mask: CollisionGroup, relativeTo: Entity?) -> [CollisionCastHit] | 在场景中两点之间构建射线,可指定碰撞类型等其他属性的射线检测方法 |
convexCast(convexShape: ShapeResource, fromPosition: SIMD3, fromOrientation: simd_quatf, toPosition: SIMD3, toOrientation: simd_quatf, query: CollisionCastQueryType, mask: CollisionGroup, relativeTo: Entity?) -> [CollisionCastHit] | 在两个物体之间构建射线,可实现更复杂的射线检测操作 |
从射线检测方法参数可以看出, Scene中的射线检测方法更侧重于在场景中通过指定位置、指定方向、指定物体、指定碰撞器等构建更复杂的射线检测需求,实现更具体的射线检测。因此,Scene中的射线检测方法特别适合于在AR场景中一个虚拟物体向另一个虚拟物体进行碰撞检测的场合,如在AR场景中,一个机器人向一个怪物发射子弹。
ARSession中提供的射线检测方法如下表所示。
射线检测方法 | 描述 |
---|---|
raycast(ARRaycastQuery) -> [ARRaycastResult] | 根据射线检测请求执行射线检测操作 |
trackedRaycast(ARRaycastQuery, updateHandler: ([ARRaycastResult]) -> Void) -> ARTrackedRaycast? | 根据射线检测请求执行射线检测操作。与raycast()方法不同的是,该方法会对投射的射线进行跟踪以便优化检测结果 |
与ARView中的射线检测方法类似,ARSession中的射线检测方法也主要处理从屏幕到场景的射线检测,但更简单易用。
除了在ARView中构建射线检测请求外,在ARFrame中也可以构建射线检测请求,如下表所示。
构建射线检测请求方法 | 描述 |
---|---|
raycastQuery(from: CGPoint, allowing: ARRaycastQuery.Target, alignment: ARRaycastQuery.TargetAlignment) -> ARRaycastQuery | 从屏幕点击位置构建射线检测请求 |
通过本节的学习可以看到,ARKit提供了非常多进行射线检测的方法,可以实现不同的射线检测需求。另外,在ARView和ARFrame类中还提供了hit-Testing方法,但官方已不建议使用,保留这些方法也仅是为了前向兼容,在此不详述。
提示
trackedRaycast方法的使用需要非常谨慎,该方法会实时跟踪发射出去的射线,即发射的射线会一直存在,该方法不应当在session(_ session: ARSession, didUpdate frame: ARFrame)、session(_ session: ARSession, didUpdate anchors: [ARAnchor])等这类执行频率非常高的方法中使用。
通过上节的学习,我们已经了解了足够多的信息进行射线检测,下面以ARView中的射线检测为例,典型的代码如下所示。
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
arView.session.run(config, options:[ ])
arView.session.delegate = arView
arView.createPlane()
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
}
}
var planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)
var planeMaterial = SimpleMaterial(color:.white,isMetallic: false)
var planeEntity : ModelEntity? = ModelEntity(mesh:planeMesh,materials:[planeMaterial])
var planeAnchor = AnchorEntity()
extension ARView : ARSessionDelegate{
func createPlane(){
let planeAnchor = AnchorEntity(plane:.horizontal)
do {
planeMaterial.baseColor = try .texture(.load(named: "AR_Placement_Indicator.png"))
planeMaterial.tintColor = UIColor.yellow.withAlphaComponent(0.9999)
planeAnchor.addChild(planeEntity!)
self.scene.addAnchor(planeAnchor)
} catch {
print("找不到文件!")
}
}
public func session(_ session: ARSession, didUpdate frame: ARFrame){
guard let result = self.raycast(from: self.center, allowing: .estimatedPlane, alignment: .horizontal).first else {
return
}
planeEntity!.setTransformMatrix(result.worldTransform, relativeTo: nil)
}
}
在上述代码中,我们实现了一个在检测到的平面上指示放置虚拟物体位置的功能。实现思想是从屏幕中心点(width/2,height/2)位置发射一条射线,与已检测到的平面进行碰撞检测,并在检测到碰撞时放置指示图标并实时校正指示图标位置。由于session(_ session: ARSession, didUpdate frame: ARFrame)方法每帧都会执行,所以我们可以实时的看到指示图标姿态变化。实现的效果如下图所示。
在上述代码清单中,我们直接使用了ARView的raycast()方法进行射线检测,当然,也可以结合makeRaycastQuery()方法使用,实现效果完全一样,代码如下所示。
public func session(_ session: ARSession, didUpdate frame: ARFrame){
guard let raycastQuery = self.makeRaycastQuery(from: self.center,
allowing: .estimatedPlane,
alignment: .horizontal) else {
return
}
guard let result = self.session.raycast(raycastQuery).first else {
return
}
planeEntity!.setTransformMatrix(result.worldTransform, relativeTo: nil)
}
Tracked Ray Casting射线检测方法使用操作也基本一致,示例代码如代码清单2-13所示。
let _ = arView.trackedRaycast(from: arView.center, allowing: .existingPlaneInfinite, alignment: .horizontal, updateHandler: { results in
guard let result = results.first else { return }
let anchor = AnchorEntity(raycastResult: result)
anchor.addChild(entity)
self.arView.scene.addAnchor(anchor)
})
ARKit提供的射线检测方法种类繁多,但使用操作方法均基本一致,只是具体细节会有所差异,在使用时,可根据需求选择不同的实现方法,查阅官方文档了解具体参数。