Performing gesture on view prevents camera action

89 Views Asked by At

I have a basic 3d interactive globe. I'm using the function centerCameraOnDot to move the camera to center on different parts of the globe. When I run the code and call centerCameraOnDot everything runs smoothly.
I can even run the function multiple times in a row on different locations, and it will animate to each one correctly.

However, if I perform any gesture on the view stop and then call the function after a few seconds, the camera never animates to the new location.
I did some debugging and the function does execute and newCameraPosition is calculated correctly.

However, the camera action doesn't perform.
This may be a result of gestures modifying the state of the scene/camera.

How do I go about performing these actions regardless of previous gestures?

I tried reinitializing the view and running the function and this obviously works, but is not practical.

import Foundation
import SceneKit
import CoreImage
import SwiftUI
import MapKit

public typealias GenericController = UIViewController

public class GlobeViewController: GenericController {
    var nodePos: CGPoint? = nil
    public var earthNode: SCNNode!
    private var sceneView : SCNView!
    private var cameraNode: SCNNode!
    private var dotCount = 50000
    
    public init(earthRadius: Double) {
        self.earthRadius = earthRadius
        super.init(nibName: nil, bundle: nil)
    }
    
    public init(earthRadius: Double, dotCount: Int) {
        self.earthRadius = earthRadius
        self.dotCount = dotCount
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func centerCameraOnDot(dotPosition: SCNVector3) {
    let fixedDistance: Float = 6.0
    let newCameraPosition = dotPosition.normalized().scaled(to: fixedDistance)

    // Position animation
    let moveAction = SCNAction.move(to: newCameraPosition, duration: 1.5)

    // Set up lookAt constraint for orientation
    let constraint = SCNLookAtConstraint(target: earthNode)
    constraint.isGimbalLockEnabled = true

    // Animate the transition
    SCNTransaction.begin()
    SCNTransaction.animationDuration = 1.5

    cameraNode.constraints = [constraint]
    cameraNode.runAction(moveAction)

    SCNTransaction.commit()
}

    public override func viewDidLoad() {
        super.viewDidLoad()
        setupScene()
        
        setupParticles()
        
        setupCamera()
        setupGlobe()
        
        setupDotGeometry()
    }
    
    private func setupScene() {
        let scene = SCNScene()
        sceneView = SCNView(frame: view.frame)
        sceneView.scene = scene
        sceneView.showsStatistics = true
        sceneView.backgroundColor = .clear
        sceneView.allowsCameraControl = true
        sceneView.isUserInteractionEnabled = true
        self.view.addSubview(sceneView)
    }
        
    private func setupParticles() {
        guard let stars = SCNParticleSystem(named: "StarsParticles.scnp", inDirectory: nil) else { return }
        stars.isLightingEnabled = false
                
        if sceneView != nil {
            sceneView.scene?.rootNode.addParticleSystem(stars)
        }
    }
    
    private func setupCamera() {
        self.cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
        sceneView.scene?.rootNode.addChildNode(cameraNode)
    }

    private func setupGlobe() {
        self.earthNode = EarthNode(radius: earthRadius, earthColor: earthColor, earthGlow: glowColor, earthReflection: reflectionColor)
        sceneView.scene?.rootNode.addChildNode(earthNode)
    }

    private func setupDotGeometry() {
        let textureMap = generateTextureMap(dots: dotCount, sphereRadius: CGFloat(earthRadius))

        let newYork = CLLocationCoordinate2D(latitude: 44.0682, longitude: -121.3153)
        let newYorkDot = closestDotPosition(to: newYork, in: textureMap)

        let dotColor = GenericColor(white: 1, alpha: 1)
        let oceanColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
        let highlightColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
        
        // threshold to determine if the pixel in the earth-dark.jpg represents terrain (0.03 represents rgb(7.65,7.65,7.65), which is almost black)
        let threshold: CGFloat = 0.03
        
        let dotGeometry = SCNSphere(radius: dotRadius)
        dotGeometry.firstMaterial?.diffuse.contents = dotColor
        dotGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
        
        let highlightGeometry = SCNSphere(radius: dotRadius)
        highlightGeometry.firstMaterial?.diffuse.contents = highlightColor
        highlightGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
        
        let oceanGeometry = SCNSphere(radius: dotRadius)
        oceanGeometry.firstMaterial?.diffuse.contents = oceanColor
        oceanGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.constant
        
        var positions = [SCNVector3]()
        var dotNodes = [SCNNode]()
        
        var highlightedNode: SCNNode? = nil
        
        for i in 0...textureMap.count - 1 {
            let u = textureMap[i].x
            let v = textureMap[i].y
            
            let pixelColor = self.getPixelColor(x: Int(u), y: Int(v))
            let isHighlight = u == newYorkDot.x && v == newYorkDot.y
            
            if (isHighlight) {
                let dotNode = SCNNode(geometry: highlightGeometry)
                dotNode.name = "NewYorkDot"
                dotNode.position = textureMap[i].position
                positions.append(dotNode.position)
                dotNodes.append(dotNode)
                
                print("myloc \(textureMap[i].position)")
                
                highlightedNode = dotNode
            } else if (pixelColor.red < threshold && pixelColor.green < threshold && pixelColor.blue < threshold) {
                let dotNode = SCNNode(geometry: dotGeometry)
                dotNode.name = "Other"
                dotNode.position = textureMap[i].position
                positions.append(dotNode.position)
                dotNodes.append(dotNode)
            }
        }
        
        DispatchQueue.main.async {
            let dotPositions = positions as NSArray
            let dotIndices = NSArray()
            let source = SCNGeometrySource(vertices: dotPositions as! [SCNVector3])
            let element = SCNGeometryElement(indices: dotIndices as! [Int32], primitiveType: .point)
            
            let pointCloud = SCNGeometry(sources: [source], elements: [element])
            
            let pointCloudNode = SCNNode(geometry: pointCloud)
            for dotNode in dotNodes {
                pointCloudNode.addChildNode(dotNode)
            }
     
            self.sceneView.scene?.rootNode.addChildNode(pointCloudNode)
            
            //performing gestures before this causes the bug
            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                if let highlightedNode = highlightedNode {
                    self.centerCameraOnDot(dotPosition: highlightedNode.position)
                }
            }
        }
   }
}
1

