如何计算Vision Pro相机与3D广告牌的夹角并判断可见性
问题描述
我有一个单面带广告的3D广告牌,需要判断它是否能被Vision Pro的相机捕捉到(即对用户可见)。我尝试用点积公式计算相机与广告牌的夹角,但得到的角度值有误,以下是我的代码,希望找出计算错误或获得替代实现方案:
class cityViewModel { var billboard = Entity() } struct CityView: View { private var cameraTracker = VisionProCameraTracker() var model = cityViewModel() var body: some View { RealityView { content in // Add the initial RealityKit content if let scene = try? await Entity(named: "newcity", in: citywithAdBundle) { content.add(scene) if let entity = scene.findEntity(named: "billboard") { model.billboard = entity print("Billboard position \(entity.position)") } updatingSceneEventsWith(content) } } .task { await cameraTracker.runArSession() } } private func updatingSceneEventsWith(_ content: RealityViewContent) { _ = content.subscribe(to: SceneEvents.Update.self) { _ in Task { let cameraTransform = await cameraTracker.getTransform() let x2 = cameraTransform!.columns.3.x let x1 = await model.billboard.position.x let y2 = cameraTransform!.columns.3.y let y1 = await model.billboard.position.y let z2 = cameraTransform!.columns.3.z let z1 = await model.billboard.position.z let distance = sqrtf( (x2-x1) * (x2-x1) + (y2-y1) * (y2-y1) + (z2-z1) * (z2-z1) ) var formatted = String(format: "%.2f m", arguments: [distance]) print("Distance from camera to billboard is:", formatted) let ab = (x1 * x2) + (y1 * y2) + (z1 * z2) let magnitudeOfAd = sqrtf( (x1 * x1) + (y1 * y1) + (y1 * y1) ) let magnititeOfCamera = sqrtf( (x2 * x2) + (y2 * y2) + (y2 * y2) ) let cosTheta = ab / (magnitudeOfAd * magnititeOfCamera) let angleInRadians = acos(cosTheta) let angleInDegrees = angleInRadians * 180 / .pi formatted = String(format: "%.2f m", arguments: [angleInDegrees]) print("Angle b/w camera and billboard is \(formatted) degrees.") } } } } @Observable class VisionProCameraTracker { let session = ARKitSession() let worldTracking = WorldTrackingProvider() func runArSession() async { Task { try? await session.run([worldTracking]) } } func getTransform() async -> simd_float4x4? { guard let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: 4) else { return nil } let transform = deviceAnchor.originFromAnchorTransform return transform } }

核心错误分析
向量选择完全错误
你当前计算的是「广告牌世界位置」与「相机世界位置」的夹角,这两个点的向量夹角和广告牌是否面向相机没有任何关系。正确的应该计算广告牌正面法线向量与相机到广告牌的视线向量的夹角。模长计算低级错误
计算向量模长时重复使用了y分量,把z分量漏掉了:// 错误代码 let magnitudeOfAd = sqrtf( (x1 * x1) + (y1 * y1) + (y1 * y1) ) let magnititeOfCamera = sqrtf( (x2 * x2) + (y2 * y2) + (y2 * y2) ) // 正确写法 let magnitudeOfAd = sqrtf( (x1 * x1) + (y1 * y1) + (z1 * z1) ) let magnititeOfCamera = sqrtf( (x2 * x2) + (y2 * y2) + (z2 * z2) )相机时间戳错误
queryDeviceAnchor(atTimestamp: 4)使用固定时间戳4,应该用当前实时时间戳CACurrentMediaTime(),否则获取的是过期的相机位置。异步任务堆积风险
在SceneEvents.Update(每秒触发60次)的闭包里直接启动Task,会导致大量异步任务堆积,严重影响性能。
修正后的实现方案
下面的代码会正确计算广告牌正面与相机视线的夹角,从而判断是否可见:
private func updatingSceneEventsWith(_ content: RealityViewContent) { var lastCheckTime = CACurrentMediaTime() _ = content.subscribe(to: SceneEvents.Update.self) { [weak self] _ in guard let self = self else { return } // 限制更新频率,每0.5秒计算一次 let currentTime = CACurrentMediaTime() guard currentTime - lastCheckTime > 0.5 else { return } lastCheckTime = currentTime Task { guard let cameraTransform = await self.cameraTracker.getTransform() else { return } // 1. 获取相机世界坐标 let cameraWorldPos = simd_float3(cameraTransform.columns.3) // 2. 获取广告牌世界坐标和正面法线(假设模型自身-z方向为正面,可根据你的模型调整) let billboardWorldPos = self.model.billboard.position let billboardWorldMatrix = self.model.billboard.transformMatrix(relativeTo: nil) // 将模型本地法线转换为世界空间法线并归一化 let billboardFrontNormal = simd_normalize(simd_float3(billboardWorldMatrix.columns.2) * -1) // 3. 计算相机到广告牌的视线向量(归一化) let viewVector = simd_normalize(billboardWorldPos - cameraWorldPos) // 4. 点积判断可见性:点积>0表示法线与视线夹角小于90度,正面朝向相机 let dotProduct = simd_dot(billboardFrontNormal, viewVector) let isVisible = dotProduct > 0 print("广告牌是否可见:\(isVisible)") // 计算具体角度(仅当可见时) if isVisible { let angleInRadians = acos(dotProduct) let angleInDegrees = angleInRadians * 180 / .pi print("相机视线与广告牌正面夹角:\(String(format: "%.2f", angleInDegrees))°") } else { print("广告牌背向相机") } // 计算距离(原代码逻辑正确,保留) let distance = simd_distance(cameraWorldPos, billboardWorldPos) print("相机到广告牌距离:\(String(format: "%.2f m", distance))") } } } // 修正相机追踪类的getTransform方法 @Observable class VisionProCameraTracker { let session = ARKitSession() let worldTracking = WorldTrackingProvider() func runArSession() async { do { try await session.run([worldTracking]) } catch { print("AR会话启动失败:\(error)") } } func getTransform() async -> simd_float4x4? { guard let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: CACurrentMediaTime()) else { return nil } // 注意:anchorFromOriginTransform才是相机的世界变换矩阵 return deviceAnchor.anchorFromOriginTransform } }
额外优化建议
- 结合视锥体检测:上述代码仅判断角度,还可以添加相机视锥体检测,避免广告牌在相机视野外但角度符合的情况,可通过
ARCamera的投影矩阵计算视锥体范围。 - 利用RealityKit内置API:如果只需要判断渲染层面的可见性(是否被遮挡、是否在视野内),可以直接读取
entity.isVisible属性,但该属性不判断正面朝向。
内容的提问来源于stack exchange,提问作者Santrack
相关产品推荐
相关产品推荐

