Files
TrackingDemo/NearbyDemo/ARViewContainer.swift

105 lines
4.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import RealityKit
import ARKit
import Combine
struct ARViewContainer: UIViewRepresentable {
@ObservedObject var arManager: ARManager
@ObservedObject var estimator: AnchorEstimator
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: false)
arView.session = arManager.session
// Create the red sphere entity representing the UWB anchor
let sphereRadius: Float = 0.05
let mesh = MeshResource.generateSphere(radius: sphereRadius)
var material = UnlitMaterial()
material.color = .init(tint: .red)
let sphereEntity = ModelEntity(mesh: mesh, materials: [material])
sphereEntity.isEnabled = false
let anchorEntity = AnchorEntity(world: .zero)
anchorEntity.addChild(sphereEntity)
arView.scene.addAnchor(anchorEntity)
let coordinator = context.coordinator
coordinator.sphereEntity = sphereEntity
// SceneEvents.Update fires every rendered frame on the main thread.
// Updating sphere position here (rather than from the Combine sink) means:
// 1. The sphere is never "frozen" between 4 Hz UWB pings it always reflects
// the latest world-space estimate relative to the smoothly moving camera.
// 2. We can apply EMA smoothing to hide step-changes from the solver.
coordinator.updateSub = arView.scene.subscribe(to: SceneEvents.Update.self) { [weak estimator, weak coordinator, weak sphereEntity, weak arView] _ in
guard let estimator, let coordinator, let sphereEntity else { return }
// EMA-smooth toward the latest estimate every frame.
// α 0.12 at 60 fps time constant 120 ms. Hides solver step-changes
// without making the sphere feel sluggish.
let alpha: Float = 0.12
if let target = estimator.anchorPosition {
if let current = coordinator.smoothedPosition {
coordinator.smoothedPosition = current + alpha * (target - current)
} else {
coordinator.smoothedPosition = target // snap on first appearance
}
sphereEntity.setPosition(coordinator.smoothedPosition!, relativeTo: nil)
sphereEntity.isEnabled = true
} else {
coordinator.smoothedPosition = nil
sphereEntity.isEnabled = false
}
// Off-screen directional indicator using the smoothed position
guard let arView else { return }
guard let position = coordinator.smoothedPosition else {
if coordinator.lastAngle != nil {
DispatchQueue.main.async { estimator.offScreenAngle = nil }
coordinator.lastAngle = nil
}
return
}
let isOffScreen: Bool
if let proj = arView.project(position) {
isOffScreen = !arView.bounds.contains(proj)
} else {
isOffScreen = true
}
if isOffScreen {
guard let camera = arView.session.currentFrame?.camera else { return }
let cameraTransform = camera.transform
let localPos4 = simd_mul(simd_inverse(cameraTransform),
simd_float4(position.x, position.y, position.z, 1.0))
let angle = Double(atan2(localPos4.y, localPos4.x))
if coordinator.lastAngle == nil || abs(coordinator.lastAngle! - angle) > 0.05 {
coordinator.lastAngle = angle
DispatchQueue.main.async { estimator.offScreenAngle = angle }
}
} else {
if coordinator.lastAngle != nil {
DispatchQueue.main.async { estimator.offScreenAngle = nil }
coordinator.lastAngle = nil
}
}
}
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator {
var sphereEntity: ModelEntity?
var updateSub: (any Cancellable)?
var smoothedPosition: simd_float3? = nil
var lastAngle: Double?
}
}