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)
}
}
}
}
}
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
centerCameraOnDotis called.You should make sure any gesture-related modifications to the camera or the scene are either reset or properly handled before running the
centerCameraOnDotfunction.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.
Another approach was suggested by ColdLogic:
Using gesture recognizers like
UIPanGestureRecognizerorUITapGestureRecognizerin 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:
That approach could help prevent the issue you are experiencing where gestures interfere with the
centerCameraOnDotfunction.If
SCNView.allowsCameraControlis enabled, the built-in camera controls may be interfering with your custom animations. You can try explicitly disablingallowsCameraControlbefore 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
centerCameraOnDotfunction would then be: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.
The fact that setting
sceneView.pointOfView = cameraNodeat the beginning ofcenterCameraOnDot()resolves the issue indicates that the internal state of theSCNViewor itspointOfViewis being altered by the gestures, even if it is not immediately visible through the properties of thecameraNode. That could be due to SceneKit's internal handling of camera controls and gesture interactions.To avoid the visual reset caused by reassigning
cameraNodetosceneView.pointOfView, you can try preserving the current camera position and orientation, then restoring them after reassigningpointOfView.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
pointOfVieweven if it is not visible in thecameraNodeproperties. You might want to investigate how gesture recognizers are set up and whether they interact with thepointOfViewproperty.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:
Given your findings, a potential modification to your
centerCameraOnDotmethod could be: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.