在上节中我们已经了解了环境探头以及如何使用自动环境探头,这节一起了解如何使用手动配置环境探头。
在使用自动环境反射时,开发人员无须进行有关环境反射的任何操作,只需要设置自动环境反射即可,其余工作完全由 RealityKit 自动完成,这适用于基本的常见环境反射。但这种环境反射方案是一种普适性的反射,并没有专门针对某特定虚拟元素进行优化,在某些情况下效果并不精细,并且我们也无法进行干预调优,如一辆行驶的赛车对环境的反射就需要更精细的控制,这时就需要手动控制环境探头的生成及更新。
使用手动控制环境探头时,我们需要将配置中 environmentTexturing 属性设置为 manual,并决定在什么地方、什么时候设置与更新环境探头。通常而言,可以遵循以下流程:
(1) 在场景中某个特定位置创建 AREnvironmentProbeAnchor 锚点。
(2)将创建的 AREnvironmentProbeAnchor 锚点添加到 ARSession 中。
(3)使用 session(_:didUpdate:)代理方法根据需要更新环境探头。
使用手动方式控制环境探头的示例代码如下所示。
//
// ManualEnvirmentTexturing.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/1/30.
//
import SwiftUI
import ARKit
import RealityKit
struct ManualEnvirmentTexturing: View {
@State var automatic: Bool = true
var body: some View {
ManualEnvirmentTexturingContainer(automatic: $automatic)
.overlay(content: {
VStack {
Spacer()
HStack{
Text(automatic ? "HDR" : "HDR Off")
.background(GeometryReader{ _ in
Color.white
})
.padding(10)
.offset(x: 0)
Toggle(isOn: $automatic) {}
.frame(width: 50)
.offset(x: 0)
}
Spacer().frame(height: 40)
}
})
.edgesIgnoringSafeArea(.all)
.navigationTitle("环境探头")
}
}
struct ManualEnvirmentTexturingContainer:UIViewRepresentable {
@Binding var automatic: Bool
func makeUIView(context: Context) -> ARView
{
let arView = ARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
config.environmentTexturing = .manual
context.coordinator.arView = uiView
uiView.session.delegate = context.coordinator
if automatic {
config.wantsHDREnvironmentTextures = true
}else{
config.wantsHDREnvironmentTextures = false
}
uiView.session.run(config, options: [])
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject,ARSessionDelegate {
var manualProbe: ManualProbe?
var arView: ARView? = nil
var parent: ManualEnvirmentTexturingContainer
init(parent: ManualEnvirmentTexturingContainer) {
self.parent = parent
}
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
guard let anchor = anchors.first as? ARPlaneAnchor else {
return
}
//球体
let mesh = MeshResource.generateSphere(radius: 0.05)
let meterial = SimpleMaterial(color: .blue, isMetallic: true)
let modelEntity = ModelEntity(mesh: mesh, materials: [meterial])
let planAnchor = AnchorEntity(anchor: anchor)
//放在正上方5cm处
modelEntity.transform.translation = [0, planAnchor.transform.translation.y + 0.05,0]
manualProbe = ManualProbe(shpereEntity: modelEntity)
updateProbe()
planAnchor.addChild(manualProbe!.shpereEntity)
arView?.scene.addAnchor(planAnchor)
//只添加一次
session.delegate = nil
session.run(ARWorldTrackingConfiguration())
manualProbe?.isPlaced = true
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
if let manualProbe = manualProbe,
manualProbe.requireRefresh,
Date().timeIntervalSince1970 - manualProbe.lastUpadateTime > 1{
self.manualProbe?.lastUpadateTime = Date().timeIntervalSince1970
updateProbe()
}
}
func updateProbe() {
guard let manualProbe = manualProbe else {
return
}
//移除旧的
if let probAnchor = manualProbe.objectProbeAnchor {
self.arView?.session.remove(anchor: probAnchor)
self.manualProbe?.objectProbeAnchor = nil
}
var extent = (manualProbe.shpereEntity.model?.mesh.bounds.extents)! * manualProbe.shpereEntity.transform.scale
extent.x *= 3
extent.y *= 3
extent.z *= 2
// let verticalOffset = SIMD3(0, extent.y, 0)
// var probeTransform = manualProbe.shpereEntity.transform
// probeTransform.translation += verticalOffset
let position = simd_float4x4(
SIMD4(1,0,0,0),
SIMD4(0,1,0,0),
SIMD4(0,0,1,0),
SIMD4(manualProbe.shpereEntity.transform.translation,1)
)
self.manualProbe?.objectProbeAnchor = AREnvironmentProbeAnchor(name: "objectProbe",transform: position, extent:extent)
self.arView?.session.add(anchor: (self.manualProbe?.objectProbeAnchor)!)
}
}
}
struct ManualProbe {
var objectProbeAnchor: AREnvironmentProbeAnchor?
var requireRefresh = true
var lastUpadateTime = Date().timeIntervalSince1970
var dateTime = Date()
var shpereEntity: ModelEntity
var isPlaced = false
}
在代码中,为更好地组织代码,我们自定义了 ManualProbe 类管理环境探头。在 ARKit 检测到水平平面并放置圆球后,将生成的环境探头放置在園球上方5cm 的地方以更精准地反射圆球的周边环境。在 session(_:didUpdate:)代理方法中我们对环境探头更新率进行了控制,设置为每秒更新一次,与所有的 ARAnchor一样,AREnvironmentProbeAnchor 锚点无法修改,更新时只能移除原锚点信息,创建新的锚点使用。updateProbe()方法负责所有的环境探头更新操作(具体细节稍后详述)。通过及时地更新就能反射用户环境中动态的变化,效果如图所示。
手动放置环境探头主要是为了获得对特定虚拟对象的最精确环境反射信息,绑定环境探头与虚拟对象位置可以提高反射渲染的质量,因此,手动将反射探头放置在重要的虚拟对象中或其附近会产生为该对象生成最准确的环境反射信息。通常而言,自动放置可以提供对真实环境比较好的宏观环境信息,而手动放置能提供在某个特定点上对周围环境更准确的环境映射从而提升反射的质量。
反射探头负责捕获环境纹理信息,每个反射探头都有一个比例(Scale)、方向(Orientation)、位置(Position) 和大小(Size)。比例、方向和位置属性定义了反射探头相对于 ARSession 空间的空间信息,大小则定义了反射探头反射的范围,无限大小表示环境纹理可用于全局,而有限大小表示反射探头只能捕获其周围特定区域的环境信息。
在手动放置时,为了使放置的反射探头能更好地发挥作用,通常反射探头的放置位置与大小设置应遵循以下原则:
(1)反射探头的位置应当放在需要反射的虚拟物体顶部中央,高度应该为虚拟物体高的两倍,如下图所示。这可以确保反射探头下部与虚拟物体下部对齐,并捕获到虚拟物体放置平面的环境信息。
(2)反射探头的长与宽应该为虚拟物体长与宽的3倍,确保反射探头能捕获到虚拟物体周边的环境信息。
在AR 中使用反射探头反射环境可以大大增强虚拟物体的可信度,但由于AR摄像头获取的环境信息不充分,ARKit 只能得到摄像头拍摄的真实环境部分数据,而无法获取摄像头未拍摄部分的环境数据,需要利用人工智能的方式对不足信息进行补充,需要补充的信息计算量大,对资源要求高,这对移动平台的性能与电池续航提出了非常高的要求,因此为更好的扬长避短,在AR 中使用反射探头反射真实环境需要注意以下几点。
1. 避免精确反射
如上所述,AR中从摄像头获取的信息不足以对周围环境进行精准再现,因此反射体对环境的反射也不能做到非常精准,希望利用反射探头实现对真实环境的镜面反射是不现实的。因为在 AR 中不能获取完全的立方体贴图并且立方体贴图更新也不实时(为降低硬件消耗),通常在小面积上可以使用高反射率而在大面积上使用低反射率,达到既营造反射效果又避免反射不准确而带来的负面作用。
2. 对移动对象的处理
烘焙的环境贴图不能反映环境的变化,实时的反射探头又会带来过大的性能消耗,对移动对象的反射处理需要特别进行优化。在设置反射探头时可以考虑以下方法:
(1)如果移动物体移动路径可知或可以预测,可以提前在其经过的路径上放置多个反射探头并进行烘焙,这样移动物体可以根据距离的远近对不同的反射探头生成的立方体贴图采样。
(2)创建一个全局的环境反射,如Skybox,这样当移动物体移动出某个反射探头的范围时仍然可以反射而不是突然出现反射中断。
(3)当移动物体移动到一个新的位置后重新创建一个反射探头并销毁原来位置的反射探头。
3. 防止滥用
在AR中,当用户移动位置或者调整虚拟对象大小时,应用程序都会重新创建反射探头,因此我们需要限制此类更新,如更新频率不应大于1次每秒。
4.避免突然切换
突然地移除反射探头或者添加新的反射探头会让用户感到不适。在 ARKit 中使用自动放置反射探头的模式下,只要 ARSession启动,就会创建一个全局的类似 Skybox 的大背景以防止反射突然切换。在手动放置时,开发者应该确保反射的自然过渡,确保虚拟物体始终能反射合适的环境,或者使用一个全种环境下都能适应的静态立方体贴图作为过渡手段。
具体代码地址:https://github.com/duzhaoquan/ARkitDemo.git