iOS Swift中如何让视频匹配ARKit图像锚点物理尺寸
解决ARKit图像锚点视频尺寸适配问题
先来说说你的handleFoundImage函数里存在的几个关键问题,以及对应的修复方案:
核心问题分析
- 视频节点未绑定到锚点节点:你把视频节点直接加到了
sceneView.scene.rootNode,而不是ARKit检测到的锚点对应的node上。这样视频的位置和变换无法跟随锚点实时更新,手动设置scnNodeBarn.transform = node.transform只是一次性同步,后续锚点移动时视频不会跟着动。 - 视频比例与图像尺寸不匹配:直接用图像的物理尺寸创建
SCNPlane,但视频本身有固定宽高比,会导致视频拉伸变形。 - 全局节点复用与观察者重复添加:全局的
scnNodeBarn会被多次复用,每次调用handleFoundImage都会覆盖之前的节点;同时重复添加AVPlayerItemDidPlayToEndTime观察者会导致内存泄漏和重复触发播放循环。 - 不必要的锚点移除:每次检测到锚点后调用
session.remove(anchor: anchor),会让ARKit停止跟踪该锚点,导致视频无法稳定跟随图像。
修改后的完整代码及说明
1. 修复handleFoundImage函数
private func handleFoundImage(_ imageAnchor: ARImageAnchor, _ node: SCNNode, nameVideo: String) { let name = imageAnchor.referenceImage.name! DispatchQueue.main.async { self.label.text = "You are watching \"\(name)\"" } // 1. 校验视频路径有效性 guard let documentsPathURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { print("无法获取文档目录") return } let videopath = documentsPathURL.appendingPathComponent("3DScenes/\(nameVideo)") guard FileManager.default.fileExists(atPath: videopath.path) else { print("视频文件不存在:\(videopath.path)") return } // 2. 初始化AVPlayer并处理循环播放(先移除旧观察者避免重复触发) NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) let avPlayerItem = AVPlayerItem(url: videopath) avPlayer = AVPlayer(playerItem: avPlayerItem) avPlayer?.play() NotificationCenter.default.addObserver( self, selector: #selector(restartVideo), name: .AVPlayerItemDidPlayToEndTime, object: avPlayerItem ) // 3. 计算视频与图像的适配尺寸,保持视频宽高比 let imagePhysicalSize = imageAnchor.referenceImage.physicalSize let videoTrack = avPlayerItem.asset.tracks(withMediaType: .video).first guard let videoNaturalSize = videoTrack?.naturalSize else { print("无法获取视频尺寸") return } let videoAspectRatio = videoNaturalSize.width / videoNaturalSize.height let imageAspectRatio = imagePhysicalSize.width / imagePhysicalSize.height var planeWidth: CGFloat var planeHeight: CGFloat // 适配规则:优先让视频完全填充图像区域,同时保持比例 if videoAspectRatio > imageAspectRatio { planeWidth = imagePhysicalSize.width planeHeight = planeWidth / videoAspectRatio } else { planeHeight = imagePhysicalSize.height planeWidth = planeHeight * videoAspectRatio } // 4. 创建视频平面和节点 let avMaterial = SCNMaterial() avMaterial.diffuse.contents = avPlayer avMaterial.isDoubleSided = true // 确保从正面能看到视频 let videoPlane = SCNPlane(width: planeWidth, height: planeHeight) videoPlane.materials = [avMaterial] let videoNode = SCNNode(geometry: videoPlane) // 旋转平面:SCNPlane默认垂直Y轴,旋转后和图像锚点平面对齐 videoNode.eulerAngles.x = -.pi / 2 // 5. 将视频节点添加到锚点对应的node上,自动跟随锚点移动 node.addChildNode(videoNode) self.currentNode = videoNode } // 单独的视频重启方法,避免闭包内存泄漏 @objc private func restartVideo() { avPlayer?.seek(to: .zero) avPlayer?.play() }
2. 其他代码的优化点
- 安全清理节点:在
imageLost中增加判空,避免崩溃:
@objc func imageLost(_ sender: Timer){ currentNode?.removeFromParentNode() currentNode = nil // 同时暂停视频并清理观察者 avPlayer?.pause() NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) }
- 移除不必要的锚点删除:删除
self.sceneView.session.remove(anchor: anchor)这一行,否则ARKit会停止跟踪图像锚点,视频无法稳定跟随图像移动。 - 移除全局节点:删除全局的
scnNodeBarn变量,每次创建新的videoNode,避免节点复用导致的混乱。
关键原理说明
- 锚点节点绑定:ARKit返回的
node直接关联图像锚点,将视频节点作为它的子节点,就能自动继承锚点的位置、旋转和缩放,完美贴合图像的物理位置。 - 宽高比适配:通过对比视频和图像的宽高比,计算出不拉伸的平面尺寸,保证视频显示比例正确,同时填充图像区域(你也可以调整适配规则,比如优先让视频完整显示、留空边)。
内容的提问来源于stack exchange,提问作者iOSDude
相关产品推荐
相关产品推荐

