我有一个用 SceneKit 构建的 3D 交互式地球仪,其中国家/地区用点表示。下面的函数会获取一个位置并将相机动画移动到该位置。
如果用户不与地球仪交互,那么我可以连续调用该函数并将相机设置为新位置。
但是,如果用户在场景上执行任何手势,则相机动画将不起作用。
在不同的 SO 线程(下面链接)中找到的解决方案在函数开头使用了行
sceneView.pointOfView = cameraNode
。但是,这条线会导致地球仪在设置动画之前重置到其原始位置。我一直在试图找到一种方法来绕过这个场景重置,但没有运气。
我假设在地球上执行手势会为场景创建一个新的视角并覆盖相机的视角。因此,在动画之前将场景的视点设置回相机可以解决该问题。
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) {
sceneView.pointOfView = cameraNode //HERE RESETS
let fixedDistance: Float = 5.0
let newCameraPosition = dotPosition.normalized().scaled(to: fixedDistance)
let moveAction = SCNAction.move(to: newCameraPosition, duration: 1.5)
let constraint = SCNLookAtConstraint(target: earthNode)
constraint.isGimbalLockEnabled = true
sceneView.gestureRecognizers?.forEach { $0.isEnabled = false }
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5
self.cameraNode.constraints = [constraint]
self.cameraNode.runAction(moveAction) {
DispatchQueue.main.async {
self.sceneView.gestureRecognizers?.forEach { $0.isEnabled = true }
}
}
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)
}
}
}
}
}
sceneView.pointOfView
时,相机的位置和方向立即更改为pointOfView
节点的变换,这会导致观察到的重置。
尝试并保留当前相机变换:在设置
sceneView.pointOfView = cameraNode
之前,存储当前相机变换。这包括它的位置、旋转以及与场景设置相关的任何其他属性。您的
centerCameraOnDot
功能将是:
func centerCameraOnDot(dotPosition: SCNVector3) {
// Store the current camera transformation
let currentCameraTransform = cameraNode.transform
// Set the point of view to the camera node
sceneView.pointOfView = cameraNode
// Reapply the stored transformation
cameraNode.transform = currentCameraTransform
// Rest of your animation code
}
看看这是否有助于将相机转换到新的视角,而无需重置地球仪的位置。
这并没有解决问题。
替代方法:更新相机节点而不改变
pointOfView
您可以尝试根据用户交互来更新
pointOfView
的位置和方向,而不是直接操作 sceneView
的 cameraNode
属性。该方法涉及拦截用户手势并手动将其转换应用于 cameraNode
。以下是如何实现这一点的概述:
将自定义手势识别器添加到
sceneView
或利用 SceneKit 的默认手势处理来检测用户交互。cameraNode
。这使 cameraNode
与用户的视角保持同步。cameraNode
的位置和方向设置动画,而不是使用 sceneView.pointOfView
。
可能看起来像:
override func viewDidLoad() {
super.viewDidLoad()
setupGestureRecognizers()
// Other setup code
}
private func setupGestureRecognizers() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
sceneView.addGestureRecognizer(panGesture)
// Add other gestures as needed
}
@objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
// Calculate the transformation based on the gesture
// Apply the transformation to the cameraNode
// Rest of the code
}
func centerCameraOnDot(dotPosition: SCNVector3) {
// Directly animate cameraNode to new position
// No need to alter sceneView.pointOfView
// Rest of the code
}
该方法需要更多地手动处理相机转换,但可以更好地控制相机响应用户交互的行为。它还避免了更改
pointOfView
时重置地球仪位置的问题。
还尝试添加日志来跟踪用户交互前后以及动画到新位置时相机的位置和方向。这可以帮助识别意外的变化。
并使用 SceneKit 的调试工具,例如显示统计信息或 SCNView 的调试选项,以更好地了解场景的状态。
分别测试手势处理和相机动画代码的每个部分,以找出问题的原因。