You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS Swift中如何让视频匹配ARKit图像锚点物理尺寸

解决ARKit图像锚点视频尺寸适配问题

先来说说你的handleFoundImage函数里存在的几个关键问题,以及对应的修复方案:

核心问题分析

  1. 视频节点未绑定到锚点节点:你把视频节点直接加到了sceneView.scene.rootNode,而不是ARKit检测到的锚点对应的node上。这样视频的位置和变换无法跟随锚点实时更新,手动设置scnNodeBarn.transform = node.transform只是一次性同步,后续锚点移动时视频不会跟着动。
  2. 视频比例与图像尺寸不匹配:直接用图像的物理尺寸创建SCNPlane,但视频本身有固定宽高比,会导致视频拉伸变形。
  3. 全局节点复用与观察者重复添加:全局的scnNodeBarn会被多次复用,每次调用handleFoundImage都会覆盖之前的节点;同时重复添加AVPlayerItemDidPlayToEndTime观察者会导致内存泄漏和重复触发播放循环。
  4. 不必要的锚点移除:每次检测到锚点后调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:44:13