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

iOS16下SCNLookAtConstraint与SCNDistanceConstraint失效问题求助

问题

运行环境为iOS16,尝试掌握SCNLookAtConstraint的使用,但Demo应用出现异常:点击场景时立方体节点移动,但相机节点并未跟随。已尝试在构建逻辑、Coordinator及Renderer回调中添加相机约束(包含SCNLookAtConstraint和SCNDistanceConstraint),代码能识别到约束但未生效,鼠标拖动场景和点击操作均无法让相机正常跟随。相关代码如下:

import SwiftUI
import SceneKit
import Combine

let screenSize: CGRect = UIScreen.main.bounds
let screenWidth: CGFloat = UIScreen.main.bounds.width
let screenHeight: CGFloat = UIScreen.main.bounds.height

let push = PassthroughSubject<Float,Never>()
var pusher: AnyCancellable!

struct CoreView: View {
 @State var scene = SCNScene()
 @State var place:Float = 0.0

 var body: some View {
    CustomSceneView(scene: scene, options: [])
        .frame(width: screenWidth, height: screenHeight)
        .onTapGesture {
            place += 0.1
            push.send(place)
        }
 }
}

struct ContentView: View {
 var body: some View {
    CoreView()
 }
}

struct CustomSceneView: UIViewRepresentable {

var scene: SCNScene
var options: [Any]
var delegate: SCNRenderer!

var view = SCNView()

func makeUIView(context: Context) -> SCNView {

    view.scene = scene
    view.allowsCameraControl = true
    view.autoenablesDefaultLighting = false
    
    let cubeGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
    cubeGeometry.firstMaterial?.diffuse.contents = UIColor.red
    let cubeNode = SCNNode(geometry: cubeGeometry)
    cubeNode.name = "cubeNode"
    cubeNode.simdPosition = SIMD3(0,0,0)
    
    let camera = SCNCamera()
    camera.fieldOfView = 60.0
    camera.focalLength = 100.0
    camera.sensorHeight = 0.0
    camera.wantsDepthOfField = true
    camera.focusDistance = 0.8
    camera.fStop = 5.6
    camera.wantsHDR = false
    camera.apertureBladeCount = 4
    camera.motionBlurIntensity = 1.0
    
    camera.name = "camera"
    
    let cameraNode = SCNNode()
    cameraNode.camera = camera
    cameraNode.name = "cameraNode"
    
    let lookAtConstraint = SCNLookAtConstraint(target: cubeNode)
    lookAtConstraint.influenceFactor = 1.0
    lookAtConstraint.isGimbalLockEnabled = true


    
    view.delegate = context.coordinator
    scene.rootNode.addChildNode(cameraNode)
    
    view.pointOfView = scene.rootNode.childNode(withName: "cameraNode", recursively: true)
    view.pointOfView?.simdPosition = SIMD3<Float>(0, 0.0, 2)
    scene.rootNode.addChildNode(cubeNode)
    
    view.defaultCameraController.interactionMode = .fly
    view.defaultCameraController.inertiaEnabled = true
    //view.defaultCameraController.maximumVerticalAngle = 45
    view.allowsCameraControl = true
    
    pusher = push.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
        .throttle(for: 0.01, scheduler: RunLoop.main, latest: true)
        .sink(receiveValue: { [self] distance in
            cubeNode.simdPosition = SIMD3(distance,0,0)
        })
    
    return view
    
}

func updateUIView(_ view: SCNView, context: Context) {
    print("update")
    
}

func makeCoordinator() -> Coordinator {
    Coordinator(view)
}
}

class Coordinator: NSObject, SCNSceneRendererDelegate {
private let view: SCNView
private var cubeNode: SCNNode!
private var cameraNode: SCNNode!

init(_ view: SCNView) {
    self.view = view
    super.init()
}

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    
    cameraNode = renderer.scene?.rootNode.childNode(withName: "cameraNode", recursively: true)
    cubeNode = renderer.scene?.rootNode.childNode(withName: "cubeNode", recursively: true)
    
    print("cubeNode \(cubeNode)")
    print("cameraNode \(cameraNode)")
    
    if cubeNode != nil && cameraNode != nil {
        let distanceConstraint = SCNDistanceConstraint(target: cubeNode)
        distanceConstraint.minimumDistance = 1
        distanceConstraint.maximumDistance = 2
        
        let lookAtConstraint = SCNLookAtConstraint(target: cubeNode)
        lookAtConstraint.influenceFactor = 1.0
        lookAtConstraint.isGimbalLockEnabled = true
        cameraNode.constraints = [lookAtConstraint, distanceConstraint]
    }
}
}
解决方案

问题出在三个核心点,逐一修复即可解决:

  • 默认相机控制与约束冲突:开启allowsCameraControl = true后,SceneKit的默认相机控制器会直接接管相机的位置和旋转,完全覆盖自定义约束。必须将其设置为false,让约束生效。
  • 重复添加约束:在renderer(_:updateAtTime:)回调中每帧都重新创建并添加约束,既无必要还会导致逻辑混乱。应在相机节点初始化时一次性添加约束,后续无需重复操作。
  • 全局Combine订阅风险:全局的push和pusher容易引发内存泄漏或订阅异常,改用SwiftUI的Binding来传递位置变化,将订阅逻辑放在Coordinator中管理生命周期更安全。

修复后的完整代码:

import SwiftUI
import SceneKit
import Combine

struct CoreView: View {
    @State private var scene = SCNScene()
    @State private var place: Float = 0.0

    var body: some View {
        CustomSceneView(scene: scene, targetPosition: $place)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .onTapGesture {
                place += 0.1
            }
    }
}

struct ContentView: View {
    var body: some View {
        CoreView()
    }
}

struct CustomSceneView: UIViewRepresentable {
    let scene: SCNScene
    @Binding var targetPosition: Float

    func makeUIView(context: Context) -> SCNView {
        let view = SCNView()
        view.scene = scene
        view.allowsCameraControl = false // 关闭默认相机控制,让约束生效
        view.autoenablesDefaultLighting = true // 开启默认光照,便于观察立方体

        // 创建立方体节点
        let cubeGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
        cubeGeometry.firstMaterial?.diffuse.contents = UIColor.red
        let cubeNode = SCNNode(geometry: cubeGeometry)
        cubeNode.name = "cubeNode"
        cubeNode.simdPosition = SIMD3(0, 0, 0)
        scene.rootNode.addChildNode(cubeNode)

        // 创建相机和相机节点
        let camera = SCNCamera()
        camera.fieldOfView = 60.0
        camera.wantsDepthOfField = true
        camera.focusDistance = 0.8
        camera.fStop = 5.6

        let cameraNode = SCNNode()
        cameraNode.camera = camera
        cameraNode.name = "cameraNode"
        cameraNode.simdPosition = SIMD3<Float>(0, 0.0, 2)
        scene.rootNode.addChildNode(cameraNode)

        // 添加相机约束(一次性设置,无需重复操作)
        let lookAtConstraint = SCNLookAtConstraint(target: cubeNode)
        lookAtConstraint.influenceFactor = 1.0
        lookAtConstraint.isGimbalLockEnabled = true

        let distanceConstraint = SCNDistanceConstraint(target: cubeNode)
        distanceConstraint.minimumDistance = 1
        distanceConstraint.maximumDistance = 2

        cameraNode.constraints = [lookAtConstraint, distanceConstraint]

        view.pointOfView = cameraNode

        // 监听位置变化,移动立方体
        context.coordinator.cubeNode = cubeNode
        context.coordinator.positionSubscription = $targetPosition
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .throttle(for: 0.01, scheduler: RunLoop.main, latest: true)
            .sink { [weak cubeNode] distance in
                cubeNode?.simdPosition = SIMD3(distance, 0, 0)
            }

        return view
    }

    func updateUIView(_ view: SCNView, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    class Coordinator: NSObject {
        var cubeNode: SCNNode?
        var positionSubscription: AnyCancellable?
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 03:05:21