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

SceneKit相机角度适配矩阵变换:平移手势3D节点漂移问题

解决SceneKit相机旋转时UIPan手势移动节点的漂移问题

我太懂你这个痛点了——相机垂直平面时平移节点好好的,一旋转相机,触摸位置直接飘得没边儿!这是因为原代码里的unprojectPoint用法没考虑相机视角变化后,触摸射线和目标平面的正确交点逻辑。下面给你修正后的实现,用射线平面相交+向量运算的方法彻底解决漂移问题:

@objc func handlePan(_ sender: UIPanGestureRecognizer) {
    guard let sceneView = self.sceneView else { return }
    
    // 把平移偏移转换成视图内的当前触摸位置
    let translation = sender.translation(in: sceneView)
    let currentTouchPoint = CGPoint(
        x: sceneView.bounds.midX + translation.x,
        y: sceneView.bounds.midY + translation.y
    )
    
    // 生成相机发出的、穿过触摸点的射线(近平面到远平面的两点)
    let rayStart = sceneView.unprojectPoint(SCNVector3(currentTouchPoint.x, currentTouchPoint.y, 0))
    let rayEnd = sceneView.unprojectPoint(SCNVector3(currentTouchPoint.x, currentTouchPoint.y, 1))
    // 计算射线的方向向量并归一化
    let rayDirection = rayEnd - rayStart
    let normalizedDirection = rayDirection.normalized()
    
    // 定义你要移动节点的目标平面:这里是y=0平面
    let planeNormal = SCNVector3(0, 1, 0) // 平面的法向量
    let planeOrigin = SCNVector3Zero // 平面上的任意参考点
    
    // 用射线平面相交公式计算交点
    let denominator = normalizedDirection.dot(planeNormal)
    guard abs(denominator) > 0.0001 else {
        // 射线和平面平行,没交点,直接返回
        return
    }
    let t = (planeOrigin - rayStart).dot(planeNormal) / denominator
    let targetWorldPoint = rayStart + normalizedDirection * t
    
    // 给节点设置位置(固定y=0)
    worldHandle?.position = SCNVector3(targetWorldPoint.x, 0, targetWorldPoint.z)
}

// 给SCNVector3加些辅助计算的扩展,方便向量操作
extension SCNVector3 {
    static func - (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 {
        return SCNVector3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z)
    }
    
    static func + (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 {
        return SCNVector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z)
    }
    
    static func * (lhs: SCNVector3, scalar: Float) -> SCNVector3 {
        return SCNVector3(lhs.x * scalar, lhs.y * scalar, lhs.z * scalar)
    }
    
    func normalized() -> SCNVector3 {
        let length = sqrt(x*x + y*y + z*z)
        guard length > 0.0001 else { return self }
        return SCNVector3(x/length, y/length, z/length)
    }
    
    func dot(_ vector: SCNVector3) -> Float {
        return x*vector.x + y*vector.y + z*vector.z
    }
}

为啥这个方法能搞定漂移?

原代码的问题在于,直接用固定z值的unprojectPoint计算位置,当相机旋转后,这个z值对应的空间点根本不在你要的y=0平面上,自然会飘。而修正后的逻辑是:

  1. 先算出触摸点对应的相机射线(从相机近平面穿到远平面的一条3D线)
  2. 用几何公式精确计算这条射线和y=0平面的交点——这个交点就是触摸在目标平面上的真实位置
  3. 不管相机怎么旋转,射线和平面的交点都是基于3D空间的几何关系计算的,完全不受透视偏移影响

小细节调整

如果你的目标平面不是y=0,只需要修改planeNormal(平面法向量)和planeOrigin(平面上任意一点)就行,比如要在y=2的平面上移动节点,把planeOrigin改成SCNVector3(0,2,0)就好。

内容的提问来源于stack exchange,提问作者JP Silvashy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:39:19