There are 1 best solutions below

7
VonC On BEST ANSWER

The gestures on the view might be altering the state of the camera or the scene, preventing the camera from animating to a new position when centerCameraOnDot is called.

You should make sure any gesture-related modifications to the camera or the scene are either reset or properly handled before running the centerCameraOnDot function.

Before animating the camera to a new position, reset any transformations or constraints that might have been altered by gestures. That could be as simple as setting the camera to a known state or removing constraints added by gestures.
And check if there are any active gestures. If there are, you might need to wait until they are completed or forcefully end them.

Temporarily disable gesture recognizers when the camera animation starts and re-enable them after the animation is completed. That prevents any gesture interference during the camera movement.

func centerCameraOnDot(dotPosition: SCNVector3) {
    let fixedDistance: Float = 6.0
    let newCameraPosition = dotPosition.normalized().scaled(to: fixedDistance)

    // Disable gestures here
    sceneView.gestureRecognizers?.forEach { $0.isEnabled = false }

    // Reset camera state if needed
    // e.g., cameraNode.transform = SCNMatrix4Identity

    // Position animation
    let moveAction = SCNAction.move(to: newCameraPosition, duration: 1.5)

    // Set up lookAt constraint for orientation
    let constraint = SCNLookAtConstraint(target: earthNode)
    constraint.isGimbalLockEnabled = true

    // Animate the transition
    SCNTransaction.begin()
    SCNTransaction.animationDuration = 1.5

    cameraNode.constraints = [constraint]
    cameraNode.runAction(moveAction) {
        // Re-enable gestures here
        self.sceneView.gestureRecognizers?.forEach { $0.isEnabled = true }
    }

    SCNTransaction.commit()
}

Another approach was suggested by ColdLogic:

You have to use gesture recognizers if you want only specific things to happen during specific gestures.
You can sort of accomplish the same thing with touchesBegan by checking the number of taps, state of the touch, and other factors. But it's easier to just use a UIPanGestureRecognizer or a UITapGestureRecognizer.

Using gesture recognizers like UIPanGestureRecognizer or UITapGestureRecognizer in your SceneKit application can provide more control over how user interactions affect the scene and the camera.
By using gesture recognizers, you can more easily disable or modify gesture interactions during camera animations. For example, you could disable a pan gesture recognizer while the camera is animating to prevent unintended interference.
And implementing custom gesture recognizers can enable you to manage the state of the scene and camera more effectively. For instance, you can set up a gesture recognizer specifically for moving the camera, and make sure it does not interfere with other scene interactions.

As an example:

override func viewDidLoad() {
    super.viewDidLoad()
    setupScene()
    // Other setup methods

    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
    sceneView.addGestureRecognizer(panGesture)
}

@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    // Handle the pan gesture
    if gesture.state == .began {
        // Optionally disable camera animation here
    } else if gesture.state == .changed {
        // Update camera or scene based on pan movement
    } else if gesture.state == .ended {
        // Optionally re-enable camera animation here
    }
}

That approach could help prevent the issue you are experiencing where gestures interfere with the centerCameraOnDot function.


But what Im encountering is if I do any gesture on the view then stop and wait 10 seconds then run the camera animation, it just doesn't run.

If SCNView.allowsCameraControl is enabled, the built-in camera controls may be interfering with your custom animations. You can try explicitly disabling allowsCameraControl before your animation and re-enabling it afterward.

In addition to resetting the camera's transform, make sure all constraints (including any potential hidden constraints applied by SceneKit's default camera control) are removed or reset before starting the animation.

There might be ongoing animations or actions on the camera node that are not immediately visible. Checking and stopping these before starting a new animation could help. Sometimes, adding a slight delay before starting the animation allows the scene to settle after a gesture.

Your centerCameraOnDot function would then be:

func centerCameraOnDot(dotPosition: SCNVector3) {
    let fixedDistance: Float = 6.0
    let newCameraPosition = dotPosition.normalized().scaled(to: fixedDistance)

    // Disable built-in camera controls
    sceneView.allowsCameraControl = false

    // Clear any existing animations or constraints
    cameraNode.removeAllActions()
    cameraNode.removeAllAnimations()
    cameraNode.constraints = nil

    // Reset camera state
    cameraNode.transform = SCNMatrix4Identity

    // Position animation
    let moveAction = SCNAction.move(to: newCameraPosition, duration: 1.5)

    // Set up lookAt constraint for orientation
    let constraint = SCNLookAtConstraint(target: earthNode)
    constraint.isGimbalLockEnabled = true

    // Optionally add a delay here
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        // Animate the transition
        SCNTransaction.begin()
        SCNTransaction.animationDuration = 1.5

        self.cameraNode.constraints = [constraint]
        self.cameraNode.runAction(moveAction) {
            // Re-enable built-in camera controls
            self.sceneView.allowsCameraControl = true
        }

        SCNTransaction.commit()
    }
}

That way, the camera should be in a known state before starting the animation and that no conflicting actions are interfering. The slight delay before the animation can also help in situations where the scene might need a moment to settle after a gesture.


I also found that if I simply add the line sceneView.pointOfView = cameraNode at the very beginning of centerCameraOnDot(), then the camera action works even after a gesture. This line resets the globe to its original position, and then the camera is able to animate correctly.

The fact that setting sceneView.pointOfView = cameraNode at the beginning of centerCameraOnDot() resolves the issue indicates that the internal state of the SCNView or its pointOfView is being altered by the gestures, even if it is not immediately visible through the properties of the cameraNode. That could be due to SceneKit's internal handling of camera controls and gesture interactions.

To avoid the visual reset caused by reassigning cameraNode to sceneView.pointOfView, you can try preserving the current camera position and orientation, then restoring them after reassigning pointOfView.

If you have not already, make sure the built-in camera controls (allowsCameraControl) are disabled during your custom camera movements. That can help prevent internal state changes caused by SceneKit's default handling of camera gestures.

It seems that the gesture interaction is somehow affecting pointOfView even if it is not visible in the cameraNode properties. You might want to investigate how gesture recognizers are set up and whether they interact with the pointOfView property.

As you correctly pointed out, UI updates, including enabling/disabling gesture recognizers, should be performed on the main thread. Here is how you can modify the relevant part of the code:

DispatchQueue.main.async {
    self.sceneView.gestureRecognizers?.forEach { $0.isEnabled = true }
}

Given your findings, a potential modification to your centerCameraOnDot method could be:

func centerCameraOnDot(dotPosition: SCNVector3) {
    let fixedDistance: Float = 6.0
    let newCameraPosition = dotPosition.normalized().scaled(to: fixedDistance)

    // Preserve current camera state
    let currentPOV = sceneView.pointOfView
    let currentPosition = cameraNode.position
    let currentOrientation = cameraNode.orientation

    // Reset pointOfView to refresh internal state
    sceneView.pointOfView = cameraNode

    // Restore camera state to avoid visual reset
    cameraNode.position = currentPosition
    cameraNode.orientation = currentOrientation

    // Your existing camera animation code
}

That code attempts to refresh the internal state by reassigning pointOfView, but then immediately restores the camera's position and orientation to prevent a visible jump. That might be a workaround to refresh whatever internal state is causing the issue without a noticeable visual impact